Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c0f03d96 authored by Sunny Goyal's avatar Sunny Goyal
Browse files

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
parent e09e1f22
Loading
Loading
Loading
Loading
+16 −14
Original line number Diff line number Diff line
@@ -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;
            }
+39 −17
Original line number Diff line number Diff line
@@ -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());
+23 −0
Original line number Diff line number Diff line
@@ -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);
    }
}
+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();
    }
}
+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;
        }
    }
}