Merge "Fix allowlist token issues" into sc-dev am: ce91b479d7
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/29583045
Change-Id: I913cbbf7140c0ded1f9f40793aae47fe7f4bebc1
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 74a828c..b0aef53 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -2519,8 +2519,11 @@
if (mAllowlistToken == null) {
mAllowlistToken = processAllowlistToken;
}
- // Propagate this token to all pending intents that are unmarshalled from the parcel.
- parcel.setClassCookie(PendingIntent.class, mAllowlistToken);
+ // Propagate this token to all pending intents that are unmarshalled from the parcel,
+ // or keep the one we're already propagating, if that's the case.
+ if (!parcel.hasClassCookie(PendingIntent.class)) {
+ parcel.setClassCookie(PendingIntent.class, mAllowlistToken);
+ }
when = parcel.readLong();
creationTime = parcel.readLong();
@@ -2977,9 +2980,24 @@
});
}
try {
- // IMPORTANT: Add marshaling code in writeToParcelImpl as we
- // want to intercept all pending events written to the parcel.
- writeToParcelImpl(parcel, flags);
+ boolean mustClearCookie = false;
+ if (!parcel.hasClassCookie(Notification.class)) {
+ // This is the "root" notification, and not an "inner" notification (including
+ // publicVersion or anything else that might be embedded in extras). So we want
+ // to use its token for every inner notification (might be null).
+ parcel.setClassCookie(Notification.class, mAllowlistToken);
+ mustClearCookie = true;
+ }
+ try {
+ // IMPORTANT: Add marshaling code in writeToParcelImpl as we
+ // want to intercept all pending events written to the parcel.
+ writeToParcelImpl(parcel, flags);
+ } finally {
+ if (mustClearCookie) {
+ parcel.removeClassCookie(Notification.class, mAllowlistToken);
+ }
+ }
+
synchronized (this) {
// Must be written last!
parcel.writeArraySet(allPendingIntents);
@@ -2994,7 +3012,10 @@
private void writeToParcelImpl(Parcel parcel, int flags) {
parcel.writeInt(1);
- parcel.writeStrongBinder(mAllowlistToken);
+ // Always use the same token as the root notification (might be null).
+ IBinder rootNotificationToken = (IBinder) parcel.getClassCookie(Notification.class);
+ parcel.writeStrongBinder(rootNotificationToken);
+
parcel.writeLong(when);
parcel.writeLong(creationTime);
if (mSmallIcon == null && icon != 0) {
@@ -3350,18 +3371,23 @@
* Sets the token used for background operations for the pending intents associated with this
* notification.
*
- * This token is automatically set during deserialization for you, you usually won't need to
- * call this unless you want to change the existing token, if any.
+ * Note: Should <em>only</em> be invoked by NotificationManagerService, since this is normally
+ * populated by unparceling (and also used there). Any other usage is suspect.
*
* @hide
*/
- public void clearAllowlistToken() {
- mAllowlistToken = null;
+ public void overrideAllowlistToken(IBinder token) {
+ mAllowlistToken = token;
if (publicVersion != null) {
- publicVersion.clearAllowlistToken();
+ publicVersion.overrideAllowlistToken(token);
}
}
+ /** @hide */
+ public IBinder getAllowlistToken() {
+ return mAllowlistToken;
+ }
+
/**
* @hide
*/
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index d0a77a0..403a5be 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -652,6 +652,28 @@
}
/** @hide */
+ public void removeClassCookie(Class clz, Object expectedCookie) {
+ if (mClassCookies != null) {
+ Object removedCookie = mClassCookies.remove(clz);
+ if (removedCookie != expectedCookie) {
+ Log.wtf(TAG, "Expected to remove " + expectedCookie + " (with key=" + clz
+ + ") but instead removed " + removedCookie);
+ }
+ } else {
+ Log.wtf(TAG, "Expected to remove " + expectedCookie + " (with key=" + clz
+ + ") but no cookies were present");
+ }
+ }
+
+ /**
+ * Whether {@link #setClassCookie} has been called with the specified {@code clz}.
+ * @hide
+ */
+ public boolean hasClassCookie(Class clz) {
+ return mClassCookies != null && mClassCookies.containsKey(clz);
+ }
+
+ /** @hide */
public final void adoptClassCookies(Parcel from) {
mClassCookies = from.mClassCookies;
}
diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java
index dcb3e2f..a21ea30 100644
--- a/core/tests/coretests/src/android/os/ParcelTest.java
+++ b/core/tests/coretests/src/android/os/ParcelTest.java
@@ -16,15 +16,20 @@
package android.os;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import android.platform.test.annotations.Presubmit;
+import android.util.Log;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
+
@Presubmit
@RunWith(AndroidJUnit4.class)
public class ParcelTest {
@@ -110,4 +115,48 @@
assertEquals(string, p.readString16());
}
}
+
+ @Test
+ public void testClassCookies() {
+ Parcel p = Parcel.obtain();
+ assertThat(p.hasClassCookie(ParcelTest.class)).isFalse();
+
+ p.setClassCookie(ParcelTest.class, "string_cookie");
+ assertThat(p.hasClassCookie(ParcelTest.class)).isTrue();
+ assertThat(p.getClassCookie(ParcelTest.class)).isEqualTo("string_cookie");
+
+ p.removeClassCookie(ParcelTest.class, "string_cookie");
+ assertThat(p.hasClassCookie(ParcelTest.class)).isFalse();
+ assertThat(p.getClassCookie(ParcelTest.class)).isEqualTo(null);
+
+ p.setClassCookie(ParcelTest.class, "to_be_discarded_cookie");
+ p.recycle();
+ assertThat(p.getClassCookie(ParcelTest.class)).isNull();
+ }
+
+ @Test
+ public void testClassCookies_removeUnexpected() {
+ Parcel p = Parcel.obtain();
+
+ assertLogsWtf(() -> p.removeClassCookie(ParcelTest.class, "not_present"));
+
+ p.setClassCookie(ParcelTest.class, "value");
+
+ assertLogsWtf(() -> p.removeClassCookie(ParcelTest.class, "different"));
+ assertThat(p.getClassCookie(ParcelTest.class)).isNull(); // still removed
+
+ p.recycle();
+ }
+
+ private static void assertLogsWtf(Runnable test) {
+ ArrayList<Log.TerribleFailure> wtfs = new ArrayList<>();
+ Log.TerribleFailureHandler oldHandler = Log.setWtfHandler(
+ (tag, what, system) -> wtfs.add(what));
+ try {
+ test.run();
+ } finally {
+ Log.setWtfHandler(oldHandler);
+ }
+ assertThat(wtfs).hasSize(1);
+ }
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
old mode 100755
new mode 100644
index 515832c..90ae2a7
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -592,7 +592,7 @@
private static final int MY_UID = Process.myUid();
private static final int MY_PID = Process.myPid();
- private static final IBinder ALLOWLIST_TOKEN = new Binder();
+ static final IBinder ALLOWLIST_TOKEN = new Binder();
protected RankingHandler mRankingHandler;
private long mLastOverRateLogTime;
private float mMaxPackageEnqueueRate = DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE;
@@ -4347,7 +4347,7 @@
// Remove background token before returning notification to untrusted app, this
// ensures the app isn't able to perform background operations that are
// associated with notification interactions.
- notification.clearAllowlistToken();
+ notification.overrideAllowlistToken(null);
return new StatusBarNotification(
sbn.getPackageName(),
sbn.getOpPkg(),
@@ -6335,6 +6335,15 @@
+ " trying to post for invalid pkg " + pkg + " in user " + incomingUserId);
}
+ IBinder allowlistToken = notification.getAllowlistToken();
+ if (allowlistToken != null && allowlistToken != ALLOWLIST_TOKEN) {
+ throw new SecurityException(
+ "Unexpected allowlist token received from " + callingUid);
+ }
+ // allowlistToken is populated by unparceling, so it can be null if the notification was
+ // posted from inside system_server. Ensure it's the expected value.
+ notification.overrideAllowlistToken(ALLOWLIST_TOKEN);
+
checkRestrictedCategories(notification);
// Fix the notification as best we can.
@@ -7079,6 +7088,11 @@
@Override
public void run() {
synchronized (mNotificationLock) {
+ // allowlistToken is populated by unparceling, so it will be absent if the
+ // EnqueueNotificationRunnable is created directly by NMS (as we do for group
+ // summaries) instead of via notify(). Fix that.
+ r.getNotification().overrideAllowlistToken(ALLOWLIST_TOKEN);
+
final Long snoozeAt =
mSnoozeHelper.getSnoozeTimeForUnpostedNotification(
r.getUser().getIdentifier(),
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 d4de88f..cd337aa 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -58,6 +58,7 @@
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
@@ -101,6 +102,7 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AlarmManager;
@@ -151,6 +153,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Parcel;
+import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -210,6 +213,7 @@
import com.android.server.wm.WindowManagerInternal;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
import org.junit.After;
import org.junit.Before;
@@ -243,7 +247,12 @@
private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId";
private static final int UID_HEADLESS = 1000000;
+ private TestableContext mContext = spy(getContext());
private final int mUid = Binder.getCallingUid();
+ private final @UserIdInt int mUserId = UserHandle.getUserId(mUid);
+ private final UserHandle mUser = UserHandle.of(mUserId);
+ private final String mPkg = mContext.getPackageName();
+
private TestableNotificationManagerService mService;
private INotificationManager mBinderService;
private NotificationManagerInternal mInternalService;
@@ -254,7 +263,6 @@
private PackageManager mPackageManagerClient;
@Mock
private WindowManagerInternal mWindowManagerInternal;
- private TestableContext mContext = spy(getContext());
private final String PKG = mContext.getPackageName();
private TestableLooper mTestableLooper;
@Mock
@@ -1827,7 +1835,7 @@
notif.getNotification().flags |= Notification.FLAG_NO_CLEAR;
mService.addNotification(notif);
mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0, true,
- notif.getUserId(), 0, null);
+ notif.getUserId(), REASON_CANCEL, null);
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
@@ -2072,7 +2080,7 @@
notif.getNotification().flags |= Notification.FLAG_NO_CLEAR;
mService.addNotification(notif);
mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0,
- Notification.FLAG_ONGOING_EVENT, true, notif.getUserId(), 0, null);
+ Notification.FLAG_ONGOING_EVENT, true, notif.getUserId(), REASON_CANCEL, null);
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
@@ -2125,7 +2133,7 @@
notif.getNotification().flags |= Notification.FLAG_ONGOING_EVENT;
mService.addNotification(notif);
mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0, true,
- notif.getUserId(), 0, null);
+ notif.getUserId(), REASON_CANCEL, null);
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
@@ -9055,4 +9063,342 @@
nru = mService.makeRankingUpdateLocked(null);
assertEquals(2, nru.getRankingMap().getOrderedKeys().length);
}
+
+ @Test
+ public void enqueueNotification_acceptsCorrectToken() throws RemoteException {
+ Notification sent = new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("content"))
+ .build();
+ Notification received = parcelAndUnparcel(sent, Notification.CREATOR);
+ assertThat(received.getAllowlistToken()).isEqualTo(
+ NotificationManagerService.ALLOWLIST_TOKEN);
+
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 1,
+ parcelAndUnparcel(received, Notification.CREATOR), mUserId);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getNotification().getAllowlistToken())
+ .isEqualTo(NotificationManagerService.ALLOWLIST_TOKEN);
+ }
+
+ @Test
+ public void enqueueNotification_acceptsNullToken_andPopulatesIt() throws RemoteException {
+ Notification receivedWithoutParceling = new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("content"))
+ .build();
+ assertThat(receivedWithoutParceling.getAllowlistToken()).isNull();
+
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 1,
+ parcelAndUnparcel(receivedWithoutParceling, Notification.CREATOR), mUserId);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getNotification().getAllowlistToken())
+ .isEqualTo(NotificationManagerService.ALLOWLIST_TOKEN);
+ }
+
+ @Test
+ public void enqueueNotification_directlyThroughRunnable_populatesAllowlistToken() {
+ Notification receivedWithoutParceling = new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("content"))
+ .build();
+ NotificationRecord record = new NotificationRecord(
+ mContext,
+ new StatusBarNotification(mPkg, mPkg, 1, "tag", mUid, 44, receivedWithoutParceling,
+ mUser, "groupKey", 0),
+ mTestNotificationChannel);
+ assertThat(record.getNotification().getAllowlistToken()).isNull();
+
+ mWorkerHandler.post(
+ mService.new EnqueueNotificationRunnable(mUserId, record, false));
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getNotification().getAllowlistToken())
+ .isEqualTo(NotificationManagerService.ALLOWLIST_TOKEN);
+ }
+
+ @Test
+ public void enqueueNotification_rejectsOtherToken() throws RemoteException {
+ Notification sent = new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("content"))
+ .build();
+ sent.overrideAllowlistToken(new Binder());
+ Notification received = parcelAndUnparcel(sent, Notification.CREATOR);
+ assertThat(received.getAllowlistToken()).isEqualTo(sent.getAllowlistToken());
+
+ assertThrows(SecurityException.class, () ->
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 1,
+ parcelAndUnparcel(received, Notification.CREATOR), mUserId));
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).isEmpty();
+ }
+
+ @Test
+ public void enqueueNotification_customParcelingWithFakeInnerToken_hasCorrectTokenInIntents()
+ throws RemoteException {
+ Notification sentFromApp = new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("content"))
+ .setPublicVersion(new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("public"))
+ .build())
+ .build();
+ sentFromApp.publicVersion.overrideAllowlistToken(new Binder());
+
+ // Instead of using the normal parceling, assume the caller parcels it by hand, including a
+ // null token in the outer notification (as would be expected, and as is verified by
+ // enqueue) but trying to sneak in a different one in the inner notification, hoping it gets
+ // propagated to the PendingIntents.
+ Parcel parcelSentFromApp = Parcel.obtain();
+ writeNotificationToParcelCustom(parcelSentFromApp, sentFromApp, new ArraySet<>(
+ Lists.newArrayList(sentFromApp.contentIntent,
+ sentFromApp.publicVersion.contentIntent)));
+
+ // Use the unparceling as received in enqueueNotificationWithTag()
+ parcelSentFromApp.setDataPosition(0);
+ Notification receivedByNms = new Notification(parcelSentFromApp);
+
+ // Verify that all the pendingIntents have the correct token.
+ assertThat(receivedByNms.contentIntent.getWhitelistToken()).isEqualTo(
+ NotificationManagerService.ALLOWLIST_TOKEN);
+ assertThat(receivedByNms.publicVersion.contentIntent.getWhitelistToken()).isEqualTo(
+ NotificationManagerService.ALLOWLIST_TOKEN);
+ }
+
+ /**
+ * Replicates the behavior of {@link Notification#writeToParcel} but excluding the
+ * "always use the same allowlist token as the root notification" parts.
+ */
+ private static void writeNotificationToParcelCustom(Parcel parcel, Notification notif,
+ ArraySet<PendingIntent> allPendingIntents) {
+ int flags = 0;
+ parcel.writeInt(1); // version?
+
+ parcel.writeStrongBinder(notif.getAllowlistToken());
+ parcel.writeLong(notif.when);
+ parcel.writeLong(1234L); // notif.creationTime is private
+ if (notif.getSmallIcon() != null) {
+ parcel.writeInt(1);
+ notif.getSmallIcon().writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ parcel.writeInt(notif.number);
+ if (notif.contentIntent != null) {
+ parcel.writeInt(1);
+ notif.contentIntent.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (notif.deleteIntent != null) {
+ parcel.writeInt(1);
+ notif.deleteIntent.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (notif.tickerText != null) {
+ parcel.writeInt(1);
+ TextUtils.writeToParcel(notif.tickerText, parcel, flags);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (notif.tickerView != null) {
+ parcel.writeInt(1);
+ notif.tickerView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (notif.contentView != null) {
+ parcel.writeInt(1);
+ notif.contentView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (notif.getLargeIcon() != null) {
+ parcel.writeInt(1);
+ notif.getLargeIcon().writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(notif.defaults);
+ parcel.writeInt(notif.flags);
+
+ if (notif.sound != null) {
+ parcel.writeInt(1);
+ notif.sound.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ parcel.writeInt(notif.audioStreamType);
+
+ if (notif.audioAttributes != null) {
+ parcel.writeInt(1);
+ notif.audioAttributes.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeLongArray(notif.vibrate);
+ parcel.writeInt(notif.ledARGB);
+ parcel.writeInt(notif.ledOnMS);
+ parcel.writeInt(notif.ledOffMS);
+ parcel.writeInt(notif.iconLevel);
+
+ if (notif.fullScreenIntent != null) {
+ parcel.writeInt(1);
+ notif.fullScreenIntent.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(notif.priority);
+
+ parcel.writeString8(notif.category);
+
+ parcel.writeString8(notif.getGroup());
+
+ parcel.writeString8(notif.getSortKey());
+
+ parcel.writeBundle(notif.extras); // null ok
+
+ parcel.writeTypedArray(notif.actions, 0); // null ok
+
+ if (notif.bigContentView != null) {
+ parcel.writeInt(1);
+ notif.bigContentView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ if (notif.headsUpContentView != null) {
+ parcel.writeInt(1);
+ notif.headsUpContentView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(notif.visibility);
+
+ if (notif.publicVersion != null) {
+ parcel.writeInt(1);
+ writeNotificationToParcelCustom(parcel, notif.publicVersion, new ArraySet<>());
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(notif.color);
+
+ if (notif.getChannelId() != null) {
+ parcel.writeInt(1);
+ parcel.writeString8(notif.getChannelId());
+ } else {
+ parcel.writeInt(0);
+ }
+ parcel.writeLong(notif.getTimeoutAfter());
+
+ if (notif.getShortcutId() != null) {
+ parcel.writeInt(1);
+ parcel.writeString8(notif.getShortcutId());
+ } else {
+ parcel.writeInt(0);
+ }
+
+ if (notif.getLocusId() != null) {
+ parcel.writeInt(1);
+ notif.getLocusId().writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(notif.getBadgeIconType());
+
+ if (notif.getSettingsText() != null) {
+ parcel.writeInt(1);
+ TextUtils.writeToParcel(notif.getSettingsText(), parcel, flags);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(notif.getGroupAlertBehavior());
+
+ if (notif.getBubbleMetadata() != null) {
+ parcel.writeInt(1);
+ notif.getBubbleMetadata().writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeBoolean(notif.getAllowSystemGeneratedContextualActions());
+
+ parcel.writeInt(Notification.FOREGROUND_SERVICE_DEFAULT); // no getter for mFgsDeferBehavior
+
+ // mUsesStandardHeader is not written because it should be recomputed in listeners
+
+ parcel.writeArraySet(allPendingIntents);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void getActiveNotifications_doesNotLeakAllowlistToken() throws RemoteException {
+ Notification sentFromApp = new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("content"))
+ .setPublicVersion(new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("public"))
+ .build())
+ .extend(new Notification.WearableExtender()
+ .addPage(new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("wearPage"))
+ .build()))
+ .build();
+ // Binder transition: app -> NMS
+ Notification receivedByNms = parcelAndUnparcel(sentFromApp, Notification.CREATOR);
+ assertThat(receivedByNms.getAllowlistToken()).isEqualTo(
+ NotificationManagerService.ALLOWLIST_TOKEN);
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 1,
+ parcelAndUnparcel(receivedByNms, Notification.CREATOR), mUserId);
+ waitForIdle();
+ assertThat(mService.mNotificationList).hasSize(1);
+ Notification posted = mService.mNotificationList.get(0).getNotification();
+ assertThat(posted.getAllowlistToken()).isEqualTo(
+ NotificationManagerService.ALLOWLIST_TOKEN);
+ assertThat(posted.contentIntent.getWhitelistToken()).isEqualTo(
+ NotificationManagerService.ALLOWLIST_TOKEN);
+
+ ParceledListSlice<StatusBarNotification> listSentFromNms =
+ mBinderService.getAppActiveNotifications(mPkg, mUserId);
+ // Binder transition: NMS -> app. App doesn't have the allowlist token so clear it
+ // (having a different one would produce the same effect; the relevant thing is to not let
+ // out ALLOWLIST_TOKEN).
+ // Note: for other tests, this is restored by constructing TestableNMS in setup().
+ Notification.processAllowlistToken = null;
+ ParceledListSlice<StatusBarNotification> listReceivedByApp = parcelAndUnparcel(
+ listSentFromNms, ParceledListSlice.CREATOR);
+ Notification gottenBackByApp = listReceivedByApp.getList().get(0).getNotification();
+
+ assertThat(gottenBackByApp.getAllowlistToken()).isNull();
+ assertThat(gottenBackByApp.contentIntent.getWhitelistToken()).isNull();
+ assertThat(gottenBackByApp.publicVersion.getAllowlistToken()).isNull();
+ assertThat(gottenBackByApp.publicVersion.contentIntent.getWhitelistToken()).isNull();
+ assertThat(new Notification.WearableExtender(gottenBackByApp).getPages()
+ .get(0).getAllowlistToken()).isNull();
+ assertThat(new Notification.WearableExtender(gottenBackByApp).getPages()
+ .get(0).contentIntent.getWhitelistToken()).isNull();
+ }
+
+ private static <T extends Parcelable> T parcelAndUnparcel(T source,
+ Parcelable.Creator<T> creator) {
+ Parcel parcel = Parcel.obtain();
+ source.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return creator.createFromParcel(parcel);
+ }
+
+ private PendingIntent createPendingIntent(String action) {
+ return PendingIntent.getActivity(mContext, 0,
+ new Intent(action).setPackage(mContext.getPackageName()),
+ PendingIntent.FLAG_MUTABLE);
+ }
}