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