Nachding
Nachding

Reputation: 465

Detect only screenshot with FileObserver Android

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 !

Upvotes: 19

Views: 16634

Answers (4)

Soo Chun Jung
Soo Chun Jung

Reputation: 595

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

Upvotes: 0

Akexorcist
Akexorcist

Reputation: 2297

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

Upvotes: 5

Mehul Kanzariya
Mehul Kanzariya

Reputation: 1348

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

Upvotes: 1

alijandro
alijandro

Reputation: 12147

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.

Upvotes: 25

Related Questions