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