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.fingerprint; 18 19 import android.content.Context; 20 import android.graphics.Color; 21 import android.graphics.PixelFormat; 22 import android.graphics.drawable.AnimatedVectorDrawable; 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.text.TextUtils; 30 import android.util.DisplayMetrics; 31 import android.util.Log; 32 import android.view.KeyEvent; 33 import android.view.LayoutInflater; 34 import android.view.MotionEvent; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.WindowManager; 38 import android.view.animation.Interpolator; 39 import android.widget.Button; 40 import android.widget.ImageView; 41 import android.widget.LinearLayout; 42 import android.widget.TextView; 43 44 import com.android.systemui.Interpolators; 45 import com.android.systemui.R; 46 47 /** 48 * This class loads the view for the system-provided dialog. The view consists of: 49 * Application Icon, Title, Subtitle, Description, Fingerprint Icon, Error/Help message area, 50 * and positive/negative buttons. 51 */ 52 public class FingerprintDialogView extends LinearLayout { 53 54 private static final String TAG = "FingerprintDialogView"; 55 56 private static final int ANIMATION_DURATION_SHOW = 250; // ms 57 private static final int ANIMATION_DURATION_AWAY = 350; // ms 58 59 private static final int STATE_NONE = 0; 60 private static final int STATE_FINGERPRINT = 1; 61 private static final int STATE_FINGERPRINT_ERROR = 2; 62 private static final int STATE_FINGERPRINT_AUTHENTICATED = 3; 63 64 private final IBinder mWindowToken = new Binder(); 65 private final Interpolator mLinearOutSlowIn; 66 private final WindowManager mWindowManager; 67 private final float mAnimationTranslationOffset; 68 private final int mErrorColor; 69 private final int mTextColor; 70 private final int mFingerprintColor; 71 72 private ViewGroup mLayout; 73 private final TextView mErrorText; 74 private Handler mHandler; 75 private Bundle mBundle; 76 private final LinearLayout mDialog; 77 private int mLastState; 78 private boolean mAnimatingAway; 79 private boolean mWasForceRemoved; 80 81 private final float mDisplayWidth; 82 83 private final Runnable mShowAnimationRunnable = new Runnable() { 84 @Override 85 public void run() { 86 mLayout.animate() 87 .alpha(1f) 88 .setDuration(ANIMATION_DURATION_SHOW) 89 .setInterpolator(mLinearOutSlowIn) 90 .withLayer() 91 .start(); 92 mDialog.animate() 93 .translationY(0) 94 .setDuration(ANIMATION_DURATION_SHOW) 95 .setInterpolator(mLinearOutSlowIn) 96 .withLayer() 97 .start(); 98 } 99 }; 100 FingerprintDialogView(Context context, Handler handler)101 public FingerprintDialogView(Context context, Handler handler) { 102 super(context); 103 mHandler = handler; 104 mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; 105 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 106 mAnimationTranslationOffset = getResources() 107 .getDimension(R.dimen.fingerprint_dialog_animation_translation_offset); 108 mErrorColor = Color.parseColor( 109 getResources().getString(R.color.fingerprint_dialog_error_color)); 110 mTextColor = Color.parseColor( 111 getResources().getString(R.color.fingerprint_dialog_text_light_color)); 112 mFingerprintColor = Color.parseColor( 113 getResources().getString(R.color.fingerprint_dialog_fingerprint_color)); 114 115 DisplayMetrics metrics = new DisplayMetrics(); 116 mWindowManager.getDefaultDisplay().getMetrics(metrics); 117 mDisplayWidth = metrics.widthPixels; 118 119 // Create the dialog 120 LayoutInflater factory = LayoutInflater.from(getContext()); 121 mLayout = (ViewGroup) factory.inflate(R.layout.fingerprint_dialog, this, false); 122 addView(mLayout); 123 124 mDialog = mLayout.findViewById(R.id.dialog); 125 126 mErrorText = mLayout.findViewById(R.id.error); 127 128 mLayout.setOnKeyListener(new View.OnKeyListener() { 129 boolean downPressed = false; 130 @Override 131 public boolean onKey(View v, int keyCode, KeyEvent event) { 132 if (keyCode != KeyEvent.KEYCODE_BACK) { 133 return false; 134 } 135 if (event.getAction() == KeyEvent.ACTION_DOWN && downPressed == false) { 136 downPressed = true; 137 } else if (event.getAction() == KeyEvent.ACTION_DOWN) { 138 downPressed = false; 139 } else if (event.getAction() == KeyEvent.ACTION_UP && downPressed == true) { 140 downPressed = false; 141 mHandler.obtainMessage(FingerprintDialogImpl.MSG_USER_CANCELED).sendToTarget(); 142 } 143 return true; 144 } 145 }); 146 147 final View space = mLayout.findViewById(R.id.space); 148 final View leftSpace = mLayout.findViewById(R.id.left_space); 149 final View rightSpace = mLayout.findViewById(R.id.right_space); 150 final Button negative = mLayout.findViewById(R.id.button2); 151 final Button positive = mLayout.findViewById(R.id.button1); 152 153 setDismissesDialog(space); 154 setDismissesDialog(leftSpace); 155 setDismissesDialog(rightSpace); 156 157 negative.setOnClickListener((View v) -> { 158 mHandler.obtainMessage(FingerprintDialogImpl.MSG_BUTTON_NEGATIVE).sendToTarget(); 159 }); 160 161 positive.setOnClickListener((View v) -> { 162 mHandler.obtainMessage(FingerprintDialogImpl.MSG_BUTTON_POSITIVE).sendToTarget(); 163 }); 164 165 mLayout.setFocusableInTouchMode(true); 166 mLayout.requestFocus(); 167 } 168 169 @Override onAttachedToWindow()170 public void onAttachedToWindow() { 171 super.onAttachedToWindow(); 172 173 final TextView title = mLayout.findViewById(R.id.title); 174 final TextView subtitle = mLayout.findViewById(R.id.subtitle); 175 final TextView description = mLayout.findViewById(R.id.description); 176 final Button negative = mLayout.findViewById(R.id.button2); 177 final Button positive = mLayout.findViewById(R.id.button1); 178 179 mDialog.getLayoutParams().width = (int) mDisplayWidth; 180 181 mLastState = STATE_NONE; 182 updateFingerprintIcon(STATE_FINGERPRINT); 183 184 title.setText(mBundle.getCharSequence(BiometricPrompt.KEY_TITLE)); 185 title.setSelected(true); 186 187 final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE); 188 if (TextUtils.isEmpty(subtitleText)) { 189 subtitle.setVisibility(View.GONE); 190 } else { 191 subtitle.setVisibility(View.VISIBLE); 192 subtitle.setText(subtitleText); 193 } 194 195 final CharSequence descriptionText = mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION); 196 if (TextUtils.isEmpty(descriptionText)) { 197 description.setVisibility(View.GONE); 198 } else { 199 description.setVisibility(View.VISIBLE); 200 description.setText(descriptionText); 201 } 202 203 negative.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT)); 204 205 final CharSequence positiveText = 206 mBundle.getCharSequence(BiometricPrompt.KEY_POSITIVE_TEXT); 207 positive.setText(positiveText); // needs to be set for marquee to work 208 if (positiveText != null) { 209 positive.setVisibility(View.VISIBLE); 210 } else { 211 positive.setVisibility(View.GONE); 212 } 213 214 if (!mWasForceRemoved) { 215 // Dim the background and slide the dialog up 216 mDialog.setTranslationY(mAnimationTranslationOffset); 217 mLayout.setAlpha(0f); 218 postOnAnimation(mShowAnimationRunnable); 219 } else { 220 // Show the dialog immediately 221 mLayout.animate().cancel(); 222 mDialog.animate().cancel(); 223 mDialog.setAlpha(1.0f); 224 mDialog.setTranslationY(0); 225 mLayout.setAlpha(1.0f); 226 } 227 mWasForceRemoved = false; 228 } 229 setDismissesDialog(View v)230 private void setDismissesDialog(View v) { 231 v.setClickable(true); 232 v.setOnTouchListener((View view, MotionEvent event) -> { 233 mHandler.obtainMessage(FingerprintDialogImpl.MSG_HIDE_DIALOG, true /* userCanceled */) 234 .sendToTarget(); 235 return true; 236 }); 237 } 238 startDismiss()239 public void startDismiss() { 240 mAnimatingAway = true; 241 242 final Runnable endActionRunnable = new Runnable() { 243 @Override 244 public void run() { 245 mWindowManager.removeView(FingerprintDialogView.this); 246 mAnimatingAway = false; 247 } 248 }; 249 250 postOnAnimation(new Runnable() { 251 @Override 252 public void run() { 253 mLayout.animate() 254 .alpha(0f) 255 .setDuration(ANIMATION_DURATION_AWAY) 256 .setInterpolator(mLinearOutSlowIn) 257 .withLayer() 258 .start(); 259 mDialog.animate() 260 .translationY(mAnimationTranslationOffset) 261 .setDuration(ANIMATION_DURATION_AWAY) 262 .setInterpolator(mLinearOutSlowIn) 263 .withLayer() 264 .withEndAction(endActionRunnable) 265 .start(); 266 } 267 }); 268 } 269 270 /** 271 * Force remove the window, cancelling any animation that's happening. This should only be 272 * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method 273 * will cause the dialog to show without an animation the next time it's attached. 274 */ forceRemove()275 public void forceRemove() { 276 mLayout.animate().cancel(); 277 mDialog.animate().cancel(); 278 mWindowManager.removeView(FingerprintDialogView.this); 279 mAnimatingAway = false; 280 mWasForceRemoved = true; 281 } 282 isAnimatingAway()283 public boolean isAnimatingAway() { 284 return mAnimatingAway; 285 } 286 setBundle(Bundle bundle)287 public void setBundle(Bundle bundle) { 288 mBundle = bundle; 289 } 290 291 // Clears the temporary message and shows the help message. resetMessage()292 protected void resetMessage() { 293 updateFingerprintIcon(STATE_FINGERPRINT); 294 mErrorText.setText(R.string.fingerprint_dialog_touch_sensor); 295 mErrorText.setTextColor(mTextColor); 296 } 297 298 // Shows an error/help message showTemporaryMessage(String message)299 private void showTemporaryMessage(String message) { 300 mHandler.removeMessages(FingerprintDialogImpl.MSG_CLEAR_MESSAGE); 301 updateFingerprintIcon(STATE_FINGERPRINT_ERROR); 302 mErrorText.setText(message); 303 mErrorText.setTextColor(mErrorColor); 304 mErrorText.setContentDescription(message); 305 mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_CLEAR_MESSAGE), 306 BiometricPrompt.HIDE_DIALOG_DELAY); 307 } 308 showHelpMessage(String message)309 public void showHelpMessage(String message) { 310 showTemporaryMessage(message); 311 } 312 showErrorMessage(String error)313 public void showErrorMessage(String error) { 314 showTemporaryMessage(error); 315 mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_HIDE_DIALOG, 316 false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY); 317 } 318 updateFingerprintIcon(int newState)319 private void updateFingerprintIcon(int newState) { 320 Drawable icon = getAnimationForTransition(mLastState, newState); 321 322 if (icon == null) { 323 Log.e(TAG, "Animation not found"); 324 return; 325 } 326 327 final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable 328 ? (AnimatedVectorDrawable) icon 329 : null; 330 331 final ImageView fingerprint_icon = mLayout.findViewById(R.id.fingerprint_icon); 332 fingerprint_icon.setImageDrawable(icon); 333 334 if (animation != null && shouldAnimateForTransition(mLastState, newState)) { 335 animation.forceAnimationOnUI(); 336 animation.start(); 337 } 338 339 mLastState = newState; 340 } 341 shouldAnimateForTransition(int oldState, int newState)342 private boolean shouldAnimateForTransition(int oldState, int newState) { 343 if (oldState == STATE_NONE && newState == STATE_FINGERPRINT) { 344 return false; 345 } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) { 346 return true; 347 } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) { 348 return true; 349 } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_AUTHENTICATED) { 350 // TODO(b/77328470): add animation when fingerprint is authenticated 351 return false; 352 } 353 return false; 354 } 355 getAnimationForTransition(int oldState, int newState)356 private Drawable getAnimationForTransition(int oldState, int newState) { 357 int iconRes; 358 if (oldState == STATE_NONE && newState == STATE_FINGERPRINT) { 359 iconRes = R.drawable.fingerprint_dialog_fp_to_error; 360 } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) { 361 iconRes = R.drawable.fingerprint_dialog_fp_to_error; 362 } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) { 363 iconRes = R.drawable.fingerprint_dialog_error_to_fp; 364 } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_AUTHENTICATED) { 365 // TODO(b/77328470): add animation when fingerprint is authenticated 366 iconRes = R.drawable.fingerprint_dialog_error_to_fp; 367 } 368 else { 369 return null; 370 } 371 return mContext.getDrawable(iconRes); 372 } 373 getLayoutParams()374 public WindowManager.LayoutParams getLayoutParams() { 375 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 376 ViewGroup.LayoutParams.MATCH_PARENT, 377 ViewGroup.LayoutParams.MATCH_PARENT, 378 WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL, 379 WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 380 PixelFormat.TRANSLUCENT); 381 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; 382 lp.setTitle("FingerprintDialogView"); 383 lp.token = mWindowToken; 384 return lp; 385 } 386 } 387