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