Configure existing ThemePicker Robolectric tests to run with atest.

Bug: 260777356
Bug: 260925899
Test: this is the test
Change-Id: I25c4efdb73dc5eb88611efac669de13fa2be0c3d
(cherry picked from commit 9a9c7c18096f41f07eef5f8e7350dd3edbc8b892)
Merged-In: I25c4efdb73dc5eb88611efac669de13fa2be0c3d
diff --git a/tests/robotests/Android.bp b/tests/robotests/Android.bp
new file mode 100644
index 0000000..e0a37c2
--- /dev/null
+++ b/tests/robotests/Android.bp
@@ -0,0 +1,17 @@
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_robolectric_test {
+    name: "ThemePickerRoboTests",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    java_resource_dirs: ["config"],
+    libs: [
+        "androidx.test.core",
+        "androidx.test.runner",
+    ],
+    instrumentation_for: "ThemePicker",
+}
diff --git a/tests/robotests/AndroidManifest.xml b/tests/robotests/AndroidManifest.xml
new file mode 100644
index 0000000..753aa9e
--- /dev/null
+++ b/tests/robotests/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.customization">
+    <application/>
+</manifest>
diff --git a/tests/robotests/config/robolectric.properties b/tests/robotests/config/robolectric.properties
new file mode 100644
index 0000000..fab7251
--- /dev/null
+++ b/tests/robotests/config/robolectric.properties
@@ -0,0 +1 @@
+sdk=NEWEST_SDK
diff --git a/tests/robotests/res/values/overlayable_icons_test.xml b/tests/robotests/res/values/overlayable_icons_test.xml
new file mode 100644
index 0000000..73cffe1
--- /dev/null
+++ b/tests/robotests/res/values/overlayable_icons_test.xml
@@ -0,0 +1,34 @@
+<!--
+   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.
+-->
+<resources>
+  <!-- overlayable_icons references all of the drawables in this package
+       that are being overlayed by resource overlays. If you remove/rename
+       any of these resources, you must also change the resource overlay icons.-->
+  <array name="overlayable_icons">
+    <item>@drawable/ic_add_24px</item>
+    <item>@drawable/ic_close_24px</item>
+    <item>@drawable/ic_colorize_24px</item>
+    <item>@drawable/ic_delete_24px</item>
+    <item>@drawable/ic_font</item>
+    <item>@drawable/ic_nav_clock</item>
+    <item>@drawable/ic_nav_grid</item>
+    <item>@drawable/ic_nav_theme</item>
+    <item>@drawable/ic_nav_wallpaper</item>
+    <item>@drawable/ic_shapes_24px</item>
+    <item>@drawable/ic_tune</item>
+    <item>@drawable/ic_wifi_24px</item>
+  </array>
+</resources>
diff --git a/tests/robotests/src/com/android/customization/model/clock/BaseClockManagerTest.java b/tests/robotests/src/com/android/customization/model/clock/BaseClockManagerTest.java
new file mode 100644
index 0000000..c96e7f8
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/model/clock/BaseClockManagerTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.customization.model.clock;
+
+import static junit.framework.TestCase.assertTrue;
+import static junit.framework.TestCase.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager.Callback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class BaseClockManagerTest {
+
+    private static final String CURRENT_CLOCK = "current_clock";
+
+    @Mock ClockProvider mProvider;
+    private TestClockManager mManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mManager = new TestClockManager(mProvider);
+    }
+
+    @Test
+    public void testIsAvailable() {
+        // GIVEN that the ClockProvider is available
+        when(mProvider.isAvailable()).thenReturn(true);
+        // THEN the BaseClockManager is true
+        assertTrue(mManager.isAvailable());
+    }
+
+    @Test
+    public void testApply() {
+        final String id = "id";
+        Clockface clock = new Clockface.Builder().setId(id).build();
+
+        mManager.apply(clock, new Callback() {
+            @Override
+            public void onSuccess() {
+                //Nothing to do here, the test passed
+            }
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                fail("onError was called when grid had been applied successfully");
+            }
+        });
+
+        assertEquals(id, mManager.getClockId());
+    }
+
+    @Test
+    public void testFetch() {
+        mManager.fetchOptions(null, false);
+        verify(mProvider).fetch(eq(null), anyBoolean());
+    }
+
+    /**
+     * Testable BaseClockManager that provides basic implementations of abstract methods.
+     */
+    private static final class TestClockManager extends BaseClockManager {
+
+        private String mClockId;
+
+        TestClockManager(ClockProvider provider) {
+            super(provider);
+        }
+
+        String getClockId() {
+            return mClockId;
+        }
+
+        @Override
+        protected void handleApply(Clockface option, Callback callback) {
+            mClockId = option.getId();
+            callback.onSuccess();
+        }
+
+        @Override
+        protected String lookUpCurrentClock() {
+            return CURRENT_CLOCK;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/customization/model/clock/ClockManagerTest.java b/tests/robotests/src/com/android/customization/model/clock/ClockManagerTest.java
new file mode 100644
index 0000000..574548a
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/model/clock/ClockManagerTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.customization.model.clock;
+
+import static junit.framework.TestCase.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.provider.Settings.Secure;
+
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.module.ThemesUserEventLogger;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class ClockManagerTest {
+
+    private static final String CLOCK_ID = "id";
+    private static final String CLOCK_FIELD = "clock";
+    private static final String CLOCK_FACE_SETTING = "fake_clock_face_setting";
+
+    @Mock ClockProvider mProvider;
+    @Mock ThemesUserEventLogger mLogger;
+    private ContentResolver mContentResolver;
+    private ClockManager mManager;
+    @Mock private Clockface mMockClockface;
+    @Mock private Callback mMockCallback;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContentResolver = RuntimeEnvironment.application.getContentResolver();
+        mManager = new ClockManager(mContentResolver, mProvider, mLogger);
+    }
+
+    @Test
+    public void testApply() throws JSONException {
+        Clockface clock = new Clockface.Builder().setId(CLOCK_ID).build();
+
+        mManager.apply(clock, new Callback() {
+            @Override
+            public void onSuccess() {
+                //Nothing to do here, the test passed
+            }
+
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                fail("onError was called when grid had been applied successfully");
+            }
+        });
+
+        // THEN the clock id is written to secure settings.
+        JSONObject json =
+                new JSONObject(Secure.getString(mContentResolver, ClockManager.CLOCK_FACE_SETTING));
+        assertEquals(CLOCK_ID, json.getString(CLOCK_FIELD));
+        // AND the event is logged
+        verify(mLogger).logClockApplied(clock);
+    }
+
+    @Test
+    public void testApply_whenJSONExceptionOccurs_callsOnError() {
+        doAnswer((invocation) -> {
+            throw new JSONException("Fake Test Excepton");
+        }).when(mMockClockface).getId();
+
+        mManager.apply(mMockClockface, mMockCallback);
+
+        verify(mMockCallback).onError(null);
+    }
+
+    @Test
+    public void testGetCurrentClock_returnsClockId() throws JSONException {
+        // Secure settings contains a clock id
+        JSONObject json = new JSONObject().put(CLOCK_FIELD, CLOCK_ID);
+        Secure.putString(mContentResolver, ClockManager.CLOCK_FACE_SETTING, json.toString());
+
+        // The current clock is that id
+        assertEquals(CLOCK_ID, mManager.getCurrentClock());
+    }
+
+    @Test
+    public void testGetCurrentClock_whenJSONExceptionOccurs_returnsClockFaceSetting() {
+        // Secure settings contains a clock face setting with invalid format to cause JSONException.
+        Secure.putString(mContentResolver, ClockManager.CLOCK_FACE_SETTING, CLOCK_FACE_SETTING);
+
+        // The current clock is the clock face setting which is saved in secure settings.
+        assertEquals(CLOCK_FACE_SETTING, mManager.getCurrentClock());
+    }
+
+    @Test
+    public void testGetCurrentClock_withNullIdInSecureSettings_returnsNullId() {
+        // Secure settings contains a null clock id
+        Secure.putString(mContentResolver, ClockManager.CLOCK_FACE_SETTING, /* value= */ null);
+
+        // The current clock is null
+        assertEquals(null, mManager.getCurrentClock());
+    }
+
+    @Test
+    public void testGetCurrentClock_withEmptyIdInSecureSettings_returnsEmptyId() {
+        // Secure settings contains an empty clock id
+        Secure.putString(mContentResolver, ClockManager.CLOCK_FACE_SETTING, /* value= */ "");
+
+        // The current clock is empty
+        assertEquals("", mManager.getCurrentClock());
+    }
+}
diff --git a/tests/robotests/src/com/android/customization/model/color/ColorCustomizationManagerTest.kt b/tests/robotests/src/com/android/customization/model/color/ColorCustomizationManagerTest.kt
new file mode 100644
index 0000000..80d01c6
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/model/color/ColorCustomizationManagerTest.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2022 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.customization.model.color
+
+import android.app.WallpaperColors
+import android.graphics.Color
+import com.android.customization.model.CustomizationManager
+import com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR
+import com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE
+import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_HOME
+import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_PRESET
+import com.android.customization.model.color.ColorOptionsProvider.OVERLAY_COLOR_BOTH
+import com.android.customization.model.color.ColorOptionsProvider.OVERLAY_COLOR_INDEX
+import com.android.customization.model.color.ColorOptionsProvider.OVERLAY_COLOR_SOURCE
+import com.android.customization.model.theme.OverlayManagerCompat
+import com.android.systemui.monet.Style
+import com.google.common.truth.Truth.assertThat
+import org.json.JSONObject
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+
+/** Tests of {@link ColorCustomizationManager}. */
+// TODO(b/222433744): most of these tests are failing due to the manager apk missing in the image
+@RunWith(RobolectricTestRunner::class)
+class ColorCustomizationManagerTest {
+
+    @get:Rule val rule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var provider: ColorOptionsProvider
+    @Mock private lateinit var mockOM: OverlayManagerCompat
+
+    private lateinit var manager: ColorCustomizationManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val application = RuntimeEnvironment.application
+        manager = ColorCustomizationManager(provider, application.contentResolver, mockOM)
+    }
+
+    @Test
+    @Ignore("b/260925899")
+    fun testParseSettings() {
+        val source = COLOR_SOURCE_HOME
+        val style = Style.SPRITZ
+        val someColor = "aabbcc"
+        val someOtherColor = "bbccdd"
+        val settings =
+            mapOf(
+                OVERLAY_CATEGORY_SYSTEM_PALETTE to someColor,
+                OVERLAY_CATEGORY_COLOR to someOtherColor,
+                OVERLAY_COLOR_SOURCE to source,
+                ColorOption.TIMESTAMP_FIELD to "12345"
+            )
+        val json = JSONObject(settings).toString()
+
+        manager.parseSettings(json)
+
+        assertThat(manager.currentColorSource).isEqualTo(source)
+        assertThat(manager.currentStyle).isEqualTo(style)
+        assertThat(manager.currentOverlays.size).isEqualTo(2)
+        assertThat(manager.currentOverlays.get(OVERLAY_CATEGORY_COLOR)).isEqualTo(someOtherColor)
+        assertThat(manager.currentOverlays.get(OVERLAY_CATEGORY_SYSTEM_PALETTE))
+            .isEqualTo(someColor)
+    }
+
+    @Test
+    @Ignore("b/260925899")
+    fun apply_ColorBundle_index() {
+        testApplyColorBundle(1, "1")
+        testApplyColorBundle(2, "2")
+        testApplyColorBundle(3, "3")
+        testApplyColorBundle(4, "4")
+    }
+
+    private fun testApplyColorBundle(index: Int, value: String) {
+        manager.apply(
+            getColorBundle(index),
+            object : CustomizationManager.Callback {
+                override fun onSuccess() {}
+                override fun onError(throwable: Throwable?) {}
+            }
+        )
+
+        val overlaysJson = JSONObject(manager.storedOverlays)
+
+        assertThat(overlaysJson.getString(OVERLAY_COLOR_INDEX)).isEqualTo(value)
+    }
+
+    private fun getColorBundle(index: Int): ColorBundle {
+        return ColorBundle(
+            "fake color",
+            mapOf("fake_package" to "fake_color"),
+            /* isDefault= */ false,
+            null,
+            /* index= */ index,
+            null
+        )
+    }
+
+    @Test
+    @Ignore("b/260925899")
+    fun apply_ColorSeed_index() {
+        testApplyColorSeed(1, "1")
+        testApplyColorSeed(2, "2")
+        testApplyColorSeed(3, "3")
+        testApplyColorSeed(4, "4")
+    }
+
+    private fun testApplyColorSeed(index: Int, value: String) {
+        manager.apply(
+            getColorSeed(index),
+            object : CustomizationManager.Callback {
+                override fun onSuccess() {}
+                override fun onError(throwable: Throwable?) {}
+            }
+        )
+
+        val overlaysJson = JSONObject(manager.storedOverlays)
+        assertThat(overlaysJson.getString(OVERLAY_COLOR_INDEX)).isEqualTo(value)
+    }
+
+    private fun getColorSeed(index: Int): ColorSeedOption {
+        return ColorSeedOption(
+            "fake color",
+            mapOf("fake_package" to "fake_color"),
+            /* isDefault= */ false,
+            COLOR_SOURCE_PRESET,
+            null,
+            index,
+            null
+        )
+    }
+
+    @Test
+    @Ignore("b/260925899")
+    fun testApply_colorSeedFromWallpaperBoth_shouldReturnBothValue() {
+        val wallpaperColor = WallpaperColors(Color.valueOf(Color.RED), null, null)
+        manager.setWallpaperColors(wallpaperColor, wallpaperColor)
+
+        manager.apply(
+            getColorSeed(anyInt()),
+            object : CustomizationManager.Callback {
+                override fun onSuccess() {}
+                override fun onError(throwable: Throwable?) {}
+            }
+        )
+
+        val overlaysJson = JSONObject(manager.storedOverlays)
+        assertThat(overlaysJson.getString(OVERLAY_COLOR_BOTH)).isEqualTo("1")
+    }
+
+    @Test
+    @Ignore("b/260925899")
+    fun testApply_colorSeedFromWallpaperDifferent_shouldReturnNonBothValue() {
+        val wallpaperColor1 = WallpaperColors(Color.valueOf(Color.RED), null, null)
+        val wallpaperColor2 = WallpaperColors(Color.valueOf(Color.BLUE), null, null)
+        manager.setWallpaperColors(wallpaperColor1, wallpaperColor2)
+
+        manager.apply(
+            getColorSeed(anyInt()),
+            object : CustomizationManager.Callback {
+                override fun onSuccess() {}
+                override fun onError(throwable: Throwable?) {}
+            }
+        )
+
+        val overlaysJson = JSONObject(manager.storedOverlays)
+        assertThat(overlaysJson.getString(OVERLAY_COLOR_BOTH)).isEqualTo("0")
+    }
+}
diff --git a/tests/robotests/src/com/android/customization/model/color/ColorOptionTest.kt b/tests/robotests/src/com/android/customization/model/color/ColorOptionTest.kt
new file mode 100644
index 0000000..0431c19
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/model/color/ColorOptionTest.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2022 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.customization.model.color
+
+import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_HOME
+import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_LOCK
+import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_PRESET
+import com.android.systemui.monet.Style
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.RobolectricTestRunner
+
+/** Tests of {@link ColorOption}. */
+@RunWith(RobolectricTestRunner::class)
+class ColorOptionTest {
+
+    @get:Rule val rule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var manager: ColorCustomizationManager
+
+    @Test
+    fun colorOption_Source_Preset() {
+        val bundleOption: ColorOption =
+            ColorBundle(
+                "fake color",
+                mapOf("fake_package" to "fake_color"),
+                false,
+                null,
+                /* index= */ 0,
+                null
+            )
+        assertEquals(COLOR_SOURCE_PRESET, bundleOption.source)
+    }
+
+    @Test
+    fun colorOption_bundle_index() {
+        testBundleOptionIndex(1)
+        testBundleOptionIndex(2)
+        testBundleOptionIndex(3)
+        testBundleOptionIndex(4)
+    }
+
+    private fun testBundleOptionIndex(index: Int) {
+        val bundleOption: ColorBundle =
+            ColorBundle(
+                "fake color",
+                mapOf("fake_package" to "fake_color"),
+                false,
+                null,
+                /* index= */ index,
+                null
+            )
+        assertThat(bundleOption.index).isEqualTo(index)
+    }
+
+    @Test
+    fun colorOption_Source_Seed() {
+        testSeedOptionSource(COLOR_SOURCE_HOME)
+        testSeedOptionSource(COLOR_SOURCE_LOCK)
+    }
+
+    private fun testSeedOptionSource(source: String) {
+        val seedOption: ColorOption =
+            ColorSeedOption(
+                "fake color",
+                mapOf("fake_package" to "fake_color"),
+                false,
+                source,
+                null,
+                /* index= */ 0,
+                null
+            )
+        assertThat(seedOption.source).isEqualTo(source)
+    }
+
+    @Test
+    fun colorOption_seed_style() {
+        testSeedOptionStyle(Style.TONAL_SPOT)
+        testSeedOptionStyle(Style.SPRITZ)
+        testSeedOptionStyle(Style.VIBRANT)
+        testSeedOptionStyle(Style.EXPRESSIVE)
+    }
+
+    private fun testSeedOptionStyle(style: Style) {
+        val seedOption: ColorOption =
+            ColorSeedOption(
+                "fake color",
+                mapOf("fake_package" to "fake_color"),
+                /* isDefault= */ false,
+                "fake_source",
+                style,
+                0,
+                null
+            )
+        assertThat(seedOption.style).isEqualTo(style)
+    }
+
+    @Test
+    fun colorOption_seed_index() {
+        testSeedOptionIndex(1)
+        testSeedOptionIndex(2)
+        testSeedOptionIndex(3)
+        testSeedOptionIndex(4)
+    }
+
+    private fun testSeedOptionIndex(index: Int) {
+        val seedOption: ColorOption =
+            ColorSeedOption(
+                "fake color",
+                mapOf("fake_package" to "fake_color"),
+                /* isDefault= */ false,
+                "fake_source",
+                Style.TONAL_SPOT,
+                index,
+                /* previewInfo= */ null
+            )
+        assertThat(seedOption.index).isEqualTo(index)
+    }
+
+    private fun setUpSeedOption(
+        isDefault: Boolean,
+        source: String = "some_source"
+    ): ColorSeedOption {
+        val overlays =
+            if (isDefault) {
+                HashMap()
+            } else {
+                mapOf("package" to "value", "otherPackage" to "otherValue")
+            }
+        `when`(manager.currentOverlays).thenReturn(overlays)
+        return ColorSeedOption(
+            "seed",
+            overlays,
+            isDefault,
+            source,
+            Style.TONAL_SPOT,
+            /* index= */ 0,
+            /* previewInfo= */ null
+        )
+    }
+
+    @Test
+    fun seedOption_isActive_notDefault_SourceSet() {
+        val source = "some_source"
+        val seedOption = setUpSeedOption(false, source)
+        `when`(manager.currentColorSource).thenReturn(source)
+
+        assertThat(seedOption.isActive(manager)).isTrue()
+    }
+
+    @Test
+    fun seedOption_isActive_notDefault_NoSource() {
+        val seedOption = setUpSeedOption(false)
+        `when`(manager.currentColorSource).thenReturn(null)
+
+        assertThat(seedOption.isActive(manager)).isTrue()
+    }
+
+    @Test
+    fun seedOption_isActive_notDefault_differentSource() {
+        val seedOption = setUpSeedOption(false)
+        `when`(manager.currentColorSource).thenReturn("some_other_source")
+
+        assertThat(seedOption.isActive(manager)).isFalse()
+    }
+
+    @Test
+    fun seedOption_isActive_default_emptyJson() {
+        val seedOption = setUpSeedOption(true)
+        `when`(manager.storedOverlays).thenReturn("")
+
+        assertThat(seedOption.isActive(manager)).isTrue()
+    }
+
+    @Test
+    fun seedOption_isActive_default_nonEmptyJson() {
+        val seedOption = setUpSeedOption(true)
+
+        `when`(manager.storedOverlays).thenReturn("{non-empty-json}")
+
+        // Should still be Active because overlays is empty
+        assertThat(seedOption.isActive(manager)).isTrue()
+    }
+
+    @Test
+    @Ignore("b/260925899")
+    fun seedOption_isActive_default_nonEmptyOverlays() {
+        val seedOption = setUpSeedOption(true)
+
+        `when`(manager.currentOverlays).thenReturn(mapOf("a" to "b"))
+        // TODO(b/222433744): failing as it's true
+        assertThat(seedOption.isActive(manager)).isFalse()
+    }
+}
diff --git a/tests/robotests/src/com/android/customization/model/color/ColorSectionControllerTest.java b/tests/robotests/src/com/android/customization/model/color/ColorSectionControllerTest.java
new file mode 100644
index 0000000..820e641
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/model/color/ColorSectionControllerTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 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.customization.model.color;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.android.wallpaper.model.WallpaperColorsViewModel;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/**
+ * Tests of {@link ColorSectionController}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public final class ColorSectionControllerTest {
+
+    private AppCompatActivity mActivity;
+    private ColorSectionController mColorSectionController;
+
+    /**
+     * Set up the test case.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mActivity = Robolectric.buildActivity(AppCompatActivity.class).create().get();
+        mColorSectionController = new ColorSectionController(mActivity,
+                new WallpaperColorsViewModel(), mActivity, null);
+    }
+
+    /**
+     * isAvailable()'s test.
+     */
+    @Test
+    @Config(manifest = Config.NONE)
+    public void isAvailable_nullContext_shouldReturnFalse() {
+        assertThat(mColorSectionController.isAvailable(/* context= */ null)).isFalse();
+    }
+}
+
diff --git a/tests/robotests/src/com/android/customization/model/grid/GridOptionsManagerTest.java b/tests/robotests/src/com/android/customization/model/grid/GridOptionsManagerTest.java
new file mode 100644
index 0000000..89ca676
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/model/grid/GridOptionsManagerTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.customization.model.grid;
+
+import static junit.framework.TestCase.fail;
+
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.module.ThemesUserEventLogger;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class GridOptionsManagerTest {
+
+    @Mock LauncherGridOptionsProvider mProvider;
+    @Mock ThemesUserEventLogger mThemesUserEventLogger;
+    private GridOptionsManager mManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mManager = new GridOptionsManager(mProvider, mThemesUserEventLogger);
+    }
+
+    @Test
+    public void testApply() {
+        String gridName = "testName";
+        GridOption grid = new GridOption("testTitle", gridName, false, 2, 2, null, 1, "");
+        when(mProvider.applyGrid(gridName)).thenReturn(1);
+
+        mManager.apply(grid, new Callback() {
+            @Override
+            public void onSuccess() {
+                //Nothing to do here, the test passed
+            }
+
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                fail("onError was called when grid had been applied successfully");
+            }
+        });
+    }
+
+    @Test
+    @Ignore("b/260925899")
+    public void testFetch_backgroundThread() {
+        mManager.fetchOptions(null, false);
+        verify(mProvider).fetch(anyBoolean());
+    }
+}
diff --git a/tests/robotests/src/com/android/customization/model/theme/ThemeManagerTest.java b/tests/robotests/src/com/android/customization/model/theme/ThemeManagerTest.java
new file mode 100644
index 0000000..cfb8a33
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/model/theme/ThemeManagerTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.customization.model.theme;
+
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
+import static com.android.customization.model.ResourceConstants.SETTINGS_PACKAGE;
+import static com.android.customization.model.ResourceConstants.SYSUI_PACKAGE;
+import static com.android.customization.model.ResourceConstants.THEME_SETTING;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.TestCase.assertEquals;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.theme.custom.CustomTheme;
+import com.android.customization.module.ThemesUserEventLogger;
+import com.android.customization.testutils.OverlayManagerMocks;
+
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ThemeManagerTest {
+
+    @Mock OverlayManagerCompat mMockOm;
+    @Mock ThemesUserEventLogger mThemesUserEventLogger;
+    @Mock ThemeBundleProvider mThemeBundleProvider;
+    private OverlayManagerMocks mMockOmHelper;
+    private ThemeManager mThemeManager;
+    private FragmentActivity mActivity;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        FragmentActivity activity = Robolectric.buildActivity(FragmentActivity.class).get();
+        mActivity = spy(activity);
+        mMockOmHelper = new OverlayManagerMocks();
+        mMockOmHelper.setUpMock(mMockOm);
+        mThemeManager = new ThemeManager(mThemeBundleProvider, activity,
+                mMockOm, mThemesUserEventLogger);
+    }
+
+    @After
+    public void cleanUp() {
+        mMockOmHelper.clearOverlays();
+    }
+
+    @Test
+    public void apply_WithDefaultTheme_StoresEmptyJsonString() {
+        mMockOmHelper.addOverlay("test.package.name_color", ANDROID_PACKAGE,
+                OVERLAY_CATEGORY_COLOR, true, 0);
+        mMockOmHelper.addOverlay("test.package.name_font", ANDROID_PACKAGE,
+                OVERLAY_CATEGORY_FONT, true, 0);
+        mMockOmHelper.addOverlay("test.package.name_shape", ANDROID_PACKAGE,
+                OVERLAY_CATEGORY_SHAPE, true, 0);
+        mMockOmHelper.addOverlay("test.package.name_icon", ANDROID_PACKAGE,
+                OVERLAY_CATEGORY_ICON_ANDROID, true, 0);
+        mMockOmHelper.addOverlay("test.package.name_settings", SETTINGS_PACKAGE,
+                OVERLAY_CATEGORY_ICON_SETTINGS, true, 0);
+        mMockOmHelper.addOverlay("test.package.name_sysui", SYSUI_PACKAGE,
+                OVERLAY_CATEGORY_ICON_SYSUI, true, 0);
+        mMockOmHelper.addOverlay("test.package.name_themepicker", mActivity.getPackageName(),
+                OVERLAY_CATEGORY_ICON_SYSUI, true, 0);
+
+        ThemeBundle defaultTheme = new ThemeBundle.Builder().asDefault().build(mActivity);
+
+        applyTheme(defaultTheme);
+
+        assertEquals("Secure Setting should be empty JSON string after applying default theme",
+                new JSONObject().toString(),
+                Settings.Secure.getString(mActivity.getContentResolver(), THEME_SETTING));
+    }
+
+    @Test
+    public void apply_WithOverlayTheme_StoresSerializedPackagesWithTimestamp() {
+        ThemeBundle theme = getOverlayTheme();
+        final String serializedPackagesWithTimestamp = theme.getSerializedPackagesWithTimestamp();
+
+        theme = spy(theme);
+        // Makes it return the fixed serializedPackagesWithTimestamp to test. Since we will get
+        // fresh time every time, it's hard to compare for testing.
+        when(theme.getSerializedPackagesWithTimestamp())
+                .thenReturn(serializedPackagesWithTimestamp);
+
+        applyTheme(theme);
+
+        assertEquals("Secure Setting should be the overlay packages after applying theme",
+                serializedPackagesWithTimestamp,
+                Settings.Secure.getString(mActivity.getContentResolver(), THEME_SETTING));
+    }
+
+    @Test
+    public void isAvailable_ThemeBundleProviderAndOverlayManagerAreAvailable_ReturnsTrue() {
+        when(mThemeBundleProvider.isAvailable()).thenReturn(true);
+        when(mMockOm.isAvailable()).thenReturn(true);
+
+        assertTrue(mThemeManager.isAvailable());
+    }
+
+    @Test
+    public void isAvailable_ThemeBundleProviderOrOverlayManagerIsAvailable_ReturnsFalse() {
+        when(mThemeBundleProvider.isAvailable()).thenReturn(false);
+        when(mMockOm.isAvailable()).thenReturn(true);
+        assertFalse(mThemeManager.isAvailable());
+
+        when(mThemeBundleProvider.isAvailable()).thenReturn(true);
+        when(mMockOm.isAvailable()).thenReturn(false);
+        assertFalse(mThemeManager.isAvailable());
+    }
+
+    @Test
+    public void fetchOptions_ThemeBundleProviderFetches() {
+        OptionsFetchedListener listener = mock(OptionsFetchedListener.class);
+
+        mThemeManager.fetchOptions(listener, false);
+
+        verify(mThemeBundleProvider).fetch(listener, false);
+    }
+
+    @Test
+    public void removeCustomTheme_ThemeBundleProviderRemovesCustomTheme() {
+        CustomTheme customTheme = mock(CustomTheme.class);
+        mThemeManager.removeCustomTheme(customTheme);
+
+        verify(mThemeBundleProvider).removeCustomTheme(customTheme);
+    }
+
+    @Test
+    public void findThemeByPackages_ThemeBundleProviderFindsEquivalent() {
+        CustomTheme theme = mock(CustomTheme.class);
+        mThemeManager.findThemeByPackages(theme);
+
+        verify(mThemeBundleProvider).findEquivalent(theme);
+    }
+
+    @Test
+    public void storeEmptyTheme_SettingsSecureStoresEmptyTheme() {
+        mThemeManager.storeEmptyTheme();
+
+        assertEquals(
+                new JSONObject().toString(),
+                Settings.Secure.getString(mActivity.getContentResolver(), THEME_SETTING));
+    }
+
+    @Test
+    public void getStoredOverlays_GetsFromSettingsSecureWithExpectedName() {
+        ThemeBundle theme = getOverlayTheme();
+
+        applyTheme(theme);
+
+        assertEquals(
+                Settings.Secure.getString(mActivity.getContentResolver(), THEME_SETTING),
+                mThemeManager.getStoredOverlays());
+    }
+
+    private ThemeBundle getOverlayTheme() {
+        final String bundleColorPackage = "test.package.name_color";
+        final String bundleFontPackage = "test.package.name_font";
+        final String otherPackage = "other.package.name_font";
+
+        mMockOmHelper.addOverlay(bundleColorPackage, ANDROID_PACKAGE,
+                OVERLAY_CATEGORY_COLOR, false, 0);
+        mMockOmHelper.addOverlay(bundleFontPackage, ANDROID_PACKAGE,
+                OVERLAY_CATEGORY_FONT, false, 0);
+        mMockOmHelper.addOverlay(otherPackage, ANDROID_PACKAGE,
+                OVERLAY_CATEGORY_FONT, false, 0);
+
+        return new ThemeBundle.Builder()
+                .addOverlayPackage(OVERLAY_CATEGORY_COLOR, bundleColorPackage)
+                .addOverlayPackage(OVERLAY_CATEGORY_FONT, bundleFontPackage)
+                .build(mActivity);
+    }
+
+    private void applyTheme(ThemeBundle theme) {
+        mThemeManager.apply(theme, new Callback() {
+            @Override
+            public void onSuccess() {
+            }
+
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+            }
+        });
+    }
+}
diff --git a/tests/robotests/src/com/android/customization/testutils/Condition.java b/tests/robotests/src/com/android/customization/testutils/Condition.java
new file mode 100644
index 0000000..f013555
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/testutils/Condition.java
@@ -0,0 +1,20 @@
+/*
+ * 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.customization.testutils;
+
+public interface Condition {
+    boolean isTrue() throws Throwable;
+}
diff --git a/tests/robotests/src/com/android/customization/testutils/OverlayManagerMocks.java b/tests/robotests/src/com/android/customization/testutils/OverlayManagerMocks.java
new file mode 100644
index 0000000..79b2e43
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/testutils/OverlayManagerMocks.java
@@ -0,0 +1,135 @@
+/*
+ * 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.customization.testutils;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.om.OverlayManager;
+import android.text.TextUtils;
+
+import com.android.customization.model.theme.OverlayManagerCompat;
+
+import org.mockito.stubbing.Answer;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Helper class to provide mock implementation for OverlayManager, to use, create a Mockito Mock
+ * for OverlayManager and call {@link #setUpMock(OverlayManager)} with it, then use
+ * {@link #addOverlay(String, String, String, boolean, int)} to add fake OverlayInfo to be returned
+ * by the mocked OverlayManager.
+ */
+public class OverlayManagerMocks {
+    private static class MockOverlay {
+        final String mPackageName;
+        final String mTargetPackage;
+        final String mCategory;
+
+        MockOverlay(String packageName, String targetPackage, String category) {
+            this.mPackageName = packageName;
+            this.mTargetPackage = targetPackage;
+            this.mCategory = category;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            return obj instanceof MockOverlay
+                    && TextUtils.equals(((MockOverlay) obj).mPackageName, mPackageName)
+                    && TextUtils.equals(((MockOverlay) obj).mTargetPackage, mTargetPackage)
+                    && TextUtils.equals(((MockOverlay) obj).mCategory, mCategory);
+        }
+    }
+
+    private Set<MockOverlay> mAllOverlays = new HashSet<>();
+    private Set<MockOverlay> mEnabledOverlays = new HashSet<>();
+
+    private boolean setEnabled(String packageName, boolean enable, int userId) {
+        if (packageName == null) {
+            return false;
+        }
+        Set<MockOverlay> packageOverlays = mAllOverlays.stream()
+                .filter(mockOverlay -> mockOverlay.mPackageName.equals(packageName)).collect(
+                Collectors.toSet());
+        if (packageOverlays.isEmpty()) {
+            return false;
+        }
+        if (enable) {
+            mEnabledOverlays.addAll(packageOverlays);
+        } else {
+            mEnabledOverlays.removeAll(packageOverlays);
+        }
+        return true;
+    }
+
+    public void addOverlay(String packageName, String targetPackage, String category,
+            boolean enabled, int userId) {
+        MockOverlay overlay = new MockOverlay(packageName, targetPackage, category);
+        mAllOverlays.add(overlay);
+        if (enabled) {
+            mEnabledOverlays.add(overlay);
+        }
+    }
+
+    public void clearOverlays() {
+        mAllOverlays.clear();
+        mEnabledOverlays.clear();
+    }
+
+    public void setUpMock(OverlayManagerCompat mockOverlayManager) {
+        when(mockOverlayManager.getEnabledPackageName(anyString(), anyString())).then(
+                (Answer<String>) inv ->
+                        mEnabledOverlays.stream().filter(
+                                mockOverlay ->
+                                        mockOverlay.mTargetPackage.equals(inv.getArgument(0))
+                                            && mockOverlay.mCategory.equals(inv.getArgument(1)))
+                                .map(overlay -> overlay.mPackageName).findFirst().orElse(null));
+
+
+        when(mockOverlayManager.disableOverlay(anyString(), anyInt())).then(
+                (Answer<Boolean>) invocation ->
+                        setEnabled(invocation.getArgument(0),
+                                false,
+                                invocation.getArgument(1)));
+
+        when(mockOverlayManager.setEnabledExclusiveInCategory(anyString(), anyInt())).then(
+                (Answer<Boolean>) invocation ->
+                        setEnabled(
+                                invocation.getArgument(0),
+                                true,
+                                invocation.getArgument(1)));
+
+        when(mockOverlayManager.getEnabledOverlaysForTargets(any())).then(
+                (Answer<Map<String, String>>) inv ->
+                        mEnabledOverlays.stream().filter(
+                                overlay ->
+                                        Arrays.asList(inv.getArguments())
+                                                .contains(overlay.mTargetPackage))
+                                .collect(Collectors.toMap(
+                                        overlay ->
+                                                overlay.mCategory,
+                                        (Function<MockOverlay, String>) overlay ->
+                                                overlay.mPackageName))
+        );
+    }
+}
diff --git a/tests/robotests/src/com/android/customization/testutils/Wait.java b/tests/robotests/src/com/android/customization/testutils/Wait.java
new file mode 100644
index 0000000..54650ba
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/testutils/Wait.java
@@ -0,0 +1,58 @@
+/*
+ * 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.customization.testutils;
+
+
+import android.os.SystemClock;
+
+import org.junit.Assert;
+
+/**
+ * A utility class for waiting for a condition to be true.
+ */
+public class Wait {
+
+    private static final long DEFAULT_SLEEP_MS = 200;
+
+    public static void atMost(String message, Condition condition, long timeout) {
+        atMost(message, condition, timeout, DEFAULT_SLEEP_MS);
+    }
+
+    public static void atMost(String message, Condition condition, long timeout, long sleepMillis) {
+        long endTime = SystemClock.uptimeMillis() + timeout;
+        while (SystemClock.uptimeMillis() < endTime) {
+            try {
+                if (condition.isTrue()) {
+                    return;
+                }
+            } catch (Throwable t) {
+                throw new RuntimeException(t);
+            }
+            SystemClock.sleep(sleepMillis);
+        }
+
+        // Check once more before returning false.
+        try {
+            if (condition.isTrue()) {
+                return;
+            }
+        } catch (Throwable t) {
+            throw new RuntimeException(t);
+        }
+        Assert.fail(message);
+    }
+}
+