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

Commit 91c8c9ea authored by Alex Florescu's avatar Alex Florescu
Browse files

Introduce separate rotation lock settings per device state.

This will be flagged with config_perDeviceStateRotationLock.
If true, a distinct rotation lock settings can be maintained for each
device state. Some device states won't need their own rotation lock
setting and in those cases, the setting of the previous device state
will be used.

The existing global setting "Settings.System.ACCELEROMETER_ROTATION"
still works the same and reflects the setting for the currently active
state.

Test: atest SystemUiTests:RotationLockControllerImplTest
Test: atest SystemUiTests:DeviceStateRotationLockSettingControllerTest
Bug: 183001527
Change-Id: I1cbcfae7db7c01745ea7ad665432f8651d8f1546
parent 5df7e132
Loading
Loading
Loading
Loading
+55 −0
Original line number Diff line number Diff line
@@ -10119,6 +10119,61 @@ public final class Settings {
        @Readable
        public static final String GAME_DASHBOARD_ALWAYS_ON = "game_dashboard_always_on";
        /**
         * For this device state, no specific auto-rotation lock setting should be applied.
         * If the user toggles the auto-rotate lock in this state, the setting will apply to the
         * previously valid device state.
         * @hide
         */
        public static final int DEVICE_STATE_ROTATION_LOCK_IGNORED = 0;
        /**
         * For this device state, the setting for auto-rotation is locked.
         * @hide
         */
        public static final int DEVICE_STATE_ROTATION_LOCK_LOCKED = 1;
        /**
         * For this device state, the setting for auto-rotation is unlocked.
         * @hide
         */
        public static final int DEVICE_STATE_ROTATION_LOCK_UNLOCKED = 2;
        /**
         * The different settings that can be used as values with
         * {@link #DEVICE_STATE_ROTATION_LOCK}.
         * @hide
         */
        @IntDef(prefix = {"DEVICE_STATE_ROTATION_LOCK_"}, value = {
                DEVICE_STATE_ROTATION_LOCK_IGNORED,
                DEVICE_STATE_ROTATION_LOCK_LOCKED,
                DEVICE_STATE_ROTATION_LOCK_UNLOCKED,
        })
        @Retention(RetentionPolicy.SOURCE)
        @interface DeviceStateRotationLockSetting {
        }
        /**
         * Rotation lock setting keyed on device state.
         *
         * This holds a serialized map using int keys that represent Device States and value of
         * {@link DeviceStateRotationLockSetting} representing the rotation lock setting for that
         * device state.
         *
         * 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>
         * </ul>
         *
         * @hide
         */
        public static final String DEVICE_STATE_ROTATION_LOCK =
                "device_state_rotation_lock";
        /**
         * These entries are considered common between the personal and the managed profile,
         * since the managed profile doesn't get to change them.
+11 −0
Original line number Diff line number Diff line
@@ -666,6 +666,17 @@
         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
          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. -->
    <string-array name="config_perDeviceStateRotationLockDefaults" />


    <!-- Desk dock behavior -->

    <!-- The number of degrees to rotate the display when the device is in a desk dock.
+2 −0
Original line number Diff line number Diff line
@@ -3838,6 +3838,8 @@
  <java-symbol type="string" name="config_foldedArea" />
  <java-symbol type="bool" name="config_supportsConcurrentInternalDisplays" />
  <java-symbol type="bool" name="config_unfoldTransitionEnabled" />
  <java-symbol type="array" name="config_perDeviceStateRotationLockDefaults" />


  <java-symbol type="array" name="config_disableApksUnlessMatchedSku_apk_list" />
  <java-symbol type="array" name="config_disableApkUnlessMatchedSku_skus_list" />
+29 −0
Original line number Diff line number Diff line
@@ -34,7 +34,9 @@ import static android.provider.settings.validators.SettingsValidators.TILE_LIST_
import static android.provider.settings.validators.SettingsValidators.TTS_LIST_VALIDATOR;

import android.provider.Settings.Secure;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;

import java.util.Map;

@@ -287,5 +289,32 @@ public class SecureSettingsValidators {
        VALIDATORS.put(Secure.CLIPBOARD_SHOW_ACCESS_NOTIFICATIONS, BOOLEAN_VALIDATOR);
        VALIDATORS.put(Secure.NOTIFICATION_BUBBLES, BOOLEAN_VALIDATOR);
        VALIDATORS.put(Secure.LOCATION_TIME_ZONE_DETECTION_ENABLED, BOOLEAN_VALIDATOR);
        VALIDATORS.put(Secure.DEVICE_STATE_ROTATION_LOCK, value -> {
            if (TextUtils.isEmpty(value)) {
                return true;
            }
            String[] intValues = value.split(":");
            if (intValues.length % 2 != 0) {
                return false;
            }
            InclusiveIntegerRangeValidator enumValidator =
                    new InclusiveIntegerRangeValidator(
                            Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED,
                            Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED);
            ArraySet<String> keys = new ArraySet<>();
            for (int i = 0; i < intValues.length - 1; ) {
                String entryKey = intValues[i++];
                String entryValue = intValues[i++];
                if (!NON_NEGATIVE_INTEGER_VALIDATOR.validate(entryKey)
                        || !enumValidator.validate(entryValue)) {
                    return false;
                }
                // If the same device state key was specified more than once, this is invalid
                if (!keys.add(entryKey)) {
                    return false;
                }
            }
            return true;
        });
    }
}
+239 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.systemui.statusbar.policy;


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 static com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule.DEVICE_STATE_ROTATION_LOCK_DEFAULTS;

import android.annotation.Nullable;
import android.hardware.devicestate.DeviceStateManager;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;

import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.wrapper.RotationPolicyWrapper;

import java.util.concurrent.Executor;

import javax.inject.Inject;
import javax.inject.Named;

/**
 * Handles reading and writing of rotation lock settings per device state, as well as setting
 * the rotation lock when device state changes.
 **/
@SysUISingleton
public final class DeviceStateRotationLockSettingController implements Listenable,
        RotationLockController.RotationLockControllerCallback {

    private static final String TAG = "DSRotateLockSettingCon";

    private static final String SEPARATOR_REGEX = ":";

    private final SecureSettings mSecureSettings;
    private final RotationPolicyWrapper mRotationPolicyWrapper;
    private final DeviceStateManager mDeviceStateManager;
    private final Executor mMainExecutor;
    private final String[] mDeviceStateRotationLockDefaults;

    private SparseIntArray mDeviceStateRotationLockSettings;
    // TODO(b/183001527): Add API to query current device state and initialize this.
    private int mDeviceState = -1;
    @Nullable
    private DeviceStateManager.DeviceStateCallback mDeviceStateCallback;


    @Inject
    public DeviceStateRotationLockSettingController(
            SecureSettings secureSettings,
            RotationPolicyWrapper rotationPolicyWrapper,
            DeviceStateManager deviceStateManager,
            @Main Executor executor,
            @Named(DEVICE_STATE_ROTATION_LOCK_DEFAULTS) String[] deviceStateRotationLockDefaults
    ) {
        mSecureSettings = secureSettings;
        mRotationPolicyWrapper = rotationPolicyWrapper;
        mDeviceStateManager = deviceStateManager;
        mMainExecutor = executor;
        mDeviceStateRotationLockDefaults = deviceStateRotationLockDefaults;
    }

    /**
     * Loads the settings from storage.
     */
    public void initialize() {
        String serializedSetting =
                mSecureSettings.getStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
                        UserHandle.USER_CURRENT);
        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;
        }
        mDeviceStateRotationLockSettings = 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++]);
                mDeviceStateRotationLockSettings.put(key, value);
            } catch (NumberFormatException e) {
                Log.wtf(TAG, "Error deserializing one of the saved settings", e);
                fallbackOnDefaults();
                return;
            }
        }
    }

    private void fallbackOnDefaults() {
        loadDefaults();
        persistSettings();
    }

    @Override
    public void setListening(boolean listening) {
        if (listening) {
            // Note that this is called once with the initial state of the device, even if there
            // is no user action.
            mDeviceStateCallback = this::updateDeviceState;
            mDeviceStateManager.registerCallback(mMainExecutor, mDeviceStateCallback);
        } else {
            if (mDeviceStateCallback != null) {
                mDeviceStateManager.unregisterCallback(mDeviceStateCallback);
            }
        }
    }

    @Override
    public void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible) {
        if (mDeviceState == -1) {
            Log.wtf(TAG, "Device state was not initialized.");
            return;
        }

        if (rotationLocked == isRotationLockedForCurrentState()) {
            Log.v(TAG, "Rotation lock same as the current setting, no need to update.");
            return;
        }

        saveNewRotationLockSetting(rotationLocked);
    }

    private void saveNewRotationLockSetting(boolean isRotationLocked) {
        Log.v(TAG, "saveNewRotationLockSetting [state=" + mDeviceState + "] [isRotationLocked="
                + isRotationLocked + "]");

        mDeviceStateRotationLockSettings.put(mDeviceState,
                isRotationLocked
                        ? DEVICE_STATE_ROTATION_LOCK_LOCKED
                        : DEVICE_STATE_ROTATION_LOCK_UNLOCKED);
        persistSettings();
    }

    private boolean isRotationLockedForCurrentState() {
        return mDeviceStateRotationLockSettings.get(mDeviceState,
                DEVICE_STATE_ROTATION_LOCK_IGNORED) == DEVICE_STATE_ROTATION_LOCK_LOCKED;
    }

    private void updateDeviceState(int state) {
        Log.v(TAG, "updateDeviceState [state=" + state + "]");
        if (mDeviceState == state) {
            return;
        }

        int rotationLockSetting =
                mDeviceStateRotationLockSettings.get(state, DEVICE_STATE_ROTATION_LOCK_IGNORED);
        if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) {
            // We won't handle this device state. The same rotation lock setting as before should
            // apply and any changes to the rotation lock setting will be written for the previous
            // valid device state.
            Log.v(TAG, "Ignoring new device state: " + state);
            return;
        }

        // Accept the new state
        mDeviceState = state;

        // Update the rotation lock setting if needed for this new device state
        boolean newRotationLockSetting = rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_LOCKED;
        if (newRotationLockSetting != mRotationPolicyWrapper.isRotationLocked()) {
            mRotationPolicyWrapper.setRotationLock(newRotationLockSetting);
        }
    }

    private void persistSettings() {
        if (mDeviceStateRotationLockSettings.size() == 0) {
            mSecureSettings.putStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
                    /* value= */"", UserHandle.USER_CURRENT);
            return;
        }

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

        for (int i = 1; i < mDeviceStateRotationLockSettings.size(); i++) {
            stringBuilder
                    .append(SEPARATOR_REGEX)
                    .append(mDeviceStateRotationLockSettings.keyAt(i))
                    .append(SEPARATOR_REGEX)
                    .append(mDeviceStateRotationLockSettings.valueAt(i));
        }
        mSecureSettings.putStringForUser(Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
                stringBuilder.toString(), UserHandle.USER_CURRENT);
    }

    private void loadDefaults() {
        if (mDeviceStateRotationLockDefaults.length == 0) {
            Log.w(TAG, "Empty default settings");
            mDeviceStateRotationLockSettings = new SparseIntArray(/* initialCapacity= */0);
            return;
        }
        mDeviceStateRotationLockSettings =
                new SparseIntArray(mDeviceStateRotationLockDefaults.length);
        for (String serializedDefault : mDeviceStateRotationLockDefaults) {
            String[] entry = serializedDefault.split(SEPARATOR_REGEX);
            try {
                int key = Integer.parseInt(entry[0]);
                int value = Integer.parseInt(entry[1]);
                mDeviceStateRotationLockSettings.put(key, value);
            } catch (NumberFormatException e) {
                Log.wtf(TAG, "Error deserializing default settings", e);
            }
        }
    }

}
Loading