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

* changes:
  Puts parceled Notif RankingMap in SharedMemory
  Adds flag for putting RankingMap in SharedMemory
This commit is contained in:
Alexander Roederer
2023-06-12 17:47:54 +00:00
committed by Android (Google) Code Review
4 changed files with 331 additions and 3 deletions

View File

@@ -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 {

View File

@@ -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) {
out.writeParcelable(mRankingMap, 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 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) {

View File

@@ -85,6 +85,10 @@ public class SystemUiSystemPropertiesFlags {
/** Gating the holding of WakeLocks until NLSes are told about a new notification. */ /** Gating the holding of WakeLocks until NLSes are told about a new notification. */
public static final Flag WAKE_LOCK_FOR_POSTING_NOTIFICATION = public static final Flag WAKE_LOCK_FOR_POSTING_NOTIFICATION =
devFlag("persist.sysui.notification.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. == //// //// == End of flags. Everything below this line is the implementation. == ////

View File

@@ -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));
}
}