Android Mediastore

Low angle shot of CDs and music posters in a music shop.
Photo by Surya Urs on Unsplash

Sometimes, when building an Android app, I find the exact API that will solve all of my problems, only to find out it was just added in the latest version of the OS, and there’s no Compt class for it.

But sometimes, I go looking for something and find that it was added in API level 1, which is exactly the case with MediaStore.

MediaStore is an example of one of the system’s ContentProvider classes. The role of a ContentProvider, in case you haven’t worked with them before, is to expose data in your app to another app.

In the case of MediaStore, it’s the system providing access to images, including photos, videos, and music, to apps.

In the beginning

Accessing files was pretty simple in the early days of Android:

contentResolver.query(EXTERNAL_CONTENT_URI, null, null, null, "$DATE_TAKEN DESC")?.use { cursor ->
    val dataColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)

    val titleColumn = cursor.getColumnIndex(MediaStore.MediaColumns.TITLE)
    while (cursor.moveToNext()) {
        val mediaFile = File(cursor.getString(dataColumnIndex))
	    // Do something with the file.
    }
}

When Android KitKat was released, the guidance started to shift. It was recommended to use a new method on ContentResolver to open an image with its Uri, rather than from the path in the data column.

contentResolver.query(EXTERNAL_CONTENT_URI, null, null, null, "$DATE_TAKEN DESC")?.use { cursor ->
    val idColumn = cursor.getColumnIndex(MediaStore.MediaColumns._ID)

    val titleColumn = cursor.getColumnIndex(MediaStore.MediaColumns.TITLE)
    while (cursor.moveToNext()) {
        val mediaUri =
            Uri.withAppendedPath(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                cursor.getString(idColumn)
            )

        contentResolver.openFileDescriptor(mediaUri, "r").use { fileDescriptor ->
            // Do something with the FileDescriptor.
        }
    }
}

A bit more complicated, and given that the data column continued to work the same, it’s not surprising that many people ignored this guidance and continued to use direct file access everywhere.

With Android Q Beta, the column MediaStore.MediaColumns.DATA has been deprecated.

Shoot.

So, what do we do now?

What do we do no?

Panic No, that’s not right. Don’t panic, that’s it.

If you can, the best approach would be to switch from using file paths to Uris and using ContentResolver.openFileDescriptor to open them.

But what about my C++ code?

This is a more complicated question. If the app’s native code is only working with a single, or set of files, at a time, the best approach is to open the file in Kotlin (or Java), and pass the open file descriptor(s) to your native code.

contentResolver.openFileDescriptor(mediaUri, "r").use { fileDescriptor ->
    nativeMethod(fileDescriptor.fd)
}

Just note that when done this way, the file descriptor will be closed at the end of the use block.

If you’d rather have your native code take over handling the file descriptor, you can use fileDescriptor.detachFd() instead. At that point, you’ll want to call close on the file descriptor.

If your app works primarily in native code, then it gets more complicated. The best advice I’d have is to create a Kotlin/Java JNI method to, given a Uri, returns a disconnected file descriptor.

For example:

First, we’ll need a method to save a Context reference so our helper method will have access to the system’s ContentResolver:

/**
 * Method to save an Application Context and save Java references to our helper method.
 * @param env The Java environment.
 * @param context The Application Context to use later.
 */
JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_setContextNative(
        JNIEnv *env,
        jobject /* this */,
        jobject context) {

    // Save a reference to the JavaVM for later. This must be done
    // on the correct thread, etc...
    jint rs = env->GetJavaVM(&_jvm);
    assert (rs == JNI_OK);

    // Find our helper method and save the references to those.
    _mainActivityClass = env->FindClass("com/example/myapplication/MainActivityKt");
    _openUriId = env->GetMethodID(_mainActivityClass, "openUri", "(Landroid/content/Context;Landroid/net/Uri;)I");

    // Save a reference to the application Context, required to use ContentResolver.
    _context = context;
}

Next, we have a simple method to open a Uri object from C++ and return an open file descriptor:

/**
 * Open a content Uri and return an open file descriptor that C++ controls.
 * @param contentUri The Uri to open.
 * @return An open file descriptor.
 */
int crOpen(jobject contentUri) {
    JNIEnv *env;
    jint rs = _jvm->AttachCurrentThread(&env, nullptr);
    assert (rs == JNI_OK);
    return env->CallStaticIntMethod(_mainActivityClass, _openUriId, _context, contentUri);
}

The Kotlin side in this example is simple, but could be made more complex to handle error cases more appropriately:

/**
 * Opens a Uri as a
 */
fun openUri(context: Context, uri: Uri) =
    context.contentResolver.openFileDescriptor(uri, "r").use {
        it?.detachFd() ?: -ENOENT
    }
private const val ENOENT = 3025