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