Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4a5dd788 authored by Christian Göllner's avatar Christian Göllner
Browse files

Foldable rotation settings: persist postures instead of device states

Device state ids can change, and therefore shouldn't be persisted. We
define our custom ids for device postures, which will not change.

1. We define our custom posture ids in Settings.Secure. These match
   the current device state ids. This way we don't have to wipe users'
   settings.
2. We convert device ids to postures, based on the config arrays
   "config_foldedDeviceStates", "config_halfFoldedDeviceStates", and
   "config_openDeviceStates".

Test: unit tests
Test: manually by trying the different auto-rotation scenarios in
      different postures.
Fixes: 261968395
Change-Id: I396e9c92230e75e186b6a70198ee16b41e508a1e
parent 4626fa48
Loading
Loading
Loading
Loading
+30 −5
Original line number Diff line number Diff line
@@ -10989,21 +10989,46 @@ public final class Settings {
        public @interface DeviceStateRotationLockSetting {
        }
        /** @hide */
        public static final int DEVICE_STATE_ROTATION_KEY_UNKNOWN = -1;
        /** @hide */
        public static final int DEVICE_STATE_ROTATION_KEY_FOLDED = 0;
        /** @hide */
        public static final int DEVICE_STATE_ROTATION_KEY_HALF_FOLDED = 1;
        /** @hide */
        public static final int DEVICE_STATE_ROTATION_KEY_UNFOLDED = 2;
        /**
         * The different postures that can be used as keys with
         * {@link #DEVICE_STATE_ROTATION_LOCK}.
         * @hide
         */
        @IntDef(prefix = {"DEVICE_STATE_ROTATION_KEY_"}, value = {
                DEVICE_STATE_ROTATION_KEY_UNKNOWN,
                DEVICE_STATE_ROTATION_KEY_FOLDED,
                DEVICE_STATE_ROTATION_KEY_HALF_FOLDED,
                DEVICE_STATE_ROTATION_KEY_UNFOLDED,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface DeviceStateRotationLockKey {
        }
        /**
         * Rotation lock setting keyed on device state.
         *
         * This holds a serialized map using int keys that represent Device States and value of
         * This holds a serialized map using int keys that represent postures in
         * {@link DeviceStateRotationLockKey} and value of
         * {@link DeviceStateRotationLockSetting} representing the rotation lock setting for that
         * device state.
         * posture.
         *
         * Serialized as key0:value0:key1:value1:...:keyN:valueN.
         *
         * Example: "0:1:1:2:2:1"
         * This example represents a map of:
         * <ul>
         *     <li>0 -> DEVICE_STATE_ROTATION_LOCK_LOCKED</li>
         *     <li>1 -> DEVICE_STATE_ROTATION_LOCK_UNLOCKED</li>
         *     <li>2 -> DEVICE_STATE_ROTATION_LOCK_IGNORED</li>
         *     <li>DEVICE_STATE_ROTATION_KEY_FOLDED -> DEVICE_STATE_ROTATION_LOCK_LOCKED</li>
         *     <li>DEVICE_STATE_ROTATION_KEY_HALF_FOLDED -> DEVICE_STATE_ROTATION_LOCK_UNLOCKED</li>
         *     <li>DEVICE_STATE_ROTATION_KEY_UNFOLDED -> DEVICE_STATE_ROTATION_LOCK_IGNORED</li>
         * </ul>
         *
         * @hide
+7 −8
Original line number Diff line number Diff line
@@ -733,14 +733,13 @@
         display is powered on at the same time. -->
    <bool name="config_supportsConcurrentInternalDisplays">true</bool>

    <!-- Map of DeviceState to rotation lock setting. Each entry must be in the format
         "key:value", for example: "0:1".
          The keys are device states, and the values are one of
    <!-- Map of device posture to rotation lock setting. Each entry must be in the format
         "key:value", or "key:value:fallback_key" for example: "0:1" or "2:0:1". The keys are one of
         Settings.Secure.DeviceStateRotationLockKey, and the values are one of
         Settings.Secure.DeviceStateRotationLockSetting.
          Any device state that doesn't have a default set here will be treated as
          DEVICE_STATE_ROTATION_LOCK_IGNORED meaning it will not have its own rotation lock setting.
          If this map is missing, the feature is disabled and only one global rotation lock setting
           will apply, regardless of device state. -->
         The fallback is a key to a device posture that can be specified when the value is
         Settings.Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED.
     -->
    <string-array name="config_perDeviceStateRotationLockDefaults" />

    <!-- Dock behavior -->
+4 −1
Original line number Diff line number Diff line
@@ -10,7 +10,10 @@ package {
android_library {
    name: "SettingsLibDeviceStateRotationLock",

    srcs: ["src/**/*.java"],
    srcs: [
        "src/**/*.java",
        "src/**/*.kt",
    ],

    min_sdk_version: "21",
}
+58 −53
Original line number Diff line number Diff line
@@ -57,17 +57,19 @@ public final class DeviceStateRotationLockSettingsManager {
    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
    private final Set<DeviceStateRotationLockSettingsListener> mListeners = new HashSet<>();
    private final SecureSettings mSecureSettings;
    private String[] mDeviceStateRotationLockDefaults;
    private SparseIntArray mDeviceStateRotationLockSettings;
    private SparseIntArray mDeviceStateDefaultRotationLockSettings;
    private SparseIntArray mDeviceStateRotationLockFallbackSettings;
    private final PosturesHelper mPosturesHelper;
    private String[] mPostureRotationLockDefaults;
    private SparseIntArray mPostureRotationLockSettings;
    private SparseIntArray mPostureDefaultRotationLockSettings;
    private SparseIntArray mPostureRotationLockFallbackSettings;
    private String mLastSettingValue;
    private List<SettableDeviceState> mSettableDeviceStates;

    @VisibleForTesting
    DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings) {
        this.mSecureSettings = secureSettings;
        mDeviceStateRotationLockDefaults =
        mSecureSettings = secureSettings;
        mPosturesHelper = new PosturesHelper(context);
        mPostureRotationLockDefaults =
                context.getResources()
                        .getStringArray(R.array.config_perDeviceStateRotationLockDefaults);
        loadDefaults();
@@ -134,13 +136,14 @@ public final class DeviceStateRotationLockSettingsManager {

    /** Updates the rotation lock setting for a specified device state. */
    public void updateSetting(int deviceState, boolean rotationLocked) {
        if (mDeviceStateRotationLockFallbackSettings.indexOfKey(deviceState) >= 0) {
            // The setting for this device state is IGNORED, and has a fallback device state.
            // The setting for that fallback device state should be the changed in this case.
            deviceState = mDeviceStateRotationLockFallbackSettings.get(deviceState);
        }
        mDeviceStateRotationLockSettings.put(
                deviceState,
        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);
@@ -159,22 +162,23 @@ public final class DeviceStateRotationLockSettingsManager {
     */
    @Settings.Secure.DeviceStateRotationLockSetting
    public int getRotationLockSetting(int deviceState) {
        int rotationLockSetting = mDeviceStateRotationLockSettings.get(
                deviceState, /* valueIfKeyNotFound= */ DEVICE_STATE_ROTATION_LOCK_IGNORED);
        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(deviceState);
            rotationLockSetting = getFallbackRotationLockSetting(devicePosture);
        }
        return rotationLockSetting;
    }

    private int getFallbackRotationLockSetting(int deviceState) {
        int indexOfFallbackState = mDeviceStateRotationLockFallbackSettings.indexOfKey(deviceState);
        if (indexOfFallbackState < 0) {
    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 fallbackState = mDeviceStateRotationLockFallbackSettings.valueAt(indexOfFallbackState);
        return mDeviceStateRotationLockSettings.get(fallbackState,
        int fallbackPosture = mPostureRotationLockFallbackSettings.valueAt(indexOfFallback);
        return mPostureRotationLockSettings.get(fallbackPosture,
                /* valueIfKeyNotFound= */ DEVICE_STATE_ROTATION_LOCK_IGNORED);
    }

@@ -189,8 +193,8 @@ public final class DeviceStateRotationLockSettingsManager {
     * DEVICE_STATE_ROTATION_LOCK_UNLOCKED}.
     */
    public boolean isRotationLockedForAllStates() {
        for (int i = 0; i < mDeviceStateRotationLockSettings.size(); i++) {
            if (mDeviceStateRotationLockSettings.valueAt(i)
        for (int i = 0; i < mPostureRotationLockSettings.size(); i++) {
            if (mPostureRotationLockSettings.valueAt(i)
                    == DEVICE_STATE_ROTATION_LOCK_UNLOCKED) {
                return false;
            }
@@ -221,7 +225,7 @@ public final class DeviceStateRotationLockSettingsManager {
            fallbackOnDefaults();
            return;
        }
        mDeviceStateRotationLockSettings = new SparseIntArray(values.length / 2);
        mPostureRotationLockSettings = new SparseIntArray(values.length / 2);
        int key;
        int value;

@@ -230,7 +234,7 @@ public final class DeviceStateRotationLockSettingsManager {
                key = Integer.parseInt(values[i++]);
                value = Integer.parseInt(values[i++]);
                boolean isPersistedValueIgnored = value == DEVICE_STATE_ROTATION_LOCK_IGNORED;
                boolean isDefaultValueIgnored = mDeviceStateDefaultRotationLockSettings.get(key)
                boolean isDefaultValueIgnored = mPostureDefaultRotationLockSettings.get(key)
                        == DEVICE_STATE_ROTATION_LOCK_IGNORED;
                if (isPersistedValueIgnored != isDefaultValueIgnored) {
                    Log.w(TAG, "Conflict for ignored device state " + key
@@ -238,7 +242,7 @@ public final class DeviceStateRotationLockSettingsManager {
                    fallbackOnDefaults();
                    return;
                }
                mDeviceStateRotationLockSettings.put(key, value);
                mPostureRotationLockSettings.put(key, value);
            } catch (NumberFormatException e) {
                Log.wtf(TAG, "Error deserializing one of the saved settings", e);
                fallbackOnDefaults();
@@ -253,7 +257,7 @@ public final class DeviceStateRotationLockSettingsManager {
     */
    @VisibleForTesting
    public void resetStateForTesting(Resources resources) {
        mDeviceStateRotationLockDefaults =
        mPostureRotationLockDefaults =
                resources.getStringArray(R.array.config_perDeviceStateRotationLockDefaults);
        fallbackOnDefaults();
    }
@@ -264,23 +268,23 @@ public final class DeviceStateRotationLockSettingsManager {
    }

    private void persistSettings() {
        if (mDeviceStateRotationLockSettings.size() == 0) {
        if (mPostureRotationLockSettings.size() == 0) {
            persistSettingIfChanged(/* newSettingValue= */ "");
            return;
        }

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder
                .append(mDeviceStateRotationLockSettings.keyAt(0))
                .append(mPostureRotationLockSettings.keyAt(0))
                .append(SEPARATOR_REGEX)
                .append(mDeviceStateRotationLockSettings.valueAt(0));
                .append(mPostureRotationLockSettings.valueAt(0));

        for (int i = 1; i < mDeviceStateRotationLockSettings.size(); i++) {
        for (int i = 1; i < mPostureRotationLockSettings.size(); i++) {
            stringBuilder
                    .append(SEPARATOR_REGEX)
                    .append(mDeviceStateRotationLockSettings.keyAt(i))
                    .append(mPostureRotationLockSettings.keyAt(i))
                    .append(SEPARATOR_REGEX)
                    .append(mDeviceStateRotationLockSettings.valueAt(i));
                    .append(mPostureRotationLockSettings.valueAt(i));
        }
        persistSettingIfChanged(stringBuilder.toString());
    }
@@ -300,22 +304,20 @@ public final class DeviceStateRotationLockSettingsManager {
    }

    private void loadDefaults() {
        mSettableDeviceStates = new ArrayList<>(mDeviceStateRotationLockDefaults.length);
        mDeviceStateDefaultRotationLockSettings = new SparseIntArray(
                mDeviceStateRotationLockDefaults.length);
        mDeviceStateRotationLockSettings = new SparseIntArray(
                mDeviceStateRotationLockDefaults.length);
        mDeviceStateRotationLockFallbackSettings = new SparseIntArray(1);
        for (String entry : mDeviceStateRotationLockDefaults) {
        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 deviceState = Integer.parseInt(values[0]);
                int posture = Integer.parseInt(values[0]);
                int rotationLockSetting = Integer.parseInt(values[1]);
                if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) {
                    if (values.length == 3) {
                        int fallbackDeviceState = Integer.parseInt(values[2]);
                        mDeviceStateRotationLockFallbackSettings.put(deviceState,
                                fallbackDeviceState);
                        int fallbackPosture = Integer.parseInt(values[2]);
                        mPostureRotationLockFallbackSettings.put(posture, fallbackPosture);
                    } else {
                        Log.w(TAG,
                                "Rotation lock setting is IGNORED, but values have unexpected "
@@ -324,9 +326,14 @@ public final class DeviceStateRotationLockSettingsManager {
                    }
                }
                boolean isSettable = rotationLockSetting != DEVICE_STATE_ROTATION_LOCK_IGNORED;
                Integer deviceState = mPosturesHelper.postureToDeviceState(posture);
                if (deviceState != null) {
                    mSettableDeviceStates.add(new SettableDeviceState(deviceState, isSettable));
                mDeviceStateRotationLockSettings.put(deviceState, rotationLockSetting);
                mDeviceStateDefaultRotationLockSettings.put(deviceState, rotationLockSetting);
                } 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;
@@ -338,13 +345,11 @@ public final class DeviceStateRotationLockSettingsManager {
    public void dump(IndentingPrintWriter pw) {
        pw.println("DeviceStateRotationLockSettingsManager");
        pw.increaseIndent();
        pw.println("mDeviceStateRotationLockDefaults: " + Arrays.toString(
                mDeviceStateRotationLockDefaults));
        pw.println("mDeviceStateDefaultRotationLockSettings: "
                + mDeviceStateDefaultRotationLockSettings);
        pw.println("mDeviceStateRotationLockSettings: " + mDeviceStateRotationLockSettings);
        pw.println("mDeviceStateRotationLockFallbackSettings: "
                + mDeviceStateRotationLockFallbackSettings);
        pw.println("mPostureRotationLockDefaults: "
                + Arrays.toString(mPostureRotationLockDefaults));
        pw.println("mPostureDefaultRotationLockSettings: " + mPostureDefaultRotationLockSettings);
        pw.println("mDeviceStateRotationLockSettings: " + mPostureRotationLockSettings);
        pw.println("mPostureRotationLockFallbackSettings: " + mPostureRotationLockFallbackSettings);
        pw.println("mSettableDeviceStates: " + mSettableDeviceStates);
        pw.println("mLastSettingValue: " + mLastSettingValue);
        pw.decreaseIndent();
+55 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 android.content.Context
import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_KEY_FOLDED
import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_KEY_HALF_FOLDED
import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_KEY_UNFOLDED
import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_KEY_UNKNOWN
import android.provider.Settings.Secure.DeviceStateRotationLockKey
import com.android.internal.R

/** Helps to convert between device state and posture. */
class PosturesHelper(context: Context) {

    private val foldedDeviceStates =
        context.resources.getIntArray(R.array.config_foldedDeviceStates)
    private val halfFoldedDeviceStates =
        context.resources.getIntArray(R.array.config_halfFoldedDeviceStates)
    private val unfoldedDeviceStates =
        context.resources.getIntArray(R.array.config_openDeviceStates)

    @DeviceStateRotationLockKey
    fun deviceStateToPosture(deviceState: Int): Int {
        return when (deviceState) {
            in foldedDeviceStates -> DEVICE_STATE_ROTATION_KEY_FOLDED
            in halfFoldedDeviceStates -> DEVICE_STATE_ROTATION_KEY_HALF_FOLDED
            in unfoldedDeviceStates -> DEVICE_STATE_ROTATION_KEY_UNFOLDED
            else -> DEVICE_STATE_ROTATION_KEY_UNKNOWN
        }
    }

    fun postureToDeviceState(@DeviceStateRotationLockKey posture: Int): Int? {
        return when (posture) {
            DEVICE_STATE_ROTATION_KEY_FOLDED -> foldedDeviceStates.firstOrNull()
            DEVICE_STATE_ROTATION_KEY_HALF_FOLDED -> halfFoldedDeviceStates.firstOrNull()
            DEVICE_STATE_ROTATION_KEY_UNFOLDED -> unfoldedDeviceStates.firstOrNull()
            else -> null
        }
    }
}
Loading