最近有个针对系统截屏的需求,所以预研了Android平台上捕获系统截屏的方案。

最直接的方式就是监听手机的系统截屏组合键(电源键+音量下键),但是这种方式实现难度大,且有的机型使用特殊手势进行截屏,兼容性问题难以解决。

所以网上流行的方案是监听系统截屏目录下文件创建事件或者多媒体数据库图片资源变更通知。我对两种方式都做了测试,多多少少都存在一些问题,现整理如下:

通过FileObserver监听系统截屏目录下的文件创建

FileObserver可以对一个文件或者目录进行监听,它是基于linux的inotify实现,可以监听文件创建、访问、修改等操作。

虽然文档上说FileObserver可以实现递归监听,即被监听文件夹下所有文件和级联子目录的改变都会触发监听器。但是,真正实验下来发现,不是这么回事!被监听目录的子目录的本身改动以及子目录下的文件改动都不会触发监听器。因此,要想实现递归监听,必须自己递归实现对每个子目录的监听

FileObserver可以监听多种类型的事件:

事件类型 说明
ACCESS 被监听文件被访问
MODIFY 被监听文件被修改
ATTRIB 被监听文件或目录的权限、Owner等属性被改变
CLOSE_WRITE 被监听的可写文件或者目录(已经被打开)被关闭
CLOSE_NOWRITE 被监听的只读文件或者目录(已经被打开)被关闭
OPEN 被监听文件或者目录被打开
MOVED_FROM 文件或者子目录从当前被监听目录下被移走
MOVED_TO 文件或者子目录从其他目录被移动到当前被监听目录下
CREATE 在当前被监听目录下,创建文件或者子目录
DELETE 在当前被监听目录下删除一个文件
DELETE_SELF 被监听的文件或者目录本身被删除,此时监听将被停止
MOVE_SELF 被监听的文件或者目录本身被移动
ALL_EVENTS 上面多有事件的并集

FileObserver是抽象类,我们需要实现onEvent方法处理具体业务逻辑。此外,创建FileObserver对象时,需要指定被监听文件或者目录,以及需要监听的事件类型。

经过实际测试,发现使用FileObserver进行文件(夹)监控,有几点需要注意:

  1. 不要在onEvent方法中进行耗时操作,这样会导致线程被阻塞,无法监听到后续事件,最好在工作线程进行统一处理。
  2. 防止出现死循环,比如:若监听CREATE事件时,就不能在onEvent方法中在被监听目录创建文件,否则又会触发CREATE事件,导致死循环。
  3. 回调方法onEvent中的参数path,仅是文件名,不是完整路径。
  4. 在监听到CREATE事件时,需要等待几百ms,才能加载到到文件。(这点很坑,不知道有啥解决方案不?!)

OK,FileObserver的基本情况介绍完了,下面我们看下使用FileObserver监听系统截图的方案和可行性:因为我们要监听系统截图,因此理论上只需要监听系统截图目录的CREATE事件即可。基本代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//三星Note3下的系统截图目录
String path = "/storage/emulated/0/Pictures/Screenshots";
//小米4下的系统截图目录
//path = "/storage/emulated/0/DCIM/Screenshots";

//指定监听路径path和事件类型CREATE
FileObserver fileObserver = new FileObserver(path,FileObserver.CREATE) {    @Override
    public void onEvent(int event, String path) {        //这里最好启动一个线程去加载系统截屏的图片,否则会导致线程被阻塞,无法监听到后续事件。
        //此外,这里的path仅是图片文件名,不是完整路径
        //收到CREATE事件后,立即去加载图片是获取不到的,需要延迟几百毫秒才可以加载到,估计是图片正在落地。
    }
};
//开始监听
fileObserver.startWatching();
//结束监听
fileObserver.stopWatching();

但是实际测试下来发现,在三星Note3上可以准确的监听系统截图,并可以获取到系统截图图片。但是在小米4上,根本监听不到CREATE事件(实际上,截屏图片已经在系统截屏目录了)。

在小米4上仅能监听到ACCESS(被触发多次)和OPEN事件。但是OPEN事件在三星Note3上会触发多次,而且Android手机千奇百怪,要想找到一个系统截屏时,所有手机都会触发一次的FileObserver事件,会很难,而且存在很大的兼容性问题。

因此,通过FileObserver监听系统截图存在两个比较大的问题:

  1. 每个手机上保存系统截屏图片的目录不完全相同,比如上面三星Note3和小米4就不同。因此,必须先获得每个手机保存截图图片的目录,才能进行监听。
  2. 很难找到一个系统截屏时,所有手机仅会触发一次的FileObserver事件。

所以目前来看,通过FileObserver监听系统截图不靠谱。

通过ContentObserver监听多媒体数据库(图片)的资源变化

我们知道:通过系统截屏生成一张图片时,这张图片不仅会存储在系统截屏目录中,还会通过MediaProvider类在多媒体数据库中插入一条记录,方便系统图库进行查询。而且MediaProvider会将唯一标识这张图片的URI通知到感兴趣的ContentObserver。(关于多媒体数据库下面会进行详细介绍)

因此,我们的方案就是通过ContentObserver监听多媒体数据库图片资源的变化。基本代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//查询的表字段
static final String[] PROJECTION = new String[]{    MediaStore.Images.Media.DATA,MediaStore.Images.Media.DATE_ADDED};
//根据时间降序排序
static final String SORT_ORDER = MediaStore.Images.Media.DATE_ADDED + " DESC";
//mHandler表示主线程的Handler,这样回调函数onChange就会在主线程被调用
ContentObserver contentObserver = new ContentObserver(mHandler) {    @Override
    public void onChange(boolean selfChange) {        super.onChange(selfChange);
        //从API16开始,才有两个参数的onChange方法,所以这里要主动调用下面的onChange方法。
        onChange(selfChange, null);
    }

    @Override
    public void onChange(boolean selfChange, Uri uri) {        //若调用父类方法就死循环了
        //super.onChange(selfChange,uri);
        if (uri == null) { //API16以下版本
            Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJECTION, null, null,SORT_ORDER);
            if (cursor != null && cursor.moveToFirst()) {                //完整路径
                String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                //添加图片的时间,单位秒
                long dateAdded = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
                long currentTime = System.currentTimeMillis() / 1000;
                //加个过滤条件必须是3S内的图片,且路径中包含截图字样“screenshot”
                if (Math.abs(currentTime - dateAdded) <= 3l && path.toLowerCase().contains("screenshot")) {                    //这就是系统截屏的图片了,这里测试发现需要等待几百MS,才能加载到图片。因此具体实现时,最好在独立线程,每隔100MS尝试加载一次,做好超时处理。
                    Bitmap b1 = BitmapFactory.decodeFile(path);
                }
            }
        } else { //API16及以上版本
            if (uri.toString().matches(EXTERNAL_CONTENT_URI_MATCHER + "/\\d+")) {                Cursor cursor = contentResolver.query(uri, PROJECTION, null, null, null);
                if (cursor != null && cursor.moveToFirst()){                    //完整路径
                    String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                    //添加图片的时间,单位秒
                    long dateAdded = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
                    long currentTime = System.currentTimeMillis() / 1000;
                    if (Math.abs(currentTime - dateAdded) <= 3l && path.toLowerCase().contains("screenshot"))  {                        //这就是系统截屏的图片了
                        Bitmap b2 = MediaStore.Images.Media.getBitmap(contentResolver, uri);
                    }
                }
            }
        }
    }
}
//通过ContentResolver注册ContentObserver,监听"content://media/external/video/media"
getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver);

//不需要监听的时候,一定要把原来的ContentObserver注销掉。
getContentResolver().unregisterContentObserver(contentObserver);

上述代码中,我们在API16以上和以下采取了两种不同的方案:

  1. 方案1:API16以下,因为回调中没有URI,所以只能到多媒体数据库中去查询,然后取出最新的那一条记录,理论上就是系统截屏的图片了。
  2. 方案2:API16及以上,因为回调中有唯一标识图片的URI,所以可以通过MediaStore.Images.Media和URI,直接获取截屏图片。这种方式既简单,又准确!

上述方案,经过测试,发现存在一些问题:

  1. 方案1中,若收到onChange回调,立即去获取图片,是加载不到的,必须等几百毫秒,推测应该是图片还没完全落地。但是这个等待的时间应该跟机器性能有关,因此很难确定一个固定值(和FileObserver存在相同的问题)。
  2. 不仅向多媒体数据库中插入一条图片数据会触发onChange回调,更新和删除图片数据,也会触发onChange回调。
  3. 若我们主动通过MediaProvider向多媒体数据库插入、更新、删除一条图片数据,也会触发onChange回调。

简单来说,就是没办法完全确定触发onChange回调的事件一定是系统截屏行为。因此,在onChange回调方法中,判断此次回调是不是系统截屏触发的,是个难点。但是这个问题解决不好,就会造成一定的误差。比如:我通过相机拍摄了一张图片,就会触发上面的onChange回调。所以上面的代码加了两个过滤条件:必须是3S内的图片,且图片路径中包含截图字样“screenshot”。但是这样也不能确保百分之百没有误差。


综上所述,不管是通过FileObserver还是ContentObserver,都不能完全准确地监控系统截屏操作。(相比于IOS直接提供了API级别的支持,Android还是很蛋疼啊…)

多媒体数据库

Android中的多媒体数据记录(图片、音频、视频等)是存储在DB中的,即多媒体数据库。这个数据库文件存储在/data/data/com.android.providers.media/databases目录中。如下图所示:

其中internal.db是内部存储数据库文件,external.db是存储卡数据库文件。多媒体数据操作主要就是围绕这两个数据库来进行的,这两个数据库的结构是完全一样的。如下所示:

上面是存储不同多媒体数据的表,其中video表主要存储视频数据;videothumbnails表主要存储视频缩略图数据;audio_xx表主要存储音频数据,音频数据比较复杂,又需要album相关表存储专辑信息,artist相关表存储歌手信息;images表主要存储图片数据。thumbnails表主要存储图片缩略图数据。

这里我们主要看下images表结构,如下所示:

可见,images表是基于files表的视图。其中,_data字段表示图片的完整路径,data_added字段表示添加图片的时间,widthheight字段分别表示图片的宽度和高度,_display_name字段则表示图片名称。

下面看两个具体案例,我们分别通过系统截屏手势和相机获取一张图片,然后看下这两种图片在images表中的存储。
首先是截屏获得的图片,其表记录如下所示:

然后是相机拍摄出的图片,其表记录如下所示:

从上述两张图片的表数据可知:

  • 图片id确实是递增的。
  • 系统截图和相机拍摄的图片存储在不同的目录。
  • 系统截图图片是png格式,相机拍摄图片是jpeg格式。
  • bucket_display_name字段指出了图片的来源途径,它是根据_data字段生成的。
  • 系统截屏图片的宽高就是屏幕的宽高,而相机拍摄图片的宽高则和具体手机有关,但一般都大于屏幕宽高。
  • 向其他字段的含义也很明确,此处不再赘述。

上面我们是通过sql语句直接查询图片数据,其实Android系统给我们封装了MediaStore类,它提供了多媒体数据存储与获取相关API,其基本结构如下所示(详细结构可参见源码):

其中Images.ImageColumns类主要封装了images表的字段。Images.Media类主要提供了查询和插入图片数据的API(这类API很简单,都是通过ContentResolveruri,呼起对应的MediaProvider完成真正的DB操作),以及可以通过getBitmap方法获取图片的Bitmap对象,而Images.Thumbnails类则提供了操作缩略图的相关API。同样的,其他的内部类(Audio、Video)分别对应音频表和视频表。

Images.Media.getBitmap方法很便利,其实现也很简单,首先通过uri获取输入流(详情参见源码),然后通过BitmapFactory类解码获取Bitmap。如下所示:

1
2
3
4
5
6
public static final Bitmap getBitmap(ContentResolver cr, Uri url)throws FileNotFoundException, IOException {    InputStream input = cr.openInputStream(url);
    Bitmap bitmap = BitmapFactory.decodeStream(input);
    input.close();
    return bitmap;
}

MediaStore类的源码可知,它提供的API都是通过ContentResolverUri呼起对应MediaProvider来实现的,MediaProvider才是真正实现多媒体数据库操作的场所。关于MediaProvider,又是单独话题了,感兴趣的可以去看源码。

MediaStore类为每一种资源分配了单独的Uri地址,例如:视频资源的基础地址是MediaStore.Vedio.MediaEXTERNAL_CONTENT_URI,即content://media/external/video/media,图片资源的基础地址是MediaStore.Images.Media.EXTERNAL_CONTENT_URI,即content://media/external/images/media

这些基础地址都是数据集合类型,对应的个体数据类型则是在基础地址后面加上图片ID。例如:上面我们通过系统截屏获得的图片资源ID是233494,那么唯一标识这张图片的uri就是content://media/external/images/media/233494,通过这个uri,就可以获取这张图片的所有信息了(上面getBitmap方法的第二个参数就是这种个体数据类型uri)。实际操作中,要使用哪种类型的URI,则要根据具体情况而定。

因此,获取系统截屏图片的Bitmap对象有两种方式:

  1. 假如知道了图片的唯一标识URI,那么通过MediaStore.Images.Media.getBitmap方法就可以获取了。
  2. 假如不知道URI,而知道图片的本地地址(SD卡地址),那么只能通过BitmapFactory类的decodeXXX方法来搞定了。

ContentProvider的数据更新通知机制

上面介绍的第二种方案,依赖的就是ContentProvider的数据更新通知机制。因为ContentProvider是以URI形式来组织资源的,所以当数据变更时,也是以URI形式通知感兴趣的ContentObserver。

整个数据更新机制的示意图如下所示:

其中,ContentService服务就是管理所有ContentObserver监听器的场所,它运行在System进程,以多叉树的形式组织所有监听器。而MediaProvider则负责操作多媒体数据库,并以URI的形式发出数据变更通知到ContentService服务,ContentService负责从树形数据结构中找出对该URI感兴趣的ContentObserver,然后跨进程回调ContentObserver.onChange方法。

所以这里的关键点就是ContentService服务中多叉树数据结构的建立和查询。其中多叉树的节点是ObserverNode,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
class ObserverNode{    String mName;//节点名称
    ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>();//孩子节点
    ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>();//该节点上的监听器
}

class ObserverEntry{    //跨进程回调的接口
    IContentObserver observer;
    //该参数就是注册监听器时的第二个参数,若为false,则表示若变化的URI是正在监听的URI的父节点或者相同节点时,就会触发回调。若为true,则在上述时机之上,若变化的URI是正在监听的URI的子节点时,也会触发回调。
    boolean notifyForDescendants;
}

上面我们监听系统截屏事件时,监听的URI是content://media/external/images/media,且notifyForDescendents参数为true。因此,注册之后,ContentService服务的多叉树数据结构如下所示:

而当系统截屏图片插入到多媒体数据库时,MediaProvider会发出content://media/external/images/media/xxx形式的通知,该通知到达ContentService服务后,就会在上面的多叉树数据结构中进行检索,以找到对此URI感兴趣的监听器。

其中当查找到media节点时,就会把media节点中的notifyForDescendants属性为true(即正在通知的URI是content://media/external/images/media的子节点)的ObserverEntry对象收集起来。最后,通过ObserverEntry对象的observer接口属性回调到应用程序进程的ContentObserver.onChange方法,这样整个流程就完整了。

这里在应用程序进程注册URI时,需要特别注意,ContentService服务在组织多叉树数据结构时,遇到/#?这三个特殊符号,就会停止构造子节点,因此content://media/external/images/media/#content://media/external/images/media//#content://media/external/images/media/#/?等URI形成的多叉树结构都是相同的,即上面的树形结构。(一开始我在注册URI时,以为#号的作用和ContentProvider中#号一样,代表所有的整型ID,坑了我很久)。

参考文档

  1. 深入理解MediaScanner
  2. Android应用程序组件Content Provider的共享数据更新通知机制分析
  3. Detect only screenshot with FileObserver Android
原文地址:
http://ltlovezh.com/2016/06/12/Android%E5%B9%B3%E5%8F%B0%E7%9B%91%E5%90%AC%E7%B3%BB%E7%BB%9F%E6%88%AA%E5%B1%8F%E6%96%B9%E6%A1%88%E9%A2%84%E7%A0%94%E5%8F%8A%E7%9B%B8%E5%85%B3%E7%9F%A5%E8%AF%86%E7%82%B9/

Android平台监听系统截屏方案预研及相关知识点相关推荐

  1. andriod 监听系统截屏操作

    最近公司项目需要做一个需求,就是监听app截屏,然后上报埋点信息的需求,但android是没有系统的接口可监听,只能自己实现(不像ios 系统提供了,调用一下几行代码就完事),而自己实现的话就存在各种 ...

  2. Android App中监听系统截屏(截屏监听功能)

    功能需求: App内截屏监控功能,当发现用户在我们的app内进行了截屏操作时,进行对图片的二次操作,例如添加二维码,公司logo等一系列操作. 首先来app界面图及截屏监听图添加效果图 主要是利用内容 ...

  3. Android 基于4.4系统截屏的三指截屏

    根据上一篇文章Android 4.4系统原生截图解析 ,我们知道系统截屏是调用了TakeScreenshotService,为实现在任何界面都能实现三指截屏,我们就得在PhoneWindow(fram ...

  4. Android 监听 Android中监听系统网络连接打开或者关闭的实现代码

    本篇文章对Android中监听系统网络连接打开或者关闭的实现用实例进行了介绍.需要的朋友参考下 很简单,所以直接看代码 复制代码 代码如下: package xxx; import android.c ...

  5. android ContentObserver监听系统短信和备份短信到本地

    contentProvider中的一个工具,记录一下. 监听短信: package com.example.messagedemo;import android.net.Uri; import and ...

  6. android 手机监听屏幕锁屏,用户解锁

    用到的场景很多,自己发挥 ,废话不多说直接上代码: private classScreenBroadcastReceiverextendsBroadcastReceiver{privateString ...

  7. 监听Android系统截屏

    1. 原理 因为Android系统没有提供截屏的相关API,所以需要我们自己去实现.国内的Android手机都是使用定制系统的,截图方式五花八门,采用对截图按键的监听的方案并不合适.Android系统 ...

  8. Android系统截屏监听工具

    Android系统截屏监听工具 做系统截屏通知的时候,找了很多资料,测试后发现要做挺多机型适配的,而且有时候会没有监听到,有时候又监听到多次,不能实现想要的效果.一下是最终的解决方案,已在足够多的手机 ...

  9. Android 系统截屏与系统内置DVR录像冲突,导致SystemUI重启的问题解决与分享

    上周六加班在解决一个关于SystemUI内嵌的DVR录像与系统截屏操作冲突的问题,介于问题的复杂性,所以我把这个分享出来便 于以后自己更加的理解,又方便以后遇到此问题的同行能够提供一些帮助,若有疑问可 ...

最新文章

  1. N-API中的Promise功能的使用
  2. redhat7操作系统安装
  3. C# params关键字
  4. Xshell 连接 Ubuntu
  5. fguillot json rpc_hyperf与go基于jsonrpc2.0通信
  6. 机器学习入门---------numpy
  7. php 加七天减七天,php实战第七天_PHP教程
  8. Adobe两款软件存在缺陷 黑客可控制用户PC
  9. ScrollView常用(暂时用上了的)代理方法
  10. TCP解决connect函数的超时问题
  11. amaplocation无法获取高度_kali一款高度可定制的WiFi钓鱼工具 WiFiPhisher
  12. 005木桶缩放(比例非原始)
  13. 英语4级口语是计算机评分吗,2017年11月英语四级口语评分标准
  14. Git常见问题及报错
  15. python画数学函数_Python 绘制你想要的数学函数图形 !
  16. java父类和mysql主表关系_Java工程师考试题 | 学步园
  17. learn the python the hard way习题26~30总结
  18. pyLDA系列︱gensim中的主题模型(Latent Dirichlet Allocation)
  19. HFSS印刷偶极子天线(1)
  20. 一款小游戏集合自动脚本

热门文章

  1. Python进阶_wxpy学习:愉快的探索和调试
  2. Python学习笔记:常用第三方模块3
  3. CentOS 6.6下双网卡共享上网配置
  4. 二十一、“一枝一叶一花语,一生一世一英名。”(2021.5.29)
  5. 云炬Android开发笔记 19参考面包多商城优化“我的”页面
  6. 深度神经网络的反向传播算法数学推导
  7. Python1:if / while / for...in / break /continue
  8. 批处理之列出目录下所有文件
  9. C/C++头文件一览
  10. linux系统程序设计