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