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