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