diff --git a/res/values/strings.xml b/res/values/strings.xml index cbe56401774..60aab52ec6f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1054,6 +1054,7 @@ Watch Unlock + Watch When you set up Face Unlock and Fingerprint Unlock, your phone will ask for your fingerprint when you wear a mask or are in a dark area.\n\nWatch Unlock is another convenient way to unlock your phone, for example, when your fingers are wet or face isn\u2019t recognized. diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java index 8cc7d6af331..42029ff89f1 100644 --- a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java @@ -28,6 +28,7 @@ import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.settingslib.utils.ThreadUtils; @@ -76,17 +77,22 @@ public class ActiveUnlockContentListener { 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(); + mUri = getUri(authority); } else { mUri = null; } } + /** Returns Active Unlock Uri. */ + public static @NonNull Uri getUri(@NonNull String authority) { + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(CONTENT_PROVIDER_PATH) + .build(); + } + /** Returns true if start listening for updates from the ContentProvider, false otherwise. */ public synchronized boolean subscribe() { if (mSubscribed || mUri == null) { @@ -123,25 +129,40 @@ public class ActiveUnlockContentListener { 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); + + @Nullable String newValue = getContentFromUri( + mContext, mUri, mLogTag, mMethodName, mContentKey); if (!TextUtils.equals(mContent, newValue)) { mContent = newValue; mContentChangedListener.onContentChanged(mContent); } } + + /** Get the content from Uri. */ + public static @Nullable String getContentFromUri( + @NonNull Context context, + @NonNull Uri uri, + @NonNull String logTag, + @NonNull String methodName, + @NonNull String contentKey) { + ContentResolver contentResolver = context.getContentResolver(); + ContentProviderClient client = contentResolver.acquireContentProviderClient(uri); + + @Nullable Bundle bundle = null; + + try { + bundle = client.call(methodName, /* arg= */ null, /* extras = */ null); + } catch (RemoteException e) { + Log.e(logTag, "Failed to call contentProvider", e); + } finally { + client.close(); + } + + if (bundle == null) { + Log.e(logTag, "Null bundle returned from contentProvider"); + return null; + } + + return bundle.getString(contentKey); + } } diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java index 1badb0f26ec..9e8176294e5 100644 --- a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java @@ -21,8 +21,8 @@ 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"; + static final String METHOD_NAME = "getDeviceName"; + static final String DEVICE_NAME_KEY = "com.android.settings.active_unlock.device_name"; private final ActiveUnlockContentListener mActiveUnlockContentListener; public ActiveUnlockDeviceNameListener( diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java index 4ff2e900ae2..66485d37601 100644 --- a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java @@ -155,10 +155,17 @@ public class ActiveUnlockStatusUtils { return BasePreferenceController.CONDITIONALLY_UNAVAILABLE; } + /** + * Returns the title of active unlock only. + */ + public @NonNull String getTitleForActiveUnlockOnly() { + return mContext.getString(R.string.security_settings_activeunlock); + } + /** * Returns the title of the combined biometric settings entity when active unlock is enabled. */ - public String getTitleForActiveUnlock() { + public @NonNull String getTitleForActiveUnlock() { final boolean faceAllowed = Utils.hasFaceHardware(mContext); final boolean fingerprintAllowed = Utils.hasFingerprintHardware(mContext); return mContext.getString(getTitleRes(faceAllowed, fingerprintAllowed)); @@ -264,6 +271,30 @@ public class ActiveUnlockStatusUtils { return mContext.getString(getUseBiometricTitleRes(faceAllowed, fingerprintAllowed)); } + /** + * Returns the summary from content provider. + */ + @Nullable + public static String getSummaryFromContentProvider( + @NonNull Context context, @NonNull String authority, @NonNull String logTag) { + return ActiveUnlockContentListener.getContentFromUri( + context, ActiveUnlockContentListener.getUri(authority), logTag, + ActiveUnlockSummaryListener.METHOD_NAME, + ActiveUnlockSummaryListener.SUMMARY_KEY); + } + + /** + * Returns the device name from content provider. + */ + @Nullable + public static String getDeviceNameFromContentProvider( + @NonNull Context context, @NonNull String authority, @NonNull String logTag) { + return ActiveUnlockContentListener.getContentFromUri( + context, ActiveUnlockContentListener.getUri(authority), logTag, + ActiveUnlockDeviceNameListener.METHOD_NAME, + ActiveUnlockDeviceNameListener.DEVICE_NAME_KEY); + } + @StringRes private static int getUseBiometricTitleRes( boolean isFaceAllowed, boolean isFingerprintAllowed) { diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java index bcffe6297d1..38e137bd379 100644 --- a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java @@ -21,8 +21,8 @@ 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"; + static final String METHOD_NAME = "getSummary"; + static final String SUMMARY_KEY = "com.android.settings.summary"; private final ActiveUnlockContentListener mContentListener; public ActiveUnlockSummaryListener( diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java index 1d8b7a10a3c..ed4b713df20 100644 --- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java +++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java @@ -66,7 +66,7 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { @VisibleForTesting static final int CONFIRM_REQUEST = 2001; private static final int CHOOSE_LOCK_REQUEST = 2002; - protected static final int ACTIVE_UNLOCK_REQUEST = 2003; + public static final int ACTIVE_UNLOCK_REQUEST = 2003; @VisibleForTesting static final int BIOMETRIC_AUTH_REQUEST = 2004; diff --git a/src/com/android/settings/safetycenter/LockScreenSafetySource.java b/src/com/android/settings/safetycenter/LockScreenSafetySource.java index 00a4c676a80..61f05f7f02b 100644 --- a/src/com/android/settings/safetycenter/LockScreenSafetySource.java +++ b/src/com/android/settings/safetycenter/LockScreenSafetySource.java @@ -131,6 +131,7 @@ public final class LockScreenSafetySource { if (Flags.biometricsOnboardingEducation()) { FaceSafetySource.onBiometricsChanged(context); FingerprintSafetySource.onBiometricsChanged(context); + WearSafetySource.onBiometricsChanged(context); } else { BiometricsSafetySource.onBiometricsChanged(context); } diff --git a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java index a49b7e0f860..4cf40ddbdbe 100644 --- a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java +++ b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java @@ -86,6 +86,9 @@ public class SafetySourceBroadcastReceiver extends BroadcastReceiver { if (sourceIds.contains(FingerprintSafetySource.SAFETY_SOURCE_ID)) { FingerprintSafetySource.setSafetySourceData(context, safetyEvent); } + if (sourceIds.contains(WearSafetySource.SAFETY_SOURCE_ID)) { + WearSafetySource.setSafetySourceData(context, safetyEvent); + } } private static void refreshAllSafetySources(Context context, SafetyEvent safetyEvent) { @@ -95,5 +98,6 @@ public class SafetySourceBroadcastReceiver extends BroadcastReceiver { PrivateSpaceSafetySource.setSafetySourceData(context, safetyEvent); FaceSafetySource.setSafetySourceData(context, safetyEvent); FingerprintSafetySource.setSafetySourceData(context, safetyEvent); + WearSafetySource.setSafetySourceData(context, safetyEvent); } } diff --git a/src/com/android/settings/safetycenter/WearSafetySource.java b/src/com/android/settings/safetycenter/WearSafetySource.java new file mode 100644 index 00000000000..a345096728b --- /dev/null +++ b/src/com/android/settings/safetycenter/WearSafetySource.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2025 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.safetycenter; + +import static com.android.settings.biometrics.combination.BiometricsSettingsBase.ACTIVE_UNLOCK_REQUEST; + +import android.app.PendingIntent; +import android.content.Context; +import android.os.UserManager; +import android.safetycenter.SafetyEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; +import com.android.settings.flags.Flags; + +/** Wear Safety Source for Safety Center. */ +public final class WearSafetySource { + + private static final String TAG = "WearSafetySource"; + public static final String SAFETY_SOURCE_ID = "AndroidWearUnlock"; + private static boolean sIsTestingEnv = false; + private static String sSummaryForTesting = ""; + private static boolean sHasEnrolledForTesting; + + private WearSafetySource() {} + + /** Sets test value for summary. */ + @VisibleForTesting + public static void setSummaryForTesting(@NonNull String summary) { + sIsTestingEnv = true; + sSummaryForTesting = summary; + } + + /** Sets test value for hasEnrolled. */ + @VisibleForTesting + public static void setHasEnrolledForTesting(boolean hasEnrolled) { + sIsTestingEnv = true; + sHasEnrolledForTesting = hasEnrolled; + } + + /** Sets biometric safety data for Safety Center. */ + public static void setSafetySourceData( + @NonNull Context context, @NonNull SafetyEvent safetyEvent) { + if (!SafetyCenterManagerWrapper.get().isEnabled(context)) { + return; + } + if (!Flags.biometricsOnboardingEducation()) { // this source is effectively turned off + sendNullData(context, safetyEvent); + return; + } + + // Handle private profile case. + UserManager userManager = UserManager.get(context); + if (android.os.Flags.allowPrivateProfile() + && android.multiuser.Flags.enablePrivateSpaceFeatures() + && userManager.isPrivateProfile()) { + // SC always expects a response from the source if the broadcast has been sent for this + // source, therefore, we need to send a null SafetySourceData. + sendNullData(context, safetyEvent); + return; + } + + ActiveUnlockStatusUtils activeUnlockStatusUtils = new ActiveUnlockStatusUtils(context); + if (!userManager.isProfile() && activeUnlockStatusUtils.isAvailable()) { + boolean hasEnrolled = false; + String summary = ""; + + if (sIsTestingEnv) { + hasEnrolled = sHasEnrolledForTesting; + summary = sSummaryForTesting; + } else { + String authority = new ActiveUnlockStatusUtils(context).getAuthority(); + hasEnrolled = getHasEnrolledFromContentProvider(context, authority); + summary = getSummaryFromContentProvider(context, authority); + } + + BiometricSourcesUtils.setBiometricSafetySourceData( + SAFETY_SOURCE_ID, + context, + activeUnlockStatusUtils.getTitleForActiveUnlockOnly(), + summary, + PendingIntent.getActivity(context, ACTIVE_UNLOCK_REQUEST, + activeUnlockStatusUtils.getIntent(), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT), + /* enabled= */ true, + hasEnrolled, + safetyEvent); + return; + } + + sendNullData(context, safetyEvent); + } + + private static void sendNullData(Context context, SafetyEvent safetyEvent) { + SafetyCenterManagerWrapper.get() + .setSafetySourceData( + context, SAFETY_SOURCE_ID, /* safetySourceData= */ null, safetyEvent); + } + + /** Notifies Safety Center of a change in wear biometrics settings. */ + public static void onBiometricsChanged(@NonNull Context context) { + setSafetySourceData( + context, + new SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED) + .build()); + } + + private static boolean getHasEnrolledFromContentProvider( + @NonNull Context context, @Nullable String authority) { + if (authority == null) { + return false; + } + return ActiveUnlockStatusUtils.getDeviceNameFromContentProvider(context, authority, TAG) + != null; + } + + private static String getSummaryFromContentProvider( + @NonNull Context context, @Nullable String authority) { + if (authority == null) { + return ""; + } + String summary = ActiveUnlockStatusUtils.getSummaryFromContentProvider( + context, authority, TAG); + if (summary == null) { + return ""; + } + return summary; + } + +} diff --git a/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java index 563974d5287..6da6aa78103 100644 --- a/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java @@ -243,4 +243,11 @@ public class ActiveUnlockStatusUtilsTest { .isEqualTo(mApplicationContext.getString( R.string.biometric_settings_use_watch_for)); } + + @Test + public void getTitleForActiveUnlockOnly_returnsTile() { + assertThat(mActiveUnlockStatusUtils.getTitleForActiveUnlockOnly()) + .isEqualTo(mApplicationContext.getString( + R.string.security_settings_activeunlock)); + } } diff --git a/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java b/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java index f16113ab13d..6e46d2be551 100644 --- a/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java +++ b/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java @@ -527,6 +527,9 @@ public class LockScreenSafetySourceTest { verify(mSafetyCenterManagerWrapper) .setSafetySourceData( any(), eq(FingerprintSafetySource.SAFETY_SOURCE_ID), any(), any()); + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), eq(WearSafetySource.SAFETY_SOURCE_ID), any(), any()); } @Test diff --git a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java index e65d041e248..836247c38c5 100644 --- a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java +++ b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java @@ -245,6 +245,25 @@ public class SafetySourceBroadcastReceiverTest { assertThat(captor.getValue()).isEqualTo(FaceSafetySource.SAFETY_SOURCE_ID); } + @Test + public void onReceive_onRefresh_withWearUnlockSourceId_setsWearUnlockData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + Intent intent = + new Intent() + .setAction(ACTION_REFRESH_SAFETY_SOURCES) + .putExtra( + EXTRA_REFRESH_SAFETY_SOURCE_IDS, + new String[] {WearSafetySource.SAFETY_SOURCE_ID}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(mSafetyCenterManagerWrapper, times(1)) + .setSafetySourceData(any(), captor.capture(), any(), any()); + + assertThat(captor.getValue()).isEqualTo(WearSafetySource.SAFETY_SOURCE_ID); + } + @Test public void onReceive_onRefresh_withFingerprintUnlockSourceId_setsFingerprintUnlockData() { when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); @@ -332,7 +351,7 @@ public class SafetySourceBroadcastReceiverTest { new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(mSafetyCenterManagerWrapper, times(5)) + verify(mSafetyCenterManagerWrapper, times(6)) .setSafetySourceData(any(), captor.capture(), any(), any()); List safetySourceIdList = captor.getAllValues(); @@ -353,6 +372,11 @@ public class SafetySourceBroadcastReceiverTest { .anyMatch( id -> id.equals(FingerprintSafetySource.SAFETY_SOURCE_ID))) .isTrue(); + assertThat( + safetySourceIdList.stream() + .anyMatch( + id -> id.equals(WearSafetySource.SAFETY_SOURCE_ID))) + .isTrue(); assertThat( safetySourceIdList.stream() .anyMatch( diff --git a/tests/unit/src/com/android/settings/safetycenter/WearSafetySourceTest.java b/tests/unit/src/com/android/settings/safetycenter/WearSafetySourceTest.java new file mode 100644 index 00000000000..c0c982d44fe --- /dev/null +++ b/tests/unit/src/com/android/settings/safetycenter/WearSafetySourceTest.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2025 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.safetycenter; + +import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.hardware.fingerprint.FingerprintManager; +import android.os.UserHandle; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.provider.DeviceConfig; +import android.provider.Settings; +import android.safetycenter.SafetyEvent; +import android.safetycenter.SafetySourceData; +import android.safetycenter.SafetySourceStatus; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; +import com.android.settings.flags.Flags; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.ResourcesUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class WearSafetySourceTest { + + private static final ComponentName COMPONENT_NAME = new ComponentName("package", "class"); + private static final UserHandle USER_HANDLE = new UserHandle(UserHandle.myUserId()); + private static final SafetyEvent EVENT_SOURCE_STATE_CHANGED = + new SafetyEvent.Builder(SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build(); + 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 final String SUMMARY = "Wear Summary"; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + private Context mApplicationContext; + + @Mock private PackageManager mPackageManager; + @Mock private DevicePolicyManager mDevicePolicyManager; + @Mock private FingerprintManager mFingerprintManager; + @Mock private LockPatternUtils mLockPatternUtils; + @Mock private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + 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(mDevicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent(USER_HANDLE)) + .thenReturn(COMPONENT_NAME); + when(mApplicationContext.getSystemService(Context.FINGERPRINT_SERVICE)) + .thenReturn(mFingerprintManager); + when(mApplicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE)) + .thenReturn(mDevicePolicyManager); + FakeFeatureFactory featureFactory = FakeFeatureFactory.setupForTest(); + when(featureFactory.securityFeatureProvider.getLockPatternUtils(mApplicationContext)) + .thenReturn(mLockPatternUtils); + doReturn(true).when(mLockPatternUtils).isSecure(anyInt()); + SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper; + } + + @After + public void tearDown() { + SafetyCenterManagerWrapper.sInstance = null; + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetyData_whenSafetyCenterIsDisabled_doesNotSetData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(false); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + verify(mSafetyCenterManagerWrapper, never()) + .setSafetySourceData(any(), any(), any(), any()); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_whenSeparateBiometricsFlagOff_setsNullData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), eq(WearSafetySource.SAFETY_SOURCE_ID), eq(null), any()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_whenSafetyCenterIsEnabled_activeUnlockDisabled_setsNullData() { + disableActiveUnlock(mApplicationContext); + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), eq(WearSafetySource.SAFETY_SOURCE_ID), eq(null), any()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_setsDataWithCorrectSafetyEvent() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData(any(), any(), any(), eq(EVENT_SOURCE_STATE_CHANGED)); + } + + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_withWearEnabled_whenWearEnrolled_setsData() { + enableActiveUnlock(mApplicationContext); + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFingerprintManager.hasEnrolledFingerprints(anyInt())).thenReturn(false); + + WearSafetySource.setHasEnrolledForTesting(true); + WearSafetySource.setSummaryForTesting(SUMMARY); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + assertSafetySourceEnabledDataSet( + ResourcesUtils.getResourcesString(mApplicationContext, + "security_settings_activeunlock"), + SUMMARY); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_withWearEnabled_whenWearNotEnrolled_setsData() { + enableActiveUnlock(mApplicationContext); + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFingerprintManager.hasEnrolledFingerprints(anyInt())).thenReturn(false); + when(mDevicePolicyManager.getKeyguardDisabledFeatures(COMPONENT_NAME)).thenReturn(0); + + WearSafetySource.setHasEnrolledForTesting(false); + WearSafetySource.setSummaryForTesting(SUMMARY); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + assertSafetySourceDisabledDataSet( + ResourcesUtils.getResourcesString(mApplicationContext, + "security_settings_activeunlock"), + SUMMARY); + } + + private static void disableActiveUnlock(Context context) { + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_REMOTE_AUTH, + ActiveUnlockStatusUtils.CONFIG_FLAG_NAME, + /* value= */ null, + /* makeDefault=*/ false); + Settings.Secure.putString(context.getContentResolver(), TARGET_SETTING, null); + Settings.Secure.putString(context.getContentResolver(), PROVIDER_SETTING, null); + } + + private static void enableActiveUnlock(Context context) { + 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); + + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.authority = PROVIDER; + providerInfo.applicationInfo = applicationInfo; + when(packageManager.resolveContentProvider(anyString(), any())).thenReturn(providerInfo); + + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_REMOTE_AUTH, + ActiveUnlockStatusUtils.CONFIG_FLAG_NAME, + "unlock_intent_layout", + false /* makeDefault */); + } + + private void assertSafetySourceDisabledDataSet(String expectedTitle, String expectedSummary) { + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), + eq(WearSafetySource.SAFETY_SOURCE_ID), + captor.capture(), + any()); + SafetySourceData safetySourceData = captor.getValue(); + SafetySourceStatus safetySourceStatus = safetySourceData.getStatus(); + + assertThat(safetySourceStatus.getTitle().toString()).isEqualTo(expectedTitle); + assertThat(safetySourceStatus.getSummary().toString()).isEqualTo(expectedSummary); + assertThat(safetySourceStatus.isEnabled()).isTrue(); + assertThat(safetySourceStatus.getSeverityLevel()) + .isEqualTo(SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED); + + Intent clickIntent = safetySourceStatus.getPendingIntent().getIntent(); + assertThat(clickIntent).isNotNull(); + assertThat(clickIntent.getAction()).isEqualTo(TARGET); + } + + private void assertSafetySourceEnabledDataSet( + String expectedTitle, String expectedSummary) { + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), + eq(WearSafetySource.SAFETY_SOURCE_ID), + captor.capture(), + any()); + SafetySourceData safetySourceData = captor.getValue(); + SafetySourceStatus safetySourceStatus = safetySourceData.getStatus(); + + assertThat(safetySourceStatus.getTitle().toString()).isEqualTo(expectedTitle); + assertThat(safetySourceStatus.getSummary().toString()).isEqualTo(expectedSummary); + assertThat(safetySourceStatus.isEnabled()).isTrue(); + assertThat(safetySourceStatus.getSeverityLevel()) + .isEqualTo(SafetySourceData.SEVERITY_LEVEL_INFORMATION); + Intent clickIntent = safetySourceStatus.getPendingIntent().getIntent(); + assertThat(clickIntent).isNotNull(); + } +}