blob: 252c0c3b9203f8883c952c9055cb876ca8a33943 [file] [log] [blame]
/*
* 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.IconProvider.ATLEAST_T;
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.BlendMode;
import android.graphics.BlendModeColorFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
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.SystemClock;
import android.util.Log;
import android.util.TypedValue;
import androidx.annotation.Nullable;
import com.android.launcher3.icons.IconProvider.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;
private static final int NO_COLOR = -1;
// 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();
private AnimationInfo mThemeInfo = null;
private ClockDrawableWrapper(AdaptiveIconDrawable base) {
super(base.getBackground(), base.getForeground());
}
private void applyThemeData(ThemeData themeData) {
if (!IconProvider.ATLEAST_T || mThemeInfo != null) {
return;
}
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 -> {
Drawable bg = new ColorDrawable(Color.WHITE);
Drawable fg = themeData.mResources.getDrawable(resId).mutate();
return new AdaptiveIconDrawable(bg, fg);
});
if (drawable != null) {
mThemeInfo = drawable.mAnimationInfo;
}
} catch (Exception e) {
Log.e(TAG, "Error loading themed clock", e);
}
}
@Override
public Drawable getMonochrome() {
if (mThemeInfo == null) {
return null;
}
Drawable d = mThemeInfo.baseDrawableState.newDrawable().mutate();
if (d instanceof AdaptiveIconDrawable) {
Drawable mono = ((AdaptiveIconDrawable) d).getForeground();
mThemeInfo.applyTime(Calendar.getInstance(), (LayerDrawable) mono);
return mono;
}
return null;
}
/**
* 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,
@Nullable ThemeData themeData) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(pkg,
PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
Resources res = pm.getResourcesForApplication(appInfo);
ClockDrawableWrapper wrapper = forExtras(appInfo.metaData,
resId -> res.getDrawableForDensity(resId, iconDpi));
if (wrapper != null && themeData != null) {
wrapper.applyThemeData(themeData);
}
return wrapper;
} catch (Exception e) {
Log.d(TAG, "Unable to load clock drawable info", e);
}
return null;
}
@TargetApi(Build.VERSION_CODES.TIRAMISU)
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;
}
AdaptiveIconDrawable aid = (AdaptiveIconDrawable) drawable;
ClockDrawableWrapper wrapper = new ClockDrawableWrapper(aid);
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;
}
if (ATLEAST_T && aid.getMonochrome() instanceof LayerDrawable) {
wrapper.mThemeInfo = info.copyForIcon(new AdaptiveIconDrawable(
new ColorDrawable(Color.WHITE), aid.getMonochrome().mutate()));
}
info.applyTime(Calendar.getInstance(), foreground);
return wrapper;
}
@Override
public ClockBitmapInfo getExtendedInfo(Bitmap bitmap, int color,
BaseIconFactory iconFactory, float normalizationScale) {
AdaptiveIconDrawable background = new AdaptiveIconDrawable(
getBackground().getConstantState().newDrawable(), null);
Bitmap flattenBG = iconFactory.createScaledBitmap(background,
BaseIconFactory.MODE_HARDWARE_WITH_SHADOW);
// Only pass theme info if mono-icon is enabled
AnimationInfo themeInfo = iconFactory.mMonoIconEnabled ? mThemeInfo : null;
Bitmap themeBG = themeInfo == null ? null : iconFactory.getWhiteShadowLayer();
return new ClockBitmapInfo(bitmap, color, normalizationScale,
mAnimationInfo, flattenBG, themeInfo, themeBG);
}
@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());
}
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;
public AnimationInfo copyForIcon(Drawable icon) {
AnimationInfo result = new AnimationInfo();
result.baseDrawableState = icon.getConstantState();
result.defaultHour = defaultHour;
result.defaultMinute = defaultMinute;
result.defaultSecond = defaultSecond;
result.hourLayerIndex = hourLayerIndex;
result.minuteLayerIndex = minuteLayerIndex;
result.secondLayerIndex = secondLayerIndex;
return result;
}
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 boundsOffset;
public final AnimationInfo animInfo;
public final Bitmap mFlattenedBackground;
public final AnimationInfo themeData;
public final Bitmap themeBackground;
ClockBitmapInfo(Bitmap icon, int color, float scale,
AnimationInfo animInfo, Bitmap background,
AnimationInfo themeInfo, Bitmap themeBackground) {
super(icon, color);
this.boundsOffset = Math.max(ShadowGenerator.BLUR_FACTOR, (1 - scale) / 2);
this.animInfo = animInfo;
this.mFlattenedBackground = background;
this.themeData = themeInfo;
this.themeBackground = themeBackground;
}
@Override
@TargetApi(Build.VERSION_CODES.TIRAMISU)
public FastBitmapDrawable newIcon(Context context,
@DrawableCreationFlags int creationFlags) {
AnimationInfo info;
Bitmap bg;
int themedFgColor;
ColorFilter bgFilter;
if ((creationFlags & FLAG_THEMED) != 0 && themeData != null) {
int[] colors = ThemedIconDrawable.getColors(context);
Drawable tintedDrawable = themeData.baseDrawableState.newDrawable().mutate();
themedFgColor = colors[1];
tintedDrawable.setTint(colors[1]);
info = themeData.copyForIcon(tintedDrawable);
bg = themeBackground;
bgFilter = new BlendModeColorFilter(colors[0], BlendMode.SRC_IN);
} else {
info = animInfo;
themedFgColor = NO_COLOR;
bg = mFlattenedBackground;
bgFilter = null;
}
if (info == null) {
return super.newIcon(context, creationFlags);
}
ClockIconDrawable.ClockConstantState cs = new ClockIconDrawable.ClockConstantState(
icon, color, themedFgColor, boundsOffset, info, bg, bgFilter);
FastBitmapDrawable d = cs.newDrawable();
applyFlags(context, d, creationFlags);
return d;
}
@Override
public boolean canPersist() {
return false;
}
@Override
public BitmapInfo clone() {
return copyInternalsTo(new ClockBitmapInfo(icon, color, 1 - 2 * boundsOffset, animInfo,
mFlattenedBackground, themeData, themeBackground));
}
}
private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
private final Calendar mTime = Calendar.getInstance();
private final float mBoundsOffset;
private final AnimationInfo mAnimInfo;
private final Bitmap mBG;
private final Paint mBgPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
private final ColorFilter mBgFilter;
private final int mThemedFgColor;
private final AdaptiveIconDrawable mFullDrawable;
private final LayerDrawable mFG;
private final float mCanvasScale;
ClockIconDrawable(ClockConstantState cs) {
super(cs.mBitmap, cs.mIconColor);
mBoundsOffset = cs.mBoundsOffset;
mAnimInfo = cs.mAnimInfo;
mBG = cs.mBG;
mBgFilter = cs.mBgFilter;
mBgPaint.setColorFilter(cs.mBgFilter);
mThemedFgColor = cs.mThemedFgColor;
mFullDrawable =
(AdaptiveIconDrawable) mAnimInfo.baseDrawableState.newDrawable().mutate();
mFG = (LayerDrawable) mFullDrawable.getForeground();
// Time needs to be applied here since drawInternal is NOT guaranteed to be called
// before this foreground drawable is shown on the screen.
mAnimInfo.applyTime(mTime, mFG);
mCanvasScale = 1 - 2 * mBoundsOffset;
}
@Override
public void setAlpha(int alpha) {
super.setAlpha(alpha);
mBgPaint.setAlpha(alpha);
mFG.setAlpha(alpha);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
// b/211896569 AdaptiveIcon does not work properly when bounds
// are not aligned to top/left corner
mFullDrawable.setBounds(0, 0, bounds.width(), bounds.height());
}
@Override
public void drawInternal(Canvas canvas, Rect bounds) {
if (mAnimInfo == null) {
super.drawInternal(canvas, bounds);
return;
}
canvas.drawBitmap(mBG, null, bounds, mBgPaint);
// prepare and draw the foreground
mAnimInfo.applyTime(mTime, mFG);
int saveCount = canvas.save();
canvas.translate(bounds.left, bounds.top);
canvas.scale(mCanvasScale, mCanvasScale, bounds.width() / 2, bounds.height() / 2);
canvas.clipPath(mFullDrawable.getIconMask());
mFG.draw(canvas);
canvas.restoreToCount(saveCount);
reschedule();
}
@Override
public boolean isThemed() {
return mBgPaint.getColorFilter() != null;
}
@Override
protected void updateFilter() {
super.updateFilter();
int alpha = mIsDisabled ? (int) (mDisabledAlpha * FULLY_OPAQUE) : FULLY_OPAQUE;
setAlpha(alpha);
mBgPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : mBgFilter);
mFG.setColorFilter(mIsDisabled ? getDisabledColorFilter() : null);
}
@Override
public int getIconColor() {
return isThemed() ? mThemedFgColor : super.getIconColor();
}
@Override
public void run() {
if (mAnimInfo.applyTime(mTime, mFG)) {
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(mBitmap, mIconColor, mThemedFgColor, mBoundsOffset,
mAnimInfo, mBG, mBgPaint.getColorFilter());
}
private static class ClockConstantState extends FastBitmapConstantState {
private final float mBoundsOffset;
private final AnimationInfo mAnimInfo;
private final Bitmap mBG;
private final ColorFilter mBgFilter;
private final int mThemedFgColor;
ClockConstantState(Bitmap bitmap, int color, int themedFgColor,
float boundsOffset, AnimationInfo animInfo, Bitmap bg, ColorFilter bgFilter) {
super(bitmap, color);
mBoundsOffset = boundsOffset;
mAnimInfo = animInfo;
mBG = bg;
mBgFilter = bgFilter;
mThemedFgColor = themedFgColor;
}
@Override
public FastBitmapDrawable createDrawable() {
return new ClockIconDrawable(this);
}
}
}
}