美文网首页Androidandroid开发work
Android悬浮窗TYPE_TOAST小结: 源码分析

Android悬浮窗TYPE_TOAST小结: 源码分析

作者: Shawon | 来源:发表于2015-10-25 02:43 被阅读24730次

    前言

    Android无需权限显示悬浮窗, 兼谈逆向分析app这篇文章阅读量很大, 这篇文章是通过逆向分析UC浏览器的实现和兼容性处理来得到一个悬浮窗的实现小技巧, 但有很多问题没有弄明白, 比如为什么在API 18及以下TYPE_TOAST的悬浮窗无法接受触摸事件, 为什么使用TYPE_TOAST就不需要权限.

    期间@廖祜秋liaohuqiu_秋百万和我有较多探讨, 原文贴的一个demo android-UCToast也是他做的, 他也有写Android 悬浮窗的小结. 这几篇关于悬浮窗的文章, 是我和他共同探索的结果, 非常感谢.

    思路

    老实说一开始我是想看看整个事件的传播过程, 从EventHub开始, 到View.onTouchEvent, 想看看Android系统内事件分发, 不过由于绝大部分代码在Native层, 我并没有搞清楚.

    其实要想知道原因很简单, 只要grep一下TYPE_TOAST, 把每个用到的地方看一看, 自然就知道了, 但是恰好周末我手上没有源码, 只能在grepcode上面一个一个的查, 所以也花了不少时间.

    正文

    还是从最简单的地方开始, 我们调用了WindowManager.addView, WindowManager是个接口, 我们使用的是他的实现类WindowManagerImpl, 看看它的addView方法:

    @Override
    public void addView(View view, ViewGroup.LayoutParams    params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    

    mGlobalWindowManagerGlobal的实例, 再看看WindowManagerGlobal.addView:

        public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
            ......
            final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
            ......
    
            synchronized (mLock) {
                ......
                root = new ViewRootImpl(view.getContext(), display);
    
                view.setLayoutParams(wparams);
                ......
            }
    
            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                ......
        }
    

    代码中创建了一个ViewRootImpl, 调用了它的setView, 将我们要添加的view传入. 继续看ViewRootImpl.setView:

        public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
            synchronized (this) {
                if (mView == null) {
                    ......
                    mWindowAttributes.copyFrom(attrs);
                    if (mWindowAttributes.packageName == null) {
                        mWindowAttributes.packageName = mBasePackageName;
                    }
                    ......
                    try {
                        ......
                        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                getHostVisibility(), mDisplay.getDisplayId(),
                                mAttachInfo.mContentInsets, mInputChannel);
                    } catch (RemoteException e) {
                        ......
                        throw new RuntimeException("Adding window failed", e);
                    } finally {
                        ......
                    }
                    ......
                }
            }
        }
    

    对我们的分析来说最关键的代码是

    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
            getHostVisibility(), mDisplay.getDisplayId(),
            mAttachInfo.mContentInsets, mInputChannel);
    

    mWindowSession的类型是IWindowSession, mWindow的类型是IWindow.Stub, 这句代码就是利用AIDL进行IPC, 实际被调用的是Session.addToDisplay:

        @Override
        public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
                int viewVisibility, int displayId, Rect outContentInsets,
                InputChannel outInputChannel) {
            return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                    outContentInsets, outInputChannel);
        }
    

    mServiceWindowManagerService, 继续往下跟:

        public int addWindow(Session session, IWindow client, int seq,
                WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
                Rect outContentInsets, InputChannel outInputChannel) {
            int[] appOp = new int[1];
            int res = mPolicy.checkAddPermission(attrs, appOp);
            if (res != WindowManagerGlobal.ADD_OKAY) {
                return res;
            }
            ......
            final int type = attrs.type;
    
            synchronized(mWindowMap) {
                ......
    
                mPolicy.adjustWindowParamsLw(win.mAttrs);
                ......
            }
            ......
    
            return res;
        }
    

    mPolicy是标记为final的成员变量:

    final WindowManagerPolicy mPolicy = PolicyManager.makeNewWindowManager();
    

    继续看PolicyManager.makeNewWindowManager:

    public final class PolicyManager {
        private static final String POLICY_IMPL_CLASS_NAME =
            "com.android.internal.policy.impl.Policy";
    
        private static final IPolicy sPolicy;
    
        static {
            // Pull in the actual implementation of the policy at run-time
            try {
                Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
                sPolicy = (IPolicy)policyClass.newInstance();
            } catch (ClassNotFoundException ex) {
                throw new RuntimeException(
                        POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
            } catch (InstantiationException ex) {
                throw new RuntimeException(
                        POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(
                        POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
            }
        }
    
        // Cannot instantiate this class
        private PolicyManager() {}
    
        ......
        public static WindowManagerPolicy makeNewWindowManager() {
            return sPolicy.makeNewWindowManager();
        }
        ......
    }
    

    这里sPolicy是com.android.internal.policy.impl.Policy对象, 再看看它的makeNewWindowManager方法返回的是什么:

    public WindowManagerPolicy makeNewWindowManager() {
        return new PhoneWindowManager();
    }
    

    现在我们知道mPolicy实际上是PhoneWindowManager, 那么

    int res = mPolicy.checkAddPermission(attrs, appOp);
    

    实际调用的代码是:

        @Override
        public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
            int type = attrs.type;
    
            outAppOp[0] = AppOpsManager.OP_NONE;
    
            if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
                    || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
                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.
                    break;
                case TYPE_DREAM:
                case TYPE_INPUT_METHOD:
                case TYPE_WALLPAPER:
                case TYPE_PRIVATE_PRESENTATION:
                    // 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 (mContext.checkCallingOrSelfPermission(permission)
                        != PackageManager.PERMISSION_GRANTED) {
                    return WindowManagerGlobal.ADD_PERMISSION_DENIED;
                }
            }
            return WindowManagerGlobal.ADD_OKAY;
        }
    

    我截取的是4.4_r1的代码, 我们最关心的部分其实一直没有变, 那就是TYPE_TOAST根本没有做权限检查, 直接break出去了, 最后返回WindowManagerGlobal.ADD_OKAY.

    不需要权限显示悬浮窗的原因已经找到了, 接着刚才addWindow方法的分析, 继续看下面一句:

    mPolicy.adjustWindowParamsLw(win.mAttrs);
    

    也就是PhoneWindowManager.adjustWindowParamsLw, 注意这里我给出了三个版本的实现, 一个是2.0到2.3.7实现的版本, 一个是4.0.1到4.3.1实现的版本, 一个是4.4实现的版本:

    //Android 2.0 - 2.3.7 PhoneWindowManager
        public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
            switch (attrs.type) {
                case TYPE_SYSTEM_OVERLAY:
                case TYPE_SECURE_SYSTEM_OVERLAY:
                case TYPE_TOAST:
                    // These types of windows can't receive input events.
                    attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                            | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
                    break;
            }
        }
    
    //Android 4.0.1 - 4.3.1 PhoneWindowManager
        public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
            switch (attrs.type) {
                case TYPE_SYSTEM_OVERLAY:
                case TYPE_SECURE_SYSTEM_OVERLAY:
                case TYPE_TOAST:
                    // These types of windows can't receive input events.
                    attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                            | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
                    attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
                    break;
            }
        }
    
    
    //Android 4.4 PhoneWindowManager
        @Override
        public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
            switch (attrs.type) {
                case TYPE_SYSTEM_OVERLAY:
                case TYPE_SECURE_SYSTEM_OVERLAY:
                    // These types of windows can't receive input events.
                    attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                            | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
                    attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
                    break;
            }
        }
    

    grepcode上没有3.x的代码, 我也没查具体是什么, 没必要考虑3.x.
    可以看到, 在4.0.1以前, 当我们使用TYPE_TOAST, Android会偷偷给我们加上FLAG_NOT_FOCUSABLEFLAG_NOT_TOUCHABLE, 4.0.1开始, 会额外再去掉FLAG_WATCH_OUTSIDE_TOUCH, 这样真的是什么事件都没了. 而4.4开始, TYPE_TOAST被移除了, 所以从4.4开始, 使用TYPE_TOAST的同时还可以接收触摸事件和按键事件了, 而4.4以前只能显示出来, 不能交互.

    API level 18及以下使用TYPE_TOAST无法接收触摸事件的原因也找到了.

    尾声

    原文发的时候很多事情没搞清楚, 后来文章编辑了十几次, 加上这篇文章, 基本上把所有的疑问都搞明白了. 嗯, 关于这个神奇的悬浮窗的事情应该到这里就结束了.

    本人水平有限, 如有错误, 欢迎指正, 以免误导他人

    相关文章

      网友评论

      • 5e6b48d66986:这样子处理在miui 8及以上还是不生效啊,还是需要在系统那里授权才能显示,请问下有什么解决办法吗
        andyhaha007:同問!
        Hilbert1:同问
      • 926ca8dc3953:你好,你写的很棒,想了很久的问题被您解决了,我想问下你的demo可以分享学习一下么?
        Hilbert1:同问
        5e6b48d66986:这样子处理在miui 8及以上还是不生效啊,还是需要在系统那里授权才能显示,请问下有什么解决办法吗
        Shawon: @bug树下没有我 抱歉,demo是公司以前做的一个app里的代码
      • 3674d0183b56:先留言
      • 1琥珀川1:提醒一下 在android7.1 上使用TYPE_TOAST是有问题的 会闪现一下后不再显示
        cb23d8c18bfe:刚刚也在用TYPE_TOAST显示悬浮窗,发现6.0是好用的程序在7.1上面就会莫名其妙消失,换成TYPE_PHONE就好了,不知道有没有谁分析处理里面差异原因
        0f7d096211a6:@李芬_190a 使用type_phone
        26eb22d80ef4:发现了,不知道怎么处理,你有好办法么?
      • 032a54b29c34:在android 6.0上,如果使用了TYPE_TOAST显示一个长期的悬浮球在桌面上,此时一个新应用弹出权限申请窗口,那么就会弹出一个窗口,提示“screen overlay detected”,“to change this permission setting,you first have to turn off the screen overlay from settings>app”,这个如何解决
      • c10eb95bd71b:大神,TYPE_TOAST在小米5的MIUI8不给权限还是无法显示啊,原生安卓6.0倒是不需要权限可以显示,希望大神再研究一下
        csycc:@MycroftWong 小米手机上用TYPE_TOAST怎么判断没法显示悬浮窗呢?
        MycroftWong:@术巨酷 一样,遇到了这个问题,是因为有些系统改了framework层,在MIUI8中使用TYPE_TOAST仍然需要该权限。我参考九游的:如果没法显示悬浮窗,弹出一个toast让用户自己去开启权限。
        0b9a6b25fbfd:@术巨酷 遇到一样的问题,你是怎么解决的啊?强制让用户给予权限么? :joy:
      • f5e0eef91b37:棒棒的
      • coolrandy:你好,我有个问题,正常情况下悬浮窗都是无法覆盖状态栏的,采用设置flags并结合type的设置可以在三星和华为机子上覆盖掉状态栏,但是小米上面却没办法覆盖,请问一下这种情况有解决方案吗?
        bbf9d6558420:我现在也是要做这个需求 你解决了吗 能加我QQ交流下吗 79528323
      • tonnycose:怎么让悬浮框之外的View 也能获取焦点呢。
        1. 不设置WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 则外部view 不能获取焦点,且软键盘不能弹起。
        2. 如果设置WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 则悬浮框的view不能获取焦点,不能监听 focuse 变化。
        请问有什么好的解决办法吗?
      • android_Joe:学习了,路过 :smile:
      • xiaobu:TYPE_TOAST 怎么让悬浮窗避让键盘呢(在键盘之下)?
        平凡的_世界:@xiaobu 您好,悬浮窗权限避让键盘的问题,我也遇到,,不过是用了TYPE_PHONE 这个权限的问题,目前键盘这块有什么好的办法没有呢~~??
      • 8ca0e33dd47a:那怎么解决TYPE_TOAST在4.0以下版本获取不了焦点的问题呢?
        Shawon: @8ca0e33dd47a 4.4以下使用需要权限的type
      • LITTLEDREAM:條理還是很清晰的,不錯,贊一個!
      • Bot王:我还是不明白谷歌这样改的意义。
        Shawon: @英姿琉璃 Google有一些交互是使用toast进行的,比如某些Google app删除item会弹toast,toast上有撤销按钮,后来又改成snackbar了

      本文标题:Android悬浮窗TYPE_TOAST小结: 源码分析

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