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

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

Removes SmartActions from Rankings before marshal

To allow us to put RankingMap in ashmem when NotificationRankingUpdates
are sent to NotificationListeners, we move the SmartActions that
may be contained in each Ranking object into a separate list, Parcel
them and send them across binder normally, and store the rest of the
RankingMap in ashmem. Then, when we unparcel the
NotificationRankingUpdate, we re-merge the Actions into the RankingMap.

Bug: 249848655
Test: Unit tests, CTS test (see bug); manual flash + repro script
Change-Id: I62a1de307168612d30b55d94765b2a329914b7f4
parent bb6ee756
Loading
Loading
Loading
Loading
+14 −2
Original line number Diff line number Diff line
@@ -2007,6 +2007,20 @@ public abstract class NotificationListenerService extends Service {
            return mSmartActions == null ? Collections.emptyList() : mSmartActions;
        }


        /**
         * Sets the smart {@link Notification.Action} objects.
         *
         * Should ONLY be used in cases where smartActions need to be removed from, then restored
         * on, Ranking objects during Parceling, when they are transmitted between processes via
         * Shared Memory.
         *
         * @hide
         */
        public void setSmartActions(@Nullable ArrayList<Notification.Action> smartActions) {
            mSmartActions = smartActions;
        }

        /**
         * Returns a list of smart replies that can be added by the
         * {@link NotificationAssistantService}
@@ -2353,11 +2367,9 @@ public abstract class NotificationListenerService extends Service {

        /**
         * Get a reference to the actual Ranking object corresponding to the key.
         * Used only by unit tests.
         *
         * @hide
         */
        @VisibleForTesting
        public Ranking getRawRankingObject(String key) {
            return mRankings.get(key);
        }
+78 −8
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package android.service.notification;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.app.Notification;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SharedMemory;
@@ -29,9 +31,12 @@ import androidx.annotation.NonNull;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**
 * Represents an update to notification rankings.
 *
 * @hide
 */
@SuppressLint({"ParcelNotFinal", "ParcelCreator"})
@@ -64,6 +69,7 @@ public class NotificationRankingUpdate implements Parcelable {
                // 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);
                Bundle smartActionsBundle = in.readBundle(getClass().getClassLoader());

                // In the case that the ranking map can't be read, readParcelable may return null.
                // In this case, we set mRankingMap to null;
@@ -82,8 +88,13 @@ public class NotificationRankingUpdate implements Parcelable {
                mapParcel.unmarshall(payload, 0, payload.length);
                mapParcel.setDataPosition(0);

                mRankingMap = mapParcel.readParcelable(getClass().getClassLoader(),
                        android.service.notification.NotificationListenerService.RankingMap.class);
                mRankingMap =
                        mapParcel.readParcelable(
                                getClass().getClassLoader(),
                                NotificationListenerService.RankingMap.class);

                addSmartActionsFromBundleToRankingMap(smartActionsBundle);

            } catch (ErrnoException e) {
                // TODO(b/284297289): remove throw when associated flag is moved to droidfood, to
                // avoid crashes; change to Log.wtf.
@@ -101,8 +112,32 @@ public class NotificationRankingUpdate implements Parcelable {
        }
    }

    /**
     * For each key in the rankingMap, extracts lists of smart actions stored in the provided
     * bundle and adds them to the corresponding Ranking object in the provided ranking
     * map, then returns the rankingMap.
     *
     * @hide
     */
    private void addSmartActionsFromBundleToRankingMap(Bundle smartActionsBundle) {
        if (smartActionsBundle == null) {
            return;
        }

        String[] rankingMapKeys = mRankingMap.getOrderedKeys();
        for (int i = 0; i < rankingMapKeys.length; i++) {
            String key = rankingMapKeys[i];
            ArrayList<Notification.Action> smartActions =
                    smartActionsBundle.getParcelableArrayList(key, Notification.Action.class);
            // Get the ranking object from the ranking map.
            NotificationListenerService.Ranking ranking = mRankingMap.getRawRankingObject(key);
            ranking.setSmartActions(smartActions);
        }
    }

    /**
     * Confirms that the SharedMemory file descriptor is closed. Should only be used for testing.
     *
     * @hide
     */
    @TestApi
@@ -145,9 +180,45 @@ public class NotificationRankingUpdate implements Parcelable {
        if (SystemUiSystemPropertiesFlags.getResolver().isEnabled(
                SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) {
            final Parcel mapParcel = Parcel.obtain();
            ArrayList<NotificationListenerService.Ranking> marshalableRankings = new ArrayList<>();
            Bundle smartActionsBundle = new Bundle();

            // We need to separate the SmartActions from the RankingUpdate objects.
            // SmartActions can contain PendingIntents, which cannot be marshalled,
            // so we extract them to send separately.
            String[] rankingMapKeys = mRankingMap.getOrderedKeys();
            for (int i = 0; i < rankingMapKeys.length; i++) {
                String key = rankingMapKeys[i];
                NotificationListenerService.Ranking ranking = mRankingMap.getRawRankingObject(key);

                // Removes the SmartActions and stores them in a separate map.
                // Note that getSmartActions returns a Collections.emptyList() if there are no
                // smart actions, and we don't want to needlessly store an empty list object, so we
                // check for null before storing.
                List<Notification.Action> smartActions = ranking.getSmartActions();
                if (!smartActions.isEmpty()) {
                    smartActionsBundle.putParcelableList(key, smartActions);
                }

                // Create a copy of the ranking object that doesn't have the smart actions.
                NotificationListenerService.Ranking rankingCopy =
                        new NotificationListenerService.Ranking();
                rankingCopy.populate(ranking);
                rankingCopy.setSmartActions(null);
                marshalableRankings.add(rankingCopy);
            }

            // Create a new marshalable RankingMap.
            NotificationListenerService.RankingMap marshalableRankingMap =
                    new NotificationListenerService.RankingMap(
                            marshalableRankings.toArray(
                                    new NotificationListenerService.Ranking[0]
                            )
                    );

            try {
                // Parcels the ranking map and measures its size.
                mapParcel.writeParcelable(mRankingMap, flags);
                mapParcel.writeParcelable(marshalableRankingMap, flags);
                int mapSize = mapParcel.dataSize();

                // Creates a new SharedMemory object with enough space to hold the ranking map.
@@ -158,15 +229,14 @@ public class NotificationRankingUpdate implements Parcelable {

                // 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);
                // Writes the Parceled smartActions separately.
                out.writeBundle(smartActionsBundle);
            } catch (ErrnoException e) {
                // TODO(b/284297289): remove throw when associated flag is moved to droidfood, to
                // avoid crashes; change to Log.wtf.
+168 −97
Original line number Diff line number Diff line
@@ -39,7 +39,9 @@ import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.os.Bundle;
import android.os.Parcel;
import android.os.SharedMemory;
import android.testing.TestableContext;

import androidx.test.InstrumentationRegistry;
@@ -387,8 +389,13 @@ public class NotificationRankingUpdateTest {
        Assert.assertEquals(comment, a.getSmartReplies(), b.getSmartReplies());
        Assert.assertEquals(comment, a.canBubble(), b.canBubble());
        Assert.assertEquals(comment, a.isConversation(), b.isConversation());
        if (a.getConversationShortcutInfo() != null && b.getConversationShortcutInfo() != null) {
            Assert.assertEquals(comment, a.getConversationShortcutInfo().getId(),
                    b.getConversationShortcutInfo().getId());
        } else {
            // One or both must be null, so we can check for equality.
            Assert.assertEquals(a.getConversationShortcutInfo(), b.getConversationShortcutInfo());
        }
        assertActionsEqual(a.getSmartActions(), b.getSmartActions());
        Assert.assertEquals(a.getProposedImportance(), b.getProposedImportance());
        Assert.assertEquals(a.hasSensitiveContent(), b.hasSensitiveContent());
@@ -428,8 +435,13 @@ public class NotificationRankingUpdateTest {
        SystemUiSystemPropertiesFlags.TEST_RESOLVER = null;
    }

    public NotificationListenerService.Ranking createTestRanking(String key, int rank) {
    /**
     * Creates a mostly empty Test Ranking object with the specified key, rank, and smartActions.
     */
    public NotificationListenerService.Ranking createEmptyTestRanking(
            String key, int rank, ArrayList<Notification.Action> actions) {
        NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();

        ranking.populate(
                /* key= */ key,
                /* rank= */ rank,
@@ -447,7 +459,7 @@ public class NotificationRankingUpdateTest {
                /* hidden= */ false,
                /* lastAudiblyAlertedMs= */ -1,
                /* noisy= */ false,
                /* smartActions= */ null,
                /* smartActions= */ actions,
                /* smartReplies= */ null,
                /* canBubble= */ false,
                /* isTextChanged= */ false,
@@ -461,110 +473,26 @@ public class NotificationRankingUpdateTest {
        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);
        // The rankingUpdate file descriptor is only non-null in the new path.
        if (SystemUiSystemPropertiesFlags.getResolver().isEnabled(
                SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) {
            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));
    }

    // Tests parceling of NotificationRankingUpdate, and by extension, RankingMap and Ranking.
    @Test
    public void testRankingUpdate_parcel_legacy() {
    public void testRankingUpdate_parcel() {
        NotificationRankingUpdate nru = generateUpdate(getContext());
        Parcel parcel = Parcel.obtain();
        nru.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);
        NotificationRankingUpdate nru1 = NotificationRankingUpdate.CREATOR.createFromParcel(parcel);
        // The rankingUpdate file descriptor is only non-null in the new path.
        if (SystemUiSystemPropertiesFlags.getResolver().isEnabled(
                SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) {
            assertTrue(nru1.isFdNotNullAndClosed());
        }
        detailedAssertEquals(nru, nru1);
        parcel.recycle();
    }

    // Tests parceling of RankingMap and RankingMap.equals
    @Test
    public void testRankingMap_parcel_legacy() {
    public void testRankingMap_parcel() {
        NotificationListenerService.RankingMap rmap = generateUpdate(getContext()).getRankingMap();
        Parcel parcel = Parcel.obtain();
        rmap.writeToParcel(parcel, 0);
@@ -574,11 +502,12 @@ public class NotificationRankingUpdateTest {

        detailedAssertEquals(rmap, rmap1);
        Assert.assertEquals(rmap, rmap1);
        parcel.recycle();
    }

    // Tests parceling of Ranking and Ranking.equals
    @Test
    public void testRanking_parcel_legacy() {
    public void testRanking_parcel() {
        NotificationListenerService.Ranking ranking =
                generateUpdate(getContext()).getRankingMap().getRawRankingObject(mKeys[0]);
        Parcel parcel = Parcel.obtain();
@@ -588,6 +517,7 @@ public class NotificationRankingUpdateTest {
                new NotificationListenerService.Ranking(parcel);
        detailedAssertEquals("rankings differ: ", ranking, ranking1);
        Assert.assertEquals(ranking, ranking1);
        parcel.recycle();
    }

    // Tests NotificationRankingUpdate.equals(), and by extension, RankingMap and Ranking.
@@ -630,4 +560,145 @@ public class NotificationRankingUpdateTest {
        assertNotEquals(nru, nru2);
    }

    @Test
    public void testRankingUpdate_rankingConstructor() {
        NotificationRankingUpdate nru = generateUpdate(getContext());
        NotificationRankingUpdate constructedNru = new NotificationRankingUpdate(
                new NotificationListenerService.Ranking[]{
                        nru.getRankingMap().getRawRankingObject(mKeys[0]),
                        nru.getRankingMap().getRawRankingObject(mKeys[1]),
                        nru.getRankingMap().getRawRankingObject(mKeys[2]),
                        nru.getRankingMap().getRawRankingObject(mKeys[3]),
                        nru.getRankingMap().getRawRankingObject(mKeys[4])
                });

        detailedAssertEquals(nru, constructedNru);
    }

    @Test
    public void testRankingUpdate_emptyParcelInCheck() {
        NotificationRankingUpdate rankingUpdate = generateUpdate(getContext());
        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());
        parceledRankingUpdate.recycle();
    }

    @Test
    public void testRankingUpdate_describeContents() {
        NotificationRankingUpdate rankingUpdate = generateUpdate(getContext());
        assertEquals(0, rankingUpdate.describeContents());
    }

    @Test
    public void testRankingUpdate_equals() {
        NotificationListenerService.Ranking ranking = createEmptyTestRanking(TEST_KEY, 123, null);
        NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
                new NotificationListenerService.Ranking[]{ranking});
        // Reflexive equality, including handling nulls properly
        detailedAssertEquals(rankingUpdate, rankingUpdate);
        // Null or wrong class inequality
        assertFalse(rankingUpdate.equals(null));
        assertFalse(rankingUpdate.equals(ranking));

        // Different rank inequality
        NotificationListenerService.Ranking ranking2 = createEmptyTestRanking(TEST_KEY, 456, null);
        NotificationRankingUpdate rankingUpdate2 = new NotificationRankingUpdate(
                new NotificationListenerService.Ranking[]{ranking2});
        assertFalse(rankingUpdate.equals(rankingUpdate2));

        // Different key inequality
        ranking2 = createEmptyTestRanking(TEST_KEY + "DIFFERENT", 123, null);
        rankingUpdate2 = new NotificationRankingUpdate(
                new NotificationListenerService.Ranking[]{ranking2});
        assertFalse(rankingUpdate.equals(rankingUpdate2));
    }

    @Test
    public void testRankingUpdate_writesSmartActionToParcel() {
        if (!mRankingUpdateAshmem) {
            return;
        }
        ArrayList<Notification.Action> actions = new ArrayList<>();
        PendingIntent intent = PendingIntent.getBroadcast(
                getContext(),
                0 /*requestCode*/,
                new Intent("ACTION_" + TEST_KEY),
                PendingIntent.FLAG_IMMUTABLE /*flags*/);
        actions.add(new Notification.Action.Builder(null /*icon*/, TEST_KEY, intent).build());

        NotificationListenerService.Ranking ranking =
                createEmptyTestRanking(TEST_KEY, 123, actions);
        NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
                new NotificationListenerService.Ranking[]{ranking});

        Parcel parcel = Parcel.obtain();
        rankingUpdate.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);
        SharedMemory fd = parcel.readParcelable(getClass().getClassLoader(), SharedMemory.class);
        Bundle smartActionsBundle = parcel.readBundle(getClass().getClassLoader());

        // Assert the file descriptor is valid
        assertNotNull(fd);
        assertFalse(fd.getFd() == -1);

        // Assert that the smart action is in the parcel
        assertNotNull(smartActionsBundle);
        ArrayList<Notification.Action> recoveredActions =
                smartActionsBundle.getParcelableArrayList(TEST_KEY, Notification.Action.class);
        assertNotNull(recoveredActions);
        assertEquals(actions.size(), recoveredActions.size());
        assertEquals(actions.get(0).title.toString(), recoveredActions.get(0).title.toString());
        parcel.recycle();
    }

    @Test
    public void testRankingUpdate_handlesEmptySmartActionList() {
        if (!mRankingUpdateAshmem) {
            return;
        }
        ArrayList<Notification.Action> actions = new ArrayList<>();
        NotificationListenerService.Ranking ranking =
                createEmptyTestRanking(TEST_KEY, 123, actions);
        NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
                new NotificationListenerService.Ranking[]{ranking});

        Parcel parcel = Parcel.obtain();
        rankingUpdate.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);

        // Ensure that despite an empty actions list, we can still unparcel the update.
        NotificationRankingUpdate newRankingUpdate = new NotificationRankingUpdate(parcel);
        assertNotNull(newRankingUpdate);
        assertNotNull(newRankingUpdate.getRankingMap());
        detailedAssertEquals(rankingUpdate, newRankingUpdate);
        parcel.recycle();
    }

    @Test
    public void testRankingUpdate_handlesNullSmartActionList() {
        if (!mRankingUpdateAshmem) {
            return;
        }
        NotificationListenerService.Ranking ranking =
                createEmptyTestRanking(TEST_KEY, 123, null);
        NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
                new NotificationListenerService.Ranking[]{ranking});

        Parcel parcel = Parcel.obtain();
        rankingUpdate.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);

        // Ensure that despite an empty actions list, we can still unparcel the update.
        NotificationRankingUpdate newRankingUpdate = new NotificationRankingUpdate(parcel);
        assertNotNull(newRankingUpdate);
        assertNotNull(newRankingUpdate.getRankingMap());
        detailedAssertEquals(rankingUpdate, newRankingUpdate);
        parcel.recycle();
    }
}