1 /*
2  * Copyright (C) 2020 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.settingslib.notification;
17 
18 import android.annotation.ColorInt;
19 import android.content.Context;
20 import android.content.pm.ApplicationInfo;
21 import android.content.pm.LauncherApps;
22 import android.content.pm.PackageManager;
23 import android.content.pm.ShortcutInfo;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.ColorFilter;
27 import android.graphics.Paint;
28 import android.graphics.Rect;
29 import android.graphics.drawable.Drawable;
30 import android.os.UserHandle;
31 import android.util.IconDrawableFactory;
32 import android.util.Log;
33 
34 import com.android.launcher3.icons.BaseIconFactory;
35 import com.android.settingslib.R;
36 import com.android.settingslib.Utils;
37 
38 /**
39  * Factory for creating normalized conversation icons.
40  * We are not using Launcher's IconFactory because conversation rendering only runs on the UI
41  * thread, so there is no need to manage a pool across multiple threads. Launcher's rendering
42  * also includes shadows, which are only appropriate on top of wallpaper, not embedded in UI.
43  */
44 public class ConversationIconFactory extends BaseIconFactory {
45     // Geometry of the various parts of the design. All values are 1dp on a 56x56dp icon grid.
46     // Space is left around the "head" (main avatar) for
47     // ........
48     // .HHHHHH.
49     // .HHHrrrr
50     // .HHHrBBr
51     // ....rrrr
52     // This is trying to recreate the view layout in notification_template_material_conversation.xml
53 
54     private static final float HEAD_SIZE = 52f;
55     private static final float BADGE_SIZE = 12f;
56     private static final float BADGE_CENTER = 46f;
57     private static final float CIRCLE_MARGIN = 36f;
58     private static final float BADGE_ORIGIN = HEAD_SIZE - BADGE_SIZE; // 40f
59     private static final float BASE_ICON_SIZE = 56f;
60 
61     private static final float OUT_CIRCLE_DIA = (BASE_ICON_SIZE - CIRCLE_MARGIN); // 20f
62     private static final float INN_CIRCLE_DIA = (float) Math.sqrt(2 * BADGE_SIZE * BADGE_SIZE) ;
63     private static final float OUT_CIRCLE_RAD = OUT_CIRCLE_DIA / 2;
64     private static final float INN_CIRCLE_RAD = INN_CIRCLE_DIA / 2;
65     // Android draws strokes centered on the radius, so our actual radius is an avg of the outside
66     // and inside of the ring stroke
67     private static final float CIRCLE_RADIUS =
68             INN_CIRCLE_RAD + ((OUT_CIRCLE_RAD - INN_CIRCLE_RAD) / 2);
69     private static final float RING_STROKE_WIDTH = (OUT_CIRCLE_DIA - INN_CIRCLE_DIA) / 2;
70 
71     final LauncherApps mLauncherApps;
72     final PackageManager mPackageManager;
73     final IconDrawableFactory mIconDrawableFactory;
74     private int mImportantConversationColor;
75 
ConversationIconFactory(Context context, LauncherApps la, PackageManager pm, IconDrawableFactory iconDrawableFactory, int iconSizePx)76     public ConversationIconFactory(Context context, LauncherApps la, PackageManager pm,
77             IconDrawableFactory iconDrawableFactory, int iconSizePx) {
78         super(context, context.getResources().getConfiguration().densityDpi,
79                 iconSizePx);
80         mLauncherApps = la;
81         mPackageManager = pm;
82         mIconDrawableFactory = iconDrawableFactory;
83         mImportantConversationColor = context.getResources().getColor(
84                 R.color.important_conversation, null);
85     }
86 
87     /**
88      * Returns the conversation info drawable
89      */
getBaseIconDrawable(ShortcutInfo shortcutInfo)90     public Drawable getBaseIconDrawable(ShortcutInfo shortcutInfo) {
91         return mLauncherApps.getShortcutIconDrawable(shortcutInfo, mFillResIconDpi);
92     }
93 
94     /**
95      * Get the {@link Drawable} that represents the app icon, badged with the work profile icon
96      * if appropriate.
97      */
getAppBadge(String packageName, int userId)98     public Drawable getAppBadge(String packageName, int userId) {
99         Drawable badge = null;
100         try {
101             final ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
102                     packageName, PackageManager.GET_META_DATA, userId);
103             badge = Utils.getBadgedIcon(mContext, appInfo);
104         } catch (PackageManager.NameNotFoundException e) {
105             badge = mPackageManager.getDefaultActivityIcon();
106         }
107         return badge;
108     }
109 
110     /**
111      * Returns a {@link Drawable} for the entire conversation. The shortcut icon will be badged
112      * with the launcher icon of the app specified by packageName.
113      */
getConversationDrawable(ShortcutInfo info, String packageName, int uid, boolean important)114     public Drawable getConversationDrawable(ShortcutInfo info, String packageName, int uid,
115             boolean important) {
116         return getConversationDrawable(getBaseIconDrawable(info), packageName, uid, important);
117     }
118 
119     /**
120      * Returns a {@link Drawable} for the entire conversation. The drawable will be badged
121      * with the launcher icon of the app specified by packageName.
122      */
getConversationDrawable(Drawable baseIcon, String packageName, int uid, boolean important)123     public Drawable getConversationDrawable(Drawable baseIcon, String packageName, int uid,
124             boolean important) {
125         return new ConversationIconDrawable(baseIcon,
126                 getAppBadge(packageName, UserHandle.getUserId(uid)),
127                 mIconBitmapSize,
128                 mImportantConversationColor,
129                 important);
130     }
131 
132     /**
133      * Custom Drawable that overlays a badge drawable (e.g. notification small icon or app icon) on
134      * a base icon (conversation/person avatar), plus decorations indicating conversation
135      * importance.
136      */
137     public static class ConversationIconDrawable extends Drawable {
138         private Drawable mBaseIcon;
139         private Drawable mBadgeIcon;
140         private int mIconSize;
141         private Paint mRingPaint;
142         private boolean mShowRing;
143         private Paint mPaddingPaint;
144 
ConversationIconDrawable(Drawable baseIcon, Drawable badgeIcon, int iconSize, @ColorInt int ringColor, boolean showImportanceRing)145         public ConversationIconDrawable(Drawable baseIcon,
146                 Drawable badgeIcon,
147                 int iconSize,
148                 @ColorInt int ringColor,
149                 boolean showImportanceRing) {
150             mBaseIcon = baseIcon;
151             mBadgeIcon = badgeIcon;
152             mIconSize = iconSize;
153             mShowRing = showImportanceRing;
154             mRingPaint = new Paint();
155             mRingPaint.setStyle(Paint.Style.STROKE);
156             mRingPaint.setColor(ringColor);
157             mPaddingPaint = new Paint();
158             mPaddingPaint.setStyle(Paint.Style.FILL_AND_STROKE);
159             mPaddingPaint.setColor(Color.WHITE);
160         }
161 
162         /**
163          * Show or hide the importance ring.
164          */
setImportant(boolean important)165         public void setImportant(boolean important) {
166             if (important != mShowRing) {
167                 mShowRing = important;
168                 invalidateSelf();
169             }
170         }
171 
172         @Override
getIntrinsicWidth()173         public int getIntrinsicWidth() {
174             return mIconSize;
175         }
176 
177         @Override
getIntrinsicHeight()178         public int getIntrinsicHeight() {
179             return mIconSize;
180         }
181 
182         // Similar to badgeWithDrawable, but relying on the bounds of each underlying drawable
183         @Override
draw(Canvas canvas)184         public void draw(Canvas canvas) {
185             final Rect bounds = getBounds();
186 
187             // scale to our internal grid
188             final float scale = bounds.width() / BASE_ICON_SIZE;
189             final int ringStrokeWidth = (int) (RING_STROKE_WIDTH * scale);
190             final int headSize = (int) (HEAD_SIZE * scale);
191             final int badgePadding = (int) (BADGE_ORIGIN * scale);
192             final int badgeCenter = (int) (BADGE_CENTER * scale);
193 
194             mPaddingPaint.setStrokeWidth(ringStrokeWidth);
195             final float radius = (int) (CIRCLE_RADIUS * scale); // stroke outside
196             if (mBaseIcon != null) {
197                 mBaseIcon.setBounds(0,
198                         0,
199                         headSize ,
200                         headSize);
201                 mBaseIcon.draw(canvas);
202             } else {
203                 Log.w("ConversationIconFactory", "ConversationIconDrawable has null base icon");
204             }
205             if (mBadgeIcon != null) {
206                 canvas.drawCircle(badgeCenter, badgeCenter, radius, mPaddingPaint);
207                 mBadgeIcon.setBounds(
208                         badgePadding,
209                         badgePadding,
210                         headSize,
211                         headSize);
212                 mBadgeIcon.draw(canvas);
213             } else {
214                 Log.w("ConversationIconFactory", "ConversationIconDrawable has null badge icon");
215             }
216             if (mShowRing) {
217                 mRingPaint.setStrokeWidth(ringStrokeWidth);
218                 canvas.drawCircle(badgeCenter, badgeCenter, radius, mRingPaint);
219             }
220         }
221 
222         @Override
setAlpha(int alpha)223         public void setAlpha(int alpha) {
224             // unimplemented
225         }
226 
227         @Override
setColorFilter(ColorFilter colorFilter)228         public void setColorFilter(ColorFilter colorFilter) {
229             // unimplemented
230         }
231 
232         @Override
getOpacity()233         public int getOpacity() {
234             return 0;
235         }
236     }
237 }
238