diff --git a/res/values/strings.xml b/res/values/strings.xml index 540afb5cd3b..29a56e1c7c9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6802,6 +6802,9 @@ face, fingerprint, add fingerprint + + watch unlock, add watch unlock + dim screen, touchscreen, battery, smart brightness, dynamic brightness, Auto brightness diff --git a/res/xml/security_settings_combined_biometric.xml b/res/xml/security_settings_combined_biometric.xml index ef3a3fd1ff2..1bbe7b3901e 100644 --- a/res/xml/security_settings_combined_biometric.xml +++ b/res/xml/security_settings_combined_biometric.xml @@ -20,6 +20,7 @@ android:title="@string/security_settings_biometric_preference_title"> + - \ No newline at end of file + diff --git a/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java b/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java index f61f99c93f0..76a23a5f059 100644 --- a/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java @@ -25,6 +25,7 @@ import androidx.preference.Preference; import com.android.internal.widget.LockPatternUtils; import com.android.settings.Utils; +import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; import com.android.settings.core.BasePreferenceController; import com.android.settings.overlay.FeatureFactory; @@ -37,11 +38,17 @@ public abstract class BiometricStatusPreferenceController extends BasePreference protected final int mProfileChallengeUserId; private final BiometricNavigationUtils mBiometricNavigationUtils; + private final ActiveUnlockStatusUtils mActiveUnlockStatusUtils; + + /** + * @return true if the controller should be shown exclusively. + */ + protected abstract boolean isDeviceSupported(); /** * @return true if the manager is not null and the hardware is detected. */ - protected abstract boolean isDeviceSupported(); + protected abstract boolean isHardwareSupported(); /** * @return the summary text. @@ -61,13 +68,21 @@ public abstract class BiometricStatusPreferenceController extends BasePreference .getLockPatternUtils(context); mProfileChallengeUserId = Utils.getManagedProfileId(mUm, mUserId); mBiometricNavigationUtils = new BiometricNavigationUtils(getUserId()); + mActiveUnlockStatusUtils = new ActiveUnlockStatusUtils(context); } @Override public int getAvailabilityStatus() { + if (mActiveUnlockStatusUtils.isAvailable()) { + return getAvailabilityStatusWithWorkProfileCheck(); + } if (!isDeviceSupported()) { return UNSUPPORTED_ON_DEVICE; } + return getAvailabilityFromUserSupported(); + } + + private int getAvailabilityFromUserSupported() { if (isUserSupported()) { return AVAILABLE; } else { @@ -75,6 +90,21 @@ public abstract class BiometricStatusPreferenceController extends BasePreference } } + // Since this code is flag guarded by mActiveUnlockStatusUtils.isAvailable(), we don't need to + // do another check here. + private int getAvailabilityStatusWithWorkProfileCheck() { + if (!isHardwareSupported()) { + // no hardware, never show + return UNSUPPORTED_ON_DEVICE; + } + if (!isDeviceSupported() && isWorkProfileController()) { + // hardware supported but work profile, don't show + return UNSUPPORTED_ON_DEVICE; + } + // hardware supported, not work profile, active unlock enabled + return getAvailabilityFromUserSupported(); + } + @Override public void updateState(Preference preference) { if (!isAvailable()) { @@ -105,4 +135,11 @@ public abstract class BiometricStatusPreferenceController extends BasePreference protected boolean isUserSupported() { return true; } + + /** + * Returns true if the controller controls is used for work profile. + */ + protected boolean isWorkProfileController() { + return false; + } } diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java new file mode 100644 index 00000000000..c2a8f39b54b --- /dev/null +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 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.biometrics.activeunlock; + +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.settingslib.utils.ThreadUtils; + +/** Listens to updates from the content provider and fetches the latest value. */ +public class ActiveUnlockContentListener { + + /** Callback interface for updates to values from the ContentProvider. */ + public interface OnContentChangedListener { + /** + * Called when the content observer has updated. + * + * @param newValue the new value retrieved from the ContentProvider. + **/ + void onContentChanged(@Nullable String newValue); + } + + private static final String CONTENT_PROVIDER_PATH = "getSummary"; + + private final Context mContext; + private final OnContentChangedListener mContentChangedListener; + @Nullable private final Uri mUri; + private final String mLogTag; + private final String mMethodName; + private final String mContentKey; + @Nullable private String mContent; + private boolean mSubscribed = false; + private ContentObserver mContentObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + getContentFromUri(); + } + }; + + ActiveUnlockContentListener( + Context context, + OnContentChangedListener listener, + String logTag, + String methodName, + String contentKey) { + mContext = context; + mContentChangedListener = listener; + mLogTag = logTag; + mMethodName = methodName; + mContentKey = contentKey; + String authority = new ActiveUnlockStatusUtils(mContext).getAuthority(); + if (authority != null) { + mUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(CONTENT_PROVIDER_PATH) + .build(); + } else { + mUri = null; + } + + } + + /** Starts listening for updates from the ContentProvider, and fetches the current value. */ + public synchronized void subscribe() { + if (mSubscribed && mUri != null) { + return; + } + mSubscribed = true; + mContext.getContentResolver().registerContentObserver( + mUri, true /* notifyForDescendants */, mContentObserver); + ThreadUtils.postOnBackgroundThread( + () -> { + getContentFromUri(); + }); + } + + /** Stops listening for updates from the ContentProvider. */ + public synchronized void unsubscribe() { + if (!mSubscribed && mUri != null) { + return; + } + mSubscribed = false; + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + + /** Retrieves the most recently fetched value from the ContentProvider. */ + @Nullable + public String getContent() { + return mContent; + } + + private void getContentFromUri() { + if (mUri == null) { + Log.e(mLogTag, "Uri null when trying to fetch content"); + return; + } + ContentResolver contentResolver = mContext.getContentResolver(); + ContentProviderClient client = contentResolver.acquireContentProviderClient(mUri); + Bundle bundle; + try { + bundle = client.call(mMethodName, null /* arg */, null /* extras */); + } catch (RemoteException e) { + Log.e(mLogTag, "Failed to call contentProvider", e); + return; + } finally { + client.close(); + } + if (bundle == null) { + Log.e(mLogTag, "Null bundle returned from contentProvider"); + return; + } + String newValue = bundle.getString(mContentKey); + if (!TextUtils.equals(mContent, newValue)) { + mContent = newValue; + mContentChangedListener.onContentChanged(mContent); + } + } +} diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java new file mode 100644 index 00000000000..1badb0f26ec --- /dev/null +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 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.biometrics.activeunlock; + +import android.content.Context; + +/** Listens to device name updates from the content provider and fetches the latest value. */ +public class ActiveUnlockDeviceNameListener { + private static final String TAG = "ActiveUnlockDeviceNameListener"; + private static final String METHOD_NAME = "getDeviceName"; + private static final String DEVICE_NAME_KEY = "com.android.settings.active_unlock.device_name"; + + private final ActiveUnlockContentListener mActiveUnlockContentListener; + public ActiveUnlockDeviceNameListener( + Context context, ActiveUnlockContentListener.OnContentChangedListener listener) { + mActiveUnlockContentListener = new ActiveUnlockContentListener( + context, listener, TAG, METHOD_NAME, DEVICE_NAME_KEY); + } + + /** Returns whether a device is enrolled in Active Unlock. */ + public boolean hasEnrolled() { + return mActiveUnlockContentListener.getContent() != null; + } + + /** Subscribes to device name updates. */ + public void subscribe() { + mActiveUnlockContentListener.subscribe(); + } + + /** Unsubscribes from device name updates. */ + public void unsubscribe() { + mActiveUnlockContentListener.unsubscribe(); + } +} diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceController.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceController.java new file mode 100644 index 00000000000..05d4acb8f24 --- /dev/null +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceController.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2023 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.biometrics.activeunlock; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.PreferenceScreen; + +import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricStatusPreferenceController; +import com.android.settings.biometrics.activeunlock.ActiveUnlockContentListener.OnContentChangedListener; +import com.android.settingslib.RestrictedPreference; + +/** + * Preference controller for active unlock settings within the biometrics settings page, that + * controls the ability to unlock the phone with watch authentication. + */ +public class ActiveUnlockStatusPreferenceController + extends BiometricStatusPreferenceController + implements LifecycleObserver, OnContentChangedListener { + /** + * Preference key. + * + * This must match the key found in security_settings_combined_biometric.xml + **/ + public static final String KEY_ACTIVE_UNLOCK_SETTINGS = "biometric_active_unlock_settings"; + @Nullable private RestrictedPreference mPreference; + @Nullable private PreferenceScreen mPreferenceScreen; + @Nullable private String mSummary; + private final ActiveUnlockStatusUtils mActiveUnlockStatusUtils; + private final ActiveUnlockSummaryListener mActiveUnlockSummaryListener; + + public ActiveUnlockStatusPreferenceController(@NonNull Context context) { + this(context, KEY_ACTIVE_UNLOCK_SETTINGS); + } + + public ActiveUnlockStatusPreferenceController( + @NonNull Context context, @NonNull String key) { + super(context, key); + mActiveUnlockStatusUtils = new ActiveUnlockStatusUtils(context); + mActiveUnlockSummaryListener = new ActiveUnlockSummaryListener(context, this); + } + + + /** Subscribes to update preference summary dynamically. */ + @OnLifecycleEvent(Lifecycle.Event.ON_START) + public void onStart() { + mActiveUnlockSummaryListener.subscribe(); + } + + /** Resets the preference reference on resume. */ + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume() { + if (mPreferenceScreen != null) { + displayPreference(mPreferenceScreen); + } + } + + /** Unsubscribes to prevent leaked listener. */ + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + mActiveUnlockSummaryListener.unsubscribe(); + } + + @Override + public void onContentChanged(String newContent) { + mSummary = newContent; + if (mPreference != null) { + mPreference.setSummary(getSummaryText()); + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceScreen = screen; + mPreference = screen.findPreference(mPreferenceKey); + updateState(mPreference); + } + + @Override + public int getAvailabilityStatus() { + return mActiveUnlockStatusUtils.getAvailability(); + } + + @Override + protected boolean isDeviceSupported() { + // This should never be called, as getAvailabilityStatus() will return the exact value. + // However, this is an abstract method in BiometricStatusPreferenceController, and so + // needs to be overridden. + return mActiveUnlockStatusUtils.isAvailable(); + } + + @Override + protected boolean isHardwareSupported() { + // This should never be called, as getAvailabilityStatus() will return the exact value. + // However, this is an abstract method in BiometricStatusPreferenceController, and so + // needs to be overridden. + return Utils.hasFaceHardware(mContext) || Utils.hasFingerprintHardware(mContext); + } + + @Override + protected String getSummaryText() { + if (mSummary == null) { + // return non-empty string to prevent re-sizing of the tile + return " "; + } + return mSummary; + } + + @Override + protected String getSettingsClassName() { + // TODO(b/264813445): direct user to face & fingerprint setup + return null; + } +} diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java new file mode 100644 index 00000000000..439f176d548 --- /dev/null +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2023 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.biometrics.activeunlock; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.ComponentInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.provider.DeviceConfig; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.BasePreferenceController.AvailabilityStatus; + +import java.util.List; + +/** Utilities for active unlock details shared between Security Settings and Safety Center. */ +public class ActiveUnlockStatusUtils { + + /** The flag to determining whether active unlock in settings is enabled. */ + public static final String CONFIG_FLAG_NAME = "active_unlock_in_settings"; + + /** Flag value that represents the layout for unlock intent should be used. */ + public static final String UNLOCK_INTENT_LAYOUT = "unlock_intent_layout"; + + /** Flag value that represents the layout for biometric failure should be used. */ + public static final String BIOMETRIC_FAILURE_LAYOUT = "biometric_failure_layout"; + + private static final String ACTIVE_UNLOCK_PROVIDER = "active_unlock_provider"; + private static final String ACTIVE_UNLOCK_TARGET = "active_unlock_target"; + + private static final String TAG = "ActiveUnlockStatusUtils"; + + private final Context mContext; + private final ContentResolver mContentResolver; + + public ActiveUnlockStatusUtils(@NonNull Context context) { + mContext = context; + mContentResolver = mContext.getContentResolver(); + } + + /** Returns whether the active unlock settings entity should be shown. */ + public boolean isAvailable() { + return getAvailability() == BasePreferenceController.AVAILABLE; + } + + /** + * Returns whether the active unlock layout with the unlock on intent configuration should be + * used. + */ + public boolean useUnlockIntentLayout() { + return isAvailable() && UNLOCK_INTENT_LAYOUT.equals(getFlagState()); + } + + /** + * + * Returns whether the active unlock layout with the unlock on biometric failure configuration + * should be used. + */ + public boolean useBiometricFailureLayout() { + return isAvailable() && BIOMETRIC_FAILURE_LAYOUT.equals(getFlagState()); + } + + /** + * Returns the authority used to fetch dynamic active unlock content. + */ + @Nullable + public String getAuthority() { + final String authority = Settings.Secure.getString( + mContext.getContentResolver(), ACTIVE_UNLOCK_PROVIDER); + if (authority == null) { + Log.i(TAG, "authority not set"); + return null; + } + final List packageInfos = + mContext.getPackageManager().getInstalledPackages( + PackageManager.PackageInfoFlags.of(PackageManager.GET_PROVIDERS)); + for (PackageInfo packageInfo : packageInfos) { + final ProviderInfo[] providers = packageInfo.providers; + if (providers != null) { + for (ProviderInfo provider : providers) { + if (authority.equals(provider.authority) && isSystemApp(provider)) { + return authority; + } + } + } + } + Log.e(TAG, "authority not valid"); + return null; + } + + private static boolean isSystemApp(ComponentInfo componentInfo) { + final ApplicationInfo applicationInfo = componentInfo.applicationInfo; + if (applicationInfo == null) { + Log.e(TAG, "application info is null"); + return false; + } + return applicationInfo.isSystemApp(); + } + + /** + * Returns the intent used to launch the active unlock activity. + */ + @Nullable + public Intent getIntent() { + final String targetAction = Settings.Secure.getString( + mContentResolver, ACTIVE_UNLOCK_TARGET); + if (targetAction == null) { + Log.i(TAG, "Target action not set"); + return null; + } + final Intent intent = new Intent(targetAction); + final ActivityInfo activityInfo = intent.resolveActivityInfo( + mContext.getPackageManager(), PackageManager.MATCH_ALL); + if (activityInfo == null) { + Log.e(TAG, "Target activity not found"); + return null; + } + if (!isSystemApp(activityInfo)) { + Log.e(TAG, "Target application is not system"); + return null; + } + Log.i(TAG, "Target application is valid"); + return intent; + } + + /** Returns the availability status of the active unlock feature. */ + @AvailabilityStatus + int getAvailability() { + if (!Utils.hasFingerprintHardware(mContext) && !Utils.hasFaceHardware(mContext)) { + return BasePreferenceController.UNSUPPORTED_ON_DEVICE; + } + if (!UNLOCK_INTENT_LAYOUT.equals(getFlagState()) + && !BIOMETRIC_FAILURE_LAYOUT.equals(getFlagState())) { + return BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + } + if (getAuthority() != null && getIntent() != null) { + return BasePreferenceController.AVAILABLE; + } + return BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + } + + /** + * Returns the title of the combined biometric settings entity when active unlock is enabled. + */ + public String getTitleForActiveUnlock() { + final boolean faceAllowed = Utils.hasFaceHardware(mContext); + final boolean fingerprintAllowed = Utils.hasFingerprintHardware(mContext); + return mContext.getString(getTitleRes(faceAllowed, fingerprintAllowed)); + } + + @StringRes + private static int getTitleRes(boolean isFaceAllowed, boolean isFingerprintAllowed) { + if (isFaceAllowed && isFingerprintAllowed) { + return R.string.security_settings_biometric_preference_title; + } else if (isFaceAllowed) { + return R.string.security_settings_face_preference_title; + } else if (isFingerprintAllowed) { + return R.string.security_settings_fingerprint_preference_title; + } else { + // Default to original summary, but this case should never happen. + return R.string.security_settings_biometric_preference_title; + } + } + + /** + * Returns the intro of the combined biometric settings entity when active unlock is enabled. + */ + public String getIntroForActiveUnlock() { + final boolean faceAllowed = Utils.hasFaceHardware(mContext); + final boolean fingerprintAllowed = Utils.hasFingerprintHardware(mContext); + if (useBiometricFailureLayout()) { + int introRes = getIntroRes(faceAllowed, fingerprintAllowed); + return introRes == 0 ? "" : mContext.getString(introRes); + } + if (useUnlockIntentLayout() && (!faceAllowed || !fingerprintAllowed)) { + return ""; + } + return mContext.getString(R.string.biometric_settings_intro); + } + + @StringRes + private static int getIntroRes(boolean isFaceAllowed, boolean isFingerprintAllowed) { + if (isFaceAllowed && isFingerprintAllowed) { + return R.string.biometric_settings_intro_with_activeunlock; + } else if (isFaceAllowed) { + return R.string.biometric_settings_intro_with_face; + } else if (isFingerprintAllowed) { + return R.string.biometric_settings_intro_with_fingerprint; + } else { + return 0; + } + } + + /** + * Returns the summary of the unlock device entity when active unlock is enabled. + */ + public String getUnlockDeviceSummaryForActiveUnlock() { + final boolean faceAllowed = Utils.hasFaceHardware(mContext); + final boolean fingerprintAllowed = Utils.hasFingerprintHardware(mContext); + + return mContext.getString(getUnlockDeviceSummaryRes(faceAllowed, fingerprintAllowed)); + } + + @StringRes + private static int getUnlockDeviceSummaryRes( + boolean isFaceAllowed, boolean isFingerprintAllowed) { + if (isFaceAllowed && isFingerprintAllowed) { + return R.string.biometric_settings_use_face_fingerprint_or_watch_preference_summary; + } else if (isFaceAllowed) { + return R.string.biometric_settings_use_face_or_watch_preference_summary; + } else if (isFingerprintAllowed) { + return R.string.biometric_settings_use_fingerprint_or_watch_preference_summary; + } else { + return R.string.biometric_settings_use_watch_preference_summary; + } + } + + private static String getFlagState() { + return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_REMOTE_AUTH, CONFIG_FLAG_NAME); + } +} diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java new file mode 100644 index 00000000000..bcffe6297d1 --- /dev/null +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 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.biometrics.activeunlock; + +import android.content.Context; + +/** Listens to summary updates from the content provider and fetches the latest value. */ +public class ActiveUnlockSummaryListener { + private static final String TAG = "ActiveUnlockSummaryListener"; + private static final String METHOD_NAME = "getSummary"; + private static final String SUMMARY_KEY = "com.android.settings.summary"; + + private final ActiveUnlockContentListener mContentListener; + public ActiveUnlockSummaryListener( + Context context, ActiveUnlockContentListener.OnContentChangedListener listener) { + mContentListener = new ActiveUnlockContentListener( + context, listener, TAG, METHOD_NAME, SUMMARY_KEY); + } + + /** Subscribes for summary updates. */ + public void subscribe() { + mContentListener.subscribe(); + } + + /** Unsubscribes from summary updates. */ + public void unsubscribe() { + mContentListener.unsubscribe(); + } +} diff --git a/src/com/android/settings/biometrics/combination/BiometricFaceProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricFaceProfileStatusPreferenceController.java index de021261b30..c21368bff91 100644 --- a/src/com/android/settings/biometrics/combination/BiometricFaceProfileStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/combination/BiometricFaceProfileStatusPreferenceController.java @@ -46,4 +46,9 @@ public class BiometricFaceProfileStatusPreferenceController extends protected int getUserId() { return mProfileChallengeUserId; } + + @Override + protected boolean isWorkProfileController() { + return true; + } } diff --git a/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceController.java index 800139ce3f7..c9ea9449c7e 100644 --- a/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceController.java @@ -39,6 +39,11 @@ public class BiometricFaceStatusPreferenceController extends FaceStatusPreferenc @Override protected boolean isDeviceSupported() { - return Utils.isMultipleBiometricsSupported(mContext) && Utils.hasFaceHardware(mContext); + return Utils.isMultipleBiometricsSupported(mContext) && isHardwareSupported(); + } + + @Override + protected boolean isHardwareSupported() { + return Utils.hasFaceHardware(mContext); } } diff --git a/src/com/android/settings/biometrics/combination/BiometricFingerprintProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricFingerprintProfileStatusPreferenceController.java index 0c50230c989..52e44318daa 100644 --- a/src/com/android/settings/biometrics/combination/BiometricFingerprintProfileStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/combination/BiometricFingerprintProfileStatusPreferenceController.java @@ -46,4 +46,9 @@ public class BiometricFingerprintProfileStatusPreferenceController extends protected int getUserId() { return mProfileChallengeUserId; } + + @Override + protected boolean isWorkProfileController() { + return true; + } } diff --git a/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceController.java index be19cb5cada..9789417acee 100644 --- a/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceController.java @@ -40,7 +40,11 @@ public class BiometricFingerprintStatusPreferenceController extends @Override protected boolean isDeviceSupported() { - return Utils.isMultipleBiometricsSupported(mContext) - && Utils.hasFingerprintHardware(mContext); + return Utils.isMultipleBiometricsSupported(mContext) && isHardwareSupported(); + } + + @Override + protected boolean isHardwareSupported() { + return Utils.hasFingerprintHardware(mContext); } } diff --git a/src/com/android/settings/biometrics/combination/BiometricSettingsAppPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricSettingsAppPreferenceController.java index a46ae7a728c..6153a1a1907 100644 --- a/src/com/android/settings/biometrics/combination/BiometricSettingsAppPreferenceController.java +++ b/src/com/android/settings/biometrics/combination/BiometricSettingsAppPreferenceController.java @@ -24,6 +24,7 @@ import android.hardware.fingerprint.FingerprintManager; import android.provider.Settings; import com.android.settings.Utils; +import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; import com.android.settings.core.TogglePreferenceController; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedLockUtilsInternal; @@ -69,7 +70,10 @@ public class BiometricSettingsAppPreferenceController extends TogglePreferenceCo @Override public int getAvailabilityStatus() { - if (!Utils.isMultipleBiometricsSupported(mContext)) { + final ActiveUnlockStatusUtils activeUnlockStatusUtils = + new ActiveUnlockStatusUtils(mContext); + if (!Utils.isMultipleBiometricsSupported(mContext) + && !activeUnlockStatusUtils.isAvailable()) { return UNSUPPORTED_ON_DEVICE; } if (mFaceManager == null || mFingerprintManager == null) { diff --git a/src/com/android/settings/biometrics/combination/BiometricSettingsKeyguardPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricSettingsKeyguardPreferenceController.java index 2d2255805cc..cfd220e87a4 100644 --- a/src/com/android/settings/biometrics/combination/BiometricSettingsKeyguardPreferenceController.java +++ b/src/com/android/settings/biometrics/combination/BiometricSettingsKeyguardPreferenceController.java @@ -22,6 +22,7 @@ import android.content.Context; import android.provider.Settings; import com.android.settings.Utils; +import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; import com.android.settings.core.TogglePreferenceController; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; @@ -63,9 +64,18 @@ public class BiometricSettingsKeyguardPreferenceController extends TogglePrefere @Override public int getAvailabilityStatus() { + final ActiveUnlockStatusUtils activeUnlockStatusUtils = + new ActiveUnlockStatusUtils(mContext); + if (activeUnlockStatusUtils.isAvailable()) { + return getAvailabilityFromRestrictingAdmin(); + } if (!Utils.isMultipleBiometricsSupported(mContext)) { return UNSUPPORTED_ON_DEVICE; } + return getAvailabilityFromRestrictingAdmin(); + } + + private int getAvailabilityFromRestrictingAdmin() { return getRestrictingAdmin() != null ? DISABLED_FOR_USER : AVAILABLE; } diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java index 4da42d4d287..0a1d29d4c1c 100644 --- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java +++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java @@ -107,10 +107,7 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { launchChooseOrConfirmLock(); } - final Preference unlockPhonePreference = findPreference(getUnlockPhonePreferenceKey()); - if (unlockPhonePreference != null) { - unlockPhonePreference.setSummary(getUseAnyBiometricSummary()); - } + updateUnlockPhonePreferenceSummary(); final Preference useInAppsPreference = findPreference(getUseInAppsPreferenceKey()); if (useInAppsPreference != null) { @@ -309,8 +306,15 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { } } + protected void updateUnlockPhonePreferenceSummary() { + final Preference unlockPhonePreference = findPreference(getUnlockPhonePreferenceKey()); + if (unlockPhonePreference != null) { + unlockPhonePreference.setSummary(getUseAnyBiometricSummary()); + } + } + @NonNull - private String getUseAnyBiometricSummary() { + protected String getUseAnyBiometricSummary() { boolean isFaceAllowed = mFaceManager != null && mFaceManager.isHardwareDetected(); boolean isFingerprintAllowed = mFingerprintManager != null && mFingerprintManager.isHardwareDetected(); diff --git a/src/com/android/settings/biometrics/combination/CombinedBiometricProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/CombinedBiometricProfileStatusPreferenceController.java index b8706a553d3..67c267d43be 100644 --- a/src/com/android/settings/biometrics/combination/CombinedBiometricProfileStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/combination/CombinedBiometricProfileStatusPreferenceController.java @@ -62,4 +62,9 @@ public class CombinedBiometricProfileStatusPreferenceController extends protected String getSettingsClassName() { return mCombinedBiometricStatusUtils.getProfileSettingsClassName(); } + + @Override + protected boolean isWorkProfileController() { + return true; + } } diff --git a/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java b/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java index 7e76ceb09ac..a352e5a53e5 100644 --- a/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java +++ b/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java @@ -17,8 +17,15 @@ package com.android.settings.biometrics.combination; import android.app.settings.SettingsEnums; import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; import com.android.settings.R; +import com.android.settings.biometrics.activeunlock.ActiveUnlockContentListener.OnContentChangedListener; +import com.android.settings.biometrics.activeunlock.ActiveUnlockDeviceNameListener; +import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.search.SearchIndexable; @@ -32,6 +39,10 @@ public class CombinedBiometricSettings extends BiometricsSettingsBase { private static final String KEY_FINGERPRINT_SETTINGS = "biometric_fingerprint_settings"; private static final String KEY_UNLOCK_PHONE = "biometric_settings_biometric_keyguard"; private static final String KEY_USE_IN_APPS = "biometric_settings_biometric_app"; + private static final String KEY_INTRO_PREFERENCE = "biometric_intro"; + + private ActiveUnlockStatusUtils mActiveUnlockStatusUtils; + @Nullable private ActiveUnlockDeviceNameListener mActiveUnlockDeviceNameListener; @Override public void onAttach(Context context) { @@ -40,6 +51,41 @@ public class CombinedBiometricSettings extends BiometricsSettingsBase { use(BiometricSettingsAppPreferenceController.class).setUserId(mUserId); } + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mActiveUnlockStatusUtils = new ActiveUnlockStatusUtils(getActivity()); + if (mActiveUnlockStatusUtils.isAvailable()) { + updateUiForActiveUnlock(); + } + } + + private void updateUiForActiveUnlock() { + OnContentChangedListener listener = new OnContentChangedListener() { + @Override + public void onContentChanged(String newValue) { + updateUnlockPhonePreferenceSummary(); + } + }; + + mActiveUnlockDeviceNameListener = + new ActiveUnlockDeviceNameListener(getActivity(), listener); + mActiveUnlockDeviceNameListener.subscribe(); + final Preference introPreference = findPreference(KEY_INTRO_PREFERENCE); + if (introPreference != null) { + introPreference.setTitle(mActiveUnlockStatusUtils.getIntroForActiveUnlock()); + } + getActivity().setTitle(mActiveUnlockStatusUtils.getTitleForActiveUnlock()); + } + + @Override + public void onDestroy() { + if (mActiveUnlockDeviceNameListener != null) { + mActiveUnlockDeviceNameListener.unsubscribe(); + } + super.onDestroy(); + } + @Override protected int getPreferenceScreenResId() { return R.xml.security_settings_combined_biometric; @@ -75,6 +121,16 @@ public class CombinedBiometricSettings extends BiometricsSettingsBase { return SettingsEnums.COMBINED_BIOMETRIC; } + @Override + protected String getUseAnyBiometricSummary() { + // either Active Unlock is not enabled or no device is enrolled. + if (mActiveUnlockDeviceNameListener == null + || !mActiveUnlockDeviceNameListener.hasEnrolled()) { + return super.getUseAnyBiometricSummary(); + } + return mActiveUnlockStatusUtils.getUnlockDeviceSummaryForActiveUnlock(); + } + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new CombinedBiometricSearchIndexProvider(R.xml.security_settings_combined_biometric); } diff --git a/src/com/android/settings/biometrics/combination/CombinedBiometricStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/CombinedBiometricStatusPreferenceController.java index 50eb43d6545..a337c3b22ee 100644 --- a/src/com/android/settings/biometrics/combination/CombinedBiometricStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/combination/CombinedBiometricStatusPreferenceController.java @@ -25,6 +25,7 @@ import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.Utils; import com.android.settings.biometrics.BiometricStatusPreferenceController; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedPreference; @@ -84,6 +85,11 @@ public class CombinedBiometricStatusPreferenceController extends return mCombinedBiometricStatusUtils.isAvailable(); } + @Override + protected boolean isHardwareSupported() { + return Utils.hasFaceHardware(mContext) || Utils.hasFingerprintHardware(mContext); + } + @Override public void updateState(Preference preference) { super.updateState(preference); diff --git a/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java index a2e11afb7e0..122138947d4 100644 --- a/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java @@ -84,4 +84,9 @@ public class FaceProfileStatusPreferenceController extends FaceStatusPreferenceC mContext.getResources().getString( R.string.security_settings_face_profile_preference_title))); } + + @Override + protected boolean isWorkProfileController() { + return true; + } } diff --git a/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java b/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java index f18a74fa190..c71119cdeee 100644 --- a/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java @@ -86,6 +86,11 @@ public class FaceStatusPreferenceController extends BiometricStatusPreferenceCon return mFaceStatusUtils.isAvailable(); } + @Override + protected boolean isHardwareSupported() { + return Utils.hasFaceHardware(mContext); + } + @Override public void updateState(Preference preference) { super.updateState(preference); diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java index d6d0b8f8ac4..051d25498c2 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java @@ -53,4 +53,9 @@ public class FingerprintProfileStatusPreferenceController protected int getUserId() { return mProfileChallengeUserId; } + + @Override + protected boolean isWorkProfileController() { + return true; + } } diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java index 347fec72870..fba93e10b41 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java @@ -86,6 +86,11 @@ public class FingerprintStatusPreferenceController extends BiometricStatusPrefer return mFingerprintStatusUtils.isAvailable(); } + @Override + protected boolean isHardwareSupported() { + return Utils.hasFingerprintHardware(mContext); + } + @Override public void updateState(Preference preference) { super.updateState(preference); diff --git a/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListenerTest.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListenerTest.java new file mode 100644 index 00000000000..cb0c9420e80 --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListenerTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 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.biometrics.activeunlock; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.robolectric.shadows.ShadowLooper.idleMainLooper; + +import android.content.Context; +import android.content.pm.PackageManager; + +import androidx.annotation.Nullable; + +import com.android.settings.biometrics.activeunlock.ActiveUnlockContentListener.OnContentChangedListener; +import com.android.settings.testutils.ActiveUnlockTestUtils; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) +public class ActiveUnlockContentListenerTest { + + @Rule public final MockitoRule mMocks = MockitoJUnit.rule(); + @Mock private PackageManager mPackageManager; + + private Context mContext; + private ActiveUnlockContentListener mContentListener; + @Nullable private String mContent; + private int mUpdateCount; + + @Before + public void setUp() { + Robolectric.setupContentProvider( + FakeContentProvider.class, FakeContentProvider.AUTHORITY); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + OnContentChangedListener listener = new OnContentChangedListener() { + @Override + public void onContentChanged(String newValue) { + mContent = newValue; + mUpdateCount++; + } + }; + ActiveUnlockTestUtils.enable(mContext); + mContentListener = + new ActiveUnlockContentListener( + mContext, + listener, + "logTag", + FakeContentProvider.METHOD_SUMMARY, + FakeContentProvider.KEY_SUMMARY); + FakeContentProvider.init(mContext); + } + + @Test + public void subscribe_contentFetched() { + String newContent = "newContent"; + FakeContentProvider.setTileSummary(newContent); + + mContentListener.subscribe(); + idleMainLooper(); + + assertThat(mContent).isEqualTo(newContent); + } + + @Test + public void contentUpdated_contentUpdated() { + mContentListener.subscribe(); + idleMainLooper(); + + String newContent = "newContent"; + updateContent(newContent); + + assertThat(mContent).isEqualTo(newContent); + } + + @Test + public void contentUpdated_unsubscribed_contentNotUpdated() { + mContentListener.subscribe(); + idleMainLooper(); + + mContentListener.unsubscribe(); + updateContent("newContent"); + + assertThat(mContent).isNull(); + } + + @Test + public void multipleContentUpdates_contentIsNewestValueAndUpdatedTwice() { + mContentListener.subscribe(); + idleMainLooper(); + + updateContent("temporaryContent"); + String newContent = "newContent"; + updateContent(newContent); + + assertThat(mContent).isEqualTo(newContent); + assertThat(mUpdateCount).isEqualTo(2); + } + + @Test + public void duplicateContentUpdates_onContentChangedOnlyCalledOnce() { + mContentListener.subscribe(); + idleMainLooper(); + + updateContent("newContent"); + updateContent("newContent"); + + assertThat(mUpdateCount).isEqualTo(1); + } + + private void updateContent(String content) { + FakeContentProvider.setTileSummary(content); + mContext.getContentResolver().notifyChange( + FakeContentProvider.URI, null /* observer */); + idleMainLooper(); + } +} diff --git a/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceControllerTest.java new file mode 100644 index 00000000000..bf60d0173c0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceControllerTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2023 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.biometrics.activeunlock; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.robolectric.shadows.ShadowLooper.idleMainLooper; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; +import android.os.UserManager; + +import androidx.preference.PreferenceScreen; + +import com.android.settings.testutils.ActiveUnlockTestUtils; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; +import com.android.settingslib.RestrictedPreference; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) +public class ActiveUnlockStatusPreferenceControllerTest { + + @Rule public final MockitoRule mMocks = MockitoJUnit.rule(); + + @Mock private UserManager mUserManager; + @Mock private PackageManager mPackageManager; + @Mock private FingerprintManager mFingerprintManager; + @Mock private FaceManager mFaceManager; + @Mock private PreferenceScreen mPreferenceScreen; + + private Context mContext; + private ActiveUnlockStatusPreferenceController mController; + private RestrictedPreference mPreference; + + @Before + public void setUp() { + Robolectric.setupContentProvider(FakeContentProvider.class, FakeContentProvider.AUTHORITY); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + ShadowApplication.getInstance() + .setSystemService(Context.FINGERPRINT_SERVICE, mFingerprintManager); + ShadowApplication.getInstance().setSystemService(Context.FACE_SERVICE, mFaceManager); + ShadowApplication.getInstance().setSystemService(Context.USER_SERVICE, mUserManager); + when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[] {1234}); + mPreference = new RestrictedPreference(mContext); + when(mPreferenceScreen.findPreference(any())).thenReturn(mPreference); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + ActiveUnlockTestUtils.enable(mContext); + FakeContentProvider.init(mContext); + mController = new ActiveUnlockStatusPreferenceController(mContext); + } + + @After + public void tearDown() { + ActiveUnlockTestUtils.disable(mContext); + } + + @Test + public void updateState_featureFlagDisabled_isNotVisible() { + ActiveUnlockTestUtils.disable(mContext); + + mController.displayPreference(mPreferenceScreen); + + assertThat(mPreference.isVisible()).isFalse(); + } + + @Test + public void updateState_withoutFingerprint_withoutFace_isNotVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + + mController.displayPreference(mPreferenceScreen); + + assertThat(mPreference.isVisible()).isFalse(); + } + + @Test + public void updateState_withoutFingerprint_withFace_isVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + mController.displayPreference(mPreferenceScreen); + + assertThat(mPreference.isVisible()).isTrue(); + } + + @Test + public void updateState_withFingerprint_withoutFace_isVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + + mController.displayPreference(mPreferenceScreen); + + assertThat(mPreference.isVisible()).isTrue(); + } + + @Test + public void updateState_withFingerprint_withFace_isVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + mController.displayPreference(mPreferenceScreen); + + assertThat(mPreference.isVisible()).isTrue(); + } + + @Test + public void defaultState_summaryIsEmpty() { + mController.displayPreference(mPreferenceScreen); + + idleMainLooper(); + + assertThat(mPreference.getSummary().toString()).isEqualTo(" "); + } + + @Test + public void onStart_summaryIsUpdated() { + String summary = "newSummary"; + updateSummary(summary); + mController.displayPreference(mPreferenceScreen); + + mController.onStart(); + idleMainLooper(); + + assertThat(mPreference.getSummary().toString()).isEqualTo(summary); + } + + private void updateSummary(String summary) { + FakeContentProvider.setTileSummary(summary); + mContext.getContentResolver().notifyChange(FakeContentProvider.URI, null /* observer */); + idleMainLooper(); + } +} diff --git a/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java new file mode 100644 index 00000000000..d420f78ac0e --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2023 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.biometrics.activeunlock; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +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.content.pm.PackageManager; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.testutils.ActiveUnlockTestUtils; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) +public class ActiveUnlockStatusUtilsTest { + + @Rule public final MockitoRule mMocks = MockitoJUnit.rule(); + + @Mock private PackageManager mPackageManager; + @Mock private FingerprintManager mFingerprintManager; + @Mock private FaceManager mFaceManager; + + private Context mApplicationContext; + private ActiveUnlockStatusUtils mActiveUnlockStatusUtils; + + @Before + public void setUp() { + mApplicationContext = spy(ApplicationProvider.getApplicationContext()); + when(mApplicationContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mApplicationContext.getSystemService(Context.FINGERPRINT_SERVICE)) + .thenReturn(mFingerprintManager); + when(mApplicationContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + ActiveUnlockTestUtils.enable(mApplicationContext); + mActiveUnlockStatusUtils = new ActiveUnlockStatusUtils(mApplicationContext); + } + + @After + public void tearDown() { + ActiveUnlockTestUtils.disable(mApplicationContext); + } + + @Test + public void isAvailable_featureFlagDisabled_returnsConditionallyUnavailable() { + ActiveUnlockTestUtils.disable(mApplicationContext); + + assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void isAvailable_withoutFingerprint_withoutFace_returnsUnsupported() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + + assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void isAvailable_withoutFingerprint_withFace_returnsAvailable() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(AVAILABLE); + } + + @Test + public void isAvailable_withFingerprint_withoutFace_returnsAvailable() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + + assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(AVAILABLE); + } + + @Test + public void isAvailable_withFingerprint_withFace_returnsAvailable() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(AVAILABLE); + } + + @Test + public void configIsUnlockOnIntent_useUnlockIntentLayoutIsTrue() { + ActiveUnlockTestUtils.enable( + mApplicationContext, ActiveUnlockStatusUtils.UNLOCK_INTENT_LAYOUT); + + assertThat(mActiveUnlockStatusUtils.useUnlockIntentLayout()).isTrue(); + assertThat(mActiveUnlockStatusUtils.useBiometricFailureLayout()).isFalse(); + } + + @Test + public void configIsBiometricFailure_useBiometricFailureLayoutIsTrue() { + ActiveUnlockTestUtils.enable( + mApplicationContext, ActiveUnlockStatusUtils.BIOMETRIC_FAILURE_LAYOUT); + + assertThat(mActiveUnlockStatusUtils.useUnlockIntentLayout()).isFalse(); + assertThat(mActiveUnlockStatusUtils.useBiometricFailureLayout()).isTrue(); + } + + @Test + public void getTitle_faceEnabled_returnsFacePreferenceTitle() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + assertThat(mActiveUnlockStatusUtils.getTitleForActiveUnlock()) + .isEqualTo(mApplicationContext.getString( + R.string.security_settings_face_preference_title)); + } + + @Test + public void getTitle_fingerprintEnabled_returnsFingerprintPreferenceTitle() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + + assertThat(mActiveUnlockStatusUtils.getTitleForActiveUnlock()) + .isEqualTo(mApplicationContext.getString( + R.string.security_settings_fingerprint_preference_title)); + } + + @Test + public void getIntro_faceEnabled_returnsIntroWithFace() { + ActiveUnlockTestUtils.enable( + mApplicationContext, ActiveUnlockStatusUtils.BIOMETRIC_FAILURE_LAYOUT); + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + assertThat(mActiveUnlockStatusUtils.getIntroForActiveUnlock()) + .isEqualTo(mApplicationContext.getString( + R.string.biometric_settings_intro_with_face)); + } + + @Test + public void getIntro_fingerprintEnabled_returnsIntroWithFingerprint() { + ActiveUnlockTestUtils.enable( + mApplicationContext, ActiveUnlockStatusUtils.BIOMETRIC_FAILURE_LAYOUT); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + + assertThat(mActiveUnlockStatusUtils.getIntroForActiveUnlock()) + .isEqualTo(mApplicationContext.getString( + R.string.biometric_settings_intro_with_fingerprint)); + } + + @Test + public void getIntro_unlockOnIntentAndFaceEnabled_returnsEmpty() { + ActiveUnlockTestUtils.enable( + mApplicationContext, ActiveUnlockStatusUtils.UNLOCK_INTENT_LAYOUT); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + + assertThat(mActiveUnlockStatusUtils.getIntroForActiveUnlock()).isEqualTo(""); + } + + @Test + public void getIntro_unlockOnIntentAndFaceAndFingerprintEnabled_returnsDefault() { + ActiveUnlockTestUtils.enable( + mApplicationContext, ActiveUnlockStatusUtils.UNLOCK_INTENT_LAYOUT); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + assertThat(mActiveUnlockStatusUtils.getIntroForActiveUnlock()) + .isEqualTo(mApplicationContext.getString( + R.string.biometric_settings_intro)); + } + + @Test + public void getUnlockDeviceSummary_fingerprintEnabled_returnsFingerprintOrWatch() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + + assertThat(mActiveUnlockStatusUtils.getUnlockDeviceSummaryForActiveUnlock()) + .isEqualTo(mApplicationContext.getString( + R.string.biometric_settings_use_fingerprint_or_watch_preference_summary)); + } + + @Test + public void getUnlockDeviceSummary_faceEnabled_returnsFaceOrWatch() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + assertThat(mActiveUnlockStatusUtils.getUnlockDeviceSummaryForActiveUnlock()) + .isEqualTo(mApplicationContext.getString( + R.string.biometric_settings_use_face_or_watch_preference_summary)); + } +} diff --git a/tests/robotests/src/com/android/settings/biometrics/activeunlock/FakeContentProvider.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/FakeContentProvider.java new file mode 100644 index 00000000000..7bb6941cabe --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/FakeContentProvider.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 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.biometrics.activeunlock; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; + +import androidx.annotation.Nullable; + +import com.android.settings.testutils.ActiveUnlockTestUtils; + +/** ContentProvider to provider tile summary for ActiveUnlock in tests. */ +public final class FakeContentProvider extends ContentProvider { + public static final String AUTHORITY = ActiveUnlockTestUtils.PROVIDER; + public static final Uri URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .appendPath("getSummary") + .build(); + public static final String METHOD_SUMMARY = "getSummary"; + public static final String KEY_SUMMARY = "com.android.settings.summary"; + private static final String METHOD_DEVICE_NAME = "getDeviceName"; + private static final String KEY_DEVICE_NAME = "com.android.settings.active_unlock.device_name"; + @Nullable private static String sTileSummary; + @Nullable private static String sDeviceName; + + public FakeContentProvider() { + super(); + } + + public static void setTileSummary(String summary) { + sTileSummary = summary; + } + + public static void setDeviceName(String deviceName) { + sDeviceName = deviceName; + } + + public static void init(Context context) { + Settings.Secure.putString( + context.getContentResolver(), ActiveUnlockTestUtils.PROVIDER_SETTING, AUTHORITY); + sTileSummary = null; + sDeviceName = null; + } + + @Override + public Bundle call(String method, String arg, Bundle extras) { + Bundle bundle = new Bundle(); + if (METHOD_SUMMARY.equals(method)) { + bundle.putCharSequence(KEY_SUMMARY, sTileSummary); + } else if (METHOD_DEVICE_NAME.equals(method)) { + bundle.putCharSequence(KEY_DEVICE_NAME, sDeviceName); + } + return bundle; + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + return 0; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public String getType(Uri uri) { + return null; + } +} diff --git a/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceControllerTest.java new file mode 100644 index 00000000000..84a9ad42acb --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceControllerTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 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.biometrics.combination; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; +import android.os.UserManager; + +import com.android.settings.testutils.ActiveUnlockTestUtils; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; +import com.android.settingslib.RestrictedPreference; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) +public class BiometricFaceStatusPreferenceControllerTest { + + @Rule public final MockitoRule mMocks = MockitoJUnit.rule(); + + @Mock private UserManager mUserManager; + @Mock private PackageManager mPackageManager; + @Mock private FingerprintManager mFingerprintManager; + @Mock private FaceManager mFaceManager; + + private Context mContext; + private RestrictedPreference mPreference; + private BiometricFaceStatusPreferenceController mController; + + @Before + public void setUp() { + mContext = spy(RuntimeEnvironment.application); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + ShadowApplication.getInstance() + .setSystemService(Context.FINGERPRINT_SERVICE, mFingerprintManager); + ShadowApplication.getInstance().setSystemService(Context.FACE_SERVICE, mFaceManager); + ShadowApplication.getInstance().setSystemService(Context.USER_SERVICE, mUserManager); + when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[] {1234}); + mPreference = new RestrictedPreference(mContext); + mController = new BiometricFaceStatusPreferenceController(mContext, "preferenceKey"); + } + + @After + public void tearDown() { + ActiveUnlockTestUtils.disable(mContext); + } + + @Test + public void onlyFaceEnabled_preferenceNotVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + mController.updateState(mPreference); + + assertThat(mPreference.isVisible()).isFalse(); + } + + @Test + public void onlyFaceAndActiveUnlockEnabled_preferenceVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(false); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + ActiveUnlockTestUtils.enable(mContext); + + mController.updateState(mPreference); + + assertThat(mPreference.isVisible()).isTrue(); + } + + @Test + public void faceAndFingerprintEnabled_preferenceVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + mController.updateState(mPreference); + + assertThat(mPreference.isVisible()).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceControllerTest.java new file mode 100644 index 00000000000..3eb4c21f6a2 --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceControllerTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 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.biometrics.combination; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; +import android.os.UserManager; + +import com.android.settings.testutils.ActiveUnlockTestUtils; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; +import com.android.settingslib.RestrictedPreference; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) +public class BiometricFingerprintStatusPreferenceControllerTest { + + @Rule public final MockitoRule mMocks = MockitoJUnit.rule(); + + @Mock private UserManager mUserManager; + @Mock private PackageManager mPackageManager; + @Mock private FingerprintManager mFingerprintManager; + @Mock private FaceManager mFaceManager; + + private Context mContext; + private RestrictedPreference mPreference; + private BiometricFingerprintStatusPreferenceController mController; + + @Before + public void setUp() { + mContext = spy(RuntimeEnvironment.application); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + ShadowApplication.getInstance() + .setSystemService(Context.FINGERPRINT_SERVICE, mFingerprintManager); + ShadowApplication.getInstance().setSystemService(Context.FACE_SERVICE, mFaceManager); + ShadowApplication.getInstance().setSystemService(Context.USER_SERVICE, mUserManager); + when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[] {1234}); + mPreference = new RestrictedPreference(mContext); + mController = new BiometricFingerprintStatusPreferenceController(mContext, "preferenceKey"); + } + + @After + public void tearDown() { + ActiveUnlockTestUtils.disable(mContext); + } + + @Test + public void onlyFingerprintEnabled_preferenceNotVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + + mController.updateState(mPreference); + + assertThat(mPreference.isVisible()).isFalse(); + } + + @Test + public void onlyFingerprintAndActiveUnlockEnabled_preferenceVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(false); + ActiveUnlockTestUtils.enable(mContext); + + mController.updateState(mPreference); + + assertThat(mPreference.isVisible()).isTrue(); + } + + @Test + public void faceAndFingerprintEnabled_preferenceVisible() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + mController.updateState(mPreference); + + assertThat(mPreference.isVisible()).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/ActiveUnlockTestUtils.java b/tests/robotests/src/com/android/settings/testutils/ActiveUnlockTestUtils.java new file mode 100644 index 00000000000..0cecaee2939 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/ActiveUnlockTestUtils.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 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.testutils; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.provider.DeviceConfig; +import android.provider.Settings; + +import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; + +import java.util.ArrayList; + +/** Utilities class to enable or disable the Active Unlock flag in tests. */ +public final class ActiveUnlockTestUtils { + + public static final String TARGET = "com.active.unlock.target"; + public static final String PROVIDER = "com.active.unlock.provider"; + public static final String TARGET_SETTING = "active_unlock_target"; + public static final String PROVIDER_SETTING = "active_unlock_provider"; + + public static void enable(Context context) { + ActiveUnlockTestUtils.enable(context, ActiveUnlockStatusUtils.UNLOCK_INTENT_LAYOUT); + } + + public static void enable(Context context, String flagValue) { + Settings.Secure.putString( + context.getContentResolver(), TARGET_SETTING, TARGET); + Settings.Secure.putString( + context.getContentResolver(), PROVIDER_SETTING, PROVIDER); + + PackageManager packageManager = context.getPackageManager(); + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; + + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.applicationInfo = applicationInfo; + when(packageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo); + + PackageInfo packageInfo = new PackageInfo(); + packageInfo.applicationInfo = applicationInfo; + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.authority = PROVIDER; + providerInfo.applicationInfo = applicationInfo; + packageInfo.providers = new ProviderInfo[] { providerInfo }; + ArrayList packageInfos = new ArrayList<>(); + packageInfos.add(packageInfo); + when(packageManager.getInstalledPackages(any())).thenReturn(packageInfos); + + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_REMOTE_AUTH, + ActiveUnlockStatusUtils.CONFIG_FLAG_NAME, + flagValue, + false /* makeDefault */); + } + + public static void disable(Context context) { + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_REMOTE_AUTH, + ActiveUnlockStatusUtils.CONFIG_FLAG_NAME, + null /* value */, + false /* makeDefault */); + } +}