实现多个 Launcher 并存切换
实现这样一个功能,系统自带Launcher保留,用户可选择其他应用作为开机桌面(替代系统Launcher),并可以切回原来的桌面
一、建立一个作为桌面的app
首先看下Android 自带Launcner 的xml配置(Android13):
/packages/apps/Launcher3/AndroidManifest.xml
<!-- Main launcher activity. When extending only change the name, and keep all the attributes and intent filters the same --> <activity android:name="com.android.launcher3.Launcher" android:launchMode="singleTask" android:clearTaskOnLaunch="true" android:stateNotNeeded="true" android:windowSoftInputMode="adjustPan" android:screenOrientation="unspecified" android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize" android:resizeableActivity="true" android:resumeWhilePausing="true" android:taskAffinity="" android:exported="true" android:enabled="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.SHOW_WORK_APPS" /> <category android:name="android.intent.category.HOME" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.MONKEY"/> <category android:name="android.intent.category.LAUNCHER_APP" /> </intent-filter> .... </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";
创建一个应用,一般作为桌面会用到很多系统权限,配置为系统应用
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 具体逻辑
找到ResolverActivity
的 onCreate()
方法如下
@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(); ··· }
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 文件中配置做预置,添加这两个方法后,在onCreate()中调用
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 的其他环节做处理
4、加入刷机升级流程
一般在刷完新的镜像后,第一次开机先进入到开机向导,在开机向导里面点击设置完成的时候 把上述 3.3
的操作放入放到对应的流程里面
本文来自博客园,作者:阿丟啊,转载请注明原文链接:https://www.cnblogs.com/qiyuexiaxun/p/17881866.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂