• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 // TODO (b/35202196): move this class out of the root of the package.
18 package com.android.settings.password;
19 
20 import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_LOCK_ATTEMPTS_FAILED;
21 
22 import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME;
23 
24 import android.annotation.Nullable;
25 import android.app.Dialog;
26 import android.app.KeyguardManager;
27 import android.app.admin.DevicePolicyManager;
28 import android.content.Context;
29 import android.content.DialogInterface;
30 import android.content.Intent;
31 import android.content.pm.UserInfo;
32 import android.hardware.biometrics.BiometricManager;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.UserHandle;
36 import android.os.UserManager;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.view.View;
40 import android.widget.Button;
41 import android.widget.TextView;
42 
43 import androidx.appcompat.app.AlertDialog;
44 import androidx.fragment.app.DialogFragment;
45 import androidx.fragment.app.FragmentManager;
46 
47 import com.android.internal.widget.LockPatternUtils;
48 import com.android.settings.R;
49 import com.android.settings.Utils;
50 import com.android.settings.core.InstrumentedFragment;
51 
52 /**
53  * Base fragment to be shared for PIN/Pattern/Password confirmation fragments.
54  */
55 public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFragment {
56     public static final String TAG = ConfirmDeviceCredentialBaseFragment.class.getSimpleName();
57     public static final String TITLE_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.title";
58     public static final String HEADER_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.header";
59     public static final String DETAILS_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.details";
60     public static final String DARK_THEME = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.darkTheme";
61     public static final String SHOW_CANCEL_BUTTON =
62             SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showCancelButton";
63     public static final String SHOW_WHEN_LOCKED =
64             SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showWhenLocked";
65     public static final String USE_FADE_ANIMATION =
66             SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.useFadeAnimation";
67 
68     protected static final int USER_TYPE_PRIMARY = 1;
69     protected static final int USER_TYPE_MANAGED_PROFILE = 2;
70     protected static final int USER_TYPE_SECONDARY = 3;
71 
72     /** Time we wait before clearing a wrong input attempt (e.g. pattern) and the error message. */
73     protected static final long CLEAR_WRONG_ATTEMPT_TIMEOUT_MS = 3000;
74 
75     protected boolean mReturnCredentials = false;
76     protected boolean mReturnGatekeeperPassword = false;
77     protected boolean mForceVerifyPath = false;
78     protected Button mCancelButton;
79     /** Button allowing managed profile password reset, null when is not shown. */
80     @Nullable protected Button mForgotButton;
81     protected int mEffectiveUserId;
82     protected int mUserId;
83     protected UserManager mUserManager;
84     protected LockPatternUtils mLockPatternUtils;
85     protected DevicePolicyManager mDevicePolicyManager;
86     protected TextView mErrorTextView;
87     protected final Handler mHandler = new Handler();
88     protected boolean mFrp;
89     private CharSequence mFrpAlternateButtonText;
90     protected BiometricManager mBiometricManager;
91 
isInternalActivity()92     private boolean isInternalActivity() {
93         return (getActivity() instanceof ConfirmLockPassword.InternalActivity)
94                 || (getActivity() instanceof ConfirmLockPattern.InternalActivity);
95     }
96 
97     @Override
onCreate(@ullable Bundle savedInstanceState)98     public void onCreate(@Nullable Bundle savedInstanceState) {
99         super.onCreate(savedInstanceState);
100         final Intent intent = getActivity().getIntent();
101         mFrpAlternateButtonText = intent.getCharSequenceExtra(
102                 KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL);
103         mReturnCredentials = intent.getBooleanExtra(
104                 ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, false);
105 
106         mReturnGatekeeperPassword = intent.getBooleanExtra(
107                 ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, false);
108         mForceVerifyPath = intent.getBooleanExtra(
109                 ChooseLockSettingsHelper.EXTRA_KEY_FORCE_VERIFY, false);
110 
111         // Only take this argument into account if it belongs to the current profile.
112         mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras(),
113                 isInternalActivity());
114         mFrp = (mUserId == LockPatternUtils.USER_FRP);
115         mUserManager = UserManager.get(getActivity());
116         mEffectiveUserId = mUserManager.getCredentialOwnerProfile(mUserId);
117         mLockPatternUtils = new LockPatternUtils(getActivity());
118         mDevicePolicyManager = (DevicePolicyManager) getActivity().getSystemService(
119                 Context.DEVICE_POLICY_SERVICE);
120         mBiometricManager = getActivity().getSystemService(BiometricManager.class);
121     }
122 
123     @Override
onViewCreated(View view, @Nullable Bundle savedInstanceState)124     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
125         super.onViewCreated(view, savedInstanceState);
126         mCancelButton = view.findViewById(R.id.cancelButton);
127         boolean showCancelButton = getActivity().getIntent().getBooleanExtra(
128                 SHOW_CANCEL_BUTTON, false);
129         boolean hasAlternateButton = mFrp && !TextUtils.isEmpty(mFrpAlternateButtonText);
130         mCancelButton.setVisibility(showCancelButton || hasAlternateButton
131                 ? View.VISIBLE : View.GONE);
132         if (hasAlternateButton) {
133             mCancelButton.setText(mFrpAlternateButtonText);
134         }
135         mCancelButton.setOnClickListener(v -> {
136             if (hasAlternateButton) {
137                 getActivity().setResult(KeyguardManager.RESULT_ALTERNATE);
138             }
139             getActivity().finish();
140         });
141         setupForgotButtonIfManagedProfile(view);
142     }
143 
setupForgotButtonIfManagedProfile(View view)144     private void setupForgotButtonIfManagedProfile(View view) {
145         if (mUserManager.isManagedProfile(mUserId)
146                 && mUserManager.isQuietModeEnabled(UserHandle.of(mUserId))
147                 && mDevicePolicyManager.canProfileOwnerResetPasswordWhenLocked(mUserId)) {
148             mForgotButton = view.findViewById(R.id.forgotButton);
149             if (mForgotButton == null) {
150                 Log.wtf(TAG, "Forgot button not found in managed profile credential dialog");
151                 return;
152             }
153             mForgotButton.setVisibility(View.VISIBLE);
154             mForgotButton.setOnClickListener(v -> {
155                 final Intent intent = new Intent();
156                 intent.setClassName(SETTINGS_PACKAGE_NAME, ForgotPasswordActivity.class.getName());
157                 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
158                 intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
159                 getActivity().startActivity(intent);
160                 getActivity().finish();
161             });
162         }
163     }
164 
165     // User could be locked while Effective user is unlocked even though the effective owns the
166     // credential. Otherwise, fingerprint can't unlock fbe/keystore through
167     // verifyTiedProfileChallenge. In such case, we also wanna show the user message that
168     // fingerprint is disabled due to device restart.
isStrongAuthRequired()169     protected boolean isStrongAuthRequired() {
170         return mFrp
171                 || !mLockPatternUtils.isBiometricAllowedForUser(mEffectiveUserId)
172                 || !mUserManager.isUserUnlocked(mUserId);
173     }
174 
175     @Override
onResume()176     public void onResume() {
177         super.onResume();
178         refreshLockScreen();
179     }
180 
refreshLockScreen()181     protected void refreshLockScreen() {
182         updateErrorMessage(mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId));
183     }
184 
setAccessibilityTitle(CharSequence supplementalText)185     protected void setAccessibilityTitle(CharSequence supplementalText) {
186         Intent intent = getActivity().getIntent();
187         if (intent != null) {
188             CharSequence titleText = intent.getCharSequenceExtra(
189                     ConfirmDeviceCredentialBaseFragment.TITLE_TEXT);
190             if (supplementalText == null) {
191                 return;
192             }
193             if (titleText == null) {
194                 getActivity().setTitle(supplementalText);
195             } else {
196                 String accessibilityTitle =
197                         new StringBuilder(titleText).append(",").append(supplementalText).toString();
198                 getActivity().setTitle(Utils.createAccessibleSequence(titleText, accessibilityTitle));
199             }
200         }
201     }
202 
203     @Override
onPause()204     public void onPause() {
205         super.onPause();
206     }
207 
authenticationSucceeded()208     protected abstract void authenticationSucceeded();
209 
210 
prepareEnterAnimation()211     public void prepareEnterAnimation() {
212     }
213 
startEnterAnimation()214     public void startEnterAnimation() {
215     }
216 
reportFailedAttempt()217     protected void reportFailedAttempt() {
218         updateErrorMessage(
219                 mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1);
220         mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId);
221     }
222 
updateErrorMessage(int numAttempts)223     protected void updateErrorMessage(int numAttempts) {
224         final int maxAttempts =
225                 mLockPatternUtils.getMaximumFailedPasswordsForWipe(mEffectiveUserId);
226         if (maxAttempts <= 0 || numAttempts <= 0) {
227             return;
228         }
229 
230         // Update the on-screen error string
231         if (mErrorTextView != null) {
232             final String message = getActivity().getString(
233                     R.string.lock_failed_attempts_before_wipe, numAttempts, maxAttempts);
234             showError(message, 0);
235         }
236 
237         // Only show popup dialog before the last attempt and before wipe
238         final int remainingAttempts = maxAttempts - numAttempts;
239         if (remainingAttempts > 1) {
240             return;
241         }
242         final FragmentManager fragmentManager = getChildFragmentManager();
243         final int userType = getUserTypeForWipe();
244         if (remainingAttempts == 1) {
245             // Last try
246             final String title = getActivity().getString(
247                     R.string.lock_last_attempt_before_wipe_warning_title);
248             final String overrideMessageId = getLastTryOverrideErrorMessageId(userType);
249             final int defaultMessageId = getLastTryDefaultErrorMessage(userType);
250             final String message = mDevicePolicyManager.getResources().getString(
251                     overrideMessageId, () -> getString(defaultMessageId));
252             LastTryDialog.show(fragmentManager, title, message,
253                     android.R.string.ok, false /* dismiss */);
254         } else {
255             // Device, profile, or secondary user is wiped
256             final String message = getWipeMessage(userType);
257             LastTryDialog.show(fragmentManager, null /* title */, message,
258                     com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss,
259                     true /* dismiss */);
260         }
261     }
262 
getUserTypeForWipe()263     private int getUserTypeForWipe() {
264         final UserInfo userToBeWiped = mUserManager.getUserInfo(
265                 mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId));
266         if (userToBeWiped == null || userToBeWiped.isPrimary()) {
267             return USER_TYPE_PRIMARY;
268         } else if (userToBeWiped.isManagedProfile()) {
269             return USER_TYPE_MANAGED_PROFILE;
270         } else {
271             return USER_TYPE_SECONDARY;
272         }
273     }
274 
getLastTryOverrideErrorMessageId(int userType)275     protected abstract String getLastTryOverrideErrorMessageId(int userType);
getLastTryDefaultErrorMessage(int userType)276     protected abstract int getLastTryDefaultErrorMessage(int userType);
277 
getWipeMessage(int userType)278     private String getWipeMessage(int userType) {
279         switch (userType) {
280             case USER_TYPE_PRIMARY:
281                 return getString(com.android.settingslib
282                         .R.string.failed_attempts_now_wiping_device);
283             case USER_TYPE_MANAGED_PROFILE:
284                 return mDevicePolicyManager.getResources().getString(
285                         WORK_PROFILE_LOCK_ATTEMPTS_FAILED,
286                         () -> getString(com.android.settingslib
287                                 .R.string.failed_attempts_now_wiping_profile));
288             case USER_TYPE_SECONDARY:
289                 return getString(com.android.settingslib.R.string.failed_attempts_now_wiping_user);
290             default:
291                 throw new IllegalArgumentException("Unrecognized user type:" + userType);
292         }
293     }
294 
295     private final Runnable mResetErrorRunnable = new Runnable() {
296         @Override
297         public void run() {
298             mErrorTextView.setText("");
299         }
300     };
301 
showError(CharSequence msg, long timeout)302     protected void showError(CharSequence msg, long timeout) {
303         mErrorTextView.setText(msg);
304         onShowError();
305         mHandler.removeCallbacks(mResetErrorRunnable);
306         if (timeout != 0) {
307             mHandler.postDelayed(mResetErrorRunnable, timeout);
308         }
309     }
310 
onShowError()311     protected abstract void onShowError();
312 
showError(int msg, long timeout)313     protected void showError(int msg, long timeout) {
314         showError(getText(msg), timeout);
315     }
316 
317     public static class LastTryDialog extends DialogFragment {
318         private static final String TAG = LastTryDialog.class.getSimpleName();
319 
320         private static final String ARG_TITLE = "title";
321         private static final String ARG_MESSAGE = "message";
322         private static final String ARG_BUTTON = "button";
323         private static final String ARG_DISMISS = "dismiss";
324 
show(FragmentManager from, String title, String message, int button, boolean dismiss)325         static boolean show(FragmentManager from, String title, String message, int button,
326                 boolean dismiss) {
327             LastTryDialog existent = (LastTryDialog) from.findFragmentByTag(TAG);
328             if (existent != null && !existent.isRemoving()) {
329                 return false;
330             }
331             Bundle args = new Bundle();
332             args.putString(ARG_TITLE, title);
333             args.putString(ARG_MESSAGE, message);
334             args.putInt(ARG_BUTTON, button);
335             args.putBoolean(ARG_DISMISS, dismiss);
336 
337             DialogFragment dialog = new LastTryDialog();
338             dialog.setArguments(args);
339             dialog.show(from, TAG);
340             from.executePendingTransactions();
341             return true;
342         }
343 
hide(FragmentManager from)344         static void hide(FragmentManager from) {
345             LastTryDialog dialog = (LastTryDialog) from.findFragmentByTag(TAG);
346             if (dialog != null) {
347                 dialog.dismissAllowingStateLoss();
348                 from.executePendingTransactions();
349             }
350         }
351 
352         /**
353          * Dialog setup.
354          * <p>
355          * To make it less likely that the dialog is dismissed accidentally, for example if the
356          * device is malfunctioning or if the device is in a pocket, we set
357          * {@code setCanceledOnTouchOutside(false)}.
358          */
359         @Override
onCreateDialog(Bundle savedInstanceState)360         public Dialog onCreateDialog(Bundle savedInstanceState) {
361             Dialog dialog = new AlertDialog.Builder(getActivity())
362                     .setTitle(getArguments().getString(ARG_TITLE))
363                     .setMessage(getArguments().getString(ARG_MESSAGE))
364                     .setPositiveButton(getArguments().getInt(ARG_BUTTON), null)
365                     .create();
366             dialog.setCanceledOnTouchOutside(false);
367             return dialog;
368         }
369 
370         @Override
onDismiss(final DialogInterface dialog)371         public void onDismiss(final DialogInterface dialog) {
372             super.onDismiss(dialog);
373             if (getActivity() != null && getArguments().getBoolean(ARG_DISMISS)) {
374                 getActivity().finish();
375             }
376         }
377     }
378 }
379