Input device calibration and capabilities.

Finished the input device capability API.
Added a mechanism for calibrating touch devices to obtain more
accurate information about the touch contact area.
Improved pointer location to show new coordinates and capabilities.
Optimized pointer location display and formatting to avoid allocating large
numbers of temporary objects.  The GC churn was causing the application to
stutter very badly when more than a couple of fingers were down).
Added more diagnostics.

Change-Id: Ie25380278ed6f16c5b04cd9df848015850383498
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index e86e3bf..d4dd05c 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -29,6 +29,7 @@
 import android.view.InputEvent;
 import android.view.MotionEvent;
 import android.view.InputChannel;
+import android.view.InputDevice;
 
 /**
  * System private interface to the window manager.
@@ -125,6 +126,10 @@
     // Report whether the hardware supports the given keys; returns true if successful
     boolean hasKeys(in int[] keycodes, inout boolean[] keyExists);
     
+    // Get input device information.
+    InputDevice getInputDevice(int deviceId);
+    int[] getInputDeviceIds();
+    
     // For testing
     void setInTouchMode(boolean showFocus);
     
diff --git a/core/java/android/view/InputDevice.aidl b/core/java/android/view/InputDevice.aidl
new file mode 100644
index 0000000..dbc40c1
--- /dev/null
+++ b/core/java/android/view/InputDevice.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android.view.InputDevice.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+parcelable InputDevice;
diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java
index d5b2121..fb47b9c 100755
--- a/core/java/android/view/InputDevice.java
+++ b/core/java/android/view/InputDevice.java
@@ -16,6 +16,12 @@
 
 package android.view;
 
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
 /**
  * Describes the capabilities of a particular input device.
  * <p>
@@ -32,12 +38,14 @@
  * the appropriate interpretation.
  * </p>
  */
-public final class InputDevice {
+public final class InputDevice implements Parcelable {
     private int mId;
     private String mName;
     private int mSources;
     private int mKeyboardType;
     
+    private MotionRange[] mMotionRanges;
+    
     /**
      * A mask for input source classes.
      * 
@@ -246,6 +254,8 @@
      */
     public static final int MOTION_RANGE_ORIENTATION = 8;
     
+    private static final int MOTION_RANGE_LAST = MOTION_RANGE_ORIENTATION;
+    
     /**
      * There is no keyboard.
      */
@@ -261,6 +271,11 @@
      * The keyboard supports a complement of alphabetic keys.
      */
     public static final int KEYBOARD_TYPE_ALPHABETIC = 2;
+    
+    // Called by native code.
+    private InputDevice() {
+        mMotionRanges = new MotionRange[MOTION_RANGE_LAST + 1];
+    }
 
     /**
      * Gets information about the input device with the specified id.
@@ -268,8 +283,35 @@
      * @return The input device or null if not found.
      */
     public static InputDevice getDevice(int id) {
-        // TODO
-        return null;
+        IWindowManager wm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));
+        try {
+            return wm.getInputDevice(id);
+        } catch (RemoteException ex) {
+            throw new RuntimeException(
+                    "Could not get input device information from Window Manager.", ex);
+        }
+    }
+    
+    /**
+     * Gets the ids of all input devices in the system.
+     * @return The input device ids.
+     */
+    public static int[] getDeviceIds() {
+        IWindowManager wm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));
+        try {
+            return wm.getInputDeviceIds();
+        } catch (RemoteException ex) {
+            throw new RuntimeException(
+                    "Could not get input device ids from Window Manager.", ex);
+        }
+    }
+    
+    /**
+     * Gets the input device id.
+     * @return The input device id.
+     */
+    public int getId() {
+        return mId;
     }
     
     /**
@@ -307,23 +349,23 @@
     /**
      * Gets information about the range of values for a particular {@link MotionEvent}
      * coordinate.
-     * @param range The motion range constant.
+     * @param rangeType The motion range constant.
      * @return The range of values, or null if the requested coordinate is not
      * supported by the device.
      */
-    public MotionRange getMotionRange(int range) {
-        // TODO
-        return null;
+    public MotionRange getMotionRange(int rangeType) {
+        if (rangeType < 0 || rangeType > MOTION_RANGE_LAST) {
+            throw new IllegalArgumentException("Requested range is out of bounds.");
+        }
+        
+        return mMotionRanges[rangeType];
     }
     
-    /**
-     * Returns true if the device supports a particular button or key.
-     * @param keyCode The key code.
-     * @return True if the device supports the key.
-     */
-    public boolean hasKey(int keyCode) {
-        // TODO
-        return false;
+    private void addMotionRange(int rangeType, float min, float max, float flat, float fuzz) {
+        if (rangeType >= 0 && rangeType <= MOTION_RANGE_LAST) {
+            MotionRange range = new MotionRange(min, max, flat, fuzz);
+            mMotionRanges[rangeType] = range;
+        }
     }
     
     /**
@@ -331,13 +373,24 @@
      * coordinate.
      */
     public static final class MotionRange {
+        private float mMin;
+        private float mMax;
+        private float mFlat;
+        private float mFuzz;
+        
+        private MotionRange(float min, float max, float flat, float fuzz) {
+            mMin = min;
+            mMax = max;
+            mFlat = flat;
+            mFuzz = fuzz;
+        }
+        
         /**
          * Gets the minimum value for the coordinate.
          * @return The minimum value.
          */
         public float getMin() {
-            // TODO
-            return 0;
+            return mMin;
         }
         
         /**
@@ -345,8 +398,7 @@
          * @return The minimum value.
          */
         public float getMax() {
-            // TODO
-            return 0;
+            return mMax;
         }
         
         /**
@@ -354,8 +406,7 @@
          * @return The range of values.
          */
         public float getRange() {
-            // TODO
-            return 0;
+            return mMax - mMin;
         }
         
         /**
@@ -365,8 +416,7 @@
          * @return The extent of the center flat position.
          */
         public float getFlat() {
-            // TODO
-            return 0;
+            return mFlat;
         }
         
         /**
@@ -376,8 +426,127 @@
          * @return The error tolerance.
          */
         public float getFuzz() {
-            // TODO
-            return 0;
+            return mFuzz;
+        }
+    }
+    
+    public static final Parcelable.Creator<InputDevice> CREATOR
+            = new Parcelable.Creator<InputDevice>() {
+        public InputDevice createFromParcel(Parcel in) {
+            InputDevice result = new InputDevice();
+            result.readFromParcel(in);
+            return result;
+        }
+        
+        public InputDevice[] newArray(int size) {
+            return new InputDevice[size];
+        }
+    };
+    
+    private void readFromParcel(Parcel in) {
+        mId = in.readInt();
+        mName = in.readString();
+        mSources = in.readInt();
+        mKeyboardType = in.readInt();
+        
+        for (;;) {
+            int rangeType = in.readInt();
+            if (rangeType < 0) {
+                break;
+            }
+            
+            addMotionRange(rangeType,
+                    in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat());
+        }
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mId);
+        out.writeString(mName);
+        out.writeInt(mSources);
+        out.writeInt(mKeyboardType);
+        
+        for (int i = 0; i <= MOTION_RANGE_LAST; i++) {
+            MotionRange range = mMotionRanges[i];
+            if (range != null) {
+                out.writeInt(i);
+                out.writeFloat(range.mMin);
+                out.writeFloat(range.mMax);
+                out.writeFloat(range.mFlat);
+                out.writeFloat(range.mFuzz);
+            }
+        }
+        out.writeInt(-1);
+    }
+    
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+    
+    @Override
+    public String toString() {
+        StringBuilder description = new StringBuilder();
+        description.append("Input Device ").append(mId).append(": ").append(mName).append("\n");
+        
+        description.append("  Keyboard Type: ");
+        switch (mKeyboardType) {
+            case KEYBOARD_TYPE_NONE:
+                description.append("none");
+                break;
+            case KEYBOARD_TYPE_NON_ALPHABETIC:
+                description.append("non-alphabetic");
+                break;
+            case KEYBOARD_TYPE_ALPHABETIC:
+                description.append("alphabetic");
+                break;
+        }
+        description.append("\n");
+        
+        description.append("  Sources:");
+        appendSourceDescriptionIfApplicable(description, SOURCE_KEYBOARD, "keyboard");
+        appendSourceDescriptionIfApplicable(description, SOURCE_DPAD, "dpad");
+        appendSourceDescriptionIfApplicable(description, SOURCE_GAMEPAD, "gamepad");
+        appendSourceDescriptionIfApplicable(description, SOURCE_TOUCHSCREEN, "touchscreen");
+        appendSourceDescriptionIfApplicable(description, SOURCE_MOUSE, "mouse");
+        appendSourceDescriptionIfApplicable(description, SOURCE_TRACKBALL, "trackball");
+        appendSourceDescriptionIfApplicable(description, SOURCE_TOUCHPAD, "touchpad");
+        appendSourceDescriptionIfApplicable(description, SOURCE_JOYSTICK_LEFT, "joystick_left");
+        appendSourceDescriptionIfApplicable(description, SOURCE_JOYSTICK_RIGHT, "joystick_right");
+        description.append("\n");
+        
+        appendRangeDescriptionIfApplicable(description, MOTION_RANGE_X, "x");
+        appendRangeDescriptionIfApplicable(description, MOTION_RANGE_Y, "y");
+        appendRangeDescriptionIfApplicable(description, MOTION_RANGE_PRESSURE, "pressure");
+        appendRangeDescriptionIfApplicable(description, MOTION_RANGE_SIZE, "size");
+        appendRangeDescriptionIfApplicable(description, MOTION_RANGE_TOUCH_MAJOR, "touchMajor");
+        appendRangeDescriptionIfApplicable(description, MOTION_RANGE_TOUCH_MINOR, "touchMinor");
+        appendRangeDescriptionIfApplicable(description, MOTION_RANGE_TOOL_MAJOR, "toolMajor");
+        appendRangeDescriptionIfApplicable(description, MOTION_RANGE_TOOL_MINOR, "toolMinor");
+        appendRangeDescriptionIfApplicable(description, MOTION_RANGE_ORIENTATION, "orientation");
+        
+        return description.toString();
+    }
+    
+    private void appendSourceDescriptionIfApplicable(StringBuilder description, int source,
+            String sourceName) {
+        if ((mSources & source) == source) {
+            description.append(" ");
+            description.append(sourceName);
+        }
+    }
+    
+    private void appendRangeDescriptionIfApplicable(StringBuilder description,
+            int rangeType, String rangeName) {
+        MotionRange range = mMotionRanges[rangeType];
+        if (range != null) {
+            description.append("  Range[").append(rangeName);
+            description.append("]: min=").append(range.mMin);
+            description.append(" max=").append(range.mMax);
+            description.append(" flat=").append(range.mFlat);
+            description.append(" fuzz=").append(range.mFuzz);
+            description.append("\n");
         }
     }
 }
diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java
index d5a9979..939f118 100644
--- a/core/java/com/android/internal/widget/PointerLocationView.java
+++ b/core/java/com/android/internal/widget/PointerLocationView.java
@@ -19,8 +19,10 @@
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Paint;
+import android.graphics.RectF;
 import android.graphics.Paint.FontMetricsInt;
 import android.util.Log;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
@@ -29,17 +31,45 @@
 import java.util.ArrayList;
 
 public class PointerLocationView extends View {
+    private static final String TAG = "Pointer";
+    
     public static class PointerState {
-        private final ArrayList<Float> mXs = new ArrayList<Float>();
-        private final ArrayList<Float> mYs = new ArrayList<Float>();
+        // Trace of previous points.
+        private float[] mTraceX = new float[32];
+        private float[] mTraceY = new float[32];
+        private int mTraceCount;
+        
+        // True if the pointer is down.
         private boolean mCurDown;
-        private int mCurX;
-        private int mCurY;
-        private float mCurPressure;
-        private float mCurSize;
-        private int mCurWidth;
+        
+        // Most recent coordinates.
+        private MotionEvent.PointerCoords mCoords = new MotionEvent.PointerCoords();
+        
+        // Most recent velocity.
         private float mXVelocity;
         private float mYVelocity;
+        
+        public void clearTrace() {
+            mTraceCount = 0;
+        }
+        
+        public void addTrace(float x, float y) {
+            int traceCapacity = mTraceX.length;
+            if (mTraceCount == traceCapacity) {
+                traceCapacity *= 2;
+                float[] newTraceX = new float[traceCapacity];
+                System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount);
+                mTraceX = newTraceX;
+                
+                float[] newTraceY = new float[traceCapacity];
+                System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount);
+                mTraceY = newTraceY;
+            }
+            
+            mTraceX[mTraceCount] = x;
+            mTraceY[mTraceCount] = y;
+            mTraceCount += 1;
+        }
     }
 
     private final ViewConfiguration mVC;
@@ -54,11 +84,12 @@
     private boolean mCurDown;
     private int mCurNumPointers;
     private int mMaxNumPointers;
-    private final ArrayList<PointerState> mPointers
-             = new ArrayList<PointerState>();
+    private final ArrayList<PointerState> mPointers = new ArrayList<PointerState>();
     
     private final VelocityTracker mVelocity;
     
+    private final FasterStringBuilder mText = new FasterStringBuilder();
+    
     private boolean mPrintCoords = true;
     
     public PointerLocationView(Context c) {
@@ -94,6 +125,18 @@
         mPointers.add(ps);
         
         mVelocity = VelocityTracker.obtain();
+        
+        logInputDeviceCapabilities();
+    }
+    
+    private void logInputDeviceCapabilities() {
+        int[] deviceIds = InputDevice.getDeviceIds();
+        for (int i = 0; i < deviceIds.length; i++) {
+            InputDevice device = InputDevice.getDevice(deviceIds[i]);
+            if (device != null) {
+                Log.i(TAG, device.toString());
+            }
+        }
     }
 
     public void setPrintCoords(boolean state) {
@@ -113,6 +156,21 @@
                     + " bottom=" + mTextMetrics.bottom);
         }
     }
+    
+    // Draw an oval.  When angle is 0 radians, orients the major axis vertically,
+    // angles less than or greater than 0 radians rotate the major axis left or right.
+    private RectF mReusableOvalRect = new RectF();
+    private void drawOval(Canvas canvas, float x, float y, float major, float minor,
+            float angle, Paint paint) {
+        canvas.save(Canvas.MATRIX_SAVE_FLAG);
+        canvas.rotate((float) (angle * 180 / Math.PI), x, y);
+        mReusableOvalRect.left = x - minor / 2;
+        mReusableOvalRect.right = x + minor / 2;
+        mReusableOvalRect.top = y - major / 2;
+        mReusableOvalRect.bottom = y + major / 2;
+        canvas.drawOval(mReusableOvalRect, paint);
+        canvas.restore();
+    }
 
     @Override
     protected void onDraw(Canvas canvas) {
@@ -124,76 +182,80 @@
             
             final int NP = mPointers.size();
             
+            // Labels
             if (NP > 0) {
                 final PointerState ps = mPointers.get(0);
                 canvas.drawRect(0, 0, itemW-1, bottom,mTextBackgroundPaint);
-                canvas.drawText("P: " + mCurNumPointers + " / " + mMaxNumPointers,
-                        1, base, mTextPaint);
+                canvas.drawText(mText.clear()
+                        .append("P: ").append(mCurNumPointers)
+                        .append(" / ").append(mMaxNumPointers)
+                        .toString(), 1, base, mTextPaint);
                 
-                final int N = ps.mXs.size();
+                final int N = ps.mTraceCount;
                 if ((mCurDown && ps.mCurDown) || N == 0) {
                     canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint);
-                    canvas.drawText("X: " + ps.mCurX, 1 + itemW, base, mTextPaint);
+                    canvas.drawText(mText.clear()
+                            .append("X: ").append(ps.mCoords.x, 1)
+                            .toString(), 1 + itemW, base, mTextPaint);
                     canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint);
-                    canvas.drawText("Y: " + ps.mCurY, 1 + itemW * 2, base, mTextPaint);
+                    canvas.drawText(mText.clear()
+                            .append("Y: ").append(ps.mCoords.y, 1)
+                            .toString(), 1 + itemW * 2, base, mTextPaint);
                 } else {
-                    float dx = ps.mXs.get(N-1) - ps.mXs.get(0);
-                    float dy = ps.mYs.get(N-1) - ps.mYs.get(0);
+                    float dx = ps.mTraceX[N - 1] - ps.mTraceX[0];
+                    float dy = ps.mTraceY[N - 1] - ps.mTraceY[0];
                     canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom,
                             Math.abs(dx) < mVC.getScaledTouchSlop()
                             ? mTextBackgroundPaint : mTextLevelPaint);
-                    canvas.drawText("dX: " + String.format("%.1f", dx), 1 + itemW, base, mTextPaint);
+                    canvas.drawText(mText.clear()
+                            .append("dX: ").append(dx, 1)
+                            .toString(), 1 + itemW, base, mTextPaint);
                     canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom,
                             Math.abs(dy) < mVC.getScaledTouchSlop()
                             ? mTextBackgroundPaint : mTextLevelPaint);
-                    canvas.drawText("dY: " + String.format("%.1f", dy), 1 + itemW * 2, base, mTextPaint);
+                    canvas.drawText(mText.clear()
+                            .append("dY: ").append(dy, 1)
+                            .toString(), 1 + itemW * 2, base, mTextPaint);
                 }
                 
                 canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint);
-                int velocity = (int) (ps.mXVelocity * 1000);
-                canvas.drawText("Xv: " + velocity, 1 + itemW * 3, base, mTextPaint);
+                canvas.drawText(mText.clear()
+                        .append("Xv: ").append(ps.mXVelocity, 3)
+                        .toString(), 1 + itemW * 3, base, mTextPaint);
                 
                 canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint);
-                velocity = (int) (ps.mYVelocity * 1000);
-                canvas.drawText("Yv: " + velocity, 1 + itemW * 4, base, mTextPaint);
+                canvas.drawText(mText.clear()
+                        .append("Yv: ").append(ps.mYVelocity, 3)
+                        .toString(), 1 + itemW * 4, base, mTextPaint);
                 
                 canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint);
-                canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCurPressure * itemW) - 1,
+                canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCoords.pressure * itemW) - 1,
                         bottom, mTextLevelPaint);
-                canvas.drawText("Prs: " + String.format("%.2f", ps.mCurPressure), 1 + itemW * 5,
-                        base, mTextPaint);
+                canvas.drawText(mText.clear()
+                        .append("Prs: ").append(ps.mCoords.pressure, 2)
+                        .toString(), 1 + itemW * 5, base, mTextPaint);
                 
                 canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint);
-                canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCurSize * itemW) - 1,
+                canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCoords.size * itemW) - 1,
                         bottom, mTextLevelPaint);
-                canvas.drawText("Size: " + String.format("%.2f", ps.mCurSize), 1 + itemW * 6,
-                        base, mTextPaint);
+                canvas.drawText(mText.clear()
+                        .append("Size: ").append(ps.mCoords.size, 2)
+                        .toString(), 1 + itemW * 6, base, mTextPaint);
             }
             
-            for (int p=0; p<NP; p++) {
+            // Pointer trace.
+            for (int p = 0; p < NP; p++) {
                 final PointerState ps = mPointers.get(p);
                 
-                if (mCurDown && ps.mCurDown) {
-                    canvas.drawLine(0, (int)ps.mCurY, getWidth(), (int)ps.mCurY, mTargetPaint);
-                    canvas.drawLine((int)ps.mCurX, 0, (int)ps.mCurX, getHeight(), mTargetPaint);
-                    int pressureLevel = (int)(ps.mCurPressure*255);
-                    mPaint.setARGB(255, pressureLevel, 128, 255-pressureLevel);
-                    canvas.drawPoint(ps.mCurX, ps.mCurY, mPaint);
-                    canvas.drawCircle(ps.mCurX, ps.mCurY, ps.mCurWidth, mPaint);
-                }
-            }
-            
-            for (int p=0; p<NP; p++) {
-                final PointerState ps = mPointers.get(p);
-                
-                final int N = ps.mXs.size();
-                float lastX=0, lastY=0;
+                // Draw path.
+                final int N = ps.mTraceCount;
+                float lastX = 0, lastY = 0;
                 boolean haveLast = false;
                 boolean drawn = false;
                 mPaint.setARGB(255, 128, 255, 255);
-                for (int i=0; i<N; i++) {
-                    float x = ps.mXs.get(i);
-                    float y = ps.mYs.get(i);
+                for (int i=0; i < N; i++) {
+                    float x = ps.mTraceX[i];
+                    float y = ps.mTraceY[i];
                     if (Float.isNaN(x)) {
                         haveLast = false;
                         continue;
@@ -208,21 +270,57 @@
                     haveLast = true;
                 }
                 
+                // Draw velocity vector.
                 if (drawn) {
                     mPaint.setARGB(255, 255, 64, 128);
-                    float xVel = ps.mXVelocity * (1000/60);
-                    float yVel = ps.mYVelocity * (1000/60);
-                    canvas.drawLine(lastX, lastY, lastX+xVel, lastY+yVel, mPaint);
+                    float xVel = ps.mXVelocity * (1000 / 60);
+                    float yVel = ps.mYVelocity * (1000 / 60);
+                    canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
+                }
+                
+                if (mCurDown && ps.mCurDown) {
+                    // Draw crosshairs.
+                    canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);
+                    canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint);
+                    
+                    // Draw current point.
+                    int pressureLevel = (int)(ps.mCoords.pressure * 255);
+                    mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
+                    canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
+                    
+                    // Draw current touch ellipse.
+                    mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);
+                    drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,
+                            ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);
+                    
+                    // Draw current tool ellipse.
+                    mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);
+                    drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
+                            ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
                 }
             }
         }
     }
+    
+    private void logPointerCoords(MotionEvent.PointerCoords coords, int id) {
+        Log.i(TAG, mText.clear()
+                .append("Pointer ").append(id + 1)
+                .append(": (").append(coords.x, 3).append(", ").append(coords.y, 3)
+                .append(") Pressure=").append(coords.pressure, 3)
+                .append(" Size=").append(coords.size, 3)
+                .append(" TouchMajor=").append(coords.touchMajor, 3)
+                .append(" TouchMinor=").append(coords.touchMinor, 3)
+                .append(" ToolMajor=").append(coords.toolMajor, 3)
+                .append(" ToolMinor=").append(coords.toolMinor, 3)
+                .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1)
+                .append("deg").toString());
+    }
 
     public void addTouchEvent(MotionEvent event) {
         synchronized (mPointers) {
             int action = event.getAction();
             
-            //Log.i("Pointer", "Motion: action=0x" + Integer.toHexString(action)
+            //Log.i(TAG, "Motion: action=0x" + Integer.toHexString(action)
             //        + " pointers=" + event.getPointerCount());
             
             int NP = mPointers.size();
@@ -235,35 +333,33 @@
             //} else {
             //    mRect.setEmpty();
             //}
-            if (action == MotionEvent.ACTION_DOWN) {
-                mVelocity.clear();
+            if (action == MotionEvent.ACTION_DOWN
+                    || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
+                final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down
+                if (action == MotionEvent.ACTION_DOWN) {
+                    for (int p=0; p<NP; p++) {
+                        final PointerState ps = mPointers.get(p);
+                        ps.clearTrace();
+                        ps.mCurDown = false;
+                    }
+                    mCurDown = true;
+                    mMaxNumPointers = 0;
+                    mVelocity.clear();
+                }
                 
-                for (int p=0; p<NP; p++) {
-                    final PointerState ps = mPointers.get(p);
-                    ps.mXs.clear();
-                    ps.mYs.clear();
-                    ps.mCurDown = false;
-                }
-                mPointers.get(0).mCurDown = true;
-                mMaxNumPointers = 0;
-                if (mPrintCoords) {
-                    Log.i("Pointer", "Pointer 1: DOWN");
-                }
-            }
-            
-            if ((action&MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
-                final int index = (action&MotionEvent.ACTION_POINTER_INDEX_MASK)
-                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                 final int id = event.getPointerId(index);
                 while (NP <= id) {
                     PointerState ps = new PointerState();
                     mPointers.add(ps);
                     NP++;
                 }
+                
                 final PointerState ps = mPointers.get(id);
                 ps.mCurDown = true;
                 if (mPrintCoords) {
-                    Log.i("Pointer", "Pointer " + (id+1) + ": DOWN");
+                    Log.i(TAG, mText.clear().append("Pointer ")
+                            .append(id + 1).append(": DOWN").toString());
                 }
             }
             
@@ -284,58 +380,38 @@
                 final PointerState ps = mPointers.get(id);
                 final int N = event.getHistorySize();
                 for (int j=0; j<N; j++) {
+                    event.getHistoricalPointerCoords(i, j, ps.mCoords);
                     if (mPrintCoords) {
-                        Log.i("Pointer", "Pointer " + (id+1) + ": ("
-                                + event.getHistoricalX(i, j)
-                                + ", " + event.getHistoricalY(i, j) + ")"
-                                + " Prs=" + event.getHistoricalPressure(i, j)
-                                + " Size=" + event.getHistoricalSize(i, j));
+                        logPointerCoords(ps.mCoords, id);
                     }
-                    ps.mXs.add(event.getHistoricalX(i, j));
-                    ps.mYs.add(event.getHistoricalY(i, j));
+                    ps.addTrace(event.getHistoricalX(i, j), event.getHistoricalY(i, j));
                 }
+                event.getPointerCoords(i, ps.mCoords);
                 if (mPrintCoords) {
-                    Log.i("Pointer", "Pointer " + (id+1) + ": ("
-                            + event.getX(i) + ", " + event.getY(i) + ")"
-                            + " Prs=" + event.getPressure(i)
-                            + " Size=" + event.getSize(i));
+                    logPointerCoords(ps.mCoords, id);
                 }
-                ps.mXs.add(event.getX(i));
-                ps.mYs.add(event.getY(i));
-                ps.mCurX = (int)event.getX(i);
-                ps.mCurY = (int)event.getY(i);
-                //Log.i("Pointer", "Pointer #" + p + ": (" + ps.mCurX
-                //        + "," + ps.mCurY + ")");
-                ps.mCurPressure = event.getPressure(i);
-                ps.mCurSize = event.getSize(i);
-                ps.mCurWidth = (int)(ps.mCurSize*(getWidth()/3));
+                ps.addTrace(ps.mCoords.x, ps.mCoords.y);
                 ps.mXVelocity = mVelocity.getXVelocity(id);
                 ps.mYVelocity = mVelocity.getYVelocity(id);
             }
             
-            if ((action&MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
-                final int index = (action&MotionEvent.ACTION_POINTER_INDEX_MASK)
-                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+            if (action == MotionEvent.ACTION_UP
+                    || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
+                final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP
+                
                 final int id = event.getPointerId(index);
                 final PointerState ps = mPointers.get(id);
-                ps.mXs.add(Float.NaN);
-                ps.mYs.add(Float.NaN);
                 ps.mCurDown = false;
                 if (mPrintCoords) {
-                    Log.i("Pointer", "Pointer " + (id+1) + ": UP");
+                    Log.i(TAG, mText.clear().append("Pointer ")
+                            .append(id + 1).append(": UP").toString());
                 }
-            }
-            
-            if (action == MotionEvent.ACTION_UP) {
-                for (int i=0; i<NI; i++) {
-                    final int id = event.getPointerId(i);
-                    final PointerState ps = mPointers.get(id);
-                    if (ps.mCurDown) {
-                        ps.mCurDown = false;
-                        if (mPrintCoords) {
-                            Log.i("Pointer", "Pointer " + (id+1) + ": UP");
-                        }
-                    }
+
+                if (action == MotionEvent.ACTION_UP) {
+                    mCurDown = false;
+                } else {
+                    ps.addTrace(Float.NaN, Float.NaN);
                 }
             }
             
@@ -356,8 +432,120 @@
 
     @Override
     public boolean onTrackballEvent(MotionEvent event) {
-        Log.i("Pointer", "Trackball: " + event);
+        Log.i(TAG, "Trackball: " + event);
         return super.onTrackballEvent(event);
     }
     
+    // HACK
+    // A quick and dirty string builder implementation optimized for GC.
+    // Using the basic StringBuilder implementation causes the application grind to a halt when
+    // more than a couple of pointers are down due to the number of temporary objects allocated
+    // while formatting strings for drawing or logging.
+    private static final class FasterStringBuilder {
+        private char[] mChars;
+        private int mLength;
+        
+        public FasterStringBuilder() {
+            mChars = new char[64];
+        }
+        
+        public FasterStringBuilder clear() {
+            mLength = 0;
+            return this;
+        }
+        
+        public FasterStringBuilder append(String value) {
+            final int valueLength = value.length();
+            final int index = reserve(valueLength);
+            value.getChars(0, valueLength, mChars, index);
+            mLength += valueLength;
+            return this;
+        }
+        
+        public FasterStringBuilder append(int value) {
+            return append(value, 0);
+        }
+        
+        public FasterStringBuilder append(int value, int zeroPadWidth) {
+            final boolean negative = value < 0;
+            if (negative) {
+                value = - value;
+                if (value < 0) {
+                    append("-2147483648");
+                    return this;
+                }
+            }
+            
+            int index = reserve(11);
+            final char[] chars = mChars;
+            
+            if (value == 0) {
+                chars[index++] = '0';
+                mLength += 1;
+                return this;
+            }
+            
+            if (negative) {
+                chars[index++] = '-';
+            }
+
+            int divisor = 1000000000;
+            int numberWidth = 10;
+            while (value < divisor) {
+                divisor /= 10;
+                numberWidth -= 1;
+                if (numberWidth < zeroPadWidth) {
+                    chars[index++] = '0';
+                }
+            }
+            
+            do {
+                int digit = value / divisor;
+                value -= digit * divisor;
+                divisor /= 10;
+                chars[index++] = (char) (digit + '0');
+            } while (divisor != 0);
+            
+            mLength = index;
+            return this;
+        }
+        
+        public FasterStringBuilder append(float value, int precision) {
+            int scale = 1;
+            for (int i = 0; i < precision; i++) {
+                scale *= 10;
+            }
+            value = (float) (Math.rint(value * scale) / scale);
+            
+            append((int) value);
+
+            if (precision != 0) {
+                append(".");
+                value = Math.abs(value);
+                value -= Math.floor(value);
+                append((int) (value * scale), precision);
+            }
+            
+            return this;
+        }
+        
+        @Override
+        public String toString() {
+            return new String(mChars, 0, mLength);
+        }
+        
+        private int reserve(int length) {
+            final int oldLength = mLength;
+            final int newLength = mLength + length;
+            final char[] oldChars = mChars;
+            final int oldCapacity = oldChars.length;
+            if (newLength > oldCapacity) {
+                final int newCapacity = oldCapacity * 2;
+                final char[] newChars = new char[newCapacity];
+                System.arraycopy(oldChars, 0, newChars, 0, oldLength);
+                mChars = newChars;
+            }
+            return oldLength;
+        }
+    }
 }