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
This commit is contained in:
parent
e09e1f2253
commit
c0f03d9665
|
@ -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<XmlPullParser> 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<XmlPullParser> 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;
|
||||
}
|
||||
|
|
|
@ -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,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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue