/
OpenFDE桌面环境的平行视界--基础版

OpenFDE桌面环境的平行视界--基础版

华为在其 Android 平板上率先推出了“平行视界”功能,使同一应用的不同页面能够并行显示。随后,其他平板厂商也相继推出了类似功能,直到谷歌在 Android 13 上推出了官方版本。这一功能在谷歌的实现中更加完整和强大,尽管其推出时间较晚。对于这个功能而言,产品设计的创新性挑战甚至超过了技术实现的难度。本文将介绍 OpenFDE 上平行视界的实现方法。

方案调研

Android系统的APP都是按全屏设计开发的,在屏幕尺寸较大的平板上,有些应用就会显得比较空,内容比较少,于是国产厂商率先推出了平行视界的功能。最少量开发、快速适配平板的方法。可以横屏下显示多Activity。各厂商都有,但叫法不同,如华为小米就叫平行窗口(magicWindow)。很多应用,如头条、B站、抖音均使用了平行视界,如图:

image-20240814094751209-20240814-014753.png
adbc-20240814-015032.jpg

 

在老版本的Android上各家厂商都做了比较大的定制才实现这个功能。研究了华为Androi10上平行视界的实现方式后,发现是他是借助了一部分分屏模式的特性,另外再新增了一个Magic Window的模式,必须在横屏下手动设置开关,并且按照文档配置各种切换、页面、尺寸等内容,限制是比较多的。

image-20240814095428260-20240814-015436.png
image-20240814095444593-20240814-015447.png

而谷歌官方在Android 13推出了一个大屏幕设备显示方案:Activity嵌入(Activity Embedding)。该功能不同于分屏模式(将多个应用同时显示在屏幕上),而是类似华为平行视界将同一个应用的多个不同Activity同时显示到屏幕上。

settings_app-20240814-015717.png

在谷歌的方案中,特性非常丰富,切换,动画、拆分、合并,开发使用也非常方便。其原理是在framework里新增了一个TaskFrament来替代原来的Task,TaskFragment 是 可用于包含 Activity 或其他 TaskFragment 的基本容器,它还能够管理 Activity 生命周期并更新其中 Activity 的可见性。

OpenFDE的平行视界代码实现

AOSP部分的代码是基Android11版本,并没有TaskFragment 这些代码,OpenFDE使用的屏幕模式是freeform(自由窗口模式),与分屏模式不能并行。在freeform模式实现了平行显示,层级绑定和基础版平行视界功能。

ActivityRecord和Task

Activity在framework所涉及的相关数据结构如下(此处的TaskRecord在Android11上应为Task):

与APP端的对应的关系是:

ams_relations-20240814-023652.jpg

Activity-->ActivityRecord

TaskInfo-->Task

网上找到的类关系图,如果有源码肯定更加好理解:

activity_record-20240814-023927.jpg
源码功能说明

具体管理这两个数据结构的类如下,下文具体说一下怎么在这些代码中加入平行视界的功能:

记录所有activity启动定位参数的持久化数据frameworks/base/services/core/java/com/android/server/wm/LaunchParamsPersister.java activity启动前的参数设置 frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java task移动的控制器 frameworks/base/services/core/java/com/android/server/wm/TaskPositioningController.java task移动操作的控制器 frameworks/base/services/core/java/com/android/server/wm/TaskPositioner.java task启动参数修改器 frameworks/base/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java ATMS 与应用端对应的task最全管理者,resizetask, movefront在这里 frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java task父容器,可以遍历所有 taskframeworks/base/services/core/java/com/android/server/wm/WindowContainer.java 所有应用task的容器,其中mTmpNormalStacks按显示顺序保存了所有应用的taskframeworks/base/services/core/java/com/android/server/wm/TaskDisplayArea.java 每个activity在wms对应的数据结构 frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java 每个activity栈在wms对应的数据结构 frameworks/base/services/core/java/com/android/server/wm/Task.java 任务栈管理者,全局唯一 frameworks/base/services/core/java/com/android/server/wm/ActivityStackSupervisor.java 保存了所有task的数据结构,可以遍历和操作frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java

LaunchParamsPersister 在freeform模式下把各个activity启动的坐标持久化在这个路径

1|:/ # cat /data/system_ce/0/launch_params/com.jingdong.app.mall_.MainFrameActivity.xml <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <launch_params display_unique_id="local:0" windowing_mode="5" bounds="610 98 1071 954" window_layout_affinity="com.jingdong.app.mall" />

ActivityStarter 在每个activity启动函数executeRequest中修改documentLaunchMode为DOCUMENT_LAUNCH_ALWAYS,这样能保证配置的每个activity都会去走新建task的逻辑。

      // fde start: MAGIC WINDOW       int magicType = mSupervisor.getMagicWindowType(aInfo.packageName, aInfo.name);       Task task = mRootWindowContainer.findMagicTask(aInfo.taskAffinity, MAGIC_MAIN_WINDOW);       if( magicType == MAGIC_ADDITIONAL_WINDOW) {           if(task != null){               mMagicLaunch = true;               aInfo.documentLaunchMode = DOCUMENT_LAUNCH_ALWAYS;           }           mWindowAffinity = aInfo.taskAffinity;       } else {           mMagicLaunch = false;       }       // fde end
      // fde start: MAGIC WINDOW       if(mMagicLaunch){           reusedTask = mRootWindowContainer.findMagicTask(mWindowAffinity, MAGIC_ADDITIONAL_WINDOW);           // Slog.e(TAG, "getmagic task:" + reusedTask);           mAddingToTask = true;       // fde end       }

TaskLaunchParamsModifier 计算窗口坐标的函数中,根据过滤条件调整启动坐标

  private int calculate(Task task, ActivityInfo.WindowLayout layout,           ActivityRecord activity, ActivityRecord source, ActivityOptions options, int phase,           LaunchParams currentParams, LaunchParams outParams) { ... // fde start MAGIC WINDOW       if(task != null && task.type == MAGIC_ADDITIONAL_WINDOW && source != null               && source.getTask() != null               && TextUtils.equals(source.getTask().mWindowLayoutAffinity, task.mWindowLayoutAffinity)               && source.getTask().type == MAGIC_MAIN_WINDOW) {           Rect rect = new Rect(source.getConfiguration().windowConfiguration.getBounds());           if(rect != null ){               rect.offset(rect.right - rect.left, 0);               outParams.mBounds.set(rect);               return RESULT_CONTINUE;           }       }       // fde end

ActivityTaskManagerService 移动task实际是执行的resizeTask函数,在移动同一应用的平行视察时,同步移动另一个task

  @Override   public boolean resizeTask(int taskId, Rect bounds, int resizeMode) { ...               // fde start MAGIC WINDOW               if(task.type == MAGIC_MAIN_WINDOW || task.type == MAGIC_ADDITIONAL_WINDOW ){                   Task bMostTask = mRootWindowContainer.getBottomMostTask();                   Task relative = null;                   while(bMostTask != null ){                       if( TextUtils.equals(task.affinity, bMostTask.affinity)){                           relative = bMostTask;                           break;                       }                       Task above = mRootWindowContainer.getTaskAbove(bMostTask);                       // Slog.e(TAG, "resizeTask: bMostTask=" + bMostTask + " above=" + above);                       bMostTask = above;                   }                   if(relative != null && relative != task){                       Rect b = new Rect(bounds);                       if(relative.type == MAGIC_ADDITIONAL_WINDOW){                           b.left = b.left + bounds.right - bounds.left;                           b.right = b.right + bounds.right - bounds.left;                       } else if(relative.type == MAGIC_MAIN_WINDOW){                           b.left = b.left - bounds.right + bounds.left;                           b.right = b.right - bounds.right + bounds.left;                       }                       relative.resize(b, resizeMode, preserveWindow);                     }               }               // fde end

ActivityStackSupervisor 中增加了读入配置文件的代码配置文件如下,即每一个应用有一个主task,其他task显示在他的副task里

:/ # cat system_ext/etc/magicwindow_config/magic_config.xml <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <packages> <package packagename="com.jingdong.app.mall" main="MainFrameActivity" /> <package packagename="com.ss.android.article.news" main="MainActivity" /> <package packagename="com.zhihu.android" main="MainActivity" /> <package packagename="com.xingin.xhs" main="IndexActivityV2" /> <package packagename="com.sina.weibo" main="VisitorMainTabActivity" /> <package packagename="com.moji.mjweather" main="MainActivity" /> <package packagename="com.kuaishou.nebula" main="HomeActivity" /> <package packagename="ctrip.android.view" main="CtripHomeActivity" /> <package packagename="com.Qunar" main="MainActivity" /> <package packagename="com.tencent.mm" main="LauncherUI" />
窗口层级

在 Android 窗口管理中,所有的窗口都是以树形数据结构进行组织管理的,认知这棵 WMS 的树有助于我们理解窗口的管理和显示,同时,WMS 的层级也决定了其在 SurfaceFlinger 的层级结构,这恰恰决定了它的显示规则。

0cf0a19b1198ccc71692d1703899738a-20240814-031657.png

实际上所有应用的窗口都属于TaskDisplayArea,在TaskDisplayArea内部管理Task的层级是一个列表mTmpNormalStacks,保存的下标就是窗口的层级。

  // fde start MAGIC WINDOW   // reorder if magic window on top   private void recheckStackOrdering() {       int size = mTmpNormalStacks.size();       if(size == 0 ){           return;       }       ActivityStack topTask = mTmpNormalStacks.get(size - 1);       if(topTask.type == MAGIC_MAIN_WINDOW || topTask.type == MAGIC_ADDITIONAL_WINDOW){           Task magicTask = mRootWindowContainer.findMagicTask(topTask.mWindowLayoutAffinity,                   topTask.type == MAGIC_MAIN_WINDOW ? MAGIC_ADDITIONAL_WINDOW : MAGIC_MAIN_WINDOW);           if(mTmpNormalStacks.contains(magicTask)){               mTmpNormalStacks.remove(magicTask);               mTmpNormalStacks.remove(topTask);               mTmpNormalStacks.add((ActivityStack)magicTask);               mTmpNormalStacks.add(topTask);           }       }   }   // fde end

经过这些改动,基本实现了如下效果:

image-20240814112142038-20240814-032145.png

另外,对微信进行操作的时候,发现微信的LaunchUI上不能多次点击,推测是在freeform模式生命周期的问题,在ActivityTaskManagerService中强制执行一次pauseActivity

    @Override   public final void activityIdle(IBinder token, Configuration config, boolean stopProfiling) {   ... // fde start MAGIC WINDOW                     // com.tencent.mm LaunchUI need a pause lifecycle to ensure focus update               if(task.type == MAGIC_ADDITIONAL_WINDOW && task.affinity.contains("com.tencent.mm")){                   Task magicMainTask = mRootWindowContainer.findMagicTask(task.mWindowLayoutAffinity, MAGIC_MAIN_WINDOW);                   if(magicMainTask != null && magicMainTask.getTopNonFinishingActivity() != null ){                       magicMainTask.getTopNonFinishingActivity().pauseActivityLockedOnly(true);                   }               }               // fde end

ActivityRecord执行最小功能的PauseActivity

  void pauseActivityLockedOnly(boolean preserveWindow) {       try {           final ActivityLifecycleItem lifecycleItem = PauseActivityItem.obtain();           final ClientTransaction transaction = ClientTransaction.obtain(app.getThread(), appToken);           transaction.setLifecycleStateRequest(lifecycleItem);           mAtmService.getLifecycleManager().scheduleTransaction(transaction);           // Note: don't need to call pauseIfSleepingLocked() here, because the caller will only           // request resume if this activity is currently resumed, which implies we aren't           // sleeping.           removePauseTimeout();           setState(PAUSED, "relaunchActivityLockedOnly");       } catch (RemoteException e) {           if (DEBUG_SWITCH || DEBUG_STATES) Slog.i(TAG_SWITCH, "relaunchActivityLockedOnly failed", e);       }   }
patch

https://github.com/openfde/lineageos_android_frameworks_base/commit/88bd3138fe60a1e97fb532cc558cb3e2ab92365f

https://github.com/openfde/lineageos_android_frameworks_base/commit/f78e32913fe758f1452336d2df87d823bf6d4509

https://github.com/openfde/lineageos_android_frameworks_base/commit/0b803e7f61cb2326b4cbde6ab0624536c749f4a8

https://github.com/openfde/lineageos_android_frameworks_base/commit/cf6a732c13e471b9382cda7c1313ed7af2cec6de

Add label

Related content