android launcher客制化——将自己的apk设定为开机启动项(home)
android launcher客制化——将自己的apk设定为开机启动项(home)
1. Description
- download mmi version ->should go into the mmitest interface
- mmitest,一款设备硬件测试apk,主要检查设备硬件好坏。
- mmi version,相对普通版本(cu版本),mmi版本需要在一开机就启动硬件测试apk mmitest,并且mmi version 因为主要编译后用来测试设备硬件好坏,相对cu版本,除了必要的service和部分模块,一些不必要的service&& 模块则不必安装。
- 在之前 mmi version的实现中,mmitest apk会在开机完成后接收开机广播
android.intent.action.BOOT_COMPLETED
来实现自启动逻辑。 - 但是实际上启动顺序是在launcher界面启动后才启动mmitest apk,中间会有一段时间的间隔
Expect:
- 如果是mmi version,将mmitest apk作为launcher启动
- 如果是普通版本(cu版本),保持原样(默认的laucher apk 作为开机启动项,mmitest apk作为一个三方apk并隐藏log)
2. Analysis
launcher的启动流程大致如下所示:
启动activity的入口是startHomeActivityLocked方法,但是在众多的 activity中如何选择,则是在chooseBestActivity方法中进行的。该方法:
- 首先进行判断,如果launcher apk只有一个就直接将它放回给startHomeActivityLocked方法,让它启动。
- 如果有多个launcher apk,从偏好设置中获取用户设置好的默认launcher,该偏好设置文件路径为
/data/system/users/0/package-restrictions.xml
,root后可以查看。 - 如果用户还没有设置偏好的activity,则启动ResolverActivity,该 activity会让用户进行偏好设置。
回到本问题,将mmitest apk客制为laucher主要有4种方法:
- 删除系统的launcher apk
只要将mmitest apk中的androidManifest.xml中的main screen添加标签<category android:name="android.intent.category.HOME" />
&&<category android:name="android.intent.category.DEFAULT" />
后,该apk在启动时就会作为launcher进行启动。但是因为系统中还有默认的launcher也有这2个标签,所以在设备开机后,系统会询问用户期望将哪个apk作为launcher,具体逻辑如下。
private ResolveInfo chooseBestActivity(Intent intent, String resolvedType,
int flags, List<ResolveInfo> query, int userId) {
if (query != null) {
final int N = query.size();
if (N == 1) {
return query.get(0);/*如果List<ResolveInfo>中只有一个,这不需要进行选择*/
} else if (N > 1) {
final boolean debug = ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0);
// If there is more than one activity with the same priority,
// then let the user decide between them.
ResolveInfo r0 = query.get(0);
ResolveInfo r1 = query.get(1);
if (DEBUG_INTENT_MATCHING || debug) {
Slog.v(TAG, r0.activityInfo.name + "=" + r0.priority + " vs "
+ r1.activityInfo.name + "=" + r1.priority);
}
// If the first activity has a higher priority, or a different
// default, then it is always desirable to pick it.
if (r0.priority != r1.priority
|| r0.preferredOrder != r1.preferredOrder
|| r0.isDefault != r1.isDefault) {
return query.get(0);
}
// If we have saved a preference for a preferred activity for
// this Intent, use that.
ResolveInfo ri = findPreferredActivity(intent, resolvedType,/* 此处去查询用户的偏好设置,如果在多个launcher apk的情况下,用户之前已经选定了某个apk 默认一直作为launcher,则直接返回该apk*/
flags, query, r0.priority, true, false, debug, userId);
if (ri != null) {
return ri;
}
// If we have an ephemeral app, use it
for (int i = 0; i < N; i++) {
ri = query.get(i);
if (ri.activityInfo.applicationInfo.isInstantApp()) {
final String packageName = ri.activityInfo.packageName;
final PackageSetting ps = mSettings.mPackages.get(packageName);
final long packedStatus = getDomainVerificationStatusLPr(ps, userId);
final int status = (int)(packedStatus >> 32);
if (status != INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK) {
return ri;
}
}
}
ri = new ResolveInfo(mResolveInfo);
ri.activityInfo = new ActivityInfo(ri.activityInfo);
ri.activityInfo.labelRes = ResolverActivity.getLabelRes(intent.getAction());
// If all of the options come from the same package, show the application's
// label and icon instead of the generic resolver's.
// Some calls like Intent.resolveActivityInfo query the ResolveInfo from here
// and then throw away the ResolveInfo itself, meaning that the caller loses
// the resolvePackageName. Therefore the activityInfo.labelRes above provides
// a fallback for this case; we only set the target package's resources on
// the ResolveInfo, not the ActivityInfo.
final String intentPackage = intent.getPackage();
if (!TextUtils.isEmpty(intentPackage) && allHavePackage(query, intentPackage)) {
final ApplicationInfo appi = query.get(0).activityInfo.applicationInfo;
ri.resolvePackageName = intentPackage;
if (userNeedsBadging(userId)) {
ri.noResourceId = true;
} else {
ri.icon = appi.icon;
}
ri.iconResourceId = appi.icon;
ri.labelRes = appi.labelRes;
}
ri.activityInfo.applicationInfo = new ApplicationInfo(
ri.activityInfo.applicationInfo);
if (userId != 0) {
ri.activityInfo.applicationInfo.uid = UserHandle.getUid(userId,
UserHandle.getAppId(ri.activityInfo.applicationInfo.uid));
}
// Make sure that the resolver is displayable in car mode
if (ri.activityInfo.metaData == null) ri.activityInfo.metaData = new Bundle();
ri.activityInfo.metaData.putBoolean(Intent.METADATA_DOCK_HOME, true);
return ri;/*如果前面的逻辑都没有确定出一个合适的 activity,则会返回com.android.internal.app.ResolverActivity,该activity会显示一个偏好设置界面,提供用户选择*/
}
}
return null;
}
因为需要将mmitest apk设置默认launcher,所以并不希望开机后出现这个选择框。简单的做法就是将系统中lancher apk去掉,这样开机后系统只有mmitest apk具有标签<category android:name="android.intent.category.HOME" />
,默认的就会将它作为lancher启动而不出现弹框。
不过这种做法还是不能解决当前的问题,如果是普通版本(cu版本),编译后很可能就没有launcher了,不符合 如果是普通版本(cu版本),保持原样的需求。 当然可以在mkaefile文件中做好判断,即如果当前编译的是cu版本,则将默认的laucher apk加入编译,否则laucher apk不参加编译。
而这种修改虽然可以保持cu版本保留默认的laucher apk但是还是回到了最开始的问题上了,启动后会系统会询问用户期望将哪个apk作为launcher。
- 修改intent-filter priority
参考博客Android framework 使用自定的activity取代默认的Launcher界面描述的,AMS在启动launcher时,会通过resolveActivityInfo方法向PMS查询具有CATEGORY标签的组件,当有多个组件都满足条件的情况下,会依据priority的值的大小来选择,取priority值最大的一个,当有多个组件priority相同的情况,会提示用户进行选择。
所以在修改mmitest apk的androidManifest.xml标签的同时,对文件PackageManagerService.java的adjustPriority方法修改,如果检测到ApplicationInfo的packageName是mmitest 并且当前版本为mmi version,手动设置mmitest 的main screen activity的intent-filter priority为一个较大的值,否则就设为一个较小的值。此方法验证,未生效
/**
* Adjusts the priority of the given intent filter according to policy.
* <p>
* <ul>
* <li>The priority for non privileged applications is capped to '0'</li>
* <li>The priority for protected actions on privileged applications is capped to '0'</li>
* <li>The priority for unbundled updates to privileged applications is capped to the
* priority defined on the system partition</li>
* </ul>
* <p>
* <em>NOTE:</em> There is one exception. For security reasons, the setup wizard is
* allowed to obtain any priority on any action.
*/
private void adjustPriority(
List<PackageParser.Activity> systemActivities, ActivityIntentInfo intent) {
// nothing to do; priority is fine as-is
if (intent.getPriority() <= 0) {
return;
}
final ActivityInfo activityInfo = intent.activity.info;
final ApplicationInfo applicationInfo = activityInfo.applicationInfo;
final boolean privilegedApp =
((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0);
if (!privilegedApp) {
// non-privileged applications can never define a priority >0
if (DEBUG_FILTERS) {
Slog.i(TAG, "Non-privileged app; cap priority to 0;"
+ " package: " + applicationInfo.packageName
+ " activity: " + intent.activity.className
+ " origPrio: " + intent.getPriority());
}
intent.setPriority(0);
return;
}
if (systemActivities == null) {
// the system package is not disabled; we're parsing the system partition
if (isProtectedAction(intent)) {
if (mDeferProtectedFilters) {
// We can't deal with these just yet. No component should ever obtain a
// >0 priority for a protected actions, with ONE exception -- the setup
// wizard. The setup wizard, however, cannot be known until we're able to
// query it for the category CATEGORY_SETUP_WIZARD. Which we can't do
// until all intent filters have been processed. Chicken, meet egg.
// Let the filter temporarily have a high priority and rectify the
// priorities after all system packages have been scanned.
mProtectedFilters.add(intent);
if (DEBUG_FILTERS) {
Slog.i(TAG, "Protected action; save for later;"
+ " package: " + applicationInfo.packageName
+ " activity: " + intent.activity.className
+ " origPrio: " + intent.getPriority());
}
return;
} else {
if (DEBUG_FILTERS && mSetupWizardPackage == null) {
Slog.i(TAG, "No setup wizard;"
+ " All protected intents capped to priority 0");
}
if (intent.activity.info.packageName.equals(mSetupWizardPackage)) {
if (DEBUG_FILTERS) {
Slog.i(TAG, "Found setup wizard;"
+ " allow priority " + intent.getPriority() + ";"
+ " package: " + intent.activity.info.packageName
+ " activity: " + intent.activity.className
+ " priority: " + intent.getPriority());
}
// setup wizard gets whatever it wants
return;
}
if (DEBUG_FILTERS) {
Slog.i(TAG, "Protected action; cap priority to 0;"
+ " package: " + intent.activity.info.packageName
+ " activity: " + intent.activity.className
+ " origPrio: " + intent.getPriority());
}
intent.setPriority(0);
return;
}
}
// privileged apps on the system image get whatever priority they request
return;
}
-
修改偏好设置
当系统有多个apk具有标签<category android:name="android.intent.category.HOME" />
&&<category android:name="android.intent.category.DEFAULT" />
时,系统会主动询问用户希望选择哪个apk作为launcher,即设置用户的偏好。
如果是mmi version,修改此处逻辑,将弹出弹框逻辑去掉并让系统默认选择指定apk作为launcher,则可以实现本问题的需求。分析偏好设置具体逻辑可参考android设置多个类似APP其中的一个为默认。而针对本问题,可以在chooseBestActivity方法在进行最佳 activity匹配时就将指定的 activity设置进偏好设置中去, 该操作的代码实现接口为addPreferredActivity(IntentFilter filter, int match,ComponentName[] set, ComponentName activity, int userId)
-
修改系统默认laucher启动标签
参考让你自己写的Android的Launcher成为系统中第一个启动的,也是唯一的Launcher做法,创建一个私有的filter选项,让它来作为系统lancher的过滤选项。然后将mmitest apk中的androidManifest.xml中的main screen添加该私有标签,这个方法从framework层修改了launcher的启动标签,而原本的<category android:name="android.intent.category.HOME"/>
将不会作为系统launcher标签,这种做法对系统修改不可谓不大,但是这个做法同样不适用本问题。
3. solution
具体修改如下:
- 修改文件
services/core/java/com/android/server/pm/PackageManagerService.java
private void setTargetActivityAsPreferredActivity(Intent intent,List<ResolveInfo> query, int userId){/*添加方法*/
final int N =query.size();
if(SystemProperties.getBoolean("/*版本标识*/", false)){
ComponentName[] set = new ComponentName[N];
ComponentName componentName = null;
int bestMatch = 0;
for(int i=0;i<N;i++){
ResolveInfo r = query.get(i);
set[i] = new ComponentName(r.activityInfo.packageName, r.activityInfo.name);
if (r.match > bestMatch) bestMatch = r.match;
if("/*目标包名*/".equals(r.activityInfo.packageName)){
componentName=set[i];
}
}
IntentFilter filter = new IntentFilter();
if (intent.getAction() != null) {
filter.addAction(intent.getAction());
}
Set<String> categories = intent.getCategories();
if (categories != null) {
for (String cat : categories) {
filter.addCategory(cat);
}
}
filter.addCategory(Intent.CATEGORY_DEFAULT);
if(null!=componentName){
addPreferredActivity(filter, bestMatch,set,componentName,userId);
}
}
}
private ResolveInfo chooseBestActivity(Intent intent, String resolvedType,int flags, List<ResolveInfo> query, int userId) {
if (query != null) {
final int N = query.size();
if (N == 1) {
return query.get(0);
} else if (N > 1) {
setTargetActivityAsPreferredActivity(intent,query,userId);/*此处调用新加的方法*/
final boolean debug = ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0);
// If there is more than one activity with the same priority,
// then let the user decide between them.
/*此处省略*/
}
- 修改目标apk
因为希望cu版本保持launcher apk作为系统launcher,而mmi版本则mmitest apk作为 launcher,那么在cu版本的mmitest apk中不能具有<category android:name="android.intent.category.HOME"/>
, 所有需要针对2个版本准备不同的AndroidManifest.xml,具体的可以在编译脚本makefile中配置如下:
ifeq ($(/*版本标识*/),true)
${shell cp ./AndroidManifest_mmi.xml ./AndroidManifest.xml}
else
${shell cp ./AndroidManifest_cu.xml ./AndroidManifest.xml}
endif
.PHONY: $(LOCAL_PATH)/AndroidManifest.xml
$(LOCAL_PATH)/AndroidManifest.xml:
LOCAL_MANIFEST_FILE := ./AndroidManifest.xml
然后准备2份AndroidManifest.xml,即AndroidManifest_mmi.xml和./AndroidManifest_cu.xml。而./AndroidManifest_cu.xml就是apk原本的AndroidManifest.xml,./AndroidManifest_mmi.xml在原本的AndroidManifest.xml基础上加入如下语句:
<activity
android:name="/*主activity*/"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.HOME"/>
+ <category android:name="android.intent.category.DEFAULT" />
<!-- <category android:name="android.intent.category.LAUNCHER" /> do not display icon in Luncher.apk-->
</intent-filter>
</activity>
4. summary
这个问题主要需要去了解下laucher的启动流程,然后在弄清系统是怎么去获取到目标acitivity的。对于有多个相同标签时, 系统又是如何选择最优的activity 的,只要知道了这些,就可以有意识的去替换掉它,让系统启动我们需要的activity。