Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6d33ff7f authored by Brian Quezada's avatar Brian Quezada Committed by Android (Google) Code Review
Browse files

Merge "Add handling for disabling flip to screen off feature." into sc-dev

parents 41b87460 5c62143b
Loading
Loading
Loading
Loading
+44 −27
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ 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;
@@ -66,7 +67,7 @@ public class FaceDownDetector implements SensorEventListener {
    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";
    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;
@@ -139,6 +140,7 @@ public class FaceDownDetector implements SensorEventListener {
            new ExponentialMovingAverage(MOVING_AVERAGE_WEIGHT);

    private boolean mFaceDown = false;
    private boolean mInteractive = false;
    private boolean mActive = false;

    private float mPrevAcceleration = 0;
@@ -149,62 +151,64 @@ public class FaceDownDetector implements SensorEventListener {

    private final Handler mHandler;
    private final Runnable mUserActivityRunnable;
    private 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);
                checkAndUpdateActiveState(false);
                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();
        checkAndUpdateActiveState(true);
        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(new ScreenStateReceiver(), intentFilter);
        context.registerReceiver(mScreenReceiver, 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) {
    private void updateActiveState() {
        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;
        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);
                if (mPreviousResultType == SCREEN_OFF_RESULT) {
                    logScreenOff();
                }
            } else {
                mSensorManager.unregisterListener(this);
                mFaceDown = false;
                mOnFlip.accept(false);
            } else {
                if (mPreviousResultType == SCREEN_OFF_RESULT) {
                    logScreenOff();
                }
                mSensorManager.registerListener(
                        this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
            }
            mActive = shouldBeActive;
            if (DEBUG) Slog.d(TAG, "Update active - " + shouldBeActive);
        }
    }

@@ -389,6 +393,7 @@ public class FaceDownDetector implements SensorEventListener {
                case KEY_TIME_THRESHOLD_MILLIS:
                case KEY_FEATURE_ENABLED:
                    readValuesFromDeviceConfig();
                    updateActiveState();
                    return;
                default:
                    Slog.i(TAG, "Ignoring change on " + key);
@@ -401,8 +406,18 @@ public class FaceDownDetector implements SensorEventListener {
        mZAccelerationThreshold = getZAccelerationThreshold();
        mZAccelerationThresholdLenient = mZAccelerationThreshold + 1.0f;
        mTimeThreshold = getTimeThreshold();
        mIsEnabled = isEnabled();
        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
@@ -423,9 +438,11 @@ public class FaceDownDetector implements SensorEventListener {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
                checkAndUpdateActiveState(false);
                mInteractive = false;
                updateActiveState();
            } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
                checkAndUpdateActiveState(true);
                mInteractive = true;
                updateActiveState();
            }
        }
    }
+119 −29
Original line number Diff line number Diff line
@@ -16,59 +16,74 @@

package com.android.server.power;

import static android.provider.DeviceConfig.NAMESPACE_ATTENTION_MANAGER_SERVICE;

import static com.android.server.power.FaceDownDetector.KEY_FEATURE_ENABLED;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.doReturn;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorManager;
import android.os.PowerManager;
import android.provider.DeviceConfig;
import android.testing.TestableContext;

import androidx.test.platform.app.InstrumentationRegistry;

import com.android.server.display.TestUtils;
import com.android.server.testables.TestableDeviceConfig;

import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class FaceDownDetectorTest {
    @ClassRule
    public static final TestableContext sContext = new TestableContext(
            InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
    @Rule
    public TestableDeviceConfig.TestableDeviceConfigRule
            mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule();

    private final FaceDownDetector mFaceDownDetector =
            new FaceDownDetector(this::onFlip);
    private final FaceDownDetector mFaceDownDetector = new FaceDownDetector(this::onFlip);

    @Mock private SensorManager mSensorManager;
    @Mock private PowerManager mPowerManager;

    private long mCurrentTime;
    private int mOnFaceDownCalls = 0;
    private int mOnFaceDownExitCalls = 0;
    private Duration mCurrentTime;
    private int mOnFaceDownCalls;
    private int mOnFaceDownExitCalls;

    @Before
    public void setup() {
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
        sContext.addMockSystemService(SensorManager.class, mSensorManager);
        mCurrentTime = 0;
        sContext.addMockSystemService(PowerManager.class, mPowerManager);
        doReturn(true).when(mPowerManager).isInteractive();
        DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE,
                KEY_FEATURE_ENABLED, "true", false);
        mCurrentTime = Duration.ZERO;
        mOnFaceDownCalls = 0;
        mOnFaceDownExitCalls = 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));
        }
        triggerFaceDown();

        assertThat(mOnFaceDownCalls).isEqualTo(1);
        assertThat(mOnFaceDownExitCalls).isEqualTo(0);
@@ -111,15 +126,7 @@ public class FaceDownDetectorTest {
    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));
        }
        triggerFaceDown();

        // Phone flips
        for (int i = 0; i < 10; i++) {
@@ -131,8 +138,71 @@ public class FaceDownDetectorTest {
        assertThat(mOnFaceDownExitCalls).isEqualTo(1);
    }

    @Test
    public void notInteractive_doesNotTriggerFaceDown() throws Exception {
        doReturn(false).when(mPowerManager).isInteractive();
        mFaceDownDetector.systemReady(sContext);

        triggerFaceDown();

        assertThat(mOnFaceDownCalls).isEqualTo(0);
        assertThat(mOnFaceDownExitCalls).isEqualTo(0);
    }

    @Test
    public void afterDisablingFeature_doesNotTriggerFaceDown() throws Exception {
        mFaceDownDetector.systemReady(sContext);
        setEnabled(false);

        triggerFaceDown();

        assertThat(mOnFaceDownCalls).isEqualTo(0);
    }

    @Test
    public void afterReenablingWhileNonInteractive_doesNotTriggerFaceDown() throws Exception {
        mFaceDownDetector.systemReady(sContext);
        setEnabled(false);

        doReturn(false).when(mPowerManager).isInteractive();
        setEnabled(true);

        triggerFaceDown();

        assertThat(mOnFaceDownCalls).isEqualTo(0);
    }

    @Test
    public void afterReenablingWhileInteractive_doesTriggerFaceDown() throws Exception {
        mFaceDownDetector.systemReady(sContext);
        setEnabled(false);

        setEnabled(true);

        triggerFaceDown();

        assertThat(mOnFaceDownCalls).isEqualTo(1);
    }

    private void triggerFaceDown() throws Exception {
        // 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));
        }
    }

    private void setEnabled(Boolean enabled) throws Exception {
        DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE,
                KEY_FEATURE_ENABLED, enabled.toString(), false);
        waitForListenerToHandle();
    }

    private void advanceTime(Duration duration) {
        mCurrentTime += duration.toNanos();
        mCurrentTime = mCurrentTime.plus(duration);
    }

    /**
@@ -146,12 +216,11 @@ public class FaceDownDetectorTest {
                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.sensor = createSensor(Sensor.TYPE_ACCELEROMETER, Sensor.STRING_TYPE_ACCELEROMETER);
        event.values[0] = x;
        event.values[1] = y;
        event.values[2] = gravity;
        event.timestamp = mCurrentTime;
        event.timestamp = mCurrentTime.toNanos();
        return event;
    }

@@ -162,4 +231,25 @@ public class FaceDownDetectorTest {
            mOnFaceDownExitCalls++;
        }
    }

    private Sensor createSensor(int type, String strType) throws Exception {
        Constructor<Sensor> constr = Sensor.class.getDeclaredConstructor();
        constr.setAccessible(true);
        Sensor sensor = constr.newInstance();
        Method setter = Sensor.class.getDeclaredMethod("setType", Integer.TYPE);
        setter.setAccessible(true);
        setter.invoke(sensor, type);
        if (strType != null) {
            Field f = sensor.getClass().getDeclaredField("mStringType");
            f.setAccessible(true);
            f.set(sensor, strType);
        }
        return sensor;
    }

    private void waitForListenerToHandle() throws Exception {
        final CountDownLatch latch = new CountDownLatch(1);
        sContext.getMainExecutor().execute(latch::countDown);
        assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
    }
}