Created
March 9, 2018 07:49
-
-
Save maoruibin/ecd818dd5c878bbed07feceabb6bc8ea to your computer and use it in GitHub Desktop.
Android 截屏监测
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
```java | |
import android.content.Context; | |
import android.database.ContentObserver; | |
import android.database.Cursor; | |
import android.graphics.Point; | |
import android.net.Uri; | |
import android.os.Handler; | |
import android.os.Looper; | |
import android.provider.MediaStore; | |
import android.text.TextUtils; | |
import android.util.Log; | |
import android.view.Display; | |
import android.view.WindowManager; | |
import com.sina.weibo.log.remote.RemoteLogManage; | |
import com.sina.weibo.utils.LogUtil; | |
import java.util.ArrayList; | |
import java.util.List; | |
/** | |
* 截屏监听管理器 | |
* | |
* 截屏判断依据: 监听媒体数据库的数据改变, 在有数据改变时获取最后 | |
* 插入数据库的一条图片数据, 如果符合以下规则, 则认为截屏了 | |
* | |
* 1. 时间判断, 图片的生成时间在开始监听之后, 并与当前时间相隔10秒内; | |
* 2. 尺寸判断, 图片的尺寸没有超过屏幕的尺寸; | |
* 3. 路径判断, 图片路径符合包含特定的关键词。 | |
*/ | |
public class ScreenShotListenManager { | |
private static final String TAG = "ScreenShotListenManager"; | |
/** | |
* 读取媒体数据库时需要读取的列 | |
*/ | |
private static final String[] MEDIA_PROJECTIONS_API_16 = { | |
MediaStore.Images.ImageColumns.DATA, | |
MediaStore.Images.ImageColumns.DATE_TAKEN, | |
MediaStore.Images.ImageColumns.WIDTH, | |
MediaStore.Images.ImageColumns.HEIGHT, | |
}; | |
/** | |
* 截屏依据中的路径判断关键字 | |
*/ | |
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","截屏","screenshots","screencaps", | |
}; | |
private static Point sScreenRealSize; | |
/** | |
* 已回调过的路径 | |
*/ | |
private final List<String> sHasCallbackPaths = new ArrayList<String>(); | |
private Context mContext; | |
private OnScreenShotListener mListener; | |
private long mStartListenTime; | |
/** | |
* 内部存储器内容观察者 | |
*/ | |
private MediaContentObserver mInternalObserver; | |
/** | |
* 外部存储器内容观察者 | |
*/ | |
private MediaContentObserver mExternalObserver; | |
/** | |
* 运行在 UI 线程的 Handler, 用于运行监听器回调 | |
*/ | |
private final Handler mUiHandler = new Handler(Looper.getMainLooper()); | |
private ScreenShotListenManager(Context context) { | |
if (context == null) { | |
throw new IllegalArgumentException("The context must not be null."); | |
} | |
mContext = context; | |
// 获取屏幕真实的分辨率 | |
if (sScreenRealSize == null) { | |
sScreenRealSize = getRealScreenSize(); | |
} | |
} | |
public static ScreenShotListenManager newInstance(Context context) { | |
assertInMainThread(); | |
return new ScreenShotListenManager(context); | |
} | |
/** | |
* 启动监听 | |
*/ | |
public void startListen() { | |
assertInMainThread(); | |
// 记录开始监听的时间戳 | |
mStartListenTime = System.currentTimeMillis(); | |
// 创建内容观察者 | |
mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler); | |
mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler); | |
// 注册内容观察者 | |
mContext.getContentResolver().registerContentObserver( | |
MediaStore.Images.Media.INTERNAL_CONTENT_URI, | |
false, | |
mInternalObserver | |
); | |
mContext.getContentResolver().registerContentObserver( | |
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, | |
false, | |
mExternalObserver | |
); | |
} | |
/** | |
* 停止监听 | |
*/ | |
public void stopListen() { | |
assertInMainThread(); | |
// 注销内容观察者 | |
if (mInternalObserver != null) { | |
try { | |
mContext.getContentResolver().unregisterContentObserver(mInternalObserver); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
mInternalObserver = null; | |
} | |
if (mExternalObserver != null) { | |
try { | |
mContext.getContentResolver().unregisterContentObserver(mExternalObserver); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
mExternalObserver = null; | |
} | |
// 清空数据 | |
mStartListenTime = 0; | |
} | |
/** | |
* 处理获取到的一行数据 | |
*/ | |
private void handleMediaRowData(String data, long dateTaken, int width, int height) { | |
if (checkScreenShot(data, dateTaken, width, height)) { | |
if(hasExistHist(data)){ | |
LogUtil.d(TAG, "handleMediaRowData: has exist "+data); | |
return; | |
} | |
if (mListener != null) { | |
LogUtil.d(TAG, "handleMediaRowData: not exist and show dialog "+data); | |
mListener.onShot(data); | |
} | |
} | |
} | |
/** | |
* 判断指定的数据行是否符合截屏条件 | |
*/ | |
private boolean checkScreenShot(String data, long dateTaken, int width, int height) { | |
/* | |
* 判断依据一: 时间判断 | |
*/ | |
// 如果加入数据库的时间在开始监听之前, 或者与当前时间相差大于5秒, 则认为当前没有截屏 | |
if (dateTaken < mStartListenTime || (System.currentTimeMillis() - dateTaken) > 5 * 1000) { | |
return false; | |
} | |
/* | |
* 判断依据二: 尺寸判断 | |
*/ | |
if (sScreenRealSize != null) { | |
// 如果图片尺寸超出屏幕, 则认为当前没有截屏 | |
boolean isVerticalWidth = width <= sScreenRealSize.x && height <= sScreenRealSize.y; | |
boolean isHorizontalHeight = height <= sScreenRealSize.x && width <= sScreenRealSize.y; | |
if (!(isVerticalWidth || isHorizontalHeight )) { | |
return false; | |
} | |
} | |
/* | |
* 判断依据三: 路径判断 | |
*/ | |
if (TextUtils.isEmpty(data)) { | |
return false; | |
} | |
data = data.toLowerCase(); | |
// 判断图片路径是否含有指定的关键字之一, 如果有, 则认为当前截屏了 | |
for (String keyWork : KEYWORDS) { | |
if (data.contains(keyWork)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* 判断是否已回调过, 某些手机ROM截屏一次会发出多次内容改变的通知; <br/> | |
* 删除一个图片也会发通知, 同时防止删除图片时误将上一张符合截屏规则的图片当做是当前截屏. | |
*/ | |
private boolean hasExistHist(String imagePath) { | |
if (sHasCallbackPaths.contains(imagePath)) { | |
LogUtil.e(TAG,"has exist "+imagePath); | |
return true; | |
} | |
// 大概缓存15~20条记录便可 超过后删除5个 防止无限增加 | |
if (sHasCallbackPaths.size() >= 20) { | |
for (int i = 0; i < 5; i++) { | |
sHasCallbackPaths.remove(0); | |
} | |
} | |
sHasCallbackPaths.add(imagePath); | |
return false; | |
} | |
/** | |
* 获取屏幕分辨率 | |
*/ | |
private Point getRealScreenSize() { | |
Point screenSize = new Point(); | |
WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); | |
Display defaultDisplay = windowManager.getDefaultDisplay(); | |
defaultDisplay.getRealSize(screenSize); | |
return screenSize; | |
} | |
/** | |
* 设置截屏监听器 | |
*/ | |
public void setListener(OnScreenShotListener listener) { | |
mListener = listener; | |
} | |
public interface OnScreenShotListener { | |
void onShot(String imagePath); | |
} | |
private static void assertInMainThread() { | |
if (Looper.myLooper() != Looper.getMainLooper()) { | |
StackTraceElement[] elements = Thread.currentThread().getStackTrace(); | |
String methodMsg = null; | |
if (elements != null && elements.length >= 4) { | |
methodMsg = elements[3].toString(); | |
} | |
throw new IllegalStateException("Call the method must be in main thread: " + methodMsg); | |
} | |
} | |
/** | |
* 媒体内容观察者(观察媒体数据库的改变) | |
*/ | |
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); | |
handleMediaContentChange(mContentUri); | |
} | |
} | |
/** | |
* 处理媒体数据库的内容改变 | |
*/ | |
private void handleMediaContentChange(Uri contentUri) { | |
Cursor cursor = null; | |
try { | |
// 数据改变时查询数据库中最后加入的一条数据 | |
cursor = mContext.getContentResolver().query( | |
contentUri, | |
MEDIA_PROJECTIONS_API_16, | |
null, | |
null, | |
MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1" | |
); | |
if (cursor == null) { | |
return; | |
} | |
if (!cursor.moveToFirst()) { | |
return; | |
} | |
// 获取各列的索引 | |
int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); | |
int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN); | |
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 = 0; | |
int height = 0; | |
if (widthIndex >= 0 && heightIndex >= 0) { | |
width = cursor.getInt(widthIndex); | |
height = cursor.getInt(heightIndex); | |
} | |
LogUtil.e(TAG,"data : "+data+" dateTaken:"+dateTaken+" diff now: "+(System.currentTimeMillis()-dateTaken)); | |
// 处理获取到的第一行数据 | |
handleMediaRowData(data, dateTaken, width, height); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} finally { | |
if (cursor != null && !cursor.isClosed()) { | |
cursor.close(); | |
} | |
} | |
} | |
} | |
``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment