Multipart Uploads with OkHttp... and a Uri
As changes like scoped storage increase the need to be able to work with
content Uri values, I have been increasingly pointing developers
to this OkHttp issue comment.
If you have a need to do a multipart form upload, and your content is identified
by a Uri instead of a File, the InputStreamRequestBody that Jared Burrows
created (with help from Jake Wharton) is what you need.
However, I never created a sample app for this, simply because I did not have a
Web service needing that sort of upload. I realized a week ago that RequestBin
exists and can handle this scenario. So, this evening I tossed together
this sample app to demonstrate
a Kotlin port of InputStreamRequestBody and its use.
The OkHttp recipes page
shows how to use MultipartBody.Builder to create the body of a POST request. That
MultipartBody.Builder has addFormDataPart() methods that let you fill in the elements
of the “form”. For including a “file” (from the standpoint of the form), addFormDataPart()
can accept a RequestBody. The specific recipe in that OkHttp documentation shows
creating one from a File, such as by using the asRequestBody() extension function
in Kotlin.
If you have a content Uri, though, OkHttp alone will not be sufficient. OkHttp
supports non-Android environments. As a result, OkHttp knows nothing
about Uri or ContentResolver or other Android SDK classes. But RequestBody
is an abstract class, so we can create our own implementation that can use
Android SDK classes for our own Android projects. That’s what Jared did in that OkHttp
issue.
In Kotlin, an equivalent implementation looks like this:
class InputStreamRequestBody(
  private val contentType: MediaType,
  private val contentResolver: ContentResolver,
  private val uri: Uri
) : RequestBody() {
  override fun contentType() = contentType
  override fun contentLength(): Long = -1
  @Throws(IOException::class)
  override fun writeTo(sink: BufferedSink) {
    val input = contentResolver.openInputStream(uri)
    input?.use { sink.writeAll(it.source()) }
      ?: throw IOException("Could not open $uri")
  }
}
Now, I spent perhaps an hour on this sample. As the saying goes, “your mileage may vary”, and your standard concerns about using code from blog posts are warranted. That said, it’s a fairly straight-up port of Jared’s original Java sample, and it works in light testing.
You can create an instance of InputStreamRequestBody and include it in an
addFormDataPart() call where the OkHttp recipe shows the one based on the File.
So, you wind up with something like this:
val contentPart = InputStreamRequestBody(type, resolver, content)
val requestBody = MultipartBody.Builder()
  .setType(MultipartBody.FORM)
  // TODO add other form elements here
  .addFormDataPart("something", name, contentPart)
  .build()
val request = Request.Builder()
  .url(serverUrl)
  .post(requestBody)
  .build()
ok.newCall(request).execute().use { response ->
  // TODO something with the response
}
Here:
- 
    
typeis an OkHttpMediaTypeidentifying the MIME type of the content - 
    
resolveris aContentResolver, obtained from a handyContext - 
    
contentis theUrito the content to upload - 
    
nameis some “filename” for the content - 
    
serverUrlis the URL you want toPOSTto 
In the sample app, I have a version of that code that uses DocumentFile to
obtain the MIME type and name to use. My serverUrl comes from BuildConfig; 
see the sample app’s README for information about where that URL will need
to come from if you clone and try running the sample. The content Uri
comes from ACTION_OPEN_DOCUMENT: the user clicks a big “Upload Something!”
button, chooses a document, and that document gets uploaded to the server.
If you are using a different HTTP client library, it needs some similar solution
if it is going to behave well on modern versions of Android, where filesystem
access is constrained and content Uri values are increasingly common.
Many thanks to Jared and Jake for the original implementation!
UPDATE: 2020-07-06: See also this related blog post
from cketti. It shows a similar ContentUriRequestBody and also shows the
download-to-a-Uri scenario.

