blob: ea4ac2c928ce28b8d1486d9ae9a02320cccc23a9 [file] [log] [blame]
/*
* 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
+ '}';
}
}
}