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);
+}