悬浮窗权限突破及兼容性处理
2017-03-20 17:27
239 查看
转载请注明出处:
http://blog.csdn.net/brucehurrican/article/details/64129000
通常业务需要在桌面显示悬浮窗来展示某些功能,如360的悬浮球,MIUI 系统显示悬浮窗。
常用的悬浮窗代码写法如下
type 类型可以选择的有 TYPE_PHONE,TYPE_SYSTEM_ALERT和 TYPE_TOAST,对于 TYPE_PHONE,TYPE_SYSTEM_ALERT 需要在 AndroidManifest.xml 中声明权限
uses-permission android:name=”android.permission.SYSTEM_ALERT_WINDOW”
否则会报错:
permission denied for this window type
原因是在 com.android.server.policy.PhoneWindowManager 类中
方法 checkAddPermission中
从源码中可以看到当声明为 TYPE_PHONE, TYPE_SYSTEM_ALERT等类型时,需要进行权限检查。
这种通过自定义 view, FWSmallView 的方式定制悬浮窗样式,通过 WindowManager addView() 显示自定义的悬浮窗。这种方法在原生类的手机中是可以显示的,但是介于国内第三方厂商修改的 ROM,有的机型在用户未授权的情况下只能在应用内显示或者不显示,如果要在桌面显示的话,必须要用户授权,如华为荣耀系列,小米/红米手机,魅族手机,都需要用户的手动授权才能显示悬浮窗。需要注意的是,以小米/红米手机使用的 MIUI 系统默认在安装 app 时,悬浮窗功能默认是关闭的,而且开启该功能的操作路径比较深,MIUI 开启步骤有两种方法,
安全中心-授权管理-应用权限管理-XX应用-显示悬浮窗,打开
设置-更多应用-XX应用-权限管理-显示悬浮窗,打开
两种方法对于用户来说,操作都有点麻烦。
通过在代码中判断当前 view 是否是显示状态
view.getVisibility()
获取到的值是0,即View.VISIBLE,就是说,当 AndroidManifest.xml 中声明了权限的场景下,view.getVisibility()获取到的是View.VISIBLE,但是在MIUI系统中却看不到悬浮窗,此时权限管理中的悬浮窗是关闭的。推测是 MIUI针对悬浮窗功能做了自己的限制,这个限制不是来源于 android 系统的限制,完全是 MIUI应用层的限制。
那么有没有什么方法不可以绕过 MIUI的用户授权呢?答案是肯定的,上面介绍的WindowManager.LayoutParams类型中还有一个没有说,那就是 TYPE_TOAST,通过设置 TYPE_TOAST可以让悬浮窗功能正常显示不需要 MIUI用户授权。
对于使用 TYPE_TOAST类型的悬浮窗,有个版本限制需要注意,如果用户系统版本> 19时,可以接收点击/触摸事件,如果<19时则不能接收点击/触摸事件。
还有一种方式通过调用Toast 的 setView() 方法传入自定义布局文件,以自定义 toast 形式来显示悬浮窗,在MIUI、华为荣耀、魅族手机上是不需要用户授权的。但是,这种自定义 toast 的方式,有两个缺点
1. 不能控制悬浮窗显示的时间,只有两个参数 LENGTH_SHORT,LENGTH_LONG,对应的时间分别为,3.5秒,2秒。
对应源码如下:
com.android.server.notification.NotificationManagerService
2.自定义的 toast 也不能处理点击/触摸事件。
既然自定义的 toast 不行的话,只能试试修改系统的 toast 来达到显示悬浮窗的目的了。
通过反射的方式来替换系统 toast 的显示方式,达到自由控制悬浮窗显示/关闭,并处理点击/触摸事件。
代码如下:
通过上述方法反射获取toast 源码中的 show,hide,mTN。
show.invoke(mTN);
显示悬浮窗
hide.invoke(mTN);
关闭悬浮窗
并且,这个关闭的时间是可以自己控制的。
悬浮窗免授权显示的介绍就到这了。下面我来介绍下,怎样处理用户的点击/触摸事件了。
如果想要完成像360悬浮球那样,用户可以拖动小球的功能,需要重写 onTouchEvent 方法,源码如下:
这里我遇到了几个坑,
1.在 SONY的一款机型上发现的,测试时,不论怎样点击布局上的按钮,悬浮窗都无法实现点击功能。通过调试发现,每次触摸时,在MotionEvent.ACTION_UP时,SONY系统自动会将当前触摸的坐标进行偏移,导致按照(xDownInScreen == xInScreen) && (yDownInScreen == yInScreen)来判断用户点击事件时始终为 false。这样就达不到“点击”的效果了。通过加上Math.abs(xDownInScreen - xInScreen) <= 10 && Math.abs(yDownInScreen - yInScreen) <= 10来判断,如果为true,则认为用户是“点击”而非“拖动”,10只是个经验值。
2.在部分三星手机上出现,无法响点击/应触摸事件,解决办法是在 initTN() 前加入如下代码:
3.在部分 MIUI 8开发版中,出现了拖动悬浮窗时系统崩溃的情况。定位代码时发现在调用
windowManager.updateViewLayout(this, mParams);
出现 crash,即使我将该调用
try{}
catch(Exception e){}
都捕获不到异常,推测,可能是 MIUI 8开发版中,在处理界面刷新时不允许开发者 hook 系统 toast,并刷新当前窗口。如果你知道原因的话,希望告诉我下,thx。
demo传送门
参考资料:
小米手机显示悬浮窗
仿360悬浮窗
悬浮窗小结
toast 源码分析
http://blog.csdn.net/brucehurrican/article/details/64129000
通常业务需要在桌面显示悬浮窗来展示某些功能,如360的悬浮球,MIUI 系统显示悬浮窗。
常用的悬浮窗代码写法如下
private static void showSmall1(Context context) { WindowManager windowManager = getWindowManager(context); int screenWidth = windowManager.getDefaultDisplay().getWidth(); int screenHeight = windowManager.getDefaultDisplay().getHeight(); if (null == smallWindow) { smallWindow = new FWSmallView(context); if (smallWindowParams == null) { smallWindowParams = new WindowManager.LayoutParams(); smallWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE; smallWindowParams.format = PixelFormat.RGBA_8888; smallWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; smallWindowParams.gravity = Gravity.LEFT | Gravity.TOP; smallWindowParams.width = FWSmallView.viewWidth; smallWindowParams.height = FWSmallView.viewHeight; smallWindowParams.x = screenWidth; smallWindowParams.y = screenHeight / 2; } smallWindow.setParams(smallWindowParams); windowManager.addView(smallWindow, smallWindowParams); } }
type 类型可以选择的有 TYPE_PHONE,TYPE_SYSTEM_ALERT和 TYPE_TOAST,对于 TYPE_PHONE,TYPE_SYSTEM_ALERT 需要在 AndroidManifest.xml 中声明权限
uses-permission android:name=”android.permission.SYSTEM_ALERT_WINDOW”
否则会报错:
permission denied for this window type
原因是在 com.android.server.policy.PhoneWindowManager 类中
方法 checkAddPermission中
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) { int type = attrs.type; outAppOp[0] = AppOpsManager.OP_NONE; if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) || (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) || (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) { return WindowManagerGlobal.ADD_INVALID_TYPE; } if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) { // Window manager will make sure these are okay. 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. outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW; break; case TYPE_DREAM: case TYPE_INPUT_METHOD: case TYPE_WALLPAPER: case TYPE_PRIVATE_PRESENTATION: case TYPE_VOICE_INTERACTION: case TYPE_ACCESSIBILITY_OVERLAY: // 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 (permission == android.Manifest.permission.SYSTEM_ALERT_WINDOW) { final int callingUid = Binder.getCallingUid(); // system processes will be automatically allowed privilege to draw if (callingUid == Process.SYSTEM_UID) { return WindowManagerGlobal.ADD_OKAY; } // check if user has enabled this operation. SecurityException will be thrown if // this app has not been allowed by the user final int mode = mAppOpsManager.checkOp(outAppOp[0], callingUid, attrs.packageName); switch (mode) { case AppOpsManager.MODE_ALLOWED: case AppOpsManager.MODE_IGNORED: // although we return ADD_OKAY for MODE_IGNORED, the added window will // actually be hidden in WindowManagerService return WindowManagerGlobal.ADD_OKAY; case AppOpsManager.MODE_ERRORED: return WindowManagerGlobal.ADD_PERMISSION_DENIED; default: // in the default mode, we will make a decision here based on // checkCallingPermission() if (mContext.checkCallingPermission(permission) != PackageManager.PERMISSION_GRANTED) { return WindowManagerGlobal.ADD_PERMISSION_DENIED; } else { return WindowManagerGlobal.ADD_OKAY; } } } if (mContext.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { return WindowManagerGlobal.ADD_PERMISSION_DENIED; } } return WindowManagerGlobal.ADD_OKAY; }
从源码中可以看到当声明为 TYPE_PHONE, TYPE_SYSTEM_ALERT等类型时,需要进行权限检查。
这种通过自定义 view, FWSmallView 的方式定制悬浮窗样式,通过 WindowManager addView() 显示自定义的悬浮窗。这种方法在原生类的手机中是可以显示的,但是介于国内第三方厂商修改的 ROM,有的机型在用户未授权的情况下只能在应用内显示或者不显示,如果要在桌面显示的话,必须要用户授权,如华为荣耀系列,小米/红米手机,魅族手机,都需要用户的手动授权才能显示悬浮窗。需要注意的是,以小米/红米手机使用的 MIUI 系统默认在安装 app 时,悬浮窗功能默认是关闭的,而且开启该功能的操作路径比较深,MIUI 开启步骤有两种方法,
安全中心-授权管理-应用权限管理-XX应用-显示悬浮窗,打开
设置-更多应用-XX应用-权限管理-显示悬浮窗,打开
两种方法对于用户来说,操作都有点麻烦。
通过在代码中判断当前 view 是否是显示状态
view.getVisibility()
获取到的值是0,即View.VISIBLE,就是说,当 AndroidManifest.xml 中声明了权限的场景下,view.getVisibility()获取到的是View.VISIBLE,但是在MIUI系统中却看不到悬浮窗,此时权限管理中的悬浮窗是关闭的。推测是 MIUI针对悬浮窗功能做了自己的限制,这个限制不是来源于 android 系统的限制,完全是 MIUI应用层的限制。
那么有没有什么方法不可以绕过 MIUI的用户授权呢?答案是肯定的,上面介绍的WindowManager.LayoutParams类型中还有一个没有说,那就是 TYPE_TOAST,通过设置 TYPE_TOAST可以让悬浮窗功能正常显示不需要 MIUI用户授权。
对于使用 TYPE_TOAST类型的悬浮窗,有个版本限制需要注意,如果用户系统版本> 19时,可以接收点击/触摸事件,如果<19时则不能接收点击/触摸事件。
还有一种方式通过调用Toast 的 setView() 方法传入自定义布局文件,以自定义 toast 形式来显示悬浮窗,在MIUI、华为荣耀、魅族手机上是不需要用户授权的。但是,这种自定义 toast 的方式,有两个缺点
1. 不能控制悬浮窗显示的时间,只有两个参数 LENGTH_SHORT,LENGTH_LONG,对应的时间分别为,3.5秒,2秒。
对应源码如下:
com.android.server.notification.NotificationManagerService
static final int LONG_DELAY = 3500; // 3.5 seconds static final int SHORT_DELAY = 2000; // 2 seconds private void scheduleTimeoutLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); }
2.自定义的 toast 也不能处理点击/触摸事件。
既然自定义的 toast 不行的话,只能试试修改系统的 toast 来达到显示悬浮窗的目的了。
通过反射的方式来替换系统 toast 的显示方式,达到自由控制悬浮窗显示/关闭,并处理点击/触摸事件。
代码如下:
private void initTN() { int screenWidth = windowManager.getDefaultDisplay().getWidth(); int screenHeight = windowManager.getDefaultDisplay().getHeight(); try { Field tnField = toast.getClass().getDeclaredField("mTN"); tnField.setAccessible(true); mTN = tnField.get(toast); show = mTN.getClass().getMethod("show"); hide = mTN.getClass().getMethod("hide"); Field tnParamsField = mTN.getClass().getDeclaredField("mParams"); tnParamsField.setAccessible(true); if (null == mParams) { mParams = (WindowManager.LayoutParams) tnParamsField.get(mTN); mParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; /**设置动画*/ mParams.windowAnimations = android.R.anim.fade_in; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT){ mParams.type = WindowManager.LayoutParams.TYPE_PHONE; } mParams.x = screenWidth; mParams.y = screenHeight / 2; } /**调用tn.show()之前一定要先设置mNextView*/ Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView"); tnNextViewField.setAccessible(true); tnNextViewField.set(mTN, toast.getView()); } catch (Exception e) { e.printStackTrace(); } toast.setGravity(Gravity.LEFT | Gravity.TOP,mParams.x ,mParams.y); }
通过上述方法反射获取toast 源码中的 show,hide,mTN。
show.invoke(mTN);
显示悬浮窗
hide.invoke(mTN);
关闭悬浮窗
并且,这个关闭的时间是可以自己控制的。
悬浮窗免授权显示的介绍就到这了。下面我来介绍下,怎样处理用户的点击/触摸事件了。
如果想要完成像360悬浮球那样,用户可以拖动小球的功能,需要重写 onTouchEvent 方法,源码如下:
public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 手指按下时记录必要数据,纵坐标的值都需要减去状态栏高度 xInView = event.getX(); yInView = event.getY(); xDownInScreen = event.getRawX(); yDownInScreen = event.getRawY() - getStatusBarHeight(); xInScreen = event.getRawX(); yInScreen = event.getRawY() - getStatusBarHeight(); break; case MotionEvent.ACTION_MOVE: xInScreen = event.getRawX(); yInScreen = event.getRawY() - getStatusBarHeight(); // 手指移动的时候更新小悬浮窗的位置 updateViewPosition(); break; case MotionEvent.ACTION_UP: // 如果手指离开屏幕时,xDownInScreen和xInScreen相等,且yDownInScreen和yInScreen相等,则视为触发了单击事件。 if ((xDownInScreen == xInScreen) && (yDownInScreen == yInScreen)) { openBigWindow(); } else if (Math.abs(xDownInScreen - xInScreen) <= 10 && Math.abs(yDownInScreen - yInScreen) <= 10) { openBigWindow(); } LogUtils.i("isShow->"+FWmanager.isWindowShowing()); break; default: break; } return true; }
这里我遇到了几个坑,
1.在 SONY的一款机型上发现的,测试时,不论怎样点击布局上的按钮,悬浮窗都无法实现点击功能。通过调试发现,每次触摸时,在MotionEvent.ACTION_UP时,SONY系统自动会将当前触摸的坐标进行偏移,导致按照(xDownInScreen == xInScreen) && (yDownInScreen == yInScreen)来判断用户点击事件时始终为 false。这样就达不到“点击”的效果了。通过加上Math.abs(xDownInScreen - xInScreen) <= 10 && Math.abs(yDownInScreen - yInScreen) <= 10来判断,如果为true,则认为用户是“点击”而非“拖动”,10只是个经验值。
2.在部分三星手机上出现,无法响点击/应触摸事件,解决办法是在 initTN() 前加入如下代码:
view.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 手指按下时记录必要数据,纵坐标的值都需要减去状态栏高度 xInView = event.getX(); yInView = event.getY(); xDownInScreen = event.getRawX(); yDownInScreen = event.getRawY() - getStatusBarHeight(); xInScreen = event.getRawX(); yInScreen = event.getRawY() - getStatusBarHeight(); break; case MotionEvent.ACTION_MOVE: xInScreen = event.getRawX(); yInScreen = event.getRawY() - getStatusBarHeight(); // 手指移动的时候更新小悬浮窗的位置 updateViewPosition(); break; case MotionEvent.ACTION_UP: // 如果手指离开屏幕时,xDownInScreen和xInScreen相等,且yDownInScreen和yInScreen相等,则视为触发了单击事件。 if ((xDownInScreen == xInScreen) && (yDownInScreen == yInScreen)) { openBigWindow(); } else if (Math.abs(xDownInScreen - xInScreen) <= 10 && Math.abs(yDownInScreen - yInScreen) <= 10) { openBigWindow(); } LogUtils.i("isShow->" + FWmanager.isWindowShowing()); break; default: break; } return true; } });
3.在部分 MIUI 8开发版中,出现了拖动悬浮窗时系统崩溃的情况。定位代码时发现在调用
windowManager.updateViewLayout(this, mParams);
出现 crash,即使我将该调用
try{}
catch(Exception e){}
都捕获不到异常,推测,可能是 MIUI 8开发版中,在处理界面刷新时不允许开发者 hook 系统 toast,并刷新当前窗口。如果你知道原因的话,希望告诉我下,thx。
demo传送门
参考资料:
小米手机显示悬浮窗
仿360悬浮窗
悬浮窗小结
toast 源码分析
相关文章推荐
- 悬浮球(多机型悬浮窗权限设置,状态栏适配,可自动或手动设置大小,点击跳转WebView,拖拽处理)
- [置顶] 突破小米悬浮窗权限控制--不需要权限的悬浮窗
- 帧动画的大小设置与悬浮窗权限system_alert_window 6.0后需要单独处理
- 微软某个补丁引起DCOMCNFG无法对后安装的COM进行权限设置的处理
- Report Service 为用户“NT AUTHORITY、NETWORK SERVICE”授予的权限不足,无法进行此操作。(rsAccessDenied)处理
- 对1433端口SA权限的再突破
- 控件按钮提示,及权限的处理
- 突破FSO限制夺取系统权限
- [转贴]Guest权限突破
- ASP.NET DEVELOPMENT SERVER 未能开始侦听端口xxxxx以一种访问权限不允许的方式做了一个访问套接字的尝试--错误处理
- 使用继承来处理禁止客户端页面缓存和检查权限等功能
- 突破Java异常处理规则
- 突破Applet的权限限制
- vfat格式的挂载和权限处理,编辑fstab
- 使用webshell突破虚拟主机权限设置的一般思路
- 巧妙设置Sybase用户权限来处理进程
- 对1433端口SA权限的再突破- -
- jive2.5论坛Cache处理之更新---增加用户权限
- Guest权限突破8法(整理)
- AD如何委派权限可以让域用户有加入域的权限|突破10次限制