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

Commit 78a4a549 authored by Alexander Roederer's avatar Alexander Roederer Committed by Android (Google) Code Review
Browse files

Merge changes from topic "b249848655-BinderRanking" into udc-qpr-dev

* changes:
  Puts parceled Notif RankingMap in SharedMemory
  Adds flag for putting RankingMap in SharedMemory
parents 7229d8b7 f0dcb57d
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -2958,6 +2958,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 {
+128 −3
Original line number Diff line number Diff line
@@ -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) {
    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<NotificationRankingUpdate> CREATOR
            = new Parcelable.Creator<NotificationRankingUpdate>() {
        public NotificationRankingUpdate createFromParcel(Parcel parcel) {
+4 −0
Original line number Diff line number Diff line
@@ -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. == ////
+195 −0
Original line number 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));
    }
}