Skip to content

Commit

Permalink
Migrate from Picasso to Coil (#4911)
Browse files Browse the repository at this point in the history
* Migrate from Picasso to Coil

* Update to avoid placeholder blinking
  • Loading branch information
jpelgrom authored Dec 18, 2024
1 parent 764f043 commit 2f74937
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 46 deletions.
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ dependencies {

implementation(libs.jackson.module.kotlin)
implementation(libs.okhttp)
implementation(libs.picasso)

implementation(libs.bundles.coil)

"fullImplementation"(libs.play.services.location)
"fullImplementation"(libs.play.services.home)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import android.os.Build
import android.os.PowerManager
import android.telephony.TelephonyManager
import androidx.core.content.ContextCompat
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import dagger.hilt.android.HiltAndroidApp
import io.homeassistant.companion.android.common.data.keychain.KeyChainRepository
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
Expand All @@ -35,9 +39,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient

@HiltAndroidApp
open class HomeAssistantApplication : Application() {
open class HomeAssistantApplication : Application(), SingletonImageLoader.Factory {

private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO + Job())

Expand All @@ -48,6 +53,9 @@ open class HomeAssistantApplication : Application() {
@Named("keyChainRepository")
lateinit var keyChainRepository: KeyChainRepository

@Inject
lateinit var okHttpClient: OkHttpClient

@Inject
lateinit var languagesManager: LanguagesManager

Expand Down Expand Up @@ -302,4 +310,15 @@ open class HomeAssistantApplication : Application() {
ContextCompat.registerReceiver(this, templateWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
}
}

override fun newImageLoader(context: PlatformContext): ImageLoader =
ImageLoader.Builder(context)
.components {
add(
OkHttpNetworkFetcherFactory(
callFactory = okHttpClient
)
)
}
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,27 @@ import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.core.os.BundleCompat
import com.squareup.picasso.Picasso
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.size.Dimension
import coil3.size.Precision
import coil3.size.Size
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.database.widget.CameraWidgetDao
import io.homeassistant.companion.android.database.widget.CameraWidgetEntity
import io.homeassistant.companion.android.database.widget.WidgetTapAction
import io.homeassistant.companion.android.util.hasActiveConnection
import io.homeassistant.companion.android.webview.WebViewActivity
import io.homeassistant.companion.android.widgets.common.RemoteViewsTarget
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient

@AndroidEntryPoint
class CameraWidget : AppWidgetProvider() {
Expand All @@ -52,6 +58,9 @@ class CameraWidget : AppWidgetProvider() {
@Inject
lateinit var cameraWidgetDao: CameraWidgetDao

@Inject
lateinit var okHttpClient: OkHttpClient

private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())

override fun onUpdate(
Expand Down Expand Up @@ -80,7 +89,7 @@ class CameraWidget : AppWidgetProvider() {
}
mainScope.launch {
val views = getWidgetRemoteViews(context, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, views)
views?.let { appWidgetManager.updateAppWidget(appWidgetId, it) }
}
}

Expand Down Expand Up @@ -108,27 +117,30 @@ class CameraWidget : AppWidgetProvider() {
}
}

private suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int): RemoteViews {
private suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int): RemoteViews? {
val updateCameraIntent = Intent(context, CameraWidget::class.java).apply {
action = UPDATE_IMAGE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
}

return RemoteViews(context.packageName, R.layout.widget_camera).apply {
val widget = cameraWidgetDao.get(appWidgetId)
if (widget != null) {
var entityPictureUrl: String?
try {
entityPictureUrl = retrieveCameraImageUrl(widget.serverId, widget.entityId)
setViewVisibility(R.id.widgetCameraError, View.GONE)
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch entity or entity does not exist", e)
setViewVisibility(R.id.widgetCameraError, View.VISIBLE)
entityPictureUrl = null
}
val widget = cameraWidgetDao.get(appWidgetId)
var widgetCameraError = false
var url: String? = null
if (widget != null) {
try {
val entityPictureUrl = retrieveCameraImageUrl(widget.serverId, widget.entityId)
val baseUrl = serverManager.getServer(widget.serverId)?.connection?.getUrl().toString().removeSuffix("/")
val url = "$baseUrl$entityPictureUrl"
if (entityPictureUrl == null) {
url = "$baseUrl$entityPictureUrl"
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch entity or entity does not exist", e)
widgetCameraError = true
}
}

val views = RemoteViews(context.packageName, R.layout.widget_camera).apply {
if (widget != null) {
setViewVisibility(R.id.widgetCameraError, if (widgetCameraError) View.VISIBLE else View.GONE)
if (url == null) {
setImageViewResource(
R.id.widgetCameraImage,
R.drawable.app_icon_round
Expand All @@ -152,21 +164,20 @@ class CameraWidget : AppWidgetProvider() {
)
Log.d(TAG, "Fetching camera image")
Handler(Looper.getMainLooper()).post {
val picasso = Picasso.get()
if (BuildConfig.DEBUG) {
picasso.isLoggingEnabled = true
}
try {
picasso.invalidate(url)
picasso.load(url).resize(getScreenWidth(), 0).onlyScaleDown().into(
this,
R.id.widgetCameraImage,
intArrayOf(appWidgetId)
)
val request = ImageRequest.Builder(context)
.data(url)
.target(RemoteViewsTarget(context, appWidgetId, this, R.id.widgetCameraImage))
.diskCachePolicy(CachePolicy.DISABLED)
.memoryCachePolicy(CachePolicy.DISABLED)
.networkCachePolicy(CachePolicy.READ_ONLY)
.size(Size(getScreenWidth(), Dimension.Undefined))
.precision(Precision.INEXACT)
.build()
context.imageLoader.enqueue(request)
} catch (e: Exception) {
Log.e(TAG, "Unable to fetch image", e)
}
Log.d(TAG, "Fetch and load complete")
}
}

Expand All @@ -189,6 +200,8 @@ class CameraWidget : AppWidgetProvider() {
setOnClickPendingIntent(R.id.widgetCameraPlaceholder, tapWidgetPendingIntent)
}
}
// If there is an url, Coil will call appWidgetManager.updateAppWidget
return if (url == null) views else null
}

private suspend fun retrieveCameraImageUrl(serverId: Int, entityId: String): String? {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.homeassistant.companion.android.widgets.common

import android.appwidget.AppWidgetManager
import android.content.Context
import android.widget.RemoteViews
import androidx.annotation.IdRes
import coil3.Image
import coil3.target.Target
import coil3.toBitmap

/**
* Load images into RemoteViews with Coil
* (based on https://coil-kt.github.io/coil/recipes/#remote-views)
*/
class RemoteViewsTarget(
private val context: Context,
private val appWidgetId: Int,
private val remoteViews: RemoteViews,
@IdRes private val imageViewResId: Int
) : Target {

override fun onStart(placeholder: Image?) {
// Skip if null to avoid blinking (there is no placeholder)
placeholder?.let { setDrawable(it) }
}

override fun onError(error: Image?) = setDrawable(error)

override fun onSuccess(result: Image) = setDrawable(result)

private fun setDrawable(image: Image?) {
remoteViews.setImageViewBitmap(imageViewResId, image?.toBitmap())
AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, remoteViews)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import android.view.View
import android.widget.RemoteViews
import android.widget.Toast
import androidx.core.os.BundleCompat
import coil3.imageLoader
import coil3.request.ImageRequest
import com.google.android.material.color.DynamicColors
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.squareup.picasso.Picasso
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.R as commonR
import io.homeassistant.companion.android.common.data.integration.Entity
Expand All @@ -28,6 +28,7 @@ import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWid
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
import io.homeassistant.companion.android.util.hasActiveConnection
import io.homeassistant.companion.android.widgets.BaseWidgetProvider
import io.homeassistant.companion.android.widgets.common.RemoteViewsTarget
import java.util.LinkedList
import javax.inject.Inject
import kotlin.collections.HashMap
Expand Down Expand Up @@ -273,20 +274,16 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
)
Log.d(TAG, "Fetching media preview image")
Handler(Looper.getMainLooper()).post {
if (BuildConfig.DEBUG) {
Picasso.get().isLoggingEnabled = true
Picasso.get().setIndicatorsEnabled(true)
}
try {
Picasso.get().load(url).resize(1024, 1024).into(
this,
R.id.widgetMediaImage,
intArrayOf(appWidgetId)
)
val request = ImageRequest.Builder(context)
.data(url)
.target(RemoteViewsTarget(context, appWidgetId, this, R.id.widgetMediaImage))
.size(1024)
.build()
context.imageLoader.enqueue(request)
} catch (e: Exception) {
Log.e(TAG, "Unable to load image", e)
}
Log.d(TAG, "Fetch and load complete")
}
}

Expand Down
3 changes: 2 additions & 1 deletion automotive/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ dependencies {

implementation(libs.jackson.module.kotlin)
implementation(libs.okhttp)
implementation(libs.picasso)

implementation(libs.bundles.coil)

"fullImplementation"(libs.play.services.location)
"fullImplementation"(libs.play.services.home)
Expand Down
7 changes: 5 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ media3 = "1.5.0"
navigation-compose = "2.8.5"
okhttp = "5.0.0-alpha.14"
paging = "3.3.5"
picasso = "2.8"
coil = "3.0.4"
play-services-threadnetwork = "16.2.1"
play-services-home = "16.0.0"
play-services-location = "21.3.0"
Expand Down Expand Up @@ -104,6 +104,9 @@ car-core = { module = "androidx.car.app:app", version.ref = "car-versions" }
car-automotive = { module = "androidx.car.app:app-automotive", version.ref = "car-versions" }
car-projected = { module = "androidx.car.app:app-projected", version.ref = "car-versions" }
changeLog = { module = "com.github.AppDevNext:ChangeLog", version.ref = "changeLog" }
coil-oktthp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
coil-views = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
community-material-typeface = { module = "com.mikepenz:community-material-typeface", version.ref = "community-material-typeface" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
compose-animation = { module = "androidx.compose.animation:animation" }
Expand Down Expand Up @@ -148,7 +151,6 @@ paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pag
play-services-threadnetwork = { module = "com.google.android.gms:play-services-threadnetwork", version.ref = "play-services-threadnetwork" }
play-services-home = { module = "com.google.android.gms:play-services-home", version.ref = "play-services-home" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "play-services-location" }
picasso = { module = "com.squareup.picasso:picasso", version.ref = "picasso" }
play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "play-services-wearable" }
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preference-ktx" }
recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
Expand All @@ -173,6 +175,7 @@ webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
zxing = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" }

[bundles]
coil = ["coil-views", "coil-oktthp", "coil-svg"]
media3 = ["media3-exoplayer", "media3-exoplayer-hls", "media3-ui"]
paging = ["paging-runtime", "paging-compose"]
wear-tiles = ["wear-tiles", "wear-protolayout-main", "wear-protolayout-expression", "wear-protolayout-material"]

0 comments on commit 2f74937

Please sign in to comment.