/* * 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.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 com.android.systemui.statusbar.phone.StatusBar; import java.lang.ref.WeakReference; import java.util.List; import java.util.Objects; /** * Simple task to inflate views & load necessary info to display a bubble. */ public class BubbleViewInfoTask extends AsyncTask { 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 mContext; private WeakReference mStackView; private BubbleIconFactory mIconFactory; private boolean mSkipInflation; 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, boolean skipInflation, Callback c) { mBubble = b; mContext = new WeakReference<>(context); mStackView = new WeakReference<>(stackView); mIconFactory = factory; mSkipInflation = skipInflation; mCallback = c; } @Override protected BubbleViewInfo doInBackground(Void... voids) { return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble, mSkipInflation); } @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, boolean skipInflation) { BubbleViewInfo info = new BubbleViewInfo(); // View inflation: only should do this once per bubble if (!skipInflation && !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); } if (b.getShortcutInfo() != null) { info.shortcutInfo = b.getShortcutInfo(); } // App name & app icon PackageManager pm = StatusBar.getPackageManagerForUser( c, b.getUser().getIdentifier()); ApplicationInfo appInfo; Drawable badgedIcon; Drawable appIcon; try { appInfo = pm.getApplicationInfo( b.getPackageName(), 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(b.getPackageName()); badgedIcon = pm.getUserBadgedIcon(appIcon, b.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: " + b.getPackageName()); return null; } // Badged bubble image Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, b.getIcon()); if (bubbleDrawable == null) { // Default to app icon bubbleDrawable = appIcon; } BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon, b.isImportantConversation()); 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 = b.getFlyoutMessage(); if (info.flyoutMessage != null) { info.flyoutMessage.senderAvatar = loadSenderAvatar(c, info.flyoutMessage.senderIcon); } 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(NotificationEntry entry) { Objects.requireNonNull(entry); final Notification underlyingNotif = entry.getSbn().getNotification(); final Class 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 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; bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null; 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; } @Nullable static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { Objects.requireNonNull(context); if (icon == null) return null; if (icon.getType() == Icon.TYPE_URI || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { context.grantUriPermission(context.getPackageName(), icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); } return icon.loadDrawable(context); } }