Skip to content

Commit ecb3917

Browse files
committed
refactor: enhance widget update handling and improve error resilience
1 parent adb74b6 commit ecb3917

File tree

7 files changed

+131
-94
lines changed

7 files changed

+131
-94
lines changed

app/src/main/java/com/kelsos/mbrc/common/state/AppStateManager.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class AppStateManager(
2727
init {
2828
launch {
2929
val track = trackCache.restoreInfo()
30+
Timber.v("Restoring playing last played track: $track")
3031
appState.updatePlayingTrack(track)
3132
}
3233
}

app/src/main/java/com/kelsos/mbrc/common/state/PlayingTrackCache.kt

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ interface PlayingTrackCache {
2828
suspend fun persistInfo(playingTrack: PlayingTrack)
2929

3030
suspend fun restoreInfo(): PlayingTrack
31-
32-
suspend fun persistCover(cover: String)
33-
34-
suspend fun restoreCover(): String
3531
}
3632

3733
class PlayingTrackCacheImpl(
@@ -67,6 +63,11 @@ class PlayingTrackCacheImpl(
6763
.toBuilder()
6864
.setTrack(track)
6965
.build()
66+
67+
store
68+
.toBuilder()
69+
.setCover(playingTrack.coverUrl)
70+
.build()
7071
}
7172
return@withContext
7273
}
@@ -75,23 +76,17 @@ class PlayingTrackCacheImpl(
7576
override suspend fun restoreInfo(): PlayingTrack =
7677
withContext(dispatchers.io) {
7778
val track = storeFlow.first().track
79+
val cover = storeFlow.first().cover
7880

7981
return@withContext PlayingTrack(
80-
track.artist,
81-
track.title,
82-
track.album,
83-
track.year,
84-
track.path,
82+
artist = track.artist,
83+
title = track.title,
84+
album = track.album,
85+
year = track.year,
86+
path = track.path,
87+
coverUrl = cover,
8588
)
8689
}
87-
88-
override suspend fun persistCover(cover: String) {
89-
context.cacheDataStore.updateData { store ->
90-
store.toBuilder().setCover(cover).build()
91-
}
92-
}
93-
94-
override suspend fun restoreCover(): String = storeFlow.first().cover
9590
}
9691

9792
object PlayerStateSerializer : Serializer<Store> {

app/src/main/java/com/kelsos/mbrc/features/widgets/BundleData.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ class BundleData(
1212

1313
fun isInfo() = bundle.getBoolean(WidgetUpdater.INFO, false)
1414

15-
fun isCover() = bundle.getBoolean(WidgetUpdater.COVER, false)
16-
17-
fun cover(): String = bundle.getString(WidgetUpdater.COVER_PATH, "")
18-
1915
fun state(): String = bundle.getString(WidgetUpdater.PLAYER_STATE, PlayerState.UNDEFINED)
2016

2117
fun playingTrack(): PlayingTrack =
@@ -29,7 +25,6 @@ class BundleData(
2925
when {
3026
this.isState() -> "State: ${this.state()}"
3127
this.isInfo() -> "Info: ${this.playingTrack()}"
32-
this.isCover() -> "Cover: ${this.cover()}"
3328
else -> "Unknown"
3429
}
3530
}

app/src/main/java/com/kelsos/mbrc/features/widgets/RemoteViewsTarget.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ class RemoteViewsTarget(
1414
private val widget: RemoteViews,
1515
private val widgetIds: IntArray,
1616
@IdRes private val imageViewResId: Int,
17+
private val onImageUpdated: (() -> Unit)? = null,
1718
) : Target {
1819
override fun onStart(placeholder: Image?) {
19-
setDrawable(placeholder, "start")
20+
Timber.v("Image loading started for widgets ${widgetIds.joinToString(", ")}")
2021
}
2122

2223
override fun onError(error: Image?) {
@@ -35,9 +36,11 @@ class RemoteViewsTarget(
3536
Timber.v("No image found for widget setting placeholder, reason: $reason")
3637
widget.setImageViewResource(imageViewResId, R.drawable.ic_image_no_cover)
3738
} else {
38-
Timber.v("Updating image for widget, reason: $reason")
39+
Timber.v("Updating image for widgets ${widgetIds.joinToString(", ")}, reason: $reason")
3940
widget.setImageViewBitmap(imageViewResId, image.toBitmap())
4041
}
42+
43+
onImageUpdated?.invoke()
4144
manager.updateAppWidget(widgetIds, widget)
4245
}
4346
}

app/src/main/java/com/kelsos/mbrc/features/widgets/WidgetBase.kt

Lines changed: 103 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.content.Intent
99
import android.os.Bundle
1010
import android.widget.RemoteViews
1111
import coil3.imageLoader
12+
import coil3.request.CachePolicy
1213
import coil3.request.ImageRequest
1314
import coil3.request.error
1415
import coil3.size.Precision
@@ -29,45 +30,63 @@ abstract class WidgetBase : AppWidgetProvider() {
2930
intent: Intent?,
3031
) {
3132
super.onReceive(context, intent)
32-
if (intent?.action == AppWidgetManager.ACTION_APPWIDGET_UPDATE) {
33-
whenNotNull(context, intent.extras, this::updateWidget)
33+
when (intent?.action) {
34+
AppWidgetManager.ACTION_APPWIDGET_UPDATE -> {
35+
whenNotNull(context, intent.extras, this::updateWidget)
36+
}
37+
else -> {
38+
val extras = intent?.extras
39+
if (context == null || extras == null) {
40+
return
41+
}
42+
43+
val data = BundleData(extras)
44+
if (data.isInfo() || data.isState()) {
45+
updateWidget(context, extras)
46+
}
47+
}
3448
}
3549
}
3650

3751
private fun updateWidget(
3852
context: Context,
3953
extras: Bundle,
4054
) {
41-
val widgetManager = AppWidgetManager.getInstance(context)
42-
val widgets = ComponentName(context.packageName, config.widgetClass.java.name)
43-
val widgetsIds = widgetManager.getAppWidgetIds(widgets)
44-
val data = BundleData(extras)
45-
46-
if (widgetsIds.isEmpty()) {
47-
Timber.v("No $type widgets found for update")
48-
return
49-
}
50-
51-
Timber.v("Updating $type widgets ${widgetsIds.joinToString(", ")} with extras: $data")
55+
try {
56+
val widgetManager = AppWidgetManager.getInstance(context)
57+
val widgets = ComponentName(context.packageName, config.widgetClass.java.name)
58+
val widgetsIds = widgetManager.getAppWidgetIds(widgets)
59+
val data = BundleData(extras)
60+
61+
if (widgetsIds.isEmpty()) {
62+
Timber.v("No $type widgets found for update")
63+
return
64+
}
5265

53-
when {
54-
data.isCover() -> {
55-
updateCover(context, widgetManager, widgetsIds, data.cover())
66+
Timber.v("Updating $type widgets ${widgetsIds.joinToString(", ")} with extras: $data")
67+
68+
when {
69+
data.isInfo() -> {
70+
val playingTrack = data.playingTrack()
71+
updateInfoWithCover(
72+
context,
73+
widgetManager,
74+
widgetsIds,
75+
playingTrack,
76+
)
77+
}
78+
data.isState() ->
79+
updatePlayState(
80+
context,
81+
widgetManager,
82+
widgetsIds,
83+
data.state(),
84+
)
5685
}
57-
data.isInfo() ->
58-
updateInfo(
59-
context,
60-
widgetManager,
61-
widgetsIds,
62-
data.playingTrack(),
63-
)
64-
data.isState() ->
65-
updatePlayState(
66-
context,
67-
widgetManager,
68-
widgetsIds,
69-
data.state(),
70-
)
86+
} catch (e: SecurityException) {
87+
Timber.e(e, "Security error updating $type widget - check permissions")
88+
} catch (e: IllegalArgumentException) {
89+
Timber.e(e, "Invalid arguments for $type widget update")
7190
}
7291
}
7392

@@ -109,36 +128,66 @@ abstract class WidgetBase : AppWidgetProvider() {
109128
info: PlayingTrack,
110129
)
111130

112-
private fun updateInfo(
131+
private fun updateInfoWithCover(
113132
context: Context,
114133
widgetManager: AppWidgetManager,
115134
widgetsIds: IntArray,
116135
info: PlayingTrack,
117136
) {
118-
val views = RemoteViews(context.packageName, config.layout)
119-
setupTrackInfo(views, info)
120-
widgetManager.updateAppWidget(widgetsIds, views)
137+
Timber.d("updateInfoWithCover called for $type widget, coverUrl: '${info.coverUrl}'")
138+
139+
val widget = RemoteViews(context.packageName, config.layout)
140+
setupTrackInfo(widget, info)
141+
preserveActions(context, widget)
142+
143+
if (info.coverUrl.isNotBlank()) {
144+
// Load image asynchronously, but the widget is already set up with track info
145+
val target =
146+
RemoteViewsTarget(
147+
widgetManager,
148+
widget,
149+
widgetsIds,
150+
config.imageId,
151+
onImageUpdated = {
152+
// Callback to preserve actions after the image loads
153+
preserveActions(context, widget)
154+
},
155+
)
156+
157+
val request =
158+
ImageRequest
159+
.Builder(context)
160+
.data(info.coverUrl)
161+
.size(R.dimen.widget_small_height)
162+
.scale(Scale.FILL)
163+
.error(R.drawable.ic_image_no_cover)
164+
.precision(Precision.INEXACT)
165+
.memoryCachePolicy(CachePolicy.DISABLED)
166+
.diskCachePolicy(CachePolicy.ENABLED)
167+
.target(target)
168+
.build()
169+
170+
context.imageLoader.enqueue(request)
171+
} else {
172+
// No cover URL, use placeholder and update immediately
173+
widget.setImageViewResource(config.imageId, R.drawable.ic_image_no_cover)
174+
widgetManager.updateAppWidget(widgetsIds, widget)
175+
}
121176
}
122177

123-
private fun updateCover(
178+
private fun preserveActions(
124179
context: Context,
125-
widgetManager: AppWidgetManager,
126-
widgetsIds: IntArray,
127-
path: String,
180+
views: RemoteViews,
128181
) {
129-
val widget = RemoteViews(context.packageName, config.layout)
130-
val request =
131-
ImageRequest
132-
.Builder(context)
133-
.data(path)
134-
.size(R.dimen.widget_small_height)
135-
.scale(Scale.FILL)
136-
.error(R.drawable.ic_image_no_cover)
137-
.precision(Precision.INEXACT)
138-
.target(RemoteViewsTarget(widgetManager, widget, widgetsIds, config.imageId))
139-
.build()
140-
141-
context.imageLoader.enqueue(request)
182+
val intent = Intent(context, PlayerActivity::class.java)
183+
val pendingIntent =
184+
PendingIntent.getActivity(
185+
context,
186+
0,
187+
intent,
188+
PendingIntent.FLAG_IMMUTABLE,
189+
)
190+
setupActionIntents(views, pendingIntent, context)
142191
}
143192

144193
private fun updatePlayState(
@@ -157,6 +206,9 @@ abstract class WidgetBase : AppWidgetProvider() {
157206
R.drawable.ic_baseline_play_arrow_24
158207
},
159208
)
209+
210+
preserveActions(context, widget)
211+
160212
manager.updateAppWidget(widgetsIds, widget)
161213
}
162214
}

app/src/main/java/com/kelsos/mbrc/features/widgets/WidgetUpdater.kt

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@ import android.content.Context
55
import android.content.Intent
66
import com.kelsos.mbrc.common.state.PlayerState
77
import com.kelsos.mbrc.common.state.PlayingTrack
8+
import timber.log.Timber
89

910
interface WidgetUpdater {
1011
fun updatePlayingTrack(track: PlayingTrack)
1112

1213
fun updatePlayState(state: PlayerState)
1314

14-
fun updateCover(path: String = "")
15-
1615
companion object {
17-
const val COVER = "com.kelsos.mbrc.features.widgets.COVER"
18-
const val COVER_PATH = "com.kelsos.mbrc.features.widgets.COVER_PATH"
1916
const val STATE = "com.kelsos.mbrc.features.widgets.STATE"
2017
const val INFO = "com.kelsos.mbrc.features.widgets.INFO"
2118
const val TRACK_INFO = "com.kelsos.mbrc.features.widgets.TRACKINFO"
@@ -36,10 +33,6 @@ class WidgetUpdaterImpl(
3633
putExtra(WidgetUpdater.INFO, true)
3734
.putExtra(WidgetUpdater.TRACK_INFO, track)
3835

39-
private fun Intent.payload(path: String): Intent =
40-
putExtra(WidgetUpdater.COVER, true)
41-
.putExtra(WidgetUpdater.COVER_PATH, path)
42-
4336
private fun Intent.statePayload(state: PlayerState): Intent =
4437
putExtra(WidgetUpdater.STATE, true)
4538
.putExtra(WidgetUpdater.PLAYER_STATE, state.state)
@@ -56,19 +49,19 @@ class WidgetUpdaterImpl(
5649
broadcast(smallIntent, normalIntent)
5750
}
5851

59-
override fun updateCover(path: String) {
60-
val normalIntent = createIntent(WidgetNormal::class.java).payload(path)
61-
val smallIntent = createIntent(WidgetSmall::class.java).payload(path)
62-
broadcast(smallIntent, normalIntent)
63-
}
64-
6552
private fun broadcast(
6653
smallIntent: Intent,
6754
normalIntent: Intent,
6855
) {
69-
with(context) {
70-
sendBroadcast(smallIntent)
71-
sendBroadcast(normalIntent)
56+
try {
57+
with(context) {
58+
sendBroadcast(smallIntent)
59+
sendBroadcast(normalIntent)
60+
}
61+
Timber.v("Widget broadcasts sent successfully")
62+
} catch (e: SecurityException) {
63+
Timber.e(e, "Failed to send widget broadcasts")
64+
throw e
7265
}
7366
}
7467
}

app/src/main/java/com/kelsos/mbrc/networking/protocol/ProtocolActions.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ class UpdateVolume(
213213

214214
class UpdateCover(
215215
private val app: Application,
216-
private val updater: WidgetUpdater,
217216
private val moshi: Moshi,
218217
private val api: ApiBase,
219218
private val dispatchers: AppCoroutineDispatchers,
@@ -259,7 +258,6 @@ class UpdateCover(
259258
) {
260259
val newState = previousState.copy(coverUrl = coverUri)
261260
appState.updatePlayingTrack(newState)
262-
updater.updateCover(coverUri)
263261
}
264262

265263
private fun getBitmap(base64: String): Bitmap {

0 commit comments

Comments
 (0)