From 5ba40657039a94812adb55cd0c8130a60bbbecd5 Mon Sep 17 00:00:00 2001 From: Evan Severson Date: Thu, 1 Sep 2022 14:08:38 -0700 Subject: [PATCH 1/6] Make SensorToggleControllers lifecycle aware We need to watch the lifecycle so that we can unregister callbacks and not cause leaks. This change also rewrites the SensorPrivacyManagerHelper. The previous implmementation was using deprecated apis. It also had an issue where if a callback was added it would not necessarily register alistener with the callback with the service since that was only done when the value is checked. Now we register a listener when the class is instantiated and with the new API there will only be the 1. Finally we impove the tests to have more coverage and test both SensorToggleControllers and the SensorPRivacyManagerHelper class. Test: Use profiler to verify no more leaks SensorToggleControllerTest, SensorPrivacyManagerHelperTest Bug: 244280065 Change-Id: Ibf0bcee455444a104ca6800302907c3dc0de8f1f --- .../face/FaceEnrollIntroduction.java | 4 +- .../privacy/CameraToggleController.java | 26 +- .../settings/privacy/MicToggleController.java | 22 +- .../privacy/SensorToggleController.java | 71 ++- .../utils/SensorPrivacyManagerHelper.java | 293 ---------- .../utils/SensorPrivacyManagerHelper.kt | 178 ++++++ .../SensorPrivacyManagerHelperTest.java | 320 +++++++++++ .../privacy/SensorToggleControllerTest.java | 515 +++++++++++------- 8 files changed, 921 insertions(+), 508 deletions(-) delete mode 100644 src/com/android/settings/utils/SensorPrivacyManagerHelper.java create mode 100644 src/com/android/settings/utils/SensorPrivacyManagerHelper.kt create mode 100644 tests/robotests/src/com/android/settings/privacy/SensorPrivacyManagerHelperTest.java 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()); } } From 078ecbd765abca3a2a78c5af16b80796b82d3a90 Mon Sep 17 00:00:00 2001 From: SongFerngWang Date: Tue, 27 Sep 2022 20:45:17 +0800 Subject: [PATCH 2/6] [MEP] psim->esim, don't skip carrierConfigChanged with subId -1 After simSlotMapping from psim to esim, since the esim can not be enabled by the modem, the UI start to enable the esim. Therefore, when receiver receive the first carrier config changed, UI can exit waiting state, and then start to enable esim. Bug: 246556280 Test: Build pass. Verify by QA Change-Id: I4b8b2f3d1dd014fc1ddb92ebffa614d6daf9d142 --- .../network/CarrierConfigChangedReceiver.java | 16 ++++++++++++++-- .../android/settings/network/UiccSlotUtil.java | 11 +++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/com/android/settings/network/CarrierConfigChangedReceiver.java b/src/com/android/settings/network/CarrierConfigChangedReceiver.java index 8a6d47d96fc..d9ff03efb6a 100644 --- a/src/com/android/settings/network/CarrierConfigChangedReceiver.java +++ b/src/com/android/settings/network/CarrierConfigChangedReceiver.java @@ -32,9 +32,20 @@ public class CarrierConfigChangedReceiver extends BroadcastReceiver { CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED; private final CountDownLatch mLatch; + private final boolean mIsWaitingForValidSubId; - public CarrierConfigChangedReceiver(CountDownLatch latch) { + /** + * This is the CarrierConfigChanged receiver. If it receives the carrier config changed, then it + * call the CountDownLatch.countDown(). + * If the "isWaitingForValidSubId" is true, then the receiver skip the carrier config changed + * with the subId = -1. The receiver executes the countDown when the CarrierConfigChanged + * with valid subId. + * If the "isWaitingForValidSubId" is false, then the receiver executes the countDown when + * receiving any CarrierConfigChanged. + */ + public CarrierConfigChangedReceiver(CountDownLatch latch, boolean isWaitingForValidSubId) { mLatch = latch; + mIsWaitingForValidSubId = isWaitingForValidSubId; } public void registerOn(Context context) { @@ -53,7 +64,8 @@ public class CarrierConfigChangedReceiver extends BroadcastReceiver { } private void checkSubscriptionIndex(Intent intent) { - if (intent.hasExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX)) { + if (intent.hasExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX) + || !mIsWaitingForValidSubId) { int subId = intent.getIntExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, -1); Log.i(TAG, "subId from config changed: " + subId); mLatch.countDown(); diff --git a/src/com/android/settings/network/UiccSlotUtil.java b/src/com/android/settings/network/UiccSlotUtil.java index 7ba2e0f8ed0..7d8295b7822 100644 --- a/src/com/android/settings/network/UiccSlotUtil.java +++ b/src/com/android/settings/network/UiccSlotUtil.java @@ -141,7 +141,8 @@ public class UiccSlotUtil { inactiveRemovableSlot, /*removable sim's port Id*/ TelephonyManager.DEFAULT_PORT_INDEX, excludedLogicalSlotIndex), - context); + context, + /*isWaitingForValidSubId=*/ true); } /** @@ -179,7 +180,8 @@ public class UiccSlotUtil { performSwitchToSlot(telMgr, prepareUiccSlotMappings(uiccSlotMappings, /*slot is not psim*/ false, physicalSlotId, port, excludedLogicalSlotIndex), - context); + context, + /*isWaitingForValidSubId=*/ false); } /** @@ -231,7 +233,8 @@ public class UiccSlotUtil { } private static void performSwitchToSlot(TelephonyManager telMgr, - Collection uiccSlotMappings, Context context) + Collection uiccSlotMappings, Context context, + boolean isWaitingForValidSubId) throws UiccSlotsException { CarrierConfigChangedReceiver receiver = null; long waitingTimeMillis = @@ -241,7 +244,7 @@ public class UiccSlotUtil { DEFAULT_WAIT_AFTER_SWITCH_TIMEOUT_MILLIS); try { CountDownLatch latch = new CountDownLatch(1); - receiver = new CarrierConfigChangedReceiver(latch); + receiver = new CarrierConfigChangedReceiver(latch, isWaitingForValidSubId); receiver.registerOn(context); telMgr.setSimSlotMapping(uiccSlotMappings); latch.await(waitingTimeMillis, TimeUnit.MILLISECONDS); From 5f0c62f6bc7a31ef6d726a0f0acfb6431296e94f Mon Sep 17 00:00:00 2001 From: Howard Chen Date: Thu, 6 Oct 2022 18:15:43 +0800 Subject: [PATCH 3/6] Resize the DSULoader dialog dynamically Bug: 246686308 Test: Open the DSULoader and check the space Change-Id: I1f71e6874a322be5456ae4dd9fe9bcca792bd593 --- .../settings/development/DSULoader.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/development/DSULoader.java b/src/com/android/settings/development/DSULoader.java index d4fa0c64646..fede67a7766 100644 --- a/src/com/android/settings/development/DSULoader.java +++ b/src/com/android/settings/development/DSULoader.java @@ -26,6 +26,7 @@ import android.text.TextUtils; import android.util.Slog; import android.view.LayoutInflater; import android.view.View; +import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; @@ -117,6 +118,20 @@ public class DSULoader extends ListActivity { } } } + + private void resizeListView() { + ListView view = getListView(); + View item_view = mAdapter.getView(0, null, view); + item_view.measure( + MeasureSpec.makeMeasureSpec(view.getWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + ViewGroup.LayoutParams params = view.getLayoutParams(); + params.height = item_view.getMeasuredHeight() * (mAdapter.getCount() + 1); + Slog.e(TAG, "resizeListView height=" + params.height); + view.setLayoutParams(params); + view.requestLayout(); + } + // Fetcher fetches mDSUList in backgroud private class Fetcher implements Runnable { private URL mDsuList; @@ -172,6 +187,9 @@ public class DSULoader extends ListActivity { public void run() { try { fetch(mDsuList); + if (mDSUList.size() == 0) { + mDSUList.add(0, "No DSU available for this device"); + } } catch (IOException e) { Slog.e(TAG, e.toString()); mDSUList.add(0, "Network Error"); @@ -179,14 +197,12 @@ public class DSULoader extends ListActivity { Slog.e(TAG, e.toString()); mDSUList.add(0, "Metadata Error"); } - if (mDSUList.size() == 0) { - mDSUList.add(0, "No DSU available for this device"); - } runOnUiThread( new Runnable() { public void run() { mAdapter.clear(); mAdapter.addAll(mDSUList); + resizeListView(); } }); } From 4c8bae742bd608f2c0cd8b942e58de443b4f3f7c Mon Sep 17 00:00:00 2001 From: Jared Duke Date: Mon, 19 Sep 2022 14:57:35 -0700 Subject: [PATCH 4/6] Refine Settings proguard rules Avoid keeping unused library classes and remove several redundant rules. This saves ~700KB dex and a modest amount of Jetpack resources that are no longer referenced. Bug: 248580093 Test: m + presubmit + atest SettingsUnitTests Change-Id: Ic2398345a35117ccab433d2fd1c31c0f453ed77c --- proguard.flags | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/proguard.flags b/proguard.flags index 5482f1c52d0..492404ce7be 100644 --- a/proguard.flags +++ b/proguard.flags @@ -6,7 +6,7 @@ -keepattributes Exceptions # Keep all Fragments in this package, which are used by reflection. --keep public class com.android.settings.** extends androidx.fragment.app.Fragment +-keep public class com.android.settings*.** extends androidx.fragment.app.Fragment # Keep all preference controllers needed by slice and DashboardFragment. -keep class * extends com.android.settings.core.BasePreferenceController { @@ -18,13 +18,13 @@ } # We want to keep methods in Activity that could be used in the XML attribute onClick. --keepclassmembers class * extends android.app.Activity { +-keepclassmembers class com.android.settings*.** extends android.app.Activity { public void *(android.view.View); public void *(android.view.MenuItem); } # Keep setters in Views so that animations can still work. --keep public class * extends android.view.View { +-keep public class com.android.settings*.** extends android.view.View { public (android.content.Context); public (android.content.Context, android.util.AttributeSet); public (android.content.Context, android.util.AttributeSet, int); @@ -34,22 +34,16 @@ } # Keep classes that may be inflated from XML. --keepclasseswithmembers class * { +-keepclasseswithmembers class com.android.settings*.** { public (android.content.Context, android.util.AttributeSet); } --keepclasseswithmembers class * { +-keepclasseswithmembers class com.android.settings*.** { public (android.content.Context, android.util.AttributeSet, int); } --keepclasseswithmembers class * { +-keepclasseswithmembers class com.android.settings*.** { public (android.content.Context, android.util.AttributeSet, int, int); } -# Keep annotated classes or class members. --keep @androidx.annotation.Keep class * --keepclassmembers class * { - @androidx.annotation.Keep *; -} - # Keep specific fields used via reflection. -keepclassmembers class * { public static ** SEARCH_INDEX_DATA_PROVIDER; From a81bb196186327ab5b328950b6fd0372cf561ca9 Mon Sep 17 00:00:00 2001 From: Weng Su Date: Fri, 7 Oct 2022 02:08:57 +0800 Subject: [PATCH 5/6] Show policy transparency dialog if adding Wi-Fi config is not allowed - When the user is restricted by UserManager.DISALLOW_ADD_WIFI_CONFIG, clicking on a Wi-Fi network will show the Settings policy transparency dialog. - The "Not allowed by your organization" summary in Wi-Fi networks will be removed in a separate CL. Bug: 233871529 Test: manual test make RunSettingsRoboTests ROBOTEST_FILTER=Wifi Change-Id: Ia978be8e85a5809de1331f3e0edc11fbc8925c80 --- .../wifi/AddWifiNetworkPreference.java | 20 ++------- .../settings/wifi/WifiEntryPreference.java | 8 ++-- .../wifi/AddWifiNetworkPreferenceTest.java | 41 ++++++++----------- .../ConnectedWifiEntryPreferenceTest.java | 2 + .../LongPressWifiEntryPreferenceTest.java | 2 + .../wifi/ShadowRestrictedPreference.java | 31 ++++++++++++++ .../wifi/WifiEntryPreferenceTest.java | 2 + .../settings/wifi/WifiSettingsTest.java | 1 + .../wifi/dpp/WifiNetworkListFragmentTest.java | 3 ++ 9 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/wifi/ShadowRestrictedPreference.java diff --git a/src/com/android/settings/wifi/AddWifiNetworkPreference.java b/src/com/android/settings/wifi/AddWifiNetworkPreference.java index 18205391525..6aafbcacfb1 100644 --- a/src/com/android/settings/wifi/AddWifiNetworkPreference.java +++ b/src/com/android/settings/wifi/AddWifiNetworkPreference.java @@ -19,28 +19,25 @@ package com.android.settings.wifi; import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.os.UserManager; import android.util.Log; import android.widget.ImageButton; import androidx.annotation.DrawableRes; -import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; -import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.wifi.dpp.WifiDppUtils; -import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils; +import com.android.settingslib.RestrictedPreference; /** * The Preference for users to add Wi-Fi networks in WifiSettings */ -public class AddWifiNetworkPreference extends Preference { +public class AddWifiNetworkPreference extends RestrictedPreference { private static final String TAG = "AddWifiNetworkPreference"; private final Drawable mScanIconDrawable; - @VisibleForTesting - boolean mIsAddWifiConfigAllow; public AddWifiNetworkPreference(Context context) { super(context); @@ -51,8 +48,7 @@ public class AddWifiNetworkPreference extends Preference { setTitle(R.string.wifi_add_network); mScanIconDrawable = getDrawable(R.drawable.ic_scan_24dp); - mIsAddWifiConfigAllow = WifiEnterpriseRestrictionUtils.isAddWifiConfigAllowed(context); - updatePreferenceForRestriction(); + checkRestrictionAndSetDisabled(UserManager.DISALLOW_ADD_WIFI_CONFIG); } @Override @@ -79,12 +75,4 @@ public class AddWifiNetworkPreference extends Preference { } return buttonIcon; } - - @VisibleForTesting - void updatePreferenceForRestriction() { - if (!mIsAddWifiConfigAllow) { - setEnabled(false); - setSummary(R.string.not_allowed_by_ent); - } - } } diff --git a/src/com/android/settings/wifi/WifiEntryPreference.java b/src/com/android/settings/wifi/WifiEntryPreference.java index fedd9c977e9..5763abdbd81 100644 --- a/src/com/android/settings/wifi/WifiEntryPreference.java +++ b/src/com/android/settings/wifi/WifiEntryPreference.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; +import android.os.UserManager; import android.text.TextUtils; import android.view.View; import android.widget.ImageButton; @@ -30,10 +31,10 @@ import android.widget.TextView; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; import com.android.settingslib.R; +import com.android.settingslib.RestrictedPreference; import com.android.settingslib.Utils; import com.android.settingslib.wifi.WifiUtils; import com.android.wifitrackerlib.BaseWifiTracker; @@ -42,7 +43,8 @@ import com.android.wifitrackerlib.WifiEntry; /** * Preference to display a WifiEntry in a wifi picker. */ -public class WifiEntryPreference extends Preference implements WifiEntry.WifiEntryCallback, +public class WifiEntryPreference extends RestrictedPreference implements + WifiEntry.WifiEntryCallback, View.OnClickListener { private static final int[] STATE_SECURED = { @@ -86,6 +88,7 @@ public class WifiEntryPreference extends Preference implements WifiEntry.WifiEnt mWifiEntry = wifiEntry; mWifiEntry.setListener(this); mIconInjector = iconInjector; + checkRestrictionAndSetDisabled(UserManager.DISALLOW_ADD_WIFI_CONFIG); refresh(); } @@ -309,5 +312,4 @@ public class WifiEntryPreference extends Preference implements WifiEntry.WifiEnt } return buttonIcon; } - } diff --git a/tests/robotests/src/com/android/settings/wifi/AddWifiNetworkPreferenceTest.java b/tests/robotests/src/com/android/settings/wifi/AddWifiNetworkPreferenceTest.java index 04ecdabf9fb..5f88eb2a083 100644 --- a/tests/robotests/src/com/android/settings/wifi/AddWifiNetworkPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/wifi/AddWifiNetworkPreferenceTest.java @@ -17,61 +17,56 @@ package com.android.settings.wifi; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - import android.content.Context; import android.os.UserManager; import androidx.test.core.app.ApplicationProvider; -import com.android.settings.R; +import com.android.settingslib.RestrictedLockUtils; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowRestrictedPreference.class) public class AddWifiNetworkPreferenceTest { - private Context mContext; - private AddWifiNetworkPreference mPreference; + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Spy + Context mContext = ApplicationProvider.getApplicationContext(); - @Mock - private UserManager mUserManager; + private AddWifiNetworkPreference mPreference; @Before public void setUp() { - MockitoAnnotations.initMocks(this); - - mContext = spy(ApplicationProvider.getApplicationContext()); - when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); mPreference = new AddWifiNetworkPreference(mContext); - } @Test public void updatePreferenceForRestriction_isAddWifiConfigAllowed_prefIsEnabled() { - mPreference.mIsAddWifiConfigAllow = true; + // If the user is allowed to add Wi-Fi configuration then the EnforcedAdmin will be null. + RestrictedLockUtils.EnforcedAdmin enforcedAdmin = null; - mPreference.updatePreferenceForRestriction(); + mPreference.setDisabledByAdmin(enforcedAdmin); assertThat(mPreference.isEnabled()).isTrue(); - assertThat(mPreference.getSummary()).isNull(); } @Test public void updatePreferenceForRestriction_isAddWifiConfigNotAllowed_prefIsDisabled() { - mPreference.mIsAddWifiConfigAllow = false; + RestrictedLockUtils.EnforcedAdmin enforcedAdmin = new RestrictedLockUtils.EnforcedAdmin( + null /* component */, UserManager.DISALLOW_ADD_WIFI_CONFIG, null /* user */); - mPreference.updatePreferenceForRestriction(); + mPreference.setDisabledByAdmin(enforcedAdmin); assertThat(mPreference.isEnabled()).isFalse(); - assertThat(mPreference.getSummary()) - .isEqualTo(mContext.getString(R.string.not_allowed_by_ent)); } } diff --git a/tests/robotests/src/com/android/settings/wifi/ConnectedWifiEntryPreferenceTest.java b/tests/robotests/src/com/android/settings/wifi/ConnectedWifiEntryPreferenceTest.java index 401fdcff6e7..46a6132ab56 100644 --- a/tests/robotests/src/com/android/settings/wifi/ConnectedWifiEntryPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/wifi/ConnectedWifiEntryPreferenceTest.java @@ -35,8 +35,10 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowRestrictedPreference.class) public class ConnectedWifiEntryPreferenceTest { @Mock diff --git a/tests/robotests/src/com/android/settings/wifi/LongPressWifiEntryPreferenceTest.java b/tests/robotests/src/com/android/settings/wifi/LongPressWifiEntryPreferenceTest.java index fd7cc8943c9..efc2018ec85 100644 --- a/tests/robotests/src/com/android/settings/wifi/LongPressWifiEntryPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/wifi/LongPressWifiEntryPreferenceTest.java @@ -36,8 +36,10 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowRestrictedPreference.class) public class LongPressWifiEntryPreferenceTest { @Rule diff --git a/tests/robotests/src/com/android/settings/wifi/ShadowRestrictedPreference.java b/tests/robotests/src/com/android/settings/wifi/ShadowRestrictedPreference.java new file mode 100644 index 00000000000..4135ea080e7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/wifi/ShadowRestrictedPreference.java @@ -0,0 +1,31 @@ +/* + * 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.wifi; + +import com.android.settingslib.RestrictedPreference; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@Implements(RestrictedPreference.class) +public class ShadowRestrictedPreference { + + @Implementation + public void checkRestrictionAndSetDisabled(String userRestriction) { + // do nothing + } +} diff --git a/tests/robotests/src/com/android/settings/wifi/WifiEntryPreferenceTest.java b/tests/robotests/src/com/android/settings/wifi/WifiEntryPreferenceTest.java index a5d63dbd34d..0f51ef3874a 100644 --- a/tests/robotests/src/com/android/settings/wifi/WifiEntryPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/wifi/WifiEntryPreferenceTest.java @@ -40,11 +40,13 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.List; @RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowRestrictedPreference.class) public class WifiEntryPreferenceTest { private Context mContext; diff --git a/tests/robotests/src/com/android/settings/wifi/WifiSettingsTest.java b/tests/robotests/src/com/android/settings/wifi/WifiSettingsTest.java index 649f4cb45e4..b7c7270b908 100644 --- a/tests/robotests/src/com/android/settings/wifi/WifiSettingsTest.java +++ b/tests/robotests/src/com/android/settings/wifi/WifiSettingsTest.java @@ -70,6 +70,7 @@ import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowToast; @RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowRestrictedPreference.class) public class WifiSettingsTest { private static final int NUM_NETWORKS = 4; diff --git a/tests/robotests/src/com/android/settings/wifi/dpp/WifiNetworkListFragmentTest.java b/tests/robotests/src/com/android/settings/wifi/dpp/WifiNetworkListFragmentTest.java index e78f1c11348..e99b64003a2 100644 --- a/tests/robotests/src/com/android/settings/wifi/dpp/WifiNetworkListFragmentTest.java +++ b/tests/robotests/src/com/android/settings/wifi/dpp/WifiNetworkListFragmentTest.java @@ -36,6 +36,7 @@ import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.test.InstrumentationRegistry; +import com.android.settings.wifi.ShadowRestrictedPreference; import com.android.settings.wifi.WifiEntryPreference; import com.android.wifitrackerlib.SavedNetworkTracker; import com.android.wifitrackerlib.WifiEntry; @@ -45,10 +46,12 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import java.util.Arrays; @RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowRestrictedPreference.class) public class WifiNetworkListFragmentTest { private WifiNetworkListFragment mWifiNetworkListFragment; private Context mContext; From 3f8d62fbf170f8260afa69400dba3872eab2555e Mon Sep 17 00:00:00 2001 From: Lauren Winston Date: Thu, 6 Oct 2022 23:33:45 +0000 Subject: [PATCH 6/6] Revert "Add toggles for two Software Cursor settings: keyboard shift and trigger" This reverts commit 28b4b34f926c1a43bd280b63cac96800b1ad8771. Reason for revert: Software Cursor is not launching in Android U Test: manual Change-Id: I213cf64ca94fce51883a30a9d6b734b4ed874dad --- res/values/strings.xml | 7 - res/xml/accessibility_cursor_settings.xml | 10 -- ...rsorKeyboardShiftPreferenceController.java | 78 ------------ ...ursorTriggerHintsPreferenceController.java | 76 ----------- ...KeyboardShiftPreferenceControllerTest.java | 120 ------------------ ...rTriggerHintsPreferenceControllerTest.java | 120 ------------------ 6 files changed, 411 deletions(-) delete mode 100644 src/com/android/settings/accessibility/SoftwareCursorKeyboardShiftPreferenceController.java delete mode 100644 src/com/android/settings/accessibility/SoftwareCursorTriggerHintsPreferenceController.java delete mode 100644 tests/unit/src/com/android/settings/accessibility/SoftwareCursorKeyboardShiftPreferenceControllerTest.java delete mode 100644 tests/unit/src/com/android/settings/accessibility/SoftwareCursorTriggerHintsPreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 3c79253141b..b569f27e80a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11909,13 +11909,6 @@ QR code isn\u0027t a valid format - - - - Gesture detection area hints - - Shift gesture detection region above keyboard - diff --git a/res/xml/accessibility_cursor_settings.xml b/res/xml/accessibility_cursor_settings.xml index 1d23051ebbd..6bc50bb58ae 100644 --- a/res/xml/accessibility_cursor_settings.xml +++ b/res/xml/accessibility_cursor_settings.xml @@ -22,14 +22,4 @@ android:key="screen_software_cursor_preference_switch" android:title="@string/accessibility_screen_software_cursor_title" settings:controller="com.android.settings.accessibility.SoftwareCursorTogglePreferenceController"/> - - - - diff --git a/src/com/android/settings/accessibility/SoftwareCursorKeyboardShiftPreferenceController.java b/src/com/android/settings/accessibility/SoftwareCursorKeyboardShiftPreferenceController.java deleted file mode 100644 index 6ff49a3a853..00000000000 --- a/src/com/android/settings/accessibility/SoftwareCursorKeyboardShiftPreferenceController.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.accessibility; - -import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; -import static com.android.settings.accessibility.AccessibilityUtil.State.ON; - -import android.content.ContentResolver; -import android.content.Context; -import android.provider.Settings; - -import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; - -import com.android.settings.R; -import com.android.settings.core.TogglePreferenceController; - -/** Controller class for the Software Cursor keyboard shift setting. */ -public class SoftwareCursorKeyboardShiftPreferenceController extends - TogglePreferenceController { - - private static final String SETTINGS_VALUE = - Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED; - - private final ContentResolver mContentResolver; - - - public SoftwareCursorKeyboardShiftPreferenceController(Context context, String preferenceKey) { - super(context, preferenceKey); - mContentResolver = context.getContentResolver(); - } - - @Override - public boolean isChecked() { - return Settings.Secure.getInt(mContentResolver, SETTINGS_VALUE, OFF) == ON; - } - - @Override - public boolean setChecked(boolean isChecked) { - Settings.Secure.putInt(mContentResolver, SETTINGS_VALUE, isChecked ? ON : OFF); - return true; - } - - @Override - public int getAvailabilityStatus() { - return AVAILABLE; - } - - @Override - public int getSliceHighlightMenuRes() { - return R.string.menu_key_accessibility; - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - - SwitchPreference preference = screen.findPreference(getPreferenceKey()); - if (preference != null) { - preference.setOnPreferenceChangeListener(this); - preference.setChecked(isChecked()); - } - } -} diff --git a/src/com/android/settings/accessibility/SoftwareCursorTriggerHintsPreferenceController.java b/src/com/android/settings/accessibility/SoftwareCursorTriggerHintsPreferenceController.java deleted file mode 100644 index bd6e1470ac0..00000000000 --- a/src/com/android/settings/accessibility/SoftwareCursorTriggerHintsPreferenceController.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.accessibility; - -import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; -import static com.android.settings.accessibility.AccessibilityUtil.State.ON; - -import android.content.ContentResolver; -import android.content.Context; -import android.provider.Settings; - -import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; - -import com.android.settings.R; -import com.android.settings.core.TogglePreferenceController; - -/** Controller class that control accessibility software cursor trigger hints settings. */ -public class SoftwareCursorTriggerHintsPreferenceController extends TogglePreferenceController { - - private static final String SETTINGS_VALUE = - Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED; - - private final ContentResolver mContentResolver; - - public SoftwareCursorTriggerHintsPreferenceController(Context context, String preferenceKey) { - super(context, preferenceKey); - mContentResolver = context.getContentResolver(); - } - - @Override - public boolean isChecked() { - return Settings.Secure.getInt(mContentResolver, SETTINGS_VALUE, OFF) == ON; - } - - @Override - public boolean setChecked(boolean isChecked) { - Settings.Secure.putInt(mContentResolver, SETTINGS_VALUE, isChecked ? ON : OFF); - return true; - } - - @Override - public int getSliceHighlightMenuRes() { - return R.string.menu_key_accessibility; - } - - @Override - public int getAvailabilityStatus() { - return AVAILABLE; - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - - SwitchPreference preference = screen.findPreference(getPreferenceKey()); - if (preference != null) { - preference.setOnPreferenceChangeListener(this); - preference.setChecked(isChecked()); - } - } -} diff --git a/tests/unit/src/com/android/settings/accessibility/SoftwareCursorKeyboardShiftPreferenceControllerTest.java b/tests/unit/src/com/android/settings/accessibility/SoftwareCursorKeyboardShiftPreferenceControllerTest.java deleted file mode 100644 index 2df291ab9bd..00000000000 --- a/tests/unit/src/com/android/settings/accessibility/SoftwareCursorKeyboardShiftPreferenceControllerTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.accessibility; - -import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; -import static com.android.settings.accessibility.AccessibilityUtil.State.ON; - -import static com.google.common.truth.Truth.assertThat; - -import android.content.Context; -import android.os.Looper; -import android.provider.Settings; - -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.android.settings.core.BasePreferenceController; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Tests for {@link SoftwareCursorKeyboardShiftPreferenceController}. */ -@RunWith(AndroidJUnit4.class) -public class SoftwareCursorKeyboardShiftPreferenceControllerTest { - - private PreferenceScreen mScreen; - private final Context mContext = ApplicationProvider.getApplicationContext(); - private SoftwareCursorKeyboardShiftPreferenceController mController; - private SwitchPreference mSwitchPreference; - - @Before - public void setUp() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - mController = new SoftwareCursorKeyboardShiftPreferenceController(mContext, - "cursor_trigger_keyboard_shift_enabled"); - mSwitchPreference = new SwitchPreference(mContext); - mSwitchPreference.setKey(mController.getPreferenceKey()); - PreferenceManager preferenceManager = new PreferenceManager(mContext); - mScreen = preferenceManager.createPreferenceScreen(mContext); - mScreen.addPreference(mSwitchPreference); - } - - @After - public void tearDown() { - Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED, OFF); - } - - @Test - public void getAvailabilityStatus_shouldReturnAvailable() { - assertThat(mController.getAvailabilityStatus()) - .isEqualTo(BasePreferenceController.AVAILABLE); - } - - @Test - public void performClick_cursorEnabled_shouldSetSettingDisabled() { - mController.setChecked(true); - mController.displayPreference(mScreen); - - mSwitchPreference.performClick(); - - assertThat(mSwitchPreference.isChecked()).isFalse(); - assertThat(isKeyboardShiftEnabled()).isFalse(); - } - - @Test - public void performClick_cursorDisabled_shouldSetSettingEnabled() { - mController.setChecked(false); - mController.displayPreference(mScreen); - - mSwitchPreference.performClick(); - - assertThat(mSwitchPreference.isChecked()).isTrue(); - assertThat(isKeyboardShiftEnabled()).isTrue(); - } - - @Test - public void setChecked_switchChecked_shouldSetSettingEnabled() { - mController.displayPreference(mScreen); - - mController.setChecked(/* isChecked= */ true); - - assertThat(isKeyboardShiftEnabled()).isTrue(); - } - - @Test - public void setChecked_switchUnchecked_shouldSetSettingDisabled() { - mController.displayPreference(mScreen); - - mController.setChecked(/* isChecked= */ false); - - assertThat(isKeyboardShiftEnabled()).isFalse(); - } - - private boolean isKeyboardShiftEnabled() { - return Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED, OFF) == ON; - } -} diff --git a/tests/unit/src/com/android/settings/accessibility/SoftwareCursorTriggerHintsPreferenceControllerTest.java b/tests/unit/src/com/android/settings/accessibility/SoftwareCursorTriggerHintsPreferenceControllerTest.java deleted file mode 100644 index 73c118fa12d..00000000000 --- a/tests/unit/src/com/android/settings/accessibility/SoftwareCursorTriggerHintsPreferenceControllerTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.accessibility; - -import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; -import static com.android.settings.accessibility.AccessibilityUtil.State.ON; - -import static com.google.common.truth.Truth.assertThat; - -import android.content.Context; -import android.os.Looper; -import android.provider.Settings; - -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.android.settings.core.BasePreferenceController; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Tests for {@link SoftwareCursorTriggerHintsPreferenceController}. */ -@RunWith(AndroidJUnit4.class) -public class SoftwareCursorTriggerHintsPreferenceControllerTest { - - private PreferenceScreen mScreen; - private final Context mContext = ApplicationProvider.getApplicationContext(); - private SoftwareCursorTriggerHintsPreferenceController mController; - private SwitchPreference mSwitchPreference; - - @Before - public void setUp() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - mController = new SoftwareCursorTriggerHintsPreferenceController(mContext, - "cursor_trigger_hints_enabled"); - mSwitchPreference = new SwitchPreference(mContext); - mSwitchPreference.setKey(mController.getPreferenceKey()); - PreferenceManager preferenceManager = new PreferenceManager(mContext); - mScreen = preferenceManager.createPreferenceScreen(mContext); - mScreen.addPreference(mSwitchPreference); - } - - @After - public void tearDown() { - Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED, OFF); - } - - @Test - public void getAvailabilityStatus_shouldReturnAvailable() { - assertThat(mController.getAvailabilityStatus()) - .isEqualTo(BasePreferenceController.AVAILABLE); - } - - @Test - public void performClick_cursorEnabled_shouldSetSettingDisabled() { - mController.setChecked(true); - mController.displayPreference(mScreen); - - mSwitchPreference.performClick(); - - assertThat(mSwitchPreference.isChecked()).isFalse(); - assertThat(areTriggerHintsEnabled()).isFalse(); - } - - @Test - public void performClick_cursorDisabled_shouldSetSettingEnabled() { - mController.setChecked(false); - mController.displayPreference(mScreen); - - mSwitchPreference.performClick(); - - assertThat(mSwitchPreference.isChecked()).isTrue(); - assertThat(areTriggerHintsEnabled()).isTrue(); - } - - @Test - public void setChecked_switchChecked_shouldSetSettingEnabled() { - mController.displayPreference(mScreen); - - mController.setChecked(/* isChecked= */ true); - - assertThat(areTriggerHintsEnabled()).isTrue(); - } - - @Test - public void setChecked_switchUnchecked_shouldSetSettingDisabled() { - mController.displayPreference(mScreen); - - mController.setChecked(/* isChecked= */ false); - - assertThat(areTriggerHintsEnabled()).isFalse(); - } - - private boolean areTriggerHintsEnabled() { - return Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED, OFF) == ON; - } -}