Loading src/com/android/launcher3/AutoInstallsLayout.java +16 −14 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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"; Loading Loading @@ -156,7 +153,7 @@ public class AutoInstallsLayout { 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; Loading @@ -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<XmlPullParser> initialLayoutSupplier, String rootTag) { mContext = context; mAppWidgetHost = appWidgetHost; mCallback = callback; Loading @@ -180,7 +183,7 @@ public class AutoInstallsLayout { mRootTag = rootTag; mSourceRes = res; mLayoutId = layoutId; mInitialLayoutSupplier = initialLayoutSupplier; mIdp = LauncherAppState.getIDP(context); mRowCount = mIdp.numRows; Loading @@ -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; } } Loading @@ -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; Loading Loading @@ -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; } Loading src/com/android/launcher3/LauncherProvider.java +39 −17 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -505,26 +512,41 @@ 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); 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; } } private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) { InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext()); Loading tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java +23 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); } } tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java 0 → 100644 +138 −0 Original line number Diff line number Diff line /** * 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(); } } tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java 0 → 100644 +172 −0 Original line number Diff line number Diff line /** * 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; } } } Loading
src/com/android/launcher3/AutoInstallsLayout.java +16 −14 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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"; Loading Loading @@ -156,7 +153,7 @@ public class AutoInstallsLayout { 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; Loading @@ -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<XmlPullParser> initialLayoutSupplier, String rootTag) { mContext = context; mAppWidgetHost = appWidgetHost; mCallback = callback; Loading @@ -180,7 +183,7 @@ public class AutoInstallsLayout { mRootTag = rootTag; mSourceRes = res; mLayoutId = layoutId; mInitialLayoutSupplier = initialLayoutSupplier; mIdp = LauncherAppState.getIDP(context); mRowCount = mIdp.numRows; Loading @@ -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; } } Loading @@ -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; Loading Loading @@ -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; } Loading
src/com/android/launcher3/LauncherProvider.java +39 −17 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -505,26 +512,41 @@ 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); 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; } } private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) { InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext()); Loading
tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java +23 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); } }
tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java 0 → 100644 +138 −0 Original line number Diff line number Diff line /** * 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(); } }
tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java 0 → 100644 +172 −0 Original line number Diff line number Diff line /** * 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; } } }