項目開發中,為了用戶信息的安全,會有禁止頁面被截屏、錄屏的需求。
這類資料,在網上有很多,一般都是通過設置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);
}
錄屏效果圖: