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.annotation.IntDef;
20 import android.content.Context;
21 import android.content.res.ColorStateList;
22 import android.content.res.Configuration;
23 import android.graphics.Color;
24 import android.graphics.drawable.Animatable2;
25 import android.graphics.drawable.AnimatedVectorDrawable;
26 import android.graphics.drawable.Drawable;
27 import android.os.Trace;
28 import android.provider.Settings;
29 import android.text.TextUtils;
30 import android.util.AttributeSet;
31 import android.util.SparseArray;
32 import android.view.ViewTreeObserver.OnPreDrawListener;
33 
34 import com.android.internal.graphics.ColorUtils;
35 import com.android.systemui.Interpolators;
36 import com.android.systemui.R;
37 import com.android.systemui.statusbar.KeyguardAffordanceView;
38 
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 
42 /**
43  * Manages the different states and animations of the unlock icon.
44  */
45 public class LockIcon extends KeyguardAffordanceView {
46 
47     static final int STATE_LOCKED = 0;
48     static final int STATE_LOCK_OPEN = 1;
49     static final int STATE_SCANNING_FACE = 2;
50     static final int STATE_BIOMETRICS_ERROR = 3;
51     private float mDozeAmount;
52     private int mIconColor;
53     private int mOldState;
54     private int mState;
55     private boolean mPulsing;
56     private boolean mDozing;
57     private boolean mKeyguardJustShown;
58     private boolean mPredrawRegistered;
59     private final SparseArray<Drawable> mDrawableCache = new SparseArray<>();
60 
61     private final OnPreDrawListener mOnPreDrawListener = new OnPreDrawListener() {
62         @Override
63         public boolean onPreDraw() {
64             getViewTreeObserver().removeOnPreDrawListener(this);
65             mPredrawRegistered = false;
66 
67             int newState = mState;
68             Drawable icon = getIcon(newState);
69             setImageDrawable(icon, false);
70 
71             if (newState == STATE_SCANNING_FACE) {
72                 announceForAccessibility(getResources().getString(
73                         R.string.accessibility_scanning_face));
74             }
75 
76             if (icon instanceof AnimatedVectorDrawable) {
77                 final AnimatedVectorDrawable animation = (AnimatedVectorDrawable) icon;
78                 animation.forceAnimationOnUI();
79                 animation.clearAnimationCallbacks();
80                 animation.registerAnimationCallback(
81                         new Animatable2.AnimationCallback() {
82                             @Override
83                             public void onAnimationEnd(Drawable drawable) {
84                                 if (getDrawable() == animation
85                                         && newState == mState
86                                         && newState == STATE_SCANNING_FACE) {
87                                     animation.start();
88                                 } else {
89                                     Trace.endAsyncSection("LockIcon#Animation", newState);
90                                 }
91                             }
92                         });
93                 Trace.beginAsyncSection("LockIcon#Animation", newState);
94                 animation.start();
95             }
96 
97             return true;
98         }
99     };
100 
LockIcon(Context context, AttributeSet attrs)101     public LockIcon(Context context, AttributeSet attrs) {
102         super(context, attrs);
103     }
104 
105     @Override
onConfigurationChanged(Configuration newConfig)106     protected void onConfigurationChanged(Configuration newConfig) {
107         super.onConfigurationChanged(newConfig);
108         mDrawableCache.clear();
109     }
110 
111     /**
112      * Update the icon visibility
113      * @return true if the visibility changed
114      */
updateIconVisibility(boolean visible)115     boolean updateIconVisibility(boolean visible) {
116         boolean wasVisible = getVisibility() == VISIBLE;
117         if (visible != wasVisible) {
118             setVisibility(visible ? VISIBLE : INVISIBLE);
119             animate().cancel();
120             if (visible) {
121                 setScaleX(0);
122                 setScaleY(0);
123                 animate()
124                         .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
125                         .scaleX(1)
126                         .scaleY(1)
127                         .withLayer()
128                         .setDuration(233)
129                         .start();
130             }
131             return true;
132         }
133         return false;
134     }
135 
update(int newState, boolean pulsing, boolean dozing, boolean keyguardJustShown)136     void update(int newState, boolean pulsing, boolean dozing, boolean keyguardJustShown) {
137         mOldState = mState;
138         mState = newState;
139         mPulsing = pulsing;
140         mDozing = dozing;
141         mKeyguardJustShown = keyguardJustShown;
142 
143         if (!mPredrawRegistered) {
144             mPredrawRegistered = true;
145             getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
146         }
147     }
148 
setDozeAmount(float dozeAmount)149     void setDozeAmount(float dozeAmount) {
150         mDozeAmount = dozeAmount;
151         updateDarkTint();
152     }
153 
onThemeChange(int iconColor)154     void onThemeChange(int iconColor) {
155         mDrawableCache.clear();
156         mIconColor = iconColor;
157         updateDarkTint();
158     }
159 
updateDarkTint()160     private void updateDarkTint() {
161         int color = ColorUtils.blendARGB(mIconColor, Color.WHITE, mDozeAmount);
162         setImageTintList(ColorStateList.valueOf(color));
163     }
164 
getIcon(int newState)165     private Drawable getIcon(int newState) {
166         @LockAnimIndex final int lockAnimIndex =
167                 getAnimationIndexForTransition(mOldState, newState, mPulsing, mDozing,
168                         mKeyguardJustShown);
169 
170         boolean isAnim = lockAnimIndex != -1;
171         int iconRes = isAnim ? getThemedAnimationResId(lockAnimIndex) : getIconForState(newState);
172 
173         if (!mDrawableCache.contains(iconRes)) {
174             mDrawableCache.put(iconRes, getResources().getDrawable(iconRes));
175         }
176 
177         return mDrawableCache.get(iconRes);
178     }
179 
getIconForState(int state)180     private static int getIconForState(int state) {
181         int iconRes;
182         switch (state) {
183             case STATE_LOCKED:
184             // Scanning animation is a pulsing padlock. This means that the resting state is
185             // just a padlock.
186             case STATE_SCANNING_FACE:
187             // Error animation also starts and ands on the padlock.
188             case STATE_BIOMETRICS_ERROR:
189                 iconRes = com.android.internal.R.drawable.ic_lock;
190                 break;
191             case STATE_LOCK_OPEN:
192                 iconRes = com.android.internal.R.drawable.ic_lock_open;
193                 break;
194             default:
195                 throw new IllegalArgumentException();
196         }
197 
198         return iconRes;
199     }
200 
getAnimationIndexForTransition(int oldState, int newState, boolean pulsing, boolean dozing, boolean keyguardJustShown)201     private static int getAnimationIndexForTransition(int oldState, int newState, boolean pulsing,
202             boolean dozing, boolean keyguardJustShown) {
203 
204         // Never animate when screen is off
205         if (dozing && !pulsing) {
206             return -1;
207         }
208 
209         if (newState == STATE_BIOMETRICS_ERROR) {
210             return ERROR;
211         } else if (oldState != STATE_LOCK_OPEN && newState == STATE_LOCK_OPEN) {
212             return UNLOCK;
213         } else if (oldState == STATE_LOCK_OPEN && newState == STATE_LOCKED && !keyguardJustShown) {
214             return LOCK;
215         } else if (newState == STATE_SCANNING_FACE) {
216             return SCANNING;
217         }
218         return -1;
219     }
220 
221     @Retention(RetentionPolicy.SOURCE)
222     @IntDef({ERROR, UNLOCK, LOCK, SCANNING})
223     @interface LockAnimIndex {}
224     static final int ERROR = 0, UNLOCK = 1, LOCK = 2, SCANNING = 3;
225     private static final int[][] LOCK_ANIM_RES_IDS = new int[][] {
226             {
227                     R.anim.lock_to_error,
228                     R.anim.lock_unlock,
229                     R.anim.lock_lock,
230                     R.anim.lock_scanning
231             },
232             {
233                     R.anim.lock_to_error_circular,
234                     R.anim.lock_unlock_circular,
235                     R.anim.lock_lock_circular,
236                     R.anim.lock_scanning_circular
237             },
238             {
239                     R.anim.lock_to_error_filled,
240                     R.anim.lock_unlock_filled,
241                     R.anim.lock_lock_filled,
242                     R.anim.lock_scanning_filled
243             },
244             {
245                     R.anim.lock_to_error_rounded,
246                     R.anim.lock_unlock_rounded,
247                     R.anim.lock_lock_rounded,
248                     R.anim.lock_scanning_rounded
249             },
250     };
251 
getThemedAnimationResId(@ockAnimIndex int lockAnimIndex)252     private int getThemedAnimationResId(@LockAnimIndex int lockAnimIndex) {
253         final String setting = TextUtils.emptyIfNull(
254                 Settings.Secure.getString(getContext().getContentResolver(),
255                         Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES));
256         if (setting.contains("com.android.theme.icon_pack.circular.android")) {
257             return LOCK_ANIM_RES_IDS[1][lockAnimIndex];
258         } else if (setting.contains("com.android.theme.icon_pack.filled.android")) {
259             return LOCK_ANIM_RES_IDS[2][lockAnimIndex];
260         } else if (setting.contains("com.android.theme.icon_pack.rounded.android")) {
261             return LOCK_ANIM_RES_IDS[3][lockAnimIndex];
262         }
263         return LOCK_ANIM_RES_IDS[0][lockAnimIndex];
264     }
265 }
266