29

I have resource (music file) pointed by Uri. How can I check if it is available before I try to play it with MediaPlayer?

Its Uri is stored in database, so when the file is deleted or on external storage that is unmounted, then I just get exception when I call MediaPlayer.prepare().

In above situation I would like to play systems default ringtone. I could of course do that after I catch above exception, but maybe there is some more elegant solution?

edit: I forgot to mention that music files Uri's are actually acquired by using RingtonePreference. This means that I can get Uri pointing to ringtone on Internal Storage, External Storage or to default systems ringtone.

Uri's examples are:

  • content://settings/system/ringtone - for choosing default ringtone
  • content://media/internal/audio/media/60 - for ringtone on Internal Storage
  • content://media/external/audio/media/192 - for ringtone on External Storage

I was happy with proposed "new File(path).exists() method, as it saved me from mentioned exception, but after some time I noticed that it returns false for all of my ringtone choices... Any other ideas?

Koger
  • 1,783
  • 2
  • 23
  • 34
  • Do you get these URI using `RingtoneManager.getCursor()`? My understanding is that _anything_ the cursor returns should be available... – Laurent' Oct 04 '11 at 14:48

7 Answers7

33

The reason the proposed method doesn't work is because you're using the ContentProvider URI rather than the actual file path. To get the actual file path, you have to use a cursor to get the file.

Assuming String contentUri is equal to the content URI such as content://media/external/audio/media/192

ContentResolver cr = getContentResolver();
String[] projection = {MediaStore.MediaColumns.DATA}
Cursor cur = cr.query(Uri.parse(contentUri), projection, null, null, null);
if (cur != null) {
  if (cur.moveToFirst()) {
    String filePath = cur.getString(0);

    if (new File(filePath).exists()) {
      // do something if it exists
    } else {
      // File was not found
    }
  } else {
     // Uri was ok but no entry found. 
  }
  cur.close();
} else {
  // content Uri was invalid or some other error occurred 
}

I haven't used this method with sound files or internal storage, but it should work. The query should return a single row directly to your file.

tasomaniac
  • 10,234
  • 6
  • 52
  • 84
DeeV
  • 35,865
  • 9
  • 108
  • 95
  • It works great, thank you very much. After testing it, I can say that cur==null in situation, where contentUri points to content on disconnected external drive, and "File won't exist" for example in situation, when it is renamed (or deleted, I guess) – Koger Oct 04 '11 at 20:03
  • 2
    Unfortunatelly it won't work for content://settings/system/ringtone Uri, but that's pretty logical and I can work it around – Koger Oct 04 '11 at 20:22
  • The projection in my example is looking for MediaStore columns. To check for that you need to do a separate query for the Android Settings content provider. – DeeV Oct 04 '11 at 20:38
  • @DeeV What should be done to check uris of ringtones ? also, where do you search in order to know what to put there? – android developer Oct 02 '14 at 11:18
  • @androiddeveloper I've never had to search for ringtones specifically. I'm actually surprised it doesn't work for ringtones as they are included as media. I'm wondering if it's a problem with some legacy methods with managing ringtones. There is the column `MediaStore.Audio.AudioColumns.IS_RINGTONE` which I believe will list music files that the user set as ringtones. Otherwise, you may have to use the RingtoneManager class. I'll set something up to see. – DeeV Oct 02 '14 at 17:27
  • 1
    I've found a way to check if it's available, but I'm not sure if that's the best way: http://developer.android.com/reference/android/content/ContentResolver.html#openInputStream(android.net.Uri) . if it succeeds, you close the inputStream and you can assume the ringtone can be used (unless someone deletes the file just before you use it), and if not, the file was probably deleted from its original path. getting the ringtone from the RingtoneManager doesn't mean that you can use it, as the file might have been deleted after it got to the DB. – android developer Oct 02 '14 at 21:13
  • Oh, that's a whole different problem. You can go in and rearrange or delete media from outside the ContentProvider's scope and screw up everything. This is the case for everything; not just Ringtones. In an ideal world, you should use a ContentResolver's `delete` method to remove files, but people NEVER adhere to that. Generally a reboot can fix as it'll start a MediaScanner, but as I've recently found out, MediaScanner is extremely slow and unreliable. – DeeV Oct 02 '14 at 21:19
  • 1
    Cursor cur = cr.query(Uri.parse(contentUri), projection, null, null, null); throws permission denial exception. – Vikash Sharma Feb 13 '18 at 06:34
  • @Vikash exactly my problem. – Neon Warge May 28 '18 at 14:40
  • @DeeV this doesn't seem to wokr, this throws an exception indicating Permission Denial, I need to have permission Manage Documents. I just copy paste this code as is and this kept throwing this exception. Any idea what this is? – Neon Warge May 28 '18 at 14:54
  • @NeonWarge Since API 23, there are some resources that you need to ask the user for permission before you can access them. – DeeV May 29 '18 at 02:11
  • you miss ; at the end of String[] projection = {MediaStore.MediaColumns.DATA} – Yohanim Sep 18 '20 at 09:40
9

I too had this problem - I really wanted to check if a Uri was available before trying to load it, as unnecessary failures would end up crowding my Crashlytics logs.

Since the arrival of the StorageAccessFramework (SAF), DocumentProviders, etc., dealing with Uris has become more complicated. This is what I eventually used:

fun yourFunction() {

    val uriToLoad = ...

    val validUris = contentResolver.persistedUriPermissions.map { uri }

    if (isLoadable(uriToLoad, validUris) != UriLoadable.NO) {
        // Attempt to load the uri
    }
}

enum class UriLoadable {
    YES, NO, MAYBE
}

fun isLoadable(uri: Uri, granted: List<Uri>): UriLoadable {

    return when(uri.scheme) {
        "content" -> {
            if (DocumentsContract.isDocumentUri(this, uri))
                if (documentUriExists(uri) && granted.contains(uri))
                    UriLoadable.YES
                else
                    UriLoadable.NO
            else // Content URI is not from a document provider
                if (contentUriExists(uri))
                    UriLoadable.YES
                else
                    UriLoadable.NO
        }

        "file" -> if (File(uri.path).exists()) UriLoadable.YES else UriLoadable.NO

        // http, https, etc. No inexpensive way to test existence.
        else -> UriLoadable.MAYBE
    }
}

// All DocumentProviders should support the COLUMN_DOCUMENT_ID column
fun documentUriExists(uri: Uri): Boolean =
        resolveUri(uri, DocumentsContract.Document.COLUMN_DOCUMENT_ID)

// All ContentProviders should support the BaseColumns._ID column
fun contentUriExists(uri: Uri): Boolean =
        resolveUri(uri, BaseColumns._ID)

fun resolveUri(uri: Uri, column: String): Boolean {

    val cursor = contentResolver.query(uri,
            arrayOf(column), // Empty projections are bad for performance
            null,
            null,
            null)

    val result = cursor?.moveToFirst() ?: false

    cursor?.close()

    return result
}

If someone has a more elegant -- or correct -- alternative, please do comment.

jules
  • 514
  • 5
  • 8
8

Try a function like:

public static boolean checkURIResource(Context context, Uri uri) {
    Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
    boolean doesExist= (cursor != null && cursor.moveToFirst());
    if (cursor != null) {
        cursor.close();
    }
    return doesExist;
}
HB.
  • 4,116
  • 4
  • 29
  • 53
nkmuturi
  • 248
  • 3
  • 10
4

For those still looking out for a solution [works perfectly fine as of Dec 2020] and behaves as expected for all edge cases, the solution is a follows:

boolean bool = false;
        if(null != uri) {
            try {
                InputStream inputStream = context.getContentResolver().openInputStream(uri);
                inputStream.close();
                bool = true;
            } catch (Exception e) {
                Log.w(MY_TAG, "File corresponding to the uri does not exist " + uri.toString());
            }
        }

If the file corresponding to the URI exists, then you will have an input stream object to work with, else an exception will be thrown.

Do not forget to close the input stream if the file does exist.

NOTE:

DocumentFile sourceFile = DocumentFile.fromSingleUri(context, uri);
boolean bool = sourceFile.exists();

The above lines of code for DocumentFile, does handle most edge cases, but what I found out was that if a file is created programmatically and stored in some folder, the user then visits the folder and manually deletes the file (while the app is running), DocumentFile.fromSingleUri wrongly says that the file exists.

mang4521
  • 742
  • 6
  • 22
  • I'm using `DocumentFile.fromSingleUri` but I don't have the behaviour you describe. If the user moves or delete the file, if I re-create the object and call `exists()`, it return `false` – MatPag Jan 25 '21 at 16:53
  • Is this in reference to a new file created by your app [in the internal storage folder for your app]? – mang4521 Jan 29 '21 at 16:40
3

As of Kitkat you can, and you should, persist URIs that your app uses if necessary. As far as I know, there's a 128 URI limit you can persist per app, so it's up to you to maximize usage of those resources.

Personally I wouldn't deal with direct paths in this case, but rather check if persisted URI still exists, since when resource (a file) is deleted from a device, your app loses rights to that URI therefore making that check as simple as the following:

getContext().getContentResolver().getPersistedUriPermissions().forEach( {element -> element.uri == yourUri});

Meanwhile you won't need to check for URI permissions when a device is below Kitkat API level.

Usually, when reading files from URIs you're going to use ParcelFileDescriptor, thus it's going to throw if no file is available for that particular URI, therefore you should wrap it with try/catch block.

Dragas
  • 1,140
  • 13
  • 29
  • 1
    Unfortunately, the javadoc for `getPersistedUriPermissions()` seems to imply that only URIs previously taken with `takePersistableUriPermission(Uri, int)` are returned, and this list is not necessarily updated when a ContentProvider deletes a resource (my testing on Oreo bears this use case out). – jules May 02 '18 at 04:10
0

2021-08-17 10:53:50

GitHub https://github.com/javakam/FileOperator/blob/master/library_core/src/main/java/ando/file/core/FileUtils.kt#L317

基于热评第一的方案, 给出我的解决方式(兼容Q,P):

Based on the hottest solution, give me a solution (compatible with Q, P):

/**
 * 1. 检查 Uri 是否正确
 * 2. Uri 指向的文件是否存在 (可能是已删除, 也肯是系统 db 存有 Uri 相关记录, 但是文件失效或者损坏)
 *
 * 1. Check if Uri is correct
 * 2. Whether the file pointed to by Uri exists (It may be deleted, it is also possible that the system db has Uri related records, but the file is invalid or damaged)
 *
 * https://stackoverflow.com/questions/7645951/how-to-check-if-resource-pointed-by-uri-is-available
 */
fun checkRight(uri: Uri?): Boolean {
    if (uri == null) return false
    val resolver = FileOperator.getContext().contentResolver
    //1. Check Uri
    var cursor: Cursor? = null
    val isUriExist: Boolean = try {
        cursor = resolver.query(uri, null, null, null, null)
        //cursor null: content Uri was invalid or some other error occurred
        //cursor.moveToFirst() false: Uri was ok but no entry found.
        (cursor != null && cursor.moveToFirst())
    } catch (t: Throwable) {
        FileLogger.e("1.Check Uri Error: ${t.message}")
        false
    } finally {
        try {
            cursor?.close()
        } catch (t: Throwable) {
        }
    }
    //2. Check File Exist
    //如果系统 db 存有 Uri 相关记录, 但是文件失效或者损坏 (If the system db has Uri related records, but the file is invalid or damaged)
    var ins: InputStream? = null
    val isFileExist: Boolean = try {
        ins = resolver.openInputStream(uri)
        // file exists
        true
    } catch (t: Throwable) {
        // File was not found eg: open failed: ENOENT (No such file or directory)
        FileLogger.e("2. Check File Exist Error: ${t.message}")
        false
    } finally {
        try {
            ins?.close()
        } catch (t: Throwable) {
        }
    }
    return isUriExist && isFileExist
}
javakam
  • 127
  • 1
  • 5
-1

There are few methods of DocumentProvider when applied to uri, return null if there is no document underlying uri. I chose getType(uri) which returns mime type of the document/file. If no document/file exists represented in uri, it returns null. Hence, to detect whether documennt/file exists or not, you can use this method like below.

public static boolean exists(Context context, Uri uri)
{
    return context.getContentResolver().getType(uri) !=null;
    
}

Other methods mentioned like querying the uri to get documentID or opening inputstream/outputstream did not work because they throw filenotfound exception if document/file does not exist, which resulted in crashing of app.

You may attempt other methods which return null instead of throwing filenotfoundexception, if document/file does not exist.

shankar_vl
  • 72
  • 6
  • This does not work for a ringtone URI. getType does not return null if no ringtone is assigned to the URI – Dave Nottage Jul 23 '20 at 04:50
  • What I understand from the OP that he wants to ascertain whether file exists underlying the Uri before it is played. So, before acting upon uri, you just check the getType returns null. If it returns null, it means there is no file exists either due to deletion or storage dismounted. If it does return any type, then assume that a file exists. – shankar_vl Aug 06 '20 at 15:30
  • My point is that if no ringtone is assigned to the Uri, getType will not return null, however attempting to play the ringtone will raise an error. In this case, although exists correctly returns true, that does not guarantee that the Uri will be "usable". I'm finding it difficult to discover a method that can check a ringtone Uri for whether it has been assigned "None" in the settings by the user. – Dave Nottage Aug 06 '20 at 22:24
  • I don't think the method returns true when there is no ringtone i.e., file, underlying an uri. May be I am not able to comprehend the exact problem you are facing. Then, I don't see any solution other than as you mentioned, watching for exception occurring on Media.prepare method and acting based on the exception caught. – shankar_vl Aug 11 '20 at 11:46
  • The proof is in actually setting the default notification sound to None, and executing that code. Presumably, you have not even tried this. On Android 11, open Settings, go to Sound and vibration, select Advanced (at the bottom), select Default notification sound, select "My Sounds", and select None. Execute your code using the default URI for TYPE_NOTIFICATION: getType returns a non-null value. This has come back to bite me again and there still appears to be no answer that works – Dave Nottage Aug 19 '21 at 01:11