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
|
@@ -20,9 +20,11 @@ import com.audiobrowser.browser.CallbackException
|
|
|
20
20
|
import com.audiobrowser.browser.ContentNotFoundException
|
|
21
21
|
import com.audiobrowser.browser.HttpStatusException
|
|
22
22
|
import com.audiobrowser.browser.NetworkException
|
|
23
|
+
import com.audiobrowser.browser.flattenedChildren
|
|
23
24
|
import com.audiobrowser.browser.handleTrackLoad
|
|
24
25
|
import com.audiobrowser.browser.resolveMediaUrl
|
|
25
26
|
import com.audiobrowser.extension.NumberExt.Companion.toSeconds
|
|
27
|
+
import com.audiobrowser.extension.indexOfTappedTrack
|
|
26
28
|
import com.audiobrowser.model.PlayerSetupOptions
|
|
27
29
|
import com.audiobrowser.model.PlayerUpdateOptions
|
|
28
30
|
import com.audiobrowser.player.Player
|
|
@@ -35,6 +37,7 @@ import com.audiobrowser.util.SystemVolumeMonitor
|
|
|
35
37
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
36
38
|
import com.google.common.util.concurrent.ListenableFuture
|
|
37
39
|
import com.margelo.nitro.NitroModules
|
|
40
|
+
import com.margelo.nitro.audiobrowser.ArtworkRequestConfig
|
|
38
41
|
import com.margelo.nitro.audiobrowser.BatteryOptimizationStatus
|
|
39
42
|
import com.margelo.nitro.audiobrowser.BatteryOptimizationStatusChangedEvent
|
|
40
43
|
import com.margelo.nitro.audiobrowser.BatteryWarningPendingChangedEvent
|
|
@@ -50,6 +53,7 @@ import com.margelo.nitro.audiobrowser.HybridAudioBrowserSpec
|
|
|
50
53
|
import com.margelo.nitro.audiobrowser.MediaRequestConfig
|
|
51
54
|
import com.margelo.nitro.audiobrowser.NativeBrowserConfiguration
|
|
52
55
|
import com.margelo.nitro.audiobrowser.NativeGateRequest
|
|
56
|
+
import com.margelo.nitro.audiobrowser.NativeRouteEntry
|
|
53
57
|
import com.margelo.nitro.audiobrowser.NativeSetupPlayerOptions
|
|
54
58
|
import com.margelo.nitro.audiobrowser.NativeUpdateOptions
|
|
55
59
|
import com.margelo.nitro.audiobrowser.NavigationError
|
|
@@ -82,31 +86,105 @@ import com.margelo.nitro.audiobrowser.SleepTimer
|
|
|
82
86
|
import com.margelo.nitro.audiobrowser.TimedMetadata
|
|
83
87
|
import com.margelo.nitro.audiobrowser.Track
|
|
84
88
|
import com.margelo.nitro.audiobrowser.TrackMetadata
|
|
89
|
+
import com.margelo.nitro.audiobrowser.TransformableRequestConfig
|
|
85
90
|
import com.margelo.nitro.core.Promise
|
|
86
91
|
import java.util.concurrent.TimeUnit
|
|
92
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
87
93
|
import kotlin.coroutines.resume
|
|
88
94
|
import kotlin.coroutines.resumeWithException
|
|
89
95
|
import kotlin.coroutines.suspendCoroutine
|
|
96
|
+
import kotlin.time.Duration.Companion.seconds
|
|
90
97
|
import kotlinx.coroutines.CancellationException
|
|
98
|
+
import kotlinx.coroutines.CoroutineScope
|
|
91
99
|
import kotlinx.coroutines.Job
|
|
92
100
|
import kotlinx.coroutines.MainScope
|
|
101
|
+
import kotlinx.coroutines.async
|
|
102
|
+
import kotlinx.coroutines.cancel
|
|
93
103
|
import kotlinx.coroutines.cancelChildren
|
|
94
|
-
import kotlinx.coroutines.delay
|
|
95
104
|
import kotlinx.coroutines.launch
|
|
96
105
|
import kotlinx.coroutines.runBlocking
|
|
106
|
+
import kotlinx.coroutines.withTimeoutOrNull
|
|
97
107
|
import timber.log.Timber
|
|
98
108
|
|
|
99
109
|
@Keep
|
|
100
110
|
@DoNotStrip
|
|
101
111
|
class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
102
112
|
|
|
103
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Nitro calls this only when JS calls `dispose()` explicitly — never on runtime teardown, which
|
|
115
|
+
* is what [AudioBrowserLifecycleModule] is for. Both routes land here.
|
|
116
|
+
*
|
|
117
|
+
* The Service outlives this object, so everything registered in `init` or on connect has to be
|
|
118
|
+
* undone. Left alone, the running [Player] goes on invoking callbacks into a dead JSI runtime,
|
|
119
|
+
* and every reload strands another ServiceConnection and lifecycle observer, each retaining a
|
|
120
|
+
* BrowserManager.
|
|
121
|
+
*
|
|
122
|
+
* Runs the teardown on main: the state below is written from main-thread coroutines
|
|
123
|
+
* ([onServiceConnected], [onServiceDisconnected]), while dispose itself arrives on the JS thread
|
|
124
|
+
* or the teardown callback's thread.
|
|
125
|
+
*/
|
|
104
126
|
override fun dispose() {
|
|
127
|
+
current.compareAndSet(this, null)
|
|
105
128
|
systemVolumeMonitor.destroy()
|
|
106
129
|
outputMonitor.destroy()
|
|
130
|
+
// Only when still ours: on a reload the replacement may already own the slot.
|
|
131
|
+
carConnectionTarget.compareAndSet(onCarConnectedChanged, null)
|
|
132
|
+
|
|
133
|
+
// The runtime behind the config's JS functions is going away, but anything still holding
|
|
134
|
+
// this instance (an in-flight browse coroutine, the running player) can invoke them after
|
|
135
|
+
// it's gone — each invoke then throws instead of answering, so browse takes its error path
|
|
136
|
+
// rather than its fallback.
|
|
137
|
+
dropStaleJSCallbacks()
|
|
138
|
+
|
|
139
|
+
handler.post {
|
|
140
|
+
connectedService?.let { service ->
|
|
141
|
+
// Identity-guarded: both instances bind to the same singleton Service, so a late dispose
|
|
142
|
+
// must not strip the wiring a replacement has already installed.
|
|
143
|
+
if (service.player.getCallbacks() === callbacks) service.player.setCallbacks(null)
|
|
144
|
+
if (service.player.browser === this) {
|
|
145
|
+
service.player.browser = null
|
|
146
|
+
service.player.forgetBrowserRegistration()
|
|
147
|
+
// The formatter wraps a JS callback from this instance's runtime (installed by our
|
|
148
|
+
// setup(); browser still being ours means no replacement has run setup yet). Without
|
|
149
|
+
// this, every track change until the next setup() throws into the updater's fallback.
|
|
150
|
+
service.player.nowPlaying.formatter = null
|
|
151
|
+
}
|
|
152
|
+
if (service.onBatteryWarningPendingChanged === onBatteryWarningPending) {
|
|
153
|
+
service.onBatteryWarningPendingChanged = null
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
connectedService = null
|
|
157
|
+
|
|
158
|
+
// Dropping the reference disconnects nothing — the controller stays in the session's
|
|
159
|
+
// connected list, holding a binder and a listener.
|
|
160
|
+
mediaBrowserFuture?.let { MediaBrowser.releaseFuture(it) }
|
|
161
|
+
mediaBrowserFuture = null
|
|
162
|
+
setupPromise = null
|
|
163
|
+
|
|
164
|
+
serviceBinding.unbind(this)
|
|
165
|
+
|
|
166
|
+
ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
|
|
167
|
+
// After the observer removal, so this post is not the one dropped.
|
|
168
|
+
handler.removeCallbacksAndMessages(null)
|
|
169
|
+
mainScope.cancel()
|
|
170
|
+
}
|
|
107
171
|
super.dispose()
|
|
108
172
|
}
|
|
109
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Demotes the config to data-only (deep strip of every JS function field) and reverts the JS
|
|
176
|
+
* callbacks stored outside the config tree ([resolveGate]; the now-playing formatter is cleared
|
|
177
|
+
* in [dispose]'s service block, identity-guarded). Stale paths then degrade the same way as a
|
|
178
|
+
* config that never had callbacks — static request/browse layers, default error copy, fail-closed
|
|
179
|
+
* gate — instead of throwing into the error path. Mirrors iOS `dropStaleJSCallbacks`, where the
|
|
180
|
+
* same invoke is fatal rather than a throw.
|
|
181
|
+
*/
|
|
182
|
+
private fun dropStaleJSCallbacks() {
|
|
183
|
+
_configuration = _configuration.strippingJSCallbacks()
|
|
184
|
+
browserManager.config = buildConfig()
|
|
185
|
+
resolveGate = defaultResolveGate
|
|
186
|
+
}
|
|
187
|
+
|
|
110
188
|
private val mainScope = MainScope()
|
|
111
189
|
private var navigationJob: Job? = null
|
|
112
190
|
private val handler = Handler(Looper.getMainLooper())
|
|
@@ -129,8 +207,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
129
207
|
singleTrack = null,
|
|
130
208
|
handleTrackLoad = null,
|
|
131
209
|
androidControllerOfflineError = null,
|
|
210
|
+
viewMoreTitle = null,
|
|
132
211
|
carPlayLoadingTitle = null,
|
|
133
|
-
|
|
212
|
+
resolveAlbumPath = null,
|
|
134
213
|
formatNavigationError = null,
|
|
135
214
|
)
|
|
136
215
|
|
|
@@ -149,7 +228,18 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
149
228
|
private var updateOptions: PlayerUpdateOptions = PlayerUpdateOptions()
|
|
150
229
|
private var mediaBrowserFuture: ListenableFuture<MediaBrowser>? = null
|
|
151
230
|
private var setupOptions = PlayerSetupOptions()
|
|
152
|
-
|
|
231
|
+
// Written only on the main thread (ServiceConnection callbacks, dispose); read from main or,
|
|
232
|
+
// in a few places, straight from the JS thread. @Volatile gives those reads visibility — they
|
|
233
|
+
// must still tolerate staleness in both directions (a just-detached service, or null while
|
|
234
|
+
// onServiceConnected is pending). Keep writes on main; no read-modify-write.
|
|
235
|
+
@Volatile private var connectedService: Service? = null
|
|
236
|
+
|
|
237
|
+
private val serviceBinding = ServiceBinding(context)
|
|
238
|
+
|
|
239
|
+
/** Held rather than inlined so [dispose] can tell its own wiring from a replacement's. */
|
|
240
|
+
private val onBatteryWarningPending: (Boolean) -> Unit = { pending ->
|
|
241
|
+
post { onBatteryWarningPendingChanged(BatteryWarningPendingChangedEvent(pending)) }
|
|
242
|
+
}
|
|
153
243
|
private var setupPromise: ((Unit) -> Unit)? = null
|
|
154
244
|
|
|
155
245
|
// Initial player state staged before the player exists — from setup options or the imperative
|
|
@@ -168,14 +258,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
168
258
|
override var onFormattedNavigationError: (FormattedNavigationError?) -> Unit = {}
|
|
169
259
|
|
|
170
260
|
// MARK: Gate callbacks (native→JS; set from JS, native CALLS them)
|
|
171
|
-
// Default resolver: only reachable
|
|
172
|
-
//
|
|
261
|
+
// Default resolver: only reachable while hasResolver=true but no live runtime has the real
|
|
262
|
+
// `resolveGate` bound — the init window before JS assigns it, or after [dispose] reverts a dead
|
|
263
|
+
// runtime's resolver. It DENIES by default
|
|
173
264
|
// (gated=true) so an active gate never serves content during that window — same fail-closed
|
|
174
265
|
// direction as the resolver-error path in [gateDecision]. The static-gate fast path skips it
|
|
175
266
|
// entirely once a resolver-less gate is set.
|
|
176
|
-
override var resolveGate: (request: NativeGateRequest) -> Promise<Promise<GateDecision>> =
|
|
177
|
-
|
|
178
|
-
}
|
|
267
|
+
override var resolveGate: (request: NativeGateRequest) -> Promise<Promise<GateDecision>> =
|
|
268
|
+
defaultResolveGate
|
|
179
269
|
override var onGate: (event: GateEvent) -> Unit = {}
|
|
180
270
|
|
|
181
271
|
// MARK: Player callbacks
|
|
@@ -239,12 +329,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
239
329
|
}
|
|
240
330
|
|
|
241
331
|
init {
|
|
332
|
+
current.set(this)
|
|
333
|
+
|
|
242
334
|
// Auto-bind to service if it's already running
|
|
243
335
|
launchInScope {
|
|
244
336
|
try {
|
|
245
337
|
Timber.d("Attempting to auto-bind to existing AudioBrowserService from AudioBrowser")
|
|
246
338
|
val intent = Intent(context, Service::class.java)
|
|
247
|
-
val bound =
|
|
339
|
+
val bound = serviceBinding.bind(intent, this@AudioBrowser)
|
|
248
340
|
Timber.d("Auto-bind result: $bound")
|
|
249
341
|
|
|
250
342
|
if (!bound) {
|
|
@@ -286,6 +378,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
286
378
|
routes = _configuration.routes,
|
|
287
379
|
singleTrack = _configuration.singleTrack ?: false,
|
|
288
380
|
androidControllerOfflineError = _configuration.androidControllerOfflineError ?: true,
|
|
381
|
+
viewMoreTitle = _configuration.viewMoreTitle,
|
|
289
382
|
)
|
|
290
383
|
}
|
|
291
384
|
|
|
@@ -319,8 +412,8 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
319
412
|
browserManager.config = buildConfig()
|
|
320
413
|
val tabs = browserManager.queryTabs()
|
|
321
414
|
if (tabs.isNotEmpty()) {
|
|
322
|
-
Timber.d("Using first tab as default path: ${tabs[0].
|
|
323
|
-
tabs[0].
|
|
415
|
+
Timber.d("Using first tab as default path: ${tabs[0].path}")
|
|
416
|
+
tabs[0].path
|
|
324
417
|
} else {
|
|
325
418
|
Timber.d("Using root path as default: /")
|
|
326
419
|
"/"
|
|
@@ -375,8 +468,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
375
468
|
browserManager.config = buildConfig()
|
|
376
469
|
|
|
377
470
|
// Notify player that browser configuration is ready (routes/tabs available)
|
|
378
|
-
// This allows Android Auto to start browsing content
|
|
379
|
-
|
|
471
|
+
// This allows Android Auto to start browsing content.
|
|
472
|
+
// This reaches MediaSession.getConnectedControllers, which asserts the
|
|
473
|
+
// main thread, and this setter runs on the JS thread. Hop without
|
|
474
|
+
// blocking -- runBlockingOnMain would deadlock here for the same reason
|
|
475
|
+
// described for the navigation below (JS callbacks can't run while the
|
|
476
|
+
// JS thread waits). Ordering with navigationJob is preserved: both are
|
|
477
|
+
// queued on mainScope.
|
|
478
|
+
launchInScope { connectedService?.player?.notifyBrowserConfigurationReady() }
|
|
380
479
|
|
|
381
480
|
clearNavigationError()
|
|
382
481
|
|
|
@@ -408,121 +507,48 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
408
507
|
|
|
409
508
|
override fun getFormattedNavigationError(): FormattedNavigationError? = formattedNavigationError
|
|
410
509
|
|
|
411
|
-
/**
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
NavigationErrorType.EMPTY_CONTENT -> "Nothing here"
|
|
425
|
-
NavigationErrorType.TIMEOUT -> "Couldn't load"
|
|
426
|
-
}
|
|
427
|
-
// Omit an empty message so it renders as title-only (e.g. the empty-content case).
|
|
428
|
-
return FormattedNavigationError(title, error.message.takeIf { it.isNotEmpty() })
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/** Returns localized HTTP status text for the given status code */
|
|
432
|
-
private fun httpStatusText(statusCode: Int): String {
|
|
433
|
-
return when (statusCode) {
|
|
434
|
-
400 -> "Bad Request"
|
|
435
|
-
401 -> "Unauthorized"
|
|
436
|
-
403 -> "Forbidden"
|
|
437
|
-
404 -> "Not Found"
|
|
438
|
-
405 -> "Method Not Allowed"
|
|
439
|
-
408 -> "Request Timeout"
|
|
440
|
-
429 -> "Too Many Requests"
|
|
441
|
-
500 -> "Internal Server Error"
|
|
442
|
-
502 -> "Bad Gateway"
|
|
443
|
-
503 -> "Service Unavailable"
|
|
444
|
-
504 -> "Gateway Timeout"
|
|
445
|
-
else -> "Server Error"
|
|
510
|
+
/**
|
|
511
|
+
* Resolves a navigation error to its display form: the app's `formatNavigationError` for the
|
|
512
|
+
* given [path] when one is configured, else the built-in default. Shared by [setNavigationError]
|
|
513
|
+
* and the Android Auto empty tile so in-app and in-car copy cannot drift (ADR 0001).
|
|
514
|
+
*/
|
|
515
|
+
internal suspend fun resolveFormattedError(
|
|
516
|
+
error: NavigationError,
|
|
517
|
+
path: String,
|
|
518
|
+
): FormattedNavigationError {
|
|
519
|
+
val default = defaultFormattedError(error)
|
|
520
|
+
val formatter = _configuration.formatNavigationError ?: return default
|
|
521
|
+
return formattedOrDefault(mainScope, default) {
|
|
522
|
+
formatter(FormatNavigationErrorParams(error, default, path)).await()
|
|
446
523
|
}
|
|
447
524
|
}
|
|
448
525
|
|
|
449
|
-
private fun setNavigationError(
|
|
450
|
-
code: NavigationErrorType,
|
|
451
|
-
message: String,
|
|
452
|
-
path: String,
|
|
453
|
-
statusCode: Double? = null,
|
|
454
|
-
statusCodeSuccess: Boolean? = null,
|
|
455
|
-
) {
|
|
456
|
-
val navError = NavigationError(code, message, statusCode, statusCodeSuccess)
|
|
526
|
+
private fun setNavigationError(navError: NavigationError, path: String) {
|
|
457
527
|
navigationError = navError
|
|
458
528
|
onNavigationError(NavigationErrorEvent(navigationError))
|
|
459
529
|
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
} else {
|
|
475
|
-
formattedNavigationError = defaultFormatted
|
|
476
|
-
onFormattedNavigationError(formattedNavigationError)
|
|
530
|
+
// No formatter means no hop, and the field and event land in this turn: a same-turn
|
|
531
|
+
// getFormattedNavigationError, or a clearNavigationError right after, must not race a
|
|
532
|
+
// launch that has yet to run.
|
|
533
|
+
if (_configuration.formatNavigationError == null) {
|
|
534
|
+
val default = defaultFormattedError(navError)
|
|
535
|
+
formattedNavigationError = default
|
|
536
|
+
onFormattedNavigationError(default)
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
mainScope.launch {
|
|
541
|
+
val formatted = resolveFormattedError(navError, path)
|
|
542
|
+
formattedNavigationError = formatted
|
|
543
|
+
onFormattedNavigationError(formatted)
|
|
477
544
|
}
|
|
478
545
|
}
|
|
479
546
|
|
|
480
|
-
/**
|
|
547
|
+
/** Raises the navigation error [e] maps to, for a failed navigation to [path]. */
|
|
481
548
|
private fun handleBrowserException(e: Exception, path: String, logContext: String) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
setNavigationError(
|
|
486
|
-
NavigationErrorType.HTTP_ERROR,
|
|
487
|
-
e.message ?: "Server error",
|
|
488
|
-
path,
|
|
489
|
-
e.statusCode.toDouble(),
|
|
490
|
-
e.statusCode in 200..299,
|
|
491
|
-
)
|
|
492
|
-
}
|
|
493
|
-
is NetworkException -> {
|
|
494
|
-
Timber.e(e, "Network error $logContext")
|
|
495
|
-
setNavigationError(
|
|
496
|
-
NavigationErrorType.NETWORK_ERROR,
|
|
497
|
-
e.message ?: "Network request failed",
|
|
498
|
-
path,
|
|
499
|
-
)
|
|
500
|
-
}
|
|
501
|
-
is ContentNotFoundException -> {
|
|
502
|
-
Timber.e(e, "Content not found $logContext")
|
|
503
|
-
setNavigationError(
|
|
504
|
-
NavigationErrorType.CONTENT_NOT_FOUND,
|
|
505
|
-
e.message ?: "Content not found",
|
|
506
|
-
path,
|
|
507
|
-
)
|
|
508
|
-
}
|
|
509
|
-
is CallbackException -> {
|
|
510
|
-
Timber.e(e, "Callback error $logContext")
|
|
511
|
-
setNavigationError(
|
|
512
|
-
NavigationErrorType.CALLBACK_ERROR,
|
|
513
|
-
e.message ?: "An error occurred",
|
|
514
|
-
path,
|
|
515
|
-
)
|
|
516
|
-
}
|
|
517
|
-
else -> {
|
|
518
|
-
Timber.e(e, "Unexpected error $logContext")
|
|
519
|
-
setNavigationError(
|
|
520
|
-
NavigationErrorType.UNKNOWN_ERROR,
|
|
521
|
-
e.message ?: "An unexpected error occurred",
|
|
522
|
-
path,
|
|
523
|
-
)
|
|
524
|
-
}
|
|
525
|
-
}
|
|
549
|
+
val navError = navigationErrorFor(e)
|
|
550
|
+
Timber.e(e, "${navError.code} $logContext")
|
|
551
|
+
setNavigationError(navError, path)
|
|
526
552
|
}
|
|
527
553
|
|
|
528
554
|
private fun clearNavigationError() {
|
|
@@ -558,26 +584,38 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
558
584
|
}
|
|
559
585
|
|
|
560
586
|
override fun navigateTrack(track: Track) {
|
|
587
|
+
// A disabled track is unavailable — it never plays, whichever surface or
|
|
588
|
+
// stale resume path delivered the selection (Track.disabled; mirrors iOS
|
|
589
|
+
// TrackSelector.select). Refused before any state changes: an inert tap
|
|
590
|
+
// must not clear an unrelated navigation error or cancel an in-flight
|
|
591
|
+
// navigation.
|
|
592
|
+
if (track.disabled == true) {
|
|
593
|
+
Timber.d("Ignoring selection of disabled track: ${track.title}")
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
561
597
|
clearNavigationError()
|
|
562
598
|
|
|
563
599
|
// Cancel previous navigation to avoid race conditions
|
|
564
600
|
navigationJob?.cancel()
|
|
565
601
|
|
|
566
|
-
val
|
|
602
|
+
val path = track.path
|
|
567
603
|
navigationJob =
|
|
568
604
|
mainScope.launch {
|
|
569
605
|
try {
|
|
570
606
|
when {
|
|
571
|
-
// Check if this is a contextual
|
|
572
|
-
|
|
573
|
-
Timber.d("Navigating to contextual track
|
|
607
|
+
// Check if this is a contextual path (playable-only track with queue context)
|
|
608
|
+
path != null && BrowserPathHelper.isContextual(path) -> {
|
|
609
|
+
Timber.d("Navigating to contextual track path: $path")
|
|
574
610
|
|
|
575
|
-
val parentPath = BrowserPathHelper.stripTrackId(
|
|
576
|
-
val trackId = BrowserPathHelper.extractTrackId(
|
|
611
|
+
val parentPath = BrowserPathHelper.stripTrackId(path)
|
|
612
|
+
val trackId = BrowserPathHelper.extractTrackId(path)
|
|
577
613
|
|
|
578
|
-
// Check if queue already came from this parent path - just skip
|
|
614
|
+
// Check if queue already came from this parent path - just skip
|
|
615
|
+
// to the tapped surface (exact path first, identity for
|
|
616
|
+
// index-less paths — see indexOfTappedTrack)
|
|
579
617
|
if (trackId != null && parentPath == player.queueSourcePath) {
|
|
580
|
-
val index = player.tracks.
|
|
618
|
+
val index = player.tracks.indexOfTappedTrack(path, trackId)
|
|
581
619
|
if (index >= 0) {
|
|
582
620
|
Timber.d("Queue already from $parentPath, skipping to index $index")
|
|
583
621
|
handleTrackLoad(
|
|
@@ -595,8 +633,8 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
595
633
|
}
|
|
596
634
|
}
|
|
597
635
|
|
|
598
|
-
// Expand the queue from the contextual
|
|
599
|
-
val expanded = browserManager.
|
|
636
|
+
// Expand the queue from the contextual path
|
|
637
|
+
val expanded = browserManager.expandQueueFromContextualPath(path)
|
|
600
638
|
|
|
601
639
|
if (expanded != null) {
|
|
602
640
|
val (tracks, startIndex) = expanded
|
|
@@ -631,9 +669,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
631
669
|
}
|
|
632
670
|
}
|
|
633
671
|
// Navigate to browsable track to show browsing UI
|
|
634
|
-
|
|
635
|
-
Timber.d("Navigating to browsable track: $
|
|
636
|
-
browserManager.navigate(
|
|
672
|
+
path != null -> {
|
|
673
|
+
Timber.d("Navigating to browsable track: $path")
|
|
674
|
+
browserManager.navigate(path)
|
|
637
675
|
}
|
|
638
676
|
// If track is playable (has src), load it into player
|
|
639
677
|
track.src != null -> {
|
|
@@ -648,22 +686,36 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
648
686
|
)
|
|
649
687
|
}
|
|
650
688
|
else -> {
|
|
651
|
-
throw IllegalArgumentException("Track must have either
|
|
689
|
+
throw IllegalArgumentException("Track must have either a 'path' or an 'src' property")
|
|
652
690
|
}
|
|
653
691
|
}
|
|
654
692
|
} catch (e: CancellationException) {
|
|
655
693
|
throw e // Rethrow to properly cancel
|
|
656
694
|
} catch (e: Exception) {
|
|
657
|
-
handleBrowserException(e,
|
|
695
|
+
handleBrowserException(e, path ?: track.src ?: "", "navigating to track: ${track.title}")
|
|
658
696
|
}
|
|
659
697
|
}
|
|
660
698
|
}
|
|
661
699
|
|
|
662
|
-
override fun
|
|
700
|
+
override fun search(query: String): Promise<Array<Track>> {
|
|
663
701
|
return Promise.async(mainScope) {
|
|
664
702
|
Timber.d("Searching for: $query")
|
|
665
|
-
|
|
666
|
-
|
|
703
|
+
// Rejects with the error the browse surface would raise, but leaves the navigation error
|
|
704
|
+
// itself unset: a search is not a navigation, and its failure must not replace the
|
|
705
|
+
// displayed page's state (mirrors HybridAudioBrowser.search).
|
|
706
|
+
val searchResults =
|
|
707
|
+
try {
|
|
708
|
+
browserManager.search(query)
|
|
709
|
+
} catch (e: CancellationException) {
|
|
710
|
+
throw e
|
|
711
|
+
} catch (e: Exception) {
|
|
712
|
+
Timber.e(e, "Search failed for query: $query")
|
|
713
|
+
throw NavigationErrorException(navigationErrorFor(e))
|
|
714
|
+
}
|
|
715
|
+
// Flattened, not `children`: a resolved page carries its rows in
|
|
716
|
+
// `sections` (ADR 0010) and search builds exactly one untitled section,
|
|
717
|
+
// so `children` is always null here and this returned nothing at all.
|
|
718
|
+
searchResults.flattenedChildren?.toTypedArray() ?: emptyArray()
|
|
667
719
|
}
|
|
668
720
|
}
|
|
669
721
|
|
|
@@ -677,8 +729,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
677
729
|
// Invalidate cached content so future navigations fetch fresh data
|
|
678
730
|
browserManager.invalidateContentCache(path)
|
|
679
731
|
|
|
680
|
-
// Notify external media controllers (Android Auto)
|
|
681
|
-
|
|
732
|
+
// Notify external media controllers (Android Auto). Reaches MediaSession,
|
|
733
|
+
// which asserts the main thread; this override runs on the JS thread.
|
|
734
|
+
launchInScope { connectedService?.player?.notifyContentChanged(path) }
|
|
682
735
|
|
|
683
736
|
// If we're currently viewing this path, refresh the content
|
|
684
737
|
if (browserManager.getPath() == path) {
|
|
@@ -692,8 +745,10 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
692
745
|
// Clear all cached content so every path re-fetches fresh data
|
|
693
746
|
browserManager.clearContentCache()
|
|
694
747
|
|
|
695
|
-
// Notify external media controllers (Android Auto) to refresh subscribed paths
|
|
696
|
-
|
|
748
|
+
// Notify external media controllers (Android Auto) to refresh subscribed paths.
|
|
749
|
+
// Reaches MediaSession, which asserts the main thread; this override runs on
|
|
750
|
+
// the JS thread.
|
|
751
|
+
launchInScope { connectedService?.player?.invalidateAllContent() }
|
|
697
752
|
|
|
698
753
|
// Refresh whatever the browser is currently viewing
|
|
699
754
|
mainScope.launch { browserManager.refresh() }
|
|
@@ -734,14 +789,16 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
734
789
|
// Re-query every subscribed parent so a connected controller (Android
|
|
735
790
|
// Auto) swaps its lists for the gate tile without reconnecting. Notify
|
|
736
791
|
// only — the content cache stays warm for when the gate clears.
|
|
737
|
-
|
|
792
|
+
// MediaSession asserts the main thread; this override runs on the JS thread.
|
|
793
|
+
launchInScope { connectedService?.player?.invalidateAllContent() }
|
|
738
794
|
}
|
|
739
795
|
|
|
740
796
|
override fun clearGate() {
|
|
741
797
|
if (!gateState.active) return
|
|
742
798
|
// Single atomic assignment — readers never see a half-cleared triple.
|
|
743
799
|
gateState = GateState(active = false, chrome = null, hasResolver = false)
|
|
744
|
-
|
|
800
|
+
// MediaSession asserts the main thread; this override runs on the JS thread.
|
|
801
|
+
launchInScope { connectedService?.player?.invalidateAllContent() }
|
|
745
802
|
}
|
|
746
803
|
|
|
747
804
|
/**
|
|
@@ -781,7 +838,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
781
838
|
override var onCarConnectedChanged: (Boolean) -> Unit = {}
|
|
782
839
|
set(value) {
|
|
783
840
|
field = value
|
|
784
|
-
carConnectionTarget
|
|
841
|
+
carConnectionTarget.set(value)
|
|
785
842
|
// Immediately notify current state (the car may have connected before
|
|
786
843
|
// this JS runtime subscribed) — mirrors the iOS behavior.
|
|
787
844
|
value(carConnected)
|
|
@@ -811,12 +868,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
811
868
|
// Service not connected yet, bind to service
|
|
812
869
|
suspendCoroutine<Unit> { continuation ->
|
|
813
870
|
Timber.d("Binding to AudioBrowserService")
|
|
814
|
-
val bound =
|
|
815
|
-
context.bindService(
|
|
816
|
-
Intent(context, Service::class.java),
|
|
817
|
-
this@AudioBrowser,
|
|
818
|
-
Context.BIND_AUTO_CREATE,
|
|
819
|
-
)
|
|
871
|
+
val bound = serviceBinding.bind(Intent(context, Service::class.java), this@AudioBrowser)
|
|
820
872
|
|
|
821
873
|
if (!bound) {
|
|
822
874
|
continuation.resumeWithException(
|
|
@@ -832,8 +884,17 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
832
884
|
|
|
833
885
|
override fun updateOptions(options: NativeUpdateOptions) {
|
|
834
886
|
updateOptions.updateFromBridge(options)
|
|
835
|
-
// Only update the options if the service is around
|
|
836
|
-
|
|
887
|
+
// Only update the options if the service is around.
|
|
888
|
+
// applyOptions touches the Media3 player, which asserts it is only ever
|
|
889
|
+
// driven from the main thread ("Player callback method is called from a
|
|
890
|
+
// wrong thread"). Nitro invokes this from the JS thread, so hop.
|
|
891
|
+
//
|
|
892
|
+
// The hop blocks rather than launching because `updateOptions` is a single
|
|
893
|
+
// mutable holder: updateFromBridge mutates it in place on the JS thread and
|
|
894
|
+
// applyOptions reads it on main. Blocking is what keeps the two ordered.
|
|
895
|
+
connectedService?.let { service ->
|
|
896
|
+
runBlockingOnMain { service.player.applyOptions(updateOptions) }
|
|
897
|
+
}
|
|
837
898
|
}
|
|
838
899
|
|
|
839
900
|
override fun getOptions(): Options {
|
|
@@ -852,7 +913,6 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
852
913
|
|
|
853
914
|
override fun reset() = runBlockingOnMain {
|
|
854
915
|
player.stop()
|
|
855
|
-
delay(300) // Allow playback to stop
|
|
856
916
|
player.clear()
|
|
857
917
|
// clear() while IDLE emits no engine event, so the state machine never
|
|
858
918
|
// leaves STOPPED — a reset queue is "nothing loaded".
|
|
@@ -979,12 +1039,15 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
979
1039
|
player.setQueue(tracks, startIndex?.toInt() ?: 0, ((startPosition ?: 0.0) * 1000).toLong())
|
|
980
1040
|
}
|
|
981
1041
|
|
|
982
|
-
override fun setActiveTrackFavorited(favorited: Boolean) = runBlockingOnMain {
|
|
1042
|
+
override fun setActiveTrackFavorited(favorited: Boolean): Unit = runBlockingOnMain {
|
|
1043
|
+
// Applied-result is for MediaSession honesty; the JS API stays fire-and-forget.
|
|
983
1044
|
player.setActiveTrackFavorited(favorited)
|
|
1045
|
+
Unit
|
|
984
1046
|
}
|
|
985
1047
|
|
|
986
|
-
override fun toggleActiveTrackFavorited() = runBlockingOnMain {
|
|
1048
|
+
override fun toggleActiveTrackFavorited(): Unit = runBlockingOnMain {
|
|
987
1049
|
player.toggleActiveTrackFavorited()
|
|
1050
|
+
Unit
|
|
988
1051
|
}
|
|
989
1052
|
|
|
990
1053
|
override fun getQueue(): Array<Track> = runBlockingOnMain { player.tracks }
|
|
@@ -1175,13 +1238,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
1175
1238
|
}
|
|
1176
1239
|
|
|
1177
1240
|
// Wire up battery warning callback from service
|
|
1178
|
-
connectedService?.onBatteryWarningPendingChanged =
|
|
1179
|
-
post {
|
|
1180
|
-
this@AudioBrowser.onBatteryWarningPendingChanged(
|
|
1181
|
-
BatteryWarningPendingChangedEvent(pending)
|
|
1182
|
-
)
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1241
|
+
connectedService?.onBatteryWarningPendingChanged = onBatteryWarningPending
|
|
1185
1242
|
|
|
1186
1243
|
val sessionToken = SessionToken(context, ComponentName(context, Service::class.java))
|
|
1187
1244
|
mediaBrowserFuture = MediaBrowser.Builder(context, sessionToken).buildAsync()
|
|
@@ -1436,9 +1493,27 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
1436
1493
|
// callback registers as the single notification target (mirrors the iOS
|
|
1437
1494
|
// static + shared-instance pattern).
|
|
1438
1495
|
@Volatile private var carConnected = false
|
|
1439
|
-
@Volatile
|
|
1496
|
+
// Compare-and-set, not @Volatile: a disposing instance clears the slot only if it still owns
|
|
1497
|
+
// it, and a replacement may be claiming it at the same moment.
|
|
1498
|
+
private val carConnectionTarget = AtomicReference<((Boolean) -> Unit)?>(null)
|
|
1440
1499
|
private var carConnectionObserverStarted = false
|
|
1441
1500
|
|
|
1501
|
+
/**
|
|
1502
|
+
* The instance the current JS runtime is using. Nitro hands out no registry, and the teardown
|
|
1503
|
+
* callback has nothing to dispose without one.
|
|
1504
|
+
*/
|
|
1505
|
+
private val current = AtomicReference<AudioBrowser?>(null)
|
|
1506
|
+
|
|
1507
|
+
/** Called by [AudioBrowserLifecycleModule] when the React instance is torn down. */
|
|
1508
|
+
internal fun disposeCurrent() {
|
|
1509
|
+
current.getAndSet(null)?.dispose()
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/** Shared by the [resolveGate] initializer and [dispose], which reverts to it. */
|
|
1513
|
+
private val defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = {
|
|
1514
|
+
Promise.resolved(Promise.resolved(GateDecision(gated = true, gate = null)))
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1442
1517
|
private fun startCarConnectionObserver(context: Context, handler: Handler) {
|
|
1443
1518
|
// Posted to main: LiveData observation is main-thread only, and the
|
|
1444
1519
|
// single-start guard is then only ever touched from one thread.
|
|
@@ -1449,9 +1524,159 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
1449
1524
|
val connected = type != CarConnection.CONNECTION_TYPE_NOT_CONNECTED
|
|
1450
1525
|
if (connected == carConnected) return@observeForever
|
|
1451
1526
|
carConnected = connected
|
|
1452
|
-
carConnectionTarget?.invoke(connected)
|
|
1527
|
+
carConnectionTarget.get()?.invoke(connected)
|
|
1453
1528
|
}
|
|
1454
1529
|
}
|
|
1455
1530
|
}
|
|
1456
1531
|
}
|
|
1457
1532
|
}
|
|
1533
|
+
|
|
1534
|
+
// MARK: Deep JS-callback stripping (used by dispose)
|
|
1535
|
+
// Functions live at every level of the config, not just the top; each type's strip nils its own.
|
|
1536
|
+
|
|
1537
|
+
private fun NativeBrowserConfiguration.strippingJSCallbacks() =
|
|
1538
|
+
copy(
|
|
1539
|
+
requestResolver = null,
|
|
1540
|
+
browseResolver = null,
|
|
1541
|
+
handleTrackLoad = null,
|
|
1542
|
+
viewMoreTitle = null,
|
|
1543
|
+
resolveAlbumPath = null,
|
|
1544
|
+
formatNavigationError = null,
|
|
1545
|
+
request = request?.strippingJSCallbacks(),
|
|
1546
|
+
browse = browse?.strippingJSCallbacks(),
|
|
1547
|
+
media = media?.strippingJSCallbacks(),
|
|
1548
|
+
artwork = artwork?.strippingJSCallbacks(),
|
|
1549
|
+
nowPlayingArtwork = nowPlayingArtwork?.strippingJSCallbacks(),
|
|
1550
|
+
routes = routes?.map { it.strippingJSCallbacks() }?.toTypedArray(),
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1553
|
+
private fun TransformableRequestConfig.strippingJSCallbacks() =
|
|
1554
|
+
copy(transform = null, transformSync = null)
|
|
1555
|
+
|
|
1556
|
+
private fun MediaRequestConfig.strippingJSCallbacks() =
|
|
1557
|
+
copy(resolve = null, resolveSync = null, transform = null, transformSync = null)
|
|
1558
|
+
|
|
1559
|
+
private fun ArtworkRequestConfig.strippingJSCallbacks() =
|
|
1560
|
+
copy(resolve = null, resolveSync = null, transform = null, transformSync = null)
|
|
1561
|
+
|
|
1562
|
+
private fun NativeRouteEntry.strippingJSCallbacks() =
|
|
1563
|
+
copy(
|
|
1564
|
+
browseCallback = null,
|
|
1565
|
+
browseConfig = browseConfig?.strippingJSCallbacks(),
|
|
1566
|
+
searchCallback = null,
|
|
1567
|
+
searchConfig = searchConfig?.strippingJSCallbacks(),
|
|
1568
|
+
media = media?.strippingJSCallbacks(),
|
|
1569
|
+
artwork = artwork?.strippingJSCallbacks(),
|
|
1570
|
+
)
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Ceiling on the app's `formatNavigationError` hop. Same budget [Player.awaitBrowser] gives the JS
|
|
1574
|
+
* side, the only other JS wait a browse serve already blocks on.
|
|
1575
|
+
*/
|
|
1576
|
+
private val FORMATTER_TIMEOUT = 10.seconds
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* The navigation error an exception raises. Shared by [AudioBrowser]'s failed navigations, the
|
|
1580
|
+
* rejection [AudioBrowser.search] hands JS, and the Android Auto browse-error tile, so one failure
|
|
1581
|
+
* is named one way wherever it surfaces.
|
|
1582
|
+
*/
|
|
1583
|
+
internal fun navigationErrorFor(e: Exception): NavigationError =
|
|
1584
|
+
when (e) {
|
|
1585
|
+
is HttpStatusException ->
|
|
1586
|
+
NavigationError(
|
|
1587
|
+
NavigationErrorType.HTTP_ERROR,
|
|
1588
|
+
e.message ?: "Server error",
|
|
1589
|
+
e.statusCode.toDouble(),
|
|
1590
|
+
e.statusCode in 200..299,
|
|
1591
|
+
)
|
|
1592
|
+
is NetworkException ->
|
|
1593
|
+
NavigationError(
|
|
1594
|
+
NavigationErrorType.NETWORK_ERROR,
|
|
1595
|
+
e.message ?: "Network request failed",
|
|
1596
|
+
null,
|
|
1597
|
+
null,
|
|
1598
|
+
)
|
|
1599
|
+
is ContentNotFoundException ->
|
|
1600
|
+
NavigationError(
|
|
1601
|
+
NavigationErrorType.CONTENT_NOT_FOUND,
|
|
1602
|
+
e.message ?: "Content not found",
|
|
1603
|
+
null,
|
|
1604
|
+
null,
|
|
1605
|
+
)
|
|
1606
|
+
is CallbackException ->
|
|
1607
|
+
NavigationError(
|
|
1608
|
+
NavigationErrorType.CALLBACK_ERROR,
|
|
1609
|
+
e.message ?: "An error occurred",
|
|
1610
|
+
null,
|
|
1611
|
+
null,
|
|
1612
|
+
)
|
|
1613
|
+
else ->
|
|
1614
|
+
NavigationError(
|
|
1615
|
+
NavigationErrorType.UNKNOWN_ERROR,
|
|
1616
|
+
e.message ?: "An unexpected error occurred",
|
|
1617
|
+
null,
|
|
1618
|
+
null,
|
|
1619
|
+
)
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/** Carries a [NavigationError] across a rejected Promise, whose JS error keeps its message. */
|
|
1623
|
+
class NavigationErrorException(val error: NavigationError) : Exception(error.message)
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* The built-in display copy for a navigation error, and the fallback when the app's is unusable.
|
|
1627
|
+
*/
|
|
1628
|
+
internal fun defaultFormattedError(error: NavigationError): FormattedNavigationError {
|
|
1629
|
+
val title =
|
|
1630
|
+
when (error.code) {
|
|
1631
|
+
NavigationErrorType.CONTENT_NOT_FOUND -> "Content Not Found"
|
|
1632
|
+
NavigationErrorType.NETWORK_ERROR -> "Network Error"
|
|
1633
|
+
NavigationErrorType.HTTP_ERROR -> {
|
|
1634
|
+
// Use system-localized HTTP status text (e.g., "Not Found", "Service Unavailable")
|
|
1635
|
+
error.statusCode?.let { httpStatusText(it.toInt()) } ?: "Server Error"
|
|
1636
|
+
}
|
|
1637
|
+
NavigationErrorType.CALLBACK_ERROR -> "Error"
|
|
1638
|
+
NavigationErrorType.UNKNOWN_ERROR -> "Error"
|
|
1639
|
+
// Not a failure — a container that resolved with no children. Neutral copy. See ADR 0001.
|
|
1640
|
+
NavigationErrorType.EMPTY_CONTENT -> "Nothing here"
|
|
1641
|
+
NavigationErrorType.TIMEOUT -> "Couldn't load"
|
|
1642
|
+
}
|
|
1643
|
+
// Omit an empty message so it renders as title-only (e.g. the empty-content case).
|
|
1644
|
+
return FormattedNavigationError(title, error.message.takeIf { it.isNotEmpty() })
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/**
|
|
1648
|
+
* The formatter hop's fallback rule: the app's answer when it produces one, [default] when it
|
|
1649
|
+
* answers null, throws, or outruns [FORMATTER_TIMEOUT]. Kept off the JS edge so the rule is
|
|
1650
|
+
* exercisable without a Nitro Promise.
|
|
1651
|
+
*
|
|
1652
|
+
* The hop runs in [scope] and is awaited through its [kotlinx.coroutines.Deferred], because Nitro's
|
|
1653
|
+
* `Promise.await` parks in a plain `suspendCoroutine`: cancelling it does not unpark it. Awaiting
|
|
1654
|
+
* the Deferred is cancellable, so the timeout leaves the orphan to finish unobserved instead of
|
|
1655
|
+
* holding the caller open until JS answers — which for a browse serve would be forever.
|
|
1656
|
+
*/
|
|
1657
|
+
internal suspend fun formattedOrDefault(
|
|
1658
|
+
scope: CoroutineScope,
|
|
1659
|
+
default: FormattedNavigationError,
|
|
1660
|
+
format: suspend () -> FormattedNavigationError?,
|
|
1661
|
+
): FormattedNavigationError {
|
|
1662
|
+
val hop = scope.async { runCatching { format() }.getOrNull() }
|
|
1663
|
+
return withTimeoutOrNull(FORMATTER_TIMEOUT) { hop.await() } ?: default
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/** Returns localized HTTP status text for the given status code */
|
|
1667
|
+
private fun httpStatusText(statusCode: Int): String {
|
|
1668
|
+
return when (statusCode) {
|
|
1669
|
+
400 -> "Bad Request"
|
|
1670
|
+
401 -> "Unauthorized"
|
|
1671
|
+
403 -> "Forbidden"
|
|
1672
|
+
404 -> "Not Found"
|
|
1673
|
+
405 -> "Method Not Allowed"
|
|
1674
|
+
408 -> "Request Timeout"
|
|
1675
|
+
429 -> "Too Many Requests"
|
|
1676
|
+
500 -> "Internal Server Error"
|
|
1677
|
+
502 -> "Bad Gateway"
|
|
1678
|
+
503 -> "Service Unavailable"
|
|
1679
|
+
504 -> "Gateway Timeout"
|
|
1680
|
+
else -> "Server Error"
|
|
1681
|
+
}
|
|
1682
|
+
}
|