diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java index 259829643ef..7698d9a23f9 100644 --- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java +++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java @@ -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)); diff --git a/src/com/android/settings/privacy/CameraToggleController.java b/src/com/android/settings/privacy/CameraToggleController.java index 756553c4bf6..4e53ef98c10 100644 --- a/src/com/android/settings/privacy/CameraToggleController.java +++ b/src/com/android/settings/privacy/CameraToggleController.java @@ -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 diff --git a/src/com/android/settings/privacy/MicToggleController.java b/src/com/android/settings/privacy/MicToggleController.java index c2389d2439e..782c11692bd 100644 --- a/src/com/android/settings/privacy/MicToggleController.java +++ b/src/com/android/settings/privacy/MicToggleController.java @@ -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 diff --git a/src/com/android/settings/privacy/SensorToggleController.java b/src/com/android/settings/privacy/SensorToggleController.java index 53b1a2f65e0..7aa9f9f4ce8 100644 --- a/src/com/android/settings/privacy/SensorToggleController.java +++ b/src/com/android/settings/privacy/SensorToggleController.java @@ -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); + } } diff --git a/src/com/android/settings/utils/SensorPrivacyManagerHelper.java b/src/com/android/settings/utils/SensorPrivacyManagerHelper.java deleted file mode 100644 index b0300427298..00000000000 --- a/src/com/android/settings/utils/SensorPrivacyManagerHelper.java +++ /dev/null @@ -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 mCurrentUserCachedState = new SparseArray<>(); - private final SparseArray> mCachedState = new SparseArray<>(); - - private final SparseArray - mCurrentUserServiceListeners = new SparseArray<>(); - private final SparseArray> - mServiceListeners = new SparseArray<>(); - - private final ArraySet 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 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 - userServiceListeners = createUserServiceListenersIfNeededLocked(userId); - - if (!userServiceListeners.contains(sensor)) { - OnSensorPrivacyChangedListener listener = (s, enabled) -> { - SparseArray 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 createUserCachedStateIfNeededLocked(int userId) { - SparseArray userCachedState = mCachedState.get(userId); - if (userCachedState == null) { - userCachedState = new SparseArray<>(); - mCachedState.put(userId, userCachedState); - } - return userCachedState; - } - - private SparseArray createUserServiceListenersIfNeededLocked( - int userId) { - SparseArray userServiceListeners = - mServiceListeners.get(userId); - if (userServiceListeners == null) { - userServiceListeners = new SparseArray<>(); - mServiceListeners.put(userId, userServiceListeners); - } - return userServiceListeners; - } -} diff --git a/src/com/android/settings/utils/SensorPrivacyManagerHelper.kt b/src/com/android/settings/utils/SensorPrivacyManagerHelper.kt new file mode 100644 index 00000000000..3fe3ab0d50c --- /dev/null +++ b/src/com/android/settings/utils/SensorPrivacyManagerHelper.kt @@ -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, Boolean> = mutableMapOf() + private val callbacks: MutableMap, MutableSet>> = + 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>() + 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) + } + } + } + } + } +} diff --git a/tests/robotests/src/com/android/settings/privacy/SensorPrivacyManagerHelperTest.java b/tests/robotests/src/com/android/settings/privacy/SensorPrivacyManagerHelperTest.java new file mode 100644 index 00000000000..71c613c06ed --- /dev/null +++ b/tests/robotests/src/com/android/settings/privacy/SensorPrivacyManagerHelperTest.java @@ -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 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; + } +} diff --git a/tests/robotests/src/com/android/settings/privacy/SensorToggleControllerTest.java b/tests/robotests/src/com/android/settings/privacy/SensorToggleControllerTest.java index 2c04e3bd004..9f0bb736a2c 100644 --- a/tests/robotests/src/com/android/settings/privacy/SensorToggleControllerTest.java +++ b/tests/robotests/src/com/android/settings/privacy/SensorToggleControllerTest.java @@ -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> mMicListeners; - private SparseArray> 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 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 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 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 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 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 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 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 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 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 blockedResult = ArgumentCaptor.forClass(Boolean.class); + verify(mSensorPrivacyManagerHelper, times(1)) + .setSensorBlocked(ArgumentMatchers.eq(SENSOR_CAMERA), blockedResult.capture()); + + // Verify attempt to unblock + assertFalse(blockedResult.getValue()); } }