突破小米悬浮窗权限控制--不需要权限的悬浮窗

突破小米悬浮窗权限控制–不需要权限的悬浮窗

在上一篇文章讲了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
        | 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类型悬浮窗的权限封锁。

最后上一个小米系统示例图:

转载请注明出处!

时间: 2024-08-24 11:17:00

突破小米悬浮窗权限控制--不需要权限的悬浮窗的相关文章

asp.net core mvc权限控制:分配权限

前面的文章介绍了如何进行权限控制,即访问控制器或者方法的时候,要求当前用户必须具备特定的权限,但是如何在程序中进行权限的分配呢?下面就介绍下如何利用Microsoft.AspNetCore.Identity.EntityFrameworkCore框架进行权限分配. 在介绍分配方法之前,我们必须理解权限关系,这里面涉及到三个对象:用户,角色,权限,权限分配到角色,角色再分配到用户,当某个用户属于某个角色后,这个用户就具有了角色所包含的权限列表,比如现在有一个信息管理员角色,这个角色包含了信息删除权

Chapter6_访问权限控制_访问权限修饰词

Java中有四种访问权限,public,private,protected和包访问权限,它们是置于类中每一个成员之前的定义,无论是一个域还是一个方法,下面一一介绍. 一.包访问权限 如果不提供任何访问权限修饰词,这意味着是包访问权限.即当前包中的所有其他类对那个包访问权限的成员都有访问权限,但对于这个包之外的所有类,这个类是private的,所以处于一个编译单元中的所有类之间,都是可以互相访问的.包访问权限允许将包内所有的类组合起来,以便于它们之间可以轻松地相互访问.总的来说,取得对某成员的访问

权限控制:分配权限1

在介绍分配方法之前,我们必须理解权限关系,这里面涉及到三个对象:用户,角色,权限,权限分配到角色,角色再分配到用户,当某个用户属于某个角色后,这个用户就具有了角色所包含的权限列表,比如现在有一个信息管理员角色,这个角色包含了信息删除权限,当张三这个用户具有信息管理员角色后,张三就具备了信息删除的权限.在某些特殊场景下,权限也可以直接分配到用户,也就是说可以直接把某些特定的权限,绕过角色,直接分配给用户.Microsoft.AspNetCore.Identity.EntityFrameworkCo

shiro注解权限控制-5个权限注解

RequiresAuthentication: 使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证 RequiresGuest: 使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是"gust"身份,不需要经过认证或者在原先的session中存在记录. RequiresPermissions: 当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法.如果当前Subject不具有这样的权限,则方法不会被执

基于Vue实现后台系统权限控制

原文地址:http://refined-x.com/2017/08/29/基于Vue实现后台系统权限控制/,转载请注明出处. 用Vue/React这类双向绑定框架做后台系统再适合不过,后台系统相比普通前端项目除了数据交互更频繁以外,还有一个特别的需求就是对用户的权限控制,那么如何在一个Vue应用中实现权限控制呢?下面是我的一点经验. 权限控制是什么 在权限的世界里服务端提供的一切都是资源,资源可以由请求方法+请求地址来描述,权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配

简单的权限控制

手上的项目涉及到权限控制,但是权限,角色,资源访问都很简单,所以就没有写特么复杂,所以将每个用户的角色直接存储到了该用户的详细信息中.所以每个用户在登录系统时,在页面加载时判断该用户中角色属性值 具体代码如下 <% Yyry yyry = (Yyry)session.getAttribute("yyry"); int yyryids=0; String juese =""; boolean flag= false; if(yyry==null){ respon

基于资源名的MVC权限控制

在程序复杂程度不断上升的过程中,无可避免需要触碰到权限控制,而权限控制又与业务逻辑紧紧相关,市场上出现了大量的权限控制产品,而程序的开发,讲究去繁化简的抽象,在我的开发过程中,逐渐发现程序的权限控制核心不外乎两个方面:1.资源定位:2.访问控制列表.本文主要针对资源定位进行分析,并解决一些我所遇见过的问题.而在MVC上,MVC提供给我们了非常好的访问控制扩展机制,我们能够通过这些机制更好地控制系统权限. 在我们之前的开发中,针对ASP.NET下WebForm进行开发,很多人都采用了继承Page基

linux系统下的权限控制

 linux系统下的权限控制 1.文件权限 在我们的linux系统中,文件或目录的权限可以分为3种: r:4 读 w:2 写 x:1  执行 示例: 644:(4+2) (4)  (4) 第一个6:表示当前文件的拥有者的权限,6=4+2 可读可写权限 第二个4:表示当前文件的所属组权限,4=4 可读权限 第三个4:表示当前文件的组外权限,4=4 可读权限 2.查看文件权限的命令:(ls -l 或ll) 总共可以分为7大列: 第1列(分为10列): 1:文件的类型 ,-代表普通文件,d代表目录,l

asp.net core mvc权限控制:在视图中控制操作权限

在asp.net core mvc中提供了权限验证框架,前面的文章中已经介绍了如何进行权限控制配置,权限配置好后,权限验证逻辑自动就会执行,但是在某些情况下,我们可能需要在代码里或者视图中通过手工方式判断权限,我们现在就来介绍下具体的操作方法. 如果在控制器方法里想要判断当前用户是否具有某个权限,可以直接使用HttpContext.User.HasClaim(string cliamtype,string cliamvalue)方法进行判断,该方法返回bool类型,返回true表示具有权限,否则