Android 截屏分享

前言

12月中旬產(chǎn)品提出了一個(gè)需求,截屏分享的功能。我想這個(gè)需求網(wǎng)上已經(jīng)一大堆文章了。所以這里我就大致說(shuō)一下。

解決方案

1、FileObserver監(jiān)聽(tīng)截圖文件目錄數(shù)據(jù)改變。
2、ContentProvider監(jiān)聽(tīng)數(shù)據(jù)的改變。

FileObserver

不熟悉FileObserver的同學(xué)請(qǐng)點(diǎn)擊這里,采用FileObserver方式
則需要根據(jù)廠商所在的截屏文件文件夾路徑進(jìn)行適配,這點(diǎn)就有點(diǎn)煩哦。所以最終我選擇ContentProvider的方式監(jiān)聽(tīng)文件數(shù)據(jù)的變動(dòng)。

ContentObserver

ContentProvider用于將應(yīng)用數(shù)據(jù)共享出去,ContentObserver 內(nèi)容觀察者用于獲取共享數(shù)據(jù),使用它即可監(jiān)聽(tīng)到數(shù)據(jù)的變更。

創(chuàng)建內(nèi)容觀察者對(duì)象

public class CaptureFileObserver extends ContentObserver {
    private final Uri mContentUri;
    private final CaptureCallback mCaptureCallback;

    public CaptureFileObserver(Uri contentUri, CaptureCallback captureCallback, Handler handler) {
        super(handler);
        mCaptureCallback = captureCallback;
        mContentUri = contentUri;
    }
    @Override
    public void onChange(boolean selfChange, Uri uri) {
        super.onChange(selfChange, uri);
        // 觸發(fā)了截屏 注意這里會(huì)多次回調(diào)
        if (mCaptureCallback != null){
            mCaptureCallback.onMediaFileChanged(mContentUri);
        }
    }
    /**
     * 內(nèi)容觀察者回調(diào)事件
     */
    public interface CaptureCallback {

        void onMediaFileChanged(Uri contentUri);
    }
}

當(dāng)數(shù)據(jù)發(fā)生變化之后,將會(huì)回調(diào)onChange()方法通知我們數(shù)據(jù)發(fā)生了變化。

注冊(cè)內(nèi)容觀察者

public abstract class MediaFileBaseObserver implements CaptureFileObserver.CaptureCallback {
    protected Context mContext;
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    /**
     * 獲取截屏事件回調(diào)
     */
    protected CaptureCallback mCaptureCallback;
    private final CaptureFileObserver mCaptureInternalFileObserver;
    private final CaptureFileObserver mCaptureExternalFileObserver;
    private final Uri[] mContentUris = {Media.INTERNAL_CONTENT_URI, Media.EXTERNAL_CONTENT_URI};
    protected final ContentResolver mContentResolver;
    protected long mStartListenTime;
    public MediaFileBaseObserver(Context context) {
        mContext = context;
        mContentResolver = mContext.getContentResolver();
        // 內(nèi)部外部媒體文件的監(jiān)聽(tīng)
        mCaptureInternalFileObserver = new CaptureFileObserver(mContentUris[0], this, mHandler);
        mCaptureExternalFileObserver = new CaptureFileObserver(mContentUris[1], this, mHandler);
    }
    /**
     * 開(kāi)始進(jìn)行捕捉截屏監(jiān)聽(tīng)
     */
    public void registerCaptureListener(){
        // 記錄開(kāi)始監(jiān)聽(tīng)的時(shí)間 算是一個(gè)圖片是否是截屏的一個(gè)指標(biāo)
        mStartListenTime = System.currentTimeMillis();
        // 注意 第二個(gè)boolean參數(shù) 要設(shè)置為true 不然有些機(jī)型由于多媒體文件層級(jí)不同 導(dǎo)致變化監(jiān)聽(tīng)不到 所以設(shè)置后代文件夾發(fā)生了文件改變也要進(jìn)行通知
        mContentResolver.registerContentObserver(mContentUris[0],true, mCaptureInternalFileObserver);
        mContentResolver.registerContentObserver(mContentUris[1],true, mCaptureExternalFileObserver);
    }
    /**
     * 解除綁定
     */
    public void unregisterCaptureListener(){
        mContentResolver.unregisterContentObserver(mCaptureInternalFileObserver);
        mContentResolver.unregisterContentObserver(mCaptureExternalFileObserver);
    }
    /**
     * 設(shè)置回調(diào)監(jiān)聽(tīng)
     * @param captureCallback 回調(diào)
     */
    public void setCaptureCallbackListener(CaptureCallback captureCallback){
        mCaptureCallback = captureCallback;
    }
    @Override
    public void onMediaFileChanged(Uri contentUri) {
        acquireTargetFile(contentUri);
    }
    /**
     * 獲取目標(biāo)的文件
     * @param contentUri 內(nèi)容URI
     */
    abstract void acquireTargetFile(Uri contentUri);
}

這里我們對(duì)外部存儲(chǔ)圖片文件夾和內(nèi)部存儲(chǔ)圖片文件夾進(jìn)行注冊(cè)監(jiān)聽(tīng)。若發(fā)生了文件變化,則從這兩個(gè)路徑中拿所有的圖片文件路徑,并且進(jìn)行按照?qǐng)D片的添加順序進(jìn)行降序排序并且限制數(shù)量為1,也就是說(shuō)取第一張圖片。

內(nèi)部存儲(chǔ)
content://media/internal/images/media
外部存儲(chǔ)
content://media/external/images/media
public class MediaImageObserver extends MediaFileBaseObserver {
    private static final String TAG = MediaImageObserver.class.getSimpleName();
    @SuppressLint("StaticFieldLeak")
    private static volatile MediaImageObserver mInstance = null;
    private static final String[] MEDIA_STORE_IMAGE = {
            MediaStore.Images.ImageColumns.DATA,
            // 時(shí)間 這里不能用 Date_ADD 因?yàn)槭敲爰?jí) 按時(shí)間篩選不準(zhǔn)確
            MediaStore.Images.ImageColumns.DATE_TAKEN,
            // 寬
            MediaStore.Images.ImageColumns.WIDTH
    };
    // 截屏關(guān)鍵詞 隨時(shí)補(bǔ)充
    private static final String[] KEYWORDS = {
            "screenshot", "screen_shot", "screen-shot", "screen shot",
            "screencapture", "screen_capture", "screen-capture", "screen capture",
            "screencap", "screen_cap", "screen-cap", "screen cap", "Screenshot","截屏"
    };
    // 按照日期插入的順序取第一條
    private final static String QUERY_ORDER_SQL = ImageColumns.DATE_ADDED + " DESC LIMIT 1";
    private final Point mPoint;
    public static MediaFileBaseObserver getInstance(Application application) {
        if (mInstance == null) {
            synchronized (MediaFileBaseObserver.class) {
                if (mInstance == null) {
                    mInstance = new MediaImageObserver(application.getApplicationContext());
                }
            }
        }
        return mInstance;
    }
    public MediaImageObserver(Context context) {
        super(context);
        mPoint = ScreenUtil.getRealScreenSize(context);
    }
    @Override
    void acquireTargetFile(Uri contentUri) {
        Cursor cursor = null;
        try {
            if (VERSION.SDK_INT >= VERSION_CODES.Q) {
                Bundle bundle = new Bundle();
                // 按照文件時(shí)間
                bundle.putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, new String[]{FileColumns.DATE_TAKEN});
                // 降序
                bundle.putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING);
                // 取第一張
                bundle.putInt(ContentResolver.QUERY_ARG_LIMIT, 1);
                cursor = mContentResolver.query(contentUri, MEDIA_STORE_IMAGE, bundle,null);
            } else {
                // 查找
                cursor = mContentResolver.query(contentUri, MEDIA_STORE_IMAGE, null, null, QUERY_ORDER_SQL);
            }
            findImagePathByCursor(cursor);
        } catch (Exception e) {
            if (e.getMessage() != null) {
                Log.e(TAG, e.getMessage());
            } else {
                e.printStackTrace();
            }
        }finally {
            if (cursor != null && !cursor.isClosed()){
                cursor.close();
            }
        }
    }
    private void findImagePathByCursor(Cursor cursor) {
        if (cursor == null) {
            return;
        }
        if (!cursor.moveToFirst()){
            Log.d(TAG,"Cannot find newest image file");
            return;
        }
        // 獲取 文件索引
        int imageColumnIndexData = cursor.getColumnIndex(ImageColumns.DATA);
        int imageCreateDateIndexData = cursor.getColumnIndex(ImageColumns.DATE_TAKEN);
        int imageWidthColumnIndexData = cursor.getColumnIndex(ImageColumns.WIDTH);
        String imagePath = cursor.getString(imageColumnIndexData);
        int imageWidth = cursor.getInt(imageWidthColumnIndexData);
        long imageCreateDate = cursor.getLong(imageCreateDateIndexData);
        // 時(shí)間判斷 判斷截屏?xí)r間 與 截屏圖片實(shí)際生成時(shí)間的差
        if (imageCreateDate < mStartListenTime) {
           return;
        }
        // 這里只判斷width 長(zhǎng)截屏無(wú)法判斷
        if (mPoint != null && mPoint.x != imageWidth){
            return;
        }
        // path 為空
        if (TextUtils.isEmpty(imagePath)){
            return;
        }
        // 判斷關(guān)鍵詞
        String lowerCasePath = imagePath.toLowerCase();
        // 關(guān)鍵詞比對(duì)
        for (String keyword : KEYWORDS) {
            if (lowerCasePath.contains(keyword)){
                if (mCaptureCallback != null) {
                    mCaptureCallback.capture(imagePath);
                }
                break;
            }
        }
    }
}

代碼很簡(jiǎn)單,不過(guò)有個(gè)坑在于當(dāng)我們采用以下的查詢方法的時(shí)候,在編譯版本30,Android 11機(jī)型下,會(huì)報(bào)一個(gè)異常。

private final static String QUERY_ORDER_SQL = ImageColumns.DATE_ADDED + " DESC LIMIT 1";
mContentResolver.query(contentUri, MEDIA_STORE_IMAGE, null, null, QUERY_ORDER_SQL);

SQL 異常.png

費(fèi)了一番查找最終找到,若在Android 11 版本后進(jìn)行共享數(shù)據(jù)的查詢,需要使用ContentReslover#query()方法參數(shù)為Bundle的方法,查看官方文檔,將查詢條件使用Bundle組裝并跨進(jìn)程傳輸。詳細(xì)問(wèn)題解決方案

總結(jié)

截屏分享Android原生并沒(méi)有提供相關(guān)的Api,讓我們獲取,但是解決辦法還是有的,就是通過(guò)ContentObserver進(jìn)行對(duì)內(nèi)外存儲(chǔ)文件的變動(dòng)的監(jiān)聽(tīng),之后根據(jù)ContentResolver進(jìn)行Query查詢,并進(jìn)行排序篩選,在進(jìn)行二次一系列的條件篩選,最終找到我們那張截圖的圖片。

補(bǔ)充 2021/02/05

問(wèn)題1

在應(yīng)用到實(shí)際項(xiàng)目中時(shí),發(fā)現(xiàn)當(dāng)應(yīng)用退出到后臺(tái)時(shí),用戶截取圖片的時(shí)候,會(huì)將非該應(yīng)用的截圖響應(yīng)到自己的應(yīng)用中,并觸發(fā)分享,這導(dǎo)致分享不合乎邏輯。
解決辦法在感知到文件系統(tǒng)發(fā)生變化時(shí),判斷一下當(dāng)前應(yīng)用是否處于前臺(tái)即可。

/**
     * 判斷app是否在后臺(tái)啊
     *
     * @return 0 在后臺(tái) 1 在前臺(tái) 2 不存在
     */
    public static int isBackground(Context context) {
        ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
        for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
            if (appProcess.processName.equals(context.getPackageName())) {
                if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED) {
                    return 2;
                } else if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                    return 1;
                }
            }
        }
        return 2;
    }

問(wèn)題2

在某些低端機(jī)型,比如紅米6等,由于使用數(shù)據(jù)庫(kù)查詢cursor 比較慢,導(dǎo)致分享回調(diào)有延遲,用戶可能跳轉(zhuǎn)到其他的界面了,才展示彈窗,影響用戶體驗(yàn),因此這邊做了一個(gè)等待延遲條件,判斷當(dāng)前時(shí)間與最終截圖回調(diào)時(shí)間做對(duì)比,設(shè)定一個(gè)閾值攔截。

問(wèn)題3

截屏黑名單,有些界面涉及到用戶敏感信息,所以就不觸發(fā)用戶截屏。

 @Override
protected void onCreate(Bundle savedInstanceState) {
    // 禁掉截屏
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
    super.onCreate(savedInstanceState);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容