[置顶] 突破小米悬浮窗权限控制--不需要权限的悬浮窗
2016-07-06 16:52
411 查看
突破小米悬浮窗权限控制–不需要权限的悬浮窗
在上一篇文章讲了Android的Toast拓展,在原生Toast基础上对显示时长和显示动画做了二次封装,强化了Toast的部分功能。也分析了对于二次封装的ExToast设计原理,以及Toast的关键点。如果不了解的可以看看下面的链接。Toast拓展–自定义显示时间和动画
常用悬浮窗与Toast
之前分析过,Toast其实就是系统悬浮窗的一种,那它跟常用的系统悬浮窗有什么区别呢?先看一下常用的Andoird系统悬浮窗写法:
// 获取应用的Context mContext = context.getApplicationContext(); // 获取WindowManager mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); mView = setUpView(context); final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); // 类型 params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; int flags = WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; params.flags = flags; params.format = PixelFormat.TRANSLUCENT; params.width = LayoutParams.MATCH_PARENT; params.height = LayoutParams.MATCH_PARENT; params.gravity = Gravity.CENTER; mWindowManager.addView(mView, params);
再看看在Toast源码里面的写法关键代码:
final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = com.android.internal.R.style.Animation_Toast; // 类型 params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON 4000 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; ... // 获取应用的context Context context = mView.getContext().getApplicationContext(); // 获取WindowManager mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); ... if (mView.getParent() != null) { mWM.removeView(mView); } mWM.addView(mView, mParams);
上面的两段代码大致流程都是一样的:创建WindowManager.LayoutParams做窗口的配置->通过context获取WindowManager服务->通过WindowManager服务添加悬浮窗View
主要的不同点在于WindowManager.LayoutParams的type。
WindowManager.LayoutParams的type有很多种,包括各种系统对话框,锁屏窗口,电话窗口等等,但这些窗口基本上都是需要权限的。
而我们平时使用的Toast,并不需要权限就能显示,那就可以尝试直接把悬浮窗的类型设成TYPE_TOAST,来定制一个不需要权限的悬浮窗。
下面是demo代码:
import android.content.Context; import android.graphics.PixelFormat; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.TextView; public class ADToast implements View.OnTouchListener { Context mContext; WindowManager.LayoutParams params; WindowManager mWM; View mView; private float mTouchStartX; private float mTouchStartY; private float x; private float y; public ADToast(Context context){ this.mContext = context; params = new WindowManager.LayoutParams(); params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = R.style.anim_view; // 悬浮窗类型,整个demo的关键点 params.type = WindowManager.LayoutParams.TYPE_TOAST; params.gravity = Gravity.LEFT | Gravity.TOP; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mView = inflate.inflate(R.layout.float_tips_layout, null); mView.setOnTouchListener(this); } public void show(){ TextView tv = (TextView)mView.findViewById(R.id.message); tv.setText("悬浮窗"); if (mView.getParent() != null) { mWM.removeView(mView); } mWM.addView(mView, params); } public void hide(){ if(mView!=null){ mWM.removeView(mView); } } public void setText(String text){ TextView tv = (TextView)mView.findViewById(R.id.message); tv.setText(text); } private void updateViewPosition(){ //更新浮动窗口位置参数 params.x=(int) (x-mTouchStartX); params.y=(int) (y-mTouchStartY); mWM.updateViewLayout(mView, params); //刷新显示 } @Override public boolean onTouch(View v, MotionEvent event) { //获取相对屏幕的坐标,即以屏幕左上角为原点 x = event.getRawX(); y = event.getRawY(); Log.i("currP", "currX"+x+"====currY"+y); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //捕获手指触摸按下动作 //获取相对View的坐标,即以此View左上角为原点 mTouchStartX = event.getX(); mTouchStartY = event.getY(); Log.i("startP","startX"+mTouchStartX+"====startY"+mTouchStartY); break; case MotionEvent.ACTION_MOVE: //捕获手指触摸移动动作 updateViewPosition(); break; case MotionEvent.ACTION_UP: //捕获手指触摸离开动作 updateViewPosition(); break; } return true; } }
float_tips_layout.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" android:background="@android:color/black"> <TextView android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginStart="32dp" android:layout_marginEnd="32dp" android:lineSpacingExtra="16dp" android:maxLines="2" android:textColor="@android:color/white" android:shadowColor="#bbffffff" android:shadowRadius="2.75" android:textSize="40sp" /> </LinearLayout>
然而,这种使用方式,在小米最新的MIUI8系统上行不通!
使用N5原生6.0系统测试通过,使用一加3测试通过,使用魅族pro5测试通过。只有小米MIUI8,对Toast类型悬浮窗做了权限控制。实测在MIUI8中,打开悬浮窗权限可以显示这种Toast类型的悬浮窗。而使用原生Toast类,却不需要权限就可以显示,看来小米的系统在framework层对Toast类型的权限做了特殊处理。
但是,只要Toast能显示,就说明肯定有方法绕过去。最好的方法,就是把小米改动的framework层代码扒出来,看看原生Toast和自定义Toast类型悬浮窗在权限处理上的区别是什么,但是有一定的难度,在研究了一天无果后,先使用了第二种更容易实现的方法。
既然原生Toast不需要权限,那我们就在原生Toast的基础上继续封装拓展。上一篇Toast拓展文章已经对Toast的二次封装解释的比较详细了,下面直接上Demo代码。
import android.content.Context; import android.content.res.Resources; import android.os.Handler; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.TextView; import android.widget.Toast; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class MiExToast implements View.OnTouchListener { private static final String TAG = "ExToast"; public static final int LENGTH_ALWAYS = 0; public static final int LENGTH_SHORT = 2; public static final int LENGTH_LONG = 4; private Toast toast; private Context mContext; private int mDuration = LENGTH_SHORT; private int animations = -1; private boolean isShow = false; private Object mTN; private Method show; private Method hide; private WindowManager mWM; private WindowManager.LayoutParams params; private View mView; private float mTouchStartX; private float mTouchStartY; private float x; private float y; private Handler handler = new Handler(); public MiExToast(Context context){ this.mContext = context; if (toast == null) { toast = new Toast(mContext); } LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mView = inflate.inflate(R.layout.float_tips_layout, null); mView.setOnTouchListener(this); } private Runnable hideRunnable = new Runnable() { @Override public void run() { hide(); } }; /** * Show the view for the specified duration. */ public void show(){ if (isShow) return; TextView tv = (TextView)mView.findViewById(R.id.message); tv.setText("悬浮窗"); toast.setView(mView); initTN(); try { show.invoke(mTN); } catch (InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); } isShow = true; //判断duration,如果大于#LENGTH_ALWAYS 则设置消失时间 if (mDuration > LENGTH_ALWAYS) { handler.postDelayed(hideRunnable, mDuration * 1000); } } /** * Close the view if it's showing, or don't show it if it isn't showing yet. * You do not normally have to call this. Normally view will disappear on its own * after the appropriate duration. */ public void hide(){ if(!isShow) return; try { hide.invoke(mTN); } catch (InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); } isShow = false; } public void setView(View view) { toast.setView(view); } public View getView() { return toast.getView(); } /** * Set how long to show the view for. * @see #LENGTH_SHORT * @see #LENGTH_LONG * @see #LENGTH_ALWAYS */ public void setDuration(int duration) { mDuration = duration; } public int getDuration() { return mDuration; } public void setMargin(float horizontalMargin, float verticalMargin) { toast.setMargin(horizontalMargin,verticalMargin); } public float getHorizontalMargin() { return toast.getHorizontalMargin(); } public float getVerticalMargin() { return toast.getVerticalMargin(); } public void setGravity(int gravity, int xOffset, int yOffset) { toast.setGravity(gravity,xOffset,yOffset); } public int getGravity() { return toast.getGravity(); } public int getXOffset() { return toast.getXOffset(); } public int getYOffset() { return toast.getYOffset(); } public static MiExToast makeText(Context context, CharSequence text, int duration) { Toast toast = Toast.makeText(context,text,Toast.LENGTH_SHORT); MiExToast exToast = new MiExToast(context); exToast.toast = toast; exToast.mDuration = duration; return exToast; } public static MiExToast makeText(Context context, int resId, int duration) throws Resources.NotFoundException { return makeText(context, context.getResources().getText(resId), duration); } public void setText(int resId) { setText(mContext.getText(resId)); } public void setText(CharSequence s) { toast.setText(s); } public int getAnimations() { return animations; } public void setAnimations(int animations) { this.animations = animations; } private void initTN() { 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); params = (WindowManager.LayoutParams) tnParamsField.get(mTN); params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; /**设置动画*/ if (animations != -1) { params.windowAnimations = animations; } /**调用tn.show()之前一定要先设置mNextView*/ Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView"); tnNextViewField.setAccessible(true); tnNextViewField.set(mTN, toast.getView()); mWM = (WindowManager)mContext.getApplicationContext().getSystemService(Context.WINDOW_SERVICE); } catch (Exception e) { e.printStackTrace(); } setGravity(Gravity.LEFT | Gravity.TOP,0 ,0); } private void updateViewPosition(){ //更新浮动窗口位置参数 params.x=(int) (x-mTouchStartX); params.y=(int) (y-mTouchStartY); mWM.updateViewLayout(toast.getView(), params); //刷新显示 } @Override public boolean onTouch(View v, MotionEvent event) { //获取相对屏幕的坐标,即以屏幕左上角为原点 x = event.getRawX(); y = event.getRawY(); Log.i("currP", "currX"+x+"====currY"+y); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //捕获手指触摸按下动作 //获取相对View的坐标,即以此View左上角为原点 mTouchStartX = event.getX(); mTouchStartY = event.getY(); Log.i("startP","startX"+mTouchStartX+"====startY"+mTouchStartY); break; case MotionEvent.ACTION_MOVE: //捕获手指触摸移动动作 updateViewPosition(); break; case MotionEvent.ACTION_UP: //捕获手指触摸离开动作 updateViewPosition(); break; } return true; } }
example:
MiExToast miToast = new MiExToast(getApplicationContext()); miToast.setDuration(MiExToast.LENGTH_ALWAYS); miToast.setAnimations(R.style.anim_view); miToast.show();
上面的Demo类是基于上一篇文章Toast拓展–自定义显示时间和动画,进行再次拓展做出来的,它只是一个Demo,并不是工具类,不能直接拿来使用。
下面根据这个Demo,我们来分析它的原理。
下面有三个关键点:
1. Toast是可以自定义View的
2. 悬浮窗的触摸需要修改WindowManager.LayoutParams.flags,设置WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
3. 刷新悬浮窗,只需要获得WindowManager实例,调用updateViewLayout并传入View和LayoutParams即可
经过上一篇文章的讲解,对于Toast的LayoutParams实例我们可以通过反射获得,并且给他设置上可触摸的flag。关注上面代码的initTN()方法,获得的LayoutParams实例需要保持引用,因为后面还需要用上。
Field tnParamsField = mTN.getClass().getDeclaredField("mParams"); tnParamsField.setAccessible(true); params = (WindowManager.LayoutParams) tnParamsField.get(mTN); params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
然后是对二次封装的Demo类MiExToast里面的Toast实例设置View。这个应该很容易理解,Toast是可以自定义View的,设置自己的View作为悬浮窗。同时,可以对View添加一些自定义的Touch事件,在这个Demo中用户可以随意拖动悬浮窗。
public void init(){ mView = inflate.inflate(R.layout.float_tips_layout, null); mView.setOnTouchListener(this); toast.setView(mView); } @Override public boolean onTouch(View v, MotionEvent event) { //获取相对屏幕的坐标,即以屏幕左上角为原点 x = event.getRawX(); y = event.getRawY(); ... return true; }
最后就是对悬浮窗的更新,只需要通过context获取到WindowManager,即可调用updateViewLayout对悬浮窗进行更新。
private WindowManager.LayoutParams params; private void updateViewPosition(){ mWM.updateViewLayout(toast.getView(), params); //刷新显示 }
大致原理就是这样,借助原生Toast显示自定义的悬浮窗,越过小米MIUI8对于Toast类型悬浮窗的权限封锁。
最后上一个小米系统示例图:
转载请注明出处!
相关文章推荐
- mysql数据库迁移
- 对比ShareSDK,友盟三方登录/三方分享
- Java中的arraycopy
- 黑马程序员_Java基础_我的day03学习笔记
- ubuntu14.04 pip从1.5.4 升级到8.x最新版
- git 常用命令
- 直播平台的高并发架构设计4-案例和总结
- 使用shiro框架,AuthorizationInfo方法没有被执行的问题
- 人生第一个shell脚本
- Maven项目构建报错
- jmeter 脚本增强(检查点、集合点)
- Tomcat集群,Nginx集群,Tomcat+Nginx 负载均衡配置,Tomcat+Nginx集群
- 领土划分
- Mac上更新Ruby
- java回调函数
- java中list转数组操作
- cisco路由器密码恢复
- ibatis 批处理
- 循环神经网络(RNN, Recurrent Neural Networks)介绍
- U-Boot 的移植入门(2)——nand flash识别与操作