1. 程式人生 > >突破小米懸浮窗許可權控制--不需要許可權的懸浮窗

突破小米懸浮窗許可權控制--不需要許可權的懸浮窗

突破小米懸浮窗許可權控制–不需要許可權的懸浮窗

在上一篇文章講了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型別懸浮窗的許可權封鎖。

最後上一個小米系統示例圖:

小米懸浮窗示例

轉載請註明出處!