Merge changes from topic "au-initial-commit-master"

* changes:
  Update text when Active Unlock is enabled.
  Update tile summary from ContentProvider.
  Add Active Unlock tile under face & fingerprint
  Add ActiveUnlock check when picking preference
This commit is contained in:
Derek Jedral
2023-01-31 03:24:22 +00:00
committed by Android (Google) Code Review
29 changed files with 1763 additions and 11 deletions

View File

@@ -6802,6 +6802,9 @@
<!-- Search keyword for biometric settings. [CHAR_LIMIT=NONE]--> <!-- Search keyword for biometric settings. [CHAR_LIMIT=NONE]-->
<string name="keywords_biometric_settings">face, fingerprint, add fingerprint</string> <string name="keywords_biometric_settings">face, fingerprint, add fingerprint</string>
<!-- Search keyword for active unlock settings. [CHAR_LIMIT=NONE]-->
<string name="keywords_active_unlock_settings">watch unlock, add watch unlock</string>
<!-- Search keywords for adaptive brightness setting [CHAR LIMIT=NONE]--> <!-- Search keywords for adaptive brightness setting [CHAR LIMIT=NONE]-->
<string name="keywords_display_auto_brightness">dim screen, touchscreen, battery, smart brightness, dynamic brightness, Auto brightness</string> <string name="keywords_display_auto_brightness">dim screen, touchscreen, battery, smart brightness, dynamic brightness, Auto brightness</string>

View File

@@ -20,6 +20,7 @@
android:title="@string/security_settings_biometric_preference_title"> android:title="@string/security_settings_biometric_preference_title">
<com.android.settingslib.widget.TopIntroPreference <com.android.settingslib.widget.TopIntroPreference
android:key="biometric_intro"
android:title="@string/biometric_settings_intro" /> android:title="@string/biometric_settings_intro" />
<PreferenceCategory <PreferenceCategory
@@ -40,6 +41,12 @@
settings:keywords="@string/keywords_fingerprint_settings" settings:keywords="@string/keywords_fingerprint_settings"
settings:controller="com.android.settings.biometrics.combination.BiometricFingerprintStatusPreferenceController" /> settings:controller="com.android.settings.biometrics.combination.BiometricFingerprintStatusPreferenceController" />
<com.android.settingslib.RestrictedPreference
android:key="biometric_active_unlock_settings"
android:title="@string/security_settings_activeunlock_preference_title"
android:summary="@string/summary_placeholder"
settings:keywords="@string/keywords_active_unlock_settings"
settings:controller="com.android.settings.biometrics.activeunlock.ActiveUnlockStatusPreferenceController" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
@@ -59,4 +66,4 @@
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@@ -25,6 +25,7 @@ import androidx.preference.Preference;
import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternUtils;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
import com.android.settings.core.BasePreferenceController; import com.android.settings.core.BasePreferenceController;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
@@ -37,11 +38,17 @@ public abstract class BiometricStatusPreferenceController extends BasePreference
protected final int mProfileChallengeUserId; protected final int mProfileChallengeUserId;
private final BiometricNavigationUtils mBiometricNavigationUtils; 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. * @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. * @return the summary text.
@@ -61,13 +68,21 @@ public abstract class BiometricStatusPreferenceController extends BasePreference
.getLockPatternUtils(context); .getLockPatternUtils(context);
mProfileChallengeUserId = Utils.getManagedProfileId(mUm, mUserId); mProfileChallengeUserId = Utils.getManagedProfileId(mUm, mUserId);
mBiometricNavigationUtils = new BiometricNavigationUtils(getUserId()); mBiometricNavigationUtils = new BiometricNavigationUtils(getUserId());
mActiveUnlockStatusUtils = new ActiveUnlockStatusUtils(context);
} }
@Override @Override
public int getAvailabilityStatus() { public int getAvailabilityStatus() {
if (mActiveUnlockStatusUtils.isAvailable()) {
return getAvailabilityStatusWithWorkProfileCheck();
}
if (!isDeviceSupported()) { if (!isDeviceSupported()) {
return UNSUPPORTED_ON_DEVICE; return UNSUPPORTED_ON_DEVICE;
} }
return getAvailabilityFromUserSupported();
}
private int getAvailabilityFromUserSupported() {
if (isUserSupported()) { if (isUserSupported()) {
return AVAILABLE; return AVAILABLE;
} else { } 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 @Override
public void updateState(Preference preference) { public void updateState(Preference preference) {
if (!isAvailable()) { if (!isAvailable()) {
@@ -105,4 +135,11 @@ public abstract class BiometricStatusPreferenceController extends BasePreference
protected boolean isUserSupported() { protected boolean isUserSupported() {
return true; return true;
} }
/**
* Returns true if the controller controls is used for work profile.
*/
protected boolean isWorkProfileController() {
return false;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,4 +46,9 @@ public class BiometricFaceProfileStatusPreferenceController extends
protected int getUserId() { protected int getUserId() {
return mProfileChallengeUserId; return mProfileChallengeUserId;
} }
@Override
protected boolean isWorkProfileController() {
return true;
}
} }

View File

@@ -39,6 +39,11 @@ public class BiometricFaceStatusPreferenceController extends FaceStatusPreferenc
@Override @Override
protected boolean isDeviceSupported() { protected boolean isDeviceSupported() {
return Utils.isMultipleBiometricsSupported(mContext) && Utils.hasFaceHardware(mContext); return Utils.isMultipleBiometricsSupported(mContext) && isHardwareSupported();
}
@Override
protected boolean isHardwareSupported() {
return Utils.hasFaceHardware(mContext);
} }
} }

View File

@@ -46,4 +46,9 @@ public class BiometricFingerprintProfileStatusPreferenceController extends
protected int getUserId() { protected int getUserId() {
return mProfileChallengeUserId; return mProfileChallengeUserId;
} }
@Override
protected boolean isWorkProfileController() {
return true;
}
} }

View File

@@ -40,7 +40,11 @@ public class BiometricFingerprintStatusPreferenceController extends
@Override @Override
protected boolean isDeviceSupported() { protected boolean isDeviceSupported() {
return Utils.isMultipleBiometricsSupported(mContext) return Utils.isMultipleBiometricsSupported(mContext) && isHardwareSupported();
&& Utils.hasFingerprintHardware(mContext); }
@Override
protected boolean isHardwareSupported() {
return Utils.hasFingerprintHardware(mContext);
} }
} }

View File

@@ -24,6 +24,7 @@ import android.hardware.fingerprint.FingerprintManager;
import android.provider.Settings; import android.provider.Settings;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
import com.android.settings.core.TogglePreferenceController; import com.android.settings.core.TogglePreferenceController;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedLockUtilsInternal;
@@ -69,7 +70,10 @@ public class BiometricSettingsAppPreferenceController extends TogglePreferenceCo
@Override @Override
public int getAvailabilityStatus() { public int getAvailabilityStatus() {
if (!Utils.isMultipleBiometricsSupported(mContext)) { final ActiveUnlockStatusUtils activeUnlockStatusUtils =
new ActiveUnlockStatusUtils(mContext);
if (!Utils.isMultipleBiometricsSupported(mContext)
&& !activeUnlockStatusUtils.isAvailable()) {
return UNSUPPORTED_ON_DEVICE; return UNSUPPORTED_ON_DEVICE;
} }
if (mFaceManager == null || mFingerprintManager == null) { if (mFaceManager == null || mFingerprintManager == null) {

View File

@@ -22,6 +22,7 @@ import android.content.Context;
import android.provider.Settings; import android.provider.Settings;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
import com.android.settings.core.TogglePreferenceController; import com.android.settings.core.TogglePreferenceController;
import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedLockUtilsInternal;
@@ -63,9 +64,18 @@ public class BiometricSettingsKeyguardPreferenceController extends TogglePrefere
@Override @Override
public int getAvailabilityStatus() { public int getAvailabilityStatus() {
final ActiveUnlockStatusUtils activeUnlockStatusUtils =
new ActiveUnlockStatusUtils(mContext);
if (activeUnlockStatusUtils.isAvailable()) {
return getAvailabilityFromRestrictingAdmin();
}
if (!Utils.isMultipleBiometricsSupported(mContext)) { if (!Utils.isMultipleBiometricsSupported(mContext)) {
return UNSUPPORTED_ON_DEVICE; return UNSUPPORTED_ON_DEVICE;
} }
return getAvailabilityFromRestrictingAdmin();
}
private int getAvailabilityFromRestrictingAdmin() {
return getRestrictingAdmin() != null ? DISABLED_FOR_USER : AVAILABLE; return getRestrictingAdmin() != null ? DISABLED_FOR_USER : AVAILABLE;
} }

View File

@@ -107,10 +107,7 @@ public abstract class BiometricsSettingsBase extends DashboardFragment {
launchChooseOrConfirmLock(); launchChooseOrConfirmLock();
} }
final Preference unlockPhonePreference = findPreference(getUnlockPhonePreferenceKey()); updateUnlockPhonePreferenceSummary();
if (unlockPhonePreference != null) {
unlockPhonePreference.setSummary(getUseAnyBiometricSummary());
}
final Preference useInAppsPreference = findPreference(getUseInAppsPreferenceKey()); final Preference useInAppsPreference = findPreference(getUseInAppsPreferenceKey());
if (useInAppsPreference != null) { 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 @NonNull
private String getUseAnyBiometricSummary() { protected String getUseAnyBiometricSummary() {
boolean isFaceAllowed = mFaceManager != null && mFaceManager.isHardwareDetected(); boolean isFaceAllowed = mFaceManager != null && mFaceManager.isHardwareDetected();
boolean isFingerprintAllowed = boolean isFingerprintAllowed =
mFingerprintManager != null && mFingerprintManager.isHardwareDetected(); mFingerprintManager != null && mFingerprintManager.isHardwareDetected();

View File

@@ -62,4 +62,9 @@ public class CombinedBiometricProfileStatusPreferenceController extends
protected String getSettingsClassName() { protected String getSettingsClassName() {
return mCombinedBiometricStatusUtils.getProfileSettingsClassName(); return mCombinedBiometricStatusUtils.getProfileSettingsClassName();
} }
@Override
protected boolean isWorkProfileController() {
return true;
}
} }

View File

@@ -17,8 +17,15 @@ package com.android.settings.biometrics.combination;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
import android.content.Context; 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.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.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.search.SearchIndexable; 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_FINGERPRINT_SETTINGS = "biometric_fingerprint_settings";
private static final String KEY_UNLOCK_PHONE = "biometric_settings_biometric_keyguard"; 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_USE_IN_APPS = "biometric_settings_biometric_app";
private static final String KEY_INTRO_PREFERENCE = "biometric_intro";
private ActiveUnlockStatusUtils mActiveUnlockStatusUtils;
@Nullable private ActiveUnlockDeviceNameListener mActiveUnlockDeviceNameListener;
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
@@ -40,6 +51,41 @@ public class CombinedBiometricSettings extends BiometricsSettingsBase {
use(BiometricSettingsAppPreferenceController.class).setUserId(mUserId); 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 @Override
protected int getPreferenceScreenResId() { protected int getPreferenceScreenResId() {
return R.xml.security_settings_combined_biometric; return R.xml.security_settings_combined_biometric;
@@ -75,6 +121,16 @@ public class CombinedBiometricSettings extends BiometricsSettingsBase {
return SettingsEnums.COMBINED_BIOMETRIC; 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 = public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new CombinedBiometricSearchIndexProvider(R.xml.security_settings_combined_biometric); new CombinedBiometricSearchIndexProvider(R.xml.security_settings_combined_biometric);
} }

View File

@@ -25,6 +25,7 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.Utils;
import com.android.settings.biometrics.BiometricStatusPreferenceController; import com.android.settings.biometrics.BiometricStatusPreferenceController;
import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedPreference; import com.android.settingslib.RestrictedPreference;
@@ -84,6 +85,11 @@ public class CombinedBiometricStatusPreferenceController extends
return mCombinedBiometricStatusUtils.isAvailable(); return mCombinedBiometricStatusUtils.isAvailable();
} }
@Override
protected boolean isHardwareSupported() {
return Utils.hasFaceHardware(mContext) || Utils.hasFingerprintHardware(mContext);
}
@Override @Override
public void updateState(Preference preference) { public void updateState(Preference preference) {
super.updateState(preference); super.updateState(preference);

View File

@@ -84,4 +84,9 @@ public class FaceProfileStatusPreferenceController extends FaceStatusPreferenceC
mContext.getResources().getString( mContext.getResources().getString(
R.string.security_settings_face_profile_preference_title))); R.string.security_settings_face_profile_preference_title)));
} }
@Override
protected boolean isWorkProfileController() {
return true;
}
} }

View File

@@ -86,6 +86,11 @@ public class FaceStatusPreferenceController extends BiometricStatusPreferenceCon
return mFaceStatusUtils.isAvailable(); return mFaceStatusUtils.isAvailable();
} }
@Override
protected boolean isHardwareSupported() {
return Utils.hasFaceHardware(mContext);
}
@Override @Override
public void updateState(Preference preference) { public void updateState(Preference preference) {
super.updateState(preference); super.updateState(preference);

View File

@@ -53,4 +53,9 @@ public class FingerprintProfileStatusPreferenceController
protected int getUserId() { protected int getUserId() {
return mProfileChallengeUserId; return mProfileChallengeUserId;
} }
@Override
protected boolean isWorkProfileController() {
return true;
}
} }

View File

@@ -86,6 +86,11 @@ public class FingerprintStatusPreferenceController extends BiometricStatusPrefer
return mFingerprintStatusUtils.isAvailable(); return mFingerprintStatusUtils.isAvailable();
} }
@Override
protected boolean isHardwareSupported() {
return Utils.hasFingerprintHardware(mContext);
}
@Override @Override
public void updateState(Preference preference) { public void updateState(Preference preference) {
super.updateState(preference); super.updateState(preference);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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