• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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