Merge "Make SensorToggleControllers lifecycle aware"

This commit is contained in:
Evan Severson
2022-10-07 19:11:16 +00:00
committed by Android (Google) Code Review
8 changed files with 921 additions and 508 deletions

View File

@@ -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));

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View 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)
}
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}