Move code in settingslib/devicestate into its own module
Code in SettingsProvider will need to use DeviceStateRotationLockSettingsManager,
but shouldn't have to pull in the entire SettingsLib just for one class.
Bug: 196933725
Test: atest DeviceStateRotationLockSettingsManagerTest
Test: build Android
Change-Id: I93679700c1638396c5930386c82fa23f1f3ad6a0
diff --git a/packages/SettingsLib/DeviceStateRotationLock/Android.bp b/packages/SettingsLib/DeviceStateRotationLock/Android.bp
new file mode 100644
index 0000000..c642bd1
--- /dev/null
+++ b/packages/SettingsLib/DeviceStateRotationLock/Android.bp
@@ -0,0 +1,16 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+ name: "SettingsLibDeviceStateRotationLock",
+
+ srcs: ["src/**/*.java"],
+
+ min_sdk_version: "21",
+}
diff --git a/packages/SettingsLib/DeviceStateRotationLock/AndroidManifest.xml b/packages/SettingsLib/DeviceStateRotationLock/AndroidManifest.xml
new file mode 100644
index 0000000..bce6599
--- /dev/null
+++ b/packages/SettingsLib/DeviceStateRotationLock/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.devicestate">
+
+</manifest>
diff --git a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/AndroidSecureSettings.java b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/AndroidSecureSettings.java
new file mode 100644
index 0000000..8aee576
--- /dev/null
+++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/AndroidSecureSettings.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2022 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.settingslib.devicestate;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.provider.Settings;
+
+/**
+ * Implementation of {@link SecureSettings} that uses Android's {@link Settings.Secure}
+ * implementation.
+ */
+class AndroidSecureSettings implements SecureSettings {
+
+ private final ContentResolver mContentResolver;
+
+ AndroidSecureSettings(ContentResolver contentResolver) {
+ mContentResolver = contentResolver;
+ }
+
+ @Override
+ public void putStringForUser(String name, String value, int userHandle) {
+ Settings.Secure.putStringForUser(mContentResolver, name, value, userHandle);
+ }
+
+ @Override
+ public String getStringForUser(String name, int userHandle) {
+ return Settings.Secure.getStringForUser(mContentResolver, name, userHandle);
+ }
+
+ @Override
+ public void registerContentObserver(String name, boolean notifyForDescendants,
+ ContentObserver observer, int userHandle) {
+ mContentResolver.registerContentObserver(
+ Settings.Secure.getUriFor(name),
+ notifyForDescendants,
+ observer,
+ userHandle);
+ }
+}
diff --git a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java
new file mode 100644
index 0000000..4ed7e19
--- /dev/null
+++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2022 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.settingslib.devicestate;
+
+import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED;
+import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED;
+import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Manages device-state based rotation lock settings. Handles reading, writing, and listening for
+ * changes.
+ */
+public final class DeviceStateRotationLockSettingsManager {
+
+ private static final String TAG = "DSRotLockSettingsMngr";
+ private static final String SEPARATOR_REGEX = ":";
+
+ private static DeviceStateRotationLockSettingsManager sSingleton;
+
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+ private final Set<DeviceStateRotationLockSettingsListener> mListeners = new HashSet<>();
+ private final SecureSettings mSecureSettings;
+ private String[] mDeviceStateRotationLockDefaults;
+ private SparseIntArray mDeviceStateRotationLockSettings;
+ private SparseIntArray mDeviceStateRotationLockFallbackSettings;
+ private String mLastSettingValue;
+ private List<SettableDeviceState> mSettableDeviceStates;
+
+ @VisibleForTesting
+ DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings) {
+ this.mSecureSettings = secureSettings;
+ mDeviceStateRotationLockDefaults =
+ context.getResources()
+ .getStringArray(R.array.config_perDeviceStateRotationLockDefaults);
+ loadDefaults();
+ initializeInMemoryMap();
+ listenForSettingsChange();
+ }
+
+ /** Returns a singleton instance of this class */
+ public static synchronized DeviceStateRotationLockSettingsManager getInstance(Context context) {
+ if (sSingleton == null) {
+ Context applicationContext = context.getApplicationContext();
+ ContentResolver contentResolver = applicationContext.getContentResolver();
+ SecureSettings secureSettings = new AndroidSecureSettings(contentResolver);
+ sSingleton =
+ new DeviceStateRotationLockSettingsManager(applicationContext, secureSettings);
+ }
+ return sSingleton;
+ }
+
+ /** Resets the singleton instance of this class. Only used for testing. */
+ @VisibleForTesting
+ public static synchronized void resetInstance() {
+ sSingleton = null;
+ }
+
+ /** Returns true if device-state based rotation lock settings are enabled. */
+ public static boolean isDeviceStateRotationLockEnabled(Context context) {
+ return context.getResources()
+ .getStringArray(R.array.config_perDeviceStateRotationLockDefaults)
+ .length
+ > 0;
+ }
+
+ private void listenForSettingsChange() {
+ mSecureSettings
+ .registerContentObserver(
+ Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+ /* notifyForDescendants= */ false,
+ new ContentObserver(mMainHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onPersistedSettingsChanged();
+ }
+ },
+ UserHandle.USER_CURRENT);
+ }
+
+ /**
+ * Registers a {@link DeviceStateRotationLockSettingsListener} to be notified when the settings
+ * change. Can be called multiple times with different listeners.
+ */
+ public void registerListener(DeviceStateRotationLockSettingsListener runnable) {
+ mListeners.add(runnable);
+ }
+
+ /**
+ * Unregisters a {@link DeviceStateRotationLockSettingsListener}. No-op if the given instance
+ * was never registered.
+ */
+ public void unregisterListener(
+ DeviceStateRotationLockSettingsListener deviceStateRotationLockSettingsListener) {
+ if (!mListeners.remove(deviceStateRotationLockSettingsListener)) {
+ Log.w(TAG, "Attempting to unregister a listener hadn't been registered");
+ }
+ }
+
+ /** Updates the rotation lock setting for a specified device state. */
+ public void updateSetting(int deviceState, boolean rotationLocked) {
+ if (mDeviceStateRotationLockFallbackSettings.indexOfKey(deviceState) >= 0) {
+ // The setting for this device state is IGNORED, and has a fallback device state.
+ // The setting for that fallback device state should be the changed in this case.
+ deviceState = mDeviceStateRotationLockFallbackSettings.get(deviceState);
+ }
+ mDeviceStateRotationLockSettings.put(
+ deviceState,
+ rotationLocked
+ ? DEVICE_STATE_ROTATION_LOCK_LOCKED
+ : DEVICE_STATE_ROTATION_LOCK_UNLOCKED);
+ persistSettings();
+ }
+
+ /**
+ * Returns the {@link Settings.Secure.DeviceStateRotationLockSetting} for the given device
+ * state.
+ *
+ * <p>If the setting for this device state is {@link DEVICE_STATE_ROTATION_LOCK_IGNORED}, it
+ * will return the setting for the fallback device state.
+ *
+ * <p>If no fallback is specified for this device state, it will return {@link
+ * DEVICE_STATE_ROTATION_LOCK_IGNORED}.
+ */
+ @Settings.Secure.DeviceStateRotationLockSetting
+ public int getRotationLockSetting(int deviceState) {
+ int rotationLockSetting = mDeviceStateRotationLockSettings.get(
+ deviceState, /* valueIfKeyNotFound= */ DEVICE_STATE_ROTATION_LOCK_IGNORED);
+ if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) {
+ rotationLockSetting = getFallbackRotationLockSetting(deviceState);
+ }
+ return rotationLockSetting;
+ }
+
+ private int getFallbackRotationLockSetting(int deviceState) {
+ int indexOfFallbackState = mDeviceStateRotationLockFallbackSettings.indexOfKey(deviceState);
+ if (indexOfFallbackState < 0) {
+ Log.w(TAG, "Setting is ignored, but no fallback was specified.");
+ return DEVICE_STATE_ROTATION_LOCK_IGNORED;
+ }
+ int fallbackState = mDeviceStateRotationLockFallbackSettings.valueAt(indexOfFallbackState);
+ return mDeviceStateRotationLockSettings.get(fallbackState,
+ /* valueIfKeyNotFound= */ DEVICE_STATE_ROTATION_LOCK_IGNORED);
+ }
+
+
+ /** Returns true if the rotation is locked for the current device state */
+ public boolean isRotationLocked(int deviceState) {
+ return getRotationLockSetting(deviceState) == DEVICE_STATE_ROTATION_LOCK_LOCKED;
+ }
+
+ /**
+ * Returns true if there is no device state for which the current setting is {@link
+ * DEVICE_STATE_ROTATION_LOCK_UNLOCKED}.
+ */
+ public boolean isRotationLockedForAllStates() {
+ for (int i = 0; i < mDeviceStateRotationLockSettings.size(); i++) {
+ if (mDeviceStateRotationLockSettings.valueAt(i)
+ == DEVICE_STATE_ROTATION_LOCK_UNLOCKED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** Returns a list of device states and their respective auto-rotation setting availability. */
+ public List<SettableDeviceState> getSettableDeviceStates() {
+ // Returning a copy to make sure that nothing outside can mutate our internal list.
+ return new ArrayList<>(mSettableDeviceStates);
+ }
+
+ private void initializeInMemoryMap() {
+ String serializedSetting =
+ mSecureSettings.getStringForUser(
+ Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+ UserHandle.USER_CURRENT);
+ if (TextUtils.isEmpty(serializedSetting)) {
+ // No settings saved, we should load the defaults and persist them.
+ fallbackOnDefaults();
+ return;
+ }
+ String[] values = serializedSetting.split(SEPARATOR_REGEX);
+ if (values.length % 2 != 0) {
+ // Each entry should be a key/value pair, so this is corrupt.
+ Log.wtf(TAG, "Can't deserialize saved settings, falling back on defaults");
+ fallbackOnDefaults();
+ return;
+ }
+ mDeviceStateRotationLockSettings = new SparseIntArray(values.length / 2);
+ int key;
+ int value;
+
+ for (int i = 0; i < values.length - 1; ) {
+ try {
+ key = Integer.parseInt(values[i++]);
+ value = Integer.parseInt(values[i++]);
+ mDeviceStateRotationLockSettings.put(key, value);
+ } catch (NumberFormatException e) {
+ Log.wtf(TAG, "Error deserializing one of the saved settings", e);
+ fallbackOnDefaults();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Resets the state of the class and saved settings back to the default values provided by the
+ * resources config.
+ */
+ @VisibleForTesting
+ public void resetStateForTesting(Resources resources) {
+ mDeviceStateRotationLockDefaults =
+ resources.getStringArray(R.array.config_perDeviceStateRotationLockDefaults);
+ fallbackOnDefaults();
+ }
+
+ private void fallbackOnDefaults() {
+ loadDefaults();
+ persistSettings();
+ }
+
+ private void persistSettings() {
+ if (mDeviceStateRotationLockSettings.size() == 0) {
+ persistSettingIfChanged(/* newSettingValue= */ "");
+ return;
+ }
+
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder
+ .append(mDeviceStateRotationLockSettings.keyAt(0))
+ .append(SEPARATOR_REGEX)
+ .append(mDeviceStateRotationLockSettings.valueAt(0));
+
+ for (int i = 1; i < mDeviceStateRotationLockSettings.size(); i++) {
+ stringBuilder
+ .append(SEPARATOR_REGEX)
+ .append(mDeviceStateRotationLockSettings.keyAt(i))
+ .append(SEPARATOR_REGEX)
+ .append(mDeviceStateRotationLockSettings.valueAt(i));
+ }
+ persistSettingIfChanged(stringBuilder.toString());
+ }
+
+ private void persistSettingIfChanged(String newSettingValue) {
+ if (TextUtils.equals(mLastSettingValue, newSettingValue)) {
+ return;
+ }
+ mLastSettingValue = newSettingValue;
+ mSecureSettings.putStringForUser(
+ Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+ /* value= */ newSettingValue,
+ UserHandle.USER_CURRENT);
+ }
+
+ private void loadDefaults() {
+ mSettableDeviceStates = new ArrayList<>(mDeviceStateRotationLockDefaults.length);
+ mDeviceStateRotationLockSettings = new SparseIntArray(
+ mDeviceStateRotationLockDefaults.length);
+ mDeviceStateRotationLockFallbackSettings = new SparseIntArray(1);
+ for (String entry : mDeviceStateRotationLockDefaults) {
+ String[] values = entry.split(SEPARATOR_REGEX);
+ try {
+ int deviceState = Integer.parseInt(values[0]);
+ int rotationLockSetting = Integer.parseInt(values[1]);
+ if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) {
+ if (values.length == 3) {
+ int fallbackDeviceState = Integer.parseInt(values[2]);
+ mDeviceStateRotationLockFallbackSettings.put(deviceState,
+ fallbackDeviceState);
+ } else {
+ Log.w(TAG,
+ "Rotation lock setting is IGNORED, but values have unexpected "
+ + "size of "
+ + values.length);
+ }
+ }
+ boolean isSettable = rotationLockSetting != DEVICE_STATE_ROTATION_LOCK_IGNORED;
+ mSettableDeviceStates.add(new SettableDeviceState(deviceState, isSettable));
+ mDeviceStateRotationLockSettings.put(deviceState, rotationLockSetting);
+ } catch (NumberFormatException e) {
+ Log.wtf(TAG, "Error parsing settings entry. Entry was: " + entry, e);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Called when the persisted settings have changed, requiring a reinitialization of the
+ * in-memory map.
+ */
+ @VisibleForTesting
+ public void onPersistedSettingsChanged() {
+ initializeInMemoryMap();
+ notifyListeners();
+ }
+
+ private void notifyListeners() {
+ for (DeviceStateRotationLockSettingsListener r : mListeners) {
+ r.onSettingsChanged();
+ }
+ }
+
+ /** Listener for changes in device-state based rotation lock settings */
+ public interface DeviceStateRotationLockSettingsListener {
+ /** Called whenever the settings have changed. */
+ void onSettingsChanged();
+ }
+
+ /** Represents a device state and whether it has an auto-rotation setting. */
+ public static class SettableDeviceState {
+ private final int mDeviceState;
+ private final boolean mIsSettable;
+
+ SettableDeviceState(int deviceState, boolean isSettable) {
+ mDeviceState = deviceState;
+ mIsSettable = isSettable;
+ }
+
+ /** Returns the device state associated with this object. */
+ public int getDeviceState() {
+ return mDeviceState;
+ }
+
+ /** Returns whether there is an auto-rotation setting for this device state. */
+ public boolean isSettable() {
+ return mIsSettable;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SettableDeviceState)) return false;
+ SettableDeviceState that = (SettableDeviceState) o;
+ return mDeviceState == that.mDeviceState && mIsSettable == that.mIsSettable;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDeviceState, mIsSettable);
+ }
+ }
+}
diff --git a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/SecureSettings.java b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/SecureSettings.java
new file mode 100644
index 0000000..1052873
--- /dev/null
+++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/SecureSettings.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 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.settingslib.devicestate;
+
+import android.database.ContentObserver;
+
+/** Minimal wrapper interface around {@link android.provider.Settings.Secure} for easier testing. */
+interface SecureSettings {
+
+ void putStringForUser(String name, String value, int userHandle);
+
+ String getStringForUser(String name, int userHandle);
+
+ void registerContentObserver(String name, boolean notifyForDescendants,
+ ContentObserver settingsObserver, int userHandle);
+}