Android 设置默认应用

Posted 虫师魁拔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 设置默认应用相关的知识,希望对你有一定的参考价值。

一、通过 PackageManager 设置

        默认应用是项目中常见的一项设置,比如默认桌面应用、浏览器应用等。android Q之前版本设置方式通过 PackageManager 的 addPreferredActivity 接口实现。这种设置方式叫它为设置默认首选项比较恰当。设置好后,有首选项的时候就不弹出选择框。例如,设置桌面首选项应用:

    private void setDefaultLauncher(PackageManager pm, String pkgName, String clsName) 
        // 第1步:查询设备所有Home类型应用
        Intent queryIntent = new Intent();
        queryIntent.addCategory(Intent.CATEGORY_HOME);
        queryIntent.setAction(Intent.ACTION_MAIN);

        List<ResolveInfo> homeActivities = pm.queryIntentActivities(queryIntent, 0);
        if(homeActivities == null) 
            return;
        

        // 第2步:通过设置的包名类名,查找系统是否存在此Home类型应用
        ComponentName defaultLauncher = new ComponentName(pkgName, clsName);
        int activityNum = homeActivities.size();
        ComponentName[] set = new ComponentName[activityNum];

        int defaultMatch = -1;
        for(int i = 0; i < activityNum; i++)
            ResolveInfo info = homeActivities.get(i);
            Log.i(TAG, "info " + info);
            set[i] = new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
            if(clsName.equals(info.activityInfo.name)
                    && pkgName.equals(info.activityInfo.packageName))
                defaultMatch = info.match;
            
        

        // 如果没找到匹配的应用,就不继续设置
        if(defaultMatch == -1)
            return;
        

        // 第3步:通过系统接口添加默认首选项应用
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_MAIN);
        filter.addCategory(Intent.CATEGORY_HOME);
        filter.addCategory(Intent.CATEGORY_DEFAULT);

        pm.addPreferredActivity(filter, defaultMatch, set, defaultLauncher);
    

        Android Q 之后这种方法逐步废弃,使用 RoleManager 替代。虽然目前还可以使用,但需要考虑兼容问题。目前已知问题:

1、Android R 上,设置-应用-默认应用 中显示的默认应用,都是通过 RoleManager 获取以及监听变化,通过 PackageManager 这种设置方式后,设置中检测不到变化,导致设置中默认应用显示存在问题(Home 类型应用是个例外,原因见下面代码分析)。

        PackageManager (PM)是一个抽象类,ApplicationPackageManager (APM)继承实现部分抽象方法。addPreferredActivity 方法即在 APM 中实现,继而调用 PackageManagerService(PMS)中 addPreferredActivityInternal

    private void addPreferredActivityInternal(IntentFilter filter, int match,
            ComponentName[] set, ComponentName activity, boolean always, int userId,
            String opname) 

        /* 检测权限代码 省略... */

        synchronized (mLock) 
            final PreferredIntentResolver pir = mSettings.editPreferredActivitiesLPw(userId);
            // 添加默认应用到选择器,作为默认选项
            pir.addFilter(new PreferredActivity(filter, match, set, activity, always));
            scheduleWritePackageRestrictionsLocked(userId);
        
        // Home类型应用修改
        if (!updateDefaultHomeNotLocked(userId)) 
            postPreferredActivityChangedBroadcast(userId);
        
    
    private boolean updateDefaultHomeNotLocked(int userId) 
        if (Thread.holdsLock(mLock)) 
            Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName()
                    + " is holding mLock", new Throwable());
        
        // 第1步:获取默认首选项Home应用和当前保存的Home应用。注意这两个是不同的,保存位置不一样
        if (!mSystemReady) 
            // We might get called before system is ready because of package changes etc, but
            // finding preferred activity depends on settings provider, so we ignore the update
            // before that.
            return false;
        
        final Intent intent = getHomeIntent();
        final List<ResolveInfo> resolveInfos = queryIntentActivitiesInternal(intent, null,
                PackageManager.GET_META_DATA, userId);
        final ResolveInfo preferredResolveInfo = findPreferredActivityNotLocked(
                intent, null, 0, resolveInfos, 0, true, false, false, userId);
        final String packageName = preferredResolveInfo != null
                && preferredResolveInfo.activityInfo != null
                ? preferredResolveInfo.activityInfo.packageName : null;
        final String currentPackageName = mPermissionManager.getDefaultHome(userId);

        // 第2步:currentPackageName 为当前默认Home类型应用,如果设置Home应用首选项包名相同就不继续设置
        if (TextUtils.equals(currentPackageName, packageName)) 
            return false;
        
        final String[] callingPackages = getPackagesForUid(Binder.getCallingUid());
        if (callingPackages != null && ArrayUtils.contains(callingPackages,
                mRequiredPermissionControllerPackage)) 
            // PermissionController manages default home directly.
            return false;
        
        // 第3步:设置默认Home应用
        mPermissionManager.setDefaultHome(packageName, userId, (successful) -> 
            if (successful) 
                postPreferredActivityChangedBroadcast(userId);
            
        );
        return true;
    

        上面源码可以看出,通过 addPreferredActivity 设置首选项,如果设置的是 Home 应用,首选项改变后,包名改变,继续通过 

mPermissionManager.setDefaultHome

        进行设置。PermissionManagerServiceInternal 实现类是在:

PermissionManagerService.PermissionManagerServiceInternalImpl

        下面是 PermissionManagerService 中 setDefaultHome 代码实现:

        @Override
        public void setDefaultHome(String packageName, int userId, Consumer<Boolean> callback) 
            if (userId == UserHandle.USER_ALL) 
                return;
            
            // 第1步:获取默认Home应用Provider
            DefaultHomeProvider provider;
            synchronized (mLock) 
                provider = mDefaultHomeProvider;
            
            if (provider == null) 
                return;
            
            // 第2步:进行异步设置,设置成功后通过 callback 回调通知
            provider.setDefaultHomeAsync(packageName, userId, callback);
        

        DefaultHomeProvider 是 PermissionManagerServiceInternal 中一个接口类,子实现类为 RoleManagerService.DefaultHomeProvider 。实现 setDefaultHomeAsync 源码如下。

        public void setDefaultHomeAsync(@Nullable String packageName, @UserIdInt int userId,
                @NonNull Consumer<Boolean> callback) 
            // 第1步:创建回调对象
            RemoteCallback remoteCallback = new RemoteCallback(result -> 
                boolean successful = result != null;
                if (!successful) 
                    Slog.e(LOG_TAG, "Failed to set default home: " + packageName);
                
                callback.accept(successful);
            );
            // 第2步:添加RoleHolder,如果包名为空,则清空RoleHolder
            if (packageName != null) 
                getOrCreateController(userId).onAddRoleHolder(RoleManager.ROLE_HOME,
                        packageName, 0, remoteCallback);
             else 
                getOrCreateController(userId).onClearRoleHolders(RoleManager.ROLE_HOME, 0,
                        remoteCallback);
            
        

       以上是完整的 addPreferredActivity 流程,通过源码可见,除了Home类型应用在设置首选项后,还继续执行,最新添加到 RoleHolder ,保证首选项和默认项都一致之外,其他应用都是设置一个首选项。(onAddRoleHolder 部分具体流程等下一节详细说明,这里暂时不作展开说明)

        这解释了上面存在的问题一,其他类型应用,即使设置默认首选项后,由于Role未改变,而 设置-应用-默认应用 中是获取/监听的Role变化,导致设置中显示未变化。所以推荐使用 RoleManager 方式设置默认应用,来替代设置默认首选项。

二、通过 RoleManager 设置

        Android Q开始,新增 RoleManager、RoleManagerService 等类,通过 RoleManager 设置默认应用取代之前的方式。例如,设置默认桌面:

    private void setRoleHolderAsUser(String roleName, String packageName,
            int flags, UserHandle user, Context context) 

        RoleManager roleManager = (RoleManager)context.getSystemService(Context.ROLE_SERVICE);
        Executor executor = context.getMainExecutor();
        Consumer<Boolean> callback = successful -> 
            if (successful) 
                Log.d(TAG, "Package added as role holder, role: " + roleName + ", package: " + packageName);
             else 
                Log.d(TAG, "Failed to add package as role holder, role: " + roleName + ", package: "
                            + packageName);
            
        ;
        roleManager.addRoleHolderAsUser(roleName, packageName, flags, user, executor, callback);
    

        对比代码看,RoleManager 设置方式更加简洁,增加了 Consumer<Boolean> callback 的回调,根据回调结果确认设置是否成功。但是 RoleManager 设置存在以下问题:

1、RoleManager 设置是异步的,所以一般执行 addRoleHolderAsUser 后需要等一段时间才能得到回调结果,这是由于回调是基于上面代码中 Executor 进行,在添加流程完成后,对应的 Executor 线程中适当时候执行回调,视线程工作情况,时间不可控(实际中10ms-200ms延迟都遇到过),对于某些对时效性要求的场景不适用。

        addRoleHolderAsUser 方法继续下去,是在 RoleManagerService 中调用如下方法

getOrCreateController(userId).onAddRoleHolder(roleName, packageName, flags,
        callback);

        这个方法和上一节中最后一段,设置Home的方法是一致的,接着详细看实现源码逻辑。中间一段代码简单,跳过,直接看最后调用的 RoleControllerService 中方法。

            @Override
            public void onAddRoleHolder(String roleName, String packageName, int flags,
                    RemoteCallback callback) 
                // 第1步:判断非空等条件
                enforceCallerSystemUid("onAddRoleHolder");

                Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty");
                Preconditions.checkStringNotEmpty(packageName,
                        "packageName cannot be null or empty");
                Objects.requireNonNull(callback, "callback cannot be null");

                // 第2步:通过创建的 Handler 进行顺序执行
                mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
                        RoleControllerService::onAddRoleHolder, RoleControllerService.this,
                        roleName, packageName, flags, callback));
            

    RCS 是一个抽象类,它还有一个子类,位于packages/apps/PermissionController/src/com/android/permissioncontroller/role/service/RoleControllerServiceImpl.java (设置中默认应用显示的部分,也是启动 PermissionController 应用中activity)。这里 onAddRoleHolder 继续下去,调用到 RoleControllerServiceImpl 中实现。

    @Override
    @WorkerThread
    public boolean onAddRoleHolder(@NonNull String roleName, @NonNull String packageName,
            int flags) 
        if (!checkFlags(flags, RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP)) 
            return false;
        
        // 第1步: 通过roleName获取Role对象
        Role role = Roles.get(this).get(roleName);
        if (role == null) 
            Log.e(LOG_TAG, "Unknown role: " + roleName);
            return false;
        
        if (!role.isAvailable(this)) 
            Log.e(LOG_TAG, "Role is unavailable: " + roleName);
            return false;
        

        if (!role.isPackageQualified(packageName, this)) 
            Log.e(LOG_TAG, "Package does not qualify for the role, package: " + packageName
                    + ", role: " + roleName);
            return false;
        

        // 第2步: 通过roleName获取设备中当前此类型应用(一般只有一个),判断是否已经被设置过
        boolean added = false;
        if (role.isExclusive()) 
            List<String> currentPackageNames = mRoleManager.getRoleHolders(roleName);
            int currentPackageNamesSize = currentPackageNames.size();
            for (int i = 0; i < currentPackageNamesSize; i++) 
                String currentPackageName = currentPackageNames.get(i);

                if (Objects.equals(currentPackageName, packageName)) 
                    Log.i(LOG_TAG, "Package is already a role holder, package: " + packageName
                            + ", role: " + roleName);
                    added = true;
                    continue;
                

                boolean removed = removeRoleHolderInternal(role, currentPackageName, false);
                if (!removed) 
                    // TODO: Clean up?
                    return false;
                
            
        

        // 第3步: 调用系统添加保存,重点
        boolean dontKillApp = hasFlag(flags, RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP);
        added = addRoleHolderInternal(role, packageName, dontKillApp, true, added);
        if (!added) 
            return false;
        

        // 第4步: 进行本地应用 SharedPreferences 添加保存
        role.onHolderAddedAsUser(packageName, Process.myUserHandle(), this);
        role.onHolderChangedAsUser(Process.myUserHandle(), this);

        return true;
    

        addRoleHolderInternal 调用最终又回到系统 RoleManager 中,继而调用到 RoleManagerService.addRoleHolderFromController,最终到 RoleUserState.addRoleHolder

    @CheckResult
    public boolean addRoleHolder(@NonNull String roleName, @NonNull String packageName) 
        boolean changed;

        synchronized (mLock) 
            ArraySet<String> roleHolders = mRoles.get(roleName);
            if (roleHolders == null) 
                Slog.e(LOG_TAG, "Cannot add role holder for unknown role, role: " + roleName
                        + ", package: " + packageName);
                return false;
            
            // 第1步:保存到列表中
            changed = roleHolders.add(packageName);
            if (changed) 
                // 第2步:保存到本地持久化文件中
                scheduleWriteFileLocked();
            
        
        // 第3步:通知添加OK
        if (changed) 
            mCallback.onRoleHoldersChanged(roleName, mUserId, null, packageName);
        
        return true;
    

    @GuardedBy("mLock")
    private void scheduleWriteFileLocked() 
        if (mDestroyed) 
            return;
        

        if (!mWriteScheduled) 
            // 延迟 WRITE_DELAY_MILLIS(200ms) 后写入
            mWriteHandler.sendMessageDelayed(PooledLambda.obtainMessage(RoleUserState::writeFile,
                    this), WRITE_DELAY_MILLIS);
            mWriteScheduled = true;
        
    

        最终写入到本地文件的部分在 RolesPersistenceImpl.writeForUser 完成,写入文件路径为:

/data/misc_de/0/apexdata/com.android.permission/roles.xml

        这里由于写入的时候有一个 WRITE_DELAY_MILLIS (默认200ms)延迟,所以在添加默认应用后需要过200ms 后,才能通过 RolesPersistenceImpl.readForUser 获取到正确的值,对于使用此方法的代码逻辑需要注意延迟问题。

以上是关于Android 设置默认应用的主要内容,如果未能解决你的问题,请参考以下文章

在 Swift 中更改 iOS 应用程序的默认基本语言

Kotlin 编程语言成为其 Android 应用程序开发人员的首选语言

如何为 iOS 应用设置默认本地化

Android 设置默认应用

Android 设置默认应用

Android 设置默认应用