项目开发中,为了用户信息的安全,会有禁止页面被截屏、录屏的需求。

这类资料,在网上有很多,一般都是通过设置Activity的Flag解决,如:

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

这种设置可解决一般的防截屏、录屏的需求。

如果页面中有弹出Popupwindow,在录屏视频中的效果是:

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

如下面两张Gif图所示:

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

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

原因分析

看到了上面的效果,我们可能会有疑问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);
}

录屏效果图:

Demo地址

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

Android 禁止截屏、录屏 — 解决PopupWindow无法禁止录屏问题的更多相关文章

  1. Android应用内 代码截屏(获取View快照)和 禁止截屏

    1. 应用内的代码截屏(获取View的快照) Android的View类中提供了获取控件绘制缓存的方法,这种截屏的方式仅限于应用内自己的Activity界面,不需要任何权限,严格来说该方法不属于截屏, ...

  2. react native 中实现个别页面禁止截屏

    这里主要用到了原生模块,下面贴出FlagSecureModule.java的代码 package com.studyproj.flagsecure; import android.util.Log; ...

  3. Android系统截屏的实现(附代码)

    1.背景                     写博客快两年了,写了100+的文章,最火的文章也是大家最关注的就是如何实现android系统截屏.其实我们google android_screen_ ...

  4. android后台截屏实现(2)--screencap源码修改

    首先找到screencap类在Android源码中的位置,/442/frameworks/base/cmds/screencap/screencap.cpp 源码如下: /* * Copyright ...

  5. Android长截屏-- ScrollView,ListView及RecyclerView截屏

    http://blog.csdn.net/wbwjx/article/details/46674157       Android长截屏-- ScrollView,ListView及RecyclerV ...

  6. Android 长截屏原理

    https://android-notes.github.io/2016/12/03/android%E9%95%BF%E6%88%AA%E5%B1%8F%E5%8E%9F%E7%90%86/   a ...

  7. Android Activity启动黑/白屏原因与解决方式

    Android Activity启动黑/白屏原因与解决方式 我们新建一个HelloWorld项目,运行在手机上时,Activity打开之前会有一个动画,而这个动画里是全白或者全黑的(取决于你的主题是亮 ...

  8. 解决微信浏览器video全屏的问题

    解决微信浏览器video全屏的问题 在微信浏览器里面使用video标签,会自动变成全屏,改成下面就好了,起码可以在video标签之上加入其他元素. <video id="videoID ...

  9. fullpage 单屏高度超过屏幕高度,实现单屏内可以滚动并解决手机端单屏高度不正确的问题

    最近接触了好几次jquery.fullpage.js这个插件,实现整屏的滑动,效果很炫,用fullpage来实现也很简单,但是也碰到了一些问题和大家分享一下 1.单屏高度超过屏幕高度,实现单屏的滑动 ...

随机推荐

  1. 初识TDD

    什么是 TDD ? TDD 有广义和狭义的区分. 广义角度指的是 ATDD(Acceptance Test Driven Development),包括 BDD(Behavior Driven Tes ...

  2. JVM(一)内存分配

    方法区: ①存储被虚拟机加载的类信息.常量.静态变量.即时编译器编译后的代码数据 ②又称为永久代,仅对于Hotspot来讲,JRockit和IBM J9里面没有永久代的概念,1.8以后是元空间,直接使 ...

  3. PID算法的理解及场景模拟

    增量式PID算法的简化版之后的公式: △u(t)=Ae(t)-Be(t-1)+Ce(t-2) △u:PID控制器输出的下一时刻的调整量 e(t):PID控制器在当前时刻的状态变化量-给定值 e(t-1 ...

  4. Spring Boot 2.x 基础案例:整合Dubbo 2.7.3+Nacos1.1.3(配置中心)

    本文原创首发于公众号:Java技术干货 1.概述 本文将Nacos作为配置中心,实现配置外部化,动态更新.这样做的优点:不需要重启应用,便可以动态更新应用里的配置信息.在如今流行的微服务应用下,将应用 ...

  5. 解决Maven依赖jar包冲突总结

    maven导入jar包中的一些概念:      直接依赖:项目中直接导入的jar包,就是该项目的直接依赖包.      传递依赖:项目中没有直接导入的jar包,可以通过项目直接依赖jar包传递到项目中 ...

  6. Oracle基于布尔的盲注总结

    0x01 decode 函数布尔盲注 decode(字段或字段的运算,值1,值2,值3) 这个函数运行的结果是,当字段或字段的运算的值等于值1时,该函数返回值2,否则返回3 当然值1,值2,值3也可以 ...

  7. 爬虫1:html页面+beautifulsoap模块+get方式+demo

    前言:最近公司要求编写一个爬虫,需要完善后续金融项目的数据,由于工作隐私,就不付被爬的网址url了,下面总结下spider的工作原理. 语言:python:工具:jupyter:    概要:说到爬虫 ...

  8. WebSocket学习简书

    1.什么是Websocket? WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议. 2.单工,半双工和全双工通信? 在单工通信中,通信的信道是单向的,发送端 ...

  9. google hack 语法

    google hack a b c 自动对词进行拆分匹配 拆分标准 空格 "a b c " 把a b c 当成一个整体去查 " a*b" *通配符 里面是一个或 ...

  10. Linux与Git学习笔记

    Linux基础概念篇: 终端.控制器 命令行界面 (CLI).终端 (Terminal).Shell.TTY的区别 Linux下的yum与apt-get Linux中su.su -和sudo的区别 L ...