1 /*
2  * Copyright (C) 2019 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 package com.android.launcher3.icons;
17 
18 import static com.android.launcher3.icons.ThemedIconDrawable.getColors;
19 
20 import android.annotation.TargetApi;
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageManager;
24 import android.content.res.Resources;
25 import android.content.res.TypedArray;
26 import android.graphics.Bitmap;
27 import android.graphics.Canvas;
28 import android.graphics.ColorFilter;
29 import android.graphics.Paint;
30 import android.graphics.PorterDuff.Mode;
31 import android.graphics.PorterDuffColorFilter;
32 import android.graphics.Rect;
33 import android.graphics.drawable.AdaptiveIconDrawable;
34 import android.graphics.drawable.ColorDrawable;
35 import android.graphics.drawable.Drawable;
36 import android.graphics.drawable.LayerDrawable;
37 import android.os.Build;
38 import android.os.Bundle;
39 import android.os.Process;
40 import android.os.SystemClock;
41 import android.os.UserHandle;
42 import android.util.Log;
43 import android.util.TypedValue;
44 
45 import androidx.annotation.Nullable;
46 
47 import com.android.launcher3.icons.ThemedIconDrawable.ThemeData;
48 
49 import java.util.Calendar;
50 import java.util.concurrent.TimeUnit;
51 import java.util.function.IntFunction;
52 
53 /**
54  * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic
55  * clock icons
56  */
57 @TargetApi(Build.VERSION_CODES.O)
58 public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender {
59 
60     private static final String TAG = "ClockDrawableWrapper";
61 
62     private static final boolean DISABLE_SECONDS = true;
63 
64     // Time after which the clock icon should check for an update. The actual invalidate
65     // will only happen in case of any change.
66     public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L;
67 
68     private static final String LAUNCHER_PACKAGE = "com.android.launcher3";
69     private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE
70             + ".LEVEL_PER_TICK_ICON_ROUND";
71     private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX";
72     private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
73             + ".MINUTE_LAYER_INDEX";
74     private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
75             + ".SECOND_LAYER_INDEX";
76     private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE
77             + ".DEFAULT_HOUR";
78     private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE
79             + ".DEFAULT_MINUTE";
80     private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE
81             + ".DEFAULT_SECOND";
82 
83     /* Number of levels to jump per second for the second hand */
84     private static final int LEVELS_PER_SECOND = 10;
85 
86     public static final int INVALID_VALUE = -1;
87 
88     private final AnimationInfo mAnimationInfo = new AnimationInfo();
89     private int mTargetSdkVersion;
90     protected ThemeData mThemeData;
91 
ClockDrawableWrapper(AdaptiveIconDrawable base)92     public ClockDrawableWrapper(AdaptiveIconDrawable base) {
93         super(base.getBackground(), base.getForeground());
94     }
95 
96     /**
97      * Loads and returns the wrapper from the provided package, or returns null
98      * if it is unable to load.
99      */
forPackage(Context context, String pkg, int iconDpi)100     public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi) {
101         try {
102             PackageManager pm = context.getPackageManager();
103             ApplicationInfo appInfo =  pm.getApplicationInfo(pkg,
104                     PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
105             Resources res = pm.getResourcesForApplication(appInfo);
106             return forExtras(appInfo, appInfo.metaData,
107                     resId -> res.getDrawableForDensity(resId, iconDpi));
108         } catch (Exception e) {
109             Log.d(TAG, "Unable to load clock drawable info", e);
110         }
111         return null;
112     }
113 
fromThemeData(Context context, ThemeData themeData)114     private static ClockDrawableWrapper fromThemeData(Context context, ThemeData themeData) {
115         try {
116             TypedArray ta = themeData.mResources.obtainTypedArray(themeData.mResID);
117             int count = ta.length();
118             Bundle extras = new Bundle();
119             for (int i = 0; i < count; i += 2) {
120                 TypedValue v = ta.peekValue(i + 1);
121                 extras.putInt(ta.getString(i), v.type >= TypedValue.TYPE_FIRST_INT
122                         && v.type <= TypedValue.TYPE_LAST_INT
123                         ? v.data : v.resourceId);
124             }
125             ta.recycle();
126             ClockDrawableWrapper drawable = ClockDrawableWrapper.forExtras(
127                     context.getApplicationInfo(), extras, resId -> {
128                         int[] colors = getColors(context);
129                         Drawable bg = new ColorDrawable(colors[0]);
130                         Drawable fg = themeData.mResources.getDrawable(resId).mutate();
131                         fg.setTint(colors[1]);
132                         return new AdaptiveIconDrawable(bg, fg);
133                     });
134             if (drawable != null) {
135                 return drawable;
136             }
137         } catch (Exception e) {
138             Log.e(TAG, "Error loading themed clock", e);
139         }
140         return null;
141     }
142 
forExtras(ApplicationInfo appInfo, Bundle metadata, IntFunction<Drawable> drawableProvider)143     private static ClockDrawableWrapper forExtras(ApplicationInfo appInfo, Bundle metadata,
144             IntFunction<Drawable> drawableProvider) {
145         if (metadata == null) {
146             return null;
147         }
148         int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
149         if (drawableId == 0) {
150             return null;
151         }
152 
153         Drawable drawable = drawableProvider.apply(drawableId).mutate();
154         if (!(drawable instanceof AdaptiveIconDrawable)) {
155             return null;
156         }
157 
158         ClockDrawableWrapper wrapper =
159                 new ClockDrawableWrapper((AdaptiveIconDrawable) drawable);
160         wrapper.mTargetSdkVersion = appInfo.targetSdkVersion;
161         AnimationInfo info = wrapper.mAnimationInfo;
162 
163         info.baseDrawableState = drawable.getConstantState();
164 
165         info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE);
166         info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE);
167         info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE);
168 
169         info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0);
170         info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0);
171         info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0);
172 
173         LayerDrawable foreground = (LayerDrawable) wrapper.getForeground();
174         int layerCount = foreground.getNumberOfLayers();
175         if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) {
176             info.hourLayerIndex = INVALID_VALUE;
177         }
178         if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) {
179             info.minuteLayerIndex = INVALID_VALUE;
180         }
181         if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) {
182             info.secondLayerIndex = INVALID_VALUE;
183         } else if (DISABLE_SECONDS) {
184             foreground.setDrawable(info.secondLayerIndex, null);
185             info.secondLayerIndex = INVALID_VALUE;
186         }
187         info.applyTime(Calendar.getInstance(), foreground);
188         return wrapper;
189     }
190 
191     @Override
getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory, float normalizationScale, UserHandle user)192     public ClockBitmapInfo getExtendedInfo(Bitmap bitmap, int color,
193             BaseIconFactory iconFactory, float normalizationScale, UserHandle user) {
194         iconFactory.disableColorExtraction();
195         AdaptiveIconDrawable background = new AdaptiveIconDrawable(
196                 getBackground().getConstantState().newDrawable(), null);
197         BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background,
198                 Process.myUserHandle(), mTargetSdkVersion, false);
199 
200         return new ClockBitmapInfo(bitmap, color, normalizationScale,
201                 mAnimationInfo, bitmapInfo.icon, mThemeData);
202     }
203 
204     @Override
drawForPersistence(Canvas canvas)205     public void drawForPersistence(Canvas canvas) {
206         LayerDrawable foreground = (LayerDrawable) getForeground();
207         resetLevel(foreground, mAnimationInfo.hourLayerIndex);
208         resetLevel(foreground, mAnimationInfo.minuteLayerIndex);
209         resetLevel(foreground, mAnimationInfo.secondLayerIndex);
210         draw(canvas);
211         mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
212     }
213 
214     @Override
getThemedDrawable(Context context)215     public Drawable getThemedDrawable(Context context) {
216         if (mThemeData != null) {
217             ClockDrawableWrapper drawable = fromThemeData(context, mThemeData);
218             return drawable == null ? this : drawable;
219         }
220         return this;
221     }
222 
resetLevel(LayerDrawable drawable, int index)223     private void resetLevel(LayerDrawable drawable, int index) {
224         if (index != INVALID_VALUE) {
225             drawable.getDrawable(index).setLevel(0);
226         }
227     }
228 
229     private static class AnimationInfo {
230 
231         public ConstantState baseDrawableState;
232 
233         public int hourLayerIndex;
234         public int minuteLayerIndex;
235         public int secondLayerIndex;
236         public int defaultHour;
237         public int defaultMinute;
238         public int defaultSecond;
239 
applyTime(Calendar time, LayerDrawable foregroundDrawable)240         boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) {
241             time.setTimeInMillis(System.currentTimeMillis());
242 
243             // We need to rotate by the difference from the default time if one is specified.
244             int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12;
245             int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60;
246             int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60;
247 
248             boolean invalidate = false;
249             if (hourLayerIndex != INVALID_VALUE) {
250                 final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex);
251                 if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) {
252                     invalidate = true;
253                 }
254             }
255 
256             if (minuteLayerIndex != INVALID_VALUE) {
257                 final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex);
258                 if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) {
259                     invalidate = true;
260                 }
261             }
262 
263             if (secondLayerIndex != INVALID_VALUE) {
264                 final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex);
265                 if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) {
266                     invalidate = true;
267                 }
268             }
269 
270             return invalidate;
271         }
272     }
273 
274     static class ClockBitmapInfo extends BitmapInfo {
275 
276         public final float scale;
277         public final int offset;
278         public final AnimationInfo animInfo;
279         public final Bitmap mFlattenedBackground;
280 
281         public final ThemeData themeData;
282         public final ColorFilter bgFilter;
283 
ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, Bitmap background, ThemeData themeData)284         ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo,
285                 Bitmap background, ThemeData themeData) {
286             this(icon, color, scale, animInfo, background, themeData, null);
287         }
288 
ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, Bitmap background, ThemeData themeData, ColorFilter bgFilter)289         ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo,
290                 Bitmap background, ThemeData themeData, ColorFilter bgFilter) {
291             super(icon, color);
292             this.scale = scale;
293             this.animInfo = animInfo;
294             this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth());
295             this.mFlattenedBackground = background;
296             this.themeData = themeData;
297             this.bgFilter = bgFilter;
298         }
299 
300         @Override
newThemedIcon(Context context)301         public FastBitmapDrawable newThemedIcon(Context context) {
302             if (themeData != null) {
303                 ClockDrawableWrapper wrapper = fromThemeData(context, themeData);
304                 if (wrapper != null) {
305                     int[] colors = getColors(context);
306                     ColorFilter bgFilter = new PorterDuffColorFilter(colors[0], Mode.SRC_ATOP);
307                     return new ClockBitmapInfo(icon, colors[1], scale,
308                             wrapper.mAnimationInfo, mFlattenedBackground, themeData, bgFilter)
309                             .newIcon(context);
310                 }
311             }
312             return super.newThemedIcon(context);
313         }
314 
315         @Override
newIcon(Context context)316         public FastBitmapDrawable newIcon(Context context) {
317             ClockIconDrawable d = new ClockIconDrawable(this);
318             d.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
319             return d;
320         }
321 
322         @Nullable
323         @Override
toByteArray()324         public byte[] toByteArray() {
325             return null;
326         }
327 
drawBackground(Canvas canvas, Rect bounds, Paint paint)328         void drawBackground(Canvas canvas, Rect bounds, Paint paint) {
329             // draw the background that is already flattened to a bitmap
330             ColorFilter oldFilter = paint.getColorFilter();
331             if (bgFilter != null) {
332                 paint.setColorFilter(bgFilter);
333             }
334             canvas.drawBitmap(mFlattenedBackground, null, bounds, paint);
335             paint.setColorFilter(oldFilter);
336         }
337     }
338 
339     private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
340 
341         private final Calendar mTime = Calendar.getInstance();
342 
343         private final ClockBitmapInfo mInfo;
344 
345         private final AdaptiveIconDrawable mFullDrawable;
346         private final LayerDrawable mForeground;
347 
ClockIconDrawable(ClockBitmapInfo clockInfo)348         ClockIconDrawable(ClockBitmapInfo clockInfo) {
349             super(clockInfo);
350 
351             mInfo = clockInfo;
352             mFullDrawable = (AdaptiveIconDrawable) mInfo.animInfo.baseDrawableState
353                     .newDrawable().mutate();
354             mForeground = (LayerDrawable) mFullDrawable.getForeground();
355         }
356 
357         @Override
onBoundsChange(Rect bounds)358         protected void onBoundsChange(Rect bounds) {
359             super.onBoundsChange(bounds);
360             mFullDrawable.setBounds(bounds);
361         }
362 
363         @Override
drawInternal(Canvas canvas, Rect bounds)364         public void drawInternal(Canvas canvas, Rect bounds) {
365             if (mInfo == null) {
366                 super.drawInternal(canvas, bounds);
367                 return;
368             }
369             mInfo.drawBackground(canvas, bounds, mPaint);
370 
371             // prepare and draw the foreground
372             mInfo.animInfo.applyTime(mTime, mForeground);
373 
374             int saveCount = canvas.save();
375             canvas.scale(mInfo.scale, mInfo.scale,
376                     bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset);
377             canvas.clipPath(mFullDrawable.getIconMask());
378             mForeground.setBounds(bounds);
379             mForeground.draw(canvas);
380             canvas.restoreToCount(saveCount);
381 
382             reschedule();
383         }
384 
385         @Override
isThemed()386         public boolean isThemed() {
387             return mInfo.bgFilter != null;
388         }
389 
390         @Override
updateFilter()391         protected void updateFilter() {
392             super.updateFilter();
393             mFullDrawable.setColorFilter(mPaint.getColorFilter());
394         }
395 
396         @Override
run()397         public void run() {
398             if (mInfo.animInfo.applyTime(mTime, mForeground)) {
399                 invalidateSelf();
400             } else {
401                 reschedule();
402             }
403         }
404 
405         @Override
setVisible(boolean visible, boolean restart)406         public boolean setVisible(boolean visible, boolean restart) {
407             boolean result = super.setVisible(visible, restart);
408             if (visible) {
409                 reschedule();
410             } else {
411                 unscheduleSelf(this);
412             }
413             return result;
414         }
415 
reschedule()416         private void reschedule() {
417             if (!isVisible()) {
418                 return;
419             }
420 
421             unscheduleSelf(this);
422             final long upTime = SystemClock.uptimeMillis();
423             final long step = TICK_MS; /* tick every 200 ms */
424             scheduleSelf(this, upTime - ((upTime % step)) + step);
425         }
426 
427         @Override
getConstantState()428         public ConstantState getConstantState() {
429             return new ClockConstantState(mInfo, isDisabled());
430         }
431 
432         private static class ClockConstantState extends FastBitmapConstantState {
433 
434             private final ClockBitmapInfo mInfo;
435 
ClockConstantState(ClockBitmapInfo info, boolean isDisabled)436             ClockConstantState(ClockBitmapInfo info, boolean isDisabled) {
437                 super(info.icon, info.color, isDisabled);
438                 mInfo = info;
439             }
440 
441             @Override
newDrawable()442             public FastBitmapDrawable newDrawable() {
443                 ClockIconDrawable drawable = new ClockIconDrawable(mInfo);
444                 drawable.setIsDisabled(mIsDisabled);
445                 return drawable;
446             }
447         }
448     }
449 }
450