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