Taskbar is an awesome Android launcher which supports start app to freefrom windowing mode directly to provide desktop UI for Android user. And it is integrated into Android-x86 and BlissOS as alternative launcher to provide desktop experience for users. If you are not familiar with it, you can visit Taskbar’s Google Play Store page to download and experience it. This article is mainly used to introduce how Taskbar implements this feature, and which requirements of Android system needs to support Taskbar to start app in freeform windowing mode.

It needs Android device supports freeform

At section “3.8.14. Multi-windows” of Android 7.0 CDD, Google needs device implementation with screen size xlarge should support freeform mode:

Device implementations MUST NOT offer split-screen or freeform mode if both the screen height and width is less than 440 dp.

Device implementations with screen size xlarge SHOULD support freeform mode.

At Android 11 CDD section multi-windows, it also keeps this definition and restriction. So if Android device has xlarge screen size, and it is released with CTS passed, we can assume it supports freeform windowing mode, and Taskbar can work correctly to start app with freeform windowing mode. There are also many Android variants to provide desktop experience for users support freeform windowing mode directly, such as Android-x86 and BlissOS.

If you are a ROM developer or Android frameworks developer, and you want to make your device to support freeform windowing mode, you need config_freeformWindowManagement config is set to true at your frameworks/base/core/res/res/values/config.xml. It is recommended to set it with vendor overrides as what I did for boringdroid at its vendor_boringdroid project. You also should copy frameworks/native/data/etc/android.software.freeform_window_management.xml to device system/etc/permissions/android.software.freeform_window_management.xml similar likes boringdroid.mk at vendor_boringdroid

If you are a normal Android user, you can enable Enable freeform windows item at SettingsDeveloper Options page by following Android Policy’s article “Freeform windows can be enabled in Android Q without hacks”.

It needs app supports multi-window

Android developer multi-window section shows how app to declare itself that supports multi-window, including freeform windowing mode. It is very important, because not all apps can work correctly when you resizing its window to specific size.

If you are a ROM developer or Android frameworks developer, and you want to force all activities resizable for freeform windowing mode or other multi-window modes, you can modify ActivityManagerService to force all activities resizable similar like boringdroid commit: Make sure all activities support resizable. From Android 10, we also can set display.settings.xml=freeform to your device, and set display to freeform, that will permit any Activity started on this display can enter freeform windowing mode. There is an example from goldfish config.ini.freeform.

If you are an app developer, you can follow official manual to prepare your app for large screens and multi-window to ensure your app can work perfectly when it is in freeform windowing mode and other multi-window modes.

If you are a normal user, and you want to force all activities are resizable and let it can be started into freeform windowing mode by Taskbar, you can enable Make all activities resizable for multi-window, regardless of manifest values similar likes to enable freeform windows forcibly.

Let’s start an app with freeform windowing mode

The following code snippets are both from Taskbar’s U.java.

When we start an app or an Activity, we can assign a Bundle to startActivity method. Android provides another class called ActivityOptions that can be serialized to Bundle to pass launching parameters related to this Activity to frameworks, and it will calculate the final windowing mode based on ActivityOptions and other display and device configs. If your display is freeform, and every Activity started on this display will enter freeform mode, if we don’t specific any fixed fullscreen/split screen/picture in picture windowing mode with ActivityOptions. If you display is not freeform, and the Android devices support freeform, and the Activity will be started supports freeform, we can add freeform windowing mode to ActivityOptions and pass it to startActivity, and frameworks will let this Activity enter freeform windowing mode. It’s very simle and clear, right?

But there are many restrictions for third-party apps, including Taskbar. The first difficulty is that old Android versions use stack id to represent freeform windowing mode at ActivityOptions, but new versions use windowing mode id to represent freeform windowing mode. So Taskbar should make a compatibility to set freeform windowing mode for Activity for different Android versions. The second thing is restriction on non-SDK interfaces. The methods of ActivityOptions to set freeform windowing mode is hidden, and we must use reflection to access them. But from Android 9, Android restricts app to access hidden APIs. Taskbar uses some tricks to bypass this restriction.

Let’s start to dive into Taskbar’s source code to see what it does to overcome those difficulties and let the function be realized.

The first station is U#allowReflection():

public static void allowReflection() {
    GlobalHelper helper = GlobalHelper.getInstance();
    if(helper.isReflectionAllowed()) return;

    try {
        Method forName = Class.class.getDeclaredMethod("forName", String.class);
        Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);

        Class<?> vmRuntimeClass = (Class<?>) forName.invoke(null, "dalvik.system.VMRuntime");
        Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null);
        Method setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});

        Object vmRuntime = getRuntime.invoke(null);
        setHiddenApiExemptions.invoke(vmRuntime, new Object[]{new String[]{"L"}});
    } catch (Throwable ignored) {}

    helper.setReflectionAllowed(true);
}

It’s main work is to use reflection to get VMRuntime#setHiddenApiExemptions without restriction, and pass L as input to exempt all hidden APIs. Actually this method only work correctly on API 29 and earlier. If you also want to bypass this restriction on API 30, you can check StackOverflow’s question: Bypass Android’s hidden API restrictions.

Now, Taskbar can use reflection to get hidden fields and call hidden methods. The next station is U#getFreeformWindowModeId():

// From android.app.ActivityManager.StackId
private static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;
private static final int FREEFORM_WORKSPACE_STACK_ID = 2;

// From android.app.WindowConfiguration
private static final int WINDOWING_MODE_FULLSCREEN = 1;
private static final int WINDOWING_MODE_FREEFORM = 5;

private static int getFreeformWindowModeId() {
    if(getCurrentApiVersion() >= 28.0f)
        return WINDOWING_MODE_FREEFORM;
    else
        return FREEFORM_WORKSPACE_STACK_ID;
}

U#getFreeformWindowModeId() is used to get freeform windowing mode that defined in Android frameworks. Taskbar doesn’t use reflection to get those hidden fields from ActivityManager or WindowConfiguration, and copy their values to its source code directly. The U#getFreeformWindowModeId() will return different values for different Android versions.

When it gets correct freeform windowing mode id, and it will pass it to ActivityOptions at U#getActivityOptions(Context context, ApplicationType applicationType, View view):

if(stackId != -1) {
    allowReflection();
    try {
        Method method = ActivityOptions.class.getMethod(getWindowingModeMethodName(), int.class);
        method.invoke(options, stackId);
    } catch (Exception ignored) {}
}

The U#getActivityOptions(Context context, ApplicationType applicationType, View view) uses U#allowReflection() shown above to access hidden API of ActivityOptions. It use U#getWindowingModeMethodName() to get API to pass freeform windowing mode id for different Android versions:

private static String getWindowingModeMethodName() {
    if(getCurrentApiVersion() >= 28.0f)
        return "setLaunchWindowingMode";
    else
        return "setLaunchStackId";
}

Those APIs are not exposed publicly, so Google can change it if need. The user such as Taskbar must make itself methods compatible with different Android versions.

After setting correct freeform windowing mode id for ActivityOptions, the next station is to set launch bounds or window bounds to Activity’s ActivityOptions. It is at getActivityOptionsBundle(Context context,ApplicationType applicationType, View view, int left, int top, int right, int bottom):

ActivityOptions options = getActivityOptions(context, applicationType, view);
if(options == null) return null;

if(Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
    return options.toBundle();

return options.setLaunchBounds(new Rect(left, top, right, bottom)).toBundle();

The launch bounds is the initial bounds of Activity’s window after it started. Actually, Taskbar can’t get final bounds before window closed. So it uses default bounds for Activity when every start. To fix this problem, I have added a commit Support persist window bounds to ignore launch bounds from ActivityOptions and keep bounds at frameworks/base.

Summary

It’s very clear how Taskbar start app with freeform windowing mode. If app and system supports freeform windowing mode, it uses reflection to get freeform windowing mode id and pass it to ActivityOptions. And it will use startActivity with Bundle generated from ActivityOptions to tell frameworks to start specific Activity or app to freeform windowing mode. When we start an app from Launcher, we actually start its main Activity, so there some mix-uses between Activity and app. If you’re clear about it, just going head to implement your custom launcher to start app in freeform windowing mode.