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