美文网首页AndroidAndroid
Android悬浮窗遇到的那些坑

Android悬浮窗遇到的那些坑

作者: 哦嘿嘿哈哈吼 | 来源:发表于2017-02-15 19:10 被阅读11899次

现在有很多应用都有悬浮窗功能,直播类应用的小窗播放,安全类应用的加速球等等,其实现方式都是通过WindowManager.addView()来添加的,最近公司也要求在产品中加入小窗功能,在此记录一下开发中遇到的问题。

为什么有些应用可以不请求悬浮窗权限就显示悬浮窗

这个问题在这两篇文章(Android无需权限显示悬浮窗, 兼谈逆向分析appAndroid悬浮窗TYPE_TOAST小结: 源码分析)中已经做了很好的解释。
简单来说就是设置WindowManager.LayoutParams.type = TYPE_TOAST即可绕过权限,因为在view添加之前系统执行了一个检查权限的操作PhoneWindowManager.checkAddPermission(),虽然经历了很多Android版本,但是我们关心的那部分一直没有什么大变化,就是当type == TYPE_TOAST的时候switch语句直接break了,从而跳过了接下来的权限检查。

源码版本Android 7.0

    public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
        ··· ···
        String permission = null;
        switch (type) {
            //TYPE_TOAST作为高于ApplicationWindow的类型,却跳过了权限检查
            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)) {
                ··· ···
                //在这里使用了AppOpsManager去检查权限
                final int mode = mAppOpsManager.checkOpNoThrow(outAppOp[0], callingUid,
                        attrs.packageName);
                ··· ···
            }
                ··· ···
        }
        return WindowManagerGlobal.ADD_OKAY;
    }

这里需要注意的一点是TYPE_TOAST在最新的Android 7.1.1上已经被Google制裁了,只允许添加一个,并且在API 25之后会直接崩溃,具体代码可以查看这里,看一下WindowManager的diff就知道了,不过6.0以上Google已经提供了通用方法来开启悬浮窗权限,下文会提到,推荐大家去引导用户开启,不要使用暴力的解决方式。

Android 8.0对于悬浮窗又有所修改,窗口类型改为TYPE_APPLICATION_OVERLAY即可。

提醒窗口

使用 [SYSTEM_ALERT_WINDOW] 权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:

  • [TYPE_PHONE]
  • [TYPE_PRIORITY_PHONE]
  • [TYPE_SYSTEM_ALERT]
  • [TYPE_SYSTEM_OVERLAY]
  • [TYPE_SYSTEM_ERROR]
    相反,应用必须使用名为 [TYPE_APPLICATION_OVERLAY] 的新窗口类型。
    使用 [TYPE_APPLICATION_OVERLAY] 窗口类型显示应用的提醒窗口时,请记住新窗口类型的以下特性:
  • 应用的提醒窗口始终显示在状态栏和输入法等关键系统窗口的下面。
  • 系统可以移动使用 [TYPE_APPLICATION_OVERLAY] 窗口类型的窗口或调整其大小,以改善屏幕显示效果。
  • 通过打开通知栏,用户可以访问设置来阻止应用显示使用 [TYPE_APPLICATION_OVERLAY] 窗口类型显示的提醒窗口。

悬浮窗权限检查

具体代码见GitHub
在Android 6.0以上,系统提供了API来检查悬浮窗权限,那么在小于6.0的机器上该怎么检查权限呢?其实,如果你看过了WindowManager添加view的源码,系统已经告诉你答案了,在PhoneWindowManager.checkAddPermission()中,系统使用了一个叫AppOpsManager的类,最终调用其中的checkOp()方法来检查权限,但是这个方法本身是隐藏的,所以只能通过反射的方式来调用,另外还需要注意AppOpsManager是API 19才添加的,对于低于这个版本的系统并不能用此方法来检查权限。

    public static final int OP_SYSTEM_ALERT_WINDOW = 24;

    public int checkOp(int op, int uid, String packageName) {
        try {
            int mode = mService.checkOperation(op, uid, packageName);
            if (mode == MODE_ERRORED) {
                throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
            }
            return mode;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

经过试验,4.4以下的机器一般都可以直接添加悬浮窗,如果有特殊情况,只能单独适配了。
检查权限的代码如下所示:

    public static boolean checkFloatWindowPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            return Settings.canDrawOverlays(MainApplication.getInstance());
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            //AppOpsManager添加于API 19
            return checkOps();
        } else {
            //4.4以下一般都可以直接添加悬浮窗
            return true;
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    private static boolean checkOps() {
        try {
            Object object = MainApplication.getInstance().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] = 24;
            arrayOfObject1[1] = Binder.getCallingUid();
            arrayOfObject1[2] = MainApplication.getInstance().getPackageName();
            int m = (Integer) method.invoke(object, arrayOfObject1);
            //4.4至6.0之间的非国产手机,例如samsung,sony一般都可以直接添加悬浮窗
            return m == AppOpsManager.MODE_ALLOWED || !RomUtils.isDomesticSpecialRom();
        } catch (Exception ignore) {
        }
        return false;
    }

大家应该会注意到我在最后额外判断了一下!RomUtils.isDomesticSpecialRom()是否为国产Rom,因为有些三星,索尼之类的手机检查结果为MODE_IGNORED,系统本身又没有设置悬浮窗权限的页面,只会在安装应用的时候询问用户是否允许悬浮窗,如果用户禁用,除非卸载重装就再也没有开启的方法了。
因此对于这类非国产Rom手机,我统一使用了TYPE_TOAST的方法来强制开启悬浮窗以保证功能的正常使用。
此外,在一些手机上,比如oppo,代码返回有悬浮窗权限,但是实际使用过程中APP切到后台悬浮窗就消失(与TYPE_APPLICATION行为一致),这种必须要在手机自带的管家中授权悬浮窗才行,可以判断Rom版本提示用户自行开启。

悬浮窗权限设置引导

使用黑科技的方式绕过悬浮窗权限的检查是很多App的常用做法,但是我本身不太喜欢这种方式,我更倾向于引导用户自行决定是否开启悬浮窗权限,但是各个厂商的悬浮窗设置页面不尽相同,那么该怎么跳转到这个页面呢?
我的做法是手动找到开启悬浮窗的界面,然后执行adb shell dumpsys actvity activities,就可以看到授权界面的包名和Activity名称,接下来在应用中构造Intent跳转即可。

小米悬浮窗授权页面查询结果

跳转悬浮窗设置页面在6.0以后也有了通用方法,这边有一个坑是魅族6.0以上跳转这个页面会自动退出,还是需要跳转魅族自己的权限设置页面,跟6.0之前一样,出了魅族以外其他机型目前都可以正常跳转。

    private static void applyCommonPermission(Context context) {
        try {
            Class clazz = Settings.class;
            Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
            Intent intent = new Intent(field.get(null).toString());
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.setData(Uri.parse("package:" + context.getPackageName()));
            context.startActivity(intent);
        } catch (Exception e) {
            Toast.makeText(context, "进入设置页面失败,请手动设置", Toast.LENGTH_LONG).show();
        }
    }

至于Android 6.0之前的手机就要根据Rom一个一个进行适配了,因为有人问我这些Rom的判断规则是怎么来的,假如出了个新手机怎么判断,这里说一下判断Rom的思路,具体的代码就不贴了,自行去GitHub上看。
思路非常简单,我们知道Android系统里存放了一些配置文件,比如

init.rc
default.prop
/system/build.prop

这些文件里记录了很多系统属性,我们可以通过adb shell getprop来读取这些信息,找到Rom厂商所特有的字段来作为判断依据,还是以小米手机为例,执行命令后可以看到:

小米手机getprop查询结果

这些跟Rom版本有关的字段就可以拿来作为我们的判断依据。

使用悬浮窗播放视频,切换至桌面时出现音画不同步现象

这个现象的出现与使用的播放器有一定关系,我们使用的是ijkplayer。当解码方式为ijk硬解时,在6.0系统上切至桌面就会出现音画不同步,系统硬解和软解时则没有出现这种情况,主要原因是切换至桌面时系统判断应用不在前台,对应用做了降级处理,资源分配上优先级很低。解决方法也很简单,只需要开启一个前台服务Service.startForeground()即可防止被降级。

相关文章

  • Android悬浮窗遇到的那些坑

    现在有很多应用都有悬浮窗功能,直播类应用的小窗播放,安全类应用的加速球等等,其实现方式都是通过WindowMana...

  • 机型阿坑2.0

    机型坑 Android 6.0 中,使用 SYSTEM_ALERT_WINDOW 绘制的悬浮窗不能含有 eleva...

  • Android 悬浮窗-开箱即用

    开箱即用的 Android 悬浮窗 开箱即用的 Android 悬浮窗 FloatWindowX 1. 需要权限 ...

  • 悬浮窗+Dialog中的坑

    功能需求: 点击悬浮窗中的按钮显示一个Dialog 踩坑之路: 先是Context 崩溃,log:android....

  • 关于android 悬浮窗和自启动的设置, 以及获取系统的信息

    关于android 悬浮窗和自启动的设置, 以及获取系统的信息 标签(空格分隔):Android 悬浮窗 对于是否...

  • Android权限适配(二)

    本文接 Android权限适配(一) 悬浮窗权限 悬浮窗权限同样属于上文中说到的特殊权限。 悬浮窗代码的设置 要使...

  • Android 悬浮窗实现

    Android悬浮窗实现中需要注意的两点是 1、Android 6.0之后的悬浮窗动态申请 2、Window 的y...

  • android 悬浮窗

    安卓悬浮窗的书写,我们分为几个步骤: 1.添加悬浮窗权限 2.书写悬浮窗代码,搭建悬浮窗布局 3.判断悬浮窗权限是...

  • Android 悬浮窗<下>

    上篇Android 悬浮窗<上>已经将Window、WindowManager做了一些简单介绍,并且将悬浮窗适配做...

  • Android7.1悬浮窗自动消失解决办法

    问题描述 日前在做一个悬浮窗需求悬浮窗,在Android 7.1模拟器上悬浮窗会显示几秒钟就自动消失。 问题分析 ...

网友评论

  • 键盘上的麒麟臂:你好,我想问下为什么要用TYPE_TOAST
  • shuaizi:请教下博主, 我在8.0 手机上, activity的onresume方法 中使用Settings.canDrawOverlays(MainApplication.getInstance(),判断权限。 我手动开启的悬浮球权限,但判断还是false。延时0.5秒判断就是true。请问有没有遇到这样的问题?
  • Android出海:推荐个开源的悬浮窗项目,挺好的,https://github.com/YoungBill/Android-FloatWindow
  • 38d763c07940:大哥,看到你这篇文章实在是解决了我的难题,不过,不知道你有没有发现OPPO 6.0 以上的手机是无法跳转的,我找了很久都没有搞定,大神有没有解决办法
    哦嘿嘿哈哈吼:@没事就吃红烧猪蹄 好,等我让测试搞一个“前后两千万,照亮你的美”看看
    38d763c07940:@哦嘿嘿哈哈吼 是这样的,我这边找了一下好多都没有提OPPO手机6.0以上的问题,没有办法跳转,只好提示用户手动设置了,如果大神你碰到了并解决了的话,希望更新一下,谢谢啦
    哦嘿嘿哈哈吼:我手头没有这样的机器,如果是官方方式无法跳转的话,可能跟魅族一样,需要跳转到他们指定的权限设置页面,你可以找一下,可能是放到了预制的安全助手里,看一下那个界面能不能跳,不能的话只能提示用户手动设置了
  • 3555bfafbf99:特意登录账号来给你这篇文章点赞,悬浮窗权限适配 这里总结的非常好!
    不过跳转到小米权限管理,不用那么麻烦 ,可以通过

    Intent intent = new Intent();
    intent.setAction("miui.intent.action.APP_PERM_EDITOR");
    intent.addCategory(Intent.CATEGORY_DEFAULT);
    intent.putExtra("extra_pkgname", packageName);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);

    这个方法 小米官方文档 有说明:[http://dev.xiaomi.com/docs/appsmarket/technical_docs/adaptation_FAQ/]
    哦嘿嘿哈哈吼:@HarleyQuin_71c6 非常感谢,稍后我更新一下
    3555bfafbf99:第十条

本文标题:Android悬浮窗遇到的那些坑

本文链接:https://www.haomeiwen.com/subject/qmsxwttx.html