前言
Android無需許可權顯示懸浮窗, 兼談逆向分析app這篇文章閱讀量很大, 這篇文章是通過逆向分析UC瀏覽器的實現和相容性處理來得到一個懸浮窗的實現小技巧, 但有很多問題沒有弄明白, 比如為什麼在API 18及以下TYPE_TOAST
的懸浮窗無法接受觸控事件, 為什麼使用TYPE_TOAST
就不需要許可權.
期間@廖祜秋liaohuqiu_秋百萬和我有較多探討, 原文貼的一個demo android-UCToast也是他做的, 他也有寫Android 懸浮窗的小結. 這幾篇關於懸浮窗的文章, 是我和他共同探索的結果, 非常感謝.
思路
老實說一開始我是想看看整個事件的傳播過程, 從EventHub
開始, 到View.onTouchEvent
, 想看看Android系統內事件分發, 不過由於絕大部分程式碼在Native層, 我並沒有搞清楚.
其實要想知道原因很簡單, 只要grep一下TYPE_TOAST, 把每個用到的地方看一看, 自然就知道了, 但是恰好週末我手上沒有原始碼, 只能在grepcode上面一個一個的查, 所以也花了不少時間.
正文
還是從最簡單的地方開始, 我們呼叫了WindowManager.addView
, WindowManager是個介面, 我們使用的是他的實現類WindowManagerImpl
, 看看它的addView
方法:
1 2 3 4 |
@Override public void addView(View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); } |
mGlobal
是WindowManagerGlobal
的例項, 再看看WindowManagerGlobal.addView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ...... final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; ...... synchronized (mLock) { ...... root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); ...... } // do this last because it fires off messages to start doing things try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { // BadTokenException or InvalidDisplayException, clean up. ...... } |
程式碼中建立了一個ViewRootImpl
, 呼叫了它的setView
, 將我們要新增的view傳入. 繼續看ViewRootImpl.setView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { ...... mWindowAttributes.copyFrom(attrs); if (mWindowAttributes.packageName == null) { mWindowAttributes.packageName = mBasePackageName; } ...... try { ...... res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel); } catch (RemoteException e) { ...... throw new RuntimeException("Adding window failed", e); } finally { ...... } ...... } } } |
對我們的分析來說最關鍵的程式碼是
1 2 3 |
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel); |
mWindowSession的型別是IWindowSession
, mWindow的型別是IWindow.Stub
, 這句程式碼就是利用AIDL進行IPC, 實際被呼叫的是Session.addToDisplay
:
1 2 3 4 5 6 7 |
@Override public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, InputChannel outInputChannel) { return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outInputChannel); } |
mService
是WindowManagerService
, 繼續往下跟:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public int addWindow(Session session, IWindow client, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, InputChannel outInputChannel) { int[] appOp = new int[1]; int res = mPolicy.checkAddPermission(attrs, appOp); if (res != WindowManagerGlobal.ADD_OKAY) { return res; } ...... final int type = attrs.type; synchronized(mWindowMap) { ...... mPolicy.adjustWindowParamsLw(win.mAttrs); ...... } ...... return res; } |
mPolicy
是標記為final的成員變數:
1 |
final WindowManagerPolicy mPolicy = PolicyManager.makeNewWindowManager(); |
繼續看PolicyManager.makeNewWindowManager
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public final class PolicyManager { private static final String POLICY_IMPL_CLASS_NAME = "com.android.internal.policy.impl.Policy"; private static final IPolicy sPolicy; static { // Pull in the actual implementation of the policy at run-time try { Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME); sPolicy = (IPolicy)policyClass.newInstance(); } catch (ClassNotFoundException ex) { throw new RuntimeException( POLICY_IMPL_CLASS_NAME + " could not be loaded", ex); } catch (InstantiationException ex) { throw new RuntimeException( POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex); } catch (IllegalAccessException ex) { throw new RuntimeException( POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex); } } // Cannot instantiate this class private PolicyManager() {} ...... public static WindowManagerPolicy makeNewWindowManager() { return sPolicy.makeNewWindowManager(); } ...... } |
這裡sPolicy是com.android.internal.policy.impl.Policy
物件, 再看看它的makeNewWindowManager
方法返回的是什麼:
1 2 3 |
public WindowManagerPolicy makeNewWindowManager() { return new PhoneWindowManager(); } |
現在我們知道mPolicy
實際上是PhoneWindowManager
, 那麼
1 |
int res = mPolicy.checkAddPermission(attrs, appOp); |
實際呼叫的程式碼是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
@Override public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) { int type = attrs.type; outAppOp[0] = AppOpsManager.OP_NONE; if (type WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { return WindowManagerGlobal.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_DREAM: case TYPE_INPUT_METHOD: case TYPE_WALLPAPER: case TYPE_PRIVATE_PRESENTATION: // 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; outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW; break; default: permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; } if (permission != null) { if (mContext.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { return WindowManagerGlobal.ADD_PERMISSION_DENIED; } } return WindowManagerGlobal.ADD_OKAY; } |
我擷取的是4.4_r1的程式碼, 我們最關心的部分其實一直沒有變, 那就是TYPE_TOAST
根本沒有做許可權檢查, 直接break出去了, 最後返回WindowManagerGlobal.ADD_OKAY
.
不需要許可權顯示懸浮窗的原因已經找到了, 接著剛才addWindow
方法的分析, 繼續看下面一句:
1 |
mPolicy.adjustWindowParamsLw(win.mAttrs); |
也就是PhoneWindowManager.adjustWindowParamsLw
, 注意這裡我給出了三個版本的實現, 一個是2.0到2.3.7實現的版本, 一個是4.0.1到4.3.1實現的版本, 一個是4.4實現的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
//Android 2.0 - 2.3.7 PhoneWindowManager public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) { switch (attrs.type) { case TYPE_SYSTEM_OVERLAY: case TYPE_SECURE_SYSTEM_OVERLAY: case TYPE_TOAST: // These types of windows can't receive input events. attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; break; } } //Android 4.0.1 - 4.3.1 PhoneWindowManager public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) { switch (attrs.type) { case TYPE_SYSTEM_OVERLAY: case TYPE_SECURE_SYSTEM_OVERLAY: case TYPE_TOAST: // These types of windows can't receive input events. attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; break; } } //Android 4.4 PhoneWindowManager @Override public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) { switch (attrs.type) { case TYPE_SYSTEM_OVERLAY: case TYPE_SECURE_SYSTEM_OVERLAY: // These types of windows can't receive input events. attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; break; } } |
grepcode上沒有3.x的程式碼, 我也沒查具體是什麼, 沒必要考慮3.x.
可以看到, 在4.0.1以前, 當我們使用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以前只能顯示出來, 不能互動.
API level 18及以下使用TYPE_TOAST無法接收觸控事件的原因也找到了.
尾聲
原文發的時候很多事情沒搞清楚, 後來文章編輯了十幾次, 加上這篇文章, 基本上把所有的疑問都搞明白了. 嗯, 關於這個神奇的懸浮窗的事情應該到這裡就結束了.
本人水平有限, 如有錯誤, 歡迎指正, 以免誤導他人