1. 程式人生 > >WindowManager解析(二)Android懸浮框無法彈出輸入法的原因和無需許可權顯示懸浮窗

WindowManager解析(二)Android懸浮框無法彈出輸入法的原因和無需許可權顯示懸浮窗

Android懸浮框無法彈出輸入法

最近要研究懸浮窗方面的東西,遇到一個問題,我的懸浮窗裡面有一個輸入框,但是不彈出輸入法,後來找到一個方法:

在WindowManager的例項獲取方式不對,之前是這樣獲取的:

mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);

改這樣:

mWindowManager = (WindowManager) 
mContext.getApplicationContext().getSystemService(Context.WINDOW
_SERVICE);

一個是通過當前activity的上下文環境去獲取視窗服務,一個是通過application去獲取視窗服務

發現還是不行,後來我花了很長時間用谷歌翻譯了WindowManager的原始碼英文註釋才發現,原來我之前是用的:

                bigWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

LayoutParams.FLAG_NOT_TOUCH_MODAL的意思是:

        /**
         視窗標誌:即使該視窗是可對焦的(其#FLAG_NOT_FOCUSABLE未設定),
         允許視窗外的任何指標事件傳送到其後面的視窗。
         否則它將消耗所有指標事件本身,而不管它們是否在視窗內。
          */
        public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;

即不會阻擋後面的點選事件

LayoutParams.FLAG_NOT_FOCUSABLE的意思是:

        /**
         視窗標誌:此視窗不會獲得關鍵輸入焦點,因此使用者無法向其傳送鍵或其他按鈕事件。
         那些會改變它背後的任何可關注的視窗。
         此標誌還將啟用#FLAG_NOT_TOUCH_MODAL是否顯式設定。
         *
         *
         設定此標誌也意味著該視窗不需要與軟輸入法進行互動,
         因此它將被Z-排序並且與任何活動輸入法無關(通常這意味著它在輸入法之上得到Z-排序,
         所以它可以使用全屏的內容,如果需要的話可以覆蓋輸入法。
         可以使用{@link #FLAG_ALT_FOCUSABLE_IM}來修改這個行為。
         * */
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;

該視窗不需要與軟輸入法進行互動,說明是不會彈出輸入框的,把這個去掉,就能正常顯示出輸入法了,不過去掉了也會造成另外一個問題,那就是系統的返回鍵用不了!

要想在WindowManager中直接新增EditText並且能夠輸入文字,而且還不影響其他操作的,需要保證以下兩點:

type 為 LayoutParams.TYPE_SYSTEM_ALERT 而不是 LayoutParams.TYPE_SYSTEM_ERROR

防止沒有三個按鍵的問題
flag中不要有 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE

防止無法彈出輸入框的問題
flag中要有 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL

防止下方app無法獲取焦點和事件的問題

看來花費了這麼多時間翻譯了一下原始碼的註釋還是很有好處的,起碼知道了用這個屬性是為什麼,也說明要想學得好還是得多看原始碼,翻譯一下注釋。

無需許可權顯示懸浮窗

懸浮窗原理

做過懸浮窗功能的人都知道, 要想顯示懸浮窗, 要有一個服務執行在後臺, 通過getSystemService(Context.WINDOW_SERVICE)拿到WindowManager, 然後向其中addView, addView第二個引數是一個WindowManager.LayoutParams, WindowManager.LayoutParams中有一個成員type, 有各種值, 一般設定成TYPE_PHONE就可以懸浮在很多view的上方了, 但是呼叫這個方法需要申請android.permission.SYSTEM_ALERT_WINDOW許可權, 在很多機型上, 這個許可權的名字叫懸浮窗, 比如小米手機上預設是禁用這個許可權的, 有些惡意app會用這個許可權彈廣告, 而且很難追查是哪個應用彈的. 如果這個許可權被禁用, 那麼結果就是懸浮窗無法展示, 比如有道詞典的複製查詞功能, 在小米手機上經常沒用, 其實是使用者沒有授權, 而且應用也沒有引導使用者給它開啟授權。

使用TYPE_TOAST可以顯示懸浮框

使用 type 值為 WindowManager.LayoutParams.TYPE_PHONE 和 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT 需要申請 android.permission.SYSTEM_ALERT_WINDOW 許可權,否則無法顯示,報錯:

E/AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRoot$W@b64b5458 -- permission denied for this window type

但是,將type設定成TYPE_TOAST果然有奇效, 不需要android.permission.SYSTEM_ALERT_WINDOW許可權就能顯示一個懸浮窗.

原因

在Android原始碼中有這麼一段:

public int checkAddPermission(WindowManager.LayoutParams attrs) {
    int type = attrs.type;

    if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
            || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
        return WindowManagerImpl.ADD_OKAY;
    }
    String permission = null;
    switch (type) {
        case TYPE_TOAST:
            // XXX right now the app process has complete control over
            // this...  should introduce a token to let the system
            // monitor/control what they are doing.
            break;
        case TYPE_INPUT_METHOD:
        case TYPE_WALLPAPER:
            // The window manager will check these.
            break;
        case TYPE_PHONE:
        case TYPE_PRIORITY_PHONE:
        case TYPE_SYSTEM_ALERT:
        case TYPE_SYSTEM_ERROR:
        case TYPE_SYSTEM_OVERLAY:
            permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
            break;
        default:
            permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
    }
    if (permission != null) {
        if (mContext.checkCallingOrSelfPermission(permission)
                != PackageManager.PERMISSION_GRANTED) {
            return WindowManagerImpl.ADD_PERMISSION_DENIED;
        }
    }
    return WindowManagerImpl.ADD_OKAY;
}

這個方法是往系統的WindowManager裡addView的時候做許可權檢查用的, 那個type就是我們在構造WindowManager.LayoutParams時賦值的type, 可以看到, 除了TYPE_TOAST, 其他都是要許可權的, 而且非常喜感的是, 程式碼中的註釋還說他們現在對這種type毫無限制, 應該引入標記來限制開發者.

相容性

  1. type 值為 WindowManager.LayoutParams.TYPE_TOAST 顯示的 System overlay view 不需要許可權,即可在任何平臺顯示。

  2. 但僅在 API level >= 19 時可以達到目的。API level 19 以下因無法接收無法接收觸控(點選)和按鍵事件,故無法達到目的。

  3. 對於 API level < 19 的機器(MIUI除外),想要達到目的,需要:

要有 android.permission.SYSTEM_ALERT_WINDOW 許可權
將 type 設定為 WindowManager.LayoutParams.TYPE_PHONE 或者 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
MIUI V5

在給懸浮窗許可權之後,表現同 3 。但是,不給懸浮窗許可權時,應用在前臺時,卻可以顯示。這點非常不一樣。

為什麼採用 TYPE_TOAST

API level 19 之後,TYPE_SYSTEM_ALERT 就可以達到 TYPE_SYSTEM_ALERT 效果(即可以達到目的)。

但在 API level 23 (Android 6.0) 上,懸浮窗許可權也單獨弄出來了,需要到單獨開啟,這個處理和現在 Smartisan 還有小米類似。

故採用 TYPE_TOAST 能讓用不開啟懸浮窗許可權的情況下,也能顯示。為的就是儘量少請求許可權。

為什麼 API level 19 之後 TYPE_TOAST 可以接受事件

PhoneWindowManager.adjustWindowParamsLw(),API level 19 後做了調整。

當我們使用 TYPE_TOAST, Android 會偷偷給我們加上 FLAG_NOT_FOCUSABLE 和 FLAG_NOT_TOUCHABLE , 4.0.1 開始, 會額外再去掉 FLAG_WATCH_OUTSIDE_TOUCH。 這樣真的是什麼事件都沒了。

而4.4開始, TYPE_TOAST被移除了。所以從 4.4 開始, 使用 TYPE_TOAST 的同時還可以接收觸控事件和按鍵事件了, 而4.4以前只能顯示出來, 不能互動。

最後

TYPE_TOAST一直都可以顯示, 但是用TYPE_TOAST顯示出來的在2.3上無法接收點選事件, 因此還是無法隨意使用.
下面是我之前研究後臺執行緒顯示對話方塊的時候記得筆記, 大家可以看看我們專案中有需求需要在後臺任務中顯示Dialog, 專案最初的做法是用Activity模擬Dialog, 一個Activity已經承載了近20種Dialog, 程式碼混亂至極. 後來我發現Dialog可以通過改變Window Type實現不依賴Activity顯示, 然後就很興奮的要在使用這種方式來作為新的實現方式.
最初WindowType是WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, 可是這是懸浮窗了, MIUI會預設禁止(真他媽操蛋,也沒有任何提示)最終放棄. 後來試著換成了WindowManager.LayoutParams.TYPE_TOAST, 起初效果很好,MIUI也不禁止了, 哪裡都能顯示, 這下開心了. 可是後來又發現在2.3上不能接收點選事件, 也就是說Dialog上的按鈕不能點選, 這他媽就很操蛋了, 又放棄了. 又試了試其他的Type都不能滿足需求, 結果如下:TYPE_SEARCH_BAR: 未知
TYPE_ACCESSIBILITY_OVERLAY: 拒絕使用
TYPE_APPLICATION: 只能配合Activity在當前APP使用TYPE_APPLICATION_ATTACHED_DIALOG: 只能配合Activity在當前APP使用
TYPE_APPLICATION_MEDIA: 無法使用(什麼也不顯示)
TYPE_APPLICATION_PANEL: 只能配合Activity在當前APP使用(PopupWindow預設就是這個Type)
TYPE_APPLICATION_STARTING: 無法使用(什麼也不顯示)
TYPE_APPLICATION_SUB_PANEL: 只能配合Activity在當前APP使用TYPE_BASE_APPLICATION: 無法使用(什麼也不顯示)
TYPE_CHANGED: 只能配合Activity在當前APP使用
TYPE_INPUT_METHOD: 無法使用(直接崩潰)
TYPE_INPUT_METHOD_DIALOG: 無法使用(直接崩潰)
TYPE_KEYGUARD_DIALOG: 拒絕使用
TYPE_PHONE: 屬於懸浮窗(並且給一個Activity的話按下HOME鍵會出現看不到桌面上的圖示異常情況)
TYPE_TOAST: 不屬於懸浮窗, 但有懸浮窗的功能, 缺點是在Android2.3上無法接收點選事件
TYPE_SYSTEM_ALERT: 屬於懸浮窗, 但是會被禁止

其他的實戰文章

想要像銀聯一樣,在某Activity做到手機無法截圖,甚至是adb也拿不到,那麼可以在Activity中加入:

getWindow().addFlags(WindowManager.LayoutParams. FLAG_SECURE);

原始碼:

        /**
         視窗標誌:將視窗的內容視為安全的,防止視窗顯示在螢幕截圖中或在非安全顯示器上檢視。

         有關安全表面和安全顯示的更多詳細資訊,請參閱android.view.Display#FLAG_SECURE。
         */
        public static final int FLAG_SECURE             = 0x00002000;

參考文章