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

Commit 5c62143b authored by bquezada's avatar bquezada Committed by Brian Quezada
Browse files

Add handling for disabling flip to screen off feature.

Test: atest FaceDownDetectorTest & tested with local device.

Bug: 164517126
Change-Id: Ic607e58546fbba75c5874fcedf59bde4447c9dec
parent 2d60140e
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();
    }
}