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

弓背霞明剑照霜,秋风走马出咸阳。这篇文章主要讲述Android 禁止截屏录屏 — 解决PopupWindow无法禁止录屏问题相关的知识,希望能为你提供帮助。
项目开发中,为了用户信息的安全,会有禁止页面被截屏、录屏的需求。
这类资料,在网上有很多,一般都是通过设置Activity的Flag解决,如:

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

这种设置可解决一般的防截屏、录屏的需求。
如果页面中有弹出Popupwindow,在录屏视频中的效果是:
非Popupwindow区域为黑色 但Popupwindow区域仍然是可以看到的

如下面两张Gif图所示:
未设置FLAG_SECURE,录屏的效果,如下图(git图片中间的水印忽略):
Android 禁止截屏录屏 — 解决PopupWindow无法禁止录屏问题

文章图片

设置了FLAG_SECURE之后,录屏的效果,如下图(git图片中间的水印忽略):
Android 禁止截屏录屏 — 解决PopupWindow无法禁止录屏问题

文章图片

原因分析看到了上面的效果,我们可能会有疑问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 flagsThe new window flags (see WindowManager.LayoutParams). * @param maskWhich 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); PopupWindowpw = 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); }

录屏效果图:
Android 禁止截屏录屏 — 解决PopupWindow无法禁止录屏问题

文章图片

Demo地址【Android 禁止截屏录屏 — 解决PopupWindow无法禁止录屏问题】https://pan.baidu.com/s/1vDK34TRSZgFumTLfTKJ-gQ

    推荐阅读