/*
 * 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 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.OverlayConstraint;
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Pair;
import android.util.Slog;
import android.util.Xml;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 * Data structure representing the current state of all overlay packages in the
 * system.
 *
 * Modifications to the data are signaled by returning true from any state mutating method.
 *
 * @see OverlayManagerService
 */
final class OverlayManagerSettings {
    /**
     * All overlay data for all users and target packages is stored in this list.
     * This keeps memory down, while increasing the cost of running queries or mutating the
     * data. This is ok, since changing of overlays is very rare and has larger costs associated
     * with it.
     *
     * The order of the items in the list is important, those with a lower index having a lower
     * priority.
     */
    private final ArrayList<SettingsItem> mItems = new ArrayList<>();

    @NonNull
    OverlayInfo init(@NonNull final OverlayIdentifier overlay, final int userId,
            @NonNull final String targetPackageName, @Nullable final String targetOverlayableName,
            @NonNull final String baseCodePath, boolean isMutable, boolean isEnabled, int priority,
            @Nullable String overlayCategory, boolean isFabricated) {
        remove(overlay, userId);
        final SettingsItem item = new SettingsItem(overlay, userId, targetPackageName,
                targetOverlayableName, baseCodePath, OverlayInfo.STATE_UNKNOWN, isEnabled,
                isMutable, priority, overlayCategory, isFabricated,
                Collections.emptyList() /* constraints */);
        insert(item);
        return item.getOverlayInfo();
    }

    /**
     * Returns true if the settings were modified, false if they remain the same.
     */
    boolean remove(@NonNull final OverlayIdentifier overlay, final int userId) {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            return false;
        }
        mItems.remove(idx);
        return true;
    }

    @NonNull OverlayInfo getOverlayInfo(@NonNull final OverlayIdentifier overlay, final int userId)
            throws BadKeyException {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            throw new BadKeyException(overlay, userId);
        }
        return mItems.get(idx).getOverlayInfo();
    }

    @Nullable
    OverlayInfo getNullableOverlayInfo(@NonNull final OverlayIdentifier overlay, final int userId) {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            return null;
        }
        return mItems.get(idx).getOverlayInfo();
    }

    /**
     * Returns true if the settings were modified, false if they remain the same.
     */
    boolean setBaseCodePath(@NonNull final OverlayIdentifier overlay, final int userId,
            @NonNull final String path) throws BadKeyException {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            throw new BadKeyException(overlay, userId);
        }
        return mItems.get(idx).setBaseCodePath(path);
    }

    boolean setCategory(@NonNull final OverlayIdentifier overlay, final int userId,
            @Nullable String category) throws BadKeyException {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            throw new BadKeyException(overlay, userId);
        }
        return mItems.get(idx).setCategory(category);
    }

    boolean getEnabled(@NonNull final OverlayIdentifier overlay, final int userId)
            throws BadKeyException {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            throw new BadKeyException(overlay, userId);
        }
        return mItems.get(idx).isEnabled();
    }

    /**
     * Returns true if the settings were modified, false if they remain the same.
     */
    boolean setEnabled(@NonNull final OverlayIdentifier overlay, final int userId,
            final boolean enable) throws BadKeyException {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            throw new BadKeyException(overlay, userId);
        }
        return mItems.get(idx).setEnabled(enable);
    }

    boolean setConstraints(@NonNull final OverlayIdentifier overlay, final int userId,
            @NonNull final List<OverlayConstraint> constraints) throws BadKeyException {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            throw new BadKeyException(overlay, userId);
        }
        return mItems.get(idx).setConstraints(constraints);
    }

    @OverlayInfo.State int getState(@NonNull final OverlayIdentifier overlay, final int userId)
            throws BadKeyException {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            throw new BadKeyException(overlay, userId);
        }
        return mItems.get(idx).getState();
    }

    /**
     * Returns true if the settings were modified, false if they remain the same.
     */
    boolean setState(@NonNull final OverlayIdentifier overlay, final int userId,
            final @OverlayInfo.State int state) throws BadKeyException {
        final int idx = select(overlay, userId);
        if (idx < 0) {
            throw new BadKeyException(overlay, userId);
        }
        return mItems.get(idx).setState(state);
    }

    List<OverlayInfo> getOverlaysForTarget(@NonNull final String targetPackageName,
            final int userId) {
        final List<SettingsItem> items = selectWhereTarget(targetPackageName, userId);
        return CollectionUtils.map(items, SettingsItem::getOverlayInfo);
    }

    void forEachMatching(int userId, String overlayName, String targetPackageName,
            @NonNull Consumer<OverlayInfo> consumer) {
        for (int i = 0, n = mItems.size(); i < n; i++) {
            final SettingsItem item = mItems.get(i);
            if (item.getUserId() != userId) {
                continue;
            }
            if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) {
                continue;
            }
            if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) {
                continue;
            }
            consumer.accept(item.getOverlayInfo());
        }
    }

    ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) {
        final List<SettingsItem> items = selectWhereUser(userId);

        final ArrayMap<String, List<OverlayInfo>> targetInfos = new ArrayMap<>();
        for (int i = 0, n = items.size(); i < n; i++) {
            final SettingsItem item = items.get(i);
            targetInfos.computeIfAbsent(item.mTargetPackageName, (String) -> new ArrayList<>())
                    .add(item.getOverlayInfo());
        }
        return targetInfos;
    }

    Set<String> getAllBaseCodePaths() {
        // Overlays installed for multiple users have the same code path, avoid duplicates with Set.
        final Set<String> paths = new ArraySet<>();
        mItems.forEach(item -> paths.add(item.mBaseCodePath));
        return paths;
    }

    Set<Pair<OverlayIdentifier, String>> getAllIdentifiersAndBaseCodePaths() {
        // Overlays installed for multiple users have the same code path, avoid duplicates with Set.
        final Set<Pair<OverlayIdentifier, String>> set = new ArraySet<>();
        mItems.forEach(item -> set.add(new Pair<>(item.mOverlay, item.mBaseCodePath)));
        return set;
    }

    @Nullable
    Pair<OverlayIdentifier, String> getIdentifierAndBaseCodePath(@NonNull DumpState dumpState) {
        if (dumpState.getPackageName() == null) {
            return null;
        }
        OverlayIdentifier id = new OverlayIdentifier(dumpState.getPackageName(),
                dumpState.getOverlayName());
        final int userId = dumpState.getUserId();
        for (int i = 0; i < mItems.size(); i++) {
            final var item = mItems.get(i);
            if (userId != UserHandle.USER_ALL && userId != item.mUserId) {
                continue;
            }
            if (!id.equals(item.mOverlay)) {
                continue;
            }
            // Overlays installed for multiple users have the same code path, return first found.
            return new Pair<>(id, item.mBaseCodePath);
        }
        return null;
    }

    @NonNull
    List<OverlayInfo> removeIf(@NonNull final Predicate<OverlayInfo> predicate, final int userId) {
        return removeIf(info -> (predicate.test(info) && info.userId == userId));
    }

    @NonNull
    List<OverlayInfo> removeIf(final @NonNull Predicate<OverlayInfo> predicate) {
        List<OverlayInfo> removed = null;
        for (int i = mItems.size() - 1; i >= 0; i--) {
            final OverlayInfo info = mItems.get(i).getOverlayInfo();
            if (predicate.test(info)) {
                mItems.remove(i);
                removed = CollectionUtils.add(removed, info);
            }
        }
        return CollectionUtils.emptyIfNull(removed);
    }

    int[] getUsers() {
        return mItems.stream().mapToInt(SettingsItem::getUserId).distinct().toArray();
    }

    /**
     * Returns true if the settings were modified, false if they remain the same.
     */
    boolean removeUser(final int userId) {
        return mItems.removeIf(item -> {
            if (item.getUserId() == userId) {
                if (DEBUG) {
                    Slog.d(TAG, "Removing overlay " + item.mOverlay + " for user " + userId
                            + " from settings because user was removed");
                }
                return true;
            }
            return false;
        });
    }

    /**
     * Reassigns the priority of an overlay maintaining the values of the overlays other settings.
     */
    void setPriority(@NonNull final OverlayIdentifier overlay, final int userId,
            final int priority) throws BadKeyException {
        final int moveIdx = select(overlay, userId);
        if (moveIdx < 0) {
            throw new BadKeyException(overlay, userId);
        }

        final SettingsItem itemToMove = mItems.get(moveIdx);
        mItems.remove(moveIdx);
        itemToMove.setPriority(priority);
        insert(itemToMove);
    }

    /**
     * Returns true if the settings were modified, false if they remain the same.
     */
    boolean setPriority(@NonNull final OverlayIdentifier overlay,
            @NonNull final OverlayIdentifier newOverlay, final int userId) {
        if (overlay.equals(newOverlay)) {
            return false;
        }
        final int moveIdx = select(overlay, userId);
        if (moveIdx < 0) {
            return false;
        }

        final int parentIdx = select(newOverlay, userId);
        if (parentIdx < 0) {
            return false;
        }

        final SettingsItem itemToMove = mItems.get(moveIdx);

        // Make sure both packages are targeting the same package.
        if (!itemToMove.getTargetPackageName().equals(
                mItems.get(parentIdx).getTargetPackageName())) {
            return false;
        }

        mItems.remove(moveIdx);
        final int newParentIdx = select(newOverlay, userId) + 1;
        mItems.add(newParentIdx, itemToMove);
        return moveIdx != newParentIdx;
    }

    /**
     * Returns true if the settings were modified, false if they remain the same.
     */
    boolean setLowestPriority(@NonNull final OverlayIdentifier overlay, final int userId) {
        final int idx = select(overlay, userId);
        if (idx <= 0) {
            // If the item doesn't exist or is already the lowest, don't change anything.
            return false;
        }

        final SettingsItem item = mItems.get(idx);
        mItems.remove(item);
        mItems.add(0, item);
        return true;
    }

    /**
     * Returns true if the settings were modified, false if they remain the same.
     */
    boolean setHighestPriority(@NonNull final OverlayIdentifier overlay, final int userId) {
        final int idx = select(overlay, userId);

        // If the item doesn't exist or is already the highest, don't change anything.
        if (idx < 0 || idx == mItems.size() - 1) {
            return false;
        }

        final SettingsItem item = mItems.get(idx);
        mItems.remove(idx);
        mItems.add(item);
        return true;
    }

    /**
     * Inserts the item into the list of settings items.
     */
    private void insert(@NonNull SettingsItem item) {
        int i;
        for (i = mItems.size() - 1; i >= 0; i--) {
            SettingsItem parentItem = mItems.get(i);
            if (parentItem.mPriority <= item.getPriority()) {
                break;
            }
        }
        mItems.add(i + 1, item);
    }

    void dump(@NonNull final PrintWriter p, @NonNull DumpState dumpState) {
        final int userId = dumpState.getUserId();
        final String packageName = dumpState.getPackageName();
        final String overlayName = dumpState.getOverlayName();
        final String field = dumpState.getField();
        final var pw = new IndentingPrintWriter(p, "  ");

        for (int i = 0; i < mItems.size(); i++) {
            final var item = mItems.get(i);
            if (userId != UserHandle.USER_ALL && userId != item.mUserId) {
                continue;
            }
            if (packageName != null && !packageName.equals(item.mOverlay.getPackageName())) {
                continue;
            }
            if (overlayName != null && !overlayName.equals(item.mOverlay.getOverlayName())) {
                continue;
            }

            if (field != null) {
                dumpSettingsItemField(pw, item, field);
            } else {
                dumpSettingsItem(pw, item);
            }
        }
    }

    private void dumpSettingsItem(@NonNull final IndentingPrintWriter pw,
            @NonNull final SettingsItem item) {
        pw.println(item.mOverlay + ":" + item.getUserId() + " {");
        pw.increaseIndent();

        pw.println("mPackageName...........: " + item.mOverlay.getPackageName());
        pw.println("mOverlayName...........: " + item.mOverlay.getOverlayName());
        pw.println("mUserId................: " + item.getUserId());
        pw.println("mTargetPackageName.....: " + item.getTargetPackageName());
        pw.println("mTargetOverlayableName.: " + item.getTargetOverlayableName());
        pw.println("mBaseCodePath..........: " + item.getBaseCodePath());
        pw.println("mState.................: " + OverlayInfo.stateToString(item.getState()));
        pw.println("mIsEnabled.............: " + item.isEnabled());
        pw.println("mIsMutable.............: " + item.isMutable());
        pw.println("mPriority..............: " + item.mPriority);
        pw.println("mCategory..............: " + item.mCategory);
        pw.println("mIsFabricated..........: " + item.mIsFabricated);
        pw.println("mConstraints...........: "
                + OverlayConstraint.constraintsToString(item.mConstraints));

        pw.decreaseIndent();
        pw.println("}");
    }

    private void dumpSettingsItemField(@NonNull final IndentingPrintWriter pw,
            @NonNull final SettingsItem item, @NonNull final String field) {
        switch (field) {
            case "packagename":
                pw.println(item.mOverlay.getPackageName());
                break;
            case "overlayname":
                pw.println(item.mOverlay.getOverlayName());
                break;
            case "userid":
                pw.println(item.mUserId);
                break;
            case "targetpackagename":
                pw.println(item.mTargetPackageName);
                break;
            case "targetoverlayablename":
                pw.println(item.mTargetOverlayableName);
                break;
            case "basecodepath":
                pw.println(item.mBaseCodePath);
                break;
            case "state":
                pw.println(OverlayInfo.stateToString(item.mState));
                break;
            case "isenabled":
                pw.println(item.mIsEnabled);
                break;
            case "ismutable":
                pw.println(item.mIsMutable);
                break;
            case "priority":
                pw.println(item.mPriority);
                break;
            case "category":
                pw.println(item.mCategory);
                break;
        }
    }

    void restore(@NonNull final InputStream is) throws IOException, XmlPullParserException {
        Serializer.restore(mItems, is);
    }

    void persist(@NonNull final OutputStream os) throws IOException, XmlPullParserException {
        Serializer.persist(mItems, os);
    }

    @VisibleForTesting
    static final class Serializer {
        private static final String TAG_OVERLAYS = "overlays";
        private static final String TAG_ITEM = "item";
        private static final String TAG_CONSTRAINT = "constraint";

        private static final String ATTR_BASE_CODE_PATH = "baseCodePath";
        private static final String ATTR_IS_ENABLED = "isEnabled";
        private static final String ATTR_PACKAGE_NAME = "packageName";
        private static final String ATTR_OVERLAY_NAME = "overlayName";
        private static final String ATTR_STATE = "state";
        private static final String ATTR_TARGET_PACKAGE_NAME = "targetPackageName";
        private static final String ATTR_TARGET_OVERLAYABLE_NAME = "targetOverlayableName";
        private static final String ATTR_IS_STATIC = "isStatic";
        private static final String ATTR_PRIORITY = "priority";
        private static final String ATTR_CATEGORY = "category";
        private static final String ATTR_USER_ID = "userId";
        private static final String ATTR_VERSION = "version";
        private static final String ATTR_IS_FABRICATED = "fabricated";

        private static final String ATTR_CONSTRAINT_TYPE = "type";
        private static final String ATTR_CONSTRAINT_VALUE = "value";

        @VisibleForTesting
        static final int CURRENT_VERSION = 5;

        public static void restore(@NonNull final ArrayList<SettingsItem> table,
                @NonNull final InputStream is) throws IOException, XmlPullParserException {
            table.clear();
            final TypedXmlPullParser parser = Xml.resolvePullParser(is);
            XmlUtils.beginDocument(parser, TAG_OVERLAYS);
            final int version = parser.getAttributeInt(null, ATTR_VERSION);
            if (version != CURRENT_VERSION) {
                upgrade(version);
            }

            final int depth = parser.getDepth();
            while (XmlUtils.nextElementWithin(parser, depth)) {
                if (TAG_ITEM.equals(parser.getName())) {
                    final SettingsItem item = restoreRow(parser, depth + 1);
                    table.add(item);
                }
            }
        }

        // NOTE: when upgrading the overlays.xml version, ignoring the old state used to be fine,
        // as all important overlays used to be immutable. Now (starting with version 3), there are
        // mutable overlays that capture the system configuration (e.g. 3-button navigation mode),
        // and ignoring the old version is wrong.
        // So for any new version of the serialized format one is supposed to write the code that
        // converts it, instead of throwing an exception and ignoring the whole file.
        private static void upgrade(int oldVersion) throws XmlPullParserException {
            switch (oldVersion) {
                case 0:
                case 1:
                case 2:
                    // Throw an exception which will cause the overlay file to be ignored
                    // and overwritten.
                    throw new XmlPullParserException("old version " + oldVersion + "; ignoring");
                case 3:
                case 4:
                    // Upgrading from versions 3 and 4 is not a breaking change, so do not
                    // ignore the overlay file.
                    return;
                default:
                    throw new XmlPullParserException("unrecognized version " + oldVersion);
            }
        }

        private static SettingsItem restoreRow(@NonNull final TypedXmlPullParser parser,
                final int depth) throws IOException, XmlPullParserException {
            final OverlayIdentifier overlay = new OverlayIdentifier(
                    XmlUtils.readStringAttribute(parser, ATTR_PACKAGE_NAME),
                    XmlUtils.readStringAttribute(parser, ATTR_OVERLAY_NAME));
            final int userId = parser.getAttributeInt(null, ATTR_USER_ID);
            final String targetPackageName = XmlUtils.readStringAttribute(parser,
                    ATTR_TARGET_PACKAGE_NAME);
            final String targetOverlayableName = XmlUtils.readStringAttribute(parser,
                    ATTR_TARGET_OVERLAYABLE_NAME);
            final String baseCodePath = XmlUtils.readStringAttribute(parser, ATTR_BASE_CODE_PATH);
            final int state = parser.getAttributeInt(null, ATTR_STATE);
            final boolean isEnabled = parser.getAttributeBoolean(null, ATTR_IS_ENABLED, false);
            final boolean isStatic = parser.getAttributeBoolean(null, ATTR_IS_STATIC, false);
            final int priority = parser.getAttributeInt(null, ATTR_PRIORITY);
            final String category = XmlUtils.readStringAttribute(parser, ATTR_CATEGORY);
            final boolean isFabricated = parser.getAttributeBoolean(null, ATTR_IS_FABRICATED,
                    false);

            final List<OverlayConstraint> constraints = new ArrayList<>();
            while (XmlUtils.nextElementWithin(parser, depth)) {
                if (TAG_CONSTRAINT.equals(parser.getName())) {
                    final OverlayConstraint constraint = new OverlayConstraint(
                            parser.getAttributeInt(null, ATTR_CONSTRAINT_TYPE),
                            parser.getAttributeInt(null, ATTR_CONSTRAINT_VALUE));
                    constraints.add(constraint);
                }
            }

            return new SettingsItem(overlay, userId, targetPackageName, targetOverlayableName,
                    baseCodePath, state, isEnabled, !isStatic, priority, category, isFabricated,
                    constraints);
        }

        public static void persist(@NonNull final ArrayList<SettingsItem> table,
                @NonNull final OutputStream os) throws IOException {
            final TypedXmlSerializer xml = Xml.resolveSerializer(os);
            xml.startDocument(null, true);
            xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
            xml.startTag(null, TAG_OVERLAYS);
            xml.attributeInt(null, ATTR_VERSION, CURRENT_VERSION);

            final int n = table.size();
            for (int i = 0; i < n; i++) {
                final SettingsItem item = table.get(i);
                persistRow(xml, item);
            }
            xml.endTag(null, TAG_OVERLAYS);
            xml.endDocument();
        }

        private static void persistRow(@NonNull final TypedXmlSerializer xml,
                @NonNull final SettingsItem item) throws IOException {
            xml.startTag(null, TAG_ITEM);
            XmlUtils.writeStringAttribute(xml, ATTR_PACKAGE_NAME, item.mOverlay.getPackageName());
            XmlUtils.writeStringAttribute(xml, ATTR_OVERLAY_NAME, item.mOverlay.getOverlayName());
            xml.attributeInt(null, ATTR_USER_ID, item.mUserId);
            XmlUtils.writeStringAttribute(xml, ATTR_TARGET_PACKAGE_NAME, item.mTargetPackageName);
            XmlUtils.writeStringAttribute(xml, ATTR_TARGET_OVERLAYABLE_NAME,
                    item.mTargetOverlayableName);
            XmlUtils.writeStringAttribute(xml, ATTR_BASE_CODE_PATH, item.mBaseCodePath);
            xml.attributeInt(null, ATTR_STATE, item.mState);
            XmlUtils.writeBooleanAttribute(xml, ATTR_IS_ENABLED, item.mIsEnabled);
            XmlUtils.writeBooleanAttribute(xml, ATTR_IS_STATIC, !item.mIsMutable);
            xml.attributeInt(null, ATTR_PRIORITY, item.mPriority);
            XmlUtils.writeStringAttribute(xml, ATTR_CATEGORY, item.mCategory);
            XmlUtils.writeBooleanAttribute(xml, ATTR_IS_FABRICATED, item.mIsFabricated);

            for (OverlayConstraint constraint : item.mConstraints) {
                xml.startTag(null, TAG_CONSTRAINT);
                xml.attributeInt(null, ATTR_CONSTRAINT_TYPE, constraint.getType());
                xml.attributeInt(null, ATTR_CONSTRAINT_VALUE, constraint.getValue());
                xml.endTag(null, TAG_CONSTRAINT);
            }

            xml.endTag(null, TAG_ITEM);
        }
    }

    private static final class SettingsItem {
        private final int mUserId;
        private final OverlayIdentifier mOverlay;
        private final String mTargetPackageName;
        private final String mTargetOverlayableName;
        private String mBaseCodePath;
        private @OverlayInfo.State int mState;
        private boolean mIsEnabled;
        private OverlayInfo mCache;
        private final boolean mIsMutable;
        private int mPriority;
        private String mCategory;
        private final boolean mIsFabricated;
        @NonNull
        private List<OverlayConstraint> mConstraints;

        SettingsItem(@NonNull final OverlayIdentifier overlay, final int userId,
                @NonNull final String targetPackageName,
                @Nullable final String targetOverlayableName, @NonNull final String baseCodePath,
                final @OverlayInfo.State int state, final boolean isEnabled,
                final boolean isMutable, final int priority,  @Nullable String category,
                final boolean isFabricated, @NonNull final List<OverlayConstraint> constraints) {
            mOverlay = overlay;
            mUserId = userId;
            mTargetPackageName = targetPackageName;
            mTargetOverlayableName = targetOverlayableName;
            mBaseCodePath = baseCodePath;
            mState = state;
            mIsEnabled = isEnabled;
            mCategory = category;
            mCache = null;
            mIsMutable = isMutable;
            mPriority = priority;
            mIsFabricated = isFabricated;
            Objects.requireNonNull(constraints);
            mConstraints = constraints;
        }

        private String getTargetPackageName() {
            return mTargetPackageName;
        }

        private String getTargetOverlayableName() {
            return mTargetOverlayableName;
        }

        private int getUserId() {
            return mUserId;
        }

        private String getBaseCodePath() {
            return mBaseCodePath;
        }

        private boolean setBaseCodePath(@NonNull final String path) {
            if (!mBaseCodePath.equals(path)) {
                mBaseCodePath = path;
                invalidateCache();
                return true;
            }
            return false;
        }

        private @OverlayInfo.State int getState() {
            return mState;
        }

        private boolean setState(final @OverlayInfo.State int state) {
            if (mState != state) {
                mState = state;
                invalidateCache();
                return true;
            }
            return false;
        }

        private boolean isEnabled() {
            return mIsEnabled;
        }

        private boolean setEnabled(boolean enable) {
            if (!mIsMutable) {
                return false;
            }

            if (mIsEnabled != enable) {
                mIsEnabled = enable;
                invalidateCache();
                return true;
            }
            return false;
        }

        private boolean setCategory(String category) {
            if (!Objects.equals(mCategory, category)) {
                mCategory = (category == null) ? null : category.intern();
                invalidateCache();
                return true;
            }
            return false;
        }

        private boolean setConstraints(@NonNull List<OverlayConstraint> constraints) {
            Objects.requireNonNull(constraints);

            if (!mIsMutable) {
                return false;
            }

            if (!Objects.equals(mConstraints, constraints)) {
                mConstraints = constraints;
                invalidateCache();
                return true;
            }
            return false;
        }

        private OverlayInfo getOverlayInfo() {
            if (mCache == null) {
                mCache = new OverlayInfo(mOverlay.getPackageName(), mOverlay.getOverlayName(),
                        mTargetPackageName, mTargetOverlayableName, mCategory, mBaseCodePath,
                        mState, mUserId, mPriority, mIsMutable, mIsFabricated, mConstraints);
            }
            return mCache;
        }

        private void setPriority(int priority) {
            mPriority = priority;
            invalidateCache();
        }

        private void invalidateCache() {
            mCache = null;
        }

        private boolean isMutable() {
            return mIsMutable;
        }

        private int getPriority() {
            return mPriority;
        }
    }

    private int select(@NonNull final OverlayIdentifier overlay, final int userId) {
        final int n = mItems.size();
        for (int i = 0; i < n; i++) {
            final SettingsItem item = mItems.get(i);
            if (item.mUserId == userId && item.mOverlay.equals(overlay)) {
                return i;
            }
        }
        return -1;
    }

    private List<SettingsItem> selectWhereUser(final int userId) {
        final List<SettingsItem> selectedItems = new ArrayList<>();
        CollectionUtils.addIf(mItems, selectedItems, i -> i.mUserId == userId);
        return selectedItems;
    }

    private List<SettingsItem> selectWhereOverlay(@NonNull final String packageName,
            final int userId) {
        final List<SettingsItem> items = selectWhereUser(userId);
        items.removeIf(i -> !i.mOverlay.getPackageName().equals(packageName));
        return items;
    }

    private List<SettingsItem> selectWhereTarget(@NonNull final String targetPackageName,
            final int userId) {
        final List<SettingsItem> items = selectWhereUser(userId);
        items.removeIf(i -> !i.getTargetPackageName().equals(targetPackageName));
        return items;
    }

    static final class BadKeyException extends Exception {
        BadKeyException(@NonNull final OverlayIdentifier overlay, final int userId) {
            super("Bad key '" + overlay + "' for user " + userId );
        }
    }
}
