• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.internal.widget;
18 
19 import static java.lang.Float.NaN;
20 
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Insets;
28 import android.graphics.Paint;
29 import android.graphics.Paint.FontMetricsInt;
30 import android.graphics.Path;
31 import android.graphics.PorterDuff;
32 import android.graphics.RectF;
33 import android.graphics.Region;
34 import android.hardware.input.InputManager;
35 import android.hardware.input.InputManager.InputDeviceListener;
36 import android.os.Handler;
37 import android.os.RemoteException;
38 import android.os.SystemProperties;
39 import android.util.Log;
40 import android.util.Slog;
41 import android.util.SparseArray;
42 import android.view.ISystemGestureExclusionListener;
43 import android.view.InputDevice;
44 import android.view.KeyEvent;
45 import android.view.MotionEvent;
46 import android.view.MotionEvent.PointerCoords;
47 import android.view.RoundedCorner;
48 import android.view.Surface;
49 import android.view.VelocityTracker;
50 import android.view.View;
51 import android.view.ViewConfiguration;
52 import android.view.WindowInsets;
53 import android.view.WindowManagerGlobal;
54 import android.view.WindowManagerPolicyConstants.PointerEventListener;
55 
56 public class PointerLocationView extends View implements InputDeviceListener,
57         PointerEventListener {
58     private static final String TAG = "Pointer";
59 
60     // The system property key used to specify an alternate velocity tracker strategy
61     // to plot alongside the default one.  Useful for testing and comparison purposes.
62     private static final String ALT_STRATEGY_PROPERY_KEY = "debug.velocitytracker.alt";
63 
64     /**
65      * If set to a positive value between 1-255, shows an overlay with the approved (red) and
66      * rejected (blue) exclusions.
67      */
68     private static final String GESTURE_EXCLUSION_PROP = "debug.pointerlocation.showexclusion";
69 
70     // In case when it's in first time or no active pointer found, draw the empty state.
71     private static final PointerState EMPTY_POINTER_STATE = new PointerState();
72 
73     public static class PointerState {
74         private float mCurrentX = NaN;
75         private float mCurrentY = NaN;
76         private float mPreviousX = NaN;
77         private float mPreviousY = NaN;
78         private float mFirstX = NaN;
79         private float mFirstY = NaN;
80         private boolean mPreviousPointIsHistorical;
81         private boolean mCurrentPointIsHistorical;
82 
83         // True if the pointer is down.
84         @UnsupportedAppUsage
85         private boolean mCurDown;
86 
87         // Most recent coordinates.
88         private final PointerCoords mCoords = new PointerCoords();
89         private int mToolType;
90 
91         // Most recent velocity.
92         private float mXVelocity;
93         private float mYVelocity;
94         private float mAltXVelocity;
95         private float mAltYVelocity;
96 
97         // Current bounding box, if any
98         private boolean mHasBoundingBox;
99         private float mBoundingLeft;
100         private float mBoundingTop;
101         private float mBoundingRight;
102         private float mBoundingBottom;
103 
104         @UnsupportedAppUsage
PointerState()105         public PointerState() {
106         }
107 
addTrace(float x, float y, boolean isHistorical)108         void addTrace(float x, float y, boolean isHistorical) {
109             if (Float.isNaN(mFirstX)) {
110                 mFirstX = x;
111             }
112             if (Float.isNaN(mFirstY)) {
113                 mFirstY = y;
114             }
115 
116             mPreviousX = mCurrentX;
117             mPreviousY = mCurrentY;
118             mCurrentX = x;
119             mCurrentY = y;
120             mPreviousPointIsHistorical = mCurrentPointIsHistorical;
121             mCurrentPointIsHistorical = isHistorical;
122         }
123     }
124 
125     private final InputManager mIm;
126 
127     private final ViewConfiguration mVC;
128     private final Paint mTextPaint;
129     private final Paint mTextBackgroundPaint;
130     private final Paint mTextLevelPaint;
131     private final Paint mPaint;
132     private final Paint mCurrentPointPaint;
133     private final Paint mTargetPaint;
134     private final Paint mPathPaint;
135     private final FontMetricsInt mTextMetrics = new FontMetricsInt();
136     private int mHeaderBottom;
137     private int mHeaderPaddingTop = 0;
138     private Insets mWaterfallInsets = Insets.NONE;
139     @UnsupportedAppUsage
140     private boolean mCurDown;
141     @UnsupportedAppUsage
142     private int mCurNumPointers;
143     @UnsupportedAppUsage
144     private int mMaxNumPointers;
145     private int mActivePointerId;
146     @UnsupportedAppUsage
147     private final SparseArray<PointerState> mPointers = new SparseArray<PointerState>();
148     private final PointerCoords mTempCoords = new PointerCoords();
149 
150     // Draw the trace of all pointers in the current gesture in a separate layer
151     // that is not cleared on every frame so that we don't have to re-draw the
152     // entire trace on each frame. The trace bitmap is in the coordinate space of the unrotated
153     // display.
154     private Bitmap mTraceBitmap;
155     private final Canvas mTraceCanvas;
156 
157     private final Region mSystemGestureExclusion = new Region();
158     private final Region mSystemGestureExclusionRejected = new Region();
159     private final Path mSystemGestureExclusionPath = new Path();
160     private final Paint mSystemGestureExclusionPaint;
161     private final Paint mSystemGestureExclusionRejectedPaint;
162 
163     private final VelocityTracker mVelocity;
164     private final VelocityTracker mAltVelocity;
165 
166     private final FasterStringBuilder mText = new FasterStringBuilder();
167 
168     @UnsupportedAppUsage
169     private boolean mPrintCoords = true;
170 
171     private float mDensity;
172 
PointerLocationView(Context c)173     public PointerLocationView(Context c) {
174         super(c);
175         setFocusableInTouchMode(true);
176 
177         mIm = c.getSystemService(InputManager.class);
178 
179         mVC = ViewConfiguration.get(c);
180         mTextPaint = new Paint();
181         mTextPaint.setAntiAlias(true);
182         mTextPaint.setARGB(255, 0, 0, 0);
183         mTextBackgroundPaint = new Paint();
184         mTextBackgroundPaint.setAntiAlias(false);
185         mTextBackgroundPaint.setARGB(128, 255, 255, 255);
186         mTextLevelPaint = new Paint();
187         mTextLevelPaint.setAntiAlias(false);
188         mTextLevelPaint.setARGB(192, 255, 0, 0);
189         mPaint = new Paint();
190         mPaint.setAntiAlias(true);
191         mPaint.setARGB(255, 255, 255, 255);
192         mPaint.setStyle(Paint.Style.STROKE);
193         mCurrentPointPaint = new Paint();
194         mCurrentPointPaint.setAntiAlias(true);
195         mCurrentPointPaint.setARGB(255, 255, 0, 0);
196         mCurrentPointPaint.setStyle(Paint.Style.STROKE);
197         mTargetPaint = new Paint();
198         mTargetPaint.setAntiAlias(false);
199         mTargetPaint.setARGB(255, 0, 0, 192);
200         mPathPaint = new Paint();
201         mPathPaint.setAntiAlias(false);
202         mPathPaint.setARGB(255, 0, 96, 255);
203         mPathPaint.setStyle(Paint.Style.STROKE);
204 
205         mTraceCanvas = new Canvas();
206         configureTraceBitmap();
207 
208         configureDensityDependentFactors();
209 
210         mSystemGestureExclusionPaint = new Paint();
211         mSystemGestureExclusionPaint.setARGB(25, 255, 0, 0);
212         mSystemGestureExclusionPaint.setStyle(Paint.Style.FILL_AND_STROKE);
213 
214         mSystemGestureExclusionRejectedPaint = new Paint();
215         mSystemGestureExclusionRejectedPaint.setARGB(25, 0, 0, 255);
216         mSystemGestureExclusionRejectedPaint.setStyle(Paint.Style.FILL_AND_STROKE);
217 
218         mActivePointerId = 0;
219 
220         mVelocity = VelocityTracker.obtain();
221 
222         String altStrategy = SystemProperties.get(ALT_STRATEGY_PROPERY_KEY);
223         if (altStrategy.length() != 0) {
224             Log.d(TAG, "Comparing default velocity tracker strategy with " + altStrategy);
225             mAltVelocity = VelocityTracker.obtain(altStrategy);
226         } else {
227             mAltVelocity = null;
228         }
229     }
230 
setPrintCoords(boolean state)231     public void setPrintCoords(boolean state) {
232         mPrintCoords = state;
233     }
234 
235     @Override
onApplyWindowInsets(WindowInsets insets)236     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
237         int headerPaddingTop = 0;
238         Insets waterfallInsets = Insets.NONE;
239 
240         final RoundedCorner topLeftRounded =
241                 insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT);
242         if (topLeftRounded != null) {
243             headerPaddingTop = topLeftRounded.getRadius();
244         }
245 
246         final RoundedCorner topRightRounded =
247                 insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT);
248         if (topRightRounded != null) {
249             headerPaddingTop = Math.max(headerPaddingTop, topRightRounded.getRadius());
250         }
251 
252         if (insets.getDisplayCutout() != null) {
253             headerPaddingTop =
254                     Math.max(headerPaddingTop, insets.getDisplayCutout().getSafeInsetTop());
255             waterfallInsets = insets.getDisplayCutout().getWaterfallInsets();
256         }
257 
258         mHeaderPaddingTop = headerPaddingTop;
259         mWaterfallInsets = waterfallInsets;
260         return super.onApplyWindowInsets(insets);
261     }
262 
263     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)264     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
265         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
266         mTextPaint.getFontMetricsInt(mTextMetrics);
267         mHeaderBottom = mHeaderPaddingTop - mTextMetrics.ascent + mTextMetrics.descent + 2;
268         if (false) {
269             Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent
270                     + " descent=" + mTextMetrics.descent
271                     + " leading=" + mTextMetrics.leading
272                     + " top=" + mTextMetrics.top
273                     + " bottom=" + mTextMetrics.bottom);
274         }
275     }
276 
277     // Draw an oval.  When angle is 0 radians, orients the major axis vertically,
278     // angles less than or greater than 0 radians rotate the major axis left or right.
279     private final RectF mReusableOvalRect = new RectF();
280 
drawOval(Canvas canvas, float x, float y, float major, float minor, float angle, Paint paint)281     private void drawOval(Canvas canvas, float x, float y, float major, float minor,
282             float angle, Paint paint) {
283         canvas.save(Canvas.MATRIX_SAVE_FLAG);
284         canvas.rotate((float) (angle * 180 / Math.PI), x, y);
285         mReusableOvalRect.left = x - minor / 2;
286         mReusableOvalRect.right = x + minor / 2;
287         mReusableOvalRect.top = y - major / 2;
288         mReusableOvalRect.bottom = y + major / 2;
289         canvas.drawOval(mReusableOvalRect, paint);
290         canvas.restore();
291     }
292 
293     @Override
onDraw(Canvas canvas)294     protected void onDraw(Canvas canvas) {
295         final int NP = mPointers.size();
296 
297         // Pointer trace.
298         canvas.save();
299         rotateCanvasToUnrotatedDisplay(canvas);
300         canvas.drawBitmap(mTraceBitmap, 0, 0, null);
301         canvas.restore();
302 
303         if (!mSystemGestureExclusion.isEmpty()) {
304             mSystemGestureExclusionPath.reset();
305             mSystemGestureExclusion.getBoundaryPath(mSystemGestureExclusionPath);
306             canvas.drawPath(mSystemGestureExclusionPath, mSystemGestureExclusionPaint);
307         }
308 
309         if (!mSystemGestureExclusionRejected.isEmpty()) {
310             mSystemGestureExclusionPath.reset();
311             mSystemGestureExclusionRejected.getBoundaryPath(mSystemGestureExclusionPath);
312             canvas.drawPath(mSystemGestureExclusionPath, mSystemGestureExclusionRejectedPaint);
313         }
314 
315         // Labels
316         drawLabels(canvas);
317 
318         // Current pointer states.
319         canvas.save();
320         rotateCanvasToUnrotatedDisplay(canvas);
321         for (int p = 0; p < NP; p++) {
322             final PointerState ps = mPointers.valueAt(p);
323             float lastX = ps.mCurrentX, lastY = ps.mCurrentY;
324 
325             if (!Float.isNaN(lastX) && !Float.isNaN(lastY)) {
326                 // Draw velocity vector.
327                 mPaint.setARGB(255, 255, 64, 128);
328                 float xVel = ps.mXVelocity * (1000 / 60);
329                 float yVel = ps.mYVelocity * (1000 / 60);
330                 canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
331 
332                 // Draw velocity vector using an alternate VelocityTracker strategy.
333                 if (mAltVelocity != null) {
334                     mPaint.setARGB(255, 64, 255, 128);
335                     xVel = ps.mAltXVelocity * (1000 / 60);
336                     yVel = ps.mAltYVelocity * (1000 / 60);
337                     canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
338                 }
339             }
340 
341             if (mCurDown && ps.mCurDown) {
342                 // Draw crosshairs.
343                 canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);
344                 // Extend crosshairs to cover screen regardless of rotation (ie. since the rotated
345                 // canvas can "expose" content past 0 and up-to the largest screen dimension).
346                 canvas.drawLine(ps.mCoords.x, -getHeight(), ps.mCoords.x,
347                         Math.max(getHeight(), getWidth()), 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, and ensure it has a minimum size of 24dp.
365                 final float arrowSize = Math.max(ps.mCoords.toolMajor * 0.7f, 24 * mDensity);
366                 mPaint.setARGB(255, pressureLevel, 255, 0);
367                 float orientationVectorX = (float) (Math.sin(ps.mCoords.orientation)
368                         * arrowSize);
369                 float orientationVectorY = (float) (-Math.cos(ps.mCoords.orientation)
370                         * arrowSize);
371                 if (ps.mToolType == MotionEvent.TOOL_TYPE_STYLUS
372                         || ps.mToolType == MotionEvent.TOOL_TYPE_ERASER) {
373                     // Show full circle orientation.
374                     canvas.drawLine(ps.mCoords.x, ps.mCoords.y,
375                             ps.mCoords.x + orientationVectorX,
376                             ps.mCoords.y + orientationVectorY,
377                             mPaint);
378                 } else {
379                     // Show half circle orientation.
380                     canvas.drawLine(
381                             ps.mCoords.x - orientationVectorX,
382                             ps.mCoords.y - orientationVectorY,
383                             ps.mCoords.x + orientationVectorX,
384                             ps.mCoords.y + orientationVectorY,
385                             mPaint);
386                 }
387 
388                 // Draw the tilt point along the orientation arrow.
389                 float tiltScale = (float) Math.sin(
390                         ps.mCoords.getAxisValue(MotionEvent.AXIS_TILT));
391                 canvas.drawCircle(
392                         ps.mCoords.x + orientationVectorX * tiltScale,
393                         ps.mCoords.y + orientationVectorY * tiltScale,
394                         3.0f * mDensity, mPaint);
395 
396                 // Draw the current bounding box
397                 if (ps.mHasBoundingBox) {
398                     canvas.drawRect(ps.mBoundingLeft, ps.mBoundingTop,
399                             ps.mBoundingRight, ps.mBoundingBottom, mPaint);
400                 }
401             }
402         }
403         canvas.restore();
404     }
405 
drawLabels(Canvas canvas)406     private void drawLabels(Canvas canvas) {
407         final int w = getWidth() - mWaterfallInsets.left - mWaterfallInsets.right;
408         final int itemW = w / 7;
409         final int base = mHeaderPaddingTop - mTextMetrics.ascent + 1;
410         final int bottom = mHeaderBottom;
411 
412         canvas.save();
413         canvas.translate(mWaterfallInsets.left, 0);
414         final PointerState ps = mPointers.get(mActivePointerId, EMPTY_POINTER_STATE);
415 
416         canvas.drawRect(0, mHeaderPaddingTop, itemW - 1, bottom, mTextBackgroundPaint);
417         canvas.drawText(mText.clear()
418                 .append("P: ").append(mCurNumPointers)
419                 .append(" / ").append(mMaxNumPointers)
420                 .toString(), 1, base, mTextPaint);
421 
422         if ((mCurDown && ps.mCurDown) || Float.isNaN(ps.mCurrentX)) {
423             canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
424                     mTextBackgroundPaint);
425             canvas.drawText(mText.clear()
426                     .append("X: ").append(ps.mCoords.x, 1)
427                     .toString(), 1 + itemW, base, mTextPaint);
428             canvas.drawRect(itemW * 2, mHeaderPaddingTop, (itemW * 3) - 1, bottom,
429                     mTextBackgroundPaint);
430             canvas.drawText(mText.clear()
431                     .append("Y: ").append(ps.mCoords.y, 1)
432                     .toString(), 1 + itemW * 2, base, mTextPaint);
433         } else {
434             float dx = ps.mCurrentX - ps.mFirstX;
435             float dy = ps.mCurrentY - ps.mFirstY;
436             canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
437                     Math.abs(dx) < mVC.getScaledTouchSlop()
438                             ? mTextBackgroundPaint : mTextLevelPaint);
439             canvas.drawText(mText.clear()
440                     .append("dX: ").append(dx, 1)
441                     .toString(), 1 + itemW, base, mTextPaint);
442             canvas.drawRect(itemW * 2, mHeaderPaddingTop, (itemW * 3) - 1, bottom,
443                     Math.abs(dy) < mVC.getScaledTouchSlop()
444                             ? mTextBackgroundPaint : mTextLevelPaint);
445             canvas.drawText(mText.clear()
446                     .append("dY: ").append(dy, 1)
447                     .toString(), 1 + itemW * 2, base, mTextPaint);
448         }
449 
450         canvas.drawRect(itemW * 3, mHeaderPaddingTop, (itemW * 4) - 1, bottom,
451                 mTextBackgroundPaint);
452         canvas.drawText(mText.clear()
453                 .append("Xv: ").append(ps.mXVelocity, 3)
454                 .toString(), 1 + itemW * 3, base, mTextPaint);
455 
456         canvas.drawRect(itemW * 4, mHeaderPaddingTop, (itemW * 5) - 1, bottom,
457                 mTextBackgroundPaint);
458         canvas.drawText(mText.clear()
459                 .append("Yv: ").append(ps.mYVelocity, 3)
460                 .toString(), 1 + itemW * 4, base, mTextPaint);
461 
462         canvas.drawRect(itemW * 5, mHeaderPaddingTop, (itemW * 6) - 1, bottom,
463                 mTextBackgroundPaint);
464         canvas.drawRect(itemW * 5, mHeaderPaddingTop,
465                 (itemW * 5) + (ps.mCoords.pressure * itemW) - 1, bottom, mTextLevelPaint);
466         canvas.drawText(mText.clear()
467                 .append("Prs: ").append(ps.mCoords.pressure, 2)
468                 .toString(), 1 + itemW * 5, base, mTextPaint);
469 
470         canvas.drawRect(itemW * 6, mHeaderPaddingTop, w, bottom, mTextBackgroundPaint);
471         canvas.drawRect(itemW * 6, mHeaderPaddingTop,
472                 (itemW * 6) + (ps.mCoords.size * itemW) - 1, bottom, mTextLevelPaint);
473         canvas.drawText(mText.clear()
474                 .append("Size: ").append(ps.mCoords.size, 2)
475                 .toString(), 1 + itemW * 6, base, mTextPaint);
476         canvas.restore();
477     }
478 
logMotionEvent(String type, MotionEvent event)479     private void logMotionEvent(String type, MotionEvent event) {
480         final int action = event.getAction();
481         final int N = event.getHistorySize();
482         final int NI = event.getPointerCount();
483         for (int historyPos = 0; historyPos < N; historyPos++) {
484             for (int i = 0; i < NI; i++) {
485                 final int id = event.getPointerId(i);
486                 event.getHistoricalPointerCoords(i, historyPos, mTempCoords);
487                 logCoords(type, action, i, mTempCoords, id, event);
488             }
489         }
490         for (int i = 0; i < NI; i++) {
491             final int id = event.getPointerId(i);
492             event.getPointerCoords(i, mTempCoords);
493             logCoords(type, action, i, mTempCoords, id, event);
494         }
495     }
496 
logCoords(String type, int action, int index, MotionEvent.PointerCoords coords, int id, MotionEvent event)497     private void logCoords(String type, int action, int index,
498             MotionEvent.PointerCoords coords, int id, MotionEvent event) {
499         final int toolType = event.getToolType(index);
500         final int buttonState = event.getButtonState();
501         final String prefix;
502         switch (action & MotionEvent.ACTION_MASK) {
503             case MotionEvent.ACTION_DOWN:
504                 prefix = "DOWN";
505                 break;
506             case MotionEvent.ACTION_UP:
507                 prefix = "UP";
508                 break;
509             case MotionEvent.ACTION_MOVE:
510                 prefix = "MOVE";
511                 break;
512             case MotionEvent.ACTION_CANCEL:
513                 prefix = "CANCEL";
514                 break;
515             case MotionEvent.ACTION_OUTSIDE:
516                 prefix = "OUTSIDE";
517                 break;
518             case MotionEvent.ACTION_POINTER_DOWN:
519                 if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
520                         >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
521                     prefix = "DOWN";
522                 } else {
523                     prefix = "MOVE";
524                 }
525                 break;
526             case MotionEvent.ACTION_POINTER_UP:
527                 if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
528                         >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
529                     prefix = "UP";
530                 } else {
531                     prefix = "MOVE";
532                 }
533                 break;
534             case MotionEvent.ACTION_HOVER_MOVE:
535                 prefix = "HOVER MOVE";
536                 break;
537             case MotionEvent.ACTION_HOVER_ENTER:
538                 prefix = "HOVER ENTER";
539                 break;
540             case MotionEvent.ACTION_HOVER_EXIT:
541                 prefix = "HOVER EXIT";
542                 break;
543             case MotionEvent.ACTION_SCROLL:
544                 prefix = "SCROLL";
545                 break;
546             default:
547                 prefix = Integer.toString(action);
548                 break;
549         }
550 
551         Log.i(TAG, mText.clear()
552                 .append(type).append(" id ").append(id + 1)
553                 .append(": ")
554                 .append(prefix)
555                 .append(" (").append(coords.x, 3).append(", ").append(coords.y, 3)
556                 .append(") Pressure=").append(coords.pressure, 3)
557                 .append(" Size=").append(coords.size, 3)
558                 .append(" TouchMajor=").append(coords.touchMajor, 3)
559                 .append(" TouchMinor=").append(coords.touchMinor, 3)
560                 .append(" ToolMajor=").append(coords.toolMajor, 3)
561                 .append(" ToolMinor=").append(coords.toolMinor, 3)
562                 .append(" Orientation=").append((float) (coords.orientation * 180 / Math.PI), 1)
563                 .append("deg")
564                 .append(" Tilt=").append((float) (
565                         coords.getAxisValue(MotionEvent.AXIS_TILT) * 180 / Math.PI), 1)
566                 .append("deg")
567                 .append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1)
568                 .append(" VScroll=").append(coords.getAxisValue(MotionEvent.AXIS_VSCROLL), 1)
569                 .append(" HScroll=").append(coords.getAxisValue(MotionEvent.AXIS_HSCROLL), 1)
570                 .append(" BoundingBox=[(")
571                 .append(event.getAxisValue(MotionEvent.AXIS_GENERIC_1), 3)
572                 .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_2), 3).append(")")
573                 .append(", (").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_3), 3)
574                 .append(", ").append(event.getAxisValue(MotionEvent.AXIS_GENERIC_4), 3)
575                 .append(")]")
576                 .append(" ToolType=").append(MotionEvent.toolTypeToString(toolType))
577                 .append(" ButtonState=").append(MotionEvent.buttonStateToString(buttonState))
578                 .toString());
579     }
580 
581     @Override
onPointerEvent(MotionEvent event)582     public void onPointerEvent(MotionEvent event) {
583         // PointerLocationView stores and draws events in the unrotated display space, so undo the
584         // event's rotation to bring it back to the unrotated display space.
585         event.transform(MotionEvent.createRotateMatrix(inverseRotation(event.getSurfaceRotation()),
586                 mTraceBitmap.getWidth(), mTraceBitmap.getHeight()));
587 
588         final int action = event.getAction();
589 
590         if (action == MotionEvent.ACTION_DOWN
591                 || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
592             final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
593                     >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down
594             if (action == MotionEvent.ACTION_DOWN) {
595                 mPointers.clear();
596                 mCurDown = true;
597                 mCurNumPointers = 0;
598                 mMaxNumPointers = 0;
599                 mVelocity.clear();
600                 mTraceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
601                 if (mAltVelocity != null) {
602                     mAltVelocity.clear();
603                 }
604             }
605 
606             mCurNumPointers += 1;
607             if (mMaxNumPointers < mCurNumPointers) {
608                 mMaxNumPointers = mCurNumPointers;
609             }
610 
611             final int id = event.getPointerId(index);
612             PointerState ps = mPointers.get(id);
613             if (ps == null) {
614                 ps = new PointerState();
615                 mPointers.put(id, ps);
616             }
617 
618             if (!mPointers.contains(mActivePointerId)
619                     || !mPointers.get(mActivePointerId).mCurDown) {
620                 mActivePointerId = id;
621             }
622 
623             ps.mCurDown = true;
624             InputDevice device = InputDevice.getDevice(event.getDeviceId());
625             ps.mHasBoundingBox = device != null &&
626                     device.getMotionRange(MotionEvent.AXIS_GENERIC_1) != null;
627         }
628 
629         final int NI = event.getPointerCount();
630 
631         mVelocity.addMovement(event);
632         mVelocity.computeCurrentVelocity(1);
633         if (mAltVelocity != null) {
634             mAltVelocity.addMovement(event);
635             mAltVelocity.computeCurrentVelocity(1);
636         }
637 
638         final int N = event.getHistorySize();
639         for (int historyPos = 0; historyPos < N; historyPos++) {
640             for (int i = 0; i < NI; i++) {
641                 final int id = event.getPointerId(i);
642                 final PointerState ps = mCurDown ? mPointers.get(id) : null;
643                 final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
644                 event.getHistoricalPointerCoords(i, historyPos, coords);
645                 if (mPrintCoords) {
646                     logCoords("Pointer", action, i, coords, id, event);
647                 }
648                 if (ps != null) {
649                     ps.addTrace(coords.x, coords.y, /*isHistorical*/ true);
650                     updateDrawTrace(ps);
651                 }
652             }
653         }
654         for (int i = 0; i < NI; i++) {
655             final int id = event.getPointerId(i);
656             final PointerState ps = mCurDown ? mPointers.get(id) : null;
657             final PointerCoords coords = ps != null ? ps.mCoords : mTempCoords;
658             event.getPointerCoords(i, coords);
659             if (mPrintCoords) {
660                 logCoords("Pointer", action, i, coords, id, event);
661             }
662             if (ps != null) {
663                 ps.addTrace(coords.x, coords.y, /*isHistorical*/ false);
664                 updateDrawTrace(ps);
665                 ps.mXVelocity = mVelocity.getXVelocity(id);
666                 ps.mYVelocity = mVelocity.getYVelocity(id);
667                 if (mAltVelocity != null) {
668                     ps.mAltXVelocity = mAltVelocity.getXVelocity(id);
669                     ps.mAltYVelocity = mAltVelocity.getYVelocity(id);
670                 }
671                 ps.mToolType = event.getToolType(i);
672 
673                 if (ps.mHasBoundingBox) {
674                     ps.mBoundingLeft = event.getAxisValue(MotionEvent.AXIS_GENERIC_1, i);
675                     ps.mBoundingTop = event.getAxisValue(MotionEvent.AXIS_GENERIC_2, i);
676                     ps.mBoundingRight = event.getAxisValue(MotionEvent.AXIS_GENERIC_3, i);
677                     ps.mBoundingBottom = event.getAxisValue(MotionEvent.AXIS_GENERIC_4, i);
678                 }
679             }
680         }
681 
682         if (action == MotionEvent.ACTION_UP
683                 || action == MotionEvent.ACTION_CANCEL
684                 || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
685             final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
686                     >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP
687 
688             final int id = event.getPointerId(index);
689             final PointerState ps = mPointers.get(id);
690             if (ps == null) {
691                 Slog.wtf(TAG, "Could not find pointer id=" + id + " in mPointers map,"
692                         + " size=" + mPointers.size() + " pointerindex=" + index
693                         + " action=0x" + Integer.toHexString(action));
694                 return;
695             }
696             ps.mCurDown = false;
697 
698             if (action == MotionEvent.ACTION_UP
699                     || action == MotionEvent.ACTION_CANCEL) {
700                 mCurDown = false;
701                 mCurNumPointers = 0;
702             } else {
703                 mCurNumPointers -= 1;
704                 if (mActivePointerId == id) {
705                     mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
706                 }
707                 ps.addTrace(Float.NaN, Float.NaN, true);
708             }
709         }
710 
711         invalidate();
712     }
713 
updateDrawTrace(PointerState ps)714     private void updateDrawTrace(PointerState ps) {
715         mPaint.setARGB(255, 128, 255, 255);
716         float x = ps.mCurrentX;
717         float y = ps.mCurrentY;
718         float lastX = ps.mPreviousX;
719         float lastY = ps.mPreviousY;
720         if (Float.isNaN(x) || Float.isNaN(y) || Float.isNaN(lastX) || Float.isNaN(lastY)) {
721             return;
722         }
723         mTraceCanvas.drawLine(lastX, lastY, x, y, mPathPaint);
724         Paint paint = ps.mPreviousPointIsHistorical ? mPaint : mCurrentPointPaint;
725         mTraceCanvas.drawPoint(lastX, lastY, paint);
726     }
727 
728     @Override
onTouchEvent(MotionEvent event)729     public boolean onTouchEvent(MotionEvent event) {
730         onPointerEvent(event);
731 
732         if (event.getAction() == MotionEvent.ACTION_DOWN && !isFocused()) {
733             requestFocus();
734         }
735         return true;
736     }
737 
738     @Override
onGenericMotionEvent(MotionEvent event)739     public boolean onGenericMotionEvent(MotionEvent event) {
740         final int source = event.getSource();
741         if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
742             onPointerEvent(event);
743         } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
744             logMotionEvent("Joystick", event);
745         } else if ((source & InputDevice.SOURCE_CLASS_POSITION) != 0) {
746             logMotionEvent("Position", event);
747         } else {
748             logMotionEvent("Generic", event);
749         }
750         return true;
751     }
752 
753     @Override
onKeyDown(int keyCode, KeyEvent event)754     public boolean onKeyDown(int keyCode, KeyEvent event) {
755         if (shouldLogKey(keyCode)) {
756             final int repeatCount = event.getRepeatCount();
757             if (repeatCount == 0) {
758                 Log.i(TAG, "Key Down: " + event);
759             } else {
760                 Log.i(TAG, "Key Repeat #" + repeatCount + ": " + event);
761             }
762             return true;
763         }
764         return super.onKeyDown(keyCode, event);
765     }
766 
767     @Override
onKeyUp(int keyCode, KeyEvent event)768     public boolean onKeyUp(int keyCode, KeyEvent event) {
769         if (shouldLogKey(keyCode)) {
770             Log.i(TAG, "Key Up: " + event);
771             return true;
772         }
773         return super.onKeyUp(keyCode, event);
774     }
775 
shouldLogKey(int keyCode)776     private static boolean shouldLogKey(int keyCode) {
777         switch (keyCode) {
778             case KeyEvent.KEYCODE_DPAD_UP:
779             case KeyEvent.KEYCODE_DPAD_DOWN:
780             case KeyEvent.KEYCODE_DPAD_LEFT:
781             case KeyEvent.KEYCODE_DPAD_RIGHT:
782             case KeyEvent.KEYCODE_DPAD_CENTER:
783                 return true;
784             default:
785                 return KeyEvent.isGamepadButton(keyCode)
786                         || KeyEvent.isModifierKey(keyCode);
787         }
788     }
789 
790     @Override
onTrackballEvent(MotionEvent event)791     public boolean onTrackballEvent(MotionEvent event) {
792         logMotionEvent("Trackball", event);
793         return true;
794     }
795 
796     @Override
onAttachedToWindow()797     protected void onAttachedToWindow() {
798         super.onAttachedToWindow();
799 
800         mIm.registerInputDeviceListener(this, getHandler());
801         if (shouldShowSystemGestureExclusion()) {
802             try {
803                 WindowManagerGlobal.getWindowManagerService()
804                         .registerSystemGestureExclusionListener(mSystemGestureExclusionListener,
805                                 mContext.getDisplayId());
806             } catch (RemoteException e) {
807                 throw e.rethrowFromSystemServer();
808             }
809             final int alpha = systemGestureExclusionOpacity();
810             mSystemGestureExclusionPaint.setAlpha(alpha);
811             mSystemGestureExclusionRejectedPaint.setAlpha(alpha);
812         } else {
813             mSystemGestureExclusion.setEmpty();
814         }
815         logInputDevices();
816     }
817 
818     @Override
onDetachedFromWindow()819     protected void onDetachedFromWindow() {
820         super.onDetachedFromWindow();
821 
822         mIm.unregisterInputDeviceListener(this);
823         try {
824             WindowManagerGlobal.getWindowManagerService().unregisterSystemGestureExclusionListener(
825                     mSystemGestureExclusionListener, mContext.getDisplayId());
826         } catch (RemoteException e) {
827             throw e.rethrowFromSystemServer();
828         } catch (IllegalArgumentException e) {
829             Log.e(TAG, "Failed to unregister window manager callbacks", e);
830         }
831     }
832 
833     @Override
onInputDeviceAdded(int deviceId)834     public void onInputDeviceAdded(int deviceId) {
835         logInputDeviceState(deviceId, "Device Added");
836     }
837 
838     @Override
onInputDeviceChanged(int deviceId)839     public void onInputDeviceChanged(int deviceId) {
840         logInputDeviceState(deviceId, "Device Changed");
841     }
842 
843     @Override
onInputDeviceRemoved(int deviceId)844     public void onInputDeviceRemoved(int deviceId) {
845         logInputDeviceState(deviceId, "Device Removed");
846     }
847 
logInputDevices()848     private void logInputDevices() {
849         int[] deviceIds = InputDevice.getDeviceIds();
850         for (int i = 0; i < deviceIds.length; i++) {
851             logInputDeviceState(deviceIds[i], "Device Enumerated");
852         }
853     }
854 
logInputDeviceState(int deviceId, String state)855     private void logInputDeviceState(int deviceId, String state) {
856         InputDevice device = mIm.getInputDevice(deviceId);
857         if (device != null) {
858             Log.i(TAG, state + ": " + device);
859         } else {
860             Log.i(TAG, state + ": " + deviceId);
861         }
862     }
863 
shouldShowSystemGestureExclusion()864     private static boolean shouldShowSystemGestureExclusion() {
865         return systemGestureExclusionOpacity() > 0;
866     }
867 
systemGestureExclusionOpacity()868     private static int systemGestureExclusionOpacity() {
869         int x = SystemProperties.getInt(GESTURE_EXCLUSION_PROP, 0);
870         return x >= 0 && x <= 255 ? x : 0;
871     }
872 
873     // HACK
874     // A quick and dirty string builder implementation optimized for GC.
875     // Using String.format causes the application grind to a halt when
876     // more than a couple of pointers are down due to the number of
877     // temporary objects allocated while formatting strings for drawing or logging.
878     private static final class FasterStringBuilder {
879         private char[] mChars;
880         private int mLength;
881 
FasterStringBuilder()882         public FasterStringBuilder() {
883             mChars = new char[64];
884         }
885 
clear()886         public FasterStringBuilder clear() {
887             mLength = 0;
888             return this;
889         }
890 
append(String value)891         public FasterStringBuilder append(String value) {
892             final int valueLength = value.length();
893             final int index = reserve(valueLength);
894             value.getChars(0, valueLength, mChars, index);
895             mLength += valueLength;
896             return this;
897         }
898 
append(int value)899         public FasterStringBuilder append(int value) {
900             return append(value, 0);
901         }
902 
append(int value, int zeroPadWidth)903         public FasterStringBuilder append(int value, int zeroPadWidth) {
904             final boolean negative = value < 0;
905             if (negative) {
906                 value = -value;
907                 if (value < 0) {
908                     append("-2147483648");
909                     return this;
910                 }
911             }
912 
913             int index = reserve(11);
914             final char[] chars = mChars;
915 
916             if (value == 0) {
917                 chars[index++] = '0';
918                 mLength += 1;
919                 return this;
920             }
921 
922             if (negative) {
923                 chars[index++] = '-';
924             }
925 
926             int divisor = 1000000000;
927             int numberWidth = 10;
928             while (value < divisor) {
929                 divisor /= 10;
930                 numberWidth -= 1;
931                 if (numberWidth < zeroPadWidth) {
932                     chars[index++] = '0';
933                 }
934             }
935 
936             do {
937                 int digit = value / divisor;
938                 value -= digit * divisor;
939                 divisor /= 10;
940                 chars[index++] = (char) (digit + '0');
941             } while (divisor != 0);
942 
943             mLength = index;
944             return this;
945         }
946 
947         public FasterStringBuilder append(float value, int precision) {
948             int scale = 1;
949             for (int i = 0; i < precision; i++) {
950                 scale *= 10;
951             }
952             value = (float) (Math.rint(value * scale) / scale);
953 
954             // Corner case: (int)-0.1 will become zero, so the negative sign gets lost
955             if ((int) value == 0 && value < 0) {
956                 append("-");
957             }
958             append((int) value);
959 
960             if (precision != 0) {
961                 append(".");
962                 value = Math.abs(value);
963                 value -= Math.floor(value);
964                 append((int) (value * scale), precision);
965             }
966 
967             return this;
968         }
969 
970         @Override
971         public String toString() {
972             return new String(mChars, 0, mLength);
973         }
974 
975         private int reserve(int length) {
976             final int oldLength = mLength;
977             final int newLength = mLength + length;
978             final char[] oldChars = mChars;
979             final int oldCapacity = oldChars.length;
980             if (newLength > oldCapacity) {
981                 final int newCapacity = oldCapacity * 2;
982                 final char[] newChars = new char[newCapacity];
983                 System.arraycopy(oldChars, 0, newChars, 0, oldLength);
984                 mChars = newChars;
985             }
986             return oldLength;
987         }
988     }
989 
990     private final ISystemGestureExclusionListener mSystemGestureExclusionListener =
991             new ISystemGestureExclusionListener.Stub() {
992                 @Override
993                 public void onSystemGestureExclusionChanged(int displayId,
994                         Region systemGestureExclusion,
995                         Region systemGestureExclusionUnrestricted) {
996                     Region exclusion = Region.obtain(systemGestureExclusion);
997                     Region rejected = Region.obtain();
998                     if (systemGestureExclusionUnrestricted != null) {
999                         rejected.set(systemGestureExclusionUnrestricted);
1000                         rejected.op(exclusion, Region.Op.DIFFERENCE);
1001                     }
1002                     Handler handler = getHandler();
1003                     if (handler != null) {
1004                         handler.post(() -> {
1005                             mSystemGestureExclusion.set(exclusion);
1006                             mSystemGestureExclusionRejected.set(rejected);
1007                             exclusion.recycle();
1008                             invalidate();
1009                         });
1010                     }
1011                 }
1012             };
1013 
1014     @Override
1015     protected void onConfigurationChanged(Configuration newConfig) {
1016         super.onConfigurationChanged(newConfig);
1017         configureTraceBitmap();
1018         configureDensityDependentFactors();
1019     }
1020 
1021     // Compute size by display density.
1022     private void configureDensityDependentFactors() {
1023         mDensity = getResources().getDisplayMetrics().density;
1024         mTextPaint.setTextSize(10 * mDensity);
1025         mPaint.setStrokeWidth(1 * mDensity);
1026         mCurrentPointPaint.setStrokeWidth(1 * mDensity);
1027         mPathPaint.setStrokeWidth(1 * mDensity);
1028     }
1029 
1030     private void configureTraceBitmap() {
1031         final var display = mContext.getDisplay();
1032         final boolean rotated = display.getRotation() == Surface.ROTATION_90
1033                 || display.getRotation() == Surface.ROTATION_270;
1034         int unrotatedWidth = rotated ? display.getHeight() : display.getWidth();
1035         int unrotatedHeight = rotated ? display.getWidth() : display.getHeight();
1036 
1037         if (mTraceBitmap != null && mTraceBitmap.getWidth() == unrotatedWidth
1038                 && mTraceBitmap.getHeight() == unrotatedHeight) {
1039             return;
1040         }
1041         if (unrotatedWidth <= 0 || unrotatedHeight <= 0) {
1042             Slog.w(TAG, "Ignoring configuration: invalid display size: " + unrotatedWidth + "x"
1043                     + unrotatedHeight);
1044             // Initialize the bitmap to an arbitrary size. It should be reconfigured with a valid
1045             // size in the future.
1046             unrotatedWidth = 100;
1047             unrotatedHeight = 100;
1048         }
1049         mTraceBitmap = Bitmap.createBitmap(unrotatedWidth, unrotatedHeight,
1050                 Bitmap.Config.ARGB_8888);
1051         mTraceCanvas.setBitmap(mTraceBitmap);
1052     }
1053 
1054     private static int inverseRotation(@Surface.Rotation int rotation) {
1055         return switch(rotation) {
1056             case Surface.ROTATION_0 -> Surface.ROTATION_0;
1057             case Surface.ROTATION_90 -> Surface.ROTATION_270;
1058             case Surface.ROTATION_180 -> Surface.ROTATION_180;
1059             case Surface.ROTATION_270 -> Surface.ROTATION_90;
1060             default -> {
1061                 Slog.e(TAG, "Received unexpected surface rotation: " + rotation);
1062                 yield Surface.ROTATION_0;
1063             }
1064         };
1065     }
1066 
1067     private void rotateCanvasToUnrotatedDisplay(Canvas c) {
1068         switch (inverseRotation(mContext.getDisplay().getRotation())) {
1069             case Surface.ROTATION_90 -> {
1070                 c.rotate(90);
1071                 c.translate(0, -mTraceBitmap.getHeight());
1072             }
1073             case Surface.ROTATION_180 -> {
1074                 c.rotate(180);
1075                 c.translate(-mTraceBitmap.getWidth(), -mTraceBitmap.getHeight());
1076             }
1077             case Surface.ROTATION_270 -> {
1078                 c.rotate(270);
1079                 c.translate(-mTraceBitmap.getWidth(), 0);
1080             }
1081         }
1082     }
1083 }
1084