Loading core/proto/android/server/peopleservice.proto +9 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading services/people/java/com/android/server/people/PeopleService.java +7 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()); } } Loading services/people/java/com/android/server/people/data/ConversationInfo.java +70 −3 Original line number Diff line number Diff line Loading @@ -90,6 +90,11 @@ public class ConversationInfo { @Nullable private String mNotificationChannelId; @Nullable private String mParentNotificationChannelId; private long mLastEventTimestamp; @ShortcutFlags private int mShortcutFlags; Loading @@ -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; } Loading Loading @@ -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); Loading Loading @@ -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; } Loading @@ -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 Loading @@ -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()) { Loading Loading @@ -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) { Loading @@ -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; Loading Loading @@ -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)); Loading Loading @@ -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; Loading @@ -408,6 +458,11 @@ public class ConversationInfo { @Nullable private String mNotificationChannelId; @Nullable private String mParentNotificationChannelId; private long mLastEventTimestamp; @ShortcutFlags private int mShortcutFlags; Loading @@ -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; } Loading Loading @@ -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; Loading services/people/java/com/android/server/people/data/DataManager.java +155 −54 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading Loading @@ -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) { Loading Loading @@ -278,7 +344,6 @@ public class DataManager { } pruneUninstalledPackageData(userData); final NotificationListener notificationListener = mNotificationListeners.get(userId); userData.forAllPackages(packageData -> { if (signal.isCanceled()) { return; Loading @@ -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); }); } Loading Loading @@ -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, Loading Loading @@ -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) { Loading Loading @@ -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); } } Loading Loading @@ -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)); Loading @@ -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); } Loading Loading @@ -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)); } Loading Loading @@ -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)); } } Loading services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java +12 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
core/proto/android/server/peopleservice.proto +9 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading
services/people/java/com/android/server/people/PeopleService.java +7 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()); } } Loading
services/people/java/com/android/server/people/data/ConversationInfo.java +70 −3 Original line number Diff line number Diff line Loading @@ -90,6 +90,11 @@ public class ConversationInfo { @Nullable private String mNotificationChannelId; @Nullable private String mParentNotificationChannelId; private long mLastEventTimestamp; @ShortcutFlags private int mShortcutFlags; Loading @@ -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; } Loading Loading @@ -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); Loading Loading @@ -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; } Loading @@ -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 Loading @@ -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()) { Loading Loading @@ -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) { Loading @@ -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; Loading Loading @@ -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)); Loading Loading @@ -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; Loading @@ -408,6 +458,11 @@ public class ConversationInfo { @Nullable private String mNotificationChannelId; @Nullable private String mParentNotificationChannelId; private long mLastEventTimestamp; @ShortcutFlags private int mShortcutFlags; Loading @@ -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; } Loading Loading @@ -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; Loading
services/people/java/com/android/server/people/data/DataManager.java +155 −54 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading Loading @@ -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) { Loading Loading @@ -278,7 +344,6 @@ public class DataManager { } pruneUninstalledPackageData(userData); final NotificationListener notificationListener = mNotificationListeners.get(userId); userData.forAllPackages(packageData -> { if (signal.isCanceled()) { return; Loading @@ -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); }); } Loading Loading @@ -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, Loading Loading @@ -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) { Loading Loading @@ -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); } } Loading Loading @@ -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)); Loading @@ -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); } Loading Loading @@ -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)); } Loading Loading @@ -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)); } } Loading
services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java +12 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes