Android 禁止截屏、錄屏 — 解決PopupWindow無法禁止錄屏問題


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

錄屏效果圖:
錄屏效果圖.gif

Demo地址

https://pan.baidu.com/s/1vDK34TRSZgFumTLfTKJ-gQ


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM