| /* |
| * 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; |
| } |
| } |
| } |