Add initial Robolectric tests

Set up the infrastructure for tests and add a few initial Robolectric
tests for ThemePicker.

Bug: 118758604

Change-Id: Ie6f34c840d31d24349fd12355f93369f51168802
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
new file mode 100644
index 0000000..3037a47
--- /dev/null
+++ b/robolectric_tests/Android.mk
@@ -0,0 +1,55 @@
+# 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.
+
+#############################################
+# ThenePicker Robolectric test target.      #
+#############################################
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := ThemePickerRoboTests
+LOCAL_SDK_VERSION := system_current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    androidx.test.runner \
+    androidx.test.rules \
+    mockito-robolectric-prebuilt \
+    truth-prebuilt
+LOCAL_JAVA_LIBRARIES := \
+    platform-robolectric-3.6.2-prebuilt
+
+LOCAL_JAVA_RESOURCE_DIRS := config
+
+LOCAL_INSTRUMENTATION_FOR := ThemePicker
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+############################################
+# Target to run the previous target.       #
+############################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := RunThemePickerRoboTests
+LOCAL_SDK_VERSION := system_current
+LOCAL_JAVA_LIBRARIES := \
+    ThemePickerRoboTests
+
+LOCAL_TEST_PACKAGE := ThemePicker
+
+LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src \
+
+LOCAL_ROBOTEST_TIMEOUT := 36000
+
+include prebuilts/misc/common/robolectric/3.6.2/run_robotests.mk
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
new file mode 100644
index 0000000..197c393
--- /dev/null
+++ b/robolectric_tests/config/robolectric.properties
@@ -0,0 +1,2 @@
+manifest=vendor/unbundled_google/packages/WallpaperPickerGoogle/AndroidManifest.xml
+sdk=27
diff --git a/robolectric_tests/robolectric_gradle_config/robolectric.properties b/robolectric_tests/robolectric_gradle_config/robolectric.properties
new file mode 100644
index 0000000..926e354
--- /dev/null
+++ b/robolectric_tests/robolectric_gradle_config/robolectric.properties
@@ -0,0 +1,3 @@
+# Do not include the manifest definition in this version
+# as it is specified directly in the gradle file
+sdk=27
\ No newline at end of file
diff --git a/robolectric_tests/src/com/android/customization/model/grid/GridOptionsManagerTest.java b/robolectric_tests/src/com/android/customization/model/grid/GridOptionsManagerTest.java
new file mode 100644
index 0000000..abbbaa0
--- /dev/null
+++ b/robolectric_tests/src/com/android/customization/model/grid/GridOptionsManagerTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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 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 GridOptionsManagerTest {
+
+    @Mock LauncherGridOptionsProvider mProvider;
+    private GridOptionsManager mManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mManager = new GridOptionsManager(mProvider);
+    }
+
+    @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
+    public void testFetch_backgroundThread() {
+        mManager.fetchOptions(null);
+        Robolectric.flushBackgroundThreadScheduler();
+        verify(mProvider).fetch(anyBoolean());
+    }
+}
diff --git a/robolectric_tests/src/com/android/customization/model/theme/ThemeManagerTest.java b/robolectric_tests/src/com/android/customization/model/theme/ThemeManagerTest.java
new file mode 100644
index 0000000..bb5df66
--- /dev/null
+++ b/robolectric_tests/src/com/android/customization/model/theme/ThemeManagerTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.TestCase.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.testutils.Condition;
+import com.android.customization.testutils.OverlayManagerMocks;
+import com.android.customization.testutils.Wait;
+import com.android.wallpaper.module.WallpaperSetter;
+
+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;
+
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+public class ThemeManagerTest {
+
+    @Mock OverlayManagerCompat mMockOm;
+    @Mock WallpaperSetter mMockWallpaperSetter;
+    private OverlayManagerMocks mMockOmHelper;
+    private ThemeManager mThemeManager;
+    private Activity mActivity;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        Activity activity = Robolectric.buildActivity(FragmentActivity.class).get();
+        mActivity = spy(activity);
+        mMockOmHelper = new OverlayManagerMocks();
+        mMockOmHelper.setUpMock(mMockOm);
+        ThemeBundleProvider provider = mock(ThemeBundleProvider.class);
+        mThemeManager = new ThemeManager(provider, activity, mMockWallpaperSetter, mMockOm);
+    }
+
+    @After
+    public void cleanUp() {
+        mMockOmHelper.clearOverlays();
+    }
+
+    @Test
+    public void testApply_DefaultTheme() {
+        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);
+
+        ThemeBundle defaultTheme = new ThemeBundle.Builder().asDefault().build();
+
+        applyThemeAndWaitForCondition(defaultTheme, "Overlays didn't get disabled", () -> {
+            verify(mMockOm, times(6)).disableOverlay(anyString(), anyInt());
+            return true;
+        });
+
+        assertEquals("Secure Setting should be emtpy after applying default theme",
+                "",
+                Settings.Secure.getString(mActivity.getContentResolver(), THEME_SETTING));
+    }
+
+    @Test
+    public void testApply_NonDefault() {
+        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);
+
+        ThemeBundle theme = new ThemeBundle.Builder()
+                .addOverlayPackage(OVERLAY_CATEGORY_COLOR, bundleColorPackage)
+                .addOverlayPackage(OVERLAY_CATEGORY_FONT, bundleFontPackage)
+                .build();
+
+        applyThemeAndWaitForCondition(theme, "Overlays didn't get enabled", () -> {
+            verify(mMockOm, times(2)).setEnabledExclusiveInCategory(anyString(), anyInt());
+            Map<String, String> overlays = mMockOm.getEnabledOverlaysForTargets(ANDROID_PACKAGE);
+            assertEquals(2, overlays.size());
+            assertTrue(bundleColorPackage  + " should be enabled",
+                    overlays.values().contains(bundleColorPackage));
+            assertTrue(bundleFontPackage  + " should be enabled",
+                    overlays.values().contains(bundleFontPackage));
+            assertFalse(otherPackage  + " should not be enabled",
+                    overlays.values().contains(otherPackage));
+            return true;
+        });
+
+        assertEquals("Secure Setting was not properly set after applying theme",
+                theme.getSerializedPackages(),
+                Settings.Secure.getString(mActivity.getContentResolver(), THEME_SETTING));
+    }
+
+    private void applyThemeAndWaitForCondition(ThemeBundle theme, String message,
+            Condition condition) {
+        boolean[] done = {false, false};
+        mThemeManager.apply(theme, new Callback() {
+            @Override
+            public void onSuccess() {
+                done[0] = true;
+            }
+
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                done[0] = true;
+                done[1] = true;
+            }
+        });
+        // TODO: refactor these tests so we can get rid of the long wait.
+        Wait.atMost(message, () -> done[0] && condition.isTrue(), 1000);
+        // done[1] is only set to true in the onError callback.
+        assertFalse(done[1]);
+    }
+}
diff --git a/robolectric_tests/src/com/android/customization/testutils/Condition.java b/robolectric_tests/src/com/android/customization/testutils/Condition.java
new file mode 100644
index 0000000..f013555
--- /dev/null
+++ b/robolectric_tests/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/robolectric_tests/src/com/android/customization/testutils/OverlayManagerMocks.java b/robolectric_tests/src/com/android/customization/testutils/OverlayManagerMocks.java
new file mode 100644
index 0000000..ed77224
--- /dev/null
+++ b/robolectric_tests/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 packageName;
+        final String targetPackage;
+        final String category;
+
+        public MockOverlay(String packageName, String targetPackage, String category) {
+            this.packageName = packageName;
+            this.targetPackage = targetPackage;
+            this.category = category;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            return obj instanceof MockOverlay
+                    && TextUtils.equals(((MockOverlay) obj).packageName, packageName)
+                    && TextUtils.equals(((MockOverlay) obj).targetPackage, targetPackage)
+                    && TextUtils.equals(((MockOverlay) obj).category, category);
+        }
+    }
+
+    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.packageName.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.targetPackage.equals(inv.getArgument(0))
+                                            && mockOverlay.category.equals(inv.getArgument(1)))
+                                .map(overlay -> overlay.packageName).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.targetPackage))
+                                .collect(Collectors.toMap(
+                                        overlay ->
+                                                overlay.category,
+                                        (Function<MockOverlay, String>) overlay ->
+                                                overlay.packageName))
+        );
+    }
+}
\ No newline at end of file
diff --git a/robolectric_tests/src/com/android/customization/testutils/Wait.java b/robolectric_tests/src/com/android/customization/testutils/Wait.java
new file mode 100644
index 0000000..54650ba
--- /dev/null
+++ b/robolectric_tests/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);
+    }
+}
+