I'm trying to implement a method that will and detect when a screenshot has been created and immediately delete it. I'm using FileObserver to observe the screenshot directory and flag when a new file is created in said directory. I don't know that the syntax is entirely correct, so any help would be appreciated. Thanks!
private void deleteMostRecentScreenshot() {
/**
* Set the path for the screenshot directory.
*/
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString() + "Screenshots";
OLog.d(TAG, path);
/**
* Array of the files in our screenshot directory, sorted so that the oldest pictures are first.
*/
final File[] screenshots = new File(path).listFiles();
Arrays.sort(screenshots, new Comparator<File>() {
#Override
public int compare(File f1, File f2) {
return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
}
});
/**
* Watch for when a new file is created in our directory, ignoring "probes" created when the camera
* is launched. If a new file is created, delete the last file in the array, which should be
* the newest picture.
*/
FileObserver fileObserver = new FileObserver(path) {
#Override
public void onEvent(int event, String path) {
OLog.d(TAG, event + "" + path);
if (event == FileObserver.CREATE && !path.equals(".probe")) {
OLog.d(TAG, "File Created ["
+ Environment.getExternalStorageState()
+ "Screenshots"
+ path
+ "]");
screenshots[screenshots.length - 1].delete();
}
}
};
fileObserver.startWatching();
I had asked a few of the other programmers I work with if they saw anything out of place, looked at numerous other SO articles and still wasn't sure why it wasn't working as intended.
It is unclear exactly what you are intending. Here are two oddities that I see:
Your FileObserver will randomly stop observing files, as it will be eligible for garbage collection as soon as deleteMostRecentScreenshot() returns. And, as the documentation points out, "If a FileObserver is garbage collected, it will stop sending events. To ensure you keep receiving events, you must keep a reference to the FileObserver instance from some other live object."
Your statement that "the last file in the array... should be the newest picture" is incorrect. At best, it will have been the most-recently-modified file as of the time when you called listFiles(). Changes that occur after listFiles() is called will not affect your screenshots array. Since your FileObserver is given the path to the file, you might consider just using that path.
Related
android.os.FileObserver requires a java.io.File to function.
But with Android 10 Google restricted access to everything but your app's private directory due to the famous "Storage Access Framework". Thus, accessing anything via java.io.File breaks and renders FileObserver useless unless you intend to use it in your app's private directory. However, I want to be notified when something is changed in a certain directory on external storage. I would also like to avoid periodically checking for changes.
I tried using ContentResolver.registerContentObserver(uri,notifyForDescendants,observer) and ran into some problems with that method:
Every Uri I have plugged in so far was accepted
It neither fails nor notifies if the Uri doesn't work
I cannot find any documentation telling me which Uris actually work
The only thing I got working to some extent is the following approach:
// works, but returns all changes to the external storage
contentResolver.registerContentObserver(MediaStore.Files.getContentUri("external"), true, contentObserver)
Unfortunately this includes all of the external storage and only returns Media Uris when changes happen - for example content://media/external/file/67226.
Is there a way to find out whether or not that Uri points to my directory?
Or is there a way to make registerContentObserver() work with a Uri in such a way that I get a notification whenever something in the folder has changed?
I also had no success trying various Uris related to DocumentsFile and external storage Uris.
I kept getting errors when even trying to use the base constructor such as the following -
No direct method <init>(Ljava/util/List;I)V in class Landroid/os/FileObserver; or its super classes (declaration of 'android.os.FileObserver' appears in /system/framework/framework.jar!classes2.dex)
From a comment on Detect file change using FileObserver on Android:
I saw that message (or something like that) when i was trying to use constructor FileObserver(File). Use of deprecated FileObserver(String) solved my problem.... Original FileObserver has bugs.
Full disclosure, I was using the Xamarin.Android API; however, the gist and the commenter I quoted were both working with Java. At any rate, indeed - tried again using the counterpart String constructor and I was finally able to make and use the observer. Grinds my gears to use a deprecated API, but apparently they're hanging onto it at least up to and including Android 12.0.0_r3... still, would much prefer the supported constructors actually work. Maybe there's some warrant here for filing an issue.
I found a way to implement FileObserver on Android 10 with ContentObserver, but it might only work with media files since it works with media content uris.
The uri for ContentResolver.registerContentObserver() should be the file's corresponding media uri (e.g. content://media/external/file/49) which is queried by file path.
fun getMediaUri(context: Context, file: File): Uri? {
val externalUri = MediaStore.Files.getContentUri("external")
context.contentResolver.query(
externalUri,
null,
"${MediaStore.Files.FileColumns.DATA} = ?",
arrayOf(file.path),
null
)?.use { cursor ->
if (cursor.moveToFirst()) {
val idIndex = cursor.getColumnIndex("_id")
val id = cursor.getLong(idIndex)
return Uri.withAppendedPath(externalUri, "$id")
}
}
return null
}
Then ContentObserver.onChange() will be triggered for every file change with uri: content://media/external/file/{id}; uri in ContentObserver.onChange(boolean selfChange, Uri uri) will always be content://media/external/file; only registered file will be with id (e.g. content://media/external/file/49?deletedata=false).
Does what FileObserver used to do when input uri's path matches registered uri.
I have a temporary solution for this issue, so let's see if this can help.
I start an infinite while loop watching for file created and file deleted (if you want file modified or file renamed you have to implement more) using DocumentFile. Below is my sample:
private static int currentFileIndirectory = 0;
private static final int FILE_CREATED = 0;
private static final int FILE_DELETED = 1;
private static DocumentFile[] onDirectoryChanged(DocumentFile[] documentFiles, int event) {
Log.d("FileUtil", "onDirectoryChanged: " + event);
if (event == FILE_CREATED) {
} else {
}
return documentFiles;
}
private static boolean didStartWatching = false;
private static void startWatchingDirectory(final DocumentFile directory) {
if (!didStartWatching) {
didStartWatching = true;
DocumentFile[] documentFiles = directory.listFiles();
if (null != documentFiles && documentFiles.length > 0) {
currentFileIndirectory = documentFiles.length;
}
new Thread(new Runnable() {
public void run() {
while (true) {
DocumentFile[] documentFiles = directory.listFiles();
if (null != documentFiles && documentFiles.length > 0) {
if (documentFiles.length != currentFileIndirectory) {
if (documentFiles.length > currentFileIndirectory) {//file created
DocumentFile[] newFiles = new DocumentFile[documentFiles.length - currentFileIndirectory];
onDirectoryChanged(newFiles, FILE_CREATED);
} else {//file Deleted
onDirectoryChanged(null, FILE_DELETED);
}
currentFileIndirectory = documentFiles.length;
}
}
}
}
}).start();
}
}
I need to implement a service in android that must be able to monitor a folder to detect a certain file and read what it contains. I'm having a strange behavior with my code and I can't find the reason. This is my relevant code.
public void onCreate(){
lectorFichCSV = new LectorFichCSV(); //object to read CSV files
ftpFileObserver = new FileObserver(filePath.getAbsolutePath()){
public void onEvent(int event, String file) {
if((FileObserver.CREATE & event) != 0){
Log.i("INFO: ", filePath.getAbsolutePath() + "/" + file + " is created");
if(file.substring(0,3).equals("RVE")){ //If file is created and the one I expect
try{
Log.i("INFO: ", "We have a RVE answer");
is = new FileInputStream(filePath + "/" + file);
lineaVent = lectorFichCSV.parseCSVFileAsList(is); //Get information in a list
//Get dao from ORMLite
dao = getHelper().getLineaVentDao();
Iterator<String[]> iterator = lineaVent.iterator();
if(iterator.hasNext()){
String[] aux = iterator.next();
Log.i("INFO:", "CodLineaVent "+aux[0]);
if(aux[2].equals("S")){
//Update DB information accordin to my file
UpdateBuilder<LineaVent, Integer> updateBuilder = dao.updateBuilder();
updateBuilder.where().eq("_id", aux[0]);
updateBuilder.updateColumnValue("valido", true);
updateBuilder.updateColumnValue("saldo", true);
updateBuilder.update();
lineaVent.clear();
}else if(aux[2].equals("N")){
UpdateBuilder<LineaVent, Integer> updateBuilder = dao.updateBuilder();
updateBuilder.where().eq("_id", aux[0]);
updateBuilder.updateColumnValue("saldo", false);
updateBuilder.update();
lineaVent.clear();
}
File fileToDel = new File(filePath + "/" + file);
fileToDel.delete();
}
}catch(FileNotFoundException e){
e.printStackTrace();
}catch(SQLException e){
e.printStackTrace();
}
}
I debugged the code and sometimes is working and sometimes I get lineaVent.size() == 0. I'm going crazy with this, I'm thinking, is it possible that events occurs faster than the creation of my file? that would be the reason when I tried to parse my CSV file into my List object is size = 0? In that case I'm not getting any FileNotFoundException.
Any help will be appreciate. Thank you.
I am not an expert with the inotify POSIX API that, IIRC, underlies FileObserver. However, given that there are separate events for CREATE, MODIFY, and CLOSE_WRITE, it stands to reason that the CREATE event is solely for file creation -- in other words, allocating a new entry in the filesystem for the file. That would either create an empty file, or perhaps a file with some initial load of bytes, but where other MODIFY calls might be needed to write out the full contents. CLOSE_WRITE would then be called to indicate that whoever was writing to the file has now closed their file handle.
Hence, if you are watching for some file to be created, to read it in, watch for CREATE, then watch for CLOSE_WRITE on that same file, and then try to read it, and see if that works better.
The process seemed quite simplistic at first, but there must be something that I am missing going forward with this task. There was a settings file that I wanted to create local to my application for storing a whole bunch of data (not preference worthy). I ended up saving the file with the following code snippet.
protected File createSettingsFileLocation(String fileNameF)
{
File directoryFile = context_.getDir("settings", Context.MODE_PRIVATE);
File settingsFile;
settingsFile = new File(directoryFile, fileNameF);
if (!settingsFile.exists())
{
try
{
settingsFile.createNewFile();
} catch(IOException e)
{
Log.e(MyConstants.LOG_TAG, "Could not create the file as intended within internal storage.");
return null;
}
}
return settingsFile;
}
and then proceeded to retrieve the file later by looking for it locally with the following code snippets.
public String getCurrentFileContainingSettings()
{
List<String >settingFilesInFolder = getLocalStorageFileNames();
if (settingFilesInFolder == null || settingFilesInFolder.isEmpty())
{
return null;
}
String pathToCurrentSettingsFile = settingFilesInFolder.get(0);
return pathToCurrentSettingsFile;
}
protected List<String> getLocalStorageFileNames()
{
return Arrays.asList(context_.fileList());
}
However, the settingFilesInFolder always returns no entries, so I get null back from the getCurrentFileContainingSettings(). As what I could see from the documentation it seems as thought I was doing it right. But, I must be missing something, so I was hoping that someone could point something out to me. I could potentially hard-code the file name once it has been created within the system in a preference file for access later the first time that the settings are created, but I shouldn't have to do something like that I would think.
fileList() only looks in getFilesDir(), not in its subdirectories, such as the one you created via getDir(). Use standard Java file I/O (e.g., list()) instead.
I'm receiving this java.lang.IllegalStateException: not connected to MediaScannerService exception in some crash reports from my app.
They are not too many, but I don’t know what’s wrong in my code, because on my phone/emulator it works OK.
I’m using a method to call the MediaScanner adapted from a SO question/answer at How to get and set (change) ID3 tag (metadata) of audio files?
The method:
public static void scanMedia(Context context, final File[] file, final String[] mime) {
msc = new MediaScannerConnection(context, new MediaScannerConnectionClient() {
public void onScanCompleted(String path, Uri uri) {
Utils.logger("d", "Scanned " + path + ":", DEBUG_TAG);
Utils.logger("d", "-> uri: " + uri, DEBUG_TAG);
msc.disconnect();
}
public void onMediaScannerConnected() {
for (int i = 0; i < file.length; i++) {
msc.scanFile(file[i].getAbsolutePath(), mime[i]);
}
}
});
msc.connect();
}
My calls:
Utils.scanMedia(getApplicationContext(),
new File[] {myVideo},
new String[] {"video/*"});
or
Utils.scanMedia(getApplicationContext(),
new File[] {myOtherVideo, myAudio},
new String[] {"video/*", "audio/*"});`
How can avoid those exceptions?
It's a race condition. You are iterating over multiple files in the onMediaScannerConnected() method. But you disconnect() the connection you use to add files.
Say you have three files. File one starts and for file two you can call scanFile() without any problems as well. But before you call scanFile() for the third file, the first one has already been completed. Thus Android calls your callback method onScanCompleted(). And here you are calling disconnect() thus closing the connection you want to use for the third file. Thus with the third scanFile()call the connection is no longer valid!
This might happen, or not. Depending on which thread runs how fast and gets processing time in which particular order. Thus you get this exceptions only every now and then.
I will provide a pull request with a fix for ytdownloader if you like.
There's an exporting feature in my application. It's just a copy operation since all my settings are store in shared preference.
I just copy the xml file from /data/data/package.name/shared_prefs/settings.xml to SD card. It works fine on my HTC desire. However, it might not work on Samsung devices, and i got the following error while I try to copy the file.
I/System.out( 3166): /data/data/package.name/shared_prefs/settings.xml (No such file or directory)
in the directory.
Anyone know how to fix it, or is there another simple way to store the shared preference ?
Thanks.
Never never never never never never never never never hardwire paths.
Unfortunately, there's no getSharedPreferenceDir() anywhere that I can think of. The best solution I can think of will be:
new File(getFilesDir(), "../shared_prefs")
This way if a device manufacturer elects to change partition names, you are covered.
Try this and see if it helps.
CommonsWare's suggestion would a be clever hack, but unfortunately it won't work.
Samsung does not always put the shared_prefs directory in the same parent directory as the getFilesDir().
I'd recommend testing for the existence of (hardcode it, except for package name):
/dbdata/databases/<package_name>/shared_prefs/package.name_preferences.xml and if it exists use it, otherwise fall back to either CommonsWare's suggestion of new File(getFilesDir(), "../shared_prefs") or just /data/data/<package_name>/shared_prefs/package.name_preferences.xml.
A warning though that this method could potentially have problems if a user switched from a Samsung rom to a custom rom without wiping, as the /dbdata/databases file might be unused but still exist.
More details
On some Samsung devices, such as the Galaxy S series running froyo, the setup is this:
/data/data/<package_name>/(lib|files|databases)
Sometimes there's a shared_prefs there too, but it's just Samsung's attempt to confuse you! Don't trust it! (I think it can happen as a left over from a 2.1 upgrade to 2.2, but it might be a left over from users switching roms. I don't really know, I just have both included in my app's bug report interface and sometimes see both files).
And:
/dbdata/databases/<package_name>/shared_prefs
That's the real shared_prefs directory.
However on the Galaxy Tab on Froyo, it's weird. Generally you have: /data/data/<package_name>/(lib|shared_prefs|files|databases)
With no /dbdata/databases/<package_name> directory, but it seems the system apps do have:
/dbdata/databases/<package_name>/yourdatabase.db
And added bonus is that /dbdata/databases/<package_name> is not removed when your app is uninstalled. Good luck using SharedPreferences if the user ever reinstalls your app!
Try using
context.getFilesDir().getParentFile().getAbsolutePath()
Best way to get valid path on all devices - run method Context.getSharedPrefsFile defined as:
/**
* {#hide}
* Return the full path to the shared prefs file for the given prefs group name.
*
* <p>Note: this is not generally useful for applications, since they should
* not be directly accessing the file system.
*/
public abstract File getSharedPrefsFile(String name);
Because of it hidden need use reflection and use fallback on fail:
private File getSharedPrefsFile(String name) {
Context context = ...;
File file = null;
try {
if (Build.VERSION.SDK_INT >= 24) {
try {
Method m = context.getClass().getMethod("getSharedPreferencesPath", new Class[] {String.class});
file = (File)m.invoke(context, new Object[]{name});
} catch (Throwable e) {
Log.w("App TAG", "Failed call getSharedPreferencesPath", e);
}
}
if (file == null) {
Method m = context.getClass().getMethod("getSharedPrefsFile", new Class[] {String.class});
file = (File)m.invoke(context, new Object[]{name});
}
} catch (Throwable e) {
Log.w("App TAG", "Failed call getSharedPrefsFile", e);
file = new File(context.getFilesDir(), "../shared_prefs/" + name + ".xml");
}
return file;
}
On some Samsungs implements like this:
public File getSharedPrefsFile(String paramString) {
return makeFilename(getPreferencesDir(), paramString + ".xml");
}
private File getPreferencesDir() {
synchronized (this.mSync) {
if (this.mPreferencesDir == null) {
this.mPreferencesDir = new File("/dbdata/databases/" + getPackageName() + "/", "shared_prefs");
}
File localFile = this.mPreferencesDir;
return localFile;
}
}
On other Android like this:
public File getSharedPrefsFile(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
}
return mPreferencesDir;
}
}
private File getDataDirFile() {
if (mPackageInfo != null) {
return mPackageInfo.getDataDirFile();
}
throw new RuntimeException("Not supported in system context");
}
After while Google change API for level 24 and later:
https://android.googlesource.com/platform/frameworks/base/+/6a6cdafaec56fcd793214678c7fcc52f0b860cfc%5E%21/core/java/android/app/ContextImpl.java
I've tested in Samsung P1010 with:
//I'm in a IntentService class
File file = this.getDir("shared_prefs", MODE_PRIVATE);
I got:
"/data/data/package.name/app_shared_prefs"
It works fine to me. I can run ffmpeg in this folder.
Look:
Context.getDir
You have to create the shared_prefs directory:
try{
String dir="/data/data/package.name/shared_prefs";
// Create one directory
boolean success = (new File(dir)).mkdirs();
if (success) {
// now copy the file
}
}catch (Exception e){//Catch exception if any
System.err.println("Error: " + e.getMessage());
}
Also... the package of your app is package.name? Make sure you are referring to the right package.