1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.devicestate; 18 19 import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED; 20 import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED; 21 import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED; 22 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.database.ContentObserver; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.os.UserHandle; 30 import android.provider.Settings; 31 import android.text.TextUtils; 32 import android.util.IndentingPrintWriter; 33 import android.util.Log; 34 import android.util.SparseIntArray; 35 36 import com.android.internal.R; 37 import com.android.internal.annotations.VisibleForTesting; 38 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Objects; 44 import java.util.Set; 45 46 /** 47 * Manages device-state based rotation lock settings. Handles reading, writing, and listening for 48 * changes. 49 */ 50 public final class DeviceStateRotationLockSettingsManager { 51 52 private static final String TAG = "DSRotLockSettingsMngr"; 53 private static final String SEPARATOR_REGEX = ":"; 54 55 private static DeviceStateRotationLockSettingsManager sSingleton; 56 57 private final Handler mMainHandler = new Handler(Looper.getMainLooper()); 58 private final Set<DeviceStateRotationLockSettingsListener> mListeners = new HashSet<>(); 59 private final SecureSettings mSecureSettings; 60 private final PosturesHelper mPosturesHelper; 61 private String[] mPostureRotationLockDefaults; 62 private SparseIntArray mPostureRotationLockSettings; 63 private SparseIntArray mPostureDefaultRotationLockSettings; 64 private SparseIntArray mPostureRotationLockFallbackSettings; 65 private List<SettableDeviceState> mSettableDeviceStates; 66 67 @VisibleForTesting DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings)68 DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings) { 69 mSecureSettings = secureSettings; 70 mPosturesHelper = new PosturesHelper(context); 71 mPostureRotationLockDefaults = 72 context.getResources() 73 .getStringArray(R.array.config_perDeviceStateRotationLockDefaults); 74 loadDefaults(); 75 initializeInMemoryMap(); 76 listenForSettingsChange(); 77 } 78 79 /** Returns a singleton instance of this class */ getInstance(Context context)80 public static synchronized DeviceStateRotationLockSettingsManager getInstance(Context context) { 81 if (sSingleton == null) { 82 Context applicationContext = context.getApplicationContext(); 83 ContentResolver contentResolver = applicationContext.getContentResolver(); 84 SecureSettings secureSettings = new AndroidSecureSettings(contentResolver); 85 sSingleton = 86 new DeviceStateRotationLockSettingsManager(applicationContext, secureSettings); 87 } 88 return sSingleton; 89 } 90 91 /** Resets the singleton instance of this class. Only used for testing. */ 92 @VisibleForTesting resetInstance()93 public static synchronized void resetInstance() { 94 sSingleton = null; 95 } 96 97 /** Returns true if device-state based rotation lock settings are enabled. */ isDeviceStateRotationLockEnabled(Context context)98 public static boolean isDeviceStateRotationLockEnabled(Context context) { 99 return context.getResources() 100 .getStringArray(R.array.config_perDeviceStateRotationLockDefaults).length > 0; 101 } 102 listenForSettingsChange()103 private void listenForSettingsChange() { 104 mSecureSettings 105 .registerContentObserver( 106 Settings.Secure.DEVICE_STATE_ROTATION_LOCK, 107 /* notifyForDescendants= */ false, 108 new ContentObserver(mMainHandler) { 109 @Override 110 public void onChange(boolean selfChange) { 111 onPersistedSettingsChanged(); 112 } 113 }, 114 UserHandle.USER_CURRENT); 115 } 116 117 /** 118 * Registers a {@link DeviceStateRotationLockSettingsListener} to be notified when the settings 119 * change. Can be called multiple times with different listeners. 120 */ registerListener(DeviceStateRotationLockSettingsListener runnable)121 public void registerListener(DeviceStateRotationLockSettingsListener runnable) { 122 mListeners.add(runnable); 123 } 124 125 /** 126 * Unregisters a {@link DeviceStateRotationLockSettingsListener}. No-op if the given instance 127 * was never registered. 128 */ unregisterListener( DeviceStateRotationLockSettingsListener deviceStateRotationLockSettingsListener)129 public void unregisterListener( 130 DeviceStateRotationLockSettingsListener deviceStateRotationLockSettingsListener) { 131 if (!mListeners.remove(deviceStateRotationLockSettingsListener)) { 132 Log.w(TAG, "Attempting to unregister a listener hadn't been registered"); 133 } 134 } 135 136 /** Updates the rotation lock setting for a specified device state. */ updateSetting(int deviceState, boolean rotationLocked)137 public void updateSetting(int deviceState, boolean rotationLocked) { 138 int posture = mPosturesHelper.deviceStateToPosture(deviceState); 139 if (mPostureRotationLockFallbackSettings.indexOfKey(posture) >= 0) { 140 // The setting for this device posture is IGNORED, and has a fallback posture. 141 // The setting for that fallback posture should be the changed in this case. 142 posture = mPostureRotationLockFallbackSettings.get(posture); 143 } 144 mPostureRotationLockSettings.put( 145 posture, 146 rotationLocked 147 ? DEVICE_STATE_ROTATION_LOCK_LOCKED 148 : DEVICE_STATE_ROTATION_LOCK_UNLOCKED); 149 persistSettings(); 150 } 151 152 /** 153 * Returns the {@link Settings.Secure.DeviceStateRotationLockSetting} for the given device 154 * state. 155 * 156 * <p>If the setting for this device state is {@link DEVICE_STATE_ROTATION_LOCK_IGNORED}, it 157 * will return the setting for the fallback device state. 158 * 159 * <p>If no fallback is specified for this device state, it will return {@link 160 * DEVICE_STATE_ROTATION_LOCK_IGNORED}. 161 */ 162 @Settings.Secure.DeviceStateRotationLockSetting getRotationLockSetting(int deviceState)163 public int getRotationLockSetting(int deviceState) { 164 int devicePosture = mPosturesHelper.deviceStateToPosture(deviceState); 165 int rotationLockSetting = mPostureRotationLockSettings.get( 166 devicePosture, /* valueIfKeyNotFound= */ DEVICE_STATE_ROTATION_LOCK_IGNORED); 167 if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) { 168 rotationLockSetting = getFallbackRotationLockSetting(devicePosture); 169 } 170 return rotationLockSetting; 171 } 172 getFallbackRotationLockSetting(int devicePosture)173 private int getFallbackRotationLockSetting(int devicePosture) { 174 int indexOfFallback = mPostureRotationLockFallbackSettings.indexOfKey(devicePosture); 175 if (indexOfFallback < 0) { 176 Log.w(TAG, "Setting is ignored, but no fallback was specified."); 177 return DEVICE_STATE_ROTATION_LOCK_IGNORED; 178 } 179 int fallbackPosture = mPostureRotationLockFallbackSettings.valueAt(indexOfFallback); 180 return mPostureRotationLockSettings.get(fallbackPosture, 181 /* valueIfKeyNotFound= */ DEVICE_STATE_ROTATION_LOCK_IGNORED); 182 } 183 184 185 /** Returns true if the rotation is locked for the current device state */ isRotationLocked(int deviceState)186 public boolean isRotationLocked(int deviceState) { 187 return getRotationLockSetting(deviceState) == DEVICE_STATE_ROTATION_LOCK_LOCKED; 188 } 189 190 /** 191 * Returns true if there is no device state for which the current setting is {@link 192 * DEVICE_STATE_ROTATION_LOCK_UNLOCKED}. 193 */ isRotationLockedForAllStates()194 public boolean isRotationLockedForAllStates() { 195 for (int i = 0; i < mPostureRotationLockSettings.size(); i++) { 196 if (mPostureRotationLockSettings.valueAt(i) 197 == DEVICE_STATE_ROTATION_LOCK_UNLOCKED) { 198 return false; 199 } 200 } 201 return true; 202 } 203 204 /** Returns a list of device states and their respective auto-rotation setting availability. */ getSettableDeviceStates()205 public List<SettableDeviceState> getSettableDeviceStates() { 206 // Returning a copy to make sure that nothing outside can mutate our internal list. 207 return new ArrayList<>(mSettableDeviceStates); 208 } 209 initializeInMemoryMap()210 private void initializeInMemoryMap() { 211 String serializedSetting = getPersistedSettingValue(); 212 if (TextUtils.isEmpty(serializedSetting)) { 213 // No settings saved, we should load the defaults and persist them. 214 fallbackOnDefaults(); 215 return; 216 } 217 String[] values = serializedSetting.split(SEPARATOR_REGEX); 218 if (values.length % 2 != 0) { 219 // Each entry should be a key/value pair, so this is corrupt. 220 Log.wtf(TAG, "Can't deserialize saved settings, falling back on defaults"); 221 fallbackOnDefaults(); 222 return; 223 } 224 mPostureRotationLockSettings = new SparseIntArray(values.length / 2); 225 int key; 226 int value; 227 228 for (int i = 0; i < values.length - 1; ) { 229 try { 230 key = Integer.parseInt(values[i++]); 231 value = Integer.parseInt(values[i++]); 232 boolean isPersistedValueIgnored = value == DEVICE_STATE_ROTATION_LOCK_IGNORED; 233 boolean isDefaultValueIgnored = mPostureDefaultRotationLockSettings.get(key) 234 == DEVICE_STATE_ROTATION_LOCK_IGNORED; 235 if (isPersistedValueIgnored != isDefaultValueIgnored) { 236 Log.w(TAG, "Conflict for ignored device state " + key 237 + ". Falling back on defaults"); 238 fallbackOnDefaults(); 239 return; 240 } 241 mPostureRotationLockSettings.put(key, value); 242 } catch (NumberFormatException e) { 243 Log.wtf(TAG, "Error deserializing one of the saved settings", e); 244 fallbackOnDefaults(); 245 return; 246 } 247 } 248 } 249 250 /** 251 * Resets the state of the class and saved settings back to the default values provided by the 252 * resources config. 253 */ 254 @VisibleForTesting resetStateForTesting(Resources resources)255 public void resetStateForTesting(Resources resources) { 256 mPostureRotationLockDefaults = 257 resources.getStringArray(R.array.config_perDeviceStateRotationLockDefaults); 258 fallbackOnDefaults(); 259 } 260 fallbackOnDefaults()261 private void fallbackOnDefaults() { 262 loadDefaults(); 263 persistSettings(); 264 } 265 persistSettings()266 private void persistSettings() { 267 if (mPostureRotationLockSettings.size() == 0) { 268 persistSettingIfChanged(/* newSettingValue= */ ""); 269 return; 270 } 271 272 StringBuilder stringBuilder = new StringBuilder(); 273 stringBuilder 274 .append(mPostureRotationLockSettings.keyAt(0)) 275 .append(SEPARATOR_REGEX) 276 .append(mPostureRotationLockSettings.valueAt(0)); 277 278 for (int i = 1; i < mPostureRotationLockSettings.size(); i++) { 279 stringBuilder 280 .append(SEPARATOR_REGEX) 281 .append(mPostureRotationLockSettings.keyAt(i)) 282 .append(SEPARATOR_REGEX) 283 .append(mPostureRotationLockSettings.valueAt(i)); 284 } 285 persistSettingIfChanged(stringBuilder.toString()); 286 } 287 persistSettingIfChanged(String newSettingValue)288 private void persistSettingIfChanged(String newSettingValue) { 289 String lastSettingValue = getPersistedSettingValue(); 290 Log.v(TAG, "persistSettingIfChanged: " 291 + "last=" + lastSettingValue + ", " 292 + "new=" + newSettingValue); 293 if (TextUtils.equals(lastSettingValue, newSettingValue)) { 294 return; 295 } 296 mSecureSettings.putStringForUser( 297 Settings.Secure.DEVICE_STATE_ROTATION_LOCK, 298 /* value= */ newSettingValue, 299 UserHandle.USER_CURRENT); 300 } 301 getPersistedSettingValue()302 private String getPersistedSettingValue() { 303 return mSecureSettings.getStringForUser( 304 Settings.Secure.DEVICE_STATE_ROTATION_LOCK, 305 UserHandle.USER_CURRENT); 306 } 307 loadDefaults()308 private void loadDefaults() { 309 mSettableDeviceStates = new ArrayList<>(mPostureRotationLockDefaults.length); 310 mPostureDefaultRotationLockSettings = new SparseIntArray( 311 mPostureRotationLockDefaults.length); 312 mPostureRotationLockSettings = new SparseIntArray(mPostureRotationLockDefaults.length); 313 mPostureRotationLockFallbackSettings = new SparseIntArray(1); 314 for (String entry : mPostureRotationLockDefaults) { 315 String[] values = entry.split(SEPARATOR_REGEX); 316 try { 317 int posture = Integer.parseInt(values[0]); 318 int rotationLockSetting = Integer.parseInt(values[1]); 319 if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) { 320 if (values.length == 3) { 321 int fallbackPosture = Integer.parseInt(values[2]); 322 mPostureRotationLockFallbackSettings.put(posture, fallbackPosture); 323 } else { 324 Log.w(TAG, 325 "Rotation lock setting is IGNORED, but values have unexpected " 326 + "size of " 327 + values.length); 328 } 329 } 330 boolean isSettable = rotationLockSetting != DEVICE_STATE_ROTATION_LOCK_IGNORED; 331 Integer deviceState = mPosturesHelper.postureToDeviceState(posture); 332 if (deviceState != null) { 333 mSettableDeviceStates.add(new SettableDeviceState(deviceState, isSettable)); 334 } else { 335 Log.wtf(TAG, "No matching device state for posture: " + posture); 336 } 337 mPostureRotationLockSettings.put(posture, rotationLockSetting); 338 mPostureDefaultRotationLockSettings.put(posture, rotationLockSetting); 339 } catch (NumberFormatException e) { 340 Log.wtf(TAG, "Error parsing settings entry. Entry was: " + entry, e); 341 return; 342 } 343 } 344 } 345 346 /** Dumps internal state. */ dump(IndentingPrintWriter pw)347 public void dump(IndentingPrintWriter pw) { 348 pw.println("DeviceStateRotationLockSettingsManager"); 349 pw.increaseIndent(); 350 pw.println("mPostureRotationLockDefaults: " 351 + Arrays.toString(mPostureRotationLockDefaults)); 352 pw.println("mPostureDefaultRotationLockSettings: " + mPostureDefaultRotationLockSettings); 353 pw.println("mDeviceStateRotationLockSettings: " + mPostureRotationLockSettings); 354 pw.println("mPostureRotationLockFallbackSettings: " + mPostureRotationLockFallbackSettings); 355 pw.println("mSettableDeviceStates: " + mSettableDeviceStates); 356 pw.decreaseIndent(); 357 } 358 359 /** 360 * Called when the persisted settings have changed, requiring a reinitialization of the 361 * in-memory map. 362 */ 363 @VisibleForTesting onPersistedSettingsChanged()364 public void onPersistedSettingsChanged() { 365 initializeInMemoryMap(); 366 notifyListeners(); 367 } 368 notifyListeners()369 private void notifyListeners() { 370 for (DeviceStateRotationLockSettingsListener r : mListeners) { 371 r.onSettingsChanged(); 372 } 373 } 374 375 /** Listener for changes in device-state based rotation lock settings */ 376 public interface DeviceStateRotationLockSettingsListener { 377 /** Called whenever the settings have changed. */ onSettingsChanged()378 void onSettingsChanged(); 379 } 380 381 /** Represents a device state and whether it has an auto-rotation setting. */ 382 public static class SettableDeviceState { 383 private final int mDeviceState; 384 private final boolean mIsSettable; 385 SettableDeviceState(int deviceState, boolean isSettable)386 SettableDeviceState(int deviceState, boolean isSettable) { 387 mDeviceState = deviceState; 388 mIsSettable = isSettable; 389 } 390 391 /** Returns the device state associated with this object. */ getDeviceState()392 public int getDeviceState() { 393 return mDeviceState; 394 } 395 396 /** Returns whether there is an auto-rotation setting for this device state. */ isSettable()397 public boolean isSettable() { 398 return mIsSettable; 399 } 400 401 @Override equals(Object o)402 public boolean equals(Object o) { 403 if (this == o) return true; 404 if (!(o instanceof SettableDeviceState)) return false; 405 SettableDeviceState that = (SettableDeviceState) o; 406 return mDeviceState == that.mDeviceState && mIsSettable == that.mIsSettable; 407 } 408 409 @Override hashCode()410 public int hashCode() { 411 return Objects.hash(mDeviceState, mIsSettable); 412 } 413 414 @Override toString()415 public String toString() { 416 return "SettableDeviceState{" 417 + "mDeviceState=" + mDeviceState 418 + ", mIsSettable=" + mIsSettable 419 + '}'; 420 } 421 } 422 } 423