Support remote device credentials validation in UI.
Test: m RunSettingsRoboTests -j ROBOTEST_FILTER=com.android.settings.password
Test: Manual
Bug: 258505917
Change-Id: Ifb9f15728eb8396b34c844d28f71a8e6e1aad837
diff --git a/Android.bp b/Android.bp
index dc7270e..ea2c983 100644
--- a/Android.bp
+++ b/Android.bp
@@ -87,6 +87,7 @@
"fuelgauge-log-protos-lite",
"fuelgauge-usage-state-protos-lite",
"contextualcards",
+ "securebox",
"settings-logtags",
"statslog-settings",
"zxing-core-1.7",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 846d246..fcdb3ff 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -88,6 +88,7 @@
<uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
<uses-permission android:name="android.permission.READ_SEARCH_INDEXABLES" />
<uses-permission android:name="android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE" />
+ <uses-permission android:name="android.permission.BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE" />
<uses-permission android:name="android.permission.OEM_UNLOCK_STATE" />
<uses-permission android:name="android.permission.MANAGE_USER_OEM_UNLOCK_STATE" />
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG" />
@@ -2370,6 +2371,17 @@
<activity-alias android:name=".ConfirmDeviceCredentialActivity"
android:targetActivity=".password.ConfirmDeviceCredentialActivity"
android:exported="true" />
+ <!-- Activity alias for remote lockscreen validation. Enforces required permission -->
+ <activity-alias
+ android:name=".ConfirmRemoteDeviceCredentialActivity"
+ android:targetActivity=".password.ConfirmDeviceCredentialActivity"
+ android:permission="android.permission.CHECK_REMOTE_LOCKSCREEN"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.app.action.CONFIRM_REMOTE_DEVICE_CREDENTIAL"/>
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity-alias>
<!-- Note this must not be exported since it authenticates the given user -->
<activity android:name=".password.ConfirmDeviceCredentialActivity$InternalActivity"
diff --git a/res/layout-land/confirm_lock_pattern_normal_base.xml b/res/layout-land/confirm_lock_pattern_normal_base.xml
index 9a5097e..18fb142 100644
--- a/res/layout-land/confirm_lock_pattern_normal_base.xml
+++ b/res/layout-land/confirm_lock_pattern_normal_base.xml
@@ -15,10 +15,12 @@
-->
<com.google.android.setupdesign.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/setup_wizard_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:icon="@drawable/ic_lock">
+ android:icon="@drawable/ic_lock"
+ app:sudUseBottomProgressBar="true">
<com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient
android:id="@+id/topLayout"
@@ -60,6 +62,16 @@
android:layout_marginEnd="?attr/sudMarginEnd"
android:gravity="center_vertical"/>
+ <CheckBox
+ android:id="@+id/checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="?attr/sudMarginStart"
+ android:layout_marginEnd="?attr/sudMarginEnd"
+ android:layout_marginTop="12dp"
+ android:visibility="gone"
+ android:checked="true" />
+
<Button
android:id="@+id/cancelButton"
style="@style/SudGlifButton.Secondary"
diff --git a/res/layout/confirm_lock_password_normal.xml b/res/layout/confirm_lock_password_normal.xml
index c2a7b5d..d4cc332 100644
--- a/res/layout/confirm_lock_password_normal.xml
+++ b/res/layout/confirm_lock_password_normal.xml
@@ -15,11 +15,13 @@
-->
<com.google.android.setupdesign.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/setup_wizard_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:icon="@drawable/ic_lock"
- android:importantForAutofill="noExcludeDescendants">
+ android:importantForAutofill="noExcludeDescendants"
+ app:sudUseBottomProgressBar="true">
<com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient
android:id="@+id/topLayout"
@@ -27,14 +29,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <TextView
- android:id="@+id/sud_layout_description"
- style="@style/SudDescription.Glif"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="?attr/sudMarginStart"
- android:layout_marginEnd="?attr/sudMarginEnd" />
-
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
@@ -73,6 +67,17 @@
android:layout_height="0dp"
android:layout_weight="1" />
+ <CheckBox
+ android:id="@+id/checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="?attr/sudMarginStart"
+ android:layout_marginEnd="?attr/sudMarginEnd"
+ android:layout_marginTop="12dp"
+ android:layout_gravity="center_horizontal"
+ android:visibility="gone"
+ android:checked="true" />
+
<Button
android:id="@+id/cancelButton"
style="@style/SudGlifButton.Secondary"
diff --git a/res/layout/confirm_lock_pattern_normal_base.xml b/res/layout/confirm_lock_pattern_normal_base.xml
index 44d9ab7..51bab67 100644
--- a/res/layout/confirm_lock_pattern_normal_base.xml
+++ b/res/layout/confirm_lock_pattern_normal_base.xml
@@ -15,10 +15,12 @@
-->
<com.google.android.setupdesign.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/setup_wizard_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:icon="@drawable/ic_lock">
+ android:icon="@drawable/ic_lock"
+ app:sudUseBottomProgressBar="true">
<com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient
android:id="@+id/topLayout"
@@ -61,6 +63,16 @@
android:layout_marginTop="12dp"
android:gravity="center_vertical"/>
+ <CheckBox
+ android:id="@+id/checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="?attr/sudMarginStart"
+ android:layout_marginEnd="?attr/sudMarginEnd"
+ android:layout_marginTop="12dp"
+ android:visibility="gone"
+ android:checked="true" />
+
<Button
android:id="@+id/cancelButton"
style="@style/SudGlifButton.Secondary"
@@ -68,7 +80,6 @@
android:layout_height="wrap_content"
android:layout_marginStart="?attr/sudMarginStart"
android:layout_marginEnd="?attr/sudMarginEnd"
- android:layout_marginBottom="80dp"
android:text="@string/cancel" />
<Button
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a5271f9..4318f55 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -3339,6 +3339,24 @@
<!-- Header shown when the password needs to be solved because the device was factory reset. [CHAR LIMIT=100] -->
<string name="lockpassword_confirm_your_password_header_frp">Verify password</string>
+ <!-- Header shown when prompted for remote device credential validation. [CHAR LIMIT=17] -->
+ <string name="lockpassword_remote_validation_header">Verify it\u0027s you</string>
+
+ <!-- Details shown when pattern is prompted for remote device credential validation. [CHAR LIMIT=100] -->
+ <string name="lockpassword_remote_validation_pattern_details">Enter your other device\u0027s pattern to securely transfer Google Accounts, settings, and more. Your pattern is encrypted.</string>
+ <!-- Details shown when PIN is prompted for remote device credential validation. [CHAR LIMIT=100] -->
+ <string name="lockpassword_remote_validation_pin_details">Enter your other device\u0027s PIN to securely transfer Google Accounts, settings, and more. Your PIN is encrypted.</string>
+ <!-- Details shown when password is prompted for remote device credential validation. [CHAR LIMIT=100] -->
+ <string name="lockpassword_remote_validation_password_details">Enter your other device\u0027s password to securely transfer Google Accounts, settings, and more. Your password is encrypted.</string>
+
+ <!-- Checkbox label to set pattern as new screen lock if remote device credential validation succeeds. [CHAR LIMIT=43] -->
+ <string name="lockpassword_remote_validation_set_pattern_as_screenlock">Also use pattern to unlock this device</string>
+ <!-- Checkbox label to set PIN as new screen lock if remote device credential validation succeeds. [CHAR LIMIT=43] -->
+ <string name="lockpassword_remote_validation_set_pin_as_screenlock">Also use PIN to unlock this device</string>
+ <!-- Checkbox label to set password as new screen lock if remote device credential validation succeeds. [CHAR LIMIT=43] -->
+ <string name="lockpassword_remote_validation_set_password_as_screenlock">Also use password to unlock this device</string>
+
+
<!-- Security & location settings screen, change security method screen instruction if user
enters incorrect PIN [CHAR LIMIT=30] -->
<string name="lockpassword_invalid_pin">Wrong PIN</string>
diff --git a/src/com/android/settings/password/ChooseLockSettingsHelper.java b/src/com/android/settings/password/ChooseLockSettingsHelper.java
index 6ccf7be..a1cd09f 100644
--- a/src/com/android/settings/password/ChooseLockSettingsHelper.java
+++ b/src/com/android/settings/password/ChooseLockSettingsHelper.java
@@ -22,7 +22,9 @@
import android.annotation.Nullable;
import android.app.Activity;
import android.app.KeyguardManager;
+import android.app.StartLockscreenValidationRequest;
import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentSender;
import android.os.UserManager;
@@ -41,6 +43,8 @@
import com.google.android.setupcompat.util.WizardManagerHelper;
+import java.util.Optional;
+
public final class ChooseLockSettingsHelper {
private static final String TAG = "ChooseLockSettingsHelper";
@@ -132,6 +136,7 @@
@Nullable private CharSequence mHeader;
@Nullable private CharSequence mDescription;
@Nullable private CharSequence mAlternateButton;
+ @Nullable private CharSequence mCheckBoxLabel;
private boolean mReturnCredentials;
private boolean mExternal;
private boolean mForegroundOnly;
@@ -139,6 +144,9 @@
private int mUserId;
private boolean mAllowAnyUserId;
private boolean mForceVerifyPath;
+ private boolean mRemoteLockscreenValidation;
+ @Nullable private StartLockscreenValidationRequest mStartLockscreenValidationRequest;
+ @Nullable private ComponentName mRemoteLockscreenValidationServiceComponent;
boolean mRequestGatekeeperPasswordHandle;
public Builder(@NonNull Activity activity) {
@@ -192,6 +200,15 @@
}
/**
+ * @param checkboxLabel text for the checkbox
+ */
+ @NonNull
+ public Builder setCheckboxLabel(@Nullable CharSequence checkboxLabel) {
+ mCheckBoxLabel = checkboxLabel;
+ return this;
+ }
+
+ /**
* @param returnCredentials if true, puts the following credentials into intent for
* onActivityResult with the following keys:
* {@link #EXTRA_KEY_PASSWORD},
@@ -254,6 +271,42 @@
}
/**
+ * @param isRemoteLockscreenValidation if true, remote device validation flow will be
+ * started. {@link #setStartLockscreenValidationRequest} and
+ * {@link #setRemoteLockscreenValidationServiceComponent}
+ * must also be used to set the required data.
+ */
+ @NonNull public Builder setRemoteLockscreenValidation(
+ boolean isRemoteLockscreenValidation) {
+ mRemoteLockscreenValidation = isRemoteLockscreenValidation;
+ return this;
+ }
+
+ /**
+ * @param startLockScreenValidationRequest contains information necessary to perform remote
+ * lockscreen validation such as the remote device's
+ * lockscreen type, public key to be used for
+ * encryption, and remaining attempts.
+ */
+ @NonNull public Builder setStartLockscreenValidationRequest(
+ StartLockscreenValidationRequest startLockScreenValidationRequest) {
+ mStartLockscreenValidationRequest = startLockScreenValidationRequest;
+ return this;
+ }
+
+ /**
+ * @param remoteLockscreenValidationServiceComponent the {@link ComponentName} of the
+ * {@link android.service.remotelockscreenvalidation.RemoteLockscreenValidationService}
+ * that will be used to validate the lockscreen guess.
+ */
+ @NonNull public Builder setRemoteLockscreenValidationServiceComponent(
+ ComponentName remoteLockscreenValidationServiceComponent) {
+ mRemoteLockscreenValidationServiceComponent =
+ remoteLockscreenValidationServiceComponent;
+ return this;
+ }
+
+ /**
* Requests that LockSettingsService return a handle to the Gatekeeper Password (instead of
* the Gatekeeper HAT). This allows us to use a single entry of the user's credential
* to create multiple Gatekeeper HATs containing distinct challenges via
@@ -315,49 +368,41 @@
return launchConfirmationActivity(mBuilder.mRequestCode, mBuilder.mTitle, mBuilder.mHeader,
mBuilder.mDescription, mBuilder.mReturnCredentials, mBuilder.mExternal,
mBuilder.mForceVerifyPath, mBuilder.mUserId, mBuilder.mAlternateButton,
- mBuilder.mAllowAnyUserId, mBuilder.mForegroundOnly,
- mBuilder.mRequestGatekeeperPasswordHandle);
+ mBuilder.mCheckBoxLabel, mBuilder.mRemoteLockscreenValidation,
+ mBuilder.mStartLockscreenValidationRequest,
+ mBuilder.mRemoteLockscreenValidationServiceComponent, mBuilder.mAllowAnyUserId,
+ mBuilder.mForegroundOnly, mBuilder.mRequestGatekeeperPasswordHandle);
}
private boolean launchConfirmationActivity(int request, @Nullable CharSequence title,
@Nullable CharSequence header, @Nullable CharSequence description,
boolean returnCredentials, boolean external, boolean forceVerifyPath,
- int userId, @Nullable CharSequence alternateButton, boolean allowAnyUser,
- boolean foregroundOnly, boolean requestGatekeeperPasswordHandle) {
- final int effectiveUserId = UserManager.get(mActivity).getCredentialOwnerProfile(userId);
- boolean launched = false;
-
- switch (mLockPatternUtils.getKeyguardStoredPasswordQuality(effectiveUserId)) {
- case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING:
- launched = launchConfirmationActivity(request, title, header, description,
- returnCredentials || forceVerifyPath
- ? ConfirmLockPattern.InternalActivity.class
- : ConfirmLockPattern.class, returnCredentials, external,
- forceVerifyPath, userId, alternateButton, allowAnyUser,
- foregroundOnly, requestGatekeeperPasswordHandle);
- break;
- case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
- case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
- case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC:
- case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC:
- case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX:
- case DevicePolicyManager.PASSWORD_QUALITY_MANAGED:
- launched = launchConfirmationActivity(request, title, header, description,
- returnCredentials || forceVerifyPath
- ? ConfirmLockPassword.InternalActivity.class
- : ConfirmLockPassword.class, returnCredentials, external,
- forceVerifyPath, userId, alternateButton, allowAnyUser,
- foregroundOnly, requestGatekeeperPasswordHandle);
- break;
+ int userId, @Nullable CharSequence alternateButton,
+ @Nullable CharSequence checkboxLabel, boolean remoteLockscreenValidation,
+ @Nullable StartLockscreenValidationRequest startLockScreenValidationRequest,
+ @Nullable ComponentName remoteLockscreenValidationServiceComponent,
+ boolean allowAnyUser, boolean foregroundOnly, boolean requestGatekeeperPasswordHandle) {
+ Optional<Class<?>> activityClass = determineAppropriateActivityClass(
+ returnCredentials, forceVerifyPath, userId, startLockScreenValidationRequest);
+ if (activityClass.isEmpty()) {
+ return false;
}
- return launched;
+
+ return launchConfirmationActivity(request, title, header, description, activityClass.get(),
+ returnCredentials, external, forceVerifyPath, userId, alternateButton,
+ checkboxLabel, remoteLockscreenValidation, startLockScreenValidationRequest,
+ remoteLockscreenValidationServiceComponent, allowAnyUser, foregroundOnly,
+ requestGatekeeperPasswordHandle);
}
private boolean launchConfirmationActivity(int request, CharSequence title, CharSequence header,
CharSequence message, Class<?> activityClass, boolean returnCredentials,
boolean external, boolean forceVerifyPath, int userId,
- @Nullable CharSequence alternateButton, boolean allowAnyUser,
- boolean foregroundOnly, boolean requestGatekeeperPasswordHandle) {
+ @Nullable CharSequence alternateButton, @Nullable CharSequence checkbox,
+ boolean remoteLockscreenValidation,
+ @Nullable StartLockscreenValidationRequest startLockScreenValidationRequest,
+ @Nullable ComponentName remoteLockscreenValidationServiceComponent,
+ boolean allowAnyUser, boolean foregroundOnly, boolean requestGatekeeperPasswordHandle) {
final Intent intent = new Intent();
intent.putExtra(ConfirmDeviceCredentialBaseFragment.TITLE_TEXT, title);
intent.putExtra(ConfirmDeviceCredentialBaseFragment.HEADER_TEXT, header);
@@ -367,10 +412,16 @@
intent.putExtra(ConfirmDeviceCredentialBaseFragment.SHOW_CANCEL_BUTTON, false);
intent.putExtra(ConfirmDeviceCredentialBaseFragment.SHOW_WHEN_LOCKED, external);
intent.putExtra(ConfirmDeviceCredentialBaseFragment.USE_FADE_ANIMATION, external);
+ intent.putExtra(ConfirmDeviceCredentialBaseFragment.IS_REMOTE_LOCKSCREEN_VALIDATION,
+ remoteLockscreenValidation);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, returnCredentials);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FORCE_VERIFY, forceVerifyPath);
intent.putExtra(Intent.EXTRA_USER_ID, userId);
intent.putExtra(KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL, alternateButton);
+ intent.putExtra(KeyguardManager.EXTRA_CHECKBOX_LABEL, checkbox);
+ intent.putExtra(KeyguardManager.EXTRA_START_LOCKSCREEN_VALIDATION_REQUEST,
+ startLockScreenValidationRequest);
+ intent.putExtra(Intent.EXTRA_COMPONENT_NAME, remoteLockscreenValidationServiceComponent);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOREGROUND_ONLY, foregroundOnly);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_ALLOW_ANY_USER, allowAnyUser);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE,
@@ -405,6 +456,58 @@
return true;
}
+ private Optional<Integer> passwordQualityToLockTypes(int quality) {
+ switch (quality) {
+ case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING:
+ return Optional.of(KeyguardManager.PATTERN);
+ case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
+ case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
+ return Optional.of(KeyguardManager.PIN);
+ case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC:
+ case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC:
+ case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX:
+ case DevicePolicyManager.PASSWORD_QUALITY_MANAGED:
+ return Optional.of(KeyguardManager.PASSWORD);
+ }
+ Log.e(TAG, String.format(
+ "Cannot determine appropriate activity class for password quality %d",
+ quality));
+ return Optional.empty();
+ }
+
+ private Optional<Class<?>> determineAppropriateActivityClass(boolean returnCredentials,
+ boolean forceVerifyPath, int userId,
+ @Nullable StartLockscreenValidationRequest startLockscreenValidationRequest) {
+ int lockType;
+ if (startLockscreenValidationRequest != null) {
+ lockType = startLockscreenValidationRequest.getLockscreenUiType();
+ } else {
+ final int effectiveUserId = UserManager
+ .get(mActivity).getCredentialOwnerProfile(userId);
+ Optional<Integer> lockTypeOptional = passwordQualityToLockTypes(
+ mLockPatternUtils.getKeyguardStoredPasswordQuality(effectiveUserId));
+ if (lockTypeOptional.isEmpty()) {
+ return Optional.empty();
+ }
+ lockType = lockTypeOptional.get();
+ }
+
+ switch (lockType) {
+ case KeyguardManager.PASSWORD:
+ case KeyguardManager.PIN:
+ return Optional.of(returnCredentials || forceVerifyPath
+ ? ConfirmLockPassword.InternalActivity.class
+ : ConfirmLockPassword.class);
+ case KeyguardManager.PATTERN:
+ return Optional.of(returnCredentials || forceVerifyPath
+ ? ConfirmLockPattern.InternalActivity.class
+ : ConfirmLockPattern.class);
+ }
+ Log.e(TAG, String.format("Cannot determine appropriate activity class for lock type %d",
+ lockType));
+ return Optional.empty();
+ }
+
private void copyOptionalExtras(Intent inIntent, Intent outIntent) {
IntentSender intentSender = inIntent.getParcelableExtra(Intent.EXTRA_INTENT);
if (intentSender != null) {
diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
index e56e86c..31d9c74 100644
--- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
+++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
@@ -28,8 +28,10 @@
import android.app.Activity;
import android.app.KeyguardManager;
+import android.app.StartLockscreenValidationRequest;
import android.app.admin.DevicePolicyManager;
import android.app.trust.TrustManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
@@ -97,7 +99,7 @@
private boolean mCheckDevicePolicyManager;
private String mTitle;
- private String mDetails;
+ private CharSequence mDetails;
private int mUserId;
private int mCredentialMode;
private boolean mGoingToBackground;
@@ -178,10 +180,12 @@
mCheckDevicePolicyManager = intent
.getBooleanExtra(KeyguardManager.EXTRA_DISALLOW_BIOMETRICS_IF_POLICY_EXISTS, false);
mTitle = intent.getStringExtra(KeyguardManager.EXTRA_TITLE);
- mDetails = intent.getStringExtra(KeyguardManager.EXTRA_DESCRIPTION);
+ mDetails = intent.getCharSequenceExtra(KeyguardManager.EXTRA_DESCRIPTION);
String alternateButton = intent.getStringExtra(
KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL);
boolean frp = KeyguardManager.ACTION_CONFIRM_FRP_CREDENTIAL.equals(intent.getAction());
+ boolean remoteValidation =
+ KeyguardManager.ACTION_CONFIRM_REMOTE_DEVICE_CREDENTIAL.equals(intent.getAction());
mUserId = UserHandle.myUserId();
if (isInternalActivity()) {
@@ -230,6 +234,28 @@
.setExternal(true)
.setUserId(LockPatternUtils.USER_FRP)
.show();
+ } else if (remoteValidation) {
+ StartLockscreenValidationRequest startLockScreenValidationRequest =
+ intent.getParcelableExtra(
+ KeyguardManager.EXTRA_START_LOCKSCREEN_VALIDATION_REQUEST,
+ StartLockscreenValidationRequest.class);
+ ComponentName remoteLockscreenValidationServiceComponent =
+ intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName.class);
+
+ String checkboxLabel = intent.getStringExtra(KeyguardManager.EXTRA_CHECKBOX_LABEL);
+ final ChooseLockSettingsHelper.Builder builder =
+ new ChooseLockSettingsHelper.Builder(this);
+ launchedCDC = builder
+ .setRemoteLockscreenValidation(true)
+ .setStartLockscreenValidationRequest(startLockScreenValidationRequest)
+ .setRemoteLockscreenValidationServiceComponent(
+ remoteLockscreenValidationServiceComponent)
+ .setHeader(mTitle) // Show the title in the header location
+ .setDescription(mDetails)
+ .setCheckboxLabel(checkboxLabel)
+ .setAlternateButton(alternateButton)
+ .setExternal(true)
+ .show();
} else if (isEffectiveUserManagedProfile && isInternalActivity()) {
mCredentialMode = CREDENTIAL_MANAGED;
if (isBiometricAllowed(effectiveUserId, mUserId)) {
diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
index 7787ae1..7c3df61 100644
--- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
+++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
@@ -24,8 +24,11 @@
import android.annotation.Nullable;
import android.app.Dialog;
import android.app.KeyguardManager;
+import android.app.RemoteLockscreenValidationResult;
+import android.app.StartLockscreenValidationRequest;
import android.app.admin.DevicePolicyManager;
import android.app.admin.ManagedSubscriptionsPolicy;
+import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -35,11 +38,15 @@
import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
+import android.service.remotelockscreenvalidation.IRemoteLockscreenValidationCallback;
+import android.service.remotelockscreenvalidation.RemoteLockscreenValidationClient;
import android.telecom.TelecomManager;
import android.text.TextUtils;
+import android.util.FeatureFlagUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
+import android.widget.CheckBox;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
@@ -47,10 +54,16 @@
import androidx.fragment.app.FragmentManager;
import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockscreenCredential;
+import com.android.security.SecureBox;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.InstrumentedFragment;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+
/**
* Base fragment to be shared for PIN/Pattern/Password confirmation fragments.
*/
@@ -66,6 +79,8 @@
SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showWhenLocked";
public static final String USE_FADE_ANIMATION =
SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.useFadeAnimation";
+ public static final String IS_REMOTE_LOCKSCREEN_VALIDATION =
+ SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.isRemoteLockscreenValidation";
protected static final int USER_TYPE_PRIMARY = 1;
protected static final int USER_TYPE_MANAGED_PROFILE = 2;
@@ -77,6 +92,7 @@
protected boolean mReturnCredentials = false;
protected boolean mReturnGatekeeperPassword = false;
protected boolean mForceVerifyPath = false;
+ protected CheckBox mCheckBox;
protected Button mCancelButton;
/** Button allowing managed profile password reset, null when is not shown. */
@Nullable protected Button mForgotButton;
@@ -88,8 +104,13 @@
protected TextView mErrorTextView;
protected final Handler mHandler = new Handler();
protected boolean mFrp;
- private CharSequence mFrpAlternateButtonText;
+ protected boolean mRemoteValidation;
+ protected CharSequence mAlternateButtonText;
protected BiometricManager mBiometricManager;
+ @Nullable protected StartLockscreenValidationRequest mStartLockscreenValidationRequest;
+ /** Credential saved so the credential can be set for device if remote validation passes */
+ @Nullable protected LockscreenCredential mDeviceCredentialGuess;
+ @Nullable protected RemoteLockscreenValidationClient mRemoteLockscreenValidationClient;
private boolean isInternalActivity() {
return (getActivity() instanceof ConfirmLockPassword.InternalActivity)
@@ -100,7 +121,7 @@
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getActivity().getIntent();
- mFrpAlternateButtonText = intent.getCharSequenceExtra(
+ mAlternateButtonText = intent.getCharSequenceExtra(
KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL);
mReturnCredentials = intent.getBooleanExtra(
ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, false);
@@ -110,6 +131,41 @@
mForceVerifyPath = intent.getBooleanExtra(
ChooseLockSettingsHelper.EXTRA_KEY_FORCE_VERIFY, false);
+ if (intent.getBooleanExtra(IS_REMOTE_LOCKSCREEN_VALIDATION, false)) {
+ if (FeatureFlagUtils.isEnabled(getContext(),
+ FeatureFlagUtils.SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION)) {
+ mRemoteValidation = true;
+ } else {
+ Log.e(TAG, "Remote device credential validation not enabled.");
+ getActivity().finish();
+ }
+ }
+ if (mRemoteValidation) {
+ mStartLockscreenValidationRequest = intent.getParcelableExtra(
+ KeyguardManager.EXTRA_START_LOCKSCREEN_VALIDATION_REQUEST,
+ StartLockscreenValidationRequest.class);
+ if (mStartLockscreenValidationRequest == null
+ || mStartLockscreenValidationRequest.getRemainingAttempts() == 0) {
+ Log.e(TAG, "StartLockscreenValidationRequest is null or "
+ + "no more attempts for remote lockscreen validation.");
+ getActivity().finish();
+ }
+
+ ComponentName remoteLockscreenValidationServiceComponent =
+ intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName.class);
+ if (remoteLockscreenValidationServiceComponent == null) {
+ Log.e(TAG, "RemoteLockscreenValidationService ComponentName is null");
+ getActivity().finish();
+ }
+ mRemoteLockscreenValidationClient = RemoteLockscreenValidationClient
+ .create(getContext(), remoteLockscreenValidationServiceComponent);
+ if (!mRemoteLockscreenValidationClient.isServiceAvailable()) {
+ Log.e(TAG, String.format("RemoteLockscreenValidationService at %s is not available",
+ remoteLockscreenValidationServiceComponent.getClassName()));
+ getActivity().finish();
+ }
+ }
+
// Only take this argument into account if it belongs to the current profile.
mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras(),
isInternalActivity());
@@ -126,13 +182,14 @@
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mCancelButton = view.findViewById(R.id.cancelButton);
- boolean showCancelButton = getActivity().getIntent().getBooleanExtra(
+ boolean showCancelButton = mRemoteValidation || getActivity().getIntent().getBooleanExtra(
SHOW_CANCEL_BUTTON, false);
- boolean hasAlternateButton = mFrp && !TextUtils.isEmpty(mFrpAlternateButtonText);
+ boolean hasAlternateButton = (mFrp || mRemoteValidation) && !TextUtils.isEmpty(
+ mAlternateButtonText);
mCancelButton.setVisibility(showCancelButton || hasAlternateButton
? View.VISIBLE : View.GONE);
if (hasAlternateButton) {
- mCancelButton.setText(mFrpAlternateButtonText);
+ mCancelButton.setText(mAlternateButtonText);
}
mCancelButton.setOnClickListener(v -> {
if (hasAlternateButton) {
@@ -141,6 +198,11 @@
getActivity().finish();
});
setupForgotButtonIfManagedProfile(view);
+
+ mCheckBox = view.findViewById(R.id.checkbox);
+ if (mCheckBox != null && mRemoteValidation) {
+ mCheckBox.setVisibility(View.VISIBLE);
+ }
setupEmergencyCallButtonIfManagedSubscription(view);
}
@@ -232,8 +294,21 @@
super.onPause();
}
+ @Override
+ public void onDestroy() {
+ if (mRemoteLockscreenValidationClient != null) {
+ mRemoteLockscreenValidationClient.disconnect();
+ }
+ if (mDeviceCredentialGuess != null) {
+ mDeviceCredentialGuess.zeroize();
+ }
+ super.onDestroy();
+ }
+
protected abstract void authenticationSucceeded();
+ protected abstract void onRemoteDeviceCredentialValidationResult(
+ RemoteLockscreenValidationResult result);
public void prepareEnterAnimation() {
}
@@ -335,6 +410,46 @@
}
}
+ protected void validateGuess(LockscreenCredential credentialGuess) {
+ if (mCheckBox.isChecked()) {
+ // Keep credential in memory since user wants to set guess as screen lock.
+ mDeviceCredentialGuess = credentialGuess;
+ } else if (mDeviceCredentialGuess != null) {
+ mDeviceCredentialGuess.zeroize();
+ }
+
+ mRemoteLockscreenValidationClient.validateLockscreenGuess(
+ encryptDeviceCredentialGuess(credentialGuess.getCredential()),
+ new IRemoteLockscreenValidationCallback.Stub() {
+ @Override
+ public void onSuccess(RemoteLockscreenValidationResult result) {
+ mHandler.post(()->onRemoteDeviceCredentialValidationResult(result));
+ }
+
+ @Override
+ public void onFailure(String message) {
+ Log.e(TAG, "A failure occurred while trying "
+ + "to validate lockscreen guess: " + message);
+ mHandler.post(()->getActivity().finish());
+ }
+ });
+ }
+
+ private byte[] encryptDeviceCredentialGuess(byte[] guess) {
+ try {
+ byte[] encodedPublicKey = mStartLockscreenValidationRequest.getSourcePublicKey();
+ PublicKey publicKey = SecureBox.decodePublicKey(encodedPublicKey);
+ return SecureBox.encrypt(
+ publicKey,
+ /* sharedSecret= */ null,
+ LockPatternUtils.ENCRYPTED_REMOTE_CREDENTIALS_HEADER,
+ guess);
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ Log.w(TAG, "Error encrypting device credential guess. Returning empty byte[].", e);
+ return new byte[0];
+ }
+ }
+
protected abstract void onShowError();
protected void showError(int msg, long timeout) {
diff --git a/src/com/android/settings/password/ConfirmLockPassword.java b/src/com/android/settings/password/ConfirmLockPassword.java
index 48c9aa8..83dc85c 100644
--- a/src/com/android/settings/password/ConfirmLockPassword.java
+++ b/src/com/android/settings/password/ConfirmLockPassword.java
@@ -27,6 +27,8 @@
import static android.app.admin.DevicePolicyResources.UNDEFINED;
import android.annotation.Nullable;
+import android.app.KeyguardManager;
+import android.app.RemoteLockscreenValidationResult;
import android.app.admin.DevicePolicyManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
@@ -42,6 +44,7 @@
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
+import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
@@ -114,13 +117,13 @@
super.onWindowFocusChanged(hasFocus);
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.main_content);
if (fragment != null && fragment instanceof ConfirmLockPasswordFragment) {
- ((ConfirmLockPasswordFragment)fragment).onWindowFocusChanged(hasFocus);
+ ((ConfirmLockPasswordFragment) fragment).onWindowFocusChanged(hasFocus);
}
}
public static class ConfirmLockPasswordFragment extends ConfirmDeviceCredentialBaseFragment
implements OnClickListener, OnEditorActionListener,
- CredentialCheckResultTracker.Listener {
+ CredentialCheckResultTracker.Listener, SaveChosenLockWorkerBase.Listener {
private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result";
private ImeAwareEditText mPasswordEntry;
private TextViewInputDisabler mPasswordEntryInputDisabler;
@@ -134,6 +137,7 @@
private DisappearAnimationUtils mDisappearAnimationUtils;
private boolean mIsManagedProfile;
private GlifLayout mGlifLayout;
+ private CharSequence mCheckBoxLabel;
// required constructor for fragments
public ConfirmLockPasswordFragment() {
@@ -160,11 +164,19 @@
mPasswordEntry.requestFocus();
mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordEntry);
mErrorTextView = (TextView) view.findViewById(R.id.errorText);
- mIsAlpha = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == storedQuality
- || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == storedQuality
- || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == storedQuality
- || DevicePolicyManager.PASSWORD_QUALITY_MANAGED == storedQuality;
+ if (mRemoteValidation) {
+ mIsAlpha = mStartLockscreenValidationRequest.getLockscreenUiType()
+ == KeyguardManager.PASSWORD;
+ // ProgressBar visibility is set to GONE until interacted with.
+ // Set progress bar to INVISIBLE, so the EditText does not get bumped down later.
+ mGlifLayout.setProgressBarShown(false);
+ } else {
+ mIsAlpha = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == storedQuality
+ || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == storedQuality
+ || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == storedQuality
+ || DevicePolicyManager.PASSWORD_QUALITY_MANAGED == storedQuality;
+ }
mImm = (InputMethodManager) getActivity().getSystemService(
Context.INPUT_METHOD_SERVICE);
@@ -187,6 +199,7 @@
}
mGlifLayout.setHeaderText(headerMessage);
mGlifLayout.setDescriptionText(detailsMessage);
+ mCheckBoxLabel = intent.getCharSequenceExtra(KeyguardManager.EXTRA_CHECKBOX_LABEL);
}
int currentType = mPasswordEntry.getInputType();
if (mIsAlpha) {
@@ -227,6 +240,19 @@
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
+ if (mRemoteValidation) {
+ if (mCheckBox != null) {
+ mCheckBox.setText(TextUtils.isEmpty(mCheckBoxLabel)
+ ? getDefaultCheckboxLabel()
+ : mCheckBoxLabel);
+ }
+ if (mCancelButton != null && TextUtils.isEmpty(mAlternateButtonText)) {
+ mCancelButton.setText(mIsAlpha
+ ? R.string.lockpassword_forgot_password
+ : R.string.lockpassword_forgot_pin);
+ }
+ }
+
if (mForgotButton != null) {
mForgotButton.setText(mIsAlpha
? R.string.lockpassword_forgot_password
@@ -237,7 +263,9 @@
@Override
public void onDestroy() {
super.onDestroy();
- mPasswordEntry.setText(null);
+ if (mPasswordEntry != null) {
+ mPasswordEntry.setText(null);
+ }
// Force a garbage collection to remove remnant of user password shards from memory.
// Execute this with a slight delay to allow the activity lifecycle to complete and
// the instance to become gc-able.
@@ -253,6 +281,9 @@
return mIsAlpha ? getString(R.string.lockpassword_confirm_your_password_header_frp)
: getString(R.string.lockpassword_confirm_your_pin_header_frp);
}
+ if (mRemoteValidation) {
+ return getString(R.string.lockpassword_remote_validation_header);
+ }
if (mIsManagedProfile) {
if (mIsAlpha) {
return mDevicePolicyManager.getResources().getString(
@@ -273,6 +304,11 @@
return mIsAlpha ? getString(R.string.lockpassword_confirm_your_password_details_frp)
: getString(R.string.lockpassword_confirm_your_pin_details_frp);
}
+ if (mRemoteValidation) {
+ return getContext().getString(mIsAlpha
+ ? R.string.lockpassword_remote_validation_password_details
+ : R.string.lockpassword_remote_validation_pin_details);
+ }
boolean isStrongAuthRequired = isStrongAuthRequired();
// Map boolean flags to an index by isStrongAuth << 2 + isManagedProfile << 1 + isAlpha.
int index = ((isStrongAuthRequired ? 1 : 0) << 2) + ((mIsManagedProfile ? 1 : 0) << 1)
@@ -281,6 +317,16 @@
DETAIL_TEXT_OVERRIDES[index], () -> getString(DETAIL_TEXTS[index]));
}
+ private String getDefaultCheckboxLabel() {
+ if (mRemoteValidation) {
+ return getString(mIsAlpha
+ ? R.string.lockpassword_remote_validation_set_password_as_screenlock
+ : R.string.lockpassword_remote_validation_set_pin_as_screenlock);
+ }
+ throw new IllegalStateException(
+ "Trying to get default checkbox label for illegal flow");
+ }
+
private int getErrorMessage() {
return mIsAlpha ? R.string.lockpassword_invalid_password
: R.string.lockpassword_invalid_pin;
@@ -392,6 +438,7 @@
mImm.hideSoftInputFromWindow(mPasswordEntry.getWindowToken(), 0 /*flags*/);
} else {
mPasswordEntry.scheduleShowSoftInput();
+ mPasswordEntry.requestFocus();
}
}
@@ -413,12 +460,18 @@
if (TextUtils.isEmpty(passwordText)) {
return;
}
- final LockscreenCredential credential =
- mIsAlpha ? LockscreenCredential.createPassword(passwordText)
+ final LockscreenCredential credential = mIsAlpha
+ ? LockscreenCredential.createPassword(passwordText)
: LockscreenCredential.createPin(passwordText);
mPasswordEntryInputDisabler.setInputEnabled(false);
+ if (mRemoteValidation) {
+ validateGuess(credential);
+ mGlifLayout.setProgressBarShown(true);
+ return;
+ }
+
Intent intent = new Intent();
// TODO(b/161956762): Sanitize this
if (mReturnGatekeeperPassword) {
@@ -547,6 +600,44 @@
}
@Override
+ protected void onRemoteDeviceCredentialValidationResult(
+ RemoteLockscreenValidationResult result) {
+ switch (result.getResultCode()) {
+ case RemoteLockscreenValidationResult.RESULT_GUESS_VALID:
+ if (mCheckBox.isChecked()) {
+ ChooseLockPassword.SaveAndFinishWorker saveAndFinishWorker =
+ new ChooseLockPassword.SaveAndFinishWorker();
+ Log.i(TAG, "Setting device screen lock to the other device's screen lock.");
+ getFragmentManager().beginTransaction().add(saveAndFinishWorker, null)
+ .commit();
+ getFragmentManager().executePendingTransactions();
+ saveAndFinishWorker.setListener(this);
+ saveAndFinishWorker.start(
+ mLockPatternUtils,
+ /* requestGatekeeperPassword= */ false,
+ mDeviceCredentialGuess,
+ /* currentCredential= */ null,
+ mEffectiveUserId);
+ return;
+ }
+ mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(),
+ /* timeoutMs= */ 0, mEffectiveUserId);
+ break;
+ case RemoteLockscreenValidationResult.RESULT_GUESS_INVALID:
+ mCredentialCheckResultTracker.setResult(/* matched= */ false, new Intent(),
+ /* timeoutMs= */ 0, mEffectiveUserId);
+ break;
+ case RemoteLockscreenValidationResult.RESULT_LOCKOUT:
+ mCredentialCheckResultTracker.setResult(/* matched= */ false, new Intent(),
+ (int) result.getTimeoutMillis(), mEffectiveUserId);
+ break;
+ case RemoteLockscreenValidationResult.RESULT_NO_REMAINING_ATTEMPTS:
+ getActivity().finish();
+ }
+ mGlifLayout.setProgressBarShown(false);
+ }
+
+ @Override
public void onCredentialChecked(boolean matched, Intent intent, int timeoutMs,
int effectiveUserId, boolean newResult) {
onPasswordChecked(matched, intent, timeoutMs, effectiveUserId, newResult);
@@ -601,5 +692,19 @@
}
return false;
}
+
+ /**
+ * Callback for when the device credential guess used for remote validation was set as the
+ * current device's device credential.
+ */
+ @Override
+ public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) {
+ if (mDeviceCredentialGuess != null) {
+ mDeviceCredentialGuess.zeroize();
+ }
+ mGlifLayout.setProgressBarShown(false);
+ mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(),
+ /* timeoutMs= */ 0, mEffectiveUserId);
+ }
}
}
diff --git a/src/com/android/settings/password/ConfirmLockPattern.java b/src/com/android/settings/password/ConfirmLockPattern.java
index b4f0aa3..0013d7a 100644
--- a/src/com/android/settings/password/ConfirmLockPattern.java
+++ b/src/com/android/settings/password/ConfirmLockPattern.java
@@ -25,6 +25,8 @@
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.Activity;
+import android.app.KeyguardManager;
+import android.app.RemoteLockscreenValidationResult;
import android.app.settings.SettingsEnums;
import android.content.Intent;
import android.os.AsyncTask;
@@ -33,6 +35,7 @@
import android.os.SystemClock;
import android.os.UserManager;
import android.text.TextUtils;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -89,7 +92,8 @@
}
public static class ConfirmLockPatternFragment extends ConfirmDeviceCredentialBaseFragment
- implements AppearAnimationCreator<Object>, CredentialCheckResultTracker.Listener {
+ implements AppearAnimationCreator<Object>, CredentialCheckResultTracker.Listener,
+ SaveChosenLockWorkerBase.Listener {
private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result";
@@ -105,6 +109,7 @@
// caller-supplied text for various prompts
private CharSequence mHeaderText;
private CharSequence mDetailsText;
+ private CharSequence mCheckBoxLabel;
private AppearAnimationUtils mAppearAnimationUtils;
private DisappearAnimationUtils mDisappearAnimationUtils;
@@ -148,6 +153,7 @@
ConfirmDeviceCredentialBaseFragment.HEADER_TEXT);
mDetailsText = intent.getCharSequenceExtra(
ConfirmDeviceCredentialBaseFragment.DETAILS_TEXT);
+ mCheckBoxLabel = intent.getCharSequenceExtra(KeyguardManager.EXTRA_CHECKBOX_LABEL);
}
if (TextUtils.isEmpty(mHeaderText) && mIsManagedProfile) {
mHeaderText = mDevicePolicyManager.getOrganizationNameForUser(mUserId);
@@ -174,7 +180,8 @@
// ability to disable the pattern in L. Remove this block after
// ensuring it's safe to do so. (Note that ConfirmLockPassword
// doesn't have this).
- if (!mFrp && !mLockPatternUtils.isLockPatternEnabled(mEffectiveUserId)) {
+ if (!mFrp && !mRemoteValidation
+ && !mLockPatternUtils.isLockPatternEnabled(mEffectiveUserId)) {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
}
@@ -203,12 +210,33 @@
FRAGMENT_TAG_CHECK_LOCK_RESULT).commit();
}
+ if (mRemoteValidation) {
+ // ProgressBar visibility is set to GONE until interacted with.
+ // Set progress bar to INVISIBLE, so the pattern does not get bumped down later.
+ mGlifLayout.setProgressBarShown(false);
+ // Lock pattern is generally not visible until the user has set a lockscreen for the
+ // first time. For a new user, this means that the pattern will always be hidden.
+ // Despite this prerequisite, we want to show the pattern anyway for this flow.
+ mLockPatternView.setInStealthMode(false);
+ }
+
return view;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
+ if (mRemoteValidation) {
+ if (mCheckBox != null) {
+ mCheckBox.setText(TextUtils.isEmpty(mCheckBoxLabel)
+ ? getDefaultCheckboxLabel()
+ : mCheckBoxLabel);
+ }
+ if (mCancelButton != null && TextUtils.isEmpty(mAlternateButtonText)) {
+ mCancelButton.setText(R.string.lockpassword_forgot_pattern);
+ }
+ }
+
if (mForgotButton != null) {
mForgotButton.setText(R.string.lockpassword_forgot_pattern);
}
@@ -271,6 +299,10 @@
if (mFrp) {
return getString(R.string.lockpassword_confirm_your_pattern_details_frp);
}
+ if (mRemoteValidation) {
+ return getString(
+ R.string.lockpassword_remote_validation_pattern_details);
+ }
final boolean isStrongAuthRequired = isStrongAuthRequired();
if (mIsManagedProfile) {
if (isStrongAuthRequired) {
@@ -335,11 +367,11 @@
} else {
mGlifLayout.setHeaderText(getDefaultHeader());
}
- if (mDetailsText != null) {
- mGlifLayout.setDescriptionText(mDetailsText);
- } else {
- mGlifLayout.setDescriptionText(getDefaultDetails());
- }
+
+ CharSequence detailsText =
+ mDetailsText == null ? getDefaultDetails() : mDetailsText;
+ mGlifLayout.setDescriptionText(detailsText);
+
mErrorTextView.setText("");
updateErrorMessage(
mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId));
@@ -371,7 +403,9 @@
private String getDefaultHeader() {
if (mFrp) return getString(R.string.lockpassword_confirm_your_pattern_header_frp);
-
+ if (mRemoteValidation) {
+ return getString(R.string.lockpassword_remote_validation_header);
+ }
if (mIsManagedProfile) {
return mDevicePolicyManager.getResources().getString(
CONFIRM_WORK_PROFILE_PATTERN_HEADER,
@@ -381,6 +415,14 @@
return getString(R.string.lockpassword_confirm_your_pattern_header);
}
+ private String getDefaultCheckboxLabel() {
+ if (mRemoteValidation) {
+ return getString(R.string.lockpassword_remote_validation_set_pattern_as_screenlock);
+ }
+ throw new IllegalStateException(
+ "Trying to get default checkbox label for illegal flow");
+ }
+
private Runnable mClearPatternRunnable = new Runnable() {
public void run() {
mLockPatternView.clearPattern();
@@ -431,7 +473,7 @@
* an existing lock pattern.
*/
private LockPatternView.OnPatternListener mConfirmExistingLockPatternListener
- = new LockPatternView.OnPatternListener() {
+ = new LockPatternView.OnPatternListener() {
public void onPatternStart() {
mLockPatternView.removeCallbacks(mClearPatternRunnable);
@@ -453,6 +495,13 @@
mLockPatternView.setEnabled(false);
final LockscreenCredential credential = LockscreenCredential.createPattern(pattern);
+
+ if (mRemoteValidation) {
+ validateGuess(credential);
+ mGlifLayout.setProgressBarShown(true);
+ return;
+ }
+
// TODO(b/161956762): Sanitize this
Intent intent = new Intent();
if (mReturnGatekeeperPassword) {
@@ -564,6 +613,44 @@
}
@Override
+ protected void onRemoteDeviceCredentialValidationResult(
+ RemoteLockscreenValidationResult result) {
+ switch (result.getResultCode()) {
+ case RemoteLockscreenValidationResult.RESULT_GUESS_VALID:
+ if (mCheckBox.isChecked()) {
+ Log.i(TAG, "Setting device screen lock to the other device's screen lock.");
+ ChooseLockPattern.SaveAndFinishWorker saveAndFinishWorker =
+ new ChooseLockPattern.SaveAndFinishWorker();
+ getFragmentManager().beginTransaction().add(saveAndFinishWorker, null)
+ .commit();
+ getFragmentManager().executePendingTransactions();
+ saveAndFinishWorker.setListener(this);
+ saveAndFinishWorker.start(
+ mLockPatternUtils,
+ /* requestGatekeeperPassword= */ false,
+ mDeviceCredentialGuess,
+ /* currentCredential= */ null,
+ mEffectiveUserId);
+ return;
+ }
+ mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(),
+ /* timeoutMs= */ 0, mEffectiveUserId);
+ break;
+ case RemoteLockscreenValidationResult.RESULT_GUESS_INVALID:
+ mCredentialCheckResultTracker.setResult(/* matched= */ false, new Intent(),
+ /* timeoutMs= */ 0, mEffectiveUserId);
+ break;
+ case RemoteLockscreenValidationResult.RESULT_LOCKOUT:
+ mCredentialCheckResultTracker.setResult(/* matched= */ false, new Intent(),
+ (int) result.getTimeoutMillis(), mEffectiveUserId);
+ break;
+ case RemoteLockscreenValidationResult.RESULT_NO_REMAINING_ATTEMPTS:
+ getActivity().finish();
+ }
+ mGlifLayout.setProgressBarShown(false);
+ }
+
+ @Override
public void onCredentialChecked(boolean matched, Intent intent, int timeoutMs,
int effectiveUserId, boolean newResult) {
onPatternChecked(matched, intent, timeoutMs, effectiveUserId, newResult);
@@ -632,5 +719,19 @@
appearing, interpolator, finishListener);
}
}
+
+ /**
+ * Callback for when the device credential guess used for remote validation was set as the
+ * current device's device credential.
+ */
+ @Override
+ public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) {
+ if (mDeviceCredentialGuess != null) {
+ mDeviceCredentialGuess.zeroize();
+ }
+ mGlifLayout.setProgressBarShown(false);
+ mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(),
+ /* timeoutMs= */ 0, mEffectiveUserId);
+ }
}
}
diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockSettingsHelperTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockSettingsHelperTest.java
index 053bbe0..2777529 100644
--- a/tests/robotests/src/com/android/settings/password/ChooseLockSettingsHelperTest.java
+++ b/tests/robotests/src/com/android/settings/password/ChooseLockSettingsHelperTest.java
@@ -1,5 +1,9 @@
package com.android.settings.password;
+import static com.android.settings.password.TestUtils.COMPONENT_NAME;
+import static com.android.settings.password.TestUtils.VALID_REMAINING_ATTEMPTS;
+import static com.android.settings.password.TestUtils.createStartLockscreenValidationRequest;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
@@ -10,6 +14,8 @@
import static org.mockito.Mockito.when;
import android.app.Activity;
+import android.app.KeyguardManager;
+import android.app.StartLockscreenValidationRequest;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Intent;
@@ -160,6 +166,93 @@
startedIntent.getComponent());
}
+ @Test
+ public void launchConfirmPassword_remoteValidation_passwordLockType() throws Exception {
+ Activity activity = Robolectric.setupActivity(Activity.class);
+ ShadowActivity shadowActivity = Shadows.shadowOf(activity);
+ StartLockscreenValidationRequest request = createStartLockscreenValidationRequest(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS);
+
+ ChooseLockSettingsHelper chooseLockSettingsHelper = getChooseLockSettingsHelper(
+ new ChooseLockSettingsHelper.Builder(activity)
+ .setRemoteLockscreenValidation(true)
+ .setStartLockscreenValidationRequest(request)
+ .setRemoteLockscreenValidationServiceComponent(COMPONENT_NAME));
+ chooseLockSettingsHelper.launch();
+
+ Intent startedIntent = shadowActivity.getNextStartedActivity();
+ assertEquals(new ComponentName("com.android.settings",
+ ConfirmLockPassword.class.getName()), startedIntent.getComponent());
+ assertThat(startedIntent.getBooleanExtra(
+ ConfirmDeviceCredentialBaseFragment.IS_REMOTE_LOCKSCREEN_VALIDATION, false)
+ ).isTrue();
+ assertThat(startedIntent.getParcelableExtra(
+ KeyguardManager.EXTRA_START_LOCKSCREEN_VALIDATION_REQUEST,
+ StartLockscreenValidationRequest.class)
+ ).isEqualTo(request);
+ assertThat(startedIntent.getParcelableExtra(
+ Intent.EXTRA_COMPONENT_NAME, ComponentName.class)
+ ).isEqualTo(COMPONENT_NAME);
+ }
+
+ @Test
+ public void launchConfirmPassword_remoteValidation_pinLockType() throws Exception {
+ Activity activity = Robolectric.setupActivity(Activity.class);
+ ShadowActivity shadowActivity = Shadows.shadowOf(activity);
+ StartLockscreenValidationRequest request = createStartLockscreenValidationRequest(
+ KeyguardManager.PIN, VALID_REMAINING_ATTEMPTS);
+
+ ChooseLockSettingsHelper chooseLockSettingsHelper = getChooseLockSettingsHelper(
+ new ChooseLockSettingsHelper.Builder(activity)
+ .setRemoteLockscreenValidation(true)
+ .setStartLockscreenValidationRequest(request)
+ .setRemoteLockscreenValidationServiceComponent(COMPONENT_NAME));
+ chooseLockSettingsHelper.launch();
+
+ Intent startedIntent = shadowActivity.getNextStartedActivity();
+ assertEquals(new ComponentName("com.android.settings",
+ ConfirmLockPassword.class.getName()), startedIntent.getComponent());
+ assertThat(startedIntent.getBooleanExtra(
+ ConfirmDeviceCredentialBaseFragment.IS_REMOTE_LOCKSCREEN_VALIDATION, false)
+ ).isTrue();
+ assertThat(startedIntent.getParcelableExtra(
+ KeyguardManager.EXTRA_START_LOCKSCREEN_VALIDATION_REQUEST,
+ StartLockscreenValidationRequest.class)
+ ).isEqualTo(request);
+ assertThat(startedIntent.getParcelableExtra(
+ Intent.EXTRA_COMPONENT_NAME, ComponentName.class)
+ ).isEqualTo(COMPONENT_NAME);
+ }
+
+ @Test
+ public void launchConfirmPattern_remoteValidation_patternLockType() throws Exception {
+ Activity activity = Robolectric.setupActivity(Activity.class);
+ ShadowActivity shadowActivity = Shadows.shadowOf(activity);
+ StartLockscreenValidationRequest request = createStartLockscreenValidationRequest(
+ KeyguardManager.PATTERN, VALID_REMAINING_ATTEMPTS);
+
+ ChooseLockSettingsHelper chooseLockSettingsHelper = getChooseLockSettingsHelper(
+ new ChooseLockSettingsHelper.Builder(activity)
+ .setRemoteLockscreenValidation(true)
+ .setStartLockscreenValidationRequest(request)
+ .setRemoteLockscreenValidationServiceComponent(COMPONENT_NAME));
+ chooseLockSettingsHelper.launch();
+
+ Intent startedIntent = shadowActivity.getNextStartedActivity();
+ assertEquals(new ComponentName("com.android.settings",
+ ConfirmLockPattern.class.getName()), startedIntent.getComponent());
+ assertThat(startedIntent.getBooleanExtra(
+ ConfirmDeviceCredentialBaseFragment.IS_REMOTE_LOCKSCREEN_VALIDATION, false)
+ ).isTrue();
+ assertThat(startedIntent.getParcelableExtra(
+ KeyguardManager.EXTRA_START_LOCKSCREEN_VALIDATION_REQUEST,
+ StartLockscreenValidationRequest.class)
+ ).isEqualTo(request);
+ assertThat(startedIntent.getParcelableExtra(
+ Intent.EXTRA_COMPONENT_NAME, ComponentName.class)
+ ).isEqualTo(COMPONENT_NAME);
+ }
+
private ChooseLockSettingsHelper getChooseLockSettingsHelper(
ChooseLockSettingsHelper.Builder builder) {
LockPatternUtils mockLockPatternUtils = mock(LockPatternUtils.class);
diff --git a/tests/robotests/src/com/android/settings/password/ConfirmCredentialTest.java b/tests/robotests/src/com/android/settings/password/ConfirmCredentialTest.java
index e210c5f..40b359e 100644
--- a/tests/robotests/src/com/android/settings/password/ConfirmCredentialTest.java
+++ b/tests/robotests/src/com/android/settings/password/ConfirmCredentialTest.java
@@ -16,25 +16,162 @@
package com.android.settings.password;
+import static com.android.settings.password.TestUtils.NO_MORE_REMAINING_ATTEMPTS;
+import static com.android.settings.password.TestUtils.PACKAGE_NAME;
+import static com.android.settings.password.TestUtils.SERVICE_NAME;
+import static com.android.settings.password.TestUtils.VALID_REMAINING_ATTEMPTS;
+import static com.android.settings.password.TestUtils.buildConfirmDeviceCredentialBaseActivity;
+import static com.android.settings.password.TestUtils.createPackageInfoWithService;
+import static com.android.settings.password.TestUtils.createRemoteLockscreenValidationIntent;
+import static com.android.settings.password.TestUtils.createStartLockscreenValidationRequest;
+import static com.android.settings.password.TestUtils.getConfirmDeviceCredentialBaseFragment;
+
import static com.google.common.truth.Truth.assertThat;
+import android.Manifest;
+import android.app.KeyguardManager;
+import android.app.admin.ManagedSubscriptionsPolicy;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.util.FeatureFlagUtils;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
+import androidx.test.core.app.ApplicationProvider;
import com.android.settings.password.ConfirmDeviceCredentialBaseFragment.LastTryDialog;
+import com.android.settings.testutils.shadow.ShadowDevicePolicyManager;
+import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
+import com.android.settings.testutils.shadow.ShadowUserManager;
+import com.android.settings.testutils.shadow.ShadowUtils;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplicationPackageManager;
@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {
+ ShadowLockPatternUtils.class,
+ ShadowUtils.class,
+ ShadowDevicePolicyManager.class,
+ ShadowUserManager.class,
+ ShadowApplicationPackageManager.class
+})
public class ConfirmCredentialTest {
- private Context mContext = RuntimeEnvironment.application;
+ private Context mContext;
+ private ShadowApplicationPackageManager mShadowApplicationPackageManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = ApplicationProvider.getApplicationContext();
+
+ mShadowApplicationPackageManager =
+ (ShadowApplicationPackageManager) Shadows.shadowOf(mContext.getPackageManager());
+ mShadowApplicationPackageManager.addPackageNoDefaults(
+ TestUtils.createPackageInfoWithService(
+ PACKAGE_NAME, SERVICE_NAME,
+ Manifest.permission.BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE));
+
+ final ShadowDevicePolicyManager shadowDpm = ShadowDevicePolicyManager.getShadow();
+ shadowDpm.setManagedSubscriptionsPolicy(
+ new ManagedSubscriptionsPolicy(
+ ManagedSubscriptionsPolicy.TYPE_ALL_PERSONAL_SUBSCRIPTIONS));
+
+ FeatureFlagUtils.setEnabled(mContext,
+ FeatureFlagUtils.SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION, true);
+ }
+
+ @Test
+ public void onCreate_successfullyStart() {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(ConfirmLockPassword.class, new Intent());
+ ConfirmDeviceCredentialBaseFragment fragment =
+ getConfirmDeviceCredentialBaseFragment(activity);
+
+ assertThat(activity.isFinishing()).isFalse();
+ assertThat(fragment.mRemoteValidation).isFalse();
+ }
+
+ @Test
+ public void onCreate_remoteValidation_successfullyStart() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity = buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class, createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS));
+ ConfirmDeviceCredentialBaseFragment fragment =
+ getConfirmDeviceCredentialBaseFragment(activity);
+
+ assertThat(activity.isFinishing()).isFalse();
+ assertThat(fragment.mRemoteValidation).isTrue();
+ }
+
+ @Test
+ public void onCreate_remoteValidation_flagDisabled_finishActivity() throws Exception {
+ FeatureFlagUtils.setEnabled(mContext,
+ FeatureFlagUtils.SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION, false);
+
+ ConfirmDeviceCredentialBaseActivity activity = buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS));
+
+ assertThat(activity.isFinishing()).isTrue();
+ }
+
+ @Test
+ public void onCreate_remoteValidation_invalidServiceComponentName_finishActivity()
+ throws Exception {
+ Intent intentWithInvalidComponentName = new Intent()
+ .putExtra(ConfirmDeviceCredentialBaseFragment.IS_REMOTE_LOCKSCREEN_VALIDATION, true)
+ .putExtra(KeyguardManager.EXTRA_START_LOCKSCREEN_VALIDATION_REQUEST,
+ createStartLockscreenValidationRequest(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS))
+ .putExtra(Intent.EXTRA_COMPONENT_NAME, new ComponentName("pkg", "cls"));
+
+ ConfirmDeviceCredentialBaseActivity activity = buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class, intentWithInvalidComponentName);
+
+ assertThat(activity.isFinishing()).isTrue();
+ }
+
+ @Test
+ public void onCreate_remoteValidation_serviceDoesNotRequestCorrectPermission_finishActivity()
+ throws Exception {
+ // Remove package with valid ServiceInfo
+ mShadowApplicationPackageManager.removePackage(PACKAGE_NAME);
+ // Add a service that does not request the BIND_REMOTE_LOCKSCREEN_SERVICE permission
+ mShadowApplicationPackageManager.addPackageNoDefaults(
+ createPackageInfoWithService(
+ PACKAGE_NAME,
+ SERVICE_NAME,
+ Manifest.permission.BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE));
+
+ ConfirmDeviceCredentialBaseActivity activity = buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS));
+
+ assertThat(activity.isFinishing()).isTrue();
+ }
+
+ @Test
+ public void onCreate_remoteValidation_noMoreAttempts_finishActivity() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity = buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, NO_MORE_REMAINING_ATTEMPTS));
+
+ assertThat(activity.isFinishing()).isTrue();
+ }
@Test
public void testLastTryDialogShownExactlyOnce() {
diff --git a/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java b/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java
new file mode 100644
index 0000000..77a013d
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java
@@ -0,0 +1,308 @@
+/*
+ * 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.password;
+
+import static com.android.settings.password.ConfirmLockPassword.ConfirmLockPasswordFragment;
+import static com.android.settings.password.TestUtils.GUESS_INVALID_RESULT;
+import static com.android.settings.password.TestUtils.GUESS_VALID_RESULT;
+import static com.android.settings.password.TestUtils.LOCKOUT_RESULT;
+import static com.android.settings.password.TestUtils.NO_REMAINING_ATTEMPTS_RESULT;
+import static com.android.settings.password.TestUtils.PACKAGE_NAME;
+import static com.android.settings.password.TestUtils.SERVICE_NAME;
+import static com.android.settings.password.TestUtils.TIMEOUT_MS;
+import static com.android.settings.password.TestUtils.VALID_REMAINING_ATTEMPTS;
+import static com.android.settings.password.TestUtils.buildConfirmDeviceCredentialBaseActivity;
+import static com.android.settings.password.TestUtils.createRemoteLockscreenValidationIntent;
+import static com.android.settings.password.TestUtils.getConfirmDeviceCredentialBaseFragment;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import android.Manifest;
+import android.app.KeyguardManager;
+import android.app.admin.ManagedSubscriptionsPolicy;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.service.remotelockscreenvalidation.IRemoteLockscreenValidationCallback;
+import android.service.remotelockscreenvalidation.RemoteLockscreenValidationClient;
+import android.text.InputType;
+import android.util.FeatureFlagUtils;
+import android.widget.ImeAwareEditText;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.settings.R;
+import com.android.settings.testutils.shadow.ShadowDevicePolicyManager;
+import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
+import com.android.settings.testutils.shadow.ShadowUserManager;
+import com.android.settings.testutils.shadow.ShadowUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplicationPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {
+ ShadowLockPatternUtils.class,
+ ShadowUtils.class,
+ ShadowDevicePolicyManager.class,
+ ShadowUserManager.class,
+ ShadowApplicationPackageManager.class
+})
+public class ConfirmLockPasswordTest {
+
+ @Mock
+ CredentialCheckResultTracker mCredentialCheckResultTracker;
+ @Mock
+ RemoteLockscreenValidationClient mRemoteLockscreenValidationClient;
+ @Captor
+ ArgumentCaptor<IRemoteLockscreenValidationCallback> mCallbackCaptor;
+
+ private Context mContext;
+ private LockPatternUtils mLockPatternUtils;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = ApplicationProvider.getApplicationContext();
+ mLockPatternUtils = new LockPatternUtils(mContext);
+
+ ShadowApplicationPackageManager shadowApplicationPackageManager =
+ (ShadowApplicationPackageManager) Shadows.shadowOf(mContext.getPackageManager());
+ shadowApplicationPackageManager.addPackageNoDefaults(
+ TestUtils.createPackageInfoWithService(
+ PACKAGE_NAME,
+ SERVICE_NAME,
+ Manifest.permission.BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE));
+
+ final ShadowDevicePolicyManager shadowDpm = ShadowDevicePolicyManager.getShadow();
+ shadowDpm.setManagedSubscriptionsPolicy(
+ new ManagedSubscriptionsPolicy(
+ ManagedSubscriptionsPolicy.TYPE_ALL_PERSONAL_SUBSCRIPTIONS));
+
+ // Set false by default so we can check if lock was set when remote validation succeeds.
+ ShadowLockPatternUtils.setIsSecure(UserHandle.myUserId(), false);
+
+ FeatureFlagUtils.setEnabled(mContext,
+ FeatureFlagUtils.SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION, true);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowLockPatternUtils.reset();
+ }
+
+ @Test
+ public void onCreate_remoteValidation_password_successfullyStart() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS));
+ ConfirmLockPasswordFragment fragment =
+ (ConfirmLockPasswordFragment) getConfirmDeviceCredentialBaseFragment(activity);
+
+ assertThat(activity.isFinishing()).isFalse();
+ assertThat(fragment.mRemoteValidation).isTrue();
+ ImeAwareEditText editText = (ImeAwareEditText) activity.findViewById(R.id.password_entry);
+ assertThat(editText.getInputType()).isEqualTo(
+ InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ }
+
+ @Test
+ public void onCreate_remoteValidation_pin_successfullyStart() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PIN, VALID_REMAINING_ATTEMPTS));
+ ConfirmLockPasswordFragment fragment =
+ (ConfirmLockPasswordFragment) getConfirmDeviceCredentialBaseFragment(activity);
+
+ assertThat(activity.isFinishing()).isFalse();
+ assertThat(fragment.mRemoteValidation).isTrue();
+ ImeAwareEditText editText = (ImeAwareEditText) activity.findViewById(R.id.password_entry);
+ assertThat(editText.getInputType()).isEqualTo(
+ InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
+ }
+
+ @Test
+ public void handleNext_normalFlow_doesNotAttemptRemoteLockscreenValidation() {
+ ConfirmLockPassword activity = Robolectric.buildActivity(
+ ConfirmLockPassword.class, new Intent()).setup().get();
+ ConfirmLockPasswordFragment fragment =
+ (ConfirmLockPasswordFragment) getConfirmDeviceCredentialBaseFragment(activity);
+ ImeAwareEditText passwordEntry = activity.findViewById(R.id.password_entry);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerHandleNext(fragment, passwordEntry);
+
+ verifyNoInteractions(mRemoteLockscreenValidationClient);
+ }
+
+ @Test
+ public void handleNext_remoteValidation_correctGuess_checkboxChecked() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS));
+ ConfirmLockPasswordFragment fragment =
+ (ConfirmLockPasswordFragment) getConfirmDeviceCredentialBaseFragment(activity);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ ImeAwareEditText passwordEntry = activity.findViewById(R.id.password_entry);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerHandleNext(fragment, passwordEntry);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(GUESS_VALID_RESULT);
+
+ verify(mCredentialCheckResultTracker).setResult(
+ eq(true), any(), eq(0), eq(fragment.mEffectiveUserId));
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isTrue();
+ assertThat(fragment.mDeviceCredentialGuess).isNotNull();
+ }
+
+ @Test
+ public void handleNext_remoteValidation_correctGuess_checkboxUnchecked() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS));
+ ConfirmLockPasswordFragment fragment =
+ (ConfirmLockPasswordFragment) getConfirmDeviceCredentialBaseFragment(activity);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ fragment.mCheckBox.setChecked(false);
+ ImeAwareEditText passwordEntry = activity.findViewById(R.id.password_entry);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerHandleNext(fragment, passwordEntry);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(GUESS_VALID_RESULT);
+
+ verify(mCredentialCheckResultTracker).setResult(
+ eq(true), any(), eq(0), eq(fragment.mEffectiveUserId));
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse();
+ assertThat(fragment.mDeviceCredentialGuess).isNull();
+ }
+
+ @Test
+ public void handleNext_remoteValidation_guessInvalid() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS));
+ ConfirmLockPasswordFragment fragment =
+ (ConfirmLockPasswordFragment) getConfirmDeviceCredentialBaseFragment(activity);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ ImeAwareEditText passwordEntry = activity.findViewById(R.id.password_entry);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerHandleNext(fragment, passwordEntry);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(GUESS_INVALID_RESULT);
+
+ verify(mCredentialCheckResultTracker).setResult(
+ eq(false), any(), eq(0), eq(fragment.mEffectiveUserId));
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse();
+ }
+
+ @Test
+ public void handleNext_remoteValidation_lockout() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS));
+ ConfirmLockPasswordFragment fragment =
+ (ConfirmLockPasswordFragment) getConfirmDeviceCredentialBaseFragment(activity);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ ImeAwareEditText passwordEntry = activity.findViewById(R.id.password_entry);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerHandleNext(fragment, passwordEntry);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(LOCKOUT_RESULT);
+
+ verify(mCredentialCheckResultTracker).setResult(
+ eq(false), any(), eq(TIMEOUT_MS), eq(fragment.mEffectiveUserId));
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse();
+ }
+
+ @Test
+ public void handleNext_remoteValidation_noRemainingAttempts_finishActivity() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPassword.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PASSWORD, VALID_REMAINING_ATTEMPTS));
+ ConfirmLockPasswordFragment fragment =
+ (ConfirmLockPasswordFragment) getConfirmDeviceCredentialBaseFragment(activity);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ ImeAwareEditText passwordEntry = activity.findViewById(R.id.password_entry);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerHandleNext(fragment, passwordEntry);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(NO_REMAINING_ATTEMPTS_RESULT);
+
+ assertThat(activity.isFinishing()).isTrue();
+ verify(mCredentialCheckResultTracker, never())
+ .setResult(anyBoolean(), any(), anyInt(), anyInt());
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse();
+ }
+
+ private void triggerHandleNext(
+ ConfirmLockPasswordFragment fragment, ImeAwareEditText passwordEntry) {
+ passwordEntry.setText("Password");
+ ReflectionHelpers.callInstanceMethod(fragment, "handleNext");
+ }
+
+}
diff --git a/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java b/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java
new file mode 100644
index 0000000..4374e38
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.password;
+
+import static com.android.settings.password.TestUtils.GUESS_INVALID_RESULT;
+import static com.android.settings.password.TestUtils.GUESS_VALID_RESULT;
+import static com.android.settings.password.TestUtils.LOCKOUT_RESULT;
+import static com.android.settings.password.TestUtils.NO_REMAINING_ATTEMPTS_RESULT;
+import static com.android.settings.password.TestUtils.PACKAGE_NAME;
+import static com.android.settings.password.TestUtils.SERVICE_NAME;
+import static com.android.settings.password.TestUtils.TIMEOUT_MS;
+import static com.android.settings.password.TestUtils.VALID_REMAINING_ATTEMPTS;
+import static com.android.settings.password.TestUtils.buildConfirmDeviceCredentialBaseActivity;
+import static com.android.settings.password.TestUtils.createPackageInfoWithService;
+import static com.android.settings.password.TestUtils.createRemoteLockscreenValidationIntent;
+import static com.android.settings.password.TestUtils.getConfirmDeviceCredentialBaseFragment;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import android.Manifest;
+import android.app.KeyguardManager;
+import android.app.admin.ManagedSubscriptionsPolicy;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.service.remotelockscreenvalidation.IRemoteLockscreenValidationCallback;
+import android.service.remotelockscreenvalidation.RemoteLockscreenValidationClient;
+import android.util.FeatureFlagUtils;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockPatternView;
+import com.android.settings.R;
+import com.android.settings.testutils.shadow.ShadowDevicePolicyManager;
+import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
+import com.android.settings.testutils.shadow.ShadowUserManager;
+import com.android.settings.testutils.shadow.ShadowUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplicationPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {
+ ShadowLockPatternUtils.class,
+ ShadowUtils.class,
+ ShadowDevicePolicyManager.class,
+ ShadowUserManager.class,
+ ShadowApplicationPackageManager.class
+})
+public class ConfirmLockPatternTest {
+
+ @Mock
+ CredentialCheckResultTracker mCredentialCheckResultTracker;
+ @Mock
+ RemoteLockscreenValidationClient mRemoteLockscreenValidationClient;
+ @Captor
+ ArgumentCaptor<IRemoteLockscreenValidationCallback> mCallbackCaptor;
+
+ private Context mContext;
+ private LockPatternUtils mLockPatternUtils;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = ApplicationProvider.getApplicationContext();
+ mLockPatternUtils = new LockPatternUtils(mContext);
+
+ ShadowApplicationPackageManager shadowApplicationPackageManager =
+ (ShadowApplicationPackageManager) Shadows.shadowOf(mContext.getPackageManager());
+ shadowApplicationPackageManager.addPackageNoDefaults(
+ createPackageInfoWithService(
+ PACKAGE_NAME,
+ SERVICE_NAME,
+ Manifest.permission.BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE));
+
+ final ShadowDevicePolicyManager shadowDpm = ShadowDevicePolicyManager.getShadow();
+ shadowDpm.setManagedSubscriptionsPolicy(
+ new ManagedSubscriptionsPolicy(
+ ManagedSubscriptionsPolicy.TYPE_ALL_PERSONAL_SUBSCRIPTIONS));
+
+ // Set false by default so we can check if lock was set when remote validation succeeds.
+ ShadowLockPatternUtils.setIsSecure(UserHandle.myUserId(), false);
+
+ FeatureFlagUtils.setEnabled(mContext,
+ FeatureFlagUtils.SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION, true);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowLockPatternUtils.reset();
+ }
+
+ @Test
+ public void onCreate_remoteValidation_successfullyStart() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPattern.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PATTERN, VALID_REMAINING_ATTEMPTS));
+ ConfirmDeviceCredentialBaseFragment fragment =
+ getConfirmDeviceCredentialBaseFragment(activity);
+
+ assertThat(activity.isFinishing()).isFalse();
+ assertThat(fragment.mRemoteValidation).isTrue();
+ LockPatternView lockPatternView = (LockPatternView) activity.findViewById(R.id.lockPattern);
+ assertThat(lockPatternView.isInStealthMode()).isFalse();
+ }
+
+ @Test
+ public void onPatternDetected_normalFlow_doesNotAttemptRemoteLockscreenValidation() {
+ ConfirmLockPattern activity = Robolectric.buildActivity(
+ ConfirmLockPattern.class, new Intent()).setup().get();
+ ConfirmDeviceCredentialBaseFragment fragment =
+ getConfirmDeviceCredentialBaseFragment(activity);
+ LockPatternView lockPatternView = activity.findViewById(R.id.lockPattern);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerOnPatternDetected(lockPatternView);
+
+ verifyNoInteractions(mRemoteLockscreenValidationClient);
+ }
+
+ @Test
+ public void onPatternDetected_remoteValidation_guessValid_checkboxChecked() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPattern.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PATTERN, VALID_REMAINING_ATTEMPTS));
+ ConfirmDeviceCredentialBaseFragment fragment =
+ getConfirmDeviceCredentialBaseFragment(activity);
+ LockPatternView lockPatternView = activity.findViewById(R.id.lockPattern);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerOnPatternDetected(lockPatternView);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(GUESS_VALID_RESULT);
+
+ verify(mCredentialCheckResultTracker).setResult(
+ eq(true), any(), eq(0), eq(fragment.mEffectiveUserId));
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isTrue();
+ assertThat(fragment.mDeviceCredentialGuess).isNotNull();
+ }
+
+ @Test
+ public void onPatternDetected_remoteValidation_guessValid_checkboxUnchecked() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPattern.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PATTERN, VALID_REMAINING_ATTEMPTS));
+ ConfirmDeviceCredentialBaseFragment fragment =
+ getConfirmDeviceCredentialBaseFragment(activity);
+ LockPatternView lockPatternView = activity.findViewById(R.id.lockPattern);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ fragment.mCheckBox.setChecked(false);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerOnPatternDetected(lockPatternView);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(GUESS_VALID_RESULT);
+
+ verify(mCredentialCheckResultTracker).setResult(
+ eq(true), any(), eq(0), eq(fragment.mEffectiveUserId));
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse();
+ assertThat(fragment.mDeviceCredentialGuess).isNull();
+ }
+
+ @Test
+ public void onPatternDetected_remoteValidation_guessInvalid() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPattern.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PATTERN, VALID_REMAINING_ATTEMPTS));
+ ConfirmDeviceCredentialBaseFragment fragment =
+ getConfirmDeviceCredentialBaseFragment(activity);
+ LockPatternView lockPatternView = activity.findViewById(R.id.lockPattern);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerOnPatternDetected(lockPatternView);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(GUESS_INVALID_RESULT);
+
+ verify(mCredentialCheckResultTracker).setResult(
+ eq(false), any(), eq(0), eq(fragment.mEffectiveUserId));
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse();
+ }
+
+ @Test
+ public void onPatternDetected_remoteValidation_lockout() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPattern.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PATTERN, VALID_REMAINING_ATTEMPTS));
+ ConfirmDeviceCredentialBaseFragment fragment =
+ getConfirmDeviceCredentialBaseFragment(activity);
+ LockPatternView lockPatternView = activity.findViewById(R.id.lockPattern);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerOnPatternDetected(lockPatternView);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(LOCKOUT_RESULT);
+
+ verify(mCredentialCheckResultTracker).setResult(
+ eq(false), any(), eq(TIMEOUT_MS), eq(fragment.mEffectiveUserId));
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse();
+ }
+
+ @Test
+ public void onPatternDetected_noRemainingAttempts_finishActivity() throws Exception {
+ ConfirmDeviceCredentialBaseActivity activity =
+ buildConfirmDeviceCredentialBaseActivity(
+ ConfirmLockPattern.class,
+ createRemoteLockscreenValidationIntent(
+ KeyguardManager.PATTERN, VALID_REMAINING_ATTEMPTS));
+ ConfirmDeviceCredentialBaseFragment fragment =
+ getConfirmDeviceCredentialBaseFragment(activity);
+ LockPatternView lockPatternView = activity.findViewById(R.id.lockPattern);
+ ReflectionHelpers.setField(fragment,
+ "mCredentialCheckResultTracker", mCredentialCheckResultTracker);
+ fragment.mRemoteLockscreenValidationClient = mRemoteLockscreenValidationClient;
+
+ triggerOnPatternDetected(lockPatternView);
+ verify(mRemoteLockscreenValidationClient)
+ .validateLockscreenGuess(any(), mCallbackCaptor.capture());
+ mCallbackCaptor.getValue().onSuccess(NO_REMAINING_ATTEMPTS_RESULT);
+
+ assertThat(activity.isFinishing()).isTrue();
+ verifyNoInteractions(mCredentialCheckResultTracker);
+ assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse();
+ }
+
+ private void triggerOnPatternDetected(LockPatternView lockPatternView) {
+ List<LockPatternView.Cell> pattern = List.of(LockPatternView.Cell.of(0, 0));
+ lockPatternView.setPattern(LockPatternView.DisplayMode.Correct, pattern);
+ ReflectionHelpers.callInstanceMethod(lockPatternView, "notifyPatternDetected");
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/password/TestUtils.java b/tests/robotests/src/com/android/settings/password/TestUtils.java
new file mode 100644
index 0000000..246d926
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/password/TestUtils.java
@@ -0,0 +1,107 @@
+/*
+ * 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.password;
+
+import android.app.KeyguardManager;
+import android.app.RemoteLockscreenValidationResult;
+import android.app.StartLockscreenValidationRequest;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.ServiceInfo;
+
+import com.android.security.SecureBox;
+import com.android.settings.R;
+
+import org.robolectric.Robolectric;
+
+import java.security.NoSuchAlgorithmException;
+
+public final class TestUtils {
+
+ public static final String SERVICE_NAME = "SERVICE_NAME";
+ public static final String PACKAGE_NAME = "PACKAGE_NAME";
+ public static final ComponentName COMPONENT_NAME =
+ new ComponentName(PACKAGE_NAME, SERVICE_NAME);
+ public static final int VALID_REMAINING_ATTEMPTS = 5;
+ public static final int NO_MORE_REMAINING_ATTEMPTS = 0;
+ public static final int TIMEOUT_MS = 10000;
+ public static final RemoteLockscreenValidationResult GUESS_VALID_RESULT =
+ new RemoteLockscreenValidationResult.Builder()
+ .setResultCode(RemoteLockscreenValidationResult.RESULT_GUESS_VALID)
+ .build();
+ public static final RemoteLockscreenValidationResult GUESS_INVALID_RESULT =
+ new RemoteLockscreenValidationResult.Builder()
+ .setResultCode(RemoteLockscreenValidationResult.RESULT_GUESS_INVALID)
+ .build();
+ public static final RemoteLockscreenValidationResult LOCKOUT_RESULT =
+ new RemoteLockscreenValidationResult.Builder()
+ .setResultCode(RemoteLockscreenValidationResult.RESULT_LOCKOUT)
+ .setTimeoutMillis(TIMEOUT_MS)
+ .build();
+ public static final RemoteLockscreenValidationResult NO_REMAINING_ATTEMPTS_RESULT =
+ new RemoteLockscreenValidationResult.Builder()
+ .setResultCode(RemoteLockscreenValidationResult.RESULT_NO_REMAINING_ATTEMPTS)
+ .build();
+
+ private TestUtils() {
+ }
+
+ public static PackageInfo createPackageInfoWithService(
+ String packageName, String serviceName, String requiredServicePermission) {
+ ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.name = serviceName;
+ serviceInfo.applicationInfo = new ApplicationInfo();
+ serviceInfo.permission = requiredServicePermission;
+
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = packageName;
+ packageInfo.services = new ServiceInfo[]{serviceInfo};
+ return packageInfo;
+ }
+
+ public static Intent createRemoteLockscreenValidationIntent(
+ int lockscreenType, int remainingAttempts) throws Exception {
+ return new Intent()
+ .putExtra(ConfirmDeviceCredentialBaseFragment.IS_REMOTE_LOCKSCREEN_VALIDATION, true)
+ .putExtra(KeyguardManager.EXTRA_START_LOCKSCREEN_VALIDATION_REQUEST,
+ createStartLockscreenValidationRequest(lockscreenType, remainingAttempts))
+ .putExtra(Intent.EXTRA_COMPONENT_NAME, COMPONENT_NAME);
+ }
+
+ public static StartLockscreenValidationRequest createStartLockscreenValidationRequest(
+ int lockscreenType, int remainingAttempts) throws NoSuchAlgorithmException {
+ return new StartLockscreenValidationRequest.Builder()
+ .setLockscreenUiType(lockscreenType)
+ .setRemainingAttempts(remainingAttempts)
+ .setSourcePublicKey(SecureBox.genKeyPair().getPublic().getEncoded())
+ .build();
+ }
+
+ public static ConfirmDeviceCredentialBaseActivity buildConfirmDeviceCredentialBaseActivity(
+ Class<? extends ConfirmDeviceCredentialBaseActivity> impl, Intent intent) {
+ return Robolectric.buildActivity(impl, intent).setup().get();
+ }
+
+ public static ConfirmDeviceCredentialBaseFragment getConfirmDeviceCredentialBaseFragment(
+ ConfirmDeviceCredentialBaseActivity activity) {
+ return (ConfirmDeviceCredentialBaseFragment)
+ activity.getSupportFragmentManager().findFragmentById(R.id.main_content);
+ }
+
+}
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowDevicePolicyManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowDevicePolicyManager.java
index a2e4430..0396503 100644
--- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowDevicePolicyManager.java
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowDevicePolicyManager.java
@@ -8,6 +8,7 @@
import android.annotation.UserIdInt;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManager.DeviceOwnerType;
+import android.app.admin.ManagedSubscriptionsPolicy;
import android.app.admin.PasswordMetrics;
import android.app.admin.PasswordPolicy;
import android.content.ComponentName;
@@ -29,6 +30,7 @@
private Map<Integer, CharSequence> mSupportMessagesMap = new HashMap<>();
private boolean mIsAdminActiveAsUser = false;
private ComponentName mDeviceOwnerComponentName;
+ private ManagedSubscriptionsPolicy mManagedSubscriptionsPolicy;
private int mDeviceOwnerUserId = -1;
private int mPasswordMinQuality = PASSWORD_QUALITY_UNSPECIFIED;
private int mPasswordMinLength = 0;
@@ -85,6 +87,10 @@
mDeviceOwnerTypes.put(admin.getPackageName(), deviceOwnerType);
}
+ public void setManagedSubscriptionsPolicy(ManagedSubscriptionsPolicy policy) {
+ mManagedSubscriptionsPolicy = policy;
+ }
+
@DeviceOwnerType
public int getDeviceOwnerType(@NonNull ComponentName admin) {
return mDeviceOwnerTypes.getOrDefault(admin.getPackageName(), DEVICE_OWNER_TYPE_DEFAULT);
@@ -99,6 +105,11 @@
return policy.getMinMetrics();
}
+ @Implementation
+ public ManagedSubscriptionsPolicy getManagedSubscriptionsPolicy() {
+ return mManagedSubscriptionsPolicy;
+ }
+
public void setPasswordQuality(int quality) {
mPasswordMinQuality = quality;
}
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java
index 157c218..c6c6c55 100644
--- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java
@@ -16,6 +16,8 @@
package com.android.settings.testutils.shadow;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.admin.DevicePolicyManager;
import android.app.admin.PasswordMetrics;
import android.content.ComponentName;
@@ -43,6 +45,10 @@
private static Map<Integer, PasswordMetrics> sUserToMetricsMap = new HashMap<>();
private static Map<Integer, PasswordMetrics> sUserToProfileMetricsMap = new HashMap<>();
private static Map<Integer, Boolean> sUserToIsSecureMap = new HashMap<>();
+ private static Map<Integer, Boolean> sUserToPatternEverChosenMap = new HashMap<>();
+ private static Map<Integer, Boolean> sUserToVisiblePatternEnabledMap = new HashMap<>();
+ private static Map<Integer, Boolean> sUserToBiometricAllowedMap = new HashMap<>();
+ private static Map<Integer, Boolean> sUserToLockPatternEnabledMap = new HashMap<>();
private static boolean sIsUserOwnsFrpCredential;
@@ -53,6 +59,10 @@
sUserToMetricsMap.clear();
sUserToProfileMetricsMap.clear();
sUserToIsSecureMap.clear();
+ sUserToPatternEverChosenMap.clear();
+ sUserToVisiblePatternEnabledMap.clear();
+ sUserToBiometricAllowedMap.clear();
+ sUserToLockPatternEnabledMap.clear();
sDeviceEncryptionEnabled = false;
sIsUserOwnsFrpCredential = false;
}
@@ -136,6 +146,56 @@
sIsUserOwnsFrpCredential = isUserOwnsFrpCredential;
}
+ @Implementation
+ public boolean isVisiblePatternEnabled(int userId) {
+ return sUserToVisiblePatternEnabledMap.getOrDefault(userId, false);
+ }
+
+ public static void setIsVisiblePatternEnabled(int userId, boolean isVisiblePatternEnabled) {
+ sUserToVisiblePatternEnabledMap.put(userId, isVisiblePatternEnabled);
+ }
+
+ @Implementation
+ public boolean isPatternEverChosen(int userId) {
+ return sUserToPatternEverChosenMap.getOrDefault(userId, true);
+ }
+
+ public static void setIsPatternEverChosen(int userId, boolean isPatternEverChosen) {
+ sUserToPatternEverChosenMap.put(userId, isPatternEverChosen);
+ }
+
+ @Implementation
+ public boolean isBiometricAllowedForUser(int userId) {
+ return sUserToBiometricAllowedMap.getOrDefault(userId, false);
+ }
+
+ public static void setIsBiometricAllowedForUser(int userId, boolean isBiometricAllowed) {
+ sUserToBiometricAllowedMap.put(userId, isBiometricAllowed);
+ }
+
+ @Implementation
+ public boolean isLockPatternEnabled(int userId) {
+ return sUserToBiometricAllowedMap.getOrDefault(userId, false);
+ }
+
+ public static void setIsLockPatternEnabled(int userId, boolean isLockPatternEnabled) {
+ sUserToLockPatternEnabledMap.put(userId, isLockPatternEnabled);
+ }
+
+ @Implementation
+ public boolean setLockCredential(@NonNull LockscreenCredential newCredential,
+ @NonNull LockscreenCredential savedCredential, int userHandle) {
+ setIsSecure(userHandle, true);
+ return true;
+ }
+
+ @Implementation
+ public boolean checkCredential(@NonNull LockscreenCredential credential, int userId,
+ @Nullable LockPatternUtils.CheckCredentialProgressCallback progressCallback)
+ throws LockPatternUtils.RequestThrottledException {
+ return true;
+ }
+
public static void setRequiredPasswordComplexity(int userHandle, int complexity) {
sUserToComplexityMap.put(userHandle, complexity);
}