Android U 自由窗口(浮窗)——启动流程(从launcher到systemUI)

133 阅读10分钟

简介

本文主要介绍从多任务中启动自由窗口的流程。 操作手法:打开任意应用后,进入到多任务,点击应用图标,选择【自由窗口】。

层级结构

这里以后台存在dialer、messaging应用,通过多任务中启动messaging应用为自由窗口为例。 使用命令adb shell dumpsys activity containers抓取对应的层级结构,在DefaultTaskDisplayArea中找到对应的task。 启动前:

#1 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
 #3 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
  #0 Task=1155 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
   #0 ActivityRecord{6fc2a2d u0 com.android.launcher3/.uioverrides.QuickstepLauncher t1155} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
    #0 e485e74 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
 #2 Task=1161 type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
  #0 ActivityRecord{99290ba u0 com.android.dialer/.main.impl.MainActivity t1161} type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
   #0 6e78921 com.android.dialer/com.android.dialer.main.impl.MainActivity type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
 #1 Task=1160 type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
  #0 ActivityRecord{bfe1cf6 u0 com.android.messaging/.ui.conversationlist.ConversationListActivity t1160} type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
   #0 a1f0bc7 com.android.messaging/com.android.messaging.ui.conversationlist.ConversationListActivity type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]

#1 Task=1160这是我们要启动为自由窗口的task,我们主要观察其窗口模式和窗口大小的变化,即mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]的变化。

启动后:

#1 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
 #3 Task=1160 type=standard mode=freeform override-mode=freeform requested-bounds=[50,134][770,1614] bounds=[50,134][820,1614]
  #0 ActivityRecord{bfe1cf6 u0 com.android.messaging/.ui.conversationlist.ConversationListActivity t1160} type=standard mode=freeform override-mode=undefined requested-bounds=[0,0][0,0] bounds=[50,134][820,1614]
   #0 63479c4 com.android.messaging/com.android.messaging.ui.conversationlist.ConversationListActivity type=standard mode=freeform override-mode=undefined requested-bounds=[0,0][0,0] bounds=[50,134][820,1614]
 #2 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
  #0 Task=1155 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
   #0 ActivityRecord{6fc2a2d u0 com.android.launcher3/.uioverrides.QuickstepLauncher t1155} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
    #0 e485e74 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
 #1 Task=1161 type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
  #0 ActivityRecord{99290ba u0 com.android.dialer/.main.impl.MainActivity t1161} type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
   #0 6e78921 com.android.dialer/com.android.dialer.main.impl.MainActivity type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]

这里我们看到#3 Task=1160也就是我们启动为自由窗口的task,明显可以看到其窗口模式变为了自由窗口,即mode=freeform override-mode=freeform。窗口大小也发生了相应的变化,即requested-bounds=[50,134][770,1614] bounds=[50,134][820,1614]

另外还有补充说明一点,切换为自由窗口前后Task=1160前面的序号 #1 变为了 #3,这个序号最大的task表示在最前台,即切换为自由窗口的Task=1160在最前台。

开启自由窗口

运行adb命令开启自由窗口选项

adb shell settings put global enable_freeform_support 1
adb shell settings put global force_resizable_activities 1

原生机在多任务中原本点击应用图标不会出现【自由窗口】的选项,通过命令开启后会显示出来。 在这里插入图片描述

查找相关代码

在这里插入图片描述 在根据recent_task_option_freeform找到了相应的代码。 在这里插入图片描述

桌面侧流程

FREE_FORM创建

代码路径:packages/apps/Launcher3/quickstep/src/com/android/quickstep/TaskShortcutFactory.java

    TaskShortcutFactory FREE_FORM = new TaskShortcutFactory() {
        @Override
        public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
                TaskIdAttributeContainer taskContainer) {
            //获取任务列表中对应的task
            final Task task  = taskContainer.getTask();
            if (!task.isDockable) {
                return null;
            }
            if (!isAvailable(activity, task.key.displayId)) {
                return null;
            }
            //创建了自由窗口
            //这里我们主要关注FreeformSystemShortcut
            return Collections.singletonList(new FreeformSystemShortcut(
                    R.drawable.ic_caption_desktop_button_foreground,
                    R.string.recent_task_option_freeform, activity, taskContainer,
                    LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP));
        }

        //判断是否支持自由窗口
        //这里我们主要关注supportsFreeformMultiWindow
        private boolean isAvailable(BaseDraggingActivity activity, int displayId) {
            return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity)
                    && !SystemProperties.getBoolean("persist.wm.debug.desktop_mode", false)
                    && !SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false);
        }
    };

点击任意任务的icon就会走这些流程,taskContainer.getTask()这里会获取对应的任务列表中的task。 在这里插入图片描述

创建自由窗口

new FreeformSystemShortcut(
                   R.drawable.ic_caption_desktop_button_foreground,
                   R.string.recent_task_option_freeform, activity, taskContainer,
                   LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP)

先看前两个参数R.drawable.ic_caption_desktop_button_foreground(资源路径:packages/apps/Launcher3/res/drawable/ic_caption_desktop_button_foreground.xml)和 R.string.recent_task_option_freeform(资源路径:packages/apps/Launcher3/quickstep/res/values-zh-rCN/strings.xml) 在这里插入图片描述

activity:就是桌面,通过添加log打印结果:com.android.launcher3.uioverrides.QuickstepLauncher@c88e80f

taskContainer:最近任务列表中的View的一些属性,其为TaskView内部类的TaskIdAttributeContainer对象,常用于获取TaskThumbnailView、Task、IconView等TaskView的属性。 通过添加log打印结果:taskContainer:TaskIdAttributeContainer{mThumbnailView=com.android.quickstep.views.TaskThumbnailView{3a39e6e V.ED..... ........ 0,224-978,2234 #7f0902a7 app:id/snapshot}, mTask=[id=1171 windowingMode=1 user=0 lastActiveTime=150231] null, mIconView=com.android.quickstep.views.IconView{ff221e9 V.ED..CL. ...P.... 405,0-573,168 #7f09018f app:id/icon}, mStagePosition=-1, mA11yNodeId=2131296947}。 从这里我们可以看到taskContainer中有 id=1171的Task ,也就是我们当前从最近任务列表中选中的task,代码中也是通过 taskContainer.getTask()获取对应的任务列表中的task。

LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP:一个log相关的常量,大概意思就是通过多任务启动浮窗。

判断是否支持自由窗口

ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity)

代码路径:frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java

    /**
     * Returns true if the system supports freeform multi-window.
     */
    public boolean supportsFreeformMultiWindow(Context context) {
        final boolean freeformDevOption = Settings.Global.getInt(context.getContentResolver(),
                Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
        return ActivityTaskManager.supportsMultiWindow(context)
                && (context.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT)
                || freeformDevOption);
    }

代码路径:frameworks/base/core/java/android/provider/Settings.java

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

配置项通过adb shell settings put global enable_freeform_support 1命令打开。

后续继续分析FreeformSystemShortcut类,看看自由窗口是如何创建的。

自由窗口的创建

桌面侧自由窗口流程主要是在TaskShortcutFactory中的内部类FreeformSystemShortcut实现。 代码路径:packages/apps/Launcher3/quickstep/src/com/android/quickstep/TaskShortcutFactory.java

    class FreeformSystemShortcut extends SystemShortcut<BaseDraggingActivity> {
        private static final String TAG = "FreeformSystemShortcut";

        private Handler mHandler;

        private final RecentsView mRecentsView;
        private final TaskThumbnailView mThumbnailView;
        private final TaskView mTaskView;
        private final LauncherEvent mLauncherEvent;

        public FreeformSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity,
                TaskIdAttributeContainer taskContainer, LauncherEvent launcherEvent) {
            super(iconRes, textRes, activity, taskContainer.getItemInfo(),
                    taskContainer.getTaskView());
            mLauncherEvent = launcherEvent;
            mHandler = new Handler(Looper.getMainLooper());
            //最近任务列表中选中的应用的View
            mTaskView = taskContainer.getTaskView();
            //这里activity就是桌面
            //通过桌面getOverviewPanel方法获取了整个最近任务View
            mRecentsView = activity.getOverviewPanel();
            mThumbnailView = taskContainer.getThumbnailView();
        }

这里构造方法中就是从前面创建FREE_FORM时,传递过来的值复制给了FreeformSystemShortcut的成员变量。 通过taskContainer.getTaskView()获取到了当前最近任务中选中的TaskView,其打印如下: mTaskViewTaskView{mTask=[id=1171 windowingMode=1 user=0 lastActiveTime=150231] null, mIconView=com.android.quickstep.views.IconView{ff221e9 V.ED..CL. ...P.... 405,0-573,168 #7f09018f app:id/icon}, mActivity=com.android.launcher3.uioverrides.QuickstepLauncher@d676323, mTaskViewId=1, mTaskIdContainer=[1171, -1], mTaskIdAttributeContainer=[TaskIdAttributeContainer{mThumbnailView=com.android.quickstep.views.TaskThumbnailView{3a39e6e V.ED..... ........ 0,224-978,2234 #7f0902a7 app:id/snapshot}, mTask=[id=1171 windowingMode=1 user=0 lastActiveTime=150231] null, mIconView=com.android.quickstep.views.IconView{ff221e9 V.ED..CL. ...P.... 405,0-573,168 #7f09018f app:id/icon}, mStagePosition=-1, mA11yNodeId=2131296947}, null]}

监听onClick事件

        //监听onClick事件
        //响应最近任务中点击【自由窗口】的事件
        @Override
        public void onClick(View view) {
            dismissTaskMenuView(mTarget);
            //获取最近任务列表
            RecentsView rv = mTarget.getOverviewPanel();
            rv.switchToScreenshot(() -> {
                rv.finishRecentsAnimation(true /* toHome */, () -> {
                    mTarget.returnToHomescreen();
                    //在最近任务中通过startActivity启动应用
                    rv.getHandler().post(this::startActivity);
                });
            });
        }

mTarget:实际上就是桌面,其打印结果为com.android.launcher3.uioverrides.QuickstepLauncher@6da5963 rv:是RecentsView对象,也就是最近任务列表,通过 getOverviewPanel() 方法获取。其打印结果为com.android.quickstep.views.LauncherRecentsView{ade49d1 V.ED..... .......D 0,0-1440,2960 #7f09022a app:id/overview_panel},其对应的id就是 overview_panel

这里onClick监听的就是这个【自由窗口】的事件。 在这里插入图片描述

启动自由窗口

        //启动需要进入自由窗口Activity
        private void startActivity() {
            //通过TaskView(最近任务列表中选中的应用的View)获取其TaskKey
            final Task.TaskKey taskKey = mTaskView.getTask().key;
            //通过TaskKey获取其taskId
            final int taskId = taskKey.id;
            //创建响应的options
            final ActivityOptions options = makeLaunchOptions(mTarget);
            if (options != null) {
                options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
            }
            //options不为空,则从最近任务重启动activity,传递参数为taskId和options
            if (options != null
                    && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
                            options)) {
                final Runnable animStartedListener = () -> {
                    // Hide the task view and wait for the window to be resized
                    // TODO: Consider animating in launcher and do an in-place start activity
                    //       afterwards
                    mRecentsView.setIgnoreResetTask(taskId);
                    mTaskView.setAlpha(0f);
                };
                
                //一些缩略图相关操作
                ......
            }
        }

这个方法中有两个比较关键的点

  1. 设置ActivityOptions,主要用来设置自由窗口相关参数。
  2. 通过startActivityFromRecents启动自由窗口。

makeLaunchOptions

final ActivityOptions options = makeLaunchOptions(mTarget);

        //设置Options
        private ActivityOptions makeLaunchOptions(Activity activity) {
            ActivityOptions activityOptions = ActivityOptions.makeBasic();
            //设置窗口模式,即ActivityOptions.mLaunchWindowingMode为WINDOWING_MODE_FREEFORM
            activityOptions.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);
            // Arbitrary bounds only because freeform is in dev mode right now
            //主要设置需要启动为自由窗口的应用界面窗口相关的大小(以传递过来的activity为参照等比缩小)
            //这里activity参数传递过的是桌面(QuickstepLauncher)
            //其实就是以桌面的容器尺寸来等比创建一个Rect作为后续启动的自由窗口的尺寸
            final View decorView = activity.getWindow().getDecorView();
            final WindowInsets insets = decorView.getRootWindowInsets();
            //设置浮窗初始大小
            final Rect r = new Rect(0, 0, decorView.getWidth() / 2, decorView.getHeight() / 2);
            //设置偏移量,避免浮窗在状态栏之上显示
            //insets.getSystemWindowInsetLeft()获取左边insets相关大小(比较少见)
            //insets.getSystemWindowInsetTop()获取顶部边insets相关大小(比如状态栏)
            r.offsetTo(insets.getSystemWindowInsetLeft() + 50,
                    insets.getSystemWindowInsetTop() + 50);
            activityOptions.setLaunchBounds(r);
            return activityOptions;
        }

这个方法主通过ActivityOptions来设置 窗口模式(mLaunchWindowingMode)窗口大小(mLaunchBounds) ,后续启动自由窗口时会传递。 另外,浮窗大小最后还会在Task.computeFreeformBounds中进行校准。bounds的最终设置是在WindowConfiguration.setBounds,可以通过在这里添加堆栈来看看最终设置bounds的值以及其流程。例如:

代码路径:frameworks/base/core/java/android/app/WindowConfiguration.java

    /**
     * Sets the bounds to the provided {@link Rect}.
     * @param rect the new bounds value.
     */
    public void setBounds(Rect rect) {
        //添加堆栈 start
        //这里判断窗口模式为浮窗的情况打印log
        if (mWindowingMode == WINDOWING_MODE_FREEFORM){
            android.util.Slog.i("TAG","setBounds rect:"+rect,new Exception());
        }
        //end
        if (rect == null) {
            mBounds.setEmpty();
            return;
        }

        mBounds.set(rect);
    }

startActivityFromRecents

ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,options) 这里的传递需要进入到自由窗口的taskId,以及需要设置的options参数。后续继续分析该流程。

systemUI侧

systemUI在这里的作用是作为桌面和system_server通信的中转站。 代码路径:frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java

    /**
     * Starts a task from Recents synchronously.
     */
    public boolean startActivityFromRecents(int taskId, ActivityOptions options) {
        try {
            //这里传递过来的options不为空
            Bundle optsBundle = options == null ? null : options.toBundle();
            //调用startActivityFromRecents
            return ActivityManager.isStartResultSuccessful(
                    getService().startActivityFromRecents(
                            taskId, optsBundle));
        } catch (Exception e) {
            return false;
        }
    }

这里就是通过获取ActivityTaskManagerService调用其startActivityFromRecents方法,并传递taskIdoptsBundle

taskId:TaskShortcutFactory中startActivity传递过来的需要启动为自由窗口的应用taskId。

optsBundle:TaskShortcutFactory中通过makeLaunchOptions创建的值。