Android中截圖監聽實現
目的
監聽到使用者在使用我們的app時進行了截圖操作
方案
由於Android沒有提供系統級別的監聽,只能自己動手搞定一個。其中最大的風險就是Android裝置的多樣性,會導致有些手機上監聽不到,(畢竟是我們自己實現的)
目前網上資料大部分都是使用ContentObserver,FileObserver這兩種方式。且實現的效果還不錯。
ContentObserver 這種方式,唯一的缺點就是慢一點,需要大家自己把握,目前我驗證的基本上10s的時間是相容性最好的,我就驗證了10個手機(哎呀,資源是個大問題)
FileObserver 這種方式速度上很快,但是坑也很多,但是還不得不用,因為在一些手機上ContentObsever監聽失敗,但是目前我沒遇到。但是這種方式的坑應該是很多的,我這邊遇到的,在小米的手機上,截圖的路徑上會多個.大概是這樣樣子的:
fileObserver路徑 /storage/emulated/0/DCIM/Screenshots/.Screenshot_2018-10-19-15-08-21-167_com.sohu.sohuvideo.png contentObserver 路徑 /storage/emulated/0/DCIM/Screenshots/Screenshot_2018-10-19-15-08-21-167_com.sohu.sohuvideo.png
看到了嗎?
好吧我上個圖示記出來吧

小米機型兩種路徑.png
然後在華為的榮耀8和榮耀9手機上 FileObserver沒有監聽到,但是ContentObsever在這款機型上效果不錯(嘿嘿嘿)。
實現
廢話不多說,直接上程式碼
截圖管理者
ScreenShotManager.java
public class ScreenShotManager { private static final String TAG = "ScreenShotManager"; /** * 已回撥過的路徑 */ private final List<String> sHasCallbackPaths = new ArrayList<String>(); private Context mContext; // 回撥監聽 private OnScreenShotListener mListener; // contentProvider 監聽 private AbsScreenShotResolver screenShotResolver; private AbsScreenShotResolver mFileObserver; private ScreenShotManager(Context context) { if (context == null) { throw new IllegalArgumentException("The context must not be null."); } mContext = context; } public static ScreenShotManager newInstance(Application context) { assertInMainThread(); return new ScreenShotManager(context); } public static ScreenShotManager newInstance(Application context, OnScreenShotListener listener) { ScreenShotManager screenShotManager = newInstance(context); screenShotManager.setListener(listener); return screenShotManager; } /** * 啟動監聽 */ public void startListen() { assertInMainThread(); sHasCallbackPaths.clear(); // contentProvider 監聽 screenShotResolver = new MediaContentObserverImpl(mContext, this); screenShotResolver.startListen(); // FileObserver 監聽 mFileObserver = new ScreenShotFileObserverImpl(mContext, this); mFileObserver.startListen(); } /** * 停止監聽 */ public void stopListen() { assertInMainThread(); if (screenShotResolver != null) { screenShotResolver.stopListen(); } if (mFileObserver != null) { mFileObserver.stopListen(); } sHasCallbackPaths.clear(); } /** * 處理資料 */ public void handleData(String data) { if (mListener != null && !checkCallback(data)) { mListener.onShot(data); } } /** * 判斷是否已回撥過, 某些手機ROM截圖一次會發出多次內容改變的通知; <br/> * 刪除一個圖片也會發通知, 同時防止刪除圖片時誤將上一張符合截圖規則的圖片當做是當前截圖. */ 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 static interface OnScreenShotListener { public void onShot(String imagePath); } //由於觀察者的實現都是在子執行緒進行的,保證管理者的物件唯一,要求必須在主執行緒中使用 private static void assertInMainThread() { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("Call the method must be in main thread: "); } } }
2.ContentObserver實現方式
MediaContentObserverImpl.java
public class MediaContentObserverImpl extends AbsScreenShotResolver { private static final String TAG = "MediaContentObserverImp"; /** * 讀取媒體資料庫時需要讀取的列, 其中 WIDTH 和 HEIGHT 欄位在 API 16 以後才有 * 因此在16 之前 可以只查詢 * MediaStore.Images.ImageColumns.DATA, * MediaStore.Images.ImageColumns.DATE_TAKEN, * 我目前的app是支援範圍最低6.0 */ private static final String[] MEDIA_PROJECTIONS = { MediaStore.MediaColumns._ID, MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.DATE_TAKEN, MediaStore.Images.ImageColumns.WIDTH, MediaStore.Images.ImageColumns.HEIGHT, }; /** * 內部儲存器內容觀察者 */ private MediaContentObserver mInternalObserver; /** * 外部儲存器內容觀察者 */ private MediaContentObserver mExternalObserver; /** * Handler, 用於執行監聽器回撥 */ private final Handler mMainHandler = new Handler(Looper.getMainLooper()); public MediaContentObserverImpl(Context context, ScreenShotManager screenShotManager) { super(context); mScreenShotManager = screenShotManager; } @Override public void startListen() { // 建立內容觀察者 mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mMainHandler); mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mMainHandler); // 記錄開始監聽的時間戳 mStartListenTime = System.currentTimeMillis(); // 註冊內容觀察者 mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.INTERNAL_CONTENT_URI, false, mInternalObserver ); mContext.getContentResolver().registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mExternalObserver ); } @Override public void stopListen() { // 登出內容觀察者 if (mInternalObserver != null) { mContext.getContentResolver().unregisterContentObserver(mInternalObserver); } if (mExternalObserver != null) { mContext.getContentResolver().unregisterContentObserver(mExternalObserver); } } /** * 處理媒體資料庫的內容改變 * * @param contentUri uri 地址 */ private void handleChange(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); // 檔案索引值 int ringtoneID = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); // 寬高獲取 int widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH); int heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT); // 獲取行資料 String data = cursor.getString(dataIndex); long dateTaken = cursor.getLong(dateTakenIndex); int width = cursor.getInt(widthIndex); int height = cursor.getInt(heightIndex); // 獲取uri資訊 Uri imageContentUri = Uri.withAppendedPath(contentUri, "" + ringtoneID); // 處理獲取到的第一行資料 handleMediaRowData(data, dateTaken, width, height, imageContentUri); } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } } private void handleMediaRowData(String data, long dateTaken, int width, int height, Uri contentUri) { if (checkScreenShot(data, dateTaken, width, height)) { Log.d(TAG, "ScreenShot: path = " + data + "; size = " + width + " * " + height + "; date = " + dateTaken + " contentUri = " + contentUri); handleMediaRowData(data, CONTENT_FROM_TYPE); } else { // 如果在觀察區間媒體資料庫有資料改變,又不符合截圖規則,則輸出到 log 待分析 Log.w(TAG, "Media content changed, but not screenshot: path = " + data + "; size = " + width + " * " + height + "; date = " + dateTaken + " contentUri = " + contentUri); } } /** * 媒體內容觀察者(觀察媒體資料庫的改變) */ private class MediaContentObserver extends ContentObserver { private Uri mContentUri; public MediaContentObserver(Uri contentUri, Handler handler) { super(handler); mContentUri = contentUri; } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); handleChange(mContentUri); } } }
- FileObserver
public class ScreenShotFileObserverImpl extends AbsScreenShotResolver { private static final String TAG = "ScreenShotFileObserverI"; // 監控的路徑 private static final String[] paths = new String[]{ Environment.getExternalStorageDirectory() + File.separator + Environment.DIRECTORY_PICTURES + File.separator + "Screenshots" + File.separator, Environment.getExternalStorageDirectory() + File.separator + Environment.DIRECTORY_DCIM + File.separator + "Screenshots" + File.separator, }; // 檔案監聽物件集合 private List<FileObserver> mFileObserverList; public ScreenShotFileObserverImpl(Context context, ScreenShotManager screenShotManager) { super(context); mScreenShotManager = screenShotManager; mFileObserverList = new ArrayList<>(); } @Override public void startListen() { stopListen(); for (String path : paths) { if (path != null && path.length() > 0) { FileObserver observer = new ScreenShotFileObserver(path); observer.startWatching(); mFileObserverList.add(observer); } } } @Override public void stopListen() { for (FileObserver observer : mFileObserverList) { observer.stopWatching(); } } private class ScreenShotFileObserver extends FileObserver { private String mPath; public ScreenShotFileObserver(String path) { super(path); mPath = path; Log.e(TAG, "ScreenShotFileObserver: " + mPath); } @Override public void onEvent(int event, @Nullable String path) { Log.e(TAG, "onEvent: "+ event +" : "+ path); if (event == FileObserver.CREATE && path != null) { if (path.length() > 0) { String result = mPath + path; // 全路徑 handleMediaRowData(result, FILE_FROM_TYPE); } } } } }
監控基類
AbsScreenShotResolver
public abstract class AbsScreenShotResolver { private static final String TAG = "AbsScreenShotResolver"; // 截圖依據中的路徑判斷關鍵字 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", "截圖" }; public static String CONTENT_FROM_TYPE = "contentFromData"; public static String FILE_FROM_TYPE = "fileFromData"; long mStartListenTime; protected Context mContext; private Point sScreenRealSize; protected ScreenShotManager mScreenShotManager; AbsScreenShotResolver(Context context) { mContext = context; // 獲取螢幕真實的解析度 if (sScreenRealSize == null) { sScreenRealSize = getRealScreenSize(); if (sScreenRealSize != null) { Log.d(TAG, "螢幕 Real Size: " + sScreenRealSize.x + " * " + sScreenRealSize.y); } else { Log.e(TAG, "獲取失敗"); } } } public abstract void startListen(); public abstract void stopListen(); protected void handleRowData(String data, String fromType) { Log.e(TAG, "handleRowData: " + data + " type: " + fromType); mScreenShotManager.handleData(data); } /** * 判斷指定的資料行是否符合截圖條件 * content 資料是否符合要求 */ boolean checkContentData(String data, long dateTaken, int width, int height) { /* * 時間判斷 10s的間隔 */ // 如果加入資料庫的時間在開始監聽之前, 或者與當前時間相差大於10秒, 則認為當前沒有截圖 if (dateTaken < mStartListenTime || (System.currentTimeMillis() - dateTaken) > 10 * 1000) { return false; } /* * 尺寸判斷 超過螢幕肯定不行 */ if (sScreenRealSize != null) { // 如果圖片尺寸超出螢幕, 則認為當前沒有截圖 if (!((width <= sScreenRealSize.x && height <= sScreenRealSize.y) || (height <= sScreenRealSize.x && width <= sScreenRealSize.y))) { return false; } } /* *這個路徑判斷,其實是認為新增的,但是大部分手機都符合這個路徑 */ if (TextUtils.isEmpty(data)) { return false; } data = data.toLowerCase(); // 判斷圖片路徑是否含有指定的關鍵字之一, 如果有, 則認為當前截圖了 for (String keyWork : KEYWORDS) { if (data.contains(keyWork)) { return true; } } return false; } /** * 獲取螢幕解析度 */ private Point getRealScreenSize() { Point screenSize = null; try { screenSize = new Point(); WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); Display defaultDisplay = windowManager.getDefaultDisplay(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { defaultDisplay.getRealSize(screenSize); } else { try { Method mGetRawW = Display.class.getMethod("getRawWidth"); Method mGetRawH = Display.class.getMethod("getRawHeight"); screenSize.set( (Integer) mGetRawW.invoke(defaultDisplay), (Integer) mGetRawH.invoke(defaultDisplay) ); } catch (Exception e) { screenSize.set(defaultDisplay.getWidth(), defaultDisplay.getHeight()); e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); } return screenSize; } }
目前的實現只是參考,畢竟測試的機型有限。