Created
October 7, 2019 18:31
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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