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

Commit 6b6ce463 authored by Shivangi Dubey's avatar Shivangi Dubey Committed by Android (Google) Code Review
Browse files

Merge "Add logging for DeviceStateAutoRotateSettingIssueLogger atom" into main

parents 99c2aa8a 86e6fba2
Loading
Loading
Loading
Loading
+77 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm;

import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
import android.provider.Settings;

import com.android.window.flags.Flags;

/**
 * Syncs ACCELEROMETER_ROTATION and DEVICE_STATE_ROTATION_LOCK setting to consistent values.
 * <ul>
 * <li>On device state change: Reads value of DEVICE_STATE_ROTATION_LOCK for new device state and
 * writes into ACCELEROMETER_ROTATION.</li>
 * <li>On ACCELEROMETER_ROTATION setting change: Write updated ACCELEROMETER_ROTATION value into
 * DEVICE_STATE_ROTATION_LOCK setting for current device state.</li>
 * <li>On DEVICE_STATE_ROTATION_LOCK setting change: If the key for the changed value matches
 * current device state, write updated auto rotate value to ACCELEROMETER_ROTATION.</li>
 * </ul>
 *
 * @see Settings.System#ACCELEROMETER_ROTATION
 * @see Settings.Secure#DEVICE_STATE_ROTATION_LOCK
 */

public class DeviceStateAutoRotateSettingController {
    private final DeviceStateAutoRotateSettingIssueLogger mDeviceStateAutoRotateSettingIssueLogger;
    private final Context mContext;
    private final Handler mHandler;

    public DeviceStateAutoRotateSettingController(Context context,
            DeviceStateAutoRotateSettingIssueLogger deviceStateAutoRotateSettingIssueLogger,
            Handler handler) {
        // TODO(b/350946537) Refactor implementation
        mDeviceStateAutoRotateSettingIssueLogger = deviceStateAutoRotateSettingIssueLogger;
        mContext = context;
        mHandler = handler;
        registerDeviceStateAutoRotateSettingObserver();
    }

    /** Notify controller device state has changed */
    public void onDeviceStateChange(DeviceStateController.DeviceState deviceState) {
        if (Flags.enableDeviceStateAutoRotateSettingLogging()) {
            mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange();
        }
    }

    private void registerDeviceStateAutoRotateSettingObserver() {
        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(Settings.Secure.DEVICE_STATE_ROTATION_LOCK),
                false,
                new ContentObserver(mHandler) {
                    @Override
                    public void onChange(boolean selfChange) {
                        if (Flags.enableDeviceStateAutoRotateSettingLogging()) {
                            mDeviceStateAutoRotateSettingIssueLogger
                                    .onDeviceStateAutoRotateSettingChange();
                        }
                    }
                });
    }
}
+94 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm;

import static android.util.MathUtils.abs;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;

import java.util.function.LongSupplier;

/**
 * Logs potential race conditions that lead to incorrect auto-rotate setting.
 *
 * Before go/auto-rotate-refactor, there is a race condition that happen during device state
 * changes, as a result, incorrect auto-rotate setting are written for a device state in
 * DEVICE_STATE_ROTATION_LOCK. Realistically, users shouldn’t be able to change
 * DEVICE_STATE_ROTATION_LOCK while the device folds/unfolds.
 *
 * This class monitors the time between a device state change and a subsequent change to the device
 * state based auto-rotate setting.  If the duration is less than a threshold
 * (DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD), a potential issue is logged. The logging of
 * the atom is not expected to occur often, realistically estimated once a month on few devices.
 * But the number could be bigger, as that's what this metric is set to reveal.
 *
 * @see #DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD_MILLIS
 */
public class DeviceStateAutoRotateSettingIssueLogger {
    @VisibleForTesting
    static final long DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD_MILLIS = 1500;
    private static final long TIME_NOT_SET = -1;

    private final LongSupplier mElapsedTimeMillisSupplier;

    @ElapsedRealtimeLong
    private long mLastDeviceStateChangeTime = TIME_NOT_SET;
    @ElapsedRealtimeLong
    private long mLastDeviceStateAutoRotateSettingChangeTime = TIME_NOT_SET;

    public DeviceStateAutoRotateSettingIssueLogger(
            @NonNull LongSupplier elapsedTimeMillisSupplier) {
        mElapsedTimeMillisSupplier = elapsedTimeMillisSupplier;
    }

    /** Notify logger that device state has changed. */
    public void onDeviceStateChange() {
        mLastDeviceStateChangeTime = mElapsedTimeMillisSupplier.getAsLong();
        onStateChange();
    }

    /** Notify logger that device state based auto rotate setting has changed. */
    public void onDeviceStateAutoRotateSettingChange() {
        mLastDeviceStateAutoRotateSettingChangeTime = mElapsedTimeMillisSupplier.getAsLong();
        onStateChange();
    }

    private void onStateChange() {
        // Only move forward if both of the events have occurred already
        if (mLastDeviceStateChangeTime != TIME_NOT_SET
                && mLastDeviceStateAutoRotateSettingChangeTime != TIME_NOT_SET) {
            final long duration =
                    mLastDeviceStateAutoRotateSettingChangeTime - mLastDeviceStateChangeTime;
            boolean isDeviceStateChangeFirst = duration > 0;

            if (abs(duration)
                    < DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD_MILLIS) {
                FrameworkStatsLog.write(
                        FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED,
                        (int) abs(duration),
                        isDeviceStateChangeFirst);
            }

            mLastDeviceStateAutoRotateSettingChangeTime = TIME_NOT_SET;
            mLastDeviceStateChangeTime = TIME_NOT_SET;
        }
    }
}
+14 −0
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@ import com.android.internal.protolog.ProtoLog;
import com.android.server.UiThread;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.window.flags.Flags;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -108,6 +109,8 @@ public class DisplayRotation {
    private final Object mLock;
    @Nullable
    private final DisplayRotationImmersiveAppCompatPolicy mCompatPolicyForImmersiveApps;
    @Nullable
    private DeviceStateAutoRotateSettingController mDeviceStateAutoRotateSettingController;

    public final boolean isDefaultDisplay;
    private final boolean mSupportAutoRotation;
@@ -298,6 +301,14 @@ public class DisplayRotation {
        } else {
            mFoldController = null;
        }

        if (mFoldController != null && (Flags.enableDeviceStateAutoRotateSettingLogging()
                || Flags.enableDeviceStateAutoRotateSettingRefactor())) {
            mDeviceStateAutoRotateSettingController =
                    new DeviceStateAutoRotateSettingController(mContext,
                            new DeviceStateAutoRotateSettingIssueLogger(
                                    SystemClock::elapsedRealtime), mService.mH);
        }
    }

    private static boolean isFoldable(Context context) {
@@ -1667,6 +1678,9 @@ public class DisplayRotation {
        if (mFoldController != null) {
            synchronized (mLock) {
                mFoldController.foldStateChanged(deviceState);
                if (mDeviceStateAutoRotateSettingController != null) {
                    mDeviceStateAutoRotateSettingController.onDeviceStateChange(deviceState);
                }
            }
        }
    }
+118 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm;

import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;

import androidx.test.filters.SmallTest;

import com.android.server.testutils.OffsettableClock;
import com.android.server.testutils.TestHandler;
import com.android.window.flags.Flags;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/**
 * Test class for {@link DeviceStateAutoRotateSettingController}.
 *
 * <p>Build/Install/Run: atest WmTests:DeviceStateAutoRotateSettingControllerTests
 */
@SmallTest
@Presubmit
public class DeviceStateAutoRotateSettingControllerTests {
    private static final OffsettableClock sClock = new OffsettableClock.Stopped();

    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    private DeviceStateAutoRotateSettingController mDeviceStateAutoRotateSettingController;
    @Mock
    private DeviceStateAutoRotateSettingIssueLogger mMockLogger;
    @Mock
    private Context mMockContext;
    @Mock
    private ContentResolver mMockContentResolver;
    @Captor
    private ArgumentCaptor<ContentObserver> mContentObserverCaptor;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver);
        mDeviceStateAutoRotateSettingController = new DeviceStateAutoRotateSettingController(
                mMockContext, mMockLogger, new TestHandler(null, sClock));
        verify(mMockContentResolver)
                .registerContentObserver(
                        eq(Settings.Secure.getUriFor(Settings.Secure.DEVICE_STATE_ROTATION_LOCK)),
                        anyBoolean(),
                        mContentObserverCaptor.capture());
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_LOGGING)
    public void loggingFlagEnabled_onDeviceStateChanged_loggerNotified() {
        mDeviceStateAutoRotateSettingController.onDeviceStateChange(
                DeviceStateController.DeviceState.FOLDED);

        verify(mMockLogger, times(1)).onDeviceStateChange();
    }

    @Test
    @DisableFlags(Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_LOGGING)
    public void loggingFlagDisabled_onDeviceStateChanged_loggerNotNotified() {
        mDeviceStateAutoRotateSettingController.onDeviceStateChange(
                DeviceStateController.DeviceState.FOLDED);

        verify(mMockLogger, never()).onDeviceStateChange();
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_LOGGING)
    public void loggingFlagEnabled_settingChanged_loggerNotified() {
        mContentObserverCaptor.getValue().onChange(false);

        verify(mMockLogger, times(1)).onDeviceStateAutoRotateSettingChange();
    }

    @Test
    @DisableFlags(Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_LOGGING)
    public void loggingFlagDisabled_settingChanged_loggerNotNotified() {
        mContentObserverCaptor.getValue().onChange(false);

        verify(mMockLogger, never()).onDeviceStateAutoRotateSettingChange();
    }
}
+146 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.server.wm.DeviceStateAutoRotateSettingIssueLogger.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD_MILLIS;

import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;

import android.platform.test.annotations.Presubmit;

import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;

import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.wm.utils.CurrentTimeMillisSupplierFake;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockitoAnnotations;

/**
 * Test class for {@link DeviceStateAutoRotateSettingIssueLogger}.
 *
 * <p>Build/Install/Run: atest WmTests:DeviceStateAutoRotateSettingIssueLoggerTests
 */
@SmallTest
@Presubmit
public class DeviceStateAutoRotateSettingIssueLoggerTests {
    private static final int DELAY = 500;

    private DeviceStateAutoRotateSettingIssueLogger mDeviceStateAutoRotateSettingIssueLogger;
    private StaticMockitoSession mStaticMockitoSession;
    @NonNull
    private CurrentTimeMillisSupplierFake mTestTimeSupplier;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mStaticMockitoSession = mockitoSession().mockStatic(
                FrameworkStatsLog.class).startMocking();
        mTestTimeSupplier = new CurrentTimeMillisSupplierFake();
        mDeviceStateAutoRotateSettingIssueLogger =
                new DeviceStateAutoRotateSettingIssueLogger(mTestTimeSupplier);
    }

    @After
    public void teardown() {
        mStaticMockitoSession.finishMocking();
    }

    @Test
    public void onStateChange_deviceStateChangedFirst_isDeviceStateFirstTrue() {
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange();
        mTestTimeSupplier.delay(DELAY);
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange();

        verify(() ->
                FrameworkStatsLog.write(
                        eq(FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED),
                        anyInt(),
                        eq(true)));
    }

    @Test
    public void onStateChange_autoRotateSettingChangedFirst_isDeviceStateFirstFalse() {
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange();
        mTestTimeSupplier.delay(DELAY);
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange();

        verify(() ->
                FrameworkStatsLog.write(
                        eq(FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED),
                        anyInt(),
                        eq(false)));
    }

    @Test
    public void onStateChange_deviceStateDidNotChange_doNotReport() {
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange();

        verify(() ->
                FrameworkStatsLog.write(
                        eq(FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED),
                        anyInt(),
                        anyBoolean()), never());
    }

    @Test
    public void onStateChange_autoRotateSettingDidNotChange_doNotReport() {
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange();

        verify(() ->
                FrameworkStatsLog.write(
                        eq(FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED),
                        anyInt(),
                        anyBoolean()), never());
    }

    @Test
    public void onStateChange_issueOccurred_correctDurationReported() {
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange();
        mTestTimeSupplier.delay(DELAY);
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange();

        verify(() ->
                FrameworkStatsLog.write(
                        eq(FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED),
                        eq(DELAY),
                        anyBoolean()));
    }

    @Test
    public void onStateChange_durationLongerThanThreshold_doNotReport() {
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange();
        mTestTimeSupplier.delay(
                DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD_MILLIS + DELAY);
        mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange();

        verify(() ->
                FrameworkStatsLog.write(
                        eq(FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED),
                        anyInt(),
                        anyBoolean()), never());
    }
}