• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.om;
18 
19 import static com.android.server.om.OverlayManagerService.DEBUG;
20 import static com.android.server.om.OverlayManagerService.TAG;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.om.OverlayConstraint;
25 import android.content.om.OverlayIdentifier;
26 import android.content.om.OverlayInfo;
27 import android.os.UserHandle;
28 import android.util.ArrayMap;
29 import android.util.ArraySet;
30 import android.util.IndentingPrintWriter;
31 import android.util.Pair;
32 import android.util.Slog;
33 import android.util.Xml;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.CollectionUtils;
37 import com.android.internal.util.XmlUtils;
38 import com.android.modules.utils.TypedXmlPullParser;
39 import com.android.modules.utils.TypedXmlSerializer;
40 
41 import org.xmlpull.v1.XmlPullParserException;
42 
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.io.OutputStream;
46 import java.io.PrintWriter;
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.List;
50 import java.util.Objects;
51 import java.util.Set;
52 import java.util.function.Consumer;
53 import java.util.function.Predicate;
54 
55 /**
56  * Data structure representing the current state of all overlay packages in the
57  * system.
58  *
59  * Modifications to the data are signaled by returning true from any state mutating method.
60  *
61  * @see OverlayManagerService
62  */
63 final class OverlayManagerSettings {
64     /**
65      * All overlay data for all users and target packages is stored in this list.
66      * This keeps memory down, while increasing the cost of running queries or mutating the
67      * data. This is ok, since changing of overlays is very rare and has larger costs associated
68      * with it.
69      *
70      * The order of the items in the list is important, those with a lower index having a lower
71      * priority.
72      */
73     private final ArrayList<SettingsItem> mItems = new ArrayList<>();
74 
75     @NonNull
init(@onNull 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)76     OverlayInfo init(@NonNull final OverlayIdentifier overlay, final int userId,
77             @NonNull final String targetPackageName, @Nullable final String targetOverlayableName,
78             @NonNull final String baseCodePath, boolean isMutable, boolean isEnabled, int priority,
79             @Nullable String overlayCategory, boolean isFabricated) {
80         remove(overlay, userId);
81         final SettingsItem item = new SettingsItem(overlay, userId, targetPackageName,
82                 targetOverlayableName, baseCodePath, OverlayInfo.STATE_UNKNOWN, isEnabled,
83                 isMutable, priority, overlayCategory, isFabricated,
84                 Collections.emptyList() /* constraints */);
85         insert(item);
86         return item.getOverlayInfo();
87     }
88 
89     /**
90      * Returns true if the settings were modified, false if they remain the same.
91      */
remove(@onNull final OverlayIdentifier overlay, final int userId)92     boolean remove(@NonNull final OverlayIdentifier overlay, final int userId) {
93         final int idx = select(overlay, userId);
94         if (idx < 0) {
95             return false;
96         }
97         mItems.remove(idx);
98         return true;
99     }
100 
getOverlayInfo(@onNull final OverlayIdentifier overlay, final int userId)101     @NonNull OverlayInfo getOverlayInfo(@NonNull final OverlayIdentifier overlay, final int userId)
102             throws BadKeyException {
103         final int idx = select(overlay, userId);
104         if (idx < 0) {
105             throw new BadKeyException(overlay, userId);
106         }
107         return mItems.get(idx).getOverlayInfo();
108     }
109 
110     @Nullable
getNullableOverlayInfo(@onNull final OverlayIdentifier overlay, final int userId)111     OverlayInfo getNullableOverlayInfo(@NonNull final OverlayIdentifier overlay, final int userId) {
112         final int idx = select(overlay, userId);
113         if (idx < 0) {
114             return null;
115         }
116         return mItems.get(idx).getOverlayInfo();
117     }
118 
119     /**
120      * Returns true if the settings were modified, false if they remain the same.
121      */
setBaseCodePath(@onNull final OverlayIdentifier overlay, final int userId, @NonNull final String path)122     boolean setBaseCodePath(@NonNull final OverlayIdentifier overlay, final int userId,
123             @NonNull final String path) throws BadKeyException {
124         final int idx = select(overlay, userId);
125         if (idx < 0) {
126             throw new BadKeyException(overlay, userId);
127         }
128         return mItems.get(idx).setBaseCodePath(path);
129     }
130 
setCategory(@onNull final OverlayIdentifier overlay, final int userId, @Nullable String category)131     boolean setCategory(@NonNull final OverlayIdentifier overlay, final int userId,
132             @Nullable String category) throws BadKeyException {
133         final int idx = select(overlay, userId);
134         if (idx < 0) {
135             throw new BadKeyException(overlay, userId);
136         }
137         return mItems.get(idx).setCategory(category);
138     }
139 
getEnabled(@onNull final OverlayIdentifier overlay, final int userId)140     boolean getEnabled(@NonNull final OverlayIdentifier overlay, final int userId)
141             throws BadKeyException {
142         final int idx = select(overlay, userId);
143         if (idx < 0) {
144             throw new BadKeyException(overlay, userId);
145         }
146         return mItems.get(idx).isEnabled();
147     }
148 
149     /**
150      * Returns true if the settings were modified, false if they remain the same.
151      */
setEnabled(@onNull final OverlayIdentifier overlay, final int userId, final boolean enable)152     boolean setEnabled(@NonNull final OverlayIdentifier overlay, final int userId,
153             final boolean enable) throws BadKeyException {
154         final int idx = select(overlay, userId);
155         if (idx < 0) {
156             throw new BadKeyException(overlay, userId);
157         }
158         return mItems.get(idx).setEnabled(enable);
159     }
160 
setConstraints(@onNull final OverlayIdentifier overlay, final int userId, @NonNull final List<OverlayConstraint> constraints)161     boolean setConstraints(@NonNull final OverlayIdentifier overlay, final int userId,
162             @NonNull final List<OverlayConstraint> constraints) throws BadKeyException {
163         final int idx = select(overlay, userId);
164         if (idx < 0) {
165             throw new BadKeyException(overlay, userId);
166         }
167         return mItems.get(idx).setConstraints(constraints);
168     }
169 
getState(@onNull final OverlayIdentifier overlay, final int userId)170     @OverlayInfo.State int getState(@NonNull final OverlayIdentifier overlay, final int userId)
171             throws BadKeyException {
172         final int idx = select(overlay, userId);
173         if (idx < 0) {
174             throw new BadKeyException(overlay, userId);
175         }
176         return mItems.get(idx).getState();
177     }
178 
179     /**
180      * Returns true if the settings were modified, false if they remain the same.
181      */
setState(@onNull final OverlayIdentifier overlay, final int userId, final @OverlayInfo.State int state)182     boolean setState(@NonNull final OverlayIdentifier overlay, final int userId,
183             final @OverlayInfo.State int state) throws BadKeyException {
184         final int idx = select(overlay, userId);
185         if (idx < 0) {
186             throw new BadKeyException(overlay, userId);
187         }
188         return mItems.get(idx).setState(state);
189     }
190 
getOverlaysForTarget(@onNull final String targetPackageName, final int userId)191     List<OverlayInfo> getOverlaysForTarget(@NonNull final String targetPackageName,
192             final int userId) {
193         final List<SettingsItem> items = selectWhereTarget(targetPackageName, userId);
194         return CollectionUtils.map(items, SettingsItem::getOverlayInfo);
195     }
196 
forEachMatching(int userId, String overlayName, String targetPackageName, @NonNull Consumer<OverlayInfo> consumer)197     void forEachMatching(int userId, String overlayName, String targetPackageName,
198             @NonNull Consumer<OverlayInfo> consumer) {
199         for (int i = 0, n = mItems.size(); i < n; i++) {
200             final SettingsItem item = mItems.get(i);
201             if (item.getUserId() != userId) {
202                 continue;
203             }
204             if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) {
205                 continue;
206             }
207             if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) {
208                 continue;
209             }
210             consumer.accept(item.getOverlayInfo());
211         }
212     }
213 
getOverlaysForUser(final int userId)214     ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) {
215         final List<SettingsItem> items = selectWhereUser(userId);
216 
217         final ArrayMap<String, List<OverlayInfo>> targetInfos = new ArrayMap<>();
218         for (int i = 0, n = items.size(); i < n; i++) {
219             final SettingsItem item = items.get(i);
220             targetInfos.computeIfAbsent(item.mTargetPackageName, (String) -> new ArrayList<>())
221                     .add(item.getOverlayInfo());
222         }
223         return targetInfos;
224     }
225 
getAllBaseCodePaths()226     Set<String> getAllBaseCodePaths() {
227         // Overlays installed for multiple users have the same code path, avoid duplicates with Set.
228         final Set<String> paths = new ArraySet<>();
229         mItems.forEach(item -> paths.add(item.mBaseCodePath));
230         return paths;
231     }
232 
getAllIdentifiersAndBaseCodePaths()233     Set<Pair<OverlayIdentifier, String>> getAllIdentifiersAndBaseCodePaths() {
234         // Overlays installed for multiple users have the same code path, avoid duplicates with Set.
235         final Set<Pair<OverlayIdentifier, String>> set = new ArraySet<>();
236         mItems.forEach(item -> set.add(new Pair<>(item.mOverlay, item.mBaseCodePath)));
237         return set;
238     }
239 
240     @Nullable
getIdentifierAndBaseCodePath(@onNull DumpState dumpState)241     Pair<OverlayIdentifier, String> getIdentifierAndBaseCodePath(@NonNull DumpState dumpState) {
242         if (dumpState.getPackageName() == null) {
243             return null;
244         }
245         OverlayIdentifier id = new OverlayIdentifier(dumpState.getPackageName(),
246                 dumpState.getOverlayName());
247         final int userId = dumpState.getUserId();
248         for (int i = 0; i < mItems.size(); i++) {
249             final var item = mItems.get(i);
250             if (userId != UserHandle.USER_ALL && userId != item.mUserId) {
251                 continue;
252             }
253             if (!id.equals(item.mOverlay)) {
254                 continue;
255             }
256             // Overlays installed for multiple users have the same code path, return first found.
257             return new Pair<>(id, item.mBaseCodePath);
258         }
259         return null;
260     }
261 
262     @NonNull
removeIf(@onNull final Predicate<OverlayInfo> predicate, final int userId)263     List<OverlayInfo> removeIf(@NonNull final Predicate<OverlayInfo> predicate, final int userId) {
264         return removeIf(info -> (predicate.test(info) && info.userId == userId));
265     }
266 
267     @NonNull
removeIf(final @NonNull Predicate<OverlayInfo> predicate)268     List<OverlayInfo> removeIf(final @NonNull Predicate<OverlayInfo> predicate) {
269         List<OverlayInfo> removed = null;
270         for (int i = mItems.size() - 1; i >= 0; i--) {
271             final OverlayInfo info = mItems.get(i).getOverlayInfo();
272             if (predicate.test(info)) {
273                 mItems.remove(i);
274                 removed = CollectionUtils.add(removed, info);
275             }
276         }
277         return CollectionUtils.emptyIfNull(removed);
278     }
279 
getUsers()280     int[] getUsers() {
281         return mItems.stream().mapToInt(SettingsItem::getUserId).distinct().toArray();
282     }
283 
284     /**
285      * Returns true if the settings were modified, false if they remain the same.
286      */
removeUser(final int userId)287     boolean removeUser(final int userId) {
288         return mItems.removeIf(item -> {
289             if (item.getUserId() == userId) {
290                 if (DEBUG) {
291                     Slog.d(TAG, "Removing overlay " + item.mOverlay + " for user " + userId
292                             + " from settings because user was removed");
293                 }
294                 return true;
295             }
296             return false;
297         });
298     }
299 
300     /**
301      * Reassigns the priority of an overlay maintaining the values of the overlays other settings.
302      */
303     void setPriority(@NonNull final OverlayIdentifier overlay, final int userId,
304             final int priority) throws BadKeyException {
305         final int moveIdx = select(overlay, userId);
306         if (moveIdx < 0) {
307             throw new BadKeyException(overlay, userId);
308         }
309 
310         final SettingsItem itemToMove = mItems.get(moveIdx);
311         mItems.remove(moveIdx);
312         itemToMove.setPriority(priority);
313         insert(itemToMove);
314     }
315 
316     /**
317      * Returns true if the settings were modified, false if they remain the same.
318      */
319     boolean setPriority(@NonNull final OverlayIdentifier overlay,
320             @NonNull final OverlayIdentifier newOverlay, final int userId) {
321         if (overlay.equals(newOverlay)) {
322             return false;
323         }
324         final int moveIdx = select(overlay, userId);
325         if (moveIdx < 0) {
326             return false;
327         }
328 
329         final int parentIdx = select(newOverlay, userId);
330         if (parentIdx < 0) {
331             return false;
332         }
333 
334         final SettingsItem itemToMove = mItems.get(moveIdx);
335 
336         // Make sure both packages are targeting the same package.
337         if (!itemToMove.getTargetPackageName().equals(
338                 mItems.get(parentIdx).getTargetPackageName())) {
339             return false;
340         }
341 
342         mItems.remove(moveIdx);
343         final int newParentIdx = select(newOverlay, userId) + 1;
344         mItems.add(newParentIdx, itemToMove);
345         return moveIdx != newParentIdx;
346     }
347 
348     /**
349      * Returns true if the settings were modified, false if they remain the same.
350      */
351     boolean setLowestPriority(@NonNull final OverlayIdentifier overlay, final int userId) {
352         final int idx = select(overlay, userId);
353         if (idx <= 0) {
354             // If the item doesn't exist or is already the lowest, don't change anything.
355             return false;
356         }
357 
358         final SettingsItem item = mItems.get(idx);
359         mItems.remove(item);
360         mItems.add(0, item);
361         return true;
362     }
363 
364     /**
365      * Returns true if the settings were modified, false if they remain the same.
366      */
367     boolean setHighestPriority(@NonNull final OverlayIdentifier overlay, final int userId) {
368         final int idx = select(overlay, userId);
369 
370         // If the item doesn't exist or is already the highest, don't change anything.
371         if (idx < 0 || idx == mItems.size() - 1) {
372             return false;
373         }
374 
375         final SettingsItem item = mItems.get(idx);
376         mItems.remove(idx);
377         mItems.add(item);
378         return true;
379     }
380 
381     /**
382      * Inserts the item into the list of settings items.
383      */
384     private void insert(@NonNull SettingsItem item) {
385         int i;
386         for (i = mItems.size() - 1; i >= 0; i--) {
387             SettingsItem parentItem = mItems.get(i);
388             if (parentItem.mPriority <= item.getPriority()) {
389                 break;
390             }
391         }
392         mItems.add(i + 1, item);
393     }
394 
395     void dump(@NonNull final PrintWriter p, @NonNull DumpState dumpState) {
396         final int userId = dumpState.getUserId();
397         final String packageName = dumpState.getPackageName();
398         final String overlayName = dumpState.getOverlayName();
399         final String field = dumpState.getField();
400         final var pw = new IndentingPrintWriter(p, "  ");
401 
402         for (int i = 0; i < mItems.size(); i++) {
403             final var item = mItems.get(i);
404             if (userId != UserHandle.USER_ALL && userId != item.mUserId) {
405                 continue;
406             }
407             if (packageName != null && !packageName.equals(item.mOverlay.getPackageName())) {
408                 continue;
409             }
410             if (overlayName != null && !overlayName.equals(item.mOverlay.getOverlayName())) {
411                 continue;
412             }
413 
414             if (field != null) {
415                 dumpSettingsItemField(pw, item, field);
416             } else {
417                 dumpSettingsItem(pw, item);
418             }
419         }
420     }
421 
422     private void dumpSettingsItem(@NonNull final IndentingPrintWriter pw,
423             @NonNull final SettingsItem item) {
424         pw.println(item.mOverlay + ":" + item.getUserId() + " {");
425         pw.increaseIndent();
426 
427         pw.println("mPackageName...........: " + item.mOverlay.getPackageName());
428         pw.println("mOverlayName...........: " + item.mOverlay.getOverlayName());
429         pw.println("mUserId................: " + item.getUserId());
430         pw.println("mTargetPackageName.....: " + item.getTargetPackageName());
431         pw.println("mTargetOverlayableName.: " + item.getTargetOverlayableName());
432         pw.println("mBaseCodePath..........: " + item.getBaseCodePath());
433         pw.println("mState.................: " + OverlayInfo.stateToString(item.getState()));
434         pw.println("mIsEnabled.............: " + item.isEnabled());
435         pw.println("mIsMutable.............: " + item.isMutable());
436         pw.println("mPriority..............: " + item.mPriority);
437         pw.println("mCategory..............: " + item.mCategory);
438         pw.println("mIsFabricated..........: " + item.mIsFabricated);
439         pw.println("mConstraints...........: "
440                 + OverlayConstraint.constraintsToString(item.mConstraints));
441 
442         pw.decreaseIndent();
443         pw.println("}");
444     }
445 
446     private void dumpSettingsItemField(@NonNull final IndentingPrintWriter pw,
447             @NonNull final SettingsItem item, @NonNull final String field) {
448         switch (field) {
449             case "packagename":
450                 pw.println(item.mOverlay.getPackageName());
451                 break;
452             case "overlayname":
453                 pw.println(item.mOverlay.getOverlayName());
454                 break;
455             case "userid":
456                 pw.println(item.mUserId);
457                 break;
458             case "targetpackagename":
459                 pw.println(item.mTargetPackageName);
460                 break;
461             case "targetoverlayablename":
462                 pw.println(item.mTargetOverlayableName);
463                 break;
464             case "basecodepath":
465                 pw.println(item.mBaseCodePath);
466                 break;
467             case "state":
468                 pw.println(OverlayInfo.stateToString(item.mState));
469                 break;
470             case "isenabled":
471                 pw.println(item.mIsEnabled);
472                 break;
473             case "ismutable":
474                 pw.println(item.mIsMutable);
475                 break;
476             case "priority":
477                 pw.println(item.mPriority);
478                 break;
479             case "category":
480                 pw.println(item.mCategory);
481                 break;
482         }
483     }
484 
485     void restore(@NonNull final InputStream is) throws IOException, XmlPullParserException {
486         Serializer.restore(mItems, is);
487     }
488 
489     void persist(@NonNull final OutputStream os) throws IOException, XmlPullParserException {
490         Serializer.persist(mItems, os);
491     }
492 
493     @VisibleForTesting
494     static final class Serializer {
495         private static final String TAG_OVERLAYS = "overlays";
496         private static final String TAG_ITEM = "item";
497         private static final String TAG_CONSTRAINT = "constraint";
498 
499         private static final String ATTR_BASE_CODE_PATH = "baseCodePath";
500         private static final String ATTR_IS_ENABLED = "isEnabled";
501         private static final String ATTR_PACKAGE_NAME = "packageName";
502         private static final String ATTR_OVERLAY_NAME = "overlayName";
503         private static final String ATTR_STATE = "state";
504         private static final String ATTR_TARGET_PACKAGE_NAME = "targetPackageName";
505         private static final String ATTR_TARGET_OVERLAYABLE_NAME = "targetOverlayableName";
506         private static final String ATTR_IS_STATIC = "isStatic";
507         private static final String ATTR_PRIORITY = "priority";
508         private static final String ATTR_CATEGORY = "category";
509         private static final String ATTR_USER_ID = "userId";
510         private static final String ATTR_VERSION = "version";
511         private static final String ATTR_IS_FABRICATED = "fabricated";
512 
513         private static final String ATTR_CONSTRAINT_TYPE = "type";
514         private static final String ATTR_CONSTRAINT_VALUE = "value";
515 
516         @VisibleForTesting
517         static final int CURRENT_VERSION = 5;
518 
519         public static void restore(@NonNull final ArrayList<SettingsItem> table,
520                 @NonNull final InputStream is) throws IOException, XmlPullParserException {
521             table.clear();
522             final TypedXmlPullParser parser = Xml.resolvePullParser(is);
523             XmlUtils.beginDocument(parser, TAG_OVERLAYS);
524             final int version = parser.getAttributeInt(null, ATTR_VERSION);
525             if (version != CURRENT_VERSION) {
526                 upgrade(version);
527             }
528 
529             final int depth = parser.getDepth();
530             while (XmlUtils.nextElementWithin(parser, depth)) {
531                 if (TAG_ITEM.equals(parser.getName())) {
532                     final SettingsItem item = restoreRow(parser, depth + 1);
533                     table.add(item);
534                 }
535             }
536         }
537 
538         private static void upgrade(int oldVersion) throws XmlPullParserException {
539             switch (oldVersion) {
540                 case 0:
541                 case 1:
542                 case 2:
543                     // Throw an exception which will cause the overlay file to be ignored
544                     // and overwritten.
545                     throw new XmlPullParserException("old version " + oldVersion + "; ignoring");
546                 case 3:
547                 case 4:
548                     // Upgrading from versions 3 and 4 is not a breaking change, so do not
549                     // ignore the overlay file.
550                     return;
551                 default:
552                     throw new XmlPullParserException("unrecognized version " + oldVersion);
553             }
554         }
555 
556         private static SettingsItem restoreRow(@NonNull final TypedXmlPullParser parser,
557                 final int depth) throws IOException, XmlPullParserException {
558             final OverlayIdentifier overlay = new OverlayIdentifier(
559                     XmlUtils.readStringAttribute(parser, ATTR_PACKAGE_NAME),
560                     XmlUtils.readStringAttribute(parser, ATTR_OVERLAY_NAME));
561             final int userId = parser.getAttributeInt(null, ATTR_USER_ID);
562             final String targetPackageName = XmlUtils.readStringAttribute(parser,
563                     ATTR_TARGET_PACKAGE_NAME);
564             final String targetOverlayableName = XmlUtils.readStringAttribute(parser,
565                     ATTR_TARGET_OVERLAYABLE_NAME);
566             final String baseCodePath = XmlUtils.readStringAttribute(parser, ATTR_BASE_CODE_PATH);
567             final int state = parser.getAttributeInt(null, ATTR_STATE);
568             final boolean isEnabled = parser.getAttributeBoolean(null, ATTR_IS_ENABLED, false);
569             final boolean isStatic = parser.getAttributeBoolean(null, ATTR_IS_STATIC, false);
570             final int priority = parser.getAttributeInt(null, ATTR_PRIORITY);
571             final String category = XmlUtils.readStringAttribute(parser, ATTR_CATEGORY);
572             final boolean isFabricated = parser.getAttributeBoolean(null, ATTR_IS_FABRICATED,
573                     false);
574 
575             final List<OverlayConstraint> constraints = new ArrayList<>();
576             while (XmlUtils.nextElementWithin(parser, depth)) {
577                 if (TAG_CONSTRAINT.equals(parser.getName())) {
578                     final OverlayConstraint constraint = new OverlayConstraint(
579                             parser.getAttributeInt(null, ATTR_CONSTRAINT_TYPE),
580                             parser.getAttributeInt(null, ATTR_CONSTRAINT_VALUE));
581                     constraints.add(constraint);
582                 }
583             }
584 
585             return new SettingsItem(overlay, userId, targetPackageName, targetOverlayableName,
586                     baseCodePath, state, isEnabled, !isStatic, priority, category, isFabricated,
587                     constraints);
588         }
589 
590         public static void persist(@NonNull final ArrayList<SettingsItem> table,
591                 @NonNull final OutputStream os) throws IOException {
592             final TypedXmlSerializer xml = Xml.resolveSerializer(os);
593             xml.startDocument(null, true);
594             xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
595             xml.startTag(null, TAG_OVERLAYS);
596             xml.attributeInt(null, ATTR_VERSION, CURRENT_VERSION);
597 
598             final int n = table.size();
599             for (int i = 0; i < n; i++) {
600                 final SettingsItem item = table.get(i);
601                 persistRow(xml, item);
602             }
603             xml.endTag(null, TAG_OVERLAYS);
604             xml.endDocument();
605         }
606 
607         private static void persistRow(@NonNull final TypedXmlSerializer xml,
608                 @NonNull final SettingsItem item) throws IOException {
609             xml.startTag(null, TAG_ITEM);
610             XmlUtils.writeStringAttribute(xml, ATTR_PACKAGE_NAME, item.mOverlay.getPackageName());
611             XmlUtils.writeStringAttribute(xml, ATTR_OVERLAY_NAME, item.mOverlay.getOverlayName());
612             xml.attributeInt(null, ATTR_USER_ID, item.mUserId);
613             XmlUtils.writeStringAttribute(xml, ATTR_TARGET_PACKAGE_NAME, item.mTargetPackageName);
614             XmlUtils.writeStringAttribute(xml, ATTR_TARGET_OVERLAYABLE_NAME,
615                     item.mTargetOverlayableName);
616             XmlUtils.writeStringAttribute(xml, ATTR_BASE_CODE_PATH, item.mBaseCodePath);
617             xml.attributeInt(null, ATTR_STATE, item.mState);
618             XmlUtils.writeBooleanAttribute(xml, ATTR_IS_ENABLED, item.mIsEnabled);
619             XmlUtils.writeBooleanAttribute(xml, ATTR_IS_STATIC, !item.mIsMutable);
620             xml.attributeInt(null, ATTR_PRIORITY, item.mPriority);
621             XmlUtils.writeStringAttribute(xml, ATTR_CATEGORY, item.mCategory);
622             XmlUtils.writeBooleanAttribute(xml, ATTR_IS_FABRICATED, item.mIsFabricated);
623 
624             for (OverlayConstraint constraint : item.mConstraints) {
625                 xml.startTag(null, TAG_CONSTRAINT);
626                 xml.attributeInt(null, ATTR_CONSTRAINT_TYPE, constraint.getType());
627                 xml.attributeInt(null, ATTR_CONSTRAINT_VALUE, constraint.getValue());
628                 xml.endTag(null, TAG_CONSTRAINT);
629             }
630 
631             xml.endTag(null, TAG_ITEM);
632         }
633     }
634 
635     private static final class SettingsItem {
636         private final int mUserId;
637         private final OverlayIdentifier mOverlay;
638         private final String mTargetPackageName;
639         private final String mTargetOverlayableName;
640         private String mBaseCodePath;
641         private @OverlayInfo.State int mState;
642         private boolean mIsEnabled;
643         private OverlayInfo mCache;
644         private final boolean mIsMutable;
645         private int mPriority;
646         private String mCategory;
647         private final boolean mIsFabricated;
648         @NonNull
649         private List<OverlayConstraint> mConstraints;
650 
651         SettingsItem(@NonNull final OverlayIdentifier overlay, final int userId,
652                 @NonNull final String targetPackageName,
653                 @Nullable final String targetOverlayableName, @NonNull final String baseCodePath,
654                 final @OverlayInfo.State int state, final boolean isEnabled,
655                 final boolean isMutable, final int priority,  @Nullable String category,
656                 final boolean isFabricated, @NonNull final List<OverlayConstraint> constraints) {
657             mOverlay = overlay;
658             mUserId = userId;
659             mTargetPackageName = targetPackageName;
660             mTargetOverlayableName = targetOverlayableName;
661             mBaseCodePath = baseCodePath;
662             mState = state;
663             mIsEnabled = isEnabled;
664             mCategory = category;
665             mCache = null;
666             mIsMutable = isMutable;
667             mPriority = priority;
668             mIsFabricated = isFabricated;
669             Objects.requireNonNull(constraints);
670             mConstraints = constraints;
671         }
672 
673         private String getTargetPackageName() {
674             return mTargetPackageName;
675         }
676 
677         private String getTargetOverlayableName() {
678             return mTargetOverlayableName;
679         }
680 
681         private int getUserId() {
682             return mUserId;
683         }
684 
685         private String getBaseCodePath() {
686             return mBaseCodePath;
687         }
688 
689         private boolean setBaseCodePath(@NonNull final String path) {
690             if (!mBaseCodePath.equals(path)) {
691                 mBaseCodePath = path;
692                 invalidateCache();
693                 return true;
694             }
695             return false;
696         }
697 
698         private @OverlayInfo.State int getState() {
699             return mState;
700         }
701 
702         private boolean setState(final @OverlayInfo.State int state) {
703             if (mState != state) {
704                 mState = state;
705                 invalidateCache();
706                 return true;
707             }
708             return false;
709         }
710 
711         private boolean isEnabled() {
712             return mIsEnabled;
713         }
714 
715         private boolean setEnabled(boolean enable) {
716             if (!mIsMutable) {
717                 return false;
718             }
719 
720             if (mIsEnabled != enable) {
721                 mIsEnabled = enable;
722                 invalidateCache();
723                 return true;
724             }
725             return false;
726         }
727 
728         private boolean setCategory(String category) {
729             if (!Objects.equals(mCategory, category)) {
730                 mCategory = (category == null) ? null : category.intern();
731                 invalidateCache();
732                 return true;
733             }
734             return false;
735         }
736 
737         private boolean setConstraints(@NonNull List<OverlayConstraint> constraints) {
738             Objects.requireNonNull(constraints);
739 
740             if (!mIsMutable) {
741                 return false;
742             }
743 
744             if (!Objects.equals(mConstraints, constraints)) {
745                 mConstraints = constraints;
746                 invalidateCache();
747                 return true;
748             }
749             return false;
750         }
751 
752         private OverlayInfo getOverlayInfo() {
753             if (mCache == null) {
754                 mCache = new OverlayInfo(mOverlay.getPackageName(), mOverlay.getOverlayName(),
755                         mTargetPackageName, mTargetOverlayableName, mCategory, mBaseCodePath,
756                         mState, mUserId, mPriority, mIsMutable, mIsFabricated, mConstraints);
757             }
758             return mCache;
759         }
760 
761         private void setPriority(int priority) {
762             mPriority = priority;
763             invalidateCache();
764         }
765 
766         private void invalidateCache() {
767             mCache = null;
768         }
769 
770         private boolean isMutable() {
771             return mIsMutable;
772         }
773 
774         private int getPriority() {
775             return mPriority;
776         }
777     }
778 
779     private int select(@NonNull final OverlayIdentifier overlay, final int userId) {
780         final int n = mItems.size();
781         for (int i = 0; i < n; i++) {
782             final SettingsItem item = mItems.get(i);
783             if (item.mUserId == userId && item.mOverlay.equals(overlay)) {
784                 return i;
785             }
786         }
787         return -1;
788     }
789 
790     private List<SettingsItem> selectWhereUser(final int userId) {
791         final List<SettingsItem> selectedItems = new ArrayList<>();
792         CollectionUtils.addIf(mItems, selectedItems, i -> i.mUserId == userId);
793         return selectedItems;
794     }
795 
796     private List<SettingsItem> selectWhereOverlay(@NonNull final String packageName,
797             final int userId) {
798         final List<SettingsItem> items = selectWhereUser(userId);
799         items.removeIf(i -> !i.mOverlay.getPackageName().equals(packageName));
800         return items;
801     }
802 
803     private List<SettingsItem> selectWhereTarget(@NonNull final String targetPackageName,
804             final int userId) {
805         final List<SettingsItem> items = selectWhereUser(userId);
806         items.removeIf(i -> !i.getTargetPackageName().equals(targetPackageName));
807         return items;
808     }
809 
810     static final class BadKeyException extends Exception {
811         BadKeyException(@NonNull final OverlayIdentifier overlay, final int userId) {
812             super("Bad key '" + overlay + "' for user " + userId );
813         }
814     }
815 }
816