Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f0dcb57d authored by Alexander Roederer's avatar Alexander Roederer
Browse files

Puts parceled Notif RankingMap in SharedMemory

Puts the RankingMap in a NotificationRankingUpdate in
SharedMemory when it's parceled, and reads it from SharedMemory
when unparceled. This should have the effect of making
NotificationRankingUpdate much smaller to send across Binder.

Also adds Unit Test Coverage for NotificationRankingUpdate.

Bug: 249848655
Test: atest NotificationRankingUpdateTest. Also used
NotificationShellCmd to test issuing 500+ notifications, and observed
reranking occur properly without previously observed binder transaction
failures
Change-Id: I98ebb81905e1c6a0e5e0d7bca9ac715a94e7c7b4

Change-Id: Ied726dae2179cb2f794f166349ee0e9a3e6a2b0d
parent 382123ce
Loading
Loading
Loading
Loading
+4 −0
Original line number Original line Diff line number Diff line
@@ -2958,6 +2958,10 @@ package android.service.notification {
    method @Deprecated public boolean isBound();
    method @Deprecated public boolean isBound();
  }
  }


  public class NotificationRankingUpdate implements android.os.Parcelable {
    method public final boolean isFdNotNullAndClosed();
  }

}
}


package android.service.quickaccesswallet {
package android.service.quickaccesswallet {
+128 −3
Original line number Original line Diff line number Diff line
@@ -16,32 +16,117 @@
package android.service.notification;
package android.service.notification;


import android.annotation.Nullable;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcel;
import android.os.Parcelable;
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
 * @hide
 */
 */
@SuppressLint({"ParcelNotFinal", "ParcelCreator"})
@TestApi
public class NotificationRankingUpdate implements Parcelable {
public class NotificationRankingUpdate implements Parcelable {
    private final NotificationListenerService.RankingMap mRankingMap;
    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) {
    public NotificationRankingUpdate(NotificationListenerService.Ranking[] rankings) {
        mRankingMap = new NotificationListenerService.RankingMap(rankings);
        mRankingMap = new NotificationListenerService.RankingMap(rankings);
    }
    }


    /**
     * @hide
     */
    public NotificationRankingUpdate(Parcel in) {
    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() {
    public NotificationListenerService.RankingMap getRankingMap() {
        return mRankingMap;
        return mRankingMap;
    }
    }


    /**
     * @hide
     */
    @Override
    @Override
    public int describeContents() {
    public int describeContents() {
        return 0;
        return 0;
    }
    }


    /**
     * @hide
     */
    @Override
    @Override
    public boolean equals(@Nullable Object o) {
    public boolean equals(@Nullable Object o) {
        if (this == o) return true;
        if (this == o) return true;
@@ -51,11 +136,51 @@ public class NotificationRankingUpdate implements Parcelable {
        return mRankingMap.equals(other.mRankingMap);
        return mRankingMap.equals(other.mRankingMap);
    }
    }


    /**
     * @hide
     */
    @Override
    @Override
    public void writeToParcel(Parcel out, int 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);
            out.writeParcelable(mRankingMap, flags);
        }
        }
    }


    /**
    * @hide
    */
    public static final @android.annotation.NonNull Parcelable.Creator<NotificationRankingUpdate> CREATOR
    public static final @android.annotation.NonNull Parcelable.Creator<NotificationRankingUpdate> CREATOR
            = new Parcelable.Creator<NotificationRankingUpdate>() {
            = new Parcelable.Creator<NotificationRankingUpdate>() {
        public NotificationRankingUpdate createFromParcel(Parcel parcel) {
        public NotificationRankingUpdate createFromParcel(Parcel parcel) {
+195 −0
Original line number Original line Diff line number Diff line
/*
 * 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));
    }
}