在一個風和日麗的下午,忽然收到測試同窗反饋在小米手機中有些頁面沒法正常啓動的消息,我便馬上去排查代碼,發現都是常規啓動Activity的操做,卻真的沒法實現了,這讓人感受十分詭異。而這一現象只在小米手機中發生,因此判定確定和小米手機的MIUI系統有關係,通過排查發現是小米手機中「後臺彈出界面」的權限默認被拒絕了,這樣在後臺Service中或者其餘一些後臺操做都沒法啓動Activity了。java
在小米手機的應用權限管理中有一個「後臺彈出界面權限」,該項權限會限制當APP處在後臺時彈出Activity的動做,該權限時默認關閉的,能夠在小米系統的權限管理頁看到: bash
這個時候正常的startActivity()
方法沒法彈出Activity,在Log中篩選
MIUILOG
查看系統日誌:
咱們能夠看到其中有權限拒絕的具體說明:
ExtraActivityManagerService: MIUILOG- Permission Denied Activity
複製代碼
能夠看到是權限緣由拒絕了啓動Activity動做,隨後咱們在小米的官方論壇中也看到了他們的聲明: ide
自從2019年5月份開始,小米開啓了這項權限判斷,因此以前能夠正常彈出的界面如今沒法彈出了。咱們嘗試了一些方法來繞過這個權限判斷,好比啓動一個前臺Service來跳轉、在Toast中來跳轉,都沒法繞過該權限判斷,因此繞過權限這條路是不能走了,只能想辦法正面解決。工具
咱們普通APP安裝後,此項權限默認是關閉的,固然有些大型APP具有和小米商務談判的能力,小米會在系統設置中默認給予開啓,好比「搜狗輸入法」就是默認開啓此權限的。post
可是隻有具有足夠影響力的公司才能與小米談判,並且要知足他們的各類條件,才能讓其系統中默認爲咱們開啓此權限,咱們普通APP是作不到的,因此此方法不適用於咱們普通APP。測試
咱們面對普通權限請求通常處理方案是這樣的,先判斷是會否具備此項權限,若是沒有就請求開啓此權限。可是對於小米的這種廠商獨有的權限,咱們的難點在於沒有相關API能夠判斷是否具有此權限,也沒有API去申請此權限,因此這條路是不通的。固然能夠經過反射之類的方法,去調用他系統層的一些東西,不過這樣不太靠譜,研發代價也比較大,因此能夠說是沒有直接解決方案的。ui
那麼這個問題就無解了麼?我經過一系列討論最後經過迂迴方法進行解決,得出最終可用解決方案:spa
這裏難點在於判斷Activity是否被成功打開了,至於彈窗引導本身定製引導內容便可。下面一節,具體對如何判斷Activity被成打開進行說明。日誌
這裏一樣也嘗試了多種方案,好比:code
OnCreate()
方法中進行處理(最終放棄)當startActivity()
後作一個0.5s倒計時邏輯,在要啓動的Activity的OnCreate()
方法中發一個廣播來去掉該倒計時,若是沒有被取消那麼就說明沒有啓動成功。這樣須要在每一個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
要獲取額外的權限,因此也不是合理的方法。最終這個方法也不能實現可用性。
工具類統一處理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);
複製代碼
小米後臺彈出權限問題的解決,是經過曲線救國的方法解決的,由於沒有直接的API可調用。這裏封裝成了一個工具類,任何須要添加權限判斷的地方只須要調用工具類的方法就好了,這樣既實現了統一管理,又方便調用,因此這是咱們最終採用的方案。
若是有更好的方案歡迎各位大佬前來交流。