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