20 Sep 2018
Belle

Receiving images from other apps on Android

Today I was working on making it possible to share an image (or several) from another app to Pico on Android. I needed to take the image I received and upload it to the Micro.blog API. It took me a while to piece together the different parts of this process, so I wanted to share how it works.

First up, I needed to register to receive images in my app's Manifest. Here's the example I used from the Android developer guides:

<activity android:name=".ui.MyActivity" >
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="image/*" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEND_MULTIPLE" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="image/*" />
    </intent-filter>
</activity>

This guide follows on with some example code for handling the data that's been shared with your app. The example includes two separate methods for handling images—one for a single image, and one for multiple (in Kotlin):

private fun handleSendImage(intent: Intent) {
    (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
        // Update UI to reflect image being shared
    }
}

private fun handleSendMultipleImages(intent: Intent) {
    intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)?.let {
        // Update UI to reflect multiple images being shared
    }
}

You can see that in the case where only a single image was shared, the example code casts the Parcelable from the intent extra to a Uri, and only runs the code inside the let lambda if this cast succeeds. This is great, because I'd already implemented an image picker which returned me a Uri, so I knew how to turn the Uri into a ByteArray that I could send to an API. Here's how I did that:

 private fun imageFrom(uri: Uri): ByteArray {
        val stream = contentResolver.openInputStream(uri)
        val bitmap = BitmapFactory.decodeStream(stream)
        stream.close()
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        val image = baos.toByteArray()
        return image
    }

But if you look at the example code above for handling multiple images received, you'll see I'm dealing with a Parcelable ArrayList. This is where I got stuck, because I needed a Uri for each image, but I couldn't figure out how to cast this ArrayList or even cast each item within it to what I needed.

It seems that this is because Parcelable is an Interface, so this ArrayList is full of items that conform to Parcelable, but they could be of any old type. The Kotlin compiler can't be sure that they're all Uri instances when I try to cast them that way. When I tried to cast the entire ArrayList to an ArrayList<Uri> I got a compiler warning that this was an "unchecked cast", which led me to this Stack Overflow question.

As the chosen answer to that question explains you can suppress these warnings, but since I'm not 100% sure that these will all be Uris, I went for a safer, but convenient option: Kotlin's filterIsInstance, which returns a List of the class you ask for, including only the items from the original ArrayList that are actually instances of that class. And it's just one line!

val uris: List<Uri> = it.filterIsInstance<Uri>()

So now I have a List of Uris that I can loop over, calling my imageFrom(uri) method on each one. Here's the final handleSendMultipleImages function:

private fun handleSendMultipleImages(intent: Intent) {
        intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)?.let {
            val uris: List<Uri> = it.filterIsInstance<Uri>()
            for (uri in uris) {
                val image = imageFrom(uri)
                postImage(image)
            }
        }
    }