Moving some tests to robolectric

> Adding support for simulating model load

Bug: 130562632
Change-Id: I1de8c0abe2e74d4e7e47e18914316c339920609a
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
index 62915f2..310d43c 100644
--- a/robolectric_tests/Android.mk
+++ b/robolectric_tests/Android.mk
@@ -27,7 +27,7 @@
     mockito-robolectric-prebuilt \
     truth-prebuilt
 LOCAL_JAVA_LIBRARIES := \
-    platform-robolectric-3.6.1-prebuilt
+    platform-robolectric-4.3-prebuilt
 
 LOCAL_JAVA_RESOURCE_DIRS := resources config
 
@@ -54,4 +54,4 @@
 
 LOCAL_ROBOTEST_TIMEOUT := 36000
 
-include prebuilts/misc/common/robolectric/3.6.1/run_robotests.mk
+include prebuilts/misc/common/robolectric/4.3/run_robotests.mk
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
index 5b6d94d..f7e05a4 100644
--- a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
+++ b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
@@ -1,10 +1,13 @@
 package com.android.launcher3.model;
 
+import static com.android.launcher3.shadows.ShadowLooperExecutor.reinitializeStaticExecutors;
+
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.robolectric.util.ReflectionHelpers.setField;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -29,6 +32,7 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.cache.CachingLogic;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.pm.PackageInstallerCompat;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.TestLauncherProvider;
 
@@ -53,7 +57,7 @@
 public class BaseModelUpdateTaskTestCase {
 
     public final HashMap<Class, HashMap<String, Field>> fieldCache = new HashMap<>();
-    private TestLauncherProvider mProvider;
+    public TestLauncherProvider provider;
 
     public Context targetContext;
     public UserHandle myUser;
@@ -71,9 +75,11 @@
     @Before
     public void setUp() throws Exception {
         ShadowLog.stream = System.out;
+        reinitializeStaticExecutors();
+        setField(PackageInstallerCompat.class, null, "sInstance", null);
 
-        mProvider = Robolectric.setupContentProvider(TestLauncherProvider.class);
-        ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, mProvider);
+        provider = Robolectric.setupContentProvider(TestLauncherProvider.class);
+        ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, provider);
 
         callbacks = mock(Callbacks.class);
         appState = mock(LauncherAppState.class);
diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
new file mode 100644
index 0000000..9e4a43c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.launcher3.model;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.robolectric.Shadows.shadowOf;
+import static org.robolectric.util.ReflectionHelpers.setField;
+
+import android.content.ComponentName;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.pm.PackageInstaller.SessionParams;
+import android.net.Uri;
+import android.provider.Settings;
+
+import com.android.launcher3.FolderInfo;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.shadows.LShadowLauncherApps;
+import com.android.launcher3.shadows.LShadowUserManager;
+import com.android.launcher3.shadows.ShadowLooperExecutor;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Tests for layout parser for remote layout
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {LShadowUserManager.class, LShadowLauncherApps.class, ShadowLooperExecutor.class})
+@LooperMode(Mode.PAUSED)
+public class DefaultLayoutProviderTest extends BaseModelUpdateTaskTestCase {
+
+    private static final String SETTINGS_APP = "com.android.settings";
+    private static final String TEST_PROVIDER_AUTHORITY =
+            DefaultLayoutProviderTest.class.getName().toLowerCase();
+
+    private static final int BITMAP_SIZE = 10;
+    private static final int GRID_SIZE = 4;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        InvariantDeviceProfile.INSTANCE.initializeForTesting(idp);
+        CustomWidgetManager.INSTANCE.initializeForTesting(mock(CustomWidgetManager.class));
+
+        idp.numRows = idp.numColumns = idp.numHotseatIcons = GRID_SIZE;
+        idp.iconBitmapSize = BITMAP_SIZE;
+
+        provider.setAllowLoadDefaultFavorites(true);
+        Settings.Secure.putString(targetContext.getContentResolver(),
+                "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
+
+        ShadowPackageManager spm = shadowOf(targetContext.getPackageManager());
+        spm.addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
+                TEST_PROVIDER_AUTHORITY;
+        spm.addActivityIfNotPresent(new ComponentName(SETTINGS_APP, SETTINGS_APP));
+    }
+
+    @After
+    public void cleanup() {
+        InvariantDeviceProfile.INSTANCE.initializeForTesting(null);
+        CustomWidgetManager.INSTANCE.initializeForTesting(null);
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
+        writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0)
+                .putApp(SETTINGS_APP, SETTINGS_APP));
+
+        // Verify one item in hotseat
+        assertEquals(1, bgDataModel.workspaceItems.size());
+        ItemInfo info = bgDataModel.workspaceItems.get(0);
+        assertEquals(LauncherSettings.Favorites.CONTAINER_HOTSEAT, info.container);
+        assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPLICATION, info.itemType);
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_folder() throws Exception {
+        writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
+                .addApp(SETTINGS_APP, SETTINGS_APP)
+                .addApp(SETTINGS_APP, SETTINGS_APP)
+                .addApp(SETTINGS_APP, SETTINGS_APP)
+                .build());
+
+        // Verify folder
+        assertEquals(1, bgDataModel.workspaceItems.size());
+        ItemInfo info = bgDataModel.workspaceItems.get(0);
+        assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
+        assertEquals(3, ((FolderInfo) info).contents.size());
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_widget() throws Exception {
+        String pendingAppPkg = "com.test.pending";
+
+        // Add a dummy session info so that the widget exists
+        SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+        params.setAppPackageName(pendingAppPkg);
+
+        PackageInstaller installer = targetContext.getPackageManager().getPackageInstaller();
+        int sessionId = installer.createSession(params);
+        SessionInfo sessionInfo = installer.getSessionInfo(sessionId);
+        setField(sessionInfo, "installerPackageName", "com.test");
+        setField(sessionInfo, "appIcon", BitmapInfo.LOW_RES_ICON);
+
+        writeLayoutAndLoad(new LauncherLayoutBuilder().atWorkspace(0, 1, 0)
+                .putWidget(pendingAppPkg, "DummyWidget", 2, 2));
+
+        // Verify widget
+        assertEquals(1, bgDataModel.appWidgets.size());
+        ItemInfo info = bgDataModel.appWidgets.get(0);
+        assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET, info.itemType);
+        assertEquals(2, info.spanX);
+        assertEquals(2, info.spanY);
+    }
+
+    private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        builder.build(new OutputStreamWriter(bos));
+
+        Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, targetContext);
+        shadowOf(targetContext.getContentResolver()).registerInputStream(layoutUri,
+                new ByteArrayInputStream(bos.toByteArray()));
+
+        LoaderResults results = new LoaderResults(appState, bgDataModel, allAppsList, 0,
+                new WeakReference<>(callbacks));
+        LoaderTask task = new LoaderTask(appState, allAppsList, bgDataModel, results);
+        Executors.MODEL_EXECUTOR.submit(() -> task.loadWorkspace(new ArrayList<>())).get();
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
new file mode 100644
index 0000000..204ec9b
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
@@ -0,0 +1,96 @@
+/*
+ * 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.launcher3.shadows;
+
+import static org.robolectric.util.ReflectionHelpers.ClassParameter;
+import static org.robolectric.util.ReflectionHelpers.callConstructor;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.ArraySet;
+
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageUserKey;
+
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowLauncherApps;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Extension of {@link ShadowLauncherApps} with missing shadow methods
+ */
+@Implements(value = LauncherApps.class)
+public class LShadowLauncherApps extends ShadowLauncherApps {
+
+    public final ArraySet<PackageUserKey> disabledApps = new ArraySet<>();
+    public final ArraySet<ComponentKey> disabledActivities = new ArraySet<>();
+
+    @Implementation
+    @Override
+    protected List<ShortcutInfo> getShortcuts(LauncherApps.ShortcutQuery query, UserHandle user) {
+        try {
+            return super.getShortcuts(query, user);
+        } catch (UnsupportedOperationException e) {
+            return Collections.emptyList();
+        }
+    }
+
+    @Implementation
+    protected boolean isPackageEnabled(String packageName, UserHandle user) {
+        return !disabledApps.contains(new PackageUserKey(packageName, user));
+    }
+
+    @Implementation
+    protected boolean isActivityEnabled(ComponentName component, UserHandle user) {
+        return !disabledActivities.contains(new ComponentKey(component, user));
+    }
+
+    @Implementation
+    protected LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) {
+        ResolveInfo ri = RuntimeEnvironment.application.getPackageManager()
+                .resolveActivity(intent, 0);
+        return getLauncherActivityInfo(ri.activityInfo);
+    }
+
+    public LauncherActivityInfo getLauncherActivityInfo(ActivityInfo activityInfo) {
+        return callConstructor(LauncherActivityInfo.class,
+                ClassParameter.from(Context.class, RuntimeEnvironment.application),
+                ClassParameter.from(ActivityInfo.class, activityInfo),
+                ClassParameter.from(UserHandle.class, Process.myUserHandle()));
+    }
+
+    @Implementation
+    public ApplicationInfo getApplicationInfo(String packageName, int flags, UserHandle user)
+            throws PackageManager.NameNotFoundException {
+        return RuntimeEnvironment.application.getPackageManager()
+                .getApplicationInfo(packageName, flags);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
new file mode 100644
index 0000000..edf8edb
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
@@ -0,0 +1,53 @@
+/*
+ * 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.launcher3.shadows;
+
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.SparseBooleanArray;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowUserManager;
+
+/**
+ * Extension of {@link ShadowUserManager} with missing shadow methods
+ */
+@Implements(value = UserManager.class)
+public class LShadowUserManager extends ShadowUserManager {
+
+    private final SparseBooleanArray mQuietUsers = new SparseBooleanArray();
+    private final SparseBooleanArray mLockedUsers = new SparseBooleanArray();
+
+    @Implementation
+    protected boolean isQuietModeEnabled(UserHandle userHandle) {
+        return mQuietUsers.get(userHandle.hashCode());
+    }
+
+    public void setQuietModeEnabled(UserHandle userHandle, boolean enabled) {
+        mQuietUsers.put(userHandle.hashCode(), enabled);
+    }
+
+    @Implementation
+    protected boolean isUserUnlocked(UserHandle userHandle) {
+        return !mLockedUsers.get(userHandle.hashCode());
+    }
+
+    public void setUserLocked(UserHandle userHandle, boolean enabled) {
+        mLockedUsers.put(userHandle.hashCode(), enabled);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
new file mode 100644
index 0000000..d56de3c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
@@ -0,0 +1,68 @@
+/*
+ * 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.launcher3.shadows;
+
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
+
+import static org.robolectric.shadow.api.Shadow.invokeConstructor;
+import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
+import static org.robolectric.util.ReflectionHelpers.setField;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LooperExecutor;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Shadow for {@link LooperExecutor} to provide reset functionality for static executors.
+ */
+@Implements(value = LooperExecutor.class, isInAndroidSdk = false)
+public class ShadowLooperExecutor {
+
+    // Keep reference to all created Loopers so they can be torn down after test
+    private static Set<LooperExecutor> executors =
+            Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
+
+    @RealObject private LooperExecutor realExecutor;
+
+    @Implementation
+    protected void __constructor__(Looper looper) {
+        invokeConstructor(LooperExecutor.class, realExecutor, from(Looper.class, looper));
+        executors.add(realExecutor);
+    }
+
+    /**
+     * Re-initializes any executor which may have been reset when a test finished
+     */
+    public static void reinitializeStaticExecutors() {
+        for (LooperExecutor executor : new ArrayList<>(executors)) {
+            setField(executor, "mHandler",
+                    new Handler(createAndStartNewLooper(executor.getThread().getName())));
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java b/robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
new file mode 100644
index 0000000..d3659eb
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
@@ -0,0 +1,172 @@
+/**
+ * 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.launcher3.util;
+
+
+import android.text.TextUtils;
+import android.util.Pair;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Helper class to build xml for Launcher Layout
+ */
+public class LauncherLayoutBuilder {
+
+    // Object Tags
+    private static final String TAG_WORKSPACE = "workspace";
+    private static final String TAG_AUTO_INSTALL = "autoinstall";
+    private static final String TAG_FOLDER = "folder";
+    private static final String TAG_APPWIDGET = "appwidget";
+    private static final String TAG_EXTRA = "extra";
+
+    private static final String ATTR_CONTAINER = "container";
+    private static final String ATTR_RANK = "rank";
+
+    private static final String ATTR_PACKAGE_NAME = "packageName";
+    private static final String ATTR_CLASS_NAME = "className";
+    private static final String ATTR_TITLE = "title";
+    private static final String ATTR_SCREEN = "screen";
+
+    // x and y can be specified as negative integers, in which case -1 represents the
+    // last row / column, -2 represents the second last, and so on.
+    private static final String ATTR_X = "x";
+    private static final String ATTR_Y = "y";
+    private static final String ATTR_SPAN_X = "spanX";
+    private static final String ATTR_SPAN_Y = "spanY";
+
+    private static final String ATTR_CHILDREN = "children";
+
+
+    // Style attrs -- "Extra"
+    private static final String ATTR_KEY = "key";
+    private static final String ATTR_VALUE = "value";
+
+    private static final String CONTAINER_DESKTOP = "desktop";
+    private static final String CONTAINER_HOTSEAT = "hotseat";
+
+    private final ArrayList<Pair<String, HashMap<String, Object>>> mNodes = new ArrayList<>();
+
+    public Location atHotseat(int rank) {
+        Location l = new Location();
+        l.items.put(ATTR_CONTAINER, CONTAINER_HOTSEAT);
+        l.items.put(ATTR_RANK, Integer.toString(rank));
+        return l;
+    }
+
+    public Location atWorkspace(int x, int y, int screen) {
+        Location l = new Location();
+        l.items.put(ATTR_CONTAINER, CONTAINER_DESKTOP);
+        l.items.put(ATTR_X, Integer.toString(x));
+        l.items.put(ATTR_Y, Integer.toString(y));
+        l.items.put(ATTR_SCREEN, Integer.toString(screen));
+        return l;
+    }
+
+    public String build() throws IOException {
+        StringWriter writer = new StringWriter();
+        build(writer);
+        return writer.toString();
+    }
+
+    public void build(Writer writer) throws IOException {
+        XmlSerializer serializer = Xml.newSerializer();
+        serializer.setOutput(writer);
+
+        serializer.startDocument("UTF-8", true);
+        serializer.startTag(null, TAG_WORKSPACE);
+        writeNodes(serializer, mNodes);
+        serializer.endTag(null, TAG_WORKSPACE);
+        serializer.endDocument();
+        serializer.flush();
+    }
+
+    private static void writeNodes(XmlSerializer serializer,
+            ArrayList<Pair<String, HashMap<String, Object>>> nodes) throws IOException {
+        for (Pair<String, HashMap<String, Object>> node : nodes) {
+            ArrayList<Pair<String, HashMap<String, Object>>> children = null;
+
+            serializer.startTag(null, node.first);
+            for (Map.Entry<String, Object> attr : node.second.entrySet()) {
+                if (ATTR_CHILDREN.equals(attr.getKey())) {
+                    children = (ArrayList<Pair<String, HashMap<String, Object>>>) attr.getValue();
+                } else {
+                    serializer.attribute(null, attr.getKey(), (String) attr.getValue());
+                }
+            }
+
+            if (children != null) {
+                writeNodes(serializer, children);
+            }
+            serializer.endTag(null, node.first);
+        }
+    }
+
+    public class Location {
+
+        final HashMap<String, Object> items = new HashMap<>();
+
+        public LauncherLayoutBuilder putApp(String packageName, String className) {
+            items.put(ATTR_PACKAGE_NAME, packageName);
+            items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
+            mNodes.add(Pair.create(TAG_AUTO_INSTALL, items));
+            return LauncherLayoutBuilder.this;
+        }
+
+        public LauncherLayoutBuilder putWidget(String packageName, String className,
+                int spanX, int spanY) {
+            items.put(ATTR_PACKAGE_NAME, packageName);
+            items.put(ATTR_CLASS_NAME, className);
+            items.put(ATTR_SPAN_X, Integer.toString(spanX));
+            items.put(ATTR_SPAN_Y, Integer.toString(spanY));
+            mNodes.add(Pair.create(TAG_APPWIDGET, items));
+            return LauncherLayoutBuilder.this;
+        }
+
+        public FolderBuilder putFolder(int titleResId) {
+            FolderBuilder folderBuilder = new FolderBuilder();
+            items.put(ATTR_TITLE, Integer.toString(titleResId));
+            items.put(ATTR_CHILDREN, folderBuilder.mChildren);
+            mNodes.add(Pair.create(TAG_FOLDER, items));
+            return folderBuilder;
+        }
+    }
+
+    public class FolderBuilder {
+
+        final ArrayList<Pair<String, HashMap<String, Object>>> mChildren = new ArrayList<>();
+
+        public FolderBuilder addApp(String packageName, String className) {
+            HashMap<String, Object> items = new HashMap<>();
+            items.put(ATTR_PACKAGE_NAME, packageName);
+            items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
+            mChildren.add(Pair.create(TAG_AUTO_INSTALL, items));
+            return this;
+        }
+
+        public LauncherLayoutBuilder build() {
+            return LauncherLayoutBuilder.this;
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java b/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java
index a9c1a7c..7e873e8 100644
--- a/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java
+++ b/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java
@@ -10,6 +10,8 @@
  */
 public class TestLauncherProvider extends LauncherProvider {
 
+    private boolean mAllowLoadDefaultFavorites;
+
     @Override
     public boolean onCreate() {
         return true;
@@ -18,18 +20,26 @@
     @Override
     protected synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = new MyDatabaseHelper(getContext());
+            mOpenHelper = new MyDatabaseHelper(getContext(), mAllowLoadDefaultFavorites);
         }
     }
 
+    public void setAllowLoadDefaultFavorites(boolean allowLoadDefaultFavorites) {
+        mAllowLoadDefaultFavorites = allowLoadDefaultFavorites;
+    }
+
     public SQLiteDatabase getDb() {
         createDbIfNotExists();
         return mOpenHelper.getWritableDatabase();
     }
 
     private static class MyDatabaseHelper extends DatabaseHelper {
-        public MyDatabaseHelper(Context context) {
+
+        private final boolean mAllowLoadDefaultFavorites;
+
+        MyDatabaseHelper(Context context, boolean allowLoadDefaultFavorites) {
             super(context, null);
+            mAllowLoadDefaultFavorites = allowLoadDefaultFavorites;
             initIds();
         }
 
@@ -39,7 +49,11 @@
         }
 
         @Override
-        protected void onEmptyDbCreated() { }
+        protected void onEmptyDbCreated() {
+            if (mAllowLoadDefaultFavorites) {
+                super.onEmptyDbCreated();
+            }
+        }
 
         @Override
         protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { }