安卓aosp 16新特性分屏收藏保存功能AppPairIcon剖析

背景:

前面一篇blog
仿oppo三星samsung手机分屏收藏代码实现

有给大家展示分屏收藏功能相关的实现详细步骤等,最后也实行了这种分屏收藏demo,这些都是自己在aosp14版本实现的。

不过在文章发布后,vip学员朋友反馈发现在aosp16版本上,安卓已经自带实现了这个分屏收藏的功能,所以本文将带大家以前来探索google官方是如何实现这个分屏收藏功能,对这块功能的源码进行相关的剖析。

功能展示:

先打开2个task,而且操作进入分屏,在分屏的画面下点击最近任务进入分屏,可以看到分屏的task卡片。

在安卓16版本上在分屏情况下,再进入最近任务,可以看到这时候有分屏的两个task的卡片,点击双图标
点击后会出现如下弹框,发现有一个Save app pair

点击这个"Save app pair"按钮,其实字面意思就可以理解这个按钮就是保存这对app的task卡片图标,点击后桌面出现多了一个刚才分屏的两个app图标的app:

点击后直接可以进入分屏。

源码剖析:

创建AppPairIcon部分剖析

以"Save app pair"按钮为切入点分析

packages/apps/Launcher3/quickstep/src/com/android/quickstep/TaskShortcutFactory.java

cpp 复制代码
        @Override
        public void onClick(View view) {
            dismissTaskMenuView();
            //核心就是调用了AppPairsController的saveAppPair方法
            ((RecentsView) mTarget.getOverviewPanel())
                    .getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
        }

下面重点看看AppPairsController的saveAppPair方法部分

saveAppPair方法剖析

cpp 复制代码
   /**
     * Creates a new app pair ItemInfo and adds it to the workspace.
     * <br>
     * We create WorkspaceItemInfos to save onto the app pair in the following way:
     * <br> 1. We verify that the ComponentKey from our Recents tile corresponds to a real
     * launchable app in the app store.
     * <br> 2. If it doesn't, we search for the underlying launchable app via package name, and use
     * that instead.
     * <br> 3. If that fails, we re-use the existing WorkspaceItemInfo by cloning it and replacing
     * its intent with one from PackageManager.
     * <br> 4. If everything fails, we just use the WorkspaceItemInfo as is, with its existing
     * intent. This is not preferred, but will still work in most cases (notably it will not work
     * well on trampoline apps).
     */
    public void saveAppPair(GroupedTaskView gtv) {
      //参数类型GroupedTaskView中包含了分屏两个Task的相关信息


      //通过GroupedTaskView获取TaskViewItemInfo和WorkspaceItemInfo信息
        List<TaskViewItemInfo> recentsInfos =
                gtv.getTaskContainers().stream().map(TaskContainer::getItemInfo).toList();
                //注意这里resolveAppPairWorkspaceInfo是一个匹配方法,核心就是匹配componentName
        List<WorkspaceItemInfo> apps =
                recentsInfos.stream().map(this::resolveAppPairWorkspaceInfo).toList();

        for (int i = 0; i < apps.size(); i++) {
            apps.get(i).rank = encodeRank(getIndex(i), snapPosition);
        }
        //根据两个task对应的WorkspaceItemInfo信息创建AppPairInfo对象
        AppPairInfo newAppPair = new AppPairInfo(apps);

        IconCache iconCache = LauncherAppState.getInstance(mContext.asContext()).getIconCache();
        MODEL_EXECUTOR.execute(() -> {
        //根据newAppPair创建对应的桌面分屏两个小图标
            newAppPair.getAppContents().forEach(member -> {
                member.title = "";
                member.bitmap = iconCache.getDefaultIcon(newAppPair.user);
                iconCache.getTitleAndIcon(member, member.getMatchingLookupFlag());
            });
            MAIN_EXECUTOR.execute(() -> {
                LauncherAccessibilityDelegate delegate = QuickstepLauncher.getLauncher(
                        mContext.asContext()).getAccessibilityDelegate();
                if (delegate != null) {
                //调用addToWorkspace方法添加newAppPair信息到桌面
                    delegate.addToWorkspace(newAppPair, true, (success) -> {
        
                    });
            
                }
            });
        });
    }

这saveAppPair方法里面核心步骤有以下几个:

1、参数GroupedTaskView转换成TaskViewItemInfo信息

2、TaskViewItemInfo信息转换WorkspaceItemInfo信息

3、使用WorkspaceItemInfo构造AppPairInfo信息

4、根据AppPairInfo创建对应的桌面图标Icon

5、调用addToWorkspace把这个AppPairInfo对应的Icon添加到桌面UI,及保存相关数据信息

addToWorkspace这块就不展开了,熟悉桌面同学都懂这个。

点击AppPairIcon部分剖析

packages/apps/Launcher3/src/com/android/launcher3/touch/ItemClickHandler.java

cpp 复制代码
  private static void onClick(View v) {
        Object tag = v.getTag();
        if (tag instanceof WorkspaceItemInfo) {
            onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
        } else if (tag instanceof FolderInfo) {
            onClickFolderIcon(v);
        } else if (tag instanceof AppPairInfo) {//针对AppPairInfo类型的桌面图标调用onClickAppPairIcon方法
            onClickAppPairIcon(v);
        } 

看看onClickAppPairIcon方法:

cpp 复制代码
    private static void onClickAppPairIcon(View v) {
 
        // Either the app pair is not disabled, or it is a disabled state that can be handled by
        // framework directly (e.g. one app is paused), so go ahead and launch.
        launcher.launchAppPair(icon);//核心就是调用launchAppPair
    }

这里再来看看 launcher.launchAppPair方法,在QuickstepLauncher中:

packages/apps/Launcher3/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java

cpp 复制代码
/**
 * Launches two apps as an app pair.
 */
public void launchAppPair(AppPairIcon appPairIcon) {
    // Potentially show the Taskbar education once the app pair launch finishes
    //可以看到最后调用到了SplitSelectStateController的launchAppPair方法
    mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon,
            CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE,
            (success) -> {
                if (success && mTaskbarUIController != null) {
                    mTaskbarUIController.showEduOnAppLaunch();
                }
            });
}

下面看看launchAppPair具体实现

packages/apps/Launcher3/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java

cpp 复制代码
    /**
     * Launches an app pair by searching the RecentsModel for running instances of each app, and
     * staging either those running instances or launching the apps as new Intents.
     *
     * @param cuj Should be an integer from {@link Cuj} or -1 if no CUJ needs to be logged for jank
     *            monitoring
     * @param callback Called after the app pair launch finishes animating, or null if no method is
     *                 to be called
     */
    public void launchAppPair(AppPairIcon appPairIcon, int cuj,
            @Nullable Consumer<Boolean> callback) {
   
//核心调用findLastActiveTasksAndRunCallback来寻找当前活跃的task
        mSplitSelectStateController.findLastActiveTasksAndRunCallback(
                Arrays.asList(app1Key, app2Key),
                false /* findExactPairMatch */,
                foundTasks -> {
                //如果寻找到对应的活跃task,则被放到foundTasks集合中
                    @Nullable Task foundTask1 = foundTasks[0];//访问集合中的task
                    Intent task1Intent;
                    int task1Id;
                    if (foundTask1 != null) {//如果找到task,则使用taks既可以
                        task1Id = foundTask1.key.id;
                        task1Intent = null;
                    } else {//如果没找到task,那么task1Id就为INVALID_TASK_ID
                        task1Id = INVALID_TASK_ID;
                        task1Intent = app1.intent;
                    }

								//调用setInitialTaskSelect接口设置第一个要启动task相关变量
                    mSplitSelectStateController.setInitialTaskSelect(task1Intent,
                            AppPairsController.convertRankToStagePosition(app1.rank),
                            app1,
                            LAUNCHER_APP_PAIR_LAUNCH,
                            task1Id);
						
                    @Nullable Task foundTask2 = foundTasks[1];
                    //下面也是判断task2的情况,来设置secondTask的情况
                    if (foundTask2 != null) {
                        mSplitSelectStateController.setSecondTask(foundTask2, app2);
                    } else {
                        mSplitSelectStateController.setSecondTask(
                                app2.intent, app2.user, app2);
                    }

                    mSplitSelectStateController.setLaunchingIconView(appPairIcon);
							
                    //最后调用launchSplitTasks进行两个task的启动
                    mSplitSelectStateController.launchSplitTasks(
                            AppPairsController.convertRankToSnapPosition(app1.rank), callback);
                }
        );
    }

再看看最核心launchSplitTasks方法

cpp 复制代码
public void launchTasks(@Nullable Consumer<Boolean> callback, boolean freezeTaskList,
            @PersistentSnapPosition int snapPosition, @Nullable InstanceId shellInstanceId) {
 

        SplitSelectDataHolder.SplitLaunchData launchData =
                mSplitSelectDataHolder.getSplitLaunchData();
        int firstTaskId = launchData.getInitialTaskId();
        int secondTaskId = launchData.getSecondTaskId();
        ShortcutInfo firstShortcut = launchData.getInitialShortcut();
        ShortcutInfo secondShortcut = launchData.getSecondShortcut();
        PendingIntent firstPI = launchData.getInitialPendingIntent();
        PendingIntent secondPI = launchData.getSecondPendingIntent();
        Intent widgetIntent = launchData.getWidgetSecondIntent();
        int firstUserId = launchData.getInitialUserId();
        int secondUserId = launchData.getSecondUserId();
        int initialStagePosition = launchData.getInitialStagePosition();

			//根据上面的数据情况,识别不同的LaunchType然后调用不同的start接口
        switch (launchData.getSplitLaunchType()) {
            case SPLIT_TASK_TASK ->
            //2个都有taskId类型情况
                    mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
                            null /* options2 */, initialStagePosition, snapPosition,
                            remoteTransition, shellInstanceId);

            case SPLIT_TASK_PENDINGINTENT ->
                    mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle,
                            firstTaskId, extrasBundle, initialStagePosition, snapPosition,
                            remoteTransition, shellInstanceId);

            case SPLIT_TASK_SHORTCUT ->
                    mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle,
                            firstTaskId, null /*options2*/, initialStagePosition, snapPosition,
                            remoteTransition, shellInstanceId);

            case SPLIT_PENDINGINTENT_TASK ->
                    mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle,
                            secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
                            remoteTransition, shellInstanceId);

            case SPLIT_PENDINGINTENT_PENDINGINTENT ->
            
            //2个都没有taskId类型情况
                    mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut,
                            optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle,
                            initialStagePosition, snapPosition, remoteTransition,
                            shellInstanceId);

            case SPLIT_SHORTCUT_TASK ->
                    mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle,
                            secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
                            remoteTransition, shellInstanceId);
        }

    }

原文地址:
https://mp.weixin.qq.com/s/Ao7qN6uxxWjo1FLB6FZ9yA

更多framework实战开发干货,请关注"千里马学框架"

相关推荐
码农水水7 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
清蒸鳜鱼7 小时前
【Mobile Agent——Droidrun】MacOS+Android配置、使用指南
android·macos·mobileagent
2501_915918417 小时前
HTTPS 代理失效,启用双向认证(mTLS)的 iOS 应用网络怎么抓包调试
android·网络·ios·小程序·https·uni-app·iphone
峥嵘life8 小时前
Android EDLA CTS、GTS等各项测试命令汇总
android·学习·elasticsearch
Cobboo8 小时前
i单词上架鸿蒙应用市场之路:一次从 Android 到 HarmonyOS 的完整实战
android·华为·harmonyos
天下·第二8 小时前
达梦数据库适配
android·数据库·adb
定偶8 小时前
MySQL知识点
android·数据结构·数据库·mysql
iwanghang8 小时前
Android Studio 2023.2.1 新建项目 不能选择Java 解决方法
android·ide·android studio
似霰8 小时前
JNI 编程指南10——从内存角度看引用类型
android·jni
无限码力8 小时前
华为OD技术面真题 - JAVA开发 - 5
java·华为od·面试·华为od技术面真题·华为od技术面八股·华为od技术面java八股文