小米 TYPE_TOAST 懸浮窗無效的原因

joytoy發表於2021-09-09

Android中的懸浮窗顯示是一個非常棘手的問題,網上已經有很多解決方案了,大致歸為下面兩類:

  1. 設定WindowManager.LayoutParams.type = TYPE_SYSTEM_ALERT,並引導使用者開啟懸浮窗許可權。
    這種方法主要的難點在於引導使用者跳轉許可權設定頁面,由於各廠商定製的問題,需要針對許多裝置進行對應的適配,目前已有大神總結了部分機型的適配問題,詳情參見:

  2. 設定WindowManager.LayoutParams.type = TYPE_TOAST,可以繞過系統許可權檢查,但是這種方法的問題在於:

  • 懸浮窗在API 18及以下的系統無法接收Touch事件。

  • API 25中無法同時存在兩個Toast型別的懸浮窗,API 25以上系統直接禁止使用者使用TYPE_TOAST建立懸浮窗。

  • MiUI 8中修改了WindowManager中的程式碼,導致在該系統上任然無法展示出懸浮窗。

本文就針對MiUI 8上的問題進行分析。

原因分析

在實際的使用過程中我們發現,系統的Toast可以正常顯示,包括自定義的Toast,而透過WindowManger.addView新增的TYPE_TOAST的無法顯示。
我們知道系統Toast實際也是透過呼叫WindowManger.addView建立的,那麼我們可以試著模擬系統建立Toast時使用的引數,下面是在MiUI 8上建立系統Toast時的LayoutParams

圖片描述

系統Toast


其中flags的值(主要還是為了遮蔽事件)


WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE

token的值為WindowManager.LayoutParams.TYPE_TOAST
現在完全按照上面系統設定的值模擬一個LayoutParams,並嘗試建立的懸浮窗。
結果仍然失敗了。
那麼從WindowManager.addView()開始一步一步追蹤,看看在MiUI上到底發生了什麼。

圖片描述

addView


在向ViewRootImpl設定View的過程中呼叫了ViewRootImplInjectortransformWindowType函式,在這之後LayoutParamstype欄位值就發生了改變,從 2005 變成了 2003,而 2003 這個值所對應的常量為WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,在後續WindowManagerService.addWindow()執行的時候又會進行懸浮窗的鑑權,所以自然會顯示失敗。
也就是說,我們繞了半天,又回到了原點。。。
想看看在ViewRootImplInjector中發生了什麼,可是在系統 API 中搜尋不到這個類的存在,懷疑是 MiUI 自定義的,GitHub 上搜了一下,發現了 ViewRootImpInjector.smali 檔案,用Smali2Java轉化一下,得到如下程式碼:


package android.view;import miui.os.Build;import java.util.ArrayList;import android.util.Log;public class ViewRootImplInjector {    private static final String TAG = "ViewRootImpl";    
    public static void transformWindowType(View p1, WindowManager.LayoutParams p2) {        if(Build.IS_INTERNATIONAL_BUILD) {            return;
        }        if(p2.type == 0x7d5) {
            ArrayList stackTrace = new ArrayList();
            stackTrace.add("android.view.ViewRootImplInjector.transformWindowType");
            stackTrace.add("android.view.ViewRootImpl.setView");
            stackTrace.add("android.view.WindowManagerGlobal.addView");
            stackTrace.add("android.view.WindowManagerImpl.addView");
            stackTrace.add("android.widget.Toast$TN.handleShow");            try {
                StackTraceElement[] stackTraceElements = new Exception().getStackTrace();                if(stackTraceElements.length > stackTrace.size()) {                    for(int i = 0x0; i 

可以看到程式碼中判斷LayoutParams.type等於0x7d5(TYPE_TOAST)時,獲取了當前方法執行棧的資訊,如果當前方法不是按照
android.widget.Toast$TN.handleShow ->
android.view.WindowManagerImpl.addView ->
android.view.WindowManagerGlobal.addView ->
android.view.ViewRootImpl.setView ->
android.view.ViewRootImplInjector.transformWindowType
這個流程執行的話,就將LayoutParams.type的值設為0x7d3(TYPE_SYSTEM_ALERT),這個流程正是系統建立Toast的執行流程,所以這裡將所有非系統呼叫的TYPE_TOAST型別都強制轉化成了TYPE_SYSTEM_ALERT

解決方案

我們看到在transformWindowType方法的開頭,MiUI又設定了一個開關Build.IS_INTERNATIONAL_BUILD,這個應該是MiUI國際版的標識,只要我們將這個值設為true,那麼後續的程式碼就不再執行了,似乎問題就迎刃而解了。

public static void addViewToWindow(WindowManager wm, View view, WindowManager.LayoutParams params) {
    beforeAddToWindow(params);
    wm.addView(view, params);
    afterAddToWindow();
}private static void beforeAddToWindow(WindowManager.LayoutParams params) {
    setMiUI_International(true);
    setMeizuParams(params);
}private static void afterAddToWindow() {
    setMiUI_International(false);
}private static void setMiUI_International(boolean flag) {    try {
        Class BuildForMi = Class.forName("miui.os.Build");
        Field isInternational = BuildForMi.getDeclaredField("IS_INTERNATIONAL_BUILD");
        isInternational.setAccessible(true);
        isInternational.setBoolean(null, flag);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

透過反射,在addView執行之前修改IS_INTERNATIONAL_BUILDtrue,之後再將其還原。

說明

在我的小米測試機上使用該方案,確實能夠展示出懸浮窗,但是該方案未經過大量的相容性測試,所以其他機型、系統上表現如何不得而知,但是可以用類似的方法去分析。

更新

適配魅族Pro 6s

private static void setMeizuParams(WindowManager.LayoutParams params) {    try {
        Class MeizuParamsClass = Class.forName("android.view.MeizuLayoutParams");
        Field flagField = MeizuParamsClass.getDeclaredField("flags");
        flagField.setAccessible(true);
        Object MeizuParams = MeizuParamsClass.newInstance();
        flagField.setInt(MeizuParams, 0x40);

        Field mzParamsField = params.getClass().getField("meizuParams");
        mzParamsField.set(params, MeizuParams);
    } catch (Exception e) {
        e.printStackTrace();
    }
}



作者:Joe_H
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4650/viewspace-2809973/,如需轉載,請註明出處,否則將追究法律責任。

相關文章