Download multiple images in a CoroutineScope - android

I need to download multiple images, and after all downloads are completed (outside of the Main Thread), perform other actions in the activity.
I am currently using Glide to download as follows:
ImageDownloader.kt
class ImageDownloader {
fun downloadPack(context: Context, path: String, pack: PackModel) {
for (image: ImageModel in pack.images) {
Glide.with(context)
.asBitmap()
.load(image.imageFileUrl)
.listener(object : RequestListener<Bitmap> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
return false
}
override fun onResourceReady(bitmap: Bitmap?, model: Any?, target: Target<Bitmap>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
saveImage(path, pack.id, image.imageFileName, bitmap!!)
return true
}
}).submit()
}
}
private fun saveImage(path: String, id: String, fileName: String, bitmap: Bitmap) {
val dir = File(path + id)
if (!dir.exists()) dir.mkdirs()
val file = File(dir, fileName)
val out = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 75, out)
out.flush()
out.close()
// to check the download progress for each image in logcat
println("done: $fileName")
}
}
In the activity I call this method inside a CoroutineScope as follows:
PackActivity.kt
class PackActivity: AppCompatActivity() {
private lateinit var bind: ActivityPackBinding
private lateinit var path: String
private lateinit var pack: PackModel
// other basic codes
override fun onCreate(savedInstanceState: Bundle?) {
// other basic codes
path = "$filesDir/images_asset/"
pack = intent.getParcelableExtra(PACK_DATA)!!
bind.buttonDownload.setOnClickListener {
downloadPack()
}
}
private fun downloadPack() {
CoroutineScope(Dispatchers.IO).launch {
val async = async {
ImageDownloader().downloadPack(applicationContext, path, pack)
}
val result = async.await()
withContext(Dispatchers.Main) {
result.apply {
println("finished")
// other things todo
}
}
}
}
}
I am trying to proceed with other actions after downloading all the images in PackActivity.kt, but as a result, using println("finished") and checking the logcat, the code is starting even before the first download starts...
Some information:
PackModel and ImageModel are my data class, where PackModel has the Id of each pack and a list of ImageModel, which in turn has the ImageFileName and ImageFileUrl. All data is obtained from a web request.
I want the images to be saved in the folder data/data/AppPackageName/files/images_asset/PackID/... And with the tests I did, I was unable to use DownloadManager directing the images to this internal folder of the App, that's why I'm using Glide.

The first, crucial step is to adapt the async Glide request into a suspend fun using suspendCancellableCoroutine. Here's how:
private suspend fun downloadBitmap(
context: Context,
image: ImageModel
) = suspendCancellableCoroutine<Bitmap> { cont ->
Glide.with(context)
.asBitmap()
.load(image.imageFileUrl)
.listener(object : RequestListener<Bitmap> {
override fun onLoadFailed(
e: GlideException, model: Any?,
target: Target<Bitmap>?, isFirstResource: Boolean
): Boolean {
cont.resumeWith(Result.failure(e))
return false
}
override fun onResourceReady(
bitmap: Bitmap, model: Any?, target: Target<Bitmap>?,
dataSource: DataSource?, isFirstResource: Boolean
): Boolean {
cont.resumeWith(Result.success(bitmap))
return false
}
}).submit()
}
With that done, now you'll have an easy time of making concurrent downloads and awaiting on all of them:
class ImageDownloader {
suspend fun downloadPack(context: Context, path: String, pack: PackModel) {
coroutineScope {
for (image: ImageModel in pack.images) {
launch {
val bitmap = downloadBitmap(context, image)
saveImage(path, pack.id, image.imageFileName, bitmap)
}
}
}
}
private suspend fun saveImage(
path: String, id: String, imageFileName: String, bitmap: Bitmap
) {
withContext(IO) {
// your code
}
}
}
Watch carefully which dispatchers I use above: using the Main dispatcher for everything except saveImage, which is the only place that contains code that is actually blocking.
Finally, to use everything, this is all you need:
private fun downloadPack() {
GlobalScope.launch {
ImageDownloader().downloadPack(applicationContext, path, pack)
println("finished")
// other things todo
}
}
Again, everything on the Main dispatcher because the blocking code is safely corralled into the IO dispatcher.
I use GlobalScope above for the lack of knowledge of your larger context, but it's probably a bad idea. Writing CoroutineScope(IO).launch has all the same problems, plus allocating several more objects.
Think twice about what's going to happen to your downloads if the user navigates away from the app, or if they navigate away and back repeatedly, triggering a growing pile of background downloads. In the code above, i didn't treat cancellation within suspendCancellableCoroutine because I'm not that intimate with Glide. You should add a cont.onCancellation handler to be correct.

Related

Returns null when trying to get drawable from ImageView

I'm trying to solve a problem with getting the dominant color in an image, but I can't convert it to a bitmap, because I get null all the time. What could the problem be?
View itself in XML:
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/ivIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintCircleRadius="8dp"
app:srcCompat="#drawable/icon_default"
tools:srcCompat="#drawable/icon_default" />
Getting the image goes through Glide by loading the image by url (fun loadInto (...)) and after that I try to get the drawable:
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewOutput.iconFlow.collect { photoUrl ->
if (url != null) {
loadInto(url, binding.Icon)
getDominantColor(binding.ivIcon) <- null here
}
}
}
Update:
private fun getDominantColor(image: AppCompatImageView) {
val bitmap: Bitmap = (image.drawable as BitmapDrawable).bitmap
Palette.from(bitmap).generate { palette ->
val dom: Int = palette!!.getDominantColor(0x000000)
setGradientColor(dom)
}
}
fun loadInto(
url: String,
imageView: ImageView,
placeHolder: Int = 0,
errorHolder: Int = 0,
) {
Glide
.with(imageView)
.load(url)
.run { if (placeHolder != 0) placeholder(placeHolder) else this }
.run { if (errorHolder != 0) error(errorHolder) else this }
.into(imageView)
}
As mentioned by #Lino, Glide will make an asynchronous call to get the image. This will take some time and it may return null if you get the Bitmap from the ImageView right after it.
Your attempt to create a listener callback is correct. And if it is properly implemented, onResourceReady() method should be called once ready. You may refer the sample below:
Glide
.with(this#MainActivity)
.load(url)
.run { this }
.run { this }
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
// Process with your bitmap in callback here
Palette.from((resource!! as BitmapDrawable).bitmap).generate { palette ->
val dom: Int = palette!!.getDominantColor(0x000000)
setGradientColor(dom)
}
return false
}
})
.into(ivIcon)

Android: SVG Drawables Stop Working or Glitch in RecyclerView Lists

I am having an issue where I imported some SVG drawables (that are optimised in Illustrator and have short path data - so their complexity is out of discussion) and displayed them in RecyclerView items. The problem is that, after testing the application many times, they stop working or they start rendering with glitches (like missing chunks or shapes). Weirdly enough, an app cache wipe resolves the issue and they work normally until after I ran the app from Android Studio about 5-6 times.
Here is what I mean by 'stopped working' :
In one activity they appear as red warnings, in another one they appear as a fingerprint icon (tho I do not have such an icon in the entire project, nor fingerprint implementation).
Here is the implementation:
I add the entries in room database like this:
Category(icon = R.drawable.ic_category_homepage)
where a category data class looks like this:
#Entity(tableName = "categories")
data class Category(
val title: String,
#DrawableRes
val icon: Int
)
So I add the SVG drawable reference as a DrawableRes Int in the local storage. Then, when I'm displaying the icon in the adapter, I use Glide:
Glide.with(context)
.load(category.icon)
.transition(DrawableTransitionOptions.withCrossFade())
.into(itemView.categoryIV)
Here is the entire adapter:
class DrawerAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val categories: ArrayList<Category> = ArrayList()
fun submitCategories(newFeed: List<Category>, lifecycleCoroutineScope: LifecycleCoroutineScope) {
lifecycleCoroutineScope.launch {
val result = coroutineRunOnComputationThread {
val oldFeed = categories
val result: DiffUtil.DiffResult = DiffUtil.calculateDiff(
DrawerDiffCallback(oldFeed, newFeed)
)
categories.clear()
categories.addAll(newFeed)
result
}
coroutineRunOnMainThread {
result.dispatchUpdatesTo(this#DrawerAdapter)
}
}
}
override fun getItemCount(): Int = categories.size
override fun getItemId(position: Int): Long {
return if (categories.isNullOrEmpty()) 0 else categories[position].id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DrawerItemViewHolder(parent.inflate(R.layout.item_drawer_menu))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) =
(holder as DrawerItemViewHolder).bind(categories[position])
inner class DrawerItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(category: Category) = with(itemView) {
Glide.with(context)
.load(category.icon)
.transition(DrawableTransitionOptions.withCrossFade())
.into(itemDrawerIVIcon)
if (category.preConfigured && category.resTitle != null)
itemDrawerTVTitle.text = context.resources.getString(category.resTitle)
else
itemDrawerTVTitle.text = category.title
}
}
private inner class DrawerDiffCallback(
private var oldFeed: List<Category>,
private var newFeed: List<Category>
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldFeed[oldItemPosition]
val newItem = newFeed[newItemPosition]
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldFeed[oldItemPosition]
val newItem = newFeed[newItemPosition]
return oldItem == newItem
}
override fun getOldListSize(): Int = oldFeed.size
override fun getNewListSize(): Int = newFeed.size
}
}
Any idea why I get this weird behavior?
Hope this will resolve your glitching issue.
Picasso.get().load(category.icon)
.error(R.drawable.placeholder_round)
.placeholder(R.drawable.placeholder_round)
.resize(100, 100)
.into(itemDrawerIVIcon)
Just replace your Glide with Picasso with above config

build own cache, in Kotlin

I have a list with four elements: created_at, text, name, screen_name. The first represent a date of creation, the second the texto of a tweet and the latest the name and screen name of user.
I want to storage this information with lifespan, a random lifespan. For this i thinking using the cache and the implementation of this link https://medium.com/#kezhenxu94/how-to-build-your-own-cache-in-kotlin-1b0e86005591.
My questions is:
use a map key-value and save in value a string with all information (created_at, text, name, screen_name)?
how add this information in map with this code?
Please, give me a sample example for storage this data. Or if there is another way to make what i want more correctly, tell me.
My code in the moment:
class ExpirableCache(private val delegate: Cache, private val flushInterval: Long = TimeUnit.MINUTES.toMillis(1000)) : Cache {
private val dataTweet: Map<Long, Long>? = null
private var lastFlushTime = System.nanoTime()
override val size: Int
get() = delegate.size
override fun set(key: Any, value: Any) {
delegate[key] = value
}
override fun remove(key: Any): Any? {
recycle()
return delegate.remove(key)
}
override fun get(key: Any): Any? {
recycle()
return delegate[key]
}
override fun add(key: Any, value: Any) {
dataTweet[0, value]
}
override fun clear() = delegate.clear()
private fun recycle() {
val shouldRecycle = System.nanoTime() - lastFlushTime >= TimeUnit.MILLISECONDS.toNanos(flushInterval)
if (!shouldRecycle) return
delegate.clear()
}
}

Missing callback to view with pagination?

I'm attempting to get pagination up and workning with Google's new library, but seeing some odd behavior. I'm not sure where I am going wrong.
I'm followig MVP and also using some dagger injection for testability.
In the view:
val adapter = ItemsAdapter()
viewModel.getItems(itemCategoryId, keywords).observe(this, Observer {
Log.d(TAG, "Items updated $it")
adapter.setList(it)
})
The data source factory:
class ItemDataSourceFactory(
private val api: Api,
private val retryExecutor: Executor
) : DataSource.Factory<Long, Item> {
private val mutableLiveData = MutableLiveData<ItemDataSource>()
override fun create(): DataSource<Long, Item> {
val source = ItemDataSource(api, retryExecutor)
mutableLiveData.postValue(source)
return source
}
}
The data source:
class ItemDataSource(
private val api: Api,
private val retryExecutor: Executor
): ItemKeyedDataSource<Long, Item>() {
companion object {
private val TAG = ItemKeyDataSource::class.java
}
override fun getKey(item: Item): Long = item.id
override fun loadBefore(params: LoadParams<Long>, callback: LoadCallback<Item>) {
// ignored, since we only ever append to our initial load
}
override fun loadInitial(params: LoadInitialParams<Long>, callback: LoadInitialCallback<Item>) {
api.loadItems(1, params.requestedLoadSize)
.subscribe({
Logger.d(TAG, "Page 1 loaded. Count ${params.requestedLoadSize}.\nItems: ${it.items}")
callback.onResult(it.items as MutableList<Item>, 0, it.item.size)
}, {})
}
override fun loadAfter(params: LoadParams<Long>, callback: LoadCallback<Item>) {
api.loadItems(params.key, params.requestedLoadSize)
.subscribe({
Logger.d(TAG, "Page ${params.key} loaded. Count ${params.requestedLoadSize}.\nItems: ${it.items}")
callback.onResult(it.itemsas MutableList<Item>)
}, {})
}
}
And the view model:
class ItemsViewModel #Inject internal constructor(
private val repository: ItemsMvp.Repository
): ViewModel(), ItemsMvp.Model {
override fun items(categoryId: Long, keywords: String?): LiveData<PagedList<Item>> {
return repository.items(categoryId, keywords)
}
}
And the repository layer:
class ItemsRepository #Inject internal constructor(
private val api: Api,
) : ItemsMvp.Repository {
companion object {
const val DEFAULT_THREAD_POOL_SIZE = 5
const val DEFAULT_PAGE_SIZE = 20
}
private val networkExecutor = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE)
private val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(DEFAULT_PAGE_SIZE)
.setPageSize(DEFAULT_PAGE_SIZE)
.build()
override fun items(categoryId: Long, keywords: String?): LiveData<PagedList<Item>> {
val sourceFactory = ItemDataSourceFactory(api, networkExecutor)
// provide custom executor for network requests, otherwise it will default to
// Arch Components' IO pool which is also used for disk access
return LivePagedListBuilder(sourceFactory, pagedListConfig)
.setBackgroundThreadExecutor(networkExecutor)
.build()
}
}
The issue is I'm not getting an update to the view after the first page is loaded.
I see this log from the onCreate():
Items updated []
but then after when the data source returns the items, I see these logs:
Page 1 loaded. Count 20.
Items: [Item(....)]
BUT I never see the view that's subscribing to the view model get an update to set the list on the adapter. If you are curiousI'm using a PagedListAdapter.
I had two mistakes...
The adapter was never set in the view (fail)
I was extending ItemKeyedDataSource instead of PageKeyedDataSource (double fail)
I made those two adjustments and now everything is behaving as expected.

Attach artwork to mp3 Uri for ExoPlayer

I'm using the more-or-less default ExoPlayer 2.1 SimpleExoPlayer to stream from either a url Uri or a local file Uri. The Uri is loaded into the MediaSource,
MediaSource mediaSource = new ExtractorMediaSource(url,
dataSourceFactory, extractorsFactory, null, null);
The source is sometimes a Video, mp4 instead of mp3, so in the Activity I set it to the com.google.android.exoplayer2.ui.SimpleExoPlayerView
mVideoView = (SimpleExoPlayerView) findViewById(R.id.media_player);
mVideoView.setPlayer(player);
mVideoView.requestFocus();
I've read in an article:
SimpleExoPlayerView has also been updated to look at ID3 metadata, and will automatically display embedded ID3 album art during audio playbacks. If not desired, this functionality can be disabled using SimpleExoPlayerView’s setUseArtwork method.
I've seen it answered for Files, How to get and set (change) ID3 tag (metadata) of audio files?,
But I'm hoping to set the ID3 metadata for a Uri derived from a url String. Is it possible? Otherwise, is it possible to set the artwork for an ExoPlayerView without editing the ID3 metafiles? Or possible to change the ID3 meta for a File without a dependency?
Edit
So I've found an Issue which says this is solved, and the issuer linked to an override of the exo_simple_player_view here
I've found in the blog post
When a SimpleExoPlayerView is instantiated it inflates its layout from the layout file exo_simple_player_view.xml. PlaybackControlView inflates its layout from exo_playback_control_view.xml. To customize these layouts, an application can define layout files with the same names in its own res/layout* directories. These layout files override the ones provided by the ExoPlayer library.
So I have to override the simpleview somehow.
You can use this function and pass mediaUri and thunbnailUri to it
private fun PlayerView.loadArtWorkIfMp3(mediaUri: Uri, thumbnailUri: Uri) {
try {
val imageView = this.findViewById<ImageView>(R.id.exo_artwork)
if (mediaUri.lastPathSegment!!.contains("mp3")) {
this.useArtwork = true
imageView.scaleType = ImageView.ScaleType.CENTER_INSIDE
imageView.loadImage(thumbnailUri) {
this.defaultArtwork = it
}
}
} catch (e: Exception) {
Log.d("artwork", "exo_artwork not found")
}
}
and You will use loadImage function
#BindingAdapter(value = ["imageUri", "successCallback"], requireAll = false)
fun ImageView.loadImage(imgUrl: Uri?, onLoadSuccess: (resource: Drawable) -> Unit = {}) {
val requestOption = RequestOptions()
.placeholder(R.drawable.ic_music)
.error(R.drawable.ic_music)
Glide.with(this.context)
.load(imgUrl)
.transition(DrawableTransitionOptions.withCrossFade())
.apply(requestOption)
.centerInside()
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.listener(GlideImageRequestListener(object : GlideImageRequestListener.Callback {
override fun onFailure(message: String?) {
Log.d("loadImage", "onFailure:-> $message")
}
override fun onSuccess(dataSource: String, resource: Drawable) {
Log.d("loadImage", "onSuccess:-> load from $dataSource")
onLoadSuccess(resource)
}
}))
.into(this)
}
And last one
class GlideImageRequestListener(private val callback: Callback? = null) : RequestListener<Drawable> {
interface Callback {
fun onFailure(message: String?)
fun onSuccess(dataSource: String, resource: Drawable)
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
callback?.onFailure(e?.message)
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
resource?.let {
target?.onResourceReady(
it,
DrawableCrossFadeTransition(1000, isFirstResource)
)
}
callback?.onSuccess(dataSource.toString(),resource!!)
return true
}
}

Categories

Resources