react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.21
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 +35 -21
- package/android/build.gradle +35 -12
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +389 -164
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +24 -5
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
- package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
- package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
- package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +56 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
- package/android/src/main/res/values/strings.xml +25 -5
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
- package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
- package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +230 -205
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +264 -106
- package/ios/Browser/SearchDrillIn.swift +7 -7
- package/ios/Browser/SectionPresentation.swift +64 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +58 -0
- package/ios/Browser/TabBarEntries.swift +9 -3
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +227 -85
- package/ios/CarPlay/CarPlayImageLoader.swift +42 -20
- package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
- package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
- package/ios/Extension/Section+Copying.swift +29 -0
- package/ios/Extension/String+IcyCharset.swift +21 -0
- package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
- package/ios/Extension/Track+Copying.swift +8 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +253 -100
- package/ios/Model/NitroTypeStubs.swift +125 -23
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/Option/SessionCategories.swift +3 -2
- package/ios/Player/MediaIntentCriteria.swift +17 -9
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +54 -6
- package/ios/Player/PlaybackCoordinator.swift +13 -6
- package/ios/Player/PlaybackErrorHandler.swift +4 -4
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +46 -13
- package/ios/Player/RetryManager.swift +80 -18
- package/ios/Player/SeekCompletionHandler.swift +3 -1
- package/ios/Player/ShuffleOrder.swift +33 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +62 -24
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +150 -1
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
- package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/QueueManagerTests.swift +122 -10
- package/ios/Tests/RemoteCommandControllerTests.swift +172 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +90 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +127 -0
- package/ios/Tests/Support/TestTypes.swift +8 -1
- package/ios/Tests/TabBarEntriesTests.swift +19 -19
- package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackPlayer.swift +63 -35
- package/ios/TrackPlayerCallbacks.swift +5 -38
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/ios/Util/LRUCache.swift +4 -1
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/AudioBrowser.js +8 -8
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser-config.js +12 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +11 -3
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +14 -11
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +3 -2
- 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/features/playback/state.js +3 -4
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +18 -9
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +23 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +15 -4
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +13 -6
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +200 -75
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
- package/lib/commonjs/web/TrackPlayer/Player.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/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +175 -100
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/SearchManager.js +18 -23
- package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
- package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
- package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +2 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser-config.js +12 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +11 -3
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +14 -11
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +3 -2
- 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/features/playback/state.js +3 -4
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +17 -8
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +19 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +15 -4
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +13 -6
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +28 -14
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +200 -75
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
- package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/module/web/TrackPlayer/Player.js +254 -39
- package/lib/module/web/TrackPlayer/Player.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/module/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +175 -100
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/SearchManager.js +18 -23
- package/lib/module/web/browser/SearchManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/http/RequestConfigBuilder.js +110 -27
- package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/module/web/http/followMediaRedirect.js +215 -0
- package/lib/module/web/http/followMediaRedirect.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +10 -2
- package/lib/typescript/src/features/browser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +14 -11
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +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/features/playback/state.d.ts +3 -4
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +108 -56
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +7 -4
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/types/browser-native.d.ts +3 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +380 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +82 -57
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
- 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 +16 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +7 -10
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +45 -25
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +36 -11
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +15 -12
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +57 -49
- package/nitrogen/generated/android/c++/JSection.hpp +116 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +25 -46
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -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/HybridAudioBrowserSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +8 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +15 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -12
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +244 -133
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +31 -16
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +33 -18
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -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/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
- package/nitrogen/generated/ios/swift/Section.swift +82 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +28 -63
- package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
- package/nitrogen/generated/shared/c++/Section.hpp +102 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +18 -30
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
- package/package.json +24 -11
- package/src/AudioBrowser.ts +2 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +12 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +11 -3
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +14 -11
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +302 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +3 -2
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +3 -4
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +1 -6
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +126 -76
- package/src/features/player/setup.test.ts +24 -7
- package/src/features/player/setup.ts +22 -10
- package/src/features/player/validateOptions.ts +21 -0
- package/src/features/queue/activeTrack.ts +15 -4
- package/src/features/queue/queue.ts +13 -6
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +277 -0
- package/src/public-surface.test.ts +147 -0
- package/src/specs/audio-browser.nitro.ts +5 -4
- package/src/style-resolution.test.ts +210 -0
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/types/browser-native.ts +7 -3
- package/src/types/browser-nodes.ts +396 -134
- package/src/types/browser.ts +85 -59
- package/src/utils/LazyNativeEmitter.ts +28 -15
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
- package/src/web/NativeAudioBrowser.ts +239 -82
- package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
- package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
- package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
- package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
- package/src/web/TrackPlayer/Player.load.test.ts +137 -1
- package/src/web/TrackPlayer/Player.ts +284 -37
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
- package/src/web/__tests__/lifecycle.test.ts +345 -0
- package/src/web/__tests__/lifecycleHarness.ts +351 -0
- package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
- package/src/web/__tests__/loadSupersession.test.ts +175 -0
- package/src/web/__tests__/retry.test.ts +169 -0
- package/src/web/browser/BrowserManager.ts +209 -111
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/SearchManager.test.ts +75 -0
- package/src/web/browser/SearchManager.ts +25 -28
- package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
- package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
- package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
- package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/http/RequestConfigBuilder.test.ts +241 -1
- package/src/web/http/RequestConfigBuilder.ts +140 -33
- package/src/web/http/followMediaRedirect.test.ts +289 -0
- package/src/web/http/followMediaRedirect.ts +253 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- 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 +247 -0
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/ios/Model/MediaURL.swift +0 -67
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package com.audiobrowser.model
|
|
2
|
+
|
|
3
|
+
import java.nio.charset.Charset
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class IcyTitleTest {
|
|
8
|
+
/** What media3's ISO-8859-1 fallback yields for [text] sent as [charset]. */
|
|
9
|
+
private fun latin1Shaped(text: String, charset: String) =
|
|
10
|
+
String(text.toByteArray(Charset.forName(charset)), Charsets.ISO_8859_1)
|
|
11
|
+
|
|
12
|
+
@Test
|
|
13
|
+
fun `re-decodes a legacy code page title`() {
|
|
14
|
+
val title = latin1Shaped("Артист - Песня", "windows-1251")
|
|
15
|
+
assertEquals("Артист - Песня", IcyTitle.redecode(title, "windows-1251"))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun `re-decodes thai`() {
|
|
20
|
+
val title = latin1Shaped("ศิลปิน - เพลง", "windows-874")
|
|
21
|
+
assertEquals("ศิลปิน - เพลง", IcyTitle.redecode(title, "windows-874"))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Test
|
|
25
|
+
fun `re-decodes a title the station utf-8 encoded from legacy bytes`() {
|
|
26
|
+
// media3 decodes the valid UTF-8 to the same Latin-1-shaped string the fallback would give.
|
|
27
|
+
val doubleEncoded = latin1Shaped("Artist - เพลง", "windows-874").toByteArray(Charsets.UTF_8)
|
|
28
|
+
assertEquals(
|
|
29
|
+
"Artist - เพลง",
|
|
30
|
+
IcyTitle.redecode(String(doubleEncoded, Charsets.UTF_8), "windows-874"),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
fun `leaves a title already in another script`() {
|
|
36
|
+
assertEquals("Песня", IcyTitle.redecode("Песня", "windows-1251"))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
fun `leaves ascii`() {
|
|
41
|
+
assertEquals("Plain Title", IcyTitle.redecode("Plain Title", "windows-1251"))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun `ignores a charset the runtime cannot decode with`() {
|
|
46
|
+
val title = latin1Shaped("Песня", "windows-1251")
|
|
47
|
+
assertEquals(title, IcyTitle.redecode(title, "no-such-charset"))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
package com.audiobrowser.player
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.media3.common.MediaItem
|
|
5
|
+
import androidx.media3.common.MediaMetadata
|
|
6
|
+
import androidx.media3.session.MediaLibraryService
|
|
7
|
+
import androidx.media3.session.MediaSession
|
|
8
|
+
import com.audiobrowser.NavigationErrorException
|
|
9
|
+
import com.audiobrowser.TestFixtures
|
|
10
|
+
import com.audiobrowser.browser.HttpStatusException
|
|
11
|
+
import com.audiobrowser.browser.NetworkException
|
|
12
|
+
import com.audiobrowser.defaultFormattedError
|
|
13
|
+
import com.audiobrowser.formattedOrDefault
|
|
14
|
+
import com.audiobrowser.navigationErrorFor
|
|
15
|
+
import com.audiobrowser.util.BrowserPathHelper
|
|
16
|
+
import com.margelo.nitro.audiobrowser.FormattedNavigationError
|
|
17
|
+
import com.margelo.nitro.audiobrowser.NavigationError
|
|
18
|
+
import com.margelo.nitro.audiobrowser.NavigationErrorType
|
|
19
|
+
import kotlin.coroutines.Continuation
|
|
20
|
+
import kotlin.coroutines.resume
|
|
21
|
+
import kotlin.coroutines.suspendCoroutine
|
|
22
|
+
import kotlinx.coroutines.test.runTest
|
|
23
|
+
import org.junit.Assert.assertEquals
|
|
24
|
+
import org.junit.Assert.assertFalse
|
|
25
|
+
import org.junit.Assert.assertNotEquals
|
|
26
|
+
import org.junit.Assert.assertNotNull
|
|
27
|
+
import org.junit.Assert.assertNull
|
|
28
|
+
import org.junit.Assert.assertSame
|
|
29
|
+
import org.junit.Assert.assertTrue
|
|
30
|
+
import org.junit.Test
|
|
31
|
+
import org.junit.runner.RunWith
|
|
32
|
+
import org.robolectric.RobolectricTestRunner
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Tests the tiles an Android Auto browse level serves in place of a list: the empty tile of a page
|
|
36
|
+
* that resolved with zero children, and the offline and failure tiles, all worded by the app's
|
|
37
|
+
* `formatNavigationError` (ADR 0001). MediaSessionCallback itself needs a live MediaLibrarySession,
|
|
38
|
+
* so the pieces under test are the ones carved out of it: [browseLevelItems] (the empty-branch
|
|
39
|
+
* decision), the tile builders, [BrowserPathHelper.isDeadEndPath] (the drill-in guard),
|
|
40
|
+
* [announcedSearchCount] (what onSearch tells the controller to ask for), [FailedSearchSlot] (a
|
|
41
|
+
* failed search, which must not read as an empty one), [OfflineSearchRegistry] (the searches
|
|
42
|
+
* connectivity returning re-runs), [navigationErrorFor] (the one exception mapping browse, search
|
|
43
|
+
* and the JS-facing `search` rejection share) and [formattedOrDefault] (the formatter hop's
|
|
44
|
+
* fallback rule, which sits off the JNI-backed Promise edge for exactly this reason).
|
|
45
|
+
*/
|
|
46
|
+
@RunWith(RobolectricTestRunner::class)
|
|
47
|
+
class BrowseTilesTest {
|
|
48
|
+
|
|
49
|
+
private fun controller(packageName: String, uid: Int): MediaSession.ControllerInfo =
|
|
50
|
+
MediaSession.ControllerInfo.createTestOnlyControllerInfo(
|
|
51
|
+
packageName,
|
|
52
|
+
/* pid= */ 0,
|
|
53
|
+
uid,
|
|
54
|
+
/* libraryVersion= */ 2,
|
|
55
|
+
/* interfaceVersion= */ 2,
|
|
56
|
+
/* trusted= */ true,
|
|
57
|
+
Bundle.EMPTY,
|
|
58
|
+
/* isPackageNameVerified= */ true,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
private fun track(title: String): MediaItem =
|
|
62
|
+
MediaItem.Builder()
|
|
63
|
+
.setMediaId("/station/$title")
|
|
64
|
+
.setMediaMetadata(MediaMetadata.Builder().setTitle(title).build())
|
|
65
|
+
.build()
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Stands in for an app's `formatNavigationError`: different copy per error and per path, the
|
|
69
|
+
* point of ADR 0001. The search branches match the prefix the library builds car search paths
|
|
70
|
+
* from.
|
|
71
|
+
*/
|
|
72
|
+
private fun appCopy(error: NavigationError, path: String): FormattedNavigationError {
|
|
73
|
+
val isSearch = path.startsWith(BrowserPathHelper.SEARCH_PATH_PREFIX)
|
|
74
|
+
return when {
|
|
75
|
+
error.code == NavigationErrorType.NETWORK_ERROR && isSearch ->
|
|
76
|
+
FormattedNavigationError("Search needs a connection", "Reconnect and try again.")
|
|
77
|
+
error.code == NavigationErrorType.NETWORK_ERROR ->
|
|
78
|
+
FormattedNavigationError("You're offline", "Check your connection.")
|
|
79
|
+
error.code != NavigationErrorType.EMPTY_CONTENT ->
|
|
80
|
+
FormattedNavigationError("We couldn't load that", "Try again in a moment.")
|
|
81
|
+
path == "/favorites" ->
|
|
82
|
+
FormattedNavigationError("No favorites yet", "Tap the heart on a station to add it here.")
|
|
83
|
+
isSearch -> FormattedNavigationError("No results found", "Try a different search.")
|
|
84
|
+
else -> FormattedNavigationError("Nothing here", null)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
fun `an empty favorites page serves one tile carrying the app's copy for that path`() = runTest {
|
|
90
|
+
val items = browseLevelItems(emptyList()) { appCopy(emptyContentError(), "/favorites") }
|
|
91
|
+
|
|
92
|
+
assertEquals(1, items.size)
|
|
93
|
+
val tile = items.single()
|
|
94
|
+
assertEquals(BrowserPathHelper.EMPTY_PATH, tile.mediaId)
|
|
95
|
+
assertEquals("No favorites yet", tile.mediaMetadata.title)
|
|
96
|
+
assertEquals("Tap the heart on a station to add it here.", tile.mediaMetadata.subtitle)
|
|
97
|
+
assertFalse(tile.mediaMetadata.isBrowsable!!)
|
|
98
|
+
assertFalse(tile.mediaMetadata.isPlayable!!)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Test
|
|
102
|
+
fun `with no formatter the tile carries the neutral default`() = runTest {
|
|
103
|
+
val default = defaultFormattedError(emptyContentError())
|
|
104
|
+
assertEquals("Nothing here", default.title)
|
|
105
|
+
assertNull(default.message)
|
|
106
|
+
|
|
107
|
+
val tile = browseLevelItems(emptyList()) { default }.single()
|
|
108
|
+
assertEquals("Nothing here", tile.mediaMetadata.title)
|
|
109
|
+
assertEquals("", tile.mediaMetadata.subtitle)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
fun `a multi-line message collapses to the single-line subtitle`() = runTest {
|
|
114
|
+
val tile =
|
|
115
|
+
browseLevelItems(emptyList()) { FormattedNavigationError("Empty", "One\nTwo") }.single()
|
|
116
|
+
assertEquals("One Two", tile.mediaMetadata.subtitle)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Test
|
|
120
|
+
fun `a page with children is served unchanged and never asks for copy`() = runTest {
|
|
121
|
+
val children = listOf(track("a"), track("b"))
|
|
122
|
+
|
|
123
|
+
val items = browseLevelItems(children) { error("formatter must not run for a full page") }
|
|
124
|
+
|
|
125
|
+
assertSame(children, items)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
fun `a drill-in on the empty tile dead-ends`() {
|
|
130
|
+
assertTrue(BrowserPathHelper.isDeadEndPath(BrowserPathHelper.EMPTY_PATH))
|
|
131
|
+
assertFalse(BrowserPathHelper.isDeadEndPath("/favorites"))
|
|
132
|
+
// The gate tile re-serves its message instead of dead-ending.
|
|
133
|
+
assertFalse(BrowserPathHelper.isDeadEndPath(BrowserPathHelper.GATE_PATH))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test
|
|
137
|
+
fun `the formatter hop keeps the app's answer`() = runTest {
|
|
138
|
+
val custom = FormattedNavigationError("No favorites yet", "Tap the heart.")
|
|
139
|
+
assertEquals(
|
|
140
|
+
custom,
|
|
141
|
+
formattedOrDefault(backgroundScope, FormattedNavigationError("Nothing here", null)) { custom },
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Test
|
|
146
|
+
fun `the formatter hop falls back to the default on null and on a throw`() = runTest {
|
|
147
|
+
val default = FormattedNavigationError("Nothing here", null)
|
|
148
|
+
|
|
149
|
+
assertEquals(default, formattedOrDefault(backgroundScope, default) { null })
|
|
150
|
+
assertEquals(
|
|
151
|
+
default,
|
|
152
|
+
formattedOrDefault(backgroundScope, default) { throw IllegalStateException("boom") },
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Test
|
|
157
|
+
fun `an empty search serves one tile carrying the app's copy for the search path`() = runTest {
|
|
158
|
+
val searchPath = BrowserPathHelper.createSearchPath("jazz")
|
|
159
|
+
|
|
160
|
+
val items = browseLevelItems(emptyList()) { appCopy(emptyContentError(), searchPath) }
|
|
161
|
+
|
|
162
|
+
assertEquals(1, items.size)
|
|
163
|
+
val tile = items.single()
|
|
164
|
+
assertEquals(BrowserPathHelper.EMPTY_PATH, tile.mediaId)
|
|
165
|
+
assertEquals("No results found", tile.mediaMetadata.title)
|
|
166
|
+
assertEquals("Try a different search.", tile.mediaMetadata.subtitle)
|
|
167
|
+
assertFalse(tile.mediaMetadata.isBrowsable!!)
|
|
168
|
+
assertFalse(tile.mediaMetadata.isPlayable!!)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Test
|
|
172
|
+
fun `a search with hits is served unchanged and never asks for copy`() = runTest {
|
|
173
|
+
val hits = listOf(track("Jazz FM"), track("Jazz24"))
|
|
174
|
+
|
|
175
|
+
val items = browseLevelItems(hits) { error("formatter must not run for a search with hits") }
|
|
176
|
+
|
|
177
|
+
assertSame(hits, items)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Test
|
|
181
|
+
fun `a search whose every hit is disabled counts as empty and announces the tile`() {
|
|
182
|
+
val disabledOnly =
|
|
183
|
+
listOf(
|
|
184
|
+
TestFixtures.section(
|
|
185
|
+
children =
|
|
186
|
+
arrayOf(
|
|
187
|
+
TestFixtures.track(title = "Gone", disabled = true),
|
|
188
|
+
TestFixtures.track(title = "Also gone", disabled = true),
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// Nothing servable, so the announced count is the tile's — a controller told zero would never
|
|
194
|
+
// come back for it.
|
|
195
|
+
assertEquals(1, announcedSearchCount(disabledOnly))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@Test
|
|
199
|
+
fun `a search announces only its servable hits`() {
|
|
200
|
+
val mixed =
|
|
201
|
+
listOf(
|
|
202
|
+
TestFixtures.section(
|
|
203
|
+
children =
|
|
204
|
+
arrayOf(
|
|
205
|
+
TestFixtures.track(title = "Playable"),
|
|
206
|
+
TestFixtures.track(title = "Gone", disabled = true),
|
|
207
|
+
)
|
|
208
|
+
),
|
|
209
|
+
TestFixtures.section(children = arrayOf(TestFixtures.track(title = "Another"))),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
assertEquals(2, announcedSearchCount(mixed))
|
|
213
|
+
assertEquals(1, announcedSearchCount(null))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Test
|
|
217
|
+
fun `an offline search serves the offline tile with the app's copy for the search path`() {
|
|
218
|
+
val searchPath = BrowserPathHelper.createSearchPath("jazz")
|
|
219
|
+
val slot = FailedSearchSlot()
|
|
220
|
+
|
|
221
|
+
val tile = createOfflineMediaItem(appCopy(offlineError(), searchPath))
|
|
222
|
+
|
|
223
|
+
assertEquals(BrowserPathHelper.OFFLINE_PATH, tile.mediaId)
|
|
224
|
+
assertEquals("Search needs a connection", tile.mediaMetadata.title)
|
|
225
|
+
assertEquals("Reconnect and try again.", tile.mediaMetadata.subtitle)
|
|
226
|
+
assertFalse(tile.mediaMetadata.isBrowsable!!)
|
|
227
|
+
assertFalse(tile.mediaMetadata.isPlayable!!)
|
|
228
|
+
assertTrue(BrowserPathHelper.isDeadEndPath(BrowserPathHelper.OFFLINE_PATH))
|
|
229
|
+
// Offline is not a search that failed: connectivity returning is what serves results again,
|
|
230
|
+
// so the slot stays empty and a later re-query of the query is not a failure.
|
|
231
|
+
assertNull(slot.errorFor("jazz"))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@Test
|
|
235
|
+
fun `the offline tile is worded per path, and title-only by default`() {
|
|
236
|
+
val browseTile = createOfflineMediaItem(appCopy(offlineError(), "/favorites"))
|
|
237
|
+
assertEquals("You're offline", browseTile.mediaMetadata.title)
|
|
238
|
+
assertEquals("Check your connection.", browseTile.mediaMetadata.subtitle)
|
|
239
|
+
|
|
240
|
+
val default = defaultFormattedError(offlineError())
|
|
241
|
+
assertEquals("Network Error", default.title)
|
|
242
|
+
assertNull(default.message)
|
|
243
|
+
assertEquals("", createOfflineMediaItem(default).mediaMetadata.subtitle)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@Test
|
|
247
|
+
fun `the browse-error tile is worded by the error its exception mapped to`() {
|
|
248
|
+
val failure = navigationErrorFor(HttpStatusException(503, "Service Unavailable"))
|
|
249
|
+
|
|
250
|
+
val appTile = createBrowseErrorMediaItem(appCopy(failure, "/favorites"))
|
|
251
|
+
assertEquals(BrowserPathHelper.ERROR_PATH, appTile.mediaId)
|
|
252
|
+
assertEquals("We couldn't load that", appTile.mediaMetadata.title)
|
|
253
|
+
assertEquals("Try again in a moment.", appTile.mediaMetadata.subtitle)
|
|
254
|
+
|
|
255
|
+
// Without a formatter, the status text the in-app error already shows.
|
|
256
|
+
val defaultTile = createBrowseErrorMediaItem(defaultFormattedError(failure))
|
|
257
|
+
assertEquals("Service Unavailable", defaultTile.mediaMetadata.title)
|
|
258
|
+
|
|
259
|
+
// A re-read of the tile has no exception left to map, so it stands on the generic failure.
|
|
260
|
+
assertEquals("Error", defaultFormattedError(browseFailureError()).title)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@Test
|
|
264
|
+
fun `a recorded search failure carries its error for that query only, until a success clears it`() {
|
|
265
|
+
val slot = FailedSearchSlot()
|
|
266
|
+
val failure = navigationErrorFor(HttpStatusException(500, "Internal Server Error"))
|
|
267
|
+
assertNull(slot.errorFor("jazz"))
|
|
268
|
+
|
|
269
|
+
slot.record("jazz", failure)
|
|
270
|
+
assertEquals(failure, slot.errorFor("jazz"))
|
|
271
|
+
assertNull(slot.errorFor("blues"))
|
|
272
|
+
|
|
273
|
+
slot.clear()
|
|
274
|
+
assertNull(slot.errorFor("jazz"))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@Test
|
|
278
|
+
fun `a failed search rejects with the navigation error a failed browse would raise`() {
|
|
279
|
+
// Nitro's Promise is JNI-backed (its constructor calls initHybrid), so the JS-facing
|
|
280
|
+
// AudioBrowser.search cannot be built here; what it rejects with is this.
|
|
281
|
+
val rejection =
|
|
282
|
+
NavigationErrorException(navigationErrorFor(HttpStatusException(503, "Service Unavailable")))
|
|
283
|
+
assertEquals(NavigationErrorType.HTTP_ERROR, rejection.error.code)
|
|
284
|
+
assertEquals(503.0, rejection.error.statusCode!!, 0.0)
|
|
285
|
+
assertEquals(false, rejection.error.statusCodeSuccess)
|
|
286
|
+
assertEquals("Service Unavailable", rejection.message)
|
|
287
|
+
|
|
288
|
+
assertEquals(
|
|
289
|
+
NavigationErrorType.NETWORK_ERROR,
|
|
290
|
+
navigationErrorFor(NetworkException("Network request failed")).code,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
val unknown = navigationErrorFor(IllegalStateException())
|
|
294
|
+
assertEquals(NavigationErrorType.UNKNOWN_ERROR, unknown.code)
|
|
295
|
+
assertEquals("An unexpected error occurred", unknown.message)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@Test
|
|
299
|
+
fun `an offline search is remembered per controller, latest query only`() {
|
|
300
|
+
val registry = OfflineSearchRegistry()
|
|
301
|
+
val auto = controller("com.google.android.projection.gearhead", uid = 10)
|
|
302
|
+
val assistant = controller("com.google.android.googlequicksearchbox", uid = 11)
|
|
303
|
+
|
|
304
|
+
registry.record(auto, "jazz", null)
|
|
305
|
+
registry.record(assistant, "blues", null)
|
|
306
|
+
registry.record(auto, "jazz fm", null)
|
|
307
|
+
|
|
308
|
+
val pending = registry.drain()
|
|
309
|
+
assertEquals(2, pending.size)
|
|
310
|
+
assertEquals("jazz fm", pending.single { it.browser == auto }.query)
|
|
311
|
+
assertEquals("blues", pending.single { it.browser == assistant }.query)
|
|
312
|
+
// Drained, so a second connectivity change does not re-run the same searches again.
|
|
313
|
+
assertTrue(registry.drain().isEmpty())
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@Test
|
|
317
|
+
fun `a re-run search is announced by its hits, an offline one by its tile`() {
|
|
318
|
+
val hits =
|
|
319
|
+
listOf(
|
|
320
|
+
TestFixtures.section(
|
|
321
|
+
children =
|
|
322
|
+
arrayOf(TestFixtures.track(title = "Jazz FM"), TestFixtures.track(title = "Jazz24"))
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
// The re-run goes through the same count as the first, online search, so what a controller is
|
|
327
|
+
// told to fetch is what onGetSearchResult then serves from the cache that search filled.
|
|
328
|
+
assertEquals(2, announcedSearchCount(hits))
|
|
329
|
+
// While offline there is nothing to count: the one tile is announced instead.
|
|
330
|
+
assertEquals(1, announcedSearchCount(null))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
@Test
|
|
334
|
+
fun `a disconnected controller's offline search is forgotten`() {
|
|
335
|
+
val registry = OfflineSearchRegistry()
|
|
336
|
+
val auto = controller("com.google.android.projection.gearhead", uid = 10)
|
|
337
|
+
val assistant = controller("com.google.android.googlequicksearchbox", uid = 11)
|
|
338
|
+
registry.record(auto, "jazz", null)
|
|
339
|
+
registry.record(assistant, "blues", null)
|
|
340
|
+
|
|
341
|
+
registry.forget(auto)
|
|
342
|
+
|
|
343
|
+
assertEquals(listOf("blues"), registry.drain().map { it.query })
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@Test
|
|
347
|
+
fun `a remembered search keeps the params its controller asked with`() {
|
|
348
|
+
val registry = OfflineSearchRegistry()
|
|
349
|
+
val auto = controller("com.google.android.projection.gearhead", uid = 10)
|
|
350
|
+
val params =
|
|
351
|
+
MediaLibraryService.LibraryParams.Builder()
|
|
352
|
+
.setExtras(Bundle().apply { putInt("page", 3) })
|
|
353
|
+
.build()
|
|
354
|
+
|
|
355
|
+
registry.record(auto, "jazz", params)
|
|
356
|
+
|
|
357
|
+
assertEquals(3, registry.drain().single().params?.extras?.getInt("page"))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@Test
|
|
361
|
+
fun `the failure sentinel is distinct from the empty one and dead-ends too`() {
|
|
362
|
+
assertNotEquals(BrowserPathHelper.EMPTY_PATH, BrowserPathHelper.ERROR_PATH)
|
|
363
|
+
assertTrue(BrowserPathHelper.isDeadEndPath(BrowserPathHelper.ERROR_PATH))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@Test
|
|
367
|
+
fun `a formatter parked in a non-cancellable wait falls back without hanging`() = runTest {
|
|
368
|
+
val default = FormattedNavigationError("Nothing here", null)
|
|
369
|
+
var parked: Continuation<FormattedNavigationError?>? = null
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
// Nitro's Promise.await parks exactly like this — suspendCoroutine, so cancelling the
|
|
373
|
+
// coroutine never unparks it. The hop must be abandoned, not waited on.
|
|
374
|
+
val formatted =
|
|
375
|
+
formattedOrDefault(backgroundScope, default) {
|
|
376
|
+
suspendCoroutine { continuation -> parked = continuation }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
assertNotNull(parked)
|
|
380
|
+
assertEquals(default, formatted)
|
|
381
|
+
} finally {
|
|
382
|
+
parked?.resume(null)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
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
|
|
6
|
-
import com.margelo.nitro.audiobrowser.
|
|
4
|
+
import com.margelo.nitro.audiobrowser.RemoteButton
|
|
5
|
+
import com.margelo.nitro.audiobrowser.RemoteButtonLayout
|
|
7
6
|
import org.junit.Assert.assertEquals
|
|
8
7
|
import org.junit.Assert.assertFalse
|
|
9
8
|
import org.junit.Assert.assertTrue
|
|
@@ -12,8 +11,7 @@ import org.junit.Test
|
|
|
12
11
|
/**
|
|
13
12
|
* Truth tables for the pure Capability decision core: [PlayerCapabilities.isEnabled] (one rule for
|
|
14
13
|
* "available by default, only false disables; favorite is opt-in; play/pause is one control") and
|
|
15
|
-
* [
|
|
16
|
-
* and the notification player commands consume).
|
|
14
|
+
* [deriveButtonSlots] (the single slot derivation every Media3 assembly consumes).
|
|
17
15
|
*/
|
|
18
16
|
class CapabilityControlsTest {
|
|
19
17
|
|
|
@@ -37,123 +35,168 @@ class CapabilityControlsTest {
|
|
|
37
35
|
skipToPrevious = skipToPrevious,
|
|
38
36
|
jumpForward = jumpForward,
|
|
39
37
|
jumpBackward = jumpBackward,
|
|
40
|
-
favorite = favorite
|
|
38
|
+
favorite = favorite,
|
|
41
39
|
shuffleMode = null,
|
|
42
40
|
repeatMode = null,
|
|
43
41
|
playbackRate = null,
|
|
44
42
|
)
|
|
45
43
|
|
|
44
|
+
private fun layout(
|
|
45
|
+
back: RemoteButton? = null,
|
|
46
|
+
forward: RemoteButton? = null,
|
|
47
|
+
overflow: Array<RemoteButton> = emptyArray(),
|
|
48
|
+
) = RemoteButtonLayout(back = back, forward = forward, overflow = overflow)
|
|
49
|
+
|
|
46
50
|
private fun slots(
|
|
47
51
|
capabilities: PlayerCapabilities,
|
|
48
|
-
layout:
|
|
49
|
-
): List<Pair<
|
|
50
|
-
|
|
52
|
+
layout: RemoteButtonLayout? = null,
|
|
53
|
+
): List<Pair<RemoteButton, ButtonSlot>> =
|
|
54
|
+
deriveButtonSlots(capabilities, layout).map { it.button to it.slot }
|
|
51
55
|
|
|
52
56
|
// MARK: isEnabled
|
|
53
57
|
|
|
54
58
|
@Test
|
|
55
59
|
fun `everything but favorite is enabled by default`() {
|
|
56
60
|
val defaults = capabilities()
|
|
57
|
-
for (control in
|
|
58
|
-
assertEquals("$control", control !=
|
|
61
|
+
for (control in Capability.entries) {
|
|
62
|
+
assertEquals("$control", control != Capability.FAVORITE, defaults.isEnabled(control))
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
@Test
|
|
63
67
|
fun `only an explicit false disables a control`() {
|
|
64
68
|
val caps = capabilities(skipToNext = false, jumpBackward = false)
|
|
65
|
-
assertFalse(caps.isEnabled(
|
|
66
|
-
assertFalse(caps.isEnabled(
|
|
67
|
-
assertTrue(caps.isEnabled(
|
|
68
|
-
assertTrue(caps.isEnabled(
|
|
69
|
+
assertFalse(caps.isEnabled(Capability.SKIP_TO_NEXT))
|
|
70
|
+
assertFalse(caps.isEnabled(Capability.JUMP_BACKWARD))
|
|
71
|
+
assertTrue(caps.isEnabled(Capability.SKIP_TO_PREVIOUS))
|
|
72
|
+
assertTrue(caps.isEnabled(Capability.JUMP_FORWARD))
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
@Test
|
|
72
76
|
fun `play pause is one control - it survives a single half being disabled`() {
|
|
73
|
-
assertTrue(capabilities(play = false).isEnabled(
|
|
74
|
-
assertTrue(capabilities(pause = false).isEnabled(
|
|
75
|
-
assertFalse(capabilities(play = false, pause = false).isEnabled(
|
|
77
|
+
assertTrue(capabilities(play = false).isEnabled(Capability.PLAY_PAUSE))
|
|
78
|
+
assertTrue(capabilities(pause = false).isEnabled(Capability.PLAY_PAUSE))
|
|
79
|
+
assertFalse(capabilities(play = false, pause = false).isEnabled(Capability.PLAY_PAUSE))
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
@Test
|
|
79
83
|
fun `favorite is opt-in`() {
|
|
80
|
-
assertFalse(capabilities().isEnabled(
|
|
81
|
-
assertFalse(capabilities(favorite = false).isEnabled(
|
|
82
|
-
assertTrue(capabilities(favorite = true).isEnabled(
|
|
84
|
+
assertFalse(capabilities().isEnabled(Capability.FAVORITE))
|
|
85
|
+
assertFalse(capabilities(favorite = false).isEnabled(Capability.FAVORITE))
|
|
86
|
+
assertTrue(capabilities(favorite = true).isEnabled(Capability.FAVORITE))
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
// MARK:
|
|
89
|
+
// MARK: deriveButtonSlots — capability defaults
|
|
86
90
|
|
|
87
91
|
@Test
|
|
88
|
-
fun `default layout puts skip on the primary
|
|
92
|
+
fun `default layout puts skip on the primary positions, jump and favorite in overflow`() {
|
|
89
93
|
val derived = slots(capabilities(favorite = true))
|
|
90
94
|
assertEquals(
|
|
91
95
|
listOf(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
RemoteButton.SKIP_TO_PREVIOUS to ButtonSlot.BACK,
|
|
97
|
+
RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD,
|
|
98
|
+
RemoteButton.JUMP_BACKWARD to ButtonSlot.OVERFLOW,
|
|
99
|
+
RemoteButton.JUMP_FORWARD to ButtonSlot.OVERFLOW,
|
|
100
|
+
RemoteButton.FAVORITE to ButtonSlot.OVERFLOW,
|
|
97
101
|
),
|
|
98
102
|
derived,
|
|
99
103
|
)
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
@Test
|
|
103
|
-
fun `jump falls back to
|
|
107
|
+
fun `jump falls back to a primary position when skip is disabled`() {
|
|
104
108
|
val derived = slots(capabilities(skipToPrevious = false, skipToNext = false))
|
|
105
109
|
assertEquals(
|
|
106
110
|
listOf(
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
RemoteButton.JUMP_BACKWARD to ButtonSlot.BACK,
|
|
112
|
+
RemoteButton.JUMP_FORWARD to ButtonSlot.FORWARD,
|
|
109
113
|
),
|
|
110
114
|
derived,
|
|
111
115
|
)
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
@Test
|
|
115
|
-
fun `a fully disabled side leaves its
|
|
119
|
+
fun `a fully disabled side leaves its position empty`() {
|
|
116
120
|
val derived =
|
|
117
121
|
slots(capabilities(skipToPrevious = false, jumpBackward = false, jumpForward = false))
|
|
118
|
-
assertEquals(listOf(
|
|
122
|
+
assertEquals(listOf(RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD), derived)
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
// MARK:
|
|
125
|
+
// MARK: deriveButtonSlots — explicit layout
|
|
122
126
|
|
|
123
127
|
@Test
|
|
124
|
-
fun `explicit layout maps every
|
|
125
|
-
val
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
fun `an explicit layout maps every field and keeps overflow order`() {
|
|
129
|
+
val derived =
|
|
130
|
+
slots(
|
|
131
|
+
capabilities(),
|
|
132
|
+
layout(
|
|
133
|
+
back = RemoteButton.JUMP_BACKWARD,
|
|
134
|
+
forward = RemoteButton.JUMP_FORWARD,
|
|
135
|
+
overflow = arrayOf(RemoteButton.SKIP_TO_PREVIOUS, RemoteButton.SKIP_TO_NEXT),
|
|
136
|
+
),
|
|
132
137
|
)
|
|
133
|
-
val derived = slots(capabilities(), layout)
|
|
134
138
|
assertEquals(
|
|
135
139
|
listOf(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
RemoteButton.JUMP_BACKWARD to ButtonSlot.BACK,
|
|
141
|
+
RemoteButton.JUMP_FORWARD to ButtonSlot.FORWARD,
|
|
142
|
+
RemoteButton.SKIP_TO_PREVIOUS to ButtonSlot.OVERFLOW,
|
|
143
|
+
RemoteButton.SKIP_TO_NEXT to ButtonSlot.OVERFLOW,
|
|
140
144
|
),
|
|
141
145
|
derived,
|
|
142
146
|
)
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
@Test
|
|
146
|
-
fun `
|
|
147
|
-
val layout =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
fun `a null field leaves that position empty`() {
|
|
151
|
+
val derived = slots(capabilities(), layout(back = null, forward = RemoteButton.SKIP_TO_NEXT))
|
|
152
|
+
assertEquals(listOf(RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD), derived)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
fun `a layout is never merged with the capability defaults`() {
|
|
157
|
+
// Nitro collapses an omitted and a null enum field to the same Kotlin null, so a per-field
|
|
158
|
+
// merge is not expressible: an empty position must stay empty rather than falling back to the
|
|
159
|
+
// capability default. Every capability here is enabled, so a merge would add skip and jump.
|
|
160
|
+
val derived =
|
|
161
|
+
slots(capabilities(favorite = true), layout(overflow = arrayOf(RemoteButton.FAVORITE)))
|
|
162
|
+
assertEquals(listOf(RemoteButton.FAVORITE to ButtonSlot.OVERFLOW), derived)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Test
|
|
166
|
+
fun `a disabled entry drops out without promoting anything into its position`() {
|
|
167
|
+
// favorite unset (= disabled), skipToPrevious explicitly off. skip-to-next must stay on
|
|
168
|
+
// FORWARD rather than sliding into the vacated BACK position.
|
|
169
|
+
val derived =
|
|
170
|
+
slots(
|
|
171
|
+
capabilities(skipToPrevious = false),
|
|
172
|
+
layout(
|
|
173
|
+
back = RemoteButton.SKIP_TO_PREVIOUS,
|
|
174
|
+
forward = RemoteButton.SKIP_TO_NEXT,
|
|
175
|
+
overflow = arrayOf(RemoteButton.FAVORITE),
|
|
176
|
+
),
|
|
154
177
|
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
178
|
+
assertEquals(listOf(RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD), derived)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// MARK: sameAs
|
|
182
|
+
|
|
183
|
+
@Test
|
|
184
|
+
fun `sameAs compares overflow by content, not by array identity`() {
|
|
185
|
+
val a = layout(back = RemoteButton.JUMP_BACKWARD, overflow = arrayOf(RemoteButton.FAVORITE))
|
|
186
|
+
val b = layout(back = RemoteButton.JUMP_BACKWARD, overflow = arrayOf(RemoteButton.FAVORITE))
|
|
187
|
+
// Distinct Array instances — data-class `==` would call these different and republish the
|
|
188
|
+
// layout to Android Auto on every unrelated options update.
|
|
189
|
+
assertTrue(a.sameAs(b))
|
|
190
|
+
assertFalse(a.sameAs(layout(back = RemoteButton.JUMP_BACKWARD)))
|
|
191
|
+
assertFalse(
|
|
192
|
+
a.sameAs(layout(back = RemoteButton.JUMP_FORWARD, overflow = arrayOf(RemoteButton.FAVORITE)))
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Test
|
|
197
|
+
fun `sameAs treats null as its own layout`() {
|
|
198
|
+
assertTrue(null.sameAs(null))
|
|
199
|
+
assertFalse(layout(back = RemoteButton.SKIP_TO_NEXT).sameAs(null))
|
|
200
|
+
assertFalse(null.sameAs(layout(back = RemoteButton.SKIP_TO_NEXT)))
|
|
158
201
|
}
|
|
159
202
|
}
|