小米「後臺彈出界面權限」最佳適配方案

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可調用。這裏封裝成了一個工具類,任何須要添加權限判斷的地方只須要調用工具類的方法就好了,這樣既實現了統一管理,又方便調用,因此這是咱們最終採用的方案。

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

相關文章
相關標籤/搜索