1 /*
2  * Copyright (C) 2020 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.systemui.classifier;
18 
19 import static com.android.systemui.classifier.FalsingModule.IS_FOLDABLE_DEVICE;
20 
21 import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
22 import android.util.DisplayMetrics;
23 import android.view.MotionEvent;
24 import android.view.MotionEvent.PointerCoords;
25 import android.view.MotionEvent.PointerProperties;
26 
27 import com.android.systemui.dagger.SysUISingleton;
28 import com.android.systemui.dock.DockManager;
29 import com.android.systemui.statusbar.policy.BatteryController;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 
34 import javax.inject.Inject;
35 import javax.inject.Named;
36 
37 /**
38  * Acts as a cache and utility class for FalsingClassifiers.
39  */
40 @SysUISingleton
41 public class FalsingDataProvider {
42 
43     private static final long MOTION_EVENT_AGE_MS = 1000;
44     private static final long DROP_EVENT_THRESHOLD_MS = 50;
45     private static final float THREE_HUNDRED_SIXTY_DEG = (float) (2 * Math.PI);
46 
47     private final int mWidthPixels;
48     private final int mHeightPixels;
49     private BatteryController mBatteryController;
50     private final FoldStateListener mFoldStateListener;
51     private final DockManager mDockManager;
52     private boolean mIsFoldableDevice;
53     private final float mXdpi;
54     private final float mYdpi;
55     private final List<SessionListener> mSessionListeners = new ArrayList<>();
56     private final List<MotionEventListener> mMotionEventListeners = new ArrayList<>();
57     private final List<GestureFinalizedListener> mGestureFinalizedListeners = new ArrayList<>();
58 
59     private TimeLimitedMotionEventBuffer mRecentMotionEvents =
60             new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS);
61     private List<MotionEvent> mPriorMotionEvents = new ArrayList<>();
62 
63     private boolean mDirty = true;
64 
65     private float mAngle = 0;
66     private MotionEvent mFirstRecentMotionEvent;
67     private MotionEvent mLastMotionEvent;
68     private boolean mDropLastEvent;
69     private boolean mJustUnlockedWithFace;
70     private boolean mA11YAction;
71 
72     @Inject
FalsingDataProvider( DisplayMetrics displayMetrics, BatteryController batteryController, FoldStateListener foldStateListener, DockManager dockManager, @Named(IS_FOLDABLE_DEVICE) boolean isFoldableDevice)73     public FalsingDataProvider(
74             DisplayMetrics displayMetrics,
75             BatteryController batteryController,
76             FoldStateListener foldStateListener,
77             DockManager dockManager,
78             @Named(IS_FOLDABLE_DEVICE) boolean isFoldableDevice) {
79         mXdpi = displayMetrics.xdpi;
80         mYdpi = displayMetrics.ydpi;
81         mWidthPixels = displayMetrics.widthPixels;
82         mHeightPixels = displayMetrics.heightPixels;
83         mBatteryController = batteryController;
84         mFoldStateListener = foldStateListener;
85         mDockManager = dockManager;
86         mIsFoldableDevice = isFoldableDevice;
87 
88         FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi());
89         FalsingClassifier.logInfo("width, height: " + getWidthPixels() + ", " + getHeightPixels());
90     }
91 
onMotionEvent(MotionEvent motionEvent)92     void onMotionEvent(MotionEvent motionEvent) {
93         List<MotionEvent> motionEvents = unpackMotionEvent(motionEvent);
94         FalsingClassifier.logVerbose("Unpacked into: " + motionEvents.size());
95         if (BrightLineFalsingManager.DEBUG) {
96             for (MotionEvent m : motionEvents) {
97                 FalsingClassifier.logVerbose(
98                         "x,y,t: " + m.getX() + "," + m.getY() + "," + m.getEventTime());
99             }
100         }
101 
102         if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
103             // Ensure prior gesture was completed. May be a no-op.
104             completePriorGesture();
105         }
106 
107         // Drop the gesture closing event if it is close in time to a previous ACTION_MOVE event.
108         // The reason is that the closing ACTION_UP event of  a swipe can be a bit offseted from the
109         // previous ACTION_MOVE event and when it happens, it makes some classifiers fail.
110         mDropLastEvent = shouldDropEvent(motionEvent);
111 
112         mRecentMotionEvents.addAll(motionEvents);
113 
114         FalsingClassifier.logVerbose("Size: " + mRecentMotionEvents.size());
115 
116         mMotionEventListeners.forEach(listener -> listener.onMotionEvent(motionEvent));
117 
118         // We explicitly do not "finalize" a gesture on UP or CANCEL events.
119         // We wait for the next gesture to start before marking the prior gesture as complete.  This
120         // has multiple benefits. First, it makes it trivial to track the "current" or "recent"
121         // gesture, as it will always be found in mRecentMotionEvents. Second, and most importantly,
122         // it ensures that the current gesture doesn't get added to this HistoryTracker before it
123         // is analyzed.
124 
125         mDirty = true;
126     }
127 
onMotionEventComplete()128     void onMotionEventComplete() {
129         if (mRecentMotionEvents.isEmpty()) {
130             return;
131         }
132         int action = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1).getActionMasked();
133         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
134             completePriorGesture();
135         }
136     }
137 
completePriorGesture()138     private void completePriorGesture() {
139         if (!mRecentMotionEvents.isEmpty()) {
140             mGestureFinalizedListeners.forEach(listener -> listener.onGestureFinalized(
141                     mRecentMotionEvents.get(mRecentMotionEvents.size() - 1).getEventTime()));
142 
143             mPriorMotionEvents = mRecentMotionEvents;
144             mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS);
145         }
146         mDropLastEvent = false;
147         mA11YAction = false;
148     }
149 
150     /** Returns screen width in pixels. */
getWidthPixels()151     public int getWidthPixels() {
152         return mWidthPixels;
153     }
154 
155     /** Returns screen height in pixels. */
getHeightPixels()156     public int getHeightPixels() {
157         return mHeightPixels;
158     }
159 
getXdpi()160     public float getXdpi() {
161         return mXdpi;
162     }
163 
getYdpi()164     public float getYdpi() {
165         return mYdpi;
166     }
167 
168     /**
169      * Get the {@link MotionEvent}s of the most recent gesture.
170      *
171      * Note that this list may not include the last recorded event.
172      * @see #mDropLastEvent
173      */
getRecentMotionEvents()174     public List<MotionEvent> getRecentMotionEvents() {
175         if (!mDropLastEvent || mRecentMotionEvents.isEmpty()) {
176             return mRecentMotionEvents;
177         } else {
178             return mRecentMotionEvents.subList(0, mRecentMotionEvents.size() - 1);
179         }
180     }
181 
getPriorMotionEvents()182     public List<MotionEvent> getPriorMotionEvents() {
183         return mPriorMotionEvents;
184     }
185 
186     /**
187      * Get the first recorded {@link MotionEvent} of the most recent gesture.
188      *
189      * Note that MotionEvents are not kept forever. As a gesture gets longer in duration, older
190      * MotionEvents may expire and be ejected.
191      */
getFirstRecentMotionEvent()192     public MotionEvent getFirstRecentMotionEvent() {
193         recalculateData();
194         return mFirstRecentMotionEvent;
195     }
196 
197     /**
198      * Get the last {@link MotionEvent} of the most recent gesture.
199      *
200      * Note that this may be the event prior to the last recorded event.
201      * @see #mDropLastEvent
202      */
getLastMotionEvent()203     public MotionEvent getLastMotionEvent() {
204         recalculateData();
205         return mLastMotionEvent;
206     }
207 
208     /**
209      * Returns the angle between the first and last point of the recent points.
210      *
211      * The angle will be in radians, always be between 0 and 2*PI, inclusive.
212      */
getAngle()213     public float getAngle() {
214         recalculateData();
215         return mAngle;
216     }
217 
218     /** Returns if the most recent gesture is more horizontal than vertical. */
isHorizontal()219     public boolean isHorizontal() {
220         recalculateData();
221         if (mRecentMotionEvents.isEmpty()) {
222             return false;
223         }
224 
225         return Math.abs(mFirstRecentMotionEvent.getX() - mLastMotionEvent.getX()) > Math
226                 .abs(mFirstRecentMotionEvent.getY() - mLastMotionEvent.getY());
227     }
228 
229     /**
230      * Is the most recent gesture more right than left.
231      *
232      * This does not mean the gesture is mostly horizontal. Simply that it ended at least one pixel
233      * to the right of where it started. See also {@link #isHorizontal()}.
234      */
isRight()235     public boolean isRight() {
236         recalculateData();
237         if (mRecentMotionEvents.isEmpty()) {
238             return false;
239         }
240 
241         return mLastMotionEvent.getX() > mFirstRecentMotionEvent.getX();
242     }
243 
244     /** Returns if the most recent gesture is more vertical than horizontal. */
isVertical()245     public boolean isVertical() {
246         return !isHorizontal();
247     }
248 
249     /**
250      * Is the most recent gesture more up than down.
251      *
252      * This does not mean the gesture is mostly vertical. Simply that it ended at least one pixel
253      * higher than it started. See also {@link #isVertical()}.
254      */
isUp()255     public boolean isUp() {
256         recalculateData();
257         if (mRecentMotionEvents.isEmpty()) {
258             return false;
259         }
260 
261         return mLastMotionEvent.getY() < mFirstRecentMotionEvent.getY();
262     }
263 
isFromTrackpad()264     public boolean isFromTrackpad() {
265         if (mRecentMotionEvents.isEmpty()) {
266             return false;
267         }
268 
269         int classification = mRecentMotionEvents.get(0).getClassification();
270         return classification == MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
271                 || classification == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE;
272     }
273 
recalculateData()274     private void recalculateData() {
275         if (!mDirty) {
276             return;
277         }
278 
279         List<MotionEvent> recentMotionEvents = getRecentMotionEvents();
280         if (recentMotionEvents.isEmpty()) {
281             mFirstRecentMotionEvent = null;
282             mLastMotionEvent = null;
283         } else {
284             mFirstRecentMotionEvent = recentMotionEvents.get(0);
285             mLastMotionEvent = recentMotionEvents.get(recentMotionEvents.size() - 1);
286         }
287 
288         calculateAngleInternal();
289 
290         mDirty = false;
291     }
292 
shouldDropEvent(MotionEvent event)293     private boolean shouldDropEvent(MotionEvent event) {
294         if (mRecentMotionEvents.size() < 3) return false;
295 
296         MotionEvent lastEvent = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1);
297         boolean isCompletingGesture = event.getActionMasked() == MotionEvent.ACTION_UP
298                 && lastEvent.getActionMasked() == MotionEvent.ACTION_MOVE;
299         boolean isRecentEvent =
300                 event.getEventTime() - lastEvent.getEventTime() < DROP_EVENT_THRESHOLD_MS;
301         return isCompletingGesture && isRecentEvent;
302     }
303 
304     private void calculateAngleInternal() {
305         if (mRecentMotionEvents.size() < 2) {
306             mAngle = Float.MAX_VALUE;
307         } else {
308             float lastX = mLastMotionEvent.getX() - mFirstRecentMotionEvent.getX();
309             float lastY = mLastMotionEvent.getY() - mFirstRecentMotionEvent.getY();
310 
311             mAngle = (float) Math.atan2(lastY, lastX);
312             while (mAngle < 0) {
313                 mAngle += THREE_HUNDRED_SIXTY_DEG;
314             }
315             while (mAngle > THREE_HUNDRED_SIXTY_DEG) {
316                 mAngle -= THREE_HUNDRED_SIXTY_DEG;
317             }
318         }
319     }
320 
321     private List<MotionEvent> unpackMotionEvent(MotionEvent motionEvent) {
322         List<MotionEvent> motionEvents = new ArrayList<>();
323         List<PointerProperties> pointerPropertiesList = new ArrayList<>();
324         int pointerCount = motionEvent.getPointerCount();
325         for (int i = 0; i < pointerCount; i++) {
326             PointerProperties pointerProperties = new PointerProperties();
327             motionEvent.getPointerProperties(i, pointerProperties);
328             pointerPropertiesList.add(pointerProperties);
329         }
330         PointerProperties[] pointerPropertiesArray = new PointerProperties[pointerPropertiesList
331                 .size()];
332         pointerPropertiesList.toArray(pointerPropertiesArray);
333 
334         int historySize = motionEvent.getHistorySize();
335         for (int i = 0; i < historySize; i++) {
336             List<PointerCoords> pointerCoordsList = new ArrayList<>();
337             for (int j = 0; j < pointerCount; j++) {
338                 PointerCoords pointerCoords = new PointerCoords();
339                 motionEvent.getHistoricalPointerCoords(j, i, pointerCoords);
340                 pointerCoordsList.add(pointerCoords);
341             }
342             motionEvents.add(MotionEvent.obtain(
343                     motionEvent.getDownTime(),
344                     motionEvent.getHistoricalEventTime(i),
345                     motionEvent.getAction(),
346                     pointerCount,
347                     pointerPropertiesArray,
348                     pointerCoordsList.toArray(new PointerCoords[0]),
349                     motionEvent.getMetaState(),
350                     motionEvent.getButtonState(),
351                     motionEvent.getXPrecision(),
352                     motionEvent.getYPrecision(),
353                     motionEvent.getDeviceId(),
354                     motionEvent.getEdgeFlags(),
355                     motionEvent.getSource(),
356                     motionEvent.getDisplayId(),
357                     motionEvent.getFlags(),
358                     motionEvent.getClassification()
359             ));
360         }
361 
362         motionEvents.add(MotionEvent.obtainNoHistory(motionEvent));
363 
364         return motionEvents;
365     }
366 
367     /** Register a {@link SessionListener}. */
368     public void addSessionListener(SessionListener listener) {
369         mSessionListeners.add(listener);
370     }
371 
372     /** Unregister a {@link SessionListener}. */
373     public void removeSessionListener(SessionListener listener) {
374         mSessionListeners.remove(listener);
375     }
376 
377     /** Register a {@link MotionEventListener}. */
378     public void addMotionEventListener(MotionEventListener listener) {
379         mMotionEventListeners.add(listener);
380     }
381 
382     /** Unegister a {@link MotionEventListener}. */
383     public void removeMotionEventListener(MotionEventListener listener) {
384         mMotionEventListeners.remove(listener);
385     }
386 
387     /** Register a {@link GestureFinalizedListener}. */
388     public void addGestureCompleteListener(GestureFinalizedListener listener) {
389         mGestureFinalizedListeners.add(listener);
390     }
391 
392     /** Unregister a {@link GestureFinalizedListener}. */
393     public void removeGestureCompleteListener(GestureFinalizedListener listener) {
394         mGestureFinalizedListeners.remove(listener);
395     }
396 
397     /** Return whether last gesture was an A11y action. */
398     public boolean isA11yAction() {
399         return mA11YAction;
400     }
401 
402     /** Set whether last gesture was an A11y action. */
403     public void onA11yAction() {
404         completePriorGesture();
405         this.mA11YAction = true;
406     }
407 
408     void onSessionStarted() {
409         mSessionListeners.forEach(SessionListener::onSessionStarted);
410     }
411 
412     void onSessionEnd() {
413         for (MotionEvent ev : mRecentMotionEvents) {
414             ev.recycle();
415         }
416 
417         mRecentMotionEvents.clear();
418 
419         mDirty = true;
420 
421         mSessionListeners.forEach(SessionListener::onSessionEnded);
422     }
423 
424     public boolean isJustUnlockedWithFace() {
425         return mJustUnlockedWithFace;
426     }
427 
428     public void setJustUnlockedWithFace(boolean justUnlockedWithFace) {
429         mJustUnlockedWithFace = justUnlockedWithFace;
430     }
431 
432     /** Returns true if phone is sitting in a dock or is wirelessly charging. */
433     public boolean isDocked() {
434         return mBatteryController.isWirelessCharging() || mDockManager.isDocked();
435     }
436 
437     public boolean isUnfolded() {
438         return mIsFoldableDevice && Boolean.FALSE.equals(mFoldStateListener.getFolded());
439     }
440 
441     /** Implement to be alerted abotu the beginning and ending of falsing tracking. */
442     public interface SessionListener {
443         /** Called when the lock screen is shown and falsing-tracking begins. */
444         void onSessionStarted();
445 
446         /** Called when the lock screen exits and falsing-tracking ends. */
447         void onSessionEnded();
448     }
449 
450     /** Callback for receiving {@link android.view.MotionEvent}s as they are reported. */
451     public interface MotionEventListener {
452         /** */
453         void onMotionEvent(MotionEvent ev);
454     }
455 
456     /** Callback to be alerted when the current gesture ends. */
457     public interface GestureFinalizedListener {
458         /**
459          * Called just before a new gesture starts.
460          *
461          * Any pending work on a prior gesture can be considered cemented in place.
462          */
463         void onGestureFinalized(long completionTimeMs);
464     }
465 }
466