1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.people.data;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserIdInt;
22 import android.annotation.WorkerThread;
23 import android.app.Notification;
24 import android.app.NotificationChannel;
25 import android.app.NotificationChannelGroup;
26 import android.app.NotificationManager;
27 import android.app.Person;
28 import android.app.people.ConversationChannel;
29 import android.app.people.ConversationStatus;
30 import android.app.prediction.AppTarget;
31 import android.app.prediction.AppTargetEvent;
32 import android.app.usage.UsageEvents;
33 import android.content.BroadcastReceiver;
34 import android.content.ComponentName;
35 import android.content.ContentResolver;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.content.pm.LauncherApps;
40 import android.content.pm.LauncherApps.ShortcutQuery;
41 import android.content.pm.PackageManager;
42 import android.content.pm.PackageManagerInternal;
43 import android.content.pm.ShortcutInfo;
44 import android.content.pm.ShortcutManager.ShareShortcutInfo;
45 import android.content.pm.ShortcutServiceInternal;
46 import android.content.pm.UserInfo;
47 import android.database.ContentObserver;
48 import android.net.Uri;
49 import android.os.CancellationSignal;
50 import android.os.Handler;
51 import android.os.Looper;
52 import android.os.Process;
53 import android.os.RemoteException;
54 import android.os.UserHandle;
55 import android.os.UserManager;
56 import android.provider.CallLog;
57 import android.provider.ContactsContract.Contacts;
58 import android.provider.Telephony.MmsSms;
59 import android.service.notification.NotificationListenerService;
60 import android.service.notification.StatusBarNotification;
61 import android.telecom.TelecomManager;
62 import android.text.TextUtils;
63 import android.text.format.DateUtils;
64 import android.util.ArrayMap;
65 import android.util.ArraySet;
66 import android.util.Log;
67 import android.util.Pair;
68 import android.util.Slog;
69 import android.util.SparseArray;
70 
71 import com.android.internal.annotations.GuardedBy;
72 import com.android.internal.annotations.VisibleForTesting;
73 import com.android.internal.app.ChooserActivity;
74 import com.android.internal.content.PackageMonitor;
75 import com.android.internal.os.BackgroundThread;
76 import com.android.internal.telephony.SmsApplication;
77 import com.android.server.LocalServices;
78 import com.android.server.notification.NotificationManagerInternal;
79 import com.android.server.notification.ShortcutHelper;
80 import com.android.server.people.PeopleService;
81 
82 import java.util.ArrayList;
83 import java.util.Arrays;
84 import java.util.Collection;
85 import java.util.Collections;
86 import java.util.Comparator;
87 import java.util.HashSet;
88 import java.util.List;
89 import java.util.Map;
90 import java.util.Objects;
91 import java.util.PriorityQueue;
92 import java.util.Set;
93 import java.util.concurrent.Executor;
94 import java.util.concurrent.Executors;
95 import java.util.concurrent.ScheduledExecutorService;
96 import java.util.concurrent.ScheduledFuture;
97 import java.util.concurrent.TimeUnit;
98 import java.util.function.BiConsumer;
99 import java.util.function.Consumer;
100 import java.util.function.Function;
101 
102 /**
103  * A class manages the lifecycle of the conversations and associated data, and exposes the methods
104  * to access the data in People Service and other system services.
105  */
106 public class DataManager {
107 
108     private static final String TAG = "DataManager";
109     private static final boolean DEBUG = false;
110 
111     private static final long RECENT_NOTIFICATIONS_MAX_AGE_MS = 10 * DateUtils.DAY_IN_MILLIS;
112     private static final long QUERY_EVENTS_MAX_AGE_MS = 5L * DateUtils.MINUTE_IN_MILLIS;
113     private static final long USAGE_STATS_QUERY_INTERVAL_SEC = 120L;
114     @VisibleForTesting
115     static final int MAX_CACHED_RECENT_SHORTCUTS = 30;
116 
117     private final Context mContext;
118     private final Injector mInjector;
119     private final ScheduledExecutorService mScheduledExecutor;
120     private final Object mLock = new Object();
121 
122     private final SparseArray<UserData> mUserDataArray = new SparseArray<>();
123     private final SparseArray<BroadcastReceiver> mBroadcastReceivers = new SparseArray<>();
124     private final SparseArray<ContentObserver> mContactsContentObservers = new SparseArray<>();
125     private final SparseArray<ScheduledFuture<?>> mUsageStatsQueryFutures = new SparseArray<>();
126     private final SparseArray<NotificationListener> mNotificationListeners = new SparseArray<>();
127     private final SparseArray<PackageMonitor> mPackageMonitors = new SparseArray<>();
128     @GuardedBy("mLock")
129     private final List<PeopleService.ConversationsListener> mConversationsListeners =
130             new ArrayList<>(1);
131     private final Handler mHandler;
132     private ContentObserver mCallLogContentObserver;
133     private ContentObserver mMmsSmsContentObserver;
134 
135     private ShortcutServiceInternal mShortcutServiceInternal;
136     private PackageManagerInternal mPackageManagerInternal;
137     private NotificationManagerInternal mNotificationManagerInternal;
138     private UserManager mUserManager;
139     private ConversationStatusExpirationBroadcastReceiver mStatusExpReceiver;
140 
DataManager(Context context)141     public DataManager(Context context) {
142         this(context, new Injector(), BackgroundThread.get().getLooper());
143     }
144 
DataManager(Context context, Injector injector, Looper looper)145     DataManager(Context context, Injector injector, Looper looper) {
146         mContext = context;
147         mInjector = injector;
148         mScheduledExecutor = mInjector.createScheduledExecutor();
149         mHandler = new Handler(looper);
150     }
151 
152     /** Initialization. Called when the system services are up running. */
initialize()153     public void initialize() {
154         mShortcutServiceInternal = LocalServices.getService(ShortcutServiceInternal.class);
155         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
156         mNotificationManagerInternal = LocalServices.getService(NotificationManagerInternal.class);
157         mUserManager = mContext.getSystemService(UserManager.class);
158 
159         mShortcutServiceInternal.addShortcutChangeCallback(new ShortcutServiceCallback());
160 
161         mStatusExpReceiver = new ConversationStatusExpirationBroadcastReceiver();
162         mContext.registerReceiver(mStatusExpReceiver,
163                 ConversationStatusExpirationBroadcastReceiver.getFilter(),
164                 Context.RECEIVER_NOT_EXPORTED);
165 
166         IntentFilter shutdownIntentFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
167         BroadcastReceiver shutdownBroadcastReceiver = new ShutdownBroadcastReceiver();
168         mContext.registerReceiver(shutdownBroadcastReceiver, shutdownIntentFilter);
169     }
170 
171     /** This method is called when a user is unlocked. */
onUserUnlocked(int userId)172     public void onUserUnlocked(int userId) {
173         synchronized (mLock) {
174             UserData userData = mUserDataArray.get(userId);
175             if (userData == null) {
176                 userData = new UserData(userId, mScheduledExecutor);
177                 mUserDataArray.put(userId, userData);
178             }
179             userData.setUserUnlocked();
180         }
181         mScheduledExecutor.execute(() -> setupUser(userId));
182     }
183 
184     /** This method is called when a user is stopping. */
onUserStopping(int userId)185     public void onUserStopping(int userId) {
186         synchronized (mLock) {
187             UserData userData = mUserDataArray.get(userId);
188             if (userData != null) {
189                 userData.setUserStopped();
190             }
191         }
192         mScheduledExecutor.execute(() -> cleanupUser(userId));
193     }
194 
195     /**
196      * Iterates through all the {@link PackageData}s owned by the unlocked users who are in the
197      * same profile group as the calling user.
198      */
forPackagesInProfile(@serIdInt int callingUserId, Consumer<PackageData> consumer)199     void forPackagesInProfile(@UserIdInt int callingUserId, Consumer<PackageData> consumer) {
200         List<UserInfo> users = mUserManager.getEnabledProfiles(callingUserId);
201         for (UserInfo userInfo : users) {
202             UserData userData = getUnlockedUserData(userInfo.id);
203             if (userData != null) {
204                 userData.forAllPackages(consumer);
205             }
206         }
207     }
208 
209     /** Gets the {@link PackageData} for the given package and user. */
210     @Nullable
getPackage(@onNull String packageName, @UserIdInt int userId)211     public PackageData getPackage(@NonNull String packageName, @UserIdInt int userId) {
212         UserData userData = getUnlockedUserData(userId);
213         return userData != null ? userData.getPackageData(packageName) : null;
214     }
215 
216     /** Gets the {@link ShortcutInfo} for the given shortcut ID. */
217     @Nullable
getShortcut(@onNull String packageName, @UserIdInt int userId, @NonNull String shortcutId)218     public ShortcutInfo getShortcut(@NonNull String packageName, @UserIdInt int userId,
219             @NonNull String shortcutId) {
220         List<ShortcutInfo> shortcuts = getShortcuts(packageName, userId,
221                 Collections.singletonList(shortcutId));
222         if (shortcuts != null && !shortcuts.isEmpty()) {
223             if (DEBUG) Log.d(TAG, "Found shortcut for " + shortcuts.get(0).getLabel());
224             return shortcuts.get(0);
225         }
226         return null;
227     }
228 
229     /**
230      * Gets the {@link ShareShortcutInfo}s from all packages owned by the calling user that match
231      * the specified {@link IntentFilter}.
232      */
getShareShortcuts(@onNull IntentFilter intentFilter, @UserIdInt int callingUserId)233     public List<ShareShortcutInfo> getShareShortcuts(@NonNull IntentFilter intentFilter,
234             @UserIdInt int callingUserId) {
235         return mShortcutServiceInternal.getShareTargets(
236                 mContext.getPackageName(), intentFilter, callingUserId);
237     }
238 
239     /**
240      * Returns a {@link ConversationChannel} with the associated {@code shortcutId} if existent.
241      * Otherwise, returns null.
242      */
243     @Nullable
getConversation(String packageName, int userId, String shortcutId)244     public ConversationChannel getConversation(String packageName, int userId, String shortcutId) {
245         UserData userData = getUnlockedUserData(userId);
246         if (userData != null) {
247             PackageData packageData = userData.getPackageData(packageName);
248             // App may have been uninstalled.
249             if (packageData != null) {
250                 ConversationInfo conversationInfo = packageData.getConversationInfo(shortcutId);
251                 return getConversationChannel(packageName, userId, shortcutId, conversationInfo);
252             }
253         }
254         return null;
255     }
256 
getConversationInfo(String packageName, int userId, String shortcutId)257     ConversationInfo getConversationInfo(String packageName, int userId, String shortcutId) {
258         UserData userData = getUnlockedUserData(userId);
259         if (userData != null) {
260             PackageData packageData = userData.getPackageData(packageName);
261             // App may have been uninstalled.
262             if (packageData != null) {
263                 return packageData.getConversationInfo(shortcutId);
264             }
265         }
266         return null;
267     }
268 
269     @Nullable
getConversationChannel(String packageName, int userId, String shortcutId, ConversationInfo conversationInfo)270     private ConversationChannel getConversationChannel(String packageName, int userId,
271             String shortcutId, ConversationInfo conversationInfo) {
272         ShortcutInfo shortcutInfo = getShortcut(packageName, userId, shortcutId);
273         return getConversationChannel(
274                 shortcutInfo, conversationInfo, packageName, userId, shortcutId);
275     }
276 
277     @Nullable
getConversationChannel(ShortcutInfo shortcutInfo, ConversationInfo conversationInfo, String packageName, int userId, String shortcutId)278     private ConversationChannel getConversationChannel(ShortcutInfo shortcutInfo,
279             ConversationInfo conversationInfo, String packageName, int userId, String shortcutId) {
280         if (conversationInfo == null || conversationInfo.isDemoted()) {
281             return null;
282         }
283         if (shortcutInfo == null) {
284             Slog.e(TAG, "Shortcut no longer found");
285             mInjector.getBackgroundExecutor().execute(
286                     () -> removeConversations(packageName, userId, Set.of(shortcutId)));
287             return null;
288         }
289         int uid = mPackageManagerInternal.getPackageUid(packageName, 0, userId);
290         NotificationChannel parentChannel =
291                 mNotificationManagerInternal.getNotificationChannel(packageName, uid,
292                         conversationInfo.getNotificationChannelId());
293         NotificationChannelGroup parentChannelGroup = null;
294         if (parentChannel != null) {
295             parentChannelGroup =
296                     mNotificationManagerInternal.getNotificationChannelGroup(packageName,
297                             uid, parentChannel.getId());
298         }
299         return new ConversationChannel(shortcutInfo, uid, parentChannel,
300                 parentChannelGroup,
301                 conversationInfo.getLastEventTimestamp(),
302                 hasActiveNotifications(packageName, userId, shortcutId), false,
303                 getStatuses(conversationInfo));
304     }
305 
306     /** Returns the cached non-customized recent conversations. */
getRecentConversations(@serIdInt int callingUserId)307     public List<ConversationChannel> getRecentConversations(@UserIdInt int callingUserId) {
308         List<ConversationChannel> conversationChannels = new ArrayList<>();
309         forPackagesInProfile(callingUserId, packageData -> {
310             packageData.forAllConversations(conversationInfo -> {
311                 if (!isCachedRecentConversation(conversationInfo)) {
312                     return;
313                 }
314                 String shortcutId = conversationInfo.getShortcutId();
315                 ConversationChannel channel = getConversationChannel(packageData.getPackageName(),
316                         packageData.getUserId(), shortcutId, conversationInfo);
317                 if (channel == null || channel.getNotificationChannel() == null) {
318                     return;
319                 }
320                 conversationChannels.add(channel);
321             });
322         });
323         return conversationChannels;
324     }
325 
326     /**
327      * Uncaches the shortcut that's associated with the specified conversation so this conversation
328      * will not show up in the recent conversations list.
329      */
removeRecentConversation(String packageName, int userId, String shortcutId, @UserIdInt int callingUserId)330     public void removeRecentConversation(String packageName, int userId, String shortcutId,
331             @UserIdInt int callingUserId) {
332         if (!hasActiveNotifications(packageName, userId, shortcutId)) {
333             mShortcutServiceInternal.uncacheShortcuts(callingUserId, mContext.getPackageName(),
334                     packageName, Collections.singletonList(shortcutId), userId,
335                     ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
336         }
337     }
338 
339     /**
340      * Uncaches the shortcuts for all the recent conversations that they don't have active
341      * notifications.
342      */
removeAllRecentConversations(@serIdInt int callingUserId)343     public void removeAllRecentConversations(@UserIdInt int callingUserId) {
344         pruneOldRecentConversations(callingUserId, Long.MAX_VALUE);
345     }
346 
347     /**
348      * Uncaches the shortcuts for all the recent conversations that haven't been interacted with
349      * recently.
350      */
pruneOldRecentConversations(@serIdInt int callingUserId, long currentTimeMs)351     public void pruneOldRecentConversations(@UserIdInt int callingUserId, long currentTimeMs) {
352         forPackagesInProfile(callingUserId, packageData -> {
353             String packageName = packageData.getPackageName();
354             int userId = packageData.getUserId();
355             List<String> idsToUncache = new ArrayList<>();
356             packageData.forAllConversations(conversationInfo -> {
357                 String shortcutId = conversationInfo.getShortcutId();
358                 if (isCachedRecentConversation(conversationInfo)
359                         && (currentTimeMs - conversationInfo.getLastEventTimestamp()
360                         > RECENT_NOTIFICATIONS_MAX_AGE_MS)
361                         && !hasActiveNotifications(packageName, userId, shortcutId)) {
362                     idsToUncache.add(shortcutId);
363                 }
364             });
365             if (!idsToUncache.isEmpty()) {
366                 mShortcutServiceInternal.uncacheShortcuts(callingUserId, mContext.getPackageName(),
367                         packageName, idsToUncache, userId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
368             }
369         });
370     }
371 
372     /**
373      * Removes any status with an expiration time in the past.
374      */
pruneExpiredConversationStatuses(@serIdInt int callingUserId, long currentTimeMs)375     public void pruneExpiredConversationStatuses(@UserIdInt int callingUserId, long currentTimeMs) {
376         forPackagesInProfile(callingUserId, packageData -> {
377             final ConversationStore cs = packageData.getConversationStore();
378             packageData.forAllConversations(conversationInfo -> {
379                 ConversationInfo.Builder builder = new ConversationInfo.Builder(conversationInfo);
380                 List<ConversationStatus> newStatuses = new ArrayList<>();
381                 for (ConversationStatus status : conversationInfo.getStatuses()) {
382                     if (status.getEndTimeMillis() < 0
383                             || currentTimeMs < status.getEndTimeMillis()) {
384                         newStatuses.add(status);
385                     }
386                 }
387                 builder.setStatuses(newStatuses);
388                 updateConversationStoreThenNotifyListeners(cs, builder.build(),
389                         packageData.getPackageName(),
390                         packageData.getUserId());
391             });
392         });
393     }
394 
395     /** Returns whether {@code shortcutId} is backed by Conversation. */
isConversation(String packageName, int userId, String shortcutId)396     public boolean isConversation(String packageName, int userId, String shortcutId) {
397         ConversationChannel channel = getConversation(packageName, userId, shortcutId);
398         return channel != null
399                 && channel.getShortcutInfo() != null
400                 && !TextUtils.isEmpty(channel.getShortcutInfo().getLabel());
401     }
402 
403     /**
404      * Returns the last notification interaction with the specified conversation. If the
405      * conversation can't be found or no interactions have been recorded, returns 0L.
406      */
getLastInteraction(String packageName, int userId, String shortcutId)407     public long getLastInteraction(String packageName, int userId, String shortcutId) {
408         final PackageData packageData = getPackage(packageName, userId);
409         if (packageData != null) {
410             final ConversationInfo conversationInfo = packageData.getConversationInfo(shortcutId);
411             if (conversationInfo != null) {
412                 return conversationInfo.getLastEventTimestamp();
413             }
414         }
415         return 0L;
416     }
417 
addOrUpdateStatus(String packageName, int userId, String conversationId, ConversationStatus status)418     public void addOrUpdateStatus(String packageName, int userId, String conversationId,
419             ConversationStatus status) {
420         ConversationStore cs = getConversationStoreOrThrow(packageName, userId);
421         ConversationInfo convToModify = getConversationInfoOrThrow(cs, conversationId);
422         ConversationInfo.Builder builder = new ConversationInfo.Builder(convToModify);
423         builder.addOrUpdateStatus(status);
424         updateConversationStoreThenNotifyListeners(cs, builder.build(), packageName, userId);
425 
426         if (status.getEndTimeMillis() >= 0) {
427             mStatusExpReceiver.scheduleExpiration(
428                     mContext, userId, packageName, conversationId, status);
429         }
430 
431     }
432 
clearStatus(String packageName, int userId, String conversationId, String statusId)433     public void clearStatus(String packageName, int userId, String conversationId,
434             String statusId) {
435         ConversationStore cs = getConversationStoreOrThrow(packageName, userId);
436         ConversationInfo convToModify = getConversationInfoOrThrow(cs, conversationId);
437         ConversationInfo.Builder builder = new ConversationInfo.Builder(convToModify);
438         builder.clearStatus(statusId);
439         updateConversationStoreThenNotifyListeners(cs, builder.build(), packageName, userId);
440     }
441 
clearStatuses(String packageName, int userId, String conversationId)442     public void clearStatuses(String packageName, int userId, String conversationId) {
443         ConversationStore cs = getConversationStoreOrThrow(packageName, userId);
444         ConversationInfo convToModify = getConversationInfoOrThrow(cs, conversationId);
445         ConversationInfo.Builder builder = new ConversationInfo.Builder(convToModify);
446         builder.setStatuses(null);
447         updateConversationStoreThenNotifyListeners(cs, builder.build(), packageName, userId);
448     }
449 
getStatuses(String packageName, int userId, String conversationId)450     public @NonNull List<ConversationStatus> getStatuses(String packageName, int userId,
451             String conversationId) {
452         ConversationStore cs = getConversationStoreOrThrow(packageName, userId);
453         ConversationInfo conversationInfo = getConversationInfoOrThrow(cs, conversationId);
454         return getStatuses(conversationInfo);
455     }
456 
getStatuses(ConversationInfo conversationInfo)457     private @NonNull List<ConversationStatus> getStatuses(ConversationInfo conversationInfo) {
458         Collection<ConversationStatus> statuses = conversationInfo.getStatuses();
459         if (statuses != null) {
460             final ArrayList<ConversationStatus> list = new ArrayList<>(statuses.size());
461             list.addAll(statuses);
462             return list;
463         }
464         return new ArrayList<>();
465     }
466 
467     /**
468      * Returns a conversation store for a package, if it exists.
469      */
getConversationStoreOrThrow(String packageName, int userId)470     private @NonNull ConversationStore getConversationStoreOrThrow(String packageName, int userId) {
471         final PackageData packageData = getPackage(packageName, userId);
472         if (packageData == null) {
473             throw new IllegalArgumentException("No settings exist for package " + packageName);
474         }
475         ConversationStore cs = packageData.getConversationStore();
476         if (cs == null) {
477             throw new IllegalArgumentException("No conversations exist for package " + packageName);
478         }
479         return cs;
480     }
481 
482     /**
483      * Returns a conversation store for a package, if it exists.
484      */
getConversationInfoOrThrow(ConversationStore cs, String conversationId)485     private @NonNull ConversationInfo getConversationInfoOrThrow(ConversationStore cs,
486             String conversationId) {
487         ConversationInfo ci = cs.getConversation(conversationId);
488 
489         if (ci == null) {
490             throw new IllegalArgumentException("Conversation does not exist");
491         }
492         return ci;
493     }
494 
495     /** Reports the sharing related {@link AppTargetEvent} from App Prediction Manager. */
reportShareTargetEvent(@onNull AppTargetEvent event, @NonNull IntentFilter intentFilter)496     public void reportShareTargetEvent(@NonNull AppTargetEvent event,
497             @NonNull IntentFilter intentFilter) {
498         AppTarget appTarget = event.getTarget();
499         if (appTarget == null || event.getAction() != AppTargetEvent.ACTION_LAUNCH) {
500             return;
501         }
502         UserData userData = getUnlockedUserData(appTarget.getUser().getIdentifier());
503         if (userData == null) {
504             return;
505         }
506         PackageData packageData = userData.getOrCreatePackageData(appTarget.getPackageName());
507         @Event.EventType int eventType = mimeTypeToShareEventType(intentFilter.getDataType(0));
508         EventHistoryImpl eventHistory;
509         if (ChooserActivity.LAUNCH_LOCATION_DIRECT_SHARE.equals(event.getLaunchLocation())) {
510             // Direct share event
511             if (appTarget.getShortcutInfo() == null) {
512                 return;
513             }
514             String shortcutId = appTarget.getShortcutInfo().getId();
515             // Skip storing chooserTargets sharing events
516             if (ChooserActivity.CHOOSER_TARGET.equals(shortcutId)) {
517                 return;
518             }
519             if (packageData.getConversationStore().getConversation(shortcutId) == null) {
520                 addOrUpdateConversationInfo(appTarget.getShortcutInfo());
521             }
522             eventHistory = packageData.getEventStore().getOrCreateEventHistory(
523                     EventStore.CATEGORY_SHORTCUT_BASED, shortcutId);
524         } else {
525             // App share event
526             eventHistory = packageData.getEventStore().getOrCreateEventHistory(
527                     EventStore.CATEGORY_CLASS_BASED, appTarget.getClassName());
528         }
529         eventHistory.addEvent(new Event(System.currentTimeMillis(), eventType));
530     }
531 
532     /**
533      * Queries events for moving app to foreground between {@code startTime} and {@code endTime}.
534      */
535     @NonNull
queryAppMovingToForegroundEvents(@serIdInt int callingUserId, long startTime, long endTime)536     public List<UsageEvents.Event> queryAppMovingToForegroundEvents(@UserIdInt int callingUserId,
537             long startTime, long endTime) {
538         return UsageStatsQueryHelper.queryAppMovingToForegroundEvents(callingUserId, startTime,
539                 endTime);
540     }
541 
542     /**
543      * Queries usage stats of apps within {@code packageNameFilter} between {@code startTime} and
544      * {@code endTime}.
545      *
546      * @return a map which keys are package names and values are {@link AppUsageStatsData}.
547      */
548     @NonNull
queryAppUsageStats( @serIdInt int callingUserId, long startTime, long endTime, Set<String> packageNameFilter)549     public Map<String, AppUsageStatsData> queryAppUsageStats(
550             @UserIdInt int callingUserId, long startTime,
551             long endTime, Set<String> packageNameFilter) {
552         return UsageStatsQueryHelper.queryAppUsageStats(callingUserId, startTime, endTime,
553                 packageNameFilter);
554     }
555 
556     /** Prunes the data for the specified user. */
pruneDataForUser(@serIdInt int userId, @NonNull CancellationSignal signal)557     public void pruneDataForUser(@UserIdInt int userId, @NonNull CancellationSignal signal) {
558         UserData userData = getUnlockedUserData(userId);
559         if (userData == null || signal.isCanceled()) {
560             return;
561         }
562         pruneUninstalledPackageData(userData);
563 
564         userData.forAllPackages(packageData -> {
565             if (signal.isCanceled()) {
566                 return;
567             }
568             packageData.getEventStore().pruneOldEvents();
569             if (!packageData.isDefaultDialer()) {
570                 packageData.getEventStore().deleteEventHistories(EventStore.CATEGORY_CALL);
571             }
572             if (!packageData.isDefaultSmsApp()) {
573                 packageData.getEventStore().deleteEventHistories(EventStore.CATEGORY_SMS);
574             }
575             packageData.pruneOrphanEvents();
576             pruneExpiredConversationStatuses(userId, System.currentTimeMillis());
577             pruneOldRecentConversations(userId, System.currentTimeMillis());
578             cleanupCachedShortcuts(userId, MAX_CACHED_RECENT_SHORTCUTS);
579         });
580     }
581 
582     /** Retrieves a backup payload blob for specified user id. */
583     @Nullable
getBackupPayload(@serIdInt int userId)584     public byte[] getBackupPayload(@UserIdInt int userId) {
585         UserData userData = getUnlockedUserData(userId);
586         if (userData == null) {
587             return null;
588         }
589         return userData.getBackupPayload();
590     }
591 
592     /** Attempts to restore data for the specified user id. */
restore(@serIdInt int userId, @NonNull byte[] payload)593     public void restore(@UserIdInt int userId, @NonNull byte[] payload) {
594         UserData userData = getUnlockedUserData(userId);
595         if (userData == null) {
596             return;
597         }
598         userData.restore(payload);
599     }
600 
setupUser(@serIdInt int userId)601     private void setupUser(@UserIdInt int userId) {
602         synchronized (mLock) {
603             UserData userData = getUnlockedUserData(userId);
604             if (userData == null) {
605                 return;
606             }
607             userData.loadUserData();
608 
609             updateDefaultDialer(userData);
610             updateDefaultSmsApp(userData);
611 
612             ScheduledFuture<?> scheduledFuture = mScheduledExecutor.scheduleAtFixedRate(
613                     new UsageStatsQueryRunnable(userId), 1L, USAGE_STATS_QUERY_INTERVAL_SEC,
614                     TimeUnit.SECONDS);
615             mUsageStatsQueryFutures.put(userId, scheduledFuture);
616 
617             IntentFilter intentFilter = new IntentFilter();
618             intentFilter.addAction(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED);
619             intentFilter.addAction(SmsApplication.ACTION_DEFAULT_SMS_PACKAGE_CHANGED_INTERNAL);
620 
621             if (mBroadcastReceivers.get(userId) == null) {
622                 BroadcastReceiver broadcastReceiver = new PerUserBroadcastReceiver(userId);
623                 mBroadcastReceivers.put(userId, broadcastReceiver);
624                 mContext.registerReceiverAsUser(
625                         broadcastReceiver, UserHandle.of(userId), intentFilter, null, null);
626             } else {
627                 // Stopped was not called on this user before setup is called again. This
628                 // could happen during consecutive rapid user switching.
629                 if (DEBUG) Log.d(TAG, "PerUserBroadcastReceiver was registered for: " + userId);
630             }
631 
632             ContentObserver contactsContentObserver = new ContactsContentObserver(
633                     BackgroundThread.getHandler());
634             mContactsContentObservers.put(userId, contactsContentObserver);
635             mContext.getContentResolver().registerContentObserver(
636                     Contacts.CONTENT_URI, /* notifyForDescendants= */ true,
637                     contactsContentObserver, userId);
638 
639             NotificationListener notificationListener = new NotificationListener(userId);
640             mNotificationListeners.put(userId, notificationListener);
641             try {
642                 notificationListener.registerAsSystemService(mContext,
643                         new ComponentName(mContext, getClass()), userId);
644             } catch (RemoteException e) {
645                 // Should never occur for local calls.
646             }
647 
648             if (mPackageMonitors.get(userId) == null) {
649                 PackageMonitor packageMonitor = new PerUserPackageMonitor();
650                 packageMonitor.register(mContext, null, UserHandle.of(userId), true);
651                 mPackageMonitors.put(userId, packageMonitor);
652             } else {
653                 // Stopped was not called on this user before setup is called again. This
654                 // could happen during consecutive rapid user switching.
655                 if (DEBUG) Log.d(TAG, "PerUserPackageMonitor was registered for: " + userId);
656             }
657 
658             if (userId == UserHandle.USER_SYSTEM) {
659                 // The call log and MMS/SMS messages are shared across user profiles. So only need
660                 // to register the content observers once for the primary user.
661                 mCallLogContentObserver = new CallLogContentObserver(BackgroundThread.getHandler());
662                 mContext.getContentResolver().registerContentObserver(
663                         CallLog.CONTENT_URI, /* notifyForDescendants= */ true,
664                         mCallLogContentObserver, UserHandle.USER_SYSTEM);
665 
666                 mMmsSmsContentObserver = new MmsSmsContentObserver(BackgroundThread.getHandler());
667                 mContext.getContentResolver().registerContentObserver(
668                         MmsSms.CONTENT_URI, /* notifyForDescendants= */ false,
669                         mMmsSmsContentObserver, UserHandle.USER_SYSTEM);
670             }
671 
672             DataMaintenanceService.scheduleJob(mContext, userId);
673         }
674     }
675 
cleanupUser(@serIdInt int userId)676     private void cleanupUser(@UserIdInt int userId) {
677         synchronized (mLock) {
678             UserData userData = mUserDataArray.get(userId);
679             if (userData == null || userData.isUnlocked()) {
680                 return;
681             }
682             ContentResolver contentResolver = mContext.getContentResolver();
683             if (mUsageStatsQueryFutures.indexOfKey(userId) >= 0) {
684                 mUsageStatsQueryFutures.get(userId).cancel(true);
685             }
686             if (mBroadcastReceivers.indexOfKey(userId) >= 0) {
687                 mContext.unregisterReceiver(mBroadcastReceivers.get(userId));
688             }
689             if (mContactsContentObservers.indexOfKey(userId) >= 0) {
690                 contentResolver.unregisterContentObserver(mContactsContentObservers.get(userId));
691             }
692             if (mNotificationListeners.indexOfKey(userId) >= 0) {
693                 try {
694                     mNotificationListeners.get(userId).unregisterAsSystemService();
695                 } catch (RemoteException e) {
696                     // Should never occur for local calls.
697                 }
698             }
699             if (mPackageMonitors.indexOfKey(userId) >= 0) {
700                 mPackageMonitors.get(userId).unregister();
701             }
702             if (userId == UserHandle.USER_SYSTEM) {
703                 if (mCallLogContentObserver != null) {
704                     contentResolver.unregisterContentObserver(mCallLogContentObserver);
705                     mCallLogContentObserver = null;
706                 }
707                 if (mMmsSmsContentObserver != null) {
708                     contentResolver.unregisterContentObserver(mMmsSmsContentObserver);
709                     mCallLogContentObserver = null;
710                 }
711             }
712 
713             DataMaintenanceService.cancelJob(mContext, userId);
714         }
715     }
716 
717     /**
718      * Converts {@code mimeType} to {@link Event.EventType}.
719      */
mimeTypeToShareEventType(String mimeType)720     public int mimeTypeToShareEventType(String mimeType) {
721         if (mimeType == null) {
722             return Event.TYPE_SHARE_OTHER;
723         }
724         if (mimeType.startsWith("text/")) {
725             return Event.TYPE_SHARE_TEXT;
726         } else if (mimeType.startsWith("image/")) {
727             return Event.TYPE_SHARE_IMAGE;
728         } else if (mimeType.startsWith("video/")) {
729             return Event.TYPE_SHARE_VIDEO;
730         }
731         return Event.TYPE_SHARE_OTHER;
732     }
733 
pruneUninstalledPackageData(@onNull UserData userData)734     private void pruneUninstalledPackageData(@NonNull UserData userData) {
735         Set<String> installApps = new ArraySet<>();
736         mPackageManagerInternal.forEachInstalledPackage(
737                 pkg -> installApps.add(pkg.getPackageName()), userData.getUserId());
738         List<String> packagesToDelete = new ArrayList<>();
739         userData.forAllPackages(packageData -> {
740             if (!installApps.contains(packageData.getPackageName())) {
741                 packagesToDelete.add(packageData.getPackageName());
742             }
743         });
744         for (String packageName : packagesToDelete) {
745             if (DEBUG) Log.d(TAG, "Deleting packages data for: " + packageName);
746             userData.deletePackageData(packageName);
747         }
748     }
749 
750     /** Gets a list of {@link ShortcutInfo}s with the given shortcut IDs. */
getShortcuts( @onNull String packageName, @UserIdInt int userId, @Nullable List<String> shortcutIds)751     private List<ShortcutInfo> getShortcuts(
752             @NonNull String packageName, @UserIdInt int userId,
753             @Nullable List<String> shortcutIds) {
754         @ShortcutQuery.QueryFlags int queryFlags = ShortcutQuery.FLAG_MATCH_DYNAMIC
755                 | ShortcutQuery.FLAG_MATCH_PINNED | ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
756                 | ShortcutQuery.FLAG_MATCH_CACHED | ShortcutQuery.FLAG_GET_PERSONS_DATA;
757         if (DEBUG) Log.d(TAG, " Get shortcuts with IDs: " + shortcutIds);
758         return mShortcutServiceInternal.getShortcuts(
759                 UserHandle.USER_SYSTEM, mContext.getPackageName(),
760                 /*changedSince=*/ 0, packageName, shortcutIds, /*locusIds=*/ null,
761                 /*componentName=*/ null, queryFlags, userId, Process.myPid(), Process.myUid());
762     }
763 
forAllUnlockedUsers(Consumer<UserData> consumer)764     private void forAllUnlockedUsers(Consumer<UserData> consumer) {
765         for (int i = 0; i < mUserDataArray.size(); i++) {
766             int userId = mUserDataArray.keyAt(i);
767             UserData userData = mUserDataArray.get(userId);
768             if (userData.isUnlocked()) {
769                 consumer.accept(userData);
770             }
771         }
772     }
773 
774     @Nullable
getUnlockedUserData(int userId)775     private UserData getUnlockedUserData(int userId) {
776         UserData userData = mUserDataArray.get(userId);
777         return userData != null && userData.isUnlocked() ? userData : null;
778     }
779 
updateDefaultDialer(@onNull UserData userData)780     private void updateDefaultDialer(@NonNull UserData userData) {
781         TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
782         String defaultDialer = telecomManager != null
783                 ? telecomManager.getDefaultDialerPackage(
784                 new UserHandle(userData.getUserId())) : null;
785         userData.setDefaultDialer(defaultDialer);
786     }
787 
updateDefaultSmsApp(@onNull UserData userData)788     private void updateDefaultSmsApp(@NonNull UserData userData) {
789         ComponentName component = SmsApplication.getDefaultSmsApplicationAsUser(
790                 mContext, /* updateIfNeeded= */ false, UserHandle.of(userData.getUserId()));
791         String defaultSmsApp = component != null ? component.getPackageName() : null;
792         userData.setDefaultSmsApp(defaultSmsApp);
793     }
794 
795     @Nullable
getPackageIfConversationExists(StatusBarNotification sbn, Consumer<ConversationInfo> conversationConsumer)796     private PackageData getPackageIfConversationExists(StatusBarNotification sbn,
797             Consumer<ConversationInfo> conversationConsumer) {
798         Notification notification = sbn.getNotification();
799         String shortcutId = notification.getShortcutId();
800         if (shortcutId == null) {
801             return null;
802         }
803         PackageData packageData = getPackage(sbn.getPackageName(),
804                 sbn.getUser().getIdentifier());
805         if (packageData == null) {
806             return null;
807         }
808         ConversationInfo conversationInfo =
809                 packageData.getConversationStore().getConversation(shortcutId);
810         if (conversationInfo == null) {
811             return null;
812         }
813         conversationConsumer.accept(conversationInfo);
814         return packageData;
815     }
816 
isCachedRecentConversation(ConversationInfo conversationInfo)817     private boolean isCachedRecentConversation(ConversationInfo conversationInfo) {
818         return isEligibleForCleanUp(conversationInfo)
819                 && conversationInfo.getLastEventTimestamp() > 0L;
820     }
821 
822     /**
823      * Conversations that are cached and not customized are eligible for clean-up, even if they
824      * don't have an associated notification event with them.
825      */
isEligibleForCleanUp(ConversationInfo conversationInfo)826     private boolean isEligibleForCleanUp(ConversationInfo conversationInfo) {
827         return conversationInfo.isShortcutCachedForNotification()
828                 && Objects.equals(conversationInfo.getNotificationChannelId(),
829                 conversationInfo.getParentNotificationChannelId());
830     }
831 
hasActiveNotifications(String packageName, @UserIdInt int userId, String shortcutId)832     private boolean hasActiveNotifications(String packageName, @UserIdInt int userId,
833             String shortcutId) {
834         NotificationListener notificationListener = mNotificationListeners.get(userId);
835         return notificationListener != null
836                 && notificationListener.hasActiveNotifications(packageName, shortcutId);
837     }
838 
839     /**
840      * Cleans up the oldest cached shortcuts that don't have active notifications for the recent
841      * conversations. After the cleanup, normally, the total number of cached shortcuts will be
842      * less than or equal to the target count. However, there are exception cases: e.g. when all
843      * the existing cached shortcuts have active notifications.
844      */
cleanupCachedShortcuts(@serIdInt int userId, int targetCachedCount)845     private void cleanupCachedShortcuts(@UserIdInt int userId, int targetCachedCount) {
846         UserData userData = getUnlockedUserData(userId);
847         if (userData == null) {
848             return;
849         }
850         // pair of <package name, conversation info>
851         List<Pair<String, ConversationInfo>> cachedConvos = new ArrayList<>();
852         userData.forAllPackages(packageData -> {
853                 packageData.forAllConversations(conversationInfo -> {
854                     if (isEligibleForCleanUp(conversationInfo)) {
855                         cachedConvos.add(
856                                 Pair.create(packageData.getPackageName(), conversationInfo));
857                     }
858                 });
859         });
860         if (cachedConvos.size() <= targetCachedCount) {
861             return;
862         }
863         int numToUncache = cachedConvos.size() - targetCachedCount;
864         // Max heap keeps the oldest cached conversations.
865         PriorityQueue<Pair<String, ConversationInfo>> maxHeap = new PriorityQueue<>(
866                 numToUncache + 1,
867                 Comparator.comparingLong((Pair<String, ConversationInfo> pair) ->
868                         Math.max(
869                             pair.second.getLastEventTimestamp(),
870                             pair.second.getCreationTimestamp())).reversed());
871         for (Pair<String, ConversationInfo> cached : cachedConvos) {
872             if (hasActiveNotifications(cached.first, userId, cached.second.getShortcutId())) {
873                 continue;
874             }
875             maxHeap.offer(cached);
876             if (maxHeap.size() > numToUncache) {
877                 maxHeap.poll();
878             }
879         }
880         while (!maxHeap.isEmpty()) {
881             Pair<String, ConversationInfo> toUncache = maxHeap.poll();
882             mShortcutServiceInternal.uncacheShortcuts(userId,
883                     mContext.getPackageName(), toUncache.first,
884                     Collections.singletonList(toUncache.second.getShortcutId()),
885                     userId, ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
886         }
887     }
888 
889     @VisibleForTesting
890     @WorkerThread
addOrUpdateConversationInfo(@onNull ShortcutInfo shortcutInfo)891     void addOrUpdateConversationInfo(@NonNull ShortcutInfo shortcutInfo) {
892         UserData userData = getUnlockedUserData(shortcutInfo.getUserId());
893         if (userData == null) {
894             return;
895         }
896         PackageData packageData = userData.getOrCreatePackageData(shortcutInfo.getPackage());
897         ConversationStore conversationStore = packageData.getConversationStore();
898         ConversationInfo oldConversationInfo =
899                 conversationStore.getConversation(shortcutInfo.getId());
900         if (oldConversationInfo == null) {
901             if (DEBUG) Log.d(TAG, "Nothing previously stored about conversation.");
902         }
903         ConversationInfo.Builder builder = oldConversationInfo != null
904                 ? new ConversationInfo.Builder(oldConversationInfo)
905                 : new ConversationInfo.Builder().setCreationTimestamp(System.currentTimeMillis());
906 
907         builder.setShortcutId(shortcutInfo.getId());
908         builder.setLocusId(shortcutInfo.getLocusId());
909         builder.setShortcutFlags(shortcutInfo.getFlags());
910         builder.setContactUri(null);
911         builder.setContactPhoneNumber(null);
912         builder.setContactStarred(false);
913 
914         if (shortcutInfo.getPersons() != null && shortcutInfo.getPersons().length != 0) {
915             Person person = shortcutInfo.getPersons()[0];
916             builder.setPersonImportant(person.isImportant());
917             builder.setPersonBot(person.isBot());
918             String contactUri = person.getUri();
919             if (contactUri != null) {
920                 ContactsQueryHelper helper = mInjector.createContactsQueryHelper(mContext);
921                 if (helper.query(contactUri)) {
922                     builder.setContactUri(helper.getContactUri());
923                     builder.setContactStarred(helper.isStarred());
924                     builder.setContactPhoneNumber(helper.getPhoneNumber());
925                 }
926             }
927         }
928         updateConversationStoreThenNotifyListeners(conversationStore, builder.build(),
929                 shortcutInfo);
930     }
931 
932     @VisibleForTesting
getContactsContentObserverForTesting(@serIdInt int userId)933     ContentObserver getContactsContentObserverForTesting(@UserIdInt int userId) {
934         return mContactsContentObservers.get(userId);
935     }
936 
937     @VisibleForTesting
getCallLogContentObserverForTesting()938     ContentObserver getCallLogContentObserverForTesting() {
939         return mCallLogContentObserver;
940     }
941 
942     @VisibleForTesting
getMmsSmsContentObserverForTesting()943     ContentObserver getMmsSmsContentObserverForTesting() {
944         return mMmsSmsContentObserver;
945     }
946 
947     @VisibleForTesting
getNotificationListenerServiceForTesting(@serIdInt int userId)948     NotificationListener getNotificationListenerServiceForTesting(@UserIdInt int userId) {
949         return mNotificationListeners.get(userId);
950     }
951 
952     @VisibleForTesting
getPackageMonitorForTesting(@serIdInt int userId)953     PackageMonitor getPackageMonitorForTesting(@UserIdInt int userId) {
954         return mPackageMonitors.get(userId);
955     }
956 
957     @VisibleForTesting
getUserDataForTesting(@serIdInt int userId)958     UserData getUserDataForTesting(@UserIdInt int userId) {
959         return mUserDataArray.get(userId);
960     }
961 
962     /** Observer that observes the changes in the Contacts database. */
963     private class ContactsContentObserver extends ContentObserver {
964 
965         private long mLastUpdatedTimestamp;
966 
ContactsContentObserver(Handler handler)967         private ContactsContentObserver(Handler handler) {
968             super(handler);
969             mLastUpdatedTimestamp = System.currentTimeMillis();
970         }
971 
972         @Override
onChange(boolean selfChange, Uri uri, @UserIdInt int userId)973         public void onChange(boolean selfChange, Uri uri, @UserIdInt int userId) {
974             ContactsQueryHelper helper = mInjector.createContactsQueryHelper(mContext);
975             if (!helper.querySince(mLastUpdatedTimestamp)) {
976                 return;
977             }
978             Uri contactUri = helper.getContactUri();
979 
980             final ConversationSelector conversationSelector = new ConversationSelector();
981             UserData userData = getUnlockedUserData(userId);
982             if (userData == null) {
983                 return;
984             }
985             userData.forAllPackages(packageData -> {
986                 ConversationInfo ci =
987                         packageData.getConversationStore().getConversationByContactUri(contactUri);
988                 if (ci != null) {
989                     conversationSelector.mConversationStore =
990                             packageData.getConversationStore();
991                     conversationSelector.mConversationInfo = ci;
992                     conversationSelector.mPackageName = packageData.getPackageName();
993                 }
994             });
995             if (conversationSelector.mConversationInfo == null) {
996                 return;
997             }
998 
999             ConversationInfo.Builder builder =
1000                     new ConversationInfo.Builder(conversationSelector.mConversationInfo);
1001             builder.setContactStarred(helper.isStarred());
1002             builder.setContactPhoneNumber(helper.getPhoneNumber());
1003             updateConversationStoreThenNotifyListeners(conversationSelector.mConversationStore,
1004                     builder.build(),
1005                     conversationSelector.mPackageName, userId);
1006             mLastUpdatedTimestamp = helper.getLastUpdatedTimestamp();
1007         }
1008 
1009         private class ConversationSelector {
1010             private ConversationStore mConversationStore = null;
1011             private ConversationInfo mConversationInfo = null;
1012             private String mPackageName = null;
1013         }
1014     }
1015 
1016     /** Observer that observes the changes in the call log database. */
1017     private class CallLogContentObserver extends ContentObserver implements
1018             BiConsumer<String, Event> {
1019 
1020         private final CallLogQueryHelper mCallLogQueryHelper;
1021         private long mLastCallTimestamp;
1022 
CallLogContentObserver(Handler handler)1023         private CallLogContentObserver(Handler handler) {
1024             super(handler);
1025             mCallLogQueryHelper = mInjector.createCallLogQueryHelper(mContext, this);
1026             mLastCallTimestamp = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
1027         }
1028 
1029         @Override
onChange(boolean selfChange)1030         public void onChange(boolean selfChange) {
1031             if (mCallLogQueryHelper.querySince(mLastCallTimestamp)) {
1032                 mLastCallTimestamp = mCallLogQueryHelper.getLastCallTimestamp();
1033             }
1034         }
1035 
1036         @Override
accept(String phoneNumber, Event event)1037         public void accept(String phoneNumber, Event event) {
1038             forAllUnlockedUsers(userData -> {
1039                 PackageData defaultDialer = userData.getDefaultDialer();
1040                 if (defaultDialer == null) {
1041                     return;
1042                 }
1043                 ConversationStore conversationStore = defaultDialer.getConversationStore();
1044                 if (conversationStore.getConversationByPhoneNumber(phoneNumber) == null) {
1045                     return;
1046                 }
1047                 EventStore eventStore = defaultDialer.getEventStore();
1048                 eventStore.getOrCreateEventHistory(
1049                         EventStore.CATEGORY_CALL, phoneNumber).addEvent(event);
1050             });
1051         }
1052     }
1053 
1054     /** Observer that observes the changes in the MMS & SMS database. */
1055     private class MmsSmsContentObserver extends ContentObserver implements
1056             BiConsumer<String, Event> {
1057 
1058         private final MmsQueryHelper mMmsQueryHelper;
1059         private long mLastMmsTimestamp;
1060 
1061         private final SmsQueryHelper mSmsQueryHelper;
1062         private long mLastSmsTimestamp;
1063 
MmsSmsContentObserver(Handler handler)1064         private MmsSmsContentObserver(Handler handler) {
1065             super(handler);
1066             mMmsQueryHelper = mInjector.createMmsQueryHelper(mContext, this);
1067             mSmsQueryHelper = mInjector.createSmsQueryHelper(mContext, this);
1068             mLastSmsTimestamp = mLastMmsTimestamp =
1069                     System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
1070         }
1071 
1072         @Override
onChange(boolean selfChange)1073         public void onChange(boolean selfChange) {
1074             if (mMmsQueryHelper.querySince(mLastMmsTimestamp)) {
1075                 mLastMmsTimestamp = mMmsQueryHelper.getLastMessageTimestamp();
1076             }
1077             if (mSmsQueryHelper.querySince(mLastSmsTimestamp)) {
1078                 mLastSmsTimestamp = mSmsQueryHelper.getLastMessageTimestamp();
1079             }
1080         }
1081 
1082         @Override
accept(String phoneNumber, Event event)1083         public void accept(String phoneNumber, Event event) {
1084             forAllUnlockedUsers(userData -> {
1085                 PackageData defaultSmsApp = userData.getDefaultSmsApp();
1086                 if (defaultSmsApp == null) {
1087                     return;
1088                 }
1089                 ConversationStore conversationStore = defaultSmsApp.getConversationStore();
1090                 if (conversationStore.getConversationByPhoneNumber(phoneNumber) == null) {
1091                     return;
1092                 }
1093                 EventStore eventStore = defaultSmsApp.getEventStore();
1094                 eventStore.getOrCreateEventHistory(
1095                         EventStore.CATEGORY_SMS, phoneNumber).addEvent(event);
1096             });
1097         }
1098     }
1099 
1100     /** Listener for the shortcut data changes. */
1101     private class ShortcutServiceCallback implements LauncherApps.ShortcutChangeCallback {
1102 
1103         @Override
onShortcutsAddedOrUpdated(@onNull String packageName, @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user)1104         public void onShortcutsAddedOrUpdated(@NonNull String packageName,
1105                 @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
1106             mInjector.getBackgroundExecutor().execute(() -> {
1107                 PackageData packageData = getPackage(packageName, user.getIdentifier());
1108                 boolean hasCachedShortcut = false;
1109                 for (ShortcutInfo shortcut : shortcuts) {
1110                     if (ShortcutHelper.isConversationShortcut(
1111                             shortcut, mShortcutServiceInternal, user.getIdentifier())) {
1112                         if (shortcut.isCached()) {
1113                             ConversationInfo conversationInfo = packageData != null
1114                                     ? packageData.getConversationInfo(shortcut.getId()) : null;
1115                             if (conversationInfo == null
1116                                     || !conversationInfo.isShortcutCachedForNotification()) {
1117                                 hasCachedShortcut = true;
1118                             }
1119                         }
1120                         addOrUpdateConversationInfo(shortcut);
1121                     }
1122                 }
1123                 // Added at least one new conversation. Uncache older existing cached
1124                 // shortcuts to ensure the cache size is under the limit.
1125                 if (hasCachedShortcut) {
1126                     cleanupCachedShortcuts(user.getIdentifier(),
1127                             MAX_CACHED_RECENT_SHORTCUTS);
1128                 }
1129             });
1130         }
1131 
1132         @Override
onShortcutsRemoved(@onNull String packageName, @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user)1133         public void onShortcutsRemoved(@NonNull String packageName,
1134                 @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
1135             mInjector.getBackgroundExecutor().execute(() -> {
1136                 HashSet<String> shortcutIds = new HashSet<>();
1137                 for (ShortcutInfo shortcutInfo : shortcuts) {
1138                     shortcutIds.add(shortcutInfo.getId());
1139                 }
1140                 removeConversations(packageName, user.getIdentifier(), shortcutIds);
1141             });
1142         }
1143     }
1144 
removeConversations( @onNull String packageName, @NonNull int userId, @NonNull Set<String> shortcutIds)1145     private void removeConversations(
1146             @NonNull String packageName, @NonNull int userId, @NonNull Set<String> shortcutIds) {
1147         PackageData packageData = getPackage(packageName, userId);
1148         if (packageData != null) {
1149             for (String shortcutId : shortcutIds) {
1150                 if (DEBUG) Log.d(TAG, "Deleting shortcut: " + shortcutId);
1151                 packageData.deleteDataForConversation(shortcutId);
1152             }
1153         }
1154         try {
1155             int uid = mContext.getPackageManager().getPackageUidAsUser(
1156                     packageName, userId);
1157             mNotificationManagerInternal.onConversationRemoved(packageName, uid, shortcutIds);
1158         } catch (PackageManager.NameNotFoundException e) {
1159             Slog.e(TAG, "Package not found when removing conversation: " + packageName, e);
1160         }
1161     }
1162 
1163     /** Listener for the notifications and their settings changes. */
1164     private class NotificationListener extends NotificationListenerService {
1165 
1166         private final int mUserId;
1167 
1168         // Conversation package name + shortcut ID -> Keys of active notifications
1169         @GuardedBy("this")
1170         private final Map<Pair<String, String>, Set<String>> mActiveNotifKeys = new ArrayMap<>();
1171 
NotificationListener(int userId)1172         private NotificationListener(int userId) {
1173             mUserId = userId;
1174         }
1175 
1176         @Override
onNotificationPosted(StatusBarNotification sbn, RankingMap map)1177         public void onNotificationPosted(StatusBarNotification sbn, RankingMap map) {
1178             if (sbn.getUser().getIdentifier() != mUserId) {
1179                 return;
1180             }
1181             String shortcutId = sbn.getNotification().getShortcutId();
1182             PackageData packageData = getPackageIfConversationExists(sbn, conversationInfo -> {
1183                 synchronized (this) {
1184                     Set<String> notificationKeys = mActiveNotifKeys.computeIfAbsent(
1185                             Pair.create(sbn.getPackageName(), shortcutId),
1186                             (unusedKey) -> new HashSet<>());
1187                     notificationKeys.add(sbn.getKey());
1188                 }
1189             });
1190 
1191             if (packageData != null) {
1192                 Ranking rank = new Ranking();
1193                 map.getRanking(sbn.getKey(), rank);
1194                 ConversationInfo conversationInfo = packageData.getConversationInfo(shortcutId);
1195                 if (conversationInfo == null) {
1196                     return;
1197                 }
1198                 if (DEBUG) Log.d(TAG, "Last event from notification: " + sbn.getPostTime());
1199                 ConversationInfo.Builder updated = new ConversationInfo.Builder(conversationInfo)
1200                         .setLastEventTimestamp(sbn.getPostTime())
1201                         .setNotificationChannelId(rank.getChannel().getId());
1202                 if (!TextUtils.isEmpty(rank.getChannel().getParentChannelId())) {
1203                     updated.setParentNotificationChannelId(rank.getChannel().getParentChannelId());
1204                 } else {
1205                     updated.setParentNotificationChannelId(sbn.getNotification().getChannelId());
1206                 }
1207                 packageData.getConversationStore().addOrUpdate(updated.build());
1208 
1209                 EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateEventHistory(
1210                         EventStore.CATEGORY_SHORTCUT_BASED, shortcutId);
1211                 eventHistory.addEvent(new Event(sbn.getPostTime(), Event.TYPE_NOTIFICATION_POSTED));
1212             }
1213         }
1214 
1215         @Override
onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason)1216         public synchronized void onNotificationRemoved(StatusBarNotification sbn,
1217                 RankingMap rankingMap, int reason) {
1218             if (sbn.getUser().getIdentifier() != mUserId) {
1219                 return;
1220             }
1221             String shortcutId = sbn.getNotification().getShortcutId();
1222             PackageData packageData = getPackageIfConversationExists(sbn, conversationInfo -> {
1223                 Pair<String, String> conversationKey =
1224                         Pair.create(sbn.getPackageName(), shortcutId);
1225                 synchronized (this) {
1226                     Set<String> notificationKeys = mActiveNotifKeys.computeIfAbsent(
1227                             conversationKey, (unusedKey) -> new HashSet<>());
1228                     notificationKeys.remove(sbn.getKey());
1229                     if (notificationKeys.isEmpty()) {
1230                         mActiveNotifKeys.remove(conversationKey);
1231                         cleanupCachedShortcuts(mUserId, MAX_CACHED_RECENT_SHORTCUTS);
1232                     }
1233                 }
1234             });
1235 
1236             if (reason != REASON_CLICK || packageData == null) {
1237                 return;
1238             }
1239             long currentTime = System.currentTimeMillis();
1240             ConversationInfo conversationInfo = packageData.getConversationInfo(shortcutId);
1241             if (conversationInfo == null) {
1242                 return;
1243             }
1244             if (DEBUG) Log.d(TAG, "Last event from notification removed: " + currentTime);
1245             ConversationInfo updated = new ConversationInfo.Builder(conversationInfo)
1246                     .setLastEventTimestamp(currentTime)
1247                     .build();
1248             packageData.getConversationStore().addOrUpdate(updated);
1249 
1250             EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateEventHistory(
1251                     EventStore.CATEGORY_SHORTCUT_BASED, shortcutId);
1252             eventHistory.addEvent(new Event(currentTime, Event.TYPE_NOTIFICATION_OPENED));
1253         }
1254 
1255         @Override
onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, int modificationType)1256         public void onNotificationChannelModified(String pkg, UserHandle user,
1257                 NotificationChannel channel, int modificationType) {
1258             if (user.getIdentifier() != mUserId) {
1259                 return;
1260             }
1261             PackageData packageData = getPackage(pkg, user.getIdentifier());
1262             String shortcutId = channel.getConversationId();
1263             if (packageData == null || shortcutId == null) {
1264                 return;
1265             }
1266             ConversationStore conversationStore = packageData.getConversationStore();
1267             ConversationInfo conversationInfo = conversationStore.getConversation(shortcutId);
1268             if (conversationInfo == null) {
1269                 return;
1270             }
1271             ConversationInfo.Builder builder = new ConversationInfo.Builder(conversationInfo);
1272             switch (modificationType) {
1273                 case NOTIFICATION_CHANNEL_OR_GROUP_ADDED:
1274                 case NOTIFICATION_CHANNEL_OR_GROUP_UPDATED:
1275                     builder.setNotificationChannelId(channel.getId());
1276                     builder.setImportant(channel.isImportantConversation());
1277                     builder.setDemoted(channel.isDemoted());
1278                     builder.setNotificationSilenced(
1279                             channel.getImportance() <= NotificationManager.IMPORTANCE_LOW);
1280                     builder.setBubbled(channel.canBubble());
1281                     break;
1282                 case NOTIFICATION_CHANNEL_OR_GROUP_DELETED:
1283                     // If the notification channel is deleted, revert all the notification settings
1284                     // to the default value.
1285                     builder.setNotificationChannelId(null);
1286                     builder.setImportant(false);
1287                     builder.setDemoted(false);
1288                     builder.setNotificationSilenced(false);
1289                     builder.setBubbled(false);
1290                     break;
1291             }
1292             updateConversationStoreThenNotifyListeners(conversationStore, builder.build(), pkg,
1293                     packageData.getUserId());
1294         }
1295 
hasActiveNotifications(String packageName, String shortcutId)1296         synchronized boolean hasActiveNotifications(String packageName, String shortcutId) {
1297             return mActiveNotifKeys.containsKey(Pair.create(packageName, shortcutId));
1298         }
1299     }
1300 
1301     /**
1302      * A {@link Runnable} that queries the Usage Stats Service for recent events for a specified
1303      * user.
1304      */
1305     private class UsageStatsQueryRunnable implements Runnable,
1306             UsageStatsQueryHelper.EventListener {
1307 
1308         private final UsageStatsQueryHelper mUsageStatsQueryHelper;
1309         private long mLastEventTimestamp;
1310 
UsageStatsQueryRunnable(int userId)1311         private UsageStatsQueryRunnable(int userId) {
1312             mUsageStatsQueryHelper = mInjector.createUsageStatsQueryHelper(userId,
1313                     (packageName) -> getPackage(packageName, userId), this);
1314             mLastEventTimestamp = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
1315         }
1316 
1317         @Override
run()1318         public void run() {
1319             if (mUsageStatsQueryHelper.querySince(mLastEventTimestamp)) {
1320                 mLastEventTimestamp = mUsageStatsQueryHelper.getLastEventTimestamp();
1321             }
1322         }
1323 
1324         @Override
onEvent(PackageData packageData, ConversationInfo conversationInfo, Event event)1325         public void onEvent(PackageData packageData, ConversationInfo conversationInfo,
1326                 Event event) {
1327             if (event.getType() == Event.TYPE_IN_APP_CONVERSATION) {
1328                 if (DEBUG) Log.d(TAG, "Last event from in-app: " + event.getTimestamp());
1329                 ConversationInfo updated = new ConversationInfo.Builder(conversationInfo)
1330                         .setLastEventTimestamp(event.getTimestamp())
1331                         .build();
1332                 updateConversationStoreThenNotifyListeners(packageData.getConversationStore(),
1333                         updated,
1334                         packageData.getPackageName(), packageData.getUserId());
1335             }
1336         }
1337     }
1338 
1339     /** Adds {@code listener} to be notified on conversation changes. */
addConversationsListener( @onNull PeopleService.ConversationsListener listener)1340     public void addConversationsListener(
1341             @NonNull PeopleService.ConversationsListener listener) {
1342         synchronized (mLock) {
1343             mConversationsListeners.add(Objects.requireNonNull(listener));
1344         }
1345     }
1346 
1347     @VisibleForTesting
updateConversationStoreThenNotifyListeners(ConversationStore cs, ConversationInfo modifiedConv, String packageName, int userId)1348     void updateConversationStoreThenNotifyListeners(ConversationStore cs,
1349             ConversationInfo modifiedConv,
1350             String packageName, int userId) {
1351         cs.addOrUpdate(modifiedConv);
1352         ConversationChannel channel = getConversationChannel(packageName, userId,
1353                 modifiedConv.getShortcutId(), modifiedConv);
1354         if (channel != null) {
1355             notifyConversationsListeners(Arrays.asList(channel));
1356         }
1357     }
1358 
updateConversationStoreThenNotifyListeners(ConversationStore cs, ConversationInfo modifiedConv, @NonNull ShortcutInfo shortcutInfo)1359     private void updateConversationStoreThenNotifyListeners(ConversationStore cs,
1360             ConversationInfo modifiedConv, @NonNull ShortcutInfo shortcutInfo) {
1361         cs.addOrUpdate(modifiedConv);
1362         ConversationChannel channel = getConversationChannel(
1363                 shortcutInfo, modifiedConv, shortcutInfo.getPackage(), shortcutInfo.getUserId(),
1364                 shortcutInfo.getId());
1365         if (channel != null) {
1366             notifyConversationsListeners(Arrays.asList(channel));
1367         }
1368     }
1369 
1370 
1371     @VisibleForTesting
notifyConversationsListeners( @ullable final List<ConversationChannel> changedConversations)1372     void notifyConversationsListeners(
1373             @Nullable final List<ConversationChannel> changedConversations) {
1374         mHandler.post(() -> {
1375             try {
1376                 final List<PeopleService.ConversationsListener> copy;
1377                 synchronized (mLock) {
1378                     copy = new ArrayList<>(mConversationsListeners);
1379                 }
1380                 for (PeopleService.ConversationsListener listener : copy) {
1381                     listener.onConversationsUpdate(changedConversations);
1382                 }
1383             } catch (Exception e) {
1384             }
1385         });
1386     }
1387 
1388     /** A {@link BroadcastReceiver} that receives the intents for a specified user. */
1389     private class PerUserBroadcastReceiver extends BroadcastReceiver {
1390 
1391         private final int mUserId;
1392 
PerUserBroadcastReceiver(int userId)1393         private PerUserBroadcastReceiver(int userId) {
1394             mUserId = userId;
1395         }
1396 
1397         @Override
onReceive(Context context, Intent intent)1398         public void onReceive(Context context, Intent intent) {
1399             UserData userData = getUnlockedUserData(mUserId);
1400             if (userData == null) {
1401                 return;
1402             }
1403             if (TelecomManager.ACTION_DEFAULT_DIALER_CHANGED.equals(intent.getAction())) {
1404                 String defaultDialer = intent.getStringExtra(
1405                         TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME);
1406                 userData.setDefaultDialer(defaultDialer);
1407             } else if (SmsApplication.ACTION_DEFAULT_SMS_PACKAGE_CHANGED_INTERNAL.equals(
1408                     intent.getAction())) {
1409                 updateDefaultSmsApp(userData);
1410             }
1411         }
1412     }
1413 
1414     private class PerUserPackageMonitor extends PackageMonitor {
1415 
1416         @Override
onPackageRemoved(String packageName, int uid)1417         public void onPackageRemoved(String packageName, int uid) {
1418             super.onPackageRemoved(packageName, uid);
1419 
1420             int userId = getChangingUserId();
1421             UserData userData = getUnlockedUserData(userId);
1422             if (userData != null) {
1423                 if (DEBUG) Log.d(TAG, "Delete package data for: " + packageName);
1424                 userData.deletePackageData(packageName);
1425             }
1426         }
1427     }
1428 
1429     private class ShutdownBroadcastReceiver extends BroadcastReceiver {
1430 
1431         @Override
onReceive(Context context, Intent intent)1432         public void onReceive(Context context, Intent intent) {
1433             forAllUnlockedUsers(userData -> userData.forAllPackages(PackageData::saveToDisk));
1434         }
1435     }
1436 
1437     @VisibleForTesting
1438     static class Injector {
1439 
createScheduledExecutor()1440         ScheduledExecutorService createScheduledExecutor() {
1441             return Executors.newSingleThreadScheduledExecutor();
1442         }
1443 
getBackgroundExecutor()1444         Executor getBackgroundExecutor() {
1445             return BackgroundThread.getExecutor();
1446         }
1447 
createContactsQueryHelper(Context context)1448         ContactsQueryHelper createContactsQueryHelper(Context context) {
1449             return new ContactsQueryHelper(context);
1450         }
1451 
createCallLogQueryHelper(Context context, BiConsumer<String, Event> eventConsumer)1452         CallLogQueryHelper createCallLogQueryHelper(Context context,
1453                 BiConsumer<String, Event> eventConsumer) {
1454             return new CallLogQueryHelper(context, eventConsumer);
1455         }
1456 
createMmsQueryHelper(Context context, BiConsumer<String, Event> eventConsumer)1457         MmsQueryHelper createMmsQueryHelper(Context context,
1458                 BiConsumer<String, Event> eventConsumer) {
1459             return new MmsQueryHelper(context, eventConsumer);
1460         }
1461 
createSmsQueryHelper(Context context, BiConsumer<String, Event> eventConsumer)1462         SmsQueryHelper createSmsQueryHelper(Context context,
1463                 BiConsumer<String, Event> eventConsumer) {
1464             return new SmsQueryHelper(context, eventConsumer);
1465         }
1466 
createUsageStatsQueryHelper(@serIdInt int userId, Function<String, PackageData> packageDataGetter, UsageStatsQueryHelper.EventListener eventListener)1467         UsageStatsQueryHelper createUsageStatsQueryHelper(@UserIdInt int userId,
1468                 Function<String, PackageData> packageDataGetter,
1469                 UsageStatsQueryHelper.EventListener eventListener) {
1470             return new UsageStatsQueryHelper(userId, packageDataGetter, eventListener);
1471         }
1472     }
1473 }
1474