在Android系統應用中,有一種需求是當用戶對當前(常常是自己)的應用進行截圖操作時,應用需要能夠“偵測”到用戶對其進行了截屏操作然后做出反應。
不過系統層面並未開放直接有效的監聽截圖操作api,沒有像開屏息屏等操作會有系統的廣播。可能Android認為截屏並非是一種系統行為,而是一種系統的
擴展功能吧。不過還是有間接的方式。
這里記錄兩種能夠在一定程度上達到監聽截圖操作的方法。
注意:兩種方法需要權限 "android.permission.WRITE_EXTERNAL_STORAGE".API>=23需動態申請。沒有的還是要注意加一下。
一, FileObserver(API 1)
Monitors files (using inotify) to fire an event after files are accessed or changed by by any process on the device (including this one). FileObserver is an abstract class; subclasses must implement the event handler onEvent(int, String)
.
Each FileObserver instance monitors a single file or directory. If a directory is monitored, events will be triggered for all files and subdirectories inside the monitored directory.
An event mask is used to specify which changes or actions to report. Event type constants are used to describe the possible changes in the event mask as well as what actually happened in event callbacks
-----
從上面官網的原話可以知道FileObserver的幾個特點:一個FileObserver實例監聽一個單一特定的目錄,任何進程對監聽目錄下的任何文件和子文件夾的改動都會觸發回調;
可以指定監聽某種特定的操作(比如創建或刪除),也可以監聽任何類型的操作。
可猜想FileObserver的功能廣泛。對於截屏的文件,這里可以用一個實例,監聽截圖所存儲的目錄,一旦發現有該目錄下有新文件,則認為有新的截圖添加進來。
使用方法:
private static FileObserver fileObserver; private static String SCREEN_SHOT_FOLDER_PATH; private static String lastShownSnapshot; private static void initFileObserver() { SCREEN_SHOT_FOLDER_PATH = Environment.getExternalStorageDirectory() + File.separator + Environment.DIRECTORY_PICTURES + File.separator + "Screenshots" + File.separator; fileObserver = new FileObserver(SCREEN_SHOT_FOLDER_PATH, FileObserver.CREATE) { @Override public void onEvent(int event, String path) { onCreateEvent(event, path); } }; private static void onCreateEvent(int event,String path){ if (null != path&&event==FileObserver.CREATE && event == FileObserver.CREATE &&(!path.equals(lastShownSnapshot))) { lastShownSnapshot = path; // 有些手機同一張截圖會觸發多個CREATE事件,避免重復展示 String resultPath = SCREEN_SHOT_FOLDER_PATH + path; //...resultPath即為圖片路徑 }
問題:部分手機不會收到回調。
二,ContentObserver(API 1)
https://developer.android.com/reference/android/database/ContentObserver
根據官方文檔的描述,ContentObserver是對一組特定Uri所表示的內容進行觀察的類,當所觀察的內容改變時會收到回調。實現步驟為:
1.繼承ContentObserver,重寫onChange(boolean selfChange)方法,api 16以上重寫onChange(boolean selfChange,Uri uri)
2.創建一個它的一個實例,可傳遞一個Handler對象,也可傳空。有handler的話,onChange里的回調還會被組合成一條message添加到該handler的消息隊列。
3.處理回調。回調只是告訴你內容發生了變化。而獲取變化的內容則做些額外的工作。
以下是代碼示例(部分)
private MediaContentObserver mExternalObserver; public void startListen(){ if (mExternalObserver == null) { mExternalObserver = new MediaContentObserver(null); mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mExternalObserver ); } } public void stopListen() { if (mExternalObserver != null) { mContext.getContentResolver().unregisterContentObserver(mExternalObserver); mExternalObserver = null; } } private class MediaContentObserver extends ContentObserver { MediaContentObserver(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange, Uri uri) { super.onChange(selfChange, uri); //handleChange(uri); } }
經測試,該方式比FileObserver有更高的兼容性,因為不是監聽的某個特定的文件夾,而是監聽的圖片數據庫(指定uri為MediaStore.Images.Media.EXTERNAL_CONTENT_URI)。缺點就是回調相對FileObserver要慢一些)
-------
兩種方式的原理其實都是基於對文件或文件夾的監聽。當有截圖操作發生時,雖然不能監聽這個用戶操作,但可以監聽這個操作產生的結果,也就是必有圖片
文件產生。所以反過來想,如果截圖的文件不在監聽的文件夾范圍內,監聽自然也就無效了。所以這里的相關實現不能用於對兼容性要求很高的需求。因為
對於android大量的ROM,你不能保證所有的截圖都保存在相同的目錄內,而且第三方截圖軟件的目錄也可能是不一樣的。
知道了如何獲取截屏圖片后,就可以拿到圖片文件並做一些自己的事情了,下面是相對完整的一個例子。
import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.provider.MediaStore; import android.text.TextUtils; import android.util.Log; import com.dianshijia.tvlive.base.application.GlobalApplication; import com.dianshijia.tvlive.utils.DeviceUtil; import java.util.LinkedList; import java.util.List; /** * create by Jin * * @description */ public class ScreenShotListener { private static final String TAG = "ScreenShotListener"; private static final String[] MEDIA_PROJECTIONS = { MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.DATE_TAKEN }; /** * 截屏依據中的路徑判斷關鍵字 */ 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" }; /** * 已回調過的路徑 */ private final List<String> sHasCallbackPaths = new LinkedList<>(); private Context mContext; private OnScreenShotListener mListener; private long mStartListenTime; private MediaContentObserver mInternalObserver; private MediaContentObserver mExternalObserver; private ScreenShotListener(Context context) { mContext = context; } public static ScreenShotListener newInstance(Context context) { return new ScreenShotListener(context); } /** * 啟動監聽 */ public void startListener() { sHasCallbackPaths.clear(); // 記錄開始監聽的時間戳 mStartListenTime = System.currentTimeMillis(); if (mInternalObserver == null) { mInternalObserver = new MediaContentObserver(null); mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.INTERNAL_CONTENT_URI, false, mInternalObserver ); } if (mExternalObserver == null) { mExternalObserver = new MediaContentObserver(null); mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mExternalObserver ); } } /** * 停止監聽 */ public void stopListener() { if (mInternalObserver != null) { mContext.getContentResolver().unregisterContentObserver(mInternalObserver); mInternalObserver = null; } if (mExternalObserver != null) { mContext.getContentResolver().unregisterContentObserver(mExternalObserver); mExternalObserver = null; } // 清空數據 mStartListenTime = 0; sHasCallbackPaths.clear(); } /** * 處理媒體數據庫的內容改變 */ private void handleContentChange(Uri contentUri) { Cursor cursor = null; try { // 數據改變時查詢數據庫中最后加入的一條數據 cursor = mContext.getContentResolver().query( contentUri, MEDIA_PROJECTIONS, null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1" ); if (cursor == null) { Log.e(TAG, "Deviant logic."); return; } if (!cursor.moveToFirst()) { Log.d(TAG, "Cursor no data."); return; } // 獲取各列的索引 int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN); // 獲取行數據 String data = cursor.getString(dataIndex); long dateTaken = cursor.getLong(dateTakenIndex); // 處理獲取到的第一行數據 handleMediaRowData(data, dateTaken); } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } } /** * 處理獲取到的一行數據 */ private void handleMediaRowData(String data, long dateTaken) { if (checkScreenShot(data, dateTaken)) { if (mListener != null && !checkCallback(data)) { mListener.onShot(data); } } else { // 如果在觀察區間媒體數據庫有數據改變,又不符合截屏規則,則輸出到 log 待分析 Log.w(TAG, "Media content changed, but not screenshot: path = " + data + " date = " + dateTaken); } } /** * 判斷指定的數據行是否符合截屏條件 */ private boolean checkScreenShot(String data, long dateTaken) { //時間 if (dateTaken < mStartListenTime || (System.currentTimeMillis() - dateTaken) > 10 * 1000) { return false; } //路徑 if (TextUtils.isEmpty(data)) { return false; } data = data.toLowerCase(); // 判斷圖片路徑是否含有指定的關鍵字之一, 如果有, 則認為當前截屏了 for (String keyWork : KEYWORDS) { if (data.contains(keyWork)) { return true; } } return false; } /** * 判斷是否已回調過, 某些手機ROM截屏一次會發出多次內容改變的通知; * 刪除一個圖片也會發通知, 同時防止刪除圖片時誤將上一張符合截屏規則 *的圖片當做是當前截屏. */ private boolean checkCallback(String imagePath) { if (sHasCallbackPaths.contains(imagePath)) { return true; } // 大概緩存15~20條記錄便可 if (sHasCallbackPaths.size() >= 20) { for (int i = 0; i < 5; i++) { sHasCallbackPaths.remove(0); } } sHasCallbackPaths.add(imagePath); return false; } public void setListener(OnScreenShotListener listener) { mListener = listener; } public interface OnScreenShotListener { void onShot(String imagePath); } /** * 媒體內容觀察者(觀察媒體數據庫的改變) */ private class MediaContentObserver extends ContentObserver { MediaContentObserver(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange, Uri uri) { super.onChange(selfChange, uri); if (DeviceUtil.isRunningForeground(GlobalApplication.mAppContext)) { handleContentChange(uri); } } } }