react-native-audio-browser 0.2.0-next.12 → 0.2.0-next.14
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.
- package/README.md +10 -0
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +8 -1
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +52 -0
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -11
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +1 -1
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +11 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +11 -1
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +21 -22
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +17 -2
- package/android/src/main/java/com/audiobrowser/player/Player.kt +3 -0
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +73 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +16 -6
- package/android/src/main/res/values/strings.xml +37 -2
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +50 -5
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +9 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +56 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +4 -4
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +77 -10
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +2 -2
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +10 -9
- package/ios/Browser/BrowserManager.swift +106 -45
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +81 -15
- package/ios/Browser/SectionPresentation.swift +30 -0
- package/ios/Browser/StyleResolver.swift +11 -0
- package/ios/Browser/TabBarEntries.swift +7 -1
- package/ios/CarPlay/CarPlayController.swift +139 -35
- package/ios/CarPlay/CarPlayListItemFactory.swift +99 -27
- package/ios/Extension/Section+Copying.swift +1 -3
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +56 -12
- package/ios/Model/NitroTypeStubs.swift +60 -1
- package/ios/Player/MediaIntentCriteria.swift +13 -5
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +53 -5
- package/ios/Player/PlaybackCoordinator.swift +11 -4
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +36 -10
- package/ios/Player/ShuffleOrder.swift +23 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +76 -1
- package/ios/Tests/QueueManagerTests.swift +72 -6
- package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +2 -2
- package/ios/Tests/SectionPresentationTests.swift +29 -0
- package/ios/Tests/StyleResolverTests.swift +60 -0
- package/ios/Tests/Support/TestTypes.swift +7 -0
- package/ios/TrackPlayer.swift +57 -34
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/features/browser-config.js +1 -0
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/playback/progress.js +2 -1
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/features/browser-config.js +1 -0
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/playback/progress.js +2 -1
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -1
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +116 -9
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +36 -0
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -0
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +6 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -0
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +13 -0
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -0
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +20 -1
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +6 -0
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +4 -0
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +6 -0
- package/nitrogen/generated/android/c++/JSection.hpp +7 -5
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +30 -3
- package/nitrogen/generated/android/c++/JTrack.hpp +4 -0
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +24 -3
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +6 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +2 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +18 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +15 -3
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +9 -0
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +48 -1
- package/nitrogen/generated/ios/swift/Section.swift +1 -19
- package/nitrogen/generated/ios/swift/SectionStyle.swift +70 -1
- package/nitrogen/generated/ios/swift/TrackStyle.swift +59 -1
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -2
- package/nitrogen/generated/shared/c++/Section.hpp +1 -5
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +32 -2
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
- package/package.json +7 -2
- package/src/features/browser-config.ts +1 -0
- package/src/features/playback/progress.ts +2 -1
- package/src/features/playback/rate.ts +8 -1
- package/src/style-resolution.test.ts +72 -1
- package/src/types/browser-native.ts +5 -1
- package/src/types/browser-nodes.ts +124 -9
- package/src/types/browser.ts +38 -0
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +246 -0
|
@@ -32,7 +32,6 @@ import com.margelo.nitro.audiobrowser.PlayerCapabilities
|
|
|
32
32
|
import com.margelo.nitro.audiobrowser.RemoteButtonLayout
|
|
33
33
|
import com.margelo.nitro.audiobrowser.SearchParams
|
|
34
34
|
import com.margelo.nitro.audiobrowser.Section
|
|
35
|
-
import com.margelo.nitro.audiobrowser.StyleDisplay
|
|
36
35
|
import com.margelo.nitro.audiobrowser.Track
|
|
37
36
|
import kotlinx.coroutines.CancellationException
|
|
38
37
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -419,33 +418,33 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
419
418
|
/**
|
|
420
419
|
* Flattens a page's sections to MediaItems for browse delivery — the Media3 boundary where
|
|
421
420
|
* sections die (ADR 0010): the MediaBrowser protocol has no section node, so each child is
|
|
422
|
-
* stamped with its owning section's title/style hints. A
|
|
423
|
-
*
|
|
424
|
-
*
|
|
421
|
+
* stamped with its owning section's title/style hints. A section with a `path` gains a browsable
|
|
422
|
+
* "view more" row under the same header — at every `display`, because no Android Auto header is
|
|
423
|
+
* tappable at any of them, so the row is the section's only "view all" affordance here. Http(s)
|
|
424
|
+
* artwork routes through the content:// provider so Android Auto can load it via the
|
|
425
|
+
* ArtworkContentProvider.
|
|
425
426
|
*/
|
|
426
|
-
private fun toMediaItems(sections: List<Section>): List<MediaItem> {
|
|
427
|
+
private suspend fun toMediaItems(sections: List<Section>): List<MediaItem> {
|
|
427
428
|
val registry = player.browseArtworkRegistry
|
|
428
429
|
val authority = com.audiobrowser.util.ArtworkUris.authorityFor(player.context.packageName)
|
|
429
430
|
return sections.flatMap { section ->
|
|
430
431
|
// Android Auto has no disabled affordance, so an unavailable track hides
|
|
431
432
|
// — never a normal-looking dead row (Track.disabled's rendering ladder).
|
|
432
|
-
val
|
|
433
|
-
val items =
|
|
434
|
-
|
|
435
|
-
val isTileSection = section.style?.display == StyleDisplay.GRID
|
|
436
|
-
// No "view all" under an empty section — CarPlay skips empty sections
|
|
433
|
+
val children = section.children.filter { it.disabled != true }
|
|
434
|
+
val items = children.map { TrackFactory.toBrowseMediaItem(it, registry, authority, section) }
|
|
435
|
+
// No "view more" under an empty section — CarPlay skips empty sections
|
|
437
436
|
// outright, and a lone navigation tile under a header is a dead end.
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
437
|
+
if (section.path == null || children.isEmpty()) return@flatMap items
|
|
438
|
+
// The browser is already up at every call site, so awaiting it here is a field read, and
|
|
439
|
+
// viewMoreTitle is memoized per content generation — both are free after the first row.
|
|
440
|
+
val title = player.awaitBrowser().browserManager.viewMoreTitle()
|
|
441
|
+
items +
|
|
442
|
+
TrackFactory.toBrowseMediaItem(
|
|
443
|
+
TrackFactory.navigationTrack(section, title),
|
|
444
|
+
registry,
|
|
445
|
+
authority,
|
|
446
|
+
section,
|
|
447
|
+
)
|
|
449
448
|
}
|
|
450
449
|
}
|
|
451
450
|
|
|
@@ -453,7 +452,7 @@ class MediaSessionCallback(private val player: Player) :
|
|
|
453
452
|
* Flat lists (tabs, search results) convert as one untitled list section — which contributes no
|
|
454
453
|
* group or style hints — so exactly one Track→MediaItem path exists at the Media3 boundary.
|
|
455
454
|
*/
|
|
456
|
-
private fun toFlatMediaItems(tracks: List<Track>): List<MediaItem> =
|
|
455
|
+
private suspend fun toFlatMediaItems(tracks: List<Track>): List<MediaItem> =
|
|
457
456
|
toMediaItems(listOf(untitledSection(tracks.toTypedArray())))
|
|
458
457
|
|
|
459
458
|
override fun onGetItem(
|
|
@@ -4,8 +4,11 @@ import android.content.Context
|
|
|
4
4
|
import android.content.SharedPreferences
|
|
5
5
|
import androidx.core.content.edit
|
|
6
6
|
import androidx.media3.common.C
|
|
7
|
+
import com.audiobrowser.util.EMPTY_TRACK_STYLE
|
|
7
8
|
import com.audiobrowser.util.artworkOf
|
|
8
9
|
import com.audiobrowser.util.toArtworkRendering
|
|
10
|
+
import com.audiobrowser.util.toCardImage
|
|
11
|
+
import com.audiobrowser.util.toImageShape
|
|
9
12
|
import com.audiobrowser.util.toStyleDisplay
|
|
10
13
|
import com.audiobrowser.util.toWireString
|
|
11
14
|
import com.audiobrowser.util.url
|
|
@@ -164,7 +167,11 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
164
167
|
// Apply player settings from persisted state
|
|
165
168
|
player.repeatMode = state.repeatMode
|
|
166
169
|
player.shuffleMode = state.shuffleEnabled
|
|
167
|
-
|
|
170
|
+
// Guarded because this input is untrusted: ExoPlayer's setPlaybackSpeed rejects a
|
|
171
|
+
// non-positive speed with IllegalArgumentException, and a snapshot holding one (a
|
|
172
|
+
// corrupted prefs file — the normal write path can't produce it) would then throw on
|
|
173
|
+
// every resume, permanently, since the bad value stays persisted.
|
|
174
|
+
if (state.playbackSpeed > 0f) player.playbackSpeed = state.playbackSpeed
|
|
168
175
|
|
|
169
176
|
Timber.d(
|
|
170
177
|
"Restored player settings: repeatMode=${state.repeatMode}, shuffle=${state.shuffleEnabled}, speed=${state.playbackSpeed}"
|
|
@@ -216,6 +223,10 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
216
223
|
JSONObject().apply {
|
|
217
224
|
put("display", style.display?.toWireString())
|
|
218
225
|
put("artworkRendering", style.artworkRendering?.toWireString())
|
|
226
|
+
put("imageShape", style.imageShape?.toWireString())
|
|
227
|
+
put("accessorySymbol", style.accessorySymbol)
|
|
228
|
+
put("cardTint", style.cardTint)
|
|
229
|
+
put("cardImage", style.cardImage?.toWireString())
|
|
219
230
|
}
|
|
220
231
|
},
|
|
221
232
|
)
|
|
@@ -254,11 +265,15 @@ class PlaybackStateStore(private val player: Player) {
|
|
|
254
265
|
TrackStyle(
|
|
255
266
|
display = style.optString("display").toStyleDisplay(),
|
|
256
267
|
artworkRendering = style.optString("artworkRendering").toArtworkRendering(),
|
|
268
|
+
imageShape = style.optString("imageShape").toImageShape(),
|
|
269
|
+
accessorySymbol = style.optString("accessorySymbol").takeIf { it.isNotEmpty() },
|
|
270
|
+
cardTint = style.optString("cardTint").takeIf { it.isNotEmpty() },
|
|
271
|
+
cardImage = style.optString("cardImage").toCardImage(),
|
|
257
272
|
)
|
|
258
273
|
}
|
|
259
274
|
// An empty block collapses to "no declaration" — one shape on
|
|
260
275
|
// every platform, matching the browse decoders.
|
|
261
|
-
?.takeIf { it
|
|
276
|
+
?.takeIf { it != EMPTY_TRACK_STYLE },
|
|
262
277
|
disabled =
|
|
263
278
|
if (obj.has("disabled") && !obj.isNull("disabled")) obj.getBoolean("disabled")
|
|
264
279
|
else null,
|
|
@@ -668,6 +668,9 @@ class Player(internal val context: Context) {
|
|
|
668
668
|
) {
|
|
669
669
|
val mediaItems = TrackFactory.toMedia3(tracks).toMutableList()
|
|
670
670
|
exoPlayer.setMediaItems(mediaItems, startIndex, startPositionMs)
|
|
671
|
+
if (mediaItems.isNotEmpty()) {
|
|
672
|
+
exoPlayer.setShuffleOrder(shuffleOrderLedBy(startIndex, mediaItems.size))
|
|
673
|
+
}
|
|
671
674
|
queueSourcePath = sourcePath
|
|
672
675
|
exoPlayer.prepare()
|
|
673
676
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.audiobrowser.player
|
|
2
|
+
|
|
3
|
+
import androidx.media3.exoplayer.source.ShuffleOrder
|
|
4
|
+
import kotlin.random.Random
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A shuffle order over [count] items with [startIndex] pinned to shuffle position 0.
|
|
8
|
+
*
|
|
9
|
+
* Media3 does not do this for us. `ExoPlayerImpl.setMediaSources` hands the start index to
|
|
10
|
+
* `ShuffleOrder.cloneAndSet(insertionCount, startIndex)`, but that is an interface default of
|
|
11
|
+
* `cloneAndClear().cloneAndInsert(0, insertionCount)` — the start index is discarded — and
|
|
12
|
+
* `DefaultShuffleOrder` does not override it (verified against the pinned media3 build). So with
|
|
13
|
+
* shuffle on, the track playback starts from lands at a random position in the order, and a 1-in-N
|
|
14
|
+
* share of queues put it last: `nextMediaItemIndex` is then `INDEX_UNSET` from the very first
|
|
15
|
+
* track, so auto-advance ends the queue after one track and fires `onQueueEnded`. The
|
|
16
|
+
* voice-assistant cold resume expands a queue exactly this way, so it sits squarely on that path.
|
|
17
|
+
*
|
|
18
|
+
* Applied unconditionally by `Player.setQueue`, like the Swift twin's `adoptInitialCurrent`:
|
|
19
|
+
* enabling shuffle later does not regenerate the order, so it has to be sane whether or not shuffle
|
|
20
|
+
* is on right now.
|
|
21
|
+
*/
|
|
22
|
+
internal fun shuffleOrderLedBy(startIndex: Int, count: Int, random: Random = Random): ShuffleOrder {
|
|
23
|
+
val first = startIndex.coerceIn(0, (count - 1).coerceAtLeast(0))
|
|
24
|
+
val indices = IntArray(count)
|
|
25
|
+
if (count > 0) {
|
|
26
|
+
indices[0] = first
|
|
27
|
+
((0 until count) - first).shuffled().forEachIndexed { i, index -> indices[i + 1] = index }
|
|
28
|
+
}
|
|
29
|
+
return ShuffleOrder.DefaultShuffleOrder(indices, random.nextLong())
|
|
30
|
+
}
|
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
package com.audiobrowser.util
|
|
2
2
|
|
|
3
3
|
import com.margelo.nitro.audiobrowser.ArtworkRendering
|
|
4
|
+
import com.margelo.nitro.audiobrowser.CardImage
|
|
5
|
+
import com.margelo.nitro.audiobrowser.GridTile
|
|
6
|
+
import com.margelo.nitro.audiobrowser.ImageShape
|
|
7
|
+
import com.margelo.nitro.audiobrowser.SectionStyle
|
|
4
8
|
import com.margelo.nitro.audiobrowser.StyleDisplay
|
|
9
|
+
import com.margelo.nitro.audiobrowser.TrackStyle
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The all-nil blocks a decode collapses to "no declaration". Every field is listed exactly once
|
|
13
|
+
* here — the decoders and the persistence restore compare against these instead of re-enumerating
|
|
14
|
+
* the fields in each guard (a constructor gaining a field breaks this file, not silently a guard).
|
|
15
|
+
*/
|
|
16
|
+
internal val EMPTY_TRACK_STYLE =
|
|
17
|
+
TrackStyle(
|
|
18
|
+
display = null,
|
|
19
|
+
artworkRendering = null,
|
|
20
|
+
imageShape = null,
|
|
21
|
+
accessorySymbol = null,
|
|
22
|
+
cardTint = null,
|
|
23
|
+
cardImage = null,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
internal val EMPTY_SECTION_STYLE =
|
|
27
|
+
SectionStyle(
|
|
28
|
+
display = null,
|
|
29
|
+
artworkRendering = null,
|
|
30
|
+
imageShape = null,
|
|
31
|
+
accessorySymbol = null,
|
|
32
|
+
cardTint = null,
|
|
33
|
+
cardImage = null,
|
|
34
|
+
gridWrap = null,
|
|
35
|
+
gridTile = null,
|
|
36
|
+
)
|
|
5
37
|
|
|
6
38
|
/**
|
|
7
39
|
* The style block's wire strings (ADR 0011) ↔ Nitro enums, shared by the browse decoder and the
|
|
@@ -33,3 +65,44 @@ internal fun String?.toArtworkRendering(): ArtworkRendering? =
|
|
|
33
65
|
"stencil" -> ArtworkRendering.STENCIL
|
|
34
66
|
else -> null
|
|
35
67
|
}
|
|
68
|
+
|
|
69
|
+
internal fun ImageShape.toWireString(): String =
|
|
70
|
+
when (this) {
|
|
71
|
+
ImageShape.CIRCULAR -> "circular"
|
|
72
|
+
ImageShape.ROUNDED_RECTANGLE -> "rounded-rectangle"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
internal fun String?.toImageShape(): ImageShape? =
|
|
76
|
+
when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
|
|
77
|
+
"circular" -> ImageShape.CIRCULAR
|
|
78
|
+
"rounded-rectangle" -> ImageShape.ROUNDED_RECTANGLE
|
|
79
|
+
else -> null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
internal fun GridTile.toWireString(): String =
|
|
83
|
+
when (this) {
|
|
84
|
+
GridTile.PLAIN -> "plain"
|
|
85
|
+
GridTile.CARD -> "card"
|
|
86
|
+
GridTile.CONDENSED -> "condensed"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
internal fun String?.toGridTile(): GridTile? =
|
|
90
|
+
when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
|
|
91
|
+
"plain" -> GridTile.PLAIN
|
|
92
|
+
"card" -> GridTile.CARD
|
|
93
|
+
"condensed" -> GridTile.CONDENSED
|
|
94
|
+
else -> null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
internal fun CardImage.toWireString(): String =
|
|
98
|
+
when (this) {
|
|
99
|
+
CardImage.NORMAL -> "normal"
|
|
100
|
+
CardImage.BACKGROUND -> "background"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
internal fun String?.toCardImage(): CardImage? =
|
|
104
|
+
when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
|
|
105
|
+
"normal" -> CardImage.NORMAL
|
|
106
|
+
"background" -> CardImage.BACKGROUND
|
|
107
|
+
else -> null
|
|
108
|
+
}
|
|
@@ -25,13 +25,16 @@ object TrackFactory {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* A synthetic browsable Track for a section's "view
|
|
29
|
-
*
|
|
28
|
+
* A synthetic browsable Track for a section's "view more" row — the section has a path but no
|
|
29
|
+
* Track to hang it on (ADR 0010). It carries the section's declared item block as its own,
|
|
30
30
|
* `display` included: the section's declaration about this content's layout is the only declared
|
|
31
31
|
* promise the "view all" page can ever have (there is no consumer-authored handle to declare one
|
|
32
32
|
* on), and without it Android Auto would always render that page as a list.
|
|
33
|
+
*
|
|
34
|
+
* [title] is the consumer's `viewMoreTitle`, one string for every section — not the section's own
|
|
35
|
+
* title, which the group header above the row already shows.
|
|
33
36
|
*/
|
|
34
|
-
fun navigationTrack(section: Section): Track =
|
|
37
|
+
fun navigationTrack(section: Section, title: String): Track =
|
|
35
38
|
Track(
|
|
36
39
|
id = null,
|
|
37
40
|
path = section.path,
|
|
@@ -39,8 +42,8 @@ object TrackFactory {
|
|
|
39
42
|
artwork = null,
|
|
40
43
|
artworkSource = null,
|
|
41
44
|
request = null,
|
|
42
|
-
title =
|
|
43
|
-
subtitle =
|
|
45
|
+
title = title,
|
|
46
|
+
subtitle = null,
|
|
44
47
|
artist = null,
|
|
45
48
|
albumPath = null,
|
|
46
49
|
album = null,
|
|
@@ -49,7 +52,14 @@ object TrackFactory {
|
|
|
49
52
|
duration = null,
|
|
50
53
|
style =
|
|
51
54
|
section.style?.let {
|
|
52
|
-
TrackStyle(
|
|
55
|
+
TrackStyle(
|
|
56
|
+
display = it.display,
|
|
57
|
+
artworkRendering = it.artworkRendering,
|
|
58
|
+
imageShape = it.imageShape,
|
|
59
|
+
accessorySymbol = it.accessorySymbol,
|
|
60
|
+
cardTint = it.cardTint,
|
|
61
|
+
cardImage = it.cardImage,
|
|
62
|
+
)
|
|
53
63
|
},
|
|
54
64
|
disabled = null,
|
|
55
65
|
favorited = null,
|
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Default copy for the two error tiles the library invents in the Android Auto
|
|
4
|
+
browse list. These rows have no consumer-authored Track behind them, so there
|
|
5
|
+
is nothing in the browse tree to read a title from; English here because a
|
|
6
|
+
default that renders beats a blank row.
|
|
7
|
+
|
|
8
|
+
Resources rather than a browser-config option — the route the library's
|
|
9
|
+
cross-platform copy takes (`formatNavigationError`, `carPlayLoadingTitle`,
|
|
10
|
+
`viewMoreTitle`) — because these two are not cross-platform. They exist only
|
|
11
|
+
because Media3 drops the message from `LibraryResult.ofError` on the legacy
|
|
12
|
+
browse bridge, so a row is the sole signal available; CarPlay has no
|
|
13
|
+
counterpart, stating failures in the list's centered empty view instead
|
|
14
|
+
(CarPlayController.showMessage). Android-only copy in the Android-idiomatic
|
|
15
|
+
place: an app re-skins either by declaring the same name in its own
|
|
16
|
+
strings.xml, and a `values-<locale>/` file can add translations later without
|
|
17
|
+
touching the API.
|
|
18
|
+
-->
|
|
2
19
|
<resources>
|
|
3
|
-
<!--
|
|
20
|
+
<!--
|
|
21
|
+
The offline tile: shown in place of a page's contents when the device is
|
|
22
|
+
offline and the consumer left `androidControllerOfflineError` on. Distinct
|
|
23
|
+
from the browse-error tile below — this one is used only when the network
|
|
24
|
+
monitor says we are actually offline, so it may name the cause. The
|
|
25
|
+
subtitle is the tile's second line.
|
|
26
|
+
-->
|
|
4
27
|
<string name="audio_browser_offline_error">No internet connection</string>
|
|
5
28
|
<string name="audio_browser_offline_error_subtitle">Check your connection and try again</string>
|
|
6
29
|
|
|
7
|
-
<!--
|
|
30
|
+
<!--
|
|
31
|
+
The browse-error tile: shown when a browse fails while ONLINE — a server
|
|
32
|
+
error, a bad status, a timeout waiting for the JS browser. Deliberately
|
|
33
|
+
vague, and deliberately not "no internet connection": the genuinely-offline
|
|
34
|
+
case is caught earlier and gets the tile above, so anything reaching this
|
|
35
|
+
one has a working connection and would be misdiagnosed by that wording.
|
|
36
|
+
|
|
37
|
+
A tile rather than an error dialog because Media3 drops the message from
|
|
38
|
+
`LibraryResult.ofError` on the legacy browse bridge, which would leave a
|
|
39
|
+
bare "No items" screen. Verified on a head unit (2026-06): the Android Auto
|
|
40
|
+
browse list never renders error text by any other route, so tiles are the
|
|
41
|
+
only in-browse signal the library controls.
|
|
42
|
+
-->
|
|
8
43
|
<string name="audio_browser_browse_error">Something went wrong</string>
|
|
9
44
|
<string name="audio_browser_browse_error_subtitle">Please try again later</string>
|
|
10
45
|
</resources>
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
package com.audiobrowser
|
|
2
2
|
|
|
3
3
|
import com.audiobrowser.util.artworkOf
|
|
4
|
+
import com.margelo.nitro.audiobrowser.ArtworkRendering
|
|
4
5
|
import com.margelo.nitro.audiobrowser.ArtworkRequestConfig
|
|
6
|
+
import com.margelo.nitro.audiobrowser.CardImage
|
|
7
|
+
import com.margelo.nitro.audiobrowser.GridTile
|
|
5
8
|
import com.margelo.nitro.audiobrowser.ImageQueryParams
|
|
9
|
+
import com.margelo.nitro.audiobrowser.ImageShape
|
|
6
10
|
import com.margelo.nitro.audiobrowser.ImageSource
|
|
7
11
|
import com.margelo.nitro.audiobrowser.MediaRequestConfig
|
|
8
12
|
import com.margelo.nitro.audiobrowser.NativeRouteEntry
|
|
9
13
|
import com.margelo.nitro.audiobrowser.ResolvedTrack
|
|
10
14
|
import com.margelo.nitro.audiobrowser.Section
|
|
11
15
|
import com.margelo.nitro.audiobrowser.SectionStyle
|
|
16
|
+
import com.margelo.nitro.audiobrowser.StyleDisplay
|
|
12
17
|
import com.margelo.nitro.audiobrowser.Track
|
|
18
|
+
import com.margelo.nitro.audiobrowser.TrackStyle
|
|
13
19
|
import com.margelo.nitro.audiobrowser.TransformableRequestConfig
|
|
14
20
|
|
|
15
21
|
/**
|
|
@@ -28,6 +34,7 @@ object TestFixtures {
|
|
|
28
34
|
album: String? = null,
|
|
29
35
|
favorited: Boolean? = null,
|
|
30
36
|
disabled: Boolean? = null,
|
|
37
|
+
style: TrackStyle? = null,
|
|
31
38
|
) =
|
|
32
39
|
Track(
|
|
33
40
|
id = id,
|
|
@@ -44,7 +51,7 @@ object TestFixtures {
|
|
|
44
51
|
description = null,
|
|
45
52
|
genre = null,
|
|
46
53
|
duration = null,
|
|
47
|
-
style =
|
|
54
|
+
style = style,
|
|
48
55
|
disabled = disabled,
|
|
49
56
|
favorited = favorited,
|
|
50
57
|
live = null,
|
|
@@ -52,12 +59,11 @@ object TestFixtures {
|
|
|
52
59
|
|
|
53
60
|
/** A page [Section] — plain list by default. */
|
|
54
61
|
fun section(
|
|
55
|
-
children: Array<Track
|
|
62
|
+
children: Array<Track> = emptyArray(),
|
|
56
63
|
title: String? = null,
|
|
57
|
-
subtitle: String? = null,
|
|
58
64
|
style: SectionStyle? = null,
|
|
59
65
|
path: String? = null,
|
|
60
|
-
) = Section(title = title,
|
|
66
|
+
) = Section(title = title, style = style, path = path, children = children)
|
|
61
67
|
|
|
62
68
|
fun transformableConfig(
|
|
63
69
|
baseUrl: String? = null,
|
|
@@ -109,6 +115,7 @@ object TestFixtures {
|
|
|
109
115
|
path: String = "https://api.example.com/channel/abc",
|
|
110
116
|
artwork: String? = null,
|
|
111
117
|
artworkSource: ImageSource? = null,
|
|
118
|
+
style: TrackStyle? = null,
|
|
112
119
|
) =
|
|
113
120
|
Track(
|
|
114
121
|
id = null,
|
|
@@ -125,7 +132,7 @@ object TestFixtures {
|
|
|
125
132
|
description = null,
|
|
126
133
|
genre = null,
|
|
127
134
|
duration = null,
|
|
128
|
-
style =
|
|
135
|
+
style = style,
|
|
129
136
|
disabled = null,
|
|
130
137
|
favorited = null,
|
|
131
138
|
live = null,
|
|
@@ -201,4 +208,42 @@ object TestFixtures {
|
|
|
201
208
|
contentType = null,
|
|
202
209
|
userAgent = null,
|
|
203
210
|
)
|
|
211
|
+
|
|
212
|
+
fun trackStyle(
|
|
213
|
+
display: StyleDisplay? = null,
|
|
214
|
+
artworkRendering: ArtworkRendering? = null,
|
|
215
|
+
imageShape: ImageShape? = null,
|
|
216
|
+
accessorySymbol: String? = null,
|
|
217
|
+
cardTint: String? = null,
|
|
218
|
+
cardImage: CardImage? = null,
|
|
219
|
+
) =
|
|
220
|
+
TrackStyle(
|
|
221
|
+
display = display,
|
|
222
|
+
artworkRendering = artworkRendering,
|
|
223
|
+
imageShape = imageShape,
|
|
224
|
+
accessorySymbol = accessorySymbol,
|
|
225
|
+
cardTint = cardTint,
|
|
226
|
+
cardImage = cardImage,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
fun sectionStyle(
|
|
230
|
+
display: StyleDisplay? = null,
|
|
231
|
+
artworkRendering: ArtworkRendering? = null,
|
|
232
|
+
imageShape: ImageShape? = null,
|
|
233
|
+
accessorySymbol: String? = null,
|
|
234
|
+
cardTint: String? = null,
|
|
235
|
+
cardImage: CardImage? = null,
|
|
236
|
+
gridWrap: Boolean? = null,
|
|
237
|
+
gridTile: GridTile? = null,
|
|
238
|
+
) =
|
|
239
|
+
SectionStyle(
|
|
240
|
+
gridWrap = gridWrap,
|
|
241
|
+
gridTile = gridTile,
|
|
242
|
+
display = display,
|
|
243
|
+
artworkRendering = artworkRendering,
|
|
244
|
+
imageShape = imageShape,
|
|
245
|
+
accessorySymbol = accessorySymbol,
|
|
246
|
+
cardTint = cardTint,
|
|
247
|
+
cardImage = cardImage,
|
|
248
|
+
)
|
|
204
249
|
}
|
|
@@ -144,4 +144,13 @@ class BrowserManagerLayerResolutionTest {
|
|
|
144
144
|
assertSame(request, browserManager.resolvedRequestLayerForTest)
|
|
145
145
|
assertNull(browserManager.resolvedBrowseLayerForTest)
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
@Test
|
|
149
|
+
fun `viewMoreTitle falls back to the in-code default when unconfigured`() = runTest {
|
|
150
|
+
// The only viewMoreTitle path a JVM test can reach — a configured callback returns a Nitro
|
|
151
|
+
// Promise, see the resolver-thunk gap noted above.
|
|
152
|
+
browserManager.config = BrowserConfig()
|
|
153
|
+
|
|
154
|
+
assertEquals(BrowserManager.DEFAULT_VIEW_MORE_TITLE, browserManager.viewMoreTitle())
|
|
155
|
+
}
|
|
147
156
|
}
|
|
@@ -4,6 +4,7 @@ import androidx.media3.common.C
|
|
|
4
4
|
import androidx.media3.common.MediaItem
|
|
5
5
|
import com.audiobrowser.TestFixtures.resolvedTrack
|
|
6
6
|
import com.audiobrowser.TestFixtures.section
|
|
7
|
+
import com.audiobrowser.TestFixtures.sectionStyle
|
|
7
8
|
import com.audiobrowser.TestFixtures.staticRoute
|
|
8
9
|
import com.audiobrowser.TestFixtures.track
|
|
9
10
|
import com.audiobrowser.util.BrowserPathHelper
|
|
@@ -183,7 +184,7 @@ class BrowserManagerTest {
|
|
|
183
184
|
section(
|
|
184
185
|
title = "Row",
|
|
185
186
|
style =
|
|
186
|
-
|
|
187
|
+
sectionStyle(
|
|
187
188
|
display = com.margelo.nitro.audiobrowser.StyleDisplay.GRID,
|
|
188
189
|
artworkRendering = null,
|
|
189
190
|
gridWrap = false,
|
|
@@ -252,7 +253,7 @@ class BrowserManagerTest {
|
|
|
252
253
|
section(
|
|
253
254
|
title = "Most Played",
|
|
254
255
|
style =
|
|
255
|
-
|
|
256
|
+
sectionStyle(
|
|
256
257
|
display = com.margelo.nitro.audiobrowser.StyleDisplay.GRID,
|
|
257
258
|
artworkRendering = null,
|
|
258
259
|
gridWrap = false,
|