diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java index 4597b16568849..b33424c563a38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java @@ -33,6 +33,7 @@ import com.android.keyguard.AlphaOptimizedLinearLayout; import com.android.systemui.R; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntry.OnSensitivityChangedListener; import java.util.List; @@ -159,20 +160,30 @@ public class HeadsUpStatusBarView extends AlphaOptimizedLinearLayout { } public void setEntry(NotificationEntry entry) { - if (entry != null) { - mShowingEntry = entry; + if (mShowingEntry != null) { + mShowingEntry.removeOnSensitivityChangedListener(mOnSensitivityChangedListener); + } + mShowingEntry = entry; + + if (mShowingEntry != null) { CharSequence text = entry.headsUpStatusBarText; if (entry.isSensitive()) { text = entry.headsUpStatusBarTextPublic; } mTextView.setText(text); - mShowingEntry.setOnSensitiveChangedListener(() -> setEntry(entry)); - } else if (mShowingEntry != null){ - mShowingEntry.setOnSensitiveChangedListener(null); - mShowingEntry = null; + mShowingEntry.addOnSensitivityChangedListener(mOnSensitivityChangedListener); } } + private final OnSensitivityChangedListener mOnSensitivityChangedListener = entry -> { + if (entry != mShowingEntry) { + throw new IllegalStateException("Got a sensitivity change for " + entry + + " but mShowingEntry is " + mShowingEntry); + } + // Update the text + setEntry(entry); + }; + @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 87be73998fccc..d8fdf928f6add 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -70,6 +70,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -262,11 +263,11 @@ public class NotificationMediaManager implements Dumpable { synchronized (mEntryManager) { NotificationEntry entry = mEntryManager .getActiveNotificationUnfiltered(mMediaNotificationKey); - if (entry == null || entry.expandedIcon == null) { + if (entry == null || entry.getIcons().getShelfIcon() == null) { return null; } - return entry.expandedIcon.getSourceIcon(); + return entry.getIcons().getShelfIcon().getSourceIcon(); } } @@ -284,8 +285,7 @@ public class NotificationMediaManager implements Dumpable { boolean metaDataChanged = false; synchronized (mEntryManager) { - Set allNotifications = - mEntryManager.getPendingAndActiveNotifications(); + Collection allNotifications = mEntryManager.getAllNotifs(); // Promote the media notification with a controller in 'playing' state, if any. NotificationEntry mediaNotification = null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 1a8454cfa1138..d7f2ae43cf9ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -329,7 +329,7 @@ public class NotificationShelf extends ActivatableNotificationView implements expandableRow.setAboveShelf(false); } if (notGoneIndex == 0) { - StatusBarIconView icon = expandableRow.getEntry().expandedIcon; + StatusBarIconView icon = expandableRow.getEntry().getIcons().getShelfIcon(); NotificationIconContainer.IconState iconState = getIconState(icon); // The icon state might be null in rare cases where the notification is actually // added to the layout, but not to the shelf. An example are replied messages, @@ -432,7 +432,7 @@ public class NotificationShelf extends ActivatableNotificationView implements // if the shelf is clipped, lets make sure we also clip the icon maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount()); } - StatusBarIconView icon = row.getEntry().expandedIcon; + StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon(); float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY(); if (shelfIconPosition < maxTop && !mAmbientState.isFullyHidden()) { int top = (int) (maxTop - shelfIconPosition); @@ -444,7 +444,7 @@ public class NotificationShelf extends ActivatableNotificationView implements } private void updateContinuousClipping(final ExpandableNotificationRow row) { - StatusBarIconView icon = row.getEntry().expandedIcon; + StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon(); boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDozing(); boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null; if (needsContinuousClipping && !isContinuousClipping) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java index 7f0479c39c95f..c6d84ff79bde7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -29,6 +29,7 @@ import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -56,9 +57,9 @@ import com.android.systemui.util.leak.LeakDetector; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -105,6 +106,10 @@ public class NotificationEntryManager implements */ public static final int UNDEFINED_DISMISS_REASON = 0; + private final Set mAllNotifications = new ArraySet<>(); + private final Set mReadOnlyAllNotifications = + Collections.unmodifiableSet(mAllNotifications); + /** Pending notifications are ones awaiting inflation */ @VisibleForTesting protected final HashMap mPendingNotifications = new HashMap<>(); @@ -468,6 +473,8 @@ public class NotificationEntryManager implements entry.removeRow(); } + mAllNotifications.remove(entry); + // Let's remove the children if this was a summary handleGroupSummaryRemoved(key); removeVisibleNotification(key); @@ -548,6 +555,7 @@ public class NotificationEntryManager implements notification, ranking, mFgsFeatureController.isForegroundServiceDismissalEnabled()); + mAllNotifications.add(entry); mLeakDetector.trackInstance(entry); @@ -708,15 +716,6 @@ public class NotificationEntryManager implements return mPendingNotifications.values(); } - /** - * @return all notifications we're currently aware of (both pending and active notifications) - */ - public Set getPendingAndActiveNotifications() { - Set allNotifs = new HashSet<>(mPendingNotifications.values()); - allNotifs.addAll(mSortedAndFiltered); - return allNotifs; - } - /** * Use this method to retrieve a notification entry that has been prepared for presentation. * Note that the notification may be filtered out and never shown to the user. @@ -842,7 +841,7 @@ public class NotificationEntryManager implements private void dumpEntry(PrintWriter pw, String indent, int i, NotificationEntry e) { pw.print(indent); - pw.println(" [" + i + "] key=" + e.getKey() + " icon=" + e.icon); + pw.println(" [" + i + "] key=" + e.getKey() + " icon=" + e.getIcons().getStatusBarIcon()); StatusBarNotification n = e.getSbn(); pw.print(indent); pw.println(" pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" @@ -861,6 +860,15 @@ public class NotificationEntryManager implements return mReadOnlyNotifications; } + /** + * Returns a collections containing ALL notifications we know about, including ones that are + * hidden or for other users. See {@link CommonNotifCollection#getAllNotifs()}. + */ + @Override + public Collection getAllNotifs() { + return mReadOnlyAllNotifications; + } + /** @return A count of the active notifications */ public int getActiveNotificationsCount() { return mReadOnlyNotifications.size(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index 749e5e2a769dc..b90cfa8ae25e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -64,24 +64,34 @@ import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent; import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer; import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler; +import com.android.systemui.statusbar.notification.collection.notifcollection.CleanUpEntryEvent; import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; +import com.android.systemui.statusbar.notification.collection.notifcollection.EntryAddedEvent; +import com.android.systemui.statusbar.notification.collection.notifcollection.EntryRemovedEvent; +import com.android.systemui.statusbar.notification.collection.notifcollection.EntryUpdatedEvent; +import com.android.systemui.statusbar.notification.collection.notifcollection.InitEntryEvent; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; +import com.android.systemui.statusbar.notification.collection.notifcollection.RankingAppliedEvent; +import com.android.systemui.statusbar.notification.collection.notifcollection.RankingUpdatedEvent; import com.android.systemui.util.Assert; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Queue; import javax.inject.Inject; import javax.inject.Singleton; @@ -124,6 +134,8 @@ public class NotifCollection implements Dumpable { private final List mLifetimeExtenders = new ArrayList<>(); private final List mDismissInterceptors = new ArrayList<>(); + private Queue mEventQueue = new ArrayDeque<>(); + private boolean mAttached = false; private boolean mAmDispatchingToOtherCode; @@ -160,8 +172,8 @@ public class NotifCollection implements Dumpable { mBuildListener = buildListener; } - /** @see NotifPipeline#getActiveNotifs() */ - Collection getActiveNotifs() { + /** @see NotifPipeline#getAllNotifs() */ + Collection getAllNotifs() { Assert.isMainThread(); return mReadOnlyNotificationSet; } @@ -242,7 +254,7 @@ public class NotifCollection implements Dumpable { } locallyDismissNotifications(entriesToLocallyDismiss); - rebuildList(); + dispatchEventsAndRebuildList(); } /** @@ -251,8 +263,7 @@ public class NotifCollection implements Dumpable { public void dismissNotification( NotificationEntry entry, @NonNull DismissedByUserStats stats) { - dismissNotifications(List.of( - new Pair(entry, stats))); + dismissNotifications(List.of(new Pair<>(entry, stats))); } /** @@ -268,7 +279,7 @@ public class NotifCollection implements Dumpable { // system process is dead if we're here. } - final List entries = new ArrayList(getActiveNotifs()); + final List entries = new ArrayList<>(getAllNotifs()); for (int i = entries.size() - 1; i >= 0; i--) { NotificationEntry entry = entries.get(i); if (!shouldDismissOnClearAll(entry, userId)) { @@ -283,7 +294,7 @@ public class NotifCollection implements Dumpable { } locallyDismissNotifications(entries); - rebuildList(); + dispatchEventsAndRebuildList(); } /** @@ -326,8 +337,9 @@ public class NotifCollection implements Dumpable { private void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { Assert.isMainThread(); - postNotification(sbn, requireRanking(rankingMap, sbn.getKey()), rankingMap); - rebuildList(); + postNotification(sbn, requireRanking(rankingMap, sbn.getKey())); + applyRanking(rankingMap); + dispatchEventsAndRebuildList(); } private void onNotificationGroupPosted(List batch) { @@ -336,9 +348,9 @@ public class NotifCollection implements Dumpable { mLogger.logNotifGroupPosted(batch.get(0).getSbn().getGroupKey(), batch.size()); for (CoalescedEvent event : batch) { - postNotification(event.getSbn(), event.getRanking(), null); + postNotification(event.getSbn(), event.getRanking()); } - rebuildList(); + dispatchEventsAndRebuildList(); } private void onNotificationRemoved( @@ -354,55 +366,49 @@ public class NotifCollection implements Dumpable { throw new IllegalStateException("No notification to remove with key " + sbn.getKey()); } entry.mCancellationReason = reason; - applyRanking(rankingMap); tryRemoveNotification(entry); - rebuildList(); + applyRanking(rankingMap); + dispatchEventsAndRebuildList(); } private void onNotificationRankingUpdate(RankingMap rankingMap) { Assert.isMainThread(); + mEventQueue.add(new RankingUpdatedEvent(rankingMap)); applyRanking(rankingMap); - dispatchNotificationRankingUpdate(rankingMap); - rebuildList(); + dispatchEventsAndRebuildList(); } private void postNotification( StatusBarNotification sbn, - Ranking ranking, - @Nullable RankingMap rankingMap) { + Ranking ranking) { NotificationEntry entry = mNotificationSet.get(sbn.getKey()); if (entry == null) { // A new notification! - mLogger.logNotifPosted(sbn.getKey()); - entry = new NotificationEntry(sbn, ranking); mNotificationSet.put(sbn.getKey(), entry); - dispatchOnEntryInit(entry); - if (rankingMap != null) { - applyRanking(rankingMap); - } - - dispatchOnEntryAdded(entry); + mLogger.logNotifPosted(sbn.getKey()); + mEventQueue.add(new InitEntryEvent(entry)); + mEventQueue.add(new EntryAddedEvent(entry)); } else { // Update to an existing entry - mLogger.logNotifUpdated(sbn.getKey()); // Notification is updated so it is essentially re-added and thus alive again, so we // can reset its state. + // TODO: If a coalesced event ever gets here, it's possible to lose track of children, + // since their rankings might have been updated earlier (and thus we may no longer + // think a child is associated with this locally-dismissed entry). cancelLocalDismissal(entry); cancelLifetimeExtension(entry); cancelDismissInterception(entry); entry.mCancellationReason = REASON_NOT_CANCELED; entry.setSbn(sbn); - if (rankingMap != null) { - applyRanking(rankingMap); - } - dispatchOnEntryUpdated(entry); + mLogger.logNotifUpdated(sbn.getKey()); + mEventQueue.add(new EntryUpdatedEvent(entry)); } } @@ -432,8 +438,8 @@ public class NotifCollection implements Dumpable { if (!isLifetimeExtended(entry)) { mNotificationSet.remove(entry.getKey()); cancelDismissInterception(entry); - dispatchOnEntryRemoved(entry, entry.mCancellationReason); - dispatchOnEntryCleanUp(entry); + mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason)); + mEventQueue.add(new CleanUpEntryEvent(entry)); return true; } else { return false; @@ -466,9 +472,16 @@ public class NotifCollection implements Dumpable { } } } + mEventQueue.add(new RankingAppliedEvent()); } - private void rebuildList() { + private void dispatchEventsAndRebuildList() { + mAmDispatchingToOtherCode = true; + while (!mEventQueue.isEmpty()) { + mEventQueue.remove().dispatchTo(mNotifCollectionListeners); + } + mAmDispatchingToOtherCode = false; + if (mBuildListener != null) { mBuildListener.onBuildList(mReadOnlyNotificationSet); } @@ -491,7 +504,7 @@ public class NotifCollection implements Dumpable { if (!isLifetimeExtended(entry)) { if (tryRemoveNotification(entry)) { - rebuildList(); + dispatchEventsAndRebuildList(); } } } @@ -660,57 +673,9 @@ public class NotifCollection implements Dumpable { || entry.getSbn().getUser().getIdentifier() == userId; } - private void dispatchOnEntryInit(NotificationEntry entry) { - mAmDispatchingToOtherCode = true; - for (NotifCollectionListener listener : mNotifCollectionListeners) { - listener.onEntryInit(entry); - } - mAmDispatchingToOtherCode = false; - } - - private void dispatchOnEntryAdded(NotificationEntry entry) { - mAmDispatchingToOtherCode = true; - for (NotifCollectionListener listener : mNotifCollectionListeners) { - listener.onEntryAdded(entry); - } - mAmDispatchingToOtherCode = false; - } - - private void dispatchOnEntryUpdated(NotificationEntry entry) { - mAmDispatchingToOtherCode = true; - for (NotifCollectionListener listener : mNotifCollectionListeners) { - listener.onEntryUpdated(entry); - } - mAmDispatchingToOtherCode = false; - } - - private void dispatchNotificationRankingUpdate(RankingMap map) { - mAmDispatchingToOtherCode = true; - for (NotifCollectionListener listener : mNotifCollectionListeners) { - listener.onRankingUpdate(map); - } - mAmDispatchingToOtherCode = false; - } - - private void dispatchOnEntryRemoved(NotificationEntry entry, @CancellationReason int reason) { - mAmDispatchingToOtherCode = true; - for (NotifCollectionListener listener : mNotifCollectionListeners) { - listener.onEntryRemoved(entry, reason); - } - mAmDispatchingToOtherCode = false; - } - - private void dispatchOnEntryCleanUp(NotificationEntry entry) { - mAmDispatchingToOtherCode = true; - for (NotifCollectionListener listener : mNotifCollectionListeners) { - listener.onEntryCleanUp(entry); - } - mAmDispatchingToOtherCode = false; - } - @Override public void dump(@NonNull FileDescriptor fd, PrintWriter pw, @NonNull String[] args) { - final List entries = new ArrayList<>(getActiveNotifs()); + final List entries = new ArrayList<>(getAllNotifs()); pw.println("\t" + TAG + " unsorted/unfiltered notifications:"); if (entries.size() == 0) { @@ -754,6 +719,7 @@ public class NotifCollection implements Dumpable { private static final String TAG = "NotifCollection"; @IntDef(prefix = { "REASON_" }, value = { + REASON_NOT_CANCELED, REASON_UNKNOWN, REASON_CLICK, REASON_CANCEL_ALL, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java index 14903cd7bb27a..17899e99275bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java @@ -82,14 +82,14 @@ public class NotifPipeline implements CommonNotifCollection { } /** - * Returns the list of "active" notifications, i.e. the notifications that are currently posted + * Returns the list of all known notifications, i.e. the notifications that are currently posted * to the phone. In general, this tracks closely to the list maintained by NotificationManager, * but it can diverge slightly due to lifetime extenders. * * The returned collection is read-only, unsorted, unfiltered, and ungrouped. */ - public Collection getActiveNotifs() { - return mNotifCollection.getActiveNotifs(); + public Collection getAllNotifs() { + return mNotifCollection.getAllNotifs(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 3e9d8a436f38b..7019b5b42cf45 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -29,8 +29,6 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCRE import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; -import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; -import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING; @@ -44,10 +42,7 @@ import android.app.NotificationManager.Policy; import android.app.Person; import android.app.RemoteInputHistoryItem; import android.content.Context; -import android.content.pm.LauncherApps; -import android.content.pm.LauncherApps.ShortcutQuery; import android.content.pm.ShortcutInfo; -import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; @@ -55,25 +50,20 @@ import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; import android.util.ArraySet; -import android.util.Log; -import android.view.View; -import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.statusbar.InflationTask; -import com.android.systemui.statusbar.StatusBarIconView; -import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; +import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController; import com.android.systemui.statusbar.notification.row.NotificationGuts; @@ -81,8 +71,6 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; @@ -102,15 +90,11 @@ import java.util.Objects; * clean this up in the future. */ public final class NotificationEntry extends ListEntry { - private static final String TAG = "NotificationEntry"; private final String mKey; private StatusBarNotification mSbn; private Ranking mRanking; - private StatusBarIcon mSmallIcon; - private StatusBarIcon mPeopleAvatar; - /* * Bookkeeping members */ @@ -142,10 +126,7 @@ public final class NotificationEntry extends ListEntry { * TODO: Remove every member beneath this line if possible */ - public StatusBarIconView icon; - public StatusBarIconView expandedIcon; - public StatusBarIconView centeredIcon; - public StatusBarIconView aodIcon; + private IconPack mIcons = IconPack.buildEmptyPack(null); private boolean interruption; public int targetSdk; private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; @@ -191,7 +172,8 @@ public final class NotificationEntry extends ListEntry { private boolean hasSentReply; private boolean mSensitive = true; - private Runnable mOnSensitiveChangedListener; + private List mOnSensitivityChangedListeners = new ArrayList<>(); + private boolean mAutoHeadsUp; private boolean mPulseSupressed; private boolean mAllowFgsDismissal; @@ -347,6 +329,15 @@ public final class NotificationEntry extends ListEntry { * TODO: Remove as many of these as possible */ + @NonNull + public IconPack getIcons() { + return mIcons; + } + + public void setIcons(@NonNull IconPack icons) { + mIcons = icons; + } + public void setInterruption() { interruption = true; } @@ -464,239 +455,6 @@ public final class NotificationEntry extends ListEntry { || SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY; } - /** - * Create the icons for a notification - * @param context the context to create the icons with - * @param sbn the notification - * @throws InflationException Exception if required icons are not valid or specified - */ - public void createIcons(Context context, StatusBarNotification sbn) - throws InflationException { - StatusBarIcon ic = getIcon(context, sbn, false /* redact */); - - // Construct the icon. - icon = new StatusBarIconView(context, - sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); - icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - - // Construct the expanded icon. - expandedIcon = new StatusBarIconView(context, - sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); - expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - - // Construct the expanded icon. - aodIcon = new StatusBarIconView(context, - sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); - aodIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - aodIcon.setIncreasedSize(true); - - try { - setIcons(ic, Collections.singletonList(icon)); - if (isSensitive()) { - ic = getIcon(context, sbn, true /* redact */); - } - setIcons(ic, Arrays.asList(expandedIcon, aodIcon)); - } catch (InflationException e) { - icon = null; - expandedIcon = null; - centeredIcon = null; - aodIcon = null; - throw e; - } - - expandedIcon.setVisibility(View.INVISIBLE); - expandedIcon.setOnVisibilityChangedListener( - newVisibility -> { - if (row != null) { - row.setIconsVisible(newVisibility != View.VISIBLE); - } - }); - - // Construct the centered icon - if (mSbn.getNotification().isMediaNotification()) { - centeredIcon = new StatusBarIconView(context, - sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); - centeredIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - try { - setIcons(ic, Collections.singletonList(centeredIcon)); - } catch (InflationException e) { - centeredIcon = null; - throw e; - } - } - } - - /** - * Determines if this icon should be tinted based on the sensitivity of the icon, its context - * and the user's indicated sensitivity preference. - * - * @param ic The icon that should/should not be tinted. - * @return - */ - private boolean shouldTintIcon(StatusBarIconView ic) { - boolean usedInSensitiveContext = (ic == expandedIcon || ic == aodIcon); - return !isImportantConversation() || (usedInSensitiveContext && isSensitive()); - } - - - private void setIcons(StatusBarIcon ic, List icons) - throws InflationException { - for (StatusBarIconView icon: icons) { - if (icon == null) { - continue; - } - icon.setTintIcons(shouldTintIcon(icon)); - if (!icon.set(ic)) { - throw new InflationException("Couldn't create icon" + ic); - } - } - } - - private StatusBarIcon getIcon(Context context, StatusBarNotification sbn, boolean redact) - throws InflationException { - Notification n = sbn.getNotification(); - final boolean showPeopleAvatar = isImportantConversation() && !redact; - - // If cached, return corresponding cached values - if (showPeopleAvatar && mPeopleAvatar != null) { - return mPeopleAvatar; - } else if (!showPeopleAvatar && mSmallIcon != null) { - return mSmallIcon; - } - - Icon icon = showPeopleAvatar ? createPeopleAvatar(context) : n.getSmallIcon(); - if (icon == null) { - throw new InflationException("No icon in notification from " + sbn.getPackageName()); - } - - StatusBarIcon ic = new StatusBarIcon( - sbn.getUser(), - sbn.getPackageName(), - icon, - n.iconLevel, - n.number, - StatusBarIconView.contentDescForNotification(context, n)); - - // Cache if important conversation. - if (isImportantConversation()) { - if (showPeopleAvatar) { - mPeopleAvatar = ic; - } else { - mSmallIcon = ic; - } - } - return ic; - } - - private Icon createPeopleAvatar(Context context) throws InflationException { - // Attempt to extract form shortcut. - String conversationId = getChannel().getConversationId(); - ShortcutQuery query = new ShortcutQuery() - .setPackage(mSbn.getPackageName()) - .setQueryFlags(FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED) - .setShortcutIds(Collections.singletonList(conversationId)); - List shortcuts = context.getSystemService(LauncherApps.class) - .getShortcuts(query, mSbn.getUser()); - Icon ic = null; - if (shortcuts != null && !shortcuts.isEmpty()) { - ic = shortcuts.get(0).getIcon(); - } - - // Fall back to notification large icon if available - if (ic == null) { - ic = mSbn.getNotification().getLargeIcon(); - } - - // Fall back to extract from message - if (ic == null) { - Bundle extras = mSbn.getNotification().extras; - List messages = Message.getMessagesFromBundleArray( - extras.getParcelableArray(Notification.EXTRA_MESSAGES)); - Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON); - - for (int i = messages.size() - 1; i >= 0; i--) { - Message message = messages.get(i); - Person sender = message.getSenderPerson(); - if (sender != null && sender != user) { - ic = message.getSenderPerson().getIcon(); - break; - } - } - } - - // Revert to small icon if still not available - if (ic == null) { - ic = mSbn.getNotification().getSmallIcon(); - } - if (ic == null) { - throw new InflationException("No icon in notification from " + mSbn.getPackageName()); - } - return ic; - } - - private void updateSensitiveIconState() { - try { - StatusBarIcon ic = getIcon(getRow().getContext(), mSbn, isSensitive()); - setIcons(ic, Arrays.asList(expandedIcon, aodIcon)); - } catch (InflationException e) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Unable to update icon", e); - } - } - } - - public void setIconTag(int key, Object tag) { - if (icon != null) { - icon.setTag(key, tag); - expandedIcon.setTag(key, tag); - } - - if (centeredIcon != null) { - centeredIcon.setTag(key, tag); - } - - if (aodIcon != null) { - aodIcon.setTag(key, tag); - } - } - - /** - * Update the notification icons. - * - * @param context the context to create the icons with. - * @param sbn the notification to read the icon from. - * @throws InflationException Exception if required icons are not valid or specified - */ - public void updateIcons(Context context, StatusBarNotification sbn) - throws InflationException { - if (icon != null) { - // Update the icon - mSmallIcon = null; - mPeopleAvatar = null; - - StatusBarIcon ic = getIcon(context, sbn, false /* redact */); - - icon.setNotification(sbn); - expandedIcon.setNotification(sbn); - aodIcon.setNotification(sbn); - setIcons(ic, Arrays.asList(icon, expandedIcon)); - - if (isSensitive()) { - ic = getIcon(context, sbn, true /* redact */); - } - setIcons(ic, Collections.singletonList(aodIcon)); - - if (centeredIcon != null) { - centeredIcon.setNotification(sbn); - setIcons(ic, Collections.singletonList(centeredIcon)); - } - } - } - - private boolean isImportantConversation() { - return getChannel() != null && getChannel().isImportantConversation(); - } - public int getContrastedColor(Context context, boolean isLowPriority, int backgroundColor) { int rawColor = isLowPriority ? Notification.COLOR_DEFAULT : @@ -1125,9 +883,8 @@ public final class NotificationEntry extends ListEntry { getRow().setSensitive(sensitive, deviceSensitive); if (sensitive != mSensitive) { mSensitive = sensitive; - updateSensitiveIconState(); - if (mOnSensitiveChangedListener != null) { - mOnSensitiveChangedListener.run(); + for (int i = 0; i < mOnSensitivityChangedListeners.size(); i++) { + mOnSensitivityChangedListeners.get(i).onSensitivityChanged(this); } } } @@ -1136,8 +893,14 @@ public final class NotificationEntry extends ListEntry { return mSensitive; } - public void setOnSensitiveChangedListener(Runnable listener) { - mOnSensitiveChangedListener = listener; + /** Add a listener to be notified when the entry's sensitivity changes. */ + public void addOnSensitivityChangedListener(OnSensitivityChangedListener listener) { + mOnSensitivityChangedListeners.add(listener); + } + + /** Remove a listener that was registered above. */ + public void removeOnSensitivityChangedListener(OnSensitivityChangedListener listener) { + mOnSensitivityChangedListeners.remove(listener); } public boolean isPulseSuppressed() { @@ -1167,6 +930,12 @@ public final class NotificationEntry extends ListEntry { } } + /** Listener interface for {@link #addOnSensitivityChangedListener} */ + public interface OnSensitivityChangedListener { + /** Called when the sensitivity changes */ + void onSensitivityChanged(@NonNull NotificationEntry entry); + } + /** @see #getDismissState() */ public enum DismissState { /** User has not dismissed this notif or its parent */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java index 370de838b9039..92426e54ec91b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java @@ -126,7 +126,7 @@ public class BubbleCoordinator implements Coordinator { mInterceptedDismissalEntries.remove(entry.getKey()); mOnEndDismissInterception.onEndDismissInterception(mDismissInterceptor, entry, createDismissedByUserStats(entry)); - } else if (mNotifPipeline.getActiveNotifs().contains(entry)) { + } else if (mNotifPipeline.getAllNotifs().contains(entry)) { // Bubbles are hiding the notifications from the shade, but the bubble was // deleted; therefore, the notification should be cancelled as if it were a user // dismissal (this won't re-enter handleInterceptDimissal because Bubbles diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinator.java index 854444fc8bb7c..b5b756d6ed9b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinator.java @@ -240,7 +240,7 @@ public class ForegroundCoordinator implements Coordinator { } private NotificationEntry findNotificationEntryWithKey(String key) { - for (NotificationEntry entry : mNotifPipeline.getActiveNotifs()) { + for (NotificationEntry entry : mNotifPipeline.getAllNotifs()) { if (entry.getKey().equals(key)) { return entry; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index 4beeedecfdf50..7237284888261 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.collection.inflation; -import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; import android.annotation.Nullable; @@ -29,8 +28,6 @@ import android.util.Log; import android.view.ViewGroup; import com.android.internal.util.NotificationMessagingUtil; -import com.android.systemui.R; -import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; @@ -38,25 +35,22 @@ import com.android.systemui.statusbar.NotificationUiAdjustment; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.NotificationClicker; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.icon.IconManager; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController; import com.android.systemui.statusbar.notification.row.NotifBindPipeline; -import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder; import com.android.systemui.statusbar.notification.row.RowContentBindParams; import com.android.systemui.statusbar.notification.row.RowContentBindStage; import com.android.systemui.statusbar.notification.row.RowInflaterTask; import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; -import com.android.systemui.statusbar.phone.KeyguardBypassController; -import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBar; import java.util.Objects; import javax.inject.Inject; -import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; @@ -66,23 +60,23 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { private static final String TAG = "NotificationViewManager"; - private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; - private final Context mContext; - private final NotifBindPipeline mNotifBindPipeline; - private final RowContentBindStage mRowContentBindStage; private final NotificationMessagingUtil mMessagingUtil; private final NotificationRemoteInputManager mNotificationRemoteInputManager; private final NotificationLockscreenUserManager mNotificationLockscreenUserManager; + private final NotifBindPipeline mNotifBindPipeline; + private final RowContentBindStage mRowContentBindStage; + private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; + private final Provider mRowInflaterTaskProvider; + private final ExpandableNotificationRowComponent.Builder + mExpandableNotificationRowComponentBuilder; + private final IconManager mIconManager; private NotificationPresenter mPresenter; private NotificationListContainer mListContainer; private NotificationRowContentBinder.InflationCallback mInflationCallback; private BindRowCallback mBindRowCallback; private NotificationClicker mNotificationClicker; - private final Provider mRowInflaterTaskProvider; - private final ExpandableNotificationRowComponent.Builder - mExpandableNotificationRowComponentBuilder; @Inject public NotificationRowBinderImpl( @@ -92,14 +86,10 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { NotificationLockscreenUserManager notificationLockscreenUserManager, NotifBindPipeline notifBindPipeline, RowContentBindStage rowContentBindStage, - @Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowLongPress, - KeyguardBypassController keyguardBypassController, - StatusBarStateController statusBarStateController, - NotificationGroupManager notificationGroupManager, - NotificationGutsManager notificationGutsManager, NotificationInterruptStateProvider notificationInterruptionStateProvider, Provider rowInflaterTaskProvider, - ExpandableNotificationRowComponent.Builder expandableNotificationRowComponentBuilder) { + ExpandableNotificationRowComponent.Builder expandableNotificationRowComponentBuilder, + IconManager iconManager) { mContext = context; mNotifBindPipeline = notifBindPipeline; mRowContentBindStage = rowContentBindStage; @@ -109,6 +99,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { mNotificationInterruptStateProvider = notificationInterruptionStateProvider; mRowInflaterTaskProvider = rowInflaterTaskProvider; mExpandableNotificationRowComponentBuilder = expandableNotificationRowComponentBuilder; + mIconManager = iconManager; } /** @@ -120,6 +111,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { mPresenter = presenter; mListContainer = listContainer; mBindRowCallback = bindRowCallback; + + mIconManager.attach(); } public void setInflationCallback(NotificationRowContentBinder.InflationCallback callback) { @@ -142,12 +135,12 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { final StatusBarNotification sbn = entry.getSbn(); if (entry.rowExists()) { - entry.updateIcons(mContext, sbn); + mIconManager.updateIcons(entry); entry.reset(); updateNotification(entry, pmUser, sbn, entry.getRow()); entry.getRowController().setOnDismissRunnable(onDismissRunnable); } else { - entry.createIcons(mContext, sbn); + mIconManager.createIcons(entry); mRowInflaterTaskProvider.get().inflate(mContext, parent, entry, row -> { // Setup the controller for the view. @@ -227,8 +220,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD && entry.targetSdk < Build.VERSION_CODES.LOLLIPOP); - // TODO: should updates to the entry be happening somewhere else? - entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP); + // TODO: should this be happening somewhere else? + mIconManager.updateIconTags(entry, entry.targetSdk); row.setOnActivatedListener(mPresenter); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/CommonNotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/CommonNotifCollection.java index 171816fd28da6..b4c2bb8b6d3b1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/CommonNotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/CommonNotifCollection.java @@ -20,6 +20,8 @@ import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import java.util.Collection; + /** * A notification collection that manages the list of {@link NotificationEntry}s that will be * rendered. @@ -34,4 +36,13 @@ public interface CommonNotifCollection { * or deleted. */ void addCollectionListener(NotifCollectionListener listener); + + /** + * Returns the list of all known notifications, i.e. the notifications that are currently posted + * to the phone. In general, this tracks closely to the list maintained by NotificationManager, + * but it can diverge slightly due to lifetime extenders. + * + * The returned collection is read-only, unsorted, unfiltered, and ungrouped. + */ + Collection getAllNotifs(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java index b2c53dae16cdc..0c0cded32db27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java @@ -70,8 +70,24 @@ public interface NotifCollectionListener { } /** - * Called whenever the RankingMap is updated by system server. By the time this listener is - * called, the Rankings of all entries will have been updated. + * Called whenever a ranking update is applied. During a ranking update, all active, + * non-lifetime-extended notification entries will have their ranking object updated. + * + * Ranking updates occur whenever a notification is added, updated, or removed, or when a + * standalone ranking is sent from the server. + */ + default void onRankingApplied() { + } + + /** + * Called whenever system server sends a standalone ranking update (i.e. one that isn't + * associated with a notification being added or removed). + * + * In general it is unsafe to depend on this method as rankings can change for other reasons. + * Instead, listen for {@link #onRankingApplied()}, which is called whenever ANY ranking update + * is applied, regardless of source. + * + * @deprecated Use {@link #onRankingApplied()} instead. */ default void onRankingUpdate(NotificationListenerService.RankingMap rankingMap) { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt new file mode 100644 index 0000000000000..2ef0368061ba1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2020 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.statusbar.notification.collection.notifcollection + +import android.service.notification.NotificationListenerService.RankingMap +import com.android.systemui.statusbar.notification.collection.NotifCollection +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** + * Set of classes that represent the various events that [NotifCollection] can dispatch to + * [NotifCollectionListener]s. + * + * These events build up in a queue and are periodically emitted in chunks by the collection. + */ + +sealed class NotifEvent { + fun dispatchTo(listeners: List) { + for (i in listeners.indices) { + dispatchToListener(listeners[i]) + } + } + + abstract fun dispatchToListener(listener: NotifCollectionListener) +} + +data class InitEntryEvent( + val entry: NotificationEntry +) : NotifEvent() { + override fun dispatchToListener(listener: NotifCollectionListener) { + listener.onEntryInit(entry) + } +} + +data class EntryAddedEvent( + val entry: NotificationEntry +) : NotifEvent() { + override fun dispatchToListener(listener: NotifCollectionListener) { + listener.onEntryAdded(entry) + } +} + +data class EntryUpdatedEvent( + val entry: NotificationEntry +) : NotifEvent() { + override fun dispatchToListener(listener: NotifCollectionListener) { + listener.onEntryUpdated(entry) + } +} + +data class EntryRemovedEvent( + val entry: NotificationEntry, + val reason: Int +) : NotifEvent() { + override fun dispatchToListener(listener: NotifCollectionListener) { + listener.onEntryRemoved(entry, reason) + } +} + +data class CleanUpEntryEvent( + val entry: NotificationEntry +) : NotifEvent() { + override fun dispatchToListener(listener: NotifCollectionListener) { + listener.onEntryCleanUp(entry) + } +} + +data class RankingUpdatedEvent( + val rankingMap: RankingMap +) : NotifEvent() { + override fun dispatchToListener(listener: NotifCollectionListener) { + listener.onRankingUpdate(rankingMap) + } +} + +class RankingAppliedEvent() : NotifEvent() { + override fun dispatchToListener(listener: NotifCollectionListener) { + listener.onRankingApplied() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt new file mode 100644 index 0000000000000..afc123faba798 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 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.statusbar.notification.icon + +import android.app.Notification +import android.content.Context +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import javax.inject.Inject + +/** + * Testable wrapper around Context. + */ +class IconBuilder @Inject constructor( + private val context: Context +) { + fun createIconView(entry: NotificationEntry): StatusBarIconView { + return StatusBarIconView( + context, + "${entry.sbn.packageName}/0x${Integer.toHexString(entry.sbn.id)}", + entry.sbn) + } + + fun getIconContentDescription(n: Notification): CharSequence { + return StatusBarIconView.contentDescForNotification(context, n) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt new file mode 100644 index 0000000000000..bb0fcafdb354b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2020 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.statusbar.notification.icon + +import android.app.Notification +import android.app.Person +import android.content.pm.LauncherApps +import android.graphics.drawable.Icon +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.ImageView +import com.android.internal.statusbar.StatusBarIcon +import com.android.systemui.R +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.notification.InflationException +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import javax.inject.Inject + +/** + * Inflates and updates icons associated with notifications + * + * Notifications are represented by icons in a few different places -- in the status bar, in the + * notification shelf, in AOD, etc. This class is in charge of inflating the views that hold these + * icons and keeping the icon assets themselves up to date as notifications change. + * + * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry. + * Long-term, it should probably live somewhere in the content inflation pipeline. + */ +class IconManager @Inject constructor( + private val notifCollection: CommonNotifCollection, + private val launcherApps: LauncherApps, + private val iconBuilder: IconBuilder +) { + fun attach() { + notifCollection.addCollectionListener(entryListener) + } + + private val entryListener = object : NotifCollectionListener { + override fun onEntryInit(entry: NotificationEntry) { + entry.addOnSensitivityChangedListener(sensitivityListener) + } + + override fun onEntryCleanUp(entry: NotificationEntry) { + entry.removeOnSensitivityChangedListener(sensitivityListener) + } + + override fun onRankingApplied() { + // When the sensitivity changes OR when the isImportantConversation status changes, + // we need to update the icons + for (entry in notifCollection.allNotifs) { + val isImportant = isImportantConversation(entry) + if (entry.icons.areIconsAvailable && + isImportant != entry.icons.isImportantConversation) { + updateIconsSafe(entry) + } + entry.icons.isImportantConversation = isImportant + } + } + } + + private val sensitivityListener = NotificationEntry.OnSensitivityChangedListener { + entry -> updateIconsSafe(entry) + } + + /** + * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the + * result in [NotificationEntry.getIcons]. + * + * @throws InflationException Exception if required icons are not valid or specified + */ + @Throws(InflationException::class) + fun createIcons(entry: NotificationEntry) { + // Construct the status bar icon view. + val sbIcon = iconBuilder.createIconView(entry) + sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE + + // Construct the shelf icon view. + val shelfIcon = iconBuilder.createIconView(entry) + shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE + + shelfIcon.visibility = View.INVISIBLE + // TODO: This doesn't belong here + shelfIcon.setOnVisibilityChangedListener { newVisibility: Int -> + if (entry.row != null) { + entry.row.setIconsVisible(newVisibility != View.VISIBLE) + } + } + + // Construct the aod icon view. + val aodIcon = iconBuilder.createIconView(entry) + aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE + aodIcon.setIncreasedSize(true) + + // Construct the centered icon view. + val centeredIcon = if (entry.sbn.notification.isMediaNotification) { + iconBuilder.createIconView(entry).apply { + scaleType = ImageView.ScaleType.CENTER_INSIDE + } + } else { + null + } + + // Set the icon views' icons + val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) + + try { + setIcon(entry, normalIconDescriptor, sbIcon) + setIcon(entry, sensitiveIconDescriptor, shelfIcon) + setIcon(entry, sensitiveIconDescriptor, aodIcon) + if (centeredIcon != null) { + setIcon(entry, normalIconDescriptor, centeredIcon) + } + entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, centeredIcon, entry.icons) + } catch (e: InflationException) { + entry.icons = IconPack.buildEmptyPack(entry.icons) + throw e + } + } + + /** + * Update the notification icons. + * + * @param entry the notification to read the icon from. + * @throws InflationException Exception if required icons are not valid or specified + */ + @Throws(InflationException::class) + fun updateIcons(entry: NotificationEntry) { + if (!entry.icons.areIconsAvailable) { + return + } + entry.icons.smallIconDescriptor = null + entry.icons.peopleAvatarDescriptor = null + + val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) + + entry.icons.statusBarIcon?.let { + it.notification = entry.sbn + setIcon(entry, normalIconDescriptor, it) + } + + entry.icons.shelfIcon?.let { + it.notification = entry.sbn + setIcon(entry, normalIconDescriptor, it) + } + + entry.icons.aodIcon?.let { + it.notification = entry.sbn + setIcon(entry, sensitiveIconDescriptor, it) + } + + entry.icons.centeredIcon?.let { + it.notification = entry.sbn + setIcon(entry, sensitiveIconDescriptor, it) + } + } + + /** + * Updates tags on the icon views to match the posting app's target SDK level + * + * Note that this method MUST be called after both [createIcons] and [updateIcons]. + */ + fun updateIconTags(entry: NotificationEntry, targetSdk: Int) { + setTagOnIconViews( + entry.icons, + R.id.icon_is_pre_L, + targetSdk < Build.VERSION_CODES.LOLLIPOP) + } + + private fun updateIconsSafe(entry: NotificationEntry) { + try { + updateIcons(entry) + } catch (e: InflationException) { + // TODO This should mark the entire row as involved in an inflation error + Log.e(TAG, "Unable to update icon", e) + } + } + + @Throws(InflationException::class) + private fun getIconDescriptors( + entry: NotificationEntry + ): Pair { + val iconDescriptor = getIconDescriptor(entry, false /* redact */) + val sensitiveDescriptor = if (entry.isSensitive) { + getIconDescriptor(entry, true /* redact */) + } else { + iconDescriptor + } + return Pair(iconDescriptor, sensitiveDescriptor) + } + + @Throws(InflationException::class) + private fun getIconDescriptor( + entry: NotificationEntry, + redact: Boolean + ): StatusBarIcon { + val n = entry.sbn.notification + val showPeopleAvatar = isImportantConversation(entry) && !redact + + val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor + val smallIconDescriptor = entry.icons.smallIconDescriptor + + // If cached, return corresponding cached values + if (showPeopleAvatar && peopleAvatarDescriptor != null) { + return peopleAvatarDescriptor + } else if (!showPeopleAvatar && smallIconDescriptor != null) { + return smallIconDescriptor + } + + val icon = + (if (showPeopleAvatar) { + createPeopleAvatar(entry) + } else { + n.smallIcon + }) ?: throw InflationException( + "No icon in notification from " + entry.sbn.packageName) + + val ic = StatusBarIcon( + entry.sbn.user, + entry.sbn.packageName, + icon, + n.iconLevel, + n.number, + iconBuilder.getIconContentDescription(n)) + + // Cache if important conversation. + if (isImportantConversation(entry)) { + if (showPeopleAvatar) { + entry.icons.peopleAvatarDescriptor = ic + } else { + entry.icons.smallIconDescriptor = ic + } + } + + return ic + } + + @Throws(InflationException::class) + private fun setIcon( + entry: NotificationEntry, + iconDescriptor: StatusBarIcon, + iconView: StatusBarIconView + ) { + iconView.setTintIcons(shouldTintIconView(entry, iconView)) + if (!iconView.set(iconDescriptor)) { + throw InflationException("Couldn't create icon $iconDescriptor") + } + } + + @Throws(InflationException::class) + private fun createPeopleAvatar(entry: NotificationEntry): Icon? { + // Attempt to extract form shortcut. + val conversationId = entry.ranking.channel.conversationId + val query = LauncherApps.ShortcutQuery() + .setPackage(entry.sbn.packageName) + .setQueryFlags( + LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC + or LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) + .setShortcutIds(listOf(conversationId)) + val shortcuts = launcherApps.getShortcuts(query, entry.sbn.user) + var ic: Icon? = null + if (shortcuts != null && shortcuts.isNotEmpty()) { + ic = shortcuts[0].icon + } + + // Fall back to notification large icon if available + if (ic == null) { + ic = entry.sbn.notification.getLargeIcon() + } + + // Fall back to extract from message + if (ic == null) { + val extras: Bundle = entry.sbn.notification.extras + val messages = Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_MESSAGES)) + val user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON) + for (i in messages.indices.reversed()) { + val message = messages[i] + val sender = message.senderPerson + if (sender != null && sender !== user) { + ic = message.senderPerson!!.icon + break + } + } + } + + // Revert to small icon if still not available + if (ic == null) { + ic = entry.sbn.notification.smallIcon + } + if (ic == null) { + throw InflationException("No icon in notification from " + entry.sbn.packageName) + } + return ic + } + + /** + * Determines if this icon should be tinted based on the sensitivity of the icon, its context + * and the user's indicated sensitivity preference. + * + * @param iconView The icon that should/should not be tinted. + */ + private fun shouldTintIconView(entry: NotificationEntry, iconView: StatusBarIconView): Boolean { + val usedInSensitiveContext = + iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon + return !isImportantConversation(entry) || usedInSensitiveContext && entry.isSensitive + } + + private fun isImportantConversation(entry: NotificationEntry): Boolean { + return entry.ranking.channel != null && entry.ranking.channel.isImportantConversation + } + + private fun setTagOnIconViews(icons: IconPack, key: Int, tag: Any) { + icons.statusBarIcon?.setTag(key, tag) + icons.shelfIcon?.setTag(key, tag) + icons.aodIcon?.setTag(key, tag) + icons.centeredIcon?.setTag(key, tag) + } +} + +private const val TAG = "IconManager" \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java new file mode 100644 index 0000000000000..054e381f096a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2020 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.statusbar.notification.icon; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.statusbar.StatusBarIcon; +import com.android.systemui.statusbar.StatusBarIconView; + +/** + * Data class for storing icons associated with a notification + */ +public final class IconPack { + + private final boolean mAreIconsAvailable; + @Nullable private final StatusBarIconView mStatusBarIcon; + @Nullable private final StatusBarIconView mShelfIcon; + @Nullable private final StatusBarIconView mAodIcon; + @Nullable private final StatusBarIconView mCenteredIcon; + + @Nullable private StatusBarIcon mSmallIconDescriptor; + @Nullable private StatusBarIcon mPeopleAvatarDescriptor; + + private boolean mIsImportantConversation; + + /** + * Builds an empty instance of IconPack that doesn't have any icons (because either they + * haven't been inflated yet or there was an error while inflating them). + */ + public static IconPack buildEmptyPack(@Nullable IconPack fromSource) { + return new IconPack(false, null, null, null, null, fromSource); + } + + /** + * Builds an instance of an IconPack that contains successfully-inflated icons + */ + public static IconPack buildPack( + @NonNull StatusBarIconView statusBarIcon, + @NonNull StatusBarIconView shelfIcon, + @NonNull StatusBarIconView aodIcon, + @Nullable StatusBarIconView centeredIcon, + @Nullable IconPack source) { + return new IconPack(true, statusBarIcon, shelfIcon, aodIcon, centeredIcon, source); + } + + private IconPack( + boolean areIconsAvailable, + @Nullable StatusBarIconView statusBarIcon, + @Nullable StatusBarIconView shelfIcon, + @Nullable StatusBarIconView aodIcon, + @Nullable StatusBarIconView centeredIcon, + @Nullable IconPack source) { + mAreIconsAvailable = areIconsAvailable; + mStatusBarIcon = statusBarIcon; + mShelfIcon = shelfIcon; + mCenteredIcon = centeredIcon; + mAodIcon = aodIcon; + if (source != null) { + mIsImportantConversation = source.mIsImportantConversation; + } + } + + /** The version of the notification icon that appears in the status bar. */ + @Nullable + public StatusBarIconView getStatusBarIcon() { + return mStatusBarIcon; + } + + /** + * The version of the icon that appears in the "shelf" at the bottom of the notification shade. + * In general, this icon also appears somewhere on the notification and is "sucked" into the + * shelf as the scrolls beyond it. + */ + @Nullable + public StatusBarIconView getShelfIcon() { + return mShelfIcon; + } + + @Nullable + public StatusBarIconView getCenteredIcon() { + return mCenteredIcon; + } + + /** The version of the icon that's shown when pulsing (in AOD). */ + @Nullable + public StatusBarIconView getAodIcon() { + return mAodIcon; + } + + @Nullable + StatusBarIcon getSmallIconDescriptor() { + return mSmallIconDescriptor; + } + + void setSmallIconDescriptor(@Nullable StatusBarIcon smallIconDescriptor) { + mSmallIconDescriptor = smallIconDescriptor; + } + + @Nullable + StatusBarIcon getPeopleAvatarDescriptor() { + return mPeopleAvatarDescriptor; + } + + void setPeopleAvatarDescriptor(@Nullable StatusBarIcon peopleAvatarDescriptor) { + mPeopleAvatarDescriptor = peopleAvatarDescriptor; + } + + boolean isImportantConversation() { + return mIsImportantConversation; + } + + void setImportantConversation(boolean importantConversation) { + mIsImportantConversation = importantConversation; + } + + public boolean getAreIconsAvailable() { + return mAreIconsAvailable; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/DungeonRow.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/DungeonRow.kt index 373457d4e3366..dbfa27f1f68ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/DungeonRow.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/DungeonRow.kt @@ -37,7 +37,7 @@ class DungeonRow(context: Context, attrs: AttributeSet) : LinearLayout(context, } (findViewById(R.id.icon) as StatusBarIconView).apply { - set(entry?.icon?.statusBarIcon) + set(entry?.icons?.statusBarIcon?.statusBarIcon) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index f61fe98309397..b1db5b5b4312f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -579,7 +579,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @VisibleForTesting void updateShelfIconColor() { - StatusBarIconView expandedIcon = mEntry.expandedIcon; + StatusBarIconView expandedIcon = mEntry.getIcons().getShelfIcon(); boolean isPreL = Boolean.TRUE.equals(expandedIcon.getTag(R.id.icon_is_pre_L)); boolean colorize = !isPreL || NotificationUtils.isGrayscale(expandedIcon, ContrastColorUtil.getInstance(mContext)); @@ -1290,7 +1290,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView setLongPressListener(null); mGroupParentWhenDismissed = mNotificationParent; mChildAfterViewWhenDismissed = null; - mEntry.icon.setDismissed(); + mEntry.getIcons().getStatusBarIcon().setDismissed(); if (isChildInGroup()) { List notificationChildren = mNotificationParent.getNotificationChildren(); @@ -1832,7 +1832,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mTranslateableViews.get(i).setTranslationX(0); } invalidateOutline(); - getEntry().expandedIcon.setScrollX(0); + getEntry().getIcons().getShelfIcon().setScrollX(0); } if (mMenuRow != null) { @@ -1912,7 +1912,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView // In order to keep the shelf in sync with this swiping, we're simply translating // it's icon by the same amount. The translation is already being used for the normal // positioning, so we can use the scrollX instead. - getEntry().expandedIcon.setScrollX((int) -translationX); + getEntry().getIcons().getShelfIcon().setScrollX((int) -translationX); } if (mMenuRow != null && mMenuRow.getMenuView() != null) { @@ -2111,7 +2111,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public StatusBarIconView getShelfIcon() { - return getEntry().expandedIcon; + return getEntry().getIcons().getShelfIcon(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index 0996ff27e9a33..14442e346db49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -453,9 +453,10 @@ public class StackStateAnimator { needsAnimation = false; } NotificationEntry entry = row.getEntry(); - StatusBarIconView icon = entry.icon; - if (entry.centeredIcon != null && entry.centeredIcon.getParent() != null) { - icon = entry.centeredIcon; + StatusBarIconView icon = entry.getIcons().getStatusBarIcon(); + final StatusBarIconView centeredIcon = entry.getIcons().getCenteredIcon(); + if (centeredIcon != null && centeredIcon.getParent() != null) { + icon = centeredIcon; } if (icon.getParent() != null) { icon.getLocationOnScreen(mTmpLocation); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java index c39ee3a902308..51c02c9f93ab2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java @@ -269,7 +269,7 @@ public class HeadsUpAppearanceController implements OnHeadsUpChangedListener, } updateIsolatedIconLocation(false /* requireUpdate */); mNotificationIconAreaController.showIconIsolated(newEntry == null ? null - : newEntry.icon, animateIsolation); + : newEntry.getIcons().getStatusBarIcon(), animateIsolation); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java index b09ccffdc4dec..f58cce58af74d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java @@ -253,8 +253,8 @@ public class NotificationIconAreaController implements DarkReceiver, boolean hidePulsing, boolean onlyShowCenteredIcon) { final boolean isCenteredNotificationIcon = mCenteredIconView != null - && entry.centeredIcon != null - && Objects.equals(entry.centeredIcon, mCenteredIconView); + && entry.getIcons().getCenteredIcon() != null + && Objects.equals(entry.getIcons().getCenteredIcon(), mCenteredIconView); if (onlyShowCenteredIcon) { return isCenteredNotificationIcon; } @@ -307,7 +307,7 @@ public class NotificationIconAreaController implements DarkReceiver, } private void updateShelfIcons() { - updateIconsForLayout(entry -> entry.expandedIcon, mShelfIcons, + updateIconsForLayout(entry -> entry.getIcons().getShelfIcon(), mShelfIcons, true /* showAmbient */, true /* showLowPriority */, false /* hideDismissed */, @@ -319,7 +319,7 @@ public class NotificationIconAreaController implements DarkReceiver, } public void updateStatusBarIcons() { - updateIconsForLayout(entry -> entry.icon, mNotificationIcons, + updateIconsForLayout(entry -> entry.getIcons().getStatusBarIcon(), mNotificationIcons, false /* showAmbient */, mShowLowPriority, true /* hideDismissed */, @@ -331,7 +331,7 @@ public class NotificationIconAreaController implements DarkReceiver, } private void updateCenterIcon() { - updateIconsForLayout(entry -> entry.centeredIcon, mCenteredIcon, + updateIconsForLayout(entry -> entry.getIcons().getCenteredIcon(), mCenteredIcon, false /* showAmbient */, true /* showLowPriority */, false /* hideDismissed */, @@ -343,7 +343,7 @@ public class NotificationIconAreaController implements DarkReceiver, } public void updateAodNotificationIcons() { - updateIconsForLayout(entry -> entry.aodIcon, mAodIcons, + updateIconsForLayout(entry -> entry.getIcons().getAodIcon(), mAodIcons, false /* showAmbient */, true /* showLowPriority */, true /* hideDismissed */, @@ -517,7 +517,7 @@ public class NotificationIconAreaController implements DarkReceiver, * Shows the icon view given in the center. */ public void showIconCentered(NotificationEntry entry) { - StatusBarIconView icon = entry == null ? null : entry.centeredIcon; + StatusBarIconView icon = entry == null ? null : entry.getIcons().getCenteredIcon(); if (!Objects.equals(mCenteredIconView, icon)) { mCenteredIconView = icon; updateNotificationIcons(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index b7184be7d567f..82de4a3b490c8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -34,16 +34,17 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import android.annotation.Nullable; @@ -83,13 +84,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -123,6 +124,8 @@ public class NotifCollectionTest extends SysuiTestCase { private NotifCollection mCollection; private BatchableNotificationHandler mNotifHandler; + private InOrder mListenerInOrder; + private NoManSimulator mNoMan; @Before @@ -133,6 +136,8 @@ public class NotifCollectionTest extends SysuiTestCase { when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(true); when(mFeatureFlags.isNewNotifPipelineEnabled()).thenReturn(true); + mListenerInOrder = inOrder(mCollectionListener); + mCollection = new NotifCollection( mStatusBarService, mock(DumpManager.class), @@ -159,10 +164,12 @@ public class NotifCollectionTest extends SysuiTestCase { .setRank(4747)); // THEN the listener is notified - verify(mCollectionListener).onEntryInit(mEntryCaptor.capture()); - NotificationEntry entry = mEntryCaptor.getValue(); + final NotificationEntry entry = mCollectionListener.getEntry(notif1.key); + + mListenerInOrder.verify(mCollectionListener).onEntryInit(entry); + mListenerInOrder.verify(mCollectionListener).onEntryAdded(entry); + mListenerInOrder.verify(mCollectionListener).onRankingApplied(); - verify(mCollectionListener).onEntryAdded(entry); assertEquals(notif1.key, entry.getKey()); assertEquals(notif1.sbn, entry.getSbn()); assertEquals(notif1.ranking, entry.getRanking()); @@ -215,12 +222,11 @@ public class NotifCollectionTest extends SysuiTestCase { assertEquals(entry2.getRanking(), capturedUpdate.getRanking()); // THEN onBuildList is called only once - verify(mBuildListener).onBuildList(mBuildListCaptor.capture()); - assertEquals(new ArraySet<>(Arrays.asList( - capturedAdds.get(0), - capturedAdds.get(1), - capturedUpdate - )), new ArraySet<>(mBuildListCaptor.getValue())); + verifyBuiltList( + List.of( + capturedAdds.get(0), + capturedAdds.get(1), + capturedUpdate)); } @Test @@ -234,9 +240,11 @@ public class NotifCollectionTest extends SysuiTestCase { .setRank(89)); // THEN the listener is notified - verify(mCollectionListener).onEntryUpdated(mEntryCaptor.capture()); + final NotificationEntry entry = mCollectionListener.getEntry(notif2.key); + + mListenerInOrder.verify(mCollectionListener).onEntryUpdated(entry); + mListenerInOrder.verify(mCollectionListener).onRankingApplied(); - NotificationEntry entry = mEntryCaptor.getValue(); assertEquals(notif2.key, entry.getKey()); assertEquals(notif2.sbn, entry.getSbn()); assertEquals(notif2.ranking, entry.getRanking()); @@ -256,8 +264,10 @@ public class NotifCollectionTest extends SysuiTestCase { mNoMan.retractNotif(notif.sbn, REASON_APP_CANCEL); // THEN the listener is notified - verify(mCollectionListener).onEntryRemoved(entry, REASON_APP_CANCEL); - verify(mCollectionListener).onEntryCleanUp(entry); + mListenerInOrder.verify(mCollectionListener).onEntryRemoved(entry, REASON_APP_CANCEL); + mListenerInOrder.verify(mCollectionListener).onEntryCleanUp(entry); + mListenerInOrder.verify(mCollectionListener).onRankingApplied(); + assertEquals(notif.sbn, entry.getSbn()); assertEquals(notif.ranking, entry.getRanking()); } @@ -415,8 +425,8 @@ public class NotifCollectionTest extends SysuiTestCase { // THEN the dismissed entry still appears in the notification set assertEquals( - new ArraySet<>(Collections.singletonList(entry1)), - new ArraySet<>(mCollection.getActiveNotifs())); + new ArraySet<>(singletonList(entry1)), + new ArraySet<>(mCollection.getAllNotifs())); } @Test @@ -444,7 +454,7 @@ public class NotifCollectionTest extends SysuiTestCase { mNoMan.retractNotif(notif2.sbn, REASON_CANCEL); assertEquals( new ArraySet<>(List.of(entry1, entry2, entry3)), - new ArraySet<>(mCollection.getActiveNotifs())); + new ArraySet<>(mCollection.getAllNotifs())); // WHEN the summary is dismissed by the user mCollection.dismissNotification(entry1, defaultStats(entry1)); @@ -452,7 +462,7 @@ public class NotifCollectionTest extends SysuiTestCase { // THEN the summary is removed, but both children stick around assertEquals( new ArraySet<>(List.of(entry2, entry3)), - new ArraySet<>(mCollection.getActiveNotifs())); + new ArraySet<>(mCollection.getAllNotifs())); assertEquals(NOT_DISMISSED, entry2.getDismissState()); assertEquals(NOT_DISMISSED, entry3.getDismissState()); } @@ -561,7 +571,7 @@ public class NotifCollectionTest extends SysuiTestCase { } @Test - public void testEndDismissInterceptionUpdatesDismissInterceptors() throws RemoteException { + public void testEndDismissInterceptionUpdatesDismissInterceptors() { // GIVEN a collection with notifications with multiple dismiss interceptors mInterceptor1.shouldInterceptDismissal = true; mInterceptor2.shouldInterceptDismissal = true; @@ -592,7 +602,7 @@ public class NotifCollectionTest extends SysuiTestCase { @Test(expected = IllegalStateException.class) - public void testEndingDismissalOfNonInterceptedThrows() throws RemoteException { + public void testEndingDismissalOfNonInterceptedThrows() { // GIVEN a collection with notifications with a dismiss interceptor that hasn't been called mInterceptor1.shouldInterceptDismissal = false; mCollection.addNotificationDismissInterceptor(mInterceptor1); @@ -820,7 +830,7 @@ public class NotifCollectionTest extends SysuiTestCase { verify(mExtender3).shouldExtendLifetime(entry2, REASON_CLICK); // THEN the entry is not removed - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); // THEN the entry properly records all extenders that returned true assertEquals(Arrays.asList(mExtender1, mExtender2), entry2.mLifetimeExtenders); @@ -841,7 +851,7 @@ public class NotifCollectionTest extends SysuiTestCase { // GIVEN a notification gets lifetime-extended by one of them mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL); - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); clearInvocations(mExtender1, mExtender2, mExtender3); // WHEN the last active extender expires (but new ones become active) @@ -856,7 +866,7 @@ public class NotifCollectionTest extends SysuiTestCase { verify(mExtender3).shouldExtendLifetime(entry2, REASON_APP_CANCEL); // THEN the entry is not removed - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); // THEN the entry properly records all extenders that returned true assertEquals(Arrays.asList(mExtender1, mExtender3), entry2.mLifetimeExtenders); @@ -878,7 +888,7 @@ public class NotifCollectionTest extends SysuiTestCase { // GIVEN a notification gets lifetime-extended by a couple of them mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL); - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); clearInvocations(mExtender1, mExtender2, mExtender3); // WHEN one (but not all) of the extenders expires @@ -886,7 +896,7 @@ public class NotifCollectionTest extends SysuiTestCase { mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2); // THEN the entry is not removed - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); // THEN we don't re-query the extenders verify(mExtender1, never()).shouldExtendLifetime(entry2, REASON_APP_CANCEL); @@ -894,7 +904,7 @@ public class NotifCollectionTest extends SysuiTestCase { verify(mExtender3, never()).shouldExtendLifetime(entry2, REASON_APP_CANCEL); // THEN the entry properly records all extenders that returned true - assertEquals(Arrays.asList(mExtender1), entry2.mLifetimeExtenders); + assertEquals(singletonList(mExtender1), entry2.mLifetimeExtenders); } @Test @@ -913,7 +923,7 @@ public class NotifCollectionTest extends SysuiTestCase { // GIVEN a notification gets lifetime-extended by a couple of them mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN); - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); clearInvocations(mExtender1, mExtender2, mExtender3); // WHEN all of the active extenders expire @@ -923,7 +933,7 @@ public class NotifCollectionTest extends SysuiTestCase { mExtender1.callback.onEndLifetimeExtension(mExtender1, entry2); // THEN the entry removed - assertFalse(mCollection.getActiveNotifs().contains(entry2)); + assertFalse(mCollection.getAllNotifs().contains(entry2)); verify(mCollectionListener).onEntryRemoved(entry2, REASON_UNKNOWN); } @@ -943,7 +953,7 @@ public class NotifCollectionTest extends SysuiTestCase { // GIVEN a notification gets lifetime-extended by a couple of them mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN); - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); clearInvocations(mExtender1, mExtender2, mExtender3); // WHEN the notification is reposted @@ -954,7 +964,7 @@ public class NotifCollectionTest extends SysuiTestCase { verify(mExtender2).cancelLifetimeExtension(entry2); // THEN the notification is still present - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); } @Test(expected = IllegalStateException.class) @@ -973,7 +983,7 @@ public class NotifCollectionTest extends SysuiTestCase { // GIVEN a notification gets lifetime-extended by a couple of them mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN); - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); clearInvocations(mExtender1, mExtender2, mExtender3); // WHEN a lifetime extender makes a reentrant call during cancelLifetimeExtension() @@ -1002,7 +1012,7 @@ public class NotifCollectionTest extends SysuiTestCase { // GIVEN a notification gets lifetime-extended by a couple of them mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN); - assertTrue(mCollection.getActiveNotifs().contains(entry2)); + assertTrue(mCollection.getAllNotifs().contains(entry2)); clearInvocations(mExtender1, mExtender2, mExtender3); // WHEN the notification is reposted @@ -1055,11 +1065,11 @@ public class NotifCollectionTest extends SysuiTestCase { // WHEN both notifications are manually dismissed together mCollection.dismissNotifications( - List.of(new Pair(entry1, defaultStats(entry1)), - new Pair(entry2, defaultStats(entry2)))); + List.of(new Pair<>(entry1, defaultStats(entry1)), + new Pair<>(entry2, defaultStats(entry2)))); // THEN build list is only called one time - verify(mBuildListener).onBuildList(any(Collection.class)); + verifyBuiltList(List.of(entry1, entry2)); } @Test @@ -1074,8 +1084,8 @@ public class NotifCollectionTest extends SysuiTestCase { DismissedByUserStats stats1 = defaultStats(entry1); DismissedByUserStats stats2 = defaultStats(entry2); mCollection.dismissNotifications( - List.of(new Pair(entry1, defaultStats(entry1)), - new Pair(entry2, defaultStats(entry2)))); + List.of(new Pair<>(entry1, defaultStats(entry1)), + new Pair<>(entry2, defaultStats(entry2)))); // THEN we send the dismissals to system server verify(mStatusBarService).onNotificationClear( @@ -1109,8 +1119,8 @@ public class NotifCollectionTest extends SysuiTestCase { // WHEN both notifications are manually dismissed together mCollection.dismissNotifications( - List.of(new Pair(entry1, defaultStats(entry1)), - new Pair(entry2, defaultStats(entry2)))); + List.of(new Pair<>(entry1, defaultStats(entry1)), + new Pair<>(entry2, defaultStats(entry2)))); // THEN the entries are marked as dismissed assertEquals(DISMISSED, entry1.getDismissState()); @@ -1134,8 +1144,8 @@ public class NotifCollectionTest extends SysuiTestCase { // WHEN both notifications are manually dismissed together mCollection.dismissNotifications( - List.of(new Pair(entry1, defaultStats(entry1)), - new Pair(entry2, defaultStats(entry2)))); + List.of(new Pair<>(entry1, defaultStats(entry1)), + new Pair<>(entry2, defaultStats(entry2)))); // THEN all interceptors get checked verify(mInterceptor1).shouldInterceptDismissal(entry1); @@ -1162,7 +1172,7 @@ public class NotifCollectionTest extends SysuiTestCase { mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier()); // THEN build list is only called one time - verify(mBuildListener).onBuildList(any(Collection.class)); + verifyBuiltList(List.of(entry1, entry2)); } @Test @@ -1271,13 +1281,18 @@ public class NotifCollectionTest extends SysuiTestCase { NotificationVisibility.obtain(entry.getKey(), 7, 2, true)); } - public CollectionEvent postNotif(NotificationEntryBuilder builder) { + private CollectionEvent postNotif(NotificationEntryBuilder builder) { clearInvocations(mCollectionListener); NotifEvent rawEvent = mNoMan.postNotif(builder); verify(mCollectionListener).onEntryAdded(mEntryCaptor.capture()); return new CollectionEvent(rawEvent, requireNonNull(mEntryCaptor.getValue())); } + private void verifyBuiltList(Collection list) { + verify(mBuildListener).onBuildList(mBuildListCaptor.capture()); + assertEquals(new ArraySet<>(list), new ArraySet<>(mBuildListCaptor.getValue())); + } + private static class RecordingCollectionListener implements NotifCollectionListener { private final Map mLastSeenEntries = new ArrayMap<>(); @@ -1303,6 +1318,14 @@ public class NotifCollectionTest extends SysuiTestCase { public void onEntryCleanUp(NotificationEntry entry) { } + @Override + public void onRankingApplied() { + } + + @Override + public void onRankingUpdate(RankingMap rankingMap) { + } + public NotificationEntry getEntry(String key) { if (!mLastSeenEntries.containsKey(key)) { throw new RuntimeException("Key not found: " + key); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinatorTest.java index 67b1aad94f518..407e1e671f861 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinatorTest.java @@ -223,7 +223,7 @@ public class ForegroundCoordinatorTest extends SysuiTestCase { .setPkg(TEST_PKG) .setId(2) .build(); - when(mNotifPipeline.getActiveNotifs()).thenReturn(List.of(entry1, entry2, entry2Other)); + when(mNotifPipeline.getAllNotifs()).thenReturn(List.of(entry1, entry2, entry2Other)); // GIVEN that entry2 is currently associated with a foreground service when(mForegroundServiceController.getStandardLayoutKey(0, TEST_PKG)) @@ -253,7 +253,7 @@ public class ForegroundCoordinatorTest extends SysuiTestCase { .setPkg(TEST_PKG) .setId(2) .build(); - when(mNotifPipeline.getActiveNotifs()).thenReturn(List.of(entry)); + when(mNotifPipeline.getAllNotifs()).thenReturn(List.of(entry)); when(mForegroundServiceController.getStandardLayoutKey(0, TEST_PKG)) .thenReturn(entry.getKey()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index e960185ebe3a2..c356e0d16512d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -329,10 +329,10 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { @Test public void testIconScrollXAfterTranslationAndReset() throws Exception { mGroupRow.setTranslation(50); - assertEquals(50, -mGroupRow.getEntry().expandedIcon.getScrollX()); + assertEquals(50, -mGroupRow.getEntry().getIcons().getShelfIcon().getScrollX()); mGroupRow.resetTranslation(); - assertEquals(0, mGroupRow.getEntry().expandedIcon.getScrollX()); + assertEquals(0, mGroupRow.getEntry().getIcons().getShelfIcon().getScrollX()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java index b568abbc8fde7..2e787ce3e2b12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Notification; +import android.content.pm.LauncherApps; import android.os.Handler; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.Ranking; @@ -61,6 +62,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationRankingManager; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; +import com.android.systemui.statusbar.notification.icon.IconBuilder; +import com.android.systemui.statusbar.notification.icon.IconManager; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; @@ -245,14 +248,13 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase { mLockscreenUserManager, pipeline, stage, - true, /* allowLongPress */ - mock(KeyguardBypassController.class), - mock(StatusBarStateController.class), - mGroupManager, - mGutsManager, mNotificationInterruptionStateProvider, RowInflaterTask::new, - mExpandableNotificationRowComponentBuilder); + mExpandableNotificationRowComponentBuilder, + new IconManager( + mEntryManager, + mock(LauncherApps.class), + new IconBuilder(mContext))); mEntryManager.setUpWithPresenter(mPresenter); mEntryManager.addNotificationEntryListener(mEntryListener); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 5a89fc44f970d..dc3374bd511be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -34,6 +34,7 @@ import android.app.NotificationChannel; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.content.pm.LauncherApps; import android.graphics.drawable.Icon; import android.os.UserHandle; import android.service.notification.StatusBarNotification; @@ -53,6 +54,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; +import com.android.systemui.statusbar.notification.icon.IconBuilder; +import com.android.systemui.statusbar.notification.icon.IconManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpansionLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; @@ -93,6 +96,7 @@ public class NotificationTestHelper { private final NotifBindPipeline mBindPipeline; private final NotifCollectionListener mBindPipelineEntryListener; private final RowContentBindStage mBindStage; + private final IconManager mIconManager; private StatusBarStateController mStatusBarStateController; public NotificationTestHelper(Context context, TestableDependency dependency) { @@ -106,6 +110,10 @@ public class NotificationTestHelper { mock(KeyguardBypassController.class), mock(NotificationGroupManager.class), mock(ConfigurationControllerImpl.class)); mGroupManager.setHeadsUpManager(mHeadsUpManager); + mIconManager = new IconManager( + mock(CommonNotifCollection.class), + mock(LauncherApps.class), + new IconBuilder(mContext)); NotificationContentInflater contentBinder = new NotificationContentInflater( mock(NotifRemoteViewCache.class), @@ -377,7 +385,7 @@ public class NotificationTestHelper { .build(); entry.setRow(row); - entry.createIcons(mContext, entry.getSbn()); + mIconManager.createIcons(entry); row.setEntry(entry); mBindPipelineEntryListener.onEntryInit(entry);