Android无需权限显示悬浮窗, 兼谈逆向分析app
如下图, 截图是在使用Chrome时截的, 但是屏幕顶部却有UC的view浮在屏幕上. 我使用的是小米, 我并没有给UC授悬浮窗权限, 所以我看到这个悬浮窗时是很震惊的.
悬浮窗原理
做过悬浮窗功能的人都知道, 要想显示悬浮窗, 要有一个服务运行在后台, 通过getSystemService(Context.WINDOW_SERVICE)
拿到WindowManager, 然后向其中addView, addView第二个参数是一个WindowManager.LayoutParams
, WindowManager.LayoutParams中有一个成员type, 有各种值, 一般设置成TYPE_PHONE就可以悬浮在很多view的上方了, 但是调用这个方法需要申请android.permission.SYSTEM_ALERT_WINDOW
权限, 在很多机型上, 这个权限的名字叫悬浮窗, 比如小米手机上默认是禁用这个权限的, 有些恶意app会用这个权限弹广告, 而且很难追查是哪个应用弹的. 如果这个权限被禁用, 那么结果就是悬浮窗无法展示, 比如有道词典的复制查词功能, 在小米手机上经常没用, 其实是用户没有授权, 而且应用也没有引导用户给它打开授权.
那么他是怎么实现的呢?有人就进行了逆向分析。
过程省略。。。直接说结论
验证
实际测试了一下, 将type设置成TYPE_TOAST果然有奇效, 不需要android.permission.SYSTEM_ALERT_WINDOW
权限就能显示一个悬浮窗.
之前我一直以为调用了系统WindowManager.addView
需要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毫无限制, 应该引入标记来限制开发者.
处理兼容性
在这篇文章刚刚公布的时候, 就有同学反馈悬浮窗无法接收事件, 刚开始我并没有特别在意, 在廖祜秋大神做了一个demo之后, 这篇文章阅读量又涨了不少, 随即收到更多反馈事件的问题, 我今天晚上借了台MIUI V5 4.2.2实测了一下, 这台机器上UC的快速搜索功能也无法正常使用.
在这个ROM上表现为:
使用TYPE_PHONE
这类需要权限的type
时, 只有在app处于前台时能显示悬浮窗, 且能正常接受触摸事件. 如果在应用详情里面授悬浮窗权限, 则工作完全正常.
(这里是MIUI V5对悬浮窗的特殊处理, 现在的ROM, 包括MIUI V6上, 如果不授权, 无法显示任何悬浮窗)
使用TYPE_TOAST
这个不需要权限的type
时, 悬浮窗正常显示, 但不能接受触摸事件.
原因是: API level >= 19
的时候, 使用TYPE_TOAST
, 能接受到触摸事件。其他情况使用TYPE_PHONE
(需要权限).
可能是为了规避在低版本TYPE_TOAST
不能接受事件的问题.
关于针对源代码的分析, 请看Android悬浮窗使用TYPE_TOAST的小结
实测效果
我之前写的一个app有悬浮窗播放功能, 支持拖动窗口和点击暂停, 关闭窗口等等, 在4.4.4上实测功能正常.
感谢微博上关注的大神廖祜秋, 他做了个demo, 虽然交互和UC不同, 可以参考一下实现.
关于这个, 他也写了一篇Android 悬浮窗的小结
其他补充
评论区的浮海大虾同学有更多补充如下:
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
:属于悬浮窗,但是会被禁止
大家如果对具体的逆向过程有兴趣,请参考原文: http://www.jianshu.com/p/167fd5f47d5c