Skip to content

Instantly share code, notes, and snippets.

@ThePromoter
Created October 7, 2019 18:31
Show Gist options
  • Save ThePromoter/314826fce4cf7d1b69bdde3a565c5235 to your computer and use it in GitHub Desktop.
Save ThePromoter/314826fce4cf7d1b69bdde3a565c5235 to your computer and use it in GitHub Desktop.
A series of kotlin classes to allow uploading files to a server using OkHttp via android Uris. Supports using streams so as to avoid memory issues, and a progress update listener to get a handle into the progress of the upload.
package com.dpinciotti.utils
import android.content.ContentResolver
import android.net.Uri
import android.provider.OpenableColumns
import timber.log.Timber
import java.io.InputStream
import javax.inject.Inject
/**
* [UriUtils] implementation to be used if the uri has a scheme of [SCHEME_CONTENT]
*/
class ContentResolverUriUtils @Inject constructor(private val contentResolver: ContentResolver): UriUtils {
override fun getContentType(uri: Uri): String? = contentResolver.getType(uri)
override fun getFileName(uri: Uri): String {
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
try {
contentResolver.query(uri, projection, null, null, null)?.use {
it.moveToFirst()
return it.getString(0)
}
} catch (e: Exception) {
Timber.e(e, "Error getting fileName from URI, falling back to manual parsing")
}
return uri.lastPathSegment ?: throw Exception("Filename cannot be inferred because no lastPathSegment was found for URI $uri")
}
override fun getInputStream(uri: Uri): InputStream =
contentResolver.openInputStream(uri) ?: throw Exception("InputStream is null for URI $uri")
override fun getFileSize(uri: Uri) =
contentResolver.openFileDescriptor(uri, READ_ONLY)?.statSize ?: 0L
companion object {
private const val READ_ONLY = "r"
}
}
package com.dpinciotti.utils
import android.content.ContentResolver.SCHEME_CONTENT
import android.content.ContentResolver.SCHEME_FILE
import android.net.Uri
import java.io.InputStream
import javax.inject.Inject
/**
* [UriUtils] implementation that can delegate the processing of URIs to custom implementations.
* This version only supports [SCHEME_CONTENT] and [SCHEME_FILE]
*/
class DelegatingUriUtils @Inject constructor(
private val contentResolverUriUtils: ContentResolverUriUtils,
private val fileUriUtils: FileUriUtils
) : UriUtils {
override fun getContentType(uri: Uri): String? =
when (uri.scheme) {
SCHEME_CONTENT -> contentResolverUriUtils.getContentType(uri)
SCHEME_FILE -> fileUriUtils.getContentType(uri)
else -> throw Exception("No UriUtils instance found to handle URIs with a scheme of ${uri.scheme}")
}
override fun getFileName(uri: Uri): String =
when (uri.scheme) {
SCHEME_CONTENT -> contentResolverUriUtils.getFileName(uri)
SCHEME_FILE -> fileUriUtils.getFileName(uri)
else -> throw Exception("No UriUtils instance found to handle URIs with a scheme of ${uri.scheme}")
}
override fun getInputStream(uri: Uri): InputStream =
when (uri.scheme) {
SCHEME_CONTENT -> contentResolverUriUtils.getInputStream(uri)
SCHEME_FILE -> fileUriUtils.getInputStream(uri)
else -> throw Exception("No UriUtils instance found to handle URIs with a scheme of ${uri.scheme}")
}
override fun getFileSize(uri: Uri): Long =
when (uri.scheme) {
SCHEME_CONTENT -> contentResolverUriUtils.getFileSize(uri)
SCHEME_FILE -> fileUriUtils.getFileSize(uri)
else -> throw Exception("No UriUtils instance found to handle URIs with a scheme of ${uri.scheme}")
}
}
package com.dpinciotti.utils
import android.net.Uri
import android.webkit.MimeTypeMap
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.util.*
import javax.inject.Inject
/**
* [UriUtils] implementation to be used if the uri has a scheme of [SCHEME_FILE]
*/
class FileUriUtils @Inject constructor() : UriUtils {
override fun getContentType(uri: Uri): String? =
MimeTypeMap.getFileExtensionFromUrl(uri.toString()).let {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.toLowerCase(Locale.ROOT))
}
override fun getFileName(uri: Uri): String =
uri.lastPathSegment ?: throw Exception("Filename cannot be inferred because no lastPathSegment was found for URI $uri")
override fun getInputStream(uri: Uri): InputStream = FileInputStream(File(uri.path))
override fun getFileSize(uri: Uri): Long = File(uri.path).length()
}
package com.dpinciotti.utils
import android.net.Uri
interface UploadProgressListener {
/**
* Called periodically as the file is streamed to the server
* @param bytesWritten The count of bytes that have been successfully sent to the server so far
* @param contentLength The total count of bytes that need to be sent to the server
*/
fun onProgressUpdate(fileUri: Uri, bytesWritten: Long, contentLength: Long) {}
}
package com.dpinciotti.utils
import android.net.Uri
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okio.BufferedSink
import okio.Source
import okio.source
/**
* An OkHttp [RequestBody] implementation to be used for uploading a [Uri]
* Optionally supports getting progress updates as file is being uploaded via the [UploadProgressListener]
*/
class UriRequestBody(
private val fileUri: Uri,
private val uriUtils: UriUtils,
private val listener: UploadProgressListener? = null
) : RequestBody() {
private val contentType: MediaType by lazy {
val contentType = uriUtils.getContentType(fileUri) ?: "*/*"
contentType.toMediaTypeOrNull() ?: throw Exception("Unable to parse contentType $contentType")
}
private val contentLength: Long by lazy {
uriUtils.getFileSize(fileUri)
}
override fun contentType() = contentType
override fun contentLength() = contentLength
override fun writeTo(sink: BufferedSink) {
uriUtils.getInputStream(fileUri).source().use {
sink.writeAllWithCallback(it)
}
}
private fun BufferedSink.writeAllWithCallback(source: Source) {
var totalBytesRead = 0L
var lastProgressPercentUpdate = 0f
val buffer = buffer
while (true) {
val readCount: Long = source.read(buffer, BUFFER_SIZE)
if (readCount == -1L) break
totalBytesRead += readCount
emitCompleteSegments()
lastProgressPercentUpdate = listener?.let { updateProgress(totalBytesRead, lastProgressPercentUpdate, it) } ?: 0f
}
// Give a final update once we're done writing everything
listener?.let { updateProgress(totalBytesRead, 0f, it) }
}
private fun updateProgress(
totalBytesRead: Long,
lastProgressPercentUpdate: Float,
listener: UploadProgressListener
): Float {
val progress = (totalBytesRead.toFloat() / contentLength.toFloat()) * 100f
//prevent publishing too many updates, which slows upload, by checking if the upload has progressed by at least 1 percent
if (lastProgressPercentUpdate == 0f || progress - lastProgressPercentUpdate > 1 || totalBytesRead >= contentLength) {
// publish progress
listener.onProgressUpdate(fileUri, totalBytesRead, contentLength)
return progress
}
return lastProgressPercentUpdate
}
companion object {
private const val BUFFER_SIZE = 8192L
}
}
package com.dpinciotti.utils
import android.net.Uri
import java.io.InputStream
/**
* Defines how to pull common meta-data out of a particular URI
*/
interface UriUtils {
fun getContentType(uri: Uri): String?
fun getFileName(uri: Uri): String
fun getInputStream(uri: Uri): InputStream
fun getFileSize(uri: Uri): Long
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment