悬浮窗权限突破及兼容性处理

转载请注明出处:
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 源码分析

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值