Merge "Send update from NMS to SysUI on LifetimeExtension" into main
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
index 90abec1..80c3551 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar;
+import static android.app.Flags.lifetimeExtensionRefactor;
+
import android.annotation.NonNull;
import android.app.Notification;
import android.app.RemoteInputHistoryItem;
@@ -29,6 +31,7 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
@@ -68,7 +71,7 @@
@NonNull
public StatusBarNotification rebuildForCanceledSmartReplies(
NotificationEntry entry) {
- return rebuildWithRemoteInputInserted(entry, null /* remoteInputTest */,
+ return rebuildWithRemoteInputInserted(entry, null /* remoteInputText */,
false /* showSpinner */, null /* mimeType */, null /* uri */);
}
@@ -97,22 +100,50 @@
StatusBarNotification rebuildWithRemoteInputInserted(NotificationEntry entry,
CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
StatusBarNotification sbn = entry.getSbn();
-
Notification.Builder b = Notification.Builder
.recoverBuilder(mContext, sbn.getNotification().clone());
- if (remoteInputText != null || uri != null) {
- RemoteInputHistoryItem newItem = uri != null
- ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
- : new RemoteInputHistoryItem(remoteInputText);
+
+ if (lifetimeExtensionRefactor()) {
+ if (entry.remoteInputs == null) {
+ entry.remoteInputs = new ArrayList<RemoteInputHistoryItem>();
+ }
+
+ // Append new remote input information to remoteInputs list
+ if (remoteInputText != null || uri != null) {
+ RemoteInputHistoryItem newItem = uri != null
+ ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
+ : new RemoteInputHistoryItem(remoteInputText);
+ // The list is latest-first, so new elements should be added as the first element.
+ entry.remoteInputs.add(0, newItem);
+ }
+
+ // Read the whole remoteInputs list from the entry, then append all of those to the sbn.
Parcelable[] oldHistoryItems = sbn.getNotification().extras
.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+
RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
? Stream.concat(
- Stream.of(newItem),
- Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
+ entry.remoteInputs.stream(),
+ Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
.toArray(RemoteInputHistoryItem[]::new)
- : new RemoteInputHistoryItem[] { newItem };
+ : entry.remoteInputs.toArray(RemoteInputHistoryItem[]::new);
b.setRemoteInputHistory(newHistoryItems);
+
+ } else {
+ if (remoteInputText != null || uri != null) {
+ RemoteInputHistoryItem newItem = uri != null
+ ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
+ : new RemoteInputHistoryItem(remoteInputText);
+ Parcelable[] oldHistoryItems = sbn.getNotification().extras
+ .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
+ ? Stream.concat(
+ Stream.of(newItem),
+ Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
+ .toArray(RemoteInputHistoryItem[]::new)
+ : new RemoteInputHistoryItem[]{newItem};
+ b.setRemoteInputHistory(newHistoryItems);
+ }
}
b.setShowRemoteInputSpinner(showSpinner);
b.setHideSmartReplies(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index cdacb10..8678f0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -40,6 +40,7 @@
import android.app.NotificationManager.Policy;
import android.app.Person;
import android.app.RemoteInput;
+import android.app.RemoteInputHistoryItem;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.net.Uri;
@@ -127,6 +128,7 @@
public int targetSdk;
private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
public CharSequence remoteInputText;
+ public List<RemoteInputHistoryItem> remoteInputs = null;
public String remoteInputMimeType;
public Uri remoteInputUri;
public ContentInfo remoteInputAttachment;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index 918bf08..28fff15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.collection.coordinator
+import android.app.Flags.lifetimeExtensionRefactor
+import android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
import android.os.Handler
import android.service.notification.NotificationListenerService.REASON_CANCEL
import android.service.notification.NotificationListenerService.REASON_CLICK
@@ -88,11 +90,21 @@
override fun attach(pipeline: NotifPipeline) {
mNotificationRemoteInputManager.setRemoteInputListener(this)
- mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) }
+ if (lifetimeExtensionRefactor()) {
+ pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender)
+ } else {
+ mRemoteInputLifetimeExtenders.forEach {
+ pipeline.addNotificationLifetimeExtender(it)
+ }
+ }
mNotifUpdater = pipeline.getInternalNotifUpdater(TAG)
pipeline.addCollectionListener(mCollectionListener)
}
+ /*
+ * Listener that updates the appearance of the notification if it has been lifetime extended
+ * by a a direct reply or a smart reply, and cancelled.
+ */
val mCollectionListener = object : NotifCollectionListener {
override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
if (DEBUG) {
@@ -100,9 +112,32 @@
" fromSystem=$fromSystem)")
}
if (fromSystem) {
- // Mark smart replies as sent whenever a notification is updated by the app,
- // otherwise the smart replies are never marked as sent.
- mSmartReplyController.stopSending(entry)
+ if (lifetimeExtensionRefactor()) {
+ if ((entry.getSbn().getNotification().flags
+ and FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+ if (mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
+ entry)) {
+ val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
+ entry.onRemoteInputInserted()
+ mNotifUpdater.onInternalNotificationUpdate(newSbn,
+ "Extending lifetime of notification with remote input")
+ } else if (mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
+ entry)) {
+ val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
+ mSmartReplyController.stopSending(entry)
+ mNotifUpdater.onInternalNotificationUpdate(newSbn,
+ "Extending lifetime of notification with smart reply")
+ }
+ } else {
+ // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
+ // should have their remote inputs list cleared.
+ entry.remoteInputs = null
+ }
+ } else {
+ // Mark smart replies as sent whenever a notification is updated by the app,
+ // otherwise the smart replies are never marked as sent.
+ mSmartReplyController.stopSending(entry)
+ }
}
}
@@ -130,8 +165,10 @@
// NOTE: This is some trickery! By removing the lifetime extensions when we know they should
// be immediately re-upped, we ensure that the side-effects of the lifetime extenders get to
// fire again, thus ensuring that we add subsequent replies to the notification.
- mRemoteInputHistoryExtender.endLifetimeExtension(entry.key)
- mSmartReplyHistoryExtender.endLifetimeExtension(entry.key)
+ if (!lifetimeExtensionRefactor()) {
+ mRemoteInputHistoryExtender.endLifetimeExtension(entry.key)
+ mSmartReplyHistoryExtender.endLifetimeExtension(entry.key)
+ }
// If we're extending for remote input being active, then from the apps point of
// view it is already canceled, so we'll need to cancel it on the apps behalf
@@ -160,15 +197,19 @@
}
override fun isNotificationKeptForRemoteInputHistory(key: String) =
+ if (!lifetimeExtensionRefactor()) {
mRemoteInputHistoryExtender.isExtending(key) ||
mSmartReplyHistoryExtender.isExtending(key)
+ } else false
override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
- mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
- mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ if (!lifetimeExtensionRefactor()) {
+ mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ }
mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
index 7073cc7..85b8b03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
@@ -15,7 +15,13 @@
*/
package com.android.systemui.statusbar.notification.collection.coordinator
+import android.app.Flags.lifetimeExtensionRefactor
+import android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR
+import android.app.Notification
+import android.app.RemoteInputHistoryItem
import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
@@ -34,6 +40,7 @@
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.captureMany
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -42,6 +49,7 @@
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.initMocks
@@ -57,6 +65,7 @@
private lateinit var entry2: NotificationEntry
@Mock private lateinit var lifetimeExtensionCallback: OnEndLifetimeExtensionCallback
+
@Mock private lateinit var rebuilder: RemoteInputNotificationRebuilder
@Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
@Mock private lateinit var mainHandler: Handler
@@ -84,9 +93,6 @@
listener = withArgCaptor {
verify(remoteInputManager).setRemoteInputListener(capture())
}
- collectionListener = withArgCaptor {
- verify(pipeline).addCollectionListener(capture())
- }
entry1 = NotificationEntryBuilder().setId(1).build()
entry2 = NotificationEntryBuilder().setId(2).build()
`when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn)
@@ -98,16 +104,23 @@
val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender
val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender
+ val collectionListeners get() = captureMany {
+ verify(pipeline, times(1)).addCollectionListener(capture())
+ }
+
@Test
fun testRemoteInputActive() {
`when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue()
- assertThat(remoteInputHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
- assertThat(smartReplyHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
+ if (!lifetimeExtensionRefactor()) {
+ assertThat(remoteInputHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
+ assertThat(smartReplyHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
+ }
assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isFalse()
}
@Test
+ @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
fun testRemoteInputHistory() {
`when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry1)).thenReturn(true)
assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse()
@@ -117,6 +130,7 @@
}
@Test
+ @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
fun testSmartReplyHistory() {
`when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry1)).thenReturn(true)
assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse()
@@ -142,4 +156,81 @@
verify(lifetimeExtensionCallback).onEndLifetimeExtension(remoteInputActiveExtender, entry1)
assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
}
+
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ fun testOnlyRemoteInputActiveLifetimeExtenderExtends() {
+ `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
+ assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue()
+ assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isTrue()
+
+ listener.onPanelCollapsed()
+ assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
+
+ // Checks that lifetimeExtensionCallback is only called the expected number of times,
+ // by the remoteInputActiveExtender.
+ // Checks that the remote input history extender and smart reply history extenders
+ // aren't attached to the pipeline.
+ verify(lifetimeExtensionCallback, times(1)).onEndLifetimeExtension(any(), any())
+ }
+
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ fun testRemoteInputLifetimeExtensionListenerTrigger() {
+ // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+ val entry = NotificationEntryBuilder()
+ .setId(3)
+ .setTag("entry")
+ .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
+ .build()
+ `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(true)
+ `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
+
+ collectionListeners.forEach {
+ it.onEntryUpdated(entry, true)
+ }
+
+ verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry)
+ }
+
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ fun testSmartReplyLifetimeExtensionListenerTrigger() {
+ // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+ val entry = NotificationEntryBuilder()
+ .setId(3)
+ .setTag("entry")
+ .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
+ .build()
+ `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
+ `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(true)
+ collectionListeners.forEach {
+ it.onEntryUpdated(entry, true)
+ }
+
+
+ verify(rebuilder, times(1)).rebuildForCanceledSmartReplies(entry)
+ verify(smartReplyController, times(1)).stopSending(entry)
+ }
+
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ fun testLifetimeExtensionListenerClearsRemoteInputs() {
+ // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+ val entry = NotificationEntryBuilder()
+ .setId(3)
+ .setTag("entry")
+ .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false)
+ .build()
+ entry.remoteInputs = ArrayList<RemoteInputHistoryItem>()
+ entry.remoteInputs.add(RemoteInputHistoryItem("Test Text"))
+ `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
+ `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
+
+ collectionListeners.forEach {
+ it.onEntryUpdated(entry, true)
+ }
+
+ assertThat(entry.remoteInputs).isNull()
+ }
}
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index d0c0543..f645eaa 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -16,6 +16,7 @@
package com.android.server.notification;
+import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT;
import static android.content.Context.BIND_AUTO_CREATE;
import static android.content.Context.BIND_FOREGROUND_SERVICE;
@@ -24,6 +25,7 @@
import static android.os.UserHandle.USER_SYSTEM;
import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -1802,6 +1804,8 @@
public ComponentName component;
public int userid;
public boolean isSystem;
+ @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ public boolean isSystemUi;
public ServiceConnection connection;
public int targetSdkVersion;
public Pair<ComponentName, Integer> mKey;
@@ -1836,6 +1840,11 @@
return isSystem;
}
+ @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ public boolean isSystemUi() {
+ return isSystemUi;
+ }
+
@Override
public String toString() {
return new StringBuilder("ManagedServiceInfo[")
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index e7ad99a..3507d2d 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -71,6 +71,7 @@
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
+import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
import static android.app.Flags.lifetimeExtensionRefactor;
import static android.app.NotificationManager.zenModeFromInterruptionFilter;
import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED;
@@ -159,6 +160,7 @@
import android.Manifest.permission;
import android.annotation.DurationMillisLong;
import android.annotation.ElapsedRealtimeLong;
+import android.annotation.FlaggedApi;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1852,6 +1854,7 @@
}
if (ACTION_NOTIFICATION_TIMEOUT.equals(action)) {
final NotificationRecord record;
+ // TODO: b/323013410 - Record should be cloned instead of used directly.
synchronized (mNotificationLock) {
record = findNotificationByKeyLocked(intent.getStringExtra(EXTRA_KEY));
}
@@ -1864,6 +1867,14 @@
FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
| FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
true, record.getUserId(), REASON_TIMEOUT, null);
+ // If cancellation will be prevented due to lifetime extension, we send an
+ // update to system UI.
+ synchronized (mNotificationLock) {
+ maybeNotifySystemUiListenerLifetimeExtendedLocked(record,
+ record.getSbn().getPackageName(),
+ mActivityManager.getPackageImportance(
+ record.getSbn().getPackageName()));
+ }
} else {
cancelNotification(record.getSbn().getUid(),
record.getSbn().getInitialPid(),
@@ -3825,7 +3836,17 @@
int mustNotHaveFlags = isCallingUidSystem() ? 0 :
(FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_AUTOGROUP_SUMMARY);
if (lifetimeExtensionRefactor()) {
+ // Also don't allow client apps to cancel lifetime extended notifs.
mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+ // If cancellation will be prevented due to lifetime extension, we send an update to
+ // system UI.
+ NotificationRecord record = null;
+ final int packageImportance = mActivityManager.getPackageImportance(pkg);
+ synchronized (mNotificationLock) {
+ record = findNotificationLocked(pkg, tag, id, userId);
+ maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg,
+ packageImportance);
+ }
}
cancelNotificationInternal(pkg, opPkg, Binder.getCallingUid(), Binder.getCallingPid(),
@@ -3845,6 +3866,16 @@
pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
| FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
userId, REASON_APP_CANCEL_ALL);
+ // If cancellation will be prevented due to lifetime extension, we send updates
+ // to system UI.
+ // In this case, we need to hold the lock to access these lists.
+ final int packageImportance = mActivityManager.getPackageImportance(pkg);
+ synchronized (mNotificationLock) {
+ notifySystemUiListenerLifetimeExtendedListLocked(mNotificationList,
+ packageImportance);
+ notifySystemUiListenerLifetimeExtendedListLocked(mEnqueuedNotifications,
+ packageImportance);
+ }
} else {
cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(),
pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB,
@@ -4891,11 +4922,19 @@
final long identity = Binder.clearCallingIdentity();
boolean notificationsRapidlyCleared = false;
final String pkg;
+ final int packageImportance;
+ final ManagedServiceInfo info;
try {
synchronized (mNotificationLock) {
- final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
+ info = mListeners.checkServiceTokenLocked(token);
pkg = info.component.getPackageName();
-
+ }
+ if (lifetimeExtensionRefactor()) {
+ packageImportance = mActivityManager.getPackageImportance(pkg);
+ } else {
+ packageImportance = IMPORTANCE_NONE;
+ }
+ synchronized (mNotificationLock) {
// Cancellation reason. If the token comes from assistant, label the
// cancellation as coming from the assistant; default to LISTENER_CANCEL.
int reason = REASON_LISTENER_CANCEL;
@@ -4917,7 +4956,7 @@
|| isNotificationRecent(r.getUpdateTimeMs());
cancelNotificationFromListenerLocked(info, callingUid, callingPid,
r.getSbn().getPackageName(), r.getSbn().getTag(),
- r.getSbn().getId(), userId, reason);
+ r.getSbn().getId(), userId, reason, packageImportance);
}
} else {
for (NotificationRecord notificationRecord : mNotificationList) {
@@ -4931,6 +4970,12 @@
REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(),
FLAG_ONGOING_EVENT | FLAG_NO_CLEAR
| FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+ // If cancellation will be prevented due to lifetime extension, we send
+ // an update to system UI.
+ notifySystemUiListenerLifetimeExtendedListLocked(mNotificationList,
+ packageImportance);
+ notifySystemUiListenerLifetimeExtendedListLocked(mEnqueuedNotifications,
+ packageImportance);
} else {
cancelAllLocked(callingUid, callingPid, info.userid,
REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(),
@@ -5051,10 +5096,14 @@
@GuardedBy("mNotificationLock")
private void cancelNotificationFromListenerLocked(ManagedServiceInfo info,
int callingUid, int callingPid, String pkg, String tag, int id, int userId,
- int reason) {
+ int reason, int packageImportance) {
int mustNotHaveFlags = FLAG_ONGOING_EVENT;
if (lifetimeExtensionRefactor()) {
mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+ // If cancellation will be prevented due to lifetime extension, we send an update
+ // to system UI.
+ NotificationRecord record = findNotificationLocked(pkg, tag, id, userId);
+ maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg, packageImportance);
}
cancelNotification(callingUid, callingPid, pkg, tag, id, 0 /* mustHaveFlags */,
mustNotHaveFlags,
@@ -5197,7 +5246,13 @@
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
final long identity = Binder.clearCallingIdentity();
+ final int packageImportance;
try {
+ if (lifetimeExtensionRefactor()) {
+ packageImportance = mActivityManager.getPackageImportance(pkg);
+ } else {
+ packageImportance = IMPORTANCE_NONE;
+ }
synchronized (mNotificationLock) {
final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
int cancelReason = REASON_LISTENER_CANCEL;
@@ -5210,7 +5265,7 @@
+ " use cancelNotification(key) instead.");
} else {
cancelNotificationFromListenerLocked(info, callingUid, callingPid,
- pkg, tag, id, info.userid, cancelReason);
+ pkg, tag, id, info.userid, cancelReason, packageImportance);
}
}
} finally {
@@ -11654,6 +11709,30 @@
});
}
+ @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ @GuardedBy("mNotificationLock")
+ private void notifySystemUiListenerLifetimeExtendedListLocked(
+ List<NotificationRecord> notificationList, int packageImportance) {
+ for (int i = notificationList.size() - 1; i >= 0; --i) {
+ NotificationRecord record = notificationList.get(i);
+ maybeNotifySystemUiListenerLifetimeExtendedLocked(record,
+ record.getSbn().getPackageName(), packageImportance);
+ }
+ }
+
+ @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ @GuardedBy("mNotificationLock")
+ private void maybeNotifySystemUiListenerLifetimeExtendedLocked(NotificationRecord record,
+ String pkg, int packageImportance) {
+ if (record != null && (record.getSbn().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+ boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND;
+ mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(),
+ record, isAppForeground,
+ mPostNotificationTrackerFactory.newTracker(null)));
+ }
+ }
+
public class NotificationListeners extends ManagedServices {
static final String TAG_ENABLED_NOTIFICATION_LISTENERS = "enabled_listeners";
static final String TAG_REQUESTED_LISTENERS = "request_listeners";
@@ -11777,6 +11856,11 @@
@Override
public void onServiceAdded(ManagedServiceInfo info) {
+ if (lifetimeExtensionRefactor()) {
+ // Only System or System UI can call registerSystemService, so if the caller is not
+ // system, we know it's system UI.
+ info.isSystemUi = !isCallerSystemOrPhone();
+ }
final INotificationListener listener = (INotificationListener) info.service;
final NotificationRankingUpdate update;
synchronized (mNotificationLock) {
@@ -12141,6 +12225,23 @@
continue;
}
+ if (lifetimeExtensionRefactor()) {
+ // Checks if this is a request to notify system UI about a notification that
+ // has been lifetime extended.
+ // (We only need to check old for the flag, because in both cancellation and
+ // update cases, old should have the flag.)
+ // If it is such a request, and this is system UI, we send the post request
+ // only to System UI, and break as we don't need to continue checking other
+ // Managed Services.
+ if (info.isSystemUi() && old != null && old.getNotification() != null
+ && (old.getNotification().flags
+ & Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+ final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
+ listenerCalls.add(() -> notifyPosted(info, oldSbn, update));
+ break;
+ }
+ }
+
// If we shouldn't notify all listeners, this means the hidden state of
// a notification was changed. Don't notifyPosted listeners targeting >= P.
// Instead, those listeners will receive notifyRankingUpdate.
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 344a4b0..4dded1d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -16,6 +16,7 @@
package com.android.server.notification;
import static android.content.Context.DEVICE_POLICY_SERVICE;
+import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
@@ -62,6 +63,7 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -1983,6 +1985,22 @@
new ComponentName("pkg1", "cmp1"))).isFalse();
}
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ public void testManagedServiceInfoIsSystemUi() {
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_COMPONENT);
+
+ ManagedServices.ManagedServiceInfo service0 = service.new ManagedServiceInfo(
+ mock(IInterface.class), ComponentName.unflattenFromString("a/a"), 0, false,
+ mock(ServiceConnection.class), 26, 34);
+
+ service0.isSystemUi = true;
+ assertThat(service0.isSystemUi()).isTrue();
+ service0.isSystemUi = false;
+ assertThat(service0.isSystemUi()).isFalse();
+ }
+
private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
throws RemoteException {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 96ffec1..046e057 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -2545,6 +2545,17 @@
assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1);
assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+
mSetFlagsRule.disableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
mBinderService.cancelNotificationWithTag(PKG, PKG, sbn.getTag(), sbn.getId(),
sbn.getUserId());
@@ -2577,6 +2588,17 @@
StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG);
assertThat(notifs.length).isEqualTo(1);
assertThat(notifs[0].getId()).isEqualTo(1);
+
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
}
@Test
@@ -2985,18 +3007,29 @@
public void testCancelNotificationsFromListener_clearAll_NoClearLifetimeExt()
throws Exception {
mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
-
final NotificationRecord notif = generateNotificationRecord(
mTestNotificationChannel, 1, null, false);
- notif.getNotification().flags = FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+ notif.getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
mService.addNotification(notif);
-
+ verify(mWorkerHandler, times(0))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
mService.getBinderService().cancelNotificationsFromListener(null, null);
waitForIdle();
-
+ // Notification not cancelled.
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
assertThat(notifs.length).isEqualTo(1);
+
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
}
@Test
@@ -3217,6 +3250,17 @@
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
assertEquals(1, notifs.length);
+
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
}
@Test
@@ -5659,6 +5703,17 @@
StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
assertThat(notifsAfter.length).isEqualTo(1);
assertThat(mService.getNotificationRecord(notif.getKey())).isEqualTo(notif);
+
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
}
@Test