diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8cad8a4ba6f..8e16de35d1d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -80,6 +80,8 @@ + + @@ -1451,7 +1453,7 @@ + android:theme="@android:style/Theme.Translucent.NoTitleBar"> diff --git a/res/anim/confirm_credential_biometric_transition_enter.xml b/res/anim/confirm_credential_biometric_transition_enter.xml new file mode 100644 index 00000000000..56f35930b8e --- /dev/null +++ b/res/anim/confirm_credential_biometric_transition_enter.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/res/anim/confirm_credential_biometric_transition_exit.xml b/res/anim/confirm_credential_biometric_transition_exit.xml new file mode 100644 index 00000000000..debdce2231d --- /dev/null +++ b/res/anim/confirm_credential_biometric_transition_exit.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 6e15f1e0e73..0b1f45c5a29 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3706,6 +3706,10 @@ Loading\u2026 + + + Use alternate method + Set screen lock diff --git a/src/com/android/settings/password/BiometricFragment.java b/src/com/android/settings/password/BiometricFragment.java new file mode 100644 index 00000000000..6e1ae10d841 --- /dev/null +++ b/src/com/android/settings/password/BiometricFragment.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2018 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.settings.SettingsEnums; +import android.content.DialogInterface; +import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback; +import android.hardware.biometrics.BiometricPrompt.AuthenticationResult; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; + +import com.android.settings.core.InstrumentedFragment; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.concurrent.Executor; + +/** + * A fragment that wraps the BiometricPrompt and manages its lifecycle. + */ +public class BiometricFragment extends InstrumentedFragment { + + private static final String KEY_TITLE = "title"; + private static final String KEY_SUBTITLE = "subtitle"; + private static final String KEY_DESCRIPTION = "description"; + private static final String KEY_NEGATIVE_TEXT = "negative_text"; + + // Re-set by the application. Should be done upon orientation changes, etc + private Executor mClientExecutor; + private AuthenticationCallback mClientCallback; + + // Created/Initialized once and retained + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private PromptInfo mPromptInfo; + private BiometricPrompt mBiometricPrompt; + private CancellationSignal mCancellationSignal; + + private AuthenticationCallback mAuthenticationCallback = + new AuthenticationCallback() { + @Override + public void onAuthenticationError(int error, @NonNull CharSequence message) { + mClientExecutor.execute(() -> { + mClientCallback.onAuthenticationError(error, message); + }); + cleanup(); + } + + @Override + public void onAuthenticationSucceeded(AuthenticationResult result) { + mClientExecutor.execute(() -> { + mClientCallback.onAuthenticationSucceeded(result); + }); + cleanup(); + } + }; + + private final DialogInterface.OnClickListener mNegativeButtonListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mAuthenticationCallback.onAuthenticationError( + BiometricConstants.BIOMETRIC_ERROR_NEGATIVE_BUTTON, + mPromptInfo.getNegativeButtonText()); + } + }; + + public static BiometricFragment newInstance(PromptInfo info) { + BiometricFragment biometricFragment = new BiometricFragment(); + biometricFragment.setArguments(info.getBundle()); + return biometricFragment; + } + + public void setCallbacks(Executor executor, AuthenticationCallback callback) { + mClientExecutor = executor; + mClientCallback = callback; + } + + public void cancel() { + if (mCancellationSignal != null) { + mCancellationSignal.cancel(); + } + cleanup(); + } + + private void cleanup() { + if (getActivity() != null) { + getActivity().getSupportFragmentManager().beginTransaction().remove(this).commit(); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + + mPromptInfo = new PromptInfo(getArguments()); + mBiometricPrompt = new BiometricPrompt.Builder(getContext()) + .setTitle(mPromptInfo.getTitle()) + .setUseDefaultTitle() // use default title if title is null/empty + .setSubtitle(mPromptInfo.getSubtitle()) + .setDescription(mPromptInfo.getDescription()) + .setNegativeButton(mPromptInfo.getNegativeButtonText(), mClientExecutor, + mNegativeButtonListener) + .build(); + mCancellationSignal = new CancellationSignal(); + + // TODO: CC doesn't use crypto for now + mBiometricPrompt.authenticate(mCancellationSignal, mClientExecutor, + mAuthenticationCallback); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.BIOMETRIC_FRAGMENT; + } + + /** + * A simple wrapper for BiometricPrompt.PromptInfo. Since we want to manage the lifecycle + * of BiometricPrompt correctly, the information needs to be stored in here. + */ + static class PromptInfo { + private final Bundle mBundle; + + private PromptInfo(Bundle bundle) { + mBundle = bundle; + } + + Bundle getBundle() { + return mBundle; + } + + public CharSequence getTitle() { + return mBundle.getCharSequence(KEY_TITLE); + } + + public CharSequence getSubtitle() { + return mBundle.getCharSequence(KEY_SUBTITLE); + } + + public CharSequence getDescription() { + return mBundle.getCharSequence(KEY_DESCRIPTION); + } + + public CharSequence getNegativeButtonText() { + return mBundle.getCharSequence(KEY_NEGATIVE_TEXT); + } + + public static class Builder { + private final Bundle mBundle = new Bundle(); + + public Builder setTitle(@NonNull CharSequence title) { + mBundle.putCharSequence(KEY_TITLE, title); + return this; + } + + public Builder setSubtitle(@Nullable CharSequence subtitle) { + mBundle.putCharSequence(KEY_SUBTITLE, subtitle); + return this; + } + + public Builder setDescription(@Nullable CharSequence description) { + mBundle.putCharSequence(KEY_DESCRIPTION, description); + return this; + } + + public Builder setNegativeButtonText(@NonNull CharSequence text) { + mBundle.putCharSequence(KEY_NEGATIVE_TEXT, text); + return this; + } + + public PromptInfo build() { + return new PromptInfo(mBundle); + } + } + } +} + diff --git a/src/com/android/settings/password/ChooseLockSettingsHelper.java b/src/com/android/settings/password/ChooseLockSettingsHelper.java index 118b51d9a52..d5182b30b02 100644 --- a/src/com/android/settings/password/ChooseLockSettingsHelper.java +++ b/src/com/android/settings/password/ChooseLockSettingsHelper.java @@ -383,11 +383,11 @@ public final class ChooseLockSettingsHelper { intent.putExtra(ConfirmDeviceCredentialBaseFragment.TITLE_TEXT, title); intent.putExtra(ConfirmDeviceCredentialBaseFragment.HEADER_TEXT, header); intent.putExtra(ConfirmDeviceCredentialBaseFragment.DETAILS_TEXT, message); - intent.putExtra(ConfirmDeviceCredentialBaseFragment.ALLOW_FP_AUTHENTICATION, external); // TODO: Remove dark theme and show_cancel_button options since they are no longer used intent.putExtra(ConfirmDeviceCredentialBaseFragment.DARK_THEME, false); intent.putExtra(ConfirmDeviceCredentialBaseFragment.SHOW_CANCEL_BUTTON, false); intent.putExtra(ConfirmDeviceCredentialBaseFragment.SHOW_WHEN_LOCKED, external); + intent.putExtra(ConfirmDeviceCredentialBaseFragment.USE_FADE_ANIMATION, external); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, returnCredentials); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, hasChallenge); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge); diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java index f5b3b054c54..f68c04ad298 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java @@ -22,21 +22,40 @@ import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; +import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; + import com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; import com.android.settings.Utils; +import java.util.concurrent.Executor; + /** * Launch this when you want to confirm the user is present by asking them to enter their * PIN/password/pattern. */ -public class ConfirmDeviceCredentialActivity extends Activity { +public class ConfirmDeviceCredentialActivity extends FragmentActivity { public static final String TAG = ConfirmDeviceCredentialActivity.class.getSimpleName(); + // The normal flow that apps go through + private static final int CREDENTIAL_NORMAL = 1; + // Unlocks the managed profile when the primary profile is unlocked + private static final int CREDENTIAL_MANAGED = 2; + + private static final String TAG_BIOMETRIC_FRAGMENT = "fragment"; + public static class InternalActivity extends ConfirmDeviceCredentialActivity { } @@ -60,57 +79,217 @@ public class ConfirmDeviceCredentialActivity extends Activity { return intent; } + private BiometricManager mBiometricManager; + private BiometricFragment mBiometricFragment; + private DevicePolicyManager mDevicePolicyManager; + private LockPatternUtils mLockPatternUtils; + private UserManager mUserManager; + private ChooseLockSettingsHelper mChooseLockSettingsHelper; + private Handler mHandler = new Handler(Looper.getMainLooper()); + + private String mTitle; + private String mDetails; + private int mUserId; + private int mEffectiveUserId; + private int mCredentialMode; + private boolean mGoingToBackground; + + private Executor mExecutor = (runnable -> { + mHandler.post(runnable); + }); + + private AuthenticationCallback mAuthenticationCallback = new AuthenticationCallback() { + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + if (!mGoingToBackground) { + if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED) { + finish(); + } else { + // All other errors go to some version of CC + showConfirmCredentials(); + } + } + + } + + public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { + setResult(Activity.RESULT_OK); + finish(); + } + }; + @Override - public void onCreate(Bundle savedInstanceState) { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mBiometricManager = getSystemService(BiometricManager.class); + mDevicePolicyManager = getSystemService(DevicePolicyManager.class); + mUserManager = UserManager.get(this); + mLockPatternUtils = new LockPatternUtils(this); + Intent intent = getIntent(); - String title = intent.getStringExtra(KeyguardManager.EXTRA_TITLE); - String details = intent.getStringExtra(KeyguardManager.EXTRA_DESCRIPTION); + mTitle = intent.getStringExtra(KeyguardManager.EXTRA_TITLE); + mDetails = intent.getStringExtra(KeyguardManager.EXTRA_DESCRIPTION); String alternateButton = intent.getStringExtra( KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL); boolean frp = KeyguardManager.ACTION_CONFIRM_FRP_CREDENTIAL.equals(intent.getAction()); - int userId = UserHandle.myUserId(); + mUserId = UserHandle.myUserId(); + mEffectiveUserId = mUserManager.getCredentialOwnerProfile(mUserId); if (isInternalActivity()) { try { - userId = Utils.getUserIdFromBundle(this, intent.getExtras()); + mUserId = Utils.getUserIdFromBundle(this, intent.getExtras()); } catch (SecurityException se) { Log.e(TAG, "Invalid intent extra", se); } } - final boolean isManagedProfile = UserManager.get(this).isManagedProfile(userId); + final boolean isManagedProfile = UserManager.get(this).isManagedProfile(mUserId); // if the client app did not hand in a title and we are about to show the work challenge, // check whether there is a policy setting the organization name and use that as title - if ((title == null) && isManagedProfile) { - title = getTitleFromOrganizationName(userId); + if ((mTitle == null) && isManagedProfile) { + mTitle = getTitleFromOrganizationName(mUserId); } - ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this); + mChooseLockSettingsHelper = new ChooseLockSettingsHelper(this); final LockPatternUtils lockPatternUtils = new LockPatternUtils(this); - boolean launched; + + boolean launchedBiometric = false; + boolean launchedCDC = false; // If the target is a managed user and user key not unlocked yet, we will force unlock // tied profile so it will enable work mode and unlock managed profile, when personal // challenge is unlocked. if (frp) { - launched = helper.launchFrpConfirmationActivity(0, title, details, alternateButton); + launchedCDC = mChooseLockSettingsHelper.launchFrpConfirmationActivity( + 0, mTitle, mDetails, alternateButton); } else if (isManagedProfile && isInternalActivity() - && !lockPatternUtils.isSeparateProfileChallengeEnabled(userId)) { + && !lockPatternUtils.isSeparateProfileChallengeEnabled(mUserId)) { + mCredentialMode = CREDENTIAL_MANAGED; + if (isBiometricAllowed()) { + showBiometricPrompt(); + launchedBiometric = true; + } else { + showConfirmCredentials(); + } + } else { + mCredentialMode = CREDENTIAL_NORMAL; + if (isBiometricAllowed()) { + // Don't need to check if biometrics / pin/pattern/pass are enrolled. It will go to + // onAuthenticationError and do the right thing automatically. + showBiometricPrompt(); + launchedBiometric = true; + } else { + showConfirmCredentials(); + } + } + + if (launchedCDC) { + finish(); + } else if (launchedBiometric) { + // Keep this activity alive until BiometricPrompt goes away + } else { + Log.d(TAG, "No pattern, password or PIN set."); + setResult(Activity.RESULT_OK); + finish(); + } + } + + @Override + protected void onStart() { + super.onStart(); + // Translucent activity that is "visible", so it doesn't complain about finish() + // not being called before onResume(). + setVisible(true); + } + + @Override + public void onPause() { + super.onPause(); + if (!isChangingConfigurations()) { + mGoingToBackground = true; + if (mBiometricFragment != null) { + mBiometricFragment.cancel(); + } + finish(); + } else { + mGoingToBackground = false; + } + } + + // User could be locked while Effective user is unlocked even though the effective owns the + // credential. Otherwise, biometric can't unlock fbe/keystore through + // verifyTiedProfileChallenge. In such case, we also wanna show the user message that + // biometric is disabled due to device restart. + private boolean isStrongAuthRequired() { + return !mLockPatternUtils.isBiometricAllowedForUser(mEffectiveUserId) + || !mUserManager.isUserUnlocked(mUserId); + } + + private boolean isBiometricDisabledByAdmin() { + final int disabledFeatures = + mDevicePolicyManager.getKeyguardDisabledFeatures(null, mEffectiveUserId); + return (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_BIOMETRICS) != 0; + } + + private boolean isBiometricAllowed() { + return !isStrongAuthRequired() && !isBiometricDisabledByAdmin(); + } + + private void showBiometricPrompt() { + mBiometricManager.setActiveUser(mUserId); + + mBiometricFragment = (BiometricFragment) getSupportFragmentManager() + .findFragmentByTag(TAG_BIOMETRIC_FRAGMENT); + boolean newFragment = false; + + if (mBiometricFragment == null) { + final BiometricFragment.PromptInfo info = new BiometricFragment.PromptInfo.Builder() + .setTitle(mTitle) + .setSubtitle(mDetails) + .setNegativeButtonText(getResources() + .getString(R.string.confirm_device_credential_use_alternate_method)) + .build(); + mBiometricFragment = BiometricFragment.newInstance(info); + newFragment = true; + } + mBiometricFragment.setCallbacks(mExecutor, mAuthenticationCallback); + + if (newFragment) { + getSupportFragmentManager().beginTransaction() + .add(mBiometricFragment, TAG_BIOMETRIC_FRAGMENT).commit(); + } + } + + /** + * Shows ConfirmDeviceCredentials for normal apps. + */ + private void showConfirmCredentials() { + boolean launched = false; + if (mCredentialMode == CREDENTIAL_MANAGED) { // We set the challenge as 0L, so it will force to unlock managed profile when it // unlocks primary profile screen lock, by calling verifyTiedProfileChallenge() - launched = helper.launchConfirmationActivityWithExternalAndChallenge( - 0 /* request code */, null /* title */, title, details, true /* isExternal */, - 0L /* challenge */, userId); - } else { - launched = helper.launchConfirmationActivity(0 /* request code */, null /* title */, - title, details, false /* returnCredentials */, true /* isExternal */, userId); + launched = mChooseLockSettingsHelper + .launchConfirmationActivityWithExternalAndChallenge( + 0 /* request code */, null /* title */, mTitle, mDetails, + true /* isExternal */, 0L /* challenge */, mUserId); + } else if (mCredentialMode == CREDENTIAL_NORMAL){ + launched = mChooseLockSettingsHelper.launchConfirmationActivity( + 0 /* request code */, null /* title */, + mTitle, mDetails, false /* returnCredentials */, true /* isExternal */, + mUserId); } if (!launched) { - Log.d(TAG, "No pattern, password or PIN set."); + Log.d(TAG, "No pin/pattern/pass set"); setResult(Activity.RESULT_OK); } finish(); } + @Override + public void finish() { + super.finish(); + // Finish without animation since the activity is just there so we can launch + // BiometricPrompt. + overridePendingTransition(R.anim.confirm_credential_biometric_transition_enter, 0); + } + private boolean isInternalActivity() { return this instanceof ConfirmDeviceCredentialActivity.InternalActivity; } diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java index cae3ae64d56..c00f9ab124c 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java @@ -140,6 +140,15 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi } } + @Override + public void finish() { + super.finish(); + if (getIntent().getBooleanExtra( + ConfirmDeviceCredentialBaseFragment.USE_FADE_ANIMATION, false)) { + overridePendingTransition(0, R.anim.confirm_credential_biometric_transition_exit); + } + } + public void prepareEnterAnimation() { getFragment().prepareEnterAnimation(); } diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java index 2e836dfdf0a..9b677aa4ace 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java @@ -64,13 +64,13 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr public static final String TITLE_TEXT = PACKAGE + ".ConfirmCredentials.title"; public static final String HEADER_TEXT = PACKAGE + ".ConfirmCredentials.header"; public static final String DETAILS_TEXT = PACKAGE + ".ConfirmCredentials.details"; - public static final String ALLOW_FP_AUTHENTICATION = - PACKAGE + ".ConfirmCredentials.allowFpAuthentication"; public static final String DARK_THEME = PACKAGE + ".ConfirmCredentials.darkTheme"; public static final String SHOW_CANCEL_BUTTON = PACKAGE + ".ConfirmCredentials.showCancelButton"; public static final String SHOW_WHEN_LOCKED = PACKAGE + ".ConfirmCredentials.showWhenLocked"; + public static final String USE_FADE_ANIMATION = + PACKAGE + ".ConfirmCredentials.useFadeAnimation"; protected static final int USER_TYPE_PRIMARY = 1; protected static final int USER_TYPE_MANAGED_PROFILE = 2; diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockSettingsHelperTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockSettingsHelperTest.java index 5d51178b65c..e3f3833ac33 100644 --- a/tests/robotests/src/com/android/settings/password/ChooseLockSettingsHelperTest.java +++ b/tests/robotests/src/com/android/settings/password/ChooseLockSettingsHelperTest.java @@ -62,8 +62,6 @@ public class ChooseLockSettingsHelperTest { assertEquals( true, (startedIntent.getFlags() & Intent.FLAG_ACTIVITY_FORWARD_RESULT) != 0); - assertEquals(true, startedIntent.getBooleanExtra( - ConfirmDeviceCredentialBaseFragment.ALLOW_FP_AUTHENTICATION, false)); assertFalse(startedIntent.getBooleanExtra( ConfirmDeviceCredentialBaseFragment.DARK_THEME, false)); assertFalse(startedIntent.getBooleanExtra( @@ -100,8 +98,6 @@ public class ChooseLockSettingsHelperTest { assertEquals( false, (startedIntent.getFlags() & Intent.FLAG_ACTIVITY_FORWARD_RESULT) != 0); - assertEquals(false, startedIntent.getBooleanExtra( - ConfirmDeviceCredentialBaseFragment.ALLOW_FP_AUTHENTICATION, false)); assertFalse(startedIntent.getBooleanExtra( ConfirmDeviceCredentialBaseFragment.DARK_THEME, false)); assertFalse(startedIntent.getBooleanExtra(