Moving some common functionality to iconlib
Bug: 183641907
Test: Manual
Change-Id: Id58171bf7cf19d56a702673f3d1f4a6647e6e5d1
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
index d7a62e1..8410275 100644
--- a/iconloaderlib/build.gradle
+++ b/iconloaderlib/build.gradle
@@ -5,7 +5,7 @@
buildToolsVersion BUILD_TOOLS_VERSION
defaultConfig {
- minSdkVersion 25
+ minSdkVersion 26
targetSdkVersion 28
versionCode 1
versionName "1.0"
diff --git a/iconloaderlib/res/values/attrs.xml b/iconloaderlib/res/values/attrs.xml
new file mode 100644
index 0000000..8f0bd2c
--- /dev/null
+++ b/iconloaderlib/res/values/attrs.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2021, 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.
+*/
+-->
+<resources>
+ <attr name="disabledIconAlpha" format="float" />
+ <attr name="loadingIconColor" format="color" />
+
+</resources>
\ No newline at end of file
diff --git a/iconloaderlib/res/values/config.xml b/iconloaderlib/res/values/config.xml
index 68c2d2e..893f955 100644
--- a/iconloaderlib/res/values/config.xml
+++ b/iconloaderlib/res/values/config.xml
@@ -24,4 +24,7 @@
<bool name="simple_cache_enable_im_memory">false</bool>
<string name="cache_db_name" translatable="false">app_icons.db</string>
+ <string name="calendar_component_name" translatable="false"></string>
+ <string name="clock_component_name" translatable="false"></string>
+
</resources>
\ No newline at end of file
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index d33f9b1..694343b 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.icons;
+import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
@@ -44,6 +45,17 @@
return LOW_RES_ICON == icon;
}
+ /**
+ * Creates a drawable for the provided BitmapInfo
+ */
+ public FastBitmapDrawable newIcon(Context context) {
+ FastBitmapDrawable drawable = isLowRes()
+ ? new PlaceHolderIconDrawable(this, context)
+ : new FastBitmapDrawable(this);
+ drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
+ return drawable;
+ }
+
public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) {
return of(bitmap, 0);
}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
new file mode 100644
index 0000000..ed9d89c
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
@@ -0,0 +1,328 @@
+/*
+ * 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 android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.AdaptiveIconDrawable;
+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.util.Log;
+
+import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 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();
+ private int mTargetSdkVersion;
+
+ 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);
+ final Bundle metadata = appInfo.metaData;
+ if (metadata == null) {
+ return null;
+ }
+ int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
+ if (drawableId == 0) {
+ return null;
+ }
+
+ Drawable drawable = pm.getResourcesForApplication(appInfo).getDrawableForDensity(
+ drawableId, iconDpi).mutate();
+ if (!(drawable instanceof AdaptiveIconDrawable)) {
+ return null;
+ }
+
+ ClockDrawableWrapper wrapper =
+ new ClockDrawableWrapper((AdaptiveIconDrawable) drawable);
+ wrapper.mTargetSdkVersion = appInfo.targetSdkVersion;
+ 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;
+ }
+ return wrapper;
+ } catch (Exception e) {
+ Log.d(TAG, "Unable to load clock drawable info", e);
+ }
+ return null;
+ }
+
+ @Override
+ public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
+ iconFactory.disableColorExtraction();
+ float [] scale = new float[1];
+ AdaptiveIconDrawable background = new AdaptiveIconDrawable(
+ getBackground().getConstantState().newDrawable(), null);
+ BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background,
+ Process.myUserHandle(), mTargetSdkVersion, false, scale);
+
+ return new ClockBitmapInfo(bitmap, color, scale[0], mAnimationInfo, bitmapInfo.icon);
+ }
+
+ @Override
+ public void prepareToDrawOnUi() {
+ mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
+ }
+
+ 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;
+ }
+ }
+
+ private static class ClockBitmapInfo extends BitmapInfo {
+
+ public final float scale;
+ public final int offset;
+ public final AnimationInfo animInfo;
+ public final Bitmap mFlattenedBackground;
+
+ ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo,
+ Bitmap background) {
+ super(icon, color);
+ this.scale = scale;
+ this.animInfo = animInfo;
+ this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth());
+ this.mFlattenedBackground = background;
+ }
+
+ @Override
+ public FastBitmapDrawable newIcon(Context context) {
+ ClockIconDrawable d = new ClockIconDrawable(this);
+ d.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
+ return d;
+ }
+ }
+
+ 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();
+ 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;
+ }
+ // draw the background that is already flattened to a bitmap
+ canvas.drawBitmap(mInfo.mFlattenedBackground, null, bounds, mPaint);
+
+ // prepare and draw the foreground
+ mInfo.animInfo.applyTime(mTime, mForeground);
+
+ canvas.scale(mInfo.scale, mInfo.scale,
+ bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset);
+ canvas.clipPath(mFullDrawable.getIconMask());
+ mForeground.draw(canvas);
+
+ reschedule();
+ }
+
+ @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 ConstantState getConstantState() {
+ return new ClockConstantState(mInfo, isDisabled());
+ }
+
+ private static class ClockConstantState extends FastBitmapConstantState {
+
+ private final ClockBitmapInfo mInfo;
+
+ ClockConstantState(ClockBitmapInfo info, boolean isDisabled) {
+ super(info.icon, info.color, isDisabled);
+ mInfo = info;
+ }
+
+ @Override
+ public FastBitmapDrawable newDrawable() {
+ ClockIconDrawable drawable = new ClockIconDrawable(mInfo);
+ drawable.setIsDisabled(mIsDisabled);
+ return drawable;
+ }
+ }
+ }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java
new file mode 100644
index 0000000..ec930f8
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2008 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 android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.Property;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.Nullable;
+
+
+public class FastBitmapDrawable extends Drawable {
+
+ private static final Interpolator ACCEL = new AccelerateInterpolator();
+ private static final Interpolator DEACCEL = new DecelerateInterpolator();
+
+ private static final float PRESSED_SCALE = 1.1f;
+
+ private static final float DISABLED_DESATURATION = 1f;
+ private static final float DISABLED_BRIGHTNESS = 0.5f;
+
+ public static final int CLICK_FEEDBACK_DURATION = 200;
+
+ private static ColorFilter sDisabledFColorFilter;
+
+ protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
+ protected Bitmap mBitmap;
+ protected final int mIconColor;
+
+ @Nullable private ColorFilter mColorFilter;
+
+ private boolean mIsPressed;
+ private boolean mIsDisabled;
+ float mDisabledAlpha = 1f;
+ private float mRoundedCornersRadius = 0f;
+ private final Path mClipPath = new Path();
+
+ // Animator and properties for the fast bitmap drawable's scale
+ private static final Property<FastBitmapDrawable, Float> SCALE
+ = new Property<FastBitmapDrawable, Float>(Float.TYPE, "scale") {
+ @Override
+ public Float get(FastBitmapDrawable fastBitmapDrawable) {
+ return fastBitmapDrawable.mScale;
+ }
+
+ @Override
+ public void set(FastBitmapDrawable fastBitmapDrawable, Float value) {
+ fastBitmapDrawable.mScale = value;
+ fastBitmapDrawable.invalidateSelf();
+ }
+ };
+ private ObjectAnimator mScaleAnimation;
+ private float mScale = 1;
+
+ private int mAlpha = 255;
+
+ public FastBitmapDrawable(Bitmap b) {
+ this(b, Color.TRANSPARENT);
+ }
+
+ public FastBitmapDrawable(BitmapInfo info) {
+ this(info.icon, info.color);
+ }
+
+ protected FastBitmapDrawable(Bitmap b, int iconColor) {
+ this(b, iconColor, false);
+ }
+
+ protected FastBitmapDrawable(Bitmap b, int iconColor, boolean isDisabled) {
+ mBitmap = b;
+ mIconColor = iconColor;
+ setFilterBitmap(true);
+ setIsDisabled(isDisabled);
+ }
+
+ @Override
+ public final void draw(Canvas canvas) {
+ if (mRoundedCornersRadius > 0) {
+ float radius = mRoundedCornersRadius * mScale;
+ mClipPath.reset();
+ mClipPath.addRoundRect(0, 0, getIntrinsicWidth(), getIntrinsicHeight(),
+ radius, radius, Path.Direction.CCW);
+ canvas.clipPath(mClipPath);
+ }
+ if (mScale != 1f) {
+ int count = canvas.save();
+ Rect bounds = getBounds();
+ canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY());
+ drawInternal(canvas, bounds);
+ canvas.restoreToCount(count);
+ } else {
+ drawInternal(canvas, getBounds());
+ }
+ }
+
+ protected void drawInternal(Canvas canvas, Rect bounds) {
+ canvas.drawBitmap(mBitmap, null, bounds, mPaint);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mColorFilter = cf;
+ updateFilter();
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (mAlpha != alpha) {
+ mAlpha = alpha;
+ mPaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setFilterBitmap(boolean filterBitmap) {
+ mPaint.setFilterBitmap(filterBitmap);
+ mPaint.setAntiAlias(filterBitmap);
+ }
+
+ @Override
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ public void resetScale() {
+ if (mScaleAnimation != null) {
+ mScaleAnimation.cancel();
+ mScaleAnimation = null;
+ }
+ mScale = 1;
+ invalidateSelf();
+ }
+
+ public float getAnimatedScale() {
+ return mScaleAnimation == null ? 1 : mScale;
+ }
+
+ public void setRoundedCornersRadius(float radius) {
+ mRoundedCornersRadius = radius;
+ }
+
+ public float getRoundedCornersRadius() {
+ return mRoundedCornersRadius;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mBitmap.getWidth();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mBitmap.getHeight();
+ }
+
+ @Override
+ public int getMinimumWidth() {
+ return getBounds().width();
+ }
+
+ @Override
+ public int getMinimumHeight() {
+ return getBounds().height();
+ }
+
+ @Override
+ public boolean isStateful() {
+ return true;
+ }
+
+ @Override
+ public ColorFilter getColorFilter() {
+ return mPaint.getColorFilter();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ boolean isPressed = false;
+ for (int s : state) {
+ if (s == android.R.attr.state_pressed) {
+ isPressed = true;
+ break;
+ }
+ }
+ if (mIsPressed != isPressed) {
+ mIsPressed = isPressed;
+
+ if (mScaleAnimation != null) {
+ mScaleAnimation.cancel();
+ mScaleAnimation = null;
+ }
+
+ if (mIsPressed) {
+ // Animate when going to pressed state
+ mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, PRESSED_SCALE);
+ mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION);
+ mScaleAnimation.setInterpolator(ACCEL);
+ mScaleAnimation.start();
+ } else {
+ if (isVisible()) {
+ mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, 1f);
+ mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION);
+ mScaleAnimation.setInterpolator(DEACCEL);
+ mScaleAnimation.start();
+ } else {
+ mScale = 1f;
+ invalidateSelf();
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public void setIsDisabled(boolean isDisabled) {
+ if (mIsDisabled != isDisabled) {
+ mIsDisabled = isDisabled;
+ updateFilter();
+ }
+ }
+
+ protected boolean isDisabled() {
+ return mIsDisabled;
+ }
+
+ private ColorFilter getDisabledColorFilter() {
+ if (sDisabledFColorFilter == null) {
+ ColorMatrix tempBrightnessMatrix = new ColorMatrix();
+ ColorMatrix tempFilterMatrix = new ColorMatrix();
+
+ tempFilterMatrix.setSaturation(1f - DISABLED_DESATURATION);
+ float scale = 1 - DISABLED_BRIGHTNESS;
+ int brightnessI = (int) (255 * DISABLED_BRIGHTNESS);
+ float[] mat = tempBrightnessMatrix.getArray();
+ mat[0] = scale;
+ mat[6] = scale;
+ mat[12] = scale;
+ mat[4] = brightnessI;
+ mat[9] = brightnessI;
+ mat[14] = brightnessI;
+ mat[18] = mDisabledAlpha;
+ tempFilterMatrix.preConcat(tempBrightnessMatrix);
+ sDisabledFColorFilter = new ColorMatrixColorFilter(tempFilterMatrix);
+ }
+ return sDisabledFColorFilter;
+ }
+
+ /**
+ * Updates the paint to reflect the current brightness and saturation.
+ */
+ protected void updateFilter() {
+ mPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : mColorFilter);
+ invalidateSelf();
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ return new FastBitmapConstantState(mBitmap, mIconColor, mIsDisabled);
+ }
+
+ protected static class FastBitmapConstantState extends ConstantState {
+ protected final Bitmap mBitmap;
+ protected final int mIconColor;
+ protected final boolean mIsDisabled;
+
+ public FastBitmapConstantState(Bitmap bitmap, int color, boolean isDisabled) {
+ mBitmap = bitmap;
+ mIconColor = color;
+ mIsDisabled = isDisabled;
+ }
+
+ @Override
+ public FastBitmapDrawable newDrawable() {
+ return new FastBitmapDrawable(mBitmap, mIconColor, mIsDisabled);
+ }
+
+ @Override
+ public int getChangingConfigurations() {
+ return 0;
+ }
+ }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
index 22f1f23..5d837fd 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
@@ -15,10 +15,16 @@
*/
package com.android.launcher3.icons;
+import android.content.Context;
+import android.content.res.TypedArray;
import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
import android.util.Log;
import androidx.annotation.ColorInt;
@@ -82,4 +88,35 @@
public static void noteNewBitmapCreated() {
sOnNewBitmapRunnable.run();
}
+
+
+ /**
+ * Returns the default path to be used by an icon
+ */
+ public static Path getShapePath(int size) {
+ AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
+ new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
+ drawable.setBounds(0, 0, size, size);
+ return new Path(drawable.getIconMask());
+ }
+
+ /**
+ * Returns the color associated with the attribute
+ */
+ public static int getAttrColor(Context context, int attr) {
+ TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ int colorAccent = ta.getColor(0, 0);
+ ta.recycle();
+ return colorAccent;
+ }
+
+ /**
+ * Returns the alpha corresponding to the theme attribute {@param attr}
+ */
+ public static float getFloat(Context context, int attr, float defValue) {
+ TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ float value = ta.getFloat(0, defValue);
+ ta.recycle();
+ return value;
+ }
}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
new file mode 100644
index 0000000..7749308
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
@@ -0,0 +1,261 @@
+/*
+ * 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 android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.launcher3.icons.BitmapInfo.Extender;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SafeCloseable;
+
+import java.util.Calendar;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+
+/**
+ * Class to handle icon loading from different packages
+ */
+public class IconProvider {
+
+ private static final String TAG = "IconProvider";
+ private static final boolean DEBUG = false;
+
+ private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
+
+ private static final String SYSTEM_STATE_SEPARATOR = " ";
+
+ // Default value returned if there are problems getting resources.
+ private static final int NO_ID = 0;
+
+ private static final BiFunction<LauncherActivityInfo, Integer, Drawable> LAI_LOADER =
+ LauncherActivityInfo::getIcon;
+
+ private static final BiFunction<ActivityInfo, PackageManager, Drawable> AI_LOADER =
+ ActivityInfo::loadUnbadgedIcon;
+
+
+ private final Context mContext;
+ private final ComponentName mCalendar;
+ private final ComponentName mClock;
+
+ public IconProvider(Context context) {
+ mContext = context;
+ mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
+ mClock = parseComponentOrNull(context, R.string.clock_component_name);
+ }
+
+ /**
+ * Adds any modification to the provided systemState for dynamic icons. This system state
+ * is used by caches to check for icon invalidation.
+ */
+ public String getSystemStateForPackage(String systemState, String packageName) {
+ if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
+ return systemState + SYSTEM_STATE_SEPARATOR + getDay();
+ } else {
+ return systemState;
+ }
+ }
+
+ /**
+ * Loads the icon for the provided LauncherActivityInfo such that it can be drawn directly
+ * on the UI
+ */
+ public Drawable getIconForUI(LauncherActivityInfo info, int iconDpi) {
+ Drawable icon = getIcon(info, iconDpi);
+ if (icon instanceof BitmapInfo.Extender) {
+ ((Extender) icon).prepareToDrawOnUi();
+ }
+ return icon;
+ }
+
+ /**
+ * Loads the icon for the provided ActivityInfo such that it can be drawn directly
+ * on the UI
+ */
+ public Drawable getIconForUI(ActivityInfo info, UserHandle user) {
+ Drawable icon = getIcon(info, user);
+ if (icon instanceof BitmapInfo.Extender) {
+ ((Extender) icon).prepareToDrawOnUi();
+ }
+ return icon;
+ }
+
+ /**
+ * Loads the icon for the provided LauncherActivityInfo
+ */
+ public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
+ return getIcon(info.getApplicationInfo().packageName, info.getUser(),
+ info, iconDpi, LAI_LOADER);
+ }
+
+ /**
+ * Loads the icon for the provided activity info
+ */
+ public Drawable getIcon(ActivityInfo info, UserHandle user) {
+ return getIcon(info.applicationInfo.packageName, user, info, mContext.getPackageManager(),
+ AI_LOADER);
+ }
+
+ private <T, P> Drawable getIcon(String packageName, UserHandle user, T obj, P param,
+ BiFunction<T, P, Drawable> loader) {
+ Drawable icon = null;
+ if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
+ icon = loadCalendarDrawable(0);
+ } else if (mClock != null
+ && mClock.getPackageName().equals(packageName)
+ && Process.myUserHandle().equals(user)) {
+ icon = loadClockDrawable(0);
+ }
+ return icon == null ? loader.apply(obj, param) : icon;
+ }
+
+ private Drawable loadCalendarDrawable(int iconDpi) {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ final Bundle metadata = pm.getActivityInfo(
+ mCalendar,
+ PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
+ .metaData;
+ final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
+ final int id = getDynamicIconId(metadata, resources);
+ if (id != NO_ID) {
+ if (DEBUG) Log.d(TAG, "Got icon #" + id);
+ return resources.getDrawableForDensity(id, iconDpi, null /* theme */);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ if (DEBUG) {
+ Log.d(TAG, "Could not get activityinfo or resources for package: "
+ + mCalendar.getPackageName());
+ }
+ }
+ return null;
+ }
+
+ private Drawable loadClockDrawable(int iconDpi) {
+ return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi);
+ }
+
+ protected boolean isClockIcon(ComponentKey key) {
+ return mClock != null && mClock.equals(key.componentName)
+ && Process.myUserHandle().equals(key.user);
+ }
+
+ /**
+ * @param metadata metadata of the default activity of Calendar
+ * @param resources from the Calendar package
+ * @return the resource id for today's Calendar icon; 0 if resources cannot be found.
+ */
+ private int getDynamicIconId(Bundle metadata, Resources resources) {
+ if (metadata == null) {
+ return NO_ID;
+ }
+ String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
+ final int arrayId = metadata.getInt(key, NO_ID);
+ if (arrayId == NO_ID) {
+ return NO_ID;
+ }
+ try {
+ return resources.obtainTypedArray(arrayId).getResourceId(getDay(), NO_ID);
+ } catch (Resources.NotFoundException e) {
+ if (DEBUG) {
+ Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
+ }
+ return NO_ID;
+ }
+ }
+
+ /**
+ * @return Today's day of the month, zero-indexed.
+ */
+ private int getDay() {
+ return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
+ }
+
+
+ /**
+ * Registers a callback to listen for calendar icon changes.
+ * The callback receives the packageName for the calendar icon
+ */
+ public static SafeCloseable registerIconChangeListener(Context context,
+ BiConsumer<String, UserHandle> callback, Handler handler) {
+ ComponentName calendar = parseComponentOrNull(context, R.string.calendar_component_name);
+ ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
+
+ if (calendar == null && clock == null) {
+ return () -> { };
+ }
+
+ BroadcastReceiver receiver = new DateTimeChangeReceiver(callback);
+ final IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+ if (calendar != null) {
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_DATE_CHANGED);
+ }
+ context.registerReceiver(receiver, filter, null, handler);
+
+ return () -> context.unregisterReceiver(receiver);
+ }
+
+ private static class DateTimeChangeReceiver extends BroadcastReceiver {
+
+ private final BiConsumer<String, UserHandle> mCallback;
+
+ DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
+ ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
+ if (clock != null) {
+ mCallback.accept(clock.getPackageName(), Process.myUserHandle());
+ }
+ }
+
+ ComponentName calendar =
+ parseComponentOrNull(context, R.string.calendar_component_name);
+ if (calendar != null) {
+ for (UserHandle user
+ : context.getSystemService(UserManager.class).getUserProfiles()) {
+ mCallback.accept(calendar.getPackageName(), user);
+ }
+ }
+ }
+ }
+
+ private static ComponentName parseComponentOrNull(Context context, int resId) {
+ String cn = context.getString(resId);
+ return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
+ }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
new file mode 100644
index 0000000..5f3343e
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 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 android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import androidx.core.graphics.ColorUtils;
+
+/**
+ * Subclass which draws a placeholder icon when the actual icon is not yet loaded
+ */
+public class PlaceHolderIconDrawable extends FastBitmapDrawable {
+
+ // Path in [0, 100] bounds.
+ private final Path mProgressPath;
+
+ public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
+ super(info);
+
+ mProgressPath = GraphicsUtils.getShapePath(100);
+ mPaint.setColor(ColorUtils.compositeColors(
+ GraphicsUtils.getAttrColor(context, R.attr.loadingIconColor), info.color));
+ }
+
+ @Override
+ protected void drawInternal(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+ canvas.translate(bounds.left, bounds.top);
+ canvas.scale(bounds.width() / 100f, bounds.height() / 100f);
+ canvas.drawPath(mProgressPath, mPaint);
+ canvas.restoreToCount(saveCount);
+ }
+
+ /** Updates this placeholder to {@code newIcon} with animation. */
+ public void animateIconUpdate(Drawable newIcon) {
+ int placeholderColor = mPaint.getColor();
+ int originalAlpha = Color.alpha(placeholderColor);
+
+ ValueAnimator iconUpdateAnimation = ValueAnimator.ofInt(originalAlpha, 0);
+ iconUpdateAnimation.setDuration(375);
+ iconUpdateAnimation.addUpdateListener(valueAnimator -> {
+ int newAlpha = (int) valueAnimator.getAnimatedValue();
+ int newColor = ColorUtils.setAlphaComponent(placeholderColor, newAlpha);
+
+ newIcon.setColorFilter(new PorterDuffColorFilter(newColor, PorterDuff.Mode.SRC_ATOP));
+ });
+ iconUpdateAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ newIcon.setColorFilter(null);
+ }
+ });
+ iconUpdateAnimation.start();
+ }
+
+}
diff --git a/iconloaderlib/src/com/android/launcher3/util/SafeCloseable.java b/iconloaderlib/src/com/android/launcher3/util/SafeCloseable.java
new file mode 100644
index 0000000..ba8ee04
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/util/SafeCloseable.java
@@ -0,0 +1,26 @@
+/*
+ * 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.util;
+
+/**
+ * Extension of closeable which does not throw an exception
+ */
+public interface SafeCloseable extends AutoCloseable {
+
+ @Override
+ void close();
+}