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