Reputation: 465
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
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;
}
}
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
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
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
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