Android Automotive OS 中 TaskView 深度解析
Android Automotive OS 中 TaskView 深度解析
TaskView 的架构与核心组件关系
TaskView 简介: TaskView 是 Android Automotive OS (AAOS) 中用于在一个应用内嵌显示另一个应用 Activity 界面的特殊容器视图。它最早在 Android 12 引入,用于取代已弃用的 ActivityView。TaskView 本质上继承自 SurfaceView
,实现了 WindowManager Shell 提供的 ShellTaskOrganizer.TaskListener
接口。这意味着 TaskView 拥有一个独立的 Surface 用于渲染嵌入的应用界面,并通过 TaskListener 接口与系统窗口管理器 (WindowManager) 通信。
与 WindowManager 和 TaskOrganizer 的关系: WindowManager 通常管理应用窗口和任务栈。TaskView 并非通过传统的多窗口机制直接由 WindowManager管理,而是借助 TaskOrganizer 接口将对特定任务的控制权委托给客户端(如 System UI)。具体而言,AAOS 提供了 ShellTaskOrganizer(位于 com.android.wm.shell
),它扩展自系统的 TaskOrganizer,用于统一管理各类窗口模式下的任务。当 TaskView 希望嵌入并启动一个 Activity 时,会通过 ShellTaskOrganizer 与系统的 ActivityTaskManager 服务通信,注册自己为特定任务的组织者。这使得 TaskView 可以拦截和控制该任务的创建、布局和销毁等事件。
与 SurfaceControl 的关系: 由于 TaskView 继承自 SurfaceView,其背后由 SurfaceControl 驱动用于图像合成。ShellTaskOrganizer 在任务创建后会提供一个该任务窗口的 SurfaceControl(通常称为 “leash”),TaskView 通过 TaskListener 接口拿到这个 SurfaceControl,并将嵌入的任务窗口重新附着到自身的 Surface 上。正是 TaskView 在任务出现时执行图面绑定的代码:当新的嵌入任务出现时,TaskView 会调用 mTransaction.reparent(mTaskLeash, getSurfaceControl())
将任务窗口的 SurfaceControl 重新设定父节点为当前 TaskView 的 Surface,并调用 .show()
显示之。通过这种方式,嵌入 Activity 的界面被合成为 TaskView 所在应用窗口的一部分,从而实现在当前应用界面中嵌入显示其他应用的内容。
架构总结: TaskView 架构可以理解为应用内的迷你窗口:主应用(如车机主页)持有 TaskView(SurfaceView),TaskView 通过 ShellTaskOrganizer 注册成为系统任务的监听者。当主应用请求启动某个目标应用的 Activity 时,系统在多窗口模式下创建该 Activity 的任务,由 ShellTaskOrganizer 捕获任务创建事件并通知对应的 TaskView。TaskView 随即获取任务的 SurfaceControl,将其嵌入自身,实现图像呈现。同时,TaskView 还负责管理该嵌入任务的生命周期事件和输入焦点,从而与 WindowManager 紧密配合,实现车载系统中分屏嵌入显示的功能。
渲染流程:任务创建、Surface 渲染与 Z 序管理
任务启动与挂载流程
启动嵌入 Activity: 主应用(例如车机的 Launcher)在需要嵌入显示某个应用时,会调用 TaskView 提供的 startActivity(...)
接口来启动目标 Activity。通常这通过传入一个 PendingIntent 或 IntentShortcut 来指向目标应用的 Activity。TaskView 在收到启动请求时,会为该 Activity 构造适当的启动参数 (ActivityOptions
) 来确保它以嵌入模式启动。其中关键的一步是设置 LaunchCookie 和窗口模式:
TaskView 调用
ActivityOptions.setLaunchCookie(cookie)
绑定一个新创建的 Binder 作为 cookie;同时调用options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
强制该 Activity 以多窗口模式启动。还会设置setRemoveWithTaskOrganizer(true)
,使该任务和 TaskOrganizer 绑定,方便稍后统一管理。TaskView 通过 ShellTaskOrganizer 提供的setPendingLaunchCookieListener(cookie, listener)
将此 cookie 与自身的 TaskListener 关联起来。这样,cookie 就成为 TaskView 和目标任务之间匹配的令牌。随后,TaskView 使用带有这些参数的 PendingIntent 发送启动请求给系统。ActivityTaskManager 收到请求后,创建目标 Activity 的任务,并检测到其中包含的 LaunchCookie 和多窗口标志。由于 ShellTaskOrganizer 之前已向系统注册为多窗口模式任务的组织者,系统会在任务创建时将事件通知给 ShellTaskOrganizer。
ShellTaskOrganizer 捕获任务并通知 TaskView: 当新的任务启动后,系统通过 binder 回调触发 ShellTaskOrganizer 的 onTaskAppeared()
方法。ShellTaskOrganizer 查询新任务的 RunningTaskInfo.launchCookies
列表,从中找到与之前注册的 cookie 相匹配的项,以定位对应的 TaskListener。匹配成功后,ShellTaskOrganizer 将该 TaskListener(即 TaskView 自身)与任务绑定,并调用 TaskView 实现的 onTaskAppeared()
回调。这一机制保证了精准匹配:只有先前由 TaskView 发起且附带特定 cookie 的任务,才会通知对应的 TaskView 实例处理。
Surface 渲染与重绑定: TaskView 的 onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash)
内部完成了关键的 SurfaceControl 重挂载逻辑。一旦 TaskView 确认自己的 Surface 已创建(即 TaskView 已附着到窗口并有有效的 Surface),它会立即执行:
1 | mTransaction.reparent(mTaskLeash, getSurfaceControl()) |
其中,mTaskLeash
是系统传递的目标任务窗口的 SurfaceControl,“getSurfaceControl()
” 则是 TaskView 自身 Surface 的控制句柄。通过 reparent
操作,目标 Activity 的窗口表面被移到了 TaskView 的 SurfaceControl 之下,成为其子表面。这一步相当于将目标应用界面嵌入到了当前应用的窗口中。接着 .show()
确保该任务的界面可见,.apply()
则提交事务。若 TaskView 的 Surface 当时尚未准备好(例如 TaskView 还未附加到界面就收到了任务出现的通知),TaskView 会暂时调用 updateTaskVisibility()
将任务标记为隐藏,以避免内容渲染在不可见的表面上。等到 TaskView 的 Surface 创建回调触发时(即宿主界面已经显示),TaskView 会再次执行 reparent
+show
来呈现嵌入内容。这种先隐藏后绑定的策略保证了渲染流程的稳定,不会出现画面闪烁或任务内容一闪而过的问题。
Z-order(Z序) 管理: 在执行 SurfaceControl 重绑定后,嵌入Activity的界面变成了宿主窗口的一部分,其 Z序关系由宿主应用布局中的 TaskView 决定。通常情况下,SurfaceView 默认在宿主应用的 UI 界面层之下绘制,但由于 TaskView 可以与宿主的其他视图共同存在于布局中,我们可以通过调整视图顺序或使用 setZOrderMediaOverlay()
等方法控制嵌入内容相对于其它UI的叠放次序。例如,在车机主页中同时嵌入导航和音乐两块内容时,通常将它们布置为互不重叠的相邻区域,以避免Z序冲突。如果需要在 TaskView 上覆盖一些宿主的前景控件(例如提示图标或遮罩),可以将这些控件放在布局中 TaskView 之后声明,从而位于更高层。如果 TaskView 之间发生重叠,后添加的 TaskView 可能在视图层次上位于前面,从而其内容盖住先前的 TaskView 内容。总体而言,TaskView 的Surface作为宿主窗口子层,遵循宿主应用的视图层级来确定叠放次序,确保系统UI(如状态栏)仍然可以悬于其上,而嵌入内容彼此之间的顺序受宿主布局控制。
大小调整与绘制更新路径
任务窗口大小绑定: TaskView 的尺寸和位置直接决定了嵌入Activity窗口的大小和位置。TaskView 初始化时会将启动参数中的 launchBounds
设为自身区域大小,这样嵌入的Activity初始就知道应绘制在特定的矩形范围内。随后,在 TaskView 布局发生变化时(例如屏幕旋转、分屏区域拖动等),TaskView 会通过调用 onLocationChanged()
主动通知系统更新嵌入任务的窗口位置和大小。onLocationChanged()
内部使用 WindowContainerTransaction 构造一个事务,将新的边界设置给嵌入任务对应的 WindowContainer(通过其 token)。随后将该事务加入 ShellTaskOrganizer 的同步队列执行。例如,当 TaskView 在屏幕上的位置或大小变化时:
1 | getBoundsOnScreen(mTmpRect); |
上述代码通过 setBounds
方法将 TaskView 在屏幕上的新坐标和尺寸应用到嵌入任务。系统随后会向嵌入的 Activity 发送配置更改事件(如大小变化),嵌入应用即可据此重新布局其界面元素,以适配 TaskView 提供的新空间。整个过程类似于常规多窗口模式下Activity收到窗口大小变化的处理。
绘制与更新路径: 在完成 SurfaceControl 绑定并设定任务窗口大小后,嵌入的 Activity 进入正常的绘制流程。嵌入Activity在它自己的进程中运行,渲染结果通过SurfaceFlinger合成时,会使用TaskView提供的SurfaceControl进行合成。因此,嵌入界面的任何UI更新(动画、内容刷新)都会实时反映到 TaskView 区域内,就像一个独立窗口那样。TaskView 并不干涉具体的绘制逻辑,但保证了Surface的有效承载。当宿主界面不可见或TaskView被销毁时,TaskView 会在内部释放资源:调用 TaskView.release()
方法将自身从 ShellTaskOrganizer 注销,并移除相应的 TaskListener。由于在启动时设置了 options.setRemoveWithTaskOrganizer(true)
,系统会在 TaskOrganizer 移除监听后将嵌入的任务一并清理(通常是销毁该任务或将其隐藏)。这样可以防止“孤儿”任务在宿主不存在时继续占用资源。整个渲染更新路径确保了嵌入内容的尺寸同步和生命周期同步:宿主调整布局会通知嵌入任务调整,宿主销毁也会令嵌入任务随之销毁,从而维护系统窗口状态的一致性。
事件处理机制:触摸、焦点等如何路由
触摸和点击事件路由
在将其他应用的UI嵌入宿主界面后,输入事件需要正确路由给嵌入的 Activity,而非误交由宿主处理。Android通过调整触摸区域的可触控性和必要的事件转发来实现这一点。
Touchable 区域调整: TaskView 实现了 ViewTreeObserver.OnComputeInternalInsetsListener
接口,用于自定义窗口的可触摸区域 (Touchable Insets)。当宿主窗口进行命中测试(HitTest)以确定点按事件该由谁处理时,TaskView 可以利用此机制调整哪些区域由宿主接收触摸,哪些区域留给嵌入内容处理。是 TaskView 内部 onComputeInternalInsets()
实现的相关片段:
1 | // 将整个根窗口区域设为可触摸 |
上述逻辑含义如下:首先暂定整个宿主窗口都可触摸,然后减去 TaskView 占据的矩形区域。结果是TaskView覆盖的区域将不再由宿主窗口处理触摸。换言之,TaskView 告诉系统:“这块区域我不参与处理”。接下来系统会将这部分区域的事件转交给其它可能的窗口处理。在本场景下,其它窗口即对应嵌入Activity所在的任务窗口。
事件转发与注入: 由于嵌入的 Activity 虽然视觉上在宿主窗口内,但在系统输入管理上依然是独立的窗口/任务,系统需要将该区域的输入事件发送到嵌入任务的输入通道。得益于之前 TaskView 启动任务时采用了多窗口模式并设定了具体的任务边界,WindowManager 已经知道在宿主屏幕上某矩形区域属于嵌入的任务窗口。因此,当用户触摸 TaskView 区域时,事件会根据坐标匹配到嵌入任务的区域,从而由系统直接派发给嵌入的 Activity。TaskView 通过 InternalInsetsListener 排除了自身对该区域事件的干扰,相当于为嵌入窗口让出了输入焦点。
需要注意,在某些Android版本中,TaskView 的输入路由能力经历了改进。早期实现中,宿主应用可能需要拥有 INJECT_EVENTS 权限并通过 InputManager 将触摸事件人工注入嵌入任务;但在 Android Automotive 13 QPR3 中,平台已经提供直接将 InputEvent 注入 TaskView 宿主Activity的能力。这意味着系统更好地支持了 TaskView 输入事件的自动转发。因此,在现代 AAOS 中,只要宿主正确设置了触摸区域和任务边界,大部分触摸事件路由是自动完成的,无需额外的人工干预。
遮挡区域处理: TaskView 还提供 setObscuredTouchRect(Rect)
方法来设置“遮挡区域”。例如宿主应用可能在 TaskView 上方浮动一些控件(如半透明覆盖层)。TaskView 在 onComputeInternalInsets
中会将标记为遮挡的区域重新并入宿主窗口的可触摸区域。这保证了重叠在 TaskView 之上的宿主UI控件仍能接收到点击,不会被误认为点在嵌入内容上。
焦点和按键事件处理
焦点管理: 在嵌入多窗口环境下,输入焦点需要在不同任务间动态切换。Android WindowManager 会根据最近的用户触摸自动将焦点赋予相应窗口。当用户点击 TaskView 区域时,系统将输入焦点切换到嵌入的任务,从而确保其能够接收后续的按键输入和文本输入(例如弹出软键盘输入内容)。对于嵌入的Activity而言,它和正常前台应用一样可以通过 onWindowFocusChanged
感知焦点变化。需要注意的是,车载环境通常没有物理键盘,但诸如方向盘按键、硬件返回键等仍需正确处理。TaskView 利用了 ShellTaskOrganizer 提供的接口来辅助焦点管理,例如调用 mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true)
在嵌入任务的根Activity上拦截返回键。这使得当用户在嵌入Activity界面按返回时,不会直接让该Activity退出,而是先通知 TaskView 的监听器。
返回键和导航事件: TaskView 定义了 TaskView.Listener.onBackPressedOnTaskRoot(int taskId)
回调,用于监听嵌入任务根Activity上的返回事件。一旦用户在嵌入界面按下返回,系统检测到该任务没有别的可返回页面(即处于任务栈根),且已通过 TaskOrganizer 注册了拦截,则不会立即销毁Activity,而是调用 TaskView 的 onBackPressedOnTaskRoot()
方法。宿主应用可以在此回调中决定如何处理:例如,可以选择将嵌入的任务最小化或移除,从而避免用户误操作直接退出重要应用(如导航)。也可以忽略该事件,防止嵌入的应用被关闭。通过这种机制,车载主机能够更好地掌控导航等核心应用的生命周期,提升驾驶过程中的体验和安全。
其它按键事件: 对于音量键、播放控制键等,由于车机系统通常将它们全局处理,不会限定于某个TaskView,因此不属于 TaskView 专门路由的范围。不过,如果存在多个显示屏或多用户场景,系统可能需要根据焦点将这些按键传递给前台的对应用户任务。一般情况下,当 TaskView 中的应用获得焦点后,它会像普通前台应用一样收到按键事件。举例来说,用户在嵌入的视频播放应用区域按下播放/暂停硬件键,该按键事件将路由给该媒体应用处理,因为它当前是焦点窗口(假设系统将媒体键分发给当前有音频焦点的应用)。
输入法支持: 当嵌入的 Activity 包含文本输入框时(例如导航应用的搜索栏),点击该输入框会触发 Android 输入法(IMF)聚焦。由于嵌入Activity已获得窗口焦点,系统的输入法服务会针对该应用开启,并在宿主屏幕上显示软键盘。TaskView 本身不参与输入法渲染,但它的存在不妨碍输入法正常工作。开发者在编写嵌入的应用时需确保其 android:windowSoftInputMode
和可调整大小属性已正确配置,以兼容在较小窗口中弹出软键盘的场景。Medium 的一篇经验总结指出,嵌入Activity应在Manifest中声明 android:resizeableActivity="true"
以适应窗口化显示。
综上,TaskView 的事件处理机制通过划分触摸区域和托管焦点切换,确保用户对嵌入界面的操作能够直接作用于对应的应用,而不会干扰宿主应用本身。同时又赋予宿主一定控制权(如返回事件),满足车载场景对交互的特殊需求。
权限与系统限制:系统应用专属原因
仅限系统应用使用: TaskView 属于隐藏平台API,普通第三方应用无权使用。这一限制有多方面原因:首先,嵌入其它应用的UI涉及跨进程窗口管理和输入路由等敏感操作,为保证安全和稳定,Android要求调用方必须是受信任的系统应用或具有系统签名。同样地,在 Android 12 之后,ActivityView 被移除时谷歌已经明确指出,其替代方案 TaskView “理所当然地是隐藏的平台API”,不应由非系统应用使用。
必要权限和保护级别: 即使在系统应用中,要使用 TaskView 完整功能也需要声明和获取一系列特殊权限。例如:
**
android.permission.MANAGE_ACTIVITY_TASKS
**:管理任务和窗口的权限。TaskOrganizer 等接口在调用时通常需要持有此权限,否则系统AMS会拒绝操作。该权限在系统Manifest中保护级别为signature|privileged
,意味着只有系统映像内带有的应用才能获得授权。AAOS 的系统UI(包括车机Launcher)就在清单中声明了此权限。**
android.permission.INJECT_EVENTS
**:注入输入事件权限。用于将触摸和按键事件注入到其他窗口,一般也是signature级别。由于TaskView场景下可能需要程序化地将输入传递给嵌入任务(特别在早期实现里),系统应用必须持有此权限才能进行input注入。**
android.permission.REORDER_TASKS
**:允许重新排列系统中的任务顺序。虽然TaskView更多是嵌入,而非直接重排后台任务,但某些情况下宿主可能需要切换嵌入任务的Z序或调整其前后台状态,该权限也是系统级Launcher通常声明的。
此外,可能还有其他相关权限(如管理Activity栈、跨用户启动Activity等)依据具体实现需求而定。根据一篇实践经验,Host应用Manifest需要设置“注入事件、管理栈、重排任务以及嵌入活动”相关的权限才能正常使用 ActivityView/TaskView。换言之,TaskView 的使用门槛非常高,只有系统签名应用且在OEM系统镜像中部署,才能通过权限检查和隐藏API访问。
安全和稳定考量: 将一应用的界面嵌入另一应用,存在潜在的安全风险,例如恶意应用可以借此绘制假界面欺骗用户或窃取用户输入。因此Android严格限制只有系统应用(如车机的默认Launcher或系统UI)才能执行此操作。一来系统应用经过签名和权限审核,被认为是可信的,不会恶意利用TaskView。二来系统应用通常肩负特定任务(如车载导航+娱乐共屏显示),使用TaskView有正当性。而第三方应用若通过反射等方式强行使用,一方面违反SDK规范,另一方面极有可能在Android版本升级时失效甚至导致崩溃。Google Play商店也不允许上架使用隐藏API的应用。
权限限制示例: 当没有适当权限时,任何试图调用TaskOrganizer或嵌入他人Activity的行为都会被AMS拒绝并抛出安全异常。例如 ActivityTaskManagerService
中明确检查调用者UID,若非系统将报告“Permission Denial requires MANAGE_ACTIVITY_TASKS”错误。因此,OEM在实现AAOS Launcher或类似功能时,通常将其作为系统应用预装,并使用系统证书签名,以便授予上述权限并调用内部API。Android 12L 引入的 android.car.permission.TEMPLATE_RENDERER
类权限也是类似道理,限制只有系统车机模板主机可以渲染第三方App内容。
归结起来,TaskView 之所以“仅限系统应用”,是Android在能力开放与安全稳健间权衡的结果。对于车载这种人命攸关的场景,更需要确保只有经过OEM测试和授权的代码才能操作多任务窗口,以防范潜在的UI欺骗和系统不稳定因素。
源码与 API 流程解读
下面结合实际源码与调用流程,说明 TaskView 的关键类和方法调用顺序,以加深对其工作原理的理解。
1. 系统Launcher 初始化 TaskView: 以 AOSP 提供的车机主页应用 CarLauncher 为例:在其 onCreate
中会加载主页布局,其中有用于嵌入地图的占位视图容器(例如 R.id.maps_card
)。CarLauncher 检测到当前不是无头系统用户后,调用自定义的 setUpTaskView(mapsCard)
方法初始化嵌入地图的 TaskView:
1 | ViewGroup mapsCard = findViewById(R.id.maps_card); |
上面代码通过 TaskViewManager 创建了一个 TaskView(实际上是 CarTaskView,CarTaskView 继承自 TaskView 以做一些车机定制)。TaskView 创建后,设置了一个 Listener(用于监听TaskView生命周期事件,如初始化完成、任务创建/移除等),然后将 TaskView 添加到布局容器 mapsCard 中。此时 TaskView 尚未真正启动任何内容,但它已经是界面上的一个 SurfaceView,占好了渲染位置。
2. 启动嵌入的 Activity: CarLauncher 在适当的时机(例如主页启动后)会调用 TaskViewManager 去启动地图应用。在 AOSP 实现中,当Car服务通知地图功能可用时,会触发 CarLauncher.startMapsInTaskView()
,其中本质是调用 mTaskView.startActivity(...)
。startActivity可以接收一个 PendingIntent 或直接的 Intent 信息来启动目标Activity。CarLauncher 将构造指向谷歌地图(或OEM地图)的 Intent 封装成 PendingIntent 传给 TaskView。
TaskView 接收到 startActivity 调用后,会执行我们前述的 prepareActivityOptions 流程:生成 LaunchCookie、设置窗口模式和 bounds,然后通过 PendingIntent.send() 发出启动请求。值得一提的是,TaskView.startActivity 内部用 try-catch 捕获了所有异常并将其包装成 RuntimeException 抛出,这提醒开发者必须在系统应用中正确配置权限,否则这里可能因权限不够而失败抛异常。
3. 任务创建与 Cookie 匹配: 当 AMS 在后台创建地图Activity对应的任务时,ShellTaskOrganizer 立即收到 onTaskAppeared
通知。它通过 RunningTaskInfo 中的 launchCookies 找到之前 TaskView 注册的 cookie,进而识别出是 CarLauncher 发起的地图任务。展示了 ShellTaskOrganizer 在回调中查找 cookie 的逻辑:遍历任务的 cookies,发现有本地存储的 cookie 则取出对应的 TaskListener(TaskView),将该 cookie 从临时映射中移除并记录监听器和任务ID的对应关系。然后调用监听器的 onTaskAppeared()
。此时控制权回到 TaskView(CarTaskView)的实现上,它根据自己的 Surface 状态进行 SurfaceControl reparent 操作,将地图任务界面绑定到 mapsCard 下的新 SurfaceView 区域内显示。一旦 surfaceCreated 且内容显示,CarLauncher 的 TaskView.Listener 会收到 onTaskCreated(taskId, baseActivity)
回调,确认地图任务已成功嵌入。CarLauncher 可以在此时记录 taskId 或执行其他操作。如果任务启动失败(例如目标应用不存在或崩溃),TaskView.Listener 可能不会触发 onTaskCreated,而是随后收到 onReleased/onTaskRemovalStarted 等来表示任务未能保留显示。
4. 任务生命周期管理: CarLauncher 作为宿主,可以通过 TaskView 提供的一些方法管理嵌入任务的生命周期。例如,如果需要移除嵌入的Activity,可以调用 taskView.release()
来释放。这将注销监听器并通过 ShellTaskOrganizer 将对应任务从界面上移除(隐藏/销毁)。TaskView.Listener.onReleased() 也会被回调通知宿主。CarLauncher 中的实现就利用这一点来在自身生命周期结束时清理嵌入的任务,避免内存泄漏或后台任务白白占用资源。实际上,在 CarLauncher 的 onDestroy 中,一般会调用 TaskView.release() 确保嵌入的地图任务也一并关闭。
此外,考虑车机场景的特殊性,CarLauncher 可能实现额外的逻辑来增强稳定性。例如任务崩溃重启:如果嵌入的地图应用崩溃了,CarLauncher 可以监听到 TaskView.Listener.onTaskRemovalStarted,然后尝试重新启动地图任务(也许延迟几秒避免反复崩溃)。有源码显示,当主界面在后台且 TaskView 内的任务崩溃时,会记录状态并在主界面回到前台时重新启动该任务。这些细节保证了嵌入的关键应用(如导航)即使异常退出也能自动恢复,提高用户体验。
5. 多实例与 CarActivityManager: 在上述流程中,我们聚焦于单一 TaskView 的情况。CarLauncher 实际上可以管理多个 TaskView(比如导航卡片、音乐卡片各一个)。TaskViewManager 可创建多个 TaskView 实例,每个独立管理一个任务。ShellTaskOrganizer 通过不同 cookie 将不同任务通知路由给各自的 TaskView,因此多个嵌入任务不会混淆。在 AOSP 车机Launcher中,通常mapsCard用于导航,还有其它卡片容器用于通信、音乐等(可能有的直接用模板而非TaskView)。CarActivityManager 是Car API的一部分,它可以帮助在不同Display或用户上启动Activity。如果车机有副屏(例如后排娱乐屏或仪表盘),CarActivityManager 可以配合 TaskView 或直接启动Activity到特定显示。不过这些调用属于更高层次,与 TaskView 本身实现原理关联不大。
总的来说,阅读 AOSP 提供的 CarLauncher/TaskViewManager 等源码可以看到,TaskView 的引入使车机Launcher无需借助 VirtualDisplay,就能以控件方式嵌入独立应用的界面。通过 Listener 回调、cookie 匹配 和 ShellTaskOrganizer 协调等机制,TaskView 实现了较为复杂的跨进程UI组合,其代码流程清晰地展示了 Android 在framework层为满足汽车场景所做的扩展。
车载多窗口场景的使用模式和注意事项
导航与媒体分屏共显
在典型的车载信息娱乐系统中,导航和媒体是两个主要功能,往往希望同时显示以方便驾驶者。例如一侧持续显示地图导航信息,另一侧显示音乐播放信息或播放列表。在Android Automotive OS中,有多种方式实现此目的,而使用 TaskView 进行分屏嵌入是其中较灵活的一种方案。
使用模式: 车机Launcher可以预先在主页布局中划分区域,例如上半部分/左侧部分作为导航卡片,下半部分/右侧部分作为媒体卡片。导航卡片区域通过 TaskView 嵌入导航应用(如Google Maps或OEM地图)的实时Activity界面;媒体卡片则可能通过另一TaskView嵌入媒体应用界面(如Spotify自带UI)。这样,驾驶者能够在同一屏幕同时看到地图和音乐的信息,并直接在各自区域进行触控操作。
注意事项: 导航应用通常需要较大的显示区域和持续的更新能力,而媒体应用的交互(切歌、播放控制)相对简单。因此布局上常给予导航区域更多空间。嵌入导航Activity时,要确保该应用支持可调整大小(大多数导航应用在AAOS上都会声明 resizeableActivity)。同时,考虑到行车安全,系统可能限制某些交互:例如车辆行驶时,不允许用户在导航区域输入目的地(这可通过检测速度,在应用层或CarService限制)。媒体区域则可能显示专辑封面、曲目等,TaskView能够让它完全按照应用原生界面呈现。如果不使用 TaskView,另一方案是使用媒体模板(由系统渲染UI),但那样灵活性较低。因而OEM可根据需求选用TaskView直接嵌入,以获得应用完整的交互能力,例如第三方媒体应用自定义的浏览界面。
焦点管理: 当同时嵌入两个任务时,触摸哪个区域就将焦点赋予哪个任务。例如驾驶者点按地图拖动,会使导航TaskView获取焦点,媒体区域暂时失去焦点,但音乐仍可播放不会暂停;反之,若用户点按媒体列表滚动,地图应用虽然失去输入焦点但仍然可见继续更新路径。Android的多窗口模式支持并行Resume:即在多窗口下两个前台可见应用都处于Resumed状态,只是只有一个获得输入焦点。因此导航和媒体应用都能在前台运行,各自执行动画或媒体播放。不过在具体实现中,OEM通常会在界面上做一些引导,例如在非焦点区域可能淡化某些控件或在焦点切换时高亮当前操作区域,以提示用户。总之,TaskView 分屏方案需要UX上确保避免用户混淆:知道当前操作针对哪一侧应用。
性能考虑: 同屏运行两个应用会占用更多内存和CPU。AAOS设备通常具备车规级SoC,能支撑双任务运行。但导航应用往往资源耗用较高,因此系统可能对媒体应用采用低刷新策略(例如媒体应用界面在非焦点时降低更新频率)以减轻负载。此外,TaskView嵌入的应用仍受Android调度策略影响,比如当导航在持续运行且高优先级占用CPU,后台媒体应用UI线程可能被压缩。这就要求OEM在选择嵌入组合时要做性能评估,平衡流畅度。实践中,多数AAOS会把导航与媒体作为固定组合,不允许用户任意更换其它应用到该布局,以便优化和测试该场景的性能。
后排娱乐和多屏场景
现代汽车常配备副驾显示或后座娱乐屏。Android Automotive对多显示(Multi-Display)和多用户都有支持。TaskView 在多屏环境下同样适用,或者作为备选方案之一:
副驾屏幕 (Passenger Display): 副驾可能有独立的屏幕,运行自己的Launcher和应用。AAOS 14中提到支持在次级Home/副驾显示上启用 TaskView。这意味着副驾的界面也可以通过 TaskView 嵌入任务。例如,副驾Launcher上半部嵌入导航(与主驾显示相同或不同视角),下半部显示副驾自己选择的媒体/应用。由于副驾通常作为同一系统不同用户运行,为隔离主驾,TaskView 可能需要配合多用户实现:将TaskView启动的Activity指定运行在副驾用户上下文中。CarActivityManager可能提供类似接口。总的来说,TaskView 可以帮助同步或镜像某些任务到副驾屏:例如导航地图在主副驾同时显示,主驾控制导航,副驾只能查看地图放大缩小等。这类似一种任务镜像,AAOS 14 引入了“显示和任务镜像”功能。
后座娱乐: 后排娱乐多为独立屏幕,可以直接跑Android应用全屏。但如果存在超宽后排屏想同时给两名乘客显示不同内容,一个思路是仍在一个Android显示上做分屏,这种情况下TaskView也可用。不过更常见的做法是OEM采用多个Android显示(每个后排座一个)或直接分区渲染,无需TaskView这种复杂方案。TaskView更多用于在一个屏幕上整合多应用界面。后排场景下还涉及驾驶状态对内容播放的限制(行驶中视频不应在主驾可见范围播放等),这些可以通过TaskView所在应用检测车辆状态来控制嵌入内容的可见性或暂停状态。
仪表盘集成: 一些高端车把导航转向提示、车速等投射在仪表盘(另一个Android显示或其他系统)上。若仪表也是Android显示,可运行一个特殊的Launcher显示仪表界面。有实验性方案是利用 TaskView 将导航应用的某些Activity(简化视图)嵌入仪表盘区域。但实际产品中,仪表盘信息通常通过Vehicle API和模板渲染,而不会把整个地图投到仪表,因为那可能分散驾驶员注意。不过任务镜像可以在一定程度上把中控的导航画面同步到仪表(只显示指引箭头等)。AAOS的 DisplayArea 配置允许指定某应用在特定display上以不同模式呈现。这属于更底层的display管理,不直接由TaskView完成。但TaskView展现出的多窗口经验,已经为未来拓展到多显示提供了框架。
实践中的经验与限制
稳定性: OEM在使用 TaskView 做多窗口界面时,需要注意嵌入应用的稳定。若第三方应用发生ANR或崩溃,会在宿主界面留下空白或黑块,这对用户影响很大。因此系统应用应通过 TaskView.Listener 监控任务状态,一旦发现任务移除/崩溃,及时在UI上做出处理(比如显示一个默认卡片或重试按钮)。前文提到CarLauncher会自动重启崩溃的地图任务,就是一个示例。
兼容性: Android版本升级可能调整多窗口行为。开发者应尽可能通过稳定的Car API调用(如 CarLauncher
使用的 CarActivityManager)来处理,而避免直接使用反射调用内部类,否则OTA升级后TaskView实现变化会导致Launcher不兼容。谷歌在Android 13、14持续改进了TaskView,例如更好地支持输入注入和多用户场景。OEM需要跟进这些改动调整代码。
交互设计: 在分屏显示时,要防止用户迷惑。例如同时嵌入的两个应用若色彩风格差异大,界面割裂感强,影响体验。OEM通常会定制嵌入区域的样式,使之与整体UI协调。有时会在TaskView周围加边框、阴影以区分区域。另外在驾驶过程中,一次只允许操作一个区域,其它区域可能临时锁定(比如副驾操作不影响主驾)。这些都需要在系统UI层面配合实现,而非TaskView自身所能解决。TaskView提供的是技术可能性,具体策略由OEM根据法规和产品要求决定。
其他方案对比:ActivityEmbedding、PIP、原生多窗口
Android Automotive OS 实现多窗口界面,除了 TaskView 之外,还有一些替代或相关方案。下面对比它们在车载环境的适用性、稳定性和灵活性:
ActivityEmbedding (活动嵌套,Jetpack WindowManager)
简介: ActivityEmbedding 是 Android 12 引入的一项特性,主要面向大屏设备(如平板、可折叠屏),允许在同一任务内根据规则把一个应用的两个 Activity 并排显示。它通过 Jetpack WindowManager 库提供API,通常由应用开发者配置规则(比如Intent级别或宽度阈值)来自动拆分布局。
适用性: 在车载场景,ActivityEmbedding 局限在同一应用中嵌入不同Activity。如果希望同时显示来自不同应用的界面(如地图+媒体),ActivityEmbedding就无能为力。除非存在一个“宿主”应用同时包含了地图和媒体模块(这在实际中并不可行,因为导航和媒体通常由不同开发商应用提供)。因此ActivityEmbedding更适合单个应用的多面板 UI,而无法实现跨应用的分屏。
稳定性和权限: ActivityEmbedding 是公开API,普通第三方应用也可使用,不需要系统权限。这意味着其行为严格受平台限制,无法越界访问别的应用UI,因而安全性有保障。但也因为这一点,它无法满足车机跨应用多窗口的需求。对于OEM而言,ActivityEmbedding能用来丰富自家应用在大屏车机上的UI(比如设置界面左右两栏),却不能用来嵌入第三方导航/媒体应用。
灵活性: 相较 TaskView 可以任意组合应用,ActivityEmbedding 灵活性低:只能在同应用下操作,且布局拆分主要基于框架预定义规则,定制程度有限。近期在 Android 14/15 上,谷歌有探索跨应用embedding的新API(如Google I/O 2023提到“跨应用Activity嵌入(trusted embedding)”),可能允许可信应用间共享界面。但截至2025年,这方面仍在演进中,尚未大规模用于汽车。
总结: ActivityEmbedding在车载多窗口方面作用有限。它更像是平板上应用内多窗功能,在车机需要的是系统级跨应用多窗。因此我们更多将其视为TaskView的补充:当OEM自研应用希望内部多页面显示时,可用ActivityEmbedding;而涉及第三方应用,并排就只能依赖TaskView等系统方案。
Picture-in-Picture (画中画,PiP)
简介: PiP模式允许一个应用在小窗口中持续播放内容(典型例子是导航悬浮窗或视频小窗),用户同时可以操作其他应用。Android Auto移动端曾广泛使用PiP以在导航外给出路线提示。
在AAOS中的表现: Android Automotive也支持PiP模式。例如当用户主界面离开导航应用时,导航可以进入PiP在角落显示小地图。PiP不需要系统应用特权,第三方导航或视频App自行决定何时进入PiP。然而主动使用PiP实现分屏有局限:PiP窗口大小有限、交互受限(通常只能简单点击恢复全屏,不能在小窗中进行完整操作)。对于车机中想要等权重并排显示的场景,PiP显得太受限制。
稳定性: PiP 是Android框架级支持的功能,稳定性较高。但多个PiP同时存在的情况一般不被支持(手机上最多一个PiP)。车机上如果想同时小窗导航和小窗视频,系统未必允许。此外,PiP窗口在AAOS上可能会被System UI特殊管控,例如行车中不让视频PiP显示等,以符合法规。
灵活性: PiP窗口的位置通常可以拖动,但大小和布局由系统策略决定,不如TaskView能精确布局在UI某个区域。PiP更适合临时性的悬浮内容(例如倒车影像触发地图缩成小窗)。如果要长期并列两个应用界面,PiP无论在观感还是操作上都不如正式的分屏。并且PiP模式下嵌入的应用对宿主而言是不知道的,宿主无法控制其内容,只能由用户操作。
总结: PiP可以作为TaskView的一种补充使用场景:例如当用户想暂时离开导航界面去设置里调整某项,导航缩为PiP继续展示路口信息。但若要设计车机主界面同时常驻两个应用,PiP并非理想方案——它缺乏应有的交互深度和布局控制。TaskView 则提供了更正式和集成的多窗口能力。
原生多窗口 (Split-screen/Multi-window)
简介: Android自7.0开始提供原生分屏模式,允许用户在屏幕上并排打开两个应用。手机/平板上用户通过最近任务界面拖拽应用进入分屏。而Android 10以后又增加了自由窗口模式(Freeform),允许应用以浮窗形式存在。
AAOS上的支持: 在Android Automotive中,原生的多窗口功能通常受限或经过修改。因为车载不像手机给最终用户自由分屏——这可能带来复杂的交互和安全风险。很多AAOS的默认行为是单应用全屏(尤其在驾驶时强制),只有系统UI自行协调的特定场景下才出现多窗口(如Launcher卡片形式)。不过,底层而言AAOS还是具备多窗口能力。某些开发者选项可以开启freeform窗口调试。但对终端用户,厂商往往隐藏这些入口。
与 TaskView 对比: 原生分屏如果强行用于车机,灵活性反而较低。它一般将屏幕硬性一分为二(50:50 或自定义比例),用户界面由系统框架管理分割线。这与车机UI希望的定制布局(比如三分之一屏幕大小地图卡片)不符。此外,原生分屏要求应用支持性好,否则会出现界面拉伸或异常。而TaskView可以有针对性地嵌入特定应用,经测试优化其布局效果。TaskView 宿主应用可以完全自定义界面设计,符合品牌和交互需求,而不用依赖系统默认的分屏控件。
稳定性和安全: 让用户自行选择任意两个应用分屏,可能出现不适配的组合(例如两个应用都试图抢音频焦点、或者同时需要大量资源导致性能问题)。车机系统考虑行车安全,会限制用户分心操作。TaskView 模式下,OEM可以预先限定哪两个应用能同屏,界面怎样排列,从而提供经过验证的组合。而自由分屏潜在组合太多,不易控制。有些车厂在工程模式下解锁了通用分屏,但在正式版本中通常关闭,以免产生不可预测的问题。
多用户场景: 需注意在AAOS的多用户环境(如驾驶员与副驾不同用户)下,原生分屏无法跨用户。而 TaskView 则可以由一个系统进程(运行在系统用户上)来嵌入不同用户的任务,只要有合适权限。因此在多用户车机上,TaskView更有优势,可以充当桥梁把其他用户界面嵌入出来(当然这涉及安全隔离,需要严格权限控制)。
总结: 原生多窗口技术是整个TaskView机制的基础(TaskView本身也是利用WM的多窗口模式),但直接暴露给用户使用并不适合车载。TaskView可被视作对原生多窗口的封装定制,由系统应用替用户完成窗口布局管理,既实现多窗口效果又确保系统级协调和安全合规。
方案选择和灵活性比较
TaskView: 由系统应用控制的嵌入,多应用跨进程,多窗口。优点是高度定制、深度集成(可获取生命周期和事件),可确保关键应用并行运行。缺点是仅限OEM系统内实现,第三方无法直接利用,开发和维护成本较高(需跟进Android源码变动)。
ActivityEmbedding: 简单、安全,适合单应用多片段界面。对车机跨应用场景无直接帮助。
PiP: 实现容易(应用自行支持),但交互有限,用于辅助手段。车机上可用作暂时画中画,不宜长期作为主界面布局。
原生分屏: Android自带,但在车机上需要严格限制。用户体验不一定好,且不利于OEM打造一致的UI风格。灵活性上看似用户可以任意组合,但车机应当提供的是受控的灵活,因此很少有量产车让用户自由拖拽分屏的。
综合来看,TaskView 提供了在系统层融合应用界面的独特能力,几乎是为了汽车等特殊场景而生。在实际项目中,OEM往往采用 TaskView 来实现导航与其他应用的固定组合窗口,并辅以模板或控件实现 less critical 的信息展示(如天气、小部件)。ActivityEmbedding更多用在同一个App内部的UI布局优化上,PiP作为极端情况下的辅助(导航悬浮),而通用分屏通常被禁用或仅在工程模式下可用。
结语
Android Automotive OS 中的 TaskView 机制深入定制了 Android 的窗口管理,使得车载系统能够在一个屏幕上安全、高效地呈现多个应用界面。这一实现涉及 WindowManager、ShellTaskOrganizer、SurfaceControl 等框架组件协同工作:通过 LaunchCookie 精确匹配任务,通过 SurfaceControl 重组屏幕合成,通过 Insets 调整输入区域,以及通过权限机制保障只有系统应用才能驱动。TaskView 满足了车载场景对于分屏多任务的需求,赋予OEM灵活打造独特UI的能力。展望未来,随着 Android 平台对跨应用嵌入支持的演进,我们可能看到更标准化的方案出现。但在当前及可预见的版本中,TaskView 仍将是 AAOS 实现导航与媒体共显、前后排多屏互动等场景的核心利器,值得深入理解和掌握其原理以更好地应用于实际开发中。
参考资料:
- AOSP 源码 – TaskView.java(frameworks/base/libs/WindowManager/Shell)等
- AOSP 源码 – CarLauncher 与 TaskView 实现(packages/apps/Car/Launcher)
- Android 开源项目文档 – Android Automotive 13 QPR3 Release、Android Automotive 14 Release
- 技术博客 – Android Automotive Launcher TaskView 源码分析
- Medium 博客 – TaskView 隐藏API说明