/*
|
* Copyright (C) 2019 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 android.content.Context;
|
import android.database.ContentObserver;
|
import android.hardware.Sensor;
|
import android.hardware.SensorEvent;
|
import android.hardware.SensorEventListener;
|
import android.hardware.SensorManager;
|
import android.hardware.biometrics.BiometricSourceType;
|
import android.net.Uri;
|
import android.os.Handler;
|
import android.os.Looper;
|
import android.os.PowerManager;
|
import android.os.UserHandle;
|
import android.provider.Settings;
|
import android.view.InputDevice;
|
import android.view.MotionEvent;
|
import android.view.accessibility.AccessibilityManager;
|
|
import com.android.internal.logging.MetricsLogger;
|
import com.android.keyguard.KeyguardUpdateMonitor;
|
import com.android.keyguard.KeyguardUpdateMonitorCallback;
|
import com.android.systemui.Dependency;
|
import com.android.systemui.UiOffloadThread;
|
import com.android.systemui.analytics.DataCollector;
|
import com.android.systemui.plugins.FalsingManager;
|
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
|
import com.android.systemui.statusbar.StatusBarState;
|
import com.android.systemui.util.AsyncSensorManager;
|
|
import java.io.PrintWriter;
|
|
/**
|
* When the phone is locked, listens to touch, sensor and phone events and sends them to
|
* DataCollector and HumanInteractionClassifier.
|
*
|
* It does not collect touch events when the bouncer shows up.
|
*/
|
public class FalsingManagerImpl implements FalsingManager {
|
private static final String ENFORCE_BOUNCER = "falsing_manager_enforce_bouncer";
|
|
private static final int[] CLASSIFIER_SENSORS = new int[] {
|
Sensor.TYPE_PROXIMITY,
|
};
|
|
private static final int[] COLLECTOR_SENSORS = new int[] {
|
Sensor.TYPE_ACCELEROMETER,
|
Sensor.TYPE_GYROSCOPE,
|
Sensor.TYPE_PROXIMITY,
|
Sensor.TYPE_LIGHT,
|
Sensor.TYPE_ROTATION_VECTOR,
|
};
|
private static final String FALSING_REMAIN_LOCKED = "falsing_failure_after_attempts";
|
private static final String FALSING_SUCCESS = "falsing_success_after_attempts";
|
|
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
private final Context mContext;
|
|
private final SensorManager mSensorManager;
|
private final DataCollector mDataCollector;
|
private final HumanInteractionClassifier mHumanInteractionClassifier;
|
private final AccessibilityManager mAccessibilityManager;
|
private final UiOffloadThread mUiOffloadThread;
|
|
private boolean mEnforceBouncer = false;
|
private boolean mBouncerOn = false;
|
private boolean mBouncerOffOnDown = false;
|
private boolean mSessionActive = false;
|
private boolean mIsTouchScreen = true;
|
private boolean mJustUnlockedWithFace = false;
|
private int mState = StatusBarState.SHADE;
|
private boolean mScreenOn;
|
private boolean mShowingAod;
|
private Runnable mPendingWtf;
|
private int mIsFalseTouchCalls;
|
private MetricsLogger mMetricsLogger;
|
|
private SensorEventListener mSensorEventListener = new SensorEventListener() {
|
@Override
|
public synchronized void onSensorChanged(SensorEvent event) {
|
mDataCollector.onSensorChanged(event);
|
mHumanInteractionClassifier.onSensorChanged(event);
|
}
|
|
@Override
|
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
mDataCollector.onAccuracyChanged(sensor, accuracy);
|
}
|
};
|
|
public StateListener mStatusBarStateListener = new StateListener() {
|
@Override
|
public void onStateChanged(int newState) {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("setStatusBarState", new StringBuilder()
|
.append("from=").append(StatusBarState.toShortString(mState))
|
.append(" to=").append(StatusBarState.toShortString(newState))
|
.toString());
|
}
|
mState = newState;
|
updateSessionActive();
|
}
|
};
|
|
protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
|
@Override
|
public void onChange(boolean selfChange) {
|
updateConfiguration();
|
}
|
};
|
private final KeyguardUpdateMonitorCallback mKeyguardUpdateCallback =
|
new KeyguardUpdateMonitorCallback() {
|
@Override
|
public void onBiometricAuthenticated(int userId,
|
BiometricSourceType biometricSourceType) {
|
if (userId == KeyguardUpdateMonitor.getCurrentUser()
|
&& biometricSourceType == BiometricSourceType.FACE) {
|
mJustUnlockedWithFace = true;
|
}
|
}
|
};
|
|
FalsingManagerImpl(Context context) {
|
mContext = context;
|
mSensorManager = Dependency.get(AsyncSensorManager.class);
|
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
|
mDataCollector = DataCollector.getInstance(mContext);
|
mHumanInteractionClassifier = HumanInteractionClassifier.getInstance(mContext);
|
mUiOffloadThread = Dependency.get(UiOffloadThread.class);
|
mScreenOn = context.getSystemService(PowerManager.class).isInteractive();
|
mMetricsLogger = new MetricsLogger();
|
|
mContext.getContentResolver().registerContentObserver(
|
Settings.Secure.getUriFor(ENFORCE_BOUNCER), false,
|
mSettingsObserver,
|
UserHandle.USER_ALL);
|
|
updateConfiguration();
|
Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener);
|
KeyguardUpdateMonitor.getInstance(context).registerCallback(mKeyguardUpdateCallback);
|
}
|
|
private void updateConfiguration() {
|
mEnforceBouncer = 0 != Settings.Secure.getInt(mContext.getContentResolver(),
|
ENFORCE_BOUNCER, 0);
|
}
|
|
private boolean shouldSessionBeActive() {
|
if (FalsingLog.ENABLED && FalsingLog.VERBOSE) {
|
FalsingLog.v("shouldBeActive", new StringBuilder()
|
.append("enabled=").append(isEnabled() ? 1 : 0)
|
.append(" mScreenOn=").append(mScreenOn ? 1 : 0)
|
.append(" mState=").append(StatusBarState.toShortString(mState))
|
.append(" mShowingAod=").append(mShowingAod ? 1 : 0)
|
.toString()
|
);
|
}
|
return isEnabled() && mScreenOn && (mState == StatusBarState.KEYGUARD) && !mShowingAod;
|
}
|
|
private boolean sessionEntrypoint() {
|
if (!mSessionActive && shouldSessionBeActive()) {
|
onSessionStart();
|
return true;
|
}
|
return false;
|
}
|
|
private void sessionExitpoint(boolean force) {
|
if (mSessionActive && (force || !shouldSessionBeActive())) {
|
mSessionActive = false;
|
if (mIsFalseTouchCalls != 0) {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i(
|
"isFalseTouchCalls", "Calls before failure: " + mIsFalseTouchCalls);
|
}
|
mMetricsLogger.histogram(FALSING_REMAIN_LOCKED, mIsFalseTouchCalls);
|
mIsFalseTouchCalls = 0;
|
}
|
|
// This can be expensive, and doesn't need to happen on the main thread.
|
mUiOffloadThread.submit(() -> {
|
mSensorManager.unregisterListener(mSensorEventListener);
|
});
|
}
|
}
|
|
public void updateSessionActive() {
|
if (shouldSessionBeActive()) {
|
sessionEntrypoint();
|
} else {
|
sessionExitpoint(false /* force */);
|
}
|
}
|
|
private void onSessionStart() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onSessionStart", "classifierEnabled=" + isClassiferEnabled());
|
clearPendingWtf();
|
}
|
mBouncerOn = false;
|
mSessionActive = true;
|
mJustUnlockedWithFace = false;
|
mIsFalseTouchCalls = 0;
|
|
if (mHumanInteractionClassifier.isEnabled()) {
|
registerSensors(CLASSIFIER_SENSORS);
|
}
|
if (mDataCollector.isEnabledFull()) {
|
registerSensors(COLLECTOR_SENSORS);
|
}
|
if (mDataCollector.isEnabled()) {
|
mDataCollector.onFalsingSessionStarted();
|
}
|
}
|
|
private void registerSensors(int [] sensors) {
|
for (int sensorType : sensors) {
|
Sensor s = mSensorManager.getDefaultSensor(sensorType);
|
if (s != null) {
|
|
// This can be expensive, and doesn't need to happen on the main thread.
|
mUiOffloadThread.submit(() -> {
|
mSensorManager.registerListener(
|
mSensorEventListener, s, SensorManager.SENSOR_DELAY_GAME);
|
});
|
}
|
}
|
}
|
|
public boolean isClassiferEnabled() {
|
return mHumanInteractionClassifier.isEnabled();
|
}
|
|
private boolean isEnabled() {
|
return mHumanInteractionClassifier.isEnabled() || mDataCollector.isEnabled();
|
}
|
|
public boolean isUnlockingDisabled() {
|
return mDataCollector.isUnlockingDisabled();
|
}
|
/**
|
* @return true if the classifier determined that this is not a human interacting with the phone
|
*/
|
public boolean isFalseTouch() {
|
if (FalsingLog.ENABLED) {
|
// We're getting some false wtfs from touches that happen after the device went
|
// to sleep. Only report missing sessions that happen when the device is interactive.
|
if (!mSessionActive && mContext.getSystemService(PowerManager.class).isInteractive()
|
&& mPendingWtf == null) {
|
int enabled = isEnabled() ? 1 : 0;
|
int screenOn = mScreenOn ? 1 : 0;
|
String state = StatusBarState.toShortString(mState);
|
Throwable here = new Throwable("here");
|
FalsingLog.wLogcat("isFalseTouch", new StringBuilder()
|
.append("Session is not active, yet there's a query for a false touch.")
|
.append(" enabled=").append(enabled)
|
.append(" mScreenOn=").append(screenOn)
|
.append(" mState=").append(state)
|
.append(". Escalating to WTF if screen does not turn on soon.")
|
.toString());
|
|
// Unfortunately we're also getting false positives for touches that happen right
|
// after the screen turns on, but before that notification has made it to us.
|
// Unfortunately there's no good way to catch that, except to wait and see if we get
|
// the screen on notification soon.
|
mPendingWtf = () -> FalsingLog.wtf("isFalseTouch", new StringBuilder()
|
.append("Session did not become active after query for a false touch.")
|
.append(" enabled=").append(enabled)
|
.append('/').append(isEnabled() ? 1 : 0)
|
.append(" mScreenOn=").append(screenOn)
|
.append('/').append(mScreenOn ? 1 : 0)
|
.append(" mState=").append(state)
|
.append('/').append(StatusBarState.toShortString(mState))
|
.append(". Look for warnings ~1000ms earlier to see root cause.")
|
.toString(), here);
|
mHandler.postDelayed(mPendingWtf, 1000);
|
}
|
}
|
if (mAccessibilityManager.isTouchExplorationEnabled()) {
|
// Touch exploration triggers false positives in the classifier and
|
// already sufficiently prevents false unlocks.
|
return false;
|
}
|
if (!mIsTouchScreen) {
|
// Unlocking with input devices besides the touchscreen should already be sufficiently
|
// anti-falsed.
|
return false;
|
}
|
if (mJustUnlockedWithFace) {
|
// Unlocking with face is a strong user presence signal, we can assume the user
|
// is present until the next session starts.
|
return false;
|
}
|
mIsFalseTouchCalls++;
|
boolean isFalse = mHumanInteractionClassifier.isFalseTouch();
|
if (!isFalse) {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("isFalseTouchCalls", "Calls before success: " + mIsFalseTouchCalls);
|
}
|
mMetricsLogger.histogram(FALSING_SUCCESS, mIsFalseTouchCalls);
|
mIsFalseTouchCalls = 0;
|
}
|
return isFalse;
|
}
|
|
private void clearPendingWtf() {
|
if (mPendingWtf != null) {
|
mHandler.removeCallbacks(mPendingWtf);
|
mPendingWtf = null;
|
}
|
}
|
|
|
public boolean shouldEnforceBouncer() {
|
return mEnforceBouncer;
|
}
|
|
public void setShowingAod(boolean showingAod) {
|
mShowingAod = showingAod;
|
updateSessionActive();
|
}
|
|
public void onScreenTurningOn() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onScreenTurningOn", new StringBuilder()
|
.append("from=").append(mScreenOn ? 1 : 0)
|
.toString());
|
clearPendingWtf();
|
}
|
mScreenOn = true;
|
if (sessionEntrypoint()) {
|
mDataCollector.onScreenTurningOn();
|
}
|
}
|
|
public void onScreenOnFromTouch() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onScreenOnFromTouch", new StringBuilder()
|
.append("from=").append(mScreenOn ? 1 : 0)
|
.toString());
|
}
|
mScreenOn = true;
|
if (sessionEntrypoint()) {
|
mDataCollector.onScreenOnFromTouch();
|
}
|
}
|
|
public void onScreenOff() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onScreenOff", new StringBuilder()
|
.append("from=").append(mScreenOn ? 1 : 0)
|
.toString());
|
}
|
mDataCollector.onScreenOff();
|
mScreenOn = false;
|
sessionExitpoint(false /* force */);
|
}
|
|
public void onSucccessfulUnlock() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onSucccessfulUnlock", "");
|
}
|
mDataCollector.onSucccessfulUnlock();
|
}
|
|
public void onBouncerShown() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onBouncerShown", new StringBuilder()
|
.append("from=").append(mBouncerOn ? 1 : 0)
|
.toString());
|
}
|
if (!mBouncerOn) {
|
mBouncerOn = true;
|
mDataCollector.onBouncerShown();
|
}
|
}
|
|
public void onBouncerHidden() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onBouncerHidden", new StringBuilder()
|
.append("from=").append(mBouncerOn ? 1 : 0)
|
.toString());
|
}
|
if (mBouncerOn) {
|
mBouncerOn = false;
|
mDataCollector.onBouncerHidden();
|
}
|
}
|
|
public void onQsDown() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onQsDown", "");
|
}
|
mHumanInteractionClassifier.setType(Classifier.QUICK_SETTINGS);
|
mDataCollector.onQsDown();
|
}
|
|
public void setQsExpanded(boolean expanded) {
|
mDataCollector.setQsExpanded(expanded);
|
}
|
|
public void onTrackingStarted(boolean secure) {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onTrackingStarted", "");
|
}
|
mHumanInteractionClassifier.setType(secure
|
? Classifier.BOUNCER_UNLOCK : Classifier.UNLOCK);
|
mDataCollector.onTrackingStarted();
|
}
|
|
public void onTrackingStopped() {
|
mDataCollector.onTrackingStopped();
|
}
|
|
public void onNotificationActive() {
|
mDataCollector.onNotificationActive();
|
}
|
|
public void onNotificationDoubleTap(boolean accepted, float dx, float dy) {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onNotificationDoubleTap", "accepted=" + accepted
|
+ " dx=" + dx + " dy=" + dy + " (px)");
|
}
|
mDataCollector.onNotificationDoubleTap();
|
}
|
|
public void setNotificationExpanded() {
|
mDataCollector.setNotificationExpanded();
|
}
|
|
public void onNotificatonStartDraggingDown() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onNotificatonStartDraggingDown", "");
|
}
|
mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DRAG_DOWN);
|
mDataCollector.onNotificatonStartDraggingDown();
|
}
|
|
public void onStartExpandingFromPulse() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onStartExpandingFromPulse", "");
|
}
|
mHumanInteractionClassifier.setType(Classifier.PULSE_EXPAND);
|
mDataCollector.onStartExpandingFromPulse();
|
}
|
|
public void onNotificatonStopDraggingDown() {
|
mDataCollector.onNotificatonStopDraggingDown();
|
}
|
|
public void onExpansionFromPulseStopped() {
|
mDataCollector.onExpansionFromPulseStopped();
|
}
|
|
public void onNotificationDismissed() {
|
mDataCollector.onNotificationDismissed();
|
}
|
|
public void onNotificatonStartDismissing() {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onNotificatonStartDismissing", "");
|
}
|
mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS);
|
mDataCollector.onNotificatonStartDismissing();
|
}
|
|
public void onNotificatonStopDismissing() {
|
mDataCollector.onNotificatonStopDismissing();
|
}
|
|
public void onCameraOn() {
|
mDataCollector.onCameraOn();
|
}
|
|
public void onLeftAffordanceOn() {
|
mDataCollector.onLeftAffordanceOn();
|
}
|
|
public void onAffordanceSwipingStarted(boolean rightCorner) {
|
if (FalsingLog.ENABLED) {
|
FalsingLog.i("onAffordanceSwipingStarted", "");
|
}
|
if (rightCorner) {
|
mHumanInteractionClassifier.setType(Classifier.RIGHT_AFFORDANCE);
|
} else {
|
mHumanInteractionClassifier.setType(Classifier.LEFT_AFFORDANCE);
|
}
|
mDataCollector.onAffordanceSwipingStarted(rightCorner);
|
}
|
|
public void onAffordanceSwipingAborted() {
|
mDataCollector.onAffordanceSwipingAborted();
|
}
|
|
public void onUnlockHintStarted() {
|
mDataCollector.onUnlockHintStarted();
|
}
|
|
public void onCameraHintStarted() {
|
mDataCollector.onCameraHintStarted();
|
}
|
|
public void onLeftAffordanceHintStarted() {
|
mDataCollector.onLeftAffordanceHintStarted();
|
}
|
|
public void onTouchEvent(MotionEvent event, int width, int height) {
|
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
mIsTouchScreen = event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN);
|
// If the bouncer was not shown during the down event,
|
// we want the entire gesture going to HumanInteractionClassifier
|
mBouncerOffOnDown = !mBouncerOn;
|
}
|
if (mSessionActive) {
|
if (!mBouncerOn) {
|
// In case bouncer is "visible", but onFullyShown has not yet been called,
|
// avoid adding the event to DataCollector
|
mDataCollector.onTouchEvent(event, width, height);
|
}
|
if (mBouncerOffOnDown) {
|
mHumanInteractionClassifier.onTouchEvent(event);
|
}
|
}
|
}
|
|
public void dump(PrintWriter pw) {
|
pw.println("FALSING MANAGER");
|
pw.print("classifierEnabled="); pw.println(isClassiferEnabled() ? 1 : 0);
|
pw.print("mSessionActive="); pw.println(mSessionActive ? 1 : 0);
|
pw.print("mBouncerOn="); pw.println(mSessionActive ? 1 : 0);
|
pw.print("mState="); pw.println(StatusBarState.toShortString(mState));
|
pw.print("mScreenOn="); pw.println(mScreenOn ? 1 : 0);
|
pw.println();
|
}
|
|
@Override
|
public void cleanup() {
|
mSensorManager.unregisterListener(mSensorEventListener);
|
mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
|
Dependency.get(StatusBarStateController.class).removeCallback(mStatusBarStateListener);
|
KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mKeyguardUpdateCallback);
|
}
|
|
public Uri reportRejectedTouch() {
|
if (mDataCollector.isEnabled()) {
|
return mDataCollector.reportRejectedTouch();
|
}
|
return null;
|
}
|
|
public boolean isReportingEnabled() {
|
return mDataCollector.isReportingEnabled();
|
}
|
}
|