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