/*
 * Copyright (C) 2016 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.om;

import static android.content.om.OverlayInfo.STATE_DISABLED;
import static android.content.om.OverlayInfo.STATE_ENABLED;
import static android.content.om.OverlayInfo.STATE_MISSING_TARGET;
import static android.content.om.OverlayInfo.STATE_NO_IDMAP;
import static android.content.om.OverlayInfo.STATE_OVERLAY_IS_BEING_REPLACED;
import static android.content.om.OverlayInfo.STATE_TARGET_IS_BEING_REPLACED;

import static com.android.server.om.OverlayManagerService.DEBUG;
import static com.android.server.om.OverlayManagerService.TAG;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.om.OverlayInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;

import com.android.internal.content.om.OverlayConfig;
import com.android.internal.util.ArrayUtils;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Internal implementation of OverlayManagerService.
 *
 * Methods in this class should only be called by the OverlayManagerService.
 * This class is not thread-safe; the caller is expected to ensure the
 * necessary thread synchronization.
 *
 * @see OverlayManagerService
 */
final class OverlayManagerServiceImpl {
    /**
     * @deprecated Not used. See {@link android.content.om.OverlayInfo#STATE_TARGET_UPGRADING}.
     */
    @Deprecated
    private static final int FLAG_TARGET_IS_BEING_REPLACED = 1 << 0;

    // Flags to use in conjunction with updateState.
    private static final int FLAG_OVERLAY_IS_BEING_REPLACED = 1 << 1;

    private final PackageManagerHelper mPackageManager;
    private final IdmapManager mIdmapManager;
    private final OverlayManagerSettings mSettings;
    private final OverlayConfig mOverlayConfig;
    private final String[] mDefaultOverlays;
    private final OverlayChangeListener mListener;

    /**
     * Helper method to merge the overlay manager's (as read from overlays.xml)
     * and package manager's (as parsed from AndroidManifest.xml files) views
     * on overlays.
     *
     * Both managers are usually in agreement, but especially after an OTA things
     * may differ. The package manager is always providing the truth; the overlay
     * manager has to adapt. Depending on what has changed about an overlay, we
     * should either scrap the overlay manager's previous settings or merge the old
     * settings with the new.
     */
    private boolean mustReinitializeOverlay(@NonNull final PackageInfo theTruth,
            @Nullable final OverlayInfo oldSettings) {
        if (oldSettings == null) {
            return true;
        }
        if (!Objects.equals(theTruth.overlayTarget, oldSettings.targetPackageName)) {
            return true;
        }
        if (!Objects.equals(theTruth.targetOverlayableName, oldSettings.targetOverlayableName)) {
            return true;
        }

        boolean isMutable = isPackageConfiguredMutable(theTruth.packageName);
        if (isMutable != oldSettings.isMutable) {
            return true;
        }

        // If an immutable overlay changes its configured enabled state, reinitialize the overlay.
        if (!isMutable && isPackageConfiguredEnabled(theTruth.packageName)
                != oldSettings.isEnabled()) {
            return true;
        }

        return false;
    }

    OverlayManagerServiceImpl(@NonNull final PackageManagerHelper packageManager,
            @NonNull final IdmapManager idmapManager,
            @NonNull final OverlayManagerSettings settings,
            @NonNull final OverlayConfig overlayConfig,
            @NonNull final String[] defaultOverlays,
            @NonNull final OverlayChangeListener listener) {
        mPackageManager = packageManager;
        mIdmapManager = idmapManager;
        mSettings = settings;
        mOverlayConfig = overlayConfig;
        mDefaultOverlays = defaultOverlays;
        mListener = listener;
    }

    /**
     * Call this to synchronize the Settings for a user with what PackageManager knows about a user.
     * Returns a list of target packages that must refresh their overlays. This list is the union
     * of two sets: the set of targets with currently active overlays, and the
     * set of targets that had, but no longer have, active overlays.
     */
    ArrayList<String> updateOverlaysForUser(final int newUserId) {
        if (DEBUG) {
            Slog.d(TAG, "updateOverlaysForUser newUserId=" + newUserId);
        }

        final Set<String> packagesToUpdateAssets = new ArraySet<>();
        final ArrayMap<String, List<OverlayInfo>> tmp = mSettings.getOverlaysForUser(newUserId);
        final int tmpSize = tmp.size();
        final ArrayMap<String, OverlayInfo> storedOverlayInfos = new ArrayMap<>(tmpSize);
        for (int i = 0; i < tmpSize; i++) {
            final List<OverlayInfo> chunk = tmp.valueAt(i);
            final int chunkSize = chunk.size();
            for (int j = 0; j < chunkSize; j++) {
                final OverlayInfo oi = chunk.get(j);
                storedOverlayInfos.put(oi.packageName, oi);
            }
        }

        // Reset overlays if something critical like the target package name
        // has changed
        List<PackageInfo> overlayPackages = mPackageManager.getOverlayPackages(newUserId);
        final int overlayPackagesSize = overlayPackages.size();
        for (int i = 0; i < overlayPackagesSize; i++) {
            final PackageInfo overlayPackage = overlayPackages.get(i);
            final OverlayInfo oi = storedOverlayInfos.get(overlayPackage.packageName);

            int priority = getPackageConfiguredPriority(overlayPackage.packageName);
            if (mustReinitializeOverlay(overlayPackage, oi)) {
                // if targetPackageName has changed the package that *used* to
                // be the target must also update its assets
                if (oi != null) {
                    packagesToUpdateAssets.add(oi.targetPackageName);
                }

                mSettings.init(overlayPackage.packageName, newUserId,
                        overlayPackage.overlayTarget,
                        overlayPackage.targetOverlayableName,
                        overlayPackage.applicationInfo.getBaseCodePath(),
                        isPackageConfiguredMutable(overlayPackage.packageName),
                        isPackageConfiguredEnabled(overlayPackage.packageName),
                        priority, overlayPackage.overlayCategory);
            } else if (priority != oi.priority) {
                mSettings.setPriority(overlayPackage.packageName, newUserId, priority);
                packagesToUpdateAssets.add(oi.targetPackageName);
            }

            storedOverlayInfos.remove(overlayPackage.packageName);
        }

        // any OverlayInfo left in storedOverlayInfos is no longer
        // installed and should be removed
        final int storedOverlayInfosSize = storedOverlayInfos.size();
        for (int i = 0; i < storedOverlayInfosSize; i++) {
            final OverlayInfo oi = storedOverlayInfos.valueAt(i);
            mSettings.remove(oi.packageName, oi.userId);
            removeIdmapIfPossible(oi);
            packagesToUpdateAssets.add(oi.targetPackageName);
        }

        // make sure every overlay's state is up-to-date; this needs to happen
        // after old overlays have been removed, or we risk removing a
        // legitimate idmap file if a new overlay package has the same apk path
        // as the removed overlay package used to have
        for (int i = 0; i < overlayPackagesSize; i++) {
            final PackageInfo overlayPackage = overlayPackages.get(i);
            try {
                updateState(overlayPackage.overlayTarget, overlayPackage.packageName,
                        newUserId, 0);
            } catch (OverlayManagerSettings.BadKeyException e) {
                Slog.e(TAG, "failed to update settings", e);
                mSettings.remove(overlayPackage.packageName, newUserId);
            }
            packagesToUpdateAssets.add(overlayPackage.overlayTarget);
        }

        // remove target packages that are not installed
        final Iterator<String> iter = packagesToUpdateAssets.iterator();
        while (iter.hasNext()) {
            String targetPackageName = iter.next();
            if (mPackageManager.getPackageInfo(targetPackageName, newUserId) == null) {
                iter.remove();
            }
        }

        // Collect all of the categories in which we have at least one overlay enabled.
        final ArraySet<String> enabledCategories = new ArraySet<>();
        final ArrayMap<String, List<OverlayInfo>> userOverlays =
                mSettings.getOverlaysForUser(newUserId);
        final int userOverlayTargetCount = userOverlays.size();
        for (int i = 0; i < userOverlayTargetCount; i++) {
            final List<OverlayInfo> overlayList = userOverlays.valueAt(i);
            final int overlayCount = overlayList != null ? overlayList.size() : 0;
            for (int j = 0; j < overlayCount; j++) {
                final OverlayInfo oi = overlayList.get(j);
                if (oi.isEnabled()) {
                    enabledCategories.add(oi.category);
                }
            }
        }

        // Enable the default overlay if its category does not have a single overlay enabled.
        for (final String defaultOverlay : mDefaultOverlays) {
            try {
                final OverlayInfo oi = mSettings.getOverlayInfo(defaultOverlay, newUserId);
                if (!enabledCategories.contains(oi.category)) {
                    Slog.w(TAG, "Enabling default overlay '" + defaultOverlay + "' for target '"
                            + oi.targetPackageName + "' in category '" + oi.category + "' for user "
                            + newUserId);
                    mSettings.setEnabled(oi.packageName, newUserId, true);
                    if (updateState(oi.targetPackageName, oi.packageName, newUserId, 0)) {
                        packagesToUpdateAssets.add(oi.targetPackageName);
                    }
                }
            } catch (OverlayManagerSettings.BadKeyException e) {
                Slog.e(TAG, "Failed to set default overlay '" + defaultOverlay + "' for user "
                        + newUserId, e);
            }
        }

        return new ArrayList<>(packagesToUpdateAssets);
    }

    void onUserRemoved(final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onUserRemoved userId=" + userId);
        }
        mSettings.removeUser(userId);
    }

    void onTargetPackageAdded(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onTargetPackageAdded packageName=" + packageName + " userId=" + userId);
        }

        updateAndRefreshOverlaysForTarget(packageName, userId, 0);
    }

    void onTargetPackageChanged(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onTargetPackageChanged packageName=" + packageName + " userId=" + userId);
        }

        updateAndRefreshOverlaysForTarget(packageName, userId, 0);
    }

    void onTargetPackageReplacing(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onTargetPackageReplacing packageName=" + packageName + " userId="
                    + userId);
        }

        updateAndRefreshOverlaysForTarget(packageName, userId, 0);
    }

    void onTargetPackageReplaced(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onTargetPackageReplaced packageName=" + packageName + " userId=" + userId);
        }

        updateAndRefreshOverlaysForTarget(packageName, userId, 0);
    }

    void onTargetPackageRemoved(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onTargetPackageRemoved packageName=" + packageName + " userId=" + userId);
        }

        updateAndRefreshOverlaysForTarget(packageName, userId, 0);
    }

    /**
     * Update the state of any overlays for this target.
     */
    private void updateAndRefreshOverlaysForTarget(@NonNull final String targetPackageName,
            final int userId, final int flags) {
        final List<OverlayInfo> targetOverlays = mSettings.getOverlaysForTarget(targetPackageName,
                userId);

        // Update the state for any overlay that targets this package.
        boolean modified = false;
        for (final OverlayInfo oi : targetOverlays) {
            final PackageInfo overlayPackage = mPackageManager.getPackageInfo(oi.packageName,
                    userId);
            if (overlayPackage == null) {
                modified |= mSettings.remove(oi.packageName, oi.userId);
                removeIdmapIfPossible(oi);
            } else {
                try {
                    modified |= updateState(targetPackageName, oi.packageName, userId, flags);
                } catch (OverlayManagerSettings.BadKeyException e) {
                    Slog.e(TAG, "failed to update settings", e);
                    modified |= mSettings.remove(oi.packageName, userId);
                }
            }
        }

        if (!modified) {
            // Update the overlay paths of the target within package manager if necessary.
            final List<String> enabledOverlayPaths = new ArrayList<>(targetOverlays.size());

            // Framework overlays are first in the overlay paths of a package within PackageManager.
            for (final OverlayInfo oi : mSettings.getOverlaysForTarget("android", userId)) {
                if (oi.isEnabled()) {
                    enabledOverlayPaths.add(oi.baseCodePath);
                }
            }

            for (final OverlayInfo oi : targetOverlays) {
                if (oi.isEnabled()) {
                    enabledOverlayPaths.add(oi.baseCodePath);
                }
            }

            // TODO(): Use getEnabledOverlayPaths(userId, targetPackageName) instead of
            // resourceDirs if in the future resourceDirs contains APKs other than overlays
            PackageInfo packageInfo = mPackageManager.getPackageInfo(targetPackageName, userId);
            ApplicationInfo appInfo = packageInfo == null ? null : packageInfo.applicationInfo;
            String[] resourceDirs = appInfo == null ? null : appInfo.resourceDirs;

            // If the lists aren't the same length, the enabled overlays have changed
            if (ArrayUtils.size(resourceDirs) != enabledOverlayPaths.size()) {
                modified = true;
            } else if (resourceDirs != null) {
                // If any element isn't equal, an overlay or the order of overlays has changed
                for (int index = 0; index < resourceDirs.length; index++) {
                    if (!resourceDirs[index].equals(enabledOverlayPaths.get(index))) {
                        modified = true;
                        break;
                    }
                }
            }
        }

        if (modified) {
            mListener.onOverlaysChanged(targetPackageName, userId);
        }
    }

    void onOverlayPackageAdded(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onOverlayPackageAdded packageName=" + packageName + " userId=" + userId);
        }

        final PackageInfo overlayPackage = mPackageManager.getPackageInfo(packageName, userId);
        if (overlayPackage == null) {
            Slog.w(TAG, "overlay package " + packageName + " was added, but couldn't be found");
            onOverlayPackageRemoved(packageName, userId);
            return;
        }

        mSettings.init(packageName, userId, overlayPackage.overlayTarget,
                overlayPackage.targetOverlayableName,
                overlayPackage.applicationInfo.getBaseCodePath(),
                isPackageConfiguredMutable(overlayPackage.packageName),
                isPackageConfiguredEnabled(overlayPackage.packageName),
                getPackageConfiguredPriority(overlayPackage.packageName),
                overlayPackage.overlayCategory);
        try {
            if (updateState(overlayPackage.overlayTarget, packageName, userId, 0)) {
                mListener.onOverlaysChanged(overlayPackage.overlayTarget, userId);
            }
        } catch (OverlayManagerSettings.BadKeyException e) {
            Slog.e(TAG, "failed to update settings", e);
            mSettings.remove(packageName, userId);
        }
    }

    void onOverlayPackageChanged(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onOverlayPackageChanged packageName=" + packageName + " userId=" + userId);
        }

        try {
            final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
            if (updateState(oi.targetPackageName, packageName, userId, 0)) {
                mListener.onOverlaysChanged(oi.targetPackageName, userId);
            }
        } catch (OverlayManagerSettings.BadKeyException e) {
            Slog.e(TAG, "failed to update settings", e);
        }
    }

    void onOverlayPackageReplacing(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onOverlayPackageReplacing packageName=" + packageName + " userId="
                    + userId);
        }

        try {
            final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
            if (updateState(oi.targetPackageName, packageName, userId,
                        FLAG_OVERLAY_IS_BEING_REPLACED)) {
                removeIdmapIfPossible(oi);
                mListener.onOverlaysChanged(oi.targetPackageName, userId);
            }
        } catch (OverlayManagerSettings.BadKeyException e) {
            Slog.e(TAG, "failed to update settings", e);
        }
    }

    void onOverlayPackageReplaced(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "onOverlayPackageReplaced packageName=" + packageName + " userId="
                    + userId);
        }

        final PackageInfo pkg = mPackageManager.getPackageInfo(packageName, userId);
        if (pkg == null) {
            Slog.w(TAG, "overlay package " + packageName + " was replaced, but couldn't be found");
            onOverlayPackageRemoved(packageName, userId);
            return;
        }

        try {
            final OverlayInfo oldOi = mSettings.getOverlayInfo(packageName, userId);
            if (mustReinitializeOverlay(pkg, oldOi)) {
                if (oldOi != null && !oldOi.targetPackageName.equals(pkg.overlayTarget)) {
                    mListener.onOverlaysChanged(pkg.overlayTarget, userId);
                }
                mSettings.init(packageName, userId, pkg.overlayTarget, pkg.targetOverlayableName,
                        pkg.applicationInfo.getBaseCodePath(),
                        isPackageConfiguredMutable(pkg.packageName),
                        isPackageConfiguredEnabled(pkg.packageName),
                        getPackageConfiguredPriority(pkg.packageName), pkg.overlayCategory);
            }

            if (updateState(pkg.overlayTarget, packageName, userId, 0)) {
                mListener.onOverlaysChanged(pkg.overlayTarget, userId);
            }
        } catch (OverlayManagerSettings.BadKeyException e) {
            Slog.e(TAG, "failed to update settings", e);
        }
    }

    void onOverlayPackageRemoved(@NonNull final String packageName, final int userId) {
        try {
            final OverlayInfo overlayInfo = mSettings.getOverlayInfo(packageName, userId);
            if (mSettings.remove(packageName, userId)) {
                removeIdmapIfPossible(overlayInfo);
                mListener.onOverlaysChanged(overlayInfo.targetPackageName, userId);
            }
        } catch (OverlayManagerSettings.BadKeyException e) {
            Slog.e(TAG, "failed to remove overlay", e);
        }
    }

    OverlayInfo getOverlayInfo(@NonNull final String packageName, final int userId) {
        try {
            return mSettings.getOverlayInfo(packageName, userId);
        } catch (OverlayManagerSettings.BadKeyException e) {
            return null;
        }
    }

    List<OverlayInfo> getOverlayInfosForTarget(@NonNull final String targetPackageName,
            final int userId) {
        return mSettings.getOverlaysForTarget(targetPackageName, userId);
    }

    Map<String, List<OverlayInfo>> getOverlaysForUser(final int userId) {
        return mSettings.getOverlaysForUser(userId);
    }

    boolean setEnabled(@NonNull final String packageName, final boolean enable,
            final int userId) {
        if (DEBUG) {
            Slog.d(TAG, String.format("setEnabled packageName=%s enable=%s userId=%d",
                        packageName, enable, userId));
        }

        final PackageInfo overlayPackage = mPackageManager.getPackageInfo(packageName, userId);
        if (overlayPackage == null) {
            return false;
        }

        try {
            final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
            if (!oi.isMutable) {
                // Ignore immutable overlays.
                return false;
            }

            boolean modified = mSettings.setEnabled(packageName, userId, enable);
            modified |= updateState(oi.targetPackageName, oi.packageName, userId, 0);

            if (modified) {
                mListener.onOverlaysChanged(oi.targetPackageName, userId);
            }
            return true;
        } catch (OverlayManagerSettings.BadKeyException e) {
            return false;
        }
    }

    boolean setEnabledExclusive(@NonNull final String packageName, boolean withinCategory,
            final int userId) {
        if (DEBUG) {
            Slog.d(TAG, String.format("setEnabledExclusive packageName=%s"
                    + " withinCategory=%s userId=%d", packageName, withinCategory, userId));
        }

        final PackageInfo overlayPackage = mPackageManager.getPackageInfo(packageName, userId);
        if (overlayPackage == null) {
            return false;
        }

        try {
            final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
            final String targetPackageName = oi.targetPackageName;

            List<OverlayInfo> allOverlays = getOverlayInfosForTarget(targetPackageName, userId);

            boolean modified = false;

            // Disable all other overlays.
            allOverlays.remove(oi);
            for (int i = 0; i < allOverlays.size(); i++) {
                final OverlayInfo disabledInfo = allOverlays.get(i);
                final String disabledOverlayPackageName = disabledInfo.packageName;
                final PackageInfo disabledOverlayPackageInfo = mPackageManager.getPackageInfo(
                        disabledOverlayPackageName, userId);
                if (disabledOverlayPackageInfo == null) {
                    modified |= mSettings.remove(disabledOverlayPackageName, userId);
                    continue;
                }

                if (!disabledInfo.isMutable) {
                    // Don't touch immutable overlays.
                    continue;
                }
                if (withinCategory && !Objects.equals(disabledOverlayPackageInfo.overlayCategory,
                        oi.category)) {
                    // Don't touch overlays from other categories.
                    continue;
                }

                // Disable the overlay.
                modified |= mSettings.setEnabled(disabledOverlayPackageName, userId, false);
                modified |= updateState(targetPackageName, disabledOverlayPackageName, userId, 0);
            }

            // Enable the selected overlay.
            modified |= mSettings.setEnabled(packageName, userId, true);
            modified |= updateState(targetPackageName, packageName, userId, 0);

            if (modified) {
                mListener.onOverlaysChanged(targetPackageName, userId);
            }
            return true;
        } catch (OverlayManagerSettings.BadKeyException e) {
            return false;
        }
    }

    private boolean isPackageConfiguredMutable(@NonNull final String packageName) {
        return mOverlayConfig.isMutable(packageName);
    }

    private int getPackageConfiguredPriority(@NonNull final String packageName) {
        return mOverlayConfig.getPriority(packageName);
    }

    private boolean isPackageConfiguredEnabled(@NonNull final String packageName) {
        return mOverlayConfig.isEnabled(packageName);
    }

    boolean setPriority(@NonNull final String packageName,
            @NonNull final String newParentPackageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "setPriority packageName=" + packageName + " newParentPackageName="
                    + newParentPackageName + " userId=" + userId);
        }

        if (!isPackageConfiguredMutable(packageName)) {
            return false;
        }

        final PackageInfo overlayPackage = mPackageManager.getPackageInfo(packageName, userId);
        if (overlayPackage == null) {
            return false;
        }

        if (mSettings.setPriority(packageName, newParentPackageName, userId)) {
            mListener.onOverlaysChanged(overlayPackage.overlayTarget, userId);
        }
        return true;
    }

    boolean setHighestPriority(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "setHighestPriority packageName=" + packageName + " userId=" + userId);
        }

        if (!isPackageConfiguredMutable(packageName)) {
            return false;
        }

        final PackageInfo overlayPackage = mPackageManager.getPackageInfo(packageName, userId);
        if (overlayPackage == null) {
            return false;
        }

        if (mSettings.setHighestPriority(packageName, userId)) {
            mListener.onOverlaysChanged(overlayPackage.overlayTarget, userId);
        }
        return true;
    }

    boolean setLowestPriority(@NonNull final String packageName, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "setLowestPriority packageName=" + packageName + " userId=" + userId);
        }

        if (!isPackageConfiguredMutable(packageName)) {
            return false;
        }

        final PackageInfo overlayPackage = mPackageManager.getPackageInfo(packageName, userId);
        if (overlayPackage == null) {
            return false;
        }

        if (mSettings.setLowestPriority(packageName, userId)) {
            mListener.onOverlaysChanged(overlayPackage.overlayTarget, userId);
        }
        return true;
    }

    void dump(@NonNull final PrintWriter pw, @NonNull DumpState dumpState) {
        mSettings.dump(pw, dumpState);
        if (dumpState.getPackageName() == null) {
            pw.println("Default overlays: " + TextUtils.join(";", mDefaultOverlays));
        }
    }

    @NonNull String[] getDefaultOverlayPackages() {
        return mDefaultOverlays;
    }

    void removeIdmapForOverlay(String packageName, int userId) {
        final OverlayInfo oi = mSettings.getOverlayInfo(packageName, userId);
        removeIdmapIfPossible(oi);
    }

    List<String> getEnabledOverlayPackageNames(@NonNull final String targetPackageName,
            final int userId) {
        final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName,
                userId);
        final List<String> paths = new ArrayList<>(overlays.size());
        final int n = overlays.size();
        for (int i = 0; i < n; i++) {
            final OverlayInfo oi = overlays.get(i);
            if (oi.isEnabled()) {
                paths.add(oi.packageName);
            }
        }
        return paths;
    }

    /**
     * Returns true if the settings/state was modified, false otherwise.
     */
    private boolean updateState(@NonNull final String targetPackageName,
            @NonNull final String overlayPackageName, final int userId, final int flags)
            throws OverlayManagerSettings.BadKeyException {

        final PackageInfo targetPackage = mPackageManager.getPackageInfo(targetPackageName, userId);
        final PackageInfo overlayPackage = mPackageManager.getPackageInfo(overlayPackageName,
                userId);

        // Immutable RROs targeting to "android", ie framework-res.apk, are handled by native
        // layers.
        boolean modified = false;
        if (targetPackage != null && overlayPackage != null
                && !("android".equals(targetPackageName)
                    && !isPackageConfiguredMutable(overlayPackageName))) {
            modified |= mIdmapManager.createIdmap(targetPackage, overlayPackage, userId);
        }

        if (overlayPackage != null) {
            modified |= mSettings.setBaseCodePath(overlayPackageName, userId,
                    overlayPackage.applicationInfo.getBaseCodePath());
            modified |= mSettings.setCategory(overlayPackageName, userId,
                    overlayPackage.overlayCategory);
        }

        final @OverlayInfo.State int currentState = mSettings.getState(overlayPackageName, userId);
        final @OverlayInfo.State int newState = calculateNewState(targetPackage, overlayPackage,
                userId, flags);
        if (currentState != newState) {
            if (DEBUG) {
                Slog.d(TAG, String.format("%s:%d: %s -> %s",
                        overlayPackageName, userId,
                        OverlayInfo.stateToString(currentState),
                        OverlayInfo.stateToString(newState)));
            }
            modified |= mSettings.setState(overlayPackageName, userId, newState);
        }
        return modified;
    }

    private @OverlayInfo.State int calculateNewState(@Nullable final PackageInfo targetPackage,
            @Nullable final PackageInfo overlayPackage, final int userId, final int flags)
            throws OverlayManagerSettings.BadKeyException {

        if ((flags & FLAG_TARGET_IS_BEING_REPLACED) != 0) {
            return STATE_TARGET_IS_BEING_REPLACED;
        }

        if ((flags & FLAG_OVERLAY_IS_BEING_REPLACED) != 0) {
            return STATE_OVERLAY_IS_BEING_REPLACED;
        }

        // assert expectation on overlay package: can only be null if the flags are used
        if (DEBUG && overlayPackage == null) {
            throw new IllegalArgumentException("null overlay package not compatible with no flags");
        }

        if (targetPackage == null) {
            return STATE_MISSING_TARGET;
        }

        if (!mIdmapManager.idmapExists(overlayPackage, userId)) {
            return STATE_NO_IDMAP;
        }

        final boolean enabled = mSettings.getEnabled(overlayPackage.packageName, userId);
        return enabled ? STATE_ENABLED : STATE_DISABLED;
    }

    private void removeIdmapIfPossible(@NonNull final OverlayInfo oi) {
        // For a given package, all Android users share the same idmap file.
        // This works because Android currently does not support users to
        // install different versions of the same package. It also means we
        // cannot remove an idmap file if any user still needs it.
        //
        // When/if the Android framework allows different versions of the same
        // package to be installed for different users, idmap file handling
        // should be revised:
        //
        // - an idmap file should be unique for each {user, package} pair
        //
        // - the path to the idmap file should be passed to the native Asset
        //   Manager layers, just like the path to the apk is passed today
        //
        // As part of that change, calls to this method should be replaced by
        // direct calls to IdmapManager.removeIdmap, without looping over all
        // users.

        if (!mIdmapManager.idmapExists(oi)) {
            return;
        }
        final int[] userIds = mSettings.getUsers();
        for (int userId : userIds) {
            try {
                final OverlayInfo tmp = mSettings.getOverlayInfo(oi.packageName, userId);
                if (tmp != null && tmp.isEnabled()) {
                    // someone is still using the idmap file -> we cannot remove it
                    return;
                }
            } catch (OverlayManagerSettings.BadKeyException e) {
                // intentionally left empty
            }
        }
        mIdmapManager.removeIdmap(oi, oi.userId);
    }

    interface OverlayChangeListener {

        /**
         * An event triggered by changes made to overlay state or settings as well as changes that
         * add or remove target packages of overlays.
         **/
        void onOverlaysChanged(@NonNull String targetPackage, int userId);
    }
}
