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

项目开发中,为了用户信息的安全,会有禁止页面被截屏、录屏的需求。
这类资料,在网上有很多,一般都是通过设置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

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