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