Merge "Add initial implementation of NotifCollection"
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user