android开发设置屏蔽录制
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android开发设置屏蔽录制相关的知识,希望对你有一定的参考价值。
项目开发中,为了用户信息的安全,会有禁止页面被截屏、录屏的需求。
这类资料,在网上有很多,一般都是通过设置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);
//获得布局参数对象,即mWindowAttributespublic final WindowManager.LayoutParams getAttributes() return mWindowAttributes;
通过源码可以看到,设置window属性的源码非常简单,即:通过window里的布局参数对象mWindowAttributes设置标识即可。
2、PopupWindow.java
//显示PopupWindowpublic void showAtLocation(View parent, int gravity, int x, int y)
mParentRootView = new WeakReference<>(parent.getRootView());
showAtLocation(parent.getWindowToken(), gravity, x, y);
//显示PopupWindowpublic 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);
录屏效果图:
//禁止截屏
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
//禁止截屏
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
以上是关于android开发设置屏蔽录制的主要内容,如果未能解决你的问题,请参考以下文章
Android N for Developers(API概览)
Android N for Developers(API概览)