diff --git a/res/values/strings.xml b/res/values/strings.xml index f77c10d577c..b1edbb4970c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1430,6 +1430,16 @@ Change unlock password + + %1$s requests a strong PIN or password. + + %1$s requests a new PIN or password. + + %1$s requests a new pattern, PIN or password. + + %1$s requests a new screen lock. + + Try again. Attempt %1$d of %2$d. diff --git a/src/com/android/settings/password/ChooseLockGeneric.java b/src/com/android/settings/password/ChooseLockGeneric.java index ca963448bd5..cae7d332117 100644 --- a/src/com/android/settings/password/ChooseLockGeneric.java +++ b/src/com/android/settings/password/ChooseLockGeneric.java @@ -18,13 +18,20 @@ package com.android.settings.password; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PARENT_PROFILE_PASSWORD; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; import static com.android.settings.password.ChooseLockPassword.ChooseLockPasswordFragment.RESULT_FINISHED; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CALLER_APP_NAME; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; import android.accessibilityservice.AccessibilityServiceInfo; import android.app.Activity; import android.app.Dialog; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; @@ -66,6 +73,8 @@ import com.android.settings.search.SearchFeatureProvider; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; import java.util.List; @@ -152,6 +161,14 @@ public class ChooseLockGeneric extends SettingsActivity { private UserManager mUserManager; private ChooseLockGenericController mController; + /** + * From intent extra {@link ChooseLockSettingsHelper#EXTRA_KEY_REQUESTED_MIN_COMPLEXITY}. + */ + @PasswordComplexity private int mRequestedMinComplexity; + + /** From intent extra {@link ChooseLockSettingsHelper#EXTRA_KEY_CALLER_APP_NAME}. */ + private String mCallerAppName = null; + protected boolean mForFingerprint = false; protected boolean mForFace = false; @@ -195,6 +212,10 @@ public class ChooseLockGeneric extends SettingsActivity { ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false); mForFace = getActivity().getIntent().getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false); + mRequestedMinComplexity = getActivity().getIntent() + .getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE); + mCallerAppName = + getActivity().getIntent().getStringExtra(EXTRA_KEY_CALLER_APP_NAME); mForChangeCredRequiredForBoot = getArguments() != null && getArguments().getBoolean( ChooseLockSettingsHelper.EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT); mUserManager = UserManager.get(getActivity()); @@ -217,7 +238,8 @@ public class ChooseLockGeneric extends SettingsActivity { UserManager.get(getActivity()), getArguments(), getActivity().getIntent().getExtras()).getIdentifier(); - mController = new ChooseLockGenericController(getContext(), mUserId); + mController = + new ChooseLockGenericController(getContext(), mUserId, mRequestedMinComplexity); if (ACTION_SET_NEW_PASSWORD.equals(chooseLockAction) && UserManager.get(getActivity()).isManagedProfile(mUserId) && mLockPatternUtils.isSeparateProfileChallengeEnabled(mUserId)) { @@ -291,6 +313,9 @@ public class ChooseLockGeneric extends SettingsActivity { // Forward the target user id to ChooseLockGeneric. chooseLockGenericIntent.putExtra(Intent.EXTRA_USER_ID, mUserId); chooseLockGenericIntent.putExtra(CONFIRM_CREDENTIALS, !mPasswordConfirmed); + chooseLockGenericIntent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, + mRequestedMinComplexity); + chooseLockGenericIntent.putExtra(EXTRA_KEY_CALLER_APP_NAME, mCallerAppName); if (mUserPassword != null) { chooseLockGenericIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, mUserPassword); @@ -461,6 +486,13 @@ public class ChooseLockGeneric extends SettingsActivity { protected void addPreferences() { addPreferencesFromResource(R.xml.security_settings_picker); + if (!TextUtils.isEmpty(mCallerAppName)) { + FooterPreferenceMixinCompat footerMixin = + new FooterPreferenceMixinCompat(this, getSettingsLifecycle()); + FooterPreference footer = footerMixin.createFooterPreference(); + footer.setTitle(getFooterString()); + } + // Used for testing purposes findPreference(ScreenLockType.NONE.preferenceKey).setViewId(R.id.lock_none); findPreference(KEY_SKIP_FINGERPRINT).setViewId(R.id.lock_none); @@ -469,6 +501,27 @@ public class ChooseLockGeneric extends SettingsActivity { findPreference(ScreenLockType.PASSWORD.preferenceKey).setViewId(R.id.lock_password); } + private String getFooterString() { + @StringRes int stringId; + switch (mRequestedMinComplexity) { + case PASSWORD_COMPLEXITY_HIGH: + stringId = R.string.unlock_footer_high_complexity_requested; + break; + case PASSWORD_COMPLEXITY_MEDIUM: + stringId = R.string.unlock_footer_medium_complexity_requested; + break; + case PASSWORD_COMPLEXITY_LOW: + stringId = R.string.unlock_footer_low_complexity_requested; + break; + case PASSWORD_COMPLEXITY_NONE: + default: + stringId = R.string.unlock_footer_none_complexity_requested; + break; + } + + return getResources().getString(stringId, mCallerAppName); + } + private void updatePreferenceText() { if (mForFingerprint) { setPreferenceTitle(ScreenLockType.PATTERN, @@ -624,6 +677,7 @@ public class ChooseLockGeneric extends SettingsActivity { ChooseLockPassword.IntentBuilder builder = new ChooseLockPassword.IntentBuilder(getContext()) .setPasswordQuality(quality) + .setRequestedMinComplexity(mRequestedMinComplexity) .setForFingerprint(mForFingerprint) .setForFace(mForFace) .setUserId(mUserId); diff --git a/src/com/android/settings/password/ChooseLockGenericController.java b/src/com/android/settings/password/ChooseLockGenericController.java index eb7ff4e5fa1..91ca9574a98 100644 --- a/src/com/android/settings/password/ChooseLockGenericController.java +++ b/src/com/android/settings/password/ChooseLockGenericController.java @@ -16,7 +16,11 @@ package com.android.settings.password; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; + import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; +import android.app.admin.PasswordMetrics; import android.content.Context; import android.os.UserHandle; @@ -36,6 +40,7 @@ public class ChooseLockGenericController { private final Context mContext; private final int mUserId; + @PasswordComplexity private final int mRequestedMinComplexity; private ManagedLockPasswordProvider mManagedPasswordProvider; private DevicePolicyManager mDpm; @@ -43,6 +48,19 @@ public class ChooseLockGenericController { this( context, userId, + PASSWORD_COMPLEXITY_NONE); + } + + /** + * @param requestedMinComplexity specifies the min password complexity to be taken into account + * when determining the available screen lock types + */ + public ChooseLockGenericController(Context context, int userId, + @PasswordComplexity int requestedMinComplexity) { + this( + context, + userId, + requestedMinComplexity, context.getSystemService(DevicePolicyManager.class), ManagedLockPasswordProvider.get(context, userId)); } @@ -51,21 +69,26 @@ public class ChooseLockGenericController { ChooseLockGenericController( Context context, int userId, + @PasswordComplexity int requestedMinComplexity, DevicePolicyManager dpm, ManagedLockPasswordProvider managedLockPasswordProvider) { mContext = context; mUserId = userId; + mRequestedMinComplexity = requestedMinComplexity; mManagedPasswordProvider = managedLockPasswordProvider; mDpm = dpm; } /** - * @return The higher quality of either the specified {@code quality} or the quality required - * by {@link DevicePolicyManager#getPasswordQuality}. + * Returns the highest quality among the specified {@code quality}, the quality required by + * {@link DevicePolicyManager#getPasswordQuality}, and the quality required by min password + * complexity. */ public int upgradeQuality(int quality) { - // Compare min allowed password quality - return Math.max(quality, mDpm.getPasswordQuality(null, mUserId)); + // Compare specified quality and dpm quality + int dpmUpgradedQuality = Math.max(quality, mDpm.getPasswordQuality(null, mUserId)); + return Math.max(dpmUpgradedQuality, + PasswordMetrics.complexityLevelToMinQuality(mRequestedMinComplexity)); } /** diff --git a/src/com/android/settings/password/ChooseLockPassword.java b/src/com/android/settings/password/ChooseLockPassword.java index 0ae664cbf4b..129f9acab96 100644 --- a/src/com/android/settings/password/ChooseLockPassword.java +++ b/src/com/android/settings/password/ChooseLockPassword.java @@ -16,14 +16,18 @@ package com.android.settings.password; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; + import android.app.Activity; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; import android.app.admin.PasswordMetrics; import android.app.settings.SettingsEnums; import android.content.Context; @@ -56,6 +60,7 @@ import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternUtils.RequestThrottledException; import com.android.internal.widget.TextViewInputDisabler; @@ -133,6 +138,11 @@ public class ChooseLockPassword extends SettingsActivity { return this; } + public IntentBuilder setRequestedMinComplexity(@PasswordComplexity int level) { + mIntent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, level); + return this; + } + public Intent build() { return mIntent; } @@ -190,12 +200,10 @@ public class ChooseLockPassword extends SettingsActivity { private int mPasswordMinNumeric = 0; private int mPasswordMinNonLetter = 0; private int mPasswordMinLengthToFulfillAllPolicies = 0; + private boolean mPasswordNumSequenceAllowed = true; + @PasswordComplexity private int mRequestedMinComplexity = PASSWORD_COMPLEXITY_NONE; protected int mUserId; private byte[] mPasswordHistoryHashFactor; - /** - * Password requirements that we need to verify. - */ - private int[] mPasswordRequirements; private LockPatternUtils mLockPatternUtils; private SaveAndFinishWorker mSaveAndFinishWorker; @@ -372,7 +380,13 @@ public class ChooseLockPassword extends SettingsActivity { mForFingerprint = intent.getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false); mForFace = intent.getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false); - processPasswordRequirements(intent); + mRequestedMinComplexity = intent.getIntExtra( + EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE); + mRequestedQuality = Math.max( + intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, mRequestedQuality), + mLockPatternUtils.getRequestedPasswordQuality(mUserId)); + + loadDpmPasswordRequirements(); mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity()); if (intent.getBooleanExtra( @@ -504,31 +518,6 @@ public class ChooseLockPassword extends SettingsActivity { } private void setupPasswordRequirementsView(View view) { - final List passwordRequirements = new ArrayList<>(); - if (mPasswordMinUpperCase > 0) { - passwordRequirements.add(MIN_UPPER_LETTERS_IN_PASSWORD); - } - if (mPasswordMinLowerCase > 0) { - passwordRequirements.add(MIN_LOWER_LETTERS_IN_PASSWORD); - } - if (mPasswordMinLetters > 0) { - if (mPasswordMinLetters > mPasswordMinUpperCase + mPasswordMinLowerCase) { - passwordRequirements.add(MIN_LETTER_IN_PASSWORD); - } - } - if (mPasswordMinNumeric > 0) { - passwordRequirements.add(MIN_NUMBER_IN_PASSWORD); - } - if (mPasswordMinSymbols > 0) { - passwordRequirements.add(MIN_SYMBOLS_IN_PASSWORD); - } - if (mPasswordMinNonLetter > 0) { - if (mPasswordMinNonLetter > mPasswordMinNumeric + mPasswordMinSymbols) { - passwordRequirements.add(MIN_NON_LETTER_IN_PASSWORD); - } - } - // Convert list to array. - mPasswordRequirements = passwordRequirements.stream().mapToInt(i -> i).toArray(); mPasswordRestrictionView = view.findViewById(R.id.password_requirements_view); mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity())); mPasswordRequirementAdapter = new PasswordRequirementAdapter(); @@ -603,13 +592,12 @@ public class ChooseLockPassword extends SettingsActivity { /** * Read the requirements from {@link DevicePolicyManager} and intent and aggregate them. - * - * @param intent the incoming intent */ - private void processPasswordRequirements(Intent intent) { + private void loadDpmPasswordRequirements() { final int dpmPasswordQuality = mLockPatternUtils.getRequestedPasswordQuality(mUserId); - mRequestedQuality = Math.max(intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, - mRequestedQuality), dpmPasswordQuality); + if (dpmPasswordQuality == PASSWORD_QUALITY_NUMERIC_COMPLEX) { + mPasswordNumSequenceAllowed = false; + } mPasswordMinLength = Math.max(LockPatternUtils.MIN_LOCK_PASSWORD_SIZE, mLockPatternUtils.getRequestedMinimumPasswordLength(mUserId)); mPasswordMaxLength = mLockPatternUtils.getMaximumPasswordLength(mRequestedQuality); @@ -620,7 +608,7 @@ public class ChooseLockPassword extends SettingsActivity { mPasswordMinSymbols = mLockPatternUtils.getRequestedPasswordMinimumSymbols(mUserId); mPasswordMinNonLetter = mLockPatternUtils.getRequestedPasswordMinimumNonLetter(mUserId); - // Modify the value based on dpm policy. + // Modify the value based on dpm policy switch (dpmPasswordQuality) { case PASSWORD_QUALITY_ALPHABETIC: if (mPasswordMinLetters == 0) { @@ -646,19 +634,88 @@ public class ChooseLockPassword extends SettingsActivity { mPasswordMinSymbols = 0; mPasswordMinNonLetter = 0; } + mPasswordMinLengthToFulfillAllPolicies = getMinLengthToFulfillAllPolicies(); } + /** + * Merges the dpm requirements and the min complexity requirements. + * + *

Since there are more than one set of metrics to meet the min complexity requirement, + * and we are not hard-coding any one of them to be the requirements the user must fulfil, + * we are taking what the user has already entered into account when compiling the list of + * requirements from min complexity. Then we merge this list with the DPM requirements, and + * present the merged set as validation results to the user on the UI. + * + *

For example, suppose min complexity requires either ALPHABETIC(8+), or + * ALPHANUMERIC(6+). If the user has entered "a", the length requirement displayed on the UI + * would be 8. Then the user appends "1" to make it "a1". We now know the user is entering + * an alphanumeric password so we would update the min complexity required min length to 6. + * This might result in a little confusion for the user but the UI does not support showing + * multiple sets of requirements / validation results as options to users, this is the best + * we can do now. + */ + private void mergeMinComplexityAndDpmRequirements(int userEnteredPasswordQuality) { + if (mRequestedMinComplexity == PASSWORD_COMPLEXITY_NONE) { + // dpm requirements are dominant if min complexity is none + return; + } + + // reset dpm requirements + loadDpmPasswordRequirements(); + + PasswordMetrics minMetrics = PasswordMetrics.getMinimumMetrics( + mRequestedMinComplexity, userEnteredPasswordQuality, mRequestedQuality, + requiresNumeric(), requiresLettersOrSymbols()); + mPasswordNumSequenceAllowed = mPasswordNumSequenceAllowed + && minMetrics.quality != PASSWORD_QUALITY_NUMERIC_COMPLEX; + mPasswordMinLength = Math.max(mPasswordMinLength, minMetrics.length); + mPasswordMinLetters = Math.max(mPasswordMinLetters, minMetrics.letters); + mPasswordMinUpperCase = Math.max(mPasswordMinUpperCase, minMetrics.upperCase); + mPasswordMinLowerCase = Math.max(mPasswordMinLowerCase, minMetrics.lowerCase); + mPasswordMinNumeric = Math.max(mPasswordMinNumeric, minMetrics.numeric); + mPasswordMinSymbols = Math.max(mPasswordMinSymbols, minMetrics.symbols); + mPasswordMinNonLetter = Math.max(mPasswordMinNonLetter, minMetrics.nonLetter); + + if (minMetrics.quality == PASSWORD_QUALITY_ALPHABETIC) { + if (!requiresLettersOrSymbols()) { + mPasswordMinLetters = 1; + } + } + if (minMetrics.quality == PASSWORD_QUALITY_ALPHANUMERIC) { + if (!requiresLettersOrSymbols()) { + mPasswordMinLetters = 1; + } + if (!requiresNumeric()) { + mPasswordMinNumeric = 1; + } + } + + mPasswordMinLengthToFulfillAllPolicies = getMinLengthToFulfillAllPolicies(); + } + + private boolean requiresLettersOrSymbols() { + // This is the condition for the password to be considered ALPHABETIC according to + // PasswordMetrics.computeForPassword() + return mPasswordMinLetters + mPasswordMinUpperCase + + mPasswordMinLowerCase + mPasswordMinSymbols + mPasswordMinNonLetter > 0; + } + + private boolean requiresNumeric() { + return mPasswordMinNumeric > 0; + } + /** * Validates PIN/Password and returns the validation result. * * @param password the raw password the user typed in * @return the validation result. */ - private int validatePassword(String password) { + @VisibleForTesting + int validatePassword(String password) { int errorCode = NO_ERROR; final PasswordMetrics metrics = PasswordMetrics.computeForPassword(password); - + mergeMinComplexityAndDpmRequirements(metrics.quality); if (password.length() < mPasswordMinLength) { if (mPasswordMinLength > mPasswordMinLengthToFulfillAllPolicies) { @@ -668,14 +725,25 @@ public class ChooseLockPassword extends SettingsActivity { errorCode |= TOO_LONG; } else { // The length requirements are fulfilled. - final int dpmQuality = mLockPatternUtils.getRequestedPasswordQuality(mUserId); - if (dpmQuality == PASSWORD_QUALITY_NUMERIC_COMPLEX && - metrics.numeric == password.length()) { + if (!mPasswordNumSequenceAllowed + && !requiresLettersOrSymbols() + && metrics.numeric == password.length()) { // Check for repeated characters or sequences (e.g. '1234', '0000', '2468') - // if DevicePolicyManager requires a complex numeric password. There can be - // two cases in the UI: 1. User chooses to enroll a PIN, 2. User chooses to - // enroll a password but enters a numeric-only pin. We should carry out the - // sequence check in both cases. + // if DevicePolicyManager or min password complexity requires a complex numeric + // password. There can be two cases in the UI: 1. User chooses to enroll a + // PIN, 2. User chooses to enroll a password but enters a numeric-only pin. We + // should carry out the sequence check in both cases. + // + // Conditions for the !requiresLettersOrSymbols() to be necessary: + // - DPM requires NUMERIC_COMPLEX + // - min complexity not NONE, user picks PASSWORD type so ALPHABETIC or + // ALPHANUMERIC is required + // Imagine user has entered "12345678", if we don't skip the sequence check, the + // validation result would show both "requires a letter" and "sequence not + // allowed", while the only requirement the user needs to know is "requires a + // letter" because once the user has fulfilled the alphabetic requirement, the + // password would not be containing only digits so this check would not be + // performed anyway. final int sequence = PasswordMetrics.maxLengthSequence(password); if (sequence > PasswordMetrics.MAX_ALLOWED_SEQUENCE) { errorCode |= CONTAIN_SEQUENTIAL_DIGITS; @@ -706,43 +774,24 @@ public class ChooseLockPassword extends SettingsActivity { } } - // Check the requirements one by one. - for (int i = 0; i < mPasswordRequirements.length; i++) { - int passwordRestriction = mPasswordRequirements[i]; - switch (passwordRestriction) { - case MIN_LETTER_IN_PASSWORD: - if (metrics.letters < mPasswordMinLetters) { - errorCode |= NOT_ENOUGH_LETTER; - } - break; - case MIN_UPPER_LETTERS_IN_PASSWORD: - if (metrics.upperCase < mPasswordMinUpperCase) { - errorCode |= NOT_ENOUGH_UPPER_CASE; - } - break; - case MIN_LOWER_LETTERS_IN_PASSWORD: - if (metrics.lowerCase < mPasswordMinLowerCase) { - errorCode |= NOT_ENOUGH_LOWER_CASE; - } - break; - case MIN_SYMBOLS_IN_PASSWORD: - if (metrics.symbols < mPasswordMinSymbols) { - errorCode |= NOT_ENOUGH_SYMBOLS; - } - break; - case MIN_NUMBER_IN_PASSWORD: - if (metrics.numeric < mPasswordMinNumeric) { - errorCode |= NOT_ENOUGH_DIGITS; - } - break; - case MIN_NON_LETTER_IN_PASSWORD: - if (metrics.nonLetter < mPasswordMinNonLetter) { - errorCode |= NOT_ENOUGH_NON_LETTER; - } - break; - } + if (metrics.letters < mPasswordMinLetters) { + errorCode |= NOT_ENOUGH_LETTER; + } + if (metrics.upperCase < mPasswordMinUpperCase) { + errorCode |= NOT_ENOUGH_UPPER_CASE; + } + if (metrics.lowerCase < mPasswordMinLowerCase) { + errorCode |= NOT_ENOUGH_LOWER_CASE; + } + if (metrics.symbols < mPasswordMinSymbols) { + errorCode |= NOT_ENOUGH_SYMBOLS; + } + if (metrics.numeric < mPasswordMinNumeric) { + errorCode |= NOT_ENOUGH_DIGITS; + } + if (metrics.nonLetter < mPasswordMinNonLetter) { + errorCode |= NOT_ENOUGH_NON_LETTER; } - return errorCode; } diff --git a/src/com/android/settings/password/ChooseLockSettingsHelper.java b/src/com/android/settings/password/ChooseLockSettingsHelper.java index 8d0fa602d66..32e8eafa2f6 100644 --- a/src/com/android/settings/password/ChooseLockSettingsHelper.java +++ b/src/com/android/settings/password/ChooseLockSettingsHelper.java @@ -45,6 +45,18 @@ public final class ChooseLockSettingsHelper { public static final String EXTRA_KEY_FOR_FACE = "for_face"; public static final String EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT = "for_cred_req_boot"; + /** + * Intent extra for passing the requested min password complexity to later steps in the set new + * screen lock flow. + */ + public static final String EXTRA_KEY_REQUESTED_MIN_COMPLEXITY = "requested_min_complexity"; + + /** + * Intent extra for passing the label of the calling app to later steps in the set new screen + * lock flow. + */ + public static final String EXTRA_KEY_CALLER_APP_NAME = "caller_app_name"; + /** * When invoked via {@link ConfirmLockPassword.InternalActivity}, this flag * controls if we relax the enforcement of diff --git a/src/com/android/settings/password/PasswordUtils.java b/src/com/android/settings/password/PasswordUtils.java new file mode 100644 index 00000000000..5f118cf0e6c --- /dev/null +++ b/src/com/android/settings/password/PasswordUtils.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2019 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.annotation.Nullable; +import android.app.ActivityManager; +import android.app.IActivityManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; + +import com.android.settings.Utils; + +public final class PasswordUtils extends com.android.settingslib.Utils { + + private static final String TAG = "Settings"; + + private static final String SETTINGS_PACKAGE_NAME = "com.android.settings"; + + /** + * Returns whether the uid which the activity with {@code activityToken} is launched from has + * been granted the {@code permission}. + */ + public static boolean isCallingAppPermitted(Context context, IBinder activityToken, + String permission) { + try { + return context.checkPermission(permission, /* pid= */ -1, + ActivityManager.getService().getLaunchedFromUid(activityToken)) + == PackageManager.PERMISSION_GRANTED; + } catch (RemoteException e) { + Log.v(TAG, "Could not talk to activity manager.", e); + return false; + } + } + + /** + * Returns the label of the package which the activity with {@code activityToken} is launched + * from or {@code null} if it is launched from the settings app itself. + */ + @Nullable + public static CharSequence getCallingAppLabel(Context context, IBinder activityToken) { + String pkg = getCallingAppPackageName(activityToken); + if (pkg == null || pkg.equals(SETTINGS_PACKAGE_NAME)) { + return null; + } + + return Utils.getApplicationLabel(context, pkg); + } + + /** + * Returns the package name which the activity with {@code activityToken} is launched from. + */ + @Nullable + private static String getCallingAppPackageName(IBinder activityToken) { + String pkg = null; + try { + pkg = ActivityManager.getService().getLaunchedFromPackage(activityToken); + } catch (RemoteException e) { + Log.v(TAG, "Could not talk to activity manager.", e); + } + return pkg; + } + + /** Crashes the calling application and provides it with {@code message}. */ + public static void crashCallingApplication(IBinder activityToken, String message) { + IActivityManager am = ActivityManager.getService(); + try { + int uid = am.getLaunchedFromUid(activityToken); + int userId = UserHandle.getUserId(uid); + am.crashApplication( + uid, + /* initialPid= */ -1, + getCallingAppPackageName(activityToken), + userId, + message); + } catch (RemoteException e) { + Log.v(TAG, "Could not talk to activity manager.", e); + } + } +} diff --git a/src/com/android/settings/password/SetNewPasswordActivity.java b/src/com/android/settings/password/SetNewPasswordActivity.java index 99f67cb13d5..8ea85144ea7 100644 --- a/src/com/android/settings/password/SetNewPasswordActivity.java +++ b/src/com/android/settings/password/SetNewPasswordActivity.java @@ -16,13 +16,22 @@ package com.android.settings.password; +import static android.Manifest.permission.GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PARENT_PROFILE_PASSWORD; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD; +import static android.app.admin.DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; + +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CALLER_APP_NAME; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; import android.app.Activity; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; +import android.app.admin.PasswordMetrics; import android.content.Intent; import android.os.Bundle; +import android.os.IBinder; import android.util.Log; import com.android.settings.Utils; @@ -37,6 +46,21 @@ public class SetNewPasswordActivity extends Activity implements SetNewPasswordCo private String mNewPasswordAction; private SetNewPasswordController mSetNewPasswordController; + /** + * From intent extra {@link DevicePolicyManager#EXTRA_PASSWORD_COMPLEXITY}. + * + *

This is used only if caller has the required permission and activity is launched by + * {@link DevicePolicyManager#ACTION_SET_NEW_PASSWORD}. + */ + private @PasswordComplexity int mRequestedMinComplexity = PASSWORD_COMPLEXITY_NONE; + + /** + * Label of the app which launches this activity. + * + *

Value would be {@code null} if launched from settings app. + */ + private String mCallerAppName = null; + @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); @@ -48,6 +72,25 @@ public class SetNewPasswordActivity extends Activity implements SetNewPasswordCo finish(); return; } + + IBinder activityToken = getActivityToken(); + mCallerAppName = (String) PasswordUtils.getCallingAppLabel(this, activityToken); + if (ACTION_SET_NEW_PASSWORD.equals(mNewPasswordAction) + && getIntent().hasExtra(EXTRA_PASSWORD_COMPLEXITY)) { + boolean hasPermission = PasswordUtils.isCallingAppPermitted( + this, activityToken, GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY); + if (hasPermission) { + mRequestedMinComplexity = PasswordMetrics.sanitizeComplexityLevel(getIntent() + .getIntExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_NONE)); + } else { + PasswordUtils.crashCallingApplication(activityToken, + "Must have permission " + GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY + + " to use extra " + EXTRA_PASSWORD_COMPLEXITY); + finish(); + return; + } + } + mSetNewPasswordController = SetNewPasswordController.create( this, this, getIntent(), getActivityToken()); mSetNewPasswordController.dispatchSetNewPasswordIntent(); @@ -60,6 +103,12 @@ public class SetNewPasswordActivity extends Activity implements SetNewPasswordCo : new Intent(this, ChooseLockGeneric.class); intent.setAction(mNewPasswordAction); intent.putExtras(chooseLockFingerprintExtras); + if (mCallerAppName != null) { + intent.putExtra(EXTRA_KEY_CALLER_APP_NAME, mCallerAppName); + } + if (mRequestedMinComplexity != PASSWORD_COMPLEXITY_NONE) { + intent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, mRequestedMinComplexity); + } startActivity(intent); finish(); } diff --git a/src/com/android/settings/password/SetupChooseLockGeneric.java b/src/com/android/settings/password/SetupChooseLockGeneric.java index a0f8bae0502..33c3edb9d38 100644 --- a/src/com/android/settings/password/SetupChooseLockGeneric.java +++ b/src/com/android/settings/password/SetupChooseLockGeneric.java @@ -16,11 +16,17 @@ package com.android.settings.password; +import static android.Manifest.permission.GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY; +import static android.app.admin.DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY; + +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; + import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; +import android.os.IBinder; import android.os.UserHandle; import android.view.LayoutInflater; import android.view.View; @@ -48,6 +54,7 @@ import com.google.android.setupdesign.GlifPreferenceLayout; * Other changes should be done to ChooseLockGeneric class instead and let this class inherit * those changes. */ +// TODO(b/123225425): Restrict SetupChooseLockGeneric to be accessible by SUW only public class SetupChooseLockGeneric extends ChooseLockGeneric { private static final String KEY_UNLOCK_SET_DO_LATER = "unlock_set_do_later"; @@ -71,6 +78,20 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { @Override protected void onCreate(Bundle savedInstance) { super.onCreate(savedInstance); + + if(getIntent().hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)) { + IBinder activityToken = getActivityToken(); + boolean hasPermission = PasswordUtils.isCallingAppPermitted( + this, activityToken, GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY); + if (!hasPermission) { + PasswordUtils.crashCallingApplication(activityToken, + "Must have permission " + GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY + + " to use extra " + EXTRA_PASSWORD_COMPLEXITY); + finish(); + return; + } + } + LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent); layout.setFitsSystemWindows(false); } diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockGenericControllerTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockGenericControllerTest.java index cbc5765ccc5..2b7bdeb6948 100644 --- a/tests/robotests/src/com/android/settings/password/ChooseLockGenericControllerTest.java +++ b/tests/robotests/src/com/android/settings/password/ChooseLockGenericControllerTest.java @@ -16,15 +16,22 @@ package com.android.settings.password; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; import static org.robolectric.RuntimeEnvironment.application; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; import android.content.ComponentName; import com.android.settings.R; @@ -58,11 +65,7 @@ public class ChooseLockGenericControllerTest { public void setUp() { MockitoAnnotations.initMocks(this); - mController = new ChooseLockGenericController( - application, - 0 /* userId */, - mDevicePolicyManager, - mManagedLockPasswordProvider); + mController = createController(PASSWORD_COMPLEXITY_NONE); SettingsShadowResources.overrideResource(R.bool.config_hide_none_security_option, false); SettingsShadowResources.overrideResource(R.bool.config_hide_swipe_security_option, false); } @@ -225,4 +228,44 @@ public class ChooseLockGenericControllerTest { assertThat(upgradedQuality).named("upgradedQuality") .isEqualTo(DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC); } + + @Test + public void upgradeQuality_complexityHigh_minQualityNumericComplex() { + when(mDevicePolicyManager.getPasswordQuality(nullable(ComponentName.class), anyInt())) + .thenReturn(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED); + ChooseLockGenericController controller = createController(PASSWORD_COMPLEXITY_HIGH); + + assertThat(controller.upgradeQuality(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED)) + .isEqualTo(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX); + } + + @Test + public void upgradeQuality_complexityMedium_minQualityNumericComplex() { + when(mDevicePolicyManager.getPasswordQuality(nullable(ComponentName.class), anyInt())) + .thenReturn(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED); + ChooseLockGenericController controller = createController(PASSWORD_COMPLEXITY_MEDIUM); + + assertThat(controller.upgradeQuality(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED)) + .isEqualTo(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX); + } + + @Test + public void upgradeQuality_complexityLow_minQualitySomething() { + when(mDevicePolicyManager.getPasswordQuality(nullable(ComponentName.class), anyInt())) + .thenReturn(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED); + ChooseLockGenericController controller = createController(PASSWORD_COMPLEXITY_LOW); + + assertThat(controller.upgradeQuality(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED)) + .isEqualTo(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); + } + + private ChooseLockGenericController createController( + @PasswordComplexity int minPasswordComplexity) { + return new ChooseLockGenericController( + application, + 0 /* userId */, + minPasswordComplexity, + mDevicePolicyManager, + mManagedLockPasswordProvider); + } } diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java index e3242148ee4..a1db12ccf24 100644 --- a/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java +++ b/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java @@ -16,6 +16,15 @@ package com.android.settings.password; +import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; + +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CALLER_APP_NAME; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; + import static com.google.common.truth.Truth.assertThat; import static org.robolectric.RuntimeEnvironment.application; @@ -27,8 +36,10 @@ import android.content.Intent; import android.provider.Settings.Global; import androidx.annotation.Nullable; +import androidx.preference.Preference; import com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; import com.android.settings.biometrics.BiometricEnrollBase; import com.android.settings.password.ChooseLockGeneric.ChooseLockGenericFragment; import com.android.settings.search.SearchFeatureProvider; @@ -36,6 +47,7 @@ import com.android.settings.testutils.shadow.ShadowLockPatternUtils; import com.android.settings.testutils.shadow.ShadowStorageManager; import com.android.settings.testutils.shadow.ShadowUserManager; import com.android.settings.testutils.shadow.ShadowUtils; +import com.android.settingslib.widget.FooterPreference; import org.junit.After; import org.junit.Before; @@ -112,6 +124,70 @@ public class ChooseLockGenericTest { assertThat(shadowOf(mActivity).getNextStartedActivity()).isNull(); } + @Test + public void updatePreferencesOrFinish_footerPreferenceAddedHighComplexityText() { + ShadowStorageManager.setIsFileEncryptedNativeOrEmulated(false); + Intent intent = new Intent() + .putExtra(EXTRA_KEY_CALLER_APP_NAME, "app name") + .putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_HIGH); + initActivity(intent); + CharSequence expectedTitle = + mActivity.getString(R.string.unlock_footer_high_complexity_requested, "app name"); + + mFragment.updatePreferencesOrFinish(false /* isRecreatingActivity */); + FooterPreference footer = mFragment.findPreference(FooterPreference.KEY_FOOTER); + + assertThat(footer.getTitle()).isEqualTo(expectedTitle); + } + + @Test + public void updatePreferencesOrFinish_footerPreferenceAddedMediumComplexityText() { + ShadowStorageManager.setIsFileEncryptedNativeOrEmulated(false); + Intent intent = new Intent() + .putExtra(EXTRA_KEY_CALLER_APP_NAME, "app name") + .putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_MEDIUM); + initActivity(intent); + CharSequence expectedTitle = + mActivity.getString(R.string.unlock_footer_medium_complexity_requested, "app name"); + + mFragment.updatePreferencesOrFinish(false /* isRecreatingActivity */); + FooterPreference footer = mFragment.findPreference(FooterPreference.KEY_FOOTER); + + assertThat(footer.getTitle()).isEqualTo(expectedTitle); + } + + @Test + public void updatePreferencesOrFinish_footerPreferenceAddedLowComplexityText() { + ShadowStorageManager.setIsFileEncryptedNativeOrEmulated(false); + Intent intent = new Intent() + .putExtra(EXTRA_KEY_CALLER_APP_NAME, "app name") + .putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_LOW); + initActivity(intent); + CharSequence expectedTitle = + mActivity.getString(R.string.unlock_footer_low_complexity_requested, "app name"); + + mFragment.updatePreferencesOrFinish(false /* isRecreatingActivity */); + FooterPreference footer = mFragment.findPreference(FooterPreference.KEY_FOOTER); + + assertThat(footer.getTitle()).isEqualTo(expectedTitle); + } + + @Test + public void updatePreferencesOrFinish_footerPreferenceAddedNoneComplexityText() { + ShadowStorageManager.setIsFileEncryptedNativeOrEmulated(false); + Intent intent = new Intent() + .putExtra(EXTRA_KEY_CALLER_APP_NAME, "app name") + .putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE); + initActivity(intent); + CharSequence expectedTitle = + mActivity.getString(R.string.unlock_footer_none_complexity_requested, "app name"); + + mFragment.updatePreferencesOrFinish(false /* isRecreatingActivity */); + FooterPreference footer = mFragment.findPreference(FooterPreference.KEY_FOOTER); + + assertThat(footer.getTitle()).isEqualTo(expectedTitle); + } + @Test public void onActivityResult_requestcode0_shouldNotFinish() { initActivity(null); @@ -165,6 +241,48 @@ public class ChooseLockGenericTest { assertThat(mActivity.isFinishing()).isTrue(); } + @Test + public void onPreferenceTreeClick_fingerprintPassesMinComplexityInfoOntoNextActivity() { + Intent intent = new Intent(ACTION_SET_NEW_PASSWORD) + .putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_HIGH) + .putExtra(EXTRA_KEY_CALLER_APP_NAME, "app name"); + initActivity(intent); + + Preference fingerprintPref = new Preference(application); + fingerprintPref.setKey("unlock_skip_fingerprint"); + boolean result = mFragment.onPreferenceTreeClick(fingerprintPref); + + assertThat(result).isTrue(); + Intent actualIntent = shadowOf(mActivity).getNextStartedActivityForResult().intent; + assertThat(actualIntent.hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)).isTrue(); + assertThat(actualIntent.getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE)) + .isEqualTo(PASSWORD_COMPLEXITY_HIGH); + assertThat(actualIntent.hasExtra(EXTRA_KEY_CALLER_APP_NAME)).isTrue(); + assertThat(actualIntent.getStringExtra(EXTRA_KEY_CALLER_APP_NAME)) + .isEqualTo("app name"); + } + + @Test + public void onPreferenceTreeClick_facePassesMinComplexityInfoOntoNextActivity() { + Intent intent = new Intent(ACTION_SET_NEW_PASSWORD) + .putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_HIGH) + .putExtra(EXTRA_KEY_CALLER_APP_NAME, "app name"); + initActivity(intent); + + Preference facePref = new Preference(application); + facePref.setKey("unlock_skip_face"); + boolean result = mFragment.onPreferenceTreeClick(facePref); + + assertThat(result).isTrue(); + Intent actualIntent = shadowOf(mActivity).getNextStartedActivityForResult().intent; + assertThat(actualIntent.hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)).isTrue(); + assertThat(actualIntent.getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE)) + .isEqualTo(PASSWORD_COMPLEXITY_HIGH); + assertThat(actualIntent.hasExtra(EXTRA_KEY_CALLER_APP_NAME)).isTrue(); + assertThat(actualIntent.getStringExtra(EXTRA_KEY_CALLER_APP_NAME)) + .isEqualTo("app name"); + } + private void initActivity(@Nullable Intent intent) { if (intent == null) { intent = new Intent(); diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java index 367cb4c36b2..404d2057aa4 100644 --- a/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java +++ b/tests/robotests/src/com/android/settings/password/ChooseLockPasswordTest.java @@ -16,19 +16,37 @@ package com.android.settings.password; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; + +import static com.android.internal.widget.LockPatternUtils.PASSWORD_TYPE_KEY; +import static com.android.settings.password.ChooseLockGeneric.CONFIRM_CREDENTIALS; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; + import static com.google.common.truth.Truth.assertThat; import static org.robolectric.RuntimeEnvironment.application; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; import android.content.Intent; import android.os.UserHandle; -import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.password.ChooseLockPassword.ChooseLockPasswordFragment; import com.android.settings.password.ChooseLockPassword.IntentBuilder; import com.android.settings.testutils.shadow.SettingsShadowResources; +import com.android.settings.testutils.shadow.ShadowDevicePolicyManager; +import com.android.settings.testutils.shadow.ShadowLockPatternUtils; import com.android.settings.testutils.shadow.ShadowUtils; import com.google.android.setupdesign.GlifLayout; @@ -44,13 +62,21 @@ import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowDrawable; @RunWith(RobolectricTestRunner.class) -@Config(shadows = {SettingsShadowResources.class, ShadowUtils.class}) +@Config(shadows = { + SettingsShadowResources.class, + ShadowUtils.class, + ShadowDevicePolicyManager.class, +}) public class ChooseLockPasswordTest { + private ShadowDevicePolicyManager mShadowDpm; + @Before public void setUp() { SettingsShadowResources.overrideResource( com.android.internal.R.string.config_headlineFontFamily, ""); + mShadowDpm = ShadowDevicePolicyManager.getShadow(); + mShadowDpm.setPasswordMaximumLength(16); } @After @@ -72,7 +98,7 @@ public class ChooseLockPasswordTest { assertThat(intent.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD)) .named("EXTRA_KEY_PASSWORD") .isEqualTo("password"); - assertThat(intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, 0)) + assertThat(intent.getIntExtra(PASSWORD_TYPE_KEY, 0)) .named("PASSWORD_TYPE_KEY") .isEqualTo(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC); assertThat(intent.getIntExtra(Intent.EXTRA_USER_ID, 0)) @@ -84,7 +110,7 @@ public class ChooseLockPasswordTest { public void intentBuilder_setChallenge_shouldAddExtras() { Intent intent = new IntentBuilder(application) .setChallenge(12345L) - .setPasswordQuality(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC) + .setPasswordQuality(PASSWORD_QUALITY_ALPHANUMERIC) .setUserId(123) .build(); @@ -94,14 +120,213 @@ public class ChooseLockPasswordTest { assertThat(intent.getLongExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0L)) .named("EXTRA_KEY_CHALLENGE") .isEqualTo(12345L); - assertThat(intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, 0)) + assertThat(intent.getIntExtra(PASSWORD_TYPE_KEY, 0)) .named("PASSWORD_TYPE_KEY") - .isEqualTo(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC); + .isEqualTo(PASSWORD_QUALITY_ALPHANUMERIC); assertThat(intent.getIntExtra(Intent.EXTRA_USER_ID, 0)) .named("EXTRA_USER_ID") .isEqualTo(123); } + @Test + public void intentBuilder_setMinComplexityMedium_hasMinComplexityExtraMedium() { + Intent intent = new IntentBuilder(application) + .setRequestedMinComplexity(PASSWORD_COMPLEXITY_MEDIUM) + .build(); + + assertThat(intent.hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)).isTrue(); + assertThat(intent.getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE)) + .isEqualTo(PASSWORD_COMPLEXITY_MEDIUM); + } + + @Test + public void intentBuilder_setMinComplexityNotCalled() { + Intent intent = new IntentBuilder(application).build(); + + assertThat(intent.hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)).isFalse(); + } + + @Test + public void processAndValidatePasswordRequirements_noMinPasswordComplexity() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_ALPHABETIC); + mShadowDpm.setPasswordMinimumLength(10); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_NONE, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "", + "Must contain at least 1 letter", + "Must be at least 10 characters"); + } + + @Test + public void processAndValidatePasswordRequirements_minPasswordComplexityStricter_pin() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_SOMETHING); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_HIGH, + /* passwordType= */ PASSWORD_QUALITY_NUMERIC, + /* userEnteredPassword= */ "", + "PIN must be at least 8 digits"); + } + + @Test + public void processAndValidatePasswordRequirements_minPasswordComplexityStricter_password() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_SOMETHING); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_MEDIUM, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "", + "Must contain at least 1 letter", + "Must be at least 4 characters"); + } + + @Test + public void processAndValidatePasswordRequirements_dpmRestrictionsStricter_password() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_ALPHANUMERIC); + mShadowDpm.setPasswordMinimumLength(9); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_LOW, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "", + "Must contain at least 1 letter", + "Must contain at least 1 numerical digit", + "Must be at least 9 characters"); + } + + @Test + public void processAndValidatePasswordRequirements_dpmLengthLonger_pin() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_NUMERIC); + mShadowDpm.setPasswordMinimumLength(11); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_MEDIUM, + /* passwordType= */ PASSWORD_QUALITY_NUMERIC, + /* userEnteredPassword= */ "", + "PIN must be at least 11 digits"); + } + + @Test + public void processAndValidatePasswordRequirements_dpmQualityComplex() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_COMPLEX); + mShadowDpm.setPasswordMinimumSymbols(2); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_HIGH, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "", + "Must contain at least 2 special symbols", + "Must be at least 6 characters"); + } + + @Test + @Config(shadows = ShadowLockPatternUtils.class) + public void processAndValidatePasswordRequirements_numericComplexNoMinComplexity_pinRequested() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_NUMERIC_COMPLEX); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_NONE, + /* passwordType= */ PASSWORD_QUALITY_NUMERIC, + /* userEnteredPassword= */ "12345678", + "Ascending, descending, or repeated sequence of digits isn't allowed"); + } + + @Test + @Config(shadows = ShadowLockPatternUtils.class) + public void processAndValidatePasswordRequirements_numericComplexNoMinComplexity_passwordRequested() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_NUMERIC_COMPLEX); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_NONE, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "12345678", + "Ascending, descending, or repeated sequence of digits isn't allowed"); + } + + @Test + @Config(shadows = ShadowLockPatternUtils.class) + public void processAndValidatePasswordRequirements_numericComplexHighComplexity_pinRequested() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_NUMERIC_COMPLEX); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_HIGH, + /* passwordType= */ PASSWORD_QUALITY_NUMERIC, + /* userEnteredPassword= */ "12345678", + "Ascending, descending, or repeated sequence of digits isn't allowed"); + } + + @Test + @Config(shadows = ShadowLockPatternUtils.class) + public void processAndValidatePasswordRequirements_numericHighComplexity_pinRequested() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_NUMERIC); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_HIGH, + /* passwordType= */ PASSWORD_QUALITY_NUMERIC, + /* userEnteredPassword= */ "12345678", + "Ascending, descending, or repeated sequence of digits isn't allowed"); + } + + @Test + @Config(shadows = ShadowLockPatternUtils.class) + public void processAndValidatePasswordRequirements_numericComplexLowComplexity_passwordRequested() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_NUMERIC_COMPLEX); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_LOW, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "12345678", + "Must contain at least 1 letter"); + } + + @Test + public void processAndValidatePasswordRequirements_requirementsUpdateAccordingToMinComplexityAndUserInput_empty() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_UNSPECIFIED); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_HIGH, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "", + "Must contain at least 1 letter", + "Must be at least 6 characters"); + } + + @Test + public void processAndValidatePasswordRequirements_requirementsUpdateAccordingToMinComplexityAndUserInput_numeric() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_UNSPECIFIED); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_HIGH, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "1", + "Must contain at least 1 letter", + "Must be at least 6 characters"); + } + + @Test + public void processAndValidatePasswordRequirements_requirementsUpdateAccordingToMinComplexityAndUserInput_alphabetic() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_UNSPECIFIED); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_HIGH, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "b", + "Must be at least 6 characters"); + } + + @Test + public void processAndValidatePasswordRequirements_requirementsUpdateAccordingToMinComplexityAndUserInput_alphanumeric() { + mShadowDpm.setPasswordQuality(PASSWORD_QUALITY_UNSPECIFIED); + + assertPasswordValidationResult( + /* minComplexity= */ PASSWORD_COMPLEXITY_HIGH, + /* passwordType= */ PASSWORD_QUALITY_ALPHABETIC, + /* userEnteredPassword= */ "b1", + "Must be at least 6 characters"); + } + @Test public void assertThat_chooseLockIconChanged_WhenFingerprintExtraSet() { ShadowDrawable drawable = setActivityAndGetIconDrawable(true); @@ -132,4 +357,18 @@ public class ChooseLockPasswordTest { ChooseLockPasswordFragment fragment = getChooseLockPasswordFragment(passwordActivity); return Shadows.shadowOf(((GlifLayout) fragment.getView()).getIcon()); } + + private void assertPasswordValidationResult(@PasswordComplexity int minComplexity, + int passwordType, String userEnteredPassword, String... expectedValidationResult) { + Intent intent = new Intent(); + intent.putExtra(CONFIRM_CREDENTIALS, false); + intent.putExtra(PASSWORD_TYPE_KEY, passwordType); + intent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, minComplexity); + ChooseLockPassword activity = buildChooseLockPasswordActivity(intent); + ChooseLockPasswordFragment fragment = getChooseLockPasswordFragment(activity); + int validateResult = fragment.validatePassword(userEnteredPassword); + String[] messages = fragment.convertErrorCodeToMessages(validateResult); + + assertThat(messages).asList().containsExactly((Object[]) expectedValidationResult); + } } diff --git a/tests/robotests/src/com/android/settings/password/PasswordUtilsTest.java b/tests/robotests/src/com/android/settings/password/PasswordUtilsTest.java new file mode 100644 index 00000000000..845d346da8f --- /dev/null +++ b/tests/robotests/src/com/android/settings/password/PasswordUtilsTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2019 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 android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import static com.android.settings.password.PasswordUtils.getCallingAppLabel; +import static com.android.settings.password.PasswordUtils.isCallingAppPermitted; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.app.IActivityManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.IBinder; +import android.os.RemoteException; + +import com.android.settings.testutils.shadow.ShadowActivityManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowActivityManager.class}) +public class PasswordUtilsTest { + + private static final String PACKAGE_NAME = "com.android.app"; + private static final String PERMISSION = "com.testing.permission"; + private static final int UID = 1234; + + @Mock + private PackageManager mPackageManager; + @Mock + private ApplicationInfo mApplicationInfo; + @Mock + private IActivityManager mActivityService; + @Mock + private IBinder mActivityToken; + + private Context mContext; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(RuntimeEnvironment.application); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + ShadowActivityManager.setService(mActivityService); + } + + @Test + public void getCallingAppLabel_activityServiceThrowsRemoteException_returnsNull() + throws Exception { + when(mActivityService.getLaunchedFromPackage(mActivityToken)) + .thenThrow(new RemoteException()); + + assertThat(getCallingAppLabel(mContext, mActivityToken)).isNull(); + } + + @Test + public void getCallingAppLabel_activityServiceReturnsSettingsApp_returnsNull() + throws Exception { + when(mActivityService.getLaunchedFromPackage(mActivityToken)) + .thenReturn("com.android.settings"); + + assertThat(getCallingAppLabel(mContext, mActivityToken)).isNull(); + } + + @Test + public void getCallingAppLabel_packageManagerThrowsNameNotFound_returnsNull() throws Exception { + when(mActivityService.getLaunchedFromPackage(mActivityToken)) + .thenReturn(PACKAGE_NAME); + when(mPackageManager.getApplicationInfo(eq(PACKAGE_NAME), anyInt())) + .thenThrow(new NameNotFoundException()); + + assertThat(getCallingAppLabel(mContext, mActivityToken)).isNull(); + } + + @Test + public void getCallingAppLabel_returnsLabel() throws Exception { + when(mActivityService.getLaunchedFromPackage(mActivityToken)) + .thenReturn(PACKAGE_NAME); + when(mPackageManager.getApplicationInfo(eq(PACKAGE_NAME), anyInt())) + .thenReturn(mApplicationInfo); + when(mApplicationInfo.loadLabel(mPackageManager)).thenReturn("label"); + + assertThat(getCallingAppLabel(mContext, mActivityToken)).isEqualTo("label"); + } + + @Test + public void isCallingAppPermitted_permissionGranted_returnsTrue() throws Exception { + when(mActivityService.getLaunchedFromUid(mActivityToken)).thenReturn(UID); + when(mContext.checkPermission(PERMISSION, -1, UID)).thenReturn(PERMISSION_GRANTED); + + assertThat(isCallingAppPermitted(mContext, mActivityToken, PERMISSION)).isTrue(); + } + + @Test + public void isCallingAppPermitted_permissionDenied_returnsFalse() throws Exception { + when(mActivityService.getLaunchedFromUid(mActivityToken)).thenReturn(UID); + when(mContext.checkPermission(PERMISSION, -1, UID)).thenReturn(PERMISSION_DENIED); + + assertThat(isCallingAppPermitted(mContext, mActivityToken, PERMISSION)).isFalse(); + } + + @Test + public void isCallingAppPermitted_throwsRemoteException_returnsFalse() throws Exception { + when(mActivityService.getLaunchedFromUid(mActivityToken)).thenThrow(new RemoteException()); + + assertThat(isCallingAppPermitted(mContext, mActivityToken, PERMISSION)).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/password/SetNewPasswordActivityTest.java b/tests/robotests/src/com/android/settings/password/SetNewPasswordActivityTest.java index 99738e75c33..d1b2b74b3c3 100644 --- a/tests/robotests/src/com/android/settings/password/SetNewPasswordActivityTest.java +++ b/tests/robotests/src/com/android/settings/password/SetNewPasswordActivityTest.java @@ -16,6 +16,16 @@ package com.android.settings.password; +import static android.Manifest.permission.GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY; +import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PARENT_PROFILE_PASSWORD; +import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD; +import static android.app.admin.DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; + +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CALLER_APP_NAME; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; + import static com.google.common.truth.Truth.assertThat; import android.content.ComponentName; @@ -23,6 +33,8 @@ import android.content.Intent; import android.os.Bundle; import android.provider.Settings; +import com.android.settings.testutils.shadow.ShadowPasswordUtils; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -31,11 +43,14 @@ 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.ShadowActivity; @RunWith(RobolectricTestRunner.class) public class SetNewPasswordActivityTest { + private static final String APP_LABEL = "label"; + private int mProvisioned; @Before @@ -48,6 +63,7 @@ public class SetNewPasswordActivityTest { public void tearDown() { Settings.Global.putInt(RuntimeEnvironment.application.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, mProvisioned); + ShadowPasswordUtils.reset(); } @Test @@ -77,4 +93,106 @@ public class SetNewPasswordActivityTest { assertThat(intent.getComponent()) .isEqualTo(new ComponentName(activity, SetupChooseLockGeneric.class)); } + + @Test + @Config(shadows = {ShadowPasswordUtils.class}) + public void testLaunchChooseLock_setNewPasswordExtraWithoutPermission() { + ShadowPasswordUtils.setCallingAppLabel(APP_LABEL); + Settings.Global.putInt(RuntimeEnvironment.application.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 1); + + Intent intent = new Intent(ACTION_SET_NEW_PASSWORD); + intent.putExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_HIGH); + SetNewPasswordActivity activity = + Robolectric.buildActivity(SetNewPasswordActivity.class, intent).create().get(); + + ShadowActivity shadowActivity = Shadows.shadowOf(activity); + assertThat(shadowActivity.getNextStartedActivityForResult()).isNull(); + } + + @Test + @Config(shadows = {ShadowPasswordUtils.class}) + public void testLaunchChooseLock_setNewPasswordExtraWithPermission() { + ShadowPasswordUtils.setCallingAppLabel(APP_LABEL); + ShadowPasswordUtils.addGrantedPermission(GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY); + Settings.Global.putInt(RuntimeEnvironment.application.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 1); + + Intent intent = new Intent(ACTION_SET_NEW_PASSWORD); + intent.putExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_HIGH); + SetNewPasswordActivity activity = + Robolectric.buildActivity(SetNewPasswordActivity.class, intent).create().get(); + + ShadowActivity shadowActivity = Shadows.shadowOf(activity); + Intent actualIntent = shadowActivity.getNextStartedActivityForResult().intent; + assertThat(actualIntent.getAction()).isEqualTo(ACTION_SET_NEW_PASSWORD); + assertThat(actualIntent.hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)).isTrue(); + assertThat(actualIntent.getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE)) + .isEqualTo(PASSWORD_COMPLEXITY_HIGH); + assertThat(actualIntent.hasExtra(EXTRA_KEY_CALLER_APP_NAME)).isTrue(); + assertThat(actualIntent.getStringExtra(EXTRA_KEY_CALLER_APP_NAME)).isEqualTo(APP_LABEL); + } + + @Test + @Config(shadows = {ShadowPasswordUtils.class}) + public void testLaunchChooseLock_setNewPasswordExtraInvalidValue() { + ShadowPasswordUtils.setCallingAppLabel(APP_LABEL); + ShadowPasswordUtils.addGrantedPermission(GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY); + Settings.Global.putInt(RuntimeEnvironment.application.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 1); + + Intent intent = new Intent(ACTION_SET_NEW_PASSWORD); + intent.putExtra(EXTRA_PASSWORD_COMPLEXITY, -1); + SetNewPasswordActivity activity = + Robolectric.buildActivity(SetNewPasswordActivity.class, intent).create().get(); + + ShadowActivity shadowActivity = Shadows.shadowOf(activity); + Intent actualIntent = shadowActivity.getNextStartedActivityForResult().intent; + assertThat(actualIntent.getAction()).isEqualTo(ACTION_SET_NEW_PASSWORD); + assertThat(actualIntent.hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)).isFalse(); + assertThat(actualIntent.hasExtra(EXTRA_KEY_CALLER_APP_NAME)).isTrue(); + assertThat(actualIntent.getStringExtra(EXTRA_KEY_CALLER_APP_NAME)).isEqualTo(APP_LABEL); + } + + @Test + @Config(shadows = {ShadowPasswordUtils.class}) + public void testLaunchChooseLock_setNewPasswordExtraNoneComplexity() { + ShadowPasswordUtils.setCallingAppLabel(APP_LABEL); + ShadowPasswordUtils.addGrantedPermission(GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY); + Settings.Global.putInt(RuntimeEnvironment.application.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 1); + + Intent intent = new Intent(ACTION_SET_NEW_PASSWORD); + intent.putExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_NONE); + SetNewPasswordActivity activity = + Robolectric.buildActivity(SetNewPasswordActivity.class, intent).create().get(); + + ShadowActivity shadowActivity = Shadows.shadowOf(activity); + Intent actualIntent = shadowActivity.getNextStartedActivityForResult().intent; + assertThat(actualIntent.getAction()).isEqualTo(ACTION_SET_NEW_PASSWORD); + assertThat(actualIntent.hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)).isFalse(); + assertThat(actualIntent.hasExtra(EXTRA_KEY_CALLER_APP_NAME)).isTrue(); + assertThat(actualIntent.getStringExtra(EXTRA_KEY_CALLER_APP_NAME)).isEqualTo(APP_LABEL); + } + + @Test + @Config(shadows = {ShadowPasswordUtils.class}) + public void testLaunchChooseLock_setNewParentProfilePasswordExtraWithPermission() { + ShadowPasswordUtils.setCallingAppLabel(APP_LABEL); + ShadowPasswordUtils.addGrantedPermission(GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY); + Settings.Global.putInt(RuntimeEnvironment.application.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 1); + + Intent intent = new Intent(ACTION_SET_NEW_PARENT_PROFILE_PASSWORD); + intent.putExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_HIGH); + SetNewPasswordActivity activity = + Robolectric.buildActivity(SetNewPasswordActivity.class, intent).create().get(); + + ShadowActivity shadowActivity = Shadows.shadowOf(activity); + Intent actualIntent = shadowActivity.getNextStartedActivityForResult().intent; + assertThat(actualIntent.getAction()).isEqualTo(ACTION_SET_NEW_PARENT_PROFILE_PASSWORD); + assertThat(actualIntent.hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)).isFalse(); + assertThat(actualIntent.hasExtra(EXTRA_KEY_CALLER_APP_NAME)).isTrue(); + assertThat(actualIntent.getStringExtra(EXTRA_KEY_CALLER_APP_NAME)).isEqualTo(APP_LABEL); + } } diff --git a/tests/robotests/src/com/android/settings/password/SetupChooseLockGenericTest.java b/tests/robotests/src/com/android/settings/password/SetupChooseLockGenericTest.java new file mode 100644 index 00000000000..63bdc381926 --- /dev/null +++ b/tests/robotests/src/com/android/settings/password/SetupChooseLockGenericTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 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 android.Manifest.permission.GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; + +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.content.Intent; + +import com.android.settings.testutils.shadow.ShadowLockPatternUtils; +import com.android.settings.testutils.shadow.ShadowPasswordUtils; +import com.android.settings.testutils.shadow.ShadowUserManager; +import com.android.settings.testutils.shadow.ShadowUtils; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowActivity; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = { + ShadowUserManager.class, + ShadowUtils.class, + ShadowLockPatternUtils.class, +}) +public class SetupChooseLockGenericTest { + + @After + public void tearDown() { + ShadowPasswordUtils.reset(); + } + + @Test + public void setupChooseLockGenericPasswordComplexityExtraWithoutPermission() { + Intent intent = new Intent("com.android.settings.SETUP_LOCK_SCREEN"); + intent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_HIGH); + SetupChooseLockGeneric activity = + Robolectric.buildActivity(SetupChooseLockGeneric.class, intent).create().get(); + + ShadowActivity shadowActivity = Shadows.shadowOf(activity); + assertThat(shadowActivity.isFinishing()).isTrue(); + } + + @Test + @Config(shadows = {ShadowPasswordUtils.class}) + public void setupChooseLockGenericPasswordComplexityExtraWithPermission() { + ShadowPasswordUtils.addGrantedPermission(GET_AND_REQUEST_SCREEN_LOCK_COMPLEXITY); + + Intent intent = new Intent("com.android.settings.SETUP_LOCK_SCREEN"); + intent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_HIGH); + SetupChooseLockGeneric activity = + Robolectric.buildActivity(SetupChooseLockGeneric.class, intent).create().get(); + + ShadowActivity shadowActivity = Shadows.shadowOf(activity); + assertThat(shadowActivity.isFinishing()).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowActivityManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowActivityManager.java index 38d658c3544..76bdaefa18e 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowActivityManager.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowActivityManager.java @@ -17,6 +17,7 @@ package com.android.settings.testutils.shadow; import android.app.ActivityManager; +import android.app.IActivityManager; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -24,6 +25,7 @@ import org.robolectric.annotation.Implements; @Implements(ActivityManager.class) public class ShadowActivityManager { private static int sCurrentUserId = 0; + private static IActivityManager sService = null; @Implementation protected static int getCurrentUser() { @@ -33,4 +35,13 @@ public class ShadowActivityManager { public static void setCurrentUser(int userId) { sCurrentUserId = userId; } + + @Implementation + public static IActivityManager getService() { + return sService; + } + + public static void setService(IActivityManager service) { + sService = service; + } } 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 6d2dbef467c..ca759164b99 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowDevicePolicyManager.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowDevicePolicyManager.java @@ -1,5 +1,7 @@ package com.android.settings.testutils.shadow; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -23,6 +25,10 @@ public class ShadowDevicePolicyManager extends org.robolectric.shadows.ShadowDev private boolean mIsAdminActiveAsUser = false; private ComponentName mDeviceOwnerComponentName; private int mDeviceOwnerUserId = -1; + private int mPasswordMinQuality = PASSWORD_QUALITY_UNSPECIFIED; + private int mPasswordMaxLength = 16; + private int mPasswordMinLength = 0; + private int mPasswordMinSymbols = 0; public void setShortSupportMessageForUser(ComponentName admin, int userHandle, String message) { mSupportMessagesMap.put(Objects.hash(admin, userHandle), message); @@ -70,6 +76,42 @@ public class ShadowDevicePolicyManager extends org.robolectric.shadows.ShadowDev mDeviceOwnerComponentName = admin; } + @Implementation + public int getPasswordQuality(ComponentName admin, int userHandle) { + return mPasswordMinQuality; + } + + public void setPasswordQuality(int quality) { + mPasswordMinQuality = quality; + } + + @Implementation + public int getPasswordMinimumLength(ComponentName admin, int userHandle) { + return mPasswordMinLength; + } + + public void setPasswordMinimumLength(int length) { + mPasswordMinLength = length; + } + + @Implementation + public int getPasswordMinimumSymbols(ComponentName admin, int userHandle) { + return mPasswordMinSymbols; + } + + public void setPasswordMinimumSymbols(int numOfSymbols) { + mPasswordMinSymbols = numOfSymbols; + } + + @Implementation + public int getPasswordMaximumLength(int quality) { + return mPasswordMaxLength; + } + + public void setPasswordMaximumLength(int length) { + mPasswordMaxLength = length; + } + public static ShadowDevicePolicyManager getShadow() { return (ShadowDevicePolicyManager) Shadow.extract( RuntimeEnvironment.application.getSystemService(DevicePolicyManager.class)); 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 663ab91e580..7ce098d9ea8 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java @@ -59,4 +59,14 @@ public class ShadowLockPatternUtils { public static void setDeviceEncryptionEnabled(boolean deviceEncryptionEnabled) { sDeviceEncryptionEnabled = deviceEncryptionEnabled; } + + @Implementation + protected byte[] getPasswordHistoryHashFactor(String currentPassword, int userId) { + return null; + } + + @Implementation + protected boolean checkPasswordHistory(String passwordToCheck, byte[] hashFactor, int userId) { + return false; + } } diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowPasswordUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowPasswordUtils.java new file mode 100644 index 00000000000..6a5c4ae7c57 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowPasswordUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.testutils.shadow; + +import android.content.Context; +import android.os.IBinder; + +import com.android.settings.password.PasswordUtils; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@Implements(PasswordUtils.class) +public class ShadowPasswordUtils { + + private static String sCallingAppLabel; + private static Set sGrantedPermissions; + + public static void reset() { + sCallingAppLabel = null; + sGrantedPermissions = null; + } + + @Implementation + protected static boolean isCallingAppPermitted(Context context, IBinder activityToken, + String permission) { + if (sGrantedPermissions == null) { + return false; + } + return sGrantedPermissions.contains(permission); + } + + public static void addGrantedPermission(String... permissions) { + if (sGrantedPermissions == null) { + sGrantedPermissions = new HashSet<>(); + } + sGrantedPermissions.addAll(Arrays.asList(permissions)); + } + + @Implementation + protected static String getCallingAppLabel(Context context, IBinder activityToken) { + return sCallingAppLabel; + } + + public static void setCallingAppLabel(String label) { + sCallingAppLabel = label; + } +}