关于Android应用中的悬浮窗(一)——权限

现在越来越多的Android APP都有悬浮窗的功能,公司项目中最新的需求也需要加入悬浮窗的功能,这次的功能是指在应用内的悬浮窗(而不是系统级别的悬浮窗)。悬浮窗功能的时候,整体分了2个部分:
- 悬浮窗的权限申请问题
- 悬浮窗的实现问题


前言

因为Android碎片化的问题,所以第一开始适配这边的权限问题的时候头皮发麻。在参考了一下文章后,总结了思路
https://blog.csdn.net/self_study/article/details/52859790
https://www.jianshu.com/p/167fd5f47d5c

悬浮窗的原理

我们知道window的type分为3类:
1. Application windows
应用窗口,比如Activity的窗口;
FIRST_APPLICATION_WINDOW与LAST_APPLICATION_WINDOW`之间,是常用的顶层应用程序窗口,须将token设置成Activity的token;
2. Sub-windows

子窗口,依赖父窗口,比如PopupWindow;
FIRST_SUB_WINDOWLAST_SUB_WINDOW之间,与顶层窗口相关联,需将token设置成它所附着宿主窗口的token;
3. System windows
系统窗口,比如状态栏、Toast、悬浮窗;
FIRST_SYSTEM_WINDOWLAST_SYSTEM_WINDOW之间,不能用于应用程序,使用时需要有特殊权限,它是特定的系统功能才能使用。
悬浮窗其实是通过通过getSystemService(Context.WINDOW_SERVICE)拿到WindowManager, 然后向其中addView, addView第二个参数是一个WindowManager.LayoutParams。如果要做系统级别的悬浮窗(应用退出后,悬浮窗仍然显示在桌面上),获取context.getSystemService(Context.WINDOW_SERVICE),我们要做的是应用内的悬浮窗,所以直接获取Activit级别的WindowManager即可,activity.getSystemService(Context.WINDOW_SERVICE),layoutParams.type是设置悬浮窗的类型的。

悬浮窗的适配

WindowManager windowManager = (WindowManager) activity.getSystemService(WINDOW_SERVICE);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); 
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
               ...(设置layoutParams其他的参数)
windowManager.addView(view,layoutParams);

添加悬浮窗时,是需要申请权限的。
通常我们会有两种做法,一是在开启悬浮窗之前,引导用户去开启权限。权限一般路径
设置--应用程序--应用--权限管理--在其他应用的上层显示 ,个别手机路径略有不同。
二是绕过权限管理,直接开启权限

在addView之前会有一个PhoneWindowManager.checkAddPermission()检查权限的操作
7.1源码中checkAddPermission()里面中

...
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:
            case TYPE_QS_DIALOG:
                // 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 (android.Manifest.permission.SYSTEM_ALERT_WINDOW.equals(permission)) {
                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.checkOpNoThrow(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:
                        try {
                            ApplicationInfo appInfo = mContext.getPackageManager()
                                    .getApplicationInfo(attrs.packageName,
                                            UserHandle.getUserId(callingUid));
                            // Don't crash legacy apps
                            if (appInfo.targetSdkVersion < Build.VERSION_CODES.M) {
                                return WindowManagerGlobal.ADD_OKAY;
                            }
                        } catch (PackageManager.NameNotFoundException e) {
                            /* ignore */
                        }
                        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;
            }
...

从以下代码可知道 TYPE_PHONETYPE_SYSTEM_ALERT 是有权限的

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;
android.Manifest.permission.SYSTEM_ALERT_WINDOW

而这个权限是需要我们去动态申请和手动开启的(这个权限是默认关闭的)。

既然 TYPE_PHONE 是需要判断权限,那 TYPE_TOAST 是不需要判断权限的,使用 TYPE_TOAST 不就行了?
使用 TYPE_TOAST 4.4之前的版本接收不到触摸事件和点击事件了 ,所以我们只能放弃了。

Android 4.4以下是不需要申请权限,可以直接添加。4.4以上是需要申请权限的,4.4-5.1检查权限是使用AppOpsManager中的checkOp()方法的,6.0的权限申请是单独管理的,和4.4-5.1的方法不一样。所以我们申请权限是需要分开申请。

private boolean checkFloatPermission(Context context) {
        if (Build.VERSION.SDK_INT >= 23) {//6.0以上
            boolean result = false;
            try {
                Class clazz = Settings.class;
                Method canDrawOverlays = clazz.getDeclaredMethod("canDrawOverlays", Context.class);
                result = (boolean) canDrawOverlays.invoke(null, context);
                Log.e(TAG, "checkFloatPermission:-->" + result);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {//4.4-5.1
            return getAppOps(context);
        } else {//4.4以下
            return true;
        }
    }

4.4-5.1版本的权限判断是通过反射来获取的

private static boolean getAppOps(Context context) {
        try {
            Object object = context.getSystemService(context.APP_OPS_SERVICE);
            if (object == null) {
                return false;
            }
            Class localClass = object.getClass();
            Class[] arrayOfClass = new Class[3];
            arrayOfClass[0] = Integer.TYPE;
            arrayOfClass[1] = Integer.TYPE;
            arrayOfClass[2] = String.class;
            Method method = localClass.getMethod("checkOp", arrayOfClass);
            if (method == null) {
                return false;
            }
            Object[] arrayOfObject1 = new Object[3];
            arrayOfObject1[0] = Integer.valueOf(24);
            arrayOfObject1[1] = Integer.valueOf(Binder.getCallingUid());
            arrayOfObject1[2] = context.getPackageName();
            int m = ((Integer) method.invoke(object, arrayOfObject1)).intValue();
            return m == AppOpsManager.MODE_ALLOWED;
        } catch (Exception e) {
            Log.e(TAG, "permissions judge: -->" + e.toString());
        }
        return false;
    }

如果权限没有开启,需要我们提示并引导用户去开始权限。

        if (Build.VERSION.SDK_INT >= 23) {//6.0以上
            try{
                Intent  intent=new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                startActivityForResult(intent, CHECKPRESSMISSON_CODE);
            }catch (Exception e)
            {
                e.printStackTrace();
            }
        } 

6.0的权限是单独的,跳转到开启悬浮窗页面后,在onActivityResult()中对不同情况做不同的逻辑处理。

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == CHECKPRESSMISSON_CODE) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (Settings.canDrawOverlays(this)) {
                    Log.e(TAG, "已经打开悬浮窗权限");
                    //进行下一步逻辑处理
                } else {
                    //提示用户需要开启
                }
            }
        }
    }

4.4-5.1的手机就提示并引导用户去设置页面打开悬浮窗的权限

Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
intent.setData(Uri.fromParts("package", getPackageName(), null));

对Android应用中悬浮窗的权限问题简单的总结了下,而设置悬浮窗的问题可以去看下一篇
https://mp.csdn.net/mdeditor/80468278

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值