本文基于android9.0来写的。

一、产品需求:如下图,类似mac的桌面系统

android apk桌面属性 android桌面系统_android

(1)区域1是系统标题栏。

(2)区域2是长显示的窗口。

(3)区域3 普通app显示的窗口。

(4) 区域4 也是一个上显示的窗口,主要用于应用的点击启动。

二、下图是根据Android系统特性画的草图:

(1)区域1是系统statusBar。

(2)区域2是长显示的窗口,用WindowMananger添加的窗口。

(3)区域3 普通app显示的窗口,定制系统默认窗口大小,定位至区域3。

(4) 区域4 也是一个长显示的窗口,同样是用WindowMananger添加的窗口。

android apk桌面属性 android桌面系统_android_02

三、主要实现思路:通过freefrom 和添加悬浮window来实现。因为app只有在freeform模式才能任意定制多窗口大小和位置。

1、区域1无需改动,还是采用系统默认stausBar。

2、修改famework AMS ,以freeform模式启动Launcher。修改HOME的的区域为上图区域3。

3、Lancher启动时,同时启动两个两个service, 然后通过WindowManger来添加区域2和区域4。

4、区域4 的Icon点击以freeform模式启动其他APP。在framework拦截启动APP的入口,以freeform模式启动,同时设置窗口大小和位置(区域3)。

四、最终实现的demo如下:

android apk桌面属性 android桌面系统_java_03

 

五、具体实现方法如下:

1、开启freeform

google 默认freeform是关闭的,如果要测试需要手动开启。

配置config_freeformWindowManagement

WORKING_DIRECTORY/frameworks/base/core/res/res/values/config.xml

如下,将config_freeformWindowManagement配置为true

android apk桌面属性 android桌面系统_bundle_04

WORKING_DIRECTORY/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

private void retrieveSettings() {
    final ContentResolver resolver = mContext.getContentResolver();
    //获取是否支持freeform
    final boolean freeformWindowManagement =
            mContext.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
                    || Settings.Global.getInt(
                            resolver, DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;

由上面代码可以看出有两种方法可以开启freeform

(1)  Settings 设置开启的属性

WORKING_DIRECTORY/frameworks/base/core/java/android/provider/Settings.java
/**
 * Whether any activity can be resized. When this is true, any
 * activity, regardless of manifest values, can be resized for multi-window.
 * (0 = false, 1 = true)
 * @hide
 */
public static final String DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES
        = "force_resizable_activities";

/**
 * Whether to enable experimental freeform support for windows.
 * @hide
 */
public static final String DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT
        = "enable_freeform_support";

这个用命令就可以开启:

adb shell settings put global enable_freeform_support  1

adb shell settings put global force_resizable_activities  1

如果要在framework中默认开启,可以这样:

private void retrieveSettings() {
    final ContentResolver resolver = mContext.getContentResolver();
    //简单粗暴开启,哈哈
    Settings.Global.putInt(resolver, DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 1);
    Settings.Global.putInt(resolver, DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 1);
    
    final boolean freeformWindowManagement =
            mContext.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
                    || Settings.Global.getInt(
                            resolver, DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;

(2)给系统添加feature:android.software.freeform_window_management

     其实android系统默认已经写好,这里可以找到:

WORKING_DIRECTORY/frameworks/native/data/etc

android apk桌面属性 android桌面系统_java_05

可以将android.software.freeform_window_management.xml 拷贝到WORKING_DIRECTORY/frameworks/base/data/etc

android apk桌面属性 android桌面系统_bundle_06

里面有个Android.mk在这里面添加如下:

########################
include $(CLEAR_VARS)
LOCAL_MODULE := android.software.freeform_window_management.xml
LOCAL_MODULE_CLASS := ETC
LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions
LOCAL_SRC_FILES := $(LOCAL_MODULE)
include $(BUILD_PREBUILT)

它最终会被拷贝到手机的system/etc/permissions/目录下,开机时PMS会通过的读取此目录下xml配置开启freeform这个feature.

但是经过测试这个还是不够,还需要开启

Settings.Global.putInt(resolver, DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 1);

2、freeform模式启动app

Android为了支持多窗口,在运行时创建了多个Stack,Stack就是类似这里虚拟桌面的作用。

每个Stack会有一个唯一的Id,在WindowConfiguration.java中定义了这些Stack的Id:如下:

public class WindowConfiguration
  implements android.os.Parcelable, java.lang.Comparable<android.app.WindowConfiguration>
{
public  WindowConfiguration() { throw new RuntimeException("Stub!"); }
public  void writeToParcel(android.os.Parcel dest, int flags) { throw new RuntimeException("Stub!"); }
public  int describeContents() { throw new RuntimeException("Stub!"); }
public  void setBounds(android.graphics.Rect rect) { throw new RuntimeException("Stub!"); }
public  void setAppBounds(android.graphics.Rect rect) { throw new RuntimeException("Stub!"); }
public  android.graphics.Rect getAppBounds() { throw new RuntimeException("Stub!"); }
public  android.graphics.Rect getBounds() { throw new RuntimeException("Stub!"); }
public  void setWindowingMode(int windowingMode) { throw new RuntimeException("Stub!"); }
public  int getWindowingMode() { throw new RuntimeException("Stub!"); }
public  void setActivityType(int activityType) { throw new RuntimeException("Stub!"); }
public  int getActivityType() { throw new RuntimeException("Stub!"); }
public  void setTo(android.app.WindowConfiguration other) { throw new RuntimeException("Stub!"); }
public  int compareTo(android.app.WindowConfiguration that) { throw new RuntimeException("Stub!"); }
public static final int ACTIVITY_TYPE_ASSISTANT = 4;
public static final int ACTIVITY_TYPE_HOME = 2;
public static final int ACTIVITY_TYPE_RECENTS = 3;
public static final int ACTIVITY_TYPE_STANDARD = 1;
public static final int ACTIVITY_TYPE_UNDEFINED = 0;
public static final int WINDOWING_MODE_FREEFORM = 5;
public static final int WINDOWING_MODE_FULLSCREEN = 1;
public static final int WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY = 4;
public static final int WINDOWING_MODE_PINNED = 2;
public static final int WINDOWING_MODE_SPLIT_SCREEN_PRIMARY = 3;
public static final int WINDOWING_MODE_SPLIT_SCREEN_SECONDARY = 4;
public static final int WINDOWING_MODE_UNDEFINED = 0;
}

例如以freeform启动activity

Intent intent = new Intent(this, AdjacentActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK);
ActivityOptions activityOptions = ActivityOptions.makeBasic();
activityOptions.setLaunchWindowingMode(5);
int left = 100;
int top = 0;
int right = 720;
int bottom = 2280;
activityOptions.setLaunchBounds(new Rect(left,top,right,bottom));
Bundle bundle = activityOptions.toBundle();
startActivity(intent,bundle)

setLaunchWindowingMode 是个testApi,我们开发的时候获取不到,可以通过反射来调用

public static ActivityOptions getActivityOptions(Context context) {
    ActivityOptions options = ActivityOptions.makeBasic();
    int freeform_stackId = 5;
    try {
        Method method = ActivityOptions.class.getMethod("setLaunchWindowingMode", int.class);
        method.invoke(options, freeform_stackId);
    } catch (Exception e) { /* Gracefully fail */ }

    return options;
}

具体详细用法可以参考

https://github.com/farmerbb/Taskbar

 

3、如何以freeform 启动launcher

/WORKING_DIRECTORY/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

boolean startHomeActivityLocked(int userId, String reason) {
    if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL
            && mTopAction == null) {
        // We are running in factory test mode, but unable to find
        // the factory test app, so just sit around displaying the
        // error message and don't try to start anything.
        return false;
    }
    Intent intent = getHomeIntent();
    ActivityInfo aInfo = resolveActivityInfo(intent, STOCK_PM_FLAGS, userId);
    if (aInfo != null) {
        intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
        // Don't do this if the home app is currently being
        // instrumented.
        aInfo = new ActivityInfo(aInfo);
        aInfo.applicationInfo = getAppInfoForUser(aInfo.applicationInfo, userId);
        ProcessRecord app = getProcessRecordLocked(aInfo.processName,
                aInfo.applicationInfo.uid, true);
        if (app == null || app.instr == null) {
            intent.setFlags(intent.getFlags() | FLAG_ACTIVITY_NEW_TASK);
            final int resolvedUserId = UserHandle.getUserId(aInfo.applicationInfo.uid);
            // For ANR debugging to verify if the user activity is the one that actually
            // launched.
            final String myReason = reason + ":" + userId + ":" + resolvedUserId;
           //这里调用启动homeActivity
            mActivityStartController.startHomeActivity(intent, aInfo, myReason);
        }
    } else {
        Slog.wtf(TAG, "No home screen found for " + intent, new Throwable());
    }

    return true;
}

 

/WORKING_DIRECTORY/frameworks/base/services/core/java/com/android/server/am/ActivityStartController.java

void startHomeActivity(Intent intent, ActivityInfo aInfo, String reason) {
    mSupervisor.moveHomeStackTaskToTop(reason);

// 这里我设置了freefrom启动参数。
    ActivityOptions activityOptions = ActivityOptions.makeBasic();
    activityOptions.setLaunchWindowingMode(5);
    Point size = new Point();
    mService.mWindowManager.getInitialDisplaySize(0,size);
    int left = 100;
    int top = 0;
    int right = size.x;
    int bottom = size.y*6/7;
    activityOptions.setLaunchBounds(new Rect(left,top,right,bottom));
    Bundle bundle = activityOptions.toBundle();

   

    mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
            .setOutActivity(tmpOutRecord)
            .setCallingUid(0)
            .setActivityInfo(aInfo)
            .setActivityOptions(bundle)
            .execute();
    mLastHomeActivityStartRecord = tmpOutRecord[0];
    if (mSupervisor.inResumeTopActivity) {
        // If we are in resume section already, home activity will be initialized, but not
        // resumed (to avoid recursive resume) and will stay that way until something pokes it
        // again. We need to schedule another resume.
        mSupervisor.scheduleResumeTopActivities();
    }
}

 

WORKING_DIRECTORY/frameworks/base/services/core/java/com/android/server/am/ActivityStarter.java

作如下修改:

// Note: This method should only be called from {@link startActivity}.
private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
        IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
        int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
        ActivityRecord[] outActivity) {

    setInitialState(r, options, inTask, doResume, startFlags, sourceRecord, voiceSession,
            voiceInteractor);

    computeLaunchingTaskFlags();

    computeSourceStack();

    mIntent.setFlags(mLaunchFlags);

    ActivityRecord reusedActivity = getReusableIntentActivity();

    int preferredWindowingMode = WINDOWING_MODE_UNDEFINED;
    int preferredLaunchDisplayId = DEFAULT_DISPLAY;

    
    //在这里对启动的activity作拦截,用freeform启动,设置setLaunchWindowingMode 为5 
    Point size = new Point();
    Display display = mSupervisor.mDisplayManager.getDisplay(0);
    DisplayMetrics displayMetrics = new DisplayMetrics();
    display.getMetrics(displayMetrics);
    size.x = displayMetrics.widthPixels;
    size.y = displayMetrics.heightPixels;
    Slog.e("ZyTest", "myTestZize displayMetrics  w =  " +  displayMetrics.widthPixels + "   h = " + displayMetrics.heightPixels);

    if (mOptions != null) {
        preferredWindowingMode = mOptions.getLaunchWindowingMode();
        preferredLaunchDisplayId = mOptions.getLaunchDisplayId();
        Slog.e("ZyTest", "preferredWindowingMode = " + preferredWindowingMode );
        int left = size.x /4;
        int top = 0;
        int right = size.x;
        int bottom = size.y*6/7;;
        mOptions.setLaunchBounds(new Rect(left,top,right,bottom));
        mOptions.setLaunchWindowingMode(5);
    }else {
        mOptions = ActivityOptions.makeBasic();
        int left = size.x /4;
        int top = 0;
        int right = size.x;
        int bottom = size.y*6/7;;
        mOptions.setLaunchBounds(new Rect(left,top,right,bottom));
        mOptions.setLaunchWindowingMode(5);
        Slog.e("ZyTest", "22222 preferredWindowingMode = " + preferredWindowingMode );
    }
    preferredWindowingMode = 5;

    // windowing mode and preferred launch display values from {@link LaunchParams} take
    // priority over those specified in {@link ActivityOptions}.
    if (!mLaunchParams.isEmpty()) {
        if (mLaunchParams.hasPreferredDisplay()) {
            preferredLaunchDisplayId = mLaunchParams.mPreferredDisplayId;
        }

        if (mLaunchParams.hasWindowingMode()) {
            preferredWindowingMode = mLaunchParams.mWindowingMode;
        }
    }

设置启动的目标栈的setWindowingMode为freeform,位置和大小自定义

private int setTaskFromReuseOrCreateNewTask(
            TaskRecord taskToAffiliate, ActivityStack topStack) {
        mTargetStack = computeStackFocus(mStartActivity, true, mLaunchFlags, mOptions);




        boolean ishome = mStartActivity.isActivityTypeHome();
        Slog.e("ZyTest", "is home llllllllll = " + ishome);

//        if(ishome){
            Point size = new Point();
//            mService.mWindowManager.getInitialDisplaySize(0,size);

//设置目标栈的setWindowingMode、位置 、大小
        Display display = mSupervisor.mDisplayManager.getDisplay(0);
        DisplayMetrics displayMetrics = new DisplayMetrics();
        display.getMetrics(displayMetrics);
        size.x = displayMetrics.widthPixels;
        size.y = displayMetrics.heightPixels;


            int left = size.x /4;
            int top = 0;
            int right = size.x;
            int bottom = size.y*6/7;
            mTargetStack.setBounds(left,top,right,bottom);
            mTargetStack.setWindowingMode(5);
//        }


        // Do no move the target stack to front yet, as we might bail if
        // isLockTaskModeViolation fails below.

        if (mReuseTask == null) {
            final TaskRecord task = mTargetStack.createTaskRecord(
                    mSupervisor.getNextTaskIdForUserLocked(mStartActivity.userId),
                    mNewTaskInfo != null ? mNewTaskInfo : mStartActivity.info,
                    mNewTaskIntent != null ? mNewTaskIntent : mIntent, mVoiceSession,
                    mVoiceInteractor, !mLaunchTaskBehind /* toTop */, mStartActivity, mSourceRecord,
                    mOptions);
            addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask - mReuseTask");
            updateBounds(mStartActivity.getTask(), mLaunchParams.mBounds);

            if (DEBUG_TASKS) Slog.v(TAG_TASKS, "Starting new activity " + mStartActivity
                    + " in new task " + mStartActivity.getTask());
        } else {
            addOrReparentStartingActivity(mReuseTask, "setTaskFromReuseOrCreateNewTask");
        }

        if (taskToAffiliate != null) {
            mStartActivity.setTaskToAffiliateWith(taskToAffiliate);
        }

        if (mService.getLockTaskController().isLockTaskModeViolation(mStartActivity.getTask())) {
            Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
            return START_RETURN_LOCK_TASK_MODE_VIOLATION;
        }

        if (mDoResume) {
            mTargetStack.moveToFront("reuseOrNewTask");
        }
        return START_SUCCESS;
    }