diff options
11 files changed, 417 insertions, 307 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java index a6a3ce06324f..dc2499602125 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java @@ -16,22 +16,15 @@ package com.android.systemui.bubbles; import android.annotation.Nullable; -import android.app.Notification; import android.content.Context; -import android.content.pm.LauncherApps; +import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Matrix; import android.graphics.Path; import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.util.AttributeSet; import android.util.PathParser; import android.widget.ImageView; -import com.android.internal.graphics.ColorUtils; -import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.DotRenderer; import com.android.systemui.Interpolators; import com.android.systemui.R; @@ -46,9 +39,9 @@ import com.android.systemui.R; public class BadgedImageView extends ImageView { /** Same value as Launcher3 dot code */ - private static final float WHITE_SCRIM_ALPHA = 0.54f; + public static final float WHITE_SCRIM_ALPHA = 0.54f; /** Same as value in Launcher3 IconShape */ - private static final int DEFAULT_PATH_SIZE = 100; + public static final int DEFAULT_PATH_SIZE = 100; static final int DOT_STATE_DEFAULT = 0; static final int DOT_STATE_SUPPRESSED_FOR_FLYOUT = 1; @@ -58,7 +51,6 @@ public class BadgedImageView extends ImageView { private int mCurrentDotState = DOT_STATE_SUPPRESSED_FOR_FLYOUT; private Bubble mBubble; - private BubbleIconFactory mBubbleIconFactory; private int mIconBitmapSize; private DotRenderer mDotRenderer; @@ -94,6 +86,18 @@ public class BadgedImageView extends ImageView { mDotRenderer = new DotRenderer(mIconBitmapSize, iconPath, DEFAULT_PATH_SIZE); } + /** + * Updates the view with provided info. + */ + public void update(Bubble bubble, Bitmap bubbleImage, int dotColor, Path dotPath) { + mBubble = bubble; + setImageBitmap(bubbleImage); + setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT); + mDotColor = dotColor; + drawDot(dotPath); + animateDot(); + } + @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); @@ -140,14 +144,6 @@ public class BadgedImageView extends ImageView { } /** - * The colour to use for the dot. - */ - void setDotColor(int color) { - mDotColor = ColorUtils.setAlphaComponent(color, 255 /* alpha */); - invalidate(); - } - - /** * @param iconPath The new icon path to use when calculating dot position. */ void drawDot(Path iconPath) { @@ -187,25 +183,6 @@ public class BadgedImageView extends ImageView { } /** - * Populates this view with a bubble. - * <p> - * This should only be called when a new bubble is being set on the view, updates to the - * current bubble should use {@link #update(Bubble)}. - * - * @param bubble the bubble to display in this view. - */ - public void setBubble(Bubble bubble) { - mBubble = bubble; - } - - /** - * @param factory Factory for creating normalized bubble icons. - */ - public void setBubbleIconFactory(BubbleIconFactory factory) { - mBubbleIconFactory = factory; - } - - /** * The key for the {@link Bubble} associated with this view, if one exists. */ @Nullable @@ -213,15 +190,6 @@ public class BadgedImageView extends ImageView { return (mBubble != null) ? mBubble.getKey() : null; } - /** - * Updates the UI based on the bubble, updates badge and animates messages as needed. - */ - public void update(Bubble bubble) { - mBubble = bubble; - setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT); - updateViews(); - } - int getDotColor() { return mDotColor; } @@ -277,47 +245,4 @@ public class BadgedImageView extends ImageView { } }).start(); } - - void updateViews() { - if (mBubble == null || mBubbleIconFactory == null) { - return; - } - - Drawable bubbleDrawable = getBubbleDrawable(mContext); - BitmapInfo badgeBitmapInfo = mBubbleIconFactory.getBadgedBitmap(mBubble); - BitmapInfo bubbleBitmapInfo = mBubbleIconFactory.getBubbleBitmap(bubbleDrawable, - badgeBitmapInfo); - setImageBitmap(bubbleBitmapInfo.icon); - - // Update badge. - mDotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, Color.WHITE, WHITE_SCRIM_ALPHA); - setDotColor(mDotColor); - - // Update dot. - Path iconPath = PathParser.createPathFromPathData( - getResources().getString(com.android.internal.R.string.config_icon_mask)); - Matrix matrix = new Matrix(); - float scale = mBubbleIconFactory.getNormalizer().getScale(bubbleDrawable, - null /* outBounds */, null /* path */, null /* outMaskShape */); - float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f; - matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, - radius /* pivot y */); - iconPath.transform(matrix); - drawDot(iconPath); - - animateDot(); - } - - Drawable getBubbleDrawable(Context context) { - if (mBubble.getShortcutInfo() != null && mBubble.usingShortcutInfo()) { - LauncherApps launcherApps = - (LauncherApps) getContext().getSystemService(Context.LAUNCHER_APPS_SERVICE); - int density = getContext().getResources().getConfiguration().densityDpi; - return launcherApps.getShortcutIconDrawable(mBubble.getShortcutInfo(), density); - } else { - Notification.BubbleMetadata metadata = mBubble.getEntry().getBubbleMetadata(); - Icon ic = metadata.getIcon(); - return ic.loadDrawable(context); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index 7934e10c8605..77c8e0b07158 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -16,6 +16,7 @@ package com.android.systemui.bubbles; +import static android.os.AsyncTask.Status.FINISHED; import static android.view.Display.INVALID_DISPLAY; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; @@ -26,20 +27,17 @@ import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.res.Resources; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Parcelable; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; -import android.view.LayoutInflater; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; @@ -59,19 +57,19 @@ class Bubble { private NotificationEntry mEntry; private final String mKey; private final String mGroupId; + + private long mLastUpdated; + private long mLastAccessed; + + // Items that are typically loaded later private String mAppName; - private Drawable mUserBadgedAppIcon; private ShortcutInfo mShortcutInfo; - - private boolean mInflated; private BadgedImageView mIconView; private BubbleExpandedView mExpandedView; - private BubbleIconFactory mBubbleIconFactory; - - private long mLastUpdated; - private long mLastAccessed; - private boolean mIsUserCreated; + private boolean mInflated; + private BubbleViewInfoTask mInflationTask; + private boolean mInflateSynchronously; /** * Whether this notification should be shown in the shade when it is also displayed as a bubble. @@ -94,37 +92,11 @@ class Bubble { /** Used in tests when no UI is required. */ @VisibleForTesting(visibility = PRIVATE) - Bubble(Context context, NotificationEntry e) { + Bubble(NotificationEntry e) { mEntry = e; mKey = e.getKey(); mLastUpdated = e.getSbn().getPostTime(); mGroupId = groupId(e); - - String shortcutId = e.getSbn().getNotification().getShortcutId(); - if (BubbleExperimentConfig.useShortcutInfoToBubble(context) - && shortcutId != null) { - mShortcutInfo = BubbleExperimentConfig.getShortcutInfo(context, - e.getSbn().getPackageName(), - e.getSbn().getUser(), shortcutId); - } - - PackageManager pm = context.getPackageManager(); - ApplicationInfo info; - try { - info = pm.getApplicationInfo( - mEntry.getSbn().getPackageName(), - PackageManager.MATCH_UNINSTALLED_PACKAGES - | PackageManager.MATCH_DISABLED_COMPONENTS - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE - | PackageManager.MATCH_DIRECT_BOOT_AWARE); - if (info != null) { - mAppName = String.valueOf(pm.getApplicationLabel(info)); - } - Drawable appIcon = pm.getApplicationIcon(mEntry.getSbn().getPackageName()); - mUserBadgedAppIcon = pm.getUserBadgedIcon(appIcon, mEntry.getSbn().getUser()); - } catch (PackageManager.NameNotFoundException unused) { - mAppName = mEntry.getSbn().getPackageName(); - } } public String getKey() { @@ -143,41 +115,22 @@ class Bubble { return mEntry.getSbn().getPackageName(); } + @Nullable public String getAppName() { return mAppName; } - Drawable getUserBadgedAppIcon() { - return mUserBadgedAppIcon; - } - @Nullable public ShortcutInfo getShortcutInfo() { return mShortcutInfo; } - /** - * Whether shortcut information should be used to populate the bubble. - * <p> - * To populate the activity use {@link LauncherApps#startShortcut(ShortcutInfo, Rect, Bundle)}. - * To populate the icon use {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)}. - */ - public boolean usingShortcutInfo() { - return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent()); - } - - void setBubbleIconFactory(BubbleIconFactory factory) { - mBubbleIconFactory = factory; - } - - boolean isInflated() { - return mInflated; - } - + @Nullable BadgedImageView getIconView() { return mIconView; } + @Nullable BubbleExpandedView getExpandedView() { return mExpandedView; } @@ -188,20 +141,62 @@ class Bubble { } } - void inflate(LayoutInflater inflater, BubbleStackView stackView) { - if (mInflated) { - return; + /** + * Sets whether to perform inflation on the same thread as the caller. This method should only + * be used in tests, not in production. + */ + @VisibleForTesting + void setInflateSynchronously(boolean inflateSynchronously) { + mInflateSynchronously = inflateSynchronously; + } + + /** + * Starts a task to inflate & load any necessary information to display a bubble. + * + * @param callback the callback to notify one the bubble is ready to be displayed. + * @param context the context for the bubble. + * @param stackView the stackView the bubble is eventually added to. + * @param iconFactory the iconfactory use to create badged images for the bubble. + */ + void inflate(BubbleViewInfoTask.Callback callback, + Context context, + BubbleStackView stackView, + BubbleIconFactory iconFactory) { + if (isBubbleLoading()) { + mInflationTask.cancel(true /* mayInterruptIfRunning */); + } + mInflationTask = new BubbleViewInfoTask(this, + context, + stackView, + iconFactory, + callback); + if (mInflateSynchronously) { + mInflationTask.onPostExecute(mInflationTask.doInBackground()); + } else { + mInflationTask.execute(); } - mIconView = (BadgedImageView) inflater.inflate( - R.layout.bubble_view, stackView, false /* attachToRoot */); - mIconView.setBubbleIconFactory(mBubbleIconFactory); - mIconView.setBubble(this); + } - mExpandedView = (BubbleExpandedView) inflater.inflate( - R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); - mExpandedView.setBubble(this, stackView); + private boolean isBubbleLoading() { + return mInflationTask != null && mInflationTask.getStatus() != FINISHED; + } - mInflated = true; + boolean isInflated() { + return mInflated; + } + + void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) { + if (!isInflated()) { + mIconView = info.imageView; + mExpandedView = info.expandedView; + mInflated = true; + } + + mShortcutInfo = info.shortcutInfo; + mAppName = info.appName; + + mExpandedView.update(this); + mIconView.update(this, info.badgedBubbleImage, info.dotColor, info.dotPath); } /** @@ -218,13 +213,12 @@ class Bubble { } } - void updateEntry(NotificationEntry entry) { + /** + * Sets the entry associated with this bubble. + */ + void setEntry(NotificationEntry entry) { mEntry = entry; mLastUpdated = entry.getSbn().getPostTime(); - if (mInflated) { - mIconView.update(this); - mExpandedView.update(this); - } } /** @@ -242,13 +236,6 @@ class Bubble { } /** - * @return the timestamp in milliseconds when this bubble was last displayed in expanded state - */ - long getLastAccessTime() { - return mLastAccessed; - } - - /** * @return the display id of the virtual display on which bubble contents is drawn. */ int getDisplayId() { @@ -352,6 +339,16 @@ class Bubble { } } + /** + * Whether shortcut information should be used to populate the bubble. + * <p> + * To populate the activity use {@link LauncherApps#startShortcut(ShortcutInfo, Rect, Bundle)}. + * To populate the icon use {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)}. + */ + boolean usingShortcutInfo() { + return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent()); + } + @Nullable PendingIntent getBubbleIntent() { Notification.BubbleMetadata data = mEntry.getBubbleMetadata(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index c82bc30338a7..7cd29ea48199 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -150,6 +150,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; + private BubbleIconFactory mBubbleIconFactory; // Tracks the id of the current (foreground) user. private int mCurrentUserId; @@ -183,6 +184,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */ private int mOrientation = Configuration.ORIENTATION_UNDEFINED; + private boolean mInflateSynchronously; + /** * Listener to be notified when some states of the bubbles change. */ @@ -352,6 +355,16 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mUserBlockedBubbles = new HashSet<>(); mScreenshotHelper = new ScreenshotHelper(context); + mBubbleIconFactory = new BubbleIconFactory(context); + } + + /** + * Sets whether to perform inflation on the same thread as the caller. This method should only + * be used in tests, not in production. + */ + @VisibleForTesting + void setInflateSynchronously(boolean inflateSynchronously) { + mInflateSynchronously = inflateSynchronously; } /** @@ -415,16 +428,23 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onUiModeChanged() { - if (mStackView != null) { - mStackView.onThemeChanged(); - } + updateForThemeChanges(); } @Override public void onOverlayChanged() { + updateForThemeChanges(); + } + + private void updateForThemeChanges() { if (mStackView != null) { mStackView.onThemeChanged(); } + mBubbleIconFactory = new BubbleIconFactory(mContext); + for (Bubble b: mBubbleData.getBubbles()) { + // Reload each bubble + b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory); + } } @Override @@ -508,14 +528,10 @@ public class BubbleController implements ConfigurationController.ConfigurationLi return (isSummary && isSuppressedSummary) || isBubbleAndSuppressed; } - void selectBubble(Bubble bubble) { - mBubbleData.setSelectedBubble(bubble); - } - @VisibleForTesting void selectBubble(String key) { Bubble bubble = mBubbleData.getBubbleWithKey(key); - selectBubble(bubble); + mBubbleData.setSelectedBubble(bubble); } /** @@ -562,11 +578,19 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) { + if (mStackView == null) { + // Lazy init stack view when a bubble is created + ensureStackViewCreated(); + } // If this is an interruptive notif, mark that it's interrupted if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) { notif.setInterruption(); } - mBubbleData.notificationEntryUpdated(notif, suppressFlyout, showInShade); + Bubble bubble = mBubbleData.getOrCreateBubble(notif); + bubble.setInflateSynchronously(mInflateSynchronously); + bubble.inflate( + b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), + mContext, mStackView, mBubbleIconFactory); } /** @@ -783,16 +807,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void applyUpdate(BubbleData.Update update) { - if (mStackView == null && update.addedBubble != null) { - // Lazy init stack view when the first bubble is added. - ensureStackViewCreated(); - } - - // If not yet initialized, ignore all other changes. - if (mStackView == null) { - return; - } - if (update.addedBubble != null) { mStackView.addBubble(update.addedBubble); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index b7df5baa4cf1..97224f1234dd 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -33,6 +33,7 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.R; import com.android.systemui.bubbles.BubbleController.DismissReason; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -48,7 +49,6 @@ import java.util.Objects; import javax.inject.Inject; import javax.inject.Singleton; -import com.android.systemui.R; /** * Keeps track of active bubbles. @@ -180,28 +180,44 @@ public class BubbleData { dispatchPendingChanges(); } - void notificationEntryUpdated(NotificationEntry entry, boolean suppressFlyout, - boolean showInShade) { + /** + * Constructs a new bubble or returns an existing one. Does not add new bubbles to + * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)} + * for that. + */ + Bubble getOrCreateBubble(NotificationEntry entry) { + Bubble bubble = getBubbleWithKey(entry.getKey()); + if (bubble == null) { + bubble = new Bubble(entry); + } else { + bubble.setEntry(entry); + } + return bubble; + } + + /** + * When this method is called it is expected that all info in the bubble has completed loading. + * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, + * BubbleStackView, BubbleIconFactory). + */ + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { if (DEBUG_BUBBLE_DATA) { - Log.d(TAG, "notificationEntryUpdated: " + entry); + Log.d(TAG, "notificationEntryUpdated: " + bubble); } - Bubble bubble = getBubbleWithKey(entry.getKey()); - suppressFlyout |= !shouldShowFlyout(entry); + Bubble prevBubble = getBubbleWithKey(bubble.getKey()); + suppressFlyout |= !shouldShowFlyout(bubble.getEntry()); - if (bubble == null) { + if (prevBubble == null) { // Create a new bubble - bubble = new Bubble(mContext, entry); bubble.setSuppressFlyout(suppressFlyout); doAdd(bubble); trim(); } else { // Updates an existing bubble - bubble.updateEntry(entry); bubble.setSuppressFlyout(suppressFlyout); doUpdate(bubble); } - if (bubble.shouldAutoExpand()) { setSelectedBubbleInternal(bubble); if (!mExpanded) { @@ -214,6 +230,7 @@ public class BubbleData { bubble.setShowInShade(!isBubbleExpandedAndSelected && showInShade); bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */); dispatchPendingChanges(); + } public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index 63d036d6362d..c1705dbb4e4b 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -99,7 +99,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList private int mExpandedViewTouchSlop; private Bubble mBubble; - private String mAppName; private BubbleController mBubbleController = Dependency.get(BubbleController.class); private WindowManager mWindowManager; @@ -339,68 +338,51 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList } } - /** - * Sets the bubble used to populate this view. - */ - public void setBubble(Bubble bubble, BubbleStackView stackView) { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "setBubble: bubble=" + (bubble != null ? bubble.getKey() : "null")); - } + void setStackView(BubbleStackView stackView) { mStackView = stackView; - mBubble = bubble; - mAppName = bubble.getAppName(); - - applyThemeAttrs(); - showSettingsIcon(); - updateExpandedView(); - } - - /** - * Lets activity view know it should be shown / populated. - */ - public void populateExpandedView() { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "populateExpandedView: " - + "bubble=" + getBubbleKey()); - } - - if (usingActivityView()) { - mActivityView.setCallback(mStateCallback); - } else { - Log.e(TAG, "Cannot populate expanded view."); - } } /** - * Updates the bubble backing this view. This will not re-populate ActivityView, it will - * only update the deep-links in the title, and the height of the view. + * Sets the bubble used to populate this view. */ - public void update(Bubble bubble) { + void update(Bubble bubble) { if (DEBUG_BUBBLE_EXPANDED_VIEW) { Log.d(TAG, "update: bubble=" + (bubble != null ? bubble.getKey() : "null")); } - if (bubble.getKey().equals(mBubble.getKey())) { + boolean isNew = mBubble == null; + if (isNew || bubble.getKey().equals(mBubble.getKey())) { mBubble = bubble; - updateSettingsContentDescription(); - updateHeight(); + mSettingsIcon.setContentDescription(getResources().getString( + R.string.bubbles_settings_button_description, bubble.getAppName())); + + if (isNew) { + mBubbleIntent = mBubble.getBubbleIntent(); + if (mBubbleIntent != null) { + setContentVisibility(false); + mActivityView.setVisibility(VISIBLE); + } + } + applyThemeAttrs(); } else { Log.w(TAG, "Trying to update entry with different key, new bubble: " + bubble.getKey() + " old bubble: " + bubble.getKey()); } } - private void updateExpandedView() { + /** + * Lets activity view know it should be shown / populated with activity content. + */ + void populateExpandedView() { if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "updateExpandedView: bubble=" - + getBubbleKey()); + Log.d(TAG, "populateExpandedView: " + + "bubble=" + getBubbleKey()); } - mBubbleIntent = mBubble.getBubbleIntent(); - if (mBubbleIntent != null) { - setContentVisibility(false); - mActivityView.setVisibility(VISIBLE); + if (usingActivityView()) { + mActivityView.setCallback(mStateCallback); + } else { + Log.e(TAG, "Cannot populate expanded view."); } - updateView(); } boolean performBackPressIfNeeded() { @@ -490,16 +472,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList } } - private void updateSettingsContentDescription() { - mSettingsIcon.setContentDescription(getResources().getString( - R.string.bubbles_settings_button_description, mAppName)); - } - - void showSettingsIcon() { - updateSettingsContentDescription(); - mSettingsIcon.setVisibility(VISIBLE); - } - /** * Update appearance of the expanded view being displayed. */ diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java index 9ff033cb3411..b32dbb724258 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java @@ -15,11 +15,14 @@ */ package com.android.systemui.bubbles; +import android.app.Notification; import android.content.Context; +import android.content.pm.LauncherApps; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.BitmapInfo; @@ -42,19 +45,40 @@ public class BubbleIconFactory extends BaseIconFactory { return mContext.getResources().getDimensionPixelSize( com.android.launcher3.icons.R.dimen.profile_badge_size); } + /** + * Returns the drawable that the developer has provided to display in the bubble. + */ + Drawable getBubbleDrawable(Bubble b, Context context) { + if (b.getShortcutInfo() != null && b.usingShortcutInfo()) { + LauncherApps launcherApps = + (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE); + int density = context.getResources().getConfiguration().densityDpi; + return launcherApps.getShortcutIconDrawable(b.getShortcutInfo(), density); + } else { + Notification.BubbleMetadata metadata = b.getEntry().getBubbleMetadata(); + Icon ic = metadata.getIcon(); + return ic.loadDrawable(context); + } + } - BitmapInfo getBadgedBitmap(Bubble b) { + /** + * Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This + * will include the workprofile indicator on the badge if appropriate. + */ + BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon) { Bitmap userBadgedBitmap = createIconBitmap( - b.getUserBadgedAppIcon(), 1f, getBadgeSize()); + userBadgedAppIcon, 1f, getBadgeSize()); Canvas c = new Canvas(); ShadowGenerator shadowGenerator = new ShadowGenerator(getBadgeSize()); c.setBitmap(userBadgedBitmap); shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c); - BitmapInfo bitmapInfo = createIconBitmap(userBadgedBitmap); - return bitmapInfo; + return createIconBitmap(userBadgedBitmap); } + /** + * Returns a {@link BitmapInfo} for the entire bubble icon including the badge. + */ BitmapInfo getBubbleBitmap(Drawable bubble, BitmapInfo badge) { BitmapInfo bubbleIconInfo = createBadgedIconBitmap(bubble, null /* user */, @@ -64,5 +88,4 @@ public class BubbleIconFactory extends BaseIconFactory { new BitmapDrawable(mContext.getResources(), badge.icon)); return bubbleIconInfo; } - } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 245b2320f99d..898768311031 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -516,14 +516,7 @@ public class BubbleStackView extends FrameLayout { * Handle theme changes. */ public void onThemeChanged() { - // Recreate icon factory to update default adaptive icon scale. - mBubbleIconFactory = new BubbleIconFactory(mContext); setUpFlyout(); - for (Bubble b: mBubbleData.getBubbles()) { - b.getIconView().setBubbleIconFactory(mBubbleIconFactory); - b.getIconView().updateViews(); - b.getExpandedView().applyThemeAttrs(); - } } /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ @@ -749,10 +742,6 @@ public class BubbleStackView extends FrameLayout { mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); } - bubble.setBubbleIconFactory(mBubbleIconFactory); - bubble.inflate(mInflater, this); - bubble.getIconView().updateViews(); - // Set the dot position to the opposite of the side the stack is resting on, since the stack // resting slightly off-screen would result in the dot also being off-screen. bubble.getIconView().setDotPosition( @@ -1567,9 +1556,6 @@ public class BubbleStackView extends FrameLayout { mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); if (mIsExpanded) { - // First update the view so that it calculates a new height (ensuring the y position - // calculation is correct) - mExpandedBubble.getExpandedView().updateView(); final float y = getExpandedViewY(); if (!mExpandedViewYAnim.isRunning()) { // We're not animating so set the value diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java new file mode 100644 index 000000000000..41f5028ed5c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java @@ -0,0 +1,182 @@ +/* + * 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.content.Context; +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.os.AsyncTask; +import android.service.notification.StatusBarNotification; +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 java.lang.ref.WeakReference; + +/** + * 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); + } + } + } + + static class BubbleViewInfo { + BadgedImageView imageView; + BubbleExpandedView expandedView; + ShortcutInfo shortcutInfo; + String appName; + Bitmap badgedBubbleImage; + int dotColor; + Path dotPath; + + @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(); + + // Shortcut info for this bubble + String shortcutId = sbn.getNotification().getShortcutId(); + if (BubbleExperimentConfig.useShortcutInfoToBubble(c) + && shortcutId != null) { + info.shortcutInfo = BubbleExperimentConfig.getShortcutInfo(c, + packageName, + sbn.getUser(), shortcutId); + } + + // App name & app icon + PackageManager pm = c.getPackageManager(); + ApplicationInfo appInfo; + Drawable badgedIcon; + 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)); + } + Drawable 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(b, c); + BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(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); + return info; + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java index 2bf855a27cc4..e0b4b81c368d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -45,9 +45,7 @@ import android.app.IActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; -import android.content.Intent; import android.content.res.Resources; -import android.graphics.drawable.Icon; import android.hardware.face.FaceManager; import android.service.notification.ZenModeConfig; import android.testing.AndroidTestingRunner; @@ -57,7 +55,6 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; -import com.android.systemui.R; import com.android.systemui.SystemUIFactory; import com.android.systemui.SysuiTestCase; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -731,6 +728,7 @@ public class BubbleControllerTest extends SysuiTestCase { data, Runnable::run, configurationController, interruptionStateProvider, zenModeController, lockscreenUserManager, groupManager, entryManager, remoteInputUriController); + setInflateSynchronously(true); } } @@ -746,17 +744,6 @@ public class BubbleControllerTest extends SysuiTestCase { } /** - * @return basic {@link android.app.Notification.BubbleMetadata.Builder} - */ - private Notification.BubbleMetadata.Builder getBuilder() { - Intent target = new Intent(mContext, BubblesTestActivity.class); - PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, target, 0); - return new Notification.BubbleMetadata.Builder() - .setIntent(bubbleIntent) - .setIcon(Icon.createWithResource(mContext, R.drawable.android)); - } - - /** * Sets the bubble metadata flags for this entry. These flags are normally set by * NotificationManagerService when the notification is sent, however, these tests do not * go through that path so we set them explicitly when testing. diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java index 1131be943ee5..c4ae4093b680 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java @@ -85,6 +85,8 @@ public class BubbleDataTest extends SysuiTestCase { private Bubble mBubbleB2; private Bubble mBubbleB3; private Bubble mBubbleC1; + private Bubble mBubbleInterruptive; + private Bubble mBubbleDismissed; private BubbleData mBubbleData; @@ -119,18 +121,20 @@ public class BubbleDataTest extends SysuiTestCase { modifyRanking(mEntryInterruptive) .setVisuallyInterruptive(true) .build(); + mBubbleInterruptive = new Bubble(mEntryInterruptive); ExpandableNotificationRow row = mNotificationTestHelper.createBubble(); mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d"); mEntryDismissed.setRow(row); + mBubbleDismissed = new Bubble(mEntryDismissed); - mBubbleA1 = new Bubble(mContext, mEntryA1); - mBubbleA2 = new Bubble(mContext, mEntryA2); - mBubbleA3 = new Bubble(mContext, mEntryA3); - mBubbleB1 = new Bubble(mContext, mEntryB1); - mBubbleB2 = new Bubble(mContext, mEntryB2); - mBubbleB3 = new Bubble(mContext, mEntryB3); - mBubbleC1 = new Bubble(mContext, mEntryC1); + mBubbleA1 = new Bubble(mEntryA1); + mBubbleA2 = new Bubble(mEntryA2); + mBubbleA3 = new Bubble(mEntryA3); + mBubbleB1 = new Bubble(mEntryB1); + mBubbleB2 = new Bubble(mEntryB2); + mBubbleB3 = new Bubble(mEntryB3); + mBubbleC1 = new Bubble(mEntryC1); mBubbleData = new BubbleData(getContext()); @@ -180,7 +184,7 @@ public class BubbleDataTest extends SysuiTestCase { mBubbleData.setListener(mListener); // Test - mBubbleData.notificationEntryUpdated(mEntryC1, /* suppressFlyout */ true, /* showInShade */ + mBubbleData.notificationEntryUpdated(mBubbleC1, /* suppressFlyout */ true, /* showInShade */ true); // Verify @@ -195,7 +199,7 @@ public class BubbleDataTest extends SysuiTestCase { mBubbleData.setListener(mListener); // Test - mBubbleData.notificationEntryUpdated(mEntryInterruptive, + mBubbleData.notificationEntryUpdated(mBubbleInterruptive, false /* suppressFlyout */, true /* showInShade */); // Verify @@ -210,11 +214,11 @@ public class BubbleDataTest extends SysuiTestCase { mBubbleData.setListener(mListener); // Test - mBubbleData.notificationEntryUpdated(mEntryC1, false /* suppressFlyout */, + mBubbleData.notificationEntryUpdated(mBubbleC1, false /* suppressFlyout */, true /* showInShade */); verifyUpdateReceived(); - mBubbleData.notificationEntryUpdated(mEntryC1, false /* suppressFlyout */, + mBubbleData.notificationEntryUpdated(mBubbleC1, false /* suppressFlyout */, true /* showInShade */); verifyUpdateReceived(); @@ -229,16 +233,16 @@ public class BubbleDataTest extends SysuiTestCase { mBubbleData.setListener(mListener); // Test - mBubbleData.notificationEntryUpdated(mEntryDismissed, false /* suppressFlyout */, - false /* showInShade */); + mBubbleData.notificationEntryUpdated(mBubbleDismissed, false /* suppressFlyout */, + true /* showInShade */); verifyUpdateReceived(); // Make it look like user swiped away row mEntryDismissed.getRow().dismiss(false /* refocusOnDismiss */); - assertThat(mBubbleData.getBubbleWithKey(mEntryDismissed.getKey()).showInShade()).isFalse(); + assertThat(mBubbleData.getBubbleWithKey(mBubbleDismissed.getKey()).showInShade()).isFalse(); - mBubbleData.notificationEntryUpdated(mEntryDismissed, false /* suppressFlyout */, - false /* showInShade */); + mBubbleData.notificationEntryUpdated(mBubbleDismissed, false /* suppressFlyout */, + true /* showInShade */); verifyUpdateReceived(); // Verify @@ -974,7 +978,10 @@ public class BubbleDataTest extends SysuiTestCase { private void sendUpdatedEntryAtTime(NotificationEntry entry, long postTime) { setPostTime(entry, postTime); - mBubbleData.notificationEntryUpdated(entry, false /* suppressFlyout*/, + // BubbleController calls this: + Bubble b = mBubbleData.getOrCreateBubble(entry); + // And then this + mBubbleData.notificationEntryUpdated(b, false /* suppressFlyout*/, true /* showInShade */); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java index 643a5d2d82d5..3c42fd1cb48d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java @@ -58,7 +58,7 @@ public class BubbleTest extends SysuiTestCase { mEntry = new NotificationEntryBuilder() .setNotification(mNotif) .build(); - mBubble = new Bubble(mContext, mEntry); + mBubble = new Bubble(mEntry); } @Test |