blob: 501e5024d9405eb2b343ca7f258c42e6d3f39a2a [file] [log] [blame]
Mady Mellor3df7ab02019-12-09 15:07:10 -08001/*
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
17package com.android.systemui.bubbles;
18
19import static com.android.systemui.bubbles.BadgedImageView.DEFAULT_PATH_SIZE;
20import static com.android.systemui.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA;
21import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
22import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
23
Mady Mellordf898fd2020-01-09 09:26:36 -080024import android.annotation.NonNull;
25import android.app.Notification;
26import android.app.Person;
Mady Mellor3df7ab02019-12-09 15:07:10 -080027import android.content.Context;
Lyn Hane566b362020-03-13 17:32:19 -070028import android.content.Intent;
Mady Mellor3df7ab02019-12-09 15:07:10 -080029import android.content.pm.ApplicationInfo;
30import android.content.pm.PackageManager;
31import android.content.pm.ShortcutInfo;
32import android.graphics.Bitmap;
33import android.graphics.Color;
34import android.graphics.Matrix;
35import android.graphics.Path;
36import android.graphics.drawable.Drawable;
Lyn Hane566b362020-03-13 17:32:19 -070037import android.graphics.drawable.Icon;
Mady Mellor3df7ab02019-12-09 15:07:10 -080038import android.os.AsyncTask;
Mady Mellordf898fd2020-01-09 09:26:36 -080039import android.os.Parcelable;
Mady Mellor3df7ab02019-12-09 15:07:10 -080040import android.service.notification.StatusBarNotification;
Mady Mellordf898fd2020-01-09 09:26:36 -080041import android.text.TextUtils;
Mady Mellor3df7ab02019-12-09 15:07:10 -080042import android.util.Log;
43import android.util.PathParser;
44import android.view.LayoutInflater;
45
46import androidx.annotation.Nullable;
47
48import com.android.internal.graphics.ColorUtils;
49import com.android.launcher3.icons.BitmapInfo;
50import com.android.systemui.R;
Mady Mellordf898fd2020-01-09 09:26:36 -080051import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Mady Mellor3df7ab02019-12-09 15:07:10 -080052
53import java.lang.ref.WeakReference;
Mady Mellordf898fd2020-01-09 09:26:36 -080054import java.util.List;
Mady Mellor3df7ab02019-12-09 15:07:10 -080055
56/**
57 * Simple task to inflate views & load necessary info to display a bubble.
58 */
59public 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 */
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 Callback mCallback;
78
79 /**
80 * Creates a task to load information for the provided {@link Bubble}. Once all info
81 * is loaded, {@link Callback} is notified.
82 */
83 BubbleViewInfoTask(Bubble b,
84 Context context,
85 BubbleStackView stackView,
86 BubbleIconFactory factory,
87 Callback c) {
88 mBubble = b;
89 mContext = new WeakReference<>(context);
90 mStackView = new WeakReference<>(stackView);
91 mIconFactory = factory;
92 mCallback = c;
93 }
94
95 @Override
96 protected BubbleViewInfo doInBackground(Void... voids) {
97 return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble);
98 }
99
100 @Override
101 protected void onPostExecute(BubbleViewInfo viewInfo) {
102 if (viewInfo != null) {
103 mBubble.setViewInfo(viewInfo);
104 if (mCallback != null && !isCancelled()) {
105 mCallback.onBubbleViewsReady(mBubble);
106 }
107 }
108 }
109
Mady Mellordf898fd2020-01-09 09:26:36 -0800110 /**
111 * Info necessary to render a bubble.
112 */
Mady Mellor3df7ab02019-12-09 15:07:10 -0800113 static class BubbleViewInfo {
114 BadgedImageView imageView;
115 BubbleExpandedView expandedView;
116 ShortcutInfo shortcutInfo;
117 String appName;
118 Bitmap badgedBubbleImage;
119 int dotColor;
120 Path dotPath;
Mady Mellordf898fd2020-01-09 09:26:36 -0800121 Bubble.FlyoutMessage flyoutMessage;
Mady Mellor3df7ab02019-12-09 15:07:10 -0800122
123 @Nullable
124 static BubbleViewInfo populate(Context c, BubbleStackView stackView,
125 BubbleIconFactory iconFactory, Bubble b) {
126 BubbleViewInfo info = new BubbleViewInfo();
127
128 // View inflation: only should do this once per bubble
129 if (!b.isInflated()) {
130 LayoutInflater inflater = LayoutInflater.from(c);
131 info.imageView = (BadgedImageView) inflater.inflate(
132 R.layout.bubble_view, stackView, false /* attachToRoot */);
133
134 info.expandedView = (BubbleExpandedView) inflater.inflate(
135 R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
136 info.expandedView.setStackView(stackView);
137 }
138
139 StatusBarNotification sbn = b.getEntry().getSbn();
140 String packageName = sbn.getPackageName();
141
Mady Mellor2ac2d3a2020-01-08 17:18:54 -0800142 // Real shortcut info for this bubble
143 String bubbleShortcutId = b.getEntry().getBubbleMetadata().getShortcutId();
144 if (bubbleShortcutId != null) {
145 info.shortcutInfo = BubbleExperimentConfig.getShortcutInfo(c, packageName,
146 sbn.getUser(), bubbleShortcutId);
147 } else {
148 // Check for experimental shortcut
149 String shortcutId = sbn.getNotification().getShortcutId();
150 if (BubbleExperimentConfig.useShortcutInfoToBubble(c) && shortcutId != null) {
151 info.shortcutInfo = BubbleExperimentConfig.getShortcutInfo(c,
152 packageName,
153 sbn.getUser(), shortcutId);
154 }
Mady Mellor3df7ab02019-12-09 15:07:10 -0800155 }
156
Mady Mellor2ac2d3a2020-01-08 17:18:54 -0800157
Mady Mellor3df7ab02019-12-09 15:07:10 -0800158 // App name & app icon
159 PackageManager pm = c.getPackageManager();
160 ApplicationInfo appInfo;
161 Drawable badgedIcon;
Mady Mellor2dae34b2020-01-07 13:52:58 -0800162 Drawable appIcon;
Mady Mellor3df7ab02019-12-09 15:07:10 -0800163 try {
164 appInfo = pm.getApplicationInfo(
165 packageName,
166 PackageManager.MATCH_UNINSTALLED_PACKAGES
167 | PackageManager.MATCH_DISABLED_COMPONENTS
168 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
169 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
170 if (appInfo != null) {
171 info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
172 }
Mady Mellor2dae34b2020-01-07 13:52:58 -0800173 appIcon = pm.getApplicationIcon(packageName);
Mady Mellor3df7ab02019-12-09 15:07:10 -0800174 badgedIcon = pm.getUserBadgedIcon(appIcon, sbn.getUser());
175 } catch (PackageManager.NameNotFoundException exception) {
176 // If we can't find package... don't think we should show the bubble.
177 Log.w(TAG, "Unable to find package: " + packageName);
178 return null;
179 }
180
181 // Badged bubble image
Mady Mellor2ac2d3a2020-01-08 17:18:54 -0800182 Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo,
183 b.getEntry().getBubbleMetadata());
Mady Mellor2dae34b2020-01-07 13:52:58 -0800184 if (bubbleDrawable == null) {
185 // Default to app icon
186 bubbleDrawable = appIcon;
187 }
188
Mady Mellor3df7ab02019-12-09 15:07:10 -0800189 BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon);
190 info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable,
191 badgeBitmapInfo).icon;
192
193 // Dot color & placement
194 Path iconPath = PathParser.createPathFromPathData(
195 c.getResources().getString(com.android.internal.R.string.config_icon_mask));
196 Matrix matrix = new Matrix();
197 float scale = iconFactory.getNormalizer().getScale(bubbleDrawable,
198 null /* outBounds */, null /* path */, null /* outMaskShape */);
199 float radius = DEFAULT_PATH_SIZE / 2f;
200 matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
201 radius /* pivot y */);
202 iconPath.transform(matrix);
203 info.dotPath = iconPath;
204 info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
205 Color.WHITE, WHITE_SCRIM_ALPHA);
Mady Mellordf898fd2020-01-09 09:26:36 -0800206
207 // Flyout
208 info.flyoutMessage = extractFlyoutMessage(c, b.getEntry());
Mady Mellor3df7ab02019-12-09 15:07:10 -0800209 return info;
210 }
211 }
Mady Mellordf898fd2020-01-09 09:26:36 -0800212
213
214 /**
215 * Returns our best guess for the most relevant text summary of the latest update to this
216 * notification, based on its type. Returns null if there should not be an update message.
217 */
218 @NonNull
219 static Bubble.FlyoutMessage extractFlyoutMessage(Context context,
220 NotificationEntry entry) {
221 final Notification underlyingNotif = entry.getSbn().getNotification();
222 final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
223
224 Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage();
225 bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean(
226 Notification.EXTRA_IS_GROUP_CONVERSATION);
227 try {
228 if (Notification.BigTextStyle.class.equals(style)) {
229 // Return the big text, it is big so probably important. If it's not there use the
230 // normal text.
231 CharSequence bigText =
232 underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
233 bubbleMessage.message = !TextUtils.isEmpty(bigText)
234 ? bigText
235 : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
236 return bubbleMessage;
237 } else if (Notification.MessagingStyle.class.equals(style)) {
238 final List<Notification.MessagingStyle.Message> messages =
239 Notification.MessagingStyle.Message.getMessagesFromBundleArray(
240 (Parcelable[]) underlyingNotif.extras.get(
241 Notification.EXTRA_MESSAGES));
242
243 final Notification.MessagingStyle.Message latestMessage =
244 Notification.MessagingStyle.findLatestIncomingMessage(messages);
245 if (latestMessage != null) {
246 bubbleMessage.message = latestMessage.getText();
247 Person sender = latestMessage.getSenderPerson();
248 bubbleMessage.senderName = sender != null
249 ? sender.getName()
250 : null;
Lyn Hane566b362020-03-13 17:32:19 -0700251
252 bubbleMessage.senderAvatar = null;
253 if (sender != null && sender.getIcon() != null) {
254 if (sender.getIcon().getType() == Icon.TYPE_URI
255 || sender.getIcon().getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
256 context.grantUriPermission(context.getPackageName(),
257 sender.getIcon().getUri(),
258 Intent.FLAG_GRANT_READ_URI_PERMISSION);
259 }
260 bubbleMessage.senderAvatar = sender.getIcon().loadDrawable(context);
261 }
Mady Mellordf898fd2020-01-09 09:26:36 -0800262 return bubbleMessage;
263 }
264 } else if (Notification.InboxStyle.class.equals(style)) {
265 CharSequence[] lines =
266 underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
267
268 // Return the last line since it should be the most recent.
269 if (lines != null && lines.length > 0) {
270 bubbleMessage.message = lines[lines.length - 1];
271 return bubbleMessage;
272 }
273 } else if (Notification.MediaStyle.class.equals(style)) {
274 // Return nothing, media updates aren't typically useful as a text update.
275 return bubbleMessage;
276 } else {
277 // Default to text extra.
278 bubbleMessage.message =
279 underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
280 return bubbleMessage;
281 }
282 } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
283 // No use crashing, we'll just return null and the caller will assume there's no update
284 // message.
285 e.printStackTrace();
286 }
287
288 return bubbleMessage;
289 }
Mady Mellor3df7ab02019-12-09 15:07:10 -0800290}