From c0f03d96656a3c70a26d3ec97e9e782fbdbe64b9 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Fri, 22 Mar 2019 14:13:36 -0700 Subject: [PATCH] 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 --- .../android/launcher3/AutoInstallsLayout.java | 30 +-- .../android/launcher3/LauncherProvider.java | 56 ++++-- .../testcomponent/TestCommandReceiver.java | 23 +++ .../ui/DefaultLayoutProviderTest.java | 138 ++++++++++++++ .../launcher3/util/LauncherLayoutBuilder.java | 172 ++++++++++++++++++ 5 files changed, 388 insertions(+), 31 deletions(-) create mode 100644 tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java create mode 100644 tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java index 62bc53a817..0d9bd31956 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -50,6 +50,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.Locale; +import java.util.function.Supplier; /** * Layout parsing code for auto installs layout @@ -76,12 +77,8 @@ public class AutoInstallsLayout { 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 @@ public class AutoInstallsLayout { // 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 @@ public class AutoInstallsLayout { protected final PackageManager mPackageManager; protected final Resources mSourceRes; - protected final int mLayoutId; + protected final Supplier mInitialLayoutSupplier; private final InvariantDeviceProfile mIdp; private final int mRowCount; @@ -171,6 +168,12 @@ public class AutoInstallsLayout { 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 initialLayoutSupplier, String rootTag) { mContext = context; mAppWidgetHost = appWidgetHost; mCallback = callback; @@ -180,7 +183,7 @@ public class AutoInstallsLayout { mRootTag = rootTag; mSourceRes = res; - mLayoutId = layoutId; + mInitialLayoutSupplier = initialLayoutSupplier; mIdp = LauncherAppState.getIDP(context); mRowCount = mIdp.numRows; @@ -193,9 +196,9 @@ public class AutoInstallsLayout { 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 @@ public class AutoInstallsLayout { /** * 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 @@ public class AutoInstallsLayout { 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 39d93c8a2d..f830301a3d 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -34,6 +34,7 @@ import android.content.Intent; 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.Process; 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.model.DbDowngradeHelper; 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 @@ public class LauncherProvider extends ContentProvider { 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 @@ public class LauncherProvider extends ContentProvider { */ 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; + } + 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; } - 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 0edb3d61bc..fa23b8d5b9 100644 --- a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java +++ b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java @@ -18,6 +18,7 @@ package com.android.launcher3.testcomponent; 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.content.ContentValues; 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 @@ public class TestCommandReceiver extends ContentProvider { 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 0000000000..1efdee8ef4 --- /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 0000000000..d3659ebdcd --- /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>> 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>> nodes) throws IOException { + for (Pair> node : nodes) { + ArrayList>> children = null; + + serializer.startTag(null, node.first); + for (Map.Entry attr : node.second.entrySet()) { + if (ATTR_CHILDREN.equals(attr.getKey())) { + children = (ArrayList>>) 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 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>> mChildren = new ArrayList<>(); + + public FolderBuilder addApp(String packageName, String className) { + HashMap 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; + } + } +}