diff --git a/core/proto/android/server/peopleservice.proto b/core/proto/android/server/peopleservice.proto index 59556c4414ced..c465233036c40 100644 --- a/core/proto/android/server/peopleservice.proto +++ b/core/proto/android/server/peopleservice.proto @@ -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. diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java index 33317a38853e9..e3f576287c805 100644 --- a/services/people/java/com/android/server/people/PeopleService.java +++ b/services/people/java/com/android/server/people/PeopleService.java @@ -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 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()); } } diff --git a/services/people/java/com/android/server/people/data/ConversationInfo.java b/services/people/java/com/android/server/people/data/ConversationInfo.java index 17378285276ff..45f389cbd3ff3 100644 --- a/services/people/java/com/android/server/people/data/ConversationInfo.java +++ b/services/people/java/com/android/server/people/data/ConversationInfo.java @@ -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; diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index 52fec339e331a..5e67e6c7cb41b 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -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 getRecentConversations(@UserIdInt int callingUserId) { + List 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 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 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 + List> 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> maxHeap = new PriorityQueue<>( + numToUncache + 1, + Comparator.comparingLong((Pair pair) -> + pair.second.getLastEventTimestamp()).reversed()); + for (Pair cached : cachedConvos) { + if (hasActiveNotifications(cached.first, userId, cached.second.getShortcutId())) { + continue; + } + maxHeap.offer(cached); + if (maxHeap.size() > numToUncache) { + maxHeap.poll(); + } + } + while (!maxHeap.isEmpty()) { + Pair 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 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 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)); } } diff --git a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java index c5d94875b684c..c6823ebfd6554 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java @@ -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()); diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java index 0a6cd51c3cc53..efba9da06746a 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java @@ -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> 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 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 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 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 void addLocalServiceMock(Class clazz, T mock) { LocalServices.removeServiceForTest(clazz); LocalServices.addService(clazz, mock);