突破小米悬浮窗权限控制--不需要权限的悬浮窗
Posted 苦逼程序员_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了突破小米悬浮窗权限控制--不需要权限的悬浮窗相关的知识,希望对你有一定的参考价值。
突破小米悬浮窗权限控制–不需要权限的悬浮窗
在上一篇文章讲了android的Toast拓展,在原生Toast基础上对显示时长和显示动画做了二次封装,强化了Toast的部分功能。也分析了对于二次封装的ExToast设计原理,以及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
| 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类型悬浮窗的权限封锁。
最后上一个小米系统示例图:
转载请注明出处!
以上是关于突破小米悬浮窗权限控制--不需要权限的悬浮窗的主要内容,如果未能解决你的问题,请参考以下文章
Android WindowManager悬浮窗:不需要申请权限实现悬浮