blob: 7660ed96d30e6ca252a75f385fa328b19121eaf7 [file] [log] [blame]
/*
* Copyright (C) 2014 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 android.annotation.SuppressLint;
import android.app.Notification;
import android.os.Bundle;
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 androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Represents an update to notification rankings.
*
* @hide
*/
@SuppressLint({"ParcelNotFinal", "ParcelCreator"})
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) {
if (Flags.rankingUpdateAshmem()) {
// 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);
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;
if (mRankingMapFd == null) {
mRankingMap = null;
return;
}
// We only need read-only access to the shared memory region.
buffer = mRankingMapFd.mapReadOnly();
byte[] payload = new byte[buffer.remaining()];
buffer.get(payload);
mapParcel.unmarshall(payload, 0, payload.length);
mapParcel.setDataPosition(0);
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.
throw new RuntimeException(e);
} finally {
mapParcel.recycle();
if (buffer != null && mRankingMapFd != null) {
SharedMemory.unmap(buffer);
mRankingMapFd.close();
}
}
} else {
mRankingMap = in.readParcelable(getClass().getClassLoader(),
android.service.notification.NotificationListenerService.RankingMap.class);
}
}
/**
* 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
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
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;
if (o == null || getClass() != o.getClass()) return false;
NotificationRankingUpdate other = (NotificationRankingUpdate) o;
return mRankingMap.equals(other.mRankingMap);
}
/**
* @hide
*/
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
if (Flags.rankingUpdateAshmem()) {
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]
)
);
ByteBuffer buffer = null;
try {
// Parcels the ranking map and measures its size.
mapParcel.writeParcelable(marshalableRankingMap, flags);
int mapSize = mapParcel.dataSize();
// Creates a new SharedMemory object with enough space to hold the ranking map.
mRankingMapFd = SharedMemory.create(mSharedMemoryName, mapSize);
// Gets a read/write buffer mapping the entire shared memory region.
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.
throw new RuntimeException(e);
} finally {
mapParcel.recycle();
// To prevent memory leaks, we can close the ranking map fd here.
// This is safe to do because a reference to this still exists.
if (buffer != null && mRankingMapFd != null) {
SharedMemory.unmap(buffer);
mRankingMapFd.close();
}
}
} else {
out.writeParcelable(mRankingMap, flags);
}
}
/**
* @hide
*/
public static final @NonNull Parcelable.Creator<NotificationRankingUpdate> CREATOR
= new Parcelable.Creator<NotificationRankingUpdate>() {
public NotificationRankingUpdate createFromParcel(Parcel parcel) {
return new NotificationRankingUpdate(parcel);
}
public NotificationRankingUpdate[] newArray(int size) {
return new NotificationRankingUpdate[size];
}
};
}