| /* |
| * 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.OverlayInfo; |
| import android.os.UserHandle; |
| import android.util.ArrayMap; |
| import android.util.Slog; |
| import android.util.Xml; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.FastXmlSerializer; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.internal.util.XmlUtils; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| /** |
| * 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<>(); |
| |
| void init(@NonNull final String packageName, final int userId, |
| @NonNull final String targetPackageName, @Nullable final String targetOverlayableName, |
| @NonNull final String baseCodePath, boolean isMutable, boolean isEnabled, int priority, |
| @Nullable String overlayCategory) { |
| remove(packageName, userId); |
| insert(new SettingsItem(packageName, userId, targetPackageName, targetOverlayableName, |
| baseCodePath, OverlayInfo.STATE_UNKNOWN, isEnabled, isMutable, priority, |
| overlayCategory)); |
| } |
| |
| /** |
| * Returns true if the settings were modified, false if they remain the same. |
| */ |
| boolean remove(@NonNull final String packageName, final int userId) { |
| final int idx = select(packageName, userId); |
| if (idx < 0) { |
| return false; |
| } |
| |
| mItems.remove(idx); |
| return true; |
| } |
| |
| @NonNull OverlayInfo getOverlayInfo(@NonNull final String packageName, final int userId) |
| throws BadKeyException { |
| final int idx = select(packageName, userId); |
| if (idx < 0) { |
| throw new BadKeyException(packageName, userId); |
| } |
| return mItems.get(idx).getOverlayInfo(); |
| } |
| |
| /** |
| * Returns true if the settings were modified, false if they remain the same. |
| */ |
| boolean setBaseCodePath(@NonNull final String packageName, final int userId, |
| @NonNull final String path) throws BadKeyException { |
| final int idx = select(packageName, userId); |
| if (idx < 0) { |
| throw new BadKeyException(packageName, userId); |
| } |
| return mItems.get(idx).setBaseCodePath(path); |
| } |
| |
| boolean setCategory(@NonNull final String packageName, final int userId, |
| @Nullable String category) throws BadKeyException { |
| final int idx = select(packageName, userId); |
| if (idx < 0) { |
| throw new BadKeyException(packageName, userId); |
| } |
| return mItems.get(idx).setCategory(category); |
| } |
| |
| boolean getEnabled(@NonNull final String packageName, final int userId) throws BadKeyException { |
| final int idx = select(packageName, userId); |
| if (idx < 0) { |
| throw new BadKeyException(packageName, userId); |
| } |
| return mItems.get(idx).isEnabled(); |
| } |
| |
| /** |
| * Returns true if the settings were modified, false if they remain the same. |
| */ |
| boolean setEnabled(@NonNull final String packageName, final int userId, final boolean enable) |
| throws BadKeyException { |
| final int idx = select(packageName, userId); |
| if (idx < 0) { |
| throw new BadKeyException(packageName, userId); |
| } |
| return mItems.get(idx).setEnabled(enable); |
| } |
| |
| @OverlayInfo.State int getState(@NonNull final String packageName, final int userId) |
| throws BadKeyException { |
| final int idx = select(packageName, userId); |
| if (idx < 0) { |
| throw new BadKeyException(packageName, userId); |
| } |
| return mItems.get(idx).getState(); |
| } |
| |
| /** |
| * Returns true if the settings were modified, false if they remain the same. |
| */ |
| boolean setState(@NonNull final String packageName, final int userId, |
| final @OverlayInfo.State int state) throws BadKeyException { |
| final int idx = select(packageName, userId); |
| if (idx < 0) { |
| throw new BadKeyException(packageName, userId); |
| } |
| return mItems.get(idx).setState(state); |
| } |
| |
| List<OverlayInfo> getOverlaysForTarget(@NonNull final String targetPackageName, |
| final int userId) { |
| // Immutable RROs targeting "android" are loaded from AssetManager, and so they should be |
| // ignored in OverlayManagerService. |
| return selectWhereTarget(targetPackageName, userId) |
| .filter((i) -> i.isMutable() || !"android".equals(i.getTargetPackageName())) |
| .map(SettingsItem::getOverlayInfo) |
| .collect(Collectors.toList()); |
| } |
| |
| ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) { |
| // Immutable RROs targeting "android" are loaded from AssetManager, and so they should be |
| // ignored in OverlayManagerService. |
| return selectWhereUser(userId) |
| .filter((i) -> i.isMutable() || !"android".equals(i.getTargetPackageName())) |
| .map(SettingsItem::getOverlayInfo) |
| .collect(Collectors.groupingBy(info -> info.targetPackageName, ArrayMap::new, |
| Collectors.toList())); |
| } |
| |
| 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) { |
| boolean removed = false; |
| for (int i = 0; i < mItems.size(); i++) { |
| final SettingsItem item = mItems.get(i); |
| if (item.getUserId() == userId) { |
| if (DEBUG) { |
| Slog.d(TAG, "Removing overlay " + item.mPackageName + " for user " + userId |
| + " from settings because user was removed"); |
| } |
| mItems.remove(i); |
| removed = true; |
| i--; |
| } |
| } |
| return removed; |
| } |
| |
| /** |
| * Reassigns the priority of an overlay maintaining the values of the overlays other settings. |
| */ |
| void setPriority(@NonNull final String packageName, final int userId, final int priority) { |
| final int moveIdx = select(packageName, userId); |
| if (moveIdx < 0) { |
| throw new BadKeyException(packageName, 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 String packageName, |
| @NonNull final String newParentPackageName, final int userId) { |
| if (packageName.equals(newParentPackageName)) { |
| return false; |
| } |
| final int moveIdx = select(packageName, userId); |
| if (moveIdx < 0) { |
| return false; |
| } |
| |
| final int parentIdx = select(newParentPackageName, 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(newParentPackageName, 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 String packageName, final int userId) { |
| final int idx = select(packageName, 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 String packageName, final int userId) { |
| final int idx = select(packageName, 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) { |
| // select items to display |
| Stream<SettingsItem> items = mItems.stream(); |
| if (dumpState.getUserId() != UserHandle.USER_ALL) { |
| items = items.filter(item -> item.mUserId == dumpState.getUserId()); |
| } |
| if (dumpState.getPackageName() != null) { |
| items = items.filter(item -> item.mPackageName.equals(dumpState.getPackageName())); |
| } |
| |
| // display items |
| final IndentingPrintWriter pw = new IndentingPrintWriter(p, " "); |
| if (dumpState.getField() != null) { |
| items.forEach(item -> dumpSettingsItemField(pw, item, dumpState.getField())); |
| } else { |
| items.forEach(item -> dumpSettingsItem(pw, item)); |
| } |
| } |
| |
| private void dumpSettingsItem(@NonNull final IndentingPrintWriter pw, |
| @NonNull final SettingsItem item) { |
| pw.println(item.mPackageName + ":" + item.getUserId() + " {"); |
| pw.increaseIndent(); |
| |
| pw.println("mPackageName...........: " + item.mPackageName); |
| 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.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.mPackageName); |
| 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 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_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"; |
| |
| @VisibleForTesting |
| static final int CURRENT_VERSION = 4; |
| |
| public static void restore(@NonNull final ArrayList<SettingsItem> table, |
| @NonNull final InputStream is) throws IOException, XmlPullParserException { |
| |
| try (InputStreamReader reader = new InputStreamReader(is)) { |
| table.clear(); |
| final XmlPullParser parser = Xml.newPullParser(); |
| parser.setInput(reader); |
| XmlUtils.beginDocument(parser, TAG_OVERLAYS); |
| int version = XmlUtils.readIntAttribute(parser, ATTR_VERSION); |
| if (version != CURRENT_VERSION) { |
| upgrade(version); |
| } |
| int depth = parser.getDepth(); |
| |
| while (XmlUtils.nextElementWithin(parser, depth)) { |
| switch (parser.getName()) { |
| case TAG_ITEM: |
| final SettingsItem item = restoreRow(parser, depth + 1); |
| table.add(item); |
| break; |
| } |
| } |
| } |
| } |
| |
| 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: |
| // Upgrading from version 3 to 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 XmlPullParser parser, final int depth) |
| throws IOException { |
| final String packageName = XmlUtils.readStringAttribute(parser, ATTR_PACKAGE_NAME); |
| final int userId = XmlUtils.readIntAttribute(parser, 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 = XmlUtils.readIntAttribute(parser, ATTR_STATE); |
| final boolean isEnabled = XmlUtils.readBooleanAttribute(parser, ATTR_IS_ENABLED); |
| final boolean isStatic = XmlUtils.readBooleanAttribute(parser, ATTR_IS_STATIC); |
| final int priority = XmlUtils.readIntAttribute(parser, ATTR_PRIORITY); |
| final String category = XmlUtils.readStringAttribute(parser, ATTR_CATEGORY); |
| |
| return new SettingsItem(packageName, userId, targetPackageName, targetOverlayableName, |
| baseCodePath, state, isEnabled, !isStatic, priority, category); |
| } |
| |
| public static void persist(@NonNull final ArrayList<SettingsItem> table, |
| @NonNull final OutputStream os) throws IOException, XmlPullParserException { |
| final FastXmlSerializer xml = new FastXmlSerializer(); |
| xml.setOutput(os, "utf-8"); |
| xml.startDocument(null, true); |
| xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); |
| xml.startTag(null, TAG_OVERLAYS); |
| XmlUtils.writeIntAttribute(xml, 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 FastXmlSerializer xml, |
| @NonNull final SettingsItem item) throws IOException { |
| xml.startTag(null, TAG_ITEM); |
| XmlUtils.writeStringAttribute(xml, ATTR_PACKAGE_NAME, item.mPackageName); |
| XmlUtils.writeIntAttribute(xml, 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); |
| XmlUtils.writeIntAttribute(xml, ATTR_STATE, item.mState); |
| XmlUtils.writeBooleanAttribute(xml, ATTR_IS_ENABLED, item.mIsEnabled); |
| XmlUtils.writeBooleanAttribute(xml, ATTR_IS_STATIC, !item.mIsMutable); |
| XmlUtils.writeIntAttribute(xml, ATTR_PRIORITY, item.mPriority); |
| XmlUtils.writeStringAttribute(xml, ATTR_CATEGORY, item.mCategory); |
| xml.endTag(null, TAG_ITEM); |
| } |
| } |
| |
| private static final class SettingsItem { |
| private final int mUserId; |
| private final String mPackageName; |
| private final String mTargetPackageName; |
| private final String mTargetOverlayableName; |
| private String mBaseCodePath; |
| private @OverlayInfo.State int mState; |
| private boolean mIsEnabled; |
| private OverlayInfo mCache; |
| private boolean mIsMutable; |
| private int mPriority; |
| private String mCategory; |
| |
| SettingsItem(@NonNull final String packageName, 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) { |
| mPackageName = packageName; |
| mUserId = userId; |
| mTargetPackageName = targetPackageName; |
| mTargetOverlayableName = targetOverlayableName; |
| mBaseCodePath = baseCodePath; |
| mState = state; |
| mIsEnabled = isEnabled; |
| mCategory = category; |
| mCache = null; |
| mIsMutable = isMutable; |
| mPriority = priority; |
| } |
| |
| 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 OverlayInfo getOverlayInfo() { |
| if (mCache == null) { |
| mCache = new OverlayInfo(mPackageName, mTargetPackageName, mTargetOverlayableName, |
| mCategory, mBaseCodePath, mState, mUserId, mPriority, mIsMutable); |
| } |
| 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 String packageName, 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.mPackageName.equals(packageName)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| private Stream<SettingsItem> selectWhereUser(final int userId) { |
| return mItems.stream().filter(item -> item.mUserId == userId); |
| } |
| |
| private Stream<SettingsItem> selectWhereTarget(@NonNull final String targetPackageName, |
| final int userId) { |
| return selectWhereUser(userId) |
| .filter(item -> item.getTargetPackageName().equals(targetPackageName)); |
| } |
| |
| static final class BadKeyException extends RuntimeException { |
| BadKeyException(@NonNull final String packageName, final int userId) { |
| super("Bad key mPackageName=" + packageName + " mUserId=" + userId); |
| } |
| } |
| } |