feat(widget): add adaptive card & circular desktop widgets - #1006
SteveZMTstudios wants to merge 8 commits into
Conversation
f5c9b4d to
e7d7146
Compare
nift4
left a comment
There was a problem hiding this comment.
Note I didn't have time to go through all of the code yet, this is only partial comments
| ) { | ||
| val uri = state.artworkUri ?: return | ||
| CoroutineScope(Dispatchers.IO).launch { | ||
| val request = ImageRequest.Builder(context) |
There was a problem hiding this comment.
I'm wondering if there's a more optimal way to do it, did you test letting image view load the cover itself? (I guess a placeholder can be achieved by stacking two imageview in a framelayout, but IDK if that is a great idea)
There was a problem hiding this comment.
My initial attempt involved handling album art loading directly with RemoteViews/ImageViews, but I encountered noticeable UI flickering issues: Whenever controls were updated (e.g., scrolling lyrics from a lyrics control, progress updates, or play/pause switching), reapplying RemoteViews caused the launcher's ImageView to reload/redraw the URI from scratch, resulting in very noticeable and distracting flickering of the album art.
Stacking two ImageViews within a FrameLayout was completely unnecessary, not only adding unnecessary layout overhead but also failing to prevent the top album art view from flickering during subsequent RemoteViews updates, as they refreshed together.
By loading the album art once in the background using Coil and caching it in memory (cachedArtworkBitmap), subsequent control updates (progress bar jumps, playback status changes) could immediately reuse the existing Bitmap, completely eliminating flickering and making updates visually smoother.
|
There's still unused methods fyi, and a conflict because your other PR was merged |
55c398c to
2637746
Compare
|
I'm confused, do you perhaps have different code locally? because in this branch there's still unused functions like createErrorNotificationChannel or initCustomCommands |
|
I apologize, I (and my computer) have been handling too many things simultaneously lately, and I made a mistake while sorting the code. I have fixed it and will carefully review the subsequent steps.
And yes, I always bring up the code collection in the feature/stevezmt branch change, which is #1003, to test their coupling. |
a3bfa04 to
9ec3c2f
Compare
|
Sorry for the delay, currently occupied with something else, will review soon. |
|
Thanks for your patience, and sorry for the delay. I tried the widgets on my phone now. My thoughts from testing: Overall, I really like the concept of the different widgets. But, I think each type of widget at least needs one provider (so one does pill/circle and another does the card ones). Otherwise I think users would simply not notice that there is different types. Also, I was wondering about the favorite and shuffle button. I think I would prefer there to either be both shuffle and repeat, or none of them. So maybe, in card layout, there could be repeat in place of current favorite, and favorite is moved to be at end of the name text view instead? Also, I noticed a minor issue where the favorite button is a different color (more grey) than the other buttons. Lastly, after closing the app, the widget became empty - it should preferably show the last played song instead, that will be played after pressing the play button (the latter part with restarting playback is already working, widget just doesn't show it). |
| when (action) { | ||
| ACTION_PREVIOUS -> { | ||
| if (player != null) player.seekToPrevious() | ||
| else sendMediaButtonFallback(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS) |
There was a problem hiding this comment.
I think this fallback may not work for previous and next button, because it uses startForegroundService() but only the play keycode will actually start the service, which means there's a crash
09-15 13:05:21.428 21710 21710 E AndroidRuntime: FATAL EXCEPTION: main
09-15 13:05:21.428 21710 21710 E AndroidRuntime: Process: org.akanework.gramophone.debug, PID: 21710
09-15 13:05:21.428 21710 21710 E AndroidRuntime: android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{8ff05c5 u0 org.akanework.gramophone.debug/org.akanework.gramophone.logic.GramophonePlaybackService c:org.akanework.gramophone.debug}
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ActivityThread.generateForegroundServiceDidNotStartInTimeException(ActivityThread.java:2349)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ActivityThread.throwRemoteServiceException(ActivityThread.java:2317)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ActivityThread.-$$Nest$mthrowRemoteServiceException(Unknown Source:0)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2691)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:109)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:232)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.os.Looper.loop(Looper.java:317)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8934)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:591)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: Caused by: android.app.StackTrace: Last startServiceCommon() call for this service was made here
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ContextImpl.startServiceCommon(ContextImpl.java:2056)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ContextImpl.startForegroundService(ContextImpl.java:2010)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.content.ContextWrapper.startForegroundService(ContextWrapper.java:860)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.content.ContextWrapper.startForegroundService(ContextWrapper.java:860)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at org.akanework.gramophone.ui.CardWidgetProvider.sendMediaButtonFallback(CardWidgetProvider.kt:205)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at org.akanework.gramophone.ui.CardWidgetProvider.handleWidgetAction(CardWidgetProvider.kt:73)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at org.akanework.gramophone.ui.CardWidgetProvider.onReceive(CardWidgetProvider.kt:53)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ActivityThread.handleReceiver(ActivityThread.java:4930)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ActivityThread.-$$Nest$mhandleReceiver(Unknown Source:0)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2527)
09-15 13:05:21.428 21710 21710 E AndroidRuntime: ... 7 more
| } | ||
|
|
||
| private fun sendMediaButtonFallback(context: Context, keyCode: Int) { | ||
| val serviceIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { |
There was a problem hiding this comment.
Media3 has a helper for this now androidx/media@1c58fa0
Summary
Card widget — a new responsive home-screen widget family for quick playback control. One provider delivers pill (1×1), circle (2×2 w/ progress ring), card (4×1), medium (4×2) and large (4×4 / wide 5×4) layouts, plus favorite & shuffle actions on wide slots.
Widget favorite action connects through a dedicated playback-service helper so the current item's heart rating and widget state stay in sync across lyric/session updates.
Widget resources use Material You system colors on Android 12+ with stable theme-token fallbacks for pre-12 and night mode; default + zh-rCN strings added for all new settings and the widget picker label.
Screenshots, CI tests and more, see #1003