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

Commit 38eb8fea authored by Evan Severson's avatar Evan Severson Committed by Android (Google) Code Review
Browse files

Merge "Make SensorToggleControllers lifecycle aware"

parents 5c23de99 5ba40657
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -171,7 +171,7 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
        final SensorPrivacyManagerHelper helper = SensorPrivacyManagerHelper
                .getInstance(getApplicationContext());
        final boolean cameraPrivacyEnabled = helper
                .isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA, mUserId);
                .isSensorBlocked(SensorPrivacyManagerHelper.SENSOR_CAMERA);
        Log.v(TAG, "cameraPrivacyEnabled : " + cameraPrivacyEnabled);
    }

@@ -370,7 +370,7 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
                .getBooleanExtra(BiometricEnrollActivity.EXTRA_REQUIRE_PARENTAL_CONSENT, false);
        final boolean cameraPrivacyEnabled = SensorPrivacyManagerHelper
                .getInstance(getApplicationContext())
                .isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA, mUserId);
                .isSensorBlocked(SensorPrivacyManagerHelper.SENSOR_CAMERA);
        final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
        final boolean isSettingUp = isSetupWizard || (parentelConsentRequired
                && !WizardManagerHelper.isUserSetupComplete(this));
+15 −7
Original line number Diff line number Diff line
@@ -18,29 +18,37 @@ package com.android.settings.privacy;

import static android.os.UserManager.DISALLOW_CAMERA_TOGGLE;

import static com.android.settings.utils.SensorPrivacyManagerHelper.CAMERA;
import static com.android.settings.utils.SensorPrivacyManagerHelper.SENSOR_CAMERA;

import android.content.Context;
import android.provider.DeviceConfig;

import androidx.annotation.VisibleForTesting;

import com.android.settings.utils.SensorPrivacyManagerHelper;

/**
 * Controller for microphone toggle
 */
public class CameraToggleController extends SensorToggleController {

    public CameraToggleController(Context context, String preferenceKey) {
        super(context, preferenceKey);
    }

    @VisibleForTesting
    public CameraToggleController(Context context, String preferenceKey,
            SensorPrivacyManagerHelper sensorPrivacyManagerHelper) {
        super(context, preferenceKey, sensorPrivacyManagerHelper, /* ignoreDeviceConfig */ true);
    }

    @Override
    public int getSensor() {
        return CAMERA;
        return SENSOR_CAMERA;
    }

    @Override
    public int getAvailabilityStatus() {
        return mSensorPrivacyManagerHelper.supportsSensorToggle(getSensor())
                && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, "camera_toggle_enabled",
                true) ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
    public String getDeviceConfigKey() {
        return "camera_toggle_enabled";
    }

    @Override
+11 −7
Original line number Diff line number Diff line
@@ -18,10 +18,11 @@ package com.android.settings.privacy;

import static android.os.UserManager.DISALLOW_MICROPHONE_TOGGLE;

import static com.android.settings.utils.SensorPrivacyManagerHelper.MICROPHONE;
import static com.android.settings.utils.SensorPrivacyManagerHelper.SENSOR_MICROPHONE;

import android.content.Context;
import android.provider.DeviceConfig;

import com.android.settings.utils.SensorPrivacyManagerHelper;

/**
 * Controller for camera toggle
@@ -31,16 +32,19 @@ public class MicToggleController extends SensorToggleController {
        super(context, preferenceKey);
    }

    public MicToggleController(Context context, String preferenceKey,
            SensorPrivacyManagerHelper sensorPrivacyManagerHelper) {
        super(context, preferenceKey, sensorPrivacyManagerHelper, /* ignoreDeviceConfig */ true);
    }

    @Override
    public int getSensor() {
        return MICROPHONE;
        return SENSOR_MICROPHONE;
    }

    @Override
    public int getAvailabilityStatus() {
        return mSensorPrivacyManagerHelper.supportsSensorToggle(getSensor())
                && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, "mic_toggle_enabled",
                true) ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
    public String getDeviceConfigKey() {
        return "mic_toggle_enabled";
    }

    @Override
+58 −13
Original line number Diff line number Diff line
@@ -16,10 +16,12 @@

package com.android.settings.privacy;

import static android.hardware.SensorPrivacyManager.Sources.SETTINGS;

import android.content.Context;
import android.provider.DeviceConfig;

import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.PreferenceScreen;

import com.android.settings.R;
@@ -27,20 +29,35 @@ import com.android.settings.core.TogglePreferenceController;
import com.android.settings.utils.SensorPrivacyManagerHelper;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.RestrictedSwitchPreference;
import com.android.settingslib.core.lifecycle.Lifecycle;

import java.util.concurrent.Executor;

/**
 * Base class for sensor toggle controllers
 */
public abstract class SensorToggleController extends TogglePreferenceController {
public abstract class SensorToggleController extends TogglePreferenceController implements
        SensorPrivacyManagerHelper.Callback, LifecycleObserver {

    protected final SensorPrivacyManagerHelper mSensorPrivacyManagerHelper;
    private final Executor mCallbackExecutor;

    private PreferenceScreen mScreen;

    /** For testing since DeviceConfig uses static method calls */
    private boolean mIgnoreDeviceConfig;

    public SensorToggleController(Context context, String preferenceKey) {
        this(context, preferenceKey, SensorPrivacyManagerHelper.getInstance(context), false);
    }

    @VisibleForTesting
    SensorToggleController(Context context, String preferenceKey,
            SensorPrivacyManagerHelper sensorPrivacyManagerHelper, boolean ignoreDeviceConfig) {
        super(context, preferenceKey);
        mSensorPrivacyManagerHelper = SensorPrivacyManagerHelper.getInstance(context);

        mIgnoreDeviceConfig = ignoreDeviceConfig;
        mSensorPrivacyManagerHelper = sensorPrivacyManagerHelper;
        mCallbackExecutor = context.getMainExecutor();
    }

@@ -49,10 +66,22 @@ public abstract class SensorToggleController extends TogglePreferenceController
     */
    public abstract int getSensor();

    /**
     * The key for the device config setting for whether the feature is enabled.
     */
    public abstract String getDeviceConfigKey();

    protected String getRestriction() {
        return null;
    }

    @Override
    public int getAvailabilityStatus() {
        return mSensorPrivacyManagerHelper.supportsSensorToggle(getSensor())
                && (mIgnoreDeviceConfig || DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
                getDeviceConfigKey(), true)) ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
    }

    @Override
    public boolean isChecked() {
        return !mSensorPrivacyManagerHelper.isSensorBlocked(getSensor());
@@ -60,8 +89,7 @@ public abstract class SensorToggleController extends TogglePreferenceController

    @Override
    public boolean setChecked(boolean isChecked) {
        mSensorPrivacyManagerHelper.setSensorBlockedForProfileGroup(SETTINGS, getSensor(),
                !isChecked);
        mSensorPrivacyManagerHelper.setSensorBlocked(getSensor(), !isChecked);
        return true;
    }

@@ -69,21 +97,38 @@ public abstract class SensorToggleController extends TogglePreferenceController
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);

        RestrictedSwitchPreference preference =
                (RestrictedSwitchPreference) screen.findPreference(getPreferenceKey());
        mScreen = screen;

        RestrictedSwitchPreference preference = mScreen.findPreference(getPreferenceKey());
        if (preference != null) {
            preference.setDisabledByAdmin(RestrictedLockUtilsInternal
                    .checkIfRestrictionEnforced(mContext, getRestriction(), mContext.getUserId()));
        }

        mSensorPrivacyManagerHelper.addSensorBlockedListener(
                getSensor(),
                (sensor, blocked) -> updateState(screen.findPreference(mPreferenceKey)),
                mCallbackExecutor);
    }

    @Override
    public int getSliceHighlightMenuRes() {
        return R.string.menu_key_privacy;
    }

    @Override
    public void onSensorPrivacyChanged(int toggleType, int sensor, boolean blocked) {
        updateState(mScreen.findPreference(mPreferenceKey));
    }

    /**
     * onStart lifecycle event
     */
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void onStart() {
        mSensorPrivacyManagerHelper.addSensorBlockedListener(getSensor(), mCallbackExecutor, this);
    }

    /**
     * onStop lifecycle event
     */
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    public void onStop() {
        mSensorPrivacyManagerHelper.removeSensorBlockedListener(this);
    }
}
+0 −293
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.settings.utils;

import android.content.Context;
import android.hardware.SensorPrivacyManager;
import android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener;
import android.util.ArraySet;
import android.util.SparseArray;

import java.util.concurrent.Executor;

/**
 * A class to help with calls to the sensor privacy manager. This class caches state when needed and
 * multiplexes multiple listeners to a minimal set of binder calls.
 */
public class SensorPrivacyManagerHelper {

    public static final int MICROPHONE = SensorPrivacyManager.Sensors.MICROPHONE;
    public static final int CAMERA = SensorPrivacyManager.Sensors.CAMERA;

    private static SensorPrivacyManagerHelper sInstance;

    private final SensorPrivacyManager mSensorPrivacyManager;

    private final SparseArray<Boolean> mCurrentUserCachedState = new SparseArray<>();
    private final SparseArray<SparseArray<Boolean>> mCachedState = new SparseArray<>();

    private final SparseArray<OnSensorPrivacyChangedListener>
            mCurrentUserServiceListeners = new SparseArray<>();
    private final SparseArray<SparseArray<OnSensorPrivacyChangedListener>>
            mServiceListeners = new SparseArray<>();

    private final ArraySet<CallbackInfo> mCallbacks = new ArraySet<>();

    private final Object mLock = new Object();

    /**
     * Callback for when the state of the sensor privacy changes.
     */
    public interface Callback {

        /**
         * Method invoked when the sensor privacy changes.
         * @param sensor The sensor which changed
         * @param blocked If the sensor is blocked
         */
        void onSensorPrivacyChanged(int sensor, boolean blocked);
    }

    private static class CallbackInfo {
        static final int CURRENT_USER = -1;

        Callback mCallback;
        Executor mExecutor;
        int mSensor;
        int mUserId;

        CallbackInfo(Callback callback, Executor executor, int sensor, int userId) {
            mCallback = callback;
            mExecutor = executor;
            mSensor = sensor;
            mUserId = userId;
        }
    }

    /**
     * Gets the singleton instance
     * @param context The context which is needed if the instance hasn't been created
     * @return the instance
     */
    public static SensorPrivacyManagerHelper getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new SensorPrivacyManagerHelper(context);
        }
        return sInstance;
    }

    /**
     * Only to be used in tests
     */
    private static void clearInstance() {
        sInstance = null;
    }

    private SensorPrivacyManagerHelper(Context context) {
        mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
    }

    /**
     * Checks if the given toggle is supported on this device
     * @param sensor The sensor to check
     * @return whether the toggle for the sensor is supported on this device.
     */
    public boolean supportsSensorToggle(int sensor) {
        return mSensorPrivacyManager.supportsSensorToggle(sensor);
    }

    /**
     * Checks if the sensor is blocked for the current user. If the user switches and the state of
     * the new user is different, this value will change.
     * @param sensor the sensor to check
     * @return true if the sensor is blocked for the current user
     */
    public boolean isSensorBlocked(int sensor) {
        synchronized (mLock) {
            Boolean blocked = mCurrentUserCachedState.get(sensor);
            if (blocked == null) {
                registerCurrentUserListenerIfNeeded(sensor);

                blocked = mSensorPrivacyManager.isSensorPrivacyEnabled(sensor);
                mCurrentUserCachedState.put(sensor, blocked);
            }

            return blocked;
        }
    }

    /**
     * Checks if the sensor is or would be blocked if the given user is the foreground user
     * @param sensor the sensor to check
     * @param userId the user to check
     * @return true if the sensor is or would be blocked if the given user is the foreground user
     */
    public boolean isSensorBlocked(int sensor, int userId) {
        synchronized (mLock) {
            SparseArray<Boolean> userCachedState = createUserCachedStateIfNeededLocked(userId);
            Boolean blocked = userCachedState.get(sensor);
            if (blocked == null) {
                registerListenerIfNeeded(sensor, userId);

                blocked = mSensorPrivacyManager.isSensorPrivacyEnabled(sensor);
                userCachedState.put(sensor, blocked);
            }

            return blocked;
        }
    }

    /**
     * Sets the sensor privacy for the current user.
     * @param source The source with which sensor privacy is toggled.
     * @param sensor The sensor to set for
     * @param blocked The state to set to
     */
    public void setSensorBlocked(int source, int sensor, boolean blocked) {
        mSensorPrivacyManager.setSensorPrivacy(source, sensor, blocked);
    }

    /**
     * Sets the sensor privacy for the given user.
     * @param source The source with which sensor privacy is toggled.
     * @param sensor The sensor to set for
     * @param blocked The state to set to
     * @param userId The user to set for
     */
    public void setSensorBlocked(int source, int sensor, boolean blocked, int userId) {
        mSensorPrivacyManager.setSensorPrivacy(source, sensor, blocked, userId);
    }

    /**
     * Sets the sensor privacy for the current profile group.
     * @param source The source with which sensor privacy is toggled.
     * @param sensor The sensor to set for
     * @param blocked The state to set to
     */
    public void setSensorBlockedForProfileGroup(int source, int sensor, boolean blocked) {
        mSensorPrivacyManager.setSensorPrivacyForProfileGroup(source, sensor, blocked);
    }

    /**
     * Sets the sensor privacy for the given user's profile group.
     * @param source The source with which sensor privacy is toggled.
     * @param sensor The sensor to set for
     * @param blocked The state to set to
     */
    public void setSensorBlockedForProfileGroup(int source, int sensor, boolean blocked,
            int userId) {
        mSensorPrivacyManager.setSensorPrivacyForProfileGroup(source, sensor, blocked, userId);
    }

    /**
     * Adds a listener for the state of the current user. If the current user changes and the state
     * of the new user is different, a callback will be received.
     * @param sensor The sensor to watch
     * @param callback The callback to invoke
     * @param executor The executor to invoke on
     */
    public void addSensorBlockedListener(int sensor, Callback callback, Executor executor) {
        synchronized (mLock) {
            mCallbacks.add(new CallbackInfo(callback, executor, sensor, CallbackInfo.CURRENT_USER));
        }
    }

    /**
     * Adds a listener for the state of the given user
     * @param sensor The sensor to watch
     * @param callback The callback to invoke
     * @param executor The executor to invoke on
     */
    public void addSensorBlockedListener(int sensor, int userId, Callback callback,
            Executor executor) {
        synchronized (mLock) {
            mCallbacks.add(new CallbackInfo(callback, executor, sensor, userId));
        }
    }

    /**
     * Removes a callback
     * @param callback The callback to remove
     */
    public void removeBlockedListener(Callback callback) {
        synchronized (mLock) {
            mCallbacks.removeIf(callbackInfo -> callbackInfo.mCallback == callback);
        }
    }

    private void registerCurrentUserListenerIfNeeded(int sensor) {
        synchronized (mLock) {
            if (!mCurrentUserServiceListeners.contains(sensor)) {
                OnSensorPrivacyChangedListener listener = (s, enabled) -> {
                    mCurrentUserCachedState.put(sensor, enabled);
                    dispatchStateChangedLocked(sensor, enabled, CallbackInfo.CURRENT_USER);
                };
                mCurrentUserServiceListeners.put(sensor, listener);
                mSensorPrivacyManager.addSensorPrivacyListener(sensor, listener);
            }
        }
    }

    private void registerListenerIfNeeded(int sensor, int userId) {
        synchronized (mLock) {
            SparseArray<OnSensorPrivacyChangedListener>
                    userServiceListeners = createUserServiceListenersIfNeededLocked(userId);

            if (!userServiceListeners.contains(sensor)) {
                OnSensorPrivacyChangedListener listener = (s, enabled) -> {
                    SparseArray<Boolean> userCachedState =
                            createUserCachedStateIfNeededLocked(userId);
                    userCachedState.put(sensor, enabled);
                    dispatchStateChangedLocked(sensor, enabled, userId);
                };
                mCurrentUserServiceListeners.put(sensor, listener);
                mSensorPrivacyManager.addSensorPrivacyListener(sensor, listener);
            }
        }
    }

    private void dispatchStateChangedLocked(int sensor, boolean blocked, int userId) {
        for (CallbackInfo callbackInfo : mCallbacks) {
            if (callbackInfo.mUserId == userId && callbackInfo.mSensor == sensor) {
                Callback callback = callbackInfo.mCallback;
                Executor executor = callbackInfo.mExecutor;

                executor.execute(() -> callback.onSensorPrivacyChanged(sensor, blocked));
            }
        }
    }

    private SparseArray<Boolean> createUserCachedStateIfNeededLocked(int userId) {
        SparseArray<Boolean> userCachedState = mCachedState.get(userId);
        if (userCachedState == null) {
            userCachedState = new SparseArray<>();
            mCachedState.put(userId, userCachedState);
        }
        return userCachedState;
    }

    private SparseArray<OnSensorPrivacyChangedListener> createUserServiceListenersIfNeededLocked(
            int userId) {
        SparseArray<OnSensorPrivacyChangedListener> userServiceListeners =
                mServiceListeners.get(userId);
        if (userServiceListeners == null) {
            userServiceListeners = new SparseArray<>();
            mServiceListeners.put(userId, userServiceListeners);
        }
        return userServiceListeners;
    }
}
Loading