1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.widget;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Paint;
22import android.graphics.RectF;
23import android.graphics.Paint.FontMetricsInt;
24import android.hardware.input.InputManager;
25import android.hardware.input.InputManager.InputDeviceListener;
26import android.os.SystemProperties;
27import android.util.Log;
28import android.view.InputDevice;
29import android.view.KeyEvent;
30import android.view.MotionEvent;
31import android.view.VelocityTracker;
32import android.view.View;
33import android.view.ViewConfiguration;
34import android.view.WindowManagerPolicy.PointerEventListener;
35import android.view.MotionEvent.PointerCoords;
36
37import java.util.ArrayList;
38
39public class PointerLocationView extends View implements InputDeviceListener,
40        PointerEventListener {
41    private static final String TAG = "Pointer";
42
43    // The system property key used to specify an alternate velocity tracker strategy
44    // to plot alongside the default one.  Useful for testing and comparison purposes.
45    private static final String ALT_STRATEGY_PROPERY_KEY = "debug.velocitytracker.alt";
46
47    public static class PointerState {
48        // Trace of previous points.
49        private float[] mTraceX = new float[32];
50        private float[] mTraceY = new float[32];
51        private boolean[] mTraceCurrent = new boolean[32];
52        private int mTraceCount;
53
54        // True if the pointer is down.
55        private boolean mCurDown;
56
57        // Most recent coordinates.
58        private PointerCoords mCoords = new PointerCoords();
59        private int mToolType;
60
61        // Most recent velocity.
62        private float mXVelocity;
63        private float mYVelocity;
64        private float mAltXVelocity;
65        private float mAltYVelocity;
66
67        // Current bounding box, if any
68        private boolean mHasBoundingBox;
69        private float mBoundingLeft;
70        private float mBoundingTop;
71        private float mBoundingRight;
72        private float mBoundingBottom;
73
74        // Position estimator.
75        private VelocityTracker.Estimator mEstimator = new VelocityTracker.Estimator();
76        private VelocityTracker.Estimator mAltEstimator = new VelocityTracker.Estimator();
77
78        public void clearTrace() {
79            mTraceCount = 0;
80        }
81
82        public void addTrace(float x, float y, boolean current) {
83            int traceCapacity = mTraceX.length;
84            if (mTraceCount == traceCapacity) {
85                traceCapacity *= 2;
86                float[] newTraceX = new float[traceCapacity];
87                System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount);
88                mTraceX = newTraceX;
89
90                float[] newTraceY = new float[traceCapacity];
91                System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount);
92                mTraceY = newTraceY;
93
94                boolean[] newTraceCurrent = new boolean[traceCapacity];
95                System.arraycopy(mTraceCurrent, 0, newTraceCurrent, 0, mTraceCount);
96                mTraceCurrent= newTraceCurrent;
97            }
98
99            mTraceX[mTraceCount] = x;
100            mTraceY[mTraceCount] = y;
101            mTraceCurrent[mTraceCount] = current;
102            mTraceCount += 1;
103        }
104    }
105
106    private final int ESTIMATE_PAST_POINTS = 4;
107    private final int ESTIMATE_FUTURE_POINTS = 2;
108    private final float ESTIMATE_INTERVAL = 0.02f;
109
110    private final InputManager mIm;
111
112    private final ViewConfiguration mVC;
113    private final Paint mTextPaint;
114    private final Paint mTextBackgroundPaint;
115    private final Paint mTextLevelPaint;
116    private final Paint mPaint;
117    private final Paint mCurrentPointPaint;
118    private final Paint mTargetPaint;
119    private final Paint mPathPaint;
120    private final FontMetricsInt mTextMetrics = new FontMetricsInt();
121    private int mHeaderBottom;
122    private boolean mCurDown;
123    private int mCurNumPointers;
124    private int mMaxNumPointers;
125    private int mActivePointerId;
126    private final ArrayList<PointerState> mPointers = new ArrayList<PointerState>();
127    private final PointerCoords mTempCoords = new PointerCoords();
128
129    private final VelocityTracker mVelocity;
130    private final VelocityTracker mAltVelocity;
131
132    private final FasterStringBuilder mText = new FasterStringBuilder();
133
134    private boolean mPrintCoords = true;
135
136    public PointerLocationView(Context c) {
137        super(c);
138        setFocusableInTouchMode(true);
139
140        mIm = c.getSystemService(InputManager.class);
141
142        mVC = ViewConfiguration.get(c);
143        mTextPaint = new Paint();
144        mTextPaint.setAntiAlias(true);
145        mTextPaint.setTextSize(10
146                * getResources().getDisplayMetrics().density);
147        mTextPaint.setARGB(255, 0, 0, 0);
148        mTextBackgroundPaint = new Paint();
149        mTextBackgroundPaint.setAntiAlias(false);
150        mTextBackgroundPaint.setARGB(128, 255, 255, 255);
151        mTextLevelPaint = new Paint();
152        mTextLevelPaint.setAntiAlias(false);
153        mTextLevelPaint.setARGB(192, 255, 0, 0);
154        mPaint = new Paint();
155        mPaint.setAntiAlias(true);
156        mPaint.setARGB(255, 255, 255, 255);
157        mPaint.setStyle(Paint.Style.STROKE);
158        mPaint.setStrokeWidth(2);
159        mCurrentPointPaint = new Paint();
160        mCurrentPointPaint.setAntiAlias(true);
161        mCurrentPointPaint.setARGB(255, 255, 0, 0);
162        mCurrentPointPaint.setStyle(Paint.Style.STROKE);
163        mCurrentPointPaint.setStrokeWidth(2);
164        mTargetPaint = new Paint();
165        mTargetPaint.setAntiAlias(false);
166        mTargetPaint.setARGB(255, 0, 0, 192);
167        mPathPaint = new Paint();
168        mPathPaint.setAntiAlias(false);
169        mPathPaint.setARGB(255, 0, 96, 255);
170        mPaint.setStyle(Paint.Style.STROKE);
171        mPaint.setStrokeWidth(1);
172
173        PointerState ps = new PointerState();
174        mPointers.add(ps);
175        mActivePointerId = 0;
176
177        mVelocity = VelocityTracker.obtain();
178
179        String altStrategy = SystemProperties.get(ALT_STRATEGY_PROPERY_KEY);
180        if (altStrategy.length() != 0) {
181            Log.d(TAG, "Comparing default velocity tracker strategy with " + altStrategy);
182            mAltVelocity = VelocityTracker.obtain(altStrategy);
183        } else {
184            mAltVelocity = null;
185        }
186    }
187
188    public void setPrintCoords(boolean state) {
189        mPrintCoords = state;
190    }
191
192    @Override
193    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
194        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
195        mTextPaint.getFontMetricsInt(mTextMetrics);
196        mHeaderBottom = -mTextMetrics.ascent+mTextMetrics.descent+2;
197        if (false) {
198            Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent
199                    + " descent=" + mTextMetrics.descent
200                    + " leading=" + mTextMetrics.leading
201                    + " top=" + mTextMetrics.top
202                    + " bottom=" + mTextMetrics.bottom);
203        }
204    }
205
206    // Draw an oval.  When angle is 0 radians, orients the major axis vertically,
207    // angles less than or greater than 0 radians rotate the major axis left or right.
208    private RectF mReusableOvalRect = new RectF();
209    private void drawOval(Canvas canvas, float x, float y, float major, float minor,
210            float angle, Paint paint) {
211        canvas.save(Canvas.MATRIX_SAVE_FLAG);
212        canvas.rotate((float) (angle * 180 / Math.PI), x, y);
213        mReusableOvalRect.left = x - minor / 2;
214        mReusableOvalRect.right = x + minor / 2;
215        mReusableOvalRect.top = y - major / 2;
216        mReusableOvalRect.bottom = y + major / 2;
217        canvas.drawOval(mReusableOvalRect, paint);
218        canvas.restore();
219    }
220
221    @Override
222    protected void onDraw(Canvas canvas) {
223        final int w = getWidth();
224        final int itemW = w/7;
225        final int base = -mTextMetrics.ascent+1;
226        final int bottom = mHeaderBottom;
227
228        final int NP = mPointers.size();
229
230        // Labels
231        if (mActivePointerId >= 0) {
232            final PointerState ps = mPointers.get(mActivePointerId);
233
234            canvas.drawRect(0, 0, itemW-1, bottom,mTextBackgroundPaint);
235            canvas.drawText(mText.clear()
236                    .append("P: ").append(mCurNumPointers)
237                    .append(" / ").append(mMaxNumPointers)
238                    .toString(), 1, base, mTextPaint);
239
240            final int N = ps.mTraceCount;
241            if ((mCurDown && ps.mCurDown) || N == 0) {
242                canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint);
243                canvas.drawText(mText.clear()
244                        .append("X: ").append(ps.mCoords.x, 1)
245                        .toString(), 1 + itemW, base, mTextPaint);
246                canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint);
247                canvas.drawText(mText.clear()
248                        .append("Y: ").append(ps.mCoords.y, 1)
249                        .toString(), 1 + itemW * 2, base, mTextPaint);
250            } else {
251                float dx = ps.mTraceX[N - 1] - ps.mTraceX[0];
252                float dy = ps.mTraceY[N - 1] - ps.mTraceY[0];
253                canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom,
254                        Math.abs(dx) < mVC.getScaledTouchSlop()
255                        ? mTextBackgroundPaint : mTextLevelPaint);
256                canvas.drawText(mText.clear()
257                        .append("dX: ").append(dx, 1)
258                        .toString(), 1 + itemW, base, mTextPaint);
259                canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom,
260                        Math.abs(dy) < mVC.getScaledTouchSlop()
261                        ? mTextBackgroundPaint : mTextLevelPaint);
262                canvas.drawText(mText.clear()
263                        .append("dY: ").append(dy, 1)
264                        .toString(), 1 + itemW * 2, base, mTextPaint);
265            }
266
267            canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint);
268            canvas.drawText(mText.clear()
269                    .append("Xv: ").append(ps.mXVelocity, 3)
270                    .toString(), 1 + itemW * 3, base, mTextPaint);
271
272            canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint);
273            canvas.drawText(mText.clear()
274                    .append("Yv: ").append(ps.mYVelocity, 3)
275                    .toString(), 1 + itemW * 4, base, mTextPaint);
276
277            canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint);
278            canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCoords.pressure * itemW) - 1,
279                    bottom, mTextLevelPaint);
280            canvas.drawText(mText.clear()
281                    .append("Prs: ").append(ps.mCoords.pressure, 2)
282                    .toString(), 1 + itemW * 5, base, mTextPaint);
283
284            canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint);
285            canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCoords.size * itemW) - 1,
286                    bottom, mTextLevelPaint);
287            canvas.drawText(mText.clear()
288                    .append("Size: ").append(ps.mCoords.size, 2)
289                    .toString(), 1 + itemW * 6, base, mTextPaint);
290        }
291
292        // Pointer trace.
293        for (int p = 0; p < NP; p++) {
294            final PointerState ps = mPointers.get(p);
295
296            // Draw path.
297            final int N = ps.mTraceCount;
298            float lastX = 0, lastY = 0;
299            boolean haveLast = false;
300            boolean drawn = false;
301            mPaint.setARGB(255, 128, 255, 255);
302            for (int i=0; i < N; i++) {
303                float x = ps.mTraceX[i];
304                float y = ps.mTraceY[i];
305                if (Float.isNaN(x)) {
306                    haveLast = false;
307                    continue;
308                }
309                if (haveLast) {
310                    canvas.drawLine(lastX, lastY, x, y, mPathPaint);
311                    final Paint paint = ps.mTraceCurrent[i] ? mCurrentPointPaint : mPaint;
312                    canvas.drawPoint(lastX, lastY, paint);
313                    drawn = true;
314                }
315                lastX = x;
316                lastY = y;
317                haveLast = true;
318            }
319
320            if (drawn) {
321                // Draw movement estimate curve.
322                mPaint.setARGB(128, 128, 0, 128);
323                float lx = ps.mEstimator.estimateX(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
324                float ly = ps.mEstimator.estimateY(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
325                for (int i = -ESTIMATE_PAST_POINTS + 1; i <= ESTIMATE_FUTURE_POINTS; i++) {
326                    float x = ps.mEstimator.estimateX(i * ESTIMATE_INTERVAL);
327                    float y = ps.mEstimator.estimateY(i * ESTIMATE_INTERVAL);
328                    canvas.drawLine(lx, ly, x, y, mPaint);
329                    lx = x;
330                    ly = y;
331                }
332
333                // Draw velocity vector.
334                mPaint.setARGB(255, 255, 64, 128);
335                float xVel = ps.mXVelocity * (1000 / 60);
336                float yVel = ps.mYVelocity * (1000 / 60);
337                canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
338
339                // Draw alternate estimate.
340                if (mAltVelocity != null) {
341                    mPaint.setARGB(128, 0, 128, 128);
342                    lx = ps.mAltEstimator.estimateX(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
343                    ly = ps.mAltEstimator.estimateY(-ESTIMATE_PAST_POINTS * ESTIMATE_INTERVAL);
344                    for (int i = -ESTIMATE_PAST_POINTS + 1; i <= ESTIMATE_FUTURE_POINTS; i++) {
345                        float x = ps.mAltEstimator.estimateX(i * ESTIMATE_INTERVAL);
346                        float y = ps.mAltEstimator.estimateY(i * ESTIMATE_INTERVAL);
347                        canvas.drawLine(lx, ly, x, y, mPaint);
348                        lx = x;
349                        ly = y;
350                    }
351
352                    mPaint.setARGB(255, 64, 255, 128);
353                    xVel = ps.mAltXVelocity * (1000 / 60);
354                    yVel = ps.mAltYVelocity * (1000 / 60);
355                    canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
356                }
357            }
358
359            if (mCurDown && ps.mCurDown) {
360                // Draw crosshairs.
361                canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);
362                canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint);
363
364                // Draw current point.
365                int pressureLevel = (int)(ps.mCoords.pressure * 255);
366                mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
367                canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
368
369                // Draw current touch ellipse.
370                mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);
371                drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,
372                        ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);
373
374                // Draw current tool ellipse.
375                mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);
376                drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
377                        ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
378
379                // Draw the orientation arrow.
380                float arrowSize = ps.mCoords.toolMajor * 0.7f;
381                if (arrowSize < 20) {
382                    arrowSize = 20;
383                }
384                mPaint.setARGB(255, pressureLevel, 255, 0);
385                float orientationVectorX = (float) (Math.sin(ps.mCoords.orientation)
386                        * arrowSize);
387                float orientationVectorY = (float) (-Math.cos(ps.mCoords.orientation)
388                        * arrowSize);
389                if (ps.mToolType == MotionEvent.TOOL_TYPE_STYLUS
390                        || ps.mToolType == MotionEvent.TOOL_TYPE_ERASER) {
391                    // Show full circle orientation.
392                    canvas.drawLine(ps.mCoords.x, ps.mCoords.y,
393                            ps.mCoords.x + orientationVectorX,
394                            ps.mCoords.y + orientationVectorY,
395                            mPaint);
396                } else {
397                    // Show half circle orientation.
398                    canvas.drawLine(
399                            ps.mCoords.x - orientationVectorX,
400                            ps.mCoords.y - orientationVectorY,
401                            ps.mCoords.x + orientationVectorX,
402                            ps.mCoords.y + orientationVectorY,
403                            mPaint);
404                }
405
406                // Draw the tilt point along the orientation arrow.
407                float tiltScale = (float) Math.sin(
408                        ps.mCoords.getAxisValue(MotionEvent.AXIS_TILT));
409                canvas.drawCircle(
410                        ps.mCoords.x + orientationVectorX * tiltScale,
411                        ps.mCoords.y + orientationVectorY * tiltScale,
412                        3.0f, mPaint);
413
414                // Draw the current bounding box
415                if (ps.mHasBoundingBox) {
416                    canvas.drawRect(ps.mBoundingLeft, ps.mBoundingTop,
417                            ps.mBoundingRight, ps.mBoundingBottom, mPaint);
418                }
419            }
420        }
421    }
422
423    private void logMotionEvent(String type, MotionEvent event) {
424        final int action = event.getAction();
425        final int N = event.getHistorySize();
426        final int NI = event.getPointerCount();
427        for (int historyPos = 0; historyPos < N; historyPos++) {
428            for (int i = 0; i < NI; i++) {
429                final int id = event.getPointerId(i);
430                event.getHistoricalPointerCoords(i, historyPos, mTempCoords);
431                logCoords(type, action, i, mTempCoords, id, event);
432            }
433        }
434        for (int i = 0; i < NI; i++) {
435            final int id = event.getPointerId(i);
436            event.getPointerCoords(i, mTempCoords);
437            logCoords(type, action, i, mTempCoords, id, event);
438        }
439    }
440
441    private void logCoords(String type, int action, int index,
442            MotionEvent.PointerCoords coords, int id, MotionEvent event) {
443        final int toolType = event.getToolType(index);
444        final int buttonState = event.getButtonState();
445        final String prefix;
446        switch (action & MotionEvent.ACTION_MASK) {
447            case MotionEvent.ACTION_DOWN:
448                prefix = "DOWN";
449                break;
450            case MotionEvent.ACTION_UP:
451                prefix = "UP";
452                break;
453            case MotionEvent.ACTION_MOVE:
454                prefix = "MOVE";
455                break;
456            case MotionEvent.ACTION_CANCEL:
457                prefix = "CANCEL";
458                break;
459            case MotionEvent.ACTION_OUTSIDE:
460                prefix = "OUTSIDE";
461                break;
462            case MotionEvent.ACTION_POINTER_DOWN:
463                if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
464                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
465                    prefix = "DOWN";
466                } else {
467                    prefix = "MOVE";
468                }
469                break;
470            case MotionEvent.ACTION_POINTER_UP:
471                if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
472                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
473                    prefix = "UP";
474                } else {
475                    prefix = "MOVE";
476                }
477                break;
478            case MotionEvent.ACTION_HOVER_MOVE:
479                prefix = "HOVER MOVE";
480                break;
481            case MotionEvent.ACTION_HOVER_ENTER:
482                prefix = "HOVER ENTER";
483                break;
484            case MotionEvent.ACTION_HOVER_EXIT:
485                prefix = "HOVER EXIT";
486                break;
487            case MotionEvent.ACTION_SCROLL:
488                prefix = "SCROLL";
489                break;
490            default:
491                prefix = Integer.toString(action);
492                break;
493        }
494
495        Log.i(TAG, mText.clear()
496                .append(type).append(" id ").append(id + 1)
497                .append(": ")
498                .append(prefix)
499                .append(" (").append(coords.x, 3).append(", ").append(coords.y, 3)
500                .append(") Pressure=").append(coords.pressure, 3)
501                .append(" Size=").append(coords.size, 3)
502                .append(" TouchMajor=").append(coords.touchMajor, 3)
503                .append(" TouchMinor=").append(coords.touchMinor, 3)
504                .append(" ToolMajor=").append(coords.toolMajor, 3)
505                .append(" ToolMinor=").append(coords.toolMinor, 3)
506                .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1)
507                .append("deg")
508                .append(" Tilt=").append((float)(
509                        coords.getAxisValue(MotionEvent.AXIS_TILT) * 180 / Math.PI), 1)
510                .append("deg")
511                .append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1)
512                .append(" VScroll=").append(coords.getAxisValue(MotionEvent.AXIS_VSCROLL), 1)
513                .append(" HScroll=").append(coords.getAxisValue(MotionEvent.AXIS_HSCROLL), 1)
514                .append(" BoundingBox=[(")
515                .append(event.getAxisValue(MotionEvent.AXIS_GENERIC_1), 3)
516                .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_2), 3).append(")")
517                .append(", (").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_3), 3)
518                .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_4), 3)
519                .append(")]")
520                .append(" ToolType=").append(MotionEvent.toolTypeToString(toolType))
521                .append(" ButtonState=").append(MotionEvent.buttonStateToString(buttonState))
522                .toString());
523    }
524
525    @Override
526    public void onPointerEvent(MotionEvent event) {
527        final int action = event.getAction();
528        int NP = mPointers.size();
529
530        if (action == MotionEvent.ACTION_DOWN
531                || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
532            final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
533                    >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down
534            if (action == MotionEvent.ACTION_DOWN) {
535                for (int p=0; p<NP; p++) {
536                    final PointerState ps = mPointers.get(p);
537                    ps.clearTrace();
538                    ps.mCurDown = false;
539                }
540                mCurDown = true;
541                mCurNumPointers = 0;
542                mMaxNumPointers = 0;
543                mVelocity.clear();
544                if (mAltVelocity != null) {
545                    mAltVelocity.clear();
546                }
547            }
548
549            mCurNumPointers += 1;
550            if (mMaxNumPointers < mCurNumPointers) {
551                mMaxNumPointers = mCurNumPointers;
552            }
553
554            final int id = event.getPointerId(index);
555            while (NP <= id) {
556                PointerState ps = new PointerState();
557                mPointers.add(ps);
558                NP++;
559            }
560
561            if (mActivePointerId < 0 ||
562                    !mPointers.get(mActivePointerId).mCurDown) {
563                mActivePointerId = id;
564            }
565
566            final PointerState ps = mPointers.get(id);
567            ps.mCurDown = true;
568            InputDevice device = InputDevice.getDevice(event.getDeviceId());
569            ps.mHasBoundingBox = device != null &&
570                    device.getMotionRange(MotionEvent.AXIS_GENERIC_1) != null;
571        }
572
573        final int NI = event.getPointerCount();
574
575        mVelocity.addMovement(event);
576        mVelocity.computeCurrentVelocity(1);
577        if (mAltVelocity != null) {
578            mAltVelocity.addMovement(event);
579            mAltVelocity.computeCurrentVelocity(1);
580        }
581
582        final int N = event.getHistorySize();
583        for (int historyPos = 0; historyPos < N; historyPos++) {
584            for (int i = 0; i < NI; i++) {
585                final int id = event.getPointerId(i);
586                final PointerState ps = mCurDown ? mPointers.get(id) : null;
587                final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
588                event.getHistoricalPointerCoords(i, historyPos, coords);
589                if (mPrintCoords) {
590                    logCoords("Pointer", action, i, coords, id, event);
591                }
592                if (ps != null) {
593                    ps.addTrace(coords.x, coords.y, false);
594                }
595            }
596        }
597        for (int i = 0; i < NI; i++) {
598            final int id = event.getPointerId(i);
599            final PointerState ps = mCurDown ? mPointers.get(id) : null;
600            final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
601            event.getPointerCoords(i, coords);
602            if (mPrintCoords) {
603                logCoords("Pointer", action, i, coords, id, event);
604            }
605            if (ps != null) {
606                ps.addTrace(coords.x, coords.y, true);
607                ps.mXVelocity = mVelocity.getXVelocity(id);
608                ps.mYVelocity = mVelocity.getYVelocity(id);
609                mVelocity.getEstimator(id, ps.mEstimator);
610                if (mAltVelocity != null) {
611                    ps.mAltXVelocity = mAltVelocity.getXVelocity(id);
612                    ps.mAltYVelocity = mAltVelocity.getYVelocity(id);
613                    mAltVelocity.getEstimator(id, ps.mAltEstimator);
614                }
615                ps.mToolType = event.getToolType(i);
616
617                if (ps.mHasBoundingBox) {
618                    ps.mBoundingLeft = event.getAxisValue(MotionEvent.AXIS_GENERIC_1, i);
619                    ps.mBoundingTop = event.getAxisValue(MotionEvent.AXIS_GENERIC_2, i);
620                    ps.mBoundingRight = event.getAxisValue(MotionEvent.AXIS_GENERIC_3, i);
621                    ps.mBoundingBottom = event.getAxisValue(MotionEvent.AXIS_GENERIC_4, i);
622                }
623            }
624        }
625
626        if (action == MotionEvent.ACTION_UP
627                || action == MotionEvent.ACTION_CANCEL
628                || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
629            final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
630                    >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP
631
632            final int id = event.getPointerId(index);
633            final PointerState ps = mPointers.get(id);
634            ps.mCurDown = false;
635
636            if (action == MotionEvent.ACTION_UP
637                    || action == MotionEvent.ACTION_CANCEL) {
638                mCurDown = false;
639                mCurNumPointers = 0;
640            } else {
641                mCurNumPointers -= 1;
642                if (mActivePointerId == id) {
643                    mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
644                }
645                ps.addTrace(Float.NaN, Float.NaN, false);
646            }
647        }
648
649        invalidate();
650    }
651
652    @Override
653    public boolean onTouchEvent(MotionEvent event) {
654        onPointerEvent(event);
655
656        if (event.getAction() == MotionEvent.ACTION_DOWN && !isFocused()) {
657            requestFocus();
658        }
659        return true;
660    }
661
662    @Override
663    public boolean onGenericMotionEvent(MotionEvent event) {
664        final int source = event.getSource();
665        if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
666            onPointerEvent(event);
667        } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
668            logMotionEvent("Joystick", event);
669        } else if ((source & InputDevice.SOURCE_CLASS_POSITION) != 0) {
670            logMotionEvent("Position", event);
671        } else {
672            logMotionEvent("Generic", event);
673        }
674        return true;
675    }
676
677    @Override
678    public boolean onKeyDown(int keyCode, KeyEvent event) {
679        if (shouldLogKey(keyCode)) {
680            final int repeatCount = event.getRepeatCount();
681            if (repeatCount == 0) {
682                Log.i(TAG, "Key Down: " + event);
683            } else {
684                Log.i(TAG, "Key Repeat #" + repeatCount + ": " + event);
685            }
686            return true;
687        }
688        return super.onKeyDown(keyCode, event);
689    }
690
691    @Override
692    public boolean onKeyUp(int keyCode, KeyEvent event) {
693        if (shouldLogKey(keyCode)) {
694            Log.i(TAG, "Key Up: " + event);
695            return true;
696        }
697        return super.onKeyUp(keyCode, event);
698    }
699
700    private static boolean shouldLogKey(int keyCode) {
701        switch (keyCode) {
702            case KeyEvent.KEYCODE_DPAD_UP:
703            case KeyEvent.KEYCODE_DPAD_DOWN:
704            case KeyEvent.KEYCODE_DPAD_LEFT:
705            case KeyEvent.KEYCODE_DPAD_RIGHT:
706            case KeyEvent.KEYCODE_DPAD_CENTER:
707                return true;
708            default:
709                return KeyEvent.isGamepadButton(keyCode)
710                    || KeyEvent.isModifierKey(keyCode);
711        }
712    }
713
714    @Override
715    public boolean onTrackballEvent(MotionEvent event) {
716        logMotionEvent("Trackball", event);
717        return true;
718    }
719
720    @Override
721    protected void onAttachedToWindow() {
722        super.onAttachedToWindow();
723
724        mIm.registerInputDeviceListener(this, getHandler());
725        logInputDevices();
726    }
727
728    @Override
729    protected void onDetachedFromWindow() {
730        super.onDetachedFromWindow();
731
732        mIm.unregisterInputDeviceListener(this);
733    }
734
735    @Override
736    public void onInputDeviceAdded(int deviceId) {
737        logInputDeviceState(deviceId, "Device Added");
738    }
739
740    @Override
741    public void onInputDeviceChanged(int deviceId) {
742        logInputDeviceState(deviceId, "Device Changed");
743    }
744
745    @Override
746    public void onInputDeviceRemoved(int deviceId) {
747        logInputDeviceState(deviceId, "Device Removed");
748    }
749
750    private void logInputDevices() {
751        int[] deviceIds = InputDevice.getDeviceIds();
752        for (int i = 0; i < deviceIds.length; i++) {
753            logInputDeviceState(deviceIds[i], "Device Enumerated");
754        }
755    }
756
757    private void logInputDeviceState(int deviceId, String state) {
758        InputDevice device = mIm.getInputDevice(deviceId);
759        if (device != null) {
760            Log.i(TAG, state + ": " + device);
761        } else {
762            Log.i(TAG, state + ": " + deviceId);
763        }
764    }
765
766    // HACK
767    // A quick and dirty string builder implementation optimized for GC.
768    // Using String.format causes the application grind to a halt when
769    // more than a couple of pointers are down due to the number of
770    // temporary objects allocated while formatting strings for drawing or logging.
771    private static final class FasterStringBuilder {
772        private char[] mChars;
773        private int mLength;
774
775        public FasterStringBuilder() {
776            mChars = new char[64];
777        }
778
779        public FasterStringBuilder clear() {
780            mLength = 0;
781            return this;
782        }
783
784        public FasterStringBuilder append(String value) {
785            final int valueLength = value.length();
786            final int index = reserve(valueLength);
787            value.getChars(0, valueLength, mChars, index);
788            mLength += valueLength;
789            return this;
790        }
791
792        public FasterStringBuilder append(int value) {
793            return append(value, 0);
794        }
795
796        public FasterStringBuilder append(int value, int zeroPadWidth) {
797            final boolean negative = value < 0;
798            if (negative) {
799                value = - value;
800                if (value < 0) {
801                    append("-2147483648");
802                    return this;
803                }
804            }
805
806            int index = reserve(11);
807            final char[] chars = mChars;
808
809            if (value == 0) {
810                chars[index++] = '0';
811                mLength += 1;
812                return this;
813            }
814
815            if (negative) {
816                chars[index++] = '-';
817            }
818
819            int divisor = 1000000000;
820            int numberWidth = 10;
821            while (value < divisor) {
822                divisor /= 10;
823                numberWidth -= 1;
824                if (numberWidth < zeroPadWidth) {
825                    chars[index++] = '0';
826                }
827            }
828
829            do {
830                int digit = value / divisor;
831                value -= digit * divisor;
832                divisor /= 10;
833                chars[index++] = (char) (digit + '0');
834            } while (divisor != 0);
835
836            mLength = index;
837            return this;
838        }
839
840        public FasterStringBuilder append(float value, int precision) {
841            int scale = 1;
842            for (int i = 0; i < precision; i++) {
843                scale *= 10;
844            }
845            value = (float) (Math.rint(value * scale) / scale);
846
847            append((int) value);
848
849            if (precision != 0) {
850                append(".");
851                value = Math.abs(value);
852                value -= Math.floor(value);
853                append((int) (value * scale), precision);
854            }
855
856            return this;
857        }
858
859        @Override
860        public String toString() {
861            return new String(mChars, 0, mLength);
862        }
863
864        private int reserve(int length) {
865            final int oldLength = mLength;
866            final int newLength = mLength + length;
867            final char[] oldChars = mChars;
868            final int oldCapacity = oldChars.length;
869            if (newLength > oldCapacity) {
870                final int newCapacity = oldCapacity * 2;
871                final char[] newChars = new char[newCapacity];
872                System.arraycopy(oldChars, 0, newChars, 0, oldLength);
873                mChars = newChars;
874            }
875            return oldLength;
876        }
877    }
878}
879