blob: 8a57a735f6cbf7c7637e3779372c4a378e39f470 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.bubbles;
import static com.android.systemui.bubbles.BadgedImageView.DEFAULT_PATH_SIZE;
import static com.android.systemui.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import android.annotation.NonNull;
import android.app.Notification;
import android.app.Person;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.AsyncTask;
import android.os.Parcelable;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
import android.util.PathParser;
import android.view.LayoutInflater;
import androidx.annotation.Nullable;
import com.android.internal.graphics.ColorUtils;
import com.android.launcher3.icons.BitmapInfo;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import java.lang.ref.WeakReference;
import java.util.List;
/**
* Simple task to inflate views & load necessary info to display a bubble.
*/
public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES;
/**
* Callback to find out when the bubble has been inflated & necessary data loaded.
*/
public interface Callback {
/**
* Called when data has been loaded for the bubble.
*/
void onBubbleViewsReady(Bubble bubble);
}
private Bubble mBubble;
private WeakReference<Context> mContext;
private WeakReference<BubbleStackView> mStackView;
private BubbleIconFactory mIconFactory;
private Callback mCallback;
/**
* Creates a task to load information for the provided {@link Bubble}. Once all info
* is loaded, {@link Callback} is notified.
*/
BubbleViewInfoTask(Bubble b,
Context context,
BubbleStackView stackView,
BubbleIconFactory factory,
Callback c) {
mBubble = b;
mContext = new WeakReference<>(context);
mStackView = new WeakReference<>(stackView);
mIconFactory = factory;
mCallback = c;
}
@Override
protected BubbleViewInfo doInBackground(Void... voids) {
return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble);
}
@Override
protected void onPostExecute(BubbleViewInfo viewInfo) {
if (viewInfo != null) {
mBubble.setViewInfo(viewInfo);
if (mCallback != null && !isCancelled()) {
mCallback.onBubbleViewsReady(mBubble);
}
}
}
/**
* Info necessary to render a bubble.
*/
static class BubbleViewInfo {
BadgedImageView imageView;
BubbleExpandedView expandedView;
ShortcutInfo shortcutInfo;
String appName;
Bitmap badgedBubbleImage;
Drawable badgedAppIcon;
int dotColor;
Path dotPath;
Bubble.FlyoutMessage flyoutMessage;
@Nullable
static BubbleViewInfo populate(Context c, BubbleStackView stackView,
BubbleIconFactory iconFactory, Bubble b) {
BubbleViewInfo info = new BubbleViewInfo();
// View inflation: only should do this once per bubble
if (!b.isInflated()) {
LayoutInflater inflater = LayoutInflater.from(c);
info.imageView = (BadgedImageView) inflater.inflate(
R.layout.bubble_view, stackView, false /* attachToRoot */);
info.expandedView = (BubbleExpandedView) inflater.inflate(
R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
info.expandedView.setStackView(stackView);
}
StatusBarNotification sbn = b.getEntry().getSbn();
String packageName = sbn.getPackageName();
String bubbleShortcutId = b.getEntry().getBubbleMetadata().getShortcutId();
if (bubbleShortcutId != null) {
info.shortcutInfo = b.getEntry().getRanking().getShortcutInfo();
}
// App name & app icon
PackageManager pm = c.getPackageManager();
ApplicationInfo appInfo;
Drawable badgedIcon;
Drawable appIcon;
try {
appInfo = pm.getApplicationInfo(
packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.MATCH_DIRECT_BOOT_AWARE);
if (appInfo != null) {
info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
}
appIcon = pm.getApplicationIcon(packageName);
badgedIcon = pm.getUserBadgedIcon(appIcon, sbn.getUser());
} catch (PackageManager.NameNotFoundException exception) {
// If we can't find package... don't think we should show the bubble.
Log.w(TAG, "Unable to find package: " + packageName);
return null;
}
// Badged bubble image
Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo,
b.getEntry().getBubbleMetadata());
if (bubbleDrawable == null) {
// Default to app icon
bubbleDrawable = appIcon;
}
BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon);
info.badgedAppIcon = badgedIcon;
info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable,
badgeBitmapInfo).icon;
// Dot color & placement
Path iconPath = PathParser.createPathFromPathData(
c.getResources().getString(com.android.internal.R.string.config_icon_mask));
Matrix matrix = new Matrix();
float scale = iconFactory.getNormalizer().getScale(bubbleDrawable,
null /* outBounds */, null /* path */, null /* outMaskShape */);
float radius = DEFAULT_PATH_SIZE / 2f;
matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
radius /* pivot y */);
iconPath.transform(matrix);
info.dotPath = iconPath;
info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
Color.WHITE, WHITE_SCRIM_ALPHA);
// Flyout
info.flyoutMessage = extractFlyoutMessage(c, b.getEntry());
return info;
}
}
/**
* Returns our best guess for the most relevant text summary of the latest update to this
* notification, based on its type. Returns null if there should not be an update message.
*/
@NonNull
static Bubble.FlyoutMessage extractFlyoutMessage(Context context,
NotificationEntry entry) {
final Notification underlyingNotif = entry.getSbn().getNotification();
final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage();
bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean(
Notification.EXTRA_IS_GROUP_CONVERSATION);
try {
if (Notification.BigTextStyle.class.equals(style)) {
// Return the big text, it is big so probably important. If it's not there use the
// normal text.
CharSequence bigText =
underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
bubbleMessage.message = !TextUtils.isEmpty(bigText)
? bigText
: underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
return bubbleMessage;
} else if (Notification.MessagingStyle.class.equals(style)) {
final List<Notification.MessagingStyle.Message> messages =
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
(Parcelable[]) underlyingNotif.extras.get(
Notification.EXTRA_MESSAGES));
final Notification.MessagingStyle.Message latestMessage =
Notification.MessagingStyle.findLatestIncomingMessage(messages);
if (latestMessage != null) {
bubbleMessage.message = latestMessage.getText();
Person sender = latestMessage.getSenderPerson();
bubbleMessage.senderName = sender != null
? sender.getName()
: null;
bubbleMessage.senderAvatar = null;
if (sender != null && sender.getIcon() != null) {
if (sender.getIcon().getType() == Icon.TYPE_URI
|| sender.getIcon().getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
context.grantUriPermission(context.getPackageName(),
sender.getIcon().getUri(),
Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
bubbleMessage.senderAvatar = sender.getIcon().loadDrawable(context);
}
return bubbleMessage;
}
} else if (Notification.InboxStyle.class.equals(style)) {
CharSequence[] lines =
underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
// Return the last line since it should be the most recent.
if (lines != null && lines.length > 0) {
bubbleMessage.message = lines[lines.length - 1];
return bubbleMessage;
}
} else if (Notification.MediaStyle.class.equals(style)) {
// Return nothing, media updates aren't typically useful as a text update.
return bubbleMessage;
} else {
// Default to text extra.
bubbleMessage.message =
underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
return bubbleMessage;
}
} catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
// No use crashing, we'll just return null and the caller will assume there's no update
// message.
e.printStackTrace();
}
return bubbleMessage;
}
}