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 static com.android.systemui.Dependency.MAIN_HANDLER_NAME;
20 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
21 
22 import android.content.Context;
23 import android.content.res.ColorStateList;
24 import android.content.res.Configuration;
25 import android.content.res.TypedArray;
26 import android.graphics.Color;
27 import android.graphics.drawable.Animatable2;
28 import android.graphics.drawable.AnimatedVectorDrawable;
29 import android.graphics.drawable.Drawable;
30 import android.hardware.biometrics.BiometricSourceType;
31 import android.os.Handler;
32 import android.os.Trace;
33 import android.util.AttributeSet;
34 import android.view.ViewGroup;
35 import android.view.accessibility.AccessibilityNodeInfo;
36 
37 import androidx.annotation.Nullable;
38 
39 import com.android.internal.graphics.ColorUtils;
40 import com.android.internal.telephony.IccCardConstants;
41 import com.android.keyguard.KeyguardUpdateMonitor;
42 import com.android.keyguard.KeyguardUpdateMonitorCallback;
43 import com.android.systemui.R;
44 import com.android.systemui.dock.DockManager;
45 import com.android.systemui.plugins.statusbar.StatusBarStateController;
46 import com.android.systemui.statusbar.KeyguardAffordanceView;
47 import com.android.systemui.statusbar.phone.ScrimController.ScrimVisibility;
48 import com.android.systemui.statusbar.policy.AccessibilityController;
49 import com.android.systemui.statusbar.policy.ConfigurationController;
50 import com.android.systemui.statusbar.policy.KeyguardMonitor;
51 import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
52 
53 import javax.inject.Inject;
54 import javax.inject.Named;
55 
56 /**
57  * Manages the different states and animations of the unlock icon.
58  */
59 public class LockIcon extends KeyguardAffordanceView implements OnUserInfoChangedListener,
60         StatusBarStateController.StateListener, ConfigurationController.ConfigurationListener,
61         UnlockMethodCache.OnUnlockMethodChangedListener {
62 
63     private static final int STATE_LOCKED = 0;
64     private static final int STATE_LOCK_OPEN = 1;
65     private static final int STATE_SCANNING_FACE = 2;
66     private static final int STATE_BIOMETRICS_ERROR = 3;
67     private final ConfigurationController mConfigurationController;
68     private final StatusBarStateController mStatusBarStateController;
69     private final UnlockMethodCache mUnlockMethodCache;
70     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
71     private final AccessibilityController mAccessibilityController;
72     private final DockManager mDockManager;
73     private final Handler mMainHandler;
74     private final KeyguardMonitor mKeyguardMonitor;
75 
76     private int mLastState = 0;
77     private boolean mTransientBiometricsError;
78     private boolean mIsFaceUnlockState;
79     private boolean mSimLocked;
80     private int mDensity;
81     private boolean mPulsing;
82     private boolean mDozing;
83     private boolean mBouncerVisible;
84     private boolean mDocked;
85     private boolean mLastDozing;
86     private boolean mLastPulsing;
87     private boolean mLastBouncerVisible;
88     private int mIconColor;
89     private float mDozeAmount;
90     private int mIconRes;
91     private boolean mWasPulsingOnThisFrame;
92     private boolean mWakeAndUnlockRunning;
93     private boolean mKeyguardShowing;
94     private boolean mShowingLaunchAffordance;
95 
96     private final KeyguardMonitor.Callback mKeyguardMonitorCallback =
97             new KeyguardMonitor.Callback() {
98                 @Override
99                 public void onKeyguardShowingChanged() {
100                     mKeyguardShowing = mKeyguardMonitor.isShowing();
101                     update(false /* force */);
102                 }
103             };
104     private final DockManager.DockEventListener mDockEventListener =
105             new DockManager.DockEventListener() {
106                 @Override
107                 public void onEvent(int event) {
108                     boolean docked = event == DockManager.STATE_DOCKED
109                             || event == DockManager.STATE_DOCKED_HIDE;
110                     if (docked != mDocked) {
111                         mDocked = docked;
112                         update(true /* force */);
113                     }
114         }
115     };
116 
117     private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
118             new KeyguardUpdateMonitorCallback() {
119                 @Override
120                 public void onSimStateChanged(int subId, int slotId,
121                         IccCardConstants.State simState) {
122                     boolean oldSimLocked = mSimLocked;
123                     mSimLocked = mKeyguardUpdateMonitor.isSimPinSecure();
124                     update(oldSimLocked != mSimLocked);
125                 }
126 
127                 @Override
128                 public void onKeyguardVisibilityChanged(boolean showing) {
129                     update();
130                 }
131 
132                 @Override
133                 public void onBiometricRunningStateChanged(boolean running,
134                         BiometricSourceType biometricSourceType) {
135                     update();
136                 }
137 
138                 @Override
139                 public void onStrongAuthStateChanged(int userId) {
140                     update();
141                 }
142     };
143 
144     @Inject
LockIcon(@amedVIEW_CONTEXT) Context context, AttributeSet attrs, StatusBarStateController statusBarStateController, ConfigurationController configurationController, AccessibilityController accessibilityController, KeyguardMonitor keyguardMonitor, @Nullable DockManager dockManager, @Named(MAIN_HANDLER_NAME) Handler mainHandler)145     public LockIcon(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
146             StatusBarStateController statusBarStateController,
147             ConfigurationController configurationController,
148             AccessibilityController accessibilityController,
149             KeyguardMonitor keyguardMonitor,
150             @Nullable DockManager dockManager,
151             @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
152         super(context, attrs);
153         mContext = context;
154         mUnlockMethodCache = UnlockMethodCache.getInstance(context);
155         mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
156         mAccessibilityController = accessibilityController;
157         mConfigurationController = configurationController;
158         mStatusBarStateController = statusBarStateController;
159         mKeyguardMonitor = keyguardMonitor;
160         mDockManager = dockManager;
161         mMainHandler = mainHandler;
162     }
163 
164     @Override
onAttachedToWindow()165     protected void onAttachedToWindow() {
166         super.onAttachedToWindow();
167         mStatusBarStateController.addCallback(this);
168         mConfigurationController.addCallback(this);
169         mKeyguardMonitor.addCallback(mKeyguardMonitorCallback);
170         mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback);
171         mUnlockMethodCache.addListener(this);
172         mSimLocked = mKeyguardUpdateMonitor.isSimPinSecure();
173         if (mDockManager != null) {
174             mDockManager.addListener(mDockEventListener);
175         }
176         onThemeChanged();
177     }
178 
179     @Override
onDetachedFromWindow()180     protected void onDetachedFromWindow() {
181         super.onDetachedFromWindow();
182         mStatusBarStateController.removeCallback(this);
183         mConfigurationController.removeCallback(this);
184         mKeyguardUpdateMonitor.removeCallback(mUpdateMonitorCallback);
185         mKeyguardMonitor.removeCallback(mKeyguardMonitorCallback);
186         mUnlockMethodCache.removeListener(this);
187         if (mDockManager != null) {
188             mDockManager.removeListener(mDockEventListener);
189         }
190     }
191 
192     @Override
onThemeChanged()193     public void onThemeChanged() {
194         TypedArray typedArray = mContext.getTheme().obtainStyledAttributes(
195                 null, new int[]{ R.attr.wallpaperTextColor }, 0, 0);
196         mIconColor = typedArray.getColor(0, Color.WHITE);
197         typedArray.recycle();
198         updateDarkTint();
199     }
200 
201     @Override
onUserInfoChanged(String name, Drawable picture, String userAccount)202     public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
203         update();
204     }
205 
206     /**
207      * If we're currently presenting an authentication error message.
208      */
setTransientBiometricsError(boolean transientBiometricsError)209     public void setTransientBiometricsError(boolean transientBiometricsError) {
210         mTransientBiometricsError = transientBiometricsError;
211         update();
212     }
213 
214     @Override
onConfigurationChanged(Configuration newConfig)215     protected void onConfigurationChanged(Configuration newConfig) {
216         super.onConfigurationChanged(newConfig);
217         final int density = newConfig.densityDpi;
218         if (density != mDensity) {
219             mDensity = density;
220             update();
221         }
222     }
223 
update()224     public void update() {
225         update(false /* force */);
226     }
227 
update(boolean force)228     public void update(boolean force) {
229         int state = getState();
230         mIsFaceUnlockState = state == STATE_SCANNING_FACE;
231         if (state != mLastState || mLastDozing != mDozing || mLastPulsing != mPulsing
232                 || mLastBouncerVisible != mBouncerVisible || force) {
233             int iconAnimRes = getAnimationResForTransition(mLastState, state, mLastPulsing,
234                     mPulsing, mLastDozing, mDozing, mBouncerVisible);
235             boolean isAnim = iconAnimRes != -1;
236 
237             int iconRes = isAnim ? iconAnimRes : getIconForState(state);
238             if (iconRes != mIconRes) {
239                 mIconRes = iconRes;
240 
241                 Drawable icon = mContext.getDrawable(iconRes);
242                 final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
243                         ? (AnimatedVectorDrawable) icon
244                         : null;
245                 setImageDrawable(icon, false);
246                 if (mIsFaceUnlockState) {
247                     announceForAccessibility(getContext().getString(
248                             R.string.accessibility_scanning_face));
249                 }
250 
251                 if (animation != null && isAnim) {
252                     animation.forceAnimationOnUI();
253                     animation.clearAnimationCallbacks();
254                     animation.registerAnimationCallback(new Animatable2.AnimationCallback() {
255                         @Override
256                         public void onAnimationEnd(Drawable drawable) {
257                             if (getDrawable() == animation && state == getState()
258                                     && doesAnimationLoop(iconAnimRes)) {
259                                 animation.start();
260                             } else {
261                                 Trace.endAsyncSection("LockIcon#Animation", state);
262                             }
263                         }
264                     });
265                     Trace.beginAsyncSection("LockIcon#Animation", state);
266                     animation.start();
267                 }
268             }
269             updateDarkTint();
270 
271             mLastState = state;
272             mLastDozing = mDozing;
273             mLastPulsing = mPulsing;
274             mLastBouncerVisible = mBouncerVisible;
275         }
276 
277         boolean onAodNotPulsingOrDocked = mDozing && (!mPulsing || mDocked);
278         boolean invisible = onAodNotPulsingOrDocked || mWakeAndUnlockRunning
279                 || mShowingLaunchAffordance;
280         setVisibility(invisible ? INVISIBLE : VISIBLE);
281         updateClickability();
282     }
283 
updateClickability()284     private void updateClickability() {
285         if (mAccessibilityController == null) {
286             return;
287         }
288         boolean canLock = mUnlockMethodCache.isMethodSecure()
289                 && mUnlockMethodCache.canSkipBouncer();
290         boolean clickToUnlock = mAccessibilityController.isAccessibilityEnabled();
291         setClickable(clickToUnlock);
292         setLongClickable(canLock && !clickToUnlock);
293         setFocusable(mAccessibilityController.isAccessibilityEnabled());
294     }
295 
296     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)297     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
298         super.onInitializeAccessibilityNodeInfo(info);
299         boolean fingerprintRunning = mKeyguardUpdateMonitor.isFingerprintDetectionRunning();
300         boolean unlockingAllowed = mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed();
301         if (fingerprintRunning && unlockingAllowed) {
302             AccessibilityNodeInfo.AccessibilityAction unlock
303                     = new AccessibilityNodeInfo.AccessibilityAction(
304                     AccessibilityNodeInfo.ACTION_CLICK,
305                     getContext().getString(R.string.accessibility_unlock_without_fingerprint));
306             info.addAction(unlock);
307             info.setHintText(getContext().getString(
308                     R.string.accessibility_waiting_for_fingerprint));
309         } else if (mIsFaceUnlockState) {
310             //Avoid 'button' to be spoken for scanning face
311             info.setClassName(LockIcon.class.getName());
312             info.setContentDescription(getContext().getString(
313                 R.string.accessibility_scanning_face));
314         }
315     }
316 
getIconForState(int state)317     private int getIconForState(int state) {
318         int iconRes;
319         switch (state) {
320             case STATE_LOCKED:
321             // Scanning animation is a pulsing padlock. This means that the resting state is
322             // just a padlock.
323             case STATE_SCANNING_FACE:
324             // Error animation also starts and ands on the padlock.
325             case STATE_BIOMETRICS_ERROR:
326                 iconRes = com.android.internal.R.drawable.ic_lock;
327                 break;
328             case STATE_LOCK_OPEN:
329                 iconRes = com.android.internal.R.drawable.ic_lock_open;
330                 break;
331             default:
332                 throw new IllegalArgumentException();
333         }
334 
335         return iconRes;
336     }
337 
doesAnimationLoop(int resourceId)338     private boolean doesAnimationLoop(int resourceId) {
339         return resourceId == com.android.internal.R.anim.lock_scanning;
340     }
341 
getAnimationResForTransition(int oldState, int newState, boolean wasPulsing, boolean pulsing, boolean wasDozing, boolean dozing, boolean bouncerVisible)342     private int getAnimationResForTransition(int oldState, int newState,
343             boolean wasPulsing, boolean pulsing, boolean wasDozing, boolean dozing,
344             boolean bouncerVisible) {
345 
346         // Never animate when screen is off
347         if (dozing && !pulsing && !mWasPulsingOnThisFrame) {
348             return -1;
349         }
350 
351         boolean isError = oldState != STATE_BIOMETRICS_ERROR && newState == STATE_BIOMETRICS_ERROR;
352         boolean justUnlocked = oldState != STATE_LOCK_OPEN && newState == STATE_LOCK_OPEN;
353         boolean justLocked = oldState == STATE_LOCK_OPEN && newState == STATE_LOCKED;
354         boolean nowPulsing = !wasPulsing && pulsing;
355         boolean turningOn = wasDozing && !dozing && !mWasPulsingOnThisFrame;
356 
357         if (isError) {
358             return com.android.internal.R.anim.lock_to_error;
359         } else if (justUnlocked) {
360             return com.android.internal.R.anim.lock_unlock;
361         } else if (justLocked) {
362             return com.android.internal.R.anim.lock_lock;
363         } else if (newState == STATE_SCANNING_FACE && bouncerVisible) {
364             return com.android.internal.R.anim.lock_scanning;
365         } else if ((nowPulsing || turningOn) && newState != STATE_LOCK_OPEN) {
366             return com.android.internal.R.anim.lock_in;
367         }
368         return -1;
369     }
370 
getState()371     private int getState() {
372         KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
373         if (mTransientBiometricsError) {
374             return STATE_BIOMETRICS_ERROR;
375         } else if ((mUnlockMethodCache.canSkipBouncer() || !mKeyguardShowing) && !mSimLocked) {
376             return STATE_LOCK_OPEN;
377         } else if (updateMonitor.isFaceDetectionRunning()) {
378             return STATE_SCANNING_FACE;
379         } else {
380             return STATE_LOCKED;
381         }
382     }
383 
384     @Override
onDozeAmountChanged(float linear, float eased)385     public void onDozeAmountChanged(float linear, float eased) {
386         mDozeAmount = eased;
387         updateDarkTint();
388     }
389 
390     /**
391      * When keyguard is in pulsing (AOD2) state.
392      * @param pulsing {@code true} when pulsing.
393      */
setPulsing(boolean pulsing)394     public void setPulsing(boolean pulsing) {
395         mPulsing = pulsing;
396         if (!mPulsing) {
397             mWasPulsingOnThisFrame = true;
398             mMainHandler.post(() -> {
399                 mWasPulsingOnThisFrame = false;
400             });
401         }
402         update();
403     }
404 
405     /**
406      * Sets the dozing state of the keyguard.
407      */
408     @Override
onDozingChanged(boolean dozing)409     public void onDozingChanged(boolean dozing) {
410         mDozing = dozing;
411         update();
412     }
413 
updateDarkTint()414     private void updateDarkTint() {
415         int color = ColorUtils.blendARGB(mIconColor, Color.WHITE, mDozeAmount);
416         setImageTintList(ColorStateList.valueOf(color));
417     }
418 
419     /**
420      * If bouncer is visible or not.
421      */
setBouncerVisible(boolean bouncerVisible)422     public void setBouncerVisible(boolean bouncerVisible) {
423         if (mBouncerVisible == bouncerVisible) {
424             return;
425         }
426         mBouncerVisible = bouncerVisible;
427         update();
428     }
429 
430     @Override
onDensityOrFontScaleChanged()431     public void onDensityOrFontScaleChanged() {
432         ViewGroup.LayoutParams lp = getLayoutParams();
433         if (lp == null) {
434             return;
435         }
436         lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_lock_width);
437         lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_lock_height);
438         setLayoutParams(lp);
439         update(true /* force */);
440     }
441 
442     @Override
onLocaleListChanged()443     public void onLocaleListChanged() {
444         setContentDescription(getContext().getText(R.string.accessibility_unlock_button));
445         update(true /* force */);
446     }
447 
448     @Override
onUnlockMethodStateChanged()449     public void onUnlockMethodStateChanged() {
450         update();
451     }
452 
453     /**
454      * We need to hide the lock whenever there's a fingerprint unlock, otherwise you'll see the
455      * icon on top of the black front scrim.
456      */
onBiometricAuthModeChanged(boolean wakeAndUnlock)457     public void onBiometricAuthModeChanged(boolean wakeAndUnlock) {
458         if (wakeAndUnlock) {
459             mWakeAndUnlockRunning = true;
460         }
461         update();
462     }
463 
464     /**
465      * When we're launching an affordance, like double pressing power to open camera.
466      */
onShowingLaunchAffordanceChanged(boolean showing)467     public void onShowingLaunchAffordanceChanged(boolean showing) {
468         mShowingLaunchAffordance = showing;
469         update();
470     }
471 
472     /**
473      * Called whenever the scrims become opaque, transparent or semi-transparent.
474      */
onScrimVisibilityChanged(@crimVisibility int scrimsVisible)475     public void onScrimVisibilityChanged(@ScrimVisibility int scrimsVisible) {
476         if (mWakeAndUnlockRunning
477                 && scrimsVisible == ScrimController.VISIBILITY_FULLY_TRANSPARENT) {
478             mWakeAndUnlockRunning = false;
479             update();
480         }
481     }
482 }
483