美文网首页
转载:小米「后台弹出界面权限」最佳适配方案

转载:小米「后台弹出界面权限」最佳适配方案

作者: 玉圣 | 来源:发表于2020-05-22 17:53 被阅读0次

    1. 后台无法弹出Activity

    在一个风和日丽的下午,突然收到测试同学反馈在小米手机中有些页面无法正常启动的消息,我便立刻去排查代码,发现都是常规启动Activity的操作,却真的无法实现了,这让人感觉十分诡异。而这一现象只在小米手机中发生,所以断定肯定和小米手机的MIUI系统有关系,经过排查发现是小米手机中“后台弹出界面”的权限默认被拒绝了,这样在后台Service中或者其他一些后台操作都无法启动Activity了。
    在小米手机的应用权限管理中有一个“后台弹出界面权限”,该项权限会限制当APP处在后台时弹出Activity的动作,该权限时默认关闭的,可以在小米系统的权限管理页看到:

    image.png

    这个时候正常的startActivity()方法无法弹出Activity,在Log中筛选MIUILOG查看系统日志:

    image.png

    我们可以看到其中有权限拒绝的具体说明:

    ExtraActivityManagerService: MIUILOG- Permission Denied Activity
    

    可以看到是权限原因拒绝了启动Activity动作,随后我们在小米的官方论坛中也看到了他们的声明:


    image.png

    2. 解决方案

    自从2019年5月份开始,小米开启了这项权限判断,所以之前可以正常弹出的界面现在无法弹出了。我们尝试了一些方法来绕过这个权限判断,比如启动一个前台Service来跳转、在Toast中来跳转,都无法绕过该权限判断,所以绕过权限这条路是不能走了,只能想办法正面解决。

    2.1 商务谈判,让小米MIUI给默认开启此权限

    我们普通APP安装后,此项权限默认是关闭的,当然有些大型APP具备和小米商务谈判的能力,小米会在系统设置中默认给予开启,比如“搜狗输入法”就是默认开启此权限的。
    但是只有具备足够影响力的公司才能与小米谈判,而且要满足他们的各种条件,才能让其系统中默认为我们开启此权限,我们普通APP是做不到的,所以此方法不适用于我们普通APP。

    2.2 进行权限判断,通过代码请求开启权限

    我们面对普通权限请求一般处理方案是这样的,先判断是会否具有此项权限,如果没有就请求开启此权限。但是对于小米的这种厂商独有的权限,我们的难点在于没有相关API可以判断是否具备此权限,也没有API去申请此权限,所以这条路是不通的。当然可以通过反射之类的方法,去调用他系统层的一些东西,不过这样不太靠谱,研发代价也比较大,所以可以说是没有直接解决方案的。
    那么这个问题就无解了么?我通过一系列讨论最后通过迂回方法进行解决,得出最终可用解决方案:

    判断要启动的Activity是否被成功启动,如果没有则代表没有获取到权限。
    弹窗提示用户去开启该权限。(由于没有后台弹出权限,无法直接跳转到系统的权限设置页,所以弹窗提示)

    这里难点在于判断Activity是否被成功打开了,至于弹窗引导自己定制引导内容即可。下面一节,具体对如何判断Activity被成打开进行说明。

    3. 判断Activity是否被成功启动

    这里同样也尝试了多种方案,比如:

    3.1 在每个Activity的OnCreate()方法中进行处理(最终放弃)

    当startActivity()后做一个0.5s倒计时逻辑,在要启动的Activity的OnCreate()方法中发一个广播来去掉该倒计时,如果没有被取消那么就说明没有启动成功。这样需要在每个Activity中做处理,过于繁琐,所以放弃。

    3.2 通过Activity栈获取栈顶Activity判断(最终放弃)

    当startActivity()后做一个0.5s倒计时逻辑,然后通过Activity栈的管理获得栈顶Activity,判断是否打开成功。这样避免每个Activity都要处理,比如我们可以查到常用方法是这样的 :

    public static String getTopActivity(Context context) {
        String packageName ;
        if (Build.VERSION.SDK_INT > 21) { 
        // 5.0及其以后的版本 
            List<ActivityManager.AppTask> tasks = mActivityManager.getAppTasks();
            if (null != tasks && tasks.size() > 0) { 
                for (ActivityManager.AppTask task:tasks){ 
                    packageName = task.getTaskInfo().baseIntent.getComponent().getPackageName(); 
                    lable = getPackageManager().getApplicationLabel(getPackageManager().getApplicationInfo(packageName,PackageManager.GET_META_DATA)).toString(); 
                    //Log.i(TAG,packageName + lable); 
                }
            }
        } 
        else{ 
            // 5.0之前 // 获取正在运行的任务栈(一个应用程序占用一个任务栈) 最近使用的任务栈会在最前面 
            // 1表示给集合设置的最大容量 
            List<RunningTaskInfo> infos = am.getRunningTasks(1); 
            // 获取最近运行的任务栈中的栈顶Activity(即用户当前操作的activity)的包名 
            packageName = mActivityManager.getRunningTasks(1).get(0).topActivity.getPackageName();
            //Log.i(TAG,packageName); 
        }
        return packageName ;
    }
    

    这个问题在于这些方法在Android 5.1之后也失效了,网上也有其他方法,使用usageStatsManager.queryUsageStats要获取额外的权限,所以也不是合理的方法。最终这个方法也不能实现可用性。

    3.3 自建工具类统一完成权限判断和弹窗引导处理,判断Activity是否被打开通过自己对栈顶Activity的管理实现(最终解决方案)

    工具类统一处理startActivity()方法,同时开启一个0.5s的倒计时。在Application中自己管理记录栈顶的Activity,用于判断栈顶Activity并完成是否成功打开,如果没有打开则展示引导弹窗。
    这样把启动Activity和权限判断都在一个工具类中处理,以后只需要调用这个工具类,就实现了启动Activity、判断权限、以及权限弹窗引导。同时自己进行Activity栈的管理,解决了无法在各个Android版本上完成对Activity启动判断的问题。
    首先在Application中监听所有Activity的生命周期,来记录栈顶Activity:

    // 在Application的OnCreate()方法中调用
    private void registerLifecycle() {
        mApplication.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
                        @Override
                        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                        }
        
                        @Override
                        public void onActivityStarted(Activity activity) {
                        }
        
                        @Override
                        public void onActivityResumed(Activity activity) {
                            // 这里记录栈顶Activity的名字
                            CustomActivityManager.setTopActivity(activity);
                        }
        
                        @Override
                        public void onActivityPaused(Activity activity) {
                        }
        
                        @Override
                        public void onActivityStopped(Activity activity) {
                            // 清除栈顶Activity
                            CustomActivityManager.clearTopActivity();
                        }
        
                        @Override
                        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
                        }
        
                        @Override
                        public void onActivityDestroyed(Activity activity) {
                        }
                    });
    }
    
    public class CustomActivityManager {
        private static final String SP_KEY_ACTIVITY_STACK_TOP = "sp_key_activity_stack_top";
    
        public static String getTopActivity() {
            // 这里从SP中读取栈顶Activity名字
        }
    
        public static void setTopActivity(Activity topActivity) {
            if (topActivity != null) {
                // 这里把栈顶Activity名字存入SP
            }
        }
    
        public static void clearTopActivity() {
            // 这里清除SP数据
        }
    }
    

    然后在工具类中做统一处理:

    public class ActivityStartCheckUtils {
        private static final int TIME_DELAY = 600;
        private static ActivityStartCheckUtils sInstance;
        private boolean mPostDelayIsRunning;
        private String mClassName;
        private IBinder mToken;
        private PermissionGuideDialog mDialog;
        private Handler mHhandler = new Handler();
    
    
        private ActivityStartCheckUtils() {
        }
    
        public static ActivityStartCheckUtils getInstance() {
            if (sInstance == null) {
                synchronized (ActivityStartCheckUtils.class) {
                    if (sInstance == null) {
                        sInstance = new ActivityStartCheckUtils();
                    }
                }
            }
            return sInstance;
        }
    
        //这里是倒计时完成后的判断逻辑
        private Runnable mRunnable = new Runnable() {
            @Override
            public void run() {
                mPostDelayIsRunning = false;
                // 判断要打开的Activity是不是已经在栈顶了
                if (!isActivityOnTop()) {
                    // context 这里根据自己项目具体处理 能获得context就行
                    Context context = MyApplication.getAppContext();//这个getAppContext需要自行修改
                    if (context != null && mToken != null) {
                        if (mDialog == null) {
                            // 自定义的Dialog,这个代码就没必要贴了
                            mDialog = new PermissionGuideDialog(context, mToken);
                        }
                        mDialog.setCancelable(false);
                        mDialog.show();
                    }
                }
            }
        };
    
        public void startActivity(Context context, Intent intent, String className, IBinder token) {
            if (context == null || intent == null || TextUtils.isEmpty(className)) {
                return;
            }
            context.startActivity(intent);
            if (token == null) {
                return;
            }
            mToken = token;
            mClassName = className;
            if (mPostDelayIsRunning) {
                mHhandler.removeCallbacks(mRunnable);
            }
            mPostDelayIsRunning = true;
            
            mHhandler.postDelayed(mRunnable, TIME_DELAY);
        }
    
        private boolean isActivityOnTop() {
            boolean result = false;
            String topActivityName = CustomActivityManager.getTopActivity();
            if (!TextUtils.isEmpty(topActivityName)) {
                if (topActivityName.contains(mClassName)) {
                    result = true;
                } 
            }
            return result;
        }
    }
    

    最后使用的时候直接调用工具类的方法startActivity()即可:

    Intent intent = new Intent();
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setClass(context, EmptyActivity.class);
    
    ActivityStartCheckUtils.getInstance().startActivity(context, intent, emptyActivityClassName, token);
    

    4. 总结

    小米后台弹出权限问题的解决,是通过曲线救国的方法解决的,因为没有直接的API可调用。这里封装成了一个工具类,任何需要添加权限判断的地方只需要调用工具类的方法就行了,这样既实现了统一管理,又方便调用,所以这是我们最终采用的方案。
    如果有更好的方案欢迎各位大佬前来交流。

    评论解决方案:

    很早就这样了,现在很多手机都有这个权限,不只有小米,OV的也有,小米的检查这个权限是有方法的,MIUI开发组有说明:

    public static boolean canBackgroundStart(Context context) { 
            AppOpsManager ops = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 
            try { 
                int op = 10021; // >= 23 
                // ops.checkOpNoThrow(op, uid, packageName) 
                Method method = ops.getClass().getMethod("checkOpNoThrow", new Class[] 
                        {int.class, int.class, String.class} 
                ); 
                Integer result = (Integer) method.invoke(ops, op, Process.myUid(), context.getPackageName()); 
                return result == AppOpsManager.MODE_ALLOWED; 
            } catch (Exception e) { 
                Log.e(TAG, "not support", e); 
            } 
            return false; 
    }
    

    相关文章

      网友评论

          本文标题:转载:小米「后台弹出界面权限」最佳适配方案

          本文链接:https://www.haomeiwen.com/subject/vnkcahtx.html