blob: 9fba44b76863056c32745f2231fb00dbebd0f5ef [file] [log] [blame]
Kevin Chyn6cf54e82018-09-18 19:13:27 -07001/*
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.biometrics;
18
Kevin Chyne1912712019-01-04 14:22:34 -080019import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ValueAnimator;
Kevin Chyn6cf54e82018-09-18 19:13:27 -070023import android.content.Context;
Kevin Chyne1912712019-01-04 14:22:34 -080024import android.graphics.Outline;
Kevin Chyn6cf54e82018-09-18 19:13:27 -070025import android.graphics.drawable.Drawable;
Kevin Chyne1912712019-01-04 14:22:34 -080026import android.os.Bundle;
27import android.text.TextUtils;
28import android.util.DisplayMetrics;
29import android.view.View;
30import android.view.ViewOutlineProvider;
Kevin Chyn6cf54e82018-09-18 19:13:27 -070031
32import com.android.systemui.R;
33
34/**
35 * This class loads the view for the system-provided dialog. The view consists of:
Kevin Chyn6bb20772018-12-27 15:14:44 -080036 * Application Icon, Title, Subtitle, Description, Biometric Icon, Error/Help message area,
Kevin Chyn6cf54e82018-09-18 19:13:27 -070037 * and positive/negative buttons.
38 */
39public class FaceDialogView extends BiometricDialogView {
Kevin Chyn6bb20772018-12-27 15:14:44 -080040
Kevin Chyne1912712019-01-04 14:22:34 -080041 private static final String TAG = "FaceDialogView";
42 private static final String KEY_DIALOG_SIZE = "key_dialog_size";
43
Kevin Chyn6bb20772018-12-27 15:14:44 -080044 private static final int HIDE_DIALOG_DELAY = 500; // ms
Kevin Chyne1912712019-01-04 14:22:34 -080045 private static final int IMPLICIT_Y_PADDING = 16; // dp
46 private static final int GROW_DURATION = 150; // ms
47 private static final int TEXT_ANIMATE_DISTANCE = 32; // dp
48
49 private static final int SIZE_UNKNOWN = 0;
50 private static final int SIZE_SMALL = 1;
51 private static final int SIZE_GROWING = 2;
52 private static final int SIZE_BIG = 3;
53
54 private int mSize;
55 private float mIconOriginalY;
56 private DialogOutlineProvider mOutlineProvider = new DialogOutlineProvider();
57
58 private final class DialogOutlineProvider extends ViewOutlineProvider {
59
60 float mY;
61
62 @Override
63 public void getOutline(View view, Outline outline) {
64 outline.setRoundRect(
65 0 /* left */,
66 (int) mY, /* top */
67 mDialog.getWidth() /* right */,
68 mDialog.getBottom(), /* bottom */
69 getResources().getDimension(R.dimen.biometric_dialog_corner_size));
70 }
71
72 int calculateSmall() {
73 final float padding = dpToPixels(IMPLICIT_Y_PADDING);
74 return mDialog.getHeight() - mBiometricIcon.getHeight() - 2 * (int) padding;
75 }
76
77 void setOutlineY(float y) {
78 mY = y;
79 }
80 }
Kevin Chyn6bb20772018-12-27 15:14:44 -080081
Kevin Chyn6cf54e82018-09-18 19:13:27 -070082 public FaceDialogView(Context context,
83 DialogViewCallback callback) {
84 super(context, callback);
85 }
86
Kevin Chyne1912712019-01-04 14:22:34 -080087 private void updateSize(int newSize) {
88 final float padding = dpToPixels(IMPLICIT_Y_PADDING);
89 final float iconSmallPositionY = mDialog.getHeight() - mBiometricIcon.getHeight() - padding;
90
91 if (newSize == SIZE_SMALL) {
92 // These fields are required and/or always hold a spot on the UI, so should be set to
93 // INVISIBLE so they keep their position
94 mTitleText.setVisibility(View.INVISIBLE);
95 mErrorText.setVisibility(View.INVISIBLE);
96 mNegativeButton.setVisibility(View.INVISIBLE);
97
98 // These fields are optional, so set them to gone or invisible depending on their
99 // usage. If they're empty, they're already set to GONE in BiometricDialogView.
100 if (!TextUtils.isEmpty(mSubtitleText.getText())) {
101 mSubtitleText.setVisibility(View.INVISIBLE);
102 }
103 if (!TextUtils.isEmpty(mDescriptionText.getText())) {
104 mDescriptionText.setVisibility(View.INVISIBLE);
105 }
106
107 // Move the biometric icon to the small spot
108 mBiometricIcon.setY(iconSmallPositionY);
109
110 // Clip the dialog to the small size
111 mDialog.setOutlineProvider(mOutlineProvider);
112 mOutlineProvider.setOutlineY(mOutlineProvider.calculateSmall());
113
114 mDialog.setClipToOutline(true);
115 mDialog.invalidateOutline();
116
117 mSize = newSize;
118 } else if (mSize == SIZE_SMALL && newSize == SIZE_BIG) {
119 mSize = SIZE_GROWING;
120
121 // Animate the outline
122 final ValueAnimator outlineAnimator =
123 ValueAnimator.ofFloat(mOutlineProvider.calculateSmall(), 0);
124 outlineAnimator.addUpdateListener((animation) -> {
125 final float y = (float) animation.getAnimatedValue();
126 mOutlineProvider.setOutlineY(y);
127 mDialog.invalidateOutline();
128 });
129
130 // Animate the icon back to original big position
131 final ValueAnimator iconAnimator =
132 ValueAnimator.ofFloat(iconSmallPositionY, mIconOriginalY);
133 iconAnimator.addUpdateListener((animation) -> {
134 final float y = (float) animation.getAnimatedValue();
135 mBiometricIcon.setY(y);
136 });
137
138 // Animate the error text so it slides up with the icon
139 final ValueAnimator textSlideAnimator =
140 ValueAnimator.ofFloat(dpToPixels(TEXT_ANIMATE_DISTANCE), 0);
141 textSlideAnimator.addUpdateListener((animation) -> {
142 final float y = (float) animation.getAnimatedValue();
143 mErrorText.setTranslationY(y);
144 });
145
146 // Opacity animator for things that should fade in (title, subtitle, details, negative
147 // button)
148 final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1);
149 opacityAnimator.addUpdateListener((animation) -> {
150 final float opacity = (float) animation.getAnimatedValue();
151
152 // These fields are required and/or always hold a spot on the UI
153 mTitleText.setAlpha(opacity);
154 mErrorText.setAlpha(opacity);
155 mNegativeButton.setAlpha(opacity);
156 mTryAgainButton.setAlpha(opacity);
157
158 // These fields are optional, so only animate them if they're supposed to be showing
159 if (!TextUtils.isEmpty(mSubtitleText.getText())) {
160 mSubtitleText.setAlpha(opacity);
161 }
162 if (!TextUtils.isEmpty(mDescriptionText.getText())) {
163 mDescriptionText.setAlpha(opacity);
164 }
165 });
166
167 // Choreograph together
168 final AnimatorSet as = new AnimatorSet();
169 as.setDuration(GROW_DURATION);
170 as.addListener(new AnimatorListenerAdapter() {
171 @Override
172 public void onAnimationStart(Animator animation) {
173 super.onAnimationStart(animation);
174 // Set the visibility of opacity-animating views back to VISIBLE
175 mTitleText.setVisibility(View.VISIBLE);
176 mErrorText.setVisibility(View.VISIBLE);
177 mNegativeButton.setVisibility(View.VISIBLE);
178 mTryAgainButton.setVisibility(View.VISIBLE);
179
180 if (!TextUtils.isEmpty(mSubtitleText.getText())) {
181 mSubtitleText.setVisibility(View.VISIBLE);
182 }
183 if (!TextUtils.isEmpty(mDescriptionText.getText())) {
184 mDescriptionText.setVisibility(View.VISIBLE);
185 }
186 }
187
188 @Override
189 public void onAnimationEnd(Animator animation) {
190 super.onAnimationEnd(animation);
191 mSize = SIZE_BIG;
192 }
193 });
194 as.play(outlineAnimator).with(iconAnimator).with(opacityAnimator)
195 .with(textSlideAnimator);
196 as.start();
197 } else if (mSize == SIZE_BIG) {
198 mDialog.setClipToOutline(false);
199 mDialog.invalidateOutline();
200
201 mBiometricIcon.setY(mIconOriginalY);
202
203 mSize = newSize;
204 }
205 }
206
207 @Override
208 public void onSaveState(Bundle bundle) {
209 super.onSaveState(bundle);
210 bundle.putInt(KEY_DIALOG_SIZE, mSize);
211 }
212
Kevin Chync9744ac2019-01-11 18:49:50 -0800213
214 @Override
215 protected void handleClearMessage(boolean requireTryAgain) {
216 // Clears the temporary message and shows the help message. If requireTryAgain is true,
217 // we will start the authenticating state again.
218 if (!requireTryAgain) {
219 updateState(STATE_AUTHENTICATING);
220 mErrorText.setText(getHintStringResourceId());
221 mErrorText.setTextColor(mTextColor);
222 mErrorText.setVisibility(View.VISIBLE);
223 } else {
224 updateState(STATE_IDLE);
225 mErrorText.setVisibility(View.INVISIBLE);
226 }
227 }
228
Kevin Chyne1912712019-01-04 14:22:34 -0800229 @Override
230 public void restoreState(Bundle bundle) {
231 super.restoreState(bundle);
232 // Keep in mind that this happens before onAttachedToWindow()
233 mSize = bundle.getInt(KEY_DIALOG_SIZE);
234 }
235
236 /**
237 * Do small/big layout here instead of onAttachedToWindow, since:
238 * 1) We need the big layout to be measured, etc for small -> big animation
239 * 2) We need the dialog measurements to know where to move the biometric icon to
240 *
241 * BiometricDialogView already sets the views to their default big state, so here we only
242 * need to hide the ones that are unnecessary.
243 */
244 @Override
245 public void onLayout(boolean changed, int left, int top, int right, int bottom) {
246 super.onLayout(changed, left, top, right, bottom);
247
248 if (mIconOriginalY == 0) {
249 mIconOriginalY = mBiometricIcon.getY();
250 }
251
252 // UNKNOWN means size hasn't been set yet. First time we create the dialog.
253 // onLayout can happen when visibility of views change (during animation, etc).
254 if (mSize != SIZE_UNKNOWN) {
255 // Probably not the cleanest way to do this, but since dialog is big by default,
256 // and small dialogs can persist across orientation changes, we need to set it to
257 // small size here again.
258 if (mSize == SIZE_SMALL) {
259 updateSize(SIZE_SMALL);
260 }
261 return;
262 }
263
264 // If we don't require confirmation, show the small dialog first (until errors occur).
265 if (!requiresConfirmation()) {
266 updateSize(SIZE_SMALL);
267 } else {
268 updateSize(SIZE_BIG);
269 }
270 }
271
272 @Override
273 public void showErrorMessage(String error) {
274 super.showErrorMessage(error);
275
276 // All error messages will cause the dialog to go from small -> big. Error messages
277 // are messages such as lockout, auth failed, etc.
278 if (mSize == SIZE_SMALL) {
279 updateSize(SIZE_BIG);
280 }
281 }
282
283 @Override
284 public void showTryAgainButton(boolean show) {
285 if (show && mSize == SIZE_SMALL) {
286 // Do not call super, we will nicely animate the alpha together with the rest
287 // of the elements in here.
288 updateSize(SIZE_BIG);
289 } else {
Kevin Chync9744ac2019-01-11 18:49:50 -0800290 if (show) {
291 mTryAgainButton.setVisibility(View.VISIBLE);
292 } else {
293 mTryAgainButton.setVisibility(View.GONE);
294 }
Kevin Chyne1912712019-01-04 14:22:34 -0800295 }
296 }
297
Kevin Chyn6cf54e82018-09-18 19:13:27 -0700298 @Override
299 protected int getHintStringResourceId() {
300 return R.string.face_dialog_looking_for_face;
301 }
302
303 @Override
304 protected int getAuthenticatedAccessibilityResourceId() {
305 if (mRequireConfirmation) {
306 return com.android.internal.R.string.face_authenticated_confirmation_required;
307 } else {
308 return com.android.internal.R.string.face_authenticated_no_confirmation_required;
309 }
310 }
311
312 @Override
313 protected int getIconDescriptionResourceId() {
314 return R.string.accessibility_face_dialog_face_icon;
315 }
316
317 @Override
Kevin Chyn6bb20772018-12-27 15:14:44 -0800318 protected boolean shouldAnimateForTransition(int oldState, int newState) {
Kevin Chyne1912712019-01-04 14:22:34 -0800319 if (oldState == STATE_ERROR && newState == STATE_IDLE) {
320 return true;
321 } else if (oldState == STATE_IDLE && newState == STATE_AUTHENTICATING) {
Kevin Chyn6bb20772018-12-27 15:14:44 -0800322 return false;
323 } else if (oldState == STATE_AUTHENTICATING && newState == STATE_ERROR) {
324 return true;
325 } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATING) {
326 return true;
327 } else if (oldState == STATE_AUTHENTICATING && newState == STATE_PENDING_CONFIRMATION) {
328 return true;
329 } else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
330 return true;
331 } else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
332 return true;
333 }
334 return false;
335 }
Kevin Chyn6cf54e82018-09-18 19:13:27 -0700336
Kevin Chyn6bb20772018-12-27 15:14:44 -0800337 @Override
338 protected int getDelayAfterAuthenticatedDurationMs() {
339 return HIDE_DIALOG_DELAY;
340 }
341
342 @Override
Kevin Chyne1912712019-01-04 14:22:34 -0800343 protected boolean shouldGrayAreaDismissDialog() {
344 if (mSize == SIZE_SMALL) {
345 return false;
346 }
347 return true;
348 }
349
350 @Override
Kevin Chyn6bb20772018-12-27 15:14:44 -0800351 protected Drawable getAnimationForTransition(int oldState, int newState) {
352 int iconRes;
Kevin Chyne1912712019-01-04 14:22:34 -0800353 if (oldState == STATE_ERROR && newState == STATE_IDLE) {
354 iconRes = R.drawable.face_dialog_error_to_face;
355 } else if (oldState == STATE_IDLE && newState == STATE_AUTHENTICATING) {
Kevin Chyn6bb20772018-12-27 15:14:44 -0800356 iconRes = R.drawable.face_dialog_face_to_error;
357 } else if (oldState == STATE_AUTHENTICATING && newState == STATE_ERROR) {
358 iconRes = R.drawable.face_dialog_face_to_error;
359 } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATING) {
360 iconRes = R.drawable.face_dialog_error_to_face;
361 } else if (oldState == STATE_AUTHENTICATING && newState == STATE_PENDING_CONFIRMATION) {
362 iconRes = R.drawable.face_dialog_face_gray_to_face_blue;
363 } else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
364 iconRes = R.drawable.face_dialog_face_blue_to_checkmark;
365 } else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
366 iconRes = R.drawable.face_dialog_face_gray_to_checkmark;
367 } else {
368 return null;
369 }
370 return mContext.getDrawable(iconRes);
Kevin Chyn6cf54e82018-09-18 19:13:27 -0700371 }
Kevin Chyne1912712019-01-04 14:22:34 -0800372
373 private float dpToPixels(float dp) {
374 return dp * ((float) mContext.getResources().getDisplayMetrics().densityDpi
375 / DisplayMetrics.DENSITY_DEFAULT);
376 }
377
378 private float pixelsToDp(float pixels) {
379 return pixels / ((float) mContext.getResources().getDisplayMetrics().densityDpi
380 / DisplayMetrics.DENSITY_DEFAULT);
381 }
Kevin Chyn6cf54e82018-09-18 19:13:27 -0700382}