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.OverlayInfo;
25 import android.util.ArrayMap;
26 import android.util.Slog;
27 import android.util.Xml;
28 
29 import com.android.internal.util.FastXmlSerializer;
30 import com.android.internal.util.IndentingPrintWriter;
31 import com.android.internal.util.XmlUtils;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.io.InputStreamReader;
39 import java.io.OutputStream;
40 import java.io.PrintWriter;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.Objects;
44 import java.util.stream.Collectors;
45 import java.util.stream.Stream;
46 
47 /**
48  * Data structure representing the current state of all overlay packages in the
49  * system.
50  *
51  * Modifications to the data are signaled by returning true from any state mutating method.
52  *
53  * @see OverlayManagerService
54  */
55 final class OverlayManagerSettings {
56     /**
57      * All overlay data for all users and target packages is stored in this list.
58      * This keeps memory down, while increasing the cost of running queries or mutating the
59      * data. This is ok, since changing of overlays is very rare and has larger costs associated
60      * with it.
61      *
62      * The order of the items in the list is important, those with a lower index having a lower
63      * priority.
64      */
65     private final ArrayList<SettingsItem> mItems = new ArrayList<>();
66 
init(@onNull final String packageName, final int userId, @NonNull final String targetPackageName, @NonNull final String baseCodePath, boolean isStatic, int priority, String overlayCategory)67     void init(@NonNull final String packageName, final int userId,
68             @NonNull final String targetPackageName, @NonNull final String baseCodePath,
69             boolean isStatic, int priority, String overlayCategory) {
70         remove(packageName, userId);
71         final SettingsItem item =
72                 new SettingsItem(packageName, userId, targetPackageName, baseCodePath,
73                         isStatic, priority, overlayCategory);
74         if (isStatic) {
75             // All static overlays are always enabled.
76             item.setEnabled(true);
77 
78             int i;
79             for (i = mItems.size() - 1; i >= 0; i--) {
80                 SettingsItem parentItem = mItems.get(i);
81                 if (parentItem.mIsStatic && parentItem.mPriority <= priority) {
82                     break;
83                 }
84             }
85             int pos = i + 1;
86             if (pos == mItems.size()) {
87                 mItems.add(item);
88             } else {
89                 mItems.add(pos, item);
90             }
91         } else {
92             mItems.add(item);
93         }
94     }
95 
96     /**
97      * Returns true if the settings were modified, false if they remain the same.
98      */
remove(@onNull final String packageName, final int userId)99     boolean remove(@NonNull final String packageName, final int userId) {
100         final int idx = select(packageName, userId);
101         if (idx < 0) {
102             return false;
103         }
104 
105         mItems.remove(idx);
106         return true;
107     }
108 
getOverlayInfo(@onNull final String packageName, final int userId)109     @NonNull OverlayInfo getOverlayInfo(@NonNull final String packageName, final int userId)
110             throws BadKeyException {
111         final int idx = select(packageName, userId);
112         if (idx < 0) {
113             throw new BadKeyException(packageName, userId);
114         }
115         return mItems.get(idx).getOverlayInfo();
116     }
117 
118     /**
119      * Returns true if the settings were modified, false if they remain the same.
120      */
setBaseCodePath(@onNull final String packageName, final int userId, @NonNull final String path)121     boolean setBaseCodePath(@NonNull final String packageName, final int userId,
122             @NonNull final String path) throws BadKeyException {
123         final int idx = select(packageName, userId);
124         if (idx < 0) {
125             throw new BadKeyException(packageName, userId);
126         }
127         return mItems.get(idx).setBaseCodePath(path);
128     }
129 
setCategory(@onNull final String packageName, final int userId, @Nullable String category)130     boolean setCategory(@NonNull final String packageName, final int userId,
131             @Nullable String category) throws BadKeyException {
132         final int idx = select(packageName, userId);
133         if (idx < 0) {
134             throw new BadKeyException(packageName, userId);
135         }
136         return mItems.get(idx).setCategory(category);
137     }
138 
getEnabled(@onNull final String packageName, final int userId)139     boolean getEnabled(@NonNull final String packageName, final int userId) throws BadKeyException {
140         final int idx = select(packageName, userId);
141         if (idx < 0) {
142             throw new BadKeyException(packageName, userId);
143         }
144         return mItems.get(idx).isEnabled();
145     }
146 
147     /**
148      * Returns true if the settings were modified, false if they remain the same.
149      */
setEnabled(@onNull final String packageName, final int userId, final boolean enable)150     boolean setEnabled(@NonNull final String packageName, final int userId, final boolean enable)
151             throws BadKeyException {
152         final int idx = select(packageName, userId);
153         if (idx < 0) {
154             throw new BadKeyException(packageName, userId);
155         }
156         return mItems.get(idx).setEnabled(enable);
157     }
158 
getState(@onNull final String packageName, final int userId)159     @OverlayInfo.State int getState(@NonNull final String packageName, final int userId)
160             throws BadKeyException {
161         final int idx = select(packageName, userId);
162         if (idx < 0) {
163             throw new BadKeyException(packageName, userId);
164         }
165         return mItems.get(idx).getState();
166     }
167 
168     /**
169      * Returns true if the settings were modified, false if they remain the same.
170      */
setState(@onNull final String packageName, final int userId, final @OverlayInfo.State int state)171     boolean setState(@NonNull final String packageName, final int userId,
172             final @OverlayInfo.State int state) throws BadKeyException {
173         final int idx = select(packageName, userId);
174         if (idx < 0) {
175             throw new BadKeyException(packageName, userId);
176         }
177         return mItems.get(idx).setState(state);
178     }
179 
getOverlaysForTarget(@onNull final String targetPackageName, final int userId)180     List<OverlayInfo> getOverlaysForTarget(@NonNull final String targetPackageName,
181             final int userId) {
182         // Static RROs targeting "android" are loaded from AssetManager, and so they should be
183         // ignored in OverlayManagerService.
184         return selectWhereTarget(targetPackageName, userId)
185                 .filter((i) -> !(i.isStatic() && "android".equals(i.getTargetPackageName())))
186                 .map(SettingsItem::getOverlayInfo)
187                 .collect(Collectors.toList());
188     }
189 
getOverlaysForUser(final int userId)190     ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) {
191         // Static RROs targeting "android" are loaded from AssetManager, and so they should be
192         // ignored in OverlayManagerService.
193         return selectWhereUser(userId)
194                 .filter((i) -> !(i.isStatic() && "android".equals(i.getTargetPackageName())))
195                 .map(SettingsItem::getOverlayInfo)
196                 .collect(Collectors.groupingBy(info -> info.targetPackageName, ArrayMap::new,
197                         Collectors.toList()));
198     }
199 
getUsers()200     int[] getUsers() {
201         return mItems.stream().mapToInt(SettingsItem::getUserId).distinct().toArray();
202     }
203 
204     /**
205      * Returns true if the settings were modified, false if they remain the same.
206      */
removeUser(final int userId)207     boolean removeUser(final int userId) {
208         boolean removed = false;
209         for (int i = 0; i < mItems.size(); i++) {
210             final SettingsItem item = mItems.get(i);
211             if (item.getUserId() == userId) {
212                 if (DEBUG) {
213                     Slog.d(TAG, "Removing overlay " + item.mPackageName + " for user " + userId
214                             + " from settings because user was removed");
215                 }
216                 mItems.remove(i);
217                 removed = true;
218                 i--;
219             }
220         }
221         return removed;
222     }
223 
224     /**
225      * Returns true if the settings were modified, false if they remain the same.
226      */
setPriority(@onNull final String packageName, @NonNull final String newParentPackageName, final int userId)227     boolean setPriority(@NonNull final String packageName,
228             @NonNull final String newParentPackageName, final int userId) {
229         if (packageName.equals(newParentPackageName)) {
230             return false;
231         }
232         final int moveIdx = select(packageName, userId);
233         if (moveIdx < 0) {
234             return false;
235         }
236 
237         final int parentIdx = select(newParentPackageName, userId);
238         if (parentIdx < 0) {
239             return false;
240         }
241 
242         final SettingsItem itemToMove = mItems.get(moveIdx);
243 
244         // Make sure both packages are targeting the same package.
245         if (!itemToMove.getTargetPackageName().equals(
246                 mItems.get(parentIdx).getTargetPackageName())) {
247             return false;
248         }
249 
250         mItems.remove(moveIdx);
251         final int newParentIdx = select(newParentPackageName, userId) + 1;
252         mItems.add(newParentIdx, itemToMove);
253         return moveIdx != newParentIdx;
254     }
255 
256     /**
257      * Returns true if the settings were modified, false if they remain the same.
258      */
setLowestPriority(@onNull final String packageName, final int userId)259     boolean setLowestPriority(@NonNull final String packageName, final int userId) {
260         final int idx = select(packageName, userId);
261         if (idx <= 0) {
262             // If the item doesn't exist or is already the lowest, don't change anything.
263             return false;
264         }
265 
266         final SettingsItem item = mItems.get(idx);
267         mItems.remove(item);
268         mItems.add(0, item);
269         return true;
270     }
271 
272     /**
273      * Returns true if the settings were modified, false if they remain the same.
274      */
setHighestPriority(@onNull final String packageName, final int userId)275     boolean setHighestPriority(@NonNull final String packageName, final int userId) {
276         final int idx = select(packageName, userId);
277 
278         // If the item doesn't exist or is already the highest, don't change anything.
279         if (idx < 0 || idx == mItems.size() - 1) {
280             return false;
281         }
282 
283         final SettingsItem item = mItems.get(idx);
284         mItems.remove(idx);
285         mItems.add(item);
286         return true;
287     }
288 
dump(@onNull final PrintWriter p)289     void dump(@NonNull final PrintWriter p) {
290         final IndentingPrintWriter pw = new IndentingPrintWriter(p, "  ");
291         pw.println("Settings");
292         pw.increaseIndent();
293 
294         if (mItems.isEmpty()) {
295             pw.println("<none>");
296             return;
297         }
298 
299         final int N = mItems.size();
300         for (int i = 0; i < N; i++) {
301             final SettingsItem item = mItems.get(i);
302             pw.println(item.mPackageName + ":" + item.getUserId() + " {");
303             pw.increaseIndent();
304 
305             pw.print("mPackageName.......: "); pw.println(item.mPackageName);
306             pw.print("mUserId............: "); pw.println(item.getUserId());
307             pw.print("mTargetPackageName.: "); pw.println(item.getTargetPackageName());
308             pw.print("mBaseCodePath......: "); pw.println(item.getBaseCodePath());
309             pw.print("mState.............: "); pw.println(OverlayInfo.stateToString(item.getState()));
310             pw.print("mIsEnabled.........: "); pw.println(item.isEnabled());
311             pw.print("mIsStatic..........: "); pw.println(item.isStatic());
312             pw.print("mPriority..........: "); pw.println(item.mPriority);
313             pw.print("mCategory..........: "); pw.println(item.mCategory);
314 
315             pw.decreaseIndent();
316             pw.println("}");
317         }
318     }
319 
restore(@onNull final InputStream is)320     void restore(@NonNull final InputStream is) throws IOException, XmlPullParserException {
321         Serializer.restore(mItems, is);
322     }
323 
persist(@onNull final OutputStream os)324     void persist(@NonNull final OutputStream os) throws IOException, XmlPullParserException {
325         Serializer.persist(mItems, os);
326     }
327 
328     private static final class Serializer {
329         private static final String TAG_OVERLAYS = "overlays";
330         private static final String TAG_ITEM = "item";
331 
332         private static final String ATTR_BASE_CODE_PATH = "baseCodePath";
333         private static final String ATTR_IS_ENABLED = "isEnabled";
334         private static final String ATTR_PACKAGE_NAME = "packageName";
335         private static final String ATTR_STATE = "state";
336         private static final String ATTR_TARGET_PACKAGE_NAME = "targetPackageName";
337         private static final String ATTR_IS_STATIC = "isStatic";
338         private static final String ATTR_PRIORITY = "priority";
339         private static final String ATTR_CATEGORY = "category";
340         private static final String ATTR_USER_ID = "userId";
341         private static final String ATTR_VERSION = "version";
342 
343         private static final int CURRENT_VERSION = 3;
344 
restore(@onNull final ArrayList<SettingsItem> table, @NonNull final InputStream is)345         public static void restore(@NonNull final ArrayList<SettingsItem> table,
346                 @NonNull final InputStream is) throws IOException, XmlPullParserException {
347 
348             try (InputStreamReader reader = new InputStreamReader(is)) {
349                 table.clear();
350                 final XmlPullParser parser = Xml.newPullParser();
351                 parser.setInput(reader);
352                 XmlUtils.beginDocument(parser, TAG_OVERLAYS);
353                 int version = XmlUtils.readIntAttribute(parser, ATTR_VERSION);
354                 if (version != CURRENT_VERSION) {
355                     upgrade(version);
356                 }
357                 int depth = parser.getDepth();
358 
359                 while (XmlUtils.nextElementWithin(parser, depth)) {
360                     switch (parser.getName()) {
361                         case TAG_ITEM:
362                             final SettingsItem item = restoreRow(parser, depth + 1);
363                             table.add(item);
364                             break;
365                     }
366                 }
367             }
368         }
369 
upgrade(int oldVersion)370         private static void upgrade(int oldVersion) throws XmlPullParserException {
371             switch (oldVersion) {
372                 case 0:
373                 case 1:
374                 case 2:
375                     // Throw an exception which will cause the overlay file to be ignored
376                     // and overwritten.
377                     throw new XmlPullParserException("old version " + oldVersion + "; ignoring");
378                 default:
379                     throw new XmlPullParserException("unrecognized version " + oldVersion);
380             }
381         }
382 
restoreRow(@onNull final XmlPullParser parser, final int depth)383         private static SettingsItem restoreRow(@NonNull final XmlPullParser parser, final int depth)
384                 throws IOException {
385             final String packageName = XmlUtils.readStringAttribute(parser, ATTR_PACKAGE_NAME);
386             final int userId = XmlUtils.readIntAttribute(parser, ATTR_USER_ID);
387             final String targetPackageName = XmlUtils.readStringAttribute(parser,
388                     ATTR_TARGET_PACKAGE_NAME);
389             final String baseCodePath = XmlUtils.readStringAttribute(parser, ATTR_BASE_CODE_PATH);
390             final int state = XmlUtils.readIntAttribute(parser, ATTR_STATE);
391             final boolean isEnabled = XmlUtils.readBooleanAttribute(parser, ATTR_IS_ENABLED);
392             final boolean isStatic = XmlUtils.readBooleanAttribute(parser, ATTR_IS_STATIC);
393             final int priority = XmlUtils.readIntAttribute(parser, ATTR_PRIORITY);
394             final String category = XmlUtils.readStringAttribute(parser, ATTR_CATEGORY);
395 
396             return new SettingsItem(packageName, userId, targetPackageName, baseCodePath,
397                     state, isEnabled, isStatic, priority, category);
398         }
399 
persist(@onNull final ArrayList<SettingsItem> table, @NonNull final OutputStream os)400         public static void persist(@NonNull final ArrayList<SettingsItem> table,
401                 @NonNull final OutputStream os) throws IOException, XmlPullParserException {
402             final FastXmlSerializer xml = new FastXmlSerializer();
403             xml.setOutput(os, "utf-8");
404             xml.startDocument(null, true);
405             xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
406             xml.startTag(null, TAG_OVERLAYS);
407             XmlUtils.writeIntAttribute(xml, ATTR_VERSION, CURRENT_VERSION);
408 
409             final int N = table.size();
410             for (int i = 0; i < N; i++) {
411                 final SettingsItem item = table.get(i);
412                 persistRow(xml, item);
413             }
414             xml.endTag(null, TAG_OVERLAYS);
415             xml.endDocument();
416         }
417 
persistRow(@onNull final FastXmlSerializer xml, @NonNull final SettingsItem item)418         private static void persistRow(@NonNull final FastXmlSerializer xml,
419                 @NonNull final SettingsItem item) throws IOException {
420             xml.startTag(null, TAG_ITEM);
421             XmlUtils.writeStringAttribute(xml, ATTR_PACKAGE_NAME, item.mPackageName);
422             XmlUtils.writeIntAttribute(xml, ATTR_USER_ID, item.mUserId);
423             XmlUtils.writeStringAttribute(xml, ATTR_TARGET_PACKAGE_NAME, item.mTargetPackageName);
424             XmlUtils.writeStringAttribute(xml, ATTR_BASE_CODE_PATH, item.mBaseCodePath);
425             XmlUtils.writeIntAttribute(xml, ATTR_STATE, item.mState);
426             XmlUtils.writeBooleanAttribute(xml, ATTR_IS_ENABLED, item.mIsEnabled);
427             XmlUtils.writeBooleanAttribute(xml, ATTR_IS_STATIC, item.mIsStatic);
428             XmlUtils.writeIntAttribute(xml, ATTR_PRIORITY, item.mPriority);
429             XmlUtils.writeStringAttribute(xml, ATTR_CATEGORY, item.mCategory);
430             xml.endTag(null, TAG_ITEM);
431         }
432     }
433 
434     private static final class SettingsItem {
435         private final int mUserId;
436         private final String mPackageName;
437         private final String mTargetPackageName;
438         private String mBaseCodePath;
439         private @OverlayInfo.State int mState;
440         private boolean mIsEnabled;
441         private OverlayInfo mCache;
442         private boolean mIsStatic;
443         private int mPriority;
444         private String mCategory;
445 
SettingsItem(@onNull final String packageName, final int userId, @NonNull final String targetPackageName, @NonNull final String baseCodePath, final @OverlayInfo.State int state, final boolean isEnabled, final boolean isStatic, final int priority, String category)446         SettingsItem(@NonNull final String packageName, final int userId,
447                 @NonNull final String targetPackageName, @NonNull final String baseCodePath,
448                 final @OverlayInfo.State int state, final boolean isEnabled, final boolean isStatic,
449                 final int priority, String category) {
450             mPackageName = packageName;
451             mUserId = userId;
452             mTargetPackageName = targetPackageName;
453             mBaseCodePath = baseCodePath;
454             mState = state;
455             mIsEnabled = isEnabled || isStatic;
456             mCategory = category;
457             mCache = null;
458             mIsStatic = isStatic;
459             mPriority = priority;
460         }
461 
SettingsItem(@onNull final String packageName, final int userId, @NonNull final String targetPackageName, @NonNull final String baseCodePath, final boolean isStatic, final int priority, String category)462         SettingsItem(@NonNull final String packageName, final int userId,
463                 @NonNull final String targetPackageName, @NonNull final String baseCodePath,
464                 final boolean isStatic, final int priority, String category) {
465             this(packageName, userId, targetPackageName, baseCodePath, OverlayInfo.STATE_UNKNOWN,
466                     false, isStatic, priority, category);
467         }
468 
getTargetPackageName()469         private String getTargetPackageName() {
470             return mTargetPackageName;
471         }
472 
getUserId()473         private int getUserId() {
474             return mUserId;
475         }
476 
getBaseCodePath()477         private String getBaseCodePath() {
478             return mBaseCodePath;
479         }
480 
setBaseCodePath(@onNull final String path)481         private boolean setBaseCodePath(@NonNull final String path) {
482             if (!mBaseCodePath.equals(path)) {
483                 mBaseCodePath = path;
484                 invalidateCache();
485                 return true;
486             }
487             return false;
488         }
489 
getState()490         private @OverlayInfo.State int getState() {
491             return mState;
492         }
493 
setState(final @OverlayInfo.State int state)494         private boolean setState(final @OverlayInfo.State int state) {
495             if (mState != state) {
496                 mState = state;
497                 invalidateCache();
498                 return true;
499             }
500             return false;
501         }
502 
isEnabled()503         private boolean isEnabled() {
504             return mIsEnabled;
505         }
506 
setEnabled(boolean enable)507         private boolean setEnabled(boolean enable) {
508             if (mIsStatic) {
509                 return false;
510             }
511 
512             if (mIsEnabled != enable) {
513                 mIsEnabled = enable;
514                 invalidateCache();
515                 return true;
516             }
517             return false;
518         }
519 
setCategory(String category)520         private boolean setCategory(String category) {
521             if (!Objects.equals(mCategory, category)) {
522                 mCategory = category.intern();
523                 invalidateCache();
524                 return true;
525             }
526             return false;
527         }
528 
getOverlayInfo()529         private OverlayInfo getOverlayInfo() {
530             if (mCache == null) {
531                 mCache = new OverlayInfo(mPackageName, mTargetPackageName, mCategory, mBaseCodePath,
532                         mState, mUserId, mPriority, mIsStatic);
533             }
534             return mCache;
535         }
536 
invalidateCache()537         private void invalidateCache() {
538             mCache = null;
539         }
540 
isStatic()541         private boolean isStatic() {
542             return mIsStatic;
543         }
544 
getPriority()545         private int getPriority() {
546             return mPriority;
547         }
548     }
549 
select(@onNull final String packageName, final int userId)550     private int select(@NonNull final String packageName, final int userId) {
551         final int N = mItems.size();
552         for (int i = 0; i < N; i++) {
553             final SettingsItem item = mItems.get(i);
554             if (item.mUserId == userId && item.mPackageName.equals(packageName)) {
555                 return i;
556             }
557         }
558         return -1;
559     }
560 
selectWhereUser(final int userId)561     private Stream<SettingsItem> selectWhereUser(final int userId) {
562         return mItems.stream().filter(item -> item.mUserId == userId);
563     }
564 
selectWhereTarget(@onNull final String targetPackageName, final int userId)565     private Stream<SettingsItem> selectWhereTarget(@NonNull final String targetPackageName,
566             final int userId) {
567         return selectWhereUser(userId)
568                 .filter(item -> item.getTargetPackageName().equals(targetPackageName));
569     }
570 
571     static final class BadKeyException extends RuntimeException {
BadKeyException(@onNull final String packageName, final int userId)572         BadKeyException(@NonNull final String packageName, final int userId) {
573             super("Bad key mPackageName=" + packageName + " mUserId=" + userId);
574         }
575     }
576 }
577