1 /* 2 * Copyright (C) 2018 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 package com.android.systemui.biometrics; 18 19 import android.app.admin.DevicePolicyManager; 20 import android.content.Context; 21 import android.graphics.PixelFormat; 22 import android.graphics.PorterDuff; 23 import android.graphics.drawable.Drawable; 24 import android.hardware.biometrics.BiometricPrompt; 25 import android.os.Binder; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.IBinder; 29 import android.os.Message; 30 import android.os.UserManager; 31 import android.text.TextUtils; 32 import android.util.DisplayMetrics; 33 import android.util.Log; 34 import android.view.KeyEvent; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.WindowManager; 39 import android.view.animation.Interpolator; 40 import android.widget.Button; 41 import android.widget.ImageView; 42 import android.widget.LinearLayout; 43 import android.widget.TextView; 44 45 import com.android.systemui.Interpolators; 46 import com.android.systemui.R; 47 import com.android.systemui.util.leak.RotationUtils; 48 49 /** 50 * Abstract base class. Shows a dialog for BiometricPrompt. 51 */ 52 public abstract class BiometricDialogView extends LinearLayout { 53 54 private static final String TAG = "BiometricDialogView"; 55 56 private static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility"; 57 private static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility"; 58 private static final String KEY_STATE = "key_state"; 59 private static final String KEY_ERROR_TEXT_VISIBILITY = "key_error_text_visibility"; 60 private static final String KEY_ERROR_TEXT_STRING = "key_error_text_string"; 61 private static final String KEY_ERROR_TEXT_IS_TEMPORARY = "key_error_text_is_temporary"; 62 private static final String KEY_ERROR_TEXT_COLOR = "key_error_text_color"; 63 64 private static final int ANIMATION_DURATION_SHOW = 250; // ms 65 private static final int ANIMATION_DURATION_AWAY = 350; // ms 66 67 protected static final int MSG_RESET_MESSAGE = 1; 68 69 protected static final int STATE_IDLE = 0; 70 protected static final int STATE_AUTHENTICATING = 1; 71 protected static final int STATE_ERROR = 2; 72 protected static final int STATE_PENDING_CONFIRMATION = 3; 73 protected static final int STATE_AUTHENTICATED = 4; 74 75 private final IBinder mWindowToken = new Binder(); 76 private final Interpolator mLinearOutSlowIn; 77 private final WindowManager mWindowManager; 78 private final UserManager mUserManager; 79 private final DevicePolicyManager mDevicePolicyManager; 80 private final float mAnimationTranslationOffset; 81 private final int mErrorColor; 82 private final float mDialogWidth; 83 protected final DialogViewCallback mCallback; 84 85 protected final ViewGroup mLayout; 86 protected final LinearLayout mDialog; 87 protected final TextView mTitleText; 88 protected final TextView mSubtitleText; 89 protected final TextView mDescriptionText; 90 protected final ImageView mBiometricIcon; 91 protected final TextView mErrorText; 92 protected final Button mPositiveButton; 93 protected final Button mNegativeButton; 94 protected final Button mTryAgainButton; 95 96 protected final int mTextColor; 97 98 private Bundle mBundle; 99 private Bundle mRestoredState; 100 101 private int mState = STATE_IDLE; 102 private boolean mAnimatingAway; 103 private boolean mWasForceRemoved; 104 private boolean mSkipIntro; 105 protected boolean mRequireConfirmation; 106 private int mUserId; // used to determine if we should show work background 107 getHintStringResourceId()108 protected abstract int getHintStringResourceId(); getAuthenticatedAccessibilityResourceId()109 protected abstract int getAuthenticatedAccessibilityResourceId(); getIconDescriptionResourceId()110 protected abstract int getIconDescriptionResourceId(); getDelayAfterAuthenticatedDurationMs()111 protected abstract int getDelayAfterAuthenticatedDurationMs(); shouldGrayAreaDismissDialog()112 protected abstract boolean shouldGrayAreaDismissDialog(); handleResetMessage()113 protected abstract void handleResetMessage(); updateIcon(int oldState, int newState)114 protected abstract void updateIcon(int oldState, int newState); 115 116 private final Runnable mShowAnimationRunnable = new Runnable() { 117 @Override 118 public void run() { 119 mLayout.animate() 120 .alpha(1f) 121 .setDuration(ANIMATION_DURATION_SHOW) 122 .setInterpolator(mLinearOutSlowIn) 123 .withLayer() 124 .start(); 125 mDialog.animate() 126 .translationY(0) 127 .setDuration(ANIMATION_DURATION_SHOW) 128 .setInterpolator(mLinearOutSlowIn) 129 .withLayer() 130 .withEndAction(() -> onDialogAnimatedIn()) 131 .start(); 132 } 133 }; 134 135 protected Handler mHandler = new Handler() { 136 @Override 137 public void handleMessage(Message msg) { 138 switch(msg.what) { 139 case MSG_RESET_MESSAGE: 140 handleResetMessage(); 141 break; 142 default: 143 Log.e(TAG, "Unhandled message: " + msg.what); 144 break; 145 } 146 } 147 }; 148 BiometricDialogView(Context context, DialogViewCallback callback)149 public BiometricDialogView(Context context, DialogViewCallback callback) { 150 super(context); 151 mCallback = callback; 152 mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; 153 mWindowManager = mContext.getSystemService(WindowManager.class); 154 mUserManager = mContext.getSystemService(UserManager.class); 155 mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); 156 mAnimationTranslationOffset = getResources() 157 .getDimension(R.dimen.biometric_dialog_animation_translation_offset); 158 mErrorColor = getResources().getColor(R.color.biometric_dialog_error); 159 mTextColor = getResources().getColor(R.color.biometric_dialog_gray); 160 161 DisplayMetrics metrics = new DisplayMetrics(); 162 mWindowManager.getDefaultDisplay().getMetrics(metrics); 163 mDialogWidth = Math.min(metrics.widthPixels, metrics.heightPixels); 164 165 // Create the dialog 166 LayoutInflater factory = LayoutInflater.from(getContext()); 167 mLayout = (ViewGroup) factory.inflate(R.layout.biometric_dialog, this, false); 168 addView(mLayout); 169 170 mLayout.setOnKeyListener(new View.OnKeyListener() { 171 boolean downPressed = false; 172 @Override 173 public boolean onKey(View v, int keyCode, KeyEvent event) { 174 if (keyCode != KeyEvent.KEYCODE_BACK) { 175 return false; 176 } 177 if (event.getAction() == KeyEvent.ACTION_DOWN && downPressed == false) { 178 downPressed = true; 179 } else if (event.getAction() == KeyEvent.ACTION_DOWN) { 180 downPressed = false; 181 } else if (event.getAction() == KeyEvent.ACTION_UP && downPressed == true) { 182 downPressed = false; 183 mCallback.onUserCanceled(); 184 } 185 return true; 186 } 187 }); 188 189 final View space = mLayout.findViewById(R.id.space); 190 final View leftSpace = mLayout.findViewById(R.id.left_space); 191 final View rightSpace = mLayout.findViewById(R.id.right_space); 192 193 mDialog = mLayout.findViewById(R.id.dialog); 194 mTitleText = mLayout.findViewById(R.id.title); 195 mSubtitleText = mLayout.findViewById(R.id.subtitle); 196 mDescriptionText = mLayout.findViewById(R.id.description); 197 mBiometricIcon = mLayout.findViewById(R.id.biometric_icon); 198 mErrorText = mLayout.findViewById(R.id.error); 199 mNegativeButton = mLayout.findViewById(R.id.button2); 200 mPositiveButton = mLayout.findViewById(R.id.button1); 201 mTryAgainButton = mLayout.findViewById(R.id.button_try_again); 202 203 mBiometricIcon.setContentDescription( 204 getResources().getString(getIconDescriptionResourceId())); 205 206 setDismissesDialog(space); 207 setDismissesDialog(leftSpace); 208 setDismissesDialog(rightSpace); 209 210 mNegativeButton.setOnClickListener((View v) -> { 211 if (mState == STATE_PENDING_CONFIRMATION || mState == STATE_AUTHENTICATED) { 212 mCallback.onUserCanceled(); 213 } else { 214 mCallback.onNegativePressed(); 215 } 216 }); 217 218 mPositiveButton.setOnClickListener((View v) -> { 219 updateState(STATE_AUTHENTICATED); 220 mHandler.postDelayed(() -> { 221 mCallback.onPositivePressed(); 222 }, getDelayAfterAuthenticatedDurationMs()); 223 }); 224 225 mTryAgainButton.setOnClickListener((View v) -> { 226 handleResetMessage(); 227 updateState(STATE_AUTHENTICATING); 228 showTryAgainButton(false /* show */); 229 mCallback.onTryAgainPressed(); 230 }); 231 232 // Must set these in order for the back button events to be received. 233 mLayout.setFocusableInTouchMode(true); 234 mLayout.requestFocus(); 235 } 236 onSaveState(Bundle bundle)237 public void onSaveState(Bundle bundle) { 238 bundle.putInt(KEY_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility()); 239 bundle.putInt(KEY_CONFIRM_VISIBILITY, mPositiveButton.getVisibility()); 240 bundle.putInt(KEY_STATE, mState); 241 bundle.putInt(KEY_ERROR_TEXT_VISIBILITY, mErrorText.getVisibility()); 242 bundle.putCharSequence(KEY_ERROR_TEXT_STRING, mErrorText.getText()); 243 bundle.putBoolean(KEY_ERROR_TEXT_IS_TEMPORARY, mHandler.hasMessages(MSG_RESET_MESSAGE)); 244 bundle.putInt(KEY_ERROR_TEXT_COLOR, mErrorText.getCurrentTextColor()); 245 } 246 247 @Override onAttachedToWindow()248 public void onAttachedToWindow() { 249 super.onAttachedToWindow(); 250 251 final ImageView backgroundView = mLayout.findViewById(R.id.background); 252 253 if (mUserManager.isManagedProfile(mUserId)) { 254 final Drawable image = getResources().getDrawable(R.drawable.work_challenge_background, 255 mContext.getTheme()); 256 image.setColorFilter(mDevicePolicyManager.getOrganizationColorForUser(mUserId), 257 PorterDuff.Mode.DARKEN); 258 backgroundView.setImageDrawable(image); 259 } else { 260 backgroundView.setImageDrawable(null); 261 backgroundView.setBackgroundColor(R.color.biometric_dialog_dim_color); 262 } 263 264 mNegativeButton.setVisibility(View.VISIBLE); 265 266 if (RotationUtils.getRotation(mContext) != RotationUtils.ROTATION_NONE) { 267 mDialog.getLayoutParams().width = (int) mDialogWidth; 268 } 269 270 if (mRestoredState == null) { 271 updateState(STATE_AUTHENTICATING); 272 mErrorText.setText(getHintStringResourceId()); 273 mErrorText.setContentDescription(mContext.getString(getHintStringResourceId())); 274 mErrorText.setVisibility(View.VISIBLE); 275 } else { 276 updateState(mState); 277 } 278 279 CharSequence titleText = mBundle.getCharSequence(BiometricPrompt.KEY_TITLE); 280 281 mTitleText.setVisibility(View.VISIBLE); 282 mTitleText.setText(titleText); 283 284 final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE); 285 if (TextUtils.isEmpty(subtitleText)) { 286 mSubtitleText.setVisibility(View.GONE); 287 } else { 288 mSubtitleText.setVisibility(View.VISIBLE); 289 mSubtitleText.setText(subtitleText); 290 } 291 292 final CharSequence descriptionText = 293 mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION); 294 if (TextUtils.isEmpty(descriptionText)) { 295 mDescriptionText.setVisibility(View.GONE); 296 } else { 297 mDescriptionText.setVisibility(View.VISIBLE); 298 mDescriptionText.setText(descriptionText); 299 } 300 301 mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT)); 302 303 if (requiresConfirmation() && mRestoredState == null) { 304 mPositiveButton.setVisibility(View.VISIBLE); 305 mPositiveButton.setEnabled(false); 306 } 307 308 if (mWasForceRemoved || mSkipIntro) { 309 // Show the dialog immediately 310 mLayout.animate().cancel(); 311 mDialog.animate().cancel(); 312 mDialog.setAlpha(1.0f); 313 mDialog.setTranslationY(0); 314 mLayout.setAlpha(1.0f); 315 } else { 316 // Dim the background and slide the dialog up 317 mDialog.setTranslationY(mAnimationTranslationOffset); 318 mLayout.setAlpha(0f); 319 postOnAnimation(mShowAnimationRunnable); 320 } 321 mWasForceRemoved = false; 322 mSkipIntro = false; 323 } 324 setDismissesDialog(View v)325 private void setDismissesDialog(View v) { 326 v.setClickable(true); 327 v.setOnClickListener(v1 -> { 328 if (mState != STATE_AUTHENTICATED && shouldGrayAreaDismissDialog()) { 329 mCallback.onUserCanceled(); 330 } 331 }); 332 } 333 startDismiss()334 public void startDismiss() { 335 mAnimatingAway = true; 336 337 // This is where final cleanup should occur. 338 final Runnable endActionRunnable = new Runnable() { 339 @Override 340 public void run() { 341 mWindowManager.removeView(BiometricDialogView.this); 342 mAnimatingAway = false; 343 // Set the icons / text back to normal state 344 handleResetMessage(); 345 showTryAgainButton(false /* show */); 346 updateState(STATE_IDLE); 347 } 348 }; 349 350 postOnAnimation(new Runnable() { 351 @Override 352 public void run() { 353 mLayout.animate() 354 .alpha(0f) 355 .setDuration(ANIMATION_DURATION_AWAY) 356 .setInterpolator(mLinearOutSlowIn) 357 .withLayer() 358 .start(); 359 mDialog.animate() 360 .translationY(mAnimationTranslationOffset) 361 .setDuration(ANIMATION_DURATION_AWAY) 362 .setInterpolator(mLinearOutSlowIn) 363 .withLayer() 364 .withEndAction(endActionRunnable) 365 .start(); 366 } 367 }); 368 } 369 370 /** 371 * Force remove the window, cancelling any animation that's happening. This should only be 372 * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method 373 * will cause the dialog to show without an animation the next time it's attached. 374 */ forceRemove()375 public void forceRemove() { 376 mLayout.animate().cancel(); 377 mDialog.animate().cancel(); 378 mWindowManager.removeView(BiometricDialogView.this); 379 mAnimatingAway = false; 380 mWasForceRemoved = true; 381 } 382 383 /** 384 * Skip the intro animation 385 */ setSkipIntro(boolean skip)386 public void setSkipIntro(boolean skip) { 387 mSkipIntro = skip; 388 } 389 isAnimatingAway()390 public boolean isAnimatingAway() { 391 return mAnimatingAway; 392 } 393 setBundle(Bundle bundle)394 public void setBundle(Bundle bundle) { 395 mBundle = bundle; 396 } 397 setRequireConfirmation(boolean requireConfirmation)398 public void setRequireConfirmation(boolean requireConfirmation) { 399 mRequireConfirmation = requireConfirmation; 400 } 401 requiresConfirmation()402 public boolean requiresConfirmation() { 403 return mRequireConfirmation; 404 } 405 setUserId(int userId)406 public void setUserId(int userId) { 407 mUserId = userId; 408 } 409 getLayout()410 public ViewGroup getLayout() { 411 return mLayout; 412 } 413 414 // Shows an error/help message showTemporaryMessage(String message)415 protected void showTemporaryMessage(String message) { 416 mHandler.removeMessages(MSG_RESET_MESSAGE); 417 mErrorText.setText(message); 418 mErrorText.setTextColor(mErrorColor); 419 mErrorText.setContentDescription(message); 420 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE), 421 BiometricPrompt.HIDE_DIALOG_DELAY); 422 } 423 424 /** 425 * Transient help message (acquire) is received, dialog stays showing. Sensor stays in 426 * "authenticating" state. 427 * @param message 428 */ onHelpReceived(String message)429 public void onHelpReceived(String message) { 430 updateState(STATE_ERROR); 431 showTemporaryMessage(message); 432 } 433 onAuthenticationFailed(String message)434 public void onAuthenticationFailed(String message) { 435 updateState(STATE_ERROR); 436 showTemporaryMessage(message); 437 } 438 439 /** 440 * Hard error is received, dialog will be dismissed soon. 441 * @param error 442 */ onErrorReceived(String error)443 public void onErrorReceived(String error) { 444 updateState(STATE_ERROR); 445 showTemporaryMessage(error); 446 showTryAgainButton(false /* show */); 447 mCallback.onErrorShown(); // TODO: Split between fp and face 448 } 449 updateState(int newState)450 public void updateState(int newState) { 451 if (newState == STATE_PENDING_CONFIRMATION) { 452 mHandler.removeMessages(MSG_RESET_MESSAGE); 453 mErrorText.setVisibility(View.INVISIBLE); 454 mPositiveButton.setVisibility(View.VISIBLE); 455 mPositiveButton.setEnabled(true); 456 } else if (newState == STATE_AUTHENTICATED) { 457 mPositiveButton.setVisibility(View.GONE); 458 mNegativeButton.setVisibility(View.GONE); 459 mErrorText.setVisibility(View.INVISIBLE); 460 } 461 462 if (newState == STATE_PENDING_CONFIRMATION || newState == STATE_AUTHENTICATED) { 463 mNegativeButton.setText(R.string.cancel); 464 } 465 466 updateIcon(mState, newState); 467 mState = newState; 468 } 469 showTryAgainButton(boolean show)470 public void showTryAgainButton(boolean show) { 471 } 472 onDialogAnimatedIn()473 public void onDialogAnimatedIn() { 474 } 475 restoreState(Bundle bundle)476 public void restoreState(Bundle bundle) { 477 mRestoredState = bundle; 478 mTryAgainButton.setVisibility(bundle.getInt(KEY_TRY_AGAIN_VISIBILITY)); 479 mPositiveButton.setVisibility(bundle.getInt(KEY_CONFIRM_VISIBILITY)); 480 mState = bundle.getInt(KEY_STATE); 481 mErrorText.setText(bundle.getCharSequence(KEY_ERROR_TEXT_STRING)); 482 mErrorText.setContentDescription(bundle.getCharSequence(KEY_ERROR_TEXT_STRING)); 483 mErrorText.setVisibility(bundle.getInt(KEY_ERROR_TEXT_VISIBILITY)); 484 mErrorText.setTextColor(bundle.getInt(KEY_ERROR_TEXT_COLOR)); 485 486 if (bundle.getBoolean(KEY_ERROR_TEXT_IS_TEMPORARY)) { 487 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE), 488 BiometricPrompt.HIDE_DIALOG_DELAY); 489 } 490 } 491 getState()492 protected int getState() { 493 return mState; 494 } 495 getLayoutParams()496 public WindowManager.LayoutParams getLayoutParams() { 497 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 498 ViewGroup.LayoutParams.MATCH_PARENT, 499 ViewGroup.LayoutParams.MATCH_PARENT, 500 WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL, 501 WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 502 PixelFormat.TRANSLUCENT); 503 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; 504 lp.setTitle("BiometricDialogView"); 505 lp.token = mWindowToken; 506 return lp; 507 } 508 } 509