diff options
| author | 2018-11-20 22:33:17 +0000 | |
|---|---|---|
| committer | 2018-11-20 22:33:17 +0000 | |
| commit | 8053ac5abdc7694bdde4cc6f640cba934208d12b (patch) | |
| tree | de6eb08cc4b86f9c82997b1ea4c052541408bcf2 | |
| parent | 0a91bc975e73fa1999a57a3ad419cb0099a2f584 (diff) | |
| parent | 4b8bbda21b67498c0199e88aca33ddb3b590dd5d (diff) | |
Merge changes from topic "alert-group-transfer-fix"
* changes:
Fix GroupAlertTransferHelper logic on update.
Fix heads up/ambient content inflation w/ groups.
18 files changed, 1079 insertions, 484 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index 314a3c32060c..417d5168641d 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -52,6 +52,7 @@ import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.phone.LockIcon; import com.android.systemui.statusbar.phone.LockscreenWallpaper; +import com.android.systemui.statusbar.phone.NotificationGroupAlertTransferHelper; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.ScrimController; @@ -138,6 +139,8 @@ public class SystemUIFactory { () -> new NotificationLockscreenUserManagerImpl(context)); providers.put(VisualStabilityManager.class, VisualStabilityManager::new); providers.put(NotificationGroupManager.class, NotificationGroupManager::new); + providers.put(NotificationGroupAlertTransferHelper.class, + NotificationGroupAlertTransferHelper::new); providers.put(NotificationMediaManager.class, () -> new NotificationMediaManager(context)); providers.put(NotificationGutsManager.class, () -> new NotificationGutsManager(context)); providers.put(AmbientPulseManager.class, () -> new AmbientPulseManager(context)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java index b6e88d6dfc11..3da6d2e877ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar; +import static com.android.systemui.statusbar.notification.NotificationData.Entry; + import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Handler; @@ -27,7 +29,7 @@ import android.util.Log; import android.view.accessibility.AccessibilityEvent; import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.statusbar.notification.NotificationData; +import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag; import java.util.stream.Stream; @@ -46,8 +48,7 @@ public abstract class AlertingNotificationManager implements NotificationLifetim * NotificationManagerService side, but we keep it to prevent the UI from looking weird and * will remove when possible. See {@link NotificationLifetimeExtender} */ - protected final ArraySet<NotificationData.Entry> mExtendedLifetimeAlertEntries = - new ArraySet<>(); + protected final ArraySet<Entry> mExtendedLifetimeAlertEntries = new ArraySet<>(); protected NotificationSafeToRemoveCallback mNotificationLifetimeFinishedCallback; protected int mMinimumDisplayTime; @@ -60,7 +61,7 @@ public abstract class AlertingNotificationManager implements NotificationLifetim * Adds the notification to be managed. * @param entry entry to show */ - public void showNotification(@NonNull NotificationData.Entry entry) { + public void showNotification(@NonNull Entry entry) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "showNotification"); } @@ -139,7 +140,7 @@ public abstract class AlertingNotificationManager implements NotificationLifetim * @return the entry */ @Nullable - public NotificationData.Entry getEntry(@NonNull String key) { + public Entry getEntry(@NonNull String key) { AlertEntry entry = mAlertEntries.get(key); return entry != null ? entry.mEntry : null; } @@ -149,7 +150,7 @@ public abstract class AlertingNotificationManager implements NotificationLifetim * @return all entries */ @NonNull - public Stream<NotificationData.Entry> getAllEntries() { + public Stream<Entry> getAllEntries() { return mAlertEntries.values().stream().map(headsUpEntry -> headsUpEntry.mEntry); } @@ -170,10 +171,17 @@ public abstract class AlertingNotificationManager implements NotificationLifetim } /** + * Gets the flag corresponding to the notification content view this alert manager will show. + * + * @return flag corresponding to the content view + */ + public abstract @InflationFlag int getContentFlag(); + + /** * Add a new entry and begin managing it. * @param entry the entry to add */ - protected final void addAlertEntry(@NonNull NotificationData.Entry entry) { + protected final void addAlertEntry(@NonNull Entry entry) { AlertEntry alertEntry = createAlertEntry(); alertEntry.setEntry(entry); mAlertEntries.put(entry.key, alertEntry); @@ -196,7 +204,7 @@ public abstract class AlertingNotificationManager implements NotificationLifetim if (alertEntry == null) { return; } - NotificationData.Entry entry = alertEntry.mEntry; + Entry entry = alertEntry.mEntry; mAlertEntries.remove(key); onAlertEntryRemoved(alertEntry); entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); @@ -243,12 +251,12 @@ public abstract class AlertingNotificationManager implements NotificationLifetim } @Override - public boolean shouldExtendLifetime(NotificationData.Entry entry) { + public boolean shouldExtendLifetime(Entry entry) { return !canRemoveImmediately(entry.key); } @Override - public void setShouldManageLifetime(NotificationData.Entry entry, boolean shouldExtend) { + public void setShouldManageLifetime(Entry entry, boolean shouldExtend) { if (shouldExtend) { mExtendedLifetimeAlertEntries.add(entry); } else { @@ -258,17 +266,17 @@ public abstract class AlertingNotificationManager implements NotificationLifetim /////////////////////////////////////////////////////////////////////////////////////////////// protected class AlertEntry implements Comparable<AlertEntry> { - @Nullable public NotificationData.Entry mEntry; + @Nullable public Entry mEntry; public long mPostTime; public long mEarliestRemovaltime; @Nullable protected Runnable mRemoveAlertRunnable; - public void setEntry(@NonNull final NotificationData.Entry entry) { + public void setEntry(@NonNull final Entry entry) { setEntry(entry, () -> removeAlertEntry(entry.key)); } - public void setEntry(@NonNull final NotificationData.Entry entry, + public void setEntry(@NonNull final Entry entry, @Nullable Runnable removeAlertRunnable) { mEntry = entry; mRemoveAlertRunnable = removeAlertRunnable; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AmbientPulseManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AmbientPulseManager.java index 21a33b0271db..f1c03049202f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AmbientPulseManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AmbientPulseManager.java @@ -26,6 +26,7 @@ import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; import com.android.systemui.statusbar.notification.NotificationData; +import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag; /** * Manager which handles high priority notifications that should "pulse" in when the device is @@ -71,6 +72,10 @@ public final class AmbientPulseManager extends AlertingNotificationManager { topEntry.extendPulse(); } + public @InflationFlag int getContentFlag() { + return FLAG_CONTENT_VIEW_AMBIENT; + } + @Override protected void onAlertEntryAdded(AlertEntry alertEntry) { NotificationData.Entry entry = alertEntry.mEntry; 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 ac0fe6efbc6e..0818513faf41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -84,6 +84,7 @@ import com.android.systemui.statusbar.notification.row.NotificationInflater; import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag; import com.android.systemui.statusbar.notification.row.RowInflaterTask; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; +import com.android.systemui.statusbar.phone.NotificationGroupAlertTransferHelper; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.phone.StatusBar; @@ -117,6 +118,8 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. private final NotificationGroupManager mGroupManager = Dependency.get(NotificationGroupManager.class); + private final NotificationGroupAlertTransferHelper mGroupAlertTransferHelper = + Dependency.get(NotificationGroupAlertTransferHelper.class); private final NotificationGutsManager mGutsManager = Dependency.get(NotificationGutsManager.class); private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); @@ -247,7 +250,8 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. } private void onPostInit() { - mGroupManager.setPendingEntries(mPendingNotifications); + mGroupAlertTransferHelper.setPendingEntries(mPendingNotifications); + mGroupManager.addOnGroupChangeListener(mGroupAlertTransferHelper); } /** @@ -554,13 +558,18 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. mPendingNotifications.remove(entry.key); // If there was an async task started after the removal, we don't want to add it back to // the list, otherwise we might get leaks. - boolean isNew = mNotificationData.get(entry.key) == null; - if (isNew && !entry.row.isRemoved()) { - showAlertingView(entry, inflatedFlags); - addEntry(entry); - } else if (!isNew && entry.row.hasLowPriorityStateUpdated()) { - mVisualStabilityManager.onLowPriorityUpdated(entry); - mPresenter.updateNotificationViews(); + if (!entry.row.isRemoved()) { + boolean isNew = mNotificationData.get(entry.key) == null; + if (isNew) { + showAlertingView(entry, inflatedFlags); + addEntry(entry); + } else { + if (entry.row.hasLowPriorityStateUpdated()) { + mVisualStabilityManager.onLowPriorityUpdated(entry); + mPresenter.updateNotificationViews(); + } + mGroupAlertTransferHelper.onInflationFinished(entry); + } } entry.row.setLowPriorityStateUpdated(false); } @@ -573,6 +582,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. private void removeNotificationInternal(String key, @Nullable NotificationListenerService.RankingMap ranking, boolean forceRemove) { abortExistingInflation(key); + mGroupAlertTransferHelper.cleanUpPendingAlertInfo(key); // Attempt to remove notifications from their alert managers (heads up, ambient pulse). // Though the remove itself may fail, it lets the manager know to remove as soon as @@ -734,8 +744,12 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp); row.setEntry(entry); - row.updateInflationFlag(FLAG_CONTENT_VIEW_HEADS_UP, shouldHeadsUp(entry)); - row.updateInflationFlag(FLAG_CONTENT_VIEW_AMBIENT, shouldPulse(entry)); + if (shouldHeadsUp(entry)) { + row.updateInflationFlag(FLAG_CONTENT_VIEW_HEADS_UP, true /* shouldInflate */); + } + if (shouldPulse(entry)) { + row.updateInflationFlag(FLAG_CONTENT_VIEW_AMBIENT, true /* shouldInflate */); + } row.setNeedsRedaction( Dependency.get(NotificationLockscreenUserManager.class).needsRedaction(entry)); row.inflateViews(); @@ -823,7 +837,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. mNotificationData.getImportance(key)); mPendingNotifications.put(key, shadeEntry); - mGroupManager.onPendingEntryAdded(shadeEntry); + mGroupAlertTransferHelper.onPendingEntryAdded(shadeEntry); } @VisibleForTesting 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 b7bdc2eb5238..b6d99b245466 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 @@ -464,7 +464,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** - * Inflate views based off the inflation flags set. Inflation happens asynchronously. + * Inflate views based off the inflation flags set. Inflation happens asynchronously. */ public void inflateViews() { mNotificationInflater.inflateNotificationViews(); @@ -511,6 +511,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** + * Whether or not a content view should be inflated. + * + * @param flag the flag corresponding to the content view + * @return true if the flag is set, false otherwise + */ + public boolean isInflationFlagSet(@InflationFlag int flag) { + return mNotificationInflater.isInflationFlagSet(flag); + } + + /** * Caches whether or not this row contains a system notification. Note, this is only cached * once per notification as the packageInfo can't technically change for a notification row. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInflater.java index e1c2f7359ce3..7086025836cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInflater.java @@ -100,17 +100,13 @@ public class NotificationInflater { public static final int FLAG_CONTENT_VIEW_ALL = ~0; - // TODO: Heads up and ambient are always inflated as a temporary workaround. - // See http://b/117933032 and http://b/117894786 /** * Content views that must be inflated at all times. */ @InflationFlag private static final int REQUIRED_INFLATION_FLAGS = FLAG_CONTENT_VIEW_CONTRACTED - | FLAG_CONTENT_VIEW_EXPANDED - | FLAG_CONTENT_VIEW_HEADS_UP - | FLAG_CONTENT_VIEW_AMBIENT; + | FLAG_CONTENT_VIEW_EXPANDED; /** * The set of content views to inflate. @@ -201,11 +197,12 @@ public class NotificationInflater { } /** - * Add flags for which content views should be inflated in addition to those already set. + * Convenience method for setting multiple flags at once. * * @param flags a set of {@link InflationFlag} corresponding to content views that should be * inflated */ + @VisibleForTesting public void addInflationFlags(@InflationFlag int flags) { mInflationFlags |= flags; } @@ -216,13 +213,12 @@ public class NotificationInflater { * @param flag the {@link InflationFlag} corresponding to the view * @return true if the flag is set and view will be inflated, false o/w */ - @VisibleForTesting public boolean isInflationFlagSet(@InflationFlag int flag) { return ((mInflationFlags & flag) != 0); } /** - * Inflate all views of this notification on a background thread. This is asynchronous and will + * Inflate views for set flags on a background thread. This is asynchronous and will * notify the callback once it's finished. */ public void inflateNotificationViews() { @@ -234,7 +230,7 @@ public class NotificationInflater { * will notify the callback once it's finished. If the content view is already inflated, this * will reinflate it. * - * @param reInflateFlags flags which views should be inflated. Should be a subset of + * @param reInflateFlags flags which views should be inflated. Should be a subset of * {@link NotificationInflater#mInflationFlags} as only those will be * inflated/reinflated. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 7bee1b8a24d7..5153b119bba8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -4540,7 +4540,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void setGroupManager(NotificationGroupManager groupManager) { this.mGroupManager = groupManager; - mGroupManager.setOnGroupChangeListener(mOnGroupChangeListener); + mGroupManager.addOnGroupChangeListener(mOnGroupChangeListener); } @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java new file mode 100644 index 000000000000..c74514e05b20 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2018 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.phone; + +import android.annotation.NonNull; +import android.app.Notification; +import android.os.SystemClock; +import android.service.notification.StatusBarNotification; +import android.util.ArrayMap; + +import com.android.systemui.Dependency; +import com.android.systemui.statusbar.AlertingNotificationManager; +import com.android.systemui.statusbar.AmbientPulseManager; +import com.android.systemui.statusbar.AmbientPulseManager.OnAmbientChangedListener; +import com.android.systemui.statusbar.InflationTask; +import com.android.systemui.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.StatusBarStateController.StateListener; +import com.android.systemui.statusbar.notification.NotificationData.Entry; +import com.android.systemui.statusbar.notification.row.NotificationInflater.AsyncInflationTask; +import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag; +import com.android.systemui.statusbar.phone.NotificationGroupManager.NotificationGroup; +import com.android.systemui.statusbar.phone.NotificationGroupManager.OnGroupChangeListener; +import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Objects; + +/** + * A helper class dealing with the alert interactions between {@link NotificationGroupManager}, + * {@link HeadsUpManager}, {@link AmbientPulseManager}. In particular, this class deals with keeping + * the correct notification in a group alerting based off the group suppression. + */ +public class NotificationGroupAlertTransferHelper implements OnGroupChangeListener, + OnHeadsUpChangedListener, OnAmbientChangedListener, StateListener { + + private static final long ALERT_TRANSFER_TIMEOUT = 300; + + /** + * The list of entries containing group alert metadata for each group. Keyed by group key. + */ + private final ArrayMap<String, GroupAlertEntry> mGroupAlertEntries = new ArrayMap<>(); + + /** + * The list of entries currently inflating that should alert after inflation. Keyed by + * notification key. + */ + private final ArrayMap<String, PendingAlertInfo> mPendingAlerts = new ArrayMap<>(); + + private HeadsUpManager mHeadsUpManager; + private final AmbientPulseManager mAmbientPulseManager = + Dependency.get(AmbientPulseManager.class); + private final NotificationGroupManager mGroupManager = + Dependency.get(NotificationGroupManager.class); + + // TODO(b/119637830): It would be good if GroupManager already had all pending notifications as + // normal children (i.e. add notifications to GroupManager before inflation) so that we don't + // have to have this dependency. We'd also have to worry less about the suppression not being up + // to date. + /** + * Notifications that are currently inflating for the first time. Used to remove an incorrectly + * alerting notification faster. + */ + private HashMap<String, Entry> mPendingNotifications; + + private boolean mIsDozing; + + public NotificationGroupAlertTransferHelper() { + Dependency.get(StatusBarStateController.class).addListener(this); + } + + /** + * Whether or not a notification has transferred its alert state to the notification and + * the notification should alert after inflating. + * + * @param entry notification to check + * @return true if the entry was transferred to and should inflate + alert + */ + public boolean isAlertTransferPending(@NonNull Entry entry) { + PendingAlertInfo alertInfo = mPendingAlerts.get(entry.key); + return alertInfo != null && alertInfo.isStillValid(); + } + + /** + * Removes any alerts pending on this entry. Note that this will not stop any inflation tasks + * started by a transfer, so this should only be used as clean-up for when inflation is stopped + * and the pending alert no longer needs to happen. + * + * @param key notification key that may have info that needs to be cleaned up + */ + public void cleanUpPendingAlertInfo(@NonNull String key) { + mPendingAlerts.remove(key); + } + + public void setHeadsUpManager(HeadsUpManager headsUpManager) { + mHeadsUpManager = headsUpManager; + } + + public void setPendingEntries(HashMap<String, Entry> pendingNotifications) { + mPendingNotifications = pendingNotifications; + } + + @Override + public void onStateChanged(int newState) {} + + @Override + public void onDozingChanged(boolean isDozing) { + if (mIsDozing != isDozing) { + for (GroupAlertEntry groupAlertEntry : mGroupAlertEntries.values()) { + groupAlertEntry.mLastAlertTransferTime = 0; + groupAlertEntry.mAlertSummaryOnNextAddition = false; + } + } + mIsDozing = isDozing; + } + + @Override + public void onGroupCreated(NotificationGroup group, String groupKey) { + mGroupAlertEntries.put(groupKey, new GroupAlertEntry(group)); + } + + @Override + public void onGroupRemoved(NotificationGroup group, String groupKey) { + mGroupAlertEntries.remove(groupKey); + } + + @Override + public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) { + AlertingNotificationManager alertManager = getActiveAlertManager(); + if (suppressed) { + if (alertManager.isAlerting(group.summary.key)) { + handleSuppressedSummaryAlerted(group.summary, alertManager); + } + } else { + // Group summary can be null if we are no longer suppressed because the summary was + // removed. In that case, we don't need to alert the summary. + if (group.summary == null) { + return; + } + GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey( + group.summary.notification)); + // Group is no longer suppressed. We should check if we need to transfer the alert + // back to the summary now that it's no longer suppressed. + if (groupAlertEntry.mAlertSummaryOnNextAddition) { + if (!alertManager.isAlerting(group.summary.key)) { + alertNotificationWhenPossible(group.summary, alertManager); + } + groupAlertEntry.mAlertSummaryOnNextAddition = false; + } else { + checkShouldTransferBack(groupAlertEntry); + } + } + } + + @Override + public void onAmbientStateChanged(Entry entry, boolean isAmbient) { + onAlertStateChanged(entry, isAmbient, mAmbientPulseManager); + } + + @Override + public void onHeadsUpStateChanged(Entry entry, boolean isHeadsUp) { + onAlertStateChanged(entry, isHeadsUp, mHeadsUpManager); + } + + private void onAlertStateChanged(Entry entry, boolean isAlerting, + AlertingNotificationManager alertManager) { + if (isAlerting && mGroupManager.isSummaryOfSuppressedGroup(entry.notification)) { + handleSuppressedSummaryAlerted(entry, alertManager); + } + } + + /** + * Called when the entry's reinflation has finished. If there is an alert pending, we then + * show the alert. + * + * @param entry entry whose inflation has finished + */ + public void onInflationFinished(@NonNull Entry entry) { + PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.key); + if (alertInfo != null) { + if (alertInfo.isStillValid()) { + alertNotificationWhenPossible(entry, getActiveAlertManager()); + } else { + // The transfer is no longer valid. Free the content. + entry.row.freeContentViewWhenSafe(alertInfo.mAlertManager.getContentFlag()); + } + } + } + + /** + * Called when a new notification has been posted but is not inflated yet. We use this to see + * as early as we can if we need to abort a transfer. + * + * @param entry entry that has been added + */ + public void onPendingEntryAdded(@NonNull Entry entry) { + String groupKey = mGroupManager.getGroupKey(entry.notification); + GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey); + if (groupAlertEntry != null) { + checkShouldTransferBack(groupAlertEntry); + } + } + + /** + * Gets the number of new notifications pending inflation that will be added to the group + * but currently aren't and should not alert. + * + * @param group group to check + * @return the number of new notifications that will be added to the group + */ + private int getPendingChildrenNotAlerting(@NonNull NotificationGroup group) { + if (mPendingNotifications == null) { + return 0; + } + int number = 0; + Collection<Entry> values = mPendingNotifications.values(); + for (Entry entry : values) { + if (isPendingNotificationInGroup(entry, group) && onlySummaryAlerts(entry)) { + number++; + } + } + return number; + } + + /** + * Checks if the pending inflations will add children to this group. + * + * @param group group to check + * @return true if a pending notification will add to this group + */ + private boolean pendingInflationsWillAddChildren(@NonNull NotificationGroup group) { + if (mPendingNotifications == null) { + return false; + } + Collection<Entry> values = mPendingNotifications.values(); + for (Entry entry : values) { + if (isPendingNotificationInGroup(entry, group)) { + return true; + } + } + return false; + } + + /** + * Checks if a new pending notification will be added to the group. + * + * @param entry pending notification + * @param group group to check + * @return true if the notification will add to the group, false o/w + */ + private boolean isPendingNotificationInGroup(@NonNull Entry entry, + @NonNull NotificationGroup group) { + String groupKey = mGroupManager.getGroupKey(group.summary.notification); + return mGroupManager.isGroupChild(entry.notification) + && Objects.equals(mGroupManager.getGroupKey(entry.notification), groupKey) + && !group.children.containsKey(entry.key); + } + + /** + * Handles the scenario where a summary that has been suppressed is alerted. A suppressed + * summary should for all intents and purposes be invisible to the user and as a result should + * not alert. When this is the case, it is our responsibility to pass the alert to the + * appropriate child which will be the representative notification alerting for the group. + * + * @param summary the summary that is suppressed and alerting + * @param alertManager the alert manager that manages the alerting summary + */ + private void handleSuppressedSummaryAlerted(@NonNull Entry summary, + @NonNull AlertingNotificationManager alertManager) { + StatusBarNotification sbn = summary.notification; + GroupAlertEntry groupAlertEntry = + mGroupAlertEntries.get(mGroupManager.getGroupKey(sbn)); + if (!mGroupManager.isSummaryOfSuppressedGroup(summary.notification) + || !alertManager.isAlerting(sbn.getKey()) + || groupAlertEntry == null) { + return; + } + + if (pendingInflationsWillAddChildren(groupAlertEntry.mGroup)) { + // New children will actually be added to this group, let's not transfer the alert. + return; + } + + Entry child = mGroupManager.getLogicalChildren(summary.notification).iterator().next(); + if (child != null) { + if (child.row.keepInParent() + || child.row.isRemoved() + || child.row.isDismissed()) { + // The notification is actually already removed. No need to alert it. + return; + } + if (!alertManager.isAlerting(child.key) && onlySummaryAlerts(summary)) { + groupAlertEntry.mLastAlertTransferTime = SystemClock.elapsedRealtime(); + } + transferAlertState(summary, child, alertManager); + } + } + + /** + * Transfers the alert state one entry to another. We remove the alert from the first entry + * immediately to have the incorrect one up as short as possible. The second should alert + * when possible. + * + * @param fromEntry entry to transfer alert from + * @param toEntry entry to transfer to + * @param alertManager alert manager for the alert type + */ + private void transferAlertState(@NonNull Entry fromEntry, @NonNull Entry toEntry, + @NonNull AlertingNotificationManager alertManager) { + alertManager.removeNotification(fromEntry.key, true /* releaseImmediately */); + alertNotificationWhenPossible(toEntry, alertManager); + } + + /** + * Determines if we need to transfer the alert back to the summary from the child and does + * so if needed. + * + * This can happen since notification groups are not delivered as a whole unit and it is + * possible we erroneously transfer the alert from the summary to the child even though + * more children are coming. Thus, if a child is added within a certain timeframe after we + * transfer, we back out and alert the summary again. + * + * @param groupAlertEntry group alert entry to check + */ + private void checkShouldTransferBack(@NonNull GroupAlertEntry groupAlertEntry) { + if (SystemClock.elapsedRealtime() - groupAlertEntry.mLastAlertTransferTime + < ALERT_TRANSFER_TIMEOUT) { + Entry summary = groupAlertEntry.mGroup.summary; + AlertingNotificationManager alertManager = getActiveAlertManager(); + + if (!onlySummaryAlerts(summary)) { + return; + } + ArrayList<Entry> children = mGroupManager.getLogicalChildren(summary.notification); + int numChildren = children.size(); + int numPendingChildren = getPendingChildrenNotAlerting(groupAlertEntry.mGroup); + numChildren += numPendingChildren; + if (numChildren <= 1) { + return; + } + boolean releasedChild = false; + for (int i = 0; i < children.size(); i++) { + Entry entry = children.get(i); + if (onlySummaryAlerts(entry) && alertManager.isAlerting(entry.key)) { + releasedChild = true; + alertManager.removeNotification(entry.key, true /* releaseImmediately */); + } + if (mPendingAlerts.containsKey(entry.key)) { + // This is the child that would've been removed if it was inflated. + releasedChild = true; + mPendingAlerts.get(entry.key).mAbortOnInflation = true; + } + } + if (releasedChild && !alertManager.isAlerting(summary.key)) { + boolean notifyImmediately = (numChildren - numPendingChildren) > 1; + if (notifyImmediately) { + alertNotificationWhenPossible(summary, alertManager); + } else { + // Should wait until the pending child inflates before alerting. + groupAlertEntry.mAlertSummaryOnNextAddition = true; + } + groupAlertEntry.mLastAlertTransferTime = 0; + } + } + } + + /** + * Tries to alert the notification. If its content view is not inflated, we inflate and continue + * when the entry finishes inflating the view. + * + * @param entry entry to show + * @param alertManager alert manager for the alert type + */ + private void alertNotificationWhenPossible(@NonNull Entry entry, + @NonNull AlertingNotificationManager alertManager) { + @InflationFlag int contentFlag = alertManager.getContentFlag(); + if (!entry.row.isInflationFlagSet(contentFlag)) { + mPendingAlerts.put(entry.key, new PendingAlertInfo(entry, alertManager)); + entry.row.updateInflationFlag(contentFlag, true /* shouldInflate */); + entry.row.inflateViews(); + return; + } + if (alertManager.isAlerting(entry.key)) { + alertManager.updateNotification(entry.key, true /* alert */); + } else { + alertManager.showNotification(entry); + } + } + + private AlertingNotificationManager getActiveAlertManager() { + return mIsDozing ? mAmbientPulseManager : mHeadsUpManager; + } + + private boolean onlySummaryAlerts(Entry entry) { + return entry.notification.getNotification().getGroupAlertBehavior() + == Notification.GROUP_ALERT_SUMMARY; + } + + /** + * Information about a pending alert used to determine if the alert is still needed when + * inflation completes. + */ + private class PendingAlertInfo { + /** + * The alert manager when the transfer is initiated. + */ + final AlertingNotificationManager mAlertManager; + + /** + * The original notification when the transfer is initiated. This is used to determine if + * the transfer is still valid if the notification is updated. + */ + final StatusBarNotification mOriginalNotification; + final Entry mEntry; + + /** + * The notification is still pending inflation but we've decided that we no longer need + * the content view (e.g. suppression might have changed and we decided we need to transfer + * back). However, there is no way to abort just this inflation if other inflation requests + * have started (see {@link AsyncInflationTask#supersedeTask(InflationTask)}). So instead + * we just flag it as aborted and free when it's inflated. + */ + boolean mAbortOnInflation; + + PendingAlertInfo(Entry entry, AlertingNotificationManager alertManager) { + mOriginalNotification = entry.notification; + mEntry = entry; + mAlertManager = alertManager; + } + + /** + * Whether or not the pending alert is still valid and should still alert after inflation. + * + * @return true if the pending alert should still occur, false o/w + */ + private boolean isStillValid() { + if (mAbortOnInflation) { + // Notification is aborted due to the transfer being explicitly cancelled + return false; + } + if (mAlertManager != getActiveAlertManager()) { + // Alert manager has changed + return false; + } + if (mEntry.notification.getGroupKey() != mOriginalNotification.getGroupKey()) { + // Groups have changed + return false; + } + if (mEntry.notification.getNotification().isGroupSummary() + != mOriginalNotification.getNotification().isGroupSummary()) { + // Notification has changed from group summary to not or vice versa + return false; + } + return true; + } + } + + /** + * Contains alert metadata for the notification group used to determine when/how the alert + * should be transferred. + */ + private static class GroupAlertEntry { + /** + * The time when the last alert transfer from summary to child happened. + */ + long mLastAlertTransferTime; + boolean mAlertSummaryOnNextAddition; + final NotificationGroup mGroup; + + GroupAlertEntry(NotificationGroup group) { + this.mGroup = group; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java index 6b12dd9519e4..8ceabf809df8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java @@ -16,17 +16,12 @@ package com.android.systemui.statusbar.phone; -import android.app.Notification; -import android.os.SystemClock; +import android.annotation.Nullable; import android.service.notification.StatusBarNotification; +import android.util.ArraySet; import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dependency; -import com.android.systemui.statusbar.AlertingNotificationManager; import com.android.systemui.statusbar.AmbientPulseManager; import com.android.systemui.statusbar.AmbientPulseManager.OnAmbientChangedListener; import com.android.systemui.statusbar.StatusBarState; @@ -40,9 +35,7 @@ import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import java.util.Objects; @@ -53,23 +46,25 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, OnAmbientChangedListener, StateListener { private static final String TAG = "NotificationGroupManager"; - private static final long ALERT_TRANSFER_TIMEOUT = 300; private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>(); - private OnGroupChangeListener mListener; + private final ArraySet<OnGroupChangeListener> mListeners = new ArraySet<>(); private int mBarState = -1; private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>(); private HeadsUpManager mHeadsUpManager; private AmbientPulseManager mAmbientPulseManager = Dependency.get(AmbientPulseManager.class); - private boolean mIsDozing; private boolean mIsUpdatingUnchangedGroup; - private HashMap<String, NotificationData.Entry> mPendingNotifications; public NotificationGroupManager() { Dependency.get(StatusBarStateController.class).addListener(this); } - public void setOnGroupChangeListener(OnGroupChangeListener listener) { - mListener = listener; + /** + * Add a listener for changes to groups. + * + * @param listener listener to add + */ + public void addOnGroupChangeListener(OnGroupChangeListener listener) { + mListeners.add(listener); } public boolean isGroupExpanded(StatusBarNotification sbn) { @@ -91,7 +86,10 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, private void setGroupExpanded(NotificationGroup group, boolean expanded) { group.expanded = expanded; if (group.summary != null) { - mListener.onGroupExpansionChanged(group.summary.row, expanded); + for (OnGroupChangeListener listener : mListeners) { + listener.onGroupExpansionChanged(group.summary.row, + expanded); + } } } @@ -127,6 +125,9 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, if (group.children.isEmpty()) { if (group.summary == null) { mGroupMap.remove(groupKey); + for (OnGroupChangeListener listener : mListeners) { + listener.onGroupRemoved(group, groupKey); + } } } } @@ -142,6 +143,9 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, if (group == null) { group = new NotificationGroup(); mGroupMap.put(groupKey, group); + for (OnGroupChangeListener listener : mListeners) { + listener.onGroupCreated(group, groupKey); + } } if (isGroupChild) { NotificationData.Entry existing = group.children.get(added.key); @@ -166,125 +170,11 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, for (NotificationData.Entry child : childrenCopy) { onEntryBecomingChild(child); } - mListener.onGroupCreatedFromChildren(group); - } - } - cleanUpAlertStatesOnAdd(group, false /* addIsPending */); - } - - public void onPendingEntryAdded(NotificationData.Entry shadeEntry) { - String groupKey = getGroupKey(shadeEntry.notification); - NotificationGroup group = mGroupMap.get(groupKey); - if (group != null) { - cleanUpAlertStatesOnAdd(group, true /* addIsPending */); - } - } - - /** - * Set whether or not the device is dozing. This allows the group manager to reset some - * specific alert state logic based off when the state changes. - * @param isDozing if the device is dozing. - */ - @VisibleForTesting - public void setDozing(boolean isDozing) { - if (mIsDozing != isDozing) { - for (NotificationGroup group : mGroupMap.values()) { - group.lastAlertTransfer = 0; - group.alertSummaryOnNextAddition = false; - } - } - mIsDozing = isDozing; - } - - /** - * Clean up the alert states when a new child was added. - * @param group The group where a view was added or will be added. - * @param addIsPending True if is the addition still pending or false has it already been added. - */ - private void cleanUpAlertStatesOnAdd(NotificationGroup group, boolean addIsPending) { - - AlertingNotificationManager alertManager = - mIsDozing ? mAmbientPulseManager : mHeadsUpManager; - if (!addIsPending && group.alertSummaryOnNextAddition) { - if (!alertManager.isAlerting(group.summary.key)) { - alertManager.showNotification(group.summary); - } - group.alertSummaryOnNextAddition = false; - } - // Because notification groups are not delivered as a whole unit, it may happen that a - // group child gets added quite a bit after the summary got posted. Our guidance is, that - // apps should always post the group summary as well and we'll hide it for them if the child - // is the only child in a group. Because of this, we also have to transfer alert to the - // child, otherwise the invisible summary would be alerted. - // This transfer to the child is not always correct in case the app has just posted another - // child in addition to the existing one, but it hasn't arrived in systemUI yet. In such - // a scenario we would transfer the alert to the old child and the wrong notification - // would be alerted. In order to avoid this, we'll recover from this issue and alert the - // summary again instead of the old child if it's within a certain timeout. - if (SystemClock.elapsedRealtime() - group.lastAlertTransfer < ALERT_TRANSFER_TIMEOUT) { - if (!onlySummaryAlerts(group.summary)) { - return; - } - int numChildren = group.children.size(); - NotificationData.Entry isolatedChild = getIsolatedChild(getGroupKey( - group.summary.notification)); - int numPendingChildren = getPendingChildrenNotAlerting(group); - numChildren += numPendingChildren; - if (isolatedChild != null) { - numChildren++; - } - if (numChildren <= 1) { - return; - } - boolean releasedChild = false; - ArrayList<NotificationData.Entry> children = new ArrayList<>(group.children.values()); - int size = children.size(); - for (int i = 0; i < size; i++) { - NotificationData.Entry entry = children.get(i); - if (onlySummaryAlerts(entry) && alertManager.isAlerting(entry.key)) { - releasedChild = true; - alertManager.removeNotification(entry.key, true /* releaseImmediately */); - } - } - if (isolatedChild != null && onlySummaryAlerts(isolatedChild) - && alertManager.isAlerting(isolatedChild.key)) { - releasedChild = true; - alertManager.removeNotification(isolatedChild.key, true /* releaseImmediately */); - } - if (releasedChild && !alertManager.isAlerting(group.summary.key)) { - boolean notifyImmediately = (numChildren - numPendingChildren) > 1; - if (notifyImmediately) { - alertManager.showNotification(group.summary); - } else { - group.alertSummaryOnNextAddition = true; + for (OnGroupChangeListener listener : mListeners) { + listener.onGroupCreatedFromChildren(group); } - group.lastAlertTransfer = 0; - } - } - } - - private int getPendingChildrenNotAlerting(NotificationGroup group) { - if (mPendingNotifications == null) { - return 0; - } - int number = 0; - String groupKey = getGroupKey(group.summary.notification); - Collection<NotificationData.Entry> values = mPendingNotifications.values(); - for (NotificationData.Entry entry : values) { - if (!isGroupChild(entry.notification)) { - continue; - } - if (!Objects.equals(getGroupKey(entry.notification), groupKey)) { - continue; - } - if (group.children.containsKey(entry.key)) { - continue; - } - if (onlySummaryAlerts(entry)) { - number++; } } - return number; } private void onEntryBecomingChild(NotificationData.Entry entry) { @@ -304,16 +194,12 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, && group.summary.notification.getNotification().isGroupSummary() && hasIsolatedChildren(group))); if (prevSuppressed != group.suppressed) { - if (group.suppressed) { - if (mHeadsUpManager.isAlerting(group.summary.key)) { - handleSuppressedSummaryAlerted(group.summary, mHeadsUpManager); - } else if (mAmbientPulseManager.isAlerting(group.summary.key)) { - handleSuppressedSummaryAlerted(group.summary, mAmbientPulseManager); + for (OnGroupChangeListener listener : mListeners) { + if (!mIsUpdatingUnchangedGroup) { + listener.onGroupSuppressionChanged(group, group.suppressed); + listener.onGroupsChanged(); } } - if (!mIsUpdatingUnchangedGroup && mListener != null) { - mListener.onGroupsChanged(); - } } } @@ -462,8 +348,7 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, * but the logical summary, i.e when a child is isolated, it still returns the summary as if * it wasn't isolated. */ - public ExpandableNotificationRow getLogicalGroupSummary( - StatusBarNotification sbn) { + public ExpandableNotificationRow getLogicalGroupSummary(StatusBarNotification sbn) { return getGroupSummary(sbn.getGroupKey()); } @@ -475,6 +360,39 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, : group.summary.row; } + /** + * Get the children that are logically in the summary's group, whether or not they are isolated. + * + * @param summary summary of a group + * @return list of the children + */ + public ArrayList<NotificationData.Entry> getLogicalChildren(StatusBarNotification summary) { + NotificationGroup group = mGroupMap.get(summary.getGroupKey()); + if (group == null) { + return null; + } + ArrayList<NotificationData.Entry> children = new ArrayList<>(group.children.values()); + NotificationData.Entry isolatedChild = getIsolatedChild(summary.getGroupKey()); + if (isolatedChild != null) { + children.add(isolatedChild); + } + return children; + } + + /** + * Get the group key. May differ from the one in the notification due to the notification + * being temporarily isolated. + * + * @param sbn notification to check + * @return the key of the notification + */ + public String getGroupKey(StatusBarNotification sbn) { + if (isIsolated(sbn)) { + return sbn.getKey(); + } + return sbn.getGroupKey(); + } + /** @return group expansion state after toggling. */ public boolean toggleGroupExpansion(StatusBarNotification sbn) { NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); @@ -489,27 +407,32 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, return mIsolatedEntries.containsKey(sbn.getKey()); } - private boolean isGroupSummary(StatusBarNotification sbn) { + /** + * Whether a notification is visually a group summary. + * + * @param sbn notification to check + * @return true if it is visually a group summary + */ + public boolean isGroupSummary(StatusBarNotification sbn) { if (isIsolated(sbn)) { return true; } return sbn.getNotification().isGroupSummary(); } - private boolean isGroupChild(StatusBarNotification sbn) { + /** + * Whether a notification is visually a group child. + * + * @param sbn notification to check + * @return true if it is visually a group child + */ + public boolean isGroupChild(StatusBarNotification sbn) { if (isIsolated(sbn)) { return false; } return sbn.isGroup() && !sbn.getNotification().isGroupSummary(); } - private String getGroupKey(StatusBarNotification sbn) { - if (isIsolated(sbn)) { - return sbn.getKey(); - } - return sbn.getGroupKey(); - } - @Override public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) { } @@ -524,23 +447,18 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, @Override public void onAmbientStateChanged(NotificationData.Entry entry, boolean isAmbient) { - onAlertStateChanged(entry, isAmbient, mAmbientPulseManager); + onAlertStateChanged(entry, isAmbient); } @Override public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) { - onAlertStateChanged(entry, isHeadsUp, mHeadsUpManager); + onAlertStateChanged(entry, isHeadsUp); } - private void onAlertStateChanged(NotificationData.Entry entry, boolean isAlerting, - AlertingNotificationManager alertManager) { - final StatusBarNotification sbn = entry.notification; + private void onAlertStateChanged(NotificationData.Entry entry, boolean isAlerting) { if (isAlerting) { if (shouldIsolate(entry)) { isolateNotification(entry); - } else if (sbn.getNotification().isGroupSummary() - && isGroupSuppressed(sbn.getGroupKey())){ - handleSuppressedSummaryAlerted(entry, alertManager); } } else { stopIsolatingNotification(entry); @@ -548,100 +466,6 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, } /** - * Handles the scenario where a summary that has been suppressed is alerted. A suppressed - * summary should for all intents and purposes be invisible to the user and as a result should - * not alert. When this is the case, it is our responsibility to pass the alert to the - * appropriate child which will be the representative notification alerting for the group. - * @param summary the summary that is suppressed and alerting - * @param alertManager the alert manager that manages the alerting summary - */ - private void handleSuppressedSummaryAlerted(@NonNull NotificationData.Entry summary, - @NonNull AlertingNotificationManager alertManager) { - StatusBarNotification sbn = summary.notification; - if (!isGroupSuppressed(sbn.getGroupKey()) - || !sbn.getNotification().isGroupSummary() - || !alertManager.isAlerting(sbn.getKey())) { - return; - } - - // The parent of a suppressed group got alerted, lets alert the child! - NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey()); - - if (notificationGroup != null) { - if (pendingInflationsWillAddChildren(notificationGroup)) { - // New children will actually be added to this group, let's not transfer the alert. - return; - } - - Iterator<NotificationData.Entry> iterator - = notificationGroup.children.values().iterator(); - NotificationData.Entry child = iterator.hasNext() ? iterator.next() : null; - if (child == null) { - child = getIsolatedChild(sbn.getGroupKey()); - } - if (child != null) { - if (child.row.keepInParent() || child.row.isRemoved() || child.row.isDismissed()) { - // the notification is actually already removed, no need to do alert on it. - return; - } - transferAlertStateToChild(summary, child, alertManager); - } - } - } - - /** - * Transfers the alert state from a given summary notification to the specified child. The - * result is the child will now alert while the summary does not. - * - * @param summary the currently alerting summary notification - * @param child the child that should receive the alert - * @param alertManager the manager for the alert - */ - private void transferAlertStateToChild(@NonNull NotificationData.Entry summary, - @NonNull NotificationData.Entry child, - @NonNull AlertingNotificationManager alertManager) { - NotificationGroup notificationGroup = mGroupMap.get(summary.notification.getGroupKey()); - if (alertManager.isAlerting(child.key)) { - alertManager.updateNotification(child.key, true /* alert */); - } else { - if (onlySummaryAlerts(summary)) { - notificationGroup.lastAlertTransfer = SystemClock.elapsedRealtime(); - } - alertManager.showNotification(child); - } - alertManager.removeNotification(summary.key, true /* releaseImmediately */); - } - - private boolean onlySummaryAlerts(NotificationData.Entry entry) { - return entry.notification.getNotification().getGroupAlertBehavior() - == Notification.GROUP_ALERT_SUMMARY; - } - - /** - * Check if the pending inflations will add children to this group. - * @param group The group to check. - */ - private boolean pendingInflationsWillAddChildren(NotificationGroup group) { - if (mPendingNotifications == null) { - return false; - } - Collection<NotificationData.Entry> values = mPendingNotifications.values(); - String groupKey = getGroupKey(group.summary.notification); - for (NotificationData.Entry entry : values) { - if (!isGroupChild(entry.notification)) { - continue; - } - if (!Objects.equals(getGroupKey(entry.notification), groupKey)) { - continue; - } - if (!group.children.containsKey(entry.key)) { - return true; - } - } - return false; - } - - /** * Whether a notification that is normally part of a group should be temporarily isolated from * the group and put in their own group visually. This generally happens when the notification * is alerting. @@ -656,10 +480,7 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, if (!sbn.isGroup() || sbn.getNotification().isGroupSummary()) { return false; } - if (!mIsDozing && !mHeadsUpManager.isAlerting(entry.key)) { - return false; - } - if (mIsDozing && !mAmbientPulseManager.isAlerting(entry.key)) { + if (!mHeadsUpManager.isAlerting(entry.key) && !mAmbientPulseManager.isAlerting(entry.key)) { return false; } return (sbn.getNotification().fullScreenIntent != null @@ -687,7 +508,9 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, // When the notification gets added afterwards it is already isolated and therefore // it doesn't lead to an update. updateSuppression(mGroupMap.get(entry.notification.getGroupKey())); - mListener.onGroupsChanged(); + for (OnGroupChangeListener listener : mListeners) { + listener.onGroupsChanged(); + } } /** @@ -702,7 +525,9 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, onEntryRemovedInternal(entry, entry.notification); mIsolatedEntries.remove(sbn.getKey()); onEntryAdded(entry); - mListener.onGroupsChanged(); + for (OnGroupChangeListener listener : mListeners) { + listener.onGroupsChanged(); + } } } @@ -729,20 +554,11 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, } } - public void setPendingEntries(HashMap<String, NotificationData.Entry> pendingNotifications) { - mPendingNotifications = pendingNotifications; - } - @Override public void onStateChanged(int newState) { setStatusBarState(newState); } - @Override - public void onDozingChanged(boolean isDozing) { - setDozing(isDozing); - } - public static class NotificationGroup { public final HashMap<String, NotificationData.Entry> children = new HashMap<>(); public NotificationData.Entry summary; @@ -751,12 +567,6 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, * Is this notification group suppressed, i.e its summary is hidden */ public boolean suppressed; - /** - * The time when the last alert transfer from group to child happened, while the summary - * has the flags to alert up on its own. - */ - public long lastAlertTransfer; - public boolean alertSummaryOnNextAddition; @Override public String toString() { @@ -777,13 +587,39 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, } public interface OnGroupChangeListener { + + /** + * A new group has been created. + * + * @param group the group that was created + * @param groupKey the group's key + */ + default void onGroupCreated(NotificationGroup group, String groupKey) {} + + /** + * A group has been removed. + * + * @param group the group that was removed + * @param groupKey the group's key + */ + default void onGroupRemoved(NotificationGroup group, String groupKey) {} + + /** + * The suppression of a group has changed. + * + * @param group the group that has changed + * @param suppressed true if the group is now suppressed, false o/w + */ + default void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {} + /** * The expansion of a group has changed. * * @param changedRow the row for which the expansion has changed, which is also the summary * @param expanded a boolean indicating the new expanded state */ - void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded); + default void onGroupExpansionChanged(ExpandableNotificationRow changedRow, + boolean expanded) {} /** * A group of children just received a summary notification and should therefore become @@ -791,12 +627,12 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener, * * @param group the group created */ - void onGroupCreatedFromChildren(NotificationGroup group); + default void onGroupCreatedFromChildren(NotificationGroup group) {} /** * The groups have changed. This can happen if the isolation of a child has changes or if a * group became suppressed / unsuppressed */ - void onGroupsChanged(); + default void onGroupsChanged() {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 8e6136692ea1..45e924fa4321 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -596,6 +596,7 @@ public class StatusBar extends SystemUI implements DemoMode, @Override public void start() { mGroupManager = Dependency.get(NotificationGroupManager.class); + mGroupAlertTransferHelper = Dependency.get(NotificationGroupAlertTransferHelper.class); mVisualStabilityManager = Dependency.get(VisualStabilityManager.class); mNotificationLogger = Dependency.get(NotificationLogger.class); mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class); @@ -832,11 +833,14 @@ public class StatusBar extends SystemUI implements DemoMode, mHeadsUpManager.addListener(this); mHeadsUpManager.addListener(mNotificationPanel); mHeadsUpManager.addListener(mGroupManager); + mHeadsUpManager.addListener(mGroupAlertTransferHelper); mHeadsUpManager.addListener(mVisualStabilityManager); mAmbientPulseManager.addListener(this); mAmbientPulseManager.addListener(mGroupManager); + mAmbientPulseManager.addListener(mGroupAlertTransferHelper); mNotificationPanel.setHeadsUpManager(mHeadsUpManager); mGroupManager.setHeadsUpManager(mHeadsUpManager); + mGroupAlertTransferHelper.setHeadsUpManager(mHeadsUpManager); putComponent(HeadsUpManager.class, mHeadsUpManager); @@ -4105,6 +4109,8 @@ public class StatusBar extends SystemUI implements DemoMode, protected NotificationGroupManager mGroupManager; + protected NotificationGroupAlertTransferHelper mGroupAlertTransferHelper; + // for heads up notifications protected HeadsUpManagerPhone mHeadsUpManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java index 6b83b70b0ca5..fdab6168d05a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -33,6 +33,7 @@ import com.android.systemui.R; import com.android.systemui.statusbar.AlertingNotificationManager; import com.android.systemui.statusbar.notification.NotificationData; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -136,6 +137,10 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { } } + public @InflationFlag int getContentFlag() { + return FLAG_CONTENT_VIEW_HEADS_UP; + } + @Override protected void onAlertEntryAdded(AlertEntry alertEntry) { NotificationData.Entry entry = alertEntry.mEntry; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java index 8b41516044f2..f49c5b47deda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar; +import static com.android.systemui.statusbar.notification.row.NotificationInflater.FLAG_CONTENT_VIEW_CONTRACTED; + import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; @@ -84,6 +86,11 @@ public class AlertingNotificationManagerTest extends SysuiTestCase { @Override protected void onAlertEntryRemoved(AlertEntry alertEntry) {} + + @Override + public int getContentFlag() { + return FLAG_CONTENT_VIEW_CONTRACTED; + } } protected AlertingNotificationManager createAlertingNotificationManager() { 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 bf8eb6216d7d..2da72e7858c8 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 @@ -53,7 +53,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationChildrenCon import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -140,10 +139,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { verify(row).updateShelfIconColor(); } - // TODO: Ignoring as a temporary workaround until heads up views can be safely freed. - // See http://b/117933032 @Test - @Ignore public void testFreeContentViewWhenSafe() throws Exception { ExpandableNotificationRow row = mNotificationTestHelper.createRow(FLAG_CONTENT_VIEW_ALL); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInflaterTest.java index 0d2d3451b90c..d6b706d73827 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInflaterTest.java @@ -117,10 +117,7 @@ public class NotificationInflaterTest extends SysuiTestCase { verify(mRow).onNotificationUpdated(); } - // TODO: Ignoring as a temporary workaround until ambient views can be safely freed. - // See http://b/117894786 @Test - @Ignore public void testInflationOnlyInflatesSetFlags() throws Exception { mNotificationInflater.updateInflationFlag(FLAG_CONTENT_VIEW_HEADS_UP, true /* shouldInflate */); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java new file mode 100644 index 000000000000..96c57f2b7991 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2018 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.phone; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.service.notification.StatusBarNotification; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.AmbientPulseManager; +import com.android.systemui.statusbar.notification.NotificationData; +import com.android.systemui.statusbar.notification.NotificationData.Entry; +import com.android.systemui.statusbar.policy.HeadsUpManager; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.HashMap; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + private NotificationGroupAlertTransferHelper mGroupAlertTransferHelper; + private NotificationGroupManager mGroupManager; + private AmbientPulseManager mAmbientPulseManager; + private HeadsUpManager mHeadsUpManager; + private final HashMap<String, Entry> mPendingEntries = new HashMap<>(); + private final NotificationGroupTestHelper mGroupTestHelper = + new NotificationGroupTestHelper(mContext); + + + @Before + public void setup() { + mAmbientPulseManager = new AmbientPulseManager(mContext); + mDependency.injectTestDependency(AmbientPulseManager.class, mAmbientPulseManager); + mHeadsUpManager = new HeadsUpManager(mContext) {}; + + mGroupManager = new NotificationGroupManager(); + mDependency.injectTestDependency(NotificationGroupManager.class, mGroupManager); + mGroupManager.setHeadsUpManager(mHeadsUpManager); + + mGroupAlertTransferHelper = new NotificationGroupAlertTransferHelper(); + mGroupAlertTransferHelper.setHeadsUpManager(mHeadsUpManager); + mGroupAlertTransferHelper.setPendingEntries(mPendingEntries); + + mGroupManager.addOnGroupChangeListener(mGroupAlertTransferHelper); + mHeadsUpManager.addListener(mGroupAlertTransferHelper); + mAmbientPulseManager.addListener(mGroupAlertTransferHelper); + } + + @Test + public void testSuppressedSummaryHeadsUpTransfersToChild() { + Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); + mHeadsUpManager.showNotification(summaryEntry); + Entry childEntry = mGroupTestHelper.createChildNotification(); + + // Summary will be suppressed because there is only one child. + mGroupManager.onEntryAdded(summaryEntry); + mGroupManager.onEntryAdded(childEntry); + + // A suppressed summary should transfer its alert state to the child. + assertFalse(mHeadsUpManager.isAlerting(summaryEntry.key)); + assertTrue(mHeadsUpManager.isAlerting(childEntry.key)); + } + + @Test + public void testSuppressedSummaryHeadsUpTransfersToChildButBackAgain() { + NotificationData.Entry summaryEntry = + mGroupTestHelper.createSummaryNotification(Notification.GROUP_ALERT_SUMMARY); + NotificationData.Entry childEntry = + mGroupTestHelper.createChildNotification(Notification.GROUP_ALERT_SUMMARY); + NotificationData.Entry childEntry2 = + mGroupTestHelper.createChildNotification(Notification.GROUP_ALERT_SUMMARY); + mHeadsUpManager.showNotification(summaryEntry); + // Trigger a transfer of alert state from summary to child. + mGroupManager.onEntryAdded(summaryEntry); + mGroupManager.onEntryAdded(childEntry); + + // Add second child notification so that summary is no longer suppressed. + mPendingEntries.put(childEntry2.key, childEntry2); + mGroupAlertTransferHelper.onPendingEntryAdded(childEntry2); + mGroupManager.onEntryAdded(childEntry2); + + // The alert state should transfer back to the summary as there is now more than one + // child and the summary should no longer be suppressed. + assertTrue(mHeadsUpManager.isAlerting(summaryEntry.key)); + assertFalse(mHeadsUpManager.isAlerting(childEntry.key)); + } + + @Test + public void testSuppressedSummaryHeadsUpDoesntTransferBackOnDozingChanged() { + NotificationData.Entry summaryEntry = + mGroupTestHelper.createSummaryNotification(Notification.GROUP_ALERT_SUMMARY); + NotificationData.Entry childEntry = + mGroupTestHelper.createChildNotification(Notification.GROUP_ALERT_SUMMARY); + NotificationData.Entry childEntry2 = + mGroupTestHelper.createChildNotification(Notification.GROUP_ALERT_SUMMARY); + mHeadsUpManager.showNotification(summaryEntry); + // Trigger a transfer of alert state from summary to child. + mGroupManager.onEntryAdded(summaryEntry); + mGroupManager.onEntryAdded(childEntry); + + // Set dozing to true. + mGroupAlertTransferHelper.onDozingChanged(true); + + // Add second child notification so that summary is no longer suppressed. + mPendingEntries.put(childEntry2.key, childEntry2); + mGroupAlertTransferHelper.onPendingEntryAdded(childEntry2); + mGroupManager.onEntryAdded(childEntry2); + + // Dozing changed so no reason to re-alert summary. + assertFalse(mHeadsUpManager.isAlerting(summaryEntry.key)); + } + + @Test + public void testSuppressedSummaryHeadsUpTransferDoesNotAlertChildIfUninflated() { + Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); + mHeadsUpManager.showNotification(summaryEntry); + Entry childEntry = mGroupTestHelper.createChildNotification(); + when(childEntry.row.isInflationFlagSet(mHeadsUpManager.getContentFlag())).thenReturn(false); + + mGroupManager.onEntryAdded(summaryEntry); + mGroupManager.onEntryAdded(childEntry); + + // Alert is immediately removed from summary, but we do not show child yet either as its + // content is not inflated. + assertFalse(mHeadsUpManager.isAlerting(summaryEntry.key)); + assertFalse(mHeadsUpManager.isAlerting(childEntry.key)); + assertTrue(mGroupAlertTransferHelper.isAlertTransferPending(childEntry)); + } + + @Test + public void testSuppressedSummaryHeadsUpTransferAlertsChildOnInflation() { + Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); + mHeadsUpManager.showNotification(summaryEntry); + Entry childEntry = mGroupTestHelper.createChildNotification(); + when(childEntry.row.isInflationFlagSet(mHeadsUpManager.getContentFlag())).thenReturn(false); + + mGroupManager.onEntryAdded(summaryEntry); + mGroupManager.onEntryAdded(childEntry); + + when(childEntry.row.isInflationFlagSet(mHeadsUpManager.getContentFlag())).thenReturn(true); + mGroupAlertTransferHelper.onInflationFinished(childEntry); + + // Alert is immediately removed from summary, and we show child as its content is inflated. + assertFalse(mHeadsUpManager.isAlerting(summaryEntry.key)); + assertTrue(mHeadsUpManager.isAlerting(childEntry.key)); + } + + @Test + public void testSuppressedSummaryHeadsUpTransferBackAbortsChildInflation() { + NotificationData.Entry summaryEntry = + mGroupTestHelper.createSummaryNotification(Notification.GROUP_ALERT_SUMMARY); + NotificationData.Entry childEntry = + mGroupTestHelper.createChildNotification(Notification.GROUP_ALERT_SUMMARY); + when(childEntry.row.isInflationFlagSet(mHeadsUpManager.getContentFlag())).thenReturn(false); + NotificationData.Entry childEntry2 = + mGroupTestHelper.createChildNotification(Notification.GROUP_ALERT_SUMMARY); + mHeadsUpManager.showNotification(summaryEntry); + // Trigger a transfer of alert state from summary to child. + mGroupManager.onEntryAdded(summaryEntry); + mGroupManager.onEntryAdded(childEntry); + + // Add second child notification so that summary is no longer suppressed. + mPendingEntries.put(childEntry2.key, childEntry2); + mGroupAlertTransferHelper.onPendingEntryAdded(childEntry2); + mGroupManager.onEntryAdded(childEntry2); + + // Child entry finishes its inflation. + when(childEntry.row.isInflationFlagSet(mHeadsUpManager.getContentFlag())).thenReturn(true); + mGroupAlertTransferHelper.onInflationFinished(childEntry); + + verify(childEntry.row, times(1)).freeContentViewWhenSafe(mHeadsUpManager.getContentFlag()); + assertFalse(mHeadsUpManager.isAlerting(childEntry.key)); + } + + @Test + public void testCleanUpPendingAlertInfo() { + NotificationData.Entry summaryEntry = + mGroupTestHelper.createSummaryNotification(Notification.GROUP_ALERT_SUMMARY); + NotificationData.Entry childEntry = + mGroupTestHelper.createChildNotification(Notification.GROUP_ALERT_SUMMARY); + when(childEntry.row.isInflationFlagSet(mHeadsUpManager.getContentFlag())).thenReturn(false); + mHeadsUpManager.showNotification(summaryEntry); + // Trigger a transfer of alert state from summary to child. + mGroupManager.onEntryAdded(summaryEntry); + mGroupManager.onEntryAdded(childEntry); + + mGroupAlertTransferHelper.cleanUpPendingAlertInfo(childEntry.key); + + assertFalse(mGroupAlertTransferHelper.isAlertTransferPending(childEntry)); + } + + @Test + public void testUpdateGroupChangeDoesNotTransfer() { + NotificationData.Entry summaryEntry = + mGroupTestHelper.createSummaryNotification(Notification.GROUP_ALERT_SUMMARY); + NotificationData.Entry childEntry = + mGroupTestHelper.createChildNotification(Notification.GROUP_ALERT_SUMMARY); + when(childEntry.row.isInflationFlagSet(mHeadsUpManager.getContentFlag())).thenReturn(false); + mHeadsUpManager.showNotification(summaryEntry); + // Trigger a transfer of alert state from summary to child. + mGroupManager.onEntryAdded(summaryEntry); + mGroupManager.onEntryAdded(childEntry); + + // Notify that entry changed groups. + StatusBarNotification oldNotification = childEntry.notification; + StatusBarNotification newSbn = spy(childEntry.notification.clone()); + doReturn("other_group").when(newSbn).getGroupKey(); + childEntry.notification = newSbn; + mGroupManager.onEntryUpdated(childEntry, oldNotification); + + assertFalse(mGroupAlertTransferHelper.isAlertTransferPending(childEntry)); + } + + @Test + public void testUpdateChildToSummaryDoesNotTransfer() { + NotificationData.Entry summaryEntry = + mGroupTestHelper.createSummaryNotification(Notification.GROUP_ALERT_SUMMARY); + NotificationData.Entry childEntry = + mGroupTestHelper.createChildNotification(Notification.GROUP_ALERT_SUMMARY); + when(childEntry.row.isInflationFlagSet(mHeadsUpManager.getContentFlag())).thenReturn(false); + mHeadsUpManager.showNotification(summaryEntry); + // Trigger a transfer of alert state from summary to child. + mGroupManager.onEntryAdded(summaryEntry); + mGroupManager.onEntryAdded(childEntry); + + // Update that child to a summary. + StatusBarNotification oldNotification = childEntry.notification; + childEntry.notification = mGroupTestHelper.createSummaryNotification( + Notification.GROUP_ALERT_SUMMARY).notification; + mGroupManager.onEntryUpdated(childEntry, oldNotification); + + assertFalse(mGroupAlertTransferHelper.isAlertTransferPending(childEntry)); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerTest.java index 464f74b3aa36..1483ae5f2a12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupManagerTest.java @@ -21,25 +21,15 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.verify; -import android.app.ActivityManager; -import android.app.Notification; -import android.os.UserHandle; -import android.service.notification.StatusBarNotification; import android.support.test.filters.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; -import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.AmbientPulseManager; import com.android.systemui.statusbar.notification.NotificationData; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; -import com.android.systemui.statusbar.phone.NotificationGroupManager.OnGroupChangeListener; import com.android.systemui.statusbar.policy.HeadsUpManager; import org.junit.Before; @@ -58,11 +48,9 @@ public class NotificationGroupManagerTest extends SysuiTestCase { @Rule public MockitoRule rule = MockitoJUnit.rule(); - private static final String TEST_CHANNEL_ID = "test_channel"; - private static final String TEST_GROUP_ID = "test_group"; - private static final String TEST_PACKAGE_NAME = "test_pkg"; private NotificationGroupManager mGroupManager; - private int mId = 0; + private final NotificationGroupTestHelper mGroupTestHelper = + new NotificationGroupTestHelper(mContext); @Mock HeadsUpManager mHeadsUpManager; @Mock AmbientPulseManager mAmbientPulseManager; @@ -77,13 +65,12 @@ public class NotificationGroupManagerTest extends SysuiTestCase { private void initializeGroupManager() { mGroupManager = new NotificationGroupManager(); mGroupManager.setHeadsUpManager(mHeadsUpManager); - mGroupManager.setOnGroupChangeListener(mock(OnGroupChangeListener.class)); } @Test public void testIsOnlyChildInGroup() { - NotificationData.Entry childEntry = createChildNotification(); - NotificationData.Entry summaryEntry = createSummaryNotification(); + NotificationData.Entry childEntry = mGroupTestHelper.createChildNotification(); + NotificationData.Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); mGroupManager.onEntryAdded(summaryEntry); mGroupManager.onEntryAdded(childEntry); @@ -93,24 +80,24 @@ public class NotificationGroupManagerTest extends SysuiTestCase { @Test public void testIsChildInGroupWithSummary() { - NotificationData.Entry childEntry = createChildNotification(); - NotificationData.Entry summaryEntry = createSummaryNotification(); + NotificationData.Entry childEntry = mGroupTestHelper.createChildNotification(); + NotificationData.Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); mGroupManager.onEntryAdded(summaryEntry); mGroupManager.onEntryAdded(childEntry); - mGroupManager.onEntryAdded(createChildNotification()); + mGroupManager.onEntryAdded(mGroupTestHelper.createChildNotification()); assertTrue(mGroupManager.isChildInGroupWithSummary(childEntry.notification)); } @Test public void testIsSummaryOfGroupWithChildren() { - NotificationData.Entry childEntry = createChildNotification(); - NotificationData.Entry summaryEntry = createSummaryNotification(); + NotificationData.Entry childEntry = mGroupTestHelper.createChildNotification(); + NotificationData.Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); mGroupManager.onEntryAdded(summaryEntry); mGroupManager.onEntryAdded(childEntry); - mGroupManager.onEntryAdded(createChildNotification()); + mGroupManager.onEntryAdded(mGroupTestHelper.createChildNotification()); assertTrue(mGroupManager.isSummaryOfGroup(summaryEntry.notification)); assertEquals(summaryEntry.row, mGroupManager.getGroupSummary(childEntry.notification)); @@ -118,11 +105,11 @@ public class NotificationGroupManagerTest extends SysuiTestCase { @Test public void testRemoveChildFromGroupWithSummary() { - NotificationData.Entry childEntry = createChildNotification(); - NotificationData.Entry summaryEntry = createSummaryNotification(); + NotificationData.Entry childEntry = mGroupTestHelper.createChildNotification(); + NotificationData.Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); mGroupManager.onEntryAdded(summaryEntry); mGroupManager.onEntryAdded(childEntry); - mGroupManager.onEntryAdded(createChildNotification()); + mGroupManager.onEntryAdded(mGroupTestHelper.createChildNotification()); mGroupManager.onEntryRemoved(childEntry); @@ -131,11 +118,11 @@ public class NotificationGroupManagerTest extends SysuiTestCase { @Test public void testRemoveSummaryFromGroupWithSummary() { - NotificationData.Entry childEntry = createChildNotification(); - NotificationData.Entry summaryEntry = createSummaryNotification(); + NotificationData.Entry childEntry = mGroupTestHelper.createChildNotification(); + NotificationData.Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); mGroupManager.onEntryAdded(summaryEntry); mGroupManager.onEntryAdded(childEntry); - mGroupManager.onEntryAdded(createChildNotification()); + mGroupManager.onEntryAdded(mGroupTestHelper.createChildNotification()); mGroupManager.onEntryRemoved(summaryEntry); @@ -145,11 +132,11 @@ public class NotificationGroupManagerTest extends SysuiTestCase { @Test public void testHeadsUpEntryIsIsolated() { - NotificationData.Entry childEntry = createChildNotification(); - NotificationData.Entry summaryEntry = createSummaryNotification(); + NotificationData.Entry childEntry = mGroupTestHelper.createChildNotification(); + NotificationData.Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); mGroupManager.onEntryAdded(summaryEntry); mGroupManager.onEntryAdded(childEntry); - mGroupManager.onEntryAdded(createChildNotification()); + mGroupManager.onEntryAdded(mGroupTestHelper.createChildNotification()); when(mHeadsUpManager.isAlerting(childEntry.key)).thenReturn(true); mGroupManager.onHeadsUpStateChanged(childEntry, true); @@ -163,12 +150,11 @@ public class NotificationGroupManagerTest extends SysuiTestCase { @Test public void testAmbientPulseEntryIsIsolated() { - mGroupManager.setDozing(true); - NotificationData.Entry childEntry = createChildNotification(); - NotificationData.Entry summaryEntry = createSummaryNotification(); + NotificationData.Entry childEntry = mGroupTestHelper.createChildNotification(); + NotificationData.Entry summaryEntry = mGroupTestHelper.createSummaryNotification(); mGroupManager.onEntryAdded(summaryEntry); mGroupManager.onEntryAdded(childEntry); - mGroupManager.onEntryAdded(createChildNotification()); + mGroupManager.onEntryAdded(mGroupTestHelper.createChildNotification()); when(mAmbientPulseManager.isAlerting(childEntry.key)).thenReturn(true); mGroupManager.onAmbientStateChanged(childEntry, true); @@ -179,128 +165,4 @@ public class NotificationGroupManagerTest extends SysuiTestCase { assertEquals(summaryEntry.row, mGroupManager.getLogicalGroupSummary(childEntry.notification)); } - - @Test - public void testSuppressedSummaryHeadsUpTransfersToChild() { - NotificationData.Entry summaryEntry = createSummaryNotification(); - when(mHeadsUpManager.isAlerting(summaryEntry.key)).thenReturn(true); - NotificationData.Entry childEntry = createChildNotification(); - - // Summary will be suppressed because there is only one child. - mGroupManager.onEntryAdded(summaryEntry); - mGroupManager.onEntryAdded(childEntry); - - // A suppressed summary should transfer its heads up state to the child. - verify(mHeadsUpManager, never()).showNotification(summaryEntry); - verify(mHeadsUpManager).showNotification(childEntry); - } - - @Test - public void testSuppressedSummaryHeadsUpTransfersToChildButBackAgain() { - mHeadsUpManager = new HeadsUpManager(mContext) {}; - mGroupManager.setHeadsUpManager(mHeadsUpManager); - NotificationData.Entry summaryEntry = - createSummaryNotification(Notification.GROUP_ALERT_SUMMARY); - NotificationData.Entry childEntry = - createChildNotification(Notification.GROUP_ALERT_SUMMARY); - NotificationData.Entry childEntry2 = - createChildNotification(Notification.GROUP_ALERT_SUMMARY); - mHeadsUpManager.showNotification(summaryEntry); - // Trigger a transfer of heads up state from summary to child. - mGroupManager.onEntryAdded(summaryEntry); - mGroupManager.onEntryAdded(childEntry); - - // Add second child notification so that summary is no longer suppressed. - mGroupManager.onEntryAdded(childEntry2); - - // The heads up state should transfer back to the summary as there is now more than one - // child and the summary should no longer be suppressed. - assertTrue(mHeadsUpManager.isAlerting(summaryEntry.key)); - assertFalse(mHeadsUpManager.isAlerting(childEntry.key)); - } - - @Test - public void testSuppressedSummaryAmbientPulseTransfersToChild() { - mGroupManager.setDozing(true); - NotificationData.Entry summaryEntry = createSummaryNotification(); - when(mAmbientPulseManager.isAlerting(summaryEntry.key)).thenReturn(true); - NotificationData.Entry childEntry = createChildNotification(); - - // Summary will be suppressed because there is only one child. - mGroupManager.onEntryAdded(summaryEntry); - mGroupManager.onEntryAdded(childEntry); - - // A suppressed summary should transfer its ambient state to the child. - verify(mAmbientPulseManager, never()).showNotification(summaryEntry); - verify(mAmbientPulseManager).showNotification(childEntry); - } - - @Test - public void testSuppressedSummaryAmbientPulseTransfersToChildButBackAgain() { - mGroupManager.setDozing(true); - mAmbientPulseManager = new AmbientPulseManager(mContext); - mDependency.injectTestDependency(AmbientPulseManager.class, mAmbientPulseManager); - initializeGroupManager(); - NotificationData.Entry summaryEntry = - createSummaryNotification(Notification.GROUP_ALERT_SUMMARY); - NotificationData.Entry childEntry = - createChildNotification(Notification.GROUP_ALERT_SUMMARY); - NotificationData.Entry childEntry2 = - createChildNotification(Notification.GROUP_ALERT_SUMMARY); - mAmbientPulseManager.showNotification(summaryEntry); - // Trigger a transfer of ambient state from summary to child. - mGroupManager.onEntryAdded(summaryEntry); - mGroupManager.onEntryAdded(childEntry); - - // Add second child notification so that summary is no longer suppressed. - mGroupManager.onEntryAdded(childEntry2); - - // The ambient state should transfer back to the summary as there is now more than one - // child and the summary should no longer be suppressed. - assertTrue(mAmbientPulseManager.isAlerting(summaryEntry.key)); - assertFalse(mAmbientPulseManager.isAlerting(childEntry.key)); - } - - private NotificationData.Entry createSummaryNotification() { - return createSummaryNotification(Notification.GROUP_ALERT_ALL); - } - - private NotificationData.Entry createSummaryNotification(int groupAlertBehavior) { - return createEntry(true, groupAlertBehavior); - } - - private NotificationData.Entry createChildNotification() { - return createChildNotification(Notification.GROUP_ALERT_ALL); - } - - private NotificationData.Entry createChildNotification(int groupAlertBehavior) { - return createEntry(false, groupAlertBehavior); - } - - private NotificationData.Entry createEntry(boolean isSummary, int groupAlertBehavior) { - Notification notif = new Notification.Builder(mContext, TEST_CHANNEL_ID) - .setContentTitle("Title") - .setSmallIcon(R.drawable.ic_person) - .setGroupAlertBehavior(groupAlertBehavior) - .setGroupSummary(isSummary) - .setGroup(TEST_GROUP_ID) - .build(); - StatusBarNotification sbn = new StatusBarNotification( - TEST_PACKAGE_NAME /* pkg */, - TEST_PACKAGE_NAME, - mId++, - null /* tag */, - 0, /* uid */ - 0 /* initialPid */, - notif, - new UserHandle(ActivityManager.getCurrentUser()), - null /* overrideGroupKey */, - 0 /* postTime */); - NotificationData.Entry entry = new NotificationData.Entry(sbn); - ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); - entry.row = row; - when(row.getEntry()).thenReturn(entry); - when(row.getStatusBarNotification()).thenReturn(sbn); - return entry; - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java new file mode 100644 index 000000000000..01f44fd4d0cc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupTestHelper.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 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.phone; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.Notification; +import android.content.Context; +import android.os.UserHandle; +import android.service.notification.StatusBarNotification; + +import com.android.systemui.R; +import com.android.systemui.statusbar.notification.NotificationData; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; + +/** + * Helper class for creating groups/summaries without having to inflate them. + */ +public final class NotificationGroupTestHelper { + private static final String TEST_CHANNEL_ID = "test_channel"; + private static final String TEST_GROUP_ID = "test_group"; + private static final String TEST_PACKAGE_NAME = "test_pkg"; + private int mId = 0; + private final Context mContext; + + public NotificationGroupTestHelper(Context context) { + mContext = context; + } + + public NotificationData.Entry createSummaryNotification() { + return createSummaryNotification(Notification.GROUP_ALERT_ALL); + } + + public NotificationData.Entry createSummaryNotification(int groupAlertBehavior) { + return createEntry(true, groupAlertBehavior); + } + + public NotificationData.Entry createChildNotification() { + return createChildNotification(Notification.GROUP_ALERT_ALL); + } + + public NotificationData.Entry createChildNotification(int groupAlertBehavior) { + return createEntry(false, groupAlertBehavior); + } + + public NotificationData.Entry createEntry(boolean isSummary, int groupAlertBehavior) { + Notification notif = new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentTitle("Title") + .setSmallIcon(R.drawable.ic_person) + .setGroupAlertBehavior(groupAlertBehavior) + .setGroupSummary(isSummary) + .setGroup(TEST_GROUP_ID) + .build(); + StatusBarNotification sbn = new StatusBarNotification( + TEST_PACKAGE_NAME /* pkg */, + TEST_PACKAGE_NAME, + mId++, + null /* tag */, + 0, /* uid */ + 0 /* initialPid */, + notif, + new UserHandle(ActivityManager.getCurrentUser()), + null /* overrideGroupKey */, + 0 /* postTime */); + NotificationData.Entry entry = new NotificationData.Entry(sbn); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + entry.row = row; + when(row.getEntry()).thenReturn(entry); + when(row.getStatusBarNotification()).thenReturn(sbn); + when(row.isInflationFlagSet(anyInt())).thenReturn(true); + return entry; + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index 7dc858ff9308..e9e8eb785d1a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -209,11 +209,11 @@ public class StatusBarTest extends SysuiTestCase { mNotificationLogger, mVisualStabilityManager, mViewHierarchyManager, mEntryManager, mScrimController, mBiometricUnlockController, mKeyguardViewMediator, mRemoteInputManager, mock(NotificationGroupManager.class), - mock(FalsingManager.class), mock(StatusBarWindowController.class), - mock(NotificationIconAreaController.class), mock(DozeScrimController.class), - mock(NotificationShelf.class), mLockscreenUserManager, - mCommandQueue, - mNotificationPresenter, mock(BubbleController.class)); + mock(NotificationGroupAlertTransferHelper.class), mock(FalsingManager.class), + mock(StatusBarWindowController.class), mock(NotificationIconAreaController.class), + mock(DozeScrimController.class), mock(NotificationShelf.class), + mLockscreenUserManager, mCommandQueue, mNotificationPresenter, + mock(BubbleController.class)); mStatusBar.mContext = mContext; mStatusBar.mComponents = mContext.getComponents(); mStatusBar.putComponent(StatusBar.class, mStatusBar); @@ -634,6 +634,7 @@ public class StatusBarTest extends SysuiTestCase { KeyguardViewMediator keyguardViewMediator, NotificationRemoteInputManager notificationRemoteInputManager, NotificationGroupManager notificationGroupManager, + NotificationGroupAlertTransferHelper notificationGroupAlertTransferHelper, FalsingManager falsingManager, StatusBarWindowController statusBarWindowController, NotificationIconAreaController notificationIconAreaController, @@ -662,6 +663,7 @@ public class StatusBarTest extends SysuiTestCase { mKeyguardViewMediator = keyguardViewMediator; mRemoteInputManager = notificationRemoteInputManager; mGroupManager = notificationGroupManager; + mGroupAlertTransferHelper = notificationGroupAlertTransferHelper; mFalsingManager = falsingManager; mStatusBarWindowController = statusBarWindowController; mNotificationIconAreaController = notificationIconAreaController; |