Extension functions that returns save Bitmap to device storage and returns result using sealed and class that represent its state.
sealed class RequestStatus<T> {
data class Loading<T>(val data: T? = null) : RequestStatus<T>()
data class Success<T>(val data: T) : RequestStatus<T>()
data class Failed<T>(val error: Exception, val data: T? = null) : RequestStatus<T>() {
val message = nullableErrorMsg(error.localizedMessage)
}
}
fun View.createAndStoreScreenshot(
appName: String,
#ColorRes backgroundColor: Int
): Flow<RequestStatus<Uri>> = flow {
emit(RequestStatus.Loading())
... Some code
try {
FileOutputStream(imageFile).use { stream ->
// Create bitmap screen capture
val bitmap = Bitmap.createBitmap(createBitmapFromView(backgroundColor))
// This method may take several seconds to complete, so it should only be called from a worker thread.
// Read https://developer.android.com/reference/android/graphics/Bitmap#compress(android.graphics.Bitmap.CompressFormat,%20int,%20java.io.OutputStream)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
val uriForFile = FileProvider.getUriForFile(
context.applicationContext,
"${context.applicationContext.packageName}.provider",
imageFile
)
emit(RequestStatus.Success(uriForFile))
}
}
catch (e: Exception) {
emit(RequestStatus.Failed(e))
}
}
Usage
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
view.createAndStoreScreenshot(
getString(R.string.app_name),
R.color.colorWhite_PrimaryDark
).onEach { req ->
when (req) {
is RequestStatus.Loading -> {
...
}
}.collect()
}
}
FileOutputStream is complaining as it can block UI thread, but you cannot switch context inside flow builder so we move on using flowOn(Dispatchers.IO) and remove the withContext(Dispatchers.IO) from the caller. Now we the Android Studio is complaining Not enough information to infer type variable T when emitting Loading and Failed state but explicitly providing the data type also marked as redundant. What is the problem here?
It should be Uri? not Uri for Loading and Failed state.
Related
I am using the Firestore database and need to fetch some data from it. How do I make the function await for the data before returning the list?
fun getExercise(bodyPart: String): MutableList<String> {
val db = Firebase.firestore
val exercisesList = mutableListOf<String>()
val exercise = db.collection("exercises")
val query = exercise.whereEqualTo("body-part", bodyPart)
query.get().addOnSuccessListener { result ->
for(temp in result) {
exercisesList.add(temp.id)
Log.d(TAG, "${temp.id} => ${temp.data}")
}
}
.addOnFailureListener { exception ->
Log.w(TAG, "Error getting documents: ", exception)
}
return exercisesList
}
I know I need to use .await() but I am new to Kotlin and can't make it work.
I see 2 possible options:
1. Change your code in order to call another function with the result instead of returning the result
fun getExercise(bodyPart: String) {
val db = Firebase.firestore
val exercisesList = mutableListOf<String>()
val exercise = db.collection("exercises")
val query = exercise.whereEqualTo("body-part", bodyPart)
query.get().addOnSuccessListener { result ->
for(temp in result) {
exercisesList.add(temp.id)
Log.d(TAG, "${temp.id} => ${temp.data}")
}
// Call another function with the result:
anotherFunction(exercisesList)
}.addOnFailureListener { exception ->
Log.w(TAG, "Error getting documents: ", exception)
}
}
2. Implement Kotlin Coroutines
This option might be a little more complex than the first one for someone who's new to the language, as you'd need to understand the concept of Kotlin Coroutines.
Start by adding the Coroutines dependency to your build.gradle file:
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}
Change your function to become a suspend function:
suspend fun getExercise(bodyPart: String): MutableList<String> {
// ...
}
Use the await() extension function to fetch the result:
suspend fun getExercise(bodyPart: String): MutableList<String> {
val db = Firebase.firestore
val exercisesList = mutableListOf<String>()
val exercise = db.collection("exercises")
val query = exercise.whereEqualTo("body-part", bodyPart)
try {
val result = query.get().await()
for (temp in result) {
exercisesList.add(temp.id)
Log.d(TAG, "${temp.id} => ${temp.data}")
}
return exercisesList
} catch (e: Exception) {
Log.w(TAG, "Error getting documents: ", exception)
return emptyList() // returning an empty list in case the fetch fails
}
}
Use a Coroutine scope when making a call to your suspend function:
// If you're calling from an Activity/Fragment, you can use the
// lifecycleScope from the lifecycle-runtime-ktx library
//
// If you're calling from a ViewModel, consider using the
// viewModelScope from the lifecycle-viewmodel-ktx library
//
// See https://d.android.com/topic/libraries/architecture/coroutines#dependencies
// for more details
lifecycleScope.launch { // Coroutine Scope
val exercises: MutableList<String> = getExercise("arms")
// use the list
}
More resources:
Coroutines on Android
How to use Kotlin Coroutines with Firebase APIs Youtube Short
Basically, I am trying to download three different images(bitmaps) from a URL and save them to Apps Internal storage, and then use the URI's from the saved file to save a new Entity to my database. I am having a lot of issues with running this in parallel and getting it to work properly. As ideally all three images would be downloaded, saved and URI's returned simultaneously. Most of my issues come from blocking calls that I cannot seem to avoid.
Here's all of the relevant code
private val okHttpClient: OkHttpClient = OkHttpClient()
suspend fun saveImageToDB(networkImageModel: CBImageNetworkModel): Result<Long> {
return withContext(Dispatchers.IO) {
try {
//Upload all three images to local storage
val edgesUri = this.async {
val req = Request.Builder().url(networkImageModel.edgesImageUrl).build()
val response = okHttpClient.newCall(req).execute() // BLOCKING
val btEdges = BitmapFactory.decodeStream(response.body?.byteStream())
return#async saveBitmapToAppStorage(btEdges, ImageType.EDGES)
}
val finalUri = this.async {
val urlFinal = URL(networkImageModel.finalImageUrl) // BLOCKING
val btFinal = BitmapFactory.decodeStream(urlFinal.openStream())
return#async saveBitmapToAppStorage(btFinal, ImageType.FINAL)
}
val labelUri = this.async {
val urlLabels = URL(networkImageModel.labelsImageUrl)
val btLabel = BitmapFactory.decodeStream(urlLabels.openStream())
return#async saveBitmapToAppStorage(btLabel, ImageType.LABELS)
}
awaitAll(edgesUri, finalUri, labelUri)
if(edgesUri.getCompleted() == null || finalUri.getCompleted() == null || labelUri.getCompleted() == null) {
return#withContext Result.failure(Exception("An image couldn't be saved"))
}
} catch (e: Exception) {
Result.failure<Long>(e)
}
try {
// Result.success( db.imageDao().insertImage(image))
Result.success(123) // A placeholder untill I actually get the URI's to create my Db Entity
} catch (e: Exception) {
Timber.e(e)
Result.failure(e)
}
}
}
//Save the bitmap and return Uri or null if failed
private fun saveBitmapToAppStorage(bitmap: Bitmap, imageType: ImageType): Uri? {
val type = when (imageType) {
ImageType.EDGES -> "edges"
ImageType.LABELS -> "labels"
ImageType.FINAL -> "final"
}
val filename = "img_" + System.currentTimeMillis().toString() + "_" + type
val file = File(context.filesDir, filename)
try {
val fos = file.outputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.close();
} catch (e: Exception) {
Timber.e(e)
return null
}
return file.toUri()
}
Here I am calling this function
viewModelScope.launch {
val imageID = appRepository.saveImageToDB(imageNetworkModel)
withContext(Dispatchers.Main) {
val uri = Uri.parse("$PAINT_DEEPLINK/$imageID")
navManager.navigate(uri)
}
}
Another issue I am facing is returning the URI in the first place and handling errors. As if one of these parts fails, I'd like to cancel the whole thing and return Result.failure(), but I am unsure on how to achieve that. As returning null just seems meh, I'd much prefer to have an error message or something along those lines.
Kotlin/Android novice here :). I'm playing around with chunked uploads using a CoroutineWorker and don't see a built-in way to maintain state for my worker in case a retry happens, but I'm having sort of a hard time believing smth like that would be missing...
My use case is the following:
Create the worker request with the path to the file to upload as input data
Worker loops over the file and performs uploads in chunks. The latest uploaded chunkIndex is being tracked.
In case of an error and subsequent Retry(), the worker somehow retrieves the current chunk index and resumes rather than starting from at the beginning again.
So basically, I really just need to preserve that chunkIndex flag. I looked into setting progress, but this seems to be hit or miss on retries (worked once, wasn't available on another attempt).
override suspend fun doWork(): Result {
try {
// TODO check if we are resuming with a given chunk index
chunkIndex = ...
// do the work
performUpload(...)
return Result.success()
} catch (e: Exception) {
// TODO cache the chunk index
return Result.retry()
}
}
Did I overlook something, or would I really have to store that index outside the worker?
You have a pretty good use-case but unfortunately you cannot cache data within Worker class or pass on the data to the next Worker object on retry! As you suspected, you will have to store the index outside of the WorkManager provided constructs!
Long answer,
The Worker object can receive and return data. It can access the data from getInputData() method. If you chain tasks, the output of one worker can be input for the next-in-line worker. This can be done by returning Result.success(output) (see below code)
public Result doWork() {
int chunkIndex = upload();
//...set the output, and we're done!
Data output = new Data.Builder()
.putInt(KEY_RESULT, result)
.build();
return Result.success(output);
}
So the problem is we cannot return data for the retry case, only for failure and success case! (Result.retry(Data data) method is missing!)
Reference: official documentation and API.
As stated in GB's answer, there seems to be no way to cache data with in the worker, or do a Result.retry(data). I ended up just doing a quick hack with SharedPreferences instead.
Solution below. Take it with a grain of salt, I have a total of ~10 hours of Kotlin under my belt ;)
var latestChunkIndex = -1
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
// get cached entry (simplified - no checking for fishy status or anything)
val transferId = id.toString()
var uploadInfo: UploadInfo = TransferCache.tryGetUpload(applicationContext, transferId) ?: TransferCache.registerUpload(applicationContext, transferId, TransferStatus.InProgress)
if(uploadInfo.status != TransferStatus.InProgress) {
TransferCache.setUploadStatus(applicationContext, transferId, TransferStatus.InProgress)
}
// resolve the current chunk - this will allow us to resume in case we're retrying
latestChunkIndex = uploadInfo.latestChunkIndex
// do the actual work
upload()
// update status and complete
TransferCache.setUploadStatus(applicationContext, id.toString(), TransferStatus.Success)
Result.success()
} catch (e: Exception) {
if (runAttemptCount > 20) {
// give up
TransferCache.setUploadStatus(applicationContext, id.toString(), TransferStatus.Error)
Result.failure()
}
// update status and schedule retry
TransferCache.setUploadStatus(applicationContext, id.toString(), TransferStatus.Paused)
Result.retry()
}
}
Within my upload function, I'm simply keeping track of my cache (I could also just do it in the exception handler of the doWork method, but I'll use the cache entry for status checks as well, and it's cheap):
private suspend fun upload() {
while ((latestChunkIndex + 1) * defaultChunkSize < fileSize) {
// doing the actual upload
...
// increment chunk number and store as progress
latestChunkIndex += 1
TransferCache.cacheUploadProgress(applicationContext, id.toString(), latestChunkIndex)
}
}
and the TransferCache looking like this (note that there is no housekeeping there, so without cleanup, this would just continue to grow!)
class UploadInfo() {
var transferId: String = ""
var status: TransferStatus = TransferStatus.Undefined
var latestChunkIndex: Int = -1
constructor(transferId: String) : this() {
this.transferId = transferId
}
}
object TransferCache {
private const val PREFERENCES_NAME = "${BuildConfig.APPLICATION_ID}.transfercache"
private val gson = Gson()
fun tryGetUpload(context: Context, transferId: String): UploadInfo? {
return getPreferences(context).tryGetUpload(transferId);
}
fun cacheUploadProgress(context: Context, transferId: String, transferredChunkIndex: Int): UploadInfo {
getPreferences(context).run {
// get or create entry, update and save
val uploadInfo = tryGetUpload(transferId)!!
uploadInfo.latestChunkIndex = transferredChunkIndex
return saveUpload(uploadInfo)
}
}
fun setUploadStatus(context: Context, transferId: String, status: TransferStatus): UploadInfo {
getPreferences(context).run {
val upload = tryGetUpload(transferId) ?: registerUpload(context, transferId, status)
if (upload.status != status) {
upload.status = status
saveUpload(upload)
}
return upload
}
}
/**
* Registers a new upload transfer. This would simply (and silently) override any
* existing registration.
*/
fun registerUpload(context: Context, transferId: String, status: TransferStatus): UploadInfo {
getPreferences(context).run {
val upload = UploadInfo(transferId).apply {
this.status = status
}
return saveUpload(upload)
}
}
private fun getPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(
PREFERENCES_NAME,
Context.MODE_PRIVATE
)
}
private fun SharedPreferences.tryGetUpload(transferId: String): UploadInfo? {
val data: String? = getString(transferId, null)
return if (data == null)
null
else
gson.fromJson(data, UploadInfo::class.java)
}
private fun SharedPreferences.saveUpload(uploadInfo: UploadInfo): UploadInfo {
val editor = edit()
editor.putString(uploadInfo.transferId, gson.toJson(uploadInfo))
editor.apply()
return uploadInfo;
}
}
I have an array of URLs, each providing a zip file. I want to download them and store them in my app folders, inside the internal memory.
Question:
Since I do not know the number of URLs I will need to access, what is the best way to go about this? I am just beginning to work with Kotlin coroutines.
This is my 'download from url' method
fun downloadResourceArchiveFromUrl(urlString: String, context: Context): Boolean {
Timber.d("-> Started downloading resource archive.. $urlString")
lateinit var file: File
try {
val url = URL(urlString)
val urlConn = url.openConnection()
urlConn.readTimeout = 5000
urlConn.connectTimeout = 10000
val inputStream = urlConn.getInputStream()
val buffInStream = BufferedInputStream(inputStream, 1024 * 5)
val fileNameFromUrl = urlString.substringAfterLast("/")
file = File(context.getDir("resources", Context.MODE_PRIVATE) , fileNameFromUrl)
val outStream = FileOutputStream(file)
val buff = ByteArray(5 * 1024)
while (buffInStream.read(buff) != -1){
outStream.write(buff, 0, buffInStream.read(buff))
}
outStream.flush()
outStream.close()
buffInStream.close()
} catch (e: Exception) {
e.printStackTrace()
Timber.d("Download finished with exception: ${e.message} -<")
return false
}
Timber.d("Download finished -<")
return true
}
Could you simply create a loop and call download method each time?
for (i in resources.indices) {
asyncAwait {
downloadResourcesFromUrl(resources[i].url, context)
return#asyncAwait
}
Also, is it a good idea to do this synchronously? Wait for every file to download then proceed to the next one?
Turn your blocking download function into a suspending one:
suspend fun downloadResourceArchiveFromUrl(
urlString: String, context: Context
): Boolean = withContext(Dispatchers.IO) {
... your function body
}
Now run your loop inside a coroutine you launch:
myActivity.launch {
resources.forEach {
val success = downloadResourceArchiveFromUrl(it.url, context)
... react to success/failure ...
}
}
Also be sure to properly implement structured concurrency on your activity.
When uploading files through Firebase Storage, the onSuccess method isn't calling.
I'm currently running Android Studio 3.0 Canary 2, with 'com.google.firebase:firebase-storage:10.2.6'.
fun uploadImage(pathToImage: String, downloadCallback: FirebaseCallback<String?>) {
val file = Uri.fromFile(File(pathToImage))
val ref = mStorRef.child("images/"+file.lastPathSegment)
ref.putFile(file).addOnSuccessListener {
object : OnSuccessListener<UploadTask.TaskSnapshot> {
override fun onSuccess(taskSnapshot: UploadTask.TaskSnapshot?) {
val url = taskSnapshot?.downloadUrl
Log.d("FirebaseManager", "Upload Successful")
downloadCallback.callback(url.toString())
}
}
}
}
By using {}, you're passing in a lambda as the listener via SAM conversion. Inside this lambda, you're defining what should happen on success: you're creating an object that's never used. To pass in the object as the listener, use ():
ref.putFile(file).addOnSuccessListener (
object : OnSuccessListener<UploadTask.TaskSnapshot> {
override fun onSuccess(taskSnapshot: UploadTask.TaskSnapshot?) {
val url = taskSnapshot?.downloadUrl
Log.d("FirebaseManager", "Upload Successful")
downloadCallback.callback(url.toString())
}
}
)
Or go with just SAM conversion, without creating an object explicitly:
ref.putFile(file).addOnSuccessListener { taskSnapshot ->
val url = taskSnapshot?.downloadUrl
Log.d("FirebaseManager", "Upload Successful")
downloadCallback.callback(url.toString())
}