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 com.android.systemui.dagger.SysUISingleton;
20 import com.android.systemui.plugins.FalsingManager;
21 import com.android.systemui.util.time.SystemClock;
22 
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.List;
26 import java.util.concurrent.DelayQueue;
27 import java.util.concurrent.Delayed;
28 import java.util.concurrent.TimeUnit;
29 
30 import javax.inject.Inject;
31 
32 /**
33  * A stateful class for tracking recent {@link FalsingManager} results.
34  *
35  * Can return a "penalty" based on recent gestures that may make it harder or easier to
36  * unlock a phone, as well as a "confidence" relating to how consistent recent falsing results
37  * have been.
38  */
39 @SysUISingleton
40 public class HistoryTracker {
41     private static final long HISTORY_MAX_AGE_MS = 10000;
42     // A score is decayed discretely every DECAY_INTERVAL_MS.
43     private static final long DECAY_INTERVAL_MS = 100;
44     // We expire items once their decay factor is below 0.1.
45     private static final double MINIMUM_SCORE = 0.1;
46     // This magic number is the factor a score is reduced by every DECAY_INTERVAL_MS.
47     // Once a score is HISTORY_MAX_AGE_MS ms old, it will be reduced by being multiplied by
48     // MINIMUM_SCORE. The math below ensures that.
49     private static final double HISTORY_DECAY =
50             Math.pow(10, Math.log10(MINIMUM_SCORE) / HISTORY_MAX_AGE_MS * DECAY_INTERVAL_MS);
51 
52     private final SystemClock mSystemClock;
53 
54     DelayQueue<CombinedResult> mResults = new DelayQueue<>();
55     private final List<BeliefListener> mBeliefListeners = new ArrayList<>();
56 
57     @Inject
HistoryTracker(SystemClock systemClock)58     HistoryTracker(SystemClock systemClock) {
59         mSystemClock = systemClock;
60     }
61 
62     /**
63      * Returns how much the HistoryClassifier thinks the past events indicate pocket dialing.
64      *
65      * A result close to 0.5 means that prior data is inconclusive (inconsistent, lacking
66      * confidence, or simply lacking in quantity).
67      *
68      * A result close to 0 means that prior gestures indicate a success.
69      *
70      * A result close to 1 means that prior gestures were very obviously false.
71      *
72      * The current gesture might be different than what is reported by this method, but there should
73      * be a high-bar to be classified differently.
74      *
75      * See also {@link #falseConfidence()}.
76      */
falseBelief()77     double falseBelief() {
78         //noinspection StatementWithEmptyBody
79         while (mResults.poll() != null) {
80             // Empty out the expired results.
81         }
82 
83         if (mResults.isEmpty()) {
84             return 0.5;
85         }
86 
87         long nowMs = mSystemClock.uptimeMillis();
88         // Get our Bayes on.
89         return mResults.stream()
90                 .map(result -> result.getDecayedScore(nowMs))
91                 .reduce(0.5,
92                         (prior, measurement) ->
93                                 (prior * measurement)
94                                         / (prior * measurement + (1 - prior) * (1 - measurement)));
95     }
96 
97     /**
98      * Returns how confident the HistoryClassifier is in its own score.
99      *
100      * A result of 0.0 means that there are no data to make a calculation with. The HistoryTracker's
101      * results have nothing to add and should not be considered.
102      *
103      * A result of 0.5 means that the data are not consistent with each other, sometimes falsing
104      * sometimes not.
105      *
106      * A result of 1 means that there are ample, fresh data to act upon that is all consistent
107      * with each other.
108      *
109      * See als {@link #falseBelief()}.
110      */
falseConfidence()111     double falseConfidence() {
112         //noinspection StatementWithEmptyBody
113         while (mResults.poll() != null) {
114             // Empty out the expired results.
115         }
116 
117         // Our confidence is 1 - the population stddev. Smaller stddev == higher confidence.
118         if (mResults.isEmpty()) {
119             return 0;
120         }
121 
122         double mean = mResults.stream()
123                 .map(CombinedResult::getScore)
124                 .reduce(0.0, Double::sum) / mResults.size();
125 
126         double stddev = Math.sqrt(
127                 mResults.stream()
128                         .map(result -> Math.pow(result.getScore() - mean, 2))
129                         .reduce(0.0, Double::sum) / mResults.size());
130 
131         return 1 - stddev;
132     }
133 
addResults(Collection<FalsingClassifier.Result> results, long uptimeMillis)134     void addResults(Collection<FalsingClassifier.Result> results, long uptimeMillis) {
135         double finalScore = 0;
136         for (FalsingClassifier.Result result : results) {
137             // A confidence of 1 adds either 0 for non-falsed or 1 for falsed.
138             // A confidence of 0 adds 0.5.
139             finalScore += (result.isFalse() ? .5 : -.5) * result.getConfidence() + 0.5;
140         }
141 
142         finalScore /= results.size();
143 
144         // Never add a 0 or  1, else Bayes breaks down (a 0 and a 1 together results in NaN). In
145         // other words,  you shouldn't need Bayes if you have 100% confidence one way or another.
146         // Instead, make the number ever so slightly smaller so that our math never breaks.
147         if (finalScore == 1) {
148             finalScore = 0.99999;
149         } else if (finalScore == 0) {
150             finalScore = 0.00001;
151         }
152 
153         //noinspection StatementWithEmptyBody
154         while (mResults.poll() != null) {
155             // Empty out the expired results.
156         }
157 
158         mResults.add(new CombinedResult(uptimeMillis, finalScore));
159 
160         mBeliefListeners.forEach(beliefListener -> beliefListener.onBeliefChanged(falseBelief()));
161     }
162 
addBeliefListener(BeliefListener listener)163     void addBeliefListener(BeliefListener listener) {
164         mBeliefListeners.add(listener);
165     }
166 
removeBeliefListener(BeliefListener listener)167     void removeBeliefListener(BeliefListener listener) {
168         mBeliefListeners.remove(listener);
169     }
170     /**
171      * Represents a falsing score combing all the classifiers together.
172      *
173      * Can "decay" over time, such that older results contribute less. Once they drop below
174      * a certain threshold, the {@link #getDelay(TimeUnit)} method will return <= 0, indicating
175      * that this result can be discarded.
176      */
177     private class CombinedResult implements Delayed {
178 
179         private final long mExpiryMs;
180         private final double mScore;
181 
CombinedResult(long uptimeMillis, double score)182         CombinedResult(long uptimeMillis, double score) {
183             mExpiryMs = uptimeMillis + HISTORY_MAX_AGE_MS;
184             mScore = score;
185         }
186 
getDecayedScore(long nowMs)187         double getDecayedScore(long nowMs) {
188             long remainingTimeMs = mExpiryMs - nowMs;
189             long decayedTimeMs = HISTORY_MAX_AGE_MS - remainingTimeMs;
190             double timeIntervals = (double) decayedTimeMs / DECAY_INTERVAL_MS;
191 
192             // Score should decay towards 0.5.
193             return (mScore - 0.5) * Math.pow(HISTORY_DECAY, timeIntervals) + 0.5;
194         }
195 
getScore()196         double getScore() {
197             return mScore;
198         }
199 
200         @Override
getDelay(TimeUnit unit)201         public long getDelay(TimeUnit unit) {
202             return unit.convert(mExpiryMs - mSystemClock.uptimeMillis(), TimeUnit.MILLISECONDS);
203         }
204 
205         @Override
compareTo(Delayed o)206         public int compareTo(Delayed o) {
207             long ourDelay = getDelay(TimeUnit.MILLISECONDS);
208             long otherDelay = o.getDelay(TimeUnit.MILLISECONDS);
209             return Long.compare(ourDelay, otherDelay);
210         }
211     }
212 
213     interface BeliefListener {
onBeliefChanged(double belief)214         void onBeliefChanged(double belief);
215     }
216 }
217