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