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

1. 后台没法弹出Activity

在一个风和日丽的下午,忽然收到测试同窗反馈在小米手机中有些页面没法正常启动的消息,我便马上去排查代码,发现都是常规启动Activity的操做,却真的没法实现了,这让人感受十分诡异。而这一现象只在小米手机中发生,因此判定确定和小米手机的MIUI系统有关系,通过排查发现是小米手机中“后台弹出界面”的权限默认被拒绝了,这样在后台Service中或者其余一些后台操做都没法启动Activity了。java

在小米手机的应用权限管理中有一个“后台弹出界面权限”,该项权限会限制当APP处在后台时弹出Activity的动做,该权限时默认关闭的,能够在小米系统的权限管理页看到: bash

小米权限界面
这个时候正常的 startActivity()方法没法弹出Activity,在Log中筛选 MIUILOG查看系统日志:
MIUI系统日志
咱们能够看到其中有权限拒绝的具体说明:

ExtraActivityManagerService: MIUILOG- Permission Denied Activity
复制代码

能够看到是权限缘由拒绝了启动Activity动做,随后咱们在小米的官方论坛中也看到了他们的声明: ide

小米官方声明

2. 解决方案

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

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

咱们普通APP安装后,此项权限默认是关闭的,固然有些大型APP具有和小米商务谈判的能力,小米会在系统设置中默认给予开启,好比“搜狗输入法”就是默认开启此权限的。post

可是只有具有足够影响力的公司才能与小米谈判,并且要知足他们的各类条件,才能让其系统中默认为咱们开启此权限,咱们普通APP是作不到的,因此此方法不适用于咱们普通APP。测试

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

咱们面对普通权限请求通常处理方案是这样的,先判断是会否具备此项权限,若是没有就请求开启此权限。可是对于小米的这种厂商独有的权限,咱们的难点在于没有相关API能够判断是否具有此权限,也没有API去申请此权限,因此这条路是不通的。固然能够经过反射之类的方法,去调用他系统层的一些东西,不过这样不太靠谱,研发代价也比较大,因此能够说是没有直接解决方案的。ui

那么这个问题就无解了么?我经过一系列讨论最后经过迂回方法进行解决,得出最终可用解决方案:spa

  1. 判断要启动的Activity是否被成功启动,若是没有则表明没有获取到权限。
  2. 弹窗提示用户去开启该权限。(因为没有后台弹出权限,没法直接跳转到系统的权限设置页,因此弹窗提示)

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

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

这里一样也尝试了多种方案,好比:code

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可调用。这里封装成了一个工具类,任何须要添加权限判断的地方只须要调用工具类的方法就好了,这样既实现了统一管理,又方便调用,因此这是咱们最终采用的方案。

若是有更好的方案欢迎各位大佬前来交流。

相关文章
相关标签/搜索