diff options
5 files changed, 449 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingController.java b/services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingController.java new file mode 100644 index 000000000000..f42d60dd417b --- /dev/null +++ b/services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingController.java @@ -0,0 +1,77 @@ +/* + * 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(); + } + } + }); + } +} diff --git a/services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingIssueLogger.java b/services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingIssueLogger.java new file mode 100644 index 000000000000..937039d838e5 --- /dev/null +++ b/services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingIssueLogger.java @@ -0,0 +1,94 @@ +/* + * 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; + } + } +} diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index f8c17550caba..786161c07247 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -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); + } } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingControllerTests.java new file mode 100644 index 000000000000..0ddce72e950c --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingControllerTests.java @@ -0,0 +1,118 @@ +/* + * 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(); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingIssueLoggerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingIssueLoggerTests.java new file mode 100644 index 000000000000..f76a9cdbb894 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingIssueLoggerTests.java @@ -0,0 +1,146 @@ +/* + * 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()); + } +} |