blob: 09bf04cb6d59c29a2d3f07ce00e4faf6fe4e9203 [file] [log] [blame]
/*
* 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<CombinedResult> mResults = new DelayQueue<>();
private final List<BeliefListener> 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<FalsingClassifier.Result> 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);
}
}