/* * Copyright (C) 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. */ package com.android.launcher3.icons; import static android.content.res.Configuration.UI_MODE_NIGHT_MASK; import static android.content.res.Configuration.UI_MODE_NIGHT_YES; import static android.content.res.Resources.ID_NULL; import static com.android.launcher3.icons.GraphicsUtils.getExpectedBitmapSize; import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CALENDAR; import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CLOCK; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; import android.os.Process; import android.os.UserHandle; import android.util.Log; import androidx.annotation.Nullable; import com.android.launcher3.icons.BitmapInfo.Extender; import com.android.launcher3.icons.cache.BaseIconCache; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; /** * Class to handle monochrome themed app icons */ @SuppressWarnings("NewApi") public class ThemedIconDrawable extends FastBitmapDrawable { public static final String TAG = "ThemedIconDrawable"; final ThemedBitmapInfo bitmapInfo; final int colorFg, colorBg; // The foreground/monochrome icon for the app private final Drawable mMonochromeIcon; private final AdaptiveIconDrawable mBgWrapper; private final Rect mBadgeBounds; protected ThemedIconDrawable(ThemedConstantState constantState) { super(constantState.mBitmap, constantState.colorFg, constantState.mIsDisabled); bitmapInfo = constantState.bitmapInfo; colorBg = constantState.colorBg; colorFg = constantState.colorFg; mMonochromeIcon = bitmapInfo.mThemeData.loadMonochromeDrawable(colorFg); mBgWrapper = new AdaptiveIconDrawable(new ColorDrawable(colorBg), null); mBadgeBounds = bitmapInfo.mUserBadge == null ? null : new Rect(0, 0, bitmapInfo.mUserBadge.getWidth(), bitmapInfo.mUserBadge.getHeight()); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); mBgWrapper.setBounds(bounds); mMonochromeIcon.setBounds(bounds); } @Override protected void drawInternal(Canvas canvas, Rect bounds) { int count = canvas.save(); canvas.scale(bitmapInfo.mNormalizationScale, bitmapInfo.mNormalizationScale, bounds.exactCenterX(), bounds.exactCenterY()); mPaint.setColor(colorBg); canvas.drawPath(mBgWrapper.getIconMask(), mPaint); mMonochromeIcon.draw(canvas); canvas.restoreToCount(count); if (mBadgeBounds != null) { canvas.drawBitmap(bitmapInfo.mUserBadge, mBadgeBounds, getBounds(), mPaint); } } @Override public boolean isThemed() { return true; } @Override public ConstantState getConstantState() { return new ThemedConstantState(bitmapInfo, colorBg, colorFg, mIsDisabled); } static class ThemedConstantState extends FastBitmapConstantState { final ThemedBitmapInfo bitmapInfo; final int colorFg, colorBg; public ThemedConstantState(ThemedBitmapInfo bitmapInfo, int colorBg, int colorFg, boolean isDisabled) { super(bitmapInfo.icon, bitmapInfo.color, isDisabled); this.bitmapInfo = bitmapInfo; this.colorBg = colorBg; this.colorFg = colorFg; } @Override public FastBitmapDrawable newDrawable() { return new ThemedIconDrawable(this); } } public static class ThemedBitmapInfo extends BitmapInfo { final ThemeData mThemeData; final float mNormalizationScale; final Bitmap mUserBadge; public ThemedBitmapInfo(Bitmap icon, int color, ThemeData themeData, float normalizationScale, Bitmap userBadge) { super(icon, color); mThemeData = themeData; mNormalizationScale = normalizationScale; mUserBadge = userBadge; } @Override public FastBitmapDrawable newThemedIcon(Context context) { int[] colors = getColors(context); FastBitmapDrawable drawable = new ThemedConstantState(this, colors[0], colors[1], false) .newDrawable(); drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f); return drawable; } @Nullable public byte[] toByteArray() { if (isNullOrLowRes()) { return null; } String resName = mThemeData.mResources.getResourceName(mThemeData.mResID); ByteArrayOutputStream out = new ByteArrayOutputStream( getExpectedBitmapSize(icon) + 3 + resName.length()); try { DataOutputStream dos = new DataOutputStream(out); dos.writeByte(TYPE_THEMED); dos.writeFloat(mNormalizationScale); dos.writeUTF(resName); icon.compress(Bitmap.CompressFormat.PNG, 100, dos); dos.flush(); dos.close(); return out.toByteArray(); } catch (IOException e) { Log.w(TAG, "Could not write bitmap"); return null; } } static ThemedBitmapInfo decode(byte[] data, int color, BitmapFactory.Options decodeOptions, UserHandle user, BaseIconCache iconCache, Context context) { try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) { dis.readByte(); // type float normalizationScale = dis.readFloat(); String resName = dis.readUTF(); int resId = context.getResources() .getIdentifier(resName, "drawable", context.getPackageName()); if (resId == ID_NULL) { return null; } Bitmap userBadgeBitmap = null; if (!Process.myUserHandle().equals(user)) { try (BaseIconFactory iconFactory = iconCache.getIconFactory()) { userBadgeBitmap = iconFactory.getUserBadgeBitmap(user); } } ThemeData themeData = new ThemeData(context.getResources(), resId); Bitmap icon = BitmapFactory.decodeStream(dis, null, decodeOptions); return new ThemedBitmapInfo(icon, color, themeData, normalizationScale, userBadgeBitmap); } catch (IOException e) { return null; } } } public static class ThemeData { final Resources mResources; final int mResID; public ThemeData(Resources resources, int resID) { mResources = resources; mResID = resID; } Drawable loadMonochromeDrawable(int accentColor) { Drawable d = mResources.getDrawable(mResID).mutate(); d.setTint(accentColor); d = new InsetDrawable(d, .2f); return d; } public Drawable wrapDrawable(Drawable original, int iconType) { if (!(original instanceof AdaptiveIconDrawable)) { return original; } AdaptiveIconDrawable aid = (AdaptiveIconDrawable) original; String resourceType = mResources.getResourceTypeName(mResID); if (iconType == ICON_TYPE_CALENDAR && "array".equals(resourceType)) { TypedArray ta = mResources.obtainTypedArray(mResID); int id = ta.getResourceId(IconProvider.getDay(), ID_NULL); ta.recycle(); return id == ID_NULL ? original : new ThemedAdaptiveIcon(aid, new ThemeData(mResources, id)); } else if (iconType == ICON_TYPE_CLOCK && "array".equals(resourceType)) { ((ClockDrawableWrapper) original).mThemeData = this; return original; } else if ("drawable".equals(resourceType)) { return new ThemedAdaptiveIcon(aid, this); } else { return original; } } } static class ThemedAdaptiveIcon extends AdaptiveIconDrawable implements Extender { protected final ThemeData mThemeData; public ThemedAdaptiveIcon(AdaptiveIconDrawable parent, ThemeData themeData) { super(parent.getBackground(), parent.getForeground()); mThemeData = themeData; } @Override public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory, float normalizationScale, UserHandle user) { Bitmap userBadge = Process.myUserHandle().equals(user) ? null : iconFactory.getUserBadgeBitmap(user); return new ThemedBitmapInfo(bitmap, color, mThemeData, normalizationScale, userBadge); } @Override public void drawForPersistence(Canvas canvas) { draw(canvas); } @Override public Drawable getThemedDrawable(Context context) { int[] colors = getColors(context); Drawable bg = new ColorDrawable(colors[0]); float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction()); Drawable fg = new InsetDrawable(mThemeData.loadMonochromeDrawable(colors[1]), inset); return new AdaptiveIconDrawable(bg, fg); } } /** * Get an int array representing background and foreground colors for themed icons */ public static int[] getColors(Context context) { Resources res = context.getResources(); int[] colors = new int[2]; if ((res.getConfiguration().uiMode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) { colors[0] = res.getColor(android.R.color.system_neutral1_800); colors[1] = res.getColor(android.R.color.system_accent1_100); } else { colors[0] = res.getColor(android.R.color.system_accent1_100); colors[1] = res.getColor(android.R.color.system_neutral2_700); } return colors; } }