19

I am currently developing an application for Android and wanted to know how to detect a screenshot. I tried with FileObserver but the problem is that all events are detected ( when device goes into sleep, message, etc. ) . How to detect only screenshot ?

Thank you in advance !

Nachding
  • 465
  • 2
  • 10
  • 20

4 Answers4

25

How did you use FileObserver to detect screen shot creation? When using FileObserver, only monitor the file creation event in screen shot directory.

    String path = Environment.getExternalStorageDirectory()
            + File.separator + Environment.DIRECTORY_PICTURES
            + File.separator + "Screenshots" + File.separator;
    Log.d(TAG, path);

    FileObserver fileObserver = new FileObserver(path, FileObserver.CREATE) {
        @Override
        public void onEvent(int event, String path) {
            Log.d(TAG, event + " " + path);
        }
    };

    fileObserver.startWatching();

Don't forget to declare corresponding permissions to access content in SD card.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Another solution to detect the screen shot is using ContentObserver, because there will be a record inserted to the system media database after screen shot. Following is the code snippet using ContentObserver to monitor the event. By using ContentObserver, it's not necessary to declare write/read external storage permissions, but you have to do some filters on the file name to make sure it's a screen shot event.

    HandlerThread handlerThread = new HandlerThread("content_observer");
    handlerThread.start();
    final Handler handler = new Handler(handlerThread.getLooper()) {

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    getContentResolver().registerContentObserver(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            true,
            new ContentObserver(handler) {
                @Override
                public boolean deliverSelfNotifications() {
                    Log.d(TAG, "deliverSelfNotifications");
                    return super.deliverSelfNotifications();
                }

                @Override
                public void onChange(boolean selfChange) {
                    super.onChange(selfChange);
                }

                @Override
                public void onChange(boolean selfChange, Uri uri) {
                    Log.d(TAG, "onChange " + uri.toString());
                    if (uri.toString().matches(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString() + "/[0-9]+")) {

                        Cursor cursor = null;
                        try {
                            cursor = getContentResolver().query(uri, new String[] {
                                    MediaStore.Images.Media.DISPLAY_NAME,
                                    MediaStore.Images.Media.DATA
                            }, null, null, null);
                            if (cursor != null && cursor.moveToFirst()) {
                                final String fileName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));
                                final String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                                // TODO: apply filter on the file name to ensure it's screen shot event
                                Log.d(TAG, "screen shot added " + fileName + " " + path);
                            }
                        } finally {
                            if (cursor != null)  {
                                cursor.close();
                            }
                        }
                    }
                    super.onChange(selfChange, uri);
                }
            }
    );

Updated

If you use second method, you have to request READ_EXTERNAL_STORAGE after version Android M, otherwise it will throw SecurityException. For more information how to request runtime permission, refer here.

ban-geoengineering
  • 18,324
  • 27
  • 171
  • 253
alijandro
  • 11,627
  • 2
  • 58
  • 74
  • 1
    `FileObserver` doesn't work for me, but the ContentProvider way works, although need some modification. We need query image content with `DESC` order, to get the latest image that cause this `onChange` event. – Piasy Jan 29 '16 at 10:08
  • ContentObserver giving exception "java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaProvider uri content://media/external/images/media from pid=31855, uid=10341 requires android.permission.READ_EXTERNAL_STORAGE, or grantUriPermission()", if the Storage Permission is not granted at MARSHMELLOW – shanraisshan Mar 09 '16 at 06:46
  • @shanrais Yes, you should check and ask the runtime permission at Android M. Please infer http://developer.android.com/training/permissions/requesting.html for more information. – alijandro Mar 09 '16 at 07:09
  • "it's not necessary to declare write/read external storage permissions" I found this statement ambiguous in your answer. – shanraisshan Mar 09 '16 at 07:17
  • 1
    @shanrais OK, I have updated the answer, thanks for your correctness. – alijandro Mar 09 '16 at 09:00
  • The first code snippet didn't work because the `path` String needed to end with the `File.separator`character. I have edited that code snippet accordingly. :-) – ban-geoengineering Sep 01 '16 at 21:41
  • For the second code snippet, you can add the necessary permission to your manifest file for Marshmallow and above to: `` – ban-geoengineering Sep 01 '16 at 23:28
  • You also need to specify `fileObserver`as a field - as mentioned here: http://stackoverflow.com/a/32409712/1617737 . You can create it in your `onCreate()` method. – ban-geoengineering Sep 02 '16 at 00:34
  • Is it also possible to get the content of the screenshot i.e the image. in addition to detecting that a screenshot was taken ? – Chetan Gowda May 22 '17 at 03:04
  • @ChetanGowda Depending on your chosen method, you would simply use the reference to the `File` or `MediaStore` entry and treat it like any other. – Abandoned Cart Feb 29 '20 at 00:37
  • @alijandro Using ContentObserver in above answer, I am getting below warning message on Pixel 4 and screenshot not detected. "CAM_SpecTypeMeta: Ignoring metadata for file which is not an image /storage/emulated/0/Pictures/Screenshots/Screenshot_20200826-223441.png" Any idea? – Sagar Yadav Aug 26 '20 at 12:45
4

I have improve the code from alijandro's comment to make it easy-to-use class and fix the problem when content observer has detect the image from camera (should be screenshot image only). Then wrap it to delegate class for convenient to use.


• ScreenshotDetectionDelegate.java

public class ScreenshotDetectionDelegate {
    private WeakReference<Activity> activityWeakReference;
    private ScreenshotDetectionListener listener;

    public ScreenshotDetectionDelegate(Activity activityWeakReference, ScreenshotDetectionListener listener) {
        this.activityWeakReference = new WeakReference<>(activityWeakReference);
        this.listener = listener;
    }

    public void startScreenshotDetection() {
        activityWeakReference.get()
                .getContentResolver()
                .registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver);
    }

    public void stopScreenshotDetection() {
        activityWeakReference.get().getContentResolver().unregisterContentObserver(contentObserver);
    }

    private ContentObserver contentObserver = new ContentObserver(new Handler()) {
        @Override
        public boolean deliverSelfNotifications() {
            return super.deliverSelfNotifications();
        }

        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
        }

        @Override
        public void onChange(boolean selfChange, Uri uri) {
            super.onChange(selfChange, uri);
            if (isReadExternalStoragePermissionGranted()) {
                String path = getFilePathFromContentResolver(activityWeakReference.get(), uri);
                if (isScreenshotPath(path)) {
                    onScreenCaptured(path);
                }
            } else {
                onScreenCapturedWithDeniedPermission();
            }
        }
    };

    private void onScreenCaptured(String path) {
        if (listener != null) {
            listener.onScreenCaptured(path);
        }
    }

    private void onScreenCapturedWithDeniedPermission() {
        if (listener != null) {
            listener.onScreenCapturedWithDeniedPermission();
        }
    }

    private boolean isScreenshotPath(String path) {
        return path != null && path.toLowerCase().contains("screenshots");
    }

    private String getFilePathFromContentResolver(Context context, Uri uri) {
        try {
            Cursor cursor = context.getContentResolver().query(uri, new String[]{
                    MediaStore.Images.Media.DISPLAY_NAME,
                    MediaStore.Images.Media.DATA
            }, null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
                String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                cursor.close();
                return path;
            }
        } catch (IllegalStateException ignored) {
        }
        return null;
    }

    private boolean isReadExternalStoragePermissionGranted() {
        return ContextCompat.checkSelfPermission(activityWeakReference.get(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
    }

    public interface ScreenshotDetectionListener {
        void onScreenCaptured(String path);

        void onScreenCapturedWithDeniedPermission();
    }
}

• ScreenshotDetectionActivity.java

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;

public abstract class ScreenshotDetectionActivity extends AppCompatActivity implements ScreenshotDetectionDelegate.ScreenshotDetectionListener {
    private static final int REQUEST_CODE_READ_EXTERNAL_STORAGE_PERMISSION = 3009;

    private ScreenshotDetectionDelegate screenshotDetectionDelegate = new ScreenshotDetectionDelegate(this, this);

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        checkReadExternalStoragePermission();
    }

    @Override
    protected void onStart() {
        super.onStart();
        screenshotDetectionDelegate.startScreenshotDetection();
    }

    @Override
    protected void onStop() {
        super.onStop();
        screenshotDetectionDelegate.stopScreenshotDetection();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_READ_EXTERNAL_STORAGE_PERMISSION:
                if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
                    showReadExternalStoragePermissionDeniedMessage();
                }
                break;
            default:
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    @Override
    public void onScreenCaptured(String path) {
        // Do something when screen was captured
    }

    @Override
    public void onScreenCapturedWithDeniedPermission() {
        // Do something when screen was captured but read external storage permission has denied
    }

    private void checkReadExternalStoragePermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestReadExternalStoragePermission();
        }
    }

    private void requestReadExternalStoragePermission() {
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_CODE_READ_EXTERNAL_STORAGE_PERMISSION);
    }

    private void showReadExternalStoragePermissionDeniedMessage() {
        Toast.makeText(this, "Read external storage permission has denied", Toast.LENGTH_SHORT).show();
    }
}

• MainActivity.java

import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends ScreenshotDetectionActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onScreenCaptured(String path) {
        Toast.make(this, path, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onScreenCapturedWithDeniedPermission() {
        Toast.make(this, "Please grant read external storage permission for screenshot detection", Toast.LENGTH_SHORT).show();
    }
}
Akexorcist
  • 2,287
  • 1
  • 16
  • 19
1

You can create FileObserver that only monitors screenshot directory plus only trigger events for file or directory creation. For more information click here.

Mehul Kanzariya
  • 888
  • 3
  • 27
  • 58
0

I made a git project for Android screenshot detection using Content Observer.

It is working fine from API 14 to the most recent version (at the time of posting).


1.ScreenShotContentObserver .class
(original screenshot delete -> inform screenshot taken and give screenshot bitmap )

public class ScreenShotContentObserver extends ContentObserver {

    private final String TAG = this.getClass().getSimpleName();
    private static final String[] PROJECTION = new String[]{
            MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATA,
            MediaStore.Images.Media.DATE_ADDED, MediaStore.Images.ImageColumns._ID
    };
    private static final long DEFAULT_DETECT_WINDOW_SECONDS = 10;
    private static final String SORT_ORDER = MediaStore.Images.Media.DATE_ADDED + " DESC";

    public static final String FILE_POSTFIX = "FROM_ASS";
    private static final String WATERMARK = "Scott";
    private ScreenShotListener mListener;
    private ContentResolver mContentResolver;
    private String lastPath;

    public ScreenShotContentObserver(Handler handler, ContentResolver contentResolver, ScreenShotListener listener) {
        super(handler);
        mContentResolver = contentResolver;
        mListener = listener;
    }

    @Override
    public boolean deliverSelfNotifications() {
        Log.e(TAG, "deliverSelfNotifications");
        return super.deliverSelfNotifications();
    }

    @Override
    synchronized public void onChange(boolean selfChange) {
        super.onChange(selfChange);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            //above API 16 Pass~!(duplicated call...)
            return;
        }
        Log.e(TAG, "[Start] onChange : " + selfChange);
        try {
            process(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            Log.e(TAG, "[Finish] general");
        } catch (Exception e) {
            Log.e(TAG, "[Finish] error : " + e.toString(), e);
        }
    }

    @Override
    synchronized public void onChange(boolean selfChange, Uri uri) {
        super.onChange(selfChange, uri);
        Log.e(TAG, "[Start] onChange : " + selfChange + " / uri : " + uri.toString());

        if (uri.toString().startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())) {
            try {
                process(uri);
                Log.e(TAG, "[Finish] general");
            } catch (Exception e) {
                Log.e(TAG, "[Finish] error : " + e.toString(), e);
            }
        } else {
            Log.e(TAG, "[Finish] not EXTERNAL_CONTENT_URI ");
        }
    }

    public void register() {
        Log.d(TAG, "register");
        mContentResolver.registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, this);
    }

    public void unregister() {
        Log.d(TAG, "unregister");
        mContentResolver.unregisterContentObserver(this);
    }

    private boolean process(Uri uri) throws Exception {
        Data result = getLatestData(uri);
        if (result == null) {
            Log.e(TAG, "[Result] result is null!!");
            return false;
        }
        if (lastPath != null && lastPath.equals(result.path)) {
            Log.e(TAG, "[Result] duplicate!!");
            return false;
        }
        long currentTime = System.currentTimeMillis() / 1000;
        if (matchPath(result.path) && matchTime(currentTime, result.dateAdded)) {
            lastPath = result.path;
            Uri screenUri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString() + "/" + result.id);
            Log.e(TAG, "[Result] This is screenshot!! : " + result.fileName + " | dateAdded : " + result.dateAdded + " / " + currentTime);
            Bitmap bitmap = MediaStore.Images.Media.getBitmap(mContentResolver, screenUri);
            Bitmap copyBitmap = bitmap.copy(bitmap.getConfig(), true);
            bitmap.recycle();
            int temp = mContentResolver.delete(screenUri, null, null);
            Log.e(TAG, "Delete Result : " + temp);
            if (mListener != null) {
                mListener.onScreenshotTaken(copyBitmap, result.fileName);
            }
            return true;
        } else {
            Log.e(TAG, "[Result] No ScreenShot : " + result.fileName);
        }
        return false;
    }

    private Data getLatestData(Uri uri) throws Exception {
        Data data = null;
        Cursor cursor = null;
        try {
            cursor = mContentResolver.query(uri, PROJECTION, null, null, SORT_ORDER);
            if (cursor != null && cursor.moveToFirst()) {
                long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID));
                String fileName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));
                String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                long dateAdded = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));

                if (fileName.contains(FILE_POSTFIX)) {
                    if (cursor.moveToNext()) {
                        id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID));
                        fileName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));
                        path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                        dateAdded = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
                    } else {
                        return null;
                    }
                }

                data = new Data();
                data.id = id;
                data.fileName = fileName;
                data.path = path;
                data.dateAdded = dateAdded;
                Log.e(TAG, "[Recent File] Name : " + fileName);
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return data;
    }

    private boolean matchPath(String path) {
        return (path.toLowerCase().contains("screenshots/") && !path.contains(FILE_POSTFIX));
    }

    private boolean matchTime(long currentTime, long dateAdded) {
        return Math.abs(currentTime - dateAdded) <= DEFAULT_DETECT_WINDOW_SECONDS;
    }

    class Data {
        long id;
        String fileName;
        String path;
        long dateAdded;
    }
}
  1. Util.class

    public static void saveImage(Context context, Bitmap bitmap, String title) throws Exception {
        OutputStream fOut = null;
        title = title.replaceAll(" ", "+");
        int index = title.lastIndexOf(".png");
        String fileName = title.substring(0, index) + ScreenShotContentObserver.FILE_POSTFIX + ".png";
        final String appDirectoryName = "Screenshots";
        final File imageRoot = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), appDirectoryName);
        imageRoot.mkdirs();
        final File file = new File(imageRoot, fileName);
        fOut = new FileOutputStream(file);
    
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
        fOut.flush();
        fOut.close();
    
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.TITLE, "XXXXX");
        values.put(MediaStore.Images.Media.DESCRIPTION, "description here");
        values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
        values.put(MediaStore.Images.ImageColumns.BUCKET_ID, file.hashCode());
        values.put(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME, file.getName());
        values.put("_data", file.getAbsolutePath());
        ContentResolver cr = context.getContentResolver();
        Uri newUri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, newUri));
    }
    
Abandoned Cart
  • 4,512
  • 1
  • 34
  • 41
Soo Chun Jung
  • 595
  • 3
  • 14