diff --git a/services/core/java/com/android/server/power/FaceDownDetector.java b/services/core/java/com/android/server/power/FaceDownDetector.java new file mode 100644 index 0000000000000..2442079bec8bf --- /dev/null +++ b/services/core/java/com/android/server/power/FaceDownDetector.java @@ -0,0 +1,456 @@ +/* + * 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.SystemClock; +import android.provider.DeviceConfig; +import android.util.Slog; + +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; + + /** + * 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. */ + private 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; + + /** + * 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 mOnFlip; + + /** Values we store for logging purposes. */ + private long mLastFlipTime = 0L; + public int mPreviousResultType = 0; + 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 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; + + public FaceDownDetector(@NonNull Consumer onFlip) { + mOnFlip = Objects.requireNonNull(onFlip); + mHandler = new Handler(Looper.getMainLooper()); + mUserActivityRunnable = () -> { + if (mFaceDown) { + exitFaceDown(USER_INTERACTION, SystemClock.uptimeMillis() - mLastFlipTime); + checkAndUpdateActiveState(false); + } + }; + } + + /** Initializes the FaceDownDetector and all necessary listeners. */ + public void systemReady(Context context) { + mSensorManager = context.getSystemService(SensorManager.class); + mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + readValuesFromDeviceConfig(); + checkAndUpdateActiveState(true); + DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_ATTENTION_MANAGER_SERVICE, + ActivityThread.currentApplication().getMainExecutor(), + (properties) -> onDeviceConfigChange(properties.getKeyset())); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + intentFilter.addAction(Intent.ACTION_SCREEN_ON); + intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); + context.registerReceiver(new ScreenStateReceiver(), intentFilter); + } + + /** + * Sets the active state of the detector. If false, we will not process accelerometer changes. + */ + private void checkAndUpdateActiveState(boolean active) { + if (mIsEnabled && mActive != active) { + final long currentTime = SystemClock.uptimeMillis(); + // Don't make active if there was recently a user interaction while face down. + if (active && mPreviousResultType == USER_INTERACTION + && currentTime - mPreviousResultTime < mUserInteractionBackoffMillis) { + return; + } + if (DEBUG) Slog.d(TAG, "Update active - " + active); + mActive = active; + if (!active) { + if (mFaceDown && mPreviousResultTime != USER_INTERACTION) { + mPreviousResultType = SCREEN_OFF_RESULT; + mPreviousResultTime = currentTime; + } + mSensorManager.unregisterListener(this); + mFaceDown = false; + mOnFlip.accept(false); + } else { + if (mPreviousResultType == SCREEN_OFF_RESULT) { + logScreenOff(); + } + mSensorManager.registerListener( + this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL); + } + } + } + + /** 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(" 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() { + 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() { + if (mPreviousResultType == SCREEN_OFF_RESULT) { + final long currentTime = SystemClock.uptimeMillis(); + FrameworkStatsLog.write(FrameworkStatsLog.FACE_DOWN_REPORTED, + mPreviousResultType, + /* millis_since_flip= */ mPreviousResultTime - mLastFlipTime, + mMillisSaved, + /* millis_until_next_screen_on= */ currentTime - mPreviousResultTime); + mPreviousResultType = -1; + } + } + + private boolean isEnabled() { + return DeviceConfig.getBoolean(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_FEATURE_ENABLED, + DEFAULT_FEATURE_ENABLED); + } + + 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 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 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(); + return; + default: + Slog.i(TAG, "Ignoring change on " + key); + } + } + } + + private void readValuesFromDeviceConfig() { + mAccelerationThreshold = getAccelerationThreshold(); + mZAccelerationThreshold = getZAccelerationThreshold(); + mZAccelerationThresholdLenient = mZAccelerationThreshold + 1.0f; + mTimeThreshold = getTimeThreshold(); + mIsEnabled = isEnabled(); + mUserInteractionBackoffMillis = getUserInteractionBackoffMillis(); + + 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())) { + checkAndUpdateActiveState(false); + } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) { + checkAndUpdateActiveState(true); + } + } + } + + 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; + } + } +} diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index b8e0156fb4ccb..f49e2f1631b93 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -119,6 +119,7 @@ public class Notifier { private final AppOpsManager mAppOps; private final SuspendBlocker mSuspendBlocker; private final WindowManagerPolicy mPolicy; + private final FaceDownDetector mFaceDownDetector; private final ActivityManagerInternal mActivityManagerInternal; private final InputManagerInternal mInputManagerInternal; private final InputMethodManagerInternal mInputMethodManagerInternal; @@ -165,12 +166,14 @@ public class Notifier { private boolean mUserActivityPending; public Notifier(Looper looper, Context context, IBatteryStats batteryStats, - SuspendBlocker suspendBlocker, WindowManagerPolicy policy) { + SuspendBlocker suspendBlocker, WindowManagerPolicy policy, + FaceDownDetector faceDownDetector) { mContext = context; mBatteryStats = batteryStats; mAppOps = mContext.getSystemService(AppOpsManager.class); mSuspendBlocker = suspendBlocker; mPolicy = policy; + mFaceDownDetector = faceDownDetector; mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class); @@ -654,6 +657,7 @@ public class Notifier { TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); tm.notifyUserActivity(); mPolicy.userActivity(); + mFaceDownDetector.userActivity(); } void postEnhancedDischargePredictionBroadcast(long delayMs) { diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 508c73ec40c2d..a135705291974 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -176,6 +176,8 @@ public final class PowerManagerService extends SystemService private static final int DIRTY_VR_MODE_CHANGED = 1 << 13; // Dirty bit: attentive timer may have timed out private static final int DIRTY_ATTENTIVE = 1 << 14; + // Dirty bit: phone flipped to face down + private static final int DIRTY_FACE_DOWN = 1 << 15; // Summarizes the state of all active wakelocks. private static final int WAKE_LOCK_CPU = 1 << 0; @@ -263,6 +265,7 @@ public final class PowerManagerService extends SystemService private final BatterySaverStateMachine mBatterySaverStateMachine; private final BatterySavingStats mBatterySavingStats; private final AttentionDetector mAttentionDetector; + private final FaceDownDetector mFaceDownDetector; private final BinderService mBinderService; private final LocalService mLocalService; private final NativeWrapper mNativeWrapper; @@ -547,6 +550,10 @@ public final class PowerManagerService extends SystemService public final float mScreenBrightnessMaximumVr; public final float mScreenBrightnessDefaultVr; + // Value we store for tracking face down behavior. + private boolean mIsFaceDown = false; + private long mLastFlipTime = 0L; + // The screen brightness mode. // One of the Settings.System.SCREEN_BRIGHTNESS_MODE_* constants. private int mScreenBrightnessModeSetting; @@ -791,8 +798,10 @@ public final class PowerManagerService extends SystemService @VisibleForTesting static class Injector { Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats, - SuspendBlocker suspendBlocker, WindowManagerPolicy policy) { - return new Notifier(looper, context, batteryStats, suspendBlocker, policy); + SuspendBlocker suspendBlocker, WindowManagerPolicy policy, + FaceDownDetector faceDownDetector) { + return new Notifier( + looper, context, batteryStats, suspendBlocker, policy, faceDownDetector); } SuspendBlocker createSuspendBlocker(PowerManagerService service, String name) { @@ -906,6 +915,7 @@ public final class PowerManagerService extends SystemService mAmbientDisplaySuppressionController = mInjector.createAmbientDisplaySuppressionController(context); mAttentionDetector = new AttentionDetector(this::onUserAttention, mLock); + mFaceDownDetector = new FaceDownDetector(this::onFlip); mBatterySavingStats = new BatterySavingStats(mLock); mBatterySaverPolicy = @@ -1011,6 +1021,26 @@ public final class PowerManagerService extends SystemService } } + private void onFlip(boolean isFaceDown) { + long millisUntilNormalTimeout = 0; + synchronized (mLock) { + mIsFaceDown = isFaceDown; + if (isFaceDown) { + final long currentTime = mClock.uptimeMillis(); + mLastFlipTime = currentTime; + final long sleepTimeout = getSleepTimeoutLocked(-1L); + final long screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout, -1L); + millisUntilNormalTimeout = + mLastUserActivityTime + screenOffTimeout - mClock.uptimeMillis(); + mDirty |= DIRTY_FACE_DOWN; + updatePowerStateLocked(); + } + } + if (isFaceDown) { + mFaceDownDetector.setMillisSaved(millisUntilNormalTimeout); + } + } + @Override public void onStart() { publishBinderService(Context.POWER_SERVICE, mBinderService, /* allowIsolated= */ false, @@ -1065,7 +1095,7 @@ public final class PowerManagerService extends SystemService mBatteryStats = BatteryStatsService.getService(); mNotifier = mInjector.createNotifier(Looper.getMainLooper(), mContext, mBatteryStats, mInjector.createSuspendBlocker(this, "PowerManagerService.Broadcasts"), - mPolicy); + mPolicy, mFaceDownDetector); mWirelessChargerDetector = mInjector.createWirelessChargerDetector(sensorManager, mInjector.createSuspendBlocker( @@ -1099,6 +1129,7 @@ public final class PowerManagerService extends SystemService mBatterySaverController.systemReady(); mBatterySaverPolicy.systemReady(); + mFaceDownDetector.systemReady(mContext); // Register for settings changes. resolver.registerContentObserver(Settings.Secure.getUriFor( @@ -2283,9 +2314,11 @@ public final class PowerManagerService extends SystemService || getWakefulnessLocked() == WAKEFULNESS_DOZING) { final long attentiveTimeout = getAttentiveTimeoutLocked(); final long sleepTimeout = getSleepTimeoutLocked(attentiveTimeout); - final long screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout, + long screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout, attentiveTimeout); final long screenDimDuration = getScreenDimDurationLocked(screenOffTimeout); + screenOffTimeout = + getScreenOffTimeoutWithFaceDownLocked(screenOffTimeout, screenDimDuration); final boolean userInactiveOverride = mUserInactiveOverrideFromWindowManager; final long nextProfileTimeout = getNextProfileTimeoutLocked(now); @@ -2541,6 +2574,16 @@ public final class PowerManagerService extends SystemService (long)(screenOffTimeout * mMaximumScreenDimRatioConfig)); } + private long getScreenOffTimeoutWithFaceDownLocked( + long screenOffTimeout, long screenDimDuration) { + // If face down, we decrease the timeout to equal the dim duration so that the + // device will go into a dim state. + if (mIsFaceDown) { + return Math.min(screenDimDuration, screenOffTimeout); + } + return screenOffTimeout; + } + /** * Updates the wakefulness of the device. * @@ -3859,6 +3902,8 @@ public final class PowerManagerService extends SystemService pw.println(" mDisplayReady=" + mDisplayReady); pw.println(" mHoldingWakeLockSuspendBlocker=" + mHoldingWakeLockSuspendBlocker); pw.println(" mHoldingDisplaySuspendBlocker=" + mHoldingDisplaySuspendBlocker); + pw.println(" mLastFlipTime=" + mLastFlipTime); + pw.println(" mIsFaceDown=" + mIsFaceDown); pw.println(); pw.println("Settings and Configuration:"); @@ -4003,6 +4048,8 @@ public final class PowerManagerService extends SystemService mNotifier.dump(pw); } + mFaceDownDetector.dump(pw); + mAmbientDisplaySuppressionController.dump(pw); } diff --git a/services/tests/servicestests/src/com/android/server/power/FaceDownDetectorTest.java b/services/tests/servicestests/src/com/android/server/power/FaceDownDetectorTest.java new file mode 100644 index 0000000000000..ef20ee7e6ecdb --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/power/FaceDownDetectorTest.java @@ -0,0 +1,165 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorManager; +import android.testing.TestableContext; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.display.TestUtils; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Constructor; +import java.time.Duration; + +public class FaceDownDetectorTest { + @ClassRule + public static final TestableContext sContext = new TestableContext( + InstrumentationRegistry.getInstrumentation().getTargetContext(), null); + + private final FaceDownDetector mFaceDownDetector = + new FaceDownDetector(this::onFlip); + + @Mock private SensorManager mSensorManager; + + private long mCurrentTime; + private int mOnFaceDownCalls = 0; + private int mOnFaceDownExitCalls = 0; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + sContext.addMockSystemService(SensorManager.class, mSensorManager); + mCurrentTime = 0; + } + + @Test + public void faceDownFor2Seconds_triggersFaceDown() throws Exception { + mFaceDownDetector.systemReady(sContext); + + // Face up + // Using 0.5 on x to simulate constant acceleration, such as a sloped surface. + mFaceDownDetector.onSensorChanged(createTestEvent(0.5f, 0.0f, 10.0f)); + + for (int i = 0; i < 200; i++) { + advanceTime(Duration.ofMillis(20)); + mFaceDownDetector.onSensorChanged(createTestEvent(0.5f, 0.0f, -10.0f)); + } + + assertThat(mOnFaceDownCalls).isEqualTo(1); + assertThat(mOnFaceDownExitCalls).isEqualTo(0); + } + + @Test + public void faceDownFor2Seconds_withMotion_DoesNotTriggerFaceDown() throws Exception { + mFaceDownDetector.systemReady(sContext); + + // Face up + mFaceDownDetector.onSensorChanged(createTestEvent(0.5f, 0.0f, 10.0f)); + + for (int i = 0; i < 100; i++) { + advanceTime(Duration.ofMillis(20)); + //Move along x direction + mFaceDownDetector.onSensorChanged(createTestEvent(0.5f * i, 0.0f, -10.0f)); + } + + assertThat(mOnFaceDownCalls).isEqualTo(0); + assertThat(mOnFaceDownExitCalls).isEqualTo(0); + } + + @Test + public void faceDownForHalfSecond_DoesNotTriggerFaceDown() throws Exception { + mFaceDownDetector.systemReady(sContext); + + // Face up + mFaceDownDetector.onSensorChanged(createTestEvent(0.5f, 0.0f, 10.0f)); + + for (int i = 0; i < 100; i++) { + advanceTime(Duration.ofMillis(5)); + mFaceDownDetector.onSensorChanged(createTestEvent(0.5f, 0.0f, -10.0f)); + } + + assertThat(mOnFaceDownCalls).isEqualTo(0); + assertThat(mOnFaceDownExitCalls).isEqualTo(0); + } + + @Test + public void faceDownFor2Seconds_followedByFaceUp_triggersFaceDownExit() throws Exception { + mFaceDownDetector.systemReady(sContext); + + // Face up + // Using 0.5 on x to simulate constant acceleration, such as a sloped surface. + mFaceDownDetector.onSensorChanged(createTestEvent(0.5f, 0.0f, 10.0f)); + + // Trigger face down + for (int i = 0; i < 100; i++) { + advanceTime(Duration.ofMillis(20)); + mFaceDownDetector.onSensorChanged(createTestEvent(0.5f, 0.0f, -10.0f)); + } + + // Phone flips + for (int i = 0; i < 10; i++) { + advanceTime(Duration.ofMillis(5)); + mFaceDownDetector.onSensorChanged(createTestEvent(0.5f, 1.0f, 0.0f)); + } + + assertThat(mOnFaceDownCalls).isEqualTo(1); + assertThat(mOnFaceDownExitCalls).isEqualTo(1); + } + + private void advanceTime(Duration duration) { + mCurrentTime += duration.toNanos(); + } + + /** + * Create a test event to replicate an accelerometer sensor event. + * @param x Acceleration along the x dimension. + * @param y Acceleration along the y dimension. + * @param gravity Acceleration along the Z dimension. Relates to + */ + private SensorEvent createTestEvent(float x, float y, float gravity) throws Exception { + final Constructor constructor = + SensorEvent.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + final SensorEvent event = constructor.newInstance(3); + event.sensor = + TestUtils.createSensor(Sensor.TYPE_ACCELEROMETER, Sensor.STRING_TYPE_ACCELEROMETER); + event.values[0] = x; + event.values[1] = y; + event.values[2] = gravity; + event.timestamp = mCurrentTime; + return event; + } + + private void onFlip(boolean isFaceDown) { + if (isFaceDown) { + mOnFaceDownCalls++; + } else { + mOnFaceDownExitCalls++; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/power/NotifierTest.java b/services/tests/servicestests/src/com/android/server/power/NotifierTest.java index 1c2d8e80ea99e..5012ca9ab420e 100644 --- a/services/tests/servicestests/src/com/android/server/power/NotifierTest.java +++ b/services/tests/servicestests/src/com/android/server/power/NotifierTest.java @@ -209,7 +209,8 @@ public class NotifierTest { private final PowerManagerService.Injector mInjector = new PowerManagerService.Injector() { @Override Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats, - SuspendBlocker suspendBlocker, WindowManagerPolicy policy) { + SuspendBlocker suspendBlocker, WindowManagerPolicy policy, + FaceDownDetector faceDownDetector) { return mNotifierMock; } @@ -296,6 +297,7 @@ public class NotifierTest { IBatteryStats.Stub.asInterface(ServiceManager.getService( BatteryStats.SERVICE_NAME)), mInjector.createSuspendBlocker(mService, "testBlocker"), + null, null); } } diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index 592eea1cd55a1..6b95cfc934ac1 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -211,7 +211,8 @@ public class PowerManagerServiceTest { mService = new PowerManagerService(mContextSpy, new Injector() { @Override Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats, - SuspendBlocker suspendBlocker, WindowManagerPolicy policy) { + SuspendBlocker suspendBlocker, WindowManagerPolicy policy, + FaceDownDetector faceDownDetector) { return mNotifierMock; }