Implement the get/remove recent conversations APIs in PeopleService

A conversation is a cached recent conversation if all below conditions
are met:
1) its shortcut is cached
2) its notification setting is not customized
3) it has recent notifications

A recent conversation can only be removed if it does not have any
active notifications.

Bug: 162593584
Test: atest + manual test
Change-Id: I0d820a6a2bb82ceb238c3f559e108829d1547291
This commit is contained in:
Danning Chen
2020-08-06 09:59:27 -07:00
committed by Beverly
parent 59fb99b1dc
commit 28f578a21a
6 changed files with 451 additions and 136 deletions

View File

@@ -46,6 +46,10 @@ message ConversationInfoProto {
// The notification channel id of the conversation.
optional string notification_channel_id = 4 [(.android.privacy).dest = DEST_EXPLICIT];
// The parent notification channel ID of the conversation. This is the notification channel where
// the notifications are posted before this conversation is customized by the user.
optional string parent_notification_channel_id = 8 [(.android.privacy).dest = DEST_EXPLICIT];
// Integer representation of shortcut bit flags.
optional int32 shortcut_flags = 5;
@@ -54,6 +58,11 @@ message ConversationInfoProto {
// The phone number of the contact.
optional string contact_phone_number = 7 [(.android.privacy).dest = DEST_EXPLICIT];
// The timestamp of the last event in millis.
optional int64 last_event_timestamp = 9;
// Next tag: 10
}
// On disk data of events.

View File

@@ -40,7 +40,6 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemService;
import com.android.server.people.data.DataManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@@ -106,17 +105,23 @@ public class PeopleService extends SystemService {
@Override
public ParceledListSlice<ConversationChannel> getRecentConversations() {
enforceSystemOrRoot("get recent conversations");
return new ParceledListSlice<>(new ArrayList<>());
return new ParceledListSlice<>(
mDataManager.getRecentConversations(
Binder.getCallingUserHandle().getIdentifier()));
}
@Override
public void removeRecentConversation(String packageName, int userId, String shortcutId) {
enforceSystemOrRoot("remove a recent conversation");
mDataManager.removeRecentConversation(packageName, userId, shortcutId,
Binder.getCallingUserHandle().getIdentifier());
}
@Override
public void removeAllRecentConversations() {
enforceSystemOrRoot("remove all recent conversations");
mDataManager.removeAllRecentConversations(
Binder.getCallingUserHandle().getIdentifier());
}
}

View File

@@ -90,6 +90,11 @@ public class ConversationInfo {
@Nullable
private String mNotificationChannelId;
@Nullable
private String mParentNotificationChannelId;
private long mLastEventTimestamp;
@ShortcutFlags
private int mShortcutFlags;
@@ -102,6 +107,8 @@ public class ConversationInfo {
mContactUri = builder.mContactUri;
mContactPhoneNumber = builder.mContactPhoneNumber;
mNotificationChannelId = builder.mNotificationChannelId;
mParentNotificationChannelId = builder.mParentNotificationChannelId;
mLastEventTimestamp = builder.mLastEventTimestamp;
mShortcutFlags = builder.mShortcutFlags;
mConversationFlags = builder.mConversationFlags;
}
@@ -129,14 +136,32 @@ public class ConversationInfo {
}
/**
* ID of the {@link android.app.NotificationChannel} where the notifications for this
* conversation are posted.
* ID of the conversation-specific {@link android.app.NotificationChannel} where the
* notifications for this conversation are posted.
*/
@Nullable
String getNotificationChannelId() {
return mNotificationChannelId;
}
/**
* ID of the parent {@link android.app.NotificationChannel} for this conversation. This is the
* notification channel where the notifications are posted before this conversation is
* customized by the user.
*/
@Nullable
String getParentNotificationChannelId() {
return mParentNotificationChannelId;
}
/**
* Timestamp of the last event, {@code 0L} if there are no events. This timestamp is for
* identifying and sorting the recent conversations. It may only count a subset of event types.
*/
long getLastEventTimestamp() {
return mLastEventTimestamp;
}
/** Whether the shortcut for this conversation is set long-lived by the app. */
public boolean isShortcutLongLived() {
return hasShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED);
@@ -202,6 +227,8 @@ public class ConversationInfo {
&& Objects.equals(mContactUri, other.mContactUri)
&& Objects.equals(mContactPhoneNumber, other.mContactPhoneNumber)
&& Objects.equals(mNotificationChannelId, other.mNotificationChannelId)
&& Objects.equals(mParentNotificationChannelId, other.mParentNotificationChannelId)
&& Objects.equals(mLastEventTimestamp, other.mLastEventTimestamp)
&& mShortcutFlags == other.mShortcutFlags
&& mConversationFlags == other.mConversationFlags;
}
@@ -209,7 +236,8 @@ public class ConversationInfo {
@Override
public int hashCode() {
return Objects.hash(mShortcutId, mLocusId, mContactUri, mContactPhoneNumber,
mNotificationChannelId, mShortcutFlags, mConversationFlags);
mNotificationChannelId, mParentNotificationChannelId, mLastEventTimestamp,
mShortcutFlags, mConversationFlags);
}
@Override
@@ -221,6 +249,8 @@ public class ConversationInfo {
sb.append(", contactUri=").append(mContactUri);
sb.append(", phoneNumber=").append(mContactPhoneNumber);
sb.append(", notificationChannelId=").append(mNotificationChannelId);
sb.append(", parentNotificationChannelId=").append(mParentNotificationChannelId);
sb.append(", lastEventTimestamp=").append(mLastEventTimestamp);
sb.append(", shortcutFlags=0x").append(Integer.toHexString(mShortcutFlags));
sb.append(" [");
if (isShortcutLongLived()) {
@@ -280,6 +310,11 @@ public class ConversationInfo {
protoOutputStream.write(ConversationInfoProto.NOTIFICATION_CHANNEL_ID,
mNotificationChannelId);
}
if (mParentNotificationChannelId != null) {
protoOutputStream.write(ConversationInfoProto.PARENT_NOTIFICATION_CHANNEL_ID,
mParentNotificationChannelId);
}
protoOutputStream.write(ConversationInfoProto.LAST_EVENT_TIMESTAMP, mLastEventTimestamp);
protoOutputStream.write(ConversationInfoProto.SHORTCUT_FLAGS, mShortcutFlags);
protoOutputStream.write(ConversationInfoProto.CONVERSATION_FLAGS, mConversationFlags);
if (mContactPhoneNumber != null) {
@@ -300,6 +335,8 @@ public class ConversationInfo {
out.writeInt(mShortcutFlags);
out.writeInt(mConversationFlags);
out.writeUTF(mContactPhoneNumber != null ? mContactPhoneNumber : "");
out.writeUTF(mParentNotificationChannelId != null ? mParentNotificationChannelId : "");
out.writeLong(mLastEventTimestamp);
} catch (IOException e) {
Slog.e(TAG, "Failed to write fields to backup payload.", e);
return null;
@@ -338,6 +375,14 @@ public class ConversationInfo {
builder.setNotificationChannelId(protoInputStream.readString(
ConversationInfoProto.NOTIFICATION_CHANNEL_ID));
break;
case (int) ConversationInfoProto.PARENT_NOTIFICATION_CHANNEL_ID:
builder.setParentNotificationChannelId(protoInputStream.readString(
ConversationInfoProto.PARENT_NOTIFICATION_CHANNEL_ID));
break;
case (int) ConversationInfoProto.LAST_EVENT_TIMESTAMP:
builder.setLastEventTimestamp(protoInputStream.readLong(
ConversationInfoProto.LAST_EVENT_TIMESTAMP));
break;
case (int) ConversationInfoProto.SHORTCUT_FLAGS:
builder.setShortcutFlags(protoInputStream.readInt(
ConversationInfoProto.SHORTCUT_FLAGS));
@@ -382,6 +427,11 @@ public class ConversationInfo {
if (!TextUtils.isEmpty(contactPhoneNumber)) {
builder.setContactPhoneNumber(contactPhoneNumber);
}
String parentNotificationChannelId = in.readUTF();
if (!TextUtils.isEmpty(parentNotificationChannelId)) {
builder.setParentNotificationChannelId(parentNotificationChannelId);
}
builder.setLastEventTimestamp(in.readLong());
} catch (IOException e) {
Slog.e(TAG, "Failed to read conversation info fields from backup payload.", e);
return null;
@@ -408,6 +458,11 @@ public class ConversationInfo {
@Nullable
private String mNotificationChannelId;
@Nullable
private String mParentNotificationChannelId;
private long mLastEventTimestamp;
@ShortcutFlags
private int mShortcutFlags;
@@ -427,6 +482,8 @@ public class ConversationInfo {
mContactUri = conversationInfo.mContactUri;
mContactPhoneNumber = conversationInfo.mContactPhoneNumber;
mNotificationChannelId = conversationInfo.mNotificationChannelId;
mParentNotificationChannelId = conversationInfo.mParentNotificationChannelId;
mLastEventTimestamp = conversationInfo.mLastEventTimestamp;
mShortcutFlags = conversationInfo.mShortcutFlags;
mConversationFlags = conversationInfo.mConversationFlags;
}
@@ -456,6 +513,16 @@ public class ConversationInfo {
return this;
}
Builder setParentNotificationChannelId(String parentNotificationChannelId) {
mParentNotificationChannelId = parentNotificationChannelId;
return this;
}
Builder setLastEventTimestamp(long lastEventTimestamp) {
mLastEventTimestamp = lastEventTimestamp;
return this;
}
Builder setShortcutFlags(@ShortcutFlags int shortcutFlags) {
mShortcutFlags = shortcutFlags;
return this;

View File

@@ -24,6 +24,7 @@ import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Person;
import android.app.people.ConversationChannel;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.usage.UsageEvents;
@@ -74,9 +75,11 @@ import com.android.server.notification.ShortcutHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -97,6 +100,7 @@ public class DataManager {
private static final long QUERY_EVENTS_MAX_AGE_MS = 5L * DateUtils.MINUTE_IN_MILLIS;
private static final long USAGE_STATS_QUERY_INTERVAL_SEC = 120L;
@VisibleForTesting static final int MAX_CACHED_RECENT_SHORTCUTS = 30;
private final Context mContext;
private final Injector mInjector;
@@ -209,6 +213,68 @@ public class DataManager {
mContext.getPackageName(), intentFilter, callingUserId);
}
/** Returns the cached non-customized recent conversations. */
public List<ConversationChannel> getRecentConversations(@UserIdInt int callingUserId) {
List<ConversationChannel> conversationChannels = new ArrayList<>();
forPackagesInProfile(callingUserId, packageData -> {
String packageName = packageData.getPackageName();
int userId = packageData.getUserId();
packageData.forAllConversations(conversationInfo -> {
if (!isCachedRecentConversation(conversationInfo)) {
return;
}
String shortcutId = conversationInfo.getShortcutId();
ShortcutInfo shortcutInfo = getShortcut(packageName, userId, shortcutId);
int uid = mPackageManagerInternal.getPackageUid(packageName, 0, userId);
NotificationChannel parentChannel =
mNotificationManagerInternal.getNotificationChannel(packageName, uid,
conversationInfo.getParentNotificationChannelId());
if (shortcutInfo == null || parentChannel == null) {
return;
}
conversationChannels.add(
new ConversationChannel(shortcutInfo, parentChannel,
conversationInfo.getLastEventTimestamp(),
hasActiveNotifications(packageName, userId, shortcutId)));
});
});
return conversationChannels;
}
/**
* Uncaches the shortcut that's associated with the specified conversation so this conversation
* will not show up in the recent conversations list.
*/
public void removeRecentConversation(String packageName, int userId, String shortcutId,
@UserIdInt int callingUserId) {
if (!hasActiveNotifications(packageName, userId, shortcutId)) {
mShortcutServiceInternal.uncacheShortcuts(callingUserId, mContext.getPackageName(),
packageName, Collections.singletonList(shortcutId), userId,
ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
}
}
/**
* Uncaches the shortcuts for all the recent conversations that they don't have active
* notifications.
*/
public void removeAllRecentConversations(@UserIdInt int callingUserId) {
forPackagesInProfile(callingUserId, packageData -> {
String packageName = packageData.getPackageName();
int userId = packageData.getUserId();
List<String> idsToUncache = new ArrayList<>();
packageData.forAllConversations(conversationInfo -> {
String shortcutId = conversationInfo.getShortcutId();
if (isCachedRecentConversation(conversationInfo)
&& !hasActiveNotifications(packageName, userId, shortcutId)) {
idsToUncache.add(shortcutId);
}
});
mShortcutServiceInternal.uncacheShortcuts(callingUserId, mContext.getPackageName(),
packageName, idsToUncache, userId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
});
}
/** Reports the sharing related {@link AppTargetEvent} from App Prediction Manager. */
public void reportShareTargetEvent(@NonNull AppTargetEvent event,
@NonNull IntentFilter intentFilter) {
@@ -278,7 +344,6 @@ public class DataManager {
}
pruneUninstalledPackageData(userData);
final NotificationListener notificationListener = mNotificationListeners.get(userId);
userData.forAllPackages(packageData -> {
if (signal.isCanceled()) {
return;
@@ -291,20 +356,7 @@ public class DataManager {
packageData.getEventStore().deleteEventHistories(EventStore.CATEGORY_SMS);
}
packageData.pruneOrphanEvents();
if (notificationListener != null) {
String packageName = packageData.getPackageName();
packageData.forAllConversations(conversationInfo -> {
if (conversationInfo.isShortcutCachedForNotification()
&& conversationInfo.getNotificationChannelId() == null
&& !notificationListener.hasActiveNotifications(
packageName, conversationInfo.getShortcutId())) {
mShortcutServiceInternal.uncacheShortcuts(userId,
mContext.getPackageName(), packageName,
Collections.singletonList(conversationInfo.getShortcutId()),
userId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
}
});
}
cleanupCachedShortcuts(userId, MAX_CACHED_RECENT_SHORTCUTS);
});
}
@@ -467,7 +519,8 @@ public class DataManager {
@NonNull String packageName, @UserIdInt int userId,
@Nullable List<String> shortcutIds) {
@ShortcutQuery.QueryFlags int queryFlags = ShortcutQuery.FLAG_MATCH_DYNAMIC
| ShortcutQuery.FLAG_MATCH_PINNED | ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
| ShortcutQuery.FLAG_MATCH_PINNED | ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
| ShortcutQuery.FLAG_MATCH_CACHED;
return mShortcutServiceInternal.getShortcuts(
UserHandle.USER_SYSTEM, mContext.getPackageName(),
/*changedSince=*/ 0, packageName, shortcutIds, /*locusIds=*/ null,
@@ -527,6 +580,68 @@ public class DataManager {
return packageData;
}
private boolean isCachedRecentConversation(ConversationInfo conversationInfo) {
return conversationInfo.isShortcutCachedForNotification()
&& conversationInfo.getNotificationChannelId() == null
&& conversationInfo.getParentNotificationChannelId() != null
&& conversationInfo.getLastEventTimestamp() > 0L;
}
private boolean hasActiveNotifications(String packageName, @UserIdInt int userId,
String shortcutId) {
NotificationListener notificationListener = mNotificationListeners.get(userId);
return notificationListener != null
&& notificationListener.hasActiveNotifications(packageName, shortcutId);
}
/**
* Cleans up the oldest cached shortcuts that don't have active notifications for the recent
* conversations. After the cleanup, normally, the total number of cached shortcuts will be
* less than or equal to the target count. However, there are exception cases: e.g. when all
* the existing cached shortcuts have active notifications.
*/
private void cleanupCachedShortcuts(@UserIdInt int userId, int targetCachedCount) {
UserData userData = getUnlockedUserData(userId);
if (userData == null) {
return;
}
// pair of <package name, conversation info>
List<Pair<String, ConversationInfo>> cachedConvos = new ArrayList<>();
userData.forAllPackages(packageData ->
packageData.forAllConversations(conversationInfo -> {
if (isCachedRecentConversation(conversationInfo)) {
cachedConvos.add(
Pair.create(packageData.getPackageName(), conversationInfo));
}
})
);
if (cachedConvos.size() <= targetCachedCount) {
return;
}
int numToUncache = cachedConvos.size() - targetCachedCount;
// Max heap keeps the oldest cached conversations.
PriorityQueue<Pair<String, ConversationInfo>> maxHeap = new PriorityQueue<>(
numToUncache + 1,
Comparator.comparingLong((Pair<String, ConversationInfo> pair) ->
pair.second.getLastEventTimestamp()).reversed());
for (Pair<String, ConversationInfo> cached : cachedConvos) {
if (hasActiveNotifications(cached.first, userId, cached.second.getShortcutId())) {
continue;
}
maxHeap.offer(cached);
if (maxHeap.size() > numToUncache) {
maxHeap.poll();
}
}
while (!maxHeap.isEmpty()) {
Pair<String, ConversationInfo> toUncache = maxHeap.poll();
mShortcutServiceInternal.uncacheShortcuts(userId,
mContext.getPackageName(), toUncache.first,
Collections.singletonList(toUncache.second.getShortcutId()),
userId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
}
}
@VisibleForTesting
@WorkerThread
void addOrUpdateConversationInfo(@NonNull ShortcutInfo shortcutInfo) {
@@ -737,9 +852,21 @@ public class DataManager {
public void onShortcutsAddedOrUpdated(@NonNull String packageName,
@NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
mInjector.getBackgroundExecutor().execute(() -> {
PackageData packageData = getPackage(packageName, user.getIdentifier());
for (ShortcutInfo shortcut : shortcuts) {
if (ShortcutHelper.isConversationShortcut(
shortcut, mShortcutServiceInternal, user.getIdentifier())) {
if (shortcut.isCached()) {
ConversationInfo conversationInfo = packageData != null
? packageData.getConversationInfo(shortcut.getId()) : null;
if (conversationInfo == null
|| !conversationInfo.isShortcutCachedForNotification()) {
// This is a newly cached shortcut. Clean up the existing cached
// shortcuts to ensure the cache size is under the limit.
cleanupCachedShortcuts(user.getIdentifier(),
MAX_CACHED_RECENT_SHORTCUTS - 1);
}
}
addOrUpdateConversationInfo(shortcut);
}
}
@@ -800,6 +927,16 @@ public class DataManager {
});
if (packageData != null) {
ConversationInfo conversationInfo = packageData.getConversationInfo(shortcutId);
if (conversationInfo == null) {
return;
}
ConversationInfo updated = new ConversationInfo.Builder(conversationInfo)
.setLastEventTimestamp(sbn.getPostTime())
.setParentNotificationChannelId(sbn.getNotification().getChannelId())
.build();
packageData.getConversationStore().addOrUpdate(updated);
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateEventHistory(
EventStore.CATEGORY_SHORTCUT_BASED, shortcutId);
eventHistory.addEvent(new Event(sbn.getPostTime(), Event.TYPE_NOTIFICATION_POSTED));
@@ -820,16 +957,7 @@ public class DataManager {
int count = mActiveNotifCounts.getOrDefault(conversationKey, 0) - 1;
if (count <= 0) {
mActiveNotifCounts.remove(conversationKey);
// The shortcut was cached by Notification Manager synchronously when the
// associated notification was posted. Uncache it here when all the
// associated notifications are removed.
if (conversationInfo.isShortcutCachedForNotification()
&& conversationInfo.getNotificationChannelId() == null) {
mShortcutServiceInternal.uncacheShortcuts(mUserId,
mContext.getPackageName(), sbn.getPackageName(),
Collections.singletonList(conversationInfo.getShortcutId()),
mUserId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
}
cleanupCachedShortcuts(mUserId, MAX_CACHED_RECENT_SHORTCUTS);
} else {
mActiveNotifCounts.put(conversationKey, count);
}
@@ -885,24 +1013,6 @@ public class DataManager {
conversationStore.addOrUpdate(builder.build());
}
synchronized void cleanupCachedShortcuts() {
for (Pair<String, String> conversationKey : mActiveNotifCounts.keySet()) {
String packageName = conversationKey.first;
String shortcutId = conversationKey.second;
PackageData packageData = getPackage(packageName, mUserId);
ConversationInfo conversationInfo =
packageData != null ? packageData.getConversationInfo(shortcutId) : null;
if (conversationInfo != null
&& conversationInfo.isShortcutCachedForNotification()
&& conversationInfo.getNotificationChannelId() == null) {
mShortcutServiceInternal.uncacheShortcuts(mUserId,
mContext.getPackageName(), packageName,
Collections.singletonList(shortcutId),
mUserId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
}
}
}
synchronized boolean hasActiveNotifications(String packageName, String shortcutId) {
return mActiveNotifCounts.containsKey(Pair.create(packageName, shortcutId));
}
@@ -975,16 +1085,7 @@ public class DataManager {
@Override
public void onReceive(Context context, Intent intent) {
forAllUnlockedUsers(userData -> {
NotificationListener listener = mNotificationListeners.get(userData.getUserId());
// Clean up the cached shortcuts because all the notifications are cleared after
// system shutdown. The associated shortcuts need to be uncached to keep in sync
// unless the settings are changed by the user.
if (listener != null) {
listener.cleanupCachedShortcuts();
}
userData.forAllPackages(PackageData::saveToDisk);
});
forAllUnlockedUsers(userData -> userData.forAllPackages(PackageData::saveToDisk));
}
}

View File

@@ -37,6 +37,7 @@ public final class ConversationInfoTest {
private static final Uri CONTACT_URI = Uri.parse("tel:+1234567890");
private static final String PHONE_NUMBER = "+1234567890";
private static final String NOTIFICATION_CHANNEL_ID = "test : abc";
private static final String PARENT_NOTIFICATION_CHANNEL_ID = "test";
@Test
public void testBuild() {
@@ -46,6 +47,8 @@ public final class ConversationInfoTest {
.setContactUri(CONTACT_URI)
.setContactPhoneNumber(PHONE_NUMBER)
.setNotificationChannelId(NOTIFICATION_CHANNEL_ID)
.setParentNotificationChannelId(PARENT_NOTIFICATION_CHANNEL_ID)
.setLastEventTimestamp(100L)
.setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED
| ShortcutInfo.FLAG_CACHED_NOTIFICATIONS)
.setImportant(true)
@@ -62,6 +65,9 @@ public final class ConversationInfoTest {
assertEquals(CONTACT_URI, conversationInfo.getContactUri());
assertEquals(PHONE_NUMBER, conversationInfo.getContactPhoneNumber());
assertEquals(NOTIFICATION_CHANNEL_ID, conversationInfo.getNotificationChannelId());
assertEquals(PARENT_NOTIFICATION_CHANNEL_ID,
conversationInfo.getParentNotificationChannelId());
assertEquals(100L, conversationInfo.getLastEventTimestamp());
assertTrue(conversationInfo.isShortcutLongLived());
assertTrue(conversationInfo.isShortcutCachedForNotification());
assertTrue(conversationInfo.isImportant());
@@ -84,6 +90,8 @@ public final class ConversationInfoTest {
assertNull(conversationInfo.getContactUri());
assertNull(conversationInfo.getContactPhoneNumber());
assertNull(conversationInfo.getNotificationChannelId());
assertNull(conversationInfo.getParentNotificationChannelId());
assertEquals(0L, conversationInfo.getLastEventTimestamp());
assertFalse(conversationInfo.isShortcutLongLived());
assertFalse(conversationInfo.isShortcutCachedForNotification());
assertFalse(conversationInfo.isImportant());
@@ -103,6 +111,8 @@ public final class ConversationInfoTest {
.setContactUri(CONTACT_URI)
.setContactPhoneNumber(PHONE_NUMBER)
.setNotificationChannelId(NOTIFICATION_CHANNEL_ID)
.setParentNotificationChannelId(PARENT_NOTIFICATION_CHANNEL_ID)
.setLastEventTimestamp(100L)
.setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED)
.setImportant(true)
.setNotificationSilenced(true)
@@ -122,6 +132,8 @@ public final class ConversationInfoTest {
assertEquals(CONTACT_URI, destination.getContactUri());
assertEquals(PHONE_NUMBER, destination.getContactPhoneNumber());
assertEquals(NOTIFICATION_CHANNEL_ID, destination.getNotificationChannelId());
assertEquals(PARENT_NOTIFICATION_CHANNEL_ID, destination.getParentNotificationChannelId());
assertEquals(100L, destination.getLastEventTimestamp());
assertTrue(destination.isShortcutLongLived());
assertFalse(destination.isImportant());
assertTrue(destination.isNotificationSilenced());

View File

@@ -29,6 +29,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
@@ -45,6 +46,7 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Person;
import android.app.job.JobScheduler;
import android.app.people.ConversationChannel;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
@@ -112,6 +114,7 @@ public final class DataManagerTest {
private static final String CONTACT_URI = "content://com.android.contacts/contacts/lookup/123";
private static final String PHONE_NUMBER = "+1234567890";
private static final String NOTIFICATION_CHANNEL_ID = "test : sc";
private static final String PARENT_NOTIFICATION_CHANNEL_ID = "test";
private static final long MILLIS_PER_MINUTE = 1000L * 60L;
@Mock private Context mContext;
@@ -133,10 +136,12 @@ public final class DataManagerTest {
private ScheduledExecutorService mExecutorService;
private NotificationChannel mNotificationChannel;
private NotificationChannel mParentNotificationChannel;
private DataManager mDataManager;
private CancellationSignal mCancellationSignal;
private ShortcutChangeCallback mShortcutChangeCallback;
private BroadcastReceiver mShutdownBroadcastReceiver;
private ShortcutInfo mShortcutInfo;
private TestInjector mInjector;
@Before
@@ -157,6 +162,11 @@ public final class DataManagerTest {
}).when(mPackageManagerInternal).forEachInstalledPackage(any(Consumer.class), anyInt());
addLocalServiceMock(NotificationManagerInternal.class, mNotificationManagerInternal);
mParentNotificationChannel = new NotificationChannel(
PARENT_NOTIFICATION_CHANNEL_ID, "test channel",
NotificationManager.IMPORTANCE_DEFAULT);
when(mNotificationManagerInternal.getNotificationChannel(anyString(), anyInt(),
anyString())).thenReturn(mParentNotificationChannel);
when(mContext.getContentResolver()).thenReturn(mContentResolver);
when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
@@ -199,6 +209,7 @@ public final class DataManagerTest {
when(mStatusBarNotification.getUser()).thenReturn(UserHandle.of(USER_ID_PRIMARY));
when(mStatusBarNotification.getPostTime()).thenReturn(System.currentTimeMillis());
when(mNotification.getShortcutId()).thenReturn(TEST_SHORTCUT_ID);
when(mNotification.getChannelId()).thenReturn(PARENT_NOTIFICATION_CHANNEL_ID);
mNotificationChannel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID, "test channel", NotificationManager.IMPORTANCE_DEFAULT);
@@ -212,6 +223,13 @@ public final class DataManagerTest {
when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(),
anyString(), anyInt(), any())).thenReturn(true);
mShortcutInfo = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
when(mShortcutServiceInternal.getShortcuts(
anyInt(), anyString(), anyLong(), anyString(), anyList(), any(), any(),
anyInt(), anyInt(), anyInt(), anyInt()))
.thenReturn(Collections.singletonList(mShortcutInfo));
verify(mShortcutServiceInternal).addShortcutChangeCallback(
mShortcutChangeCallbackCaptor.capture());
mShortcutChangeCallback = mShortcutChangeCallbackCaptor.getValue();
@@ -417,29 +435,28 @@ public final class DataManagerTest {
List<Range<Long>> activeNotificationOpenTimeSlots = getActiveSlotsForTestShortcut(
Event.NOTIFICATION_EVENT_TYPES);
assertEquals(1, activeNotificationOpenTimeSlots.size());
verify(mShortcutServiceInternal).uncacheShortcuts(
anyInt(), any(), eq(TEST_PKG_NAME),
eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
}
@Test
public void testNotificationDismissed() {
public void testUncacheShortcutsWhenNotificationsDismissed() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.addOrUpdateConversationInfo(shortcut);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
// Post one notification.
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
listenerService.onNotificationPosted(mStatusBarNotification);
// The cached conversations are above the limit because every conversation has active
// notifications. To uncache one of them, the notifications for that conversation need to
// be dismissed.
for (int i = 0; i < DataManager.MAX_CACHED_RECENT_SHORTCUTS + 1; i++) {
String shortcutId = TEST_SHORTCUT_ID + i;
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId,
buildPerson());
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
when(mNotification.getShortcutId()).thenReturn(shortcutId);
listenerService.onNotificationPosted(mStatusBarNotification);
}
// Post another notification.
// Post another notification for the last conversation.
listenerService.onNotificationPosted(mStatusBarNotification);
// Removing one of the two notifications does not un-cache the shortcut.
@@ -452,13 +469,12 @@ public final class DataManagerTest {
listenerService.onNotificationRemoved(mStatusBarNotification, null,
NotificationListenerService.REASON_CANCEL_ALL);
verify(mShortcutServiceInternal).uncacheShortcuts(
anyInt(), any(), eq(TEST_PKG_NAME),
eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
anyInt(), any(), eq(TEST_PKG_NAME), anyList(), eq(USER_ID_PRIMARY),
eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
}
@Test
public void testShortcutNotUncachedIfNotificationChannelCreated() {
public void testConversationIsNotRecentIfCustomized() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
@@ -472,15 +488,12 @@ public final class DataManagerTest {
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
assertEquals(1, mDataManager.getRecentConversations(USER_ID_PRIMARY).size());
listenerService.onNotificationChannelModified(TEST_PKG_NAME, UserHandle.of(USER_ID_PRIMARY),
mNotificationChannel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
listenerService.onNotificationRemoved(mStatusBarNotification, null,
NotificationListenerService.REASON_CANCEL_ALL);
verify(mShortcutServiceInternal, never()).uncacheShortcuts(
anyInt(), any(), eq(TEST_PKG_NAME),
eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
assertTrue(mDataManager.getRecentConversations(USER_ID_PRIMARY).isEmpty());
}
@Test
@@ -560,53 +573,6 @@ public final class DataManagerTest {
assertFalse(conversationInfo.isDemoted());
}
@Test
public void testUncacheShortcutWhenShutdown() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.addOrUpdateConversationInfo(shortcut);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
listenerService.onNotificationPosted(mStatusBarNotification);
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
mShutdownBroadcastReceiver.onReceive(mContext, new Intent());
verify(mShortcutServiceInternal).uncacheShortcuts(
anyInt(), any(), eq(TEST_PKG_NAME),
eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
}
@Test
public void testDoNotUncacheShortcutWhenShutdownIfNotificationChannelCreated() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.addOrUpdateConversationInfo(shortcut);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
listenerService.onNotificationPosted(mStatusBarNotification);
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
listenerService.onNotificationChannelModified(TEST_PKG_NAME, UserHandle.of(USER_ID_PRIMARY),
mNotificationChannel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
mShutdownBroadcastReceiver.onReceive(mContext, new Intent());
verify(mShortcutServiceInternal, never()).uncacheShortcuts(
anyInt(), any(), eq(TEST_PKG_NAME),
eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
}
@Test
public void testShortcutAddedOrUpdated() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
@@ -769,20 +735,57 @@ public final class DataManagerTest {
}
@Test
public void testPruneInactiveCachedShortcuts() {
public void testDoNotUncacheShortcutWithActiveNotifications() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
for (int i = 0; i < DataManager.MAX_CACHED_RECENT_SHORTCUTS + 1; i++) {
String shortcutId = TEST_SHORTCUT_ID + i;
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId,
buildPerson());
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
when(mNotification.getShortcutId()).thenReturn(shortcutId);
listenerService.onNotificationPosted(mStatusBarNotification);
}
mDataManager.pruneDataForUser(USER_ID_PRIMARY, mCancellationSignal);
verify(mShortcutServiceInternal, never()).uncacheShortcuts(
anyInt(), anyString(), anyString(), anyList(), anyInt(), anyInt());
}
@Test
public void testUncacheOldestCachedShortcut() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
for (int i = 0; i < DataManager.MAX_CACHED_RECENT_SHORTCUTS + 1; i++) {
String shortcutId = TEST_SHORTCUT_ID + i;
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId,
buildPerson());
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
when(mNotification.getShortcutId()).thenReturn(shortcutId);
when(mStatusBarNotification.getPostTime()).thenReturn(100L + i);
listenerService.onNotificationPosted(mStatusBarNotification);
listenerService.onNotificationRemoved(mStatusBarNotification, null,
NotificationListenerService.REASON_CANCEL);
}
// Only the shortcut #0 is uncached, all the others are not.
verify(mShortcutServiceInternal).uncacheShortcuts(
anyInt(), any(), eq(TEST_PKG_NAME),
eq(Collections.singletonList(TEST_SHORTCUT_ID)), eq(USER_ID_PRIMARY),
eq(Collections.singletonList(TEST_SHORTCUT_ID + 0)), eq(USER_ID_PRIMARY),
eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
for (int i = 1; i < DataManager.MAX_CACHED_RECENT_SHORTCUTS + 1; i++) {
verify(mShortcutServiceInternal, never()).uncacheShortcuts(
anyInt(), anyString(), anyString(),
eq(Collections.singletonList(TEST_SHORTCUT_ID + i)), anyInt(),
eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
}
}
@Test
@@ -812,6 +815,124 @@ public final class DataManagerTest {
assertEquals(conversationInfo.getShortcutId(), TEST_SHORTCUT_ID);
}
@Test
public void testGetRecentConversations() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
listenerService.onNotificationPosted(mStatusBarNotification);
List<ConversationChannel> result = mDataManager.getRecentConversations(USER_ID_PRIMARY);
assertEquals(1, result.size());
assertEquals(shortcut.getId(), result.get(0).getShortcutInfo().getId());
assertEquals(mParentNotificationChannel.getId(),
result.get(0).getParentNotificationChannel().getId());
assertEquals(mStatusBarNotification.getPostTime(), result.get(0).getLastEventTimestamp());
assertTrue(result.get(0).hasActiveNotifications());
}
@Test
public void testNonCachedShortcutNotInRecentList() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY_MANAGED,
TEST_SHORTCUT_ID, buildPerson());
mDataManager.addOrUpdateConversationInfo(shortcut);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
listenerService.onNotificationPosted(mStatusBarNotification);
List<ConversationChannel> result = mDataManager.getRecentConversations(USER_ID_PRIMARY);
assertTrue(result.isEmpty());
}
@Test
public void testCustomizedConversationNotInRecentList() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
// Post a notification and customize the notification settings.
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
listenerService.onNotificationPosted(mStatusBarNotification);
listenerService.onNotificationChannelModified(TEST_PKG_NAME, UserHandle.of(USER_ID_PRIMARY),
mNotificationChannel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
List<ConversationChannel> result = mDataManager.getRecentConversations(USER_ID_PRIMARY);
assertTrue(result.isEmpty());
}
@Test
public void testRemoveRecentConversation() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
listenerService.onNotificationPosted(mStatusBarNotification);
listenerService.onNotificationRemoved(mStatusBarNotification, null,
NotificationListenerService.REASON_CANCEL);
mDataManager.removeRecentConversation(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
USER_ID_PRIMARY);
verify(mShortcutServiceInternal).uncacheShortcuts(
anyInt(), any(), eq(TEST_PKG_NAME), eq(Collections.singletonList(TEST_SHORTCUT_ID)),
eq(USER_ID_PRIMARY), eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
}
@Test
public void testRemoveAllRecentConversations() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut1 = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, "1",
buildPerson());
shortcut1.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut1);
ShortcutInfo shortcut2 = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, "2",
buildPerson());
shortcut2.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut2);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
// Post a notification and then dismiss it for conversation #1.
when(mNotification.getShortcutId()).thenReturn("1");
listenerService.onNotificationPosted(mStatusBarNotification);
listenerService.onNotificationRemoved(mStatusBarNotification, null,
NotificationListenerService.REASON_CANCEL);
// Post a notification for conversation #2, but don't dismiss it. Its shortcut won't be
// uncached when removeAllRecentConversations() is called.
when(mNotification.getShortcutId()).thenReturn("2");
listenerService.onNotificationPosted(mStatusBarNotification);
mDataManager.removeAllRecentConversations(USER_ID_PRIMARY);
verify(mShortcutServiceInternal).uncacheShortcuts(
anyInt(), any(), eq(TEST_PKG_NAME), eq(Collections.singletonList("1")),
eq(USER_ID_PRIMARY), eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
verify(mShortcutServiceInternal, never()).uncacheShortcuts(
anyInt(), any(), eq(TEST_PKG_NAME), eq(Collections.singletonList("2")),
eq(USER_ID_PRIMARY), eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
}
private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
LocalServices.removeServiceForTest(clazz);
LocalServices.addService(clazz, mock);