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.Classifier.BACK_GESTURE; 20 import static com.android.systemui.classifier.Classifier.GENERIC; 21 import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR; 22 import static com.android.systemui.classifier.FalsingManagerProxy.FALSING_SUCCESS; 23 import static com.android.systemui.classifier.FalsingModule.BRIGHT_LINE_GESTURE_CLASSIFERS; 24 25 import android.net.Uri; 26 import android.os.Build; 27 import android.util.IndentingPrintWriter; 28 import android.util.Log; 29 import android.view.accessibility.AccessibilityManager; 30 31 import androidx.annotation.NonNull; 32 33 import com.android.internal.logging.MetricsLogger; 34 import com.android.systemui.classifier.FalsingDataProvider.SessionListener; 35 import com.android.systemui.classifier.HistoryTracker.BeliefListener; 36 import com.android.systemui.dagger.qualifiers.TestHarness; 37 import com.android.systemui.flags.FeatureFlags; 38 import com.android.systemui.flags.Flags; 39 import com.android.systemui.plugins.FalsingManager; 40 import com.android.systemui.statusbar.policy.KeyguardStateController; 41 42 import java.io.PrintWriter; 43 import java.util.ArrayDeque; 44 import java.util.ArrayList; 45 import java.util.Collection; 46 import java.util.Collections; 47 import java.util.List; 48 import java.util.Queue; 49 import java.util.Set; 50 import java.util.StringJoiner; 51 import java.util.stream.Collectors; 52 53 import javax.inject.Inject; 54 import javax.inject.Named; 55 56 /** 57 * FalsingManager designed to make clear why a touch was rejected. 58 */ 59 public class BrightLineFalsingManager implements FalsingManager { 60 61 private static final String TAG = "FalsingManager"; 62 public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 63 64 private static final int RECENT_INFO_LOG_SIZE = 40; 65 private static final int RECENT_SWIPE_LOG_SIZE = 20; 66 private static final double TAP_CONFIDENCE_THRESHOLD = 0.7; 67 private static final double FALSE_BELIEF_THRESHOLD = 0.9; 68 69 private final FalsingDataProvider mDataProvider; 70 private final LongTapClassifier mLongTapClassifier; 71 private final SingleTapClassifier mSingleTapClassifier; 72 private final DoubleTapClassifier mDoubleTapClassifier; 73 private final HistoryTracker mHistoryTracker; 74 private final KeyguardStateController mKeyguardStateController; 75 private AccessibilityManager mAccessibilityManager; 76 private final boolean mTestHarness; 77 private final MetricsLogger mMetricsLogger; 78 private int mIsFalseTouchCalls; 79 private FeatureFlags mFeatureFlags; 80 private static final Queue<String> RECENT_INFO_LOG = 81 new ArrayDeque<>(RECENT_INFO_LOG_SIZE + 1); 82 private static final Queue<DebugSwipeRecord> RECENT_SWIPES = 83 new ArrayDeque<>(RECENT_SWIPE_LOG_SIZE + 1); 84 85 private final Collection<FalsingClassifier> mClassifiers; 86 private final List<FalsingBeliefListener> mFalsingBeliefListeners = new ArrayList<>(); 87 private List<FalsingTapListener> mFalsingTapListeners = new ArrayList<>(); 88 private ProximityEvent mLastProximityEvent; 89 90 private boolean mDestroyed; 91 92 private final SessionListener mSessionListener = new SessionListener() { 93 @Override 94 public void onSessionEnded() { 95 mLastProximityEvent = null; 96 mClassifiers.forEach(FalsingClassifier::onSessionEnded); 97 } 98 99 @Override 100 public void onSessionStarted() { 101 mClassifiers.forEach(FalsingClassifier::onSessionStarted); 102 } 103 }; 104 105 private final BeliefListener mBeliefListener = new BeliefListener() { 106 @Override 107 public void onBeliefChanged(double belief) { 108 logInfo(String.format( 109 "{belief=%s confidence=%s}", 110 mHistoryTracker.falseBelief(), 111 mHistoryTracker.falseConfidence())); 112 if (belief > FALSE_BELIEF_THRESHOLD) { 113 mFalsingBeliefListeners.forEach(FalsingBeliefListener::onFalse); 114 logInfo("Triggering False Event (Threshold: " + FALSE_BELIEF_THRESHOLD + ")"); 115 } 116 } 117 }; 118 119 private final FalsingDataProvider.GestureFinalizedListener mGestureFinalizedListener = 120 new FalsingDataProvider.GestureFinalizedListener() { 121 @Override 122 public void onGestureFinalized(long completionTimeMs) { 123 if (mPriorResults != null) { 124 boolean boolResult = mPriorResults.stream().anyMatch( 125 FalsingClassifier.Result::isFalse); 126 127 mPriorResults.forEach(result -> { 128 if (result.isFalse()) { 129 String reason = result.getReason(); 130 if (reason != null) { 131 logInfo(reason); 132 } 133 } 134 }); 135 136 if (Build.IS_ENG || Build.IS_USERDEBUG) { 137 // Copy motion events, as the results returned by 138 // #getRecentMotionEvents are recycled elsewhere. 139 RECENT_SWIPES.add(new DebugSwipeRecord( 140 boolResult, 141 mPriorInteractionType, 142 mDataProvider.getRecentMotionEvents().stream().map( 143 motionEvent -> new XYDt( 144 (int) motionEvent.getX(), 145 (int) motionEvent.getY(), 146 (int) (motionEvent.getEventTime() 147 - motionEvent.getDownTime()))) 148 .collect(Collectors.toList()))); 149 while (RECENT_SWIPES.size() > RECENT_INFO_LOG_SIZE) { 150 RECENT_SWIPES.remove(); 151 } 152 } 153 154 155 mHistoryTracker.addResults(mPriorResults, completionTimeMs); 156 mPriorResults = null; 157 mPriorInteractionType = Classifier.GENERIC; 158 } else { 159 // Gestures that were not classified get treated as a false. 160 // Gestures that look like simple taps are less likely to be false 161 // than swipes. They may simply be mis-clicks. 162 double penalty = mSingleTapClassifier.isTap( 163 mDataProvider.getRecentMotionEvents(), 0).isFalse() 164 ? 0.7 : 0.8; 165 mHistoryTracker.addResults( 166 Collections.singleton( 167 FalsingClassifier.Result.falsed( 168 penalty, getClass().getSimpleName(), 169 "unclassified")), 170 completionTimeMs); 171 } 172 } 173 }; 174 175 private Collection<FalsingClassifier.Result> mPriorResults; 176 private @Classifier.InteractionType int mPriorInteractionType = Classifier.GENERIC; 177 178 @Inject BrightLineFalsingManager( FalsingDataProvider falsingDataProvider, MetricsLogger metricsLogger, @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers, SingleTapClassifier singleTapClassifier, LongTapClassifier longTapClassifier, DoubleTapClassifier doubleTapClassifier, HistoryTracker historyTracker, KeyguardStateController keyguardStateController, AccessibilityManager accessibilityManager, @TestHarness boolean testHarness, FeatureFlags featureFlags)179 public BrightLineFalsingManager( 180 FalsingDataProvider falsingDataProvider, 181 MetricsLogger metricsLogger, 182 @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers, 183 SingleTapClassifier singleTapClassifier, LongTapClassifier longTapClassifier, 184 DoubleTapClassifier doubleTapClassifier, HistoryTracker historyTracker, 185 KeyguardStateController keyguardStateController, 186 AccessibilityManager accessibilityManager, 187 @TestHarness boolean testHarness, 188 FeatureFlags featureFlags) { 189 mDataProvider = falsingDataProvider; 190 mMetricsLogger = metricsLogger; 191 mClassifiers = classifiers; 192 mSingleTapClassifier = singleTapClassifier; 193 mLongTapClassifier = longTapClassifier; 194 mDoubleTapClassifier = doubleTapClassifier; 195 mHistoryTracker = historyTracker; 196 mKeyguardStateController = keyguardStateController; 197 mAccessibilityManager = accessibilityManager; 198 mTestHarness = testHarness; 199 mFeatureFlags = featureFlags; 200 201 mDataProvider.addSessionListener(mSessionListener); 202 mDataProvider.addGestureCompleteListener(mGestureFinalizedListener); 203 mHistoryTracker.addBeliefListener(mBeliefListener); 204 } 205 206 @Override isClassifierEnabled()207 public boolean isClassifierEnabled() { 208 return true; 209 } 210 211 @Override isFalseTouch(@lassifier.InteractionType int interactionType)212 public boolean isFalseTouch(@Classifier.InteractionType int interactionType) { 213 checkDestroyed(); 214 215 mPriorInteractionType = interactionType; 216 if (skipFalsing(interactionType)) { 217 mPriorResults = getPassedResult(1); 218 logDebug("Skipped falsing"); 219 return false; 220 } 221 222 final boolean[] localResult = {false}; 223 mPriorResults = mClassifiers.stream().map(falsingClassifier -> { 224 FalsingClassifier.Result r = falsingClassifier.classifyGesture( 225 interactionType, 226 mHistoryTracker.falseBelief(), 227 mHistoryTracker.falseConfidence()); 228 localResult[0] |= r.isFalse(); 229 230 return r; 231 }).collect(Collectors.toList()); 232 233 // check for false tap if it is a seekbar interaction 234 if (interactionType == MEDIA_SEEKBAR) { 235 localResult[0] &= isFalseTap(FalsingManager.MODERATE_PENALTY); 236 } 237 238 logDebug("False Gesture (type: " + interactionType + "): " + localResult[0]); 239 240 return localResult[0]; 241 } 242 243 @Override isSimpleTap()244 public boolean isSimpleTap() { 245 checkDestroyed(); 246 247 FalsingClassifier.Result result = mSingleTapClassifier.isTap( 248 mDataProvider.getRecentMotionEvents(), 0); 249 mPriorResults = Collections.singleton(result); 250 251 return !result.isFalse(); 252 } 253 checkDestroyed()254 private void checkDestroyed() { 255 if (mDestroyed) { 256 Log.wtf(TAG, "Tried to use FalsingManager after being destroyed!"); 257 } 258 } 259 260 @Override isFalseTap(@enalty int penalty)261 public boolean isFalseTap(@Penalty int penalty) { 262 checkDestroyed(); 263 264 if (skipFalsing(GENERIC)) { 265 mPriorResults = getPassedResult(1); 266 logDebug("Skipped falsing"); 267 return false; 268 } 269 270 double falsePenalty = 0; 271 switch(penalty) { 272 case NO_PENALTY: 273 falsePenalty = 0; 274 break; 275 case LOW_PENALTY: 276 falsePenalty = 0.1; 277 break; 278 case MODERATE_PENALTY: 279 falsePenalty = 0.3; 280 break; 281 case HIGH_PENALTY: 282 falsePenalty = 0.6; 283 break; 284 } 285 286 FalsingClassifier.Result singleTapResult = 287 mSingleTapClassifier.isTap(mDataProvider.getRecentMotionEvents().isEmpty() 288 ? mDataProvider.getPriorMotionEvents() 289 : mDataProvider.getRecentMotionEvents(), falsePenalty); 290 mPriorResults = Collections.singleton(singleTapResult); 291 292 if (!singleTapResult.isFalse()) { 293 if (mDataProvider.isJustUnlockedWithFace()) { 294 // Immediately pass if a face is detected. 295 mPriorResults = getPassedResult(1); 296 logDebug("False Single Tap: false (face detected)"); 297 return false; 298 } else if (!isFalseDoubleTap()) { 299 // We must check double tapping before other heuristics. This is because 300 // the double tap will fail if there's only been one tap. We don't want that 301 // failure to be recorded in mPriorResults. 302 logDebug("False Single Tap: false (double tapped)"); 303 return false; 304 } else if (mHistoryTracker.falseBelief() > TAP_CONFIDENCE_THRESHOLD) { 305 mPriorResults = Collections.singleton( 306 FalsingClassifier.Result.falsed( 307 0, getClass().getSimpleName(), "bad history")); 308 logDebug("False Single Tap: true (bad history)"); 309 mFalsingTapListeners.forEach(FalsingTapListener::onAdditionalTapRequired); 310 return true; 311 } else { 312 mPriorResults = getPassedResult(0.1); 313 logDebug("False Single Tap: false (default)"); 314 return false; 315 } 316 317 } else { 318 logDebug("False Single Tap: " + singleTapResult.isFalse() + " (simple)"); 319 return singleTapResult.isFalse(); 320 } 321 322 } 323 324 @Override isFalseLongTap(@enalty int penalty)325 public boolean isFalseLongTap(@Penalty int penalty) { 326 checkDestroyed(); 327 328 if (skipFalsing(GENERIC)) { 329 mPriorResults = getPassedResult(1); 330 logDebug("Skipped falsing"); 331 return false; 332 } 333 334 double falsePenalty = 0; 335 switch(penalty) { 336 case NO_PENALTY: 337 falsePenalty = 0; 338 break; 339 case LOW_PENALTY: 340 falsePenalty = 0.1; 341 break; 342 case MODERATE_PENALTY: 343 falsePenalty = 0.3; 344 break; 345 case HIGH_PENALTY: 346 falsePenalty = 0.6; 347 break; 348 } 349 350 FalsingClassifier.Result longTapResult = 351 mLongTapClassifier.isTap(mDataProvider.getRecentMotionEvents().isEmpty() 352 ? mDataProvider.getPriorMotionEvents() 353 : mDataProvider.getRecentMotionEvents(), falsePenalty); 354 mPriorResults = Collections.singleton(longTapResult); 355 356 if (!longTapResult.isFalse()) { 357 if (mDataProvider.isJustUnlockedWithFace()) { 358 // Immediately pass if a face is detected. 359 mPriorResults = getPassedResult(1); 360 logDebug("False Long Tap: false (face detected)"); 361 } else { 362 mPriorResults = getPassedResult(0.1); 363 logDebug("False Long Tap: false (default)"); 364 } 365 return false; 366 } else { 367 logDebug("False Long Tap: " + longTapResult.isFalse() + " (simple)"); 368 return longTapResult.isFalse(); 369 } 370 } 371 372 @Override isFalseDoubleTap()373 public boolean isFalseDoubleTap() { 374 checkDestroyed(); 375 376 if (skipFalsing(GENERIC)) { 377 mPriorResults = getPassedResult(1); 378 logDebug("Skipped falsing"); 379 return false; 380 } 381 382 FalsingClassifier.Result result = mDoubleTapClassifier.classifyGesture( 383 Classifier.GENERIC, 384 mHistoryTracker.falseBelief(), 385 mHistoryTracker.falseConfidence()); 386 mPriorResults = Collections.singleton(result); 387 logDebug("False Double Tap: " + result.isFalse() + " reason=" + result.getReason()); 388 return result.isFalse(); 389 } 390 skipFalsing(@lassifier.InteractionType int interactionType)391 private boolean skipFalsing(@Classifier.InteractionType int interactionType) { 392 return interactionType == BACK_GESTURE 393 || !mKeyguardStateController.isShowing() 394 || mTestHarness 395 || mDataProvider.isJustUnlockedWithFace() 396 || mDataProvider.isDocked() 397 || mAccessibilityManager.isTouchExplorationEnabled() 398 || mDataProvider.isA11yAction() 399 || mDataProvider.isFromTrackpad() 400 || (mFeatureFlags.isEnabled(Flags.FALSING_OFF_FOR_UNFOLDED) 401 && mDataProvider.isUnfolded()); 402 } 403 404 @Override onProximityEvent(ProximityEvent proximityEvent)405 public void onProximityEvent(ProximityEvent proximityEvent) { 406 // TODO: some of these classifiers might allow us to abort early, meaning we don't have to 407 // make these calls. 408 mLastProximityEvent = proximityEvent; 409 mClassifiers.forEach((classifier) -> classifier.onProximityEvent(proximityEvent)); 410 } 411 412 @Override onSuccessfulUnlock()413 public void onSuccessfulUnlock() { 414 if (mIsFalseTouchCalls != 0) { 415 mMetricsLogger.histogram(FALSING_SUCCESS, mIsFalseTouchCalls); 416 mIsFalseTouchCalls = 0; 417 } 418 } 419 420 @Override isProximityNear()421 public boolean isProximityNear() { 422 return mLastProximityEvent != null && mLastProximityEvent.getCovered(); 423 } 424 425 @Override isUnlockingDisabled()426 public boolean isUnlockingDisabled() { 427 return false; 428 } 429 430 @Override shouldEnforceBouncer()431 public boolean shouldEnforceBouncer() { 432 return false; 433 } 434 435 @Override reportRejectedTouch()436 public Uri reportRejectedTouch() { 437 return null; 438 } 439 440 @Override isReportingEnabled()441 public boolean isReportingEnabled() { 442 return false; 443 } 444 445 @Override addFalsingBeliefListener(FalsingBeliefListener listener)446 public void addFalsingBeliefListener(FalsingBeliefListener listener) { 447 mFalsingBeliefListeners.add(listener); 448 } 449 450 @Override removeFalsingBeliefListener(FalsingBeliefListener listener)451 public void removeFalsingBeliefListener(FalsingBeliefListener listener) { 452 mFalsingBeliefListeners.remove(listener); 453 } 454 455 @Override addTapListener(FalsingTapListener listener)456 public void addTapListener(FalsingTapListener listener) { 457 mFalsingTapListeners.add(listener); 458 } 459 460 @Override removeTapListener(FalsingTapListener listener)461 public void removeTapListener(FalsingTapListener listener) { 462 mFalsingTapListeners.remove(listener); 463 } 464 465 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)466 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 467 IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); 468 ipw.println("BRIGHTLINE FALSING MANAGER"); 469 ipw.print("classifierEnabled="); 470 ipw.println(isClassifierEnabled() ? 1 : 0); 471 ipw.print("mJustUnlockedWithFace="); 472 ipw.println(mDataProvider.isJustUnlockedWithFace() ? 1 : 0); 473 ipw.print("isDocked="); 474 ipw.println(mDataProvider.isDocked() ? 1 : 0); 475 ipw.print("width="); 476 ipw.println(mDataProvider.getWidthPixels()); 477 ipw.print("height="); 478 ipw.println(mDataProvider.getHeightPixels()); 479 ipw.println(); 480 if (RECENT_SWIPES.size() != 0) { 481 ipw.println("Recent swipes:"); 482 ipw.increaseIndent(); 483 for (DebugSwipeRecord record : RECENT_SWIPES) { 484 ipw.println(record.getString()); 485 ipw.println(); 486 } 487 ipw.decreaseIndent(); 488 } else { 489 ipw.println("No recent swipes"); 490 } 491 ipw.println(); 492 ipw.println("Recent falsing info:"); 493 ipw.increaseIndent(); 494 for (String msg : RECENT_INFO_LOG) { 495 ipw.println(msg); 496 } 497 ipw.println(); 498 } 499 500 @Override cleanupInternal()501 public void cleanupInternal() { 502 mDestroyed = true; 503 mDataProvider.removeSessionListener(mSessionListener); 504 mDataProvider.removeGestureCompleteListener(mGestureFinalizedListener); 505 mClassifiers.forEach(FalsingClassifier::cleanup); 506 mFalsingBeliefListeners.clear(); 507 mHistoryTracker.removeBeliefListener(mBeliefListener); 508 } 509 getPassedResult(double confidence)510 private static Collection<FalsingClassifier.Result> getPassedResult(double confidence) { 511 return Collections.singleton(FalsingClassifier.Result.passed(confidence)); 512 } 513 logDebug(String msg)514 static void logDebug(String msg) { 515 logDebug(msg, null); 516 } 517 logDebug(String msg, Throwable throwable)518 static void logDebug(String msg, Throwable throwable) { 519 if (DEBUG) { 520 Log.d(TAG, msg, throwable); 521 } 522 } 523 logVerbose(String msg)524 static void logVerbose(String msg) { 525 if (DEBUG) { 526 Log.v(TAG, msg); 527 } 528 } 529 logInfo(String msg)530 static void logInfo(String msg) { 531 Log.i(TAG, msg); 532 RECENT_INFO_LOG.add(msg); 533 while (RECENT_INFO_LOG.size() > RECENT_INFO_LOG_SIZE) { 534 RECENT_INFO_LOG.remove(); 535 } 536 } 537 logError(String msg)538 static void logError(String msg) { 539 Log.e(TAG, msg); 540 } 541 542 private static class DebugSwipeRecord { 543 private static final byte VERSION = 1; // opaque version number indicating format of data. 544 private final boolean mIsFalse; 545 private final int mInteractionType; 546 private final List<XYDt> mRecentMotionEvents; 547 DebugSwipeRecord(boolean isFalse, int interactionType, List<XYDt> recentMotionEvents)548 DebugSwipeRecord(boolean isFalse, int interactionType, 549 List<XYDt> recentMotionEvents) { 550 mIsFalse = isFalse; 551 mInteractionType = interactionType; 552 mRecentMotionEvents = recentMotionEvents; 553 } 554 getString()555 String getString() { 556 StringJoiner sj = new StringJoiner(","); 557 sj.add(Integer.toString(VERSION)) 558 .add(mIsFalse ? "1" : "0") 559 .add(Integer.toString(mInteractionType)); 560 for (XYDt event : mRecentMotionEvents) { 561 sj.add(event.toString()); 562 } 563 return sj.toString(); 564 } 565 } 566 567 private static class XYDt { 568 private final int mX; 569 private final int mY; 570 private final int mDT; 571 XYDt(int x, int y, int dT)572 XYDt(int x, int y, int dT) { 573 mX = x; 574 mY = y; 575 mDT = dT; 576 } 577 578 @Override toString()579 public String toString() { 580 return mX + "," + mY + "," + mDT; 581 } 582 } 583 } 584