Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate from Picasso to Coil #4911

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]
Loading