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();
}
}
Related
We are currently obtaining the path of album art using: MediaStore.Audio.AlbumColumns.ALBUM_ART, and is successfully obtaining the path, except on pixel 3a (Android 10). After some research, the ALBUM_ART became deprecated API 29 and over as shown: Here
In this link it says: "Apps may not have file system permissions to directly access this path. Instead of trying to open this path directly, apps should use ContentResolver#loadThumbnail to gain access."
My questions are:
1) I'm already stating on the application manifest the permissions for external storage access (READ_EXTERNAL_STORAGE) and is requesting permission while navigating in-app. Which permissions do i have to provide to allow access to album art in order to obtain the path?
2) I can't seem to find any content on loadThumbnail online (and not even on ContentResolver class through code, while i am using target and compile SDK 29), if 1) can't be done, then how do i use loadThumbnail and why it's not showing on code?
Thanks in advance.
In order to use the method of ContentResolver, make sure you have the latest SDK and relevant tools installed, and in your code first instantiate a ContentResolver object and then use it accordingly:
public class MainActivity extends AppCompatActivity {
public ContentResolver resolver;
Bitmap albumArt;
Size size;
Uri uriOfItem;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
resolver = new ContentResolver(this) {
#NonNull
#Override
public Bitmap loadThumbnail(#NonNull Uri uri, #NonNull Size size, #Nullable CancellationSignal signal) throws IOException {
return super.loadThumbnail(uri, size, signal);
}
};
//uriOfItem = uri of your file
size = new Size(100, 100);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
albumArt = resolver.loadThumbnail(uriOfItem, size, null);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
EDIT: when it comes to your first question if #Rj_Innocent_Coder doesn't mind me including his comment here:
As part of the scoped-storage feature on Android Q, Google announced that SAF (storage access framework) will replace the normal storage permissions. This means that even if you will try to use storage permissions, it will only grant to access to specific types of files for File and file-path to be used
EDIT 2: after #hetoan2 's comment I check the documentation again and I noticed that ContentResolver is abstract hence not being able to use ContentResolver.loadThumbnail() as a method call. That means that within an activity you could simply use the following as well:
Bitmap albumArt = getContentResolver().loadThumbnail(uriOfFile, sizeOfAreaThatDisplaysThumbnail, cancellationSignalOrNull);
For someone else who is having issues here, this is the solution that worked for me:
if(android.os.Build.VERSION.SDK_INT >= 29)
{
val album = "Name Of Album"
val artist = "Name of Artist"
// Determine album ID first
val cursor = context.contentResolver.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
MediaStore.Audio.Albums.ALBUM_ID,
"${MediaStore.Audio.Albums.ALBUM} = '$album' AND
${MediaStore.Audio.Albums.ARTIST} = '$artist'"
,null,null)
val uri = if(cursor != null && cursor.count > 0)
{
cursor.moveToFirst()
ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, cursor.getString(0).toLong())
}
else
{
// Dummy URI that will not return an image
// If you end up here, the album is not in the DataStore
MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI
}
val bm = try
{
// Set size based on size of bitmap you want returned
context.contentResolver.loadThumbnail(uri, Size(50,50), null)
}
catch(e: java.lang.Exception)
{
// Return default image indicating no image available from DataStore
BitmapFactory.decodeResource(context.resources, R.drawable.no_image)
}
}
try this it will work and load with glide imageView
int thumbColumn = audioCursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID);
int _thumpId = audioCursor.getInt(thumbColumn);
imgFilePath = "content://media/external/audio/albumart/"+_thumpId;
audioCursor.moveToPosition(i);
Glide.with(getContext()).load(imgFilePath).placeholder(R.drawable.missed).into(tracksAlbumArt);
Update Andriod Studio latest 4.2.X and targetSdkVersion to 30
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 am writing an Android App where I need to have two persistent values that have to be read by the App. The App needs to maintain a counter which has to be stored 'somehwhere' and retrieved each time the Activity is launched. The counter is then updated and then stored before the Activity is shut down.
The important point is that I want the storing and retrieval of the counter to be done by the JNI part of my code. It should be practically invisible to the Java code.. Can this counter(memory) be accessed using JNI? If so, can you point me to which API I have to look?
I know one can use the SQLiteDatabase on the Java side. Please advise!
It's perfectly possible, but not without some Java code.
EDIT : the following is offered as an alternative to a database. Being able to read and write persistent data to/from files from native code would be a lot more flexible than a database...
Assuming you want to store and retrieve some data from a file (binary or plain text) residing on the filesystem these would be the steps to take:
JAVA : get the storage location for your app and check if it's available for reading and writing
JAVA : if the above is positive, pass it to the native layer through JNI
NATIVE : use the storage params to read/write your file
Ok, so far the abstract; lets get to the code:
1A) retreiving and checking storage:
private boolean checkExternalStorageState(){
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
// We can read and write the media
android.util.Log.i("YourActivity", "External storage is available for read/write...", null);
return true;
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
// We can only read the media : NOT ok
android.util.Log.e("YourActivity", "External storage is read only...", null);
return false;
} else {
// Something else is wrong. It may be one of many other states, but all we need
// to know is we can neither read nor write
android.util.Log.e("YourActivity", "External storage is not mounted or read only...", null);
return false;
}
}
Get the storage location :
private get storageLocation(){
File externalAppDir = getExternalFilesDir(null);
String storagePath = externalAppDir.getAbsolutePath()
}
1B) you also might want to check if a file exists (you can also do this in the native part)
private boolean fileExists(String file) {
String filePath = storagePath + "/" + file;
// see if our file exists
File dataFile = new File(filePath);
if(dataFile.exists() && dataFile.isFile())
{
// file exists
return true;
}
else
{
// file does not exist
return false;
}
}
2) Pass it to the native layer:
JAVA part:
// Wrapper for native library
public class YourNativeLib {
static {
// load required libs here
System.loadLibrary("yournativelib");
}
public static native long initGlobalStorage(String storagePath);
...enter more functions here
}
NATIVE part:
JNIEXPORT jlong JNICALL Java_com_whatever_YourNativeLib_initGlobalStorage(JNIEnv *env, jobject obj, jstring storagePath)
{
jlong data = 0;
// convert strings
const char *myStoragePath = env->GetStringUTFChars(storagepath, 0);
// and now you can use "myStoragePath" to read/write files in c/c++
//release strings
env->ReleaseStringUTFChars(storagePath, myStoragePath);
return data;
}
How to read/write binary or text files in c/c++ is well documented, I'll leave that up to you.
You can use SQLite from NDK side by including SQLite amalgamation ( http://www.sqlite.org/download.html ) in your project.
See SQLite with Android NDK
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.