Android 禁止截屏、录屏 — 解决PopupWindow无法禁止录屏问题
项目开发中,为了用户信息的安全,会有禁止页面被截屏、录屏的需求。
这类资料,在网上有很多,一般都是通过设置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无法禁止录屏问题的更多相关文章
- Android应用内 代码截屏(获取View快照)和 禁止截屏
1. 应用内的代码截屏(获取View的快照) Android的View类中提供了获取控件绘制缓存的方法,这种截屏的方式仅限于应用内自己的Activity界面,不需要任何权限,严格来说该方法不属于截屏, ...
- react native 中实现个别页面禁止截屏
这里主要用到了原生模块,下面贴出FlagSecureModule.java的代码 package com.studyproj.flagsecure; import android.util.Log; ...
- Android系统截屏的实现(附代码)
1.背景 写博客快两年了,写了100+的文章,最火的文章也是大家最关注的就是如何实现android系统截屏.其实我们google android_screen_ ...
- android后台截屏实现(2)--screencap源码修改
首先找到screencap类在Android源码中的位置,/442/frameworks/base/cmds/screencap/screencap.cpp 源码如下: /* * Copyright ...
- Android长截屏-- ScrollView,ListView及RecyclerView截屏
http://blog.csdn.net/wbwjx/article/details/46674157 Android长截屏-- ScrollView,ListView及RecyclerV ...
- 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 ...
- Android Activity启动黑/白屏原因与解决方式
Android Activity启动黑/白屏原因与解决方式 我们新建一个HelloWorld项目,运行在手机上时,Activity打开之前会有一个动画,而这个动画里是全白或者全黑的(取决于你的主题是亮 ...
- 解决微信浏览器video全屏的问题
解决微信浏览器video全屏的问题 在微信浏览器里面使用video标签,会自动变成全屏,改成下面就好了,起码可以在video标签之上加入其他元素. <video id="videoID ...
- fullpage 单屏高度超过屏幕高度,实现单屏内可以滚动并解决手机端单屏高度不正确的问题
最近接触了好几次jquery.fullpage.js这个插件,实现整屏的滑动,效果很炫,用fullpage来实现也很简单,但是也碰到了一些问题和大家分享一下 1.单屏高度超过屏幕高度,实现单屏的滑动 ...
随机推荐
- 阿里云ecs安全组端口开放设置
新买的一些云服务,上面操作都没问题还是无法访问,需要在购买的服务里加上安全规则,如图: 点击配置规则进行操作
- 无暇代码(js的整洁之道)
如果你关注代码本身和代码的编写方式,而不是只关心它是否能工作,那么你写代码是有一定的水准.专业开发人员将为未来的自己和“其他人”编写代码,而不仅仅只编写当前能工作就行的代码.在此基础上,简洁代码可以定 ...
- GitHub项目徽标
前言 GitHub徽标,GitHub Badge,你也可以叫它徽章.就是在项目README中经常看到的那些表明构建状态或者版本等信息的小图标.就像这样: 这些好看的小图标不仅简洁美观,而且包含了清晰易 ...
- at,crontab例行性任务
at:仅执行一次就结束的调度命令 at [-mldvc] TIME -m:当at的工作完成后,即使没有输出信息,也会以email的方式通知用户工作已完成 -l:相当于atq,列出系统上所有该用户的at ...
- ping通谷歌后发送QQ邮件通知
前言 国庆期间,据说是为了防止有人在重大节日发表不正当言论,很多可以kxsw的ip都被封了,可是什么时候才会解封呢,不能没事就去ping一下吧,所以我写了个定时任务,定时ping谷歌服务器,如果p ...
- MongoDB 学习笔记之 group聚合
group聚合: key: 分组字段 cond:过滤条件 reduce: curr是当前行 result是每组的结果集 initial : 组变量初始值 finalize: 统计一组后的回调函数 用g ...
- GDB使用总结
1. GDB概述 GNU发布的调试器,可以查看程序如何运行或崩溃时的状态. 主要功能: 启动程序,可以按照自定义的要求运行程序. 可让被调试的程序在你所指定的断点处停住. 当程序被停住时,可以检查此时 ...
- MacOS 导入MySQLdb 报错解决思路(解决ImportError: this is MySQLdb version (1, 2, 3, 'beta', 1), but _mysql is version (1, 2, 5, 'final', 1))
cd /Library/Python/2.7/site-packages ls rm -rf MySQL_python-1.2.5-py2.7.egg-info 然后重新import 即可
- 动态分配内存-realloc
动态分配内存---relloc 关于 malloc 就不多说了,现在看看 realloc: 函数声明: void *realloc(void *ptr, size_t size); 功能:动态改变指针 ...
- Github桌面端安装慢问题
失败截图: 原因是某些域名被墙,所以这里想安装快的话还是得翻墙.如何翻墙请自行百度. 如果翻墙还是很慢,请参考网上大神文章:https://www.yaozeyuan.online/2015/10/0 ...