android screenshot event listener

The android system does not provide a callback API for the user's screenshot behavior, so we can only go the wild way to get whether the user has taken a screenshot. Generally, two methods are used as follows:

1. Listen for changes in the directory where the screenshot is located (FileObserver)

2. Monitor changes in the media library (ContentObserver)

 

The above two methods are not omnipotent, and they need to be used in combination to achieve good results. First, let's see how to monitor the directory.

In android, we can monitor directory changes through FileObserver, let's see how to use it first 

private static final File DIRECTORY_PICTURES = new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_PICTURES);
private static final File DIRECTORY_DCIM = new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DCIM);

if (manufacturer.equalsIgnoreCase("xiaomi")) {
    DIRECTORY_SCREENSHOT = new File(DIRECTORY_DCIM, "Screenshots");
} else {
    DIRECTORY_SCREENSHOT = new File(DIRECTORY_PICTURES, "Screenshots");
}

FILE_OBSERVER = new FileObserver(DIRECTORY_SCREENSHOT.getPath(), FileObserver.ALL_EVENTS) {
    @Override
    public void onEvent(int event, String path) {
        if (event == FileObserver.CREATE) {
            String newPath = new File(DIRECTORY_SCREENSHOT, path).getAbsolutePath();
            Log.d(TAG, "path: " + newPath);
        }
    }
};

We can listen to the specified event of the specified directory, and onEvent will call back when the event is triggered. Here we only care about whether there are new files generated in the directory.

Pit 1: In practice, it is found that not all mobile phones allow such monitoring or can receive callbacks. Some mobile phones cannot receive CREATE events, but can receive other events.

I also found that sometimes the events received are not defined in FileObserver, such as 32768! The following is the meaning of the corresponding event in linux, 32768=IN_IGNORED, but why it is ignored is not clear.

http://rswiki.csie.org/lxr/http/source/include/linux/inotify.h?a=m68k#L45

Also encountered 1073741856 (1073741856 = 0x40000000 | 0x20, which is IN_OPEN | IN_ISDIR) and 1073741840 (1073741840 = 0x40000000 | 0x10, which is IN_CLOSE_NOWRITE | IN_ISDIR).

 

Pit 2: Different mobile phones have different monitoring directories. Xiaomi needs to monitor Environment.DIRECTORY_DCIM, and others can monitor Environment.DIRECTORY_PICTURES.

 

Let me say a few more words about FileObserver. FileObserver cannot perform recursive monitoring, that is to say, if there are subfolders in the folder we are monitoring, and we want to know the changes, this method is not feasible. Subfiles need to be manipulated manually.

In addition, when the directory/file we monitor is deleted and a directory/file with the same name is re-established, the previous FileObserver will not continue to work, and the monitor needs to be reset.

Also note that the FileObserver callback is not in the main thread, but in the FileObserver thread.

 

For the above reasons, we also use method 2, listening for media library changes. This method can use ContentObserver. 

private static final ContentObserver CONTENT_OBSERVER = new ContentObserver(HANDLER) {
    @Override
    public void onChange(boolean selfChange, Uri uri) {
        //Remember to check the permission to read the file first
        ContentResolver resolver = GeneralInfoHelper.getContext().getContentResolver();
        if (uri.toString().matches(MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "(/\\d+)?")) {
            Cursor cursor = resolver.query(uri, PROJECTION, null, null, MediaStore.MediaColumns.DATE_ADDED + " DESC");
            if (cursor != null && cursor.moveToFirst()) {
                //Complete route
                String newPath = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DATA));
                File file = new File(newPath);
                //file.exists() to determine whether the file exists
            }
            if (cursor != null) {
                cursor.close();
            }
        }
    }
};

getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, CONTENT_OBSERVER);

Pit 3: In practice, it is found that not all mobile phones monitor the same Uri, some with numbers and some without.

Pit 4: Remember to sort by the MediaStore.MediaColumns.DATE_ADDED field when querying the database. Note that this time unit is seconds, not milliseconds

Pit 5: Even if the order is sorted, what you get may still not be correct. This problem occurs on Meizu E2. But when I deleted the Meizu E2 screenshot folder, everything returned to normal... Here I made a simple judgment, how the difference between DATE_ADDED and the current time is within two seconds, then this data found from the database is regarded as efficient

Pit 6: When the user deletes the screenshot folder, the media library will be updated at this time, so onChange will receive a large number of callbacks at this time, so it is necessary to judge whether the file exists or not.

 

Some people may ask, why not use the second method directly?

There are 2 reasons. First, it can be seen from pit 5 that the second method is not 100% effective. Second, this method is very slow, usually with a delay of 2-3 seconds. The first method, if effective, is usually much faster than the latter.

Well, the obstacles are basically cleared, let's start to integrate the two methods

First use member variables to record the screenshot file path 

private static String sScreenshotPath;

When method 1 or method 2 receives the result, compare the received result with sScreenshotPath. If it is the same file, then there is no need to notify again, otherwise, it will be notified.

The logic is too simple, the code is not written. But the reality is not so optimistic.

Pit 7: In practice, it is found that some systems do not save screenshots directly, but first generate a hidden file, such as .screenshot.jpg, and then modify the file name (remove "."). In this case, we may receive two callbacks for the user's screenshot event (both method 1 and method 2 may receive callbacks), but the actual user only takes a screenshot once.

Here I made a special treatment. When judging whether it is the same file, only the file name is judged, regardless of the full path of the file or whether the file is hidden (that is, the "." in front of the file name is not compared) 

//Just rely on the file name instead of the full path to determine whether it is the same screenshot file, because some systems have a move operation for screenshots
private static boolean isSameFile(String newPath) {
    if (TextUtils.isEmpty(sScreenshotPath)) {
        return false;
    }

    return TextUtils.equals(removePrefixDot(new File(sScreenshotPath).getName()), removePrefixDot(new File(newPath).getName()));
}

private static String removePrefixDot(@NonNull String filename) {
    if (filename.startsWith(".")) {
        return filename.substring(1);
    }
    return filename;
}

So far, the android screenshot event monitoring has basically ended. Due to the limited testing machines, the above methods cannot be guaranteed to be foolproof.

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326143267&siteId=291194637