/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.classifier; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.util.time.SystemClock; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; import javax.inject.Inject; /** * A stateful class for tracking recent {@link FalsingManager} results. * * Can return a "penalty" based on recent gestures that may make it harder or easier to * unlock a phone, as well as a "confidence" relating to how consistent recent falsing results * have been. */ @SysUISingleton public class HistoryTracker { private static final long HISTORY_MAX_AGE_MS = 10000; // A score is decayed discretely every DECAY_INTERVAL_MS. private static final long DECAY_INTERVAL_MS = 100; // We expire items once their decay factor is below 0.1. private static final double MINIMUM_SCORE = 0.1; // This magic number is the factor a score is reduced by every DECAY_INTERVAL_MS. // Once a score is HISTORY_MAX_AGE_MS ms old, it will be reduced by being multiplied by // MINIMUM_SCORE. The math below ensures that. private static final double HISTORY_DECAY = Math.pow(10, Math.log10(MINIMUM_SCORE) / HISTORY_MAX_AGE_MS * DECAY_INTERVAL_MS); private final SystemClock mSystemClock; DelayQueue mResults = new DelayQueue<>(); private final List mBeliefListeners = new ArrayList<>(); @Inject HistoryTracker(SystemClock systemClock) { mSystemClock = systemClock; } /** * Returns how much the HistoryClassifier thinks the past events indicate pocket dialing. * * A result close to 0.5 means that prior data is inconclusive (inconsistent, lacking * confidence, or simply lacking in quantity). * * A result close to 0 means that prior gestures indicate a success. * * A result close to 1 means that prior gestures were very obviously false. * * The current gesture might be different than what is reported by this method, but there should * be a high-bar to be classified differently. * * See also {@link #falseConfidence()}. */ double falseBelief() { //noinspection StatementWithEmptyBody while (mResults.poll() != null) { // Empty out the expired results. } if (mResults.isEmpty()) { return 0.5; } long nowMs = mSystemClock.uptimeMillis(); // Get our Bayes on. return mResults.stream() .map(result -> result.getDecayedScore(nowMs)) .reduce(0.5, (prior, measurement) -> (prior * measurement) / (prior * measurement + (1 - prior) * (1 - measurement))); } /** * Returns how confident the HistoryClassifier is in its own score. * * A result of 0.0 means that there are no data to make a calculation with. The HistoryTracker's * results have nothing to add and should not be considered. * * A result of 0.5 means that the data are not consistent with each other, sometimes falsing * sometimes not. * * A result of 1 means that there are ample, fresh data to act upon that is all consistent * with each other. * * See als {@link #falseBelief()}. */ double falseConfidence() { //noinspection StatementWithEmptyBody while (mResults.poll() != null) { // Empty out the expired results. } // Our confidence is 1 - the population stddev. Smaller stddev == higher confidence. if (mResults.isEmpty()) { return 0; } double mean = mResults.stream() .map(CombinedResult::getScore) .reduce(0.0, Double::sum) / mResults.size(); double stddev = Math.sqrt( mResults.stream() .map(result -> Math.pow(result.getScore() - mean, 2)) .reduce(0.0, Double::sum) / mResults.size()); return 1 - stddev; } void addResults(Collection results, long uptimeMillis) { double finalScore = 0; for (FalsingClassifier.Result result : results) { // A confidence of 1 adds either 0 for non-falsed or 1 for falsed. // A confidence of 0 adds 0.5. finalScore += (result.isFalse() ? .5 : -.5) * result.getConfidence() + 0.5; } finalScore /= results.size(); // Never add a 0 or 1, else Bayes breaks down (a 0 and a 1 together results in NaN). In // other words, you shouldn't need Bayes if you have 100% confidence one way or another. // Instead, make the number ever so slightly smaller so that our math never breaks. if (finalScore == 1) { finalScore = 0.99999; } else if (finalScore == 0) { finalScore = 0.00001; } //noinspection StatementWithEmptyBody while (mResults.poll() != null) { // Empty out the expired results. } mResults.add(new CombinedResult(uptimeMillis, finalScore)); mBeliefListeners.forEach(beliefListener -> beliefListener.onBeliefChanged(falseBelief())); } void addBeliefListener(BeliefListener listener) { mBeliefListeners.add(listener); } void removeBeliefListener(BeliefListener listener) { mBeliefListeners.remove(listener); } /** * Represents a falsing score combing all the classifiers together. * * Can "decay" over time, such that older results contribute less. Once they drop below * a certain threshold, the {@link #getDelay(TimeUnit)} method will return <= 0, indicating * that this result can be discarded. */ private class CombinedResult implements Delayed { private final long mExpiryMs; private final double mScore; CombinedResult(long uptimeMillis, double score) { mExpiryMs = uptimeMillis + HISTORY_MAX_AGE_MS; mScore = score; } double getDecayedScore(long nowMs) { long remainingTimeMs = mExpiryMs - nowMs; long decayedTimeMs = HISTORY_MAX_AGE_MS - remainingTimeMs; double timeIntervals = (double) decayedTimeMs / DECAY_INTERVAL_MS; // Score should decay towards 0.5. return (mScore - 0.5) * Math.pow(HISTORY_DECAY, timeIntervals) + 0.5; } double getScore() { return mScore; } @Override public long getDelay(TimeUnit unit) { return unit.convert(mExpiryMs - mSystemClock.uptimeMillis(), TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { long ourDelay = getDelay(TimeUnit.MILLISECONDS); long otherDelay = o.getDelay(TimeUnit.MILLISECONDS); return Long.compare(ourDelay, otherDelay); } } interface BeliefListener { void onBeliefChanged(double belief); } }