实现多个 Launcher 并存切换

发布时间 2023-12-07 14:09:56作者: 柒月下寻

实现这样一个功能,系统自带Launcher保留,用户可选择其他应用作为开机桌面(替代系统Launcher),并可以切回原来的桌面

一、建立一个作为桌面的app

首先看下Android 自带Launcner 的xml配置(Android13):
/packages/apps/Launcher3/AndroidManifest.xml

<!--
43         Main launcher activity. When extending only change the name, and keep all the
44         attributes and intent filters the same
45         -->
46         <activity
47             android:name="com.android.launcher3.Launcher"
48             android:launchMode="singleTask"
49             android:clearTaskOnLaunch="true"
50             android:stateNotNeeded="true"
51             android:windowSoftInputMode="adjustPan"
52             android:screenOrientation="unspecified"
53             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
54             android:resizeableActivity="true"
55             android:resumeWhilePausing="true"
56             android:taskAffinity=""
57             android:exported="true"
58             android:enabled="true">
59             <intent-filter>
60                 <action android:name="android.intent.action.MAIN" />
61                 <action android:name="android.intent.action.SHOW_WORK_APPS" />
62                 <category android:name="android.intent.category.HOME" />
63                 <category android:name="android.intent.category.DEFAULT" />
64                 <category android:name="android.intent.category.MONKEY"/>
65                 <category android:name="android.intent.category.LAUNCHER_APP" />
66             </intent-filter>
67            ···
70         </activity>

可以看到其中设置了几个与常规activity不常见的category,各自对应的的Intent中的几个属性,定义如下:

	// 作为开机启动界面
	public static final String CATEGORY_HOME = "android.intent.category.HOME";
	// monkey或其他测试工具 可执行
	public static final String CATEGORY_MONKEY = "android.intent.category.MONKEY";
	// 标记Home app (非必须)
	public static final String CATEGORY_LAUNCHER_APP = "android.intent.category.LAUNCHER_APP";

创建一个应用,一般作为桌面会用到很多系统权限,配置为系统应用
image

android:sharedUserId="android.uid.system"

然后给启动activity 做属性配置,直接使用Launcher的配置即可,也可做部分删减,打包安装到设备上,返回主页的时候,就会发现有提示 要选择哪一个作为桌面,添加一个应用作为桌面的功能到此完成。

二、跳过系统选择提示框,直接进入配置的桌面

大概的实现思路有几下几种:
1、全局拦截,类似与一般app中使用hook跳转登录界面的做法,当判断跳转系统桌面时,就进入自定义的界面
2、直接替换系统预装Launcher.apk,一般的目录system/priv-app/Launcher/Launcher.apk,这种方式需要获取设备权限,且替换之后需要重启等操作才能生效,可以作为备选
3、走系统自带流程,做默认选择应用动作 直接跳过弹框选择操作

这里采用的时第三种方案实现的

1、抓取 弹框相关信息

在弹出出现时,抓取当前窗口信息和activity信息,发现此时时处在 ResolverActivity中,对应抓取命令可使用

dumpsys window | grep mCurrentFocus  //抓取前台activity
dumpsys window windows  // 查看当前的窗口信息

找到对应的代码,做进一步分析
frameworks\base\core\java\com\android\internal\app\ResolverActivity.java

2、界面逻辑简单梳理

获取所有配置android.intent.category.HOME 的界面,根据各种判断决定时候弹出弹框给用户选择

3、处理方式

3.1 实现思路

在进入当前界面时,直接判断是否配置了需要的 launcher,然后做对应跳转,直接finish ResolverActivity

3.2 具体逻辑

找到ResolverActivityonCreate()方法如下

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Use a specialized prompt when we're handling the 'Home' app startActivity()
        Log.d(TAG, "onCreate: 11111111111111111111111111111");
        final Intent intent = makeMyIntent();
        final Set<String> categories = intent.getCategories();
        if (Intent.ACTION_MAIN.equals(intent.getAction())
                && categories != null
                && categories.size() == 1
                && categories.contains(Intent.CATEGORY_HOME)) {
            // Note: this field is not set to true in the compatibility version.
            mResolvingHome = true;
        }

        setSafeForwardingMode(true);

        onCreate(savedInstanceState, intent, null, 0, null, null, true);
    }

   /**
     * Compatibility version for other bundled services that use this overload without
     * a default title resource
     */
    @UnsupportedAppUsage
    protected void onCreate(Bundle savedInstanceState, Intent intent,
                            CharSequence title, Intent[] initialIntents,
                            List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
        Log.d(TAG, "onCreate: 2222222222222222222222222222");
        onCreate(savedInstanceState, intent, title, 0, initialIntents, rList,
                supportsAlwaysUseOption);
    }

    protected void onCreate(Bundle savedInstanceState, Intent intent,
                            CharSequence title, int defaultTitleRes, Intent[] initialIntents,
                            List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
        setTheme(appliedThemeResId());
        super.onCreate(savedInstanceState);
        Log.w(TAG, "--------------------  onCreate: 333333333333333333333333");
//-------- add code --------
        if (mResolvingHome) {
            if (setDefaultLauncher()) {
                finish();
                return;
            }
        }
//-------- add code ---------
        // Determine whether we should show that intent is forwarded
        // from managed profile to owner or other way around.
        setProfileSwitchMessage(intent.getContentUserHint());

        mLaunchedFromUid = getLaunchedFromUid();
        ···
    }

image

3.3 添加默认跳转逻辑

private boolean setDefaultLauncher() {
        try {
            String configLauncher = SystemProperties.get("persist.skg.home.pkg", null);
            // 未配置其他app,走默认的流程
            if (TextUtils.isEmpty(configLauncher)) {
                Log.i(TAG, "configLauncher is empty,use default");
                return false;
            }
            String[] packConfig = configLauncher.split("/");
            String defPackageName = packConfig[0];
            String defClassName = packConfig[1];

            Log.i(TAG, "Resolver configLauncher : PackageName = " + defPackageName + " ClassName = " + defClassName);

            final PackageManager pm = getPackageManager();
            try {
                PackageInfo packageInfo = pm.getPackageInfo(defPackageName, PackageManager.GET_GIDS);
                Log.i(TAG, "configLauncher: " + packageInfo);
                if (packageInfo == null) {
                    Log.e(TAG, "配置的launcher app 未安装");
                    return false;
                }
                //TODO 过滤掉三方应用也可放在此处,一般设备不允许非系统应用由此功能
            } catch (PackageManager.NameNotFoundException e) {
                Log.e(TAG, "默认Launcher应用未安装", e);
                return false;
            }

            //清除当前默认launcher
            ArrayList<IntentFilter> intentList = new ArrayList<IntentFilter>();
            ArrayList<ComponentName> cnList = new ArrayList<ComponentName>();
            pm.getPreferredActivities(intentList, cnList, null);
            IntentFilter dhIF = null;
            for (int i = 0; i < cnList.size(); i++) {
                dhIF = intentList.get(i);
                if (dhIF.hasAction(Intent.ACTION_MAIN) && dhIF.hasCategory(Intent.CATEGORY_HOME)) {
                    pm.clearPackagePreferredActivities(cnList.get(i).getPackageName());
                }
            }
            // 添加Launcher
            IntentFilter filter = new IntentFilter();
            filter.addAction("android.intent.action.MAIN");
            filter.addCategory("android.intent.category.HOME");
            filter.addCategory("android.intent.category.DEFAULT");

            Intent intent = new Intent(Intent.ACTION_MAIN);
            intent.addCategory(Intent.CATEGORY_HOME);
            List<ResolveInfo> list = new ArrayList<ResolveInfo>();
            list = pm.queryIntentActivities(intent, 0);
            final int N = list.size();
            ComponentName[] set = new ComponentName[N];
            int bestMatch = 0;
            for (int i = 0; i < N; i++) {
                ResolveInfo r = list.get(i);
                set[i] = new ComponentName(r.activityInfo.packageName, r.activityInfo.name);
                if (r.match > bestMatch) bestMatch = r.match;
            }
            ComponentName preActivity = new ComponentName(defPackageName, defClassName);
            pm.addPreferredActivity(filter, bestMatch, set, preActivity);

            return startConfigLauncher(defPackageName, defClassName);
        } catch (Exception e) {
            Log.e(TAG, "set default launchar fail", e);
            return false;
        }
    }
    private boolean startConfigLauncher(String pkg, String cls) {
        try {
            Intent intent1 = new Intent(Intent.ACTION_MAIN);
            intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent1.addCategory(Intent.CATEGORY_LAUNCHER);
            intent1.setComponent(new ComponentName(pkg, cls));
            startActivity(intent1);
            return true;
        } catch (Exception e) {
            Log.i(TAG, pkg + " start Fail");
            return false;
        }
    }

SystemProperties中的key 值可以在mk 文件中配置做预置

PRODUCT_PROPERTY_OVERRIDES += persist.skg.home.pkg=pkgName/classname

3.4 验证

1、修改对应的 SystemProperties 值后,重启直接进入配置的应用,跳过弹框询问
2、开机进入桌面,进入一个应用,此时修改SystemProperties,按Home 或者Intent跳转主页,跳转到配置的桌面应用,生效

// 跳转代码
Intent intent = new Intent();  //创建Intent对象
intent.setAction(intent.ACTION_MAIN);  //设置action动作属性
intent.addCategory(intent.CATEGORY_HOME); //设置categoty种类显示主屏幕
startActivity(intent); //启动Activity

3.5 编译

  • 只修改了ResolverActivity,就直接重新编译 framworks.jar,再替换设备中的,替换方式可查看 这里是替换方式
  • 与之属性到mk中。重新编包刷机

3.5 待改进项

在跳转进入 ResolverActivity 之前就做默认操作,少一个跳转开启activity 的动作,或者在启动Launcher 的其他环节做处理

参考:
https://zhuanlan.zhihu.com/p/623528604