diff options
| author | 2024-10-17 13:06:19 +0000 | |
|---|---|---|
| committer | 2024-10-17 13:06:19 +0000 | |
| commit | c50ae70f90a0afc23ddd2dd714554aa8a8e51e2c (patch) | |
| tree | 8c04473312ba30778a545d2f0458a763fe4d1596 | |
| parent | 2c981392014b72781c340edc47f5e026de8ddc50 (diff) | |
| parent | e09da15e1e0daf9d842039ebf8d4e9d5d1a18361 (diff) | |
Merge "Fix allowlist token issues" into tm-dev am: e09da15e1e
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/29582686
Change-Id: I7f7c2432f6a07338bf425f1dd8e9fc534bad66ff
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
| -rw-r--r-- | core/java/android/app/Notification.java | 48 | ||||
| -rw-r--r-- | core/java/android/os/Parcel.java | 22 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/os/ParcelTest.java | 49 | ||||
| -rw-r--r--[-rwxr-xr-x] | services/core/java/com/android/server/notification/NotificationManagerService.java | 18 | ||||
| -rwxr-xr-x | services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java | 354 | 
5 files changed, 474 insertions, 17 deletions
| diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 787d920ba992..c7d406f46605 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -2565,8 +2565,11 @@ public class Notification implements Parcelable          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(); @@ -3027,9 +3030,24 @@ public class Notification implements Parcelable              });          }          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); @@ -3044,7 +3062,10 @@ public class Notification implements Parcelable      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) { @@ -3400,18 +3421,23 @@ public class Notification implements Parcelable       * 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 a7349f9e473e..4bdef89154c2 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -783,6 +783,28 @@ public final class Parcel {      }      /** @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 fdd278b9c621..29d533c6eb7a 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -16,18 +16,23 @@  package android.os; +import static com.google.common.truth.Truth.assertThat; +  import static org.junit.Assert.assertEquals;  import static org.junit.Assert.assertFalse;  import static org.junit.Assert.assertThrows;  import static org.junit.Assert.assertTrue;  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 { @@ -239,4 +244,48 @@ public class ParcelTest {          assertThrows(IllegalArgumentException.class, () -> Parcel.compareData(pA, -1, pB, iB, 0));          assertThrows(IllegalArgumentException.class, () -> Parcel.compareData(pA, 0, pB, -1, 0));      } + +    @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 index 63b88faa2c50..0a319dbdb2a3 100755..100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -640,7 +640,7 @@ public class NotificationManagerService extends SystemService {      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; @@ -4369,7 +4369,7 @@ public class NotificationManagerService extends SystemService {                      // 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(), @@ -6498,6 +6498,15 @@ public class NotificationManagerService extends SystemService {                      + " 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. @@ -7340,6 +7349,11 @@ public class NotificationManagerService extends SystemService {          @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 bb2159da8edc..1cd8cd809a11 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -63,6 +63,7 @@ import static android.service.notification.Adjustment.KEY_USER_SENTIMENT;  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; @@ -108,6 +109,7 @@ import static java.util.Collections.emptyList;  import static java.util.Collections.singletonList;  import android.annotation.SuppressLint; +import android.annotation.UserIdInt;  import android.app.ActivityManager;  import android.app.ActivityManagerInternal;  import android.app.AlarmManager; @@ -161,6 +163,7 @@ import android.os.Bundle;  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; @@ -223,6 +226,7 @@ import com.android.server.wm.ActivityTaskManagerInternal;  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.Assert; @@ -261,7 +265,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {      private static final int TEST_TASK_ID = 1;      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; @@ -279,7 +288,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {      @Mock      private PermissionHelper mPermissionHelper;      private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake(); -    private TestableContext mContext = spy(getContext());      private final String PKG = mContext.getPackageName();      private TestableLooper mTestableLooper;      @Mock @@ -1936,7 +1944,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {          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()); @@ -2633,7 +2641,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {          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()); @@ -2661,7 +2669,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {          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()); @@ -10219,4 +10227,342 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {          mInternalService.sendReviewPermissionsNotification();          verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class));      } + +    @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, 0)); +        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); +    }  } |