| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.launcher3.icons; |
| |
| import static com.android.launcher3.icons.ThemedIconDrawable.getColors; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.Paint; |
| import android.graphics.PorterDuff.Mode; |
| import android.graphics.PorterDuffColorFilter; |
| import android.graphics.Rect; |
| import android.graphics.drawable.AdaptiveIconDrawable; |
| import android.graphics.drawable.ColorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.LayerDrawable; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Process; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.util.Log; |
| import android.util.TypedValue; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.launcher3.icons.ThemedIconDrawable.ThemeData; |
| |
| import java.util.Calendar; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.IntFunction; |
| |
| /** |
| * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic |
| * clock icons |
| */ |
| @TargetApi(Build.VERSION_CODES.O) |
| public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender { |
| |
| private static final String TAG = "ClockDrawableWrapper"; |
| |
| private static final boolean DISABLE_SECONDS = true; |
| |
| // Time after which the clock icon should check for an update. The actual invalidate |
| // will only happen in case of any change. |
| public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L; |
| |
| private static final String LAUNCHER_PACKAGE = "com.android.launcher3"; |
| private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE |
| + ".LEVEL_PER_TICK_ICON_ROUND"; |
| private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX"; |
| private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE |
| + ".MINUTE_LAYER_INDEX"; |
| private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE |
| + ".SECOND_LAYER_INDEX"; |
| private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE |
| + ".DEFAULT_HOUR"; |
| private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE |
| + ".DEFAULT_MINUTE"; |
| private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE |
| + ".DEFAULT_SECOND"; |
| |
| /* Number of levels to jump per second for the second hand */ |
| private static final int LEVELS_PER_SECOND = 10; |
| |
| public static final int INVALID_VALUE = -1; |
| |
| private final AnimationInfo mAnimationInfo = new AnimationInfo(); |
| protected ThemeData mThemeData; |
| |
| public ClockDrawableWrapper(AdaptiveIconDrawable base) { |
| super(base.getBackground(), base.getForeground()); |
| } |
| |
| /** |
| * Loads and returns the wrapper from the provided package, or returns null |
| * if it is unable to load. |
| */ |
| public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi) { |
| try { |
| PackageManager pm = context.getPackageManager(); |
| ApplicationInfo appInfo = pm.getApplicationInfo(pkg, |
| PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA); |
| Resources res = pm.getResourcesForApplication(appInfo); |
| return forExtras(appInfo.metaData, |
| resId -> res.getDrawableForDensity(resId, iconDpi)); |
| } catch (Exception e) { |
| Log.d(TAG, "Unable to load clock drawable info", e); |
| } |
| return null; |
| } |
| |
| private static ClockDrawableWrapper fromThemeData(Context context, ThemeData themeData) { |
| try { |
| TypedArray ta = themeData.mResources.obtainTypedArray(themeData.mResID); |
| int count = ta.length(); |
| Bundle extras = new Bundle(); |
| for (int i = 0; i < count; i += 2) { |
| TypedValue v = ta.peekValue(i + 1); |
| extras.putInt(ta.getString(i), v.type >= TypedValue.TYPE_FIRST_INT |
| && v.type <= TypedValue.TYPE_LAST_INT |
| ? v.data : v.resourceId); |
| } |
| ta.recycle(); |
| ClockDrawableWrapper drawable = ClockDrawableWrapper.forExtras(extras, resId -> { |
| int[] colors = getColors(context); |
| Drawable bg = new ColorDrawable(colors[0]); |
| Drawable fg = themeData.mResources.getDrawable(resId).mutate(); |
| fg.setTint(colors[1]); |
| return new AdaptiveIconDrawable(bg, fg); |
| }); |
| if (drawable != null) { |
| return drawable; |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Error loading themed clock", e); |
| } |
| return null; |
| } |
| |
| private static ClockDrawableWrapper forExtras( |
| Bundle metadata, IntFunction<Drawable> drawableProvider) { |
| if (metadata == null) { |
| return null; |
| } |
| int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0); |
| if (drawableId == 0) { |
| return null; |
| } |
| |
| Drawable drawable = drawableProvider.apply(drawableId).mutate(); |
| if (!(drawable instanceof AdaptiveIconDrawable)) { |
| return null; |
| } |
| |
| ClockDrawableWrapper wrapper = |
| new ClockDrawableWrapper((AdaptiveIconDrawable) drawable); |
| AnimationInfo info = wrapper.mAnimationInfo; |
| |
| info.baseDrawableState = drawable.getConstantState(); |
| |
| info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE); |
| info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE); |
| info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE); |
| |
| info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0); |
| info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0); |
| info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0); |
| |
| LayerDrawable foreground = (LayerDrawable) wrapper.getForeground(); |
| int layerCount = foreground.getNumberOfLayers(); |
| if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) { |
| info.hourLayerIndex = INVALID_VALUE; |
| } |
| if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) { |
| info.minuteLayerIndex = INVALID_VALUE; |
| } |
| if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) { |
| info.secondLayerIndex = INVALID_VALUE; |
| } else if (DISABLE_SECONDS) { |
| foreground.setDrawable(info.secondLayerIndex, null); |
| info.secondLayerIndex = INVALID_VALUE; |
| } |
| info.applyTime(Calendar.getInstance(), foreground); |
| return wrapper; |
| } |
| |
| @Override |
| public ClockBitmapInfo getExtendedInfo(Bitmap bitmap, int color, |
| BaseIconFactory iconFactory, float normalizationScale) { |
| iconFactory.disableColorExtraction(); |
| AdaptiveIconDrawable background = new AdaptiveIconDrawable( |
| getBackground().getConstantState().newDrawable(), null); |
| BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background); |
| return new ClockBitmapInfo(bitmap, color, normalizationScale, |
| mAnimationInfo, bitmapInfo.icon, mThemeData); |
| } |
| |
| @Override |
| public void drawForPersistence(Canvas canvas) { |
| LayerDrawable foreground = (LayerDrawable) getForeground(); |
| resetLevel(foreground, mAnimationInfo.hourLayerIndex); |
| resetLevel(foreground, mAnimationInfo.minuteLayerIndex); |
| resetLevel(foreground, mAnimationInfo.secondLayerIndex); |
| draw(canvas); |
| mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground()); |
| } |
| |
| @Override |
| public Drawable getThemedDrawable(Context context) { |
| if (mThemeData != null) { |
| ClockDrawableWrapper drawable = fromThemeData(context, mThemeData); |
| return drawable == null ? this : drawable; |
| } |
| return this; |
| } |
| |
| private void resetLevel(LayerDrawable drawable, int index) { |
| if (index != INVALID_VALUE) { |
| drawable.getDrawable(index).setLevel(0); |
| } |
| } |
| |
| private static class AnimationInfo { |
| |
| public ConstantState baseDrawableState; |
| |
| public int hourLayerIndex; |
| public int minuteLayerIndex; |
| public int secondLayerIndex; |
| public int defaultHour; |
| public int defaultMinute; |
| public int defaultSecond; |
| |
| boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) { |
| time.setTimeInMillis(System.currentTimeMillis()); |
| |
| // We need to rotate by the difference from the default time if one is specified. |
| int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12; |
| int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60; |
| int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60; |
| |
| boolean invalidate = false; |
| if (hourLayerIndex != INVALID_VALUE) { |
| final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex); |
| if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) { |
| invalidate = true; |
| } |
| } |
| |
| if (minuteLayerIndex != INVALID_VALUE) { |
| final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex); |
| if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) { |
| invalidate = true; |
| } |
| } |
| |
| if (secondLayerIndex != INVALID_VALUE) { |
| final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex); |
| if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) { |
| invalidate = true; |
| } |
| } |
| |
| return invalidate; |
| } |
| } |
| |
| static class ClockBitmapInfo extends BitmapInfo { |
| |
| public final float scale; |
| public final int offset; |
| public final AnimationInfo animInfo; |
| public final Bitmap mFlattenedBackground; |
| |
| public final ThemeData themeData; |
| public final ColorFilter bgFilter; |
| |
| ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, |
| Bitmap background, ThemeData themeData) { |
| this(icon, color, scale, animInfo, background, themeData, null); |
| } |
| |
| ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, |
| Bitmap background, ThemeData themeData, ColorFilter bgFilter) { |
| super(icon, color); |
| this.scale = scale; |
| this.animInfo = animInfo; |
| this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth()); |
| this.mFlattenedBackground = background; |
| this.themeData = themeData; |
| this.bgFilter = bgFilter; |
| } |
| |
| @Override |
| public FastBitmapDrawable newIcon(Context context, |
| @DrawableCreationFlags int creationFlags) { |
| FastBitmapDrawable d = null; |
| if ((creationFlags & FLAG_THEMED) != 0 && themeData != null) { |
| ClockDrawableWrapper wrapper = fromThemeData(context, themeData); |
| if (wrapper != null) { |
| int[] colors = getColors(context); |
| ColorFilter bgFilter = new PorterDuffColorFilter(colors[0], Mode.SRC_ATOP); |
| d = new ClockBitmapInfo(icon, colors[1], scale, |
| wrapper.mAnimationInfo, mFlattenedBackground, themeData, bgFilter) |
| .newIcon(context); |
| } |
| } |
| if (d == null) { |
| d = new ClockIconDrawable(this); |
| } |
| applyFlags(context, d, creationFlags); |
| return d; |
| } |
| |
| @Nullable |
| @Override |
| public byte[] toByteArray() { |
| return null; |
| } |
| |
| void drawBackground(Canvas canvas, Rect bounds, Paint paint) { |
| // draw the background that is already flattened to a bitmap |
| ColorFilter oldFilter = paint.getColorFilter(); |
| if (bgFilter != null) { |
| paint.setColorFilter(bgFilter); |
| } |
| canvas.drawBitmap(mFlattenedBackground, null, bounds, paint); |
| paint.setColorFilter(oldFilter); |
| } |
| } |
| |
| private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable { |
| |
| private final Calendar mTime = Calendar.getInstance(); |
| |
| private final ClockBitmapInfo mInfo; |
| |
| private final AdaptiveIconDrawable mFullDrawable; |
| private final LayerDrawable mForeground; |
| |
| ClockIconDrawable(ClockBitmapInfo clockInfo) { |
| super(clockInfo); |
| |
| mInfo = clockInfo; |
| mFullDrawable = (AdaptiveIconDrawable) mInfo.animInfo.baseDrawableState |
| .newDrawable().mutate(); |
| mForeground = (LayerDrawable) mFullDrawable.getForeground(); |
| } |
| |
| @Override |
| protected void onBoundsChange(Rect bounds) { |
| super.onBoundsChange(bounds); |
| mFullDrawable.setBounds(bounds); |
| } |
| |
| @Override |
| public void drawInternal(Canvas canvas, Rect bounds) { |
| if (mInfo == null) { |
| super.drawInternal(canvas, bounds); |
| return; |
| } |
| mInfo.drawBackground(canvas, bounds, mPaint); |
| |
| // prepare and draw the foreground |
| mInfo.animInfo.applyTime(mTime, mForeground); |
| |
| int saveCount = canvas.save(); |
| canvas.scale(mInfo.scale, mInfo.scale, |
| bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset); |
| canvas.clipPath(mFullDrawable.getIconMask()); |
| mForeground.setBounds(bounds); |
| mForeground.draw(canvas); |
| canvas.restoreToCount(saveCount); |
| |
| reschedule(); |
| } |
| |
| @Override |
| public boolean isThemed() { |
| return mInfo.bgFilter != null; |
| } |
| |
| @Override |
| protected void updateFilter() { |
| super.updateFilter(); |
| mFullDrawable.setColorFilter(mPaint.getColorFilter()); |
| } |
| |
| @Override |
| public void run() { |
| if (mInfo.animInfo.applyTime(mTime, mForeground)) { |
| invalidateSelf(); |
| } else { |
| reschedule(); |
| } |
| } |
| |
| @Override |
| public boolean setVisible(boolean visible, boolean restart) { |
| boolean result = super.setVisible(visible, restart); |
| if (visible) { |
| reschedule(); |
| } else { |
| unscheduleSelf(this); |
| } |
| return result; |
| } |
| |
| private void reschedule() { |
| if (!isVisible()) { |
| return; |
| } |
| |
| unscheduleSelf(this); |
| final long upTime = SystemClock.uptimeMillis(); |
| final long step = TICK_MS; /* tick every 200 ms */ |
| scheduleSelf(this, upTime - ((upTime % step)) + step); |
| } |
| |
| @Override |
| public FastBitmapConstantState newConstantState() { |
| return new ClockConstantState(mInfo); |
| } |
| |
| private static class ClockConstantState extends FastBitmapConstantState { |
| |
| private final ClockBitmapInfo mInfo; |
| |
| ClockConstantState(ClockBitmapInfo info) { |
| super(info.icon, info.color); |
| mInfo = info; |
| } |
| |
| @Override |
| public FastBitmapDrawable createDrawable() { |
| return new ClockIconDrawable(mInfo); |
| } |
| } |
| } |
| } |