Adding support for loading the default layout from a content provider

The autority of the provider should be set in secure settings:
  launcher3.layout.provider

Bug: 127987071
Change-Id: Iccf2960aa6c0a5a8ff9621b13d8963d9daecb993
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 62bc53a..0d9bd31 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -50,6 +50,7 @@
 
 import java.io.IOException;
 import java.util.Locale;
+import java.util.function.Supplier;
 
 /**
  * Layout parsing code for auto installs layout
@@ -76,12 +77,8 @@
         if (customizationApkInfo == null) {
             return null;
         }
-        return get(context, customizationApkInfo.first, customizationApkInfo.second,
-                appWidgetHost, callback);
-    }
-
-    static AutoInstallsLayout get(Context context, String pkg, Resources targetRes,
-            AppWidgetHost appWidgetHost, LayoutParserCallback callback) {
+        String pkg = customizationApkInfo.first;
+        Resources targetRes = customizationApkInfo.second;
         InvariantDeviceProfile grid = LauncherAppState.getIDP(context);
 
         // Try with grid size and hotseat count
@@ -114,7 +111,7 @@
 
     // Object Tags
     private static final String TAG_INCLUDE = "include";
-    private static final String TAG_WORKSPACE = "workspace";
+    public static final String TAG_WORKSPACE = "workspace";
     private static final String TAG_APP_ICON = "appicon";
     private static final String TAG_AUTO_INSTALL = "autoinstall";
     private static final String TAG_FOLDER = "folder";
@@ -156,7 +153,7 @@
 
     protected final PackageManager mPackageManager;
     protected final Resources mSourceRes;
-    protected final int mLayoutId;
+    protected final Supplier<XmlPullParser> mInitialLayoutSupplier;
 
     private final InvariantDeviceProfile mIdp;
     private final int mRowCount;
@@ -171,6 +168,12 @@
     public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost,
             LayoutParserCallback callback, Resources res,
             int layoutId, String rootTag) {
+        this(context, appWidgetHost, callback, res, () -> res.getXml(layoutId), rootTag);
+    }
+
+    public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost,
+            LayoutParserCallback callback, Resources res,
+            Supplier<XmlPullParser> initialLayoutSupplier, String rootTag) {
         mContext = context;
         mAppWidgetHost = appWidgetHost;
         mCallback = callback;
@@ -180,7 +183,7 @@
         mRootTag = rootTag;
 
         mSourceRes = res;
-        mLayoutId = layoutId;
+        mInitialLayoutSupplier = initialLayoutSupplier;
 
         mIdp = LauncherAppState.getIDP(context);
         mRowCount = mIdp.numRows;
@@ -193,9 +196,9 @@
     public int loadLayout(SQLiteDatabase db, IntArray screenIds) {
         mDb = db;
         try {
-            return parseLayout(mLayoutId, screenIds);
+            return parseLayout(mInitialLayoutSupplier.get(), screenIds);
         } catch (Exception e) {
-            Log.e(TAG, "Error parsing layout: " + e);
+            Log.e(TAG, "Error parsing layout: ", e);
             return -1;
         }
     }
@@ -203,9 +206,8 @@
     /**
      * Parses the layout and returns the number of elements added on the homescreen.
      */
-    protected int parseLayout(int layoutId, IntArray screenIds)
+    protected int parseLayout(XmlPullParser parser, IntArray screenIds)
             throws XmlPullParserException, IOException {
-        XmlPullParser parser = mSourceRes.getXml(layoutId);
         beginDocument(parser, mRootTag);
         final int depth = parser.getDepth();
         int type;
@@ -248,7 +250,7 @@
             final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0);
             if (resId != 0) {
                 // recursively load some more favorites, why not?
-                return parseLayout(resId, screenIds);
+                return parseLayout(mSourceRes.getXml(resId), screenIds);
             } else {
                 return 0;
             }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 39d93c8..f830301 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -34,6 +34,7 @@
 import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ProviderInfo;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
@@ -51,8 +52,10 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.BaseColumns;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Xml;
 
 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -63,15 +66,21 @@
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.util.IOUtils;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.NoLocaleSQLiteHelper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Thunk;
 
+import org.xmlpull.v1.XmlPullParser;
+
 import java.io.File;
 import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
 import java.io.PrintWriter;
+import java.io.StringReader;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -93,8 +102,6 @@
 
     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
 
-    private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name";
-
     private final ChangeListenerWrapper mListenerWrapper = new ChangeListenerWrapper();
     private Handler mListenerHandler;
 
@@ -505,25 +512,40 @@
      */
     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
         Context ctx = getContext();
-        UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
-        Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName());
-        if (bundle == null) {
+        InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
+
+        String authority = Settings.Secure.getString(ctx.getContentResolver(),
+                "launcher3.layout.provider");
+        if (TextUtils.isEmpty(authority)) {
             return null;
         }
 
-        String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME);
-        if (packageName != null) {
-            try {
-                Resources targetResources = ctx.getPackageManager()
-                        .getResourcesForApplication(packageName);
-                return AutoInstallsLayout.get(ctx, packageName, targetResources,
-                        widgetHost, mOpenHelper);
-            } catch (NameNotFoundException e) {
-                Log.e(TAG, "Target package for restricted profile not found", e);
-                return null;
-            }
+        ProviderInfo pi = ctx.getPackageManager().resolveContentProvider(authority, 0);
+        if (pi == null) {
+            Log.e(TAG, "No provider found for authority " + authority);
+            return null;
         }
-        return null;
+        Uri uri = new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
+                .appendQueryParameter("version", "1")
+                .appendQueryParameter("gridWidth", Integer.toString(grid.numColumns))
+                .appendQueryParameter("gridHeight", Integer.toString(grid.numRows))
+                .appendQueryParameter("hotseatSize", Integer.toString(grid.numHotseatIcons))
+                .build();
+
+        try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {
+            // Read the full xml so that we fail early in case of any IO error.
+            String layout = new String(IOUtils.toByteArray(in));
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(new StringReader(layout));
+
+            Log.d(TAG, "Loading layout from " + authority);
+            return new AutoInstallsLayout(ctx, widgetHost, mOpenHelper,
+                    ctx.getPackageManager().getResourcesForApplication(pi.applicationInfo),
+                    () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
+        } catch (Exception e) {
+            Log.e(TAG, "Error getting layout stream from: " + authority , e);
+            return null;
+        }
     }
 
     private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
diff --git a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java
index 0edb3d6..fa23b8d 100644
--- a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java
+++ b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java
@@ -18,6 +18,7 @@
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
 import static android.content.pm.PackageManager.DONT_KILL_APP;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
 
 import android.app.Activity;
 import android.app.ActivityManager;
@@ -28,6 +29,13 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.util.Base64;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -104,4 +112,19 @@
         Uri uri = Uri.parse("content://" + inst.getContext().getPackageName() + ".commands");
         return inst.getTargetContext().getContentResolver().call(uri, command, arg, null);
     }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        String path = Base64.encodeToString(uri.getPath().getBytes(),
+                Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP);
+        File file = new File(getContext().getCacheDir(), path);
+        if (!file.exists()) {
+            // Create an empty file so that we can pass its descriptor
+            try {
+                file.createNewFile();
+            } catch (IOException e) { }
+        }
+
+        return ParcelFileDescriptor.open(file, MODE_READ_WRITE);
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java b/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
new file mode 100644
index 0000000..1efdee8
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
@@ -0,0 +1,138 @@
+/**
+ * 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.ui;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
+
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.testcomponent.TestCommandReceiver;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.OutputStreamWriter;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.UiSelector;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class DefaultLayoutProviderTest extends AbstractLauncherUiTest {
+
+    @Rule
+    public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
+
+    private static final String SETTINGS_APP = "com.android.settings";
+
+    private Context mContext;
+    private String mAuthority;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mContext = InstrumentationRegistry.getContext();
+
+        PackageManager pm = mTargetContext.getPackageManager();
+        ProviderInfo pi = pm.getProviderInfo(new ComponentName(mContext,
+                TestCommandReceiver.class), 0);
+        mAuthority = pi.authority;
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
+        writeLayout(new LauncherLayoutBuilder().atHotseat(0).putApp(SETTINGS_APP, SETTINGS_APP));
+
+        // Launch the home activity
+        mActivityMonitor.startLauncher();
+        waitForModelLoaded();
+
+        // Verify widget present
+        UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName())
+                .description(getSettingsApp().getLabel().toString());
+        assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT));
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_widget() throws Exception {
+        // A non-restored widget with no config screen gets restored automatically.
+        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
+
+        writeLayout(new LauncherLayoutBuilder().atWorkspace(0, 1, 0)
+                .putWidget(info.getComponent().getPackageName(),
+                        info.getComponent().getClassName(), 2, 2));
+
+        // Launch the home activity
+        mActivityMonitor.startLauncher();
+        waitForModelLoaded();
+
+        // Verify widget present
+        UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName())
+                .className(LauncherAppWidgetHostView.class).description(info.label);
+        assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT));
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_folder() throws Exception {
+        writeLayout(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());
+
+        // Launch the home activity
+        mActivityMonitor.startLauncher();
+        waitForModelLoaded();
+
+        // Verify widget present
+        UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName())
+                .descriptionContains(mTargetContext.getString(android.R.string.copy));
+        assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT));
+    }
+
+    @After
+    public void cleanup() throws Exception {
+        mDevice.executeShellCommand("settings delete secure launcher3.layout.provider");
+    }
+
+    private void writeLayout(LauncherLayoutBuilder builder) throws Exception {
+        mDevice.executeShellCommand("settings put secure launcher3.layout.provider " + mAuthority);
+        ParcelFileDescriptor pfd = mTargetContext.getContentResolver().openFileDescriptor(
+                Uri.parse("content://" + mAuthority + "/launcher_layout"), "w");
+
+        try (OutputStreamWriter writer = new OutputStreamWriter(new AutoCloseOutputStream(pfd))) {
+            builder.build(writer);
+        }
+        clearLauncherData();
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java b/tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
new file mode 100644
index 0000000..d3659eb
--- /dev/null
+++ b/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;
+        }
+    }
+}