blob: 8013a9e29dfd2af56083d9369d2cc4d807175e33 [file] [log] [blame]
Kevin Chyn42653e82018-01-19 14:15:46 -08001/*
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
17package com.android.systemui.fingerprint;
18
Kevin Chyn42653e82018-01-19 14:15:46 -080019import android.content.Context;
Kevin Chynd0544dc2018-02-06 16:30:31 -080020import android.graphics.Color;
Kevin Chyn42653e82018-01-19 14:15:46 -080021import android.graphics.PixelFormat;
Kevin Chynd0544dc2018-02-06 16:30:31 -080022import android.graphics.drawable.AnimatedVectorDrawable;
Kevin Chyn42653e82018-01-19 14:15:46 -080023import android.graphics.drawable.Drawable;
Vishwath Mohanecf00ce2018-04-05 10:28:24 -070024import android.hardware.biometrics.BiometricPrompt;
Kevin Chyn42653e82018-01-19 14:15:46 -080025import android.os.Binder;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.IBinder;
Kevin Chyna4596f32018-04-05 11:33:12 -070029import android.text.TextUtils;
Kevin Chyn27ce1942018-03-12 14:42:17 -070030import android.util.DisplayMetrics;
Kevin Chynd0544dc2018-02-06 16:30:31 -080031import android.util.Log;
Kevin Chyn42653e82018-01-19 14:15:46 -080032import android.view.KeyEvent;
33import android.view.LayoutInflater;
34import android.view.MotionEvent;
35import android.view.View;
36import android.view.ViewGroup;
Kevin Chyn42653e82018-01-19 14:15:46 -080037import android.view.WindowManager;
Kevin Chyn42653e82018-01-19 14:15:46 -080038import android.view.animation.Interpolator;
39import android.widget.Button;
40import android.widget.ImageView;
41import android.widget.LinearLayout;
42import android.widget.TextView;
43
Kevin Chyne8f3e1b2018-01-23 17:33:58 -080044import com.android.systemui.Interpolators;
Kevin Chyn42653e82018-01-19 14:15:46 -080045import com.android.systemui.R;
Kevin Chyn42653e82018-01-19 14:15:46 -080046
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 */
52public class FingerprintDialogView extends LinearLayout {
53
54 private static final String TAG = "FingerprintDialogView";
55
Kevin Chyndba919a2018-03-16 14:35:10 -070056 private static final int ANIMATION_DURATION_SHOW = 250; // ms
57 private static final int ANIMATION_DURATION_AWAY = 350; // ms
Kevin Chyn42653e82018-01-19 14:15:46 -080058
Kevin Chynd0544dc2018-02-06 16:30:31 -080059 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
Kevin Chyn42653e82018-01-19 14:15:46 -080064 private final IBinder mWindowToken = new Binder();
Kevin Chyn42653e82018-01-19 14:15:46 -080065 private final Interpolator mLinearOutSlowIn;
Kevin Chyn27e1f262018-03-08 16:38:32 -080066 private final WindowManager mWindowManager;
Kevin Chyne8f3e1b2018-01-23 17:33:58 -080067 private final float mAnimationTranslationOffset;
Kevin Chyn178ace52018-03-30 12:36:32 -070068 private final int mErrorColor;
Kevin Chynd0544dc2018-02-06 16:30:31 -080069 private final int mTextColor;
70 private final int mFingerprintColor;
Kevin Chyn42653e82018-01-19 14:15:46 -080071
72 private ViewGroup mLayout;
73 private final TextView mErrorText;
74 private Handler mHandler;
75 private Bundle mBundle;
Kevin Chyn42653e82018-01-19 14:15:46 -080076 private final LinearLayout mDialog;
Kevin Chynd0544dc2018-02-06 16:30:31 -080077 private int mLastState;
Kevin Chyn87df0682018-04-10 19:29:23 -070078 private boolean mAnimatingAway;
79 private boolean mWasForceRemoved;
Kevin Chyn42653e82018-01-19 14:15:46 -080080
Kevin Chyn27ce1942018-03-12 14:42:17 -070081 private final float mDisplayWidth;
82
Kevin Chyn87df0682018-04-10 19:29:23 -070083 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
Kevin Chyn42653e82018-01-19 14:15:46 -0800101 public FingerprintDialogView(Context context, Handler handler) {
102 super(context);
103 mHandler = handler;
Kevin Chyne8f3e1b2018-01-23 17:33:58 -0800104 mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
Kevin Chyn27e1f262018-03-08 16:38:32 -0800105 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
Kevin Chyne8f3e1b2018-01-23 17:33:58 -0800106 mAnimationTranslationOffset = getResources()
107 .getDimension(R.dimen.fingerprint_dialog_animation_translation_offset);
Kevin Chyn178ace52018-03-30 12:36:32 -0700108 mErrorColor = Color.parseColor(
109 getResources().getString(R.color.fingerprint_dialog_error_color));
Kevin Chynd0544dc2018-02-06 16:30:31 -0800110 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));
Kevin Chyn42653e82018-01-19 14:15:46 -0800114
Kevin Chyn27ce1942018-03-12 14:42:17 -0700115 DisplayMetrics metrics = new DisplayMetrics();
116 mWindowManager.getDefaultDisplay().getMetrics(metrics);
117 mDisplayWidth = metrics.widthPixels;
118
Kevin Chyn42653e82018-01-19 14:15:46 -0800119 // 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);
Kevin Chyn42653e82018-01-19 14:15:46 -0800125
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);
Kevin Chyn27ce1942018-03-12 14:42:17 -0700148 final View leftSpace = mLayout.findViewById(R.id.left_space);
149 final View rightSpace = mLayout.findViewById(R.id.right_space);
Kevin Chyn42653e82018-01-19 14:15:46 -0800150 final Button negative = mLayout.findViewById(R.id.button2);
151 final Button positive = mLayout.findViewById(R.id.button1);
152
Kevin Chyn27ce1942018-03-12 14:42:17 -0700153 setDismissesDialog(space);
154 setDismissesDialog(leftSpace);
155 setDismissesDialog(rightSpace);
Kevin Chyn42653e82018-01-19 14:15:46 -0800156
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
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);
Kevin Chyn42653e82018-01-19 14:15:46 -0800177 final Button positive = mLayout.findViewById(R.id.button1);
Kevin Chynd0544dc2018-02-06 16:30:31 -0800178
Kevin Chyn27ce1942018-03-12 14:42:17 -0700179 mDialog.getLayoutParams().width = (int) mDisplayWidth;
180
Kevin Chynd0544dc2018-02-06 16:30:31 -0800181 mLastState = STATE_NONE;
182 updateFingerprintIcon(STATE_FINGERPRINT);
Kevin Chyn42653e82018-01-19 14:15:46 -0800183
Vishwath Mohanecf00ce2018-04-05 10:28:24 -0700184 title.setText(mBundle.getCharSequence(BiometricPrompt.KEY_TITLE));
Kevin Chyn42653e82018-01-19 14:15:46 -0800185 title.setSelected(true);
Kevin Chyndba919a2018-03-16 14:35:10 -0700186
Vishwath Mohanecf00ce2018-04-05 10:28:24 -0700187 final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
Kevin Chyna4596f32018-04-05 11:33:12 -0700188 if (TextUtils.isEmpty(subtitleText)) {
Kevin Chyndba919a2018-03-16 14:35:10 -0700189 subtitle.setVisibility(View.GONE);
190 } else {
191 subtitle.setVisibility(View.VISIBLE);
192 subtitle.setText(subtitleText);
193 }
194
Vishwath Mohanecf00ce2018-04-05 10:28:24 -0700195 final CharSequence descriptionText = mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
Kevin Chyna4596f32018-04-05 11:33:12 -0700196 if (TextUtils.isEmpty(descriptionText)) {
Kevin Chyndba919a2018-03-16 14:35:10 -0700197 description.setVisibility(View.GONE);
198 } else {
Kevin Chyna4596f32018-04-05 11:33:12 -0700199 description.setVisibility(View.VISIBLE);
200 description.setText(descriptionText);
Kevin Chyndba919a2018-03-16 14:35:10 -0700201 }
202
Vishwath Mohanecf00ce2018-04-05 10:28:24 -0700203 negative.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));
Kevin Chyn42653e82018-01-19 14:15:46 -0800204
205 final CharSequence positiveText =
Vishwath Mohanecf00ce2018-04-05 10:28:24 -0700206 mBundle.getCharSequence(BiometricPrompt.KEY_POSITIVE_TEXT);
Kevin Chyn42653e82018-01-19 14:15:46 -0800207 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
Kevin Chyn87df0682018-04-10 19:29:23 -0700214 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;
Kevin Chyn42653e82018-01-19 14:15:46 -0800228 }
229
Kevin Chyn27ce1942018-03-12 14:42:17 -0700230 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
Kevin Chyn27e1f262018-03-08 16:38:32 -0800239 public void startDismiss() {
Kevin Chyn87df0682018-04-10 19:29:23 -0700240 mAnimatingAway = true;
241
Kevin Chyn27e1f262018-03-08 16:38:32 -0800242 final Runnable endActionRunnable = new Runnable() {
243 @Override
244 public void run() {
245 mWindowManager.removeView(FingerprintDialogView.this);
Kevin Chyn87df0682018-04-10 19:29:23 -0700246 mAnimatingAway = false;
Kevin Chyn27e1f262018-03-08 16:38:32 -0800247 }
248 };
249
250 postOnAnimation(new Runnable() {
251 @Override
252 public void run() {
253 mLayout.animate()
254 .alpha(0f)
Kevin Chyndba919a2018-03-16 14:35:10 -0700255 .setDuration(ANIMATION_DURATION_AWAY)
Kevin Chyn27e1f262018-03-08 16:38:32 -0800256 .setInterpolator(mLinearOutSlowIn)
257 .withLayer()
258 .start();
259 mDialog.animate()
260 .translationY(mAnimationTranslationOffset)
Kevin Chyndba919a2018-03-16 14:35:10 -0700261 .setDuration(ANIMATION_DURATION_AWAY)
Kevin Chyn27e1f262018-03-08 16:38:32 -0800262 .setInterpolator(mLinearOutSlowIn)
263 .withLayer()
264 .withEndAction(endActionRunnable)
265 .start();
266 }
267 });
268 }
269
Kevin Chyn87df0682018-04-10 19:29:23 -0700270 /**
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 */
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
283 public boolean isAnimatingAway() {
284 return mAnimatingAway;
285 }
286
Kevin Chyn42653e82018-01-19 14:15:46 -0800287 public void setBundle(Bundle bundle) {
288 mBundle = bundle;
289 }
290
Kevin Chynd0544dc2018-02-06 16:30:31 -0800291 // Clears the temporary message and shows the help message.
292 protected void resetMessage() {
293 updateFingerprintIcon(STATE_FINGERPRINT);
294 mErrorText.setText(R.string.fingerprint_dialog_touch_sensor);
295 mErrorText.setTextColor(mTextColor);
Kevin Chyn42653e82018-01-19 14:15:46 -0800296 }
297
Kevin Chynd0544dc2018-02-06 16:30:31 -0800298 // Shows an error/help message
299 private void showTemporaryMessage(String message) {
Kevin Chyn42653e82018-01-19 14:15:46 -0800300 mHandler.removeMessages(FingerprintDialogImpl.MSG_CLEAR_MESSAGE);
Kevin Chynd0544dc2018-02-06 16:30:31 -0800301 updateFingerprintIcon(STATE_FINGERPRINT_ERROR);
Kevin Chyn42653e82018-01-19 14:15:46 -0800302 mErrorText.setText(message);
Kevin Chyn178ace52018-03-30 12:36:32 -0700303 mErrorText.setTextColor(mErrorColor);
Kevin Chyn16aac7a2018-01-24 11:32:46 -0800304 mErrorText.setContentDescription(message);
Kevin Chyn42653e82018-01-19 14:15:46 -0800305 mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_CLEAR_MESSAGE),
Vishwath Mohanecf00ce2018-04-05 10:28:24 -0700306 BiometricPrompt.HIDE_DIALOG_DELAY);
Kevin Chyn42653e82018-01-19 14:15:46 -0800307 }
308
309 public void showHelpMessage(String message) {
Kevin Chynd0544dc2018-02-06 16:30:31 -0800310 showTemporaryMessage(message);
Kevin Chyn42653e82018-01-19 14:15:46 -0800311 }
312
313 public void showErrorMessage(String error) {
Kevin Chynd0544dc2018-02-06 16:30:31 -0800314 showTemporaryMessage(error);
Kevin Chyn42653e82018-01-19 14:15:46 -0800315 mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_HIDE_DIALOG,
Vishwath Mohanecf00ce2018-04-05 10:28:24 -0700316 false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY);
Kevin Chyn42653e82018-01-19 14:15:46 -0800317 }
318
Kevin Chynd0544dc2018-02-06 16:30:31 -0800319 private void updateFingerprintIcon(int newState) {
Kevin Chyn178ace52018-03-30 12:36:32 -0700320 Drawable icon = getAnimationForTransition(mLastState, newState);
Kevin Chynd0544dc2018-02-06 16:30:31 -0800321
322 if (icon == null) {
323 Log.e(TAG, "Animation not found");
324 return;
325 }
326
Kevin Chynd0544dc2018-02-06 16:30:31 -0800327 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
Kevin Chyn178ace52018-03-30 12:36:32 -0700334 if (animation != null && shouldAnimateForTransition(mLastState, newState)) {
Kevin Chynd0544dc2018-02-06 16:30:31 -0800335 animation.forceAnimationOnUI();
336 animation.start();
337 }
338
339 mLastState = newState;
340 }
341
Kevin Chyn178ace52018-03-30 12:36:32 -0700342 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
356 private Drawable getAnimationForTransition(int oldState, int newState) {
Kevin Chynd0544dc2018-02-06 16:30:31 -0800357 int iconRes;
358 if (oldState == STATE_NONE && newState == STATE_FINGERPRINT) {
Kevin Chyn178ace52018-03-30 12:36:32 -0700359 iconRes = R.drawable.fingerprint_dialog_fp_to_error;
Kevin Chynd0544dc2018-02-06 16:30:31 -0800360 } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
Kevin Chyn178ace52018-03-30 12:36:32 -0700361 iconRes = R.drawable.fingerprint_dialog_fp_to_error;
Kevin Chynd0544dc2018-02-06 16:30:31 -0800362 } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
Kevin Chyn178ace52018-03-30 12:36:32 -0700363 iconRes = R.drawable.fingerprint_dialog_error_to_fp;
Kevin Chynd0544dc2018-02-06 16:30:31 -0800364 } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_AUTHENTICATED) {
Kevin Chyn178ace52018-03-30 12:36:32 -0700365 // TODO(b/77328470): add animation when fingerprint is authenticated
366 iconRes = R.drawable.fingerprint_dialog_error_to_fp;
367 }
368 else {
Kevin Chynd0544dc2018-02-06 16:30:31 -0800369 return null;
370 }
371 return mContext.getDrawable(iconRes);
Kevin Chyn42653e82018-01-19 14:15:46 -0800372 }
373
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}