diff options
8 files changed, 381 insertions, 33 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java index 90abec17771c..80c3551b7de0 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.internal.annotations.VisibleForTesting; 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 @@ public class RemoteInputNotificationRebuilder { @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 @@ public class RemoteInputNotificationRebuilder { 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 cdacb10e1676..8678f0aad181 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.NotificationChannel; 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 final class NotificationEntry extends ListEntry { 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 918bf083f9fe..28fff1519032 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 @@ class RemoteInputCoordinator @Inject constructor( 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 @@ class RemoteInputCoordinator @Inject constructor( " 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 @@ class RemoteInputCoordinator @Inject constructor( // 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 @@ class RemoteInputCoordinator @Inject constructor( } 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 7073cc7c5707..85b8b03a1b46 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.No 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.junit.runner.RunWith 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 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { 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 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { 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 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { 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 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) fun testSmartReplyHistory() { `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry1)).thenReturn(true) assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse() @@ -142,4 +156,81 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { 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 d0c054307d0c..f645eaa28632 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_ALL; 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 @@ abstract public class ManagedServices { 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 @@ abstract public class ManagedServices { 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 e7ad99a8cf20..3507d2d56cbd 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_PEEK; 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; 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 @@ public class NotificationManagerService extends SystemService { } 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { || 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { @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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { + " 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 @@ public class NotificationManagerService extends SystemService { }); } + @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 @@ public class NotificationManagerService extends SystemService { @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 @@ public class NotificationManagerService extends SystemService { 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 344a4b0ce324..4dded1d0342d 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.IInterface; 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 @@ public class ManagedServicesTest extends UiServiceTestCase { 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 96ffec1509da..046e0570d439 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 |