Make SensorToggleControllers lifecycle aware
We need to watch the lifecycle so that we can unregister callbacks and not cause leaks. This change also rewrites the SensorPrivacyManagerHelper. The previous implmementation was using deprecated apis. It also had an issue where if a callback was added it would not necessarily register alistener with the callback with the service since that was only done when the value is checked. Now we register a listener when the class is instantiated and with the new API there will only be the 1. Finally we impove the tests to have more coverage and test both SensorToggleControllers and the SensorPRivacyManagerHelper class. Test: Use profiler to verify no more leaks SensorToggleControllerTest, SensorPrivacyManagerHelperTest Bug: 244280065 Change-Id: Ibf0bcee455444a104ca6800302907c3dc0de8f1f
This commit is contained in:
@@ -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));
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSensor() {
|
||||
return CAMERA;
|
||||
@VisibleForTesting
|
||||
public CameraToggleController(Context context, String preferenceKey,
|
||||
SensorPrivacyManagerHelper sensorPrivacyManagerHelper) {
|
||||
super(context, preferenceKey, sensorPrivacyManagerHelper, /* ignoreDeviceConfig */ true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailabilityStatus() {
|
||||
return mSensorPrivacyManagerHelper.supportsSensorToggle(getSensor())
|
||||
&& DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, "camera_toggle_enabled",
|
||||
true) ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
|
||||
public int getSensor() {
|
||||
return SENSOR_CAMERA;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDeviceConfigKey() {
|
||||
return "camera_toggle_enabled";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSensor() {
|
||||
return MICROPHONE;
|
||||
public MicToggleController(Context context, String preferenceKey,
|
||||
SensorPrivacyManagerHelper sensorPrivacyManagerHelper) {
|
||||
super(context, preferenceKey, sensorPrivacyManagerHelper, /* ignoreDeviceConfig */ true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailabilityStatus() {
|
||||
return mSensorPrivacyManagerHelper.supportsSensorToggle(getSensor())
|
||||
&& DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, "mic_toggle_enabled",
|
||||
true) ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
|
||||
public int getSensor() {
|
||||
return SENSOR_MICROPHONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDeviceConfigKey() {
|
||||
return "mic_toggle_enabled";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -1,293 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
178
src/com/android/settings/utils/SensorPrivacyManagerHelper.kt
Normal file
178
src/com/android/settings/utils/SensorPrivacyManagerHelper.kt
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.SensorPrivacyChangedParams
|
||||
import android.hardware.SensorPrivacyManager.Sources.SETTINGS
|
||||
import android.util.Log
|
||||
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.
|
||||
*
|
||||
* If you are not a test use [SensorPrivacyManagerHelper.getInstance]
|
||||
*/
|
||||
// This class uses `open` a lot for mockito
|
||||
open class SensorPrivacyManagerHelper(context: Context) :
|
||||
SensorPrivacyManager.OnSensorPrivacyChangedListener {
|
||||
private val sensorPrivacyManager: SensorPrivacyManager
|
||||
private val cache: MutableMap<Pair<Int, Int>, Boolean> = mutableMapOf()
|
||||
private val callbacks: MutableMap<Pair<Int, Int>, MutableSet<Pair<Callback, Executor>>> =
|
||||
mutableMapOf()
|
||||
private val lock = Any()
|
||||
|
||||
/**
|
||||
* Callback for when the state of the sensor privacy changes.
|
||||
*/
|
||||
interface Callback {
|
||||
/**
|
||||
* Method invoked when the sensor privacy changes.
|
||||
* @param sensor The sensor which changed
|
||||
* @param blocked If the sensor is blocked
|
||||
*/
|
||||
fun onSensorPrivacyChanged(toggleType: Int, sensor: Int, blocked: Boolean)
|
||||
}
|
||||
|
||||
init {
|
||||
sensorPrivacyManager = context.getSystemService(SensorPrivacyManager::class.java)!!
|
||||
|
||||
sensorPrivacyManager.addSensorPrivacyListener(context.mainExecutor, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
open fun supportsSensorToggle(sensor: Int): Boolean {
|
||||
return sensorPrivacyManager.supportsSensorToggle(sensor)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
open fun isSensorBlocked(toggleType: Int = TOGGLE_TYPE_ANY, sensor: Int): Boolean {
|
||||
synchronized(lock) {
|
||||
if (toggleType == TOGGLE_TYPE_ANY) {
|
||||
return isSensorBlocked(TOGGLE_TYPE_SOFTWARE, sensor) ||
|
||||
isSensorBlocked(TOGGLE_TYPE_HARDWARE, sensor)
|
||||
}
|
||||
return cache.getOrPut(toggleType to sensor) {
|
||||
sensorPrivacyManager.isSensorPrivacyEnabled(toggleType, sensor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun setSensorBlocked(sensor: Int, blocked: Boolean) {
|
||||
sensorPrivacyManager.setSensorPrivacy(SETTINGS, sensor, blocked)
|
||||
}
|
||||
|
||||
open fun addSensorBlockedListener(executor: Executor?, callback: Callback?) {
|
||||
// Not using defaults for mockito
|
||||
addSensorBlockedListener(SENSOR_ANY, executor, callback)
|
||||
}
|
||||
|
||||
open fun addSensorBlockedListener(sensor: Int, executor: Executor?, callback: Callback?) {
|
||||
// Not using defaults for mockito
|
||||
addSensorBlockedListener(TOGGLE_TYPE_ANY, sensor, executor, callback)
|
||||
}
|
||||
|
||||
open fun addSensorBlockedListener(toggleType: Int, sensor: Int,
|
||||
executor: Executor?, callback: Callback?) {
|
||||
// Note: executor and callback should be nonnull, but we want to use mockito
|
||||
if (toggleType == TOGGLE_TYPE_ANY) {
|
||||
addSensorBlockedListener(TOGGLE_TYPE_SOFTWARE, sensor, executor, callback)
|
||||
addSensorBlockedListener(TOGGLE_TYPE_HARDWARE, sensor, executor, callback)
|
||||
return
|
||||
}
|
||||
|
||||
if (sensor == SENSOR_ANY) {
|
||||
addSensorBlockedListener(toggleType, SENSOR_MICROPHONE, executor, callback)
|
||||
addSensorBlockedListener(toggleType, SENSOR_CAMERA, executor, callback)
|
||||
return
|
||||
}
|
||||
|
||||
synchronized(lock) {
|
||||
callbacks.getOrPut(toggleType to sensor) { mutableSetOf() }
|
||||
.add(callback!! to executor!!)
|
||||
}
|
||||
}
|
||||
|
||||
open fun removeSensorBlockedListener(callback: Callback) {
|
||||
val keysToRemove = mutableListOf<Pair<Int, Int>>()
|
||||
synchronized(lock) {
|
||||
callbacks.forEach { entry ->
|
||||
entry.value.removeIf {
|
||||
it.first == callback
|
||||
}
|
||||
|
||||
if (entry.value.isEmpty()) {
|
||||
keysToRemove.add(entry.key)
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach {
|
||||
callbacks.remove(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TOGGLE_TYPE_SOFTWARE = SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE
|
||||
const val TOGGLE_TYPE_HARDWARE = SensorPrivacyManager.TOGGLE_TYPE_HARDWARE
|
||||
const val SENSOR_MICROPHONE = SensorPrivacyManager.Sensors.MICROPHONE
|
||||
const val SENSOR_CAMERA = SensorPrivacyManager.Sensors.CAMERA
|
||||
|
||||
private const val TOGGLE_TYPE_ANY = -1
|
||||
private const val SENSOR_ANY = -1
|
||||
private var sInstance: SensorPrivacyManagerHelper? = null
|
||||
|
||||
/**
|
||||
* Gets the singleton instance
|
||||
* @param context The context which is needed if the instance hasn't been created
|
||||
* @return the instance
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getInstance(context: Context): SensorPrivacyManagerHelper? {
|
||||
if (sInstance == null) {
|
||||
sInstance = SensorPrivacyManagerHelper(context)
|
||||
}
|
||||
return sInstance
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSensorPrivacyChanged(sensor: Int, enabled: Boolean) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
override fun onSensorPrivacyChanged(params: SensorPrivacyChangedParams) {
|
||||
var changed: Boolean
|
||||
synchronized(lock) {
|
||||
changed = cache.put(params.toggleType to params.sensor, params.isEnabled) !=
|
||||
params.isEnabled
|
||||
|
||||
if (changed) {
|
||||
callbacks[params.toggleType to params.sensor]?.forEach {
|
||||
it.second.execute {
|
||||
it.first.onSensorPrivacyChanged(params.toggleType, params.sensor,
|
||||
params.isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.privacy;
|
||||
|
||||
import static android.hardware.SensorPrivacyManager.Sources.SETTINGS;
|
||||
|
||||
import static com.android.settings.utils.SensorPrivacyManagerHelper.SENSOR_CAMERA;
|
||||
import static com.android.settings.utils.SensorPrivacyManagerHelper.SENSOR_MICROPHONE;
|
||||
import static com.android.settings.utils.SensorPrivacyManagerHelper.TOGGLE_TYPE_HARDWARE;
|
||||
import static com.android.settings.utils.SensorPrivacyManagerHelper.TOGGLE_TYPE_SOFTWARE;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.SensorPrivacyManager;
|
||||
import android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener;
|
||||
import android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener.SensorPrivacyChangedParams;
|
||||
|
||||
import com.android.settings.utils.SensorPrivacyManagerHelper;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoSession;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class SensorPrivacyManagerHelperTest {
|
||||
|
||||
private MockitoSession mMockitoSession;
|
||||
|
||||
/** Execute synchronously */
|
||||
private Executor mExecutor = r -> r.run();
|
||||
|
||||
@Mock
|
||||
private Context mContext;
|
||||
@Mock
|
||||
private SensorPrivacyManager mSensorPrivacyManager;
|
||||
|
||||
private SensorPrivacyManagerHelper mSensorPrivacyManagerHelper;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mMockitoSession = Mockito.mockitoSession()
|
||||
.initMocks(this)
|
||||
.strictness(Strictness.WARN)
|
||||
.startMocking();
|
||||
|
||||
doReturn(mExecutor).when(mContext)
|
||||
.getMainExecutor();
|
||||
doReturn(mSensorPrivacyManager).when(mContext)
|
||||
.getSystemService(eq(SensorPrivacyManager.class));
|
||||
|
||||
mSensorPrivacyManagerHelper = new SensorPrivacyManagerHelper(mContext);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
mMockitoSession.finishMocking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a sensor privacy listener is added in constructor.
|
||||
*/
|
||||
@Test
|
||||
public void constructor_invokeAddSensorPrivacyListener() {
|
||||
verify(mSensorPrivacyManager, times(1)).addSensorPrivacyListener(eq(mExecutor),
|
||||
any(OnSensorPrivacyChangedListener.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when SensorPrivacyManagerHelper#setSensorBlocked(microphone, true) called,
|
||||
* SensorPrivacyManager#setSensorPrivacy(microphone, true) is invoked.
|
||||
*/
|
||||
@Test
|
||||
public void invokeSetMicrophoneBlocked_invokeSetMicrophonePrivacyTrue() {
|
||||
mSensorPrivacyManagerHelper.setSensorBlocked(SENSOR_MICROPHONE, true);
|
||||
verify(mSensorPrivacyManager, times(1))
|
||||
.setSensorPrivacy(eq(SETTINGS), eq(SENSOR_MICROPHONE), eq(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when SensorPrivacyManagerHelper#setSensorBlocked(microphone, false) called,
|
||||
* SensorPrivacyManager#setSensorPrivacy(microphone, false) is invoked.
|
||||
*/
|
||||
@Test
|
||||
public void invokeSetMicrophoneUnBlocked_invokeSetMicrophonePrivacyFalse() {
|
||||
mSensorPrivacyManagerHelper.setSensorBlocked(SENSOR_MICROPHONE, false);
|
||||
verify(mSensorPrivacyManager, times(1))
|
||||
.setSensorPrivacy(eq(SETTINGS), eq(SENSOR_MICROPHONE), eq(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when a callback is added with no toggleType and no sensor filter, then the
|
||||
* callback is invoked on changes to all states.
|
||||
*/
|
||||
@Test
|
||||
public void addCallbackNoFilter_invokeCallback() {
|
||||
SensorPrivacyManager.OnSensorPrivacyChangedListener listener = getServiceListener();
|
||||
|
||||
SensorPrivacyManagerHelper.Callback callback =
|
||||
mock(SensorPrivacyManagerHelper.Callback.class);
|
||||
mSensorPrivacyManagerHelper.addSensorBlockedListener(mExecutor, callback);
|
||||
|
||||
verifyAllCases(listener, (t, s, e, i) -> {
|
||||
verify(callback, times(1)).onSensorPrivacyChanged(eq(t), eq(s), eq(e));
|
||||
verify(callback, times(i + 1)).onSensorPrivacyChanged(anyInt(), anyInt(), anyBoolean());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when a callback is added with a filter to only dispatch microphone events, then the
|
||||
* callback is only invoked on changes to microphone state.
|
||||
*/
|
||||
@Test
|
||||
public void addCallbackMicrophoneOnlyFilter_invokeCallbackMicrophoneOnly() {
|
||||
SensorPrivacyManager.OnSensorPrivacyChangedListener listener = getServiceListener();
|
||||
|
||||
SensorPrivacyManagerHelper.Callback callback =
|
||||
mock(SensorPrivacyManagerHelper.Callback.class);
|
||||
mSensorPrivacyManagerHelper.addSensorBlockedListener(SENSOR_MICROPHONE, mExecutor,
|
||||
callback);
|
||||
mSensorPrivacyManagerHelper.removeSensorBlockedListener(callback);
|
||||
|
||||
verifyAllCases(listener, (t, s, e, i) -> {
|
||||
verify(callback, never()).onSensorPrivacyChanged(anyInt(), eq(SENSOR_MICROPHONE),
|
||||
anyBoolean());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when a callback is added with a filter to only dispatch camera events, then the
|
||||
* callback is only invoked on changes to camera state.
|
||||
*/
|
||||
@Test
|
||||
public void addCallbackCameraOnlyFilter_invokeCallbackCameraOnly() {
|
||||
SensorPrivacyManager.OnSensorPrivacyChangedListener listener = getServiceListener();
|
||||
|
||||
SensorPrivacyManagerHelper.Callback callback =
|
||||
mock(SensorPrivacyManagerHelper.Callback.class);
|
||||
mSensorPrivacyManagerHelper.addSensorBlockedListener(SENSOR_CAMERA, mExecutor,
|
||||
callback);
|
||||
mSensorPrivacyManagerHelper.removeSensorBlockedListener(callback);
|
||||
|
||||
verifyAllCases(listener, (t, s, e, i) -> {
|
||||
verify(callback, never()).onSensorPrivacyChanged(anyInt(), eq(SENSOR_CAMERA),
|
||||
anyBoolean());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when a callback is added with a filter to only dispatch software_toggle+microphone
|
||||
* events, then the callback is only invoked on changes to microphone state.
|
||||
*/
|
||||
@Test
|
||||
public void addCallbackSoftwareMicrophoneOnlyFilter_invokeCallbackSoftwareMicrophoneOnly() {
|
||||
SensorPrivacyManager.OnSensorPrivacyChangedListener listener = getServiceListener();
|
||||
|
||||
SensorPrivacyManagerHelper.Callback callback =
|
||||
mock(SensorPrivacyManagerHelper.Callback.class);
|
||||
mSensorPrivacyManagerHelper.addSensorBlockedListener(TOGGLE_TYPE_SOFTWARE,
|
||||
SENSOR_MICROPHONE, mExecutor, callback);
|
||||
mSensorPrivacyManagerHelper.removeSensorBlockedListener(callback);
|
||||
|
||||
verifyAllCases(listener, (t, s, e, i) -> {
|
||||
verify(callback, never()).onSensorPrivacyChanged(eq(TOGGLE_TYPE_SOFTWARE),
|
||||
eq(SENSOR_MICROPHONE), anyBoolean());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when a callback is added with a filter to only dispatch software_toggle+camera
|
||||
* events, then the callback is only invoked on changes to camera state.
|
||||
*/
|
||||
@Test
|
||||
public void addCallbackSoftwareCameraOnlyFilter_invokeCallbackSoftwareCameraOnly() {
|
||||
SensorPrivacyManager.OnSensorPrivacyChangedListener listener = getServiceListener();
|
||||
|
||||
SensorPrivacyManagerHelper.Callback callback =
|
||||
mock(SensorPrivacyManagerHelper.Callback.class);
|
||||
mSensorPrivacyManagerHelper.addSensorBlockedListener(TOGGLE_TYPE_SOFTWARE,
|
||||
SENSOR_CAMERA, mExecutor, callback);
|
||||
mSensorPrivacyManagerHelper.removeSensorBlockedListener(callback);
|
||||
|
||||
verifyAllCases(listener, (t, s, e, i) -> {
|
||||
verify(callback, never()).onSensorPrivacyChanged(eq(TOGGLE_TYPE_SOFTWARE),
|
||||
eq(SENSOR_CAMERA), anyBoolean());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when a callback is added with a filter to only dispatch hardware_toggle+microphone
|
||||
* events, then the callback is only invoked on changes to microphone state.
|
||||
*/
|
||||
@Test
|
||||
public void addCallbackHardwareMicrophoneOnlyFilter_invokeCallbackHardwareMicrophoneOnly() {
|
||||
SensorPrivacyManager.OnSensorPrivacyChangedListener listener = getServiceListener();
|
||||
|
||||
SensorPrivacyManagerHelper.Callback callback =
|
||||
mock(SensorPrivacyManagerHelper.Callback.class);
|
||||
mSensorPrivacyManagerHelper.addSensorBlockedListener(TOGGLE_TYPE_HARDWARE,
|
||||
SENSOR_MICROPHONE, mExecutor, callback);
|
||||
mSensorPrivacyManagerHelper.removeSensorBlockedListener(callback);
|
||||
|
||||
verifyAllCases(listener, (t, s, e, i) -> {
|
||||
verify(callback, never()).onSensorPrivacyChanged(eq(TOGGLE_TYPE_HARDWARE),
|
||||
eq(SENSOR_MICROPHONE), anyBoolean());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when a callback is added with a filter to only dispatch hardware_toggle+camera
|
||||
* events, then the callback is only invoked on changes to camera state.
|
||||
*/
|
||||
@Test
|
||||
public void addCallbackHardwareCameraOnlyFilter_invokeCallbackHardwareCameraOnly() {
|
||||
SensorPrivacyManager.OnSensorPrivacyChangedListener listener = getServiceListener();
|
||||
|
||||
SensorPrivacyManagerHelper.Callback callback =
|
||||
mock(SensorPrivacyManagerHelper.Callback.class);
|
||||
mSensorPrivacyManagerHelper.addSensorBlockedListener(TOGGLE_TYPE_HARDWARE,
|
||||
SENSOR_CAMERA, mExecutor, callback);
|
||||
mSensorPrivacyManagerHelper.removeSensorBlockedListener(callback);
|
||||
|
||||
verifyAllCases(listener, (t, s, e, i) -> {
|
||||
verify(callback, never()).onSensorPrivacyChanged(eq(TOGGLE_TYPE_HARDWARE),
|
||||
eq(SENSOR_CAMERA), anyBoolean());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify when a callback is removed, then the callback is never invoked on changes to state.
|
||||
*/
|
||||
@Test
|
||||
public void removeCallback_noInvokeCallback() {
|
||||
SensorPrivacyManager.OnSensorPrivacyChangedListener listener = getServiceListener();
|
||||
|
||||
SensorPrivacyManagerHelper.Callback callback =
|
||||
mock(SensorPrivacyManagerHelper.Callback.class);
|
||||
mSensorPrivacyManagerHelper.addSensorBlockedListener(mExecutor, callback);
|
||||
mSensorPrivacyManagerHelper.removeSensorBlockedListener(callback);
|
||||
|
||||
verifyAllCases(listener, (t, s, e, i) -> {
|
||||
verify(callback, never()).onSensorPrivacyChanged(anyInt(), anyInt(), anyBoolean());
|
||||
});
|
||||
}
|
||||
|
||||
private interface Verifier {
|
||||
|
||||
/**
|
||||
* This method should throw in the fail case.
|
||||
*/
|
||||
void verifyCallback(int toggleType, int sensor, boolean isEnabled, int iterationNumber);
|
||||
}
|
||||
|
||||
private void verifyAllCases(SensorPrivacyManager.OnSensorPrivacyChangedListener listener,
|
||||
Verifier verifier) {
|
||||
int[] toggleTypes = {TOGGLE_TYPE_SOFTWARE, TOGGLE_TYPE_HARDWARE};
|
||||
int[] sensors = {SENSOR_MICROPHONE, SENSOR_CAMERA};
|
||||
boolean[] enabledValues = {false, true};
|
||||
|
||||
int i = 0;
|
||||
for (int t : toggleTypes) {
|
||||
for (int s : sensors) {
|
||||
for (boolean e : enabledValues) {
|
||||
listener.onSensorPrivacyChanged(createParams(t, s, e));
|
||||
|
||||
verifier.verifyCallback(t, s, e, i++);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OnSensorPrivacyChangedListener getServiceListener() {
|
||||
ArgumentCaptor<OnSensorPrivacyChangedListener> captor =
|
||||
ArgumentCaptor.forClass(OnSensorPrivacyChangedListener.class);
|
||||
verify(mSensorPrivacyManager).addSensorPrivacyListener(eq(mExecutor),
|
||||
captor.capture());
|
||||
|
||||
OnSensorPrivacyChangedListener listener = captor.getValue();
|
||||
return listener;
|
||||
}
|
||||
|
||||
private SensorPrivacyChangedParams createParams(int toggleType, int sensor, boolean enabled) {
|
||||
SensorPrivacyChangedParams params = mock(SensorPrivacyChangedParams.class);
|
||||
doReturn(toggleType).when(params).getToggleType();
|
||||
doReturn(sensor).when(params).getSensor();
|
||||
doReturn(enabled).when(params).isEnabled();
|
||||
return params;
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
* 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.
|
||||
@@ -16,275 +16,426 @@
|
||||
|
||||
package com.android.settings.privacy;
|
||||
|
||||
import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
|
||||
import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
|
||||
import static android.hardware.SensorPrivacyManager.Sources.OTHER;
|
||||
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
|
||||
import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
|
||||
import static com.android.settings.utils.SensorPrivacyManagerHelper.SENSOR_CAMERA;
|
||||
import static com.android.settings.utils.SensorPrivacyManagerHelper.SENSOR_MICROPHONE;
|
||||
import static com.android.settings.utils.SensorPrivacyManagerHelper.TOGGLE_TYPE_SOFTWARE;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.SensorPrivacyManager;
|
||||
import android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener;
|
||||
import android.util.ArraySet;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseBooleanArray;
|
||||
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.utils.SensorPrivacyManagerHelper;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.MockitoSession;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class SensorToggleControllerTest {
|
||||
|
||||
private MockitoSession mMockitoSession;
|
||||
|
||||
@Mock
|
||||
private Context mContext;
|
||||
@Mock
|
||||
private SensorPrivacyManager mSensorPrivacyManager;
|
||||
private SparseBooleanArray mMicState;
|
||||
private SparseBooleanArray mCamState;
|
||||
private SparseArray<Set<OnSensorPrivacyChangedListener>> mMicListeners;
|
||||
private SparseArray<Set<OnSensorPrivacyChangedListener>> mCamListeners;
|
||||
private SensorPrivacyManagerHelper mSensorPrivacyManagerHelper;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = Mockito.mock(Context.class);
|
||||
mSensorPrivacyManager = Mockito.mock(SensorPrivacyManager.class);
|
||||
mMockitoSession = Mockito.mockitoSession()
|
||||
.initMocks(this)
|
||||
.strictness(Strictness.WARN)
|
||||
.startMocking();
|
||||
|
||||
try {
|
||||
Method clearInstance =
|
||||
SensorPrivacyManagerHelper.class.getDeclaredMethod("clearInstance");
|
||||
clearInstance.setAccessible(true);
|
||||
clearInstance.invoke(null);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
mMicState = new SparseBooleanArray();
|
||||
mCamState = new SparseBooleanArray();
|
||||
mMicState.put(0, false);
|
||||
mCamState.put(0, false);
|
||||
mMicState.put(10, false);
|
||||
mCamState.put(10, false);
|
||||
mMicListeners = new SparseArray<>();
|
||||
mCamListeners = new SparseArray<>();
|
||||
mMicListeners.put(0, new ArraySet<>());
|
||||
mMicListeners.put(10, new ArraySet<>());
|
||||
mCamListeners.put(0, new ArraySet<>());
|
||||
mCamListeners.put(10, new ArraySet<>());
|
||||
|
||||
doReturn(0).when(mContext).getUserId();
|
||||
doReturn(mSensorPrivacyManager).when(mContext)
|
||||
.getSystemService(SensorPrivacyManager.class);
|
||||
|
||||
doAnswer(invocation -> mMicState.get(0))
|
||||
.when(mSensorPrivacyManager).isSensorPrivacyEnabled(eq(MICROPHONE));
|
||||
doAnswer(invocation -> mCamState.get(0))
|
||||
.when(mSensorPrivacyManager).isSensorPrivacyEnabled(eq(CAMERA));
|
||||
doAnswer(invocation -> mMicState.get(invocation.getArgument(1)))
|
||||
.when(mSensorPrivacyManager).isSensorPrivacyEnabled(eq(MICROPHONE), anyInt());
|
||||
doAnswer(invocation -> mCamState.get(invocation.getArgument(1)))
|
||||
.when(mSensorPrivacyManager).isSensorPrivacyEnabled(eq(CAMERA), anyInt());
|
||||
|
||||
doAnswer(invocation -> {
|
||||
mMicState.put(0, invocation.getArgument(2));
|
||||
mMicState.put(10, invocation.getArgument(2));
|
||||
for (OnSensorPrivacyChangedListener listener : mMicListeners.get(0)) {
|
||||
listener.onSensorPrivacyChanged(MICROPHONE, mMicState.get(0));
|
||||
}
|
||||
return null;
|
||||
}).when(mSensorPrivacyManager).setSensorPrivacy(anyInt(), eq(MICROPHONE), anyBoolean());
|
||||
doAnswer(invocation -> {
|
||||
mCamState.put(0, invocation.getArgument(2));
|
||||
mCamState.put(10, invocation.getArgument(2));
|
||||
for (OnSensorPrivacyChangedListener listener : mMicListeners.get(0)) {
|
||||
listener.onSensorPrivacyChanged(CAMERA, mMicState.get(0));
|
||||
}
|
||||
return null;
|
||||
}).when(mSensorPrivacyManager).setSensorPrivacy(anyInt(), eq(CAMERA), anyBoolean());
|
||||
|
||||
doAnswer(invocation -> {
|
||||
mMicState.put(0, invocation.getArgument(2));
|
||||
mMicState.put(10, invocation.getArgument(2));
|
||||
for (OnSensorPrivacyChangedListener listener : mMicListeners.get(0)) {
|
||||
listener.onSensorPrivacyChanged(MICROPHONE, mMicState.get(0));
|
||||
}
|
||||
for (OnSensorPrivacyChangedListener listener : mMicListeners.get(10)) {
|
||||
listener.onSensorPrivacyChanged(MICROPHONE, mMicState.get(10));
|
||||
}
|
||||
return null;
|
||||
}).when(mSensorPrivacyManager)
|
||||
.setSensorPrivacyForProfileGroup(anyInt(), eq(MICROPHONE), anyBoolean());
|
||||
doAnswer(invocation -> {
|
||||
mCamState.put(0, invocation.getArgument(2));
|
||||
mCamState.put(10, invocation.getArgument(2));
|
||||
for (OnSensorPrivacyChangedListener listener : mCamListeners.get(0)) {
|
||||
listener.onSensorPrivacyChanged(CAMERA, mCamState.get(0));
|
||||
}
|
||||
for (OnSensorPrivacyChangedListener listener : mCamListeners.get(10)) {
|
||||
listener.onSensorPrivacyChanged(CAMERA, mCamState.get(10));
|
||||
}
|
||||
return null;
|
||||
}).when(mSensorPrivacyManager)
|
||||
.setSensorPrivacyForProfileGroup(anyInt(), eq(CAMERA), anyBoolean());
|
||||
|
||||
doAnswer(invocation -> mMicListeners.get(0).add(invocation.getArgument(1)))
|
||||
.when(mSensorPrivacyManager).addSensorPrivacyListener(eq(MICROPHONE), any());
|
||||
doAnswer(invocation -> mCamListeners.get(0).add(invocation.getArgument(1)))
|
||||
.when(mSensorPrivacyManager).addSensorPrivacyListener(eq(CAMERA), any());
|
||||
|
||||
doAnswer(invocation -> mMicListeners.get(invocation.getArgument(2))
|
||||
.add(invocation.getArgument(1))).when(mSensorPrivacyManager)
|
||||
.addSensorPrivacyListener(eq(MICROPHONE), anyInt(), any());
|
||||
doAnswer(invocation -> mCamListeners.get(invocation.getArgument(2))
|
||||
.add(invocation.getArgument(1))).when(mSensorPrivacyManager)
|
||||
.addSensorPrivacyListener(eq(CAMERA), anyInt(), any());
|
||||
doReturn((Executor) r -> r.run()).when(mContext).getMainExecutor();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
mMockitoSession.finishMocking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the availability status when mic toggle is not supported.
|
||||
*/
|
||||
@Test
|
||||
public void getAvailabilityStatus_MicrophoneToggleNotSupported_returnUnsupported() {
|
||||
// Return not supported
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).supportsSensorToggle(SENSOR_MICROPHONE);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
// Verify not available
|
||||
assertEquals(UNSUPPORTED_ON_DEVICE, micToggleController.getAvailabilityStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the availability status when mic toggle is supported.
|
||||
*/
|
||||
@Test
|
||||
public void getAvailabilityStatus_MicrophoneToggleSupported_returnAvailable() {
|
||||
// Return supported
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).supportsSensorToggle(SENSOR_MICROPHONE);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
// Verify available
|
||||
assertEquals(AVAILABLE, micToggleController.getAvailabilityStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the initial state shows mic unblocked when created.
|
||||
*/
|
||||
@Test
|
||||
public void isChecked_disableMicrophoneSensorPrivacy_returnTrue() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, false);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle");
|
||||
// Starts off unblocked
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_MICROPHONE);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
// Verify the controller is checked
|
||||
assertTrue(micToggleController.isChecked());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the initial state shows mic blocked when created.
|
||||
*/
|
||||
@Test
|
||||
public void isChecked_enableMicrophoneSensorPrivacy_returnFalse() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, true);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle");
|
||||
// Starts off blocked
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_MICROPHONE);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
// Verify the controller is unchecked
|
||||
assertFalse(micToggleController.isChecked());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startMicrophoneToggleController_invokeAddSensorBlockedListener() {
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
micToggleController.onStart();
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.addSensorBlockedListener(eq(SENSOR_MICROPHONE), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stopMicrophoneToggleController_invokeRemoveSensorBlockedListener() {
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
micToggleController.onStart();
|
||||
|
||||
ArgumentCaptor<SensorPrivacyManagerHelper.Callback> callbackCaptor =
|
||||
ArgumentCaptor.forClass(SensorPrivacyManagerHelper.Callback.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.addSensorBlockedListener(eq(SENSOR_MICROPHONE), any(), callbackCaptor.capture());
|
||||
|
||||
micToggleController.onStop();
|
||||
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.removeSensorBlockedListener(callbackCaptor.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the state of the mic controller switches from unblocked to blocked when state is changed
|
||||
* externally.
|
||||
*/
|
||||
@Test
|
||||
public void isChecked_disableMicrophoneSensorPrivacyThenChanged_returnFalse() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, false);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle");
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, true);
|
||||
// Starts off unblocked
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_MICROPHONE);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
// Preference is started
|
||||
micToggleController.displayPreference(mock(PreferenceScreen.class));
|
||||
micToggleController.onStart();
|
||||
|
||||
ArgumentCaptor<SensorPrivacyManagerHelper.Callback> callbackCaptor =
|
||||
ArgumentCaptor.forClass(SensorPrivacyManagerHelper.Callback.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.addSensorBlockedListener(eq(SENSOR_MICROPHONE), any(), callbackCaptor.capture());
|
||||
|
||||
// The state changed externally, update return value of isSensorBlocked and invoke callback
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_MICROPHONE);
|
||||
callbackCaptor.getValue()
|
||||
.onSensorPrivacyChanged(TOGGLE_TYPE_SOFTWARE, SENSOR_MICROPHONE, true);
|
||||
assertFalse(micToggleController.isChecked());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test the state of the mic controller switches from blocked to unblocked when state is changed
|
||||
* externally.
|
||||
*/
|
||||
@Test
|
||||
public void isChecked_enableMicrophoneSensorPrivacyThenChanged_returnTrue() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, true);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle");
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, false);
|
||||
// Starts off blocked
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_MICROPHONE);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
// Preference is started
|
||||
micToggleController.displayPreference(mock(PreferenceScreen.class));
|
||||
micToggleController.onStart();
|
||||
|
||||
// The state changed externally, update return value of isSensorBlocked and invoke callback
|
||||
ArgumentCaptor<SensorPrivacyManagerHelper.Callback> callbackCaptor =
|
||||
ArgumentCaptor.forClass(SensorPrivacyManagerHelper.Callback.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.addSensorBlockedListener(eq(SENSOR_MICROPHONE), any(), callbackCaptor.capture());
|
||||
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_MICROPHONE);
|
||||
callbackCaptor.getValue()
|
||||
.onSensorPrivacyChanged(TOGGLE_TYPE_SOFTWARE, SENSOR_MICROPHONE, false);
|
||||
assertTrue(micToggleController.isChecked());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test the mic controller requests to block the mic when unblocked on invocation of setChecked.
|
||||
*/
|
||||
@Test
|
||||
public void isMicrophoneSensorPrivacyEnabled_uncheckMicToggle_returnTrue() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, false);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle");
|
||||
public void blocked_uncheckMicToggle_returnTrue() {
|
||||
// Starts off unblocked
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_MICROPHONE);
|
||||
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
verify(mSensorPrivacyManagerHelper, never()).setSensorBlocked(anyInt(), anyBoolean());
|
||||
|
||||
// User set blocked
|
||||
micToggleController.setChecked(false);
|
||||
assertTrue(mMicState.get(0));
|
||||
|
||||
ArgumentCaptor<Boolean> blockedResult = ArgumentCaptor.forClass(Boolean.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.setSensorBlocked(ArgumentMatchers.eq(SENSOR_MICROPHONE), blockedResult.capture());
|
||||
|
||||
// Verify attempt to block
|
||||
assertTrue(blockedResult.getValue());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Test the mic controller requests to unblock the mic when blocked on invocation of setChecked.
|
||||
*/
|
||||
@Test
|
||||
public void isMicrophoneSensorPrivacyEnabled_checkMicToggle_returnFalse() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, true);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle");
|
||||
public void blocked_checkMicToggle_returnFalse() {
|
||||
// Starts off blocked
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_MICROPHONE);
|
||||
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle",
|
||||
mSensorPrivacyManagerHelper);
|
||||
verify(mSensorPrivacyManagerHelper, never()).setSensorBlocked(anyInt(), anyBoolean());
|
||||
|
||||
// User set unblocked
|
||||
micToggleController.setChecked(true);
|
||||
assertFalse(mMicState.get(0));
|
||||
|
||||
ArgumentCaptor<Boolean> blockedResult = ArgumentCaptor.forClass(Boolean.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.setSensorBlocked(ArgumentMatchers.eq(SENSOR_MICROPHONE), blockedResult.capture());
|
||||
|
||||
// Verify attempt to unblock
|
||||
assertFalse(blockedResult.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the availability status when cam toggle is not supported.
|
||||
*/
|
||||
@Test
|
||||
public void isMicrophoneSensorPrivacyEnabledForProfileUser_uncheckMicToggle_returnTrue() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, false);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle");
|
||||
micToggleController.setChecked(false);
|
||||
assertTrue(mMicState.get(10));
|
||||
public void getAvailabilityStatus_CameraToggleNotSupported_returnUnsupported() {
|
||||
// Return not supported
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).supportsSensorToggle(SENSOR_CAMERA);
|
||||
CameraToggleController cameraToggleController = new CameraToggleController(mContext,
|
||||
"cam_toggle", mSensorPrivacyManagerHelper);
|
||||
// Verify not available
|
||||
assertEquals(UNSUPPORTED_ON_DEVICE, cameraToggleController.getAvailabilityStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the availability status when cam toggle is supported.
|
||||
*/
|
||||
@Test
|
||||
public void isMicrophoneSensorPrivacyEnabledProfileUser_checkMicToggle_returnFalse() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, MICROPHONE, true);
|
||||
MicToggleController micToggleController = new MicToggleController(mContext, "mic_toggle");
|
||||
micToggleController.setChecked(true);
|
||||
assertFalse(mMicState.get(10));
|
||||
public void getAvailabilityStatus_CameraToggleSupported_returnAvailable() {
|
||||
// Return supported
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).supportsSensorToggle(SENSOR_CAMERA);
|
||||
CameraToggleController cameraToggleController = new CameraToggleController(mContext,
|
||||
"cam_toggle", mSensorPrivacyManagerHelper);
|
||||
// Verify available
|
||||
assertEquals(AVAILABLE, cameraToggleController.getAvailabilityStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the initial state shows cam unblocked when created.
|
||||
*/
|
||||
@Test
|
||||
public void isChecked_disableCameraSensorPrivacy_returnTrue() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, false);
|
||||
CameraToggleController camToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle");
|
||||
assertTrue(camToggleController.isChecked());
|
||||
// Starts off unblocked
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_CAMERA);
|
||||
CameraToggleController cameraToggleController = new CameraToggleController(mContext,
|
||||
"cam_toggle", mSensorPrivacyManagerHelper);
|
||||
// Verify the controller is checked
|
||||
assertTrue(cameraToggleController.isChecked());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the initial state shows cam blocked when created.
|
||||
*/
|
||||
@Test
|
||||
public void isChecked_enableCameraSensorPrivacy_returnFalse() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, true);
|
||||
CameraToggleController camToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle");
|
||||
assertFalse(camToggleController.isChecked());
|
||||
// Starts off blocked
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_CAMERA);
|
||||
CameraToggleController cameraToggleController = new CameraToggleController(mContext,
|
||||
"cam_toggle", mSensorPrivacyManagerHelper);
|
||||
// Verify the controller is unchecked
|
||||
assertFalse(cameraToggleController.isChecked());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startCameraToggleController_invokeAddSensorBlockedListener() {
|
||||
CameraToggleController cameraToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle", mSensorPrivacyManagerHelper);
|
||||
cameraToggleController.onStart();
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.addSensorBlockedListener(eq(SENSOR_CAMERA), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stopCameraToggleController_invokeRemoveSensorBlockedListener() {
|
||||
CameraToggleController cameraToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle", mSensorPrivacyManagerHelper);
|
||||
cameraToggleController.onStart();
|
||||
|
||||
ArgumentCaptor<SensorPrivacyManagerHelper.Callback> callbackCaptor =
|
||||
ArgumentCaptor.forClass(SensorPrivacyManagerHelper.Callback.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.addSensorBlockedListener(eq(SENSOR_CAMERA), any(), callbackCaptor.capture());
|
||||
|
||||
cameraToggleController.onStop();
|
||||
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.removeSensorBlockedListener(callbackCaptor.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the state of the cam controller switches from unblocked to blocked when state is changed
|
||||
* externally.
|
||||
*/
|
||||
@Test
|
||||
public void isChecked_disableCameraSensorPrivacyThenChanged_returnFalse() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, false);
|
||||
CameraToggleController camToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle");
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, true);
|
||||
assertFalse(camToggleController.isChecked());
|
||||
// Starts off unblocked
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_CAMERA);
|
||||
CameraToggleController cameraToggleController = new CameraToggleController(mContext,
|
||||
"cam_toggle", mSensorPrivacyManagerHelper);
|
||||
// Preference is started
|
||||
cameraToggleController.displayPreference(mock(PreferenceScreen.class));
|
||||
cameraToggleController.onStart();
|
||||
|
||||
ArgumentCaptor<SensorPrivacyManagerHelper.Callback> callbackCaptor =
|
||||
ArgumentCaptor.forClass(SensorPrivacyManagerHelper.Callback.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.addSensorBlockedListener(eq(SENSOR_CAMERA), any(), callbackCaptor.capture());
|
||||
|
||||
// The state changed externally, update return value of isSensorBlocked and invoke callback
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_CAMERA);
|
||||
callbackCaptor.getValue().onSensorPrivacyChanged(TOGGLE_TYPE_SOFTWARE, SENSOR_CAMERA, true);
|
||||
assertFalse(cameraToggleController.isChecked());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test the state of the cam controller switches from blocked to unblocked when state is changed
|
||||
* externally.
|
||||
*/
|
||||
@Test
|
||||
public void isChecked_enableCameraSensorPrivacyThenChanged_returnTrue() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, true);
|
||||
CameraToggleController camToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle");
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, false);
|
||||
assertTrue(camToggleController.isChecked());
|
||||
// Starts off blocked
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_CAMERA);
|
||||
CameraToggleController cameraToggleController = new CameraToggleController(mContext,
|
||||
"cam_toggle", mSensorPrivacyManagerHelper);
|
||||
// Preference is started
|
||||
cameraToggleController.displayPreference(mock(PreferenceScreen.class));
|
||||
cameraToggleController.onStart();
|
||||
|
||||
// The state changed externally, update return value of isSensorBlocked and invoke callback
|
||||
ArgumentCaptor<SensorPrivacyManagerHelper.Callback> callbackCaptor =
|
||||
ArgumentCaptor.forClass(SensorPrivacyManagerHelper.Callback.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.addSensorBlockedListener(eq(SENSOR_CAMERA), any(), callbackCaptor.capture());
|
||||
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_CAMERA);
|
||||
callbackCaptor.getValue()
|
||||
.onSensorPrivacyChanged(TOGGLE_TYPE_SOFTWARE, SENSOR_CAMERA, false);
|
||||
assertTrue(cameraToggleController.isChecked());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test the cam controller requests to block the cam when unblocked on invocation of setChecked.
|
||||
*/
|
||||
@Test
|
||||
public void isCameraSensorPrivacyEnabled_uncheckCanToggle_returnTrue() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, false);
|
||||
CameraToggleController camToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle");
|
||||
camToggleController.setChecked(false);
|
||||
assertTrue(mCamState.get(0));
|
||||
public void blocked_uncheckCamToggle_returnTrue() {
|
||||
// Starts off unblocked
|
||||
doReturn(false).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_CAMERA);
|
||||
|
||||
CameraToggleController cameraToggleController = new CameraToggleController(mContext,
|
||||
"cam_toggle", mSensorPrivacyManagerHelper);
|
||||
verify(mSensorPrivacyManagerHelper, never()).setSensorBlocked(anyInt(), anyBoolean());
|
||||
|
||||
// User set blocked
|
||||
cameraToggleController.setChecked(false);
|
||||
|
||||
ArgumentCaptor<Boolean> blockedResult = ArgumentCaptor.forClass(Boolean.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.setSensorBlocked(ArgumentMatchers.eq(SENSOR_CAMERA), blockedResult.capture());
|
||||
|
||||
// Verify attempt to block
|
||||
assertTrue(blockedResult.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isCameraSensorPrivacyEnabled_checkCamToggle_returnFalse() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, true);
|
||||
CameraToggleController camToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle");
|
||||
camToggleController.setChecked(true);
|
||||
assertFalse(mCamState.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isCameraSensorPrivacyEnabledForProfileUser_uncheckCamToggle_returnTrue() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, false);
|
||||
CameraToggleController camToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle");
|
||||
camToggleController.setChecked(false);
|
||||
assertTrue(mCamState.get(10));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the cam controller requests to unblock the cam when blocked on invocation of setChecked.
|
||||
*/
|
||||
@Test
|
||||
public void isCameraSensorPrivacyEnabledProfileUser_checkCamToggle_returnFalse() {
|
||||
mSensorPrivacyManager.setSensorPrivacy(OTHER, CAMERA, true);
|
||||
CameraToggleController camToggleController =
|
||||
new CameraToggleController(mContext, "cam_toggle");
|
||||
camToggleController.setChecked(true);
|
||||
assertFalse(mCamState.get(10));
|
||||
public void blocked_checkCamToggle_returnFalse() {
|
||||
// Starts off blocked
|
||||
doReturn(true).when(mSensorPrivacyManagerHelper).isSensorBlocked(SENSOR_CAMERA);
|
||||
|
||||
CameraToggleController cameraToggleController = new CameraToggleController(mContext,
|
||||
"cam_toggle", mSensorPrivacyManagerHelper);
|
||||
verify(mSensorPrivacyManagerHelper, never()).setSensorBlocked(anyInt(), anyBoolean());
|
||||
|
||||
// User set unblocked
|
||||
cameraToggleController.setChecked(true);
|
||||
|
||||
ArgumentCaptor<Boolean> blockedResult = ArgumentCaptor.forClass(Boolean.class);
|
||||
verify(mSensorPrivacyManagerHelper, times(1))
|
||||
.setSensorBlocked(ArgumentMatchers.eq(SENSOR_CAMERA), blockedResult.capture());
|
||||
|
||||
// Verify attempt to unblock
|
||||
assertFalse(blockedResult.getValue());
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user