Merge "Add initial implementation of NotifCollection"

This commit is contained in:
Ned Burns
2019-10-17 15:13:47 +00:00
committed by Android (Google) Code Review
13 changed files with 1399 additions and 79 deletions

View File

@@ -87,6 +87,7 @@ import com.android.systemui.plugins.qs.QS;
import com.android.systemui.qs.car.CarQSFragment;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.statusbar.NavigationBarController;
import com.android.systemui.statusbar.NotificationListener;
@@ -102,7 +103,7 @@ import com.android.systemui.statusbar.car.hvac.HvacController;
import com.android.systemui.statusbar.car.hvac.TemperatureView;
import com.android.systemui.statusbar.notification.BypassHeadsUpNotifier;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
import com.android.systemui.statusbar.notification.NotifPipelineInitializer;
import com.android.systemui.statusbar.notification.NewNotifPipeline;
import com.android.systemui.statusbar.notification.NotificationAlertingManager;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
@@ -140,6 +141,8 @@ import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.Lazy;
/**
* A status bar (and navigation bar) tailored for the automotive use case.
*/
@@ -252,6 +255,7 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
@Inject
public CarStatusBar(
Context context,
FeatureFlags featureFlags,
LightBarController lightBarController,
AutoHideController autoHideController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -266,7 +270,7 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
DynamicPrivacyController dynamicPrivacyController,
BypassHeadsUpNotifier bypassHeadsUpNotifier,
@Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowNotificationLongPress,
NotifPipelineInitializer notifPipelineInitializer,
Lazy<NewNotifPipeline> newNotifPipeline,
FalsingManager falsingManager,
BroadcastDispatcher broadcastDispatcher,
RemoteInputQuickSettingsDisabler remoteInputQuickSettingsDisabler,
@@ -309,6 +313,7 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
DozeParameters dozeParameters) {
super(
context,
featureFlags,
lightBarController,
autoHideController,
keyguardUpdateMonitor,
@@ -323,7 +328,7 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
dynamicPrivacyController,
bypassHeadsUpNotifier,
allowNotificationLongPress,
notifPipelineInitializer,
newNotifPipeline,
falsingManager,
broadcastDispatcher,
remoteInputQuickSettingsDisabler,

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar;
import android.annotation.NonNull;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.provider.DeviceConfig;
import android.util.ArrayMap;
import java.util.Map;
import javax.inject.Inject;
/**
* Class to manage simple DeviceConfig-based feature flags.
*
* To enable or disable a flag, run:
*
* {@code
* $ adb shell device_config put systemui <key> <true|false>
* }
*
* You will probably need to @{$ adb reboot} afterwards in order for the code to pick up the change.
*/
public class FeatureFlags {
private final Map<String, Boolean> mCachedDeviceConfigFlags = new ArrayMap<>();
@Inject
public FeatureFlags() {
DeviceConfig.addOnPropertiesChangedListener(
"systemui",
new HandlerExecutor(new Handler(Looper.getMainLooper())),
this::onPropertiesChanged);
}
public boolean isNewNotifPipelineEnabled() {
return getDeviceConfigFlag("notification.newpipeline.enabled", false);
}
private void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
synchronized (mCachedDeviceConfigFlags) {
for (String key : properties.getKeyset()) {
mCachedDeviceConfigFlags.remove(key);
}
}
}
private boolean getDeviceConfigFlag(String key, boolean defaultValue) {
synchronized (mCachedDeviceConfigFlags) {
Boolean flag = mCachedDeviceConfigFlags.get(key);
if (flag == null) {
flag = DeviceConfig.getBoolean("systemui", key, defaultValue);
mCachedDeviceConfigFlags.put(key, flag);
}
return flag;
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification;
import android.util.Log;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.notification.collection.NotifCollection;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Initialization code for the new notification pipeline.
*/
@Singleton
public class NewNotifPipeline {
private final NotifCollection mNotifCollection;
@Inject
public NewNotifPipeline(
NotifCollection notifCollection) {
mNotifCollection = notifCollection;
}
/** Hooks the new pipeline up to NotificationManager */
public void initialize(
NotificationListener notificationService) {
mNotifCollection.attach(notificationService);
Log.d(TAG, "Notif pipeline initialized");
}
private static final String TAG = "NewNotifPipeline";
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import com.android.systemui.statusbar.NotificationListener;
import javax.inject.Inject;
/**
* Initialization code for the new notification pipeline.
*/
public class NotifPipelineInitializer {
@Inject
public NotifPipelineInitializer() {
}
public void initialize(
NotificationListener notificationService) {
// TODO Put real code here
notificationService.setDownstreamListener(new NotificationListener.NotifServiceListener() {
@Override
public void onNotificationPosted(StatusBarNotification sbn,
NotificationListenerService.RankingMap rankingMap) {
Log.d(TAG, "onNotificationPosted " + sbn.getKey());
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn,
NotificationListenerService.RankingMap rankingMap) {
Log.d(TAG, "onNotificationRemoved " + sbn.getKey());
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn,
NotificationListenerService.RankingMap rankingMap, int reason) {
Log.d(TAG, "onNotificationRemoved " + sbn.getKey());
}
@Override
public void onNotificationRankingUpdate(
NotificationListenerService.RankingMap rankingMap) {
Log.d(TAG, "onNotificationRankingUpdate");
}
});
}
private static final String TAG = "NotifInitializer";
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection;
import android.service.notification.NotificationStats.DismissalSentiment;
import android.service.notification.NotificationStats.DismissalSurface;
import com.android.internal.statusbar.NotificationVisibility;
/** Information that must be supplied when dismissing a notification on the behalf of the user. */
public class DismissedByUserStats {
public final @DismissalSurface int dismissalSurface;
public final @DismissalSentiment int dismissalSentiment;
public final NotificationVisibility notificationVisibility;
public DismissedByUserStats(
@DismissalSurface int dismissalSurface,
@DismissalSentiment int dismissalSentiment,
NotificationVisibility notificationVisibility) {
this.dismissalSurface = dismissalSurface;
this.dismissalSentiment = dismissalSentiment;
this.notificationVisibility = notificationVisibility;
}
}

View File

@@ -0,0 +1,426 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED;
import static android.service.notification.NotificationListenerService.REASON_CLICK;
import static android.service.notification.NotificationListenerService.REASON_ERROR;
import static android.service.notification.NotificationListenerService.REASON_GROUP_OPTIMIZATION;
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_CHANGED;
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_SUSPENDED;
import static android.service.notification.NotificationListenerService.REASON_PROFILE_TURNED_OFF;
import static android.service.notification.NotificationListenerService.REASON_SNOOZED;
import static android.service.notification.NotificationListenerService.REASON_TIMEOUT;
import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED;
import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED;
import static com.android.internal.util.Preconditions.checkNotNull;
import android.annotation.IntDef;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.RemoteException;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotifServiceListener;
import com.android.systemui.util.Assert;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Keeps a record of all of the "active" notifications, i.e. the notifications that are currently
* posted to the phone. This collection is unsorted, ungrouped, and unfiltered. Just because a
* notification appears in this collection doesn't mean that it's currently present in the shade
* (notifications can be hidden for a variety of reasons). Code that cares about what notifications
* are *visible* right now should register listeners later in the pipeline.
*
* Each notification is represented by a {@link NotificationEntry}, which is itself made up of two
* parts: a {@link StatusBarNotification} and a {@link Ranking}. When notifications are updated,
* their underlying SBNs and Rankings are swapped out, but the enclosing NotificationEntry (and its
* associated key) remain the same. In general, an SBN can only be updated when the notification is
* reposted by the source app; Rankings are updated much more often, usually every time there is an
* update from any kind from NotificationManager.
*
* In general, this collection closely mirrors the list maintained by NotificationManager, but it
* can occasionally diverge due to lifetime extenders (see
* {@link #addNotificationLifetimeExtender(NotifLifetimeExtender)}).
*
* Interested parties can register listeners
* ({@link #addCollectionListener(NotifCollectionListener)}) to be informed when notifications are
* added, updated, or removed.
*/
@MainThread
@Singleton
public class NotifCollection {
private final IStatusBarService mStatusBarService;
private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
private final Collection<NotificationEntry> mReadOnlyNotificationSet =
Collections.unmodifiableCollection(mNotificationSet.values());
@Nullable private NotifListBuilder mListBuilder;
private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>();
private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
private boolean mAttached = false;
private boolean mAmDispatchingToOtherCode;
@Inject
public NotifCollection(IStatusBarService statusBarService) {
Assert.isMainThread();
mStatusBarService = statusBarService;
}
/** Initializes the NotifCollection and registers it to receive notification events. */
public void attach(NotificationListener listenerService) {
Assert.isMainThread();
if (mAttached) {
throw new RuntimeException("attach() called twice");
}
mAttached = true;
listenerService.setDownstreamListener(mNotifServiceListener);
}
/**
* Sets the class responsible for converting the collection into the list of currently-visible
* notifications.
*/
public void setListBuilder(NotifListBuilder listBuilder) {
Assert.isMainThread();
mListBuilder = listBuilder;
}
/**
* Returns the list of "active" notifications, i.e. the notifications that are currently posted
* to the phone. In general, this tracks closely to the list maintained by NotificationManager,
* but it can diverge slightly due to lifetime extenders.
*
* The returned list is read-only, unsorted, unfiltered, and ungrouped.
*/
public Collection<NotificationEntry> getNotifs() {
Assert.isMainThread();
return mReadOnlyNotificationSet;
}
/**
* Registers a listener to be informed when notifications are added, removed or updated.
*/
public void addCollectionListener(NotifCollectionListener listener) {
Assert.isMainThread();
mNotifCollectionListeners.add(listener);
}
/**
* Registers a lifetime extender. Lifetime extenders can cause notifications that have been
* dismissed or retracted to be temporarily retained in the collection.
*/
public void addNotificationLifetimeExtender(NotifLifetimeExtender extender) {
Assert.isMainThread();
checkForReentrantCall();
if (mLifetimeExtenders.contains(extender)) {
throw new IllegalArgumentException("Extender " + extender + " already added.");
}
mLifetimeExtenders.add(extender);
extender.setCallback(this::onEndLifetimeExtension);
}
/**
* Dismiss a notification on behalf of the user.
*/
public void dismissNotification(
NotificationEntry entry,
@CancellationReason int reason,
@NonNull DismissedByUserStats stats) {
Assert.isMainThread();
checkNotNull(stats);
checkForReentrantCall();
removeNotification(entry.key(), null, reason, stats);
}
private void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
Assert.isMainThread();
NotificationEntry entry = mNotificationSet.get(sbn.getKey());
if (entry == null) {
// A new notification!
Log.d(TAG, "POSTED " + sbn.getKey());
entry = new NotificationEntry(sbn, requireRanking(rankingMap, sbn.getKey()));
mNotificationSet.put(sbn.getKey(), entry);
applyRanking(rankingMap);
dispatchOnEntryAdded(entry);
} else {
// Update to an existing entry
Log.d(TAG, "UPDATED " + sbn.getKey());
// Notification is updated so it is essentially re-added and thus alive again. Don't
// need to keep its lifetime extended.
cancelLifetimeExtension(entry);
entry.setNotification(sbn);
applyRanking(rankingMap);
dispatchOnEntryUpdated(entry);
}
rebuildList();
}
private void onNotificationRemoved(
StatusBarNotification sbn,
@Nullable RankingMap rankingMap,
int reason) {
Assert.isMainThread();
Log.d(TAG, "REMOVED " + sbn.getKey() + " reason=" + reason);
removeNotification(sbn.getKey(), rankingMap, reason, null);
}
private void onNotificationRankingUpdate(RankingMap rankingMap) {
Assert.isMainThread();
applyRanking(rankingMap);
rebuildList();
}
private void removeNotification(
String key,
@Nullable RankingMap rankingMap,
@CancellationReason int reason,
DismissedByUserStats dismissedByUserStats) {
NotificationEntry entry = mNotificationSet.get(key);
if (entry == null) {
throw new IllegalStateException("No notification to remove with key " + key);
}
entry.mLifetimeExtenders.clear();
mAmDispatchingToOtherCode = true;
for (NotifLifetimeExtender extender : mLifetimeExtenders) {
if (extender.shouldExtendLifetime(entry, reason)) {
entry.mLifetimeExtenders.add(extender);
}
}
mAmDispatchingToOtherCode = false;
if (!isLifetimeExtended(entry)) {
mNotificationSet.remove(entry.key());
if (dismissedByUserStats != null) {
try {
mStatusBarService.onNotificationClear(
entry.sbn().getPackageName(),
entry.sbn().getTag(),
entry.sbn().getId(),
entry.sbn().getUser().getIdentifier(),
entry.sbn().getKey(),
dismissedByUserStats.dismissalSurface,
dismissedByUserStats.dismissalSentiment,
dismissedByUserStats.notificationVisibility);
} catch (RemoteException e) {
// system process is dead if we're here.
}
}
if (rankingMap != null) {
applyRanking(rankingMap);
}
dispatchOnEntryRemoved(entry, reason, dismissedByUserStats != null /* removedByUser */);
}
rebuildList();
}
private void applyRanking(RankingMap rankingMap) {
for (NotificationEntry entry : mNotificationSet.values()) {
if (!isLifetimeExtended(entry)) {
Ranking ranking = requireRanking(rankingMap, entry.key());
entry.setRanking(ranking);
}
}
}
private void rebuildList() {
if (mListBuilder != null) {
mListBuilder.onBuildList(mReadOnlyNotificationSet);
}
}
private void onEndLifetimeExtension(NotifLifetimeExtender extender, NotificationEntry entry) {
Assert.isMainThread();
if (!mAttached) {
return;
}
checkForReentrantCall();
if (!entry.mLifetimeExtenders.remove(extender)) {
throw new IllegalStateException(
String.format(
"Cannot end lifetime extension for extender \"%s\" (%s)",
extender.getName(),
extender));
}
if (!isLifetimeExtended(entry)) {
// TODO: This doesn't need to be undefined -- we can set either EXTENDER_EXPIRED or
// save the original reason
removeNotification(entry.key(), null, REASON_UNKNOWN, null);
}
}
private void cancelLifetimeExtension(NotificationEntry entry) {
mAmDispatchingToOtherCode = true;
for (NotifLifetimeExtender extender : entry.mLifetimeExtenders) {
extender.cancelLifetimeExtension(entry);
}
mAmDispatchingToOtherCode = false;
entry.mLifetimeExtenders.clear();
}
private boolean isLifetimeExtended(NotificationEntry entry) {
return entry.mLifetimeExtenders.size() > 0;
}
private void checkForReentrantCall() {
if (mAmDispatchingToOtherCode) {
throw new IllegalStateException("Reentrant call detected");
}
}
private static Ranking requireRanking(RankingMap rankingMap, String key) {
// TODO: Modify RankingMap so that we don't have to make a copy here
Ranking ranking = new Ranking();
if (!rankingMap.getRanking(key, ranking)) {
throw new IllegalArgumentException("Ranking map doesn't contain key: " + key);
}
return ranking;
}
private void dispatchOnEntryAdded(NotificationEntry entry) {
mAmDispatchingToOtherCode = true;
if (mListBuilder != null) {
mListBuilder.onBeginDispatchToListeners();
}
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onEntryAdded(entry);
}
mAmDispatchingToOtherCode = false;
}
private void dispatchOnEntryUpdated(NotificationEntry entry) {
mAmDispatchingToOtherCode = true;
if (mListBuilder != null) {
mListBuilder.onBeginDispatchToListeners();
}
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onEntryUpdated(entry);
}
mAmDispatchingToOtherCode = false;
}
private void dispatchOnEntryRemoved(
NotificationEntry entry,
@CancellationReason int reason,
boolean removedByUser) {
mAmDispatchingToOtherCode = true;
if (mListBuilder != null) {
mListBuilder.onBeginDispatchToListeners();
}
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onEntryRemoved(entry, reason, removedByUser);
}
mAmDispatchingToOtherCode = false;
}
private final NotifServiceListener mNotifServiceListener = new NotifServiceListener() {
@Override
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
NotifCollection.this.onNotificationPosted(sbn, rankingMap);
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
NotifCollection.this.onNotificationRemoved(sbn, rankingMap, REASON_UNKNOWN);
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
int reason) {
NotifCollection.this.onNotificationRemoved(sbn, rankingMap, reason);
}
@Override
public void onNotificationRankingUpdate(RankingMap rankingMap) {
NotifCollection.this.onNotificationRankingUpdate(rankingMap);
}
};
private static final String TAG = "NotifCollection";
@IntDef(prefix = { "REASON_" }, value = {
REASON_UNKNOWN,
REASON_CLICK,
REASON_CANCEL_ALL,
REASON_ERROR,
REASON_PACKAGE_CHANGED,
REASON_USER_STOPPED,
REASON_PACKAGE_BANNED,
REASON_APP_CANCEL,
REASON_APP_CANCEL_ALL,
REASON_LISTENER_CANCEL,
REASON_LISTENER_CANCEL_ALL,
REASON_GROUP_SUMMARY_CANCELED,
REASON_GROUP_OPTIMIZATION,
REASON_PACKAGE_SUSPENDED,
REASON_PROFILE_TURNED_OFF,
REASON_UNAUTOBUNDLED,
REASON_CHANNEL_BANNED,
REASON_SNOOZED,
REASON_TIMEOUT,
})
@Retention(RetentionPolicy.SOURCE)
@interface CancellationReason {}
public static final int REASON_UNKNOWN = 0;
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection;
import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
/**
* Listener interface for {@link NotifCollection}.
*/
public interface NotifCollectionListener {
/**
* Called whenever a notification with a new key is posted.
*/
default void onEntryAdded(NotificationEntry entry) {
}
/**
* Called whenever a notification with the same key as an existing notification is posted. By
* the time this listener is called, the entry's SBN and Ranking will already have been updated.
*/
default void onEntryUpdated(NotificationEntry entry) {
}
/**
* Called immediately after a notification has been removed from the collection.
*/
default void onEntryRemoved(
NotificationEntry entry,
@CancellationReason int reason,
boolean removedByUser) {
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection;
import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
/**
* A way for other code to temporarily extend the lifetime of a notification after it has been
* retracted. See {@link NotifCollection#addNotificationLifetimeExtender(NotifLifetimeExtender)}.
*/
public interface NotifLifetimeExtender {
/** Name to associate with this extender (for the purposes of debugging) */
String getName();
/**
* Called on the extender immediately after it has been registered. The extender should hang on
* to this callback and execute it whenever it no longer needs to extend the lifetime of a
* notification.
*/
void setCallback(OnEndLifetimeExtensionCallback callback);
/**
* Called by the NotifCollection whenever a notification has been retracted (by the app) or
* dismissed (by the user). If the extender returns true, it is considered to be extending the
* lifetime of that notification. Lifetime-extended notifications are kept around until all
* active extenders expire their extension by calling onEndLifetimeExtension(). This method is
* called on all lifetime extenders even if earlier ones return true (in other words, multiple
* lifetime extenders can be extending a notification at the same time).
*/
boolean shouldExtendLifetime(NotificationEntry entry, @CancellationReason int reason);
/**
* Called by the NotifCollection to inform a lifetime extender that its extension of a notif
* is no longer valid (usually because the notif has been reposted and so no longer needs
* lifetime extension). The extender should clean up any references it has to the notif in
* question.
*/
void cancelLifetimeExtension(NotificationEntry entry);
/** Callback for notifying the NotifCollection that a lifetime extension has expired. */
interface OnEndLifetimeExtensionCallback {
void onEndLifetimeExtension(NotifLifetimeExtender extender, NotificationEntry entry);
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection;
import java.util.Collection;
/**
* Interface for the class responsible for converting a NotifCollection into the final sorted,
* filtered, and grouped list of currently visible notifications.
*/
public interface NotifListBuilder {
/**
* Called after the NotifCollection has received an update from NotificationManager but before
* it dispatches any change events to its listeners. This is to inform the list builder that
* the first stage of the pipeline has been triggered. After events have been dispatched,
* onBuildList() will be called.
*
* While onBuildList() is always called after this method is called, the converse is not always
* true: sometimes the NotifCollection applies an update that does not need to dispatch events,
* in which case this method will be skipped and onBuildList will be called directly.
*/
void onBeginDispatchToListeners();
/**
* Called by the NotifCollection to indicate that something in the collection has changed and
* that the list builder should regenerate the list.
*/
void onBuildList(Collection<NotificationEntry> entries);
}

View File

@@ -94,6 +94,20 @@ public final class NotificationEntry {
public StatusBarNotification notification;
private Ranking mRanking;
/*
* Bookkeeping members
*/
/** List of lifetime extenders that are extending the lifetime of this notification. */
final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
/*
* Old members
* TODO: Remove every member beneath this line if possible
*/
public boolean noisy;
public StatusBarIconView icon;
public StatusBarIconView expandedIcon;

View File

@@ -179,6 +179,7 @@ import com.android.systemui.statusbar.BackDropView;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.CrossFadeHelper;
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.GestureRecorder;
import com.android.systemui.statusbar.KeyboardShortcuts;
import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -198,7 +199,7 @@ import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
import com.android.systemui.statusbar.notification.BypassHeadsUpNotifier;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
import com.android.systemui.statusbar.notification.NotifPipelineInitializer;
import com.android.systemui.statusbar.notification.NewNotifPipeline;
import com.android.systemui.statusbar.notification.NotificationActivityStarter;
import com.android.systemui.statusbar.notification.NotificationAlertingManager;
import com.android.systemui.statusbar.notification.NotificationClicker;
@@ -246,6 +247,7 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Lazy;
import dagger.Subcomponent;
@Singleton
@@ -370,6 +372,7 @@ public class StatusBar extends SystemUI implements DemoMode,
private final Object mQueueLock = new Object();
private final FeatureFlags mFeatureFlags;
private final StatusBarIconController mIconController;
private final DozeLog mDozeLog;
private final InjectionInflationController mInjectionInflater;
@@ -381,7 +384,7 @@ public class StatusBar extends SystemUI implements DemoMode,
private final DynamicPrivacyController mDynamicPrivacyController;
private final BypassHeadsUpNotifier mBypassHeadsUpNotifier;
private final boolean mAllowNotificationLongPress;
private final NotifPipelineInitializer mNotifPipelineInitializer;
private final Lazy<NewNotifPipeline> mNewNotifPipeline;
private final FalsingManager mFalsingManager;
private final BroadcastDispatcher mBroadcastDispatcher;
private final ConfigurationController mConfigurationController;
@@ -621,6 +624,7 @@ public class StatusBar extends SystemUI implements DemoMode,
@Inject
public StatusBar(
Context context,
FeatureFlags featureFlags,
LightBarController lightBarController,
AutoHideController autoHideController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -635,7 +639,7 @@ public class StatusBar extends SystemUI implements DemoMode,
DynamicPrivacyController dynamicPrivacyController,
BypassHeadsUpNotifier bypassHeadsUpNotifier,
@Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowNotificationLongPress,
NotifPipelineInitializer notifPipelineInitializer,
Lazy<NewNotifPipeline> newNotifPipeline,
FalsingManager falsingManager,
BroadcastDispatcher broadcastDispatcher,
RemoteInputQuickSettingsDisabler remoteInputQuickSettingsDisabler,
@@ -677,6 +681,7 @@ public class StatusBar extends SystemUI implements DemoMode,
NotifLog notifLog,
DozeParameters dozeParameters) {
super(context);
mFeatureFlags = featureFlags;
mLightBarController = lightBarController;
mAutoHideController = autoHideController;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -691,7 +696,7 @@ public class StatusBar extends SystemUI implements DemoMode,
mDynamicPrivacyController = dynamicPrivacyController;
mBypassHeadsUpNotifier = bypassHeadsUpNotifier;
mAllowNotificationLongPress = allowNotificationLongPress;
mNotifPipelineInitializer = notifPipelineInitializer;
mNewNotifPipeline = newNotifPipeline;
mFalsingManager = falsingManager;
mBroadcastDispatcher = broadcastDispatcher;
mRemoteInputQuickSettingsDisabler = remoteInputQuickSettingsDisabler;
@@ -1211,7 +1216,9 @@ public class StatusBar extends SystemUI implements DemoMode,
mGroupAlertTransferHelper.bind(mEntryManager, mGroupManager);
mNotificationListController.bind();
mNotifPipelineInitializer.initialize(mNotificationListener);
if (mFeatureFlags.isNewNotifPipelineEnabled()) {
mNewNotifPipeline.get().initialize(mNotificationListener);
}
}
/**

View File

@@ -0,0 +1,625 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_CLICK;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_UNKNOWN;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.annotation.Nullable;
import android.os.RemoteException;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.util.ArrayMap;
import androidx.test.filters.SmallTest;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationEntryBuilder;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotifServiceListener;
import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
import com.android.systemui.util.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import java.util.Arrays;
import java.util.Map;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class NotifCollectionTest extends SysuiTestCase {
@Mock private IStatusBarService mStatusBarService;
@Mock private NotificationListener mListenerService;
@Spy private RecordingCollectionListener mCollectionListener;
@Spy private RecordingLifetimeExtender mExtender1 = new RecordingLifetimeExtender("Extender1");
@Spy private RecordingLifetimeExtender mExtender2 = new RecordingLifetimeExtender("Extender2");
@Spy private RecordingLifetimeExtender mExtender3 = new RecordingLifetimeExtender("Extender3");
@Captor private ArgumentCaptor<NotifServiceListener> mListenerCaptor;
@Captor private ArgumentCaptor<NotificationEntry> mEntryCaptor;
private NotifCollection mCollection;
private NotifServiceListener mServiceListener;
private NoManSimulator mNoMan;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
Assert.sMainLooper = TestableLooper.get(this).getLooper();
mCollection = new NotifCollection(mStatusBarService);
mCollection.attach(mListenerService);
mCollection.addCollectionListener(mCollectionListener);
// Capture the listener object that the collection registers with the listener service so
// we can simulate listener service events in tests below
verify(mListenerService).setDownstreamListener(mListenerCaptor.capture());
mServiceListener = checkNotNull(mListenerCaptor.getValue());
mNoMan = new NoManSimulator(mServiceListener);
}
@Test
public void testEventDispatchedWhenNotifPosted() {
// WHEN a notification is posted
PostedNotif notif1 = mNoMan.postNotif(
buildNotif(TEST_PACKAGE, 3)
.setRank(4747));
// THEN the listener is notified
verify(mCollectionListener).onEntryAdded(mEntryCaptor.capture());
NotificationEntry entry = mEntryCaptor.getValue();
assertEquals(notif1.key, entry.key());
assertEquals(notif1.sbn, entry.sbn());
assertEquals(notif1.ranking, entry.ranking());
}
@Test
public void testEventDispatchedWhenNotifUpdated() {
// GIVEN a collection with one notif
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
.setRank(4747));
// WHEN the notif is reposted
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
.setRank(89));
// THEN the listener is notified
verify(mCollectionListener).onEntryUpdated(mEntryCaptor.capture());
NotificationEntry entry = mEntryCaptor.getValue();
assertEquals(notif2.key, entry.key());
assertEquals(notif2.sbn, entry.sbn());
assertEquals(notif2.ranking, entry.ranking());
}
@Test
public void testEventDispatchedWhenNotifRemoved() {
// GIVEN a collection with one notif
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
clearInvocations(mCollectionListener);
PostedNotif notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
clearInvocations(mCollectionListener);
// WHEN a notif is retracted
mNoMan.retractNotif(notif.sbn, REASON_APP_CANCEL);
// THEN the listener is notified
verify(mCollectionListener).onEntryRemoved(entry, REASON_APP_CANCEL, false);
assertEquals(notif.sbn, entry.sbn());
assertEquals(notif.ranking, entry.ranking());
}
@Test
public void testRankingsAreUpdatedForOtherNotifs() {
// GIVEN a collection with one notif
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
.setRank(47));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN a new notif is posted, triggering a rerank
mNoMan.setRanking(notif1.sbn.getKey(), new RankingBuilder(notif1.ranking)
.setRank(56)
.build());
mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 77));
// THEN the ranking is updated on the first entry
assertEquals(56, entry1.ranking().getRank());
}
@Test
public void testRankingUpdateIsProperlyIssuedToEveryone() {
// GIVEN a collection with a couple notifs
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
.setRank(3));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 8)
.setRank(2));
PostedNotif notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 77)
.setRank(1));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
NotificationEntry entry3 = mCollectionListener.getEntry(notif3.key);
// WHEN a ranking update is delivered
Ranking newRanking1 = new RankingBuilder(notif1.ranking)
.setRank(4)
.setExplanation("Foo bar")
.build();
Ranking newRanking2 = new RankingBuilder(notif2.ranking)
.setRank(5)
.setExplanation("baz buzz")
.build();
Ranking newRanking3 = new RankingBuilder(notif3.ranking)
.setRank(6)
.setExplanation("Penguin pizza")
.build();
mNoMan.setRanking(notif1.sbn.getKey(), newRanking1);
mNoMan.setRanking(notif2.sbn.getKey(), newRanking2);
mNoMan.setRanking(notif3.sbn.getKey(), newRanking3);
mNoMan.issueRankingUpdate();
// THEN all of the NotifEntries have their rankings properly updated
assertEquals(newRanking1, entry1.ranking());
assertEquals(newRanking2, entry2.ranking());
assertEquals(newRanking3, entry3.ranking());
}
@Test
public void testNotifEntriesAreNotPersistedAcrossRemovalAndReposting() {
// GIVEN a notification that has been posted
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN the notification is retracted and then reposted
mNoMan.retractNotif(notif1.sbn, REASON_APP_CANCEL);
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
// THEN the new NotificationEntry is a new object
NotificationEntry entry2 = mCollectionListener.getEntry(notif1.key);
assertNotEquals(entry2, entry1);
}
@Test
public void testDismissNotification() throws RemoteException {
// GIVEN a collection with a couple notifications and a lifetime extender
mCollection.addNotificationLifetimeExtender(mExtender1);
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN a notification is manually dismissed
DismissedByUserStats stats = new DismissedByUserStats(
NotificationStats.DISMISSAL_SHADE,
NotificationStats.DISMISS_SENTIMENT_NEUTRAL,
NotificationVisibility.obtain(entry2.key(), 7, 2, true));
mCollection.dismissNotification(entry2, REASON_CLICK, stats);
// THEN we check for lifetime extension
verify(mExtender1).shouldExtendLifetime(entry2, REASON_CLICK);
// THEN we send the dismissal to system server
verify(mStatusBarService).onNotificationClear(
notif2.sbn.getPackageName(),
notif2.sbn.getTag(),
88,
notif2.sbn.getUser().getIdentifier(),
notif2.sbn.getKey(),
stats.dismissalSurface,
stats.dismissalSentiment,
stats.notificationVisibility);
// THEN we fire a remove event
verify(mCollectionListener).onEntryRemoved(entry2, REASON_CLICK, true);
}
@Test(expected = IllegalStateException.class)
public void testDismissingNonExistentNotificationThrows() {
// GIVEN a collection that originally had three notifs, but where one was dismissed
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
PostedNotif notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 99));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
// WHEN we try to dismiss a notification that isn't present
mCollection.dismissNotification(
entry2,
REASON_CLICK,
new DismissedByUserStats(0, 0, NotificationVisibility.obtain("foo", 47, 3, true)));
// THEN an exception is thrown
}
@Test
public void testLifetimeExtendersAreQueriedWhenNotifRemoved() {
// GIVEN a couple notifications and a few lifetime extenders
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN a notification is removed
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
// THEN each extender is asked whether to extend, even if earlier ones return true
verify(mExtender1).shouldExtendLifetime(entry2, REASON_UNKNOWN);
verify(mExtender2).shouldExtendLifetime(entry2, REASON_UNKNOWN);
verify(mExtender3).shouldExtendLifetime(entry2, REASON_UNKNOWN);
// THEN the entry is not removed
assertTrue(mCollection.getNotifs().contains(entry2));
// THEN the entry properly records all extenders that returned true
assertEquals(Arrays.asList(mExtender1, mExtender2), entry2.mLifetimeExtenders);
}
@Test
public void testWhenLastLifetimeExtenderExpiresAllAreReQueried() {
// GIVEN a couple notifications and a few lifetime extenders
mExtender2.shouldExtendLifetime = true;
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by one of them
mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
assertTrue(mCollection.getNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN the last active extender expires (but new ones become active)
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = false;
mExtender3.shouldExtendLifetime = true;
mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
// THEN each extender is re-queried
verify(mExtender1).shouldExtendLifetime(entry2, REASON_UNKNOWN);
verify(mExtender2).shouldExtendLifetime(entry2, REASON_UNKNOWN);
verify(mExtender3).shouldExtendLifetime(entry2, REASON_UNKNOWN);
// THEN the entry is not removed
assertTrue(mCollection.getNotifs().contains(entry2));
// THEN the entry properly records all extenders that returned true
assertEquals(Arrays.asList(mExtender1, mExtender3), entry2.mLifetimeExtenders);
}
@Test
public void testExtendersAreNotReQueriedUntilFinalActiveExtenderExpires() {
// GIVEN a couple notifications and a few lifetime extenders
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
assertTrue(mCollection.getNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN one (but not all) of the extenders expires
mExtender2.shouldExtendLifetime = false;
mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
// THEN the entry is not removed
assertTrue(mCollection.getNotifs().contains(entry2));
// THEN we don't re-query the extenders
verify(mExtender1, never()).shouldExtendLifetime(eq(entry2), anyInt());
verify(mExtender2, never()).shouldExtendLifetime(eq(entry2), anyInt());
verify(mExtender3, never()).shouldExtendLifetime(eq(entry2), anyInt());
// THEN the entry properly records all extenders that returned true
assertEquals(Arrays.asList(mExtender1), entry2.mLifetimeExtenders);
}
@Test
public void testNotificationIsRemovedWhenAllLifetimeExtendersExpire() {
// GIVEN a couple notifications and a few lifetime extenders
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
assertTrue(mCollection.getNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN all of the active extenders expire
mExtender2.shouldExtendLifetime = false;
mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
mExtender1.shouldExtendLifetime = false;
mExtender1.callback.onEndLifetimeExtension(mExtender1, entry2);
// THEN the entry removed
assertFalse(mCollection.getNotifs().contains(entry2));
verify(mCollectionListener).onEntryRemoved(entry2, REASON_UNKNOWN, false);
}
@Test
public void testLifetimeExtensionIsCanceledWhenNotifIsUpdated() {
// GIVEN a few lifetime extenders and a couple notifications
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
assertTrue(mCollection.getNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN the notification is reposted
mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
// THEN all of the active lifetime extenders are canceled
verify(mExtender1).cancelLifetimeExtension(entry2);
verify(mExtender2).cancelLifetimeExtension(entry2);
// THEN the notification is still present
assertTrue(mCollection.getNotifs().contains(entry2));
}
@Test(expected = IllegalStateException.class)
public void testReentrantCallsToLifetimeExtendersThrow() {
// GIVEN a few lifetime extenders and a couple notifications
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
assertTrue(mCollection.getNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN a lifetime extender makes a reentrant call during cancelLifetimeExtension()
mExtender2.onCancelLifetimeExtension = () -> {
mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
};
// This triggers the call to cancelLifetimeExtension()
mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
// THEN an exception is thrown
}
@Test
public void testRankingIsUpdatedWhenALifetimeExtendedNotifIsReposted() {
// GIVEN a few lifetime extenders and a couple notifications
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
PostedNotif notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
PostedNotif notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
assertTrue(mCollection.getNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN the notification is reposted
PostedNotif notif2a = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)
.setRank(4747)
.setExplanation("Some new explanation"));
// THEN the notification's ranking is properly updated
assertEquals(notif2a.ranking, entry2.ranking());
}
private static NotificationEntryBuilder buildNotif(String pkg, int id, String tag) {
return new NotificationEntryBuilder()
.setPkg(pkg)
.setId(id)
.setTag(tag);
}
private static NotificationEntryBuilder buildNotif(String pkg, int id) {
return new NotificationEntryBuilder()
.setPkg(pkg)
.setId(id);
}
private static class NoManSimulator {
private final NotifServiceListener mListener;
private final Map<String, Ranking> mRankings = new ArrayMap<>();
private NoManSimulator(
NotifServiceListener listener) {
mListener = listener;
}
PostedNotif postNotif(NotificationEntryBuilder builder) {
NotificationEntry entry = builder.build();
mRankings.put(entry.key(), entry.ranking());
mListener.onNotificationPosted(entry.sbn(), buildRankingMap());
return new PostedNotif(entry.sbn(), entry.ranking());
}
void retractNotif(StatusBarNotification sbn, int reason) {
assertNotNull(mRankings.remove(sbn.getKey()));
mListener.onNotificationRemoved(sbn, buildRankingMap(), reason);
}
void issueRankingUpdate() {
mListener.onNotificationRankingUpdate(buildRankingMap());
}
void setRanking(String key, Ranking ranking) {
mRankings.put(key, ranking);
}
private RankingMap buildRankingMap() {
return new RankingMap(mRankings.values().toArray(new Ranking[0]));
}
}
private static class PostedNotif {
public final String key;
public final StatusBarNotification sbn;
public final Ranking ranking;
private PostedNotif(StatusBarNotification sbn,
Ranking ranking) {
this.key = sbn.getKey();
this.sbn = sbn;
this.ranking = ranking;
}
}
private static class RecordingCollectionListener implements NotifCollectionListener {
private final Map<String, NotificationEntry> mLastSeenEntries = new ArrayMap<>();
@Override
public void onEntryAdded(NotificationEntry entry) {
mLastSeenEntries.put(entry.key(), entry);
}
@Override
public void onEntryUpdated(NotificationEntry entry) {
}
@Override
public void onEntryRemoved(NotificationEntry entry, int reason, boolean removedByUser) {
}
public NotificationEntry getEntry(String key) {
if (!mLastSeenEntries.containsKey(key)) {
throw new RuntimeException("Key not found: " + key);
}
return mLastSeenEntries.get(key);
}
}
private static class RecordingLifetimeExtender implements NotifLifetimeExtender {
private final String mName;
public @Nullable OnEndLifetimeExtensionCallback callback;
public boolean shouldExtendLifetime = false;
public @Nullable Runnable onCancelLifetimeExtension;
private RecordingLifetimeExtender(String name) {
mName = name;
}
@Override
public String getName() {
return mName;
}
@Override
public void setCallback(OnEndLifetimeExtensionCallback callback) {
this.callback = callback;
}
@Override
public boolean shouldExtendLifetime(
NotificationEntry entry,
@CancellationReason int reason) {
return shouldExtendLifetime;
}
@Override
public void cancelLifetimeExtension(NotificationEntry entry) {
if (onCancelLifetimeExtension != null) {
onCancelLifetimeExtension.run();
}
}
}
private static final String TEST_PACKAGE = "com.android.test.collection";
private static final String TEST_PACKAGE2 = "com.android.test.collection2";
}

View File

@@ -91,6 +91,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.NavigationBarController;
import com.android.systemui.statusbar.NotificationEntryBuilder;
@@ -107,7 +108,7 @@ import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.notification.BypassHeadsUpNotifier;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
import com.android.systemui.statusbar.notification.NotifPipelineInitializer;
import com.android.systemui.statusbar.notification.NewNotifPipeline;
import com.android.systemui.statusbar.notification.NotificationAlertingManager;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -156,6 +157,7 @@ public class StatusBarTest extends SysuiTestCase {
private TestableNotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
private CommandQueue mCommandQueue;
@Mock private FeatureFlags mFeatureFlags;
@Mock private LightBarController mLightBarController;
@Mock private StatusBarIconController mStatusBarIconController;
@Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -205,7 +207,7 @@ public class StatusBarTest extends SysuiTestCase {
@Mock private KeyguardBypassController mKeyguardBypassController;
@Mock private InjectionInflationController mInjectionInflationController;
@Mock private DynamicPrivacyController mDynamicPrivacyController;
@Mock private NotifPipelineInitializer mNotifPipelineInitializer;
@Mock private NewNotifPipeline mNewNotifPipeline;
@Mock private ZenModeController mZenModeController;
@Mock private AutoHideController mAutoHideController;
@Mock private NotificationViewHierarchyManager mNotificationViewHierarchyManager;
@@ -288,6 +290,7 @@ public class StatusBarTest extends SysuiTestCase {
mStatusBar = new StatusBar(
mContext,
mFeatureFlags,
mLightBarController,
mAutoHideController,
mKeyguardUpdateMonitor,
@@ -302,7 +305,7 @@ public class StatusBarTest extends SysuiTestCase {
mDynamicPrivacyController,
mBypassHeadsUpNotifier,
true,
mNotifPipelineInitializer,
() -> mNewNotifPipeline,
new FalsingManagerFake(),
mBroadcastDispatcher,
new RemoteInputQuickSettingsDisabler(