1 /*
2  * Copyright (C) 2015 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.statusbar.phone;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.graphics.drawable.AnimatedVectorDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.graphics.drawable.InsetDrawable;
24 import android.util.AttributeSet;
25 import android.view.View;
26 import android.view.accessibility.AccessibilityNodeInfo;
27 
28 import com.android.keyguard.KeyguardUpdateMonitor;
29 import com.android.systemui.R;
30 import com.android.systemui.statusbar.KeyguardAffordanceView;
31 import com.android.systemui.statusbar.policy.AccessibilityController;
32 import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
33 
34 /**
35  * Manages the different states and animations of the unlock icon.
36  */
37 public class LockIcon extends KeyguardAffordanceView implements OnUserInfoChangedListener {
38 
39     private static final int FP_DRAW_OFF_TIMEOUT = 800;
40 
41     private static final int STATE_LOCKED = 0;
42     private static final int STATE_LOCK_OPEN = 1;
43     private static final int STATE_FACE_UNLOCK = 2;
44     private static final int STATE_FINGERPRINT = 3;
45     private static final int STATE_FINGERPRINT_ERROR = 4;
46 
47     private int mLastState = 0;
48     private boolean mLastDeviceInteractive;
49     private boolean mTransientFpError;
50     private boolean mDeviceInteractive;
51     private boolean mScreenOn;
52     private boolean mLastScreenOn;
53     private Drawable mUserAvatarIcon;
54     private TrustDrawable mTrustDrawable;
55     private final UnlockMethodCache mUnlockMethodCache;
56     private AccessibilityController mAccessibilityController;
57     private boolean mHasFingerPrintIcon;
58     private boolean mHasFaceUnlockIcon;
59     private int mDensity;
60 
61     private final Runnable mDrawOffTimeout = () -> update(true /* forceUpdate */);
62 
LockIcon(Context context, AttributeSet attrs)63     public LockIcon(Context context, AttributeSet attrs) {
64         super(context, attrs);
65         mTrustDrawable = new TrustDrawable(context);
66         setBackground(mTrustDrawable);
67         mUnlockMethodCache = UnlockMethodCache.getInstance(context);
68     }
69 
70     @Override
onVisibilityChanged(View changedView, int visibility)71     protected void onVisibilityChanged(View changedView, int visibility) {
72         super.onVisibilityChanged(changedView, visibility);
73         if (isShown()) {
74             mTrustDrawable.start();
75         } else {
76             mTrustDrawable.stop();
77         }
78     }
79 
80     @Override
onDetachedFromWindow()81     protected void onDetachedFromWindow() {
82         super.onDetachedFromWindow();
83         mTrustDrawable.stop();
84     }
85 
86     @Override
onUserInfoChanged(String name, Drawable picture, String userAccount)87     public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
88         mUserAvatarIcon = picture;
89         update();
90     }
91 
setTransientFpError(boolean transientFpError)92     public void setTransientFpError(boolean transientFpError) {
93         mTransientFpError = transientFpError;
94         update();
95     }
96 
setDeviceInteractive(boolean deviceInteractive)97     public void setDeviceInteractive(boolean deviceInteractive) {
98         mDeviceInteractive = deviceInteractive;
99         update();
100     }
101 
setScreenOn(boolean screenOn)102     public void setScreenOn(boolean screenOn) {
103         mScreenOn = screenOn;
104         update();
105     }
106 
107     @Override
onConfigurationChanged(Configuration newConfig)108     protected void onConfigurationChanged(Configuration newConfig) {
109         super.onConfigurationChanged(newConfig);
110         final int density = newConfig.densityDpi;
111         if (density != mDensity) {
112             mDensity = density;
113             mTrustDrawable.stop();
114             mTrustDrawable = new TrustDrawable(getContext());
115             setBackground(mTrustDrawable);
116             update();
117         }
118     }
119 
update()120     public void update() {
121         update(false /* force */);
122     }
123 
update(boolean force)124     public void update(boolean force) {
125         boolean visible = isShown()
126                 && KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive();
127         if (visible) {
128             mTrustDrawable.start();
129         } else {
130             mTrustDrawable.stop();
131         }
132         int state = getState();
133         boolean anyFingerprintIcon = state == STATE_FINGERPRINT || state == STATE_FINGERPRINT_ERROR;
134         mHasFaceUnlockIcon = state == STATE_FACE_UNLOCK;
135         boolean useAdditionalPadding = anyFingerprintIcon;
136         boolean trustHidden = anyFingerprintIcon;
137         if (state != mLastState || mDeviceInteractive != mLastDeviceInteractive
138                 || mScreenOn != mLastScreenOn || force) {
139             int iconAnimRes =
140                 getAnimationResForTransition(mLastState, state, mLastDeviceInteractive,
141                     mDeviceInteractive, mLastScreenOn, mScreenOn);
142             boolean isAnim = iconAnimRes != -1;
143             if (iconAnimRes == R.drawable.lockscreen_fingerprint_draw_off_animation) {
144                 anyFingerprintIcon = true;
145                 useAdditionalPadding = true;
146                 trustHidden = true;
147             } else if (iconAnimRes == R.drawable.trusted_state_to_error_animation) {
148                 anyFingerprintIcon = true;
149                 useAdditionalPadding = false;
150                 trustHidden = true;
151             } else if (iconAnimRes == R.drawable.error_to_trustedstate_animation) {
152                 anyFingerprintIcon = true;
153                 useAdditionalPadding = false;
154                 trustHidden = false;
155             }
156 
157             Drawable icon;
158             if (isAnim) {
159                 // Load the animation resource.
160                 icon = mContext.getDrawable(iconAnimRes);
161             } else {
162                 // Load the static icon resource based on the current state.
163                 icon = getIconForState(state, mScreenOn, mDeviceInteractive);
164             }
165 
166             final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
167                     ? (AnimatedVectorDrawable) icon
168                     : null;
169             int iconHeight = getResources().getDimensionPixelSize(
170                     R.dimen.keyguard_affordance_icon_height);
171             int iconWidth = getResources().getDimensionPixelSize(
172                     R.dimen.keyguard_affordance_icon_width);
173             if (!anyFingerprintIcon && (icon.getIntrinsicHeight() != iconHeight
174                     || icon.getIntrinsicWidth() != iconWidth)) {
175                 icon = new IntrinsicSizeDrawable(icon, iconWidth, iconHeight);
176             }
177             setPaddingRelative(0, 0, 0, useAdditionalPadding
178                     ? getResources().getDimensionPixelSize(
179                     R.dimen.fingerprint_icon_additional_padding)
180                     : 0);
181             setRestingAlpha(
182                     anyFingerprintIcon ? 1f : KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT);
183             setImageDrawable(icon, false);
184             if (mHasFaceUnlockIcon) {
185                 announceForAccessibility(getContext().getString(
186                     R.string.accessibility_scanning_face));
187             }
188 
189             mHasFingerPrintIcon = anyFingerprintIcon;
190             if (animation != null && isAnim) {
191                 animation.forceAnimationOnUI();
192                 animation.start();
193             }
194 
195             if (iconAnimRes == R.drawable.lockscreen_fingerprint_draw_off_animation) {
196                 removeCallbacks(mDrawOffTimeout);
197                 postDelayed(mDrawOffTimeout, FP_DRAW_OFF_TIMEOUT);
198             } else {
199                 removeCallbacks(mDrawOffTimeout);
200             }
201 
202             mLastState = state;
203             mLastDeviceInteractive = mDeviceInteractive;
204             mLastScreenOn = mScreenOn;
205         }
206 
207         // Hide trust circle when fingerprint is running.
208         boolean trustManaged = mUnlockMethodCache.isTrustManaged() && !trustHidden;
209         mTrustDrawable.setTrustManaged(trustManaged);
210         updateClickability();
211     }
212 
updateClickability()213     private void updateClickability() {
214         if (mAccessibilityController == null) {
215             return;
216         }
217         boolean clickToUnlock = mAccessibilityController.isAccessibilityEnabled();
218         boolean clickToForceLock = mUnlockMethodCache.isTrustManaged()
219                 && !clickToUnlock;
220         boolean longClickToForceLock = mUnlockMethodCache.isTrustManaged()
221                 && !clickToForceLock;
222         setClickable(clickToForceLock || clickToUnlock);
223         setLongClickable(longClickToForceLock);
224         setFocusable(mAccessibilityController.isAccessibilityEnabled());
225     }
226 
227     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)228     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
229         super.onInitializeAccessibilityNodeInfo(info);
230         if (mHasFingerPrintIcon) {
231             AccessibilityNodeInfo.AccessibilityAction unlock
232                     = new AccessibilityNodeInfo.AccessibilityAction(
233                     AccessibilityNodeInfo.ACTION_CLICK,
234                     getContext().getString(R.string.accessibility_unlock_without_fingerprint));
235             info.addAction(unlock);
236             info.setHintText(getContext().getString(
237                     R.string.accessibility_waiting_for_fingerprint));
238         } else if (mHasFaceUnlockIcon){
239             //Avoid 'button' to be spoken for scanning face
240             info.setClassName(LockIcon.class.getName());
241             info.setContentDescription(getContext().getString(
242                 R.string.accessibility_scanning_face));
243         }
244     }
245 
setAccessibilityController(AccessibilityController accessibilityController)246     public void setAccessibilityController(AccessibilityController accessibilityController) {
247         mAccessibilityController = accessibilityController;
248     }
249 
getIconForState(int state, boolean screenOn, boolean deviceInteractive)250     private Drawable getIconForState(int state, boolean screenOn, boolean deviceInteractive) {
251         int iconRes;
252         switch (state) {
253             case STATE_LOCKED:
254                 iconRes = R.drawable.ic_lock_24dp;
255                 break;
256             case STATE_LOCK_OPEN:
257                 if (mUnlockMethodCache.isTrustManaged() && mUnlockMethodCache.isTrusted()
258                     && mUserAvatarIcon != null) {
259                     return mUserAvatarIcon;
260                 } else {
261                     iconRes = R.drawable.ic_lock_open_24dp;
262                 }
263                 break;
264             case STATE_FACE_UNLOCK:
265                 iconRes = R.drawable.ic_face_unlock;
266                 break;
267             case STATE_FINGERPRINT:
268                 // If screen is off and device asleep, use the draw on animation so the first frame
269                 // gets drawn.
270                 iconRes = screenOn && deviceInteractive
271                         ? R.drawable.ic_fingerprint
272                         : R.drawable.lockscreen_fingerprint_draw_on_animation;
273                 break;
274             case STATE_FINGERPRINT_ERROR:
275                 iconRes = R.drawable.ic_fingerprint_error;
276                 break;
277             default:
278                 throw new IllegalArgumentException();
279         }
280 
281         return mContext.getDrawable(iconRes);
282     }
283 
getAnimationResForTransition(int oldState, int newState, boolean oldDeviceInteractive, boolean deviceInteractive, boolean oldScreenOn, boolean screenOn)284     private int getAnimationResForTransition(int oldState, int newState,
285             boolean oldDeviceInteractive, boolean deviceInteractive,
286             boolean oldScreenOn, boolean screenOn) {
287         if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
288             return R.drawable.lockscreen_fingerprint_fp_to_error_state_animation;
289         } else if (oldState == STATE_LOCK_OPEN && newState == STATE_FINGERPRINT_ERROR) {
290             return R.drawable.trusted_state_to_error_animation;
291         } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_LOCK_OPEN) {
292             return R.drawable.error_to_trustedstate_animation;
293         } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
294             return R.drawable.lockscreen_fingerprint_error_state_to_fp_animation;
295         } else if (oldState == STATE_FINGERPRINT && newState == STATE_LOCK_OPEN
296                 && !mUnlockMethodCache.isTrusted()) {
297             return R.drawable.lockscreen_fingerprint_draw_off_animation;
298         } else if (newState == STATE_FINGERPRINT && (!oldScreenOn && screenOn && deviceInteractive
299                 || screenOn && !oldDeviceInteractive && deviceInteractive)) {
300             return R.drawable.lockscreen_fingerprint_draw_on_animation;
301         } else {
302             return -1;
303         }
304     }
305 
getState()306     private int getState() {
307         KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
308         boolean fingerprintRunning = updateMonitor.isFingerprintDetectionRunning();
309         boolean unlockingAllowed = updateMonitor.isUnlockingWithFingerprintAllowed();
310         if (mTransientFpError) {
311             return STATE_FINGERPRINT_ERROR;
312         } else if (mUnlockMethodCache.canSkipBouncer()) {
313             return STATE_LOCK_OPEN;
314         } else if (mUnlockMethodCache.isFaceUnlockRunning()) {
315             return STATE_FACE_UNLOCK;
316         } else if (fingerprintRunning && unlockingAllowed) {
317             return STATE_FINGERPRINT;
318         } else {
319             return STATE_LOCKED;
320         }
321     }
322 
323     /**
324      * A wrapper around another Drawable that overrides the intrinsic size.
325      */
326     private static class IntrinsicSizeDrawable extends InsetDrawable {
327 
328         private final int mIntrinsicWidth;
329         private final int mIntrinsicHeight;
330 
IntrinsicSizeDrawable(Drawable drawable, int intrinsicWidth, int intrinsicHeight)331         public IntrinsicSizeDrawable(Drawable drawable, int intrinsicWidth, int intrinsicHeight) {
332             super(drawable, 0);
333             mIntrinsicWidth = intrinsicWidth;
334             mIntrinsicHeight = intrinsicHeight;
335         }
336 
337         @Override
getIntrinsicWidth()338         public int getIntrinsicWidth() {
339             return mIntrinsicWidth;
340         }
341 
342         @Override
getIntrinsicHeight()343         public int getIntrinsicHeight() {
344             return mIntrinsicHeight;
345         }
346     }
347 }
348