Launcher3-Four ways to customize Workspace

There are four ways to load layout resources in Launcher’s workspace. The loading entrance is in LauncherProvider.java. Understanding these four loading methods will be very helpful for us to develop launcher, customize workspace, and share direct resources between two applications.

Create a ContentProvider when the launcher starts, and then call the call method

packages\apps\Launcher3\src\com\android\launcher3\LauncherProvider.java
public Bundle call(String method, final String arg, final Bundle extras) {
    if (Binder.getCallingUid() != Process.myUid()) {
        return null;
    }
    createDbIfNotExists();//Create DB
    switch (method) {
        ............
        case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
            loadDefaultFavoritesIfNecessary();
            return null;
        }
       .....................
    }
    return null;
}
packages\apps\Launcher3\src\com\android\launcher3\LauncherProvider.java
synchronized private void loadDefaultFavoritesIfNecessary() {
    if (getFlagEmptyDbCreated(getContext(), mOpenHelper.getDatabaseName())) {//Read whether the database is empty from sharepreferences
        Log.d(TAG, "loading default workspace");

        AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
        AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);//The first way to customize workspace
        if (loader == null) {
            loader = AutoInstallsLayout.get(getContext(), widgetHost, mOpenHelper);//The second method of customizing workspace
        }
        if (loader == null) {//The third method of customizing workspace
            final Partner partner = Partner.get(getContext().getPackageManager());
            if (partner != null & amp; & amp; partner.hasDefaultLayout()) {
                final Resources partnerRes = partner.getResources();
                int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
                        "xml", partner.getPackageName());
                if (workspaceResId != 0) {
                    loader = new DefaultLayoutParser(getContext(), widgetHost,
                            mOpenHelper, partnerRes, workspaceResId);
                }
            }
        }

        final boolean usingExternallyProvidedLayout = loader != null;
        if (loader == null) {//The fourth method of customizing workspace
            loader = getDefaultLayoutParser(widgetHost);
        }
        .............
    }
}

There are four ways for workspace to load layout resources. Starting from the first method, the resources are obtained in sequence. If the resources cannot be obtained, the next one is obtained.

The first way to customize workspace

private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
    Context ctx = getContext();
    InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
    String authority = Settings.Secure.getString(ctx.getContentResolver(),
            "launcher3.layout.provider");//Get the authority part of contentprovider by reading settings
    if (TextUtils.isEmpty(authority)) {
        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();//Create URI based on the combination of authority, number of rows and columns, etc.

    try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {//Read contentprovider information through the stream
        // 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));//Create a string stream and parse it with pull

        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;
    }
}

The second method of customizing workspace

static final String ACTION_LAUNCHER_CUSTOMIZATION =
        "android.autoinstalls.config.action.PLAY_AUTO_INSTALL";
private static final String FORMATTED_LAYOUT_RES_WITH_HOSTEAT = "default_layout_%dx%d_h%s";
private static final String FORMATTED_LAYOUT_RES = "default_layout_%dx%d";
private static final String LAYOUT_RES = "default_layout";
packages\apps\Launcher3\src\com\android\launcher3\AutoInstallsLayout.java
static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost,
        LayoutParserCallback callback) {
    Pair<String, Resources> customizationApkInfo = Utilities.findSystemApk(
            ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager());
    if (customizationApkInfo == null) {
        return null;
    }
    String pkg = customizationApkInfo.first;
    Resources targetRes = customizationApkInfo.second;
    InvariantDeviceProfile grid = LauncherAppState.getIDP(context);

    String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
        grid.numColumns, grid.numRows, grid.numHotseatIcons);//The file name is combined by the rows and hotseats of the device, that is, default_layout_%dx%d_h%s
    int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);//Get the id of default_layout_%dx%d_h%s.xml from the pkg package

    // Try with only grid size
    if (layoutId == 0) {//Failed to obtain the id of default_layout_%dx%d_h%s.xml
        Log.d(TAG, "Formatted layout: " + layoutName
                 + " not found. Trying layout without hosteat");
        layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
            grid.numColumns, grid.numRows);//The file name is formed by combining the rows and columns of the device, that is, default_layout_%dx%d
        layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
    }

    // Try the default layout
    if (layoutId == 0) {//Failed to obtain the id of default_layout_%dx%d.xml
        Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying the default layout");
        layoutId = targetRes.getIdentifier(LAYOUT_RES, "xml", pkg);//Get the id of default_layout.xml from the pkg package
    }

    if (layoutId == 0) {
        Log.e(TAG, "Layout definition not found in package: " + pkg);
        return null;
    }
    return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId,
            TAG_WORKSPACE);
}
static Pair<String, Resources> findSystemApk(String action, PackageManager pm) {
    final Intent intent = new Intent(action);//action is android.autoinstalls.config.action.PLAY_AUTO_INSTALL
    for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {//Find the broadcast containing action in the system
        if (info.activityInfo != null & amp; & amp;
                (info.activityInfo.applicationInfo.flags & amp; ApplicationInfo.FLAG_SYSTEM) != 0) {//The action package must be a system application
            final String packageName = info.activityInfo.packageName;
            try {
                final Resources res = pm.getResourcesForApplication(packageName);//Get the resource object through the package name
                return Pair.create(packageName, res);//Return package name and resource object
            } catch (NameNotFoundException e) {
                Log.w(TAG, "Failed to find resources for " + packageName);
            }
        }
    }
    return null;
}

In the second method of customizing the workspace, first search for the broadcast of the system application containing the action android.autoinstalls.config.action.PLAY_AUTO_INSTALL, get the package name and resource object of this broadcast, and then use the package name and resource object to obtain them respectively. Specify the number of rows and columns, hotseat, number of rows and columns, and the xml file of default_layout that is not specified. If obtained, use pull to parse it.

The third way to customize workspace

public static final String RES_DEFAULT_LAYOUT = "partner_default_layout";
synchronized private void loadDefaultFavoritesIfNecessary() {
.....................
        if (loader == null) {
            final Partner partner = Partner.get(getContext().getPackageManager());
            if (partner != null & amp; & amp; partner.hasDefaultLayout()) {
                final Resources partnerRes = partner.getResources();
                int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
                        "xml", partner.getPackageName());//Get the resource id of partner_default_layout.xml
                if (workspaceResId != 0) {
                    loader = new DefaultLayoutParser(getContext(), widgetHost,
                            mOpenHelper, partnerRes, workspaceResId);
                }
            }
        }
.............................
}
private static final String
        ACTION_PARTNER_CUSTOMIZATION = "com.android.launcher3.action.PARTNER_CUSTOMIZATION";
public static synchronized Partner get(PackageManager pm) {
    if (!sSearched) {
        Pair<String, Resources> apkInfo = Utilities.findSystemApk(ACTION_PARTNER_CUSTOMIZATION, pm);//Get the application whose system application broadcast action is com.android.launcher3.action.PARTNER_CUSTOMIZATION
        if (apkInfo != null) {
            sPartner = new Partner(apkInfo.first, apkInfo.second);
        }
        sSearched = true;
    }
    return sPartner;
}

The third method is similar to the second method. After obtaining the application package name and resources containing the specified broadcast, directly obtain the id of partner_default_layout.xml, and then parse it through pull.

The fourth way to customize workspace

private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
    InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
    int defaultLayout = idp.defaultLayoutId;

    UserManagerCompat um = UserManagerCompat.getInstance(getContext());
    if (um.isDemoUser() & amp; & idp.demoModeLayoutId != 0) {
        defaultLayout = idp.demoModeLayoutId;
    }

    return new DefaultLayoutParser(getContext(), widgetHost,
            mOpenHelper, getContext().getResources(), defaultLayout);
}

The fourth way is to directly obtain the xml file under Launcher

Summary: The first way for Launcher’s workspace to load layout resources is to read the file stream through ContetProver; the second and third ways are to obtain the package name by querying the system application’s specified broadcast action, and then obtain the package name through the package name The specified xml file; the fourth method is to directly parse the default_workspace_xxx.xml in the Launcher directory. When we develop applications, we can use the first, second, and third ideas to realize resource sharing between the two applications and develop functions with special needs.

If you want to learn more programming, please follow the official account: android full access