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.app.animation.Interpolators;
35 import com.android.internal.graphics.ColorUtils;
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 = Color.TRANSPARENT;
53     private int mOldState;
54     private int mState;
55     private boolean mDozing;
56     private boolean mKeyguardJustShown;
57     private boolean mPredrawRegistered;
58     private final SparseArray<Drawable> mDrawableCache = new SparseArray<>();
59 
60     private final OnPreDrawListener mOnPreDrawListener = new OnPreDrawListener() {
61         @Override
62         public boolean onPreDraw() {
63             getViewTreeObserver().removeOnPreDrawListener(this);
64             mPredrawRegistered = false;
65 
66             int newState = mState;
67             Drawable icon = getIcon(newState);
68             setImageDrawable(icon, false);
69 
70             if (newState == STATE_SCANNING_FACE) {
71                 announceForAccessibility(getResources().getString(
72                         R.string.accessibility_scanning_face));
73             }
74 
75             if (icon instanceof AnimatedVectorDrawable) {
76                 final AnimatedVectorDrawable animation = (AnimatedVectorDrawable) icon;
77                 animation.forceAnimationOnUI();
78                 animation.clearAnimationCallbacks();
79                 animation.registerAnimationCallback(
80                         new Animatable2.AnimationCallback() {
81                             @Override
82                             public void onAnimationEnd(Drawable drawable) {
83                                 if (getDrawable() == animation
84                                         && newState == mState
85                                         && newState == STATE_SCANNING_FACE) {
86                                     animation.start();
87                                 } else {
88                                     Trace.endAsyncSection("LockIcon#Animation", newState);
89                                 }
90                             }
91                         });
92                 Trace.beginAsyncSection("LockIcon#Animation", newState);
93                 animation.start();
94             }
95 
96             return true;
97         }
98     };
99 
LockIcon(Context context, AttributeSet attrs)100     public LockIcon(Context context, AttributeSet attrs) {
101         super(context, attrs);
102     }
103 
104     @Override
onConfigurationChanged(Configuration newConfig)105     protected void onConfigurationChanged(Configuration newConfig) {
106         super.onConfigurationChanged(newConfig);
107         mDrawableCache.clear();
108     }
109 
110     /**
111      * Update the icon visibility
112      * @return true if the visibility changed
113      */
updateIconVisibility(boolean visible)114     boolean updateIconVisibility(boolean visible) {
115         boolean wasVisible = getVisibility() == VISIBLE;
116         if (visible != wasVisible) {
117             setVisibility(visible ? VISIBLE : INVISIBLE);
118             animate().cancel();
119             if (visible) {
120                 setScaleX(0);
121                 setScaleY(0);
122                 animate()
123                         .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
124                         .scaleX(1)
125                         .scaleY(1)
126                         .withLayer()
127                         .setDuration(233)
128                         .start();
129             }
130             return true;
131         }
132         return false;
133     }
134 
update(int newState, boolean dozing, boolean keyguardJustShown)135     void update(int newState, boolean dozing, boolean keyguardJustShown) {
136         mOldState = mState;
137         mState = newState;
138         mDozing = dozing;
139         mKeyguardJustShown = keyguardJustShown;
140 
141         if (!mPredrawRegistered) {
142             mPredrawRegistered = true;
143             getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
144         }
145     }
146 
setDozeAmount(float dozeAmount)147     void setDozeAmount(float dozeAmount) {
148         mDozeAmount = dozeAmount;
149         updateDarkTint();
150     }
151 
updateColor(int iconColor)152     void updateColor(int iconColor) {
153         if (mIconColor == iconColor) {
154             return;
155         }
156         mDrawableCache.clear();
157         mIconColor = iconColor;
158         updateDarkTint();
159     }
160 
updateDarkTint()161     private void updateDarkTint() {
162         int color = ColorUtils.blendARGB(mIconColor, Color.WHITE, mDozeAmount);
163         setImageTintList(ColorStateList.valueOf(color));
164     }
165 
getIcon(int newState)166     private Drawable getIcon(int newState) {
167         @LockAnimIndex final int lockAnimIndex =
168                 getAnimationIndexForTransition(mOldState, newState, mDozing, 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, getContext().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 dozing, boolean keyguardJustShown)201     private static int getAnimationIndexForTransition(int oldState, int newState, boolean dozing,
202             boolean keyguardJustShown) {
203 
204         // Never animate when screen is off
205         if (dozing) {
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