| /* |
| * 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<DeviceStateRotationLockSettingsListener> 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<SettableDeviceState> 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. |
| * |
| * <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 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<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 = 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 |
| + '}'; |
| } |
| } |
| } |