blob: b237ca27e62d8ca52389210b60b8983c5e050c31 [file] [log] [blame]
/*
* Copyright (C) 2021 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.server.power;
import static android.provider.DeviceConfig.NAMESPACE_ATTENTION_MANAGER_SERVICE;
import android.annotation.NonNull;
import android.app.ActivityThread;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemClock;
import android.provider.DeviceConfig;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
/**
* Class used to detect when the phone is placed face down. This is used for Flip to Screen Off. A
* client can use this detector to trigger state changes like screen off when the phone is face
* down.
*/
public class FaceDownDetector implements SensorEventListener {
private static final String TAG = "FaceDownDetector";
private static final boolean DEBUG = false;
private static final int SCREEN_OFF_RESULT =
FrameworkStatsLog.FACE_DOWN_REPORTED__FACE_DOWN_RESPONSE__SCREEN_OFF;
private static final int USER_INTERACTION =
FrameworkStatsLog.FACE_DOWN_REPORTED__FACE_DOWN_RESPONSE__USER_INTERACTION;
private static final int UNFLIP =
FrameworkStatsLog.FACE_DOWN_REPORTED__FACE_DOWN_RESPONSE__UNFLIP;
private static final int UNKNOWN =
FrameworkStatsLog.FACE_DOWN_REPORTED__FACE_DOWN_RESPONSE__UNKNOWN;
/**
* Used by the ExponentialMovingAverage accelerations, this determines how quickly the
* average can change. A number closer to 1 will mean it will take longer to change.
*/
private static final float MOVING_AVERAGE_WEIGHT = 0.5f;
/** DeviceConfig flag name, if {@code true}, enables Face Down features. */
static final String KEY_FEATURE_ENABLED = "enable_flip_to_screen_off";
/** Default value in absence of {@link DeviceConfig} override. */
private static final boolean DEFAULT_FEATURE_ENABLED = true;
private boolean mIsEnabled;
private int mSensorMaxLatencyMicros;
/**
* DeviceConfig flag name, determines how long to disable sensor when user interacts while
* device is flipped.
*/
private static final String KEY_INTERACTION_BACKOFF = "face_down_interaction_backoff_millis";
/** Default value in absence of {@link DeviceConfig} override. */
private static final long DEFAULT_INTERACTION_BACKOFF = 60_000;
private long mUserInteractionBackoffMillis;
/**
* DeviceConfig flag name, defines the max change in acceleration which will prevent face down
* due to movement.
*/
static final String KEY_ACCELERATION_THRESHOLD = "acceleration_threshold";
/** Default value in absence of {@link DeviceConfig} override. */
static final float DEFAULT_ACCELERATION_THRESHOLD = 0.2f;
private float mAccelerationThreshold;
/**
* DeviceConfig flag name, defines the maximum z-axis acceleration that will indicate the phone
* is face down.
*/
static final String KEY_Z_ACCELERATION_THRESHOLD = "z_acceleration_threshold";
/** Default value in absence of {@link DeviceConfig} override. */
static final float DEFAULT_Z_ACCELERATION_THRESHOLD = -9.5f;
private float mZAccelerationThreshold;
/**
* After going face down, we relax the threshold to make it more difficult to exit face down
* than to enter it.
*/
private float mZAccelerationThresholdLenient;
/**
* DeviceConfig flag name, defines the minimum amount of time that has to pass while the phone
* is face down and not moving in order to trigger face down behavior, in milliseconds.
*/
static final String KEY_TIME_THRESHOLD_MILLIS = "time_threshold_millis";
/** Default value in absence of {@link DeviceConfig} override. */
static final long DEFAULT_TIME_THRESHOLD_MILLIS = 1_000L;
private Duration mTimeThreshold;
private Sensor mAccelerometer;
private SensorManager mSensorManager;
private final Consumer<Boolean> mOnFlip;
/** Values we store for logging purposes. */
private long mLastFlipTime = 0L;
public int mPreviousResultType = UNKNOWN;
public long mPreviousResultTime = 0L;
private long mMillisSaved = 0L;
private final ExponentialMovingAverage mCurrentXYAcceleration =
new ExponentialMovingAverage(MOVING_AVERAGE_WEIGHT);
private final ExponentialMovingAverage mCurrentZAcceleration =
new ExponentialMovingAverage(MOVING_AVERAGE_WEIGHT);
private boolean mFaceDown = false;
private boolean mInteractive = false;
private boolean mActive = false;
private float mPrevAcceleration = 0;
private long mPrevAccelerationTime = 0;
private boolean mZAccelerationIsFaceDown = false;
private long mZAccelerationFaceDownTime = 0L;
private final Handler mHandler;
private final Runnable mUserActivityRunnable;
@VisibleForTesting
final BroadcastReceiver mScreenReceiver;
private Context mContext;
public FaceDownDetector(@NonNull Consumer<Boolean> onFlip) {
mOnFlip = Objects.requireNonNull(onFlip);
mHandler = new Handler(Looper.getMainLooper());
mScreenReceiver = new ScreenStateReceiver();
mUserActivityRunnable = () -> {
if (mFaceDown) {
exitFaceDown(USER_INTERACTION, SystemClock.uptimeMillis() - mLastFlipTime);
updateActiveState();
}
};
}
/** Initializes the FaceDownDetector and all necessary listeners. */
public void systemReady(Context context) {
mContext = context;
mSensorManager = context.getSystemService(SensorManager.class);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
readValuesFromDeviceConfig();
DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_ATTENTION_MANAGER_SERVICE,
ActivityThread.currentApplication().getMainExecutor(),
(properties) -> onDeviceConfigChange(properties.getKeyset()));
updateActiveState();
}
private void registerScreenReceiver(Context context) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
context.registerReceiver(mScreenReceiver, intentFilter);
}
/**
* Sets the active state of the detector. If false, we will not process accelerometer changes.
*/
private void updateActiveState() {
final long currentTime = SystemClock.uptimeMillis();
final boolean sawRecentInteraction = mPreviousResultType == USER_INTERACTION
&& currentTime - mPreviousResultTime < mUserInteractionBackoffMillis;
final boolean shouldBeActive = mInteractive && mIsEnabled && !sawRecentInteraction;
if (mActive != shouldBeActive) {
if (shouldBeActive) {
mSensorManager.registerListener(
this,
mAccelerometer,
SensorManager.SENSOR_DELAY_NORMAL,
mSensorMaxLatencyMicros
);
if (mPreviousResultType == SCREEN_OFF_RESULT) {
logScreenOff();
}
} else {
if (mFaceDown && !mInteractive) {
mPreviousResultType = SCREEN_OFF_RESULT;
mPreviousResultTime = currentTime;
}
mSensorManager.unregisterListener(this);
mFaceDown = false;
mOnFlip.accept(false);
}
mActive = shouldBeActive;
if (DEBUG) Slog.d(TAG, "Update active - " + shouldBeActive);
}
}
/** Prints state information about FaceDownDetector */
public void dump(PrintWriter pw) {
pw.println("FaceDownDetector:");
pw.println(" mFaceDown=" + mFaceDown);
pw.println(" mActive=" + mActive);
pw.println(" mLastFlipTime=" + mLastFlipTime);
pw.println(" mSensorMaxLatencyMicros=" + mSensorMaxLatencyMicros);
pw.println(" mUserInteractionBackoffMillis=" + mUserInteractionBackoffMillis);
pw.println(" mPreviousResultTime=" + mPreviousResultTime);
pw.println(" mPreviousResultType=" + mPreviousResultType);
pw.println(" mMillisSaved=" + mMillisSaved);
pw.println(" mZAccelerationThreshold=" + mZAccelerationThreshold);
pw.println(" mAccelerationThreshold=" + mAccelerationThreshold);
pw.println(" mTimeThreshold=" + mTimeThreshold);
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) return;
if (!mActive || !mIsEnabled) return;
final float x = event.values[0];
final float y = event.values[1];
mCurrentXYAcceleration.updateMovingAverage(x * x + y * y);
mCurrentZAcceleration.updateMovingAverage(event.values[2]);
// Detect movement
// If the x, y acceleration is within the acc threshold for at least a length of time longer
// than the time threshold, we set moving to true.
final long curTime = event.timestamp;
if (Math.abs(mCurrentXYAcceleration.mMovingAverage - mPrevAcceleration)
> mAccelerationThreshold) {
mPrevAcceleration = mCurrentXYAcceleration.mMovingAverage;
mPrevAccelerationTime = curTime;
}
final boolean moving = curTime - mPrevAccelerationTime <= mTimeThreshold.toNanos();
// If the z acceleration is beyond the gravity/z-acceleration threshold for at least a
// length of time longer than the time threshold, we set isFaceDownForPeriod to true.
final float zAccelerationThreshold =
mFaceDown ? mZAccelerationThresholdLenient : mZAccelerationThreshold;
final boolean isCurrentlyFaceDown =
mCurrentZAcceleration.mMovingAverage < zAccelerationThreshold;
final boolean isFaceDownForPeriod = isCurrentlyFaceDown
&& mZAccelerationIsFaceDown
&& curTime - mZAccelerationFaceDownTime > mTimeThreshold.toNanos();
if (isCurrentlyFaceDown && !mZAccelerationIsFaceDown) {
mZAccelerationFaceDownTime = curTime;
mZAccelerationIsFaceDown = true;
} else if (!isCurrentlyFaceDown) {
mZAccelerationIsFaceDown = false;
}
if (!moving && isFaceDownForPeriod && !mFaceDown) {
faceDownDetected();
} else if (!isFaceDownForPeriod && mFaceDown) {
unFlipDetected();
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
private void faceDownDetected() {
if (DEBUG) Slog.d(TAG, "Triggered faceDownDetected.");
mLastFlipTime = SystemClock.uptimeMillis();
mFaceDown = true;
mOnFlip.accept(true);
}
private void unFlipDetected() {
if (DEBUG) Slog.d(TAG, "Triggered exitFaceDown");
exitFaceDown(UNFLIP, SystemClock.uptimeMillis() - mLastFlipTime);
}
/**
* The user interacted with the screen while face down, indicated the phone is in use.
* We log this event and temporarily make this detector inactive.
*/
public void userActivity(int event) {
if (event != PowerManager.USER_ACTIVITY_EVENT_FACE_DOWN) {
mHandler.post(mUserActivityRunnable);
}
}
private void exitFaceDown(int resultType, long millisSinceFlip) {
FrameworkStatsLog.write(FrameworkStatsLog.FACE_DOWN_REPORTED,
resultType,
millisSinceFlip,
/* millis_until_normal_timeout= */ 0L,
/* millis_until_next_screen_on= */ 0L);
mFaceDown = false;
mLastFlipTime = 0L;
mPreviousResultType = resultType;
mPreviousResultTime = SystemClock.uptimeMillis();
mOnFlip.accept(false);
}
private void logScreenOff() {
final long currentTime = SystemClock.uptimeMillis();
FrameworkStatsLog.write(FrameworkStatsLog.FACE_DOWN_REPORTED,
SCREEN_OFF_RESULT,
/* millis_since_flip= */ mPreviousResultTime - mLastFlipTime,
mMillisSaved,
/* millis_until_next_screen_on= */ currentTime - mPreviousResultTime);
mPreviousResultType = UNKNOWN;
}
private boolean isEnabled() {
return DeviceConfig.getBoolean(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_FEATURE_ENABLED,
DEFAULT_FEATURE_ENABLED)
&& mContext.getResources().getBoolean(
com.android.internal.R.bool.config_flipToScreenOffEnabled);
}
private float getAccelerationThreshold() {
return getFloatFlagValue(KEY_ACCELERATION_THRESHOLD,
DEFAULT_ACCELERATION_THRESHOLD,
-2.0f,
2.0f);
}
private float getZAccelerationThreshold() {
return getFloatFlagValue(KEY_Z_ACCELERATION_THRESHOLD,
DEFAULT_Z_ACCELERATION_THRESHOLD,
-15.0f,
0.0f);
}
private long getUserInteractionBackoffMillis() {
return getLongFlagValue(KEY_INTERACTION_BACKOFF,
DEFAULT_INTERACTION_BACKOFF,
0,
3600_000);
}
private int getSensorMaxLatencyMicros() {
return mContext.getResources().getInteger(
com.android.internal.R.integer.config_flipToScreenOffMaxLatencyMicros);
}
private float getFloatFlagValue(String key, float defaultValue, float min, float max) {
final float value = DeviceConfig.getFloat(NAMESPACE_ATTENTION_MANAGER_SERVICE,
key,
defaultValue);
if (value < min || value > max) {
Slog.w(TAG, "Bad flag value supplied for: " + key);
return defaultValue;
}
return value;
}
private long getLongFlagValue(String key, long defaultValue, long min, long max) {
final long value = DeviceConfig.getLong(NAMESPACE_ATTENTION_MANAGER_SERVICE,
key,
defaultValue);
if (value < min || value > max) {
Slog.w(TAG, "Bad flag value supplied for: " + key);
return defaultValue;
}
return value;
}
private Duration getTimeThreshold() {
final long millis = DeviceConfig.getLong(NAMESPACE_ATTENTION_MANAGER_SERVICE,
KEY_TIME_THRESHOLD_MILLIS,
DEFAULT_TIME_THRESHOLD_MILLIS);
if (millis < 0 || millis > 15_000) {
Slog.w(TAG, "Bad flag value supplied for: " + KEY_TIME_THRESHOLD_MILLIS);
return Duration.ofMillis(DEFAULT_TIME_THRESHOLD_MILLIS);
}
return Duration.ofMillis(millis);
}
private void onDeviceConfigChange(@NonNull Set<String> keys) {
for (String key : keys) {
switch (key) {
case KEY_ACCELERATION_THRESHOLD:
case KEY_Z_ACCELERATION_THRESHOLD:
case KEY_TIME_THRESHOLD_MILLIS:
case KEY_FEATURE_ENABLED:
readValuesFromDeviceConfig();
updateActiveState();
return;
default:
Slog.i(TAG, "Ignoring change on " + key);
}
}
}
private void readValuesFromDeviceConfig() {
mAccelerationThreshold = getAccelerationThreshold();
mZAccelerationThreshold = getZAccelerationThreshold();
mZAccelerationThresholdLenient = mZAccelerationThreshold + 1.0f;
mTimeThreshold = getTimeThreshold();
mSensorMaxLatencyMicros = getSensorMaxLatencyMicros();
mUserInteractionBackoffMillis = getUserInteractionBackoffMillis();
final boolean oldEnabled = mIsEnabled;
mIsEnabled = isEnabled();
if (oldEnabled != mIsEnabled) {
if (!mIsEnabled) {
mContext.unregisterReceiver(mScreenReceiver);
mInteractive = false;
} else {
registerScreenReceiver(mContext);
mInteractive = mContext.getSystemService(PowerManager.class).isInteractive();
}
}
Slog.i(TAG, "readValuesFromDeviceConfig():"
+ "\nmAccelerationThreshold=" + mAccelerationThreshold
+ "\nmZAccelerationThreshold=" + mZAccelerationThreshold
+ "\nmTimeThreshold=" + mTimeThreshold
+ "\nmIsEnabled=" + mIsEnabled);
}
/**
* Sets how much screen on time might be saved as a result of this detector. Currently used for
* logging purposes.
*/
public void setMillisSaved(long millisSaved) {
mMillisSaved = millisSaved;
}
private final class ScreenStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
mInteractive = false;
updateActiveState();
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
mInteractive = true;
updateActiveState();
}
}
}
private final class ExponentialMovingAverage {
private final float mAlpha;
private final float mInitialAverage;
private float mMovingAverage;
ExponentialMovingAverage(float alpha) {
this(alpha, 0.0f);
}
ExponentialMovingAverage(float alpha, float initialAverage) {
this.mAlpha = alpha;
this.mInitialAverage = initialAverage;
this.mMovingAverage = initialAverage;
}
void updateMovingAverage(float newValue) {
mMovingAverage = newValue + mAlpha * (mMovingAverage - newValue);
}
void reset() {
mMovingAverage = this.mInitialAverage;
}
}
}