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.systemui.people.widget;
18 
19 import static android.Manifest.permission.READ_CONTACTS;
20 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
21 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
22 import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
23 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
24 import static android.content.Intent.ACTION_BOOT_COMPLETED;
25 import static android.content.Intent.ACTION_PACKAGE_ADDED;
26 import static android.content.Intent.ACTION_PACKAGE_REMOVED;
27 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
28 
29 import static com.android.systemui.people.NotificationHelper.getContactUri;
30 import static com.android.systemui.people.NotificationHelper.getHighestPriorityNotification;
31 import static com.android.systemui.people.NotificationHelper.shouldFilterOut;
32 import static com.android.systemui.people.NotificationHelper.shouldMatchNotificationByUri;
33 import static com.android.systemui.people.PeopleBackupFollowUpJob.SHARED_FOLLOW_UP;
34 import static com.android.systemui.people.PeopleSpaceUtils.EMPTY_STRING;
35 import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID;
36 import static com.android.systemui.people.PeopleSpaceUtils.PACKAGE_NAME;
37 import static com.android.systemui.people.PeopleSpaceUtils.SHORTCUT_ID;
38 import static com.android.systemui.people.PeopleSpaceUtils.USER_ID;
39 import static com.android.systemui.people.PeopleSpaceUtils.augmentTileFromNotification;
40 import static com.android.systemui.people.PeopleSpaceUtils.getMessagesCount;
41 import static com.android.systemui.people.PeopleSpaceUtils.getNotificationsByUri;
42 import static com.android.systemui.people.PeopleSpaceUtils.removeNotificationFields;
43 import static com.android.systemui.people.widget.PeopleBackupHelper.getEntryType;
44 
45 import android.annotation.NonNull;
46 import android.annotation.Nullable;
47 import android.app.INotificationManager;
48 import android.app.NotificationChannel;
49 import android.app.NotificationManager;
50 import android.app.PendingIntent;
51 import android.app.Person;
52 import android.app.backup.BackupManager;
53 import android.app.job.JobScheduler;
54 import android.app.people.ConversationChannel;
55 import android.app.people.IPeopleManager;
56 import android.app.people.PeopleManager;
57 import android.app.people.PeopleSpaceTile;
58 import android.appwidget.AppWidgetManager;
59 import android.content.BroadcastReceiver;
60 import android.content.ComponentName;
61 import android.content.Context;
62 import android.content.Intent;
63 import android.content.IntentFilter;
64 import android.content.SharedPreferences;
65 import android.content.pm.LauncherApps;
66 import android.content.pm.PackageManager;
67 import android.content.pm.ShortcutInfo;
68 import android.graphics.drawable.Icon;
69 import android.net.Uri;
70 import android.os.Bundle;
71 import android.os.RemoteException;
72 import android.os.ServiceManager;
73 import android.os.UserHandle;
74 import android.os.UserManager;
75 import android.preference.PreferenceManager;
76 import android.service.notification.ConversationChannelWrapper;
77 import android.service.notification.NotificationListenerService;
78 import android.service.notification.StatusBarNotification;
79 import android.service.notification.ZenModeConfig;
80 import android.text.TextUtils;
81 import android.util.Log;
82 import android.widget.RemoteViews;
83 
84 import com.android.internal.annotations.GuardedBy;
85 import com.android.internal.annotations.VisibleForTesting;
86 import com.android.internal.logging.UiEventLogger;
87 import com.android.internal.logging.UiEventLoggerImpl;
88 import com.android.systemui.broadcast.BroadcastDispatcher;
89 import com.android.systemui.dagger.SysUISingleton;
90 import com.android.systemui.dagger.qualifiers.Background;
91 import com.android.systemui.people.NotificationHelper;
92 import com.android.systemui.people.PeopleBackupFollowUpJob;
93 import com.android.systemui.people.PeopleSpaceUtils;
94 import com.android.systemui.people.PeopleTileViewHelper;
95 import com.android.systemui.people.SharedPreferencesHelper;
96 import com.android.systemui.statusbar.NotificationListener;
97 import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
98 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
99 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
100 import com.android.wm.shell.bubbles.Bubbles;
101 
102 import java.util.ArrayList;
103 import java.util.Arrays;
104 import java.util.Collection;
105 import java.util.Collections;
106 import java.util.HashMap;
107 import java.util.HashSet;
108 import java.util.List;
109 import java.util.Map;
110 import java.util.Objects;
111 import java.util.Optional;
112 import java.util.Set;
113 import java.util.concurrent.Executor;
114 import java.util.function.Function;
115 import java.util.stream.Collectors;
116 import java.util.stream.Stream;
117 
118 import javax.inject.Inject;
119 
120 /** Manager for People Space widget. */
121 @SysUISingleton
122 public class PeopleSpaceWidgetManager {
123     private static final String TAG = "PeopleSpaceWidgetMgr";
124     private static final boolean DEBUG = PeopleSpaceUtils.DEBUG;
125 
126     private final Object mLock = new Object();
127     private final Context mContext;
128     private LauncherApps mLauncherApps;
129     private AppWidgetManager mAppWidgetManager;
130     private IPeopleManager mIPeopleManager;
131     private SharedPreferences mSharedPrefs;
132     private PeopleManager mPeopleManager;
133     private CommonNotifCollection mNotifCollection;
134     private PackageManager mPackageManager;
135     private INotificationManager mINotificationManager;
136     private Optional<Bubbles> mBubblesOptional;
137     private UserManager mUserManager;
138     private PeopleSpaceWidgetManager mManager;
139     private BackupManager mBackupManager;
140     public UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
141     private NotificationManager mNotificationManager;
142     private BroadcastDispatcher mBroadcastDispatcher;
143     private Executor mBgExecutor;
144     @GuardedBy("mLock")
145     public static Map<PeopleTileKey, TileConversationListener>
146             mListeners = new HashMap<>();
147 
148     @GuardedBy("mLock")
149     // Map of notification key mapped to widget IDs previously updated by the contact Uri field.
150     // This is required because on notification removal, the contact Uri field is stripped and we
151     // only have the notification key to determine which widget IDs should be updated.
152     private Map<String, Set<String>> mNotificationKeyToWidgetIdsMatchedByUri = new HashMap<>();
153     private boolean mRegisteredReceivers;
154 
155     @GuardedBy("mLock")
156     public static Map<Integer, PeopleSpaceTile> mTiles = new HashMap<>();
157 
158     @Inject
PeopleSpaceWidgetManager(Context context, LauncherApps launcherApps, CommonNotifCollection notifCollection, PackageManager packageManager, Optional<Bubbles> bubblesOptional, UserManager userManager, NotificationManager notificationManager, BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor)159     public PeopleSpaceWidgetManager(Context context, LauncherApps launcherApps,
160             CommonNotifCollection notifCollection,
161             PackageManager packageManager, Optional<Bubbles> bubblesOptional,
162             UserManager userManager, NotificationManager notificationManager,
163             BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor) {
164         if (DEBUG) Log.d(TAG, "constructor");
165         mContext = context;
166         mAppWidgetManager = AppWidgetManager.getInstance(context);
167         mIPeopleManager = IPeopleManager.Stub.asInterface(
168                 ServiceManager.getService(Context.PEOPLE_SERVICE));
169         mLauncherApps = launcherApps;
170         mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
171         mPeopleManager = context.getSystemService(PeopleManager.class);
172         mNotifCollection = notifCollection;
173         mPackageManager = packageManager;
174         mINotificationManager = INotificationManager.Stub.asInterface(
175                 ServiceManager.getService(Context.NOTIFICATION_SERVICE));
176         mBubblesOptional = bubblesOptional;
177         mUserManager = userManager;
178         mBackupManager = new BackupManager(context);
179         mNotificationManager = notificationManager;
180         mManager = this;
181         mBroadcastDispatcher = broadcastDispatcher;
182         mBgExecutor = bgExecutor;
183     }
184 
185     /** Initializes {@PeopleSpaceWidgetManager}. */
init()186     public void init() {
187         synchronized (mLock) {
188             if (!mRegisteredReceivers) {
189                 if (DEBUG) Log.d(TAG, "Register receivers");
190                 IntentFilter filter = new IntentFilter();
191                 filter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
192                 filter.addAction(ACTION_BOOT_COMPLETED);
193                 filter.addAction(Intent.ACTION_LOCALE_CHANGED);
194                 filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
195                 filter.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
196                 filter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
197                 filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
198                 filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
199                 filter.addAction(Intent.ACTION_USER_UNLOCKED);
200                 mBroadcastDispatcher.registerReceiver(mBaseBroadcastReceiver, filter,
201 
202                         null /* executor */, UserHandle.ALL);
203                 IntentFilter perAppFilter = new IntentFilter(ACTION_PACKAGE_REMOVED);
204                 perAppFilter.addAction(ACTION_PACKAGE_ADDED);
205                 perAppFilter.addDataScheme("package");
206                 // BroadcastDispatcher doesn't allow data schemes.
207                 mContext.registerReceiver(mBaseBroadcastReceiver, perAppFilter);
208                 IntentFilter bootComplete = new IntentFilter(ACTION_BOOT_COMPLETED);
209                 bootComplete.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
210                 // BroadcastDispatcher doesn't allow priority.
211                 mContext.registerReceiver(mBaseBroadcastReceiver, bootComplete);
212                 mRegisteredReceivers = true;
213             }
214         }
215     }
216 
217     /** Listener for the shortcut data changes. */
218     public class TileConversationListener implements PeopleManager.ConversationListener {
219 
220         @Override
onConversationUpdate(@onNull ConversationChannel conversation)221         public void onConversationUpdate(@NonNull ConversationChannel conversation) {
222             if (DEBUG) {
223                 Log.d(TAG,
224                         "Received updated conversation: "
225                                 + conversation.getShortcutInfo().getLabel());
226             }
227             mBgExecutor.execute(() ->
228                     updateWidgetsWithConversationChanged(conversation));
229         }
230     }
231 
232     /**
233      * PeopleSpaceWidgetManager setter used for testing.
234      */
235     @VisibleForTesting
PeopleSpaceWidgetManager(Context context, AppWidgetManager appWidgetManager, IPeopleManager iPeopleManager, PeopleManager peopleManager, LauncherApps launcherApps, CommonNotifCollection notifCollection, PackageManager packageManager, Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager, INotificationManager iNotificationManager, NotificationManager notificationManager, @Background Executor executor)236     PeopleSpaceWidgetManager(Context context,
237             AppWidgetManager appWidgetManager, IPeopleManager iPeopleManager,
238             PeopleManager peopleManager, LauncherApps launcherApps,
239             CommonNotifCollection notifCollection, PackageManager packageManager,
240             Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager,
241             INotificationManager iNotificationManager, NotificationManager notificationManager,
242             @Background Executor executor) {
243         mContext = context;
244         mAppWidgetManager = appWidgetManager;
245         mIPeopleManager = iPeopleManager;
246         mPeopleManager = peopleManager;
247         mLauncherApps = launcherApps;
248         mNotifCollection = notifCollection;
249         mPackageManager = packageManager;
250         mBubblesOptional = bubblesOptional;
251         mUserManager = userManager;
252         mBackupManager = backupManager;
253         mINotificationManager = iNotificationManager;
254         mNotificationManager = notificationManager;
255         mManager = this;
256         mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
257         mBgExecutor = executor;
258     }
259 
260     /**
261      * Updates People Space widgets.
262      */
updateWidgets(int[] widgetIds)263     public void updateWidgets(int[] widgetIds) {
264         mBgExecutor.execute(() -> updateWidgetsInBackground(widgetIds));
265     }
266 
updateWidgetsInBackground(int[] widgetIds)267     private void updateWidgetsInBackground(int[] widgetIds) {
268         try {
269             if (DEBUG) Log.d(TAG, "updateWidgets called");
270             if (widgetIds.length == 0) {
271                 if (DEBUG) Log.d(TAG, "no widgets to update");
272                 return;
273             }
274             synchronized (mLock) {
275                 updateSingleConversationWidgets(widgetIds);
276             }
277         } catch (Exception e) {
278             Log.e(TAG, "failed to update widgets", e);
279         }
280     }
281 
282     /**
283      * Updates {@code appWidgetIds} with their associated conversation stored, handling a
284      * notification being posted or removed.
285      */
updateSingleConversationWidgets(int[] appWidgetIds)286     public void updateSingleConversationWidgets(int[] appWidgetIds) {
287         Map<Integer, PeopleSpaceTile> widgetIdToTile = new HashMap<>();
288         for (int appWidgetId : appWidgetIds) {
289             if (DEBUG) Log.d(TAG, "Updating widget: " + appWidgetId);
290             PeopleSpaceTile tile = getTileForExistingWidget(appWidgetId);
291             if (tile == null) {
292                 Log.e(TAG, "Matching conversation not found for widget " + appWidgetId);
293             }
294             updateAppWidgetOptionsAndView(appWidgetId, tile);
295             widgetIdToTile.put(appWidgetId, tile);
296             if (tile != null) {
297                 registerConversationListenerIfNeeded(appWidgetId,
298                         new PeopleTileKey(tile));
299             }
300         }
301         PeopleSpaceUtils.getDataFromContactsOnBackgroundThread(
302                 mContext, mManager, widgetIdToTile, appWidgetIds);
303     }
304 
305     /** Updates the current widget view with provided {@link PeopleSpaceTile}. */
updateAppWidgetViews(int appWidgetId, PeopleSpaceTile tile, Bundle options)306     private void updateAppWidgetViews(int appWidgetId, PeopleSpaceTile tile, Bundle options) {
307         PeopleTileKey key = getKeyFromStorageByWidgetId(appWidgetId);
308         if (DEBUG) Log.d(TAG, "Widget: " + appWidgetId + " for: " + key.toString());
309 
310         if (!PeopleTileKey.isValid(key)) {
311             Log.e(TAG, "Invalid tile key updating widget " + appWidgetId);
312             return;
313         }
314         RemoteViews views = PeopleTileViewHelper.createRemoteViews(mContext, tile, appWidgetId,
315                 options, key);
316 
317         // Tell the AppWidgetManager to perform an update on the current app widget.
318         if (DEBUG) Log.d(TAG, "Calling update widget for widgetId: " + appWidgetId);
319         mAppWidgetManager.updateAppWidget(appWidgetId, views);
320     }
321 
322     /** Updates tile in app widget options and the current view. */
updateAppWidgetOptionsAndViewOptional(int appWidgetId, Optional<PeopleSpaceTile> tile)323     public void updateAppWidgetOptionsAndViewOptional(int appWidgetId,
324             Optional<PeopleSpaceTile> tile) {
325         if (tile.isPresent()) {
326             updateAppWidgetOptionsAndView(appWidgetId, tile.get());
327         }
328     }
329 
330     /** Updates tile in app widget options and the current view. */
updateAppWidgetOptionsAndView(int appWidgetId, PeopleSpaceTile tile)331     public void updateAppWidgetOptionsAndView(int appWidgetId, PeopleSpaceTile tile) {
332         if (tile == null) {
333             Log.w(TAG, "Storing null tile for widget " + appWidgetId);
334         }
335         synchronized (mTiles) {
336             mTiles.put(appWidgetId, tile);
337         }
338         Bundle options = mAppWidgetManager.getAppWidgetOptions(appWidgetId);
339         updateAppWidgetViews(appWidgetId, tile, options);
340     }
341 
342     /**
343      * Returns a {@link PeopleSpaceTile} based on the {@code appWidgetId}.
344      * Widget already exists, so fetch {@link PeopleTileKey} from {@link SharedPreferences}.
345      */
346     @Nullable
getTileForExistingWidget(int appWidgetId)347     public PeopleSpaceTile getTileForExistingWidget(int appWidgetId) {
348         try {
349             return getTileForExistingWidgetThrowing(appWidgetId);
350         } catch (Exception e) {
351             Log.e(TAG, "failed to retrieve tile for existing widget " + appWidgetId, e);
352             return null;
353         }
354     }
355 
356     @Nullable
getTileForExistingWidgetThrowing(int appWidgetId)357     private PeopleSpaceTile getTileForExistingWidgetThrowing(int appWidgetId) throws
358             PackageManager.NameNotFoundException {
359         // First, check if tile is cached in memory.
360         PeopleSpaceTile tile;
361         synchronized (mTiles) {
362             tile = mTiles.get(appWidgetId);
363         }
364         if (tile != null) {
365             if (DEBUG) Log.d(TAG, "People Tile is cached for widget: " + appWidgetId);
366             return tile;
367         }
368 
369         // If tile is null, we need to retrieve from persistent storage.
370         if (DEBUG) Log.d(TAG, "Fetching key from sharedPreferences: " + appWidgetId);
371         SharedPreferences widgetSp = mContext.getSharedPreferences(
372                 String.valueOf(appWidgetId),
373                 Context.MODE_PRIVATE);
374         PeopleTileKey key = new PeopleTileKey(
375                 widgetSp.getString(SHORTCUT_ID, EMPTY_STRING),
376                 widgetSp.getInt(USER_ID, INVALID_USER_ID),
377                 widgetSp.getString(PACKAGE_NAME, EMPTY_STRING));
378 
379         return getTileFromPersistentStorage(key, appWidgetId, /* supplementFromStorage= */ true);
380     }
381 
382     /**
383      * Returns a {@link PeopleSpaceTile} based on the {@code appWidgetId}.
384      * If a {@link PeopleTileKey} is not provided, fetch one from {@link SharedPreferences}.
385      */
386     @Nullable
getTileFromPersistentStorage(PeopleTileKey key, int appWidgetId, boolean supplementFromStorage)387     public PeopleSpaceTile getTileFromPersistentStorage(PeopleTileKey key, int appWidgetId,
388             boolean supplementFromStorage) throws
389             PackageManager.NameNotFoundException {
390         if (!PeopleTileKey.isValid(key)) {
391             Log.e(TAG, "Invalid tile key finding tile for existing widget " + appWidgetId);
392             return null;
393         }
394 
395         if (mIPeopleManager == null || mLauncherApps == null) {
396             Log.d(TAG, "System services are null");
397             return null;
398         }
399         try {
400             if (DEBUG) Log.d(TAG, "Retrieving Tile from storage: " + key.toString());
401             ConversationChannel channel = mIPeopleManager.getConversation(
402                     key.getPackageName(), key.getUserId(), key.getShortcutId());
403             if (channel == null) {
404                 if (DEBUG) Log.d(TAG, "Could not retrieve conversation from storage");
405                 return null;
406             }
407 
408             // Get tile from shortcut & conversation storage.
409             PeopleSpaceTile.Builder storedTile = new PeopleSpaceTile.Builder(channel,
410                     mLauncherApps);
411             if (storedTile == null) {
412                 return storedTile.build();
413             }
414 
415             // Supplement with our storage.
416             String contactUri = mSharedPrefs.getString(String.valueOf(appWidgetId), null);
417             if (supplementFromStorage && contactUri != null
418                     && storedTile.build().getContactUri() == null) {
419                 if (DEBUG) Log.d(TAG, "Restore contact uri from storage: " + contactUri);
420                 storedTile.setContactUri(Uri.parse(contactUri));
421             }
422 
423             // Add current state.
424             return getTileWithCurrentState(storedTile.build(), ACTION_BOOT_COMPLETED);
425         } catch (RemoteException e) {
426             Log.e(TAG, "getTileFromPersistentStorage failing for widget " + appWidgetId, e);
427             return null;
428         }
429     }
430 
431     /**
432      * Check if any existing People tiles match the incoming notification change, and store the
433      * change in the tile if so.
434      */
updateWidgetsWithNotificationChanged(StatusBarNotification sbn, PeopleSpaceUtils.NotificationAction notificationAction)435     public void updateWidgetsWithNotificationChanged(StatusBarNotification sbn,
436             PeopleSpaceUtils.NotificationAction notificationAction) {
437         if (DEBUG) {
438             if (notificationAction == PeopleSpaceUtils.NotificationAction.POSTED) {
439                 Log.d(TAG, "Notification posted, key: " + sbn.getKey());
440             } else {
441                 Log.d(TAG, "Notification removed, key: " + sbn.getKey());
442             }
443         }
444         if (DEBUG) Log.d(TAG, "Fetching notifications");
445         Collection<NotificationEntry> notifications = mNotifCollection.getAllNotifs();
446         mBgExecutor.execute(
447                 () -> updateWidgetsWithNotificationChangedInBackground(
448                         sbn, notificationAction, notifications));
449     }
450 
updateWidgetsWithNotificationChangedInBackground(StatusBarNotification sbn, PeopleSpaceUtils.NotificationAction action, Collection<NotificationEntry> notifications)451     private void updateWidgetsWithNotificationChangedInBackground(StatusBarNotification sbn,
452             PeopleSpaceUtils.NotificationAction action,
453             Collection<NotificationEntry> notifications) {
454         try {
455             PeopleTileKey key = new PeopleTileKey(
456                     sbn.getShortcutId(), sbn.getUser().getIdentifier(), sbn.getPackageName());
457             if (!PeopleTileKey.isValid(key)) {
458                 if (DEBUG) Log.d(TAG, "Sbn doesn't contain valid PeopleTileKey: " + key.toString());
459                 return;
460             }
461             int[] widgetIds = mAppWidgetManager.getAppWidgetIds(
462                     new ComponentName(mContext, PeopleSpaceWidgetProvider.class)
463             );
464             if (widgetIds.length == 0) {
465                 Log.d(TAG, "No app widget ids returned");
466                 return;
467             }
468             synchronized (mLock) {
469                 Set<String> tilesUpdated = getMatchingKeyWidgetIds(key);
470                 Set<String> tilesUpdatedByUri = getMatchingUriWidgetIds(sbn, action);
471                 if (DEBUG) {
472                     Log.d(TAG, "Widgets by key to be updated:" + tilesUpdated.toString());
473                     Log.d(TAG, "Widgets by URI to be updated:" + tilesUpdatedByUri.toString());
474                 }
475                 tilesUpdated.addAll(tilesUpdatedByUri);
476                 updateWidgetIdsBasedOnNotifications(tilesUpdated, notifications);
477             }
478         } catch (Exception e) {
479             Log.e(TAG, "updateWidgetsWithNotificationChangedInBackground failing", e);
480         }
481     }
482 
483     /** Updates {@code widgetIdsToUpdate} with {@code action}. */
updateWidgetIdsBasedOnNotifications(Set<String> widgetIdsToUpdate, Collection<NotificationEntry> ungroupedNotifications)484     private void updateWidgetIdsBasedOnNotifications(Set<String> widgetIdsToUpdate,
485             Collection<NotificationEntry> ungroupedNotifications) {
486         if (widgetIdsToUpdate.isEmpty()) {
487             if (DEBUG) Log.d(TAG, "No widgets to update, returning.");
488             return;
489         }
490         try {
491             Map<PeopleTileKey, Set<NotificationEntry>> groupedNotifications =
492                     groupConversationNotifications(ungroupedNotifications);
493 
494             widgetIdsToUpdate
495                     .stream()
496                     .map(Integer::parseInt)
497                     .collect(Collectors.toMap(
498                             Function.identity(),
499                             id -> getAugmentedTileForExistingWidget(id, groupedNotifications)))
500                     .forEach((id, tile) -> updateAppWidgetOptionsAndViewOptional(id, tile));
501         } catch (Exception e) {
502             Log.e(TAG, "updateWidgetIdsBasedOnNotifications failing", e);
503         }
504     }
505 
506     /**
507      * Augments {@code tile} based on notifications returned from {@code notificationEntryManager}.
508      */
augmentTileFromNotificationEntryManager(PeopleSpaceTile tile, Optional<Integer> appWidgetId)509     public PeopleSpaceTile augmentTileFromNotificationEntryManager(PeopleSpaceTile tile,
510             Optional<Integer> appWidgetId) {
511         PeopleTileKey key = new PeopleTileKey(tile);
512         if (DEBUG) {
513             Log.d(TAG,
514                     "Augmenting tile from NotificationEntryManager widget: " + key.toString());
515         }
516         Map<PeopleTileKey, Set<NotificationEntry>> notifications =
517                 groupConversationNotifications(mNotifCollection.getAllNotifs());
518         String contactUri = null;
519         if (tile.getContactUri() != null) {
520             contactUri = tile.getContactUri().toString();
521         }
522         return augmentTileFromNotifications(tile, key, contactUri, notifications, appWidgetId);
523     }
524 
525     /** Groups active and pending notifications grouped by {@link PeopleTileKey}. */
groupConversationNotifications( Collection<NotificationEntry> notifications )526     public Map<PeopleTileKey, Set<NotificationEntry>> groupConversationNotifications(
527             Collection<NotificationEntry> notifications
528     ) {
529         if (DEBUG) Log.d(TAG, "Number of total notifications: " + notifications.size());
530         Map<PeopleTileKey, Set<NotificationEntry>> groupedNotifications =
531                 notifications
532                         .stream()
533                         .filter(entry -> NotificationHelper.isValid(entry)
534                                 && NotificationHelper.isMissedCallOrHasContent(entry)
535                                 && !shouldFilterOut(mBubblesOptional, entry))
536                         .collect(Collectors.groupingBy(
537                                 PeopleTileKey::new,
538                                 Collectors.mapping(Function.identity(), Collectors.toSet())));
539         if (DEBUG) {
540             Log.d(TAG, "Number of grouped conversation notifications keys: "
541                     + groupedNotifications.keySet().size());
542         }
543         return groupedNotifications;
544     }
545 
546     /** Augments {@code tile} based on {@code notifications}, matching {@code contactUri}. */
augmentTileFromNotifications(PeopleSpaceTile tile, PeopleTileKey key, String contactUri, Map<PeopleTileKey, Set<NotificationEntry>> notifications, Optional<Integer> appWidgetId)547     public PeopleSpaceTile augmentTileFromNotifications(PeopleSpaceTile tile, PeopleTileKey key,
548             String contactUri,
549             Map<PeopleTileKey, Set<NotificationEntry>> notifications,
550             Optional<Integer> appWidgetId) {
551         if (DEBUG) Log.d(TAG, "Augmenting tile from notifications. Tile key: " + key.toString());
552         boolean hasReadContactsPermission = mPackageManager.checkPermission(READ_CONTACTS,
553                 tile.getPackageName()) == PackageManager.PERMISSION_GRANTED;
554 
555         List<NotificationEntry> notificationsByUri = new ArrayList<>();
556         if (hasReadContactsPermission) {
557             notificationsByUri = getNotificationsByUri(mPackageManager, contactUri, notifications);
558             if (!notificationsByUri.isEmpty()) {
559                 if (DEBUG) {
560                     Log.d(TAG, "Number of notifications matched by contact URI: "
561                             + notificationsByUri.size());
562                 }
563             }
564         }
565 
566         Set<NotificationEntry> allNotifications = notifications.get(key);
567         if (allNotifications == null) {
568             allNotifications = new HashSet<>();
569         }
570         if (allNotifications.isEmpty() && notificationsByUri.isEmpty()) {
571             if (DEBUG) Log.d(TAG, "No existing notifications for tile: " + key.toString());
572             return removeNotificationFields(tile);
573         }
574 
575         // Merge notifications matched by key and by contact URI.
576         allNotifications.addAll(notificationsByUri);
577         if (DEBUG) Log.d(TAG, "Total notifications matching tile: " + allNotifications.size());
578 
579         int messagesCount = getMessagesCount(allNotifications);
580         NotificationEntry highestPriority = getHighestPriorityNotification(allNotifications);
581 
582         if (DEBUG) Log.d(TAG, "Augmenting tile from notification, key: " + key.toString());
583         return augmentTileFromNotification(mContext, tile, key, highestPriority, messagesCount,
584                 appWidgetId, mBackupManager);
585     }
586 
587     /** Returns an augmented tile for an existing widget. */
588     @Nullable
getAugmentedTileForExistingWidget(int widgetId, Map<PeopleTileKey, Set<NotificationEntry>> notifications)589     public Optional<PeopleSpaceTile> getAugmentedTileForExistingWidget(int widgetId,
590             Map<PeopleTileKey, Set<NotificationEntry>> notifications) {
591         if (DEBUG) Log.d(TAG, "Augmenting tile for existing widget: " + widgetId);
592         PeopleSpaceTile tile = getTileForExistingWidget(widgetId);
593         if (tile == null) {
594             Log.w(TAG, "Null tile for existing widget " + widgetId + ", skipping update.");
595             return Optional.empty();
596         }
597         String contactUriString = mSharedPrefs.getString(String.valueOf(widgetId), null);
598         // Should never be null, but using ofNullable for extra safety.
599         PeopleTileKey key = new PeopleTileKey(tile);
600         if (DEBUG) Log.d(TAG, "Existing widget: " + widgetId + ". Tile key: " + key.toString());
601         return Optional.ofNullable(
602                 augmentTileFromNotifications(tile, key, contactUriString, notifications,
603                         Optional.of(widgetId)));
604     }
605 
606     /** Returns stored widgets for the conversation specified. */
getMatchingKeyWidgetIds(PeopleTileKey key)607     public Set<String> getMatchingKeyWidgetIds(PeopleTileKey key) {
608         if (!PeopleTileKey.isValid(key)) {
609             return new HashSet<>();
610         }
611         return new HashSet<>(mSharedPrefs.getStringSet(key.toString(), new HashSet<>()));
612     }
613 
614     /**
615      * Updates in-memory map of tiles with matched Uris, dependent on the {@code action}.
616      *
617      * <p>If the notification was added, adds the notification based on the contact Uri within
618      * {@code sbn}.
619      * <p>If the notification was removed, removes the notification based on the in-memory map of
620      * widgets previously updated by Uri (since the contact Uri is stripped from the {@code sbn}).
621      */
622     @Nullable
getMatchingUriWidgetIds(StatusBarNotification sbn, PeopleSpaceUtils.NotificationAction action)623     private Set<String> getMatchingUriWidgetIds(StatusBarNotification sbn,
624             PeopleSpaceUtils.NotificationAction action) {
625         if (action.equals(PeopleSpaceUtils.NotificationAction.POSTED)) {
626             Set<String> widgetIdsUpdatedByUri = fetchMatchingUriWidgetIds(sbn);
627             if (widgetIdsUpdatedByUri != null && !widgetIdsUpdatedByUri.isEmpty()) {
628                 mNotificationKeyToWidgetIdsMatchedByUri.put(sbn.getKey(), widgetIdsUpdatedByUri);
629                 return widgetIdsUpdatedByUri;
630             }
631         } else {
632             // Remove the notification on any widgets where the notification was added
633             // purely based on the Uri.
634             Set<String> widgetsPreviouslyUpdatedByUri =
635                     mNotificationKeyToWidgetIdsMatchedByUri.remove(sbn.getKey());
636             if (widgetsPreviouslyUpdatedByUri != null && !widgetsPreviouslyUpdatedByUri.isEmpty()) {
637                 return widgetsPreviouslyUpdatedByUri;
638             }
639         }
640         return new HashSet<>();
641     }
642 
643     /** Fetches widget Ids that match the contact URI in {@code sbn}. */
644     @Nullable
fetchMatchingUriWidgetIds(StatusBarNotification sbn)645     private Set<String> fetchMatchingUriWidgetIds(StatusBarNotification sbn) {
646         // Check if it's a missed call notification
647         if (!shouldMatchNotificationByUri(sbn)) {
648             if (DEBUG) Log.d(TAG, "Should not supplement conversation");
649             return null;
650         }
651 
652         // Try to get the Contact Uri from the Missed Call notification directly.
653         String contactUri = getContactUri(sbn);
654         if (contactUri == null) {
655             if (DEBUG) Log.d(TAG, "No contact uri");
656             return null;
657         }
658 
659         // Supplement any tiles with the same Uri.
660         Set<String> storedWidgetIdsByUri =
661                 new HashSet<>(mSharedPrefs.getStringSet(contactUri, new HashSet<>()));
662         if (storedWidgetIdsByUri.isEmpty()) {
663             if (DEBUG) Log.d(TAG, "No tiles for contact");
664             return null;
665         }
666         return storedWidgetIdsByUri;
667     }
668 
669     /**
670      * Update the tiles associated with the incoming conversation update.
671      */
updateWidgetsWithConversationChanged(ConversationChannel conversation)672     public void updateWidgetsWithConversationChanged(ConversationChannel conversation) {
673         ShortcutInfo info = conversation.getShortcutInfo();
674         synchronized (mLock) {
675             PeopleTileKey key = new PeopleTileKey(
676                     info.getId(), info.getUserId(), info.getPackage());
677             Set<String> storedWidgetIds = getMatchingKeyWidgetIds(key);
678             for (String widgetIdString : storedWidgetIds) {
679                 if (DEBUG) {
680                     Log.d(TAG,
681                             "Conversation update for widget " + widgetIdString + " , "
682                                     + info.getLabel());
683                 }
684                 updateStorageAndViewWithConversationData(conversation,
685                         Integer.parseInt(widgetIdString));
686             }
687         }
688     }
689 
690     /**
691      * Update {@code appWidgetId} with the new data provided by {@code conversation}.
692      */
updateStorageAndViewWithConversationData(ConversationChannel conversation, int appWidgetId)693     private void updateStorageAndViewWithConversationData(ConversationChannel conversation,
694             int appWidgetId) {
695         PeopleSpaceTile storedTile = getTileForExistingWidget(appWidgetId);
696         if (storedTile == null) {
697             if (DEBUG) Log.d(TAG, "Could not find stored tile to add conversation to");
698             return;
699         }
700         PeopleSpaceTile.Builder updatedTile = storedTile.toBuilder();
701         ShortcutInfo info = conversation.getShortcutInfo();
702         Uri uri = null;
703         if (info.getPersons() != null && info.getPersons().length > 0) {
704             Person person = info.getPersons()[0];
705             uri = person.getUri() == null ? null : Uri.parse(person.getUri());
706         }
707         CharSequence label = info.getLabel();
708         if (label != null) {
709             updatedTile.setUserName(label);
710         }
711         Icon icon = PeopleSpaceTile.convertDrawableToIcon(mLauncherApps.getShortcutIconDrawable(
712                 info, 0));
713         if (icon != null) {
714             updatedTile.setUserIcon(icon);
715         }
716         if (DEBUG) Log.d(TAG, "Statuses: " + conversation.getStatuses());
717         NotificationChannel channel = conversation.getNotificationChannel();
718         if (channel != null) {
719             if (DEBUG) Log.d(TAG, "Important:" + channel.isImportantConversation());
720             updatedTile.setIsImportantConversation(channel.isImportantConversation());
721         }
722         updatedTile
723                 .setContactUri(uri)
724                 .setStatuses(conversation.getStatuses())
725                 .setLastInteractionTimestamp(conversation.getLastEventTimestamp());
726         updateAppWidgetOptionsAndView(appWidgetId, updatedTile.build());
727     }
728 
729     /**
730      * Attaches the manager to the pipeline, making it ready to receive events. Should only be
731      * called once.
732      */
attach(NotificationListener listenerService)733     public void attach(NotificationListener listenerService) {
734         if (DEBUG) Log.d(TAG, "attach");
735         listenerService.addNotificationHandler(mListener);
736     }
737 
738     private final NotificationHandler mListener = new NotificationHandler() {
739         @Override
740         public void onNotificationPosted(
741                 StatusBarNotification sbn, NotificationListenerService.RankingMap rankingMap) {
742             updateWidgetsWithNotificationChanged(sbn, PeopleSpaceUtils.NotificationAction.POSTED);
743         }
744 
745         @Override
746         public void onNotificationRemoved(
747                 StatusBarNotification sbn,
748                 NotificationListenerService.RankingMap rankingMap
749         ) {
750             updateWidgetsWithNotificationChanged(sbn, PeopleSpaceUtils.NotificationAction.REMOVED);
751         }
752 
753         @Override
754         public void onNotificationRemoved(
755                 StatusBarNotification sbn,
756                 NotificationListenerService.RankingMap rankingMap,
757                 int reason) {
758             updateWidgetsWithNotificationChanged(sbn, PeopleSpaceUtils.NotificationAction.REMOVED);
759         }
760 
761         @Override
762         public void onNotificationRankingUpdate(
763                 NotificationListenerService.RankingMap rankingMap) {
764         }
765 
766         @Override
767         public void onNotificationsInitialized() {
768             if (DEBUG) Log.d(TAG, "onNotificationsInitialized");
769         }
770 
771         @Override
772         public void onNotificationChannelModified(
773                 String pkgName,
774                 UserHandle user,
775                 NotificationChannel channel,
776                 int modificationType) {
777             if (channel.isConversation()) {
778                 mBgExecutor.execute(() -> {
779                     if (mUserManager.isUserUnlocked(user)) {
780                         updateWidgets(mAppWidgetManager.getAppWidgetIds(
781                                 new ComponentName(mContext, PeopleSpaceWidgetProvider.class)
782                         ));
783                     }
784                 });
785             }
786         }
787     };
788 
789     /**
790      * Checks if this widget has been added externally, and this the first time we are learning
791      * about the widget. If so, the widget adder should have populated options with PeopleTileKey
792      * arguments.
793      */
onAppWidgetOptionsChanged(int appWidgetId, Bundle newOptions)794     public void onAppWidgetOptionsChanged(int appWidgetId, Bundle newOptions) {
795         // Check if this widget has been added externally, and this the first time we are
796         // learning about the widget. If so, the widget adder should have populated options with
797         // PeopleTileKey arguments.
798         if (DEBUG) Log.d(TAG, "onAppWidgetOptionsChanged called for widget: " + appWidgetId);
799         PeopleTileKey optionsKey = AppWidgetOptionsHelper.getPeopleTileKeyFromBundle(newOptions);
800         if (PeopleTileKey.isValid(optionsKey)) {
801             if (DEBUG) {
802                 Log.d(TAG, "PeopleTileKey was present in Options, shortcutId: "
803                         + optionsKey.getShortcutId());
804             }
805             AppWidgetOptionsHelper.removePeopleTileKey(mAppWidgetManager, appWidgetId);
806             addNewWidget(appWidgetId, optionsKey);
807         }
808         // Update views for new widget dimensions.
809         updateWidgets(new int[]{appWidgetId});
810     }
811 
812     /** Adds a widget based on {@code key} mapped to {@code appWidgetId}. */
addNewWidget(int appWidgetId, PeopleTileKey key)813     public void addNewWidget(int appWidgetId, PeopleTileKey key) {
814         if (DEBUG) Log.d(TAG, "addNewWidget called with key for appWidgetId: " + appWidgetId);
815         PeopleSpaceTile tile = null;
816         try {
817             tile = getTileFromPersistentStorage(key, appWidgetId,  /* supplementFromStorage= */
818                     false);
819         } catch (PackageManager.NameNotFoundException e) {
820             Log.e(TAG, "Cannot add widget " + appWidgetId + " since app was uninstalled");
821             return;
822         }
823         if (tile == null) {
824             return;
825         }
826         tile = augmentTileFromNotificationEntryManager(tile, Optional.of(appWidgetId));
827 
828         PeopleTileKey existingKeyIfStored;
829         synchronized (mLock) {
830             existingKeyIfStored = getKeyFromStorageByWidgetId(appWidgetId);
831         }
832         // Delete previous storage if the widget already existed and is just reconfigured.
833         if (PeopleTileKey.isValid(existingKeyIfStored)) {
834             if (DEBUG) Log.d(TAG, "Remove previous storage for widget: " + appWidgetId);
835             deleteWidgets(new int[]{appWidgetId});
836         } else {
837             // Widget newly added.
838             mUiEventLogger.log(
839                     PeopleSpaceUtils.PeopleSpaceWidgetEvent.PEOPLE_SPACE_WIDGET_ADDED);
840         }
841 
842         synchronized (mLock) {
843             if (DEBUG) Log.d(TAG, "Add storage for : " + key.toString());
844             PeopleSpaceUtils.setSharedPreferencesStorageForTile(mContext, key, appWidgetId,
845                     tile.getContactUri(), mBackupManager);
846         }
847         if (DEBUG) Log.d(TAG, "Ensure listener is registered for widget: " + appWidgetId);
848         registerConversationListenerIfNeeded(appWidgetId, key);
849         try {
850             if (DEBUG) Log.d(TAG, "Caching shortcut for PeopleTile: " + key.toString());
851             mLauncherApps.cacheShortcuts(tile.getPackageName(),
852                     Collections.singletonList(tile.getId()),
853                     tile.getUserHandle(), LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS);
854         } catch (Exception e) {
855             Log.w(TAG, "failed to cache shortcut for widget " + appWidgetId, e);
856         }
857         PeopleSpaceTile finalTile = tile;
858         mBgExecutor.execute(
859                 () -> updateAppWidgetOptionsAndView(appWidgetId, finalTile));
860     }
861 
862     /** Registers a conversation listener for {@code appWidgetId} if not already registered. */
registerConversationListenerIfNeeded(int widgetId, PeopleTileKey key)863     public void registerConversationListenerIfNeeded(int widgetId, PeopleTileKey key) {
864         // Retrieve storage needed for registration.
865         if (!PeopleTileKey.isValid(key)) {
866             Log.w(TAG, "Invalid tile key registering listener for widget " + widgetId);
867             return;
868         }
869         TileConversationListener newListener = new TileConversationListener();
870         synchronized (mListeners) {
871             if (mListeners.containsKey(key)) {
872                 if (DEBUG) Log.d(TAG, "Already registered listener");
873                 return;
874             }
875             if (DEBUG) Log.d(TAG, "Register listener for " + widgetId + " with " + key.toString());
876             mListeners.put(key, newListener);
877         }
878         mPeopleManager.registerConversationListener(key.getPackageName(),
879                 key.getUserId(),
880                 key.getShortcutId(), newListener,
881                 mContext.getMainExecutor());
882     }
883 
884     /**
885      * Attempts to get a key from storage for {@code widgetId}, returning null if an invalid key is
886      * found.
887      */
getKeyFromStorageByWidgetId(int widgetId)888     private PeopleTileKey getKeyFromStorageByWidgetId(int widgetId) {
889         SharedPreferences widgetSp = mContext.getSharedPreferences(String.valueOf(widgetId),
890                 Context.MODE_PRIVATE);
891         PeopleTileKey key = new PeopleTileKey(
892                 widgetSp.getString(SHORTCUT_ID, EMPTY_STRING),
893                 widgetSp.getInt(USER_ID, INVALID_USER_ID),
894                 widgetSp.getString(PACKAGE_NAME, EMPTY_STRING));
895         return key;
896     }
897 
898     /** Deletes all storage, listeners, and caching for {@code appWidgetIds}. */
deleteWidgets(int[] appWidgetIds)899     public void deleteWidgets(int[] appWidgetIds) {
900         for (int widgetId : appWidgetIds) {
901             if (DEBUG) Log.d(TAG, "Widget removed: " + widgetId);
902             mUiEventLogger.log(PeopleSpaceUtils.PeopleSpaceWidgetEvent.PEOPLE_SPACE_WIDGET_DELETED);
903             // Retrieve storage needed for widget deletion.
904             PeopleTileKey key;
905             Set<String> storedWidgetIdsForKey;
906             String contactUriString;
907             synchronized (mLock) {
908                 SharedPreferences widgetSp = mContext.getSharedPreferences(String.valueOf(widgetId),
909                         Context.MODE_PRIVATE);
910                 key = new PeopleTileKey(
911                         widgetSp.getString(SHORTCUT_ID, null),
912                         widgetSp.getInt(USER_ID, INVALID_USER_ID),
913                         widgetSp.getString(PACKAGE_NAME, null));
914                 if (!PeopleTileKey.isValid(key)) {
915                     Log.e(TAG, "Invalid tile key trying to remove widget " + widgetId);
916                     return;
917                 }
918                 storedWidgetIdsForKey = new HashSet<>(
919                         mSharedPrefs.getStringSet(key.toString(), new HashSet<>()));
920                 contactUriString = mSharedPrefs.getString(String.valueOf(widgetId), null);
921             }
922             synchronized (mLock) {
923                 PeopleSpaceUtils.removeSharedPreferencesStorageForTile(mContext, key, widgetId,
924                         contactUriString);
925             }
926             // If another tile with the conversation is still stored, we need to keep the listener.
927             if (DEBUG) Log.d(TAG, "Stored widget IDs: " + storedWidgetIdsForKey.toString());
928             if (storedWidgetIdsForKey.contains(String.valueOf(widgetId))
929                     && storedWidgetIdsForKey.size() == 1) {
930                 if (DEBUG) Log.d(TAG, "Remove caching and listener");
931                 unregisterConversationListener(key, widgetId);
932                 uncacheConversationShortcut(key);
933             }
934         }
935     }
936 
937     /** Unregisters the conversation listener for {@code appWidgetId}. */
unregisterConversationListener(PeopleTileKey key, int appWidgetId)938     private void unregisterConversationListener(PeopleTileKey key, int appWidgetId) {
939         TileConversationListener registeredListener;
940         synchronized (mListeners) {
941             registeredListener = mListeners.get(key);
942             if (registeredListener == null) {
943                 if (DEBUG) Log.d(TAG, "Cannot find listener to unregister");
944                 return;
945             }
946             if (DEBUG) {
947                 Log.d(TAG, "Unregister listener for " + appWidgetId + " with " + key.toString());
948             }
949             mListeners.remove(key);
950         }
951         mPeopleManager.unregisterConversationListener(registeredListener);
952     }
953 
954     /** Uncaches the conversation shortcut. */
uncacheConversationShortcut(PeopleTileKey key)955     private void uncacheConversationShortcut(PeopleTileKey key) {
956         try {
957             if (DEBUG) Log.d(TAG, "Uncaching shortcut for PeopleTile: " + key.getShortcutId());
958             mLauncherApps.uncacheShortcuts(key.getPackageName(),
959                     Collections.singletonList(key.getShortcutId()),
960                     UserHandle.of(key.getUserId()),
961                     LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS);
962         } catch (Exception e) {
963             Log.d(TAG, "failed to uncache shortcut", e);
964         }
965     }
966 
967     /**
968      * Builds a request to pin a People Tile app widget, with a preview and storing necessary
969      * information as the callback.
970      */
requestPinAppWidget(ShortcutInfo shortcutInfo, Bundle options)971     public boolean requestPinAppWidget(ShortcutInfo shortcutInfo, Bundle options) {
972         if (DEBUG) Log.d(TAG, "Requesting pin widget, shortcutId: " + shortcutInfo.getId());
973 
974         RemoteViews widgetPreview = getPreview(shortcutInfo.getId(),
975                 shortcutInfo.getUserHandle(), shortcutInfo.getPackage(), options);
976         if (widgetPreview == null) {
977             Log.w(TAG, "Skipping pinning widget: no tile for shortcutId: " + shortcutInfo.getId());
978             return false;
979         }
980         Bundle extras = new Bundle();
981         extras.putParcelable(AppWidgetManager.EXTRA_APPWIDGET_PREVIEW, widgetPreview);
982 
983         PendingIntent successCallback =
984                 PeopleSpaceWidgetPinnedReceiver.getPendingIntent(mContext, shortcutInfo);
985 
986         ComponentName componentName = new ComponentName(mContext, PeopleSpaceWidgetProvider.class);
987         return mAppWidgetManager.requestPinAppWidget(componentName, extras, successCallback);
988     }
989 
990     /** Returns a list of map entries corresponding to user's priority conversations. */
991     @NonNull
getPriorityTiles()992     public List<PeopleSpaceTile> getPriorityTiles()
993             throws Exception {
994         List<ConversationChannelWrapper> conversations =
995                 mINotificationManager.getConversations(true).getList();
996         // Add priority conversations to tiles list.
997         Stream<ShortcutInfo> priorityConversations = conversations.stream()
998                 .filter(c -> c.getNotificationChannel() != null
999                         && c.getNotificationChannel().isImportantConversation())
1000                 .map(c -> c.getShortcutInfo());
1001         List<PeopleSpaceTile> priorityTiles = PeopleSpaceUtils.getSortedTiles(mIPeopleManager,
1002                 mLauncherApps, mUserManager,
1003                 priorityConversations);
1004         return priorityTiles;
1005     }
1006 
1007     /** Returns a list of map entries corresponding to user's recent conversations. */
1008     @NonNull
getRecentTiles()1009     public List<PeopleSpaceTile> getRecentTiles()
1010             throws Exception {
1011         if (DEBUG) Log.d(TAG, "Add recent conversations");
1012         List<ConversationChannelWrapper> conversations =
1013                 mINotificationManager.getConversations(false).getList();
1014         Stream<ShortcutInfo> nonPriorityConversations = conversations.stream()
1015                 .filter(c -> c.getNotificationChannel() == null
1016                         || !c.getNotificationChannel().isImportantConversation())
1017                 .map(c -> c.getShortcutInfo());
1018 
1019         List<ConversationChannel> recentConversationsList =
1020                 mIPeopleManager.getRecentConversations().getList();
1021         Stream<ShortcutInfo> recentConversations = recentConversationsList
1022                 .stream()
1023                 .map(c -> c.getShortcutInfo());
1024 
1025         Stream<ShortcutInfo> mergedStream = Stream.concat(nonPriorityConversations,
1026                 recentConversations);
1027         List<PeopleSpaceTile> recentTiles =
1028                 PeopleSpaceUtils.getSortedTiles(mIPeopleManager, mLauncherApps, mUserManager,
1029                         mergedStream);
1030         return recentTiles;
1031     }
1032 
1033     /**
1034      * Returns a {@link RemoteViews} preview of a Conversation's People Tile. Returns null if one
1035      * is not available.
1036      */
getPreview(String shortcutId, UserHandle userHandle, String packageName, Bundle options)1037     public RemoteViews getPreview(String shortcutId, UserHandle userHandle, String packageName,
1038             Bundle options) {
1039         PeopleSpaceTile tile;
1040         ConversationChannel channel;
1041         try {
1042             channel = mIPeopleManager.getConversation(
1043                     packageName, userHandle.getIdentifier(), shortcutId);
1044             tile = PeopleSpaceUtils.getTile(channel, mLauncherApps);
1045         } catch (Exception e) {
1046             Log.w(TAG, "failed to get conversation or tile", e);
1047             return null;
1048         }
1049         if (tile == null) {
1050             if (DEBUG) Log.i(TAG, "No tile was returned");
1051             return null;
1052         }
1053 
1054         PeopleSpaceTile augmentedTile = augmentTileFromNotificationEntryManager(tile,
1055                 Optional.empty());
1056 
1057         if (DEBUG) Log.i(TAG, "Returning tile preview for shortcutId: " + shortcutId);
1058         return PeopleTileViewHelper.createRemoteViews(mContext, augmentedTile, 0, options,
1059                 new PeopleTileKey(augmentedTile));
1060     }
1061 
1062     protected final BroadcastReceiver mBaseBroadcastReceiver = new BroadcastReceiver() {
1063 
1064         @Override
1065         public void onReceive(Context context, Intent intent) {
1066             if (DEBUG) Log.d(TAG, "Update widgets from: " + intent.getAction());
1067             mBgExecutor.execute(() -> updateWidgetsFromBroadcastInBackground(intent.getAction()));
1068         }
1069     };
1070 
1071     /** Updates any app widget to the current state, triggered by a broadcast update. */
1072     @VisibleForTesting
updateWidgetsFromBroadcastInBackground(String entryPoint)1073     void updateWidgetsFromBroadcastInBackground(String entryPoint) {
1074         int[] appWidgetIds = mAppWidgetManager.getAppWidgetIds(
1075                 new ComponentName(mContext, PeopleSpaceWidgetProvider.class));
1076         if (appWidgetIds == null) {
1077             return;
1078         }
1079         for (int appWidgetId : appWidgetIds) {
1080             if (DEBUG) Log.d(TAG, "Updating widget from broadcast, widget id: " + appWidgetId);
1081             PeopleSpaceTile existingTile = null;
1082             PeopleSpaceTile updatedTile = null;
1083             try {
1084                 synchronized (mLock) {
1085                     existingTile = getTileForExistingWidgetThrowing(appWidgetId);
1086                     if (existingTile == null) {
1087                         Log.e(TAG, "Matching conversation not found for widget "
1088                                 + appWidgetId);
1089                         continue;
1090                     }
1091                     updatedTile = getTileWithCurrentState(existingTile, entryPoint);
1092                     updateAppWidgetOptionsAndView(appWidgetId, updatedTile);
1093                 }
1094             } catch (PackageManager.NameNotFoundException e) {
1095                 // Delete data for uninstalled widgets.
1096                 Log.e(TAG, "Package no longer found for widget " + appWidgetId, e);
1097                 JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
1098                 if (jobScheduler != null
1099                         && jobScheduler.getPendingJob(PeopleBackupFollowUpJob.JOB_ID) != null) {
1100                     if (DEBUG) {
1101                         Log.d(TAG, "Device was recently restored, wait before deleting storage.");
1102                     }
1103                     continue;
1104                 }
1105                 synchronized (mLock) {
1106                     updateAppWidgetOptionsAndView(appWidgetId, updatedTile);
1107                 }
1108                 deleteWidgets(new int[]{appWidgetId});
1109             }
1110         }
1111     }
1112 
1113     /** Checks the current state of {@code tile} dependencies, modifying fields as necessary. */
1114     @Nullable
getTileWithCurrentState(PeopleSpaceTile tile, String entryPoint)1115     private PeopleSpaceTile getTileWithCurrentState(PeopleSpaceTile tile,
1116             String entryPoint) throws
1117             PackageManager.NameNotFoundException {
1118         PeopleSpaceTile.Builder updatedTile = tile.toBuilder();
1119         switch (entryPoint) {
1120             case NotificationManager
1121                     .ACTION_INTERRUPTION_FILTER_CHANGED:
1122                 updatedTile.setNotificationPolicyState(getNotificationPolicyState());
1123                 break;
1124             case Intent.ACTION_PACKAGES_SUSPENDED:
1125             case Intent.ACTION_PACKAGES_UNSUSPENDED:
1126                 updatedTile.setIsPackageSuspended(getPackageSuspended(tile));
1127                 break;
1128             case Intent.ACTION_MANAGED_PROFILE_AVAILABLE:
1129             case Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE:
1130             case Intent.ACTION_USER_UNLOCKED:
1131                 updatedTile.setIsUserQuieted(getUserQuieted(tile));
1132                 break;
1133             case Intent.ACTION_LOCALE_CHANGED:
1134                 break;
1135             case ACTION_BOOT_COMPLETED:
1136             default:
1137                 updatedTile.setIsUserQuieted(getUserQuieted(tile)).setIsPackageSuspended(
1138                         getPackageSuspended(tile)).setNotificationPolicyState(
1139                         getNotificationPolicyState());
1140         }
1141         return updatedTile.build();
1142     }
1143 
getPackageSuspended(PeopleSpaceTile tile)1144     private boolean getPackageSuspended(PeopleSpaceTile tile) throws
1145             PackageManager.NameNotFoundException {
1146         boolean packageSuspended = !TextUtils.isEmpty(tile.getPackageName())
1147                 && mPackageManager.isPackageSuspended(tile.getPackageName());
1148         if (DEBUG) Log.d(TAG, "Package suspended: " + packageSuspended);
1149         // isPackageSuspended() only throws an exception if the app has been uninstalled, and the
1150         // app data has also been cleared. We want to empty the layout when the app is uninstalled
1151         // regardless of app data clearing, which getApplicationInfoAsUser() handles.
1152         mPackageManager.getApplicationInfoAsUser(
1153                 tile.getPackageName(), PackageManager.GET_META_DATA,
1154                 PeopleSpaceUtils.getUserId(tile));
1155         return packageSuspended;
1156     }
1157 
getUserQuieted(PeopleSpaceTile tile)1158     private boolean getUserQuieted(PeopleSpaceTile tile) {
1159         boolean workProfileQuieted =
1160                 tile.getUserHandle() != null && mUserManager.isQuietModeEnabled(
1161                         tile.getUserHandle());
1162         if (DEBUG) Log.d(TAG, "Work profile quiet: " + workProfileQuieted);
1163         return workProfileQuieted;
1164     }
1165 
getNotificationPolicyState()1166     private int getNotificationPolicyState() {
1167         NotificationManager.Policy policy = mNotificationManager.getNotificationPolicy();
1168         boolean suppressVisualEffects =
1169                 NotificationManager.Policy.areAllVisualEffectsSuppressed(
1170                         policy.suppressedVisualEffects);
1171         int notificationPolicyState = 0;
1172         // If the user sees notifications in DND, we do not need to evaluate the current DND
1173         // state, just always show notifications.
1174         if (!suppressVisualEffects) {
1175             if (DEBUG) Log.d(TAG, "Visual effects not suppressed.");
1176             return PeopleSpaceTile.SHOW_CONVERSATIONS;
1177         }
1178         switch (mNotificationManager.getCurrentInterruptionFilter()) {
1179             case INTERRUPTION_FILTER_ALL:
1180                 if (DEBUG) Log.d(TAG, "All interruptions allowed");
1181                 return PeopleSpaceTile.SHOW_CONVERSATIONS;
1182             case INTERRUPTION_FILTER_PRIORITY:
1183                 if (policy.allowConversations()) {
1184                     if (policy.priorityConversationSenders == CONVERSATION_SENDERS_ANYONE) {
1185                         if (DEBUG) Log.d(TAG, "All conversations allowed");
1186                         // We only show conversations, so we can show everything.
1187                         return PeopleSpaceTile.SHOW_CONVERSATIONS;
1188                     } else if (policy.priorityConversationSenders
1189                             == NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT) {
1190                         if (DEBUG) Log.d(TAG, "Important conversations allowed");
1191                         notificationPolicyState |= PeopleSpaceTile.SHOW_IMPORTANT_CONVERSATIONS;
1192                     }
1193                 }
1194                 if (policy.allowMessages()) {
1195                     switch (policy.allowMessagesFrom()) {
1196                         case ZenModeConfig.SOURCE_CONTACT:
1197                             if (DEBUG) Log.d(TAG, "All contacts allowed");
1198                             notificationPolicyState |= PeopleSpaceTile.SHOW_CONTACTS;
1199                             return notificationPolicyState;
1200                         case ZenModeConfig.SOURCE_STAR:
1201                             if (DEBUG) Log.d(TAG, "Starred contacts allowed");
1202                             notificationPolicyState |= PeopleSpaceTile.SHOW_STARRED_CONTACTS;
1203                             return notificationPolicyState;
1204                         case ZenModeConfig.SOURCE_ANYONE:
1205                         default:
1206                             if (DEBUG) Log.d(TAG, "All messages allowed");
1207                             return PeopleSpaceTile.SHOW_CONVERSATIONS;
1208                     }
1209                 }
1210                 if (notificationPolicyState != 0) {
1211                     if (DEBUG) Log.d(TAG, "Return block state: " + notificationPolicyState);
1212                     return notificationPolicyState;
1213                 }
1214                 // If only alarms or nothing can bypass DND, the tile shouldn't show conversations.
1215             case INTERRUPTION_FILTER_NONE:
1216             case INTERRUPTION_FILTER_ALARMS:
1217             default:
1218                 if (DEBUG) Log.d(TAG, "Block conversations");
1219                 return PeopleSpaceTile.BLOCK_CONVERSATIONS;
1220         }
1221     }
1222 
1223     /**
1224      * Modifies widgets storage after a restore operation, since widget ids get remapped on restore.
1225      * This is guaranteed to run after the PeopleBackupHelper restore operation.
1226      */
remapWidgets(int[] oldWidgetIds, int[] newWidgetIds)1227     public void remapWidgets(int[] oldWidgetIds, int[] newWidgetIds) {
1228         if (DEBUG) {
1229             Log.d(TAG, "Remapping widgets, old: " + Arrays.toString(oldWidgetIds) + ". new: "
1230                     + Arrays.toString(newWidgetIds));
1231         }
1232 
1233         Map<String, String> widgets = new HashMap<>();
1234         for (int i = 0; i < oldWidgetIds.length; i++) {
1235             widgets.put(String.valueOf(oldWidgetIds[i]), String.valueOf(newWidgetIds[i]));
1236         }
1237 
1238         remapWidgetFiles(widgets);
1239         remapSharedFile(widgets);
1240         remapFollowupFile(widgets);
1241 
1242         int[] widgetIds = mAppWidgetManager.getAppWidgetIds(
1243                 new ComponentName(mContext, PeopleSpaceWidgetProvider.class));
1244         Bundle b = new Bundle();
1245         b.putBoolean(AppWidgetManager.OPTION_APPWIDGET_RESTORE_COMPLETED, true);
1246         for (int id : widgetIds) {
1247             if (DEBUG) Log.d(TAG, "Setting widget as restored, widget id:" + id);
1248             mAppWidgetManager.updateAppWidgetOptions(id, b);
1249         }
1250 
1251         updateWidgets(widgetIds);
1252     }
1253 
1254     /** Remaps widget ids in widget specific files. */
remapWidgetFiles(Map<String, String> widgets)1255     public void remapWidgetFiles(Map<String, String> widgets) {
1256         if (DEBUG) Log.d(TAG, "Remapping widget files");
1257         Map<String, PeopleTileKey> remapped = new HashMap<>();
1258         for (Map.Entry<String, String> entry : widgets.entrySet()) {
1259             String from = String.valueOf(entry.getKey());
1260             String to = String.valueOf(entry.getValue());
1261             if (Objects.equals(from, to)) {
1262                 continue;
1263             }
1264 
1265             SharedPreferences src = mContext.getSharedPreferences(from, Context.MODE_PRIVATE);
1266             PeopleTileKey key = SharedPreferencesHelper.getPeopleTileKey(src);
1267             if (PeopleTileKey.isValid(key)) {
1268                 if (DEBUG) {
1269                     Log.d(TAG, "Moving PeopleTileKey: " + key.toString() + " from file: "
1270                             + from + ", to file: " + to);
1271                 }
1272                 remapped.put(to, key);
1273                 SharedPreferencesHelper.clear(src);
1274             } else {
1275                 if (DEBUG) Log.d(TAG, "Widget file has invalid key: " + key);
1276             }
1277         }
1278         for (Map.Entry<String, PeopleTileKey> entry : remapped.entrySet()) {
1279             SharedPreferences dest = mContext.getSharedPreferences(
1280                     entry.getKey(), Context.MODE_PRIVATE);
1281             SharedPreferencesHelper.setPeopleTileKey(dest, entry.getValue());
1282         }
1283     }
1284 
1285     /** Remaps widget ids in default shared storage. */
remapSharedFile(Map<String, String> widgets)1286     public void remapSharedFile(Map<String, String> widgets) {
1287         if (DEBUG) Log.d(TAG, "Remapping shared file");
1288         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
1289         SharedPreferences.Editor editor = sp.edit();
1290         Map<String, ?> all = sp.getAll();
1291         for (Map.Entry<String, ?> entry : all.entrySet()) {
1292             String key = entry.getKey();
1293             PeopleBackupHelper.SharedFileEntryType keyType = getEntryType(entry);
1294             if (DEBUG) Log.d(TAG, "Remapping key:" + key);
1295             switch (keyType) {
1296                 case WIDGET_ID:
1297                     String newId = widgets.get(key);
1298                     if (TextUtils.isEmpty(newId)) {
1299                         Log.w(TAG, "Key is widget id without matching new id, skipping: " + key);
1300                         break;
1301                     }
1302                     if (DEBUG) Log.d(TAG, "Key is widget id: " + key + ", replace with: " + newId);
1303                     try {
1304                         editor.putString(newId, (String) entry.getValue());
1305                     } catch (Exception e) {
1306                         Log.e(TAG, "malformed entry value: " + entry.getValue(), e);
1307                     }
1308                     editor.remove(key);
1309                     break;
1310                 case PEOPLE_TILE_KEY:
1311                 case CONTACT_URI:
1312                     Set<String> oldWidgetIds;
1313                     try {
1314                         oldWidgetIds = (Set<String>) entry.getValue();
1315                     } catch (Exception e) {
1316                         Log.e(TAG, "malformed entry value: " + entry.getValue(), e);
1317                         editor.remove(key);
1318                         break;
1319                     }
1320                     Set<String> newWidgets = getNewWidgets(oldWidgetIds, widgets);
1321                     if (DEBUG) {
1322                         Log.d(TAG, "Key is PeopleTileKey or contact URI: " + key
1323                                 + ", replace values with new ids: " + newWidgets);
1324                     }
1325                     editor.putStringSet(key, newWidgets);
1326                     break;
1327                 case UNKNOWN:
1328                     Log.e(TAG, "Key not identified:" + key);
1329             }
1330         }
1331         editor.apply();
1332     }
1333 
1334     /** Remaps widget ids in follow-up job file. */
remapFollowupFile(Map<String, String> widgets)1335     public void remapFollowupFile(Map<String, String> widgets) {
1336         if (DEBUG) Log.d(TAG, "Remapping follow up file");
1337         SharedPreferences followUp = mContext.getSharedPreferences(
1338                 SHARED_FOLLOW_UP, Context.MODE_PRIVATE);
1339         SharedPreferences.Editor followUpEditor = followUp.edit();
1340         Map<String, ?> followUpAll = followUp.getAll();
1341         for (Map.Entry<String, ?> entry : followUpAll.entrySet()) {
1342             String key = entry.getKey();
1343             Set<String> oldWidgetIds;
1344             try {
1345                 oldWidgetIds = (Set<String>) entry.getValue();
1346             } catch (Exception e) {
1347                 Log.e(TAG, "malformed entry value: " + entry.getValue(), e);
1348                 followUpEditor.remove(key);
1349                 continue;
1350             }
1351             Set<String> newWidgets = getNewWidgets(oldWidgetIds, widgets);
1352             if (DEBUG) {
1353                 Log.d(TAG, "Follow up key: " + key + ", replace with new ids: " + newWidgets);
1354             }
1355             followUpEditor.putStringSet(key, newWidgets);
1356         }
1357         followUpEditor.apply();
1358     }
1359 
getNewWidgets(Set<String> oldWidgets, Map<String, String> widgetsMapping)1360     private Set<String> getNewWidgets(Set<String> oldWidgets, Map<String, String> widgetsMapping) {
1361         return oldWidgets
1362                 .stream()
1363                 .map(widgetsMapping::get)
1364                 .filter(id -> !TextUtils.isEmpty(id))
1365                 .collect(Collectors.toSet());
1366     }
1367 }
1368