Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 1299|回复: 0

Android 禁止截屏、录屏 — 解决PopupWindow无法禁止录屏问题

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-4-14 12:21:10 | 显示全部楼层 |阅读模式

    项目开发中,为了用户信息的安全,会有禁止页面被截屏、录屏的需求。
    这类资料,在网上有很多,一般都是通过设置Activity的Flag解决,如:

    //禁止页面被截屏、录屏
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
    

    这种设置可解决一般的防截屏、录屏的需求。
    如果页面中有弹出Popupwindow,在录屏视频中的效果是:

    非Popupwindow区域为黑色
    但Popupwindow区域仍然是可以看到的
    

    如下面两张Gif图所示:

    未设置FLAG_SECURE,录屏的效果,如下图(git图片中间的水印忽略):

    普通界面录屏效果.gif

    设置了FLAG_SECURE之后,录屏的效果,如下图(git图片中间的水印忽略):
    界面仅设置了FLAG_SECURE.gif(图片中间的水印忽略)

    原因分析

    看到了上面的效果,我们可能会有疑问PopupWindow不像Dialog有自己的window对象,而是使用WindowManager.addView方法将View显示在Activity窗体上的。那么,Activity已经设置了FLAG_SECURE,为什么录屏时还能看到PopupWindow?

    我们先通过getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);来分析下源码:

    1、Window.java

    //window布局参数
    private final WindowManager.LayoutParams mWindowAttributes =
            new WindowManager.LayoutParams();
    
    //添加标识
    public void addFlags(int flags) {
            setFlags(flags, flags);
        }
    
    //通过mWindowAttributes设置标识
    public void setFlags(int flags, int mask) {
            final WindowManager.LayoutParams attrs = getAttributes();
            attrs.flags = (attrs.flags&~mask) | (flags&mask);
            mForcedWindowFlags |= mask;
            dispatchWindowAttributesChanged(attrs);
        }
    
    //获得布局参数对象,即mWindowAttributes
    public final WindowManager.LayoutParams getAttributes() {
            return mWindowAttributes;
        }
    

    通过源码可以看到,设置window属性的源码非常简单,即:通过window里的布局参数对象mWindowAttributes设置标识即可。

    2、PopupWindow.java

    //显示PopupWindow
    public void showAtLocation(View parent, int gravity, int x, int y) {
            mParentRootView = new WeakReference<>(parent.getRootView());
            showAtLocation(parent.getWindowToken(), gravity, x, y);
        }
    
    //显示PopupWindow
    public void showAtLocation(IBinder token, int gravity, int x, int y) {
            if (isShowing() || mContentView == null) {
                return;
            }
    
            TransitionManager.endTransitions(mDecorView);
    
            detachFromAnchor();
    
            mIsShowing = true;
            mIsDropdown = false;
            mGravity = gravity;
            
            //创建Window布局参数对象
            final WindowManager.LayoutParams p =createPopupLayoutParams(token);
            preparePopup(p);
    
            p.x = x;
            p.y = y;
    
            invokePopup(p);
        }
    
    //创建Window布局参数对象
    protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
            final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
            p.gravity = computeGravity();
            p.flags = computeFlags(p.flags);
            p.type = mWindowLayoutType;
            p.token = token;
            p.softInputMode = mSoftInputMode;
            p.windowAnimations = computeAnimationResource();
            if (mBackground != null) {
                p.format = mBackground.getOpacity();
            } else {
                p.format = PixelFormat.TRANSLUCENT;
            }
            if (mHeightMode < 0) {
                p.height = mLastHeight = mHeightMode;
            } else {
                p.height = mLastHeight = mHeight;
            }
            if (mWidthMode < 0) {
                p.width = mLastWidth = mWidthMode;
            } else {
                p.width = mLastWidth = mWidth;
            }
            p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH
                    | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
            p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));
            return p;
        }
    
    //将PopupWindow添加到Window上
    private void invokePopup(WindowManager.LayoutParams p) {
            if (mContext != null) {
                p.packageName = mContext.getPackageName();
            }
    
            final PopupDecorView decorView = mDecorView;
            decorView.setFitsSystemWindows(mLayoutInsetDecor);
    
            setLayoutDirectionFromAnchor();
    
            mWindowManager.addView(decorView, p);
    
            if (mEnterTransition != null) {
                decorView.requestEnterTransition(mEnterTransition);
            }
        }
    

    通过PopupWindow的源码分析,我们不难看出,在调用showAtLocation时,会单独创建一个WindowManager.LayoutParams布局参数对象,用于显示PopupWindow,而该布局参数对象上并未设置任何防止截屏Flag。

    如何解决

    原因既然找到了,那么如何处理呢?
    再回头分析下Window的关键代码:

    //通过mWindowAttributes设置标识
    public void setFlags(int flags, int mask) {
            final WindowManager.LayoutParams attrs = getAttributes();
            attrs.flags = (attrs.flags&~mask) | (flags&mask);
            mForcedWindowFlags |= mask;
            dispatchWindowAttributesChanged(attrs);
        }
    

    其实只需要获得WindowManager.LayoutParams对象,再设置上flag即可。
    但是PopupWindow并没有像Activity一样有直接获得window的方法,更别说设置Flag了。我们再分析下PopupWindow的源码:

    //将PopupWindow添加到Window上
    private void invokePopup(WindowManager.LayoutParams p) {
            if (mContext != null) {
                p.packageName = mContext.getPackageName();
            }
    
            final PopupDecorView decorView = mDecorView;
            decorView.setFitsSystemWindows(mLayoutInsetDecor);
    
            setLayoutDirectionFromAnchor();
    
            //添加View
            mWindowManager.addView(decorView, p);
    
            if (mEnterTransition != null) {
                decorView.requestEnterTransition(mEnterTransition);
            }
        }
    

    我们调用showAtLocation,最终都会执行mWindowManager.addView(decorView, p);
    那么是否可以在addView之前获取到WindowManager.LayoutParams呢?

    答案很明显,默认是不可以的。因为PopupWindow并没有公开获取WindowManager.LayoutParams的方法,而且mWindowManager也是私有的。

    如何才能解决呢?
    我们可以通过hook的方式解决这个问题。我们先使用动态代理拦截PopupWindow类的addView方法,拿到WindowManager.LayoutParams对象,设置对应Flag,再反射获得mWindowManager对象去执行addView方法。

    风险分析:

    不过,通过hook的方式也有一定的风险,因为mWindowManager是私有对象,不像Public的API,谷歌后续升级Android版本不会考虑其兼容性,所以有可能后续Android版本中改了其名称,那么我们通过反射获得mWindowManager对象不就有问题了。不过从历代版本的Android源码去看,mWindowManager被改的几率不大,所以hook也是可以用的,我们尽量写代码时考虑上这种风险,避免以后出问题。

    public class PopupWindow {
        ......
        private WindowManager mWindowManager;
        ......
    }
    

    而addView方法是ViewManger接口的公共方法,我们可以放心使用。

    public interface ViewManager
    {
        public void addView(View view, ViewGroup.LayoutParams params);
        public void updateViewLayout(View view, ViewGroup.LayoutParams params);
        public void removeView(View view);
    }
    

    功能实现

    考虑到hook的可维护性和扩展性,我们将相关代码封装成一个独立的工具类吧。

    package com.ccc.ddd.testpopupwindow.utils;
    
    import android.os.Handler;
    import android.view.WindowManager;
    import android.widget.PopupWindow;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    public class PopNoRecordProxy implements InvocationHandler {
        private Object mWindowManager;//PopupWindow类的mWindowManager对象
    
        public static PopNoRecordProxy instance() {
            return new PopNoRecordProxy();
        }
    
        public void noScreenRecord(PopupWindow popupWindow) {
            if (popupWindow == null) {
                return;
            }
            try {
                //通过反射获得PopupWindow类的私有对象:mWindowManager
                Field windowManagerField = PopupWindow.class.getDeclaredField("mWindowManager");
                windowManagerField.setAccessible(true);
                mWindowManager = windowManagerField.get(popupWindow);
                if(mWindowManager == null){
                    return;
                }
                //创建WindowManager的动态代理对象proxy
                Object proxy = Proxy.newProxyInstance(Handler.class.getClassLoader(), new Class[]{WindowManager.class}, this);
    
                //注入动态代理对象proxy(即:mWindowManager对象由proxy对象来代理)
                windowManagerField.set(popupWindow, proxy);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                //拦截方法mWindowManager.addView(View view, ViewGroup.LayoutParams params);
                if (method != null && method.getName() != null && method.getName().equals("addView")
                        && args != null && args.length == 2) {
                    //获取WindowManager.LayoutParams,即:ViewGroup.LayoutParams
                    WindowManager.LayoutParams params = (WindowManager.LayoutParams) args[1];
                    //禁止录屏
                    setNoScreenRecord(params);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return method.invoke(mWindowManager, args);
        }
    
        /**
         * 禁止录屏
         */
        private void setNoScreenRecord(WindowManager.LayoutParams params) {
            setFlags(params, WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
        }
    
        /**
         * 允许录屏
         */
        private void setAllowScreenRecord(WindowManager.LayoutParams params) {
            setFlags(params, 0, WindowManager.LayoutParams.FLAG_SECURE);
        }
    
        /**
         * 设置WindowManager.LayoutParams flag属性(参考系统类Window.setFlags(int flags, int mask))
         *
         * @param params WindowManager.LayoutParams
         * @param flags  The new window flags (see WindowManager.LayoutParams).
         * @param mask   Which of the window flag bits to modify.
         */
        private void setFlags(WindowManager.LayoutParams params, int flags, int mask) {
            try {
                if (params == null) {
                    return;
                }
                params.flags = (params.flags & ~mask) | (flags & mask);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
    

    Popwindow禁止录屏工具类的使用,代码示例:

        //创建PopupWindow
        //正常项目中,该方法可改成工厂类
        //正常项目中,也可自定义PopupWindow,在其类中设置禁止录屏
        private PopupWindow createPopupWindow(View view, int width, int height) {
            PopupWindow popupWindow = new PopupWindow(view, width, height);
            //PopupWindow禁止录屏
            PopNoRecordProxy.instance().noScreenRecord(popupWindow);
            return popupWindow;
        }
    
       //显示Popupwindow
       private void showPm() {
            View view = LayoutInflater.from(this).inflate(R.layout.pm1, null);
           PopupWindow  pw = createPopupWindow(view,ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            pw1.setFocusable(false);
            pw1.showAtLocation(this.getWindow().getDecorView(), Gravity.BOTTOM | Gravity.RIGHT, PopConst.PopOffsetX, PopConst.PopOffsetY);
        }
    

    录屏效果图:
    录屏效果图.gif

    Demo地址

    https://pan.baidu.com/s/1vDK34TRSZgFumTLfTKJ-gQ

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-15 07:02 , Processed in 0.076349 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表