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