1 /* 2 * Copyright (C) 2021 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 android.content.res.Configuration.UI_MODE_NIGHT_MASK; 19 import static android.content.res.Configuration.UI_MODE_NIGHT_YES; 20 import static android.content.res.Resources.ID_NULL; 21 22 import static com.android.launcher3.icons.GraphicsUtils.getExpectedBitmapSize; 23 import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CALENDAR; 24 import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CLOCK; 25 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.content.res.TypedArray; 29 import android.graphics.Bitmap; 30 import android.graphics.BitmapFactory; 31 import android.graphics.Canvas; 32 import android.graphics.Rect; 33 import android.graphics.drawable.AdaptiveIconDrawable; 34 import android.graphics.drawable.ColorDrawable; 35 import android.graphics.drawable.Drawable; 36 import android.graphics.drawable.InsetDrawable; 37 import android.os.Process; 38 import android.os.UserHandle; 39 import android.util.Log; 40 41 import androidx.annotation.Nullable; 42 43 import com.android.launcher3.icons.BitmapInfo.Extender; 44 import com.android.launcher3.icons.cache.BaseIconCache; 45 46 import java.io.ByteArrayInputStream; 47 import java.io.ByteArrayOutputStream; 48 import java.io.DataInputStream; 49 import java.io.DataOutputStream; 50 import java.io.IOException; 51 52 /** 53 * Class to handle monochrome themed app icons 54 */ 55 @SuppressWarnings("NewApi") 56 public class ThemedIconDrawable extends FastBitmapDrawable { 57 58 public static final String TAG = "ThemedIconDrawable"; 59 60 final ThemedBitmapInfo bitmapInfo; 61 final int colorFg, colorBg; 62 63 // The foreground/monochrome icon for the app 64 private final Drawable mMonochromeIcon; 65 private final AdaptiveIconDrawable mBgWrapper; 66 private final Rect mBadgeBounds; 67 ThemedIconDrawable(ThemedConstantState constantState)68 protected ThemedIconDrawable(ThemedConstantState constantState) { 69 super(constantState.mBitmap, constantState.colorFg, constantState.mIsDisabled); 70 bitmapInfo = constantState.bitmapInfo; 71 colorBg = constantState.colorBg; 72 colorFg = constantState.colorFg; 73 74 mMonochromeIcon = bitmapInfo.mThemeData.loadMonochromeDrawable(colorFg); 75 mBgWrapper = new AdaptiveIconDrawable(new ColorDrawable(colorBg), null); 76 mBadgeBounds = bitmapInfo.mUserBadge == null ? null : 77 new Rect(0, 0, bitmapInfo.mUserBadge.getWidth(), bitmapInfo.mUserBadge.getHeight()); 78 79 } 80 81 @Override onBoundsChange(Rect bounds)82 protected void onBoundsChange(Rect bounds) { 83 super.onBoundsChange(bounds); 84 mBgWrapper.setBounds(bounds); 85 mMonochromeIcon.setBounds(bounds); 86 } 87 88 @Override drawInternal(Canvas canvas, Rect bounds)89 protected void drawInternal(Canvas canvas, Rect bounds) { 90 int count = canvas.save(); 91 canvas.scale(bitmapInfo.mNormalizationScale, bitmapInfo.mNormalizationScale, 92 bounds.exactCenterX(), bounds.exactCenterY()); 93 mPaint.setColor(colorBg); 94 canvas.drawPath(mBgWrapper.getIconMask(), mPaint); 95 mMonochromeIcon.draw(canvas); 96 canvas.restoreToCount(count); 97 if (mBadgeBounds != null) { 98 canvas.drawBitmap(bitmapInfo.mUserBadge, mBadgeBounds, getBounds(), mPaint); 99 } 100 } 101 102 @Override isThemed()103 public boolean isThemed() { 104 return true; 105 } 106 107 @Override getConstantState()108 public ConstantState getConstantState() { 109 return new ThemedConstantState(bitmapInfo, colorBg, colorFg, mIsDisabled); 110 } 111 112 static class ThemedConstantState extends FastBitmapConstantState { 113 114 final ThemedBitmapInfo bitmapInfo; 115 final int colorFg, colorBg; 116 ThemedConstantState(ThemedBitmapInfo bitmapInfo, int colorBg, int colorFg, boolean isDisabled)117 public ThemedConstantState(ThemedBitmapInfo bitmapInfo, 118 int colorBg, int colorFg, boolean isDisabled) { 119 super(bitmapInfo.icon, bitmapInfo.color, isDisabled); 120 this.bitmapInfo = bitmapInfo; 121 this.colorBg = colorBg; 122 this.colorFg = colorFg; 123 } 124 125 @Override newDrawable()126 public FastBitmapDrawable newDrawable() { 127 return new ThemedIconDrawable(this); 128 } 129 } 130 131 public static class ThemedBitmapInfo extends BitmapInfo { 132 133 final ThemeData mThemeData; 134 final float mNormalizationScale; 135 final Bitmap mUserBadge; 136 ThemedBitmapInfo(Bitmap icon, int color, ThemeData themeData, float normalizationScale, Bitmap userBadge)137 public ThemedBitmapInfo(Bitmap icon, int color, ThemeData themeData, 138 float normalizationScale, Bitmap userBadge) { 139 super(icon, color); 140 mThemeData = themeData; 141 mNormalizationScale = normalizationScale; 142 mUserBadge = userBadge; 143 } 144 145 @Override newThemedIcon(Context context)146 public FastBitmapDrawable newThemedIcon(Context context) { 147 int[] colors = getColors(context); 148 FastBitmapDrawable drawable = new ThemedConstantState(this, colors[0], colors[1], false) 149 .newDrawable(); 150 drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f); 151 return drawable; 152 } 153 154 @Nullable toByteArray()155 public byte[] toByteArray() { 156 if (isNullOrLowRes()) { 157 return null; 158 } 159 String resName = mThemeData.mResources.getResourceName(mThemeData.mResID); 160 ByteArrayOutputStream out = new ByteArrayOutputStream( 161 getExpectedBitmapSize(icon) + 3 + resName.length()); 162 try { 163 DataOutputStream dos = new DataOutputStream(out); 164 dos.writeByte(TYPE_THEMED); 165 dos.writeFloat(mNormalizationScale); 166 dos.writeUTF(resName); 167 icon.compress(Bitmap.CompressFormat.PNG, 100, dos); 168 169 dos.flush(); 170 dos.close(); 171 return out.toByteArray(); 172 } catch (IOException e) { 173 Log.w(TAG, "Could not write bitmap"); 174 return null; 175 } 176 } 177 decode(byte[] data, int color, BitmapFactory.Options decodeOptions, UserHandle user, BaseIconCache iconCache, Context context)178 static ThemedBitmapInfo decode(byte[] data, int color, 179 BitmapFactory.Options decodeOptions, UserHandle user, BaseIconCache iconCache, 180 Context context) { 181 try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) { 182 dis.readByte(); // type 183 float normalizationScale = dis.readFloat(); 184 185 String resName = dis.readUTF(); 186 int resId = context.getResources() 187 .getIdentifier(resName, "drawable", context.getPackageName()); 188 if (resId == ID_NULL) { 189 return null; 190 } 191 192 Bitmap userBadgeBitmap = null; 193 if (!Process.myUserHandle().equals(user)) { 194 try (BaseIconFactory iconFactory = iconCache.getIconFactory()) { 195 userBadgeBitmap = iconFactory.getUserBadgeBitmap(user); 196 } 197 } 198 199 ThemeData themeData = new ThemeData(context.getResources(), resId); 200 Bitmap icon = BitmapFactory.decodeStream(dis, null, decodeOptions); 201 return new ThemedBitmapInfo(icon, color, themeData, normalizationScale, 202 userBadgeBitmap); 203 } catch (IOException e) { 204 return null; 205 } 206 } 207 } 208 209 public static class ThemeData { 210 211 final Resources mResources; 212 final int mResID; 213 ThemeData(Resources resources, int resID)214 public ThemeData(Resources resources, int resID) { 215 mResources = resources; 216 mResID = resID; 217 } 218 loadMonochromeDrawable(int accentColor)219 Drawable loadMonochromeDrawable(int accentColor) { 220 Drawable d = mResources.getDrawable(mResID).mutate(); 221 d.setTint(accentColor); 222 d = new InsetDrawable(d, .2f); 223 return d; 224 } 225 wrapDrawable(Drawable original, int iconType)226 public Drawable wrapDrawable(Drawable original, int iconType) { 227 if (!(original instanceof AdaptiveIconDrawable)) { 228 return original; 229 } 230 AdaptiveIconDrawable aid = (AdaptiveIconDrawable) original; 231 String resourceType = mResources.getResourceTypeName(mResID); 232 if (iconType == ICON_TYPE_CALENDAR && "array".equals(resourceType)) { 233 TypedArray ta = mResources.obtainTypedArray(mResID); 234 int id = ta.getResourceId(IconProvider.getDay(), ID_NULL); 235 ta.recycle(); 236 return id == ID_NULL ? original 237 : new ThemedAdaptiveIcon(aid, new ThemeData(mResources, id)); 238 } else if (iconType == ICON_TYPE_CLOCK && "array".equals(resourceType)) { 239 ((ClockDrawableWrapper) original).mThemeData = this; 240 return original; 241 } else if ("drawable".equals(resourceType)) { 242 return new ThemedAdaptiveIcon(aid, this); 243 } else { 244 return original; 245 } 246 } 247 } 248 249 static class ThemedAdaptiveIcon extends AdaptiveIconDrawable implements Extender { 250 251 protected final ThemeData mThemeData; 252 ThemedAdaptiveIcon(AdaptiveIconDrawable parent, ThemeData themeData)253 public ThemedAdaptiveIcon(AdaptiveIconDrawable parent, ThemeData themeData) { 254 super(parent.getBackground(), parent.getForeground()); 255 mThemeData = themeData; 256 } 257 258 @Override getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory, float normalizationScale, UserHandle user)259 public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory, 260 float normalizationScale, UserHandle user) { 261 Bitmap userBadge = Process.myUserHandle().equals(user) 262 ? null : iconFactory.getUserBadgeBitmap(user); 263 return new ThemedBitmapInfo(bitmap, color, mThemeData, normalizationScale, userBadge); 264 } 265 266 @Override drawForPersistence(Canvas canvas)267 public void drawForPersistence(Canvas canvas) { 268 draw(canvas); 269 } 270 271 @Override getThemedDrawable(Context context)272 public Drawable getThemedDrawable(Context context) { 273 int[] colors = getColors(context); 274 Drawable bg = new ColorDrawable(colors[0]); 275 float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction()); 276 Drawable fg = new InsetDrawable(mThemeData.loadMonochromeDrawable(colors[1]), inset); 277 return new AdaptiveIconDrawable(bg, fg); 278 } 279 } 280 281 /** 282 * Get an int array representing background and foreground colors for themed icons 283 */ getColors(Context context)284 public static int[] getColors(Context context) { 285 Resources res = context.getResources(); 286 int[] colors = new int[2]; 287 if ((res.getConfiguration().uiMode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) { 288 colors[0] = res.getColor(android.R.color.system_neutral1_800); 289 colors[1] = res.getColor(android.R.color.system_accent1_100); 290 } else { 291 colors[0] = res.getColor(android.R.color.system_accent1_100); 292 colors[1] = res.getColor(android.R.color.system_neutral2_700); 293 } 294 return colors; 295 } 296 } 297