Android 禁止截屏、录屏 您所在的位置:网站首页 录屏截屏怎么弄 Android 禁止截屏、录屏

Android 禁止截屏、录屏

2023-07-03 17:06| 来源: 网络整理| 查看: 265

出chatgpt独享账号!内含120美元!仅需38元/个!独享永久使用!点击购买!

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

录屏效果图:

最后

如果你看到了这里,觉得文章写得不错就给个赞呗!欢迎大家评论讨论!如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足,定期免费分享技术干货。谢谢!



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有