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