OSDN Git Service

OMS: add internal tests
authorMårten Kongstad <marten.kongstad@sony.com>
Thu, 3 May 2018 08:48:35 +0000 (10:48 +0200)
committerTodd Kennedy <toddke@google.com>
Tue, 9 Apr 2019 13:44:34 +0000 (06:44 -0700)
Add unit tests for OMS internal components. These are different from the
OMS public API tests [that reside in core/tests/overlaytest].

Also, fix typo in OverlayInfo#toString.

Bug: 119443459
Test: atest FrameworksServicesTests:com.android.server.om
Change-Id: Icbee70800de5157a1804e0c96101f31c0bad2619

core/java/android/content/om/OverlayInfo.java
services/core/java/com/android/server/om/OverlayManagerService.java
services/core/java/com/android/server/om/OverlayManagerSettings.java
services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java [new file with mode: 0644]
services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java [new file with mode: 0644]

index fc79a42..639335e 100644 (file)
@@ -451,7 +451,7 @@ public final class OverlayInfo implements Parcelable {
     public String toString() {
         return "OverlayInfo { overlay=" + packageName + ", targetPackage=" + targetPackageName
                 + ((targetOverlayableName == null) ? ""
-                : ", targetOverlyabale=" + targetOverlayableName)
+                : ", targetOverlayable=" + targetOverlayableName)
                 + ", state=" + state + " (" + stateToString(state) + "), userId=" + userId + " }";
     }
 }
index 51d5acc..ee07c7d 100644 (file)
@@ -166,6 +166,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
  *     . . . .  . . . . . . . . . . . . . . . . . .
  * </pre>
  *
+ * <p>To test the OMS, execute:
+ * <code>
+ * atest FrameworksServicesTests:com.android.server.om  # internal tests
+ * atest OverlayDeviceTests OverlayHostTests            # public API tests
+ * </code>
+ * </p>
+ *
  * <p>Finally, here is a list of keywords used in the OMS context.</p>
  *
  * <ul>
index 36b5beb..f35c707 100644 (file)
@@ -26,6 +26,7 @@ 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;
@@ -327,7 +328,8 @@ final class OverlayManagerSettings {
         Serializer.persist(mItems, os);
     }
 
-    private static final class Serializer {
+    @VisibleForTesting
+    static final class Serializer {
         private static final String TAG_OVERLAYS = "overlays";
         private static final String TAG_ITEM = "item";
 
@@ -343,7 +345,8 @@ final class OverlayManagerSettings {
         private static final String ATTR_USER_ID = "userId";
         private static final String ATTR_VERSION = "version";
 
-        private static final int CURRENT_VERSION = 3;
+        @VisibleForTesting
+        static final int CURRENT_VERSION = 3;
 
         public static void restore(@NonNull final ArrayList<SettingsItem> table,
                 @NonNull final InputStream is) throws IOException, XmlPullParserException {
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
new file mode 100644 (file)
index 0000000..3f9a57e
--- /dev/null
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2019 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 android.content.om.OverlayInfo.STATE_DISABLED;
+import static android.content.om.OverlayInfo.STATE_ENABLED;
+import static android.content.om.OverlayInfo.STATE_MISSING_TARGET;
+import static android.content.om.OverlayInfo.STATE_TARGET_IS_BEING_REPLACED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.content.om.OverlayInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.util.ArraySet;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class OverlayManagerServiceImplTests {
+    private OverlayManagerServiceImpl mImpl;
+    private DummyDeviceState mState;
+    private DummyListener mListener;
+
+    private static final String OVERLAY = "com.dummy.overlay";
+    private static final String TARGET = "com.dummy.target";
+    private static final int USER = 0;
+
+    private static final String OVERLAY2 = OVERLAY + "2";
+    private static final String TARGET2 = TARGET + "2";
+    private static final int USER2 = USER + 1;
+
+    private static final String OVERLAY3 = OVERLAY + "3";
+    private static final int USER3 = USER2 + 1;
+
+
+    @Before
+    public void setUp() throws Exception {
+        mState = new DummyDeviceState();
+        mListener = new DummyListener();
+        DummyPackageManagerHelper pmh = new DummyPackageManagerHelper(mState);
+        mImpl = new OverlayManagerServiceImpl(pmh,
+                new DummyIdmapManager(mState, pmh),
+                new OverlayManagerSettings(),
+                new String[0],
+                mListener);
+    }
+
+    // tests: basics
+
+    @Test
+    public void testGetOverlayInfo() throws Exception {
+        installOverlayPackage(OVERLAY, TARGET, USER, false);
+        final OverlayInfo oi = mImpl.getOverlayInfo(OVERLAY, USER);
+        assertNotNull(oi);
+        assertEquals(oi.packageName, OVERLAY);
+        assertEquals(oi.targetPackageName, TARGET);
+        assertEquals(oi.userId, USER);
+    }
+
+    @Test
+    public void testGetOverlayInfosForTarget() throws Exception {
+        installOverlayPackage(OVERLAY, TARGET, USER, false);
+        installOverlayPackage(OVERLAY2, TARGET, USER, false);
+
+        installOverlayPackage(OVERLAY3, TARGET, USER2, false);
+
+        final List<OverlayInfo> ois = mImpl.getOverlayInfosForTarget(TARGET, USER);
+        assertEquals(ois.size(), 2);
+        assertTrue(ois.contains(mImpl.getOverlayInfo(OVERLAY, USER)));
+        assertTrue(ois.contains(mImpl.getOverlayInfo(OVERLAY2, USER)));
+
+        final List<OverlayInfo> ois2 = mImpl.getOverlayInfosForTarget(TARGET, USER2);
+        assertEquals(ois2.size(), 1);
+        assertTrue(ois2.contains(mImpl.getOverlayInfo(OVERLAY3, USER2)));
+
+        final List<OverlayInfo> ois3 = mImpl.getOverlayInfosForTarget(TARGET, USER3);
+        assertNotNull(ois3);
+        assertEquals(ois3.size(), 0);
+
+        final List<OverlayInfo> ois4 = mImpl.getOverlayInfosForTarget("no.such.overlay", USER);
+        assertNotNull(ois4);
+        assertEquals(ois4.size(), 0);
+    }
+
+    @Test
+    public void testGetOverlayInfosForUser() throws Exception {
+        installOverlayPackage(OVERLAY, TARGET, USER, false);
+        installOverlayPackage(OVERLAY2, TARGET, USER, false);
+        installOverlayPackage(OVERLAY3, TARGET2, USER, false);
+
+        final Map<String, List<OverlayInfo>> everything = mImpl.getOverlaysForUser(USER);
+        assertEquals(everything.size(), 2);
+
+        final List<OverlayInfo> ois = everything.get(TARGET);
+        assertNotNull(ois);
+        assertEquals(ois.size(), 2);
+        assertTrue(ois.contains(mImpl.getOverlayInfo(OVERLAY, USER)));
+        assertTrue(ois.contains(mImpl.getOverlayInfo(OVERLAY2, USER)));
+
+        final List<OverlayInfo> ois2 = everything.get(TARGET2);
+        assertNotNull(ois2);
+        assertEquals(ois2.size(), 1);
+        assertTrue(ois2.contains(mImpl.getOverlayInfo(OVERLAY3, USER)));
+
+        final Map<String, List<OverlayInfo>> everything2 = mImpl.getOverlaysForUser(USER2);
+        assertNotNull(everything2);
+        assertEquals(everything2.size(), 0);
+    }
+
+    @Test
+    public void testPriority() throws Exception {
+        installOverlayPackage(OVERLAY, TARGET, USER, false);
+        installOverlayPackage(OVERLAY2, TARGET, USER, false);
+        installOverlayPackage(OVERLAY3, TARGET, USER, false);
+
+        final OverlayInfo o1 = mImpl.getOverlayInfo(OVERLAY, USER);
+        final OverlayInfo o2 = mImpl.getOverlayInfo(OVERLAY2, USER);
+        final OverlayInfo o3 = mImpl.getOverlayInfo(OVERLAY3, USER);
+
+        assertOverlayInfoList(TARGET, USER, o1, o2, o3);
+
+        assertTrue(mImpl.setLowestPriority(OVERLAY3, USER));
+        assertOverlayInfoList(TARGET, USER, o3, o1, o2);
+
+        assertTrue(mImpl.setHighestPriority(OVERLAY3, USER));
+        assertOverlayInfoList(TARGET, USER, o1, o2, o3);
+
+        assertTrue(mImpl.setPriority(OVERLAY, OVERLAY2, USER));
+        assertOverlayInfoList(TARGET, USER, o2, o1, o3);
+    }
+
+    @Test
+    public void testOverlayInfoStateTransitions() throws Exception {
+        assertNull(mImpl.getOverlayInfo(OVERLAY, USER));
+
+        installOverlayPackage(OVERLAY, TARGET, USER, true);
+        assertState(STATE_MISSING_TARGET, OVERLAY, USER);
+
+        installTargetPackage(TARGET, USER);
+        assertState(STATE_DISABLED, OVERLAY, USER);
+
+        mImpl.setEnabled(OVERLAY, true, USER);
+        assertState(STATE_ENABLED, OVERLAY, USER);
+
+        beginUpgradeTargetPackage(TARGET, USER);
+        assertState(STATE_TARGET_IS_BEING_REPLACED, OVERLAY, USER);
+
+        endUpgradeTargetPackage(TARGET, USER);
+        assertState(STATE_ENABLED, OVERLAY, USER);
+
+        uninstallTargetPackage(TARGET, USER);
+        assertState(STATE_MISSING_TARGET, OVERLAY, USER);
+
+        installTargetPackage(TARGET, USER);
+        assertState(STATE_ENABLED, OVERLAY, USER);
+    }
+
+    @Test
+    public void testUpdateOverlaysForUser() throws Exception {
+        installTargetPackage(TARGET, USER);
+        installTargetPackage("some.other.target", USER);
+        installOverlayPackage(OVERLAY, TARGET, USER, true);
+
+        // do nothing, expect no change
+        List<String> a = mImpl.updateOverlaysForUser(USER);
+        assertEquals(1, a.size());
+        assertTrue(a.contains(TARGET));
+
+        // upgrade overlay, keep target
+        upgradeOverlayPackage(OVERLAY, TARGET, USER, true);
+        List<String> b = mImpl.updateOverlaysForUser(USER);
+        assertEquals(1, b.size());
+        assertTrue(b.contains(TARGET));
+
+        // do nothing, expect no change
+        List<String> c = mImpl.updateOverlaysForUser(USER);
+        assertEquals(1, c.size());
+        assertTrue(c.contains(TARGET));
+
+        // upgrade overlay, switch to new target
+        upgradeOverlayPackage(OVERLAY, "some.other.target", USER, true);
+        List<String> d = mImpl.updateOverlaysForUser(USER);
+        assertEquals(2, d.size());
+        assertTrue(d.containsAll(Arrays.asList(TARGET, "some.other.target")));
+
+        // do nothing, expect no change
+        List<String> e = mImpl.updateOverlaysForUser(USER);
+        assertEquals(1, e.size());
+        assertTrue(e.contains("some.other.target"));
+    }
+
+    @Test
+    public void testOnOverlayPackageUpgraded() throws Exception {
+        installTargetPackage(TARGET, USER);
+        installOverlayPackage(OVERLAY, TARGET, USER, true);
+        mImpl.onOverlayPackageReplacing(OVERLAY, USER);
+        mListener.count = 0;
+        mImpl.onOverlayPackageReplaced(OVERLAY, USER);
+        assertEquals(1, mListener.count);
+
+        // upgrade to a version where the overlay has changed its target
+        upgradeOverlayPackage(OVERLAY, "some.other.target", USER, true);
+        mImpl.onOverlayPackageReplacing(OVERLAY, USER);
+        mListener.count = 0;
+        mImpl.onOverlayPackageReplaced(OVERLAY, USER);
+        // expect once for the old target package, once for the new target package
+        assertEquals(2, mListener.count);
+
+        upgradeOverlayPackage(OVERLAY, "some.other.target", USER, true);
+        mImpl.onOverlayPackageReplacing(OVERLAY, USER);
+        mListener.count = 0;
+        mImpl.onOverlayPackageReplaced(OVERLAY, USER);
+        assertEquals(1, mListener.count);
+    }
+
+    // tests: listener interface
+
+    @Test
+    public void testListener() throws Exception {
+        installOverlayPackage(OVERLAY, TARGET, USER, true);
+        assertEquals(1, mListener.count);
+        mListener.count = 0;
+
+        installTargetPackage(TARGET, USER);
+        assertEquals(1, mListener.count);
+        mListener.count = 0;
+
+        mImpl.setEnabled(OVERLAY, true, USER);
+        assertEquals(1, mListener.count);
+        mListener.count = 0;
+
+        mImpl.setEnabled(OVERLAY, true, USER);
+        assertEquals(0, mListener.count);
+    }
+
+    // helper methods
+
+    private void assertState(int expected, final String overlayPackageName, int userId) {
+        int actual = mImpl.getOverlayInfo(OVERLAY, USER).state;
+        String msg = String.format("expected %s but was %s:",
+                OverlayInfo.stateToString(expected), OverlayInfo.stateToString(actual));
+        assertEquals(msg, expected, actual);
+    }
+
+    private void assertOverlayInfoList(final String targetPackageName, int userId,
+            OverlayInfo... overlayInfos) {
+        final List<OverlayInfo> expected =
+                mImpl.getOverlayInfosForTarget(targetPackageName, userId);
+        final List<OverlayInfo> actual = Arrays.asList(overlayInfos);
+        assertEquals(expected, actual);
+    }
+
+    private void installTargetPackage(String packageName, int userId) {
+        if (mState.select(packageName, userId) != null) {
+            throw new IllegalStateException("package already installed");
+        }
+        mState.add(packageName, null, userId, false);
+        mImpl.onTargetPackageAdded(packageName, userId);
+    }
+
+    private void beginUpgradeTargetPackage(String packageName, int userId) {
+        if (mState.select(packageName, userId) == null) {
+            throw new IllegalStateException("package not installed");
+        }
+        mState.add(packageName, null, userId, false);
+        mImpl.onTargetPackageReplacing(packageName, userId);
+    }
+
+    private void endUpgradeTargetPackage(String packageName, int userId) {
+        if (mState.select(packageName, userId) == null) {
+            throw new IllegalStateException("package not installed");
+        }
+        mState.add(packageName, null, userId, false);
+        mImpl.onTargetPackageReplaced(packageName, userId);
+    }
+
+    private void uninstallTargetPackage(String packageName, int userId) {
+        if (mState.select(packageName, userId) == null) {
+            throw new IllegalStateException("package not installed");
+        }
+        mState.remove(packageName, userId);
+        mImpl.onTargetPackageRemoved(packageName, userId);
+    }
+
+    private void installOverlayPackage(String packageName, String targetPackageName, int userId,
+            boolean canCreateIdmap) {
+        if (mState.select(packageName, userId) != null) {
+            throw new IllegalStateException("package already installed");
+        }
+        mState.add(packageName, targetPackageName, userId, canCreateIdmap);
+        mImpl.onOverlayPackageAdded(packageName, userId);
+    }
+
+    private void upgradeOverlayPackage(String packageName, String targetPackageName, int userId,
+            boolean canCreateIdmap) {
+        DummyDeviceState.Package pkg = mState.select(packageName, userId);
+        if (pkg == null) {
+            throw new IllegalStateException("package not installed, cannot upgrade");
+        }
+        pkg.targetPackageName = targetPackageName;
+        pkg.canCreateIdmap = canCreateIdmap;
+    }
+
+    private void uninstallOverlayPackage(String packageName, int userId) {
+        // implement this when adding support for downloadable overlays
+        throw new IllegalArgumentException("not implemented");
+    }
+
+    private static final class DummyDeviceState {
+        private List<Package> mPackages = new ArrayList<>();
+
+        public void add(String packageName, String targetPackageName, int userId,
+                boolean canCreateIdmap) {
+            remove(packageName, userId);
+            Package pkg = new Package();
+            pkg.packageName = packageName;
+            pkg.targetPackageName = targetPackageName;
+            pkg.userId = userId;
+            pkg.canCreateIdmap = canCreateIdmap;
+            mPackages.add(pkg);
+        }
+
+        public void remove(String packageName, int userId) {
+            final Iterator<Package> iter = mPackages.iterator();
+            while (iter.hasNext()) {
+                final Package pkg = iter.next();
+                if (pkg.packageName.equals(packageName) && pkg.userId == userId) {
+                    iter.remove();
+                    return;
+                }
+            }
+        }
+
+        public List<Package> select(int userId) {
+            List<Package> out = new ArrayList<>();
+            final int packageCount = mPackages.size();
+            for (int i = 0; i < packageCount; i++) {
+                final Package pkg = mPackages.get(i);
+                if (pkg.userId == userId) {
+                    out.add(pkg);
+                }
+            }
+            return out;
+        }
+
+        public Package select(String packageName, int userId) {
+            final int packageCount = mPackages.size();
+            for (int i = 0; i < packageCount; i++) {
+                final Package pkg = mPackages.get(i);
+                if (pkg.packageName.equals(packageName) && pkg.userId == userId) {
+                    return pkg;
+                }
+            }
+            return null;
+        }
+
+        private static final class Package {
+            public String packageName;
+            public int userId;
+            public String targetPackageName;
+            public boolean canCreateIdmap;
+        }
+    }
+
+    private static final class DummyPackageManagerHelper implements
+            OverlayManagerServiceImpl.PackageManagerHelper {
+        private final DummyDeviceState mState;
+
+        DummyPackageManagerHelper(DummyDeviceState state) {
+            mState = state;
+        }
+
+        @Override
+        public PackageInfo getPackageInfo(@NonNull String packageName, int userId) {
+            final DummyDeviceState.Package pkg = mState.select(packageName, userId);
+            if (pkg == null) {
+                return null;
+            }
+            ApplicationInfo ai = new ApplicationInfo();
+            ai.sourceDir = String.format("%s/%s/base.apk",
+                    pkg.targetPackageName == null ? "/system/app/" : "/vendor/overlay/",
+                    pkg.packageName);
+            PackageInfo pi = new PackageInfo();
+            pi.applicationInfo = ai;
+            pi.packageName = pkg.packageName;
+            pi.overlayTarget = pkg.targetPackageName;
+            pi.overlayCategory = "dummy-category-" + pkg.targetPackageName;
+            return pi;
+        }
+
+        @Override
+        public boolean signaturesMatching(@NonNull String packageName1,
+                @NonNull String packageName2, int userId) {
+            return false;
+        }
+
+        @Override
+        public List<PackageInfo> getOverlayPackages(int userId) {
+            List<PackageInfo> out = new ArrayList<>();
+            final List<DummyDeviceState.Package> packages = mState.select(userId);
+            final int packageCount = packages.size();
+            for (int i = 0; i < packageCount; i++) {
+                final DummyDeviceState.Package pkg = packages.get(i);
+                if (pkg.targetPackageName != null) {
+                    out.add(getPackageInfo(pkg.packageName, pkg.userId));
+                }
+            }
+            return out;
+        }
+    }
+
+    private static class DummyIdmapManager extends IdmapManager {
+        private final DummyDeviceState mState;
+        private Set<String> mIdmapFiles = new ArraySet<>();
+
+        DummyIdmapManager(DummyDeviceState state, DummyPackageManagerHelper packageManagerHelper) {
+            super(null, packageManagerHelper);
+            mState = state;
+        }
+
+        @Override
+        boolean createIdmap(@NonNull final PackageInfo targetPackage,
+                @NonNull final PackageInfo overlayPackage, int userId) {
+            final DummyDeviceState.Package t = mState.select(targetPackage.packageName, userId);
+            if (t == null) {
+                return false;
+            }
+            final DummyDeviceState.Package o = mState.select(overlayPackage.packageName, userId);
+            if (o == null) {
+                return false;
+            }
+            if (!o.canCreateIdmap) {
+                return false;
+            }
+            final String key = createKey(overlayPackage.packageName, userId);
+            mIdmapFiles.add(key);
+            return true;
+        }
+
+        @Override
+        boolean removeIdmap(@NonNull final OverlayInfo oi, final int userId) {
+            final String key = createKey(oi.packageName, oi.userId);
+            if (!mIdmapFiles.contains(key)) {
+                return false;
+            }
+            mIdmapFiles.remove(key);
+            return true;
+        }
+
+        @Override
+        boolean idmapExists(@NonNull final OverlayInfo oi) {
+            final String key = createKey(oi.packageName, oi.userId);
+            return mIdmapFiles.contains(key);
+        }
+
+        @Override
+        boolean idmapExists(@NonNull final PackageInfo overlayPackage, final int userId) {
+            final String key = createKey(overlayPackage.packageName, userId);
+            return mIdmapFiles.contains(key);
+        }
+
+        private String createKey(@NonNull final String packageName, final int userId) {
+            return String.format("%s:%d", packageName, userId);
+        }
+    }
+
+    private static class DummyListener implements OverlayManagerServiceImpl.OverlayChangeListener {
+        public int count;
+
+        public void onOverlaysChanged(@NonNull String targetPackage, int userId) {
+            count++;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
new file mode 100644 (file)
index 0000000..8ff8b6e
--- /dev/null
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2019 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 android.content.om.OverlayInfo.STATE_DISABLED;
+import static android.content.om.OverlayInfo.STATE_ENABLED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.om.OverlayInfo;
+import android.text.TextUtils;
+import android.util.Xml;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+@RunWith(AndroidJUnit4.class)
+public class OverlayManagerSettingsTests {
+    private OverlayManagerSettings mSettings;
+
+    private static final OverlayInfo OVERLAY_A0 = new OverlayInfo(
+            "com.dummy.overlay_a",
+            "com.dummy.target",
+            null,
+            "some-category",
+            "/data/app/com.dummy.overlay_a-1/base.apk",
+            STATE_DISABLED,
+            0,
+            0,
+            false);
+
+    private static final OverlayInfo OVERLAY_B0 = new OverlayInfo(
+            "com.dummy.overlay_b",
+            "com.dummy.target",
+            null,
+            "some-category",
+            "/data/app/com.dummy.overlay_b-1/base.apk",
+            STATE_DISABLED,
+            0,
+            0,
+            false);
+
+    private static final OverlayInfo OVERLAY_C0 = new OverlayInfo(
+            "com.dummy.overlay_c",
+            "com.dummy.target",
+            null,
+            "some-category",
+            "/data/app/com.dummy.overlay_c-1/base.apk",
+            STATE_DISABLED,
+            0,
+            0,
+            false);
+
+    private static final OverlayInfo OVERLAY_A1 = new OverlayInfo(
+            "com.dummy.overlay_a",
+            "com.dummy.target",
+            null,
+            "some-category",
+            "/data/app/com.dummy.overlay_a-1/base.apk",
+            STATE_DISABLED,
+            1,
+            0,
+            false);
+
+    private static final OverlayInfo OVERLAY_B1 = new OverlayInfo(
+            "com.dummy.overlay_b",
+            "com.dummy.target",
+            null,
+            "some-category",
+            "/data/app/com.dummy.overlay_b-1/base.apk",
+            STATE_DISABLED,
+            1,
+            0,
+            false);
+
+    @Before
+    public void setUp() throws Exception {
+        mSettings = new OverlayManagerSettings();
+    }
+
+    // tests: generic functionality
+
+    @Test
+    public void testSettingsInitiallyEmpty() throws Exception {
+        final int userId = 0;
+        Map<String, List<OverlayInfo>> map = mSettings.getOverlaysForUser(userId);
+        assertEquals(0, map.size());
+    }
+
+    @Test
+    public void testBasicSetAndGet() throws Exception {
+        assertDoesNotContain(mSettings, OVERLAY_A0.packageName, OVERLAY_A0.userId);
+
+        insert(OVERLAY_A0);
+        assertContains(mSettings, OVERLAY_A0);
+        OverlayInfo oi = mSettings.getOverlayInfo(OVERLAY_A0.packageName, OVERLAY_A0.userId);
+        assertEquals(OVERLAY_A0, oi);
+
+        assertTrue(mSettings.remove(OVERLAY_A0.packageName, OVERLAY_A0.userId));
+        assertDoesNotContain(mSettings, OVERLAY_A0.packageName, OVERLAY_A0.userId);
+    }
+
+    @Test
+    public void testGetUsers() throws Exception {
+        int[] users = mSettings.getUsers();
+        assertEquals(0, users.length);
+
+        insert(OVERLAY_A0);
+        users = mSettings.getUsers();
+        assertEquals(1, users.length);
+        assertContains(users, OVERLAY_A0.userId);
+
+        insert(OVERLAY_A1);
+        insert(OVERLAY_B1);
+        users = mSettings.getUsers();
+        assertEquals(2, users.length);
+        assertContains(users, OVERLAY_A0.userId);
+        assertContains(users, OVERLAY_A1.userId);
+    }
+
+    @Test
+    public void testGetOverlaysForUser() throws Exception {
+        insert(OVERLAY_A0);
+        insert(OVERLAY_B0);
+        insert(OVERLAY_A1);
+        insert(OVERLAY_B1);
+
+        Map<String, List<OverlayInfo>> map = mSettings.getOverlaysForUser(OVERLAY_A0.userId);
+        assertEquals(1, map.keySet().size());
+        assertTrue(map.keySet().contains(OVERLAY_A0.targetPackageName));
+
+        List<OverlayInfo> list = map.get(OVERLAY_A0.targetPackageName);
+        assertEquals(2, list.size());
+        assertTrue(list.contains(OVERLAY_A0));
+        assertTrue(list.contains(OVERLAY_B0));
+
+        // getOverlaysForUser should never return null
+        map = mSettings.getOverlaysForUser(-1);
+        assertNotNull(map);
+        assertEquals(0, map.size());
+    }
+
+    @Test
+    public void testRemoveUser() throws Exception {
+        insert(OVERLAY_A0);
+        insert(OVERLAY_B0);
+        insert(OVERLAY_A1);
+
+        assertContains(mSettings, OVERLAY_A0);
+        assertContains(mSettings, OVERLAY_B0);
+        assertContains(mSettings, OVERLAY_A1);
+
+        mSettings.removeUser(OVERLAY_A0.userId);
+
+        assertDoesNotContain(mSettings, OVERLAY_A0);
+        assertDoesNotContain(mSettings, OVERLAY_B0);
+        assertContains(mSettings, OVERLAY_A1);
+    }
+
+    @Test
+    public void testOrderOfNewlyAddedItems() throws Exception {
+        // new items are appended to the list
+        insert(OVERLAY_A0);
+        insert(OVERLAY_B0);
+        insert(OVERLAY_C0);
+
+        List<OverlayInfo> list =
+                mSettings.getOverlaysForTarget(OVERLAY_A0.targetPackageName, OVERLAY_A0.userId);
+        assertListsAreEqual(list, OVERLAY_A0, OVERLAY_B0, OVERLAY_C0);
+
+        // overlays keep their positions when updated
+        mSettings.setState(OVERLAY_B0.packageName, OVERLAY_B0.userId, STATE_ENABLED);
+        OverlayInfo oi = mSettings.getOverlayInfo(OVERLAY_B0.packageName, OVERLAY_B0.userId);
+
+        list = mSettings.getOverlaysForTarget(OVERLAY_A0.targetPackageName, OVERLAY_A0.userId);
+        assertListsAreEqual(list, OVERLAY_A0, oi, OVERLAY_C0);
+    }
+
+    @Test
+    public void testSetPriority() throws Exception {
+        insert(OVERLAY_A0);
+        insert(OVERLAY_B0);
+        insert(OVERLAY_C0);
+
+        List<OverlayInfo> list =
+                mSettings.getOverlaysForTarget(OVERLAY_A0.targetPackageName, OVERLAY_A0.userId);
+        assertListsAreEqual(list, OVERLAY_A0, OVERLAY_B0, OVERLAY_C0);
+
+        boolean changed = mSettings.setPriority(OVERLAY_B0.packageName, OVERLAY_C0.packageName,
+                OVERLAY_B0.userId);
+        assertTrue(changed);
+        list = mSettings.getOverlaysForTarget(OVERLAY_A0.targetPackageName, OVERLAY_A0.userId);
+        assertListsAreEqual(list, OVERLAY_A0, OVERLAY_C0, OVERLAY_B0);
+
+        changed =
+            mSettings.setPriority(OVERLAY_B0.packageName, "does.not.exist", OVERLAY_B0.userId);
+        assertFalse(changed);
+        list = mSettings.getOverlaysForTarget(OVERLAY_A0.targetPackageName, OVERLAY_A0.userId);
+        assertListsAreEqual(list, OVERLAY_A0, OVERLAY_C0, OVERLAY_B0);
+
+        OverlayInfo otherTarget = new OverlayInfo(
+                "com.dummy.overlay_other",
+                "com.dummy.some.other.target",
+                null,
+                "some-category",
+                "/data/app/com.dummy.overlay_other-1/base.apk",
+                STATE_DISABLED,
+                0,
+                0,
+                false);
+        insert(otherTarget);
+        changed = mSettings.setPriority(OVERLAY_A0.packageName, otherTarget.packageName,
+                OVERLAY_A0.userId);
+        assertFalse(changed);
+    }
+
+    @Test
+    public void testSetLowestPriority() throws Exception {
+        insert(OVERLAY_A0);
+        insert(OVERLAY_B0);
+        insert(OVERLAY_C0);
+
+        List<OverlayInfo> list =
+                mSettings.getOverlaysForTarget(OVERLAY_A0.targetPackageName, OVERLAY_A0.userId);
+        assertListsAreEqual(list, OVERLAY_A0, OVERLAY_B0, OVERLAY_C0);
+
+        boolean changed = mSettings.setLowestPriority(OVERLAY_B0.packageName, OVERLAY_B0.userId);
+        assertTrue(changed);
+
+        list = mSettings.getOverlaysForTarget(OVERLAY_A0.targetPackageName, OVERLAY_A0.userId);
+        assertListsAreEqual(list, OVERLAY_B0, OVERLAY_A0, OVERLAY_C0);
+    }
+
+    @Test
+    public void testSetHighestPriority() throws Exception {
+        insert(OVERLAY_A0);
+        insert(OVERLAY_B0);
+        insert(OVERLAY_C0);
+
+        List<OverlayInfo> list =
+                mSettings.getOverlaysForTarget(OVERLAY_A0.targetPackageName, OVERLAY_A0.userId);
+        assertListsAreEqual(list, OVERLAY_A0, OVERLAY_B0, OVERLAY_C0);
+
+        boolean changed = mSettings.setHighestPriority(OVERLAY_B0.packageName, OVERLAY_B0.userId);
+        assertTrue(changed);
+
+        list = mSettings.getOverlaysForTarget(OVERLAY_A0.targetPackageName, OVERLAY_A0.userId);
+        assertListsAreEqual(list, OVERLAY_A0, OVERLAY_C0, OVERLAY_B0);
+    }
+
+    // tests: persist and restore
+
+    @Test
+    public void testPersistEmpty() throws Exception {
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        mSettings.persist(os);
+        String xml = new String(os.toByteArray(), "utf-8");
+
+        assertEquals(1, countXmlTags(xml, "overlays"));
+        assertEquals(0, countXmlTags(xml, "item"));
+    }
+
+    @Test
+    public void testPersistDifferentOverlaysSameUser() throws Exception {
+        insert(OVERLAY_A0);
+        insert(OVERLAY_B0);
+
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        mSettings.persist(os);
+        final String xml = new String(os.toByteArray(), "utf-8");
+
+        assertEquals(1, countXmlTags(xml, "overlays"));
+        assertEquals(2, countXmlTags(xml, "item"));
+        assertEquals(1, countXmlAttributesWhere(xml, "item", "packageName",
+                    OVERLAY_A0.packageName));
+        assertEquals(1, countXmlAttributesWhere(xml, "item", "packageName",
+                    OVERLAY_B0.packageName));
+        assertEquals(2, countXmlAttributesWhere(xml, "item", "userId",
+                    Integer.toString(OVERLAY_A0.userId)));
+    }
+
+    @Test
+    public void testPersistSameOverlayDifferentUsers() throws Exception {
+        insert(OVERLAY_A0);
+        insert(OVERLAY_A1);
+
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        mSettings.persist(os);
+        String xml = new String(os.toByteArray(), "utf-8");
+
+        assertEquals(1, countXmlTags(xml, "overlays"));
+        assertEquals(2, countXmlTags(xml, "item"));
+        assertEquals(2, countXmlAttributesWhere(xml, "item", "packageName",
+                    OVERLAY_A0.packageName));
+        assertEquals(1, countXmlAttributesWhere(xml, "item", "userId",
+                    Integer.toString(OVERLAY_A0.userId)));
+        assertEquals(1, countXmlAttributesWhere(xml, "item", "userId",
+                    Integer.toString(OVERLAY_A1.userId)));
+    }
+
+    @Test
+    public void testPersistEnabled() throws Exception {
+        insert(OVERLAY_A0);
+        mSettings.setEnabled(OVERLAY_A0.packageName, OVERLAY_A0.userId, true);
+
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        mSettings.persist(os);
+        String xml = new String(os.toByteArray(), "utf-8");
+
+        assertEquals(1, countXmlAttributesWhere(xml, "item", "isEnabled", "true"));
+    }
+
+    @Test
+    public void testRestoreEmpty() throws Exception {
+        final int version = OverlayManagerSettings.Serializer.CURRENT_VERSION;
+        final String xml =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<overlays version=\"" + version + "\" />\n";
+        ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes("utf-8"));
+
+        mSettings.restore(is);
+        assertDoesNotContain(mSettings, "com.dummy.overlay", 0);
+    }
+
+    @Test
+    public void testRestoreSingleUserSingleOverlay() throws Exception {
+        final int version = OverlayManagerSettings.Serializer.CURRENT_VERSION;
+        final String xml =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes'?>\n"
+                + "<overlays version='" + version + "'>\n"
+                + "<item packageName='com.dummy.overlay'\n"
+                + "      userId='1234'\n"
+                + "      targetPackageName='com.dummy.target'\n"
+                + "      baseCodePath='/data/app/com.dummy.overlay-1/base.apk'\n"
+                + "      state='" + STATE_DISABLED + "'\n"
+                + "      isEnabled='false'\n"
+                + "      category='dummy-category'\n"
+                + "      isStatic='false'\n"
+                + "      priority='0' />\n"
+                + "</overlays>\n";
+        ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes("utf-8"));
+
+        mSettings.restore(is);
+        OverlayInfo oi = mSettings.getOverlayInfo("com.dummy.overlay", 1234);
+        assertNotNull(oi);
+        assertEquals("com.dummy.overlay", oi.packageName);
+        assertEquals("com.dummy.target", oi.targetPackageName);
+        assertEquals("/data/app/com.dummy.overlay-1/base.apk", oi.baseCodePath);
+        assertEquals(1234, oi.userId);
+        assertEquals(STATE_DISABLED, oi.state);
+        assertFalse(mSettings.getEnabled("com.dummy.overlay", 1234));
+    }
+
+    @Test
+    public void testPersistAndRestore() throws Exception {
+        insert(OVERLAY_A0);
+        insert(OVERLAY_B1);
+
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        mSettings.persist(os);
+        String xml = new String(os.toByteArray(), "utf-8");
+        ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes("utf-8"));
+        OverlayManagerSettings newSettings = new OverlayManagerSettings();
+        newSettings.restore(is);
+
+        OverlayInfo a = newSettings.getOverlayInfo(OVERLAY_A0.packageName, OVERLAY_A0.userId);
+        assertEquals(OVERLAY_A0, a);
+
+        OverlayInfo b = newSettings.getOverlayInfo(OVERLAY_B1.packageName, OVERLAY_B1.userId);
+        assertEquals(OVERLAY_B1, b);
+    }
+
+    private int countXmlTags(String xml, String tagToLookFor) throws Exception {
+        int count = 0;
+        XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(new StringReader(xml));
+        int event = parser.getEventType();
+        while (event != XmlPullParser.END_DOCUMENT) {
+            if (event == XmlPullParser.START_TAG && tagToLookFor.equals(parser.getName())) {
+                count++;
+            }
+            event = parser.next();
+        }
+        return count;
+    }
+
+    private int countXmlAttributesWhere(String xml, String tag, String attr, String value)
+            throws Exception {
+        int count = 0;
+        XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(new StringReader(xml));
+        int event = parser.getEventType();
+        while (event != XmlPullParser.END_DOCUMENT) {
+            if (event == XmlPullParser.START_TAG && tag.equals(parser.getName())) {
+                String v = parser.getAttributeValue(null, attr);
+                if (value.equals(v)) {
+                    count++;
+                }
+            }
+            event = parser.next();
+        }
+        return count;
+    }
+
+    private void insert(OverlayInfo oi) throws Exception {
+        mSettings.init(oi.packageName, oi.userId, oi.targetPackageName, null, oi.baseCodePath,
+                false, 0, oi.category);
+        mSettings.setState(oi.packageName, oi.userId, oi.state);
+        mSettings.setEnabled(oi.packageName, oi.userId, false);
+    }
+
+    private static void assertContains(final OverlayManagerSettings settings,
+            final OverlayInfo oi) {
+        assertContains(settings, oi.packageName, oi.userId);
+    }
+
+    private static void assertContains(final OverlayManagerSettings settings,
+            final String packageName, int userId) {
+        try {
+            settings.getOverlayInfo(packageName, userId);
+        } catch (OverlayManagerSettings.BadKeyException e) {
+            fail(String.format("settings does not contain packageName=%s userId=%d",
+                        packageName, userId));
+        }
+    }
+
+    private static void assertDoesNotContain(final OverlayManagerSettings settings,
+            final OverlayInfo oi) {
+        assertDoesNotContain(settings, oi.packageName, oi.userId);
+    }
+
+    private static void assertDoesNotContain(final OverlayManagerSettings settings,
+            final String packageName, int userId) {
+        try {
+            settings.getOverlayInfo(packageName, userId);
+            fail(String.format("settings contains packageName=%s userId=%d", packageName, userId));
+        } catch (OverlayManagerSettings.BadKeyException e) {
+            // do nothing: we expect to end up here
+        }
+    }
+
+    private static void assertContains(int[] haystack, int needle) {
+        List<Integer> list = IntStream.of(haystack)
+                .boxed()
+                .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
+        if (!list.contains(needle)) {
+            fail(String.format("integer array [%s] does not contain value %s",
+                        TextUtils.join(",", list), needle));
+        }
+    }
+
+    private static void assertDoesNotContain(int[] haystack, int needle) {
+        List<Integer> list = IntStream.of(haystack)
+                .boxed()
+                .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
+        if (list.contains(needle)) {
+            fail(String.format("integer array [%s] contains value %s",
+                        TextUtils.join(",", list), needle));
+        }
+    }
+
+    private static void assertListsAreEqual(List<OverlayInfo> list, OverlayInfo... array) {
+        List<OverlayInfo> other = Stream.of(array)
+                .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
+        assertListsAreEqual(list, other);
+    }
+
+    private static void assertListsAreEqual(List<OverlayInfo> list, List<OverlayInfo> other) {
+        if (!list.equals(other)) {
+            fail(String.format("lists [%s] and [%s] differ",
+                        TextUtils.join(",", list), TextUtils.join(",", other)));
+        }
+    }
+}