summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingController.java77
-rw-r--r--services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingIssueLogger.java94
-rw-r--r--services/core/java/com/android/server/wm/DisplayRotation.java14
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingControllerTests.java118
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingIssueLoggerTests.java146
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());
+ }
+}