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:

  1. 如果是mmi version,将mmitest apk作为launcher启动
  2. 如果是普通版本(cu版本),保持原样(默认的laucher apk 作为开机启动项,mmitest apk作为一个三方apk并隐藏log)

2. Analysis

launcher的启动流程大致如下所示:

Created with Raphaël 2.2.0 startHomeActivityLocked(int userId, String reason):ActivityManagerService.java resolveActivityInfo(Intent intent, int flags, int userId):ActivityManagerService.java resolveIntent(...):PackageManagerService.java resolveIntentInternal(...):PackageManagerService.java chooseBestActivity(intent, resolvedType, flags, query, userId):PackageManagerService.java findPreferredActivity(...):PackageManagerService.java

启动activity的入口是startHomeActivityLocked方法,但是在众多的 activity中如何选择,则是在chooseBestActivity方法中进行的。该方法:

  1. 首先进行判断,如果launcher apk只有一个就直接将它放回给startHomeActivityLocked方法,让它启动。
  2. 如果有多个launcher apk,从偏好设置中获取用户设置好的默认launcher,该偏好设置文件路径为/data/system/users/0/package-restrictions.xml,root后可以查看。
  3. 如果用户还没有设置偏好的activity,则启动ResolverActivity,该 activity会让用户进行偏好设置。

回到本问题,将mmitest apk客制为laucher主要有4种方法:

  1. 删除系统的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。

  1. 修改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;
           }
  1. 修改偏好设置
    当系统有多个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)

  2. 修改系统默认laucher启动标签
    参考让你自己写的Android的Launcher成为系统中第一个启动的,也是唯一的Launcher做法,创建一个私有的filter选项,让它来作为系统lancher的过滤选项。然后将mmitest apk中的androidManifest.xml中的main screen添加该私有标签,这个方法从framework层修改了launcher的启动标签,而原本的<category android:name="android.intent.category.HOME"/>将不会作为系统launcher标签,这种做法对系统修改不可谓不大,但是这个做法同样不适用本问题。

3. solution

具体修改如下:

  1. 修改文件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.
               /*此处省略*/
   }
  1. 修改目标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。

在这里插入图片描述

发布了38 篇原创文章 · 获赞 14 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/cw102055234/article/details/88538121