Android中两种获取并处理用户的截屏图片的方法

在Android系统应用中,有一种需求是当用户对当前(常常是自己)的应用进行截图操作时,应用需要能够“侦测”到用户对其进行了截屏操作然后做出反应。

不过系统层面并未开放直接有效的监听截图操作api,没有像开屏息屏等操作会有系统的广播。可能Android认为截屏并非是一种系统行为,而是一种系统的

扩展功能吧。不过还是有间接的方式。

这里记录两种能够在一定程度上达到监听截图操作的方法。

注意:两种方法需要权限 “android.permission.WRITE_EXTERNAL_STORAGE”.API>=23需动态申请。没有的还是要注意加一下。

一, FileObserver(API 1)

 官方中对FileObserver的简单描述

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);
            }
        }
    }

}

 

点赞