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