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 recalculateData()264 private void recalculateData() { 265 if (!mDirty) { 266 return; 267 } 268 269 List<MotionEvent> recentMotionEvents = getRecentMotionEvents(); 270 if (recentMotionEvents.isEmpty()) { 271 mFirstRecentMotionEvent = null; 272 mLastMotionEvent = null; 273 } else { 274 mFirstRecentMotionEvent = recentMotionEvents.get(0); 275 mLastMotionEvent = recentMotionEvents.get(recentMotionEvents.size() - 1); 276 } 277 278 calculateAngleInternal(); 279 280 mDirty = false; 281 } 282 shouldDropEvent(MotionEvent event)283 private boolean shouldDropEvent(MotionEvent event) { 284 if (mRecentMotionEvents.size() < 3) return false; 285 286 MotionEvent lastEvent = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1); 287 boolean isCompletingGesture = event.getActionMasked() == MotionEvent.ACTION_UP 288 && lastEvent.getActionMasked() == MotionEvent.ACTION_MOVE; 289 boolean isRecentEvent = 290 event.getEventTime() - lastEvent.getEventTime() < DROP_EVENT_THRESHOLD_MS; 291 return isCompletingGesture && isRecentEvent; 292 } 293 294 private void calculateAngleInternal() { 295 if (mRecentMotionEvents.size() < 2) { 296 mAngle = Float.MAX_VALUE; 297 } else { 298 float lastX = mLastMotionEvent.getX() - mFirstRecentMotionEvent.getX(); 299 float lastY = mLastMotionEvent.getY() - mFirstRecentMotionEvent.getY(); 300 301 mAngle = (float) Math.atan2(lastY, lastX); 302 while (mAngle < 0) { 303 mAngle += THREE_HUNDRED_SIXTY_DEG; 304 } 305 while (mAngle > THREE_HUNDRED_SIXTY_DEG) { 306 mAngle -= THREE_HUNDRED_SIXTY_DEG; 307 } 308 } 309 } 310 311 private List<MotionEvent> unpackMotionEvent(MotionEvent motionEvent) { 312 List<MotionEvent> motionEvents = new ArrayList<>(); 313 List<PointerProperties> pointerPropertiesList = new ArrayList<>(); 314 int pointerCount = motionEvent.getPointerCount(); 315 for (int i = 0; i < pointerCount; i++) { 316 PointerProperties pointerProperties = new PointerProperties(); 317 motionEvent.getPointerProperties(i, pointerProperties); 318 pointerPropertiesList.add(pointerProperties); 319 } 320 PointerProperties[] pointerPropertiesArray = new PointerProperties[pointerPropertiesList 321 .size()]; 322 pointerPropertiesList.toArray(pointerPropertiesArray); 323 324 int historySize = motionEvent.getHistorySize(); 325 for (int i = 0; i < historySize; i++) { 326 List<PointerCoords> pointerCoordsList = new ArrayList<>(); 327 for (int j = 0; j < pointerCount; j++) { 328 PointerCoords pointerCoords = new PointerCoords(); 329 motionEvent.getHistoricalPointerCoords(j, i, pointerCoords); 330 pointerCoordsList.add(pointerCoords); 331 } 332 motionEvents.add(MotionEvent.obtain( 333 motionEvent.getDownTime(), 334 motionEvent.getHistoricalEventTime(i), 335 motionEvent.getAction(), 336 pointerCount, 337 pointerPropertiesArray, 338 pointerCoordsList.toArray(new PointerCoords[0]), 339 motionEvent.getMetaState(), 340 motionEvent.getButtonState(), 341 motionEvent.getXPrecision(), 342 motionEvent.getYPrecision(), 343 motionEvent.getDeviceId(), 344 motionEvent.getEdgeFlags(), 345 motionEvent.getSource(), 346 motionEvent.getFlags() 347 )); 348 } 349 350 motionEvents.add(MotionEvent.obtainNoHistory(motionEvent)); 351 352 return motionEvents; 353 } 354 355 /** Register a {@link SessionListener}. */ 356 public void addSessionListener(SessionListener listener) { 357 mSessionListeners.add(listener); 358 } 359 360 /** Unregister a {@link SessionListener}. */ 361 public void removeSessionListener(SessionListener listener) { 362 mSessionListeners.remove(listener); 363 } 364 365 /** Register a {@link MotionEventListener}. */ 366 public void addMotionEventListener(MotionEventListener listener) { 367 mMotionEventListeners.add(listener); 368 } 369 370 /** Unegister a {@link MotionEventListener}. */ 371 public void removeMotionEventListener(MotionEventListener listener) { 372 mMotionEventListeners.remove(listener); 373 } 374 375 /** Register a {@link GestureFinalizedListener}. */ 376 public void addGestureCompleteListener(GestureFinalizedListener listener) { 377 mGestureFinalizedListeners.add(listener); 378 } 379 380 /** Unregister a {@link GestureFinalizedListener}. */ 381 public void removeGestureCompleteListener(GestureFinalizedListener listener) { 382 mGestureFinalizedListeners.remove(listener); 383 } 384 385 /** Return whether last gesture was an A11y action. */ 386 public boolean isA11yAction() { 387 return mA11YAction; 388 } 389 390 /** Set whether last gesture was an A11y action. */ 391 public void onA11yAction() { 392 completePriorGesture(); 393 this.mA11YAction = true; 394 } 395 396 void onSessionStarted() { 397 mSessionListeners.forEach(SessionListener::onSessionStarted); 398 } 399 400 void onSessionEnd() { 401 for (MotionEvent ev : mRecentMotionEvents) { 402 ev.recycle(); 403 } 404 405 mRecentMotionEvents.clear(); 406 407 mDirty = true; 408 409 mSessionListeners.forEach(SessionListener::onSessionEnded); 410 } 411 412 public boolean isJustUnlockedWithFace() { 413 return mJustUnlockedWithFace; 414 } 415 416 public void setJustUnlockedWithFace(boolean justUnlockedWithFace) { 417 mJustUnlockedWithFace = justUnlockedWithFace; 418 } 419 420 /** Returns true if phone is sitting in a dock or is wirelessly charging. */ 421 public boolean isDocked() { 422 return mBatteryController.isWirelessCharging() || mDockManager.isDocked(); 423 } 424 425 public boolean isUnfolded() { 426 return mIsFoldableDevice && Boolean.FALSE.equals(mFoldStateListener.getFolded()); 427 } 428 429 /** Implement to be alerted abotu the beginning and ending of falsing tracking. */ 430 public interface SessionListener { 431 /** Called when the lock screen is shown and falsing-tracking begins. */ 432 void onSessionStarted(); 433 434 /** Called when the lock screen exits and falsing-tracking ends. */ 435 void onSessionEnded(); 436 } 437 438 /** Callback for receiving {@link android.view.MotionEvent}s as they are reported. */ 439 public interface MotionEventListener { 440 /** */ 441 void onMotionEvent(MotionEvent ev); 442 } 443 444 /** Callback to be alerted when the current gesture ends. */ 445 public interface GestureFinalizedListener { 446 /** 447 * Called just before a new gesture starts. 448 * 449 * Any pending work on a prior gesture can be considered cemented in place. 450 */ 451 void onGestureFinalized(long completionTimeMs); 452 } 453 } 454