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