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