实现多个 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