/* * 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.IndentingPrintWriter; 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.Arrays; 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 mListeners = new HashSet<>(); private final SecureSettings mSecureSettings; private final PosturesHelper mPosturesHelper; private String[] mPostureRotationLockDefaults; private SparseIntArray mPostureRotationLockSettings; private SparseIntArray mPostureDefaultRotationLockSettings; private SparseIntArray mPostureRotationLockFallbackSettings; private List mSettableDeviceStates; @VisibleForTesting DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings) { mSecureSettings = secureSettings; mPosturesHelper = new PosturesHelper(context); mPostureRotationLockDefaults = 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) { int posture = mPosturesHelper.deviceStateToPosture(deviceState); if (mPostureRotationLockFallbackSettings.indexOfKey(posture) >= 0) { // The setting for this device posture is IGNORED, and has a fallback posture. // The setting for that fallback posture should be the changed in this case. posture = mPostureRotationLockFallbackSettings.get(posture); } mPostureRotationLockSettings.put( posture, rotationLocked ? DEVICE_STATE_ROTATION_LOCK_LOCKED : DEVICE_STATE_ROTATION_LOCK_UNLOCKED); persistSettings(); } /** * Returns the {@link Settings.Secure.DeviceStateRotationLockSetting} for the given device * state. * *

If the setting for this device state is {@link DEVICE_STATE_ROTATION_LOCK_IGNORED}, it * will return the setting for the fallback device state. * *

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 devicePosture = mPosturesHelper.deviceStateToPosture(deviceState); int rotationLockSetting = mPostureRotationLockSettings.get( devicePosture, /* valueIfKeyNotFound= */ DEVICE_STATE_ROTATION_LOCK_IGNORED); if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) { rotationLockSetting = getFallbackRotationLockSetting(devicePosture); } return rotationLockSetting; } private int getFallbackRotationLockSetting(int devicePosture) { int indexOfFallback = mPostureRotationLockFallbackSettings.indexOfKey(devicePosture); if (indexOfFallback < 0) { Log.w(TAG, "Setting is ignored, but no fallback was specified."); return DEVICE_STATE_ROTATION_LOCK_IGNORED; } int fallbackPosture = mPostureRotationLockFallbackSettings.valueAt(indexOfFallback); return mPostureRotationLockSettings.get(fallbackPosture, /* 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 < mPostureRotationLockSettings.size(); i++) { if (mPostureRotationLockSettings.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 getSettableDeviceStates() { // Returning a copy to make sure that nothing outside can mutate our internal list. return new ArrayList<>(mSettableDeviceStates); } private void initializeInMemoryMap() { String serializedSetting = getPersistedSettingValue(); 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; } mPostureRotationLockSettings = 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++]); boolean isPersistedValueIgnored = value == DEVICE_STATE_ROTATION_LOCK_IGNORED; boolean isDefaultValueIgnored = mPostureDefaultRotationLockSettings.get(key) == DEVICE_STATE_ROTATION_LOCK_IGNORED; if (isPersistedValueIgnored != isDefaultValueIgnored) { Log.w(TAG, "Conflict for ignored device state " + key + ". Falling back on defaults"); fallbackOnDefaults(); return; } mPostureRotationLockSettings.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) { mPostureRotationLockDefaults = resources.getStringArray(R.array.config_perDeviceStateRotationLockDefaults); fallbackOnDefaults(); } private void fallbackOnDefaults() { loadDefaults(); persistSettings(); } private void persistSettings() { if (mPostureRotationLockSettings.size() == 0) { persistSettingIfChanged(/* newSettingValue= */ ""); return; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder .append(mPostureRotationLockSettings.keyAt(0)) .append(SEPARATOR_REGEX) .append(mPostureRotationLockSettings.valueAt(0)); for (int i = 1; i < mPostureRotationLockSettings.size(); i++) { stringBuilder .append(SEPARATOR_REGEX) .append(mPostureRotationLockSettings.keyAt(i)) .append(SEPARATOR_REGEX) .append(mPostureRotationLockSettings.valueAt(i)); } persistSettingIfChanged(stringBuilder.toString()); } private void persistSettingIfChanged(String newSettingValue) { String lastSettingValue = getPersistedSettingValue(); Log.v(TAG, "persistSettingIfChanged: " + "last=" + lastSettingValue + ", " + "new=" + newSettingValue); if (TextUtils.equals(lastSettingValue, newSettingValue)) { return; } mSecureSettings.putStringForUser( Settings.Secure.DEVICE_STATE_ROTATION_LOCK, /* value= */ newSettingValue, UserHandle.USER_CURRENT); } private String getPersistedSettingValue() { return mSecureSettings.getStringForUser( Settings.Secure.DEVICE_STATE_ROTATION_LOCK, UserHandle.USER_CURRENT); } private void loadDefaults() { mSettableDeviceStates = new ArrayList<>(mPostureRotationLockDefaults.length); mPostureDefaultRotationLockSettings = new SparseIntArray( mPostureRotationLockDefaults.length); mPostureRotationLockSettings = new SparseIntArray(mPostureRotationLockDefaults.length); mPostureRotationLockFallbackSettings = new SparseIntArray(1); for (String entry : mPostureRotationLockDefaults) { String[] values = entry.split(SEPARATOR_REGEX); try { int posture = Integer.parseInt(values[0]); int rotationLockSetting = Integer.parseInt(values[1]); if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) { if (values.length == 3) { int fallbackPosture = Integer.parseInt(values[2]); mPostureRotationLockFallbackSettings.put(posture, fallbackPosture); } 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; Integer deviceState = mPosturesHelper.postureToDeviceState(posture); if (deviceState != null) { mSettableDeviceStates.add(new SettableDeviceState(deviceState, isSettable)); } else { Log.wtf(TAG, "No matching device state for posture: " + posture); } mPostureRotationLockSettings.put(posture, rotationLockSetting); mPostureDefaultRotationLockSettings.put(posture, rotationLockSetting); } catch (NumberFormatException e) { Log.wtf(TAG, "Error parsing settings entry. Entry was: " + entry, e); return; } } } /** Dumps internal state. */ public void dump(IndentingPrintWriter pw) { pw.println("DeviceStateRotationLockSettingsManager"); pw.increaseIndent(); pw.println("mPostureRotationLockDefaults: " + Arrays.toString(mPostureRotationLockDefaults)); pw.println("mPostureDefaultRotationLockSettings: " + mPostureDefaultRotationLockSettings); pw.println("mDeviceStateRotationLockSettings: " + mPostureRotationLockSettings); pw.println("mPostureRotationLockFallbackSettings: " + mPostureRotationLockFallbackSettings); pw.println("mSettableDeviceStates: " + mSettableDeviceStates); pw.decreaseIndent(); } /** * 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); } @Override public String toString() { return "SettableDeviceState{" + "mDeviceState=" + mDeviceState + ", mIsSettable=" + mIsSettable + '}'; } } }