/**
 * Copyright (c) 2018, 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.server.notification;

import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
import static android.app.Flags.nmSummarization;
import static android.app.Flags.nmSummarizationUi;
import static android.app.Flags.notificationClassificationUi;
import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID;
import static android.app.NotificationChannel.NEWS_ID;
import static android.app.NotificationChannel.PLACEHOLDER_CONVERSATION_ID;
import static android.app.NotificationChannel.PROMOTIONS_ID;
import static android.app.NotificationChannel.RECS_ID;
import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
import static android.app.NotificationChannel.USER_LOCKED_IMPORTANCE;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.IMPORTANCE_MAX;
import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import static android.os.Process.INVALID_UID;
import static android.os.UserHandle.USER_SYSTEM;
import static android.service.notification.Flags.notificationClassification;

import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_BUBBLE;
import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.AppOpsManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.ZenBypassingApp;
import android.content.AttributionSource;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.UserInfo;
import android.metrics.LogMaker;
import android.net.Uri;
import android.os.Build;
import android.os.UserHandle;
import android.permission.PermissionManager;
import android.provider.Settings;
import android.service.notification.Adjustment;
import android.service.notification.ConversationChannelWrapper;
import android.service.notification.NotificationListenerService;
import android.service.notification.RankingHelperProto;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IntArray;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseBooleanArray;
import android.util.StatsEvent;
import android.util.proto.ProtoOutputStream;

import androidx.annotation.VisibleForTesting;

import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.notification.NotificationChannelGroupsHelper;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.notification.NotificationRecordLogger.NotificationPullStatsEvent;
import com.android.server.notification.PermissionHelper.PackagePermission;
import com.android.server.uri.UriGrantsManagerInternal;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class PreferencesHelper implements RankingConfig {
    private static final String TAG = "NotificationPrefHelper";
    private final int XML_VERSION;
    /** What version to check to do the upgrade for bubbles. */
    private static final int XML_VERSION_BUBBLES_UPGRADE = 1;
    /** The first xml version with notification permissions enabled. */
    private static final int XML_VERSION_NOTIF_PERMISSION = 3;
    /** The first xml version that notifies users to review their notification permissions */
    private static final int XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION = 4;
    // The amount of time pacakage preferences can exist without the app being installed.
    private static final long PREF_GRACE_PERIOD_MS = Duration.ofDays(2).toMillis();

    @VisibleForTesting
    static final int NOTIFICATION_CHANNEL_COUNT_LIMIT = 5000;
    @VisibleForTesting
    static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 6000;

    private static final int NOTIFICATION_PREFERENCES_PULL_LIMIT = 1000;
    private static final int NOTIFICATION_CHANNEL_PULL_LIMIT = 2000;
    private static final int NOTIFICATION_CHANNEL_GROUP_PULL_LIMIT = 1000;
    private static final int NOTIFICATION_CHANNEL_DELETION_RETENTION_DAYS = 30;

    @VisibleForTesting
    static final String TAG_RANKING = "ranking";
    private static final String TAG_PACKAGE = "package";
    private static final String TAG_CHANNEL = "channel";
    private static final String TAG_GROUP = "channelGroup";
    private static final String TAG_DELEGATE = "delegate";
    private static final String TAG_STATUS_ICONS = "silent_status_icons";

    private static final String ATT_VERSION = "version";
    private static final String ATT_NAME = "name";
    private static final String ATT_UID = "uid";
    private static final String ATT_LAST_BUBBLES_VERSION_UPGRADE = "last_bubbles_version_upgrade";

    private static final String ATT_USERID = "userid";
    private static final String ATT_ID = "id";
    private static final String ATT_ALLOW_BUBBLE = "allow_bubble";
    private static final String ATT_PRIORITY = "priority";
    private static final String ATT_VISIBILITY = "visibility";
    private static final String ATT_IMPORTANCE = "importance";
    private static final String ATT_SHOW_BADGE = "show_badge";
    private static final String ATT_APP_USER_LOCKED_FIELDS = "app_user_locked_fields";
    private static final String ATT_ENABLED = "enabled";
    private static final String ATT_HIDE_SILENT = "hide_silent";
    private static final String ATT_SENT_INVALID_MESSAGE = "sent_invalid_msg";
    private static final String ATT_SENT_VALID_MESSAGE = "sent_valid_msg";
    private static final String ATT_USER_DEMOTED_INVALID_MSG_APP = "user_demote_msg_app";
    private static final String ATT_SENT_VALID_BUBBLE = "sent_valid_bubble";
    private static final String ATT_PROMOTE_NOTIFS = "promote";

    private static final String ATT_CREATION_TIME = "creation_time";

    private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
    private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE;
    private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED;
    @VisibleForTesting
    static final boolean DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS = true;
    private static final boolean DEFAULT_SHOW_BADGE = true;

    private static final boolean DEFAULT_APP_LOCKED_IMPORTANCE  = false;
    private static final boolean DEFAULT_CAN_HAVE_PROMOTED_NOTIFS = true;

    static final boolean DEFAULT_BUBBLES_ENABLED = true;
    @VisibleForTesting
    static final int DEFAULT_BUBBLE_PREFERENCE = BUBBLE_PREFERENCE_NONE;

    private static final int NOTIFICATION_UPDATE_LOG_SUBTYPE_FROM_APP = 0;
    private static final int NOTIFICATION_UPDATE_LOG_SUBTYPE_FROM_USER = 1;

    /**
     * Default value for what fields are user locked. See {@link LockableAppFields} for all lockable
     * fields.
     */
    private static final int DEFAULT_LOCKED_APP_FIELDS = 0;

    /**
     * All user-lockable fields for a given application.
     */
    @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE,
            LockableAppFields.USER_LOCKED_BUBBLE,
            LockableAppFields.USER_LOCKED_PROMOTABLE})
    public @interface LockableAppFields {
        int USER_LOCKED_IMPORTANCE = 0x00000001;
        int USER_LOCKED_BUBBLE = 0x00000002;
        int USER_LOCKED_PROMOTABLE = 0x00000004;
    }

    private final Object mLock = new Object();
    // pkg|uid => PackagePreferences
    @GuardedBy("mLock")
    private final ArrayMap<String, PackagePreferences> mPackagePreferences = new ArrayMap<>();
    // pkg|userId => PackagePreferences
    @GuardedBy("mLock")
    private final ArrayMap<String, PackagePreferences> mRestoredWithoutUids = new ArrayMap<>();

    private final Context mContext;
    private final PackageManager mPm;
    private final RankingHandler mRankingHandler;
    private final ZenModeHelper mZenModeHelper;
    private final PermissionHelper mPermissionHelper;
    private final PermissionManager mPermissionManager;
    private final NotificationChannelLogger mNotificationChannelLogger;
    private final AppOpsManager mAppOps;
    private final ManagedServices.UserProfiles mUserProfiles;
    private final UriGrantsManagerInternal mUgmInternal;

    private SparseBooleanArray mBadgingEnabled;
    private SparseBooleanArray mBubblesEnabled;
    private SparseBooleanArray mLockScreenShowNotifications;
    private SparseBooleanArray mLockScreenPrivateNotifications;
    private boolean mIsMediaNotificationFilteringEnabled;
    // Whether the current user has any channels marked as "priority channels" -- but not
    // necessarily whether they are permitted to bypass DND by current zen policy.
    private boolean mCurrentUserHasPriorityChannels;
    private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS;
    private final boolean mShowReviewPermissionsNotification;

    Clock mClock;

    public PreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
            ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager,
            NotificationChannelLogger notificationChannelLogger,
            AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles,
            UriGrantsManagerInternal ugmInternal,
            boolean showReviewPermissionsNotification, Clock clock) {
        mContext = context;
        mZenModeHelper = zenHelper;
        mRankingHandler = rankingHandler;
        mPermissionHelper = permHelper;
        mPermissionManager = permManager;
        mPm = pm;
        mNotificationChannelLogger = notificationChannelLogger;
        mAppOps = appOpsManager;
        mUserProfiles = userProfiles;
        mUgmInternal = ugmInternal;
        mShowReviewPermissionsNotification = showReviewPermissionsNotification;
        mIsMediaNotificationFilteringEnabled = context.getResources()
                .getBoolean(R.bool.config_quickSettingsShowMediaPlayer);
        mClock = clock;
        XML_VERSION = 4;

        updateBadgingEnabled();
        updateBubblesEnabled();
        updateMediaNotificationFilteringEnabled();
    }

    void onBootPhaseAppsCanStart() {
        // IpcDataCaches must be invalidated once data becomes available, as queries will only
        // begin to be cached after the first invalidation signal. At this point, we know about all
        // notification channels.
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            invalidateNotificationChannelCache();
            invalidateNotificationChannelGroupCache();
        }
    }

    public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId)
            throws XmlPullParserException, IOException {
        int type = parser.getEventType();
        if (type != XmlPullParser.START_TAG) return;
        String tag = parser.getName();
        if (!TAG_RANKING.equals(tag)) return;

        final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1);
        boolean upgradeForBubbles = parser.getAttributeInt(null,
                ATT_LAST_BUBBLES_VERSION_UPGRADE, -1) < Build.VERSION.SDK_INT;
        boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION);
        if (mShowReviewPermissionsNotification
                && (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION)) {
            // make a note that we should show the notification at some point.
            // it shouldn't be possible for the user to already have seen it, as the XML version
            // would be newer then.
            Settings.Global.putInt(mContext.getContentResolver(),
                    Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
                    NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
        }
        synchronized (mLock) {
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
                tag = parser.getName();
                if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
                    break;
                }
                if (type == XmlPullParser.START_TAG) {
                    if (TAG_STATUS_ICONS.equals(tag)) {
                        if (forRestore && userId != USER_SYSTEM) {
                            continue;
                        }
                        mHideSilentStatusBarIcons = parser.getAttributeBoolean(null,
                                ATT_HIDE_SILENT, DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS);
                    } else if (TAG_PACKAGE.equals(tag)) {
                        String name = parser.getAttributeValue(null, ATT_NAME);
                        if (!TextUtils.isEmpty(name)) {
                            restorePackageLocked(parser, forRestore, userId, name,
                                    upgradeForBubbles, migrateToPermission);
                        }
                    }
                }
            }
        }
    }

    @GuardedBy("mLock")
    private void restorePackageLocked(TypedXmlPullParser parser, boolean forRestore,
            @UserIdInt int userId, String name, boolean upgradeForBubbles,
            boolean migrateToPermission) {
        try {
            int uid = parser.getAttributeInt(null, ATT_UID, INVALID_UID);
            if (forRestore) {
                try {
                    uid = mPm.getPackageUidAsUser(name, userId);
                } catch (PackageManager.NameNotFoundException e) {
                    // noop
                }
            }
            boolean skipWarningLogged = false;
            boolean skipGroupWarningLogged = false;
            int bubblePref = parser.getAttributeInt(null, ATT_ALLOW_BUBBLE,
                    DEFAULT_BUBBLE_PREFERENCE);
            boolean bubbleLocked = (parser.getAttributeInt(null,
                    ATT_APP_USER_LOCKED_FIELDS, DEFAULT_LOCKED_APP_FIELDS) & USER_LOCKED_BUBBLE)
                    != 0;
            if (!bubbleLocked
                    && upgradeForBubbles
                    && uid != INVALID_UID
                    && mAppOps.noteOpNoThrow(OP_SYSTEM_ALERT_WINDOW, uid, name, null,
                    "check-notif-bubble") == AppOpsManager.MODE_ALLOWED) {
                // User hasn't changed bubble pref & the app has SAW, so allow all bubbles.
                bubblePref = BUBBLE_PREFERENCE_ALL;
            }
            int appImportance = parser.getAttributeInt(null, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);

            // when data is loaded from disk it's loaded as USER_ALL, but restored data that
            // is pending app install needs the user id that the data was restored to
            int fixedUserId = userId;
            if (!forRestore && uid == INVALID_UID) {
                fixedUserId = parser.getAttributeInt(null, ATT_USERID, USER_SYSTEM);
            }
            PackagePreferences r = getOrCreatePackagePreferencesSupportingInvalidUidLocked(
                    name, fixedUserId, uid,
                    appImportance,
                    parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY),
                    parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY),
                    parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE),
                    bubblePref, parser.getAttributeLong(null, ATT_CREATION_TIME, mClock.millis()));
            r.bubblePreference = bubblePref;
            r.priority = parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY);
            r.visibility = parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY);
            r.showBadge = parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE);
            r.lockedAppFields = parser.getAttributeInt(null,
                    ATT_APP_USER_LOCKED_FIELDS, DEFAULT_LOCKED_APP_FIELDS);
            r.hasSentInvalidMessage = parser.getAttributeBoolean(null, ATT_SENT_INVALID_MESSAGE,
                    false);
            r.hasSentValidMessage = parser.getAttributeBoolean(null, ATT_SENT_VALID_MESSAGE, false);
            r.userDemotedMsgApp = parser.getAttributeBoolean(
                    null, ATT_USER_DEMOTED_INVALID_MSG_APP, false);
            r.hasSentValidBubble = parser.getAttributeBoolean(null, ATT_SENT_VALID_BUBBLE, false);
            if (android.app.Flags.uiRichOngoing()) {
                r.canHavePromotedNotifs = parser.getAttributeBoolean(null, ATT_PROMOTE_NOTIFS,
                        DEFAULT_CAN_HAVE_PROMOTED_NOTIFS);
            }

            final int innerDepth = parser.getDepth();
            int type;
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                    && (type != XmlPullParser.END_TAG
                    || parser.getDepth() > innerDepth)) {
                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                    continue;
                }

                String tagName = parser.getName();
                // Channel groups
                if (TAG_GROUP.equals(tagName)) {
                    if (r.groups.size() >= NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT) {
                        if (!skipGroupWarningLogged) {
                            Slog.w(TAG, "Skipping further groups for " + r.pkg);
                            skipGroupWarningLogged = true;
                        }
                        continue;
                    }
                    String id = parser.getAttributeValue(null, ATT_ID);
                    CharSequence groupName = parser.getAttributeValue(null, ATT_NAME);
                    if (!TextUtils.isEmpty(id)) {
                        NotificationChannelGroup group =
                                new NotificationChannelGroup(id, groupName);
                        group.populateFromXml(parser);
                        r.groups.put(id, group);
                    }
                }
                // Channels
                if (TAG_CHANNEL.equals(tagName)) {
                    if (r.channels.size() >= NOTIFICATION_CHANNEL_COUNT_LIMIT) {
                        if (!skipWarningLogged) {
                            Slog.w(TAG, "Skipping further channels for " + r.pkg);
                            skipWarningLogged = true;
                        }
                        continue;
                    }
                    restoreChannel(parser, forRestore, r);
                }

                // Delegate
                if (TAG_DELEGATE.equals(tagName)) {
                    int delegateId = parser.getAttributeInt(null, ATT_UID, INVALID_UID);
                    String delegateName = XmlUtils.readStringAttribute(parser, ATT_NAME);
                    boolean delegateEnabled = parser.getAttributeBoolean(
                            null, ATT_ENABLED, Delegate.DEFAULT_ENABLED);
                    Delegate d = null;
                    if (delegateId != INVALID_UID && !TextUtils.isEmpty(delegateName)) {
                        d = new Delegate(delegateName, delegateId, delegateEnabled);
                    }
                    r.delegate = d;
                }
            }

            try {
                deleteDefaultChannelIfNeededLocked(r);
            } catch (PackageManager.NameNotFoundException e) {
                Slog.e(TAG, "deleteDefaultChannelIfNeededLocked - Exception: " + e);
            }

            if (migrateToPermission) {
                r.importance = appImportance;
                r.migrateToPm = true;
            }
        } catch (Exception e) {
            Slog.w(TAG, "Failed to restore pkg", e);
        }
    }

    @GuardedBy("mLock")
    private void restoreChannel(TypedXmlPullParser parser, boolean forRestore,
            PackagePreferences r) {
        try {
            String id = parser.getAttributeValue(null, ATT_ID);
            if (!notificationClassification() && SYSTEM_RESERVED_IDS.contains(id)) {
                // delete bundle channels if flag is rolled back
                return;
            }
            String channelName = parser.getAttributeValue(null, ATT_NAME);
            int channelImportance = parser.getAttributeInt(
                    null, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
            if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(channelName)) {
                // Force IMPORTANCE_LOW for reserved channels
                if (notificationClassification() && SYSTEM_RESERVED_IDS.contains(id)) {
                    channelImportance = IMPORTANCE_LOW;
                }
                NotificationChannel channel = new NotificationChannel(
                        id, channelName, channelImportance);
                if (forRestore) {
                    final boolean pkgInstalled = r.uid != INVALID_UID;
                    channel.populateFromXmlForRestore(parser, pkgInstalled, mContext);
                } else {
                    channel.populateFromXml(parser);
                }
                channel.setImportanceLockedByCriticalDeviceFunction(
                        r.defaultAppLockedImportance || r.fixedImportance);

                if (isShortcutOk(channel) && isDeletionOk(channel)) {
                    r.channels.put(id, channel);
                }
            }
        } catch (Exception e) {
            Slog.w(TAG, "could not restore channel for " + r.pkg, e);
        }
    }

    @GuardedBy("mLock")
    private boolean hasUserConfiguredSettings(PackagePreferences p){
        boolean hasChangedChannel = false;
        for (NotificationChannel channel : p.channels.values()) {
            if (channel.getUserLockedFields() != 0) {
                hasChangedChannel = true;
                break;
            }
        }
        return hasChangedChannel || p.importance == IMPORTANCE_NONE;
    }

    private boolean isShortcutOk(NotificationChannel channel) {
        boolean isInvalidShortcutChannel =
                channel.getConversationId() != null &&
                        channel.getConversationId().contains(
                                PLACEHOLDER_CONVERSATION_ID);
        return !isInvalidShortcutChannel;
    }

    private boolean isDeletionOk(NotificationChannel nc) {
        if (!nc.isDeleted()) {
            return true;
        }
        long boundary = System.currentTimeMillis() - (
                DateUtils.DAY_IN_MILLIS * NOTIFICATION_CHANNEL_DELETION_RETENTION_DAYS);
        if (nc.getDeletedTimeMs() <= boundary) {
            return false;
        }
        return true;
    }

    /**
     * Returns the {@link PackagePreferences} object associated to the pkg/uid pair. If it doesn't
     * exist, return {@code null}.
     */
    @GuardedBy("mLock")
    @Nullable
    private PackagePreferences getPackagePreferencesLocked(String pkg, int uid) {
        final String key = packagePreferencesKey(pkg, uid);
        return mPackagePreferences.get(key);
    }

    /**
     * Returns the {@link PackagePreferences} object associated to the pkg/uid pair. If it doesn't
     * exist, a new one is initialized (with appropriate defaults, e.g. default channel if pre-O)
     * and stored in {@link #mPackagePreferences}.
     *
     * @throws IllegalArgumentException if the supplied uid is not valid (i.e. {@code INVALID_UID}).
     */
    @GuardedBy("mLock")
    @NonNull
    private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg, int uid) {
        Objects.requireNonNull(pkg);
        Preconditions.checkArgument(uid != INVALID_UID,
                "Valid uid required to get settings of %s", pkg);

        // TODO (b/194833441): use permissionhelper instead of DEFAULT_IMPORTANCE
        return getOrCreatePackagePreferencesSupportingInvalidUidLocked(pkg,
                UserHandle.getUserId(uid), uid, DEFAULT_IMPORTANCE, DEFAULT_PRIORITY,
                DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE, DEFAULT_BUBBLE_PREFERENCE, mClock.millis());
    }

    /**
     * Returns the {@link PackagePreferences} object associated to the pkg/uid pair, and initializes
     * a new one (with appropriate defaults, e.g. default channel if pre-O) if it doesn't exist.
     *
     * <p>This method accepts {@link android.os.Process#INVALID_UID} as the {@code uid}
     * parameter, and in that case will create a (time-limited) entry in
     * {@link #mRestoredWithoutUids} instead of {@link #mPackagePreferences}. As such, should only
     * be used that way by the {@code readXml()} path, to support restoring NMS backups before all
     * packages have been reinstalled -- for API calls, we shouldn't create entries for
     * non-existing packages.
     */
    @GuardedBy("mLock")
    private PackagePreferences getOrCreatePackagePreferencesSupportingInvalidUidLocked(String pkg,
            @UserIdInt int userId, int uid, int importance, int priority, int visibility,
            boolean showBadge, int bubblePreference, long creationTime) {
        boolean created = false;
        final String key = packagePreferencesKey(pkg, uid);
        PackagePreferences
                r = (uid == INVALID_UID)
                ? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId))
                : mPackagePreferences.get(key);
        if (r == null) {
            created = true;
            r = new PackagePreferences();
            r.pkg = pkg;
            r.uid = uid;
            r.importance = importance;
            r.priority = priority;
            r.visibility = visibility;
            r.showBadge = showBadge;
            r.bubblePreference = bubblePreference;
            if (r.uid == INVALID_UID) {
                r.creationTime = creationTime;
            }

            try {
                createDefaultChannelIfNeededLocked(r);
            } catch (PackageManager.NameNotFoundException e) {
                Slog.e(TAG, "createDefaultChannelIfNeededLocked - Exception: " + e);
            }

            if (r.uid == INVALID_UID) {
                r.userIdWhenUidUnknown = userId;
                mRestoredWithoutUids.put(unrestoredPackageKey(pkg, userId), r);
            } else {
                mPackagePreferences.put(key, r);
            }
        }
        if (r.uid == INVALID_UID) {
            if (PREF_GRACE_PERIOD_MS < (mClock.millis() - r.creationTime)) {
                mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, userId));
            }
        }
        if (android.app.Flags.nmBinderPerfCacheChannels() && created) {
            invalidateNotificationChannelCache();
        }
        return r;
    }

    private boolean shouldHaveDefaultChannel(PackagePreferences r) throws
            PackageManager.NameNotFoundException {
        final int userId = UserHandle.getUserId(r.uid);
        final ApplicationInfo applicationInfo =
                mPm.getApplicationInfoAsUser(r.pkg, 0, userId);
        if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O) {
            // O apps should not have the default channel.
            return false;
        }

        // Otherwise, this app should have the default channel.
        return true;
    }

    @GuardedBy("mLock")
    private boolean deleteDefaultChannelIfNeededLocked(PackagePreferences r) throws
            PackageManager.NameNotFoundException {
        if (!r.channels.containsKey(DEFAULT_CHANNEL_ID)) {
            // Not present
            return false;
        }

        if (shouldHaveDefaultChannel(r)) {
            // Keep the default channel until upgraded.
            return false;
        }

        // Remove Default Channel.
        r.channels.remove(DEFAULT_CHANNEL_ID);

        return true;
    }

    @GuardedBy("mLock")
    private boolean createDefaultChannelIfNeededLocked(PackagePreferences r) throws
            PackageManager.NameNotFoundException {
        if (r.uid == INVALID_UID) {
            return false;
        }

        if (r.channels.containsKey(DEFAULT_CHANNEL_ID)) {
            r.channels.get(DEFAULT_CHANNEL_ID).setName(mContext.getString(
                    com.android.internal.R.string.default_notification_channel_label));
            return false;
        }

        if (!shouldHaveDefaultChannel(r)) {
            // Keep the default channel until upgraded.
            return false;
        }

        // Create Default Channel
        NotificationChannel channel;
        channel = new NotificationChannel(
                DEFAULT_CHANNEL_ID,
                mContext.getString(R.string.default_notification_channel_label),
                r.importance);
        channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
        channel.setLockscreenVisibility(r.visibility);
        if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) {
            channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
        }
        if (r.priority != DEFAULT_PRIORITY) {
            channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
        }
        if (r.visibility != DEFAULT_VISIBILITY) {
            channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY);
        }
        r.channels.put(channel.getId(), channel);

        return true;
    }

    @GuardedBy("mLock")
    private NotificationChannel addReservedChannelLocked(PackagePreferences p, String channelId) {
        String label = "";
        switch (channelId) {
            case PROMOTIONS_ID:
                label = mContext.getString(R.string.promotional_notification_channel_label);
                break;
            case RECS_ID:
                label = mContext.getString(R.string.recs_notification_channel_label);
                break;
            case NEWS_ID:
                label = mContext.getString(R.string.news_notification_channel_label);
                break;
            case SOCIAL_MEDIA_ID:
                label = mContext.getString(R.string.social_notification_channel_label);
                break;
        }
        NotificationChannel channel = new NotificationChannel(channelId, label, IMPORTANCE_LOW);
        p.channels.put(channelId, channel);
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            invalidateNotificationChannelCache();
        }
        return channel;
    }

    public void writeXml(TypedXmlSerializer out, boolean forBackup, int userId) throws IOException {
        out.startTag(null, TAG_RANKING);
        out.attributeInt(null, ATT_VERSION, XML_VERSION);
        out.attributeInt(null, ATT_LAST_BUBBLES_VERSION_UPGRADE, Build.VERSION.SDK_INT);
        if (mHideSilentStatusBarIcons != DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS) {
            out.startTag(null, TAG_STATUS_ICONS);
            out.attributeBoolean(null, ATT_HIDE_SILENT, mHideSilentStatusBarIcons);
            out.endTag(null, TAG_STATUS_ICONS);
        }
        ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> notifPermissions = new ArrayMap<>();
        if (forBackup) {
            notifPermissions = mPermissionHelper.getNotificationPermissionValues(userId);
        }

        synchronized (mLock) {
            final int N = mPackagePreferences.size();
            for (int i = 0; i < N; i++) {
                final PackagePreferences r = mPackagePreferences.valueAt(i);
                if (forBackup && UserHandle.getUserId(r.uid) != userId) {
                    continue;
                }
                writePackageXml(r, out, notifPermissions, forBackup);
            }

            if (!forBackup) {
                final int M = mRestoredWithoutUids.size();
                for (int i = 0; i < M; i++) {
                    final PackagePreferences r = mRestoredWithoutUids.valueAt(i);
                    writePackageXml(r, out, notifPermissions, false);
                }
            }
        }
        // Some apps have permissions set but don't have expanded notification settings
        if (!notifPermissions.isEmpty()) {
            for (Pair<Integer, String> app : notifPermissions.keySet()) {
                out.startTag(null, TAG_PACKAGE);
                out.attribute(null, ATT_NAME, app.second);
                out.attributeInt(null, ATT_IMPORTANCE,
                        notifPermissions.get(app).first ? IMPORTANCE_DEFAULT : IMPORTANCE_NONE);
                out.endTag(null, TAG_PACKAGE);
            }
        }
        out.endTag(null, TAG_RANKING);
    }

    @GuardedBy("mLock")
    private void writePackageXml(PackagePreferences r, TypedXmlSerializer out,
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> notifPermissions,
            boolean forBackup) throws
            IOException {
        out.startTag(null, TAG_PACKAGE);
        out.attribute(null, ATT_NAME, r.pkg);
        if (!notifPermissions.isEmpty()) {
            Pair<Integer, String> app = new Pair(r.uid, r.pkg);
            final Pair<Boolean, Boolean> permission = notifPermissions.get(app);
            out.attributeInt(null, ATT_IMPORTANCE,
                    permission != null && permission.first ? IMPORTANCE_DEFAULT
                            : IMPORTANCE_NONE);
            notifPermissions.remove(app);
        } else {
            if (r.importance != DEFAULT_IMPORTANCE) {
                out.attributeInt(null, ATT_IMPORTANCE, r.importance);
            }
        }
        if (r.priority != DEFAULT_PRIORITY) {
            out.attributeInt(null, ATT_PRIORITY, r.priority);
        }
        if (r.visibility != DEFAULT_VISIBILITY) {
            out.attributeInt(null, ATT_VISIBILITY, r.visibility);
        }
        if (r.bubblePreference != DEFAULT_BUBBLE_PREFERENCE) {
            out.attributeInt(null, ATT_ALLOW_BUBBLE, r.bubblePreference);
        }
        out.attributeBoolean(null, ATT_SHOW_BADGE, r.showBadge);
        out.attributeInt(null, ATT_APP_USER_LOCKED_FIELDS,
                r.lockedAppFields);
        out.attributeBoolean(null, ATT_SENT_INVALID_MESSAGE,
                r.hasSentInvalidMessage);
        out.attributeBoolean(null, ATT_SENT_VALID_MESSAGE,
                r.hasSentValidMessage);
        out.attributeBoolean(null, ATT_USER_DEMOTED_INVALID_MSG_APP,
                r.userDemotedMsgApp);
        out.attributeBoolean(null, ATT_SENT_VALID_BUBBLE, r.hasSentValidBubble);
        if (android.app.Flags.uiRichOngoing()) {
            if (r.canHavePromotedNotifs != DEFAULT_CAN_HAVE_PROMOTED_NOTIFS) {
                out.attributeBoolean(null, ATT_PROMOTE_NOTIFS, r.canHavePromotedNotifs);
            }
        }

        if (r.uid == INVALID_UID) {
            out.attributeLong(null, ATT_CREATION_TIME, r.creationTime);
            out.attributeInt(null, ATT_USERID, r.userIdWhenUidUnknown);
        }

        if (!forBackup) {
            out.attributeInt(null, ATT_UID, r.uid);
        }

        if (r.delegate != null) {
            out.startTag(null, TAG_DELEGATE);

            out.attribute(null, ATT_NAME, r.delegate.mPkg);
            out.attributeInt(null, ATT_UID, r.delegate.mUid);
            if (r.delegate.mEnabled != Delegate.DEFAULT_ENABLED) {
                out.attributeBoolean(null, ATT_ENABLED, r.delegate.mEnabled);
            }
            out.endTag(null, TAG_DELEGATE);
        }

        for (NotificationChannelGroup group : r.groups.values()) {
            group.writeXml(out);
        }

        for (NotificationChannel channel : r.channels.values()) {
            if (forBackup) {
                if (!channel.isDeleted()) {
                    channel.writeXmlForBackup(out, mContext);
                }
            } else {
                channel.writeXml(out);
            }
        }

        out.endTag(null, TAG_PACKAGE);
    }

    /**
     * Sets whether bubbles are allowed.
     *
     * @param pkg the package to allow or not allow bubbles for.
     * @param uid the uid to allow or not allow bubbles for.
     * @param bubblePreference whether bubbles are allowed.
     */
    public void setBubblesAllowed(String pkg, int uid, int bubblePreference) {
        boolean changed;
        synchronized (mLock) {
            PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid);
            changed = p.bubblePreference != bubblePreference;
            p.bubblePreference = bubblePreference;
            p.lockedAppFields = p.lockedAppFields | LockableAppFields.USER_LOCKED_BUBBLE;
        }
        if (changed) {
            updateConfig();
        }
    }

    /**
     * Whether bubbles are allowed.
     *
     * @param pkg the package to check if bubbles are allowed for
     * @param uid the uid to check if bubbles are allowed for.
     * @return whether bubbles are allowed.
     */
    @Override
    public int getBubblePreference(String pkg, int uid) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(pkg, uid);
            return p != null ? p.bubblePreference : DEFAULT_BUBBLE_PREFERENCE;
        }
    }

    public int getAppLockedFields(String pkg, int uid) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(pkg, uid);
            return p != null ? p.lockedAppFields : DEFAULT_LOCKED_APP_FIELDS;
        }
    }

    @Override
    public boolean canShowBadge(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(packageName, uid);
            return p != null ? p.showBadge : DEFAULT_SHOW_BADGE;
        }
    }

    @Override
    public void setShowBadge(String packageName, int uid, boolean showBadge) {
        boolean changed = false;
        synchronized (mLock) {
            PackagePreferences pkgPrefs = getOrCreatePackagePreferencesLocked(packageName, uid);
            if (pkgPrefs.showBadge != showBadge) {
                pkgPrefs.showBadge = showBadge;
                changed = true;
            }
        }
        if (changed) {
            updateConfig();
        }
    }

    @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
    public boolean canBePromoted(String packageName, int uid) {
        if (android.app.Flags.uiRichOngoing()) {
            Log.e(TAG, "Should not be checking here if uiRichOngoingPermission flag enabled");
            return false;
        }

        synchronized (mLock) {
            return getOrCreatePackagePreferencesLocked(packageName, uid).canHavePromotedNotifs;
        }
    }

    @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
    public boolean setCanBePromoted(String packageName, int uid, boolean promote,
            boolean fromUser) {

        if (android.app.Flags.uiRichOngoing()) {
            Log.e(TAG, "Should not be writing here if uiRichOngoingPermission flag enabled");
            return false;
        }

        boolean changed = false;
        synchronized (mLock) {
            PackagePreferences pkgPrefs = getOrCreatePackagePreferencesLocked(packageName, uid);
            if (fromUser || ((pkgPrefs.lockedAppFields & USER_LOCKED_PROMOTABLE) == 0)) {
                if (pkgPrefs.canHavePromotedNotifs != promote) {
                    pkgPrefs.canHavePromotedNotifs = promote;
                    if (fromUser) {
                        pkgPrefs.lockedAppFields |= USER_LOCKED_PROMOTABLE;
                    }
                    changed = true;
                }
            }
        }
        // no need to send a ranking update because we need to update the flag value on all pending
        // and posted notifs and NMS will take care of that
        return changed;
    }

    public boolean isInInvalidMsgState(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(packageName, uid);
            return r != null && r.hasSentInvalidMessage && !r.hasSentValidMessage;
        }
    }

    public boolean hasUserDemotedInvalidMsgApp(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(packageName, uid);
            return r != null && isInInvalidMsgState(packageName, uid) && r.userDemotedMsgApp;
        }
    }

    public void setInvalidMsgAppDemoted(String packageName, int uid, boolean isDemoted) {
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
            r.userDemotedMsgApp = isDemoted;
        }
    }

    public boolean setInvalidMessageSent(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
            boolean valueChanged = r.hasSentInvalidMessage == false;
            r.hasSentInvalidMessage = true;

            return valueChanged;
        }
    }

    public boolean setValidMessageSent(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
            boolean valueChanged = r.hasSentValidMessage == false;
            r.hasSentValidMessage = true;

            return valueChanged;
        }
    }

    @VisibleForTesting
    boolean hasSentInvalidMsg(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(packageName, uid);
            return r != null && r.hasSentInvalidMessage;
        }
    }

    @VisibleForTesting
    boolean hasSentValidMsg(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(packageName, uid);
            return r != null && r.hasSentValidMessage;
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    boolean didUserEverDemoteInvalidMsgApp(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(packageName, uid);
            return p != null && p.userDemotedMsgApp;
        }
    }

    /** Sets whether this package has sent a notification with valid bubble metadata. */
    public boolean setValidBubbleSent(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
            boolean valueChanged = !r.hasSentValidBubble;
            r.hasSentValidBubble = true;
            return valueChanged;
        }
    }

    boolean hasSentValidBubble(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(packageName, uid);
            return p != null && p.hasSentValidBubble;
        }
    }

    boolean isImportanceLocked(String pkg, int uid) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(pkg, uid);
            return p != null
                    ? p.fixedImportance || p.defaultAppLockedImportance
                    : DEFAULT_APP_LOCKED_IMPORTANCE;
        }
    }

    @Override
    public boolean isGroupBlocked(String packageName, int uid, String groupId) {
        if (groupId == null) {
            return false;
        }
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(packageName, uid);
            if (r == null) {
                return false;
            }
            NotificationChannelGroup group = r.groups.get(groupId);
            if (group == null) {
                return false;
            }
            return group.isBlocked();
        }
    }

    int getPackagePriority(String pkg, int uid) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(pkg, uid);
            return p != null ? p.priority : DEFAULT_PRIORITY;
        }
    }

    int getPackageVisibility(String pkg, int uid) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(pkg, uid);
            return p != null ? p.visibility : DEFAULT_VISIBILITY;
        }
    }

    @Override
    public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,
            boolean fromTargetApp, int callingUid, boolean fromSystemOrSystemUi) {
        Objects.requireNonNull(pkg);
        Objects.requireNonNull(group);
        Objects.requireNonNull(group.getId());
        if (TextUtils.isEmpty(group.getName())) {
            throw new IllegalArgumentException("group.getName() can't be empty");
        }
        boolean needsDndChange = false;
        boolean changed = false;
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
            if (r.groups.size() >= NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT) {
                throw new IllegalStateException("Limit exceed; cannot create more groups");
            }
            if (fromTargetApp) {
                group.setBlocked(false);
            }
            final NotificationChannelGroup oldGroup = r.groups.get(group.getId());
            if (oldGroup != null) {
                group.setChannels(oldGroup.getChannels());

                // apps can't update the blocked status or app overlay permission
                if (fromTargetApp) {
                    group.setBlocked(oldGroup.isBlocked());
                    group.unlockFields(group.getUserLockedFields());
                    group.lockFields(oldGroup.getUserLockedFields());
                } else {
                    // but the system can
                    if (group.isBlocked() != oldGroup.isBlocked()) {
                        group.lockFields(NotificationChannelGroup.USER_LOCKED_BLOCKED_STATE);
                        needsDndChange = true;
                    }
                }
            }
            if (!group.equals(oldGroup)) {
                // will log for new entries as well as name/description changes
                changed = true;
                MetricsLogger.action(getChannelGroupLog(group.getId(), pkg));
                mNotificationChannelLogger.logNotificationChannelGroup(group, uid, pkg,
                        oldGroup == null,
                        (oldGroup != null) && oldGroup.isBlocked());
            }
            r.groups.put(group.getId(), group);
        }
        if (needsDndChange) {
            updateCurrentUserHasPriorityChannels();
        }
        if (android.app.Flags.nmBinderPerfCacheChannels() && changed) {
            invalidateNotificationChannelGroupCache();
        }
    }

    @Override
    public boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel,
            boolean fromTargetApp, boolean hasDndAccess, int callingUid,
            boolean fromSystemOrSystemUi) {
        Objects.requireNonNull(pkg);
        Objects.requireNonNull(channel);
        Objects.requireNonNull(channel.getId());
        Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName()));
        Preconditions.checkArgument(channel.getImportance() >= IMPORTANCE_NONE
                && channel.getImportance() <= IMPORTANCE_MAX, "Invalid importance level");

        boolean needsPolicyFileChange = false, wasUndeleted = false, needsDndChange = false;
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
            if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
                throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
            }
            if (DEFAULT_CHANNEL_ID.equals(channel.getId())) {
                throw new IllegalArgumentException("Reserved id");
            }
            // Only the user can update bundle channel settings
            if (notificationClassification() && !fromSystemOrSystemUi
                    && SYSTEM_RESERVED_IDS.contains(channel.getId())) {
                return false;
            }
            NotificationChannel existing = r.channels.get(channel.getId());
            if (existing != null && fromTargetApp) {
                // Actually modifying an existing channel - keep most of the existing settings
                if (existing.isDeleted()) {
                    // The existing channel was deleted - undelete it.
                    existing.setDeleted(false);
                    existing.setDeletedTimeMs(-1);
                    needsPolicyFileChange = true;
                    wasUndeleted = true;

                    // log a resurrected channel as if it's new again
                    MetricsLogger.action(getChannelLog(channel, pkg).setType(
                            com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN));
                    mNotificationChannelLogger.logNotificationChannelCreated(channel, uid, pkg);
                }

                if (!Objects.equals(channel.getName().toString(), existing.getName().toString())) {
                    existing.setName(channel.getName().toString());
                    needsPolicyFileChange = true;
                }
                if (!Objects.equals(channel.getDescription(), existing.getDescription())) {
                    existing.setDescription(channel.getDescription());
                    needsPolicyFileChange = true;
                }
                if (channel.isBlockable() != existing.isBlockable()) {
                    existing.setBlockable(channel.isBlockable());
                    needsPolicyFileChange = true;
                }
                if (channel.getGroup() != null && existing.getGroup() == null) {
                    existing.setGroup(channel.getGroup());
                    needsPolicyFileChange = true;
                }

                // Apps are allowed to downgrade channel importance if the user has not changed any
                // fields on this channel yet.
                final int previousExistingImportance = existing.getImportance();
                final int previousLoggingImportance =
                        NotificationChannelLogger.getLoggingImportance(existing);
                if (existing.getUserLockedFields() == 0 &&
                        channel.getImportance() < existing.getImportance()) {
                    existing.setImportance(channel.getImportance());
                    needsPolicyFileChange = true;
                }

                if (r.fixedImportance && !channel.isBlockable() && existing.getImportance() != channel.getImportance()) {
                    existing.setImportance(channel.getImportance());
                    needsPolicyFileChange = true;
                }

                // system apps and dnd access apps can bypass dnd if the user hasn't changed any
                // fields on the channel yet
                if (existing.getUserLockedFields() == 0 && hasDndAccess) {
                    boolean bypassDnd = channel.canBypassDnd();
                    if (bypassDnd != existing.canBypassDnd() || wasUndeleted) {
                        existing.setBypassDnd(bypassDnd);
                        needsPolicyFileChange = true;

                        if (bypassDnd != mCurrentUserHasPriorityChannels
                                || previousExistingImportance != existing.getImportance()) {
                            needsDndChange = true;
                        }
                    }
                }

                if (existing.getOriginalImportance() == IMPORTANCE_UNSPECIFIED) {
                    existing.setOriginalImportance(channel.getImportance());
                    needsPolicyFileChange = true;
                }

                if (needsPolicyFileChange) {
                    updateConfig();
                }
                if (needsPolicyFileChange && !wasUndeleted) {
                    mNotificationChannelLogger.logNotificationChannelModified(existing, uid, pkg,
                            previousLoggingImportance, false);
                }
            } else {
                if (r.channels.size() >= NOTIFICATION_CHANNEL_COUNT_LIMIT) {
                    throw new IllegalStateException("Limit exceed; cannot create more channels");
                }

                needsPolicyFileChange = true;

                // Reset fields that apps aren't allowed to set.
                if (fromTargetApp && !hasDndAccess) {
                    channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
                }
                if (fromTargetApp) {
                    channel.setLockscreenVisibility(r.visibility);
                    channel.setAllowBubbles(existing != null
                            ? existing.getAllowBubbles()
                            : NotificationChannel.DEFAULT_ALLOW_BUBBLE);
                    channel.setImportantConversation(false);
                }
                clearLockedFieldsLocked(channel);

                // Verify that the app has permission to read the sound Uri
                // Only check for new channels, as regular apps can only set sound
                // before creating. See: {@link NotificationChannel#setSound}
                try {
                    PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid);
                } catch (SecurityException e) {
                    // Fallback to default Uri to prevent app crashes
                    channel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
                            Notification.AUDIO_ATTRIBUTES_DEFAULT);
                }

                channel.setImportanceLockedByCriticalDeviceFunction(
                        r.defaultAppLockedImportance || r.fixedImportance);

                if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
                    channel.setLockscreenVisibility(
                            NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
                }
                if (!r.showBadge) {
                    channel.setShowBadge(false);
                }
                channel.setOriginalImportance(channel.getImportance());

                // validate parent
                if (channel.getParentChannelId() != null) {
                    Preconditions.checkArgument(
                            r.channels.containsKey(channel.getParentChannelId()),
                            "Tried to create a conversation channel without a preexisting parent");
                }

                r.channels.put(channel.getId(), channel);
                if (channel.canBypassDnd() != mCurrentUserHasPriorityChannels) {
                    needsDndChange = true;
                }
                MetricsLogger.action(getChannelLog(channel, pkg).setType(
                        com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN));
                mNotificationChannelLogger.logNotificationChannelCreated(channel, uid, pkg);
            }
        }

        if (needsDndChange) {
            updateCurrentUserHasPriorityChannels();
        }

        if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) {
            invalidateNotificationChannelCache();
        }

        return needsPolicyFileChange;
    }

    @GuardedBy("mLock")
    @VisibleForTesting
    void clearLockedFieldsLocked(NotificationChannel channel) {
        channel.unlockFields(channel.getUserLockedFields());
    }

    void unlockNotificationChannelImportance(String pkg, int uid, String updatedChannelId) {
        Objects.requireNonNull(updatedChannelId);
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
            NotificationChannel channel = r.channels.get(updatedChannelId);
            if (channel == null || channel.isDeleted()) {
                throw new IllegalArgumentException("Channel does not exist");
            }
            channel.unlockFields(USER_LOCKED_IMPORTANCE);
        }
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            invalidateNotificationChannelCache();
        }
    }


    @Override
    public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel,
            boolean fromUser, int callingUid, boolean fromSystemOrSystemUi) {
        Objects.requireNonNull(updatedChannel);
        Objects.requireNonNull(updatedChannel.getId());
        boolean changed = false;
        boolean needsDndChange = false;
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
            NotificationChannel channel = r.channels.get(updatedChannel.getId());
            if (channel == null || channel.isDeleted()) {
                throw new IllegalArgumentException("Channel does not exist");
            }
            if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
                updatedChannel.setLockscreenVisibility(
                        NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
            }
            if (fromUser) {
                updatedChannel.lockFields(channel.getUserLockedFields());
                lockFieldsForUpdateLocked(channel, updatedChannel);
            } else {
                updatedChannel.unlockFields(updatedChannel.getUserLockedFields());
            }

            if (channel.isImportanceLockedByCriticalDeviceFunction()
                    && !(channel.isBlockable() || channel.getImportance() == IMPORTANCE_NONE)) {
                updatedChannel.setImportance(channel.getImportance());
            }

            r.channels.put(updatedChannel.getId(), updatedChannel);

            if (onlyHasDefaultChannel(pkg, uid)) {
                r.priority = updatedChannel.canBypassDnd()
                        ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT;
                r.visibility = updatedChannel.getLockscreenVisibility();
                r.showBadge = updatedChannel.canShowBadge();
                changed = true;
            }

            if (!channel.equals(updatedChannel)) {
                // only log if there are real changes
                MetricsLogger.action(getChannelLog(updatedChannel, pkg)
                        .setSubtype(fromUser ? NOTIFICATION_UPDATE_LOG_SUBTYPE_FROM_USER
                                : NOTIFICATION_UPDATE_LOG_SUBTYPE_FROM_APP));
                mNotificationChannelLogger.logNotificationChannelModified(updatedChannel, uid, pkg,
                        NotificationChannelLogger.getLoggingImportance(channel), fromUser);
                changed = true;
            }

            if (fromUser && SystemUiSystemPropertiesFlags.getResolver().isEnabled(
                    NotificationFlags.PROPAGATE_CHANNEL_UPDATES_TO_CONVERSATIONS)) {
                updateChildrenConversationChannels(r, channel, updatedChannel);
                // No need to update changed or needsDndChanged as the child channel(s) cannot be
                // relevantly affected without the parent channel already having been.
            }

            if (updatedChannel.canBypassDnd() != mCurrentUserHasPriorityChannels
                    || channel.getImportance() != updatedChannel.getImportance()) {
                needsDndChange = true;
                changed = true;
            }
        }
        if (needsDndChange) {
            updateCurrentUserHasPriorityChannels();
        }
        if (changed) {
            if (android.app.Flags.nmBinderPerfCacheChannels()) {
                invalidateNotificationChannelCache();
            }
            updateConfig();
        }
    }

    /**
     * Updates conversation channels after user changes to their parent channel. See
     * {@link #maybeUpdateChildConversationChannel}.
     */
    @GuardedBy("mLock")
    private void updateChildrenConversationChannels(@NonNull PackagePreferences packagePreferences,
            @NonNull NotificationChannel oldParent, @NonNull NotificationChannel updatedParent) {
        if (oldParent.equals(updatedParent)) {
            return;
        }
        if (oldParent.isConversation()) {
            return; // Can't have children.
        }
        for (NotificationChannel channel : packagePreferences.channels.values()) {
            // Include deleted -- otherwise they will have old settings if later resurrected.
            // Include demoted -- still attached to their parents.
            if (channel.isConversation()
                    && oldParent.getId().equals(channel.getParentChannelId())) {
                maybeUpdateChildConversationChannel(packagePreferences.pkg, packagePreferences.uid,
                        channel, oldParent, updatedParent);
            }
        }
    }

    /**
     * Apply the diff between {@code oldParent} and {@code updatedParent} to the child
     * {@code conversation} channel. Only fields that are not locked on the conversation channel
     * (see {@link NotificationChannel#LOCKABLE_FIELDS }) will be updated (so that we don't override
     * previous explicit user choices).
     *
     * <p>This will also log the change as if it was {@code fromUser=true}.
     */
    @GuardedBy("mLock")
    private void maybeUpdateChildConversationChannel(String pkg, int uid,
            @NonNull NotificationChannel conversation, @NonNull NotificationChannel oldParent,
            @NonNull NotificationChannel updatedParent) {
        boolean changed = false;
        int oldLoggingImportance = NotificationChannelLogger.getLoggingImportance(conversation);

        if ((conversation.getUserLockedFields() & NotificationChannel.USER_LOCKED_PRIORITY) == 0
                && oldParent.canBypassDnd() != updatedParent.canBypassDnd()) {
            conversation.setBypassDnd(updatedParent.canBypassDnd());
            changed = true;
        }
        if ((conversation.getUserLockedFields() & NotificationChannel.USER_LOCKED_VISIBILITY) == 0
                && oldParent.getLockscreenVisibility()
                != updatedParent.getLockscreenVisibility()) {
            conversation.setLockscreenVisibility(updatedParent.getLockscreenVisibility());
            changed = true;
        }
        if ((conversation.getUserLockedFields() & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0
                && oldParent.getImportance() != updatedParent.getImportance()) {
            conversation.setImportance(updatedParent.getImportance());
            changed = true;
        }
        if ((conversation.getUserLockedFields() & NotificationChannel.USER_LOCKED_LIGHTS) == 0
                && (oldParent.shouldShowLights() != updatedParent.shouldShowLights()
                || oldParent.getLightColor() != updatedParent.getLightColor())) {
            conversation.enableLights(updatedParent.shouldShowLights());
            conversation.setLightColor(updatedParent.getLightColor());
            changed = true;
        }
        if ((conversation.getUserLockedFields() & NotificationChannel.USER_LOCKED_SOUND) == 0
                && !Objects.equals(oldParent.getSound(), updatedParent.getSound())) {
            conversation.setSound(updatedParent.getSound(), updatedParent.getAudioAttributes());
            changed = true;
        }
        if ((conversation.getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0
                && (!Arrays.equals(oldParent.getVibrationPattern(),
                updatedParent.getVibrationPattern())
                || !Objects.equals(
                        oldParent.getVibrationEffect(), updatedParent.getVibrationEffect())
                || oldParent.shouldVibrate() != updatedParent.shouldVibrate())) {
            // enableVibration must be 2nd because setVibrationPattern may toggle it.
            conversation.setVibrationPattern(updatedParent.getVibrationPattern());
            conversation.enableVibration(updatedParent.shouldVibrate());
            changed = true;
        }
        if ((conversation.getUserLockedFields() & NotificationChannel.USER_LOCKED_SHOW_BADGE) == 0
                && oldParent.canShowBadge() != updatedParent.canShowBadge()) {
            conversation.setShowBadge(updatedParent.canShowBadge());
            changed = true;
        }
        if ((conversation.getUserLockedFields() & NotificationChannel.USER_LOCKED_ALLOW_BUBBLE) == 0
                && oldParent.getAllowBubbles() != updatedParent.getAllowBubbles()) {
            conversation.setAllowBubbles(updatedParent.getAllowBubbles());
            changed = true;
        }

        if (changed) {
            MetricsLogger.action(getChannelLog(conversation, pkg).setSubtype(
                    NOTIFICATION_UPDATE_LOG_SUBTYPE_FROM_USER));
            mNotificationChannelLogger.logNotificationChannelModified(conversation, uid, pkg,
                    oldLoggingImportance, /* fromUser= */ true);
        }
    }

    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
    public NotificationChannel getReservedChannel(String pkg, int uid,
            @Adjustment.Types int type) {
        if (!notificationClassification()) {
            return null;
        }
        Objects.requireNonNull(pkg);
        if (uid == INVALID_UID) {
            return null;
        }
        String channelId = NotificationChannel.getChannelIdForBundleType(type);
        if (channelId == null) {
            return null;
        }
        NotificationChannel channel =
                getConversationNotificationChannel(pkg, uid, channelId, null, true, false);
        return channel;
    }

    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
    public NotificationChannel createReservedChannel(String pkg, int uid,
            @Adjustment.Types int type) {
        if (!notificationClassification()) {
            return null;
        }
        Objects.requireNonNull(pkg);
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
            String channelId = NotificationChannel.getChannelIdForBundleType(type);
            if (channelId == null) {
                return null;
            }
            return addReservedChannelLocked(r, channelId);
        }
    }

    @Override
    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
            boolean includeDeleted) {
        Objects.requireNonNull(pkg);
        if (uid == INVALID_UID) {
            return null;
        }
        return getConversationNotificationChannel(pkg, uid, channelId, null, true, includeDeleted);
    }

    @Override
    public NotificationChannel getConversationNotificationChannel(String pkg, int uid,
            String channelId, String conversationId, boolean returnParentIfNoConversationChannel,
            boolean includeDeleted) {
        Preconditions.checkNotNull(pkg);
        if (uid == INVALID_UID) {
            return null;
        }
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
            if (channelId == null) {
                channelId = DEFAULT_CHANNEL_ID;
            }
            NotificationChannel channel = null;
            if (conversationId != null) {
                // look for an automatically created conversation specific channel
                channel = findConversationChannel(r, channelId, conversationId, includeDeleted);
            }
            if (channel == null && returnParentIfNoConversationChannel) {
                // look for it just based on its id
                final NotificationChannel nc = r.channels.get(channelId);
                if (nc != null && (includeDeleted || !nc.isDeleted())) {
                    return nc;
                }
            }
            return channel;
        }
    }

    private NotificationChannel findConversationChannel(PackagePreferences p, String parentId,
            String conversationId, boolean includeDeleted) {
        for (NotificationChannel nc : p.channels.values()) {
            if (conversationId.equals(nc.getConversationId())
                    && parentId.equals(nc.getParentChannelId())
                    && (includeDeleted || !nc.isDeleted())) {
                return nc;
            }
        }
        return null;
    }

    @Override
    public boolean deleteNotificationChannel(String pkg, int uid, String channelId,
            int callingUid, boolean fromSystemOrSystemUi) {
        boolean deletedChannel = false;
        boolean channelBypassedDnd = false;
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return false;
            }
            NotificationChannel channel = r.channels.get(channelId);
            if (channel != null) {
                channelBypassedDnd = channel.canBypassDnd();
                deletedChannel = deleteNotificationChannelLocked(channel, pkg, uid);
            }
        }
        if (channelBypassedDnd) {
            updateCurrentUserHasPriorityChannels();
        }

        if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) {
            invalidateNotificationChannelCache();
        }
        return deletedChannel;
    }

    @GuardedBy("mLock")
    private boolean deleteNotificationChannelLocked(NotificationChannel channel, String pkg,
            int uid) {
        if (!channel.isDeleted()) {
            channel.setDeleted(true);
            channel.setDeletedTimeMs(System.currentTimeMillis());
            LogMaker lm = getChannelLog(channel, pkg);
            lm.setType(com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_CLOSE);
            MetricsLogger.action(lm);
            mNotificationChannelLogger.logNotificationChannelDeleted(channel, uid, pkg);
            return true;
        }
        return false;
    }

    @Override
    @VisibleForTesting
    public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) {
        Objects.requireNonNull(pkg);
        Objects.requireNonNull(channelId);
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return;
            }
            r.channels.remove(channelId);
        }
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            invalidateNotificationChannelCache();
        }
    }

    @Override
    public void permanentlyDeleteNotificationChannels(String pkg, int uid) {
        Objects.requireNonNull(pkg);
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return;
            }
            boolean deleted = false;
            int N = r.channels.size() - 1;
            for (int i = N; i >= 0; i--) {
                String key = r.channels.keyAt(i);
                if (!DEFAULT_CHANNEL_ID.equals(key)) {
                    r.channels.remove(key);
                    deleted = true;
                }
            }
            if (android.app.Flags.nmBinderPerfCacheChannels() && deleted) {
                invalidateNotificationChannelCache();
            }
        }
    }

    // Update all reserved channels for the given adjustment type(s) when enabled or disabled.
    // If disabled, all relevant channels are marked as deleted until the type is re-enabled.
    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
    void updateReservedChannels(List<Integer> userIds, List<Integer> changedTypes,
            boolean enabled) {
        if (!notificationClassification()) {
            return;
        }
        boolean shouldBeDeleted = !enabled;  // just for ease of reading boolean logic
        boolean updated = false;
        synchronized (mLock) {
            for (PackagePreferences p : mPackagePreferences.values()) {
                if (userIds.contains(UserHandle.getUserId(p.uid))) {
                    for (int type : changedTypes) {
                        String channelId = NotificationChannel.getChannelIdForBundleType(type);
                        NotificationChannel c = p.channels.get(channelId);
                        if (c != null && c.isDeleted() != shouldBeDeleted) {
                            c.setDeleted(shouldBeDeleted);
                            c.setDeletedTimeMs(shouldBeDeleted ? System.currentTimeMillis() : -1);
                            updated = true;
                        }
                    }
                }
            }
        }
        if (updated) {
            // We shouldn't need to sort upon update: if any current notifications are affected
            // they should be reclassified as part of the enable/disable operation.
            if (android.app.Flags.nmBinderPerfCacheChannels()) {
                invalidateNotificationChannelCache();
            }
        }
    }

    public boolean shouldHideSilentStatusIcons() {
        return mHideSilentStatusBarIcons;
    }

    public void setHideSilentStatusIcons(boolean hide) {
        mHideSilentStatusBarIcons = hide;
    }

    public void updateFixedImportance(List<UserInfo> users) {
        for (UserInfo user : users) {
            List<PackageInfo> packages = mPm.getInstalledPackagesAsUser(
                    0, user.getUserHandle().getIdentifier());
            for (PackageInfo pi : packages) {
                boolean fixed = mPermissionHelper.isPermissionFixed(
                        pi.packageName, user.getUserHandle().getIdentifier());
                if (fixed) {
                    synchronized (mLock) {
                        PackagePreferences p = getOrCreatePackagePreferencesLocked(
                                pi.packageName, pi.applicationInfo.uid);
                        p.fixedImportance = true;
                        for (NotificationChannel channel : p.channels.values()) {
                            channel.setImportanceLockedByCriticalDeviceFunction(true);
                        }
                    }
                }
            }
        }
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            invalidateNotificationChannelCache();
        }
    }

    public void updateDefaultApps(int userId, ArraySet<String> toRemove,
            ArraySet<Pair<String, Integer>> toAdd) {
        synchronized (mLock) {
            for (PackagePreferences p : mPackagePreferences.values()) {
                if (userId == UserHandle.getUserId(p.uid)) {
                    if (toRemove != null && toRemove.contains(p.pkg)) {
                        p.defaultAppLockedImportance = false;
                        if (!p.fixedImportance) {
                            for (NotificationChannel channel : p.channels.values()) {
                                channel.setImportanceLockedByCriticalDeviceFunction(false);
                            }
                        }
                    }
                }
            }
            if (toAdd != null) {
                for (Pair<String, Integer> approvedApp : toAdd) {
                    PackagePreferences p = getOrCreatePackagePreferencesLocked(
                            approvedApp.first,
                            approvedApp.second);
                    p.defaultAppLockedImportance = true;
                    for (NotificationChannel channel : p.channels.values()) {
                        channel.setImportanceLockedByCriticalDeviceFunction(true);
                    }
                }
            }
        }
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            invalidateNotificationChannelCache();
        }
    }

    public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg,
            int uid, String groupId, boolean includeDeleted) {
        Objects.requireNonNull(pkg);
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null || groupId == null || !r.groups.containsKey(groupId)) {
                return null;
            }
            return NotificationChannelGroupsHelper.getGroupWithChannels(groupId,
                    r.channels.values(), r.groups, includeDeleted);
        }
    }

    public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg,
            int uid) {
        Objects.requireNonNull(pkg);
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return null;
            }
            return r.groups.get(groupId);
        }
    }

    public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
            int uid, NotificationChannelGroupsHelper.Params params) {
        Objects.requireNonNull(pkg);
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return ParceledListSlice.emptyList();
            }
            return new ParceledListSlice<>(
                    NotificationChannelGroupsHelper.getGroupsWithChannels(r.channels.values(),
                            r.groups, params));
        }
    }

    public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid,
            String groupId) {
        List<NotificationChannel> deletedChannels = new ArrayList<>();
        boolean groupBypassedDnd = false;
        boolean deleted = false;
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null || TextUtils.isEmpty(groupId)) {
                return deletedChannels;
            }

            NotificationChannelGroup channelGroup = r.groups.remove(groupId);
            if (channelGroup != null) {
                deleted = true;
                mNotificationChannelLogger.logNotificationChannelGroupDeleted(channelGroup, uid,
                        pkg);
            }

            int N = r.channels.size();
            for (int i = 0; i < N; i++) {
                final NotificationChannel nc = r.channels.valueAt(i);
                if (groupId.equals(nc.getGroup())) {
                    groupBypassedDnd |= nc.canBypassDnd();
                    deleteNotificationChannelLocked(nc, pkg, uid);
                    deletedChannels.add(nc);
                }
            }
        }
        if (groupBypassedDnd) {
            updateCurrentUserHasPriorityChannels();
        }
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            if (deletedChannels.size() > 0) {
                invalidateNotificationChannelCache();
            }
            if (deleted) {
                invalidateNotificationChannelGroupCache();
            }
        }
        return deletedChannels;
    }

    /**
     * Returns all notification channel groups for the provided package and uid, without channel
     * information included. Note that this method returns the object instances from the internal
     * structure; do not modify the returned groups before copying or parceling.
     */
    @Override
    public Collection<NotificationChannelGroup> getNotificationChannelGroupsWithoutChannels(
            String pkg, int uid) {
        List<NotificationChannelGroup> groups = new ArrayList<>();
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return groups;
            }
            groups.addAll(r.groups.values());
        }
        return groups;
    }

    public NotificationChannelGroup getGroupForChannel(String pkg, int uid, String channelId) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(pkg, uid);
            if (p != null) {
                NotificationChannel nc = p.channels.get(channelId);
                if (nc != null && !nc.isDeleted()) {
                    if (nc.getGroup() != null) {
                        NotificationChannelGroup group = p.groups.get(nc.getGroup());
                        if (group != null) {
                            return group;
                        }
                    }
                }
            }
        }
        return null;
    }

    public ArrayList<ConversationChannelWrapper> getConversations(IntArray userIds,
            boolean onlyImportant) {
        synchronized (mLock) {
            ArrayList<ConversationChannelWrapper> conversations = new ArrayList<>();
            for (PackagePreferences p : mPackagePreferences.values()) {
                if (userIds.binarySearch(UserHandle.getUserId(p.uid)) >= 0) {
                    int N = p.channels.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationChannel nc = p.channels.valueAt(i);
                        if (!TextUtils.isEmpty(nc.getConversationId()) && !nc.isDeleted()
                                && !nc.isDemoted()
                                && (nc.isImportantConversation() || !onlyImportant)) {
                            ConversationChannelWrapper conversation =
                                    new ConversationChannelWrapper();
                            conversation.setPkg(p.pkg);
                            conversation.setUid(p.uid);
                            conversation.setNotificationChannel(nc);
                            NotificationChannel parent = p.channels.get(nc.getParentChannelId());
                            conversation.setParentChannelLabel(parent == null
                                    ? null
                                    : parent.getName());
                            boolean blockedByGroup = false;
                            if (nc.getGroup() != null) {
                                NotificationChannelGroup group = p.groups.get(nc.getGroup());
                                if (group != null) {
                                    if (group.isBlocked()) {
                                        blockedByGroup = true;
                                    } else {
                                        conversation.setGroupLabel(group.getName());
                                    }
                                }
                            }
                            if (!blockedByGroup) {
                                conversations.add(conversation);
                            }
                        }
                    }
                }
            }

            return conversations;
        }
    }

    public ArrayList<ConversationChannelWrapper> getConversations(String pkg, int uid) {
        Objects.requireNonNull(pkg);
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return new ArrayList<>();
            }
            ArrayList<ConversationChannelWrapper> conversations = new ArrayList<>();
            int N = r.channels.size();
            for (int i = 0; i < N; i++) {
                final NotificationChannel nc = r.channels.valueAt(i);
                if (!TextUtils.isEmpty(nc.getConversationId())
                        && !nc.isDeleted()
                        && !nc.isDemoted()) {
                    ConversationChannelWrapper conversation = new ConversationChannelWrapper();
                    conversation.setPkg(r.pkg);
                    conversation.setUid(r.uid);
                    conversation.setNotificationChannel(nc);
                    NotificationChannel parent = r.channels.get(nc.getParentChannelId());
                    conversation.setParentChannelLabel(parent == null
                            ? null
                            : parent.getName());
                    boolean blockedByGroup = false;
                    if (nc.getGroup() != null) {
                        NotificationChannelGroup group = r.groups.get(nc.getGroup());
                        if (group != null) {
                            if (group.isBlocked()) {
                                blockedByGroup = true;
                            } else {
                                conversation.setGroupLabel(group.getName());
                            }
                        }
                    }
                    if (!blockedByGroup) {
                        conversations.add(conversation);
                    }
                }
            }

            return conversations;
        }
    }

    public @NonNull List<String> deleteConversations(String pkg, int uid,
            Set<String> conversationIds) {
        List<String> deletedChannelIds = new ArrayList<>();
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return deletedChannelIds;
            }
            int N = r.channels.size();
            for (int i = 0; i < N; i++) {
                final NotificationChannel nc = r.channels.valueAt(i);
                if (nc.getConversationId() != null
                        && conversationIds.contains(nc.getConversationId())) {
                    nc.setDeleted(true);
                    nc.setDeletedTimeMs(System.currentTimeMillis());
                    LogMaker lm = getChannelLog(nc, pkg);
                    lm.setType(
                            com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_CLOSE);
                    MetricsLogger.action(lm);
                    mNotificationChannelLogger.logNotificationChannelDeleted(nc, uid, pkg);

                    deletedChannelIds.add(nc.getId());
                }
            }
        }
        if (!deletedChannelIds.isEmpty()) {
            if (mCurrentUserHasPriorityChannels) {
                updateCurrentUserHasPriorityChannels();
            }
            if (android.app.Flags.nmBinderPerfCacheChannels()) {
                invalidateNotificationChannelCache();
            }
        }
        return deletedChannelIds;
    }

    @Override
    public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
            boolean includeDeleted, boolean includeBundles) {
        Objects.requireNonNull(pkg);
        if (uid == INVALID_UID) {
            return ParceledListSlice.emptyList();
        }
        List<NotificationChannel> channels = new ArrayList<>();
        synchronized (mLock) {
            PackagePreferences r;
            if (android.app.Flags.nmBinderPerfCacheChannels()) {
                r = getOrCreatePackagePreferencesLocked(pkg, uid);
            } else {
                r = getPackagePreferencesLocked(pkg, uid);
            }
            if (r == null) {
                return ParceledListSlice.emptyList();
            }
            int N = r.channels.size();
            for (int i = 0; i < N; i++) {
                final NotificationChannel nc = r.channels.valueAt(i);
                if (includeDeleted || !nc.isDeleted()) {
                    if (includeBundles || !SYSTEM_RESERVED_IDS.contains(nc.getId())) {
                        channels.add(nc);
                    }
                }
            }
            return new ParceledListSlice<>(channels);
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    // Gets the entire list of notification channels for this package, with no filtering and
    // without creating package preferences. For testing only, specifically to confirm the
    // notification channels of a removed/deleted package.
    protected List<NotificationChannel> getRemovedPkgNotificationChannels(String pkg, int uid) {
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null || r.channels == null) {
                return new ArrayList<>();
            }
            return new ArrayList<>(r.channels.values());
        }
    }

    /**
     * Gets all notification channels associated with the given pkg and uid that can bypass dnd
     */
    public ParceledListSlice<NotificationChannel> getNotificationChannelsBypassingDnd(String pkg,
            int uid) {
        List<NotificationChannel> channels = new ArrayList<>();
        synchronized (mLock) {
            final PackagePreferences r = mPackagePreferences.get(
                    packagePreferencesKey(pkg, uid));
            if (r != null) {
                for (NotificationChannel channel : r.channels.values()) {
                    if (channelIsLiveLocked(r, channel) && channel.canBypassDnd()) {
                        channels.add(channel);
                    }
                }
            }
        }
        return new ParceledListSlice<>(channels);
    }

    /**
     * Gets all apps that can bypass DND, and a boolean indicating whether all (true) or some
     * (false) of its notification channels can currently bypass.
     */
    public @NonNull ArrayList<ZenBypassingApp> getPackagesBypassingDnd(@UserIdInt int userId) {
        ArrayList<ZenBypassingApp> bypassing = new ArrayList<>();
        synchronized (mLock) {
            for (PackagePreferences p : mPackagePreferences.values()) {
                if (UserHandle.getUserId(p.uid) != userId) {
                    continue;
                }
                int totalChannelCount = p.channels.size();
                int bypassingCount = 0;
                if  (totalChannelCount == 0) {
                    continue;
                }
                for (NotificationChannel channel : p.channels.values()) {
                    if (channelIsLiveLocked(p, channel) && channel.canBypassDnd()) {
                        bypassingCount++;
                    }
                }
                if (bypassingCount > 0) {
                    bypassing.add(new ZenBypassingApp(p.pkg, totalChannelCount == bypassingCount));
                }
            }
        }
        return bypassing;
    }

    /**
     * Gets all apps for this user that have a nonzero number of channels. This count does not
     * include deleted channels.
     */
    @FlaggedApi(android.app.Flags.FLAG_NM_BINDER_PERF_GET_APPS_WITH_CHANNELS)
    public @NonNull List<String> getPackagesWithAnyChannels(@UserIdInt int userId) {
        List<String> pkgs = new ArrayList<>();
        synchronized (mLock) {
            for (PackagePreferences p : mPackagePreferences.values()) {
                if (UserHandle.getUserId(p.uid) != userId) {
                    continue;
                }
                for (NotificationChannel c : p.channels.values()) {
                    if (!c.isDeleted()) {
                        pkgs.add(p.pkg);
                        break;
                    }
                }
            }
        }
        return pkgs;
    }

    /**
     * True for pre-O apps that only have the default channel, or pre O apps that have no
     * channels yet. This method will create the default channel for pre-O apps that don't have it.
     * Should never be true for O+ targeting apps, but that's enforced on boot/when an app
     * upgrades.
     */
    public boolean onlyHasDefaultChannel(String pkg, int uid) {
        synchronized (mLock) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
            if (r.channels.containsKey(DEFAULT_CHANNEL_ID)) {
                if (r.channels.size() == 1) {
                    return true;
                }
                if (notificationClassification()) {
                    if (r.channels.size() <= 5) {
                        for (NotificationChannel c : r.channels.values()) {
                            if (!SYSTEM_RESERVED_IDS.contains(c.getId()) &&
                                    !DEFAULT_CHANNEL_ID.equals(c.getId())) {
                                return false;
                            }
                            return true;
                        }
                    } else {
                        return false;
                    }
                }
            }
            return false;
        }
    }

    public int getDeletedChannelCount(String pkg, int uid) {
        Objects.requireNonNull(pkg);
        int deletedCount = 0;
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return deletedCount;
            }
            int N = r.channels.size();
            for (int i = 0; i < N; i++) {
                final NotificationChannel nc = r.channels.valueAt(i);
                if (nc.isDeleted()) {
                    deletedCount++;
                }
            }
            return deletedCount;
        }
    }

    public int getBlockedChannelCount(String pkg, int uid) {
        Objects.requireNonNull(pkg);
        int blockedCount = 0;
        synchronized (mLock) {
            PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
            if (r == null) {
                return blockedCount;
            }
            int N = r.channels.size();
            for (int i = 0; i < N; i++) {
                final NotificationChannel nc = r.channels.valueAt(i);
                if (!nc.isDeleted() && IMPORTANCE_NONE == nc.getImportance()) {
                    blockedCount++;
                }
            }
            return blockedCount;
        }
    }

    /**
     * Syncs {@link #mCurrentUserHasPriorityChannels} with the current user's notification
     * policy before updating. Must be called:
     * <ul>
     *     <li>On system init, after channels and DND configurations are loaded.
     *     <li>When the current user is switched, after the corresponding DND config is loaded.
     *     <li>If users are removed (the removed user could've been a profile of the current one).
     * </ul>
     */
    void syncHasPriorityChannels() {
        mCurrentUserHasPriorityChannels =
                (mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).state
                        & NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS) != 0;

        updateCurrentUserHasPriorityChannels();
    }

    /**
     * Updates the user's NotificationPolicy based on whether the current userId has channels
     * marked as "priority" (which might bypass DND, depending on the zen rule details). It should
     * be called whenever a channel is created, updated, or deleted, or when the current user (or
     * its profiles) change.
     */
    private void updateCurrentUserHasPriorityChannels() {
        ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>();

        final IntArray currentUserIds = mUserProfiles.getCurrentProfileIds();
        synchronized (mLock) {
            final int numPackagePreferences = mPackagePreferences.size();
            for (int i = 0; i < numPackagePreferences; i++) {
                final PackagePreferences r = mPackagePreferences.valueAt(i);
                if (!currentUserIds.contains(UserHandle.getUserId(r.uid))) {
                    continue; // Package isn't associated with any profile of the current userId.
                }

                for (NotificationChannel channel : r.channels.values()) {
                    if (channelIsLiveLocked(r, channel) && channel.canBypassDnd()) {
                        candidatePkgs.add(new Pair<>(r.pkg, r.uid));
                        break;
                    }
                }
            }
        }
        for (int i = candidatePkgs.size() - 1; i >= 0; i--) {
            Pair<String, Integer> app = candidatePkgs.valueAt(i);
            if (!mPermissionHelper.hasPermission(app.second)) {
                candidatePkgs.removeAt(i);
            }
        }
        boolean haveBypassingApps = candidatePkgs.size() > 0;
        if (mCurrentUserHasPriorityChannels != haveBypassingApps) {
            mCurrentUserHasPriorityChannels = haveBypassingApps;
            mZenModeHelper.updateHasPriorityChannels(UserHandle.CURRENT,
                    mCurrentUserHasPriorityChannels);
        }
    }

    @GuardedBy("mLock")
    private boolean channelIsLiveLocked(PackagePreferences pkgPref, NotificationChannel channel) {
        // Channel is in a group that's blocked
        if (isGroupBlocked(pkgPref.pkg, pkgPref.uid, channel.getGroup())) {
            return false;
        }

        // Channel is deleted or is blocked
        if (channel.isDeleted() || channel.getImportance() == IMPORTANCE_NONE) {
            return false;
        }

        return true;
    }

    /**
     * Whether the current user has any channels marked as "priority channels"
     * ({@link NotificationChannel#canBypassDnd}), but not necessarily whether they are permitted
     * to bypass the filters set by the current zen policy.
     */
    public boolean hasPriorityChannels() {
        return mCurrentUserHasPriorityChannels;
    }

    /**
     * Sets whether any notifications from the app, represented by the given {@code pkgName} and
     * {@code uid}, have their importance locked by the user. Locked notifications don't get
     * considered for sentiment adjustments (and thus never show a blocking helper).
     */
    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    void setAppImportanceLocked(String packageName, int uid) {
        synchronized (mLock) {
            PackagePreferences prefs = getOrCreatePackagePreferencesLocked(packageName, uid);
            if ((prefs.lockedAppFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0) {
                return;
            }

            prefs.lockedAppFields =
                    prefs.lockedAppFields | LockableAppFields.USER_LOCKED_IMPORTANCE;
        }
        updateConfig();
    }

    /**
     * Returns the delegate for a given package, if it's allowed by the package and the user.
     */
    public @Nullable String getNotificationDelegate(String sourcePkg, int sourceUid) {
        synchronized (mLock) {
            PackagePreferences prefs = getPackagePreferencesLocked(sourcePkg, sourceUid);

            if (prefs == null || prefs.delegate == null) {
                return null;
            }
            if (!prefs.delegate.mEnabled) {
                return null;
            }
            return prefs.delegate.mPkg;
        }
    }

    /**
     * Used by an app to delegate notification posting privileges to another apps.
     */
    public void setNotificationDelegate(String sourcePkg, int sourceUid,
            String delegatePkg, int delegateUid) {
        synchronized (mLock) {
            PackagePreferences prefs = getOrCreatePackagePreferencesLocked(sourcePkg, sourceUid);
            prefs.delegate = new Delegate(delegatePkg, delegateUid, true);
        }
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            // If package delegates change, then which packages can get what channel information
            // also changes, so we need to clear the cache.
            invalidateNotificationChannelCache();
        }
    }

    /**
     * Used by an app to turn off its notification delegate.
     */
    public void revokeNotificationDelegate(String sourcePkg, int sourceUid) {
        synchronized (mLock) {
            PackagePreferences prefs = getPackagePreferencesLocked(sourcePkg, sourceUid);
            if (prefs != null && prefs.delegate != null) {
                prefs.delegate.mEnabled = false;
            }
        }
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            invalidateNotificationChannelCache();
        }
    }

    /**
     * Returns whether the given app is allowed on post notifications on behalf of the other given
     * app.
     */
    public boolean isDelegateAllowed(String sourcePkg, int sourceUid,
            String potentialDelegatePkg, int potentialDelegateUid) {
        synchronized (mLock) {
            PackagePreferences prefs = getPackagePreferencesLocked(sourcePkg, sourceUid);

            return prefs != null && prefs.isValidDelegate(potentialDelegatePkg,
                    potentialDelegateUid);
        }
    }

    @GuardedBy("mLock")
    private void lockFieldsForUpdateLocked(NotificationChannel original,
            NotificationChannel update) {
        if (original.canBypassDnd() != update.canBypassDnd()) {
            update.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
        }
        if (original.getLockscreenVisibility() != update.getLockscreenVisibility()) {
            update.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY);
        }
        if (original.getImportance() != update.getImportance()) {
            update.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
        }
        if (original.shouldShowLights() != update.shouldShowLights()
                || original.getLightColor() != update.getLightColor()) {
            update.lockFields(NotificationChannel.USER_LOCKED_LIGHTS);
        }
        if (!Objects.equals(original.getSound(), update.getSound())) {
            update.lockFields(NotificationChannel.USER_LOCKED_SOUND);
        }
        if (!Arrays.equals(original.getVibrationPattern(), update.getVibrationPattern())
                || !Objects.equals(original.getVibrationEffect(), update.getVibrationEffect())
                || original.shouldVibrate() != update.shouldVibrate()) {
            update.lockFields(NotificationChannel.USER_LOCKED_VIBRATION);
        }
        if (original.canShowBadge() != update.canShowBadge()) {
            update.lockFields(NotificationChannel.USER_LOCKED_SHOW_BADGE);
        }
        if (original.getAllowBubbles() != update.getAllowBubbles()) {
            update.lockFields(NotificationChannel.USER_LOCKED_ALLOW_BUBBLE);
        }
    }

    public void dump(PrintWriter pw, String prefix,
            @NonNull NotificationManagerService.DumpFilter filter,
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) {
        pw.print(prefix);
        pw.println("per-package config version: " + XML_VERSION);

        pw.println("PackagePreferences:");
        synchronized (mLock) {
            dumpPackagePreferencesLocked(pw, prefix, filter, mPackagePreferences, pkgPermissions);
            pw.println("Restored without uid:");
            dumpPackagePreferencesLocked(pw, prefix, filter, mRestoredWithoutUids, null);
        }
    }

    public void dump(ProtoOutputStream proto,
            @NonNull NotificationManagerService.DumpFilter filter,
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) {
        synchronized (mLock) {
            dumpPackagePreferencesLocked(proto, RankingHelperProto.RECORDS, filter,
                    mPackagePreferences, pkgPermissions);
            dumpPackagePreferencesLocked(proto, RankingHelperProto.RECORDS_RESTORED_WITHOUT_UID,
                    filter, mRestoredWithoutUids, null);
        }
    }

    @GuardedBy("mLock")
    private void dumpPackagePreferencesLocked(PrintWriter pw, String prefix,
            @NonNull NotificationManagerService.DumpFilter filter,
            ArrayMap<String, PackagePreferences> packagePreferences,
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> packagePermissions) {
        // Used for tracking which package preferences we've seen already for notification
        // permission reasons; after handling packages with local preferences, we'll want to dump
        // the ones with notification permissions set but not local prefs.
        Set<Pair<Integer, String>> pkgsWithPermissionsToHandle = null;
        if (packagePermissions != null) {
            pkgsWithPermissionsToHandle = packagePermissions.keySet();
        }
        final int N = packagePreferences.size();
        for (int i = 0; i < N; i++) {
            final PackagePreferences r = packagePreferences.valueAt(i);
            if (filter.matches(r.pkg)) {
                pw.print(prefix);
                pw.print("  AppSettings: ");
                pw.print(r.pkg);
                pw.print(" (");
                pw.print(r.uid == INVALID_UID ? "INVALID_UID" : Integer.toString(r.uid));
                pw.print(')');
                Pair<Integer, String> key = new Pair<>(r.uid, r.pkg);
                if (packagePermissions != null && pkgsWithPermissionsToHandle.contains(key)) {
                    pw.print(" importance=");
                    pw.print(NotificationListenerService.Ranking.importanceToString(
                            packagePermissions.get(key).first
                                    ? IMPORTANCE_DEFAULT : IMPORTANCE_NONE));
                    pw.print(" userSet=");
                    pw.print(packagePermissions.get(key).second);
                    pkgsWithPermissionsToHandle.remove(key);
                }
                if (r.priority != DEFAULT_PRIORITY) {
                    pw.print(" priority=");
                    pw.print(Notification.priorityToString(r.priority));
                }
                if (r.visibility != DEFAULT_VISIBILITY) {
                    pw.print(" visibility=");
                    pw.print(Notification.visibilityToString(r.visibility));
                }
                if (r.showBadge != DEFAULT_SHOW_BADGE) {
                    pw.print(" showBadge=");
                    pw.print(r.showBadge);
                }
                if (r.defaultAppLockedImportance != DEFAULT_APP_LOCKED_IMPORTANCE) {
                    pw.print(" defaultAppLocked=");
                    pw.print(r.defaultAppLockedImportance);
                }
                if (r.fixedImportance != DEFAULT_APP_LOCKED_IMPORTANCE) {
                    pw.print(" fixedImportance=");
                    pw.print(r.fixedImportance);
                }
                if (android.app.Flags.uiRichOngoing()
                        && r.canHavePromotedNotifs != DEFAULT_CAN_HAVE_PROMOTED_NOTIFS) {
                    pw.print(" promoted=");
                    pw.print(r.canHavePromotedNotifs);
                }
                pw.println();
                if (r.delegate != null) {
                    pw.print(prefix);
                    pw.printf("    Delegate: %s (%s) enabled=%s", r.delegate.mPkg, r.delegate.mUid,
                            r.delegate.mEnabled);
                    pw.println();
                }
                for (NotificationChannel channel : r.channels.values()) {
                    pw.print(prefix);
                    channel.dump(pw, "    ", filter.redact);
                }
                for (NotificationChannelGroup group : r.groups.values()) {
                    pw.print(prefix);
                    pw.print("  ");
                    pw.print("  ");
                    pw.println(group);
                }
            }
        }
        // Handle any remaining packages with permissions
        if (pkgsWithPermissionsToHandle != null) {
            for (Pair<Integer, String> p : pkgsWithPermissionsToHandle) {
                // p.first is the uid of this package; p.second is the package name
                if (filter.matches(p.second)) {
                    pw.print(prefix);
                    pw.print("  AppSettings: ");
                    pw.print(p.second);
                    pw.print(" (");
                    pw.print(p.first == INVALID_UID ? "INVALID_UID" : Integer.toString(p.first));
                    pw.print(')');
                    pw.print(" importance=");
                    pw.print(NotificationListenerService.Ranking.importanceToString(
                            packagePermissions.get(p).first
                                    ? IMPORTANCE_DEFAULT : IMPORTANCE_NONE));
                    pw.print(" userSet=");
                    pw.print(packagePermissions.get(p).second);
                    pw.println();
                }
            }
        }
    }

    @GuardedBy("mLock")
    private void dumpPackagePreferencesLocked(ProtoOutputStream proto, long fieldId,
            @NonNull NotificationManagerService.DumpFilter filter,
            ArrayMap<String, PackagePreferences> packagePreferences,
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> packagePermissions) {
        Set<Pair<Integer, String>> pkgsWithPermissionsToHandle = null;
        if (packagePermissions != null) {
            pkgsWithPermissionsToHandle = packagePermissions.keySet();
        }

        final int N = packagePreferences.size();
        long fToken;
        for (int i = 0; i < N; i++) {
            final PackagePreferences r = packagePreferences.valueAt(i);
            if (filter.matches(r.pkg)) {
                fToken = proto.start(fieldId);

                proto.write(RankingHelperProto.RecordProto.PACKAGE, r.pkg);
                proto.write(RankingHelperProto.RecordProto.UID, r.uid);
                Pair<Integer, String> key = new Pair<>(r.uid, r.pkg);
                if (packagePermissions != null && pkgsWithPermissionsToHandle.contains(key)) {
                    proto.write(RankingHelperProto.RecordProto.IMPORTANCE,
                            packagePermissions.get(key).first
                                    ? IMPORTANCE_DEFAULT : IMPORTANCE_NONE);
                    pkgsWithPermissionsToHandle.remove(key);
                }
                proto.write(RankingHelperProto.RecordProto.PRIORITY, r.priority);
                proto.write(RankingHelperProto.RecordProto.VISIBILITY, r.visibility);
                proto.write(RankingHelperProto.RecordProto.SHOW_BADGE, r.showBadge);

                for (NotificationChannel channel : r.channels.values()) {
                    channel.dumpDebug(proto, RankingHelperProto.RecordProto.CHANNELS);
                }
                for (NotificationChannelGroup group : r.groups.values()) {
                    group.dumpDebug(proto, RankingHelperProto.RecordProto.CHANNEL_GROUPS);
                }

                proto.end(fToken);
            }
        }

        if (pkgsWithPermissionsToHandle != null) {
            for (Pair<Integer, String> p : pkgsWithPermissionsToHandle) {
                if (filter.matches(p.second)) {
                    fToken = proto.start(fieldId);
                    proto.write(RankingHelperProto.RecordProto.PACKAGE, p.second);
                    proto.write(RankingHelperProto.RecordProto.UID, p.first);
                    proto.write(RankingHelperProto.RecordProto.IMPORTANCE,
                            packagePermissions.get(p).first
                                    ? IMPORTANCE_DEFAULT : IMPORTANCE_NONE);
                    proto.end(fToken);
                }
            }
        }
    }

    /**
     * @return State of the full screen intent permission for this package.
     */
    @VisibleForTesting
    int getFsiState(String pkg, int uid, boolean requestedFSIPermission) {
        if (!requestedFSIPermission) {
            return PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
        }
        final AttributionSource attributionSource =
                new AttributionSource.Builder(uid).setPackageName(pkg).build();

        final int result = mPermissionManager.checkPermissionForPreflight(
                android.Manifest.permission.USE_FULL_SCREEN_INTENT, attributionSource);

        if (result == PermissionManager.PERMISSION_GRANTED) {
            return PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
        }
        return PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
    }

    /**
     * @return True if the current full screen intent permission state for this package was set by
     * the user.
     */
    @VisibleForTesting
    boolean isFsiPermissionUserSet(String pkg, int uid, int fsiState, int currentPermissionFlags) {
        if (fsiState == PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED) {
            return false;
        }
        return (currentPermissionFlags & PackageManager.FLAG_PERMISSION_USER_SET) != 0;
    }

    /**
     * Fills out {@link PackageNotificationPreferences} proto and wraps it in a {@link StatsEvent}.
     */
    public void pullPackagePreferencesStats(List<StatsEvent> events,
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) {
        pullPackagePreferencesStats(events, pkgPermissions, new ArrayMap<>());
    }


    /**
     * Fills out {@link PackageNotificationPreferences} proto and wraps it in a {@link StatsEvent}.
     * @param events Newly filled out StatsEvent protos are added to this list as output.
     * @param pkgPermissions Maps from a pair representing a uid and package to a pair of booleans,
     *                       where the first represents whether the notification permission was
     *                       granted to that package, and the second represents whether the
     *                       permission was user-set.
     * @param adjustmentDeniedPackages A map of user id -> package name -> the set of adjustments
     *                                 that are not allowed for that package/user.
     */
    public void pullPackagePreferencesStats(List<StatsEvent> events,
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions,
            @NonNull Map<Integer, Map<String, List<String>>> adjustmentDeniedPackages) {
        Set<Pair<Integer, String>> pkgsWithPermissionsToHandle = null;
        if (pkgPermissions != null) {
            pkgsWithPermissionsToHandle = pkgPermissions.keySet();
        }
        int pulledEvents = 0;
        synchronized (mLock) {
            for (int i = 0; i < mPackagePreferences.size(); i++) {
                if (pulledEvents > NOTIFICATION_PREFERENCES_PULL_LIMIT) {
                    break;
                }
                pulledEvents++;
                final PackagePreferences r = mPackagePreferences.valueAt(i);

                // collect whether this package's importance info was user-set for later, if needed
                // before the migration is enabled, this will simply default to false in all cases.
                boolean importanceIsUserSet = false;
                // Even if this package's data is not present, we need to write something;
                // default to IMPORTANCE_UNSPECIFIED. If PM doesn't know about the package
                // for some reason, notifications are not allowed, but in logged output we want
                // to distinguish this case from the actually-banned packages.
                int importance = IMPORTANCE_UNSPECIFIED;
                Pair<Integer, String> key = new Pair<>(r.uid, r.pkg);
                if (pkgPermissions != null && pkgsWithPermissionsToHandle.contains(key)) {
                    Pair<Boolean, Boolean> permissionPair = pkgPermissions.get(key);
                    importance = permissionPair.first
                            ? IMPORTANCE_DEFAULT : IMPORTANCE_NONE;
                    // cache the second value for writing later
                    importanceIsUserSet = permissionPair.second;

                    pkgsWithPermissionsToHandle.remove(key);
                }

                final boolean requestedFSIPermission = mPermissionHelper.hasRequestedPermission(
                        android.Manifest.permission.USE_FULL_SCREEN_INTENT, r.pkg, r.uid);

                final int fsiState = getFsiState(r.pkg, r.uid, requestedFSIPermission);

                final int currentPermissionFlags = mPm.getPermissionFlags(
                        android.Manifest.permission.USE_FULL_SCREEN_INTENT, r.pkg,
                        UserHandle.getUserHandleForUid(r.uid));

                final boolean fsiIsUserSet =
                        isFsiPermissionUserSet(r.pkg, r.uid, fsiState,
                                currentPermissionFlags);

                if (!(notificationClassificationUi() || nmSummarization() || nmSummarizationUi())
                        && adjustmentDeniedPackages.keySet().size() > 0) {
                    Slog.w(TAG, "Pkg adjustment types improperly allowed without flag set");
                }

                int[] deniedAdjustmentsForPackage =
                        getDeniedAdjustmentsForPackage(adjustmentDeniedPackages,
                                UserHandle.getUserId(r.uid), r.pkg);

                events.add(FrameworkStatsLog.buildStatsEvent(
                        PACKAGE_NOTIFICATION_PREFERENCES,
                        /* optional int32 uid = 1 [(is_uid) = true] */ r.uid,
                        /* optional int32 importance = 2 */ importance,
                        /* optional int32 visibility = 3 */ r.visibility,
                        /* optional int32 user_locked_fields = 4 */ r.lockedAppFields,
                        /* optional bool user_set_importance = 5 */ importanceIsUserSet,
                        /* optional FsiState fsi_state = 6 */ fsiState,
                        /* optional bool is_fsi_permission_user_set = 7 */ fsiIsUserSet,
                        /* repeated int32 allowed_bundle_types = 8 */ new int[]{},
                        /* repeated AdjustmentKey denied_adjustments = 9 */
                        deniedAdjustmentsForPackage));
            }
        }

        // handle remaining packages with PackageManager permissions but not local settings
        if (pkgPermissions != null) {
            for (Pair<Integer, String> p : pkgsWithPermissionsToHandle) {
                if (pulledEvents > NOTIFICATION_PREFERENCES_PULL_LIMIT) {
                    break;
                }
                pulledEvents++;

                // Because all fields are required in FrameworkStatsLog.buildStatsEvent, we have
                // to fill in default values for all the unspecified fields.
                events.add(FrameworkStatsLog.buildStatsEvent(
                        PACKAGE_NOTIFICATION_PREFERENCES,
                        /* optional int32 uid = 1 [(is_uid) = true] */ p.first,
                        /* optional int32 importance = 2 */ pkgPermissions.get(p).first
                                ? IMPORTANCE_DEFAULT : IMPORTANCE_NONE,
                        /* optional int32 visibility = 3 */ DEFAULT_VISIBILITY,
                        /* optional int32 user_locked_fields = 4 */ DEFAULT_LOCKED_APP_FIELDS,
                        /* optional bool user_set_importance = 5 */ pkgPermissions.get(p).second,
                        /* optional FsiState fsi_state = 6 */ 0,
                        /* optional bool is_fsi_permission_user_set = 7 */ false,
                        /* repeated BundleTypes allowed_bundle_types = 8 */ new int[]{},
                        /* repeated AdjustmentKey denied_adjustments = 9 */ new int[]{}));
            }
        }
    }

    private int[] getDeniedAdjustmentsForPackage(
            @NonNull Map<Integer, Map<String, List<String>>> adjustmentDeniedPackages,
            @UserIdInt int userId, String pkg) {
        if (notificationClassificationUi()) {
            if (adjustmentDeniedPackages.containsKey(userId)) {
                List<String> deniedKeys = adjustmentDeniedPackages.get(userId).getOrDefault(pkg,
                        Collections.EMPTY_LIST);
                int[] out = new int[deniedKeys.size()];
                for (int i = 0; i < deniedKeys.size(); i++) {
                    out[i] = NotificationPullStatsEvent.adjustmentKeyEnum(deniedKeys.get(i));
                }
                return out;
            }
        }
        return new int[]{};
    }

    /**
     * Fills out {@link PackageNotificationChannelPreferences} proto and wraps it in a
     * {@link StatsEvent}.
     */
    public void pullPackageChannelPreferencesStats(List<StatsEvent> events) {
        synchronized (mLock) {
            int totalChannelsPulled = 0;
            for (int i = 0; i < mPackagePreferences.size(); i++) {
                if (totalChannelsPulled > NOTIFICATION_CHANNEL_PULL_LIMIT) {
                    break;
                }
                final PackagePreferences r = mPackagePreferences.valueAt(i);
                for (NotificationChannel channel : r.channels.values()) {
                    if (++totalChannelsPulled > NOTIFICATION_CHANNEL_PULL_LIMIT) {
                        break;
                    }
                    events.add(FrameworkStatsLog.buildStatsEvent(
                            PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES,
                            /* optional int32 uid = 1 [(is_uid) = true] */ r.uid,
                            /* optional string channel_id = 2 */ channel.getId(),
                            /* optional string channel_name = 3 */ channel.getName().toString(),
                            /* optional string description = 4 */ channel.getDescription(),
                            /* optional int32 importance = 5 */ channel.getImportance(),
                            /* optional int32 user_locked_fields = 6 */
                            channel.getUserLockedFields(),
                            /* optional bool is_deleted = 7 */ channel.isDeleted(),
                            /* optional bool is_conversation = 8 */
                            channel.getConversationId() != null,
                            /* optional bool is_demoted_conversation = 9 */ channel.isDemoted(),
                            /* optional bool is_important_conversation = 10 */
                            channel.isImportantConversation()));
                }
            }
        }
    }

    /**
     * Fills out {@link PackageNotificationChannelGroupPreferences} proto and wraps it in a
     * {@link StatsEvent}.
     */
    public void pullPackageChannelGroupPreferencesStats(List<StatsEvent> events) {
        synchronized (mLock) {
            int totalGroupsPulled = 0;
            for (int i = 0; i < mPackagePreferences.size(); i++) {
                if (totalGroupsPulled > NOTIFICATION_CHANNEL_GROUP_PULL_LIMIT) {
                    break;
                }
                final PackagePreferences r = mPackagePreferences.valueAt(i);
                for (NotificationChannelGroup groupChannel : r.groups.values()) {
                    if (++totalGroupsPulled > NOTIFICATION_CHANNEL_GROUP_PULL_LIMIT) {
                        break;
                    }
                    events.add(FrameworkStatsLog.buildStatsEvent(
                            PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES,
                            /* optional int32 uid = 1 [(is_uid) = true] */ r.uid,
                            /* optional string group_id = 2 */ groupChannel.getId(),
                            /* optional string group_name = 3 */ groupChannel.getName().toString(),
                            /* optional string description = 4 */ groupChannel.getDescription(),
                            /* optional bool is_blocked = 5 */ groupChannel.isBlocked(),
                            /* optional int32 user_locked_fields = 6 */
                            groupChannel.getUserLockedFields()));
                }
            }
        }
    }

    public JSONObject dumpJson(NotificationManagerService.DumpFilter filter,
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) {
        JSONObject ranking = new JSONObject();
        JSONArray PackagePreferencess = new JSONArray();
        synchronized (mLock) {
            try {
                ranking.put("noUid", mRestoredWithoutUids.size());
            } catch (JSONException e) {
                // pass
            }
        }

        // Track data that we've handled from the permissions-based list
        Set<Pair<Integer, String>> pkgsWithPermissionsToHandle = null;
        if (pkgPermissions != null) {
            pkgsWithPermissionsToHandle = pkgPermissions.keySet();
        }

        synchronized (mLock) {
            final int N = mPackagePreferences.size();
            for (int i = 0; i < N; i++) {
                final PackagePreferences r = mPackagePreferences.valueAt(i);
                if (filter == null || filter.matches(r.pkg)) {
                    JSONObject PackagePreferences = new JSONObject();
                    try {
                        PackagePreferences.put("userId", UserHandle.getUserId(r.uid));
                        PackagePreferences.put("packageName", r.pkg);
                        Pair<Integer, String> key = new Pair<>(r.uid, r.pkg);
                        if (pkgPermissions != null
                                && pkgsWithPermissionsToHandle.contains(key)) {
                            PackagePreferences.put("importance",
                                    NotificationListenerService.Ranking.importanceToString(
                                            pkgPermissions.get(key).first
                                                    ? IMPORTANCE_DEFAULT : IMPORTANCE_NONE));
                            pkgsWithPermissionsToHandle.remove(key);
                        }
                        if (r.priority != DEFAULT_PRIORITY) {
                            PackagePreferences.put("priority",
                                    Notification.priorityToString(r.priority));
                        }
                        if (r.visibility != DEFAULT_VISIBILITY) {
                            PackagePreferences.put("visibility",
                                    Notification.visibilityToString(r.visibility));
                        }
                        if (r.showBadge != DEFAULT_SHOW_BADGE) {
                            PackagePreferences.put("showBadge", Boolean.valueOf(r.showBadge));
                        }
                        JSONArray channels = new JSONArray();
                        for (NotificationChannel channel : r.channels.values()) {
                            channels.put(channel.toJson());
                        }
                        PackagePreferences.put("channels", channels);
                        JSONArray groups = new JSONArray();
                        for (NotificationChannelGroup group : r.groups.values()) {
                            groups.put(group.toJson());
                        }
                        PackagePreferences.put("groups", groups);
                    } catch (JSONException e) {
                        // pass
                    }
                    PackagePreferencess.put(PackagePreferences);
                }
            }
        }

        // handle packages for which there are permissions but no local settings
        if (pkgsWithPermissionsToHandle != null) {
            for (Pair<Integer, String> p : pkgsWithPermissionsToHandle) {
                if (filter == null || filter.matches(p.second)) {
                    JSONObject PackagePreferences = new JSONObject();
                    try {
                        PackagePreferences.put("userId", UserHandle.getUserId(p.first));
                        PackagePreferences.put("packageName", p.second);
                        PackagePreferences.put("importance",
                                NotificationListenerService.Ranking.importanceToString(
                                        pkgPermissions.get(p).first
                                                ? IMPORTANCE_DEFAULT : IMPORTANCE_NONE));
                    } catch (JSONException e) {
                        // pass
                    }
                    PackagePreferencess.put(PackagePreferences);
                }
            }
        }

        try {
            ranking.put("PackagePreferencess", PackagePreferencess);
        } catch (JSONException e) {
            // pass
        }
        return ranking;
    }

    /**
     * Dump only the ban information as structured JSON for the stats collector.
     *
     * This is intentionally redundant with {#link dumpJson} because the old
     * scraper will expect this format.
     *
     * @param filter
     * @return
     */
    public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter,
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) {
        JSONArray bans = new JSONArray();
        Map<Integer, String> packageBans = getPermissionBasedPackageBans(pkgPermissions);
        for (Map.Entry<Integer, String> ban : packageBans.entrySet()) {
            final int userId = UserHandle.getUserId(ban.getKey());
            final String packageName = ban.getValue();
            if (filter == null || filter.matches(packageName)) {
                JSONObject banJson = new JSONObject();
                try {
                    banJson.put("userId", userId);
                    banJson.put("packageName", packageName);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                bans.put(banJson);
            }
        }
        return bans;
    }

    public Map<Integer, String> getPackageBans() {
        synchronized (mLock) {
            final int N = mPackagePreferences.size();
            ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
            for (int i = 0; i < N; i++) {
                final PackagePreferences r = mPackagePreferences.valueAt(i);
                if (r.importance == IMPORTANCE_NONE) {
                    packageBans.put(r.uid, r.pkg);
                }
            }

            return packageBans;
        }
    }

    // Same functionality as getPackageBans by extracting the set of packages from the provided
    // map that are disallowed from sending notifications.
    protected Map<Integer, String> getPermissionBasedPackageBans(
            ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) {
        ArrayMap<Integer, String> packageBans = new ArrayMap<>();
        if (pkgPermissions != null) {
            for (Pair<Integer, String> p : pkgPermissions.keySet()) {
                if (!pkgPermissions.get(p).first) {
                    packageBans.put(p.first, p.second);
                }
            }
        }
        return packageBans;
    }

    /**
     * Dump only the channel information as structured JSON for the stats collector.
     *
     * This is intentionally redundant with {#link dumpJson} because the old
     * scraper will expect this format.
     *
     * @param filter
     * @return
     */
    public JSONArray dumpChannelsJson(NotificationManagerService.DumpFilter filter) {
        JSONArray channels = new JSONArray();
        Map<String, Integer> packageChannels = getPackageChannels();
        for (Map.Entry<String, Integer> channelCount : packageChannels.entrySet()) {
            final String packageName = channelCount.getKey();
            if (filter == null || filter.matches(packageName)) {
                JSONObject channelCountJson = new JSONObject();
                try {
                    channelCountJson.put("packageName", packageName);
                    channelCountJson.put("channelCount", channelCount.getValue());
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                channels.put(channelCountJson);
            }
        }
        return channels;
    }

    private Map<String, Integer> getPackageChannels() {
        ArrayMap<String, Integer> packageChannels = new ArrayMap<>();
        synchronized (mLock) {
            for (int i = 0; i < mPackagePreferences.size(); i++) {
                final PackagePreferences r = mPackagePreferences.valueAt(i);
                int channelCount = 0;
                for (int j = 0; j < r.channels.size(); j++) {
                    if (!r.channels.valueAt(j).isDeleted()) {
                        channelCount++;
                    }
                }
                packageChannels.put(r.pkg, channelCount);
            }
        }
        return packageChannels;
    }

    public void onUserRemoved(int userId) {
        synchronized (mLock) {
            boolean removed = false;
            int N = mPackagePreferences.size();
            for (int i = N - 1; i >= 0; i--) {
                PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
                if (UserHandle.getUserId(PackagePreferences.uid) == userId) {
                    mPackagePreferences.removeAt(i);
                    removed = true;
                }
            }
            if (android.app.Flags.nmBinderPerfCacheChannels() && removed) {
                invalidateNotificationChannelCache();
                invalidateNotificationChannelGroupCache();
            }
        }
    }

    protected void onLocaleChanged(Context context, int userId) {
        synchronized (mLock) {
            boolean updated = false;
            int N = mPackagePreferences.size();
            for (int i = 0; i < N; i++) {
                PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
                if (UserHandle.getUserId(PackagePreferences.uid) == userId) {
                    if (PackagePreferences.channels.containsKey(
                            DEFAULT_CHANNEL_ID)) {
                        PackagePreferences.channels.get(
                                DEFAULT_CHANNEL_ID).setName(
                                context.getResources().getString(
                                        R.string.default_notification_channel_label));
                        updated = true;
                    }
                    // TODO (b/346396459): Localize all reserved channels
                }
            }
            if (android.app.Flags.nmBinderPerfCacheChannels() && updated) {
                invalidateNotificationChannelCache();
            }
        }
    }

    public boolean onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList,
            int[] uidList) {
        if (pkgList == null || pkgList.length == 0) {
            return false; // nothing to do
        }
        boolean updated = false;
        if (removingPackage) {
            // Remove notification settings for uninstalled package
            int size = Math.min(pkgList.length, uidList.length);
            for (int i = 0; i < size; i++) {
                final String pkg = pkgList[i];
                final int uid = uidList[i];
                synchronized (mLock) {
                    mPackagePreferences.remove(packagePreferencesKey(pkg, uid));
                    mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, changeUserId));
                }
                updated = true;
            }
        } else {
            for (String pkg : pkgList) {
                try {
                    // Package install
                    int uid = mPm.getPackageUidAsUser(pkg, changeUserId);
                    PackagePermission p = null;
                    synchronized (mLock) {
                        final PackagePreferences r =
                                mRestoredWithoutUids.get(unrestoredPackageKey(pkg, changeUserId));
                        if (r != null) {
                            r.uid = uid;
                            mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, changeUserId));
                            mPackagePreferences.put(packagePreferencesKey(r.pkg, r.uid), r);

                            // Try to restore any unrestored sound resources
                            for (NotificationChannel channel : r.channels.values()) {
                                if (!channel.isSoundRestored()) {
                                    Uri uri = channel.getSound();
                                    Uri restoredUri =
                                            channel.restoreSoundUri(
                                                    mContext,
                                                    uri,
                                                    true,
                                                    channel.getAudioAttributes().getUsage());
                                    if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(
                                            restoredUri)) {
                                        Slog.w(TAG,
                                                "Could not restore sound: " + uri + " for channel: "
                                                        + channel);
                                    }
                                    channel.setSound(restoredUri, channel.getAudioAttributes());
                                }
                            }

                            if (r.migrateToPm) {
                                p = new PackagePermission(
                                        r.pkg, UserHandle.getUserId(r.uid),
                                        r.importance != IMPORTANCE_NONE,
                                        hasUserConfiguredSettings(r));
                            }
                            updated = true;
                        }
                    }
                    if (p != null) {
                        mPermissionHelper.setNotificationPermission(p);
                    }
                } catch (Exception e) {
                    Slog.e(TAG, "could not restore " + pkg, e);
                }
                // Package upgrade
                try {
                    synchronized (mLock) {
                        PackagePreferences fullPackagePreferences = getPackagePreferencesLocked(pkg,
                                mPm.getPackageUidAsUser(pkg, changeUserId));
                        if (fullPackagePreferences != null) {
                            updated |= createDefaultChannelIfNeededLocked(fullPackagePreferences);
                            updated |= deleteDefaultChannelIfNeededLocked(fullPackagePreferences);
                        }
                    }
                } catch (PackageManager.NameNotFoundException e) {
                }
            }
        }

        if (updated) {
            updateConfig();
            if (android.app.Flags.nmBinderPerfCacheChannels()) {
                invalidateNotificationChannelCache();
                invalidateNotificationChannelGroupCache();
            }
        }
        return updated;
    }

    public void clearData(String pkg, int uid) {
        synchronized (mLock) {
            PackagePreferences p = getPackagePreferencesLocked(pkg, uid);
            if (p != null) {
                p.channels = new ArrayMap<>();
                p.groups = new ArrayMap<>();
                p.delegate = null;
                p.lockedAppFields = DEFAULT_LOCKED_APP_FIELDS;
                p.bubblePreference = DEFAULT_BUBBLE_PREFERENCE;
                p.importance = DEFAULT_IMPORTANCE;
                p.priority = DEFAULT_PRIORITY;
                p.visibility = DEFAULT_VISIBILITY;
                p.showBadge = DEFAULT_SHOW_BADGE;
                if (android.app.Flags.nmBinderPerfCacheChannels()) {
                    invalidateNotificationChannelCache();
                    invalidateNotificationChannelGroupCache();
                }
            }
        }
    }

    private LogMaker getChannelLog(NotificationChannel channel, String pkg) {
        return new LogMaker(
                com.android.internal.logging.nano.MetricsProto.MetricsEvent
                        .ACTION_NOTIFICATION_CHANNEL)
                .setType(com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_UPDATE)
                .setPackageName(pkg)
                .addTaggedData(
                        com.android.internal.logging.nano.MetricsProto.MetricsEvent
                                .FIELD_NOTIFICATION_CHANNEL_ID,
                        channel.getId())
                .addTaggedData(
                        com.android.internal.logging.nano.MetricsProto.MetricsEvent
                                .FIELD_NOTIFICATION_CHANNEL_IMPORTANCE,
                        channel.getImportance());
    }

    private LogMaker getChannelGroupLog(String groupId, String pkg) {
        return new LogMaker(
                com.android.internal.logging.nano.MetricsProto.MetricsEvent
                        .ACTION_NOTIFICATION_CHANNEL_GROUP)
                .setType(com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_UPDATE)
                .addTaggedData(
                        com.android.internal.logging.nano.MetricsProto.MetricsEvent
                                .FIELD_NOTIFICATION_CHANNEL_GROUP_ID,
                        groupId)
                .setPackageName(pkg);
    }

    /** Requests check of the feature setting for showing media notifications in quick settings. */
    public void updateMediaNotificationFilteringEnabled() {
        // TODO(b/192412820): Consolidate SHOW_MEDIA_ON_QUICK_SETTINGS into compile-time value.
        final boolean newValue = Settings.Global.getInt(mContext.getContentResolver(),
                Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) > 0
                        && mContext.getResources().getBoolean(
                                R.bool.config_quickSettingsShowMediaPlayer);
        if (newValue != mIsMediaNotificationFilteringEnabled) {
            mIsMediaNotificationFilteringEnabled = newValue;
            updateConfig();
        }
    }

    /** Returns true if the setting is enabled for showing media notifications in quick settings. */
    public boolean isMediaNotificationFilteringEnabled() {
        return mIsMediaNotificationFilteringEnabled;
    }

    public void updateBadgingEnabled() {
        if (mBadgingEnabled == null) {
            mBadgingEnabled = new SparseBooleanArray();
        }
        boolean changed = false;
        // update the cached values
        for (int index = 0; index < mBadgingEnabled.size(); index++) {
            int userId = mBadgingEnabled.keyAt(index);
            final boolean oldValue = mBadgingEnabled.get(userId);
            final boolean newValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                    Settings.Secure.NOTIFICATION_BADGING,
                    DEFAULT_SHOW_BADGE ? 1 : 0, userId) != 0;
            mBadgingEnabled.put(userId, newValue);
            changed |= oldValue != newValue;
        }
        if (changed) {
            updateConfig();
        }
    }

    public boolean badgingEnabled(UserHandle userHandle) {
        int userId = userHandle.getIdentifier();
        if (userId == UserHandle.USER_ALL) {
            return false;
        }
        if (mBadgingEnabled.indexOfKey(userId) < 0) {
            mBadgingEnabled.put(userId,
                    Settings.Secure.getIntForUser(mContext.getContentResolver(),
                            Settings.Secure.NOTIFICATION_BADGING,
                            DEFAULT_SHOW_BADGE ? 1 : 0, userId) != 0);
        }
        return mBadgingEnabled.get(userId, DEFAULT_SHOW_BADGE);
    }

    /** Updates whether bubbles are enabled for this user. */
    public void updateBubblesEnabled() {
        if (mBubblesEnabled == null) {
            mBubblesEnabled = new SparseBooleanArray();
        }
        boolean changed = false;
        // update the cached values
        for (int index = 0; index < mBubblesEnabled.size(); index++) {
            int userId = mBubblesEnabled.keyAt(index);
            final boolean oldValue = mBubblesEnabled.get(userId);
            final boolean newValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                    Settings.Secure.NOTIFICATION_BUBBLES,
                    DEFAULT_BUBBLES_ENABLED ? 1 : 0, userId) != 0;
            mBubblesEnabled.put(userId, newValue);
            changed |= oldValue != newValue;
        }
        if (changed) {
            updateConfig();
        }
    }

    /** Returns true if bubbles are enabled for this user. */
    public boolean bubblesEnabled(UserHandle userHandle) {
        int userId = userHandle.getIdentifier();
        if (userId == UserHandle.USER_ALL) {
            return false;
        }
        if (mBubblesEnabled.indexOfKey(userId) < 0) {
            mBubblesEnabled.put(userId,
                    Settings.Secure.getIntForUser(mContext.getContentResolver(),
                            Settings.Secure.NOTIFICATION_BUBBLES,
                            DEFAULT_BUBBLES_ENABLED ? 1 : 0, userId) != 0);
        }
        return mBubblesEnabled.get(userId, DEFAULT_BUBBLES_ENABLED);
    }

    public void updateLockScreenPrivateNotifications() {
        if (mLockScreenPrivateNotifications == null) {
            mLockScreenPrivateNotifications = new SparseBooleanArray();
        }
        boolean changed = false;
        // update the cached values
        for (int index = 0; index < mLockScreenPrivateNotifications.size(); index++) {
            int userId = mLockScreenPrivateNotifications.keyAt(index);
            final boolean oldValue = mLockScreenPrivateNotifications.get(userId);
            final boolean newValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                    Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1, userId) != 0;
            mLockScreenPrivateNotifications.put(userId, newValue);
            changed |= oldValue != newValue;
        }
        if (changed) {
            updateConfig();
        }
    }

    public void updateLockScreenShowNotifications() {
        if (mLockScreenShowNotifications == null) {
            mLockScreenShowNotifications = new SparseBooleanArray();
        }
        boolean changed = false;
        // update the cached values
        for (int index = 0; index < mLockScreenShowNotifications.size(); index++) {
            int userId = mLockScreenShowNotifications.keyAt(index);
            final boolean oldValue = mLockScreenShowNotifications.get(userId);
            final boolean newValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                    Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, userId) != 0;
            mLockScreenShowNotifications.put(userId, newValue);
            changed |= oldValue != newValue;
        }
        if (changed) {
            updateConfig();
        }
    }

    @Override
    public boolean canShowNotificationsOnLockscreen(int userId) {
        if (mLockScreenShowNotifications == null) {
            mLockScreenShowNotifications = new SparseBooleanArray();
        }
        return mLockScreenShowNotifications.get(userId, true);
    }

    @Override
    public boolean canShowPrivateNotificationsOnLockScreen(int userId) {
        if (mLockScreenPrivateNotifications == null) {
            mLockScreenPrivateNotifications = new SparseBooleanArray();
        }
        return mLockScreenPrivateNotifications.get(userId, true);
    }

    public void unlockAllNotificationChannels() {
        synchronized (mLock) {
            final int numPackagePreferences = mPackagePreferences.size();
            for (int i = 0; i < numPackagePreferences; i++) {
                final PackagePreferences r = mPackagePreferences.valueAt(i);
                for (NotificationChannel channel : r.channels.values()) {
                    channel.unlockFields(USER_LOCKED_IMPORTANCE);
                }
            }
        }
        if (android.app.Flags.nmBinderPerfCacheChannels()) {
            invalidateNotificationChannelCache();
        }
    }

    public void migrateNotificationPermissions(List<UserInfo> users) {
        for (UserInfo user : users) {
            List<PackageInfo> packages = mPm.getInstalledPackagesAsUser(
                    PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL),
                    user.getUserHandle().getIdentifier());
            for (PackageInfo pi : packages) {
                synchronized (mLock) {
                    PackagePreferences p = getOrCreatePackagePreferencesLocked(
                            pi.packageName, pi.applicationInfo.uid);
                    if (p.migrateToPm && p.uid != INVALID_UID) {
                        try {
                            PackagePermission pkgPerm = new PackagePermission(
                                    p.pkg, UserHandle.getUserId(p.uid),
                                    p.importance != IMPORTANCE_NONE,
                                    hasUserConfiguredSettings(p));
                            mPermissionHelper.setNotificationPermission(pkgPerm);
                        } catch (Exception e) {
                            Slog.e(TAG, "could not migrate setting for " + p.pkg, e);
                        }
                    }
                }
            }
        }
    }

    private void updateConfig() {
        mRankingHandler.requestSort();
    }

    @VisibleForTesting
    // Utility method for overriding in tests to confirm that the cache gets cleared.
    protected void invalidateNotificationChannelCache() {
        NotificationManager.invalidateNotificationChannelCache();
    }

    @VisibleForTesting
    protected void invalidateNotificationChannelGroupCache() {
        NotificationManager.invalidateNotificationChannelGroupCache();
    }

    private static String packagePreferencesKey(String pkg, int uid) {
        return pkg + "|" + uid;
    }

    private static String unrestoredPackageKey(String pkg, @UserIdInt int userId) {
        return pkg + "|" + userId;
    }

    private static class PackagePreferences {
        String pkg;
        int uid = INVALID_UID;
        int importance = DEFAULT_IMPORTANCE;
        int priority = DEFAULT_PRIORITY;
        int visibility = DEFAULT_VISIBILITY;
        boolean showBadge = DEFAULT_SHOW_BADGE;
        int bubblePreference = DEFAULT_BUBBLE_PREFERENCE;
        int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS;
        // these fields are loaded on boot from a different source of truth and so are not
        // written to notification policy xml
        boolean defaultAppLockedImportance = DEFAULT_APP_LOCKED_IMPORTANCE;
        boolean fixedImportance = DEFAULT_APP_LOCKED_IMPORTANCE;

        boolean hasSentInvalidMessage = false;
        boolean hasSentValidMessage = false;
        // note: only valid while hasSentMessage is false and hasSentInvalidMessage is true
        boolean userDemotedMsgApp = false;
        boolean hasSentValidBubble = false;

        boolean migrateToPm = false;
        long creationTime;

        @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
        // Until we enable the UI, we should return false.
        boolean canHavePromotedNotifs = android.app.Flags.uiRichOngoing();

        @UserIdInt int userIdWhenUidUnknown;

        Delegate delegate = null;
        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
        Map<String, NotificationChannelGroup> groups = new ConcurrentHashMap<>();

        public boolean isValidDelegate(String pkg, int uid) {
            return delegate != null && delegate.isAllowed(pkg, uid);
        }
    }

    private static class Delegate {
        static final boolean DEFAULT_ENABLED = true;

        final String mPkg;
        final int mUid;
        boolean mEnabled;

        Delegate(String pkg, int uid, boolean enabled) {
            mPkg = pkg;
            mUid = uid;
            mEnabled = enabled;
        }

        public boolean isAllowed(String pkg, int uid) {
            if (pkg == null || uid == INVALID_UID) {
                return false;
            }
            return pkg.equals(mPkg)
                    && uid == mUid
                    && mEnabled;
        }
    }
}
