/*
 * 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.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
import static android.appwidget.flags.Flags.generatedPreviews;
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.appwidget.AppWidgetProviderInfo;
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.Trace;
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.util.SparseBooleanArray;
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.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.Dumpable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
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.res.R;
import com.android.systemui.settings.UserTracker;
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.io.PrintWriter;
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 implements Dumpable {

    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 Optional<AppWidgetManager> mAppWidgetManagerOptional;
    private IPeopleManager mIPeopleManager;
    private SharedPreferences mSharedPrefs;
    private PeopleManager mPeopleManager;
    private CommonNotifCollection mNotifCollection;
    private PackageManager mPackageManager;
    private INotificationManager mINotificationManager;
    private Optional<Bubbles> 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<PeopleTileKey, TileConversationListener>
            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<String, Set<String>> mNotificationKeyToWidgetIdsMatchedByUri = new HashMap<>();
    private boolean mRegisteredReceivers;

    @GuardedBy("mLock")
    public static Map<Integer, PeopleSpaceTile> mTiles = new HashMap<>();

    @NonNull private final UserTracker mUserTracker;
    @NonNull private final SparseBooleanArray mUpdatedPreviews = new SparseBooleanArray();
    @NonNull private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
            new KeyguardUpdateMonitorCallback() {
                @Override
                public void onUserUnlocked() {
                    if (DEBUG) {
                        Log.d(TAG, "onUserUnlocked " + mUserTracker.getUserId());
                    }
                    updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
                }
            };

    @Inject
    public PeopleSpaceWidgetManager(Context context,
            Optional<AppWidgetManager> appWidgetManagerOptional,
            LauncherApps launcherApps, CommonNotifCollection notifCollection,
            PackageManager packageManager, Optional<Bubbles> bubblesOptional,
            UserManager userManager, NotificationManager notificationManager,
            BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor,
            DumpManager dumpManager, @NonNull UserTracker userTracker,
            @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor) {
        if (DEBUG) Log.d(TAG, "constructor");
        mContext = context;
        mAppWidgetManagerOptional = appWidgetManagerOptional;
        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;
        dumpManager.registerNormalDumpable(TAG, this);
        mUserTracker = userTracker;
        keyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
    }

    /** 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,
            Optional<AppWidgetManager> appWidgetManager, IPeopleManager iPeopleManager,
            PeopleManager peopleManager, LauncherApps launcherApps,
            CommonNotifCollection notifCollection, PackageManager packageManager,
            Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager,
            INotificationManager iNotificationManager, NotificationManager notificationManager,
            @Background Executor executor, UserTracker userTracker) {
        mContext = context;
        mAppWidgetManagerOptional = 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;
        mUserTracker = userTracker;
    }

    /**
     * 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<Integer, PeopleSpaceTile> 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) {
        if (mAppWidgetManagerOptional.isEmpty()) {
            return;
        }

        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);
        mAppWidgetManagerOptional.get().updateAppWidget(appWidgetId, views);
    }

    /** Updates tile in app widget options and the current view. */
    public void updateAppWidgetOptionsAndViewOptional(int appWidgetId,
            Optional<PeopleSpaceTile> 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 (mAppWidgetManagerOptional.isEmpty()) {
            return;
        }

        if (tile == null) {
            Log.w(TAG, "Storing null tile for widget " + appWidgetId);
        }
        synchronized (mTiles) {
            mTiles.put(appWidgetId, tile);
        }
        Bundle options = mAppWidgetManagerOptional.get().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<NotificationEntry> notifications = mNotifCollection.getAllNotifs();
        mBgExecutor.execute(
                () -> updateWidgetsWithNotificationChangedInBackground(
                        sbn, notificationAction, notifications));
    }

    private void updateWidgetsWithNotificationChangedInBackground(StatusBarNotification sbn,
            PeopleSpaceUtils.NotificationAction action,
            Collection<NotificationEntry> notifications) {
        if (mAppWidgetManagerOptional.isEmpty()) {
            return;
        }

        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 = mAppWidgetManagerOptional.get().getAppWidgetIds(
                    new ComponentName(mContext, PeopleSpaceWidgetProvider.class)
            );
            if (widgetIds.length == 0) {
                Log.d(TAG, "No app widget ids returned");
                return;
            }
            synchronized (mLock) {
                Set<String> tilesUpdated = getMatchingKeyWidgetIds(key);
                Set<String> 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<String> widgetIdsToUpdate,
            Collection<NotificationEntry> ungroupedNotifications) {
        if (widgetIdsToUpdate.isEmpty()) {
            if (DEBUG) Log.d(TAG, "No widgets to update, returning.");
            return;
        }
        try {
            Map<PeopleTileKey, Set<NotificationEntry>> 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<Integer> appWidgetId) {
        PeopleTileKey key = new PeopleTileKey(tile);
        if (DEBUG) {
            Log.d(TAG,
                    "Augmenting tile from NotificationEntryManager widget: " + key.toString());
        }
        Map<PeopleTileKey, Set<NotificationEntry>> 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<PeopleTileKey, Set<NotificationEntry>> groupConversationNotifications(
            Collection<NotificationEntry> notifications
    ) {
        if (DEBUG) Log.d(TAG, "Number of total notifications: " + notifications.size());
        Map<PeopleTileKey, Set<NotificationEntry>> 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<PeopleTileKey, Set<NotificationEntry>> notifications,
            Optional<Integer> 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<NotificationEntry> 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<NotificationEntry> 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<PeopleSpaceTile> getAugmentedTileForExistingWidget(int widgetId,
            Map<PeopleTileKey, Set<NotificationEntry>> 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<String> 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}.
     *
     * <p>If the notification was added, adds the notification based on the contact Uri within
     * {@code sbn}.
     * <p>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<String> getMatchingUriWidgetIds(StatusBarNotification sbn,
            PeopleSpaceUtils.NotificationAction action) {
        if (action.equals(PeopleSpaceUtils.NotificationAction.POSTED)) {
            Set<String> 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<String> 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<String> 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<String> 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<String> 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 (mAppWidgetManagerOptional.isEmpty()) {
                return;
            }

            if (channel.isConversation()) {
                mBgExecutor.execute(() -> {
                    if (mUserManager.isUserUnlocked(user)) {
                        updateWidgets(mAppWidgetManagerOptional.get().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);
        if (mAppWidgetManagerOptional.isEmpty()) {
            return;
        }

        PeopleTileKey optionsKey = AppWidgetOptionsHelper.getPeopleTileKeyFromBundle(newOptions);
        if (PeopleTileKey.isValid(optionsKey)) {
            if (DEBUG) {
                Log.d(TAG, "PeopleTileKey was present in Options, shortcutId: "
                        + optionsKey.getShortcutId());
            }
            AppWidgetOptionsHelper.removePeopleTileKey(mAppWidgetManagerOptional.get(),
                    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<String> 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());

        if (mAppWidgetManagerOptional.isEmpty()) {
            return false;
        }

        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 mAppWidgetManagerOptional.get().requestPinAppWidget(componentName, extras,
                successCallback);
    }

    /** Returns a list of map entries corresponding to user's priority conversations. */
    @NonNull
    public List<PeopleSpaceTile> getPriorityTiles()
            throws Exception {
        List<ConversationChannelWrapper> conversations =
                mINotificationManager.getConversations(true).getList();
        // Add priority conversations to tiles list.
        Stream<ShortcutInfo> priorityConversations = conversations.stream()
                .filter(c -> c.getNotificationChannel() != null
                        && c.getNotificationChannel().isImportantConversation())
                .map(c -> c.getShortcutInfo());
        List<PeopleSpaceTile> priorityTiles = PeopleSpaceUtils.getSortedTiles(mIPeopleManager,
                mLauncherApps, mUserManager,
                priorityConversations);
        return priorityTiles;
    }

    /** Returns a list of map entries corresponding to user's recent conversations. */
    @NonNull
    public List<PeopleSpaceTile> getRecentTiles()
            throws Exception {
        if (DEBUG) Log.d(TAG, "Add recent conversations");
        List<ConversationChannelWrapper> conversations =
                mINotificationManager.getConversations(false).getList();
        Stream<ShortcutInfo> nonPriorityConversations = conversations.stream()
                .filter(c -> c.getNotificationChannel() == null
                        || !c.getNotificationChannel().isImportantConversation())
                .map(c -> c.getShortcutInfo());

        List<ConversationChannel> recentConversationsList =
                mIPeopleManager.getRecentConversations().getList();
        Stream<ShortcutInfo> recentConversations = recentConversationsList
                .stream()
                .map(c -> c.getShortcutInfo());

        Stream<ShortcutInfo> mergedStream = Stream.concat(nonPriorityConversations,
                recentConversations);
        List<PeopleSpaceTile> 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) {
        if (mAppWidgetManagerOptional.isEmpty()) {
            return;
        }

        int[] appWidgetIds = mAppWidgetManagerOptional.get().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<String, String> 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);

        if (mAppWidgetManagerOptional.isEmpty()) {
            return;
        }

        int[] widgetIds = mAppWidgetManagerOptional.get().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);
            mAppWidgetManagerOptional.get().updateAppWidgetOptions(id, b);
        }

        updateWidgets(widgetIds);
    }

    /** Remaps widget ids in widget specific files. */
    public void remapWidgetFiles(Map<String, String> widgets) {
        if (DEBUG) Log.d(TAG, "Remapping widget files");
        Map<String, PeopleTileKey> remapped = new HashMap<>();
        for (Map.Entry<String, String> 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<String, PeopleTileKey> 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<String, String> widgets) {
        if (DEBUG) Log.d(TAG, "Remapping shared file");
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
        SharedPreferences.Editor editor = sp.edit();
        Map<String, ?> all = sp.getAll();
        for (Map.Entry<String, ?> 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<String> oldWidgetIds;
                    try {
                        oldWidgetIds = (Set<String>) entry.getValue();
                    } catch (Exception e) {
                        Log.e(TAG, "malformed entry value: " + entry.getValue(), e);
                        editor.remove(key);
                        break;
                    }
                    Set<String> 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<String, String> 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<String, ?> followUpAll = followUp.getAll();
        for (Map.Entry<String, ?> entry : followUpAll.entrySet()) {
            String key = entry.getKey();
            Set<String> oldWidgetIds;
            try {
                oldWidgetIds = (Set<String>) entry.getValue();
            } catch (Exception e) {
                Log.e(TAG, "malformed entry value: " + entry.getValue(), e);
                followUpEditor.remove(key);
                continue;
            }
            Set<String> 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<String> getNewWidgets(Set<String> oldWidgets, Map<String, String> widgetsMapping) {
        return oldWidgets
                .stream()
                .map(widgetsMapping::get)
                .filter(id -> !TextUtils.isEmpty(id))
                .collect(Collectors.toSet());
    }

    @Override
    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_APP, TAG + ".dump");
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
        Map<String, ?> all = sp.getAll();
        pw.println("People widget list:");
        for (Map.Entry<String, ?> entry : all.entrySet()) {
            String key = entry.getKey();
            PeopleBackupHelper.SharedFileEntryType keyType = getEntryType(entry);
            switch (keyType) {
                case WIDGET_ID:
                    SharedPreferences widgetSp = mContext.getSharedPreferences(key,
                            Context.MODE_PRIVATE);
                    pw.print("People widget (valid) [");
                    pw.print(key);
                    pw.print("] shortcut id: \"");
                    pw.print(widgetSp.getString(SHORTCUT_ID, EMPTY_STRING));
                    pw.print("\", user id: ");
                    pw.print(widgetSp.getInt(USER_ID, INVALID_USER_ID));
                    pw.print(", package: ");
                    pw.println(widgetSp.getString(PACKAGE_NAME, EMPTY_STRING));
                    break;
                case PEOPLE_TILE_KEY:
                case CONTACT_URI:
                    pw.print("Extra data [");
                    pw.print(key);
                    pw.print(" : ");
                    pw.print((Set<String>) entry.getValue());
                    pw.println("]");
                    break;
            }
        }

        Trace.traceEnd(Trace.TRACE_TAG_APP);
    }

    @VisibleForTesting
    void updateGeneratedPreviewForUser(UserHandle user) {
        if (!generatedPreviews() || mUpdatedPreviews.get(user.getIdentifier())
                || !mUserManager.isUserUnlocked(user) || mAppWidgetManagerOptional.isEmpty()) {
            return;
        }

        // The widget provider may be disabled on SystemUI implementers, e.g. TvSystemUI.
        ComponentName provider = new ComponentName(mContext, PeopleSpaceWidgetProvider.class);
        List<AppWidgetProviderInfo> infos =
                mAppWidgetManagerOptional.get().getInstalledProvidersForPackage(
                        mContext.getPackageName(), user);
        if (infos.stream().noneMatch(info -> info.provider.equals(provider))) {
            return;
        }

        if (DEBUG) {
            Log.d(TAG, "Updating People Space widget preview for user " + user.getIdentifier());
        }
        boolean success = mAppWidgetManagerOptional.get().setWidgetPreview(
                provider, WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD,
                new RemoteViews(mContext.getPackageName(),
                        R.layout.people_space_placeholder_layout));
        if (DEBUG && !success) {
            Log.d(TAG, "Failed to update generated preview for user " + user.getIdentifier());
        }
        mUpdatedPreviews.put(user.getIdentifier(), success);
    }
}
