react-native-audio-browser 0.2.0-next.3 → 0.2.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +11 -11
  2. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  3. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +9 -5
  4. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  5. package/android/src/main/java/com/audiobrowser/player/Player.kt +19 -8
  6. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +100 -56
  7. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  8. package/lib/commonjs/features/player/options.js +45 -15
  9. package/lib/commonjs/features/player/options.js.map +1 -1
  10. package/lib/commonjs/features/player/setup.js +13 -6
  11. package/lib/commonjs/features/player/setup.js.map +1 -1
  12. package/lib/module/features/player/options.js +45 -15
  13. package/lib/module/features/player/options.js.map +1 -1
  14. package/lib/module/features/player/setup.js +13 -6
  15. package/lib/module/features/player/setup.js.map +1 -1
  16. package/lib/typescript/src/features/player/options.d.ts +84 -35
  17. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  18. package/lib/typescript/src/features/player/setup.d.ts +10 -3
  19. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  20. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -1
  21. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
  22. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +5 -5
  23. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +9 -9
  24. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +5 -5
  25. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +5 -5
  26. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
  27. package/nitrogen/generated/android/c++/JOptions.hpp +5 -5
  28. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  29. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  30. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  31. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
  37. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +36 -51
  38. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +6 -6
  39. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +6 -6
  40. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  41. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  42. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  43. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  44. package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
  45. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  46. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  47. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  48. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  49. package/package.json +1 -1
  50. package/src/features/player/options.ts +95 -36
  51. package/src/features/player/setup.test.ts +12 -4
  52. package/src/features/player/setup.ts +13 -6
  53. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  54. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  55. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  57. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  58. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -3,11 +3,11 @@ package com.audiobrowser.model
3
3
  import com.margelo.nitro.audiobrowser.AndroidOptions
4
4
  import com.margelo.nitro.audiobrowser.AppKilledPlaybackBehavior
5
5
  import com.margelo.nitro.audiobrowser.NativeUpdateOptions
6
- import com.margelo.nitro.audiobrowser.NotificationButtonLayout
7
6
  import com.margelo.nitro.audiobrowser.Options
8
7
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
8
+ import com.margelo.nitro.audiobrowser.RemoteButtonLayout
9
9
  import com.margelo.nitro.audiobrowser.Variant_NullType_Double
10
- import com.margelo.nitro.audiobrowser.Variant_NullType_NotificationButtonLayout
10
+ import com.margelo.nitro.audiobrowser.Variant_NullType_RemoteButtonLayout
11
11
 
12
12
  /**
13
13
  * Update options for the AudioBrowser that can be changed at runtime. These options control player
@@ -37,8 +37,8 @@ data class PlayerUpdateOptions(
37
37
  playbackRate = null,
38
38
  ),
39
39
 
40
- // Notification button layout (null = derive from capabilities)
41
- var notificationButtons: NotificationButtonLayout? = null,
40
+ // Ordered button layout (null = derive from capabilities)
41
+ var remoteButtonLayout: RemoteButtonLayout? = null,
42
42
 
43
43
  // Android-specific runtime options (all under android.* in JS)
44
44
  var appKilledPlaybackBehavior: AppKilledPlaybackBehavior =
@@ -66,12 +66,12 @@ data class PlayerUpdateOptions(
66
66
  // Update boolean options
67
67
  androidOptions.skipSilence?.let { skipSilence = it }
68
68
 
69
- // Handle notificationButtons - variant allows distinguishing undefined from null
70
- androidOptions.notificationButtons?.let { variant ->
71
- notificationButtons =
69
+ // Handle remoteButtonLayout - variant allows distinguishing undefined from null
70
+ androidOptions.remoteButtonLayout?.let { variant ->
71
+ remoteButtonLayout =
72
72
  when (variant) {
73
- is Variant_NullType_NotificationButtonLayout.First -> null
74
- is Variant_NullType_NotificationButtonLayout.Second -> variant.value
73
+ is Variant_NullType_RemoteButtonLayout.First -> null
74
+ is Variant_NullType_RemoteButtonLayout.Second -> variant.value
75
75
  }
76
76
  }
77
77
  }
@@ -84,8 +84,8 @@ data class PlayerUpdateOptions(
84
84
  AndroidOptions(
85
85
  appKilledPlaybackBehavior = appKilledPlaybackBehavior,
86
86
  skipSilence = skipSilence,
87
- notificationButtons =
88
- notificationButtons?.let { Variant_NullType_NotificationButtonLayout.create(it) },
87
+ remoteButtonLayout =
88
+ remoteButtonLayout?.let { Variant_NullType_RemoteButtonLayout.create(it) },
89
89
  ),
90
90
  forwardJumpInterval = forwardJumpInterval,
91
91
  backwardJumpInterval = backwardJumpInterval,
@@ -1,19 +1,22 @@
1
1
  package com.audiobrowser.player
2
2
 
3
- import com.margelo.nitro.audiobrowser.NotificationButton
4
- import com.margelo.nitro.audiobrowser.NotificationButtonLayout
5
3
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
4
+ import com.margelo.nitro.audiobrowser.RemoteButton
5
+ import com.margelo.nitro.audiobrowser.RemoteButtonLayout
6
6
 
7
7
  /**
8
- * The pure decision core for Capability-gated controls: one rule for whether a control is available
9
- * ([isEnabled]) and one derivation of the notification button layout ([deriveNotificationSlots]).
10
- * MediaSessionCommandManager's three Media3 assemblies (external player commands, session button
11
- * layout, notification commands/layout) all consume these decisions — previously each
8
+ * The pure decision core for Capabilities: one rule for whether a Capability is enabled
9
+ * ([isEnabled]) and one derivation of the button layout ([deriveButtonSlots]).
10
+ * MediaSessionCommandManager's Media3 assemblies all consume these decisions — previously each
12
11
  * re-implemented them, and the copies had drifted.
13
12
  */
14
13
 
15
- /** A user-facing control gated by a Capability. */
16
- enum class Control {
14
+ /**
15
+ * A control that a Capability gates. Mirrors the [PlayerCapabilities] flags Android acts on —
16
+ * `shuffleMode`, `repeatMode` and `playbackRate` exist on the struct but only drive CarPlay, so
17
+ * they have no entry here.
18
+ */
19
+ enum class Capability {
17
20
  PLAY_PAUSE,
18
21
  STOP,
19
22
  SEEK_TO,
@@ -25,95 +28,145 @@ enum class Control {
25
28
  }
26
29
 
27
30
  /**
28
- * Whether a Capability-gated control is available. Everything is enabled by default and only an
29
- * explicit `false` disables — except FAVORITE, which is opt-in, and PLAY_PAUSE, which is one
30
- * control on every surface and only goes away when both halves are disabled.
31
+ * Whether a Capability is enabled. Everything is enabled by default and only an explicit `false`
32
+ * disables — except FAVORITE, which is opt-in, and PLAY_PAUSE, which is one control on every
33
+ * surface and only goes away when both halves are disabled.
31
34
  */
32
- fun PlayerCapabilities.isEnabled(control: Control): Boolean =
33
- when (control) {
34
- Control.PLAY_PAUSE -> !(play == false && pause == false)
35
- Control.STOP -> stop != false
36
- Control.SEEK_TO -> seekTo != false
37
- Control.SKIP_TO_NEXT -> skipToNext != false
38
- Control.SKIP_TO_PREVIOUS -> skipToPrevious != false
39
- Control.JUMP_FORWARD -> jumpForward != false
40
- Control.JUMP_BACKWARD -> jumpBackward != false
41
- Control.FAVORITE -> favoriteEnabled
35
+ fun PlayerCapabilities.isEnabled(capability: Capability): Boolean =
36
+ when (capability) {
37
+ Capability.PLAY_PAUSE -> !(play == false && pause == false)
38
+ Capability.STOP -> stop != false
39
+ Capability.SEEK_TO -> seekTo != false
40
+ Capability.SKIP_TO_NEXT -> skipToNext != false
41
+ Capability.SKIP_TO_PREVIOUS -> skipToPrevious != false
42
+ Capability.JUMP_FORWARD -> jumpForward != false
43
+ Capability.JUMP_BACKWARD -> jumpBackward != false
44
+ Capability.FAVORITE -> favoriteEnabled
42
45
  }
43
46
 
44
- /** Notification slot, mapped to `CommandButton.SLOT_*` at Media3 assembly time. */
45
- enum class NotificationSlot {
47
+ /**
48
+ * The three positions Android actually offers, mapped to `CommandButton.SLOT_*` at Media3 assembly
49
+ * time. There is no secondary pair: every system surface flattens the layout to back, forward and
50
+ * overflow, so a fourth or fifth position would have nowhere to render.
51
+ *
52
+ * Media3 does define SLOT_BACK_SECONDARY and SLOT_FORWARD_SECONDARY, but `DisplayConstraints`
53
+ * defaults both to **0 buttons** — only a controller that opts in with `setMaxButtonsForSlot` shows
54
+ * them, which no system surface does.
55
+ */
56
+ enum class ButtonSlot {
46
57
  BACK,
47
58
  FORWARD,
48
- BACK_SECONDARY,
49
- FORWARD_SECONDARY,
50
59
  OVERFLOW,
51
60
  }
52
61
 
53
- data class SlottedButton(val button: NotificationButton, val slot: NotificationSlot)
62
+ data class SlottedButton(val button: RemoteButton, val slot: ButtonSlot)
54
63
 
55
- private val NotificationButton.control: Control
64
+ /** The Capability gating a button. */
65
+ private val RemoteButton.capability: Capability
56
66
  get() =
57
67
  when (this) {
58
- NotificationButton.SKIP_TO_PREVIOUS -> Control.SKIP_TO_PREVIOUS
59
- NotificationButton.SKIP_TO_NEXT -> Control.SKIP_TO_NEXT
60
- NotificationButton.JUMP_BACKWARD -> Control.JUMP_BACKWARD
61
- NotificationButton.JUMP_FORWARD -> Control.JUMP_FORWARD
62
- NotificationButton.FAVORITE -> Control.FAVORITE
68
+ RemoteButton.SKIP_TO_PREVIOUS -> Capability.SKIP_TO_PREVIOUS
69
+ RemoteButton.SKIP_TO_NEXT -> Capability.SKIP_TO_NEXT
70
+ RemoteButton.JUMP_BACKWARD -> Capability.JUMP_BACKWARD
71
+ RemoteButton.JUMP_FORWARD -> Capability.JUMP_FORWARD
72
+ RemoteButton.FAVORITE -> Capability.FAVORITE
63
73
  }
64
74
 
65
75
  /** Whether [button]'s gating Capability allows it. */
66
- fun PlayerCapabilities.allows(button: NotificationButton): Boolean = isEnabled(button.control)
76
+ fun PlayerCapabilities.allows(button: RemoteButton): Boolean = isEnabled(button.capability)
77
+
78
+ /**
79
+ * Whether two layouts describe the same arrangement.
80
+ *
81
+ * [RemoteButtonLayout] is a generated data class whose `overflow` is an `Array`, and Kotlin
82
+ * data-class equality compares arrays by *reference*. Plain `==` therefore reports "changed" for
83
+ * two identical layouts whenever overflow is set, because every options update carries a fresh
84
+ * array across the bridge — which would republish the layout to Android Auto on every unrelated
85
+ * `updateOptions` call.
86
+ */
87
+ internal fun RemoteButtonLayout?.sameAs(other: RemoteButtonLayout?): Boolean =
88
+ if (this == null || other == null) {
89
+ this == null && other == null
90
+ } else {
91
+ back == other.back && forward == other.forward && overflow.contentEquals(other.overflow)
92
+ }
67
93
 
68
94
  /**
69
- * The single derivation of the notification button layout: an explicit [layout]'s slots filtered to
70
- * allowed buttons, or — with no layout — the capability defaults: skip on the primary slots (jump
71
- * falling back to a primary slot when its skip is disabled), jump on the secondary slots otherwise,
72
- * favorite in overflow.
95
+ * The single derivation of the button layout.
96
+ *
97
+ * The two parameters answer different questions, and only one of them is authoritative:
98
+ * - [capabilities] — *may* this button exist? Admission.
99
+ * - [layout] — *where* does it go? Arrangement.
100
+ *
101
+ * **A layout can rearrange buttons but never add one.** Every entry is filtered through [allows]
102
+ * regardless of its position, so naming a button whose Capability is disabled does nothing —
103
+ * listing `FAVORITE` while `capabilities.favorite` is off leaves it off. This is the usual
104
+ * surprise, because JUMP_FORWARD and JUMP_BACKWARD default to disabled: a layout full of jump
105
+ * buttons silently produces none until those capabilities are turned on.
106
+ *
107
+ * With no [layout], [capabilities] does both jobs — it decides membership *and* infers positions:
108
+ * skip takes the primary positions, jump falls back into a primary position when its skip is
109
+ * disabled and otherwise sits in overflow, favorite in overflow.
110
+ *
111
+ * With a [layout], each field maps to its position — `back` to BACK, `forward` to FORWARD, every
112
+ * `overflow` entry to OVERFLOW in order — and disabled entries drop out. A dropped entry leaves its
113
+ * position empty rather than promoting anything into it: turning off one capability must not
114
+ * silently rearrange the row.
115
+ *
116
+ * A [layout] is all-or-nothing: every field is required, so it fully describes the arrangement and
117
+ * nothing is merged with the capability defaults. The bridge could not support a per-field merge
118
+ * anyway — Nitro maps both an omitted and a null enum field to Kotlin `null`, so native cannot tell
119
+ * "leave this empty" from "derive this one".
120
+ *
121
+ * [capabilities] has a second job outside this function: it also drives the player and session
122
+ * commands, which govern what a Bluetooth remote or headset can trigger. That is why placement is
123
+ * purely cosmetic here — a button left out of the layout disappears from every screen while still
124
+ * responding to a headset.
125
+ *
126
+ * @param capabilities What the player is allowed to do. Gates every button, in both modes.
127
+ * @param layout Explicit placement, or null to infer placement from [capabilities].
73
128
  */
74
- fun deriveNotificationSlots(
129
+ fun deriveButtonSlots(
75
130
  capabilities: PlayerCapabilities,
76
- layout: NotificationButtonLayout?,
131
+ layout: RemoteButtonLayout?,
77
132
  ): List<SlottedButton> {
78
133
  if (layout != null) {
79
134
  return buildList {
80
- layout.back?.let { add(SlottedButton(it, NotificationSlot.BACK)) }
81
- layout.forward?.let { add(SlottedButton(it, NotificationSlot.FORWARD)) }
82
- layout.backSecondary?.let { add(SlottedButton(it, NotificationSlot.BACK_SECONDARY)) }
83
- layout.forwardSecondary?.let { add(SlottedButton(it, NotificationSlot.FORWARD_SECONDARY)) }
84
- layout.overflow?.forEach { add(SlottedButton(it, NotificationSlot.OVERFLOW)) }
135
+ layout.back?.let { add(SlottedButton(it, ButtonSlot.BACK)) }
136
+ layout.forward?.let { add(SlottedButton(it, ButtonSlot.FORWARD)) }
137
+ layout.overflow.forEach { add(SlottedButton(it, ButtonSlot.OVERFLOW)) }
85
138
  }
86
139
  .filter { capabilities.allows(it.button) }
87
140
  }
88
141
 
89
142
  return buildList {
90
- val skipPrevious = capabilities.isEnabled(Control.SKIP_TO_PREVIOUS)
91
- val skipNext = capabilities.isEnabled(Control.SKIP_TO_NEXT)
92
- val jumpBackward = capabilities.isEnabled(Control.JUMP_BACKWARD)
93
- val jumpForward = capabilities.isEnabled(Control.JUMP_FORWARD)
143
+ val skipPrevious = capabilities.isEnabled(Capability.SKIP_TO_PREVIOUS)
144
+ val skipNext = capabilities.isEnabled(Capability.SKIP_TO_NEXT)
145
+ val jumpBackward = capabilities.isEnabled(Capability.JUMP_BACKWARD)
146
+ val jumpForward = capabilities.isEnabled(Capability.JUMP_FORWARD)
94
147
 
95
- // Primary slots: skip, with jump falling back when its skip is disabled.
148
+ // Primary positions: skip, with jump falling back when its skip is disabled.
96
149
  if (skipPrevious) {
97
- add(SlottedButton(NotificationButton.SKIP_TO_PREVIOUS, NotificationSlot.BACK))
150
+ add(SlottedButton(RemoteButton.SKIP_TO_PREVIOUS, ButtonSlot.BACK))
98
151
  } else if (jumpBackward) {
99
- add(SlottedButton(NotificationButton.JUMP_BACKWARD, NotificationSlot.BACK))
152
+ add(SlottedButton(RemoteButton.JUMP_BACKWARD, ButtonSlot.BACK))
100
153
  }
101
154
  if (skipNext) {
102
- add(SlottedButton(NotificationButton.SKIP_TO_NEXT, NotificationSlot.FORWARD))
155
+ add(SlottedButton(RemoteButton.SKIP_TO_NEXT, ButtonSlot.FORWARD))
103
156
  } else if (jumpForward) {
104
- add(SlottedButton(NotificationButton.JUMP_FORWARD, NotificationSlot.FORWARD))
157
+ add(SlottedButton(RemoteButton.JUMP_FORWARD, ButtonSlot.FORWARD))
105
158
  }
106
159
 
107
- // Jump moves to the secondary slots when skip holds the primary.
160
+ // Jump moves to overflow when skip holds the primary position.
108
161
  if (skipPrevious && jumpBackward) {
109
- add(SlottedButton(NotificationButton.JUMP_BACKWARD, NotificationSlot.BACK_SECONDARY))
162
+ add(SlottedButton(RemoteButton.JUMP_BACKWARD, ButtonSlot.OVERFLOW))
110
163
  }
111
164
  if (skipNext && jumpForward) {
112
- add(SlottedButton(NotificationButton.JUMP_FORWARD, NotificationSlot.FORWARD_SECONDARY))
165
+ add(SlottedButton(RemoteButton.JUMP_FORWARD, ButtonSlot.OVERFLOW))
113
166
  }
114
167
 
115
- if (capabilities.isEnabled(Control.FAVORITE)) {
116
- add(SlottedButton(NotificationButton.FAVORITE, NotificationSlot.OVERFLOW))
168
+ if (capabilities.isEnabled(Capability.FAVORITE)) {
169
+ add(SlottedButton(RemoteButton.FAVORITE, ButtonSlot.OVERFLOW))
117
170
  }
118
171
  }
119
172
  }
@@ -24,8 +24,8 @@ import com.margelo.nitro.audiobrowser.GateEvent
24
24
  import com.margelo.nitro.audiobrowser.GateReason
25
25
  import com.margelo.nitro.audiobrowser.MediaReference
26
26
  import com.margelo.nitro.audiobrowser.NativeGateRequest
27
- import com.margelo.nitro.audiobrowser.NotificationButtonLayout
28
27
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
28
+ import com.margelo.nitro.audiobrowser.RemoteButtonLayout
29
29
  import com.margelo.nitro.audiobrowser.SearchParams
30
30
  import com.margelo.nitro.audiobrowser.Track
31
31
  import kotlinx.coroutines.CancellationException
@@ -160,16 +160,20 @@ class MediaSessionCallback(private val player: Player) :
160
160
  fun updateMediaSession(
161
161
  mediaSession: MediaSession,
162
162
  capabilities: PlayerCapabilities,
163
- notificationButtons: NotificationButtonLayout?,
163
+ remoteButtonLayout: RemoteButtonLayout?,
164
164
  searchAvailable: Boolean,
165
+ forwardJumpInterval: Double,
166
+ backwardJumpInterval: Double,
165
167
  ) {
166
168
  // Store as MediaLibrarySession for notifyChildrenChanged support
167
169
  this.mediaLibrarySession = mediaSession as? MediaLibraryService.MediaLibrarySession
168
170
  commandManager.updateMediaSession(
169
171
  mediaSession,
170
172
  capabilities,
171
- notificationButtons,
173
+ remoteButtonLayout,
172
174
  searchAvailable,
175
+ forwardJumpInterval,
176
+ backwardJumpInterval,
173
177
  )
174
178
  }
175
179
 
@@ -400,8 +404,8 @@ class MediaSessionCallback(private val player: Player) :
400
404
  /**
401
405
  * Converts tracks to MediaItems for browse delivery, routing http(s) artwork through the
402
406
  * content:// provider so Android Auto can load it via the ArtworkContentProvider. Image-row
403
- * tracks (a CarPlay-only rendering) are expanded into their items as regular grouped rows
404
- * first — see [TrackFactory.expandImageRows].
407
+ * tracks (a CarPlay-only rendering) are expanded into their items as regular grouped rows first —
408
+ * see [TrackFactory.expandImageRows].
405
409
  */
406
410
  private fun toMediaItems(tracks: List<Track>): List<MediaItem> {
407
411
  val registry = player.browseArtworkRegistry