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