Android悬浮窗遇到的那些坑

现在有很多应用都有悬浮窗功能,直播类应用的小窗播放,安全类应用的加速球等等,其实现方式都是通过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()即可防止被降级。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,391评论 25 707
  • 这篇博客主要介绍的是 Android 主流各种机型和各种版本的悬浮窗权限适配,但是由于碎片化的问题,所以在适配方面...
    Shawn_Dut阅读 10,236评论 15 44
  • 转载请注明出处:Android悬浮窗权限适配 悬浮窗相信大家都不陌生,比如360手机卫士的加速球,视频应用的小窗,...
    夏末m阅读 11,608评论 1 6
  • 文/不不不不不不热 我家有两只猫,一只黄色,一只花色。 黄色的是一只大猫,雄性,温驯、有责任感,有着厚重的气息。 ...
    不不不不不不热阅读 540评论 7 4
  • 今天遇到高中时的一位宿友,也是我的学长,看到他肿得夸张的脸庞,我有点儿惊叹。因为高中那会儿,用来形容他的词是“瘦不...
    灰鸽1号阅读 223评论 0 2