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