/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.people.widget; import static android.Manifest.permission.READ_CONTACTS; import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS; import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.content.Intent.ACTION_BOOT_COMPLETED; import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; import static com.android.systemui.people.NotificationHelper.getContactUri; import static com.android.systemui.people.NotificationHelper.getHighestPriorityNotification; import static com.android.systemui.people.NotificationHelper.shouldFilterOut; import static com.android.systemui.people.NotificationHelper.shouldMatchNotificationByUri; import static com.android.systemui.people.PeopleBackupFollowUpJob.SHARED_FOLLOW_UP; import static com.android.systemui.people.PeopleSpaceUtils.EMPTY_STRING; import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID; import static com.android.systemui.people.PeopleSpaceUtils.PACKAGE_NAME; import static com.android.systemui.people.PeopleSpaceUtils.SHORTCUT_ID; import static com.android.systemui.people.PeopleSpaceUtils.USER_ID; import static com.android.systemui.people.PeopleSpaceUtils.augmentTileFromNotification; import static com.android.systemui.people.PeopleSpaceUtils.getMessagesCount; import static com.android.systemui.people.PeopleSpaceUtils.getNotificationsByUri; import static com.android.systemui.people.PeopleSpaceUtils.removeNotificationFields; import static com.android.systemui.people.widget.PeopleBackupHelper.getEntryType; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.INotificationManager; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Person; import android.app.backup.BackupManager; import android.app.job.JobScheduler; import android.app.people.ConversationChannel; import android.app.people.IPeopleManager; import android.app.people.PeopleManager; import android.app.people.PeopleSpaceTile; import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.preference.PreferenceManager; import android.service.notification.ConversationChannelWrapper; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.text.TextUtils; import android.util.Log; import android.widget.RemoteViews; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLoggerImpl; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.people.NotificationHelper; import com.android.systemui.people.PeopleBackupFollowUpJob; import com.android.systemui.people.PeopleSpaceUtils; import com.android.systemui.people.PeopleTileViewHelper; import com.android.systemui.people.SharedPreferencesHelper; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationListener.NotificationHandler; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.wm.shell.bubbles.Bubbles; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.inject.Inject; /** Manager for People Space widget. */ @SysUISingleton public class PeopleSpaceWidgetManager { private static final String TAG = "PeopleSpaceWidgetMgr"; private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; private final Object mLock = new Object(); private final Context mContext; private LauncherApps mLauncherApps; private AppWidgetManager mAppWidgetManager; private IPeopleManager mIPeopleManager; private SharedPreferences mSharedPrefs; private PeopleManager mPeopleManager; private CommonNotifCollection mNotifCollection; private PackageManager mPackageManager; private INotificationManager mINotificationManager; private Optional mBubblesOptional; private UserManager mUserManager; private PeopleSpaceWidgetManager mManager; private BackupManager mBackupManager; public UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); private NotificationManager mNotificationManager; private BroadcastDispatcher mBroadcastDispatcher; private Executor mBgExecutor; @GuardedBy("mLock") public static Map mListeners = new HashMap<>(); @GuardedBy("mLock") // Map of notification key mapped to widget IDs previously updated by the contact Uri field. // This is required because on notification removal, the contact Uri field is stripped and we // only have the notification key to determine which widget IDs should be updated. private Map> mNotificationKeyToWidgetIdsMatchedByUri = new HashMap<>(); private boolean mRegisteredReceivers; @GuardedBy("mLock") public static Map mTiles = new HashMap<>(); @Inject public PeopleSpaceWidgetManager(Context context, LauncherApps launcherApps, CommonNotifCollection notifCollection, PackageManager packageManager, Optional bubblesOptional, UserManager userManager, NotificationManager notificationManager, BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor) { if (DEBUG) Log.d(TAG, "constructor"); mContext = context; mAppWidgetManager = AppWidgetManager.getInstance(context); mIPeopleManager = IPeopleManager.Stub.asInterface( ServiceManager.getService(Context.PEOPLE_SERVICE)); mLauncherApps = launcherApps; mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); mPeopleManager = context.getSystemService(PeopleManager.class); mNotifCollection = notifCollection; mPackageManager = packageManager; mINotificationManager = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)); mBubblesOptional = bubblesOptional; mUserManager = userManager; mBackupManager = new BackupManager(context); mNotificationManager = notificationManager; mManager = this; mBroadcastDispatcher = broadcastDispatcher; mBgExecutor = bgExecutor; } /** Initializes {@PeopleSpaceWidgetManager}. */ public void init() { synchronized (mLock) { if (!mRegisteredReceivers) { if (DEBUG) Log.d(TAG, "Register receivers"); IntentFilter filter = new IntentFilter(); filter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED); filter.addAction(ACTION_BOOT_COMPLETED); filter.addAction(Intent.ACTION_LOCALE_CHANGED); filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); filter.addAction(Intent.ACTION_PACKAGES_SUSPENDED); filter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED); filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); filter.addAction(Intent.ACTION_USER_UNLOCKED); mBroadcastDispatcher.registerReceiver(mBaseBroadcastReceiver, filter, null /* executor */, UserHandle.ALL); IntentFilter perAppFilter = new IntentFilter(ACTION_PACKAGE_REMOVED); perAppFilter.addAction(ACTION_PACKAGE_ADDED); perAppFilter.addDataScheme("package"); // BroadcastDispatcher doesn't allow data schemes. mContext.registerReceiver(mBaseBroadcastReceiver, perAppFilter); IntentFilter bootComplete = new IntentFilter(ACTION_BOOT_COMPLETED); bootComplete.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); // BroadcastDispatcher doesn't allow priority. mContext.registerReceiver(mBaseBroadcastReceiver, bootComplete); mRegisteredReceivers = true; } } } /** Listener for the shortcut data changes. */ public class TileConversationListener implements PeopleManager.ConversationListener { @Override public void onConversationUpdate(@NonNull ConversationChannel conversation) { if (DEBUG) { Log.d(TAG, "Received updated conversation: " + conversation.getShortcutInfo().getLabel()); } mBgExecutor.execute(() -> updateWidgetsWithConversationChanged(conversation)); } } /** * PeopleSpaceWidgetManager setter used for testing. */ @VisibleForTesting PeopleSpaceWidgetManager(Context context, AppWidgetManager appWidgetManager, IPeopleManager iPeopleManager, PeopleManager peopleManager, LauncherApps launcherApps, CommonNotifCollection notifCollection, PackageManager packageManager, Optional bubblesOptional, UserManager userManager, BackupManager backupManager, INotificationManager iNotificationManager, NotificationManager notificationManager, @Background Executor executor) { mContext = context; mAppWidgetManager = appWidgetManager; mIPeopleManager = iPeopleManager; mPeopleManager = peopleManager; mLauncherApps = launcherApps; mNotifCollection = notifCollection; mPackageManager = packageManager; mBubblesOptional = bubblesOptional; mUserManager = userManager; mBackupManager = backupManager; mINotificationManager = iNotificationManager; mNotificationManager = notificationManager; mManager = this; mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); mBgExecutor = executor; } /** * Updates People Space widgets. */ public void updateWidgets(int[] widgetIds) { mBgExecutor.execute(() -> updateWidgetsInBackground(widgetIds)); } private void updateWidgetsInBackground(int[] widgetIds) { try { if (DEBUG) Log.d(TAG, "updateWidgets called"); if (widgetIds.length == 0) { if (DEBUG) Log.d(TAG, "no widgets to update"); return; } synchronized (mLock) { updateSingleConversationWidgets(widgetIds); } } catch (Exception e) { Log.e(TAG, "failed to update widgets", e); } } /** * Updates {@code appWidgetIds} with their associated conversation stored, handling a * notification being posted or removed. */ public void updateSingleConversationWidgets(int[] appWidgetIds) { Map widgetIdToTile = new HashMap<>(); for (int appWidgetId : appWidgetIds) { if (DEBUG) Log.d(TAG, "Updating widget: " + appWidgetId); PeopleSpaceTile tile = getTileForExistingWidget(appWidgetId); if (tile == null) { Log.e(TAG, "Matching conversation not found for widget " + appWidgetId); } updateAppWidgetOptionsAndView(appWidgetId, tile); widgetIdToTile.put(appWidgetId, tile); if (tile != null) { registerConversationListenerIfNeeded(appWidgetId, new PeopleTileKey(tile)); } } PeopleSpaceUtils.getDataFromContactsOnBackgroundThread( mContext, mManager, widgetIdToTile, appWidgetIds); } /** Updates the current widget view with provided {@link PeopleSpaceTile}. */ private void updateAppWidgetViews(int appWidgetId, PeopleSpaceTile tile, Bundle options) { PeopleTileKey key = getKeyFromStorageByWidgetId(appWidgetId); if (DEBUG) Log.d(TAG, "Widget: " + appWidgetId + " for: " + key.toString()); if (!PeopleTileKey.isValid(key)) { Log.e(TAG, "Invalid tile key updating widget " + appWidgetId); return; } RemoteViews views = PeopleTileViewHelper.createRemoteViews(mContext, tile, appWidgetId, options, key); // Tell the AppWidgetManager to perform an update on the current app widget. if (DEBUG) Log.d(TAG, "Calling update widget for widgetId: " + appWidgetId); mAppWidgetManager.updateAppWidget(appWidgetId, views); } /** Updates tile in app widget options and the current view. */ public void updateAppWidgetOptionsAndViewOptional(int appWidgetId, Optional tile) { if (tile.isPresent()) { updateAppWidgetOptionsAndView(appWidgetId, tile.get()); } } /** Updates tile in app widget options and the current view. */ public void updateAppWidgetOptionsAndView(int appWidgetId, PeopleSpaceTile tile) { if (tile == null) { Log.w(TAG, "Storing null tile for widget " + appWidgetId); } synchronized (mTiles) { mTiles.put(appWidgetId, tile); } Bundle options = mAppWidgetManager.getAppWidgetOptions(appWidgetId); updateAppWidgetViews(appWidgetId, tile, options); } /** * Returns a {@link PeopleSpaceTile} based on the {@code appWidgetId}. * Widget already exists, so fetch {@link PeopleTileKey} from {@link SharedPreferences}. */ @Nullable public PeopleSpaceTile getTileForExistingWidget(int appWidgetId) { try { return getTileForExistingWidgetThrowing(appWidgetId); } catch (Exception e) { Log.e(TAG, "failed to retrieve tile for existing widget " + appWidgetId, e); return null; } } @Nullable private PeopleSpaceTile getTileForExistingWidgetThrowing(int appWidgetId) throws PackageManager.NameNotFoundException { // First, check if tile is cached in memory. PeopleSpaceTile tile; synchronized (mTiles) { tile = mTiles.get(appWidgetId); } if (tile != null) { if (DEBUG) Log.d(TAG, "People Tile is cached for widget: " + appWidgetId); return tile; } // If tile is null, we need to retrieve from persistent storage. if (DEBUG) Log.d(TAG, "Fetching key from sharedPreferences: " + appWidgetId); SharedPreferences widgetSp = mContext.getSharedPreferences( String.valueOf(appWidgetId), Context.MODE_PRIVATE); PeopleTileKey key = new PeopleTileKey( widgetSp.getString(SHORTCUT_ID, EMPTY_STRING), widgetSp.getInt(USER_ID, INVALID_USER_ID), widgetSp.getString(PACKAGE_NAME, EMPTY_STRING)); return getTileFromPersistentStorage(key, appWidgetId, /* supplementFromStorage= */ true); } /** * Returns a {@link PeopleSpaceTile} based on the {@code appWidgetId}. * If a {@link PeopleTileKey} is not provided, fetch one from {@link SharedPreferences}. */ @Nullable public PeopleSpaceTile getTileFromPersistentStorage(PeopleTileKey key, int appWidgetId, boolean supplementFromStorage) throws PackageManager.NameNotFoundException { if (!PeopleTileKey.isValid(key)) { Log.e(TAG, "Invalid tile key finding tile for existing widget " + appWidgetId); return null; } if (mIPeopleManager == null || mLauncherApps == null) { Log.d(TAG, "System services are null"); return null; } try { if (DEBUG) Log.d(TAG, "Retrieving Tile from storage: " + key.toString()); ConversationChannel channel = mIPeopleManager.getConversation( key.getPackageName(), key.getUserId(), key.getShortcutId()); if (channel == null) { if (DEBUG) Log.d(TAG, "Could not retrieve conversation from storage"); return null; } // Get tile from shortcut & conversation storage. PeopleSpaceTile.Builder storedTile = new PeopleSpaceTile.Builder(channel, mLauncherApps); if (storedTile == null) { return storedTile.build(); } // Supplement with our storage. String contactUri = mSharedPrefs.getString(String.valueOf(appWidgetId), null); if (supplementFromStorage && contactUri != null && storedTile.build().getContactUri() == null) { if (DEBUG) Log.d(TAG, "Restore contact uri from storage: " + contactUri); storedTile.setContactUri(Uri.parse(contactUri)); } // Add current state. return getTileWithCurrentState(storedTile.build(), ACTION_BOOT_COMPLETED); } catch (RemoteException e) { Log.e(TAG, "getTileFromPersistentStorage failing for widget " + appWidgetId, e); return null; } } /** * Check if any existing People tiles match the incoming notification change, and store the * change in the tile if so. */ public void updateWidgetsWithNotificationChanged(StatusBarNotification sbn, PeopleSpaceUtils.NotificationAction notificationAction) { if (DEBUG) { if (notificationAction == PeopleSpaceUtils.NotificationAction.POSTED) { Log.d(TAG, "Notification posted, key: " + sbn.getKey()); } else { Log.d(TAG, "Notification removed, key: " + sbn.getKey()); } } if (DEBUG) Log.d(TAG, "Fetching notifications"); Collection notifications = mNotifCollection.getAllNotifs(); mBgExecutor.execute( () -> updateWidgetsWithNotificationChangedInBackground( sbn, notificationAction, notifications)); } private void updateWidgetsWithNotificationChangedInBackground(StatusBarNotification sbn, PeopleSpaceUtils.NotificationAction action, Collection notifications) { try { PeopleTileKey key = new PeopleTileKey( sbn.getShortcutId(), sbn.getUser().getIdentifier(), sbn.getPackageName()); if (!PeopleTileKey.isValid(key)) { if (DEBUG) Log.d(TAG, "Sbn doesn't contain valid PeopleTileKey: " + key.toString()); return; } int[] widgetIds = mAppWidgetManager.getAppWidgetIds( new ComponentName(mContext, PeopleSpaceWidgetProvider.class) ); if (widgetIds.length == 0) { Log.d(TAG, "No app widget ids returned"); return; } synchronized (mLock) { Set tilesUpdated = getMatchingKeyWidgetIds(key); Set tilesUpdatedByUri = getMatchingUriWidgetIds(sbn, action); if (DEBUG) { Log.d(TAG, "Widgets by key to be updated:" + tilesUpdated.toString()); Log.d(TAG, "Widgets by URI to be updated:" + tilesUpdatedByUri.toString()); } tilesUpdated.addAll(tilesUpdatedByUri); updateWidgetIdsBasedOnNotifications(tilesUpdated, notifications); } } catch (Exception e) { Log.e(TAG, "updateWidgetsWithNotificationChangedInBackground failing", e); } } /** Updates {@code widgetIdsToUpdate} with {@code action}. */ private void updateWidgetIdsBasedOnNotifications(Set widgetIdsToUpdate, Collection ungroupedNotifications) { if (widgetIdsToUpdate.isEmpty()) { if (DEBUG) Log.d(TAG, "No widgets to update, returning."); return; } try { Map> groupedNotifications = groupConversationNotifications(ungroupedNotifications); widgetIdsToUpdate .stream() .map(Integer::parseInt) .collect(Collectors.toMap( Function.identity(), id -> getAugmentedTileForExistingWidget(id, groupedNotifications))) .forEach((id, tile) -> updateAppWidgetOptionsAndViewOptional(id, tile)); } catch (Exception e) { Log.e(TAG, "updateWidgetIdsBasedOnNotifications failing", e); } } /** * Augments {@code tile} based on notifications returned from {@code notificationEntryManager}. */ public PeopleSpaceTile augmentTileFromNotificationEntryManager(PeopleSpaceTile tile, Optional appWidgetId) { PeopleTileKey key = new PeopleTileKey(tile); if (DEBUG) { Log.d(TAG, "Augmenting tile from NotificationEntryManager widget: " + key.toString()); } Map> notifications = groupConversationNotifications(mNotifCollection.getAllNotifs()); String contactUri = null; if (tile.getContactUri() != null) { contactUri = tile.getContactUri().toString(); } return augmentTileFromNotifications(tile, key, contactUri, notifications, appWidgetId); } /** Groups active and pending notifications grouped by {@link PeopleTileKey}. */ public Map> groupConversationNotifications( Collection notifications ) { if (DEBUG) Log.d(TAG, "Number of total notifications: " + notifications.size()); Map> groupedNotifications = notifications .stream() .filter(entry -> NotificationHelper.isValid(entry) && NotificationHelper.isMissedCallOrHasContent(entry) && !shouldFilterOut(mBubblesOptional, entry)) .collect(Collectors.groupingBy( PeopleTileKey::new, Collectors.mapping(Function.identity(), Collectors.toSet()))); if (DEBUG) { Log.d(TAG, "Number of grouped conversation notifications keys: " + groupedNotifications.keySet().size()); } return groupedNotifications; } /** Augments {@code tile} based on {@code notifications}, matching {@code contactUri}. */ public PeopleSpaceTile augmentTileFromNotifications(PeopleSpaceTile tile, PeopleTileKey key, String contactUri, Map> notifications, Optional appWidgetId) { if (DEBUG) Log.d(TAG, "Augmenting tile from notifications. Tile key: " + key.toString()); boolean hasReadContactsPermission = mPackageManager.checkPermission(READ_CONTACTS, tile.getPackageName()) == PackageManager.PERMISSION_GRANTED; List notificationsByUri = new ArrayList<>(); if (hasReadContactsPermission) { notificationsByUri = getNotificationsByUri(mPackageManager, contactUri, notifications); if (!notificationsByUri.isEmpty()) { if (DEBUG) { Log.d(TAG, "Number of notifications matched by contact URI: " + notificationsByUri.size()); } } } Set allNotifications = notifications.get(key); if (allNotifications == null) { allNotifications = new HashSet<>(); } if (allNotifications.isEmpty() && notificationsByUri.isEmpty()) { if (DEBUG) Log.d(TAG, "No existing notifications for tile: " + key.toString()); return removeNotificationFields(tile); } // Merge notifications matched by key and by contact URI. allNotifications.addAll(notificationsByUri); if (DEBUG) Log.d(TAG, "Total notifications matching tile: " + allNotifications.size()); int messagesCount = getMessagesCount(allNotifications); NotificationEntry highestPriority = getHighestPriorityNotification(allNotifications); if (DEBUG) Log.d(TAG, "Augmenting tile from notification, key: " + key.toString()); return augmentTileFromNotification(mContext, tile, key, highestPriority, messagesCount, appWidgetId, mBackupManager); } /** Returns an augmented tile for an existing widget. */ @Nullable public Optional getAugmentedTileForExistingWidget(int widgetId, Map> notifications) { if (DEBUG) Log.d(TAG, "Augmenting tile for existing widget: " + widgetId); PeopleSpaceTile tile = getTileForExistingWidget(widgetId); if (tile == null) { Log.w(TAG, "Null tile for existing widget " + widgetId + ", skipping update."); return Optional.empty(); } String contactUriString = mSharedPrefs.getString(String.valueOf(widgetId), null); // Should never be null, but using ofNullable for extra safety. PeopleTileKey key = new PeopleTileKey(tile); if (DEBUG) Log.d(TAG, "Existing widget: " + widgetId + ". Tile key: " + key.toString()); return Optional.ofNullable( augmentTileFromNotifications(tile, key, contactUriString, notifications, Optional.of(widgetId))); } /** Returns stored widgets for the conversation specified. */ public Set getMatchingKeyWidgetIds(PeopleTileKey key) { if (!PeopleTileKey.isValid(key)) { return new HashSet<>(); } return new HashSet<>(mSharedPrefs.getStringSet(key.toString(), new HashSet<>())); } /** * Updates in-memory map of tiles with matched Uris, dependent on the {@code action}. * *

If the notification was added, adds the notification based on the contact Uri within * {@code sbn}. *

If the notification was removed, removes the notification based on the in-memory map of * widgets previously updated by Uri (since the contact Uri is stripped from the {@code sbn}). */ @Nullable private Set getMatchingUriWidgetIds(StatusBarNotification sbn, PeopleSpaceUtils.NotificationAction action) { if (action.equals(PeopleSpaceUtils.NotificationAction.POSTED)) { Set widgetIdsUpdatedByUri = fetchMatchingUriWidgetIds(sbn); if (widgetIdsUpdatedByUri != null && !widgetIdsUpdatedByUri.isEmpty()) { mNotificationKeyToWidgetIdsMatchedByUri.put(sbn.getKey(), widgetIdsUpdatedByUri); return widgetIdsUpdatedByUri; } } else { // Remove the notification on any widgets where the notification was added // purely based on the Uri. Set widgetsPreviouslyUpdatedByUri = mNotificationKeyToWidgetIdsMatchedByUri.remove(sbn.getKey()); if (widgetsPreviouslyUpdatedByUri != null && !widgetsPreviouslyUpdatedByUri.isEmpty()) { return widgetsPreviouslyUpdatedByUri; } } return new HashSet<>(); } /** Fetches widget Ids that match the contact URI in {@code sbn}. */ @Nullable private Set fetchMatchingUriWidgetIds(StatusBarNotification sbn) { // Check if it's a missed call notification if (!shouldMatchNotificationByUri(sbn)) { if (DEBUG) Log.d(TAG, "Should not supplement conversation"); return null; } // Try to get the Contact Uri from the Missed Call notification directly. String contactUri = getContactUri(sbn); if (contactUri == null) { if (DEBUG) Log.d(TAG, "No contact uri"); return null; } // Supplement any tiles with the same Uri. Set storedWidgetIdsByUri = new HashSet<>(mSharedPrefs.getStringSet(contactUri, new HashSet<>())); if (storedWidgetIdsByUri.isEmpty()) { if (DEBUG) Log.d(TAG, "No tiles for contact"); return null; } return storedWidgetIdsByUri; } /** * Update the tiles associated with the incoming conversation update. */ public void updateWidgetsWithConversationChanged(ConversationChannel conversation) { ShortcutInfo info = conversation.getShortcutInfo(); synchronized (mLock) { PeopleTileKey key = new PeopleTileKey( info.getId(), info.getUserId(), info.getPackage()); Set storedWidgetIds = getMatchingKeyWidgetIds(key); for (String widgetIdString : storedWidgetIds) { if (DEBUG) { Log.d(TAG, "Conversation update for widget " + widgetIdString + " , " + info.getLabel()); } updateStorageAndViewWithConversationData(conversation, Integer.parseInt(widgetIdString)); } } } /** * Update {@code appWidgetId} with the new data provided by {@code conversation}. */ private void updateStorageAndViewWithConversationData(ConversationChannel conversation, int appWidgetId) { PeopleSpaceTile storedTile = getTileForExistingWidget(appWidgetId); if (storedTile == null) { if (DEBUG) Log.d(TAG, "Could not find stored tile to add conversation to"); return; } PeopleSpaceTile.Builder updatedTile = storedTile.toBuilder(); ShortcutInfo info = conversation.getShortcutInfo(); Uri uri = null; if (info.getPersons() != null && info.getPersons().length > 0) { Person person = info.getPersons()[0]; uri = person.getUri() == null ? null : Uri.parse(person.getUri()); } CharSequence label = info.getLabel(); if (label != null) { updatedTile.setUserName(label); } Icon icon = PeopleSpaceTile.convertDrawableToIcon(mLauncherApps.getShortcutIconDrawable( info, 0)); if (icon != null) { updatedTile.setUserIcon(icon); } if (DEBUG) Log.d(TAG, "Statuses: " + conversation.getStatuses()); NotificationChannel channel = conversation.getNotificationChannel(); if (channel != null) { if (DEBUG) Log.d(TAG, "Important:" + channel.isImportantConversation()); updatedTile.setIsImportantConversation(channel.isImportantConversation()); } updatedTile .setContactUri(uri) .setStatuses(conversation.getStatuses()) .setLastInteractionTimestamp(conversation.getLastEventTimestamp()); updateAppWidgetOptionsAndView(appWidgetId, updatedTile.build()); } /** * Attaches the manager to the pipeline, making it ready to receive events. Should only be * called once. */ public void attach(NotificationListener listenerService) { if (DEBUG) Log.d(TAG, "attach"); listenerService.addNotificationHandler(mListener); } private final NotificationHandler mListener = new NotificationHandler() { @Override public void onNotificationPosted( StatusBarNotification sbn, NotificationListenerService.RankingMap rankingMap) { updateWidgetsWithNotificationChanged(sbn, PeopleSpaceUtils.NotificationAction.POSTED); } @Override public void onNotificationRemoved( StatusBarNotification sbn, NotificationListenerService.RankingMap rankingMap ) { updateWidgetsWithNotificationChanged(sbn, PeopleSpaceUtils.NotificationAction.REMOVED); } @Override public void onNotificationRemoved( StatusBarNotification sbn, NotificationListenerService.RankingMap rankingMap, int reason) { updateWidgetsWithNotificationChanged(sbn, PeopleSpaceUtils.NotificationAction.REMOVED); } @Override public void onNotificationRankingUpdate( NotificationListenerService.RankingMap rankingMap) { } @Override public void onNotificationsInitialized() { if (DEBUG) Log.d(TAG, "onNotificationsInitialized"); } @Override public void onNotificationChannelModified( String pkgName, UserHandle user, NotificationChannel channel, int modificationType) { if (channel.isConversation()) { mBgExecutor.execute(() -> { if (mUserManager.isUserUnlocked(user)) { updateWidgets(mAppWidgetManager.getAppWidgetIds( new ComponentName(mContext, PeopleSpaceWidgetProvider.class) )); } }); } } }; /** * Checks if this widget has been added externally, and this the first time we are learning * about the widget. If so, the widget adder should have populated options with PeopleTileKey * arguments. */ public void onAppWidgetOptionsChanged(int appWidgetId, Bundle newOptions) { // Check if this widget has been added externally, and this the first time we are // learning about the widget. If so, the widget adder should have populated options with // PeopleTileKey arguments. if (DEBUG) Log.d(TAG, "onAppWidgetOptionsChanged called for widget: " + appWidgetId); PeopleTileKey optionsKey = AppWidgetOptionsHelper.getPeopleTileKeyFromBundle(newOptions); if (PeopleTileKey.isValid(optionsKey)) { if (DEBUG) { Log.d(TAG, "PeopleTileKey was present in Options, shortcutId: " + optionsKey.getShortcutId()); } AppWidgetOptionsHelper.removePeopleTileKey(mAppWidgetManager, appWidgetId); addNewWidget(appWidgetId, optionsKey); } // Update views for new widget dimensions. updateWidgets(new int[]{appWidgetId}); } /** Adds a widget based on {@code key} mapped to {@code appWidgetId}. */ public void addNewWidget(int appWidgetId, PeopleTileKey key) { if (DEBUG) Log.d(TAG, "addNewWidget called with key for appWidgetId: " + appWidgetId); PeopleSpaceTile tile = null; try { tile = getTileFromPersistentStorage(key, appWidgetId, /* supplementFromStorage= */ false); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Cannot add widget " + appWidgetId + " since app was uninstalled"); return; } if (tile == null) { return; } tile = augmentTileFromNotificationEntryManager(tile, Optional.of(appWidgetId)); PeopleTileKey existingKeyIfStored; synchronized (mLock) { existingKeyIfStored = getKeyFromStorageByWidgetId(appWidgetId); } // Delete previous storage if the widget already existed and is just reconfigured. if (PeopleTileKey.isValid(existingKeyIfStored)) { if (DEBUG) Log.d(TAG, "Remove previous storage for widget: " + appWidgetId); deleteWidgets(new int[]{appWidgetId}); } else { // Widget newly added. mUiEventLogger.log( PeopleSpaceUtils.PeopleSpaceWidgetEvent.PEOPLE_SPACE_WIDGET_ADDED); } synchronized (mLock) { if (DEBUG) Log.d(TAG, "Add storage for : " + key.toString()); PeopleSpaceUtils.setSharedPreferencesStorageForTile(mContext, key, appWidgetId, tile.getContactUri(), mBackupManager); } if (DEBUG) Log.d(TAG, "Ensure listener is registered for widget: " + appWidgetId); registerConversationListenerIfNeeded(appWidgetId, key); try { if (DEBUG) Log.d(TAG, "Caching shortcut for PeopleTile: " + key.toString()); mLauncherApps.cacheShortcuts(tile.getPackageName(), Collections.singletonList(tile.getId()), tile.getUserHandle(), LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS); } catch (Exception e) { Log.w(TAG, "failed to cache shortcut for widget " + appWidgetId, e); } PeopleSpaceTile finalTile = tile; mBgExecutor.execute( () -> updateAppWidgetOptionsAndView(appWidgetId, finalTile)); } /** Registers a conversation listener for {@code appWidgetId} if not already registered. */ public void registerConversationListenerIfNeeded(int widgetId, PeopleTileKey key) { // Retrieve storage needed for registration. if (!PeopleTileKey.isValid(key)) { Log.w(TAG, "Invalid tile key registering listener for widget " + widgetId); return; } TileConversationListener newListener = new TileConversationListener(); synchronized (mListeners) { if (mListeners.containsKey(key)) { if (DEBUG) Log.d(TAG, "Already registered listener"); return; } if (DEBUG) Log.d(TAG, "Register listener for " + widgetId + " with " + key.toString()); mListeners.put(key, newListener); } mPeopleManager.registerConversationListener(key.getPackageName(), key.getUserId(), key.getShortcutId(), newListener, mContext.getMainExecutor()); } /** * Attempts to get a key from storage for {@code widgetId}, returning null if an invalid key is * found. */ private PeopleTileKey getKeyFromStorageByWidgetId(int widgetId) { SharedPreferences widgetSp = mContext.getSharedPreferences(String.valueOf(widgetId), Context.MODE_PRIVATE); PeopleTileKey key = new PeopleTileKey( widgetSp.getString(SHORTCUT_ID, EMPTY_STRING), widgetSp.getInt(USER_ID, INVALID_USER_ID), widgetSp.getString(PACKAGE_NAME, EMPTY_STRING)); return key; } /** Deletes all storage, listeners, and caching for {@code appWidgetIds}. */ public void deleteWidgets(int[] appWidgetIds) { for (int widgetId : appWidgetIds) { if (DEBUG) Log.d(TAG, "Widget removed: " + widgetId); mUiEventLogger.log(PeopleSpaceUtils.PeopleSpaceWidgetEvent.PEOPLE_SPACE_WIDGET_DELETED); // Retrieve storage needed for widget deletion. PeopleTileKey key; Set storedWidgetIdsForKey; String contactUriString; synchronized (mLock) { SharedPreferences widgetSp = mContext.getSharedPreferences(String.valueOf(widgetId), Context.MODE_PRIVATE); key = new PeopleTileKey( widgetSp.getString(SHORTCUT_ID, null), widgetSp.getInt(USER_ID, INVALID_USER_ID), widgetSp.getString(PACKAGE_NAME, null)); if (!PeopleTileKey.isValid(key)) { Log.e(TAG, "Invalid tile key trying to remove widget " + widgetId); return; } storedWidgetIdsForKey = new HashSet<>( mSharedPrefs.getStringSet(key.toString(), new HashSet<>())); contactUriString = mSharedPrefs.getString(String.valueOf(widgetId), null); } synchronized (mLock) { PeopleSpaceUtils.removeSharedPreferencesStorageForTile(mContext, key, widgetId, contactUriString); } // If another tile with the conversation is still stored, we need to keep the listener. if (DEBUG) Log.d(TAG, "Stored widget IDs: " + storedWidgetIdsForKey.toString()); if (storedWidgetIdsForKey.contains(String.valueOf(widgetId)) && storedWidgetIdsForKey.size() == 1) { if (DEBUG) Log.d(TAG, "Remove caching and listener"); unregisterConversationListener(key, widgetId); uncacheConversationShortcut(key); } } } /** Unregisters the conversation listener for {@code appWidgetId}. */ private void unregisterConversationListener(PeopleTileKey key, int appWidgetId) { TileConversationListener registeredListener; synchronized (mListeners) { registeredListener = mListeners.get(key); if (registeredListener == null) { if (DEBUG) Log.d(TAG, "Cannot find listener to unregister"); return; } if (DEBUG) { Log.d(TAG, "Unregister listener for " + appWidgetId + " with " + key.toString()); } mListeners.remove(key); } mPeopleManager.unregisterConversationListener(registeredListener); } /** Uncaches the conversation shortcut. */ private void uncacheConversationShortcut(PeopleTileKey key) { try { if (DEBUG) Log.d(TAG, "Uncaching shortcut for PeopleTile: " + key.getShortcutId()); mLauncherApps.uncacheShortcuts(key.getPackageName(), Collections.singletonList(key.getShortcutId()), UserHandle.of(key.getUserId()), LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS); } catch (Exception e) { Log.d(TAG, "failed to uncache shortcut", e); } } /** * Builds a request to pin a People Tile app widget, with a preview and storing necessary * information as the callback. */ public boolean requestPinAppWidget(ShortcutInfo shortcutInfo, Bundle options) { if (DEBUG) Log.d(TAG, "Requesting pin widget, shortcutId: " + shortcutInfo.getId()); RemoteViews widgetPreview = getPreview(shortcutInfo.getId(), shortcutInfo.getUserHandle(), shortcutInfo.getPackage(), options); if (widgetPreview == null) { Log.w(TAG, "Skipping pinning widget: no tile for shortcutId: " + shortcutInfo.getId()); return false; } Bundle extras = new Bundle(); extras.putParcelable(AppWidgetManager.EXTRA_APPWIDGET_PREVIEW, widgetPreview); PendingIntent successCallback = PeopleSpaceWidgetPinnedReceiver.getPendingIntent(mContext, shortcutInfo); ComponentName componentName = new ComponentName(mContext, PeopleSpaceWidgetProvider.class); return mAppWidgetManager.requestPinAppWidget(componentName, extras, successCallback); } /** Returns a list of map entries corresponding to user's priority conversations. */ @NonNull public List getPriorityTiles() throws Exception { List conversations = mINotificationManager.getConversations(true).getList(); // Add priority conversations to tiles list. Stream priorityConversations = conversations.stream() .filter(c -> c.getNotificationChannel() != null && c.getNotificationChannel().isImportantConversation()) .map(c -> c.getShortcutInfo()); List priorityTiles = PeopleSpaceUtils.getSortedTiles(mIPeopleManager, mLauncherApps, mUserManager, priorityConversations); return priorityTiles; } /** Returns a list of map entries corresponding to user's recent conversations. */ @NonNull public List getRecentTiles() throws Exception { if (DEBUG) Log.d(TAG, "Add recent conversations"); List conversations = mINotificationManager.getConversations(false).getList(); Stream nonPriorityConversations = conversations.stream() .filter(c -> c.getNotificationChannel() == null || !c.getNotificationChannel().isImportantConversation()) .map(c -> c.getShortcutInfo()); List recentConversationsList = mIPeopleManager.getRecentConversations().getList(); Stream recentConversations = recentConversationsList .stream() .map(c -> c.getShortcutInfo()); Stream mergedStream = Stream.concat(nonPriorityConversations, recentConversations); List recentTiles = PeopleSpaceUtils.getSortedTiles(mIPeopleManager, mLauncherApps, mUserManager, mergedStream); return recentTiles; } /** * Returns a {@link RemoteViews} preview of a Conversation's People Tile. Returns null if one * is not available. */ public RemoteViews getPreview(String shortcutId, UserHandle userHandle, String packageName, Bundle options) { PeopleSpaceTile tile; ConversationChannel channel; try { channel = mIPeopleManager.getConversation( packageName, userHandle.getIdentifier(), shortcutId); tile = PeopleSpaceUtils.getTile(channel, mLauncherApps); } catch (Exception e) { Log.w(TAG, "failed to get conversation or tile", e); return null; } if (tile == null) { if (DEBUG) Log.i(TAG, "No tile was returned"); return null; } PeopleSpaceTile augmentedTile = augmentTileFromNotificationEntryManager(tile, Optional.empty()); if (DEBUG) Log.i(TAG, "Returning tile preview for shortcutId: " + shortcutId); return PeopleTileViewHelper.createRemoteViews(mContext, augmentedTile, 0, options, new PeopleTileKey(augmentedTile)); } protected final BroadcastReceiver mBaseBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) Log.d(TAG, "Update widgets from: " + intent.getAction()); mBgExecutor.execute(() -> updateWidgetsFromBroadcastInBackground(intent.getAction())); } }; /** Updates any app widget to the current state, triggered by a broadcast update. */ @VisibleForTesting void updateWidgetsFromBroadcastInBackground(String entryPoint) { int[] appWidgetIds = mAppWidgetManager.getAppWidgetIds( new ComponentName(mContext, PeopleSpaceWidgetProvider.class)); if (appWidgetIds == null) { return; } for (int appWidgetId : appWidgetIds) { if (DEBUG) Log.d(TAG, "Updating widget from broadcast, widget id: " + appWidgetId); PeopleSpaceTile existingTile = null; PeopleSpaceTile updatedTile = null; try { synchronized (mLock) { existingTile = getTileForExistingWidgetThrowing(appWidgetId); if (existingTile == null) { Log.e(TAG, "Matching conversation not found for widget " + appWidgetId); continue; } updatedTile = getTileWithCurrentState(existingTile, entryPoint); updateAppWidgetOptionsAndView(appWidgetId, updatedTile); } } catch (PackageManager.NameNotFoundException e) { // Delete data for uninstalled widgets. Log.e(TAG, "Package no longer found for widget " + appWidgetId, e); JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class); if (jobScheduler != null && jobScheduler.getPendingJob(PeopleBackupFollowUpJob.JOB_ID) != null) { if (DEBUG) { Log.d(TAG, "Device was recently restored, wait before deleting storage."); } continue; } synchronized (mLock) { updateAppWidgetOptionsAndView(appWidgetId, updatedTile); } deleteWidgets(new int[]{appWidgetId}); } } } /** Checks the current state of {@code tile} dependencies, modifying fields as necessary. */ @Nullable private PeopleSpaceTile getTileWithCurrentState(PeopleSpaceTile tile, String entryPoint) throws PackageManager.NameNotFoundException { PeopleSpaceTile.Builder updatedTile = tile.toBuilder(); switch (entryPoint) { case NotificationManager .ACTION_INTERRUPTION_FILTER_CHANGED: updatedTile.setNotificationPolicyState(getNotificationPolicyState()); break; case Intent.ACTION_PACKAGES_SUSPENDED: case Intent.ACTION_PACKAGES_UNSUSPENDED: updatedTile.setIsPackageSuspended(getPackageSuspended(tile)); break; case Intent.ACTION_MANAGED_PROFILE_AVAILABLE: case Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE: case Intent.ACTION_USER_UNLOCKED: updatedTile.setIsUserQuieted(getUserQuieted(tile)); break; case Intent.ACTION_LOCALE_CHANGED: break; case ACTION_BOOT_COMPLETED: default: updatedTile.setIsUserQuieted(getUserQuieted(tile)).setIsPackageSuspended( getPackageSuspended(tile)).setNotificationPolicyState( getNotificationPolicyState()); } return updatedTile.build(); } private boolean getPackageSuspended(PeopleSpaceTile tile) throws PackageManager.NameNotFoundException { boolean packageSuspended = !TextUtils.isEmpty(tile.getPackageName()) && mPackageManager.isPackageSuspended(tile.getPackageName()); if (DEBUG) Log.d(TAG, "Package suspended: " + packageSuspended); // isPackageSuspended() only throws an exception if the app has been uninstalled, and the // app data has also been cleared. We want to empty the layout when the app is uninstalled // regardless of app data clearing, which getApplicationInfoAsUser() handles. mPackageManager.getApplicationInfoAsUser( tile.getPackageName(), PackageManager.GET_META_DATA, PeopleSpaceUtils.getUserId(tile)); return packageSuspended; } private boolean getUserQuieted(PeopleSpaceTile tile) { boolean workProfileQuieted = tile.getUserHandle() != null && mUserManager.isQuietModeEnabled( tile.getUserHandle()); if (DEBUG) Log.d(TAG, "Work profile quiet: " + workProfileQuieted); return workProfileQuieted; } private int getNotificationPolicyState() { NotificationManager.Policy policy = mNotificationManager.getNotificationPolicy(); boolean suppressVisualEffects = NotificationManager.Policy.areAllVisualEffectsSuppressed( policy.suppressedVisualEffects); int notificationPolicyState = 0; // If the user sees notifications in DND, we do not need to evaluate the current DND // state, just always show notifications. if (!suppressVisualEffects) { if (DEBUG) Log.d(TAG, "Visual effects not suppressed."); return PeopleSpaceTile.SHOW_CONVERSATIONS; } switch (mNotificationManager.getCurrentInterruptionFilter()) { case INTERRUPTION_FILTER_ALL: if (DEBUG) Log.d(TAG, "All interruptions allowed"); return PeopleSpaceTile.SHOW_CONVERSATIONS; case INTERRUPTION_FILTER_PRIORITY: if (policy.allowConversations()) { if (policy.priorityConversationSenders == CONVERSATION_SENDERS_ANYONE) { if (DEBUG) Log.d(TAG, "All conversations allowed"); // We only show conversations, so we can show everything. return PeopleSpaceTile.SHOW_CONVERSATIONS; } else if (policy.priorityConversationSenders == NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT) { if (DEBUG) Log.d(TAG, "Important conversations allowed"); notificationPolicyState |= PeopleSpaceTile.SHOW_IMPORTANT_CONVERSATIONS; } } if (policy.allowMessages()) { switch (policy.allowMessagesFrom()) { case ZenModeConfig.SOURCE_CONTACT: if (DEBUG) Log.d(TAG, "All contacts allowed"); notificationPolicyState |= PeopleSpaceTile.SHOW_CONTACTS; return notificationPolicyState; case ZenModeConfig.SOURCE_STAR: if (DEBUG) Log.d(TAG, "Starred contacts allowed"); notificationPolicyState |= PeopleSpaceTile.SHOW_STARRED_CONTACTS; return notificationPolicyState; case ZenModeConfig.SOURCE_ANYONE: default: if (DEBUG) Log.d(TAG, "All messages allowed"); return PeopleSpaceTile.SHOW_CONVERSATIONS; } } if (notificationPolicyState != 0) { if (DEBUG) Log.d(TAG, "Return block state: " + notificationPolicyState); return notificationPolicyState; } // If only alarms or nothing can bypass DND, the tile shouldn't show conversations. case INTERRUPTION_FILTER_NONE: case INTERRUPTION_FILTER_ALARMS: default: if (DEBUG) Log.d(TAG, "Block conversations"); return PeopleSpaceTile.BLOCK_CONVERSATIONS; } } /** * Modifies widgets storage after a restore operation, since widget ids get remapped on restore. * This is guaranteed to run after the PeopleBackupHelper restore operation. */ public void remapWidgets(int[] oldWidgetIds, int[] newWidgetIds) { if (DEBUG) { Log.d(TAG, "Remapping widgets, old: " + Arrays.toString(oldWidgetIds) + ". new: " + Arrays.toString(newWidgetIds)); } Map widgets = new HashMap<>(); for (int i = 0; i < oldWidgetIds.length; i++) { widgets.put(String.valueOf(oldWidgetIds[i]), String.valueOf(newWidgetIds[i])); } remapWidgetFiles(widgets); remapSharedFile(widgets); remapFollowupFile(widgets); int[] widgetIds = mAppWidgetManager.getAppWidgetIds( new ComponentName(mContext, PeopleSpaceWidgetProvider.class)); Bundle b = new Bundle(); b.putBoolean(AppWidgetManager.OPTION_APPWIDGET_RESTORE_COMPLETED, true); for (int id : widgetIds) { if (DEBUG) Log.d(TAG, "Setting widget as restored, widget id:" + id); mAppWidgetManager.updateAppWidgetOptions(id, b); } updateWidgets(widgetIds); } /** Remaps widget ids in widget specific files. */ public void remapWidgetFiles(Map widgets) { if (DEBUG) Log.d(TAG, "Remapping widget files"); Map remapped = new HashMap<>(); for (Map.Entry entry : widgets.entrySet()) { String from = String.valueOf(entry.getKey()); String to = String.valueOf(entry.getValue()); if (Objects.equals(from, to)) { continue; } SharedPreferences src = mContext.getSharedPreferences(from, Context.MODE_PRIVATE); PeopleTileKey key = SharedPreferencesHelper.getPeopleTileKey(src); if (PeopleTileKey.isValid(key)) { if (DEBUG) { Log.d(TAG, "Moving PeopleTileKey: " + key.toString() + " from file: " + from + ", to file: " + to); } remapped.put(to, key); SharedPreferencesHelper.clear(src); } else { if (DEBUG) Log.d(TAG, "Widget file has invalid key: " + key); } } for (Map.Entry entry : remapped.entrySet()) { SharedPreferences dest = mContext.getSharedPreferences( entry.getKey(), Context.MODE_PRIVATE); SharedPreferencesHelper.setPeopleTileKey(dest, entry.getValue()); } } /** Remaps widget ids in default shared storage. */ public void remapSharedFile(Map widgets) { if (DEBUG) Log.d(TAG, "Remapping shared file"); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); SharedPreferences.Editor editor = sp.edit(); Map all = sp.getAll(); for (Map.Entry entry : all.entrySet()) { String key = entry.getKey(); PeopleBackupHelper.SharedFileEntryType keyType = getEntryType(entry); if (DEBUG) Log.d(TAG, "Remapping key:" + key); switch (keyType) { case WIDGET_ID: String newId = widgets.get(key); if (TextUtils.isEmpty(newId)) { Log.w(TAG, "Key is widget id without matching new id, skipping: " + key); break; } if (DEBUG) Log.d(TAG, "Key is widget id: " + key + ", replace with: " + newId); try { editor.putString(newId, (String) entry.getValue()); } catch (Exception e) { Log.e(TAG, "malformed entry value: " + entry.getValue(), e); } editor.remove(key); break; case PEOPLE_TILE_KEY: case CONTACT_URI: Set oldWidgetIds; try { oldWidgetIds = (Set) entry.getValue(); } catch (Exception e) { Log.e(TAG, "malformed entry value: " + entry.getValue(), e); editor.remove(key); break; } Set newWidgets = getNewWidgets(oldWidgetIds, widgets); if (DEBUG) { Log.d(TAG, "Key is PeopleTileKey or contact URI: " + key + ", replace values with new ids: " + newWidgets); } editor.putStringSet(key, newWidgets); break; case UNKNOWN: Log.e(TAG, "Key not identified:" + key); } } editor.apply(); } /** Remaps widget ids in follow-up job file. */ public void remapFollowupFile(Map widgets) { if (DEBUG) Log.d(TAG, "Remapping follow up file"); SharedPreferences followUp = mContext.getSharedPreferences( SHARED_FOLLOW_UP, Context.MODE_PRIVATE); SharedPreferences.Editor followUpEditor = followUp.edit(); Map followUpAll = followUp.getAll(); for (Map.Entry entry : followUpAll.entrySet()) { String key = entry.getKey(); Set oldWidgetIds; try { oldWidgetIds = (Set) entry.getValue(); } catch (Exception e) { Log.e(TAG, "malformed entry value: " + entry.getValue(), e); followUpEditor.remove(key); continue; } Set newWidgets = getNewWidgets(oldWidgetIds, widgets); if (DEBUG) { Log.d(TAG, "Follow up key: " + key + ", replace with new ids: " + newWidgets); } followUpEditor.putStringSet(key, newWidgets); } followUpEditor.apply(); } private Set getNewWidgets(Set oldWidgets, Map widgetsMapping) { return oldWidgets .stream() .map(widgetsMapping::get) .filter(id -> !TextUtils.isEmpty(id)) .collect(Collectors.toSet()); } }