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