diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 24491a8124c59..79f5a9e0ae5be 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2962,6 +2962,10 @@ package android.service.notification { method @Deprecated public boolean isBound(); } + public class NotificationRankingUpdate implements android.os.Parcelable { + method public final boolean isFdNotNullAndClosed(); + } + } package android.service.quickaccesswallet { diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java index a853714c0e9da..75640bd47ab9b 100644 --- a/core/java/android/service/notification/NotificationRankingUpdate.java +++ b/core/java/android/service/notification/NotificationRankingUpdate.java @@ -16,32 +16,117 @@ package android.service.notification; import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; +import android.os.SharedMemory; +import android.system.ErrnoException; +import android.system.OsConstants; + +import androidx.annotation.NonNull; + +import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; + +import java.nio.ByteBuffer; /** + * Represents an update to notification rankings. * @hide */ +@SuppressLint({"ParcelNotFinal", "ParcelCreator"}) +@TestApi public class NotificationRankingUpdate implements Parcelable { private final NotificationListenerService.RankingMap mRankingMap; + // The ranking map is stored in shared memory when parceled, for sending across the binder. + // This is done because the ranking map can grow large if there are many notifications. + private SharedMemory mRankingMapFd = null; + private final String mSharedMemoryName = "NotificationRankingUpdatedSharedMemory"; + + /** + * @hide + */ public NotificationRankingUpdate(NotificationListenerService.Ranking[] rankings) { mRankingMap = new NotificationListenerService.RankingMap(rankings); } + /** + * @hide + */ public NotificationRankingUpdate(Parcel in) { - mRankingMap = in.readParcelable(getClass().getClassLoader(), android.service.notification.NotificationListenerService.RankingMap.class); + if (SystemUiSystemPropertiesFlags.getResolver().isEnabled( + SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) { + // Recover the ranking map from the SharedMemory and store it in mapParcel. + final Parcel mapParcel = Parcel.obtain(); + ByteBuffer buffer = null; + try { + // The ranking map should be stored in shared memory when it is parceled, so we + // unwrap the SharedMemory object. + mRankingMapFd = in.readParcelable(getClass().getClassLoader(), SharedMemory.class); + + // In the case that the ranking map can't be read, readParcelable may return null. + // In this case, we set mRankingMap to null; + if (mRankingMapFd == null) { + mRankingMap = null; + return; + } + // We only need read-only access to the shared memory region. + buffer = mRankingMapFd.mapReadOnly(); + if (buffer == null) { + mRankingMap = null; + return; + } + byte[] payload = new byte[buffer.remaining()]; + buffer.get(payload); + mapParcel.unmarshall(payload, 0, payload.length); + mapParcel.setDataPosition(0); + + mRankingMap = mapParcel.readParcelable(getClass().getClassLoader(), + android.service.notification.NotificationListenerService.RankingMap.class); + } catch (ErrnoException e) { + // TODO(b/284297289): remove throw when associated flag is moved to droidfood, to + // avoid crashes; change to Log.wtf. + throw new RuntimeException(e); + } finally { + mapParcel.recycle(); + if (buffer != null) { + mRankingMapFd.unmap(buffer); + } + } + } else { + mRankingMap = in.readParcelable(getClass().getClassLoader(), + android.service.notification.NotificationListenerService.RankingMap.class); + } } + /** + * Confirms that the SharedMemory file descriptor is closed. Should only be used for testing. + * @hide + */ + @TestApi + public final boolean isFdNotNullAndClosed() { + return mRankingMapFd != null && mRankingMapFd.getFd() == -1; + } + + /** + * @hide + */ public NotificationListenerService.RankingMap getRankingMap() { return mRankingMap; } + /** + * @hide + */ @Override public int describeContents() { return 0; } + /** + * @hide + */ @Override public boolean equals(@Nullable Object o) { if (this == o) return true; @@ -51,11 +136,51 @@ public class NotificationRankingUpdate implements Parcelable { return mRankingMap.equals(other.mRankingMap); } + /** + * @hide + */ @Override - public void writeToParcel(Parcel out, int flags) { - out.writeParcelable(mRankingMap, flags); + public void writeToParcel(@NonNull Parcel out, int flags) { + if (SystemUiSystemPropertiesFlags.getResolver().isEnabled( + SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) { + final Parcel mapParcel = Parcel.obtain(); + try { + // Parcels the ranking map and measures its size. + mapParcel.writeParcelable(mRankingMap, flags); + int mapSize = mapParcel.dataSize(); + + // Creates a new SharedMemory object with enough space to hold the ranking map. + SharedMemory mRankingMapFd = SharedMemory.create(mSharedMemoryName, mapSize); + if (mRankingMapFd == null) { + return; + } + + // Gets a read/write buffer mapping the entire shared memory region. + final ByteBuffer buffer = mRankingMapFd.mapReadWrite(); + + // Puts the ranking map into the shared memory region buffer. + buffer.put(mapParcel.marshall(), 0, mapSize); + + // Protects the region from being written to, by setting it to be read-only. + mRankingMapFd.setProtect(OsConstants.PROT_READ); + + // Puts the SharedMemory object in the parcel. + out.writeParcelable(mRankingMapFd, flags); + } catch (ErrnoException e) { + // TODO(b/284297289): remove throw when associated flag is moved to droidfood, to + // avoid crashes; change to Log.wtf. + throw new RuntimeException(e); + } finally { + mapParcel.recycle(); + } + } else { + out.writeParcelable(mRankingMap, flags); + } } + /** + * @hide + */ public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public NotificationRankingUpdate createFromParcel(Parcel parcel) { diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java index c9e76009136ab..6b8ae946e68ca 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java @@ -85,6 +85,10 @@ public class SystemUiSystemPropertiesFlags { /** Gating the holding of WakeLocks until NLSes are told about a new notification. */ public static final Flag WAKE_LOCK_FOR_POSTING_NOTIFICATION = devFlag("persist.sysui.notification.wake_lock_for_posting_notification"); + + /** Gating storing NotificationRankingUpdate ranking map in shared memory. */ + public static final Flag RANKING_UPDATE_ASHMEM = devFlag( + "persist.sysui.notification.ranking_update_ashmem"); } //// == End of flags. Everything below this line is the implementation. == //// diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java new file mode 100644 index 0000000000000..a84ac55f0d5a2 --- /dev/null +++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.notification; + +import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.os.Parcel; + +import androidx.test.filters.SmallTest; + +import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@SmallTest +@RunWith(Parameterized.class) +public class NotificationRankingUpdateTest { + + private static final String NOTIFICATION_CHANNEL_ID = "test_channel_id"; + private static final String TEST_KEY = "key"; + + private NotificationChannel mNotificationChannel; + + // TODO(b/284297289): remove this flag set once resolved. + @Parameterized.Parameters(name = "rankingUpdateAshmem={0}") + public static Boolean[] getRankingUpdateAshmem() { + return new Boolean[] { true, false }; + } + + @Parameterized.Parameter + public boolean mRankingUpdateAshmem; + + @Before + public void setUp() { + mNotificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "test channel", + NotificationManager.IMPORTANCE_DEFAULT); + + SystemUiSystemPropertiesFlags.TEST_RESOLVER = flag -> { + if (flag.mSysPropKey.equals(RANKING_UPDATE_ASHMEM.mSysPropKey)) { + return mRankingUpdateAshmem; + } + return new SystemUiSystemPropertiesFlags.DebugResolver().isEnabled(flag); + }; + } + + @After + public void tearDown() { + SystemUiSystemPropertiesFlags.TEST_RESOLVER = null; + } + + public NotificationListenerService.Ranking createTestRanking(String key, int rank) { + NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking(); + ranking.populate( + /* key= */ key, + /* rank= */ rank, + /* matchesInterruptionFilter= */ false, + /* visibilityOverride= */ 0, + /* suppressedVisualEffects= */ 0, + mNotificationChannel.getImportance(), + /* explanation= */ null, + /* overrideGroupKey= */ null, + mNotificationChannel, + /* overridePeople= */ null, + /* snoozeCriteria= */ null, + /* showBadge= */ true, + /* userSentiment= */ 0, + /* hidden= */ false, + /* lastAudiblyAlertedMs= */ -1, + /* noisy= */ false, + /* smartActions= */ null, + /* smartReplies= */ null, + /* canBubble= */ false, + /* isTextChanged= */ false, + /* isConversation= */ false, + /* shortcutInfo= */ null, + /* rankingAdjustment= */ 0, + /* isBubble= */ false, + /* proposedImportance= */ 0, + /* sensitiveContent= */ false + ); + return ranking; + } + + @Test + public void testRankingUpdate_rankingConstructor() { + NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123); + NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate( + new NotificationListenerService.Ranking[]{ranking}); + + NotificationListenerService.RankingMap retrievedRankings = rankingUpdate.getRankingMap(); + NotificationListenerService.Ranking retrievedRanking = + new NotificationListenerService.Ranking(); + assertTrue(retrievedRankings.getRanking(TEST_KEY, retrievedRanking)); + assertEquals(123, retrievedRanking.getRank()); + } + + @Test + public void testRankingUpdate_parcelConstructor() { + NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123); + NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate( + new NotificationListenerService.Ranking[]{ranking}); + + Parcel parceledRankingUpdate = Parcel.obtain(); + rankingUpdate.writeToParcel(parceledRankingUpdate, 0); + parceledRankingUpdate.setDataPosition(0); + + NotificationRankingUpdate retrievedRankingUpdate = new NotificationRankingUpdate( + parceledRankingUpdate); + + NotificationListenerService.RankingMap retrievedRankings = + retrievedRankingUpdate.getRankingMap(); + assertNotNull(retrievedRankings); + assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed()); + NotificationListenerService.Ranking retrievedRanking = + new NotificationListenerService.Ranking(); + assertTrue(retrievedRankings.getRanking(TEST_KEY, retrievedRanking)); + assertEquals(123, retrievedRanking.getRank()); + assertTrue(retrievedRankingUpdate.equals(rankingUpdate)); + parceledRankingUpdate.recycle(); + } + + @Test + public void testRankingUpdate_emptyParcelInCheck() { + NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123); + NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate( + new NotificationListenerService.Ranking[]{ranking}); + + Parcel parceledRankingUpdate = Parcel.obtain(); + rankingUpdate.writeToParcel(parceledRankingUpdate, 0); + + // This will fail to read the parceledRankingUpdate, because the data position hasn't + // been reset, so it'll find no data to read. + NotificationRankingUpdate retrievedRankingUpdate = new NotificationRankingUpdate( + parceledRankingUpdate); + assertNull(retrievedRankingUpdate.getRankingMap()); + } + + @Test + public void testRankingUpdate_describeContents() { + NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123); + NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate( + new NotificationListenerService.Ranking[]{ranking}); + assertEquals(0, rankingUpdate.describeContents()); + } + + @Test + public void testRankingUpdate_equals() { + NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123); + NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate( + new NotificationListenerService.Ranking[]{ranking}); + // Reflexive equality. + assertTrue(rankingUpdate.equals(rankingUpdate)); + // Null or wrong class inequality. + assertFalse(rankingUpdate.equals(null)); + assertFalse(rankingUpdate.equals(ranking)); + + // Different ranking contents inequality. + NotificationListenerService.Ranking ranking2 = createTestRanking(TEST_KEY, 456); + NotificationRankingUpdate rankingUpdate2 = new NotificationRankingUpdate( + new NotificationListenerService.Ranking[]{ranking2}); + assertFalse(rankingUpdate.equals(rankingUpdate2)); + + // Same ranking contents equality. + ranking2 = createTestRanking(TEST_KEY, 123); + rankingUpdate2 = new NotificationRankingUpdate( + new NotificationListenerService.Ranking[]{ranking2}); + assertTrue(rankingUpdate.equals(rankingUpdate2)); + } +}