1 /*
2  * Copyright (C) 2019 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 
17 package com.android.systemui.bubbles;
18 
19 import static com.android.systemui.bubbles.BadgedImageView.DEFAULT_PATH_SIZE;
20 import static com.android.systemui.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA;
21 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
22 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
23 
24 import android.annotation.NonNull;
25 import android.app.Notification;
26 import android.app.Person;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ShortcutInfo;
32 import android.graphics.Bitmap;
33 import android.graphics.Color;
34 import android.graphics.Matrix;
35 import android.graphics.Path;
36 import android.graphics.drawable.Drawable;
37 import android.graphics.drawable.Icon;
38 import android.os.AsyncTask;
39 import android.os.Parcelable;
40 import android.text.TextUtils;
41 import android.util.Log;
42 import android.util.PathParser;
43 import android.view.LayoutInflater;
44 
45 import androidx.annotation.Nullable;
46 
47 import com.android.internal.graphics.ColorUtils;
48 import com.android.launcher3.icons.BitmapInfo;
49 import com.android.systemui.R;
50 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
51 
52 import java.lang.ref.WeakReference;
53 import java.util.List;
54 import java.util.Objects;
55 
56 /**
57  * Simple task to inflate views & load necessary info to display a bubble.
58  */
59 public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> {
60     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES;
61 
62 
63     /**
64      * Callback to find out when the bubble has been inflated & necessary data loaded.
65      */
66     public interface Callback {
67         /**
68          * Called when data has been loaded for the bubble.
69          */
onBubbleViewsReady(Bubble bubble)70         void onBubbleViewsReady(Bubble bubble);
71     }
72 
73     private Bubble mBubble;
74     private WeakReference<Context> mContext;
75     private WeakReference<BubbleStackView> mStackView;
76     private BubbleIconFactory mIconFactory;
77     private boolean mSkipInflation;
78     private Callback mCallback;
79 
80     /**
81      * Creates a task to load information for the provided {@link Bubble}. Once all info
82      * is loaded, {@link Callback} is notified.
83      */
BubbleViewInfoTask(Bubble b, Context context, BubbleStackView stackView, BubbleIconFactory factory, boolean skipInflation, Callback c)84     BubbleViewInfoTask(Bubble b,
85             Context context,
86             BubbleStackView stackView,
87             BubbleIconFactory factory,
88             boolean skipInflation,
89             Callback c) {
90         mBubble = b;
91         mContext = new WeakReference<>(context);
92         mStackView = new WeakReference<>(stackView);
93         mIconFactory = factory;
94         mSkipInflation = skipInflation;
95         mCallback = c;
96     }
97 
98     @Override
doInBackground(Void... voids)99     protected BubbleViewInfo doInBackground(Void... voids) {
100         return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble,
101                 mSkipInflation);
102     }
103 
104     @Override
onPostExecute(BubbleViewInfo viewInfo)105     protected void onPostExecute(BubbleViewInfo viewInfo) {
106         if (viewInfo != null) {
107             mBubble.setViewInfo(viewInfo);
108             if (mCallback != null && !isCancelled()) {
109                 mCallback.onBubbleViewsReady(mBubble);
110             }
111         }
112     }
113 
114     /**
115      * Info necessary to render a bubble.
116      */
117     static class BubbleViewInfo {
118         BadgedImageView imageView;
119         BubbleExpandedView expandedView;
120         ShortcutInfo shortcutInfo;
121         String appName;
122         Bitmap badgedBubbleImage;
123         Drawable badgedAppIcon;
124         int dotColor;
125         Path dotPath;
126         Bubble.FlyoutMessage flyoutMessage;
127 
128         @Nullable
populate(Context c, BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b, boolean skipInflation)129         static BubbleViewInfo populate(Context c, BubbleStackView stackView,
130                 BubbleIconFactory iconFactory, Bubble b, boolean skipInflation) {
131             BubbleViewInfo info = new BubbleViewInfo();
132 
133             // View inflation: only should do this once per bubble
134             if (!skipInflation && !b.isInflated()) {
135                 LayoutInflater inflater = LayoutInflater.from(c);
136                 info.imageView = (BadgedImageView) inflater.inflate(
137                         R.layout.bubble_view, stackView, false /* attachToRoot */);
138 
139                 info.expandedView = (BubbleExpandedView) inflater.inflate(
140                         R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
141                 info.expandedView.setStackView(stackView);
142             }
143 
144             if (b.getShortcutInfo() != null) {
145                 info.shortcutInfo = b.getShortcutInfo();
146             }
147 
148             // App name & app icon
149             PackageManager pm = c.getPackageManager();
150             ApplicationInfo appInfo;
151             Drawable badgedIcon;
152             Drawable appIcon;
153             try {
154                 appInfo = pm.getApplicationInfo(
155                         b.getPackageName(),
156                         PackageManager.MATCH_UNINSTALLED_PACKAGES
157                                 | PackageManager.MATCH_DISABLED_COMPONENTS
158                                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
159                                 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
160                 if (appInfo != null) {
161                     info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
162                 }
163                 appIcon = pm.getApplicationIcon(b.getPackageName());
164                 badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser());
165             } catch (PackageManager.NameNotFoundException exception) {
166                 // If we can't find package... don't think we should show the bubble.
167                 Log.w(TAG, "Unable to find package: " + b.getPackageName());
168                 return null;
169             }
170 
171             // Badged bubble image
172             Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo,
173                     b.getIcon());
174             if (bubbleDrawable == null) {
175                 // Default to app icon
176                 bubbleDrawable = appIcon;
177             }
178 
179             BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon,
180                     b.isImportantConversation());
181             info.badgedAppIcon = badgedIcon;
182             info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable,
183                     badgeBitmapInfo).icon;
184 
185             // Dot color & placement
186             Path iconPath = PathParser.createPathFromPathData(
187                     c.getResources().getString(com.android.internal.R.string.config_icon_mask));
188             Matrix matrix = new Matrix();
189             float scale = iconFactory.getNormalizer().getScale(bubbleDrawable,
190                     null /* outBounds */, null /* path */, null /* outMaskShape */);
191             float radius = DEFAULT_PATH_SIZE / 2f;
192             matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
193                     radius /* pivot y */);
194             iconPath.transform(matrix);
195             info.dotPath = iconPath;
196             info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
197                     Color.WHITE, WHITE_SCRIM_ALPHA);
198 
199             // Flyout
200             info.flyoutMessage = b.getFlyoutMessage();
201             if (info.flyoutMessage != null) {
202                 info.flyoutMessage.senderAvatar =
203                         loadSenderAvatar(c, info.flyoutMessage.senderIcon);
204             }
205             return info;
206         }
207     }
208 
209 
210     /**
211      * Returns our best guess for the most relevant text summary of the latest update to this
212      * notification, based on its type. Returns null if there should not be an update message.
213      */
214     @NonNull
extractFlyoutMessage(NotificationEntry entry)215     static Bubble.FlyoutMessage extractFlyoutMessage(NotificationEntry entry) {
216         Objects.requireNonNull(entry);
217         final Notification underlyingNotif = entry.getSbn().getNotification();
218         final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
219 
220         Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage();
221         bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean(
222                 Notification.EXTRA_IS_GROUP_CONVERSATION);
223         try {
224             if (Notification.BigTextStyle.class.equals(style)) {
225                 // Return the big text, it is big so probably important. If it's not there use the
226                 // normal text.
227                 CharSequence bigText =
228                         underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
229                 bubbleMessage.message = !TextUtils.isEmpty(bigText)
230                         ? bigText
231                         : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
232                 return bubbleMessage;
233             } else if (Notification.MessagingStyle.class.equals(style)) {
234                 final List<Notification.MessagingStyle.Message> messages =
235                         Notification.MessagingStyle.Message.getMessagesFromBundleArray(
236                                 (Parcelable[]) underlyingNotif.extras.get(
237                                         Notification.EXTRA_MESSAGES));
238 
239                 final Notification.MessagingStyle.Message latestMessage =
240                         Notification.MessagingStyle.findLatestIncomingMessage(messages);
241                 if (latestMessage != null) {
242                     bubbleMessage.message = latestMessage.getText();
243                     Person sender = latestMessage.getSenderPerson();
244                     bubbleMessage.senderName = sender != null ? sender.getName() : null;
245                     bubbleMessage.senderAvatar = null;
246                     bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null;
247                     return bubbleMessage;
248                 }
249             } else if (Notification.InboxStyle.class.equals(style)) {
250                 CharSequence[] lines =
251                         underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
252 
253                 // Return the last line since it should be the most recent.
254                 if (lines != null && lines.length > 0) {
255                     bubbleMessage.message = lines[lines.length - 1];
256                     return bubbleMessage;
257                 }
258             } else if (Notification.MediaStyle.class.equals(style)) {
259                 // Return nothing, media updates aren't typically useful as a text update.
260                 return bubbleMessage;
261             } else {
262                 // Default to text extra.
263                 bubbleMessage.message =
264                         underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
265                 return bubbleMessage;
266             }
267         } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
268             // No use crashing, we'll just return null and the caller will assume there's no update
269             // message.
270             e.printStackTrace();
271         }
272 
273         return bubbleMessage;
274     }
275 
276     @Nullable
loadSenderAvatar(@onNull final Context context, @Nullable final Icon icon)277     static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) {
278         Objects.requireNonNull(context);
279         if (icon == null) return null;
280         if (icon.getType() == Icon.TYPE_URI || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
281             context.grantUriPermission(context.getPackageName(),
282                     icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION);
283         }
284         return icon.loadDrawable(context);
285     }
286 }
287