Android中截屏监听实现
2018-10-23 本文已影响28人
Stay_Li
目的
监听到用户在使用我们的app时进行了截屏操作
方案
由于Android没有提供系统级别的监听,只能自己动手搞定一个。其中最大的风险就是Android设备的多样性,会导致有些手机上监听不到,(毕竟是我们自己实现的)
目前网上资料大部分都是使用ContentObserver,FileObserver这两种方式。且实现的效果还不错。
ContentObserver 这种方式,唯一的缺点就是慢一点,需要大家自己把握,目前我验证的截图成功,基本上1s左右的时间是兼容性最好的,当然这个最好自己实测,我就验证了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) {
/*
* 时间判断 1s的间隔
*/
// 如果加入数据库的时间在开始监听之前, 或者与当前时间相差大于1秒, 则认为当前没有截屏
if (dateTaken < mStartListenTime || (System.currentTimeMillis() - dateTaken) > 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;
}
}
目前的实现只是参考,毕竟测试的机型有限。
今天在这款机型上
93E1F068-D766-47ea-B5C5-F4FD8117766E.png
发现问题:
原因 是FileObserver监听到的文件,返回给我们路径时,但是文件并未完全写入完整,因此我们应该使用Handler做一个循环检查文件大小的操作。当文件完整后再使用okhttp进行上传的操作,具体的异常信息
java.net.ProtocolException: expected 6807 bytes but received 8081
at okhttp3.internal.http.Http1xStream$FixedLengthSink.write(Http1xStream.java:279)
at okio.RealBufferedSink.flush(RealBufferedSink.java:216)
at okio.ForwardingSink.flush(ForwardingSink.java:39)
at okio.RealBufferedSink.flush(RealBufferedSink.java:218)
at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.java:47)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:45)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:67)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:109)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:67)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:124)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:67)
at com.test.log.SimpleInterceptor.intercept(SimpleInterceptor.java:34)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:67)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:170)
at okhttp3.RealCall.access$100(RealCall.java:33)
at okhttp3.RealCall$AsyncCall.execute(RealCall.java:120)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:841)
不知道还有没有其他坑啊