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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (989) hide show
  1. package/README.md +36 -22
  2. package/android/build.gradle +35 -12
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +541 -278
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +52 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +27 -9
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +288 -0
  12. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
  13. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
  14. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  15. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
  16. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  17. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  18. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  19. package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
  20. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
  21. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
  22. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  23. package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
  24. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
  25. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
  26. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  27. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
  28. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
  29. package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
  30. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  31. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
  32. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  33. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  34. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
  35. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
  36. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  37. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
  38. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
  39. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  40. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +62 -40
  41. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  42. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +110 -0
  43. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
  44. package/android/src/main/res/values/strings.xml +25 -5
  45. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +100 -0
  46. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
  47. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  48. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
  49. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
  50. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +40 -2
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +301 -0
  52. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
  53. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +168 -0
  54. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
  55. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +160 -0
  56. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  57. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  58. package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
  59. package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
  60. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
  61. package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
  62. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  63. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  64. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  65. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  66. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  67. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  68. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
  69. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
  70. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  71. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  72. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +55 -16
  73. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
  74. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  75. package/ios/AVPlayerEngine.swift +330 -0
  76. package/ios/AVPlayerEngineCallbacks.swift +15 -0
  77. package/ios/Browser/BrowseGate.swift +58 -0
  78. package/ios/Browser/BrowserConfig.swift +117 -78
  79. package/ios/Browser/BrowserManager.swift +209 -603
  80. package/ios/Browser/BrowserPathHelper.swift +50 -46
  81. package/ios/Browser/InertStyleDiagnostic.swift +296 -0
  82. package/ios/Browser/JsonModels.swift +264 -106
  83. package/ios/Browser/NavigationErrorState.swift +41 -0
  84. package/ios/Browser/PageResolver.swift +227 -0
  85. package/ios/Browser/RequestLayer.swift +90 -0
  86. package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +193 -121
  87. package/ios/Browser/SearchDrillIn.swift +7 -7
  88. package/ios/Browser/SectionScope.swift +58 -24
  89. package/ios/Browser/StyleResolver.swift +58 -0
  90. package/ios/Browser/TabBarEntries.swift +9 -3
  91. package/ios/Browser/TrackIdTemplate.swift +37 -0
  92. package/ios/CarPlay/CPInterfaceController+Safe.swift +100 -43
  93. package/ios/CarPlay/CarPlayArtworkResolver.swift +36 -83
  94. package/ios/CarPlay/CarPlayController.swift +42 -1296
  95. package/ios/CarPlay/CarPlayImageLoader.swift +78 -97
  96. package/ios/CarPlay/CarPlayListItemFactory.swift +303 -92
  97. package/ios/CarPlay/CarPlayNowPlayingManager.swift +129 -136
  98. package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
  99. package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
  100. package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
  101. package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
  102. package/ios/CarPlay/CarPlaySession.swift +209 -0
  103. package/ios/CarPlay/CarPlayTemplates.swift +63 -0
  104. package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
  105. package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +17 -9
  106. package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
  107. package/ios/CarPlay/RNABMediaAddHandler.swift +8 -24
  108. package/ios/CarPlay/RNABMediaAffinityHandler.swift +7 -23
  109. package/ios/CarPlay/RNABMediaIntentHandler.swift +18 -19
  110. package/ios/CarPlay/SectionPresentation.swift +77 -0
  111. package/ios/CarPlay/TopTemplateReplacer.swift +96 -0
  112. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  113. package/ios/Extension/NavigationError+Conversion.swift +14 -4
  114. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  115. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  116. package/ios/Extension/Section+Copying.swift +29 -0
  117. package/ios/Extension/String+IcyCharset.swift +21 -0
  118. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  119. package/ios/Extension/Track+Copying.swift +8 -12
  120. package/ios/Extension/Track+Identity.swift +18 -0
  121. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  122. package/ios/Extension/UIImage+Adaptive.swift +12 -0
  123. package/ios/Extension/URLError+Connectivity.swift +15 -0
  124. package/ios/Http/HttpClient.swift +66 -142
  125. package/ios/HybridAudioBrowser+Browser.swift +153 -0
  126. package/ios/HybridAudioBrowser+Options.swift +144 -0
  127. package/ios/HybridAudioBrowser+Player.swift +397 -0
  128. package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
  129. package/ios/HybridAudioBrowser+Stubs.swift +46 -0
  130. package/ios/HybridAudioBrowser.swift +153 -1610
  131. package/ios/Model/NitroTypeStubs.swift +146 -24
  132. package/ios/Model/PlayerUpdateOptions.swift +8 -1
  133. package/ios/Model/RemoteCommand.swift +18 -0
  134. package/ios/Model/TrackPlayerError.swift +2 -2
  135. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
  136. package/ios/Observer/ItemObserver.swift +90 -0
  137. package/ios/Observer/PlayerObserver.swift +59 -0
  138. package/ios/Option/SessionCategories.swift +42 -92
  139. package/ios/Player/AudioSessionController.swift +177 -0
  140. package/ios/Player/DeferredSeek.swift +70 -0
  141. package/ios/Player/ErrorStatus.swift +27 -0
  142. package/ios/Player/MediaLoader.swift +62 -35
  143. package/ios/Player/NowPlayingUpdater.swift +197 -50
  144. package/ios/Player/PlaybackFailure.swift +125 -0
  145. package/ios/Player/PlaybackPersistence.swift +73 -0
  146. package/ios/Player/PlaybackTimer.swift +8 -2
  147. package/ios/Player/Player.swift +1078 -0
  148. package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
  149. package/ios/Player/PlayerEngine.swift +87 -0
  150. package/ios/Player/PlayerStatusTypes.swift +2 -6
  151. package/ios/Player/QueueManager.swift +93 -67
  152. package/ios/Player/RetryLoop.swift +261 -0
  153. package/ios/Player/ShuffleOrder.swift +33 -0
  154. package/ios/Player/SleepTimerManager.swift +1 -1
  155. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +46 -0
  156. package/ios/RemoteCommand/RemoteCommandController.swift +87 -331
  157. package/ios/Tests/BrowserPathHelperTests.swift +56 -15
  158. package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -373
  159. package/ios/Tests/DeferredSeekTests.swift +70 -0
  160. package/ios/Tests/EmitterTests.swift +29 -0
  161. package/ios/Tests/GenerationMemoTests.swift +121 -0
  162. package/ios/Tests/InertStyleDiagnosticTests.swift +209 -0
  163. package/ios/Tests/JsonModelsTests.swift +164 -1
  164. package/ios/Tests/LRUCacheTests.swift +110 -0
  165. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  166. package/ios/Tests/MediaLoaderTests.swift +12 -48
  167. package/ios/Tests/ObserverTests.swift +88 -0
  168. package/ios/Tests/PlaybackErrorTests.swift +76 -0
  169. package/ios/Tests/PlaybackFailureTests.swift +189 -0
  170. package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
  171. package/ios/Tests/PlayerPolicyTests.swift +420 -0
  172. package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +300 -248
  173. package/ios/Tests/PlayingStateTests.swift +42 -0
  174. package/ios/Tests/QueueManagerTests.swift +196 -115
  175. package/ios/Tests/ReadyValueTests.swift +73 -0
  176. package/ios/Tests/RemoteCommandControllerTests.swift +152 -0
  177. package/ios/Tests/RemoteCommandTests.swift +65 -0
  178. package/ios/Tests/RequestLayerTests.swift +112 -0
  179. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  180. package/ios/Tests/RetryLoopTests.swift +270 -0
  181. package/ios/Tests/SearchDrillInTests.swift +16 -16
  182. package/ios/Tests/SectionPresentationTests.swift +98 -0
  183. package/ios/Tests/SectionScopeTests.swift +87 -55
  184. package/ios/Tests/StyleResolverTests.swift +132 -0
  185. package/ios/Tests/Support/TestTypes.swift +108 -104
  186. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  187. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  188. package/ios/Tests/TopTemplateReplacerTests.swift +172 -0
  189. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  190. package/ios/Tests/TrackIdentityTests.swift +20 -0
  191. package/ios/Tests/TrackSelectorTests.swift +168 -33
  192. package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
  193. package/ios/TrackSelector.swift +46 -31
  194. package/ios/Util/ArtworkImageFetcher.swift +58 -1
  195. package/ios/Util/Emitter.swift +42 -0
  196. package/ios/Util/GenerationMemo.swift +36 -0
  197. package/ios/Util/LRUCache.swift +23 -159
  198. package/ios/Util/NetworkMonitor.swift +43 -26
  199. package/ios/Util/ReadyValue.swift +56 -0
  200. package/ios/Util/SFSymbolRenderer.swift +59 -31
  201. package/ios/Util/SVGProcessor.swift +9 -12
  202. package/lib/commonjs/AudioBrowser.js +8 -8
  203. package/lib/commonjs/AudioBrowser.js.map +1 -1
  204. package/lib/commonjs/CLAUDE.md +52 -0
  205. package/lib/commonjs/features/browser-config.js +12 -8
  206. package/lib/commonjs/features/browser-config.js.map +1 -1
  207. package/lib/commonjs/features/browser.js +11 -3
  208. package/lib/commonjs/features/browser.js.map +1 -1
  209. package/lib/commonjs/features/carConnection.js +1 -1
  210. package/lib/commonjs/features/equalizer.js +1 -1
  211. package/lib/commonjs/features/errors.js +3 -3
  212. package/lib/commonjs/features/favorites.js +14 -11
  213. package/lib/commonjs/features/favorites.js.map +1 -1
  214. package/lib/commonjs/features/gate.js +16 -1
  215. package/lib/commonjs/features/gate.js.map +1 -1
  216. package/lib/commonjs/features/metadata.js +20 -3
  217. package/lib/commonjs/features/metadata.js.map +1 -1
  218. package/lib/commonjs/features/network.js +1 -1
  219. package/lib/commonjs/features/nowPlaying.js +1 -1
  220. package/lib/commonjs/features/output.js +1 -1
  221. package/lib/commonjs/features/playback/controls.js +4 -2
  222. package/lib/commonjs/features/playback/controls.js.map +1 -1
  223. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  224. package/lib/commonjs/features/playback/playing.js +1 -1
  225. package/lib/commonjs/features/playback/progress.js +3 -2
  226. package/lib/commonjs/features/playback/progress.js.map +1 -1
  227. package/lib/commonjs/features/playback/rate.js +8 -1
  228. package/lib/commonjs/features/playback/rate.js.map +1 -1
  229. package/lib/commonjs/features/playback/state.js +3 -4
  230. package/lib/commonjs/features/playback/state.js.map +1 -1
  231. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  232. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  233. package/lib/commonjs/features/playback/volume.js +1 -1
  234. package/lib/commonjs/features/player/options.js +48 -34
  235. package/lib/commonjs/features/player/options.js.map +1 -1
  236. package/lib/commonjs/features/player/setup.js +18 -9
  237. package/lib/commonjs/features/player/setup.js.map +1 -1
  238. package/lib/commonjs/features/player/validateOptions.js +23 -0
  239. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  240. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  241. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  242. package/lib/commonjs/features/queue/queue.js +13 -6
  243. package/lib/commonjs/features/queue/queue.js.map +1 -1
  244. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  245. package/lib/commonjs/features/queue/shuffle.js +1 -1
  246. package/lib/commonjs/features/remoteControls.js +13 -92
  247. package/lib/commonjs/features/remoteControls.js.map +1 -1
  248. package/lib/commonjs/features/sleepTimer.js +13 -1
  249. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  250. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  251. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  252. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  253. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  254. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  255. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  256. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  257. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  258. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  259. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  260. package/lib/commonjs/web/NativeAudioBrowser.js +201 -76
  261. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  262. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  263. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  264. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  265. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  266. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  267. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  268. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
  269. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  270. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  271. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  272. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  273. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  274. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  275. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  276. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  277. package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
  278. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  279. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  280. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  281. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  282. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  283. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  284. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  285. package/lib/commonjs/web/browser/sections.js +89 -0
  286. package/lib/commonjs/web/browser/sections.js.map +1 -0
  287. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  288. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  289. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  290. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  291. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  292. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  293. package/lib/commonjs/web/util/BrowserPathHelper.js +62 -11
  294. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  295. package/lib/module/AudioBrowser.js +2 -1
  296. package/lib/module/AudioBrowser.js.map +1 -1
  297. package/lib/module/CLAUDE.md +52 -0
  298. package/lib/module/features/browser-config.js +12 -8
  299. package/lib/module/features/browser-config.js.map +1 -1
  300. package/lib/module/features/browser.js +11 -3
  301. package/lib/module/features/browser.js.map +1 -1
  302. package/lib/module/features/carConnection.js +1 -1
  303. package/lib/module/features/equalizer.js +1 -1
  304. package/lib/module/features/errors.js +3 -3
  305. package/lib/module/features/favorites.js +14 -11
  306. package/lib/module/features/favorites.js.map +1 -1
  307. package/lib/module/features/gate.js +16 -1
  308. package/lib/module/features/gate.js.map +1 -1
  309. package/lib/module/features/metadata.js +20 -3
  310. package/lib/module/features/metadata.js.map +1 -1
  311. package/lib/module/features/network.js +1 -1
  312. package/lib/module/features/nowPlaying.js +1 -1
  313. package/lib/module/features/output.js +1 -1
  314. package/lib/module/features/playback/controls.js +4 -2
  315. package/lib/module/features/playback/controls.js.map +1 -1
  316. package/lib/module/features/playback/playWhenReady.js +1 -1
  317. package/lib/module/features/playback/playing.js +1 -1
  318. package/lib/module/features/playback/progress.js +3 -2
  319. package/lib/module/features/playback/progress.js.map +1 -1
  320. package/lib/module/features/playback/rate.js +8 -1
  321. package/lib/module/features/playback/rate.js.map +1 -1
  322. package/lib/module/features/playback/state.js +3 -4
  323. package/lib/module/features/playback/state.js.map +1 -1
  324. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  325. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  326. package/lib/module/features/playback/volume.js +1 -1
  327. package/lib/module/features/player/options.js +47 -32
  328. package/lib/module/features/player/options.js.map +1 -1
  329. package/lib/module/features/player/setup.js +17 -8
  330. package/lib/module/features/player/setup.js.map +1 -1
  331. package/lib/module/features/player/validateOptions.js +19 -0
  332. package/lib/module/features/player/validateOptions.js.map +1 -0
  333. package/lib/module/features/queue/activeTrack.js +15 -4
  334. package/lib/module/features/queue/activeTrack.js.map +1 -1
  335. package/lib/module/features/queue/queue.js +13 -6
  336. package/lib/module/features/queue/queue.js.map +1 -1
  337. package/lib/module/features/queue/repeatMode.js +1 -1
  338. package/lib/module/features/queue/shuffle.js +1 -1
  339. package/lib/module/features/remoteControls.js +13 -92
  340. package/lib/module/features/remoteControls.js.map +1 -1
  341. package/lib/module/features/sleepTimer.js +13 -1
  342. package/lib/module/features/sleepTimer.js.map +1 -1
  343. package/lib/module/test-utils/reactNativeStub.js +58 -0
  344. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  345. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  346. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  347. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  348. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  349. package/lib/module/utils/getTrackIdentity.js +20 -0
  350. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  351. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  352. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  353. package/lib/module/web/NativeAudioBrowser.js +201 -76
  354. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  355. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  356. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  357. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  358. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  359. package/lib/module/web/TrackPlayer/Player.js +254 -39
  360. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  361. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
  362. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  363. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  364. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  365. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  366. package/lib/module/web/browser/BrowserManager.js +175 -100
  367. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  368. package/lib/module/web/browser/FavoriteManager.js +31 -21
  369. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  370. package/lib/module/web/browser/NavigationErrorManager.js +3 -1
  371. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  372. package/lib/module/web/browser/SearchManager.js +18 -23
  373. package/lib/module/web/browser/SearchManager.js.map +1 -1
  374. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  375. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  376. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  377. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  378. package/lib/module/web/browser/sections.js +83 -0
  379. package/lib/module/web/browser/sections.js.map +1 -0
  380. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  381. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  382. package/lib/module/web/http/followMediaRedirect.js +215 -0
  383. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  384. package/lib/module/web/player/NowPlayingManager.js +4 -3
  385. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  386. package/lib/module/web/util/BrowserPathHelper.js +62 -11
  387. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  388. package/lib/typescript/package.json +1 -0
  389. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  390. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  391. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  392. package/lib/typescript/src/features/browser.d.ts +10 -2
  393. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  394. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  395. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  396. package/lib/typescript/src/features/errors.d.ts +3 -3
  397. package/lib/typescript/src/features/favorites.d.ts +14 -11
  398. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  399. package/lib/typescript/src/features/gate.d.ts +18 -2
  400. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  401. package/lib/typescript/src/features/metadata.d.ts +20 -3
  402. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  403. package/lib/typescript/src/features/network.d.ts +1 -1
  404. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  405. package/lib/typescript/src/features/output.d.ts +1 -1
  406. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  407. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  408. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  409. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  410. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  411. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  412. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  413. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  414. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  415. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  416. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  417. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  418. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  419. package/lib/typescript/src/features/player/options.d.ts +108 -56
  420. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  421. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  422. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  423. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  424. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  425. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  426. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  427. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  428. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  429. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  430. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  431. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  432. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  433. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  434. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  435. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  436. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  437. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  438. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  439. package/lib/typescript/src/types/browser-nodes.d.ts +387 -126
  440. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  441. package/lib/typescript/src/types/browser.d.ts +93 -61
  442. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  443. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  444. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  445. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  446. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  447. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  448. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  449. package/lib/typescript/src/utils/resolveAssetSource.d.ts +1 -2
  450. package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
  451. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  452. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  453. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  454. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +4 -2
  455. package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +2 -2
  456. package/nitrogen/generated/android/c++/JAndroidAudioOffloadSettings.hpp +2 -2
  457. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +12 -12
  458. package/nitrogen/generated/android/c++/JAndroidPlayerWakeMode.hpp +2 -2
  459. package/nitrogen/generated/android/c++/JAppKilledPlaybackBehavior.hpp +2 -2
  460. package/nitrogen/generated/android/c++/JArtworkRendering.hpp +58 -0
  461. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +10 -5
  462. package/nitrogen/generated/android/c++/JArtworkVariants.hpp +2 -2
  463. package/nitrogen/generated/android/c++/JBatteryOptimizationStatus.hpp +2 -2
  464. package/nitrogen/generated/android/c++/JBatteryOptimizationStatusChangedEvent.hpp +2 -2
  465. package/nitrogen/generated/android/c++/JBatteryWarningPendingChangedEvent.hpp +2 -2
  466. package/nitrogen/generated/android/c++/JBrowseError.hpp +2 -2
  467. package/nitrogen/generated/android/c++/JBrowseResult.hpp +19 -7
  468. package/nitrogen/generated/android/c++/JBrowserSourceCallbackParam.hpp +2 -2
  469. package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +2 -2
  470. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  471. package/nitrogen/generated/android/c++/JCarPlaySiriListButtonPosition.hpp +2 -2
  472. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  473. package/nitrogen/generated/android/c++/JChapterMetadata.hpp +2 -2
  474. package/nitrogen/generated/android/c++/JEqualizerSettings.hpp +10 -10
  475. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +10 -5
  476. package/nitrogen/generated/android/c++/JFormatNavigationErrorParams.hpp +11 -5
  477. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +10 -5
  478. package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +2 -2
  479. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +2 -2
  480. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  481. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +10 -5
  482. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.hpp +4 -2
  483. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +10 -5
  484. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +10 -5
  485. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +2 -2
  486. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.hpp +2 -2
  487. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  488. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +10 -5
  489. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +2 -2
  490. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +18 -12
  491. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +10 -4
  492. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  493. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +18 -6
  494. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  495. package/nitrogen/generated/android/c++/JFunc_void_BatteryOptimizationStatusChangedEvent.hpp +2 -2
  496. package/nitrogen/generated/android/c++/JFunc_void_BatteryWarningPendingChangedEvent.hpp +2 -2
  497. package/nitrogen/generated/android/c++/JFunc_void_EqualizerSettings.hpp +2 -2
  498. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +10 -5
  499. package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +2 -2
  500. package/nitrogen/generated/android/c++/JFunc_void_NavigationErrorEvent.hpp +2 -2
  501. package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +2 -2
  502. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -12
  503. package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +2 -2
  504. package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -2
  505. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +10 -5
  506. package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -2
  507. package/nitrogen/generated/android/c++/JFunc_void_PlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  508. package/nitrogen/generated/android/c++/JFunc_void_PlaybackProgressUpdatedEvent.hpp +2 -2
  509. package/nitrogen/generated/android/c++/JFunc_void_PlaybackQueueEndedEvent.hpp +2 -2
  510. package/nitrogen/generated/android/c++/JFunc_void_PlayingState.hpp +2 -2
  511. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpBackwardEvent.hpp +2 -2
  512. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpForwardEvent.hpp +2 -2
  513. package/nitrogen/generated/android/c++/JFunc_void_RemotePlayIdEvent.hpp +2 -2
  514. package/nitrogen/generated/android/c++/JFunc_void_RemotePlaySearchEvent.hpp +2 -2
  515. package/nitrogen/generated/android/c++/JFunc_void_RemoteSeekEvent.hpp +2 -2
  516. package/nitrogen/generated/android/c++/JFunc_void_RemoteSkipEvent.hpp +2 -2
  517. package/nitrogen/generated/android/c++/JFunc_void_RepeatModeChangedEvent.hpp +2 -2
  518. package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +2 -2
  519. package/nitrogen/generated/android/c++/JFunc_void_TrackMetadata.hpp +2 -2
  520. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
  521. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +2 -2
  522. package/nitrogen/generated/android/c++/JFunc_void_std__optional_FormattedNavigationError_.hpp +2 -2
  523. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +17 -5
  524. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.hpp +2 -2
  525. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +2 -2
  526. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ChapterMetadata_.hpp +16 -16
  527. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +24 -18
  528. package/nitrogen/generated/android/c++/JGate.hpp +2 -2
  529. package/nitrogen/generated/android/c++/JGateDecision.hpp +2 -2
  530. package/nitrogen/generated/android/c++/JGateEvent.hpp +2 -2
  531. package/nitrogen/generated/android/c++/JGateReason.hpp +2 -2
  532. package/nitrogen/generated/android/c++/JGridTile.hpp +64 -0
  533. package/nitrogen/generated/android/c++/JHttpMethod.hpp +2 -2
  534. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +77 -53
  535. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +3 -3
  536. package/nitrogen/generated/android/c++/JIOSCategory.hpp +2 -2
  537. package/nitrogen/generated/android/c++/JIOSCategoryMode.hpp +2 -2
  538. package/nitrogen/generated/android/c++/JIOSCategoryOptions.hpp +2 -2
  539. package/nitrogen/generated/android/c++/JIOSCategoryPolicy.hpp +2 -2
  540. package/nitrogen/generated/android/c++/JIOSOptions.hpp +17 -11
  541. package/nitrogen/generated/android/c++/JImageContext.hpp +2 -2
  542. package/nitrogen/generated/android/c++/JImageQueryParams.hpp +2 -2
  543. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  544. package/nitrogen/generated/android/c++/JImageSource.hpp +2 -2
  545. package/nitrogen/generated/android/c++/JMediaReference.hpp +2 -2
  546. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +10 -5
  547. package/nitrogen/generated/android/c++/JMediaTransformParams.hpp +2 -2
  548. package/nitrogen/generated/android/c++/JNativeAndroidSetupOptions.hpp +2 -2
  549. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +48 -21
  550. package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +2 -2
  551. package/nitrogen/generated/android/c++/JNativeIOSSetupOptions.hpp +10 -10
  552. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +16 -4
  553. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +17 -14
  554. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +9 -12
  555. package/nitrogen/generated/android/c++/JNavigationError.hpp +2 -2
  556. package/nitrogen/generated/android/c++/JNavigationErrorEvent.hpp +2 -2
  557. package/nitrogen/generated/android/c++/JNavigationErrorKind.hpp +58 -0
  558. package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +2 -2
  559. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +12 -12
  560. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +17 -11
  561. package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +2 -2
  562. package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +2 -2
  563. package/nitrogen/generated/android/c++/JOptions.hpp +9 -12
  564. package/nitrogen/generated/android/c++/JOutput.hpp +2 -2
  565. package/nitrogen/generated/android/c++/JOutputType.hpp +2 -2
  566. package/nitrogen/generated/android/c++/JPlayback.hpp +2 -2
  567. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +10 -5
  568. package/nitrogen/generated/android/c++/JPlaybackError.hpp +2 -2
  569. package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -2
  570. package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +2 -2
  571. package/nitrogen/generated/android/c++/JPlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  572. package/nitrogen/generated/android/c++/JPlaybackProgressUpdatedEvent.hpp +2 -2
  573. package/nitrogen/generated/android/c++/JPlaybackQueueEndedEvent.hpp +2 -2
  574. package/nitrogen/generated/android/c++/JPlaybackState.hpp +2 -2
  575. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +7 -13
  576. package/nitrogen/generated/android/c++/JPlayingState.hpp +2 -2
  577. package/nitrogen/generated/android/c++/JProgress.hpp +2 -2
  578. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  579. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  580. package/nitrogen/generated/android/c++/JRemoteJumpBackwardEvent.hpp +2 -2
  581. package/nitrogen/generated/android/c++/JRemoteJumpForwardEvent.hpp +2 -2
  582. package/nitrogen/generated/android/c++/JRemotePlayIdEvent.hpp +2 -2
  583. package/nitrogen/generated/android/c++/JRemotePlaySearchEvent.hpp +2 -2
  584. package/nitrogen/generated/android/c++/JRemoteSeekEvent.hpp +2 -2
  585. package/nitrogen/generated/android/c++/JRemoteSkipEvent.hpp +2 -2
  586. package/nitrogen/generated/android/c++/JRepeatMode.hpp +2 -2
  587. package/nitrogen/generated/android/c++/JRepeatModeChangedEvent.hpp +2 -2
  588. package/nitrogen/generated/android/c++/JRequestConfig.hpp +2 -2
  589. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +67 -59
  590. package/nitrogen/generated/android/c++/JRetryConfig.hpp +2 -2
  591. package/nitrogen/generated/android/c++/JSearchMode.hpp +2 -2
  592. package/nitrogen/generated/android/c++/JSearchParams.hpp +2 -2
  593. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  594. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  595. package/nitrogen/generated/android/c++/JSleepTimer.hpp +4 -4
  596. package/nitrogen/generated/android/c++/JSleepTimerEndOfTrack.hpp +2 -2
  597. package/nitrogen/generated/android/c++/JSleepTimerTime.hpp +2 -2
  598. package/nitrogen/generated/android/c++/JStallReason.hpp +2 -2
  599. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  600. package/nitrogen/generated/android/c++/JTimedMetadata.hpp +2 -2
  601. package/nitrogen/generated/android/c++/JTrack.hpp +27 -48
  602. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +18 -12
  603. package/nitrogen/generated/android/c++/JTrackMetadata.hpp +2 -2
  604. package/nitrogen/generated/android/c++/JTrackRequest.hpp +2 -2
  605. package/nitrogen/generated/android/c++/JTrackStyle.hpp +47 -19
  606. package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +2 -2
  607. package/nitrogen/generated/android/c++/JVariant_Boolean_AndroidAudioOffloadSettings.hpp +3 -3
  608. package/nitrogen/generated/android/c++/JVariant_Boolean_RetryConfig.hpp +3 -3
  609. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
  610. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  611. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  612. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +20 -8
  613. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +3 -3
  614. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioOffloadSettings.kt +15 -0
  615. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +20 -3
  616. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  617. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +37 -0
  618. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +15 -0
  619. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryOptimizationStatusChangedEvent.kt +13 -0
  620. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryWarningPendingChangedEvent.kt +13 -0
  621. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseError.kt +13 -0
  622. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseResult.kt +16 -7
  623. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowserSourceCallbackParam.kt +15 -0
  624. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  625. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  626. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ChapterMetadata.kt +19 -0
  627. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/EqualizerSettings.kt +27 -0
  628. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteChangedEvent.kt +15 -0
  629. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNavigationErrorParams.kt +25 -3
  630. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +21 -0
  631. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +15 -0
  632. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +0 -2
  633. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  634. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +0 -2
  635. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.kt +0 -2
  636. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +0 -2
  637. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +0 -2
  638. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +0 -2
  639. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.kt +0 -2
  640. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  641. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.kt +0 -2
  642. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +0 -2
  643. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.kt +0 -2
  644. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.kt +0 -2
  645. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +78 -0
  646. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt +0 -2
  647. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void.kt +0 -2
  648. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryOptimizationStatusChangedEvent.kt +0 -2
  649. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryWarningPendingChangedEvent.kt +0 -2
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_EqualizerSettings.kt +0 -2
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_FavoriteChangedEvent.kt +0 -2
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_GateEvent.kt +0 -2
  653. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NavigationErrorEvent.kt +0 -2
  654. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NowPlayingMetadata.kt +0 -2
  655. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Options.kt +0 -2
  656. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Output.kt +0 -2
  657. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Playback.kt +0 -2
  658. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackActiveTrackChangedEvent.kt +0 -2
  659. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackErrorEvent.kt +0 -2
  660. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackPlayWhenReadyChangedEvent.kt +0 -2
  661. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackProgressUpdatedEvent.kt +0 -2
  662. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackQueueEndedEvent.kt +0 -2
  663. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlayingState.kt +0 -2
  664. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpBackwardEvent.kt +0 -2
  665. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpForwardEvent.kt +0 -2
  666. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlayIdEvent.kt +0 -2
  667. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlaySearchEvent.kt +0 -2
  668. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSeekEvent.kt +0 -2
  669. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSkipEvent.kt +0 -2
  670. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RepeatModeChangedEvent.kt +0 -2
  671. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TimedMetadata.kt +0 -2
  672. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TrackMetadata.kt +0 -2
  673. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +0 -2
  674. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_double.kt +0 -2
  675. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_FormattedNavigationError_.kt +0 -2
  676. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_ResolvedTrack_.kt +0 -2
  677. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.kt +0 -2
  678. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__string.kt +0 -2
  679. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_ChapterMetadata_.kt +0 -2
  680. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_Track_.kt +0 -2
  681. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Gate.kt +15 -0
  682. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateDecision.kt +15 -0
  683. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateEvent.kt +13 -0
  684. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +25 -0
  685. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +3 -1
  686. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +25 -3
  687. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageContext.kt +15 -0
  688. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageQueryParams.kt +15 -0
  689. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  690. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageSource.kt +19 -0
  691. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +35 -0
  692. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaTransformParams.kt +15 -0
  693. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeAndroidSetupOptions.kt +31 -0
  694. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +51 -5
  695. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeGateRequest.kt +17 -0
  696. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeIOSSetupOptions.kt +21 -0
  697. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +27 -0
  698. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +29 -0
  699. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +23 -0
  700. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationError.kt +19 -0
  701. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorEvent.kt +13 -0
  702. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorKind.kt +23 -0
  703. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +20 -3
  704. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +25 -3
  705. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +29 -0
  706. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +17 -0
  707. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +23 -0
  708. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Output.kt +17 -0
  709. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Playback.kt +15 -0
  710. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackActiveTrackChangedEvent.kt +21 -0
  711. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +21 -0
  712. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorEvent.kt +13 -0
  713. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackPlayWhenReadyChangedEvent.kt +13 -0
  714. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackProgressUpdatedEvent.kt +19 -0
  715. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackQueueEndedEvent.kt +15 -0
  716. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +37 -2
  717. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayingState.kt +15 -0
  718. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Progress.kt +17 -0
  719. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  720. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +61 -0
  721. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpBackwardEvent.kt +13 -0
  722. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpForwardEvent.kt +13 -0
  723. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlayIdEvent.kt +15 -0
  724. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlaySearchEvent.kt +13 -0
  725. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSeekEvent.kt +13 -0
  726. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSkipEvent.kt +13 -0
  727. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RepeatModeChangedEvent.kt +13 -0
  728. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RequestConfig.kt +27 -0
  729. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +67 -15
  730. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +17 -0
  731. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +27 -0
  732. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +66 -0
  733. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +86 -0
  734. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimer.kt +18 -8
  735. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerEndOfTrack.kt +13 -0
  736. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerTime.kt +13 -0
  737. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  738. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TimedMetadata.kt +21 -0
  739. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +55 -12
  740. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackLoadEvent.kt +17 -0
  741. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackMetadata.kt +43 -0
  742. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +17 -0
  743. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +58 -5
  744. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +31 -0
  745. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_AndroidAudioOffloadSettings.kt +16 -7
  746. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_RetryConfig.kt +16 -7
  747. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_NullType_Double.kt +16 -7
  748. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +24 -15
  749. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +18 -8
  750. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +16 -7
  751. package/nitrogen/generated/ios/AudioBrowser+autolinking.rb +2 -0
  752. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +38 -29
  753. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +779 -665
  754. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +34 -16
  755. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +36 -18
  756. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  757. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  758. package/nitrogen/generated/ios/swift/BrowseResult.swift +12 -0
  759. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  760. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  761. package/nitrogen/generated/ios/swift/FormatNavigationErrorParams.swift +7 -2
  762. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  763. package/nitrogen/generated/ios/swift/GridTile.swift +48 -0
  764. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  765. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  766. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  767. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  768. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  769. package/nitrogen/generated/ios/swift/NavigationErrorKind.swift +40 -0
  770. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  771. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  772. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  773. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  774. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  775. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  776. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  777. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  778. package/nitrogen/generated/ios/swift/SleepTimer.swift +13 -0
  779. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  780. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  781. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  782. package/nitrogen/generated/ios/swift/Variant_Bool_AndroidAudioOffloadSettings.swift +12 -0
  783. package/nitrogen/generated/ios/swift/Variant_Bool_RetryConfig.swift +12 -0
  784. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +12 -0
  785. package/nitrogen/generated/ios/swift/Variant_NullType_RemoteButtonLayout.swift +30 -0
  786. package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +13 -0
  787. package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +12 -0
  788. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  789. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  790. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  791. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  792. package/nitrogen/generated/shared/c++/FormatNavigationErrorParams.hpp +9 -2
  793. package/nitrogen/generated/shared/c++/GridTile.hpp +84 -0
  794. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  795. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  796. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  797. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  798. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  799. package/nitrogen/generated/shared/c++/NavigationErrorKind.hpp +76 -0
  800. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  801. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  802. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  803. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  804. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  805. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  806. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  807. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  808. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  809. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  810. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  811. package/package.json +24 -11
  812. package/src/AudioBrowser.ts +2 -1
  813. package/src/CLAUDE.md +52 -0
  814. package/src/features/battery.test.ts +79 -0
  815. package/src/features/browser-config.test.ts +21 -6
  816. package/src/features/browser-config.ts +12 -8
  817. package/src/features/browser.test.ts +1 -1
  818. package/src/features/browser.ts +11 -3
  819. package/src/features/carConnection.ts +1 -1
  820. package/src/features/equalizer.ts +1 -1
  821. package/src/features/errors.ts +3 -3
  822. package/src/features/favorites.ts +14 -11
  823. package/src/features/gate.ts +16 -1
  824. package/src/features/hook-wiring.test.ts +302 -0
  825. package/src/features/metadata.ts +20 -3
  826. package/src/features/network.ts +1 -1
  827. package/src/features/nowPlaying.ts +1 -1
  828. package/src/features/output.ts +1 -1
  829. package/src/features/playback/controls.ts +4 -2
  830. package/src/features/playback/playWhenReady.ts +1 -1
  831. package/src/features/playback/playing.ts +1 -1
  832. package/src/features/playback/progress.test.ts +169 -0
  833. package/src/features/playback/progress.ts +3 -2
  834. package/src/features/playback/rate.ts +8 -1
  835. package/src/features/playback/state.ts +3 -4
  836. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  837. package/src/features/playback/trackPlaybackTime.ts +1 -6
  838. package/src/features/playback/volume.ts +1 -1
  839. package/src/features/player/options.ts +126 -76
  840. package/src/features/player/setup.test.ts +24 -7
  841. package/src/features/player/setup.ts +22 -10
  842. package/src/features/player/validateOptions.ts +21 -0
  843. package/src/features/queue/activeTrack.ts +15 -4
  844. package/src/features/queue/queue.ts +13 -6
  845. package/src/features/queue/repeatMode.ts +1 -1
  846. package/src/features/queue/shuffle.ts +1 -1
  847. package/src/features/remoteControls.ts +13 -92
  848. package/src/features/sleepTimer.test.ts +172 -0
  849. package/src/features/sleepTimer.ts +9 -1
  850. package/src/nitro-stub-drift.test.ts +277 -0
  851. package/src/public-surface.test.ts +147 -0
  852. package/src/specs/audio-browser.nitro.ts +5 -4
  853. package/src/style-resolution.test.ts +210 -0
  854. package/src/test-utils/reactNativeStub.ts +60 -0
  855. package/src/types/browser-native.ts +7 -3
  856. package/src/types/browser-nodes.ts +403 -134
  857. package/src/types/browser.ts +97 -63
  858. package/src/utils/LazyNativeEmitter.ts +28 -15
  859. package/src/utils/NativeUpdatedValue.ts +29 -13
  860. package/src/utils/getTrackIdentity.ts +20 -0
  861. package/src/utils/useDebug.test.ts +182 -0
  862. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  863. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  864. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  865. package/src/web/NativeAudioBrowser.ts +240 -83
  866. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  867. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  868. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  869. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  870. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  871. package/src/web/TrackPlayer/Player.ts +284 -37
  872. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
  873. package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
  874. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  875. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  876. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  877. package/src/web/__tests__/lifecycle.test.ts +345 -0
  878. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  879. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  880. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  881. package/src/web/__tests__/retry.test.ts +169 -0
  882. package/src/web/browser/BrowserManager.ts +209 -111
  883. package/src/web/browser/FavoriteManager.test.ts +84 -0
  884. package/src/web/browser/FavoriteManager.ts +28 -23
  885. package/src/web/browser/NavigationErrorManager.ts +3 -1
  886. package/src/web/browser/SearchManager.test.ts +75 -0
  887. package/src/web/browser/SearchManager.ts +25 -28
  888. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  889. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  890. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  891. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  892. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  893. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  894. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  895. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  896. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  897. package/src/web/browser/sections.test.ts +91 -0
  898. package/src/web/browser/sections.ts +72 -0
  899. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  900. package/src/web/http/RequestConfigBuilder.ts +140 -33
  901. package/src/web/http/followMediaRedirect.test.ts +289 -0
  902. package/src/web/http/followMediaRedirect.ts +253 -0
  903. package/src/web/player/NowPlayingManager.ts +4 -3
  904. package/src/web/util/BrowserPathHelper.test.ts +68 -0
  905. package/src/web/util/BrowserPathHelper.ts +70 -13
  906. package/website/guide/android-auto.md +114 -0
  907. package/website/guide/android-certificates.md +409 -0
  908. package/website/guide/artwork.md +255 -0
  909. package/website/guide/audio-output.md +135 -0
  910. package/website/guide/automotive.md +86 -0
  911. package/website/guide/basic-usage.md +151 -0
  912. package/website/guide/battery.md +115 -0
  913. package/website/guide/browser.md +587 -0
  914. package/website/guide/carplay.md +650 -0
  915. package/website/guide/configuration.md +173 -0
  916. package/website/guide/equalizer.md +231 -0
  917. package/website/guide/errors.md +310 -0
  918. package/website/guide/favorites.md +154 -0
  919. package/website/guide/gate.md +143 -0
  920. package/website/guide/getting-started.md +46 -0
  921. package/website/guide/hooks.md +238 -0
  922. package/website/guide/live-streams.md +125 -0
  923. package/website/guide/metadata.md +239 -0
  924. package/website/guide/migrating-from-track-player.md +244 -0
  925. package/website/guide/native-callback-fetch.md +127 -0
  926. package/website/guide/network.md +86 -0
  927. package/website/guide/now-playing.md +175 -0
  928. package/website/guide/playback.md +328 -0
  929. package/website/guide/queue.md +300 -0
  930. package/website/guide/remote-controls.md +256 -0
  931. package/website/guide/search.md +255 -0
  932. package/website/guide/sleep-timer.md +268 -0
  933. package/website/guide/track.md +247 -0
  934. package/ios/Bridge.h +0 -8
  935. package/ios/Browser/MediaResolveComposer.swift +0 -114
  936. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  937. package/ios/Model/MediaURL.swift +0 -67
  938. package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
  939. package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
  940. package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
  941. package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
  942. package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
  943. package/ios/Observer/PlayerStateObserver.swift +0 -50
  944. package/ios/Observer/PlayerTimeObserver.swift +0 -55
  945. package/ios/Option/PitchAlgorithms.swift +0 -17
  946. package/ios/Option/TimeEventFrequency.swift +0 -18
  947. package/ios/Player/LoadSeekCoordinator.swift +0 -99
  948. package/ios/Player/MediaLoaderDelegate.swift +0 -11
  949. package/ios/Player/PlaybackCoordinator.swift +0 -754
  950. package/ios/Player/PlaybackEffectHandler.swift +0 -54
  951. package/ios/Player/PlaybackErrorHandler.swift +0 -175
  952. package/ios/Player/PlaybackStateStore.swift +0 -27
  953. package/ios/Player/PlayingStateManager.swift +0 -33
  954. package/ios/Player/RetryManager.swift +0 -322
  955. package/ios/Player/SeekCompletionHandler.swift +0 -3
  956. package/ios/Player/SleepTimerHandling.swift +0 -15
  957. package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
  958. package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -325
  959. package/ios/Tests/MediaResolveTests.swift +0 -126
  960. package/ios/Tests/OnceValueTests.swift +0 -79
  961. package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -547
  962. package/ios/Tests/PlaybackStateStoreTests.swift +0 -48
  963. package/ios/Tests/PlayingStateManagerTests.swift +0 -57
  964. package/ios/Tests/RetryManagerTests.swift +0 -89
  965. package/ios/TrackPlayer.swift +0 -1014
  966. package/ios/TrackPlayerCallbacks.swift +0 -86
  967. package/ios/Util/MetadataAdapter.swift +0 -131
  968. package/ios/Util/OnceValue.swift +0 -104
  969. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  970. package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +0 -58
  971. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  972. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  973. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  974. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  975. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  976. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  977. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteConfig.kt +0 -38
  978. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  979. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  980. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  981. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  982. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  983. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  984. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  985. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  986. package/nitrogen/generated/ios/swift/Variant_NullType_NotificationButtonLayout.swift +0 -18
  987. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  988. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  989. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -11,22 +11,32 @@ import androidx.media3.session.MediaSession
11
11
  import androidx.media3.session.SessionCommand
12
12
  import androidx.media3.session.SessionError
13
13
  import androidx.media3.session.SessionResult
14
+ import com.audiobrowser.browser.BrowserManager
14
15
  import com.audiobrowser.browser.handleTrackLoad
16
+ import com.audiobrowser.browser.normalizedSections
17
+ import com.audiobrowser.browser.styleResolvedSections
18
+ import com.audiobrowser.browser.untitledSection
19
+ import com.audiobrowser.extension.indexOfTappedTrack
15
20
  import com.audiobrowser.extension.toTrack
21
+ import com.audiobrowser.navigationErrorFor
16
22
  import com.audiobrowser.util.BrowserPathHelper
17
23
  import com.audiobrowser.util.RatingFavorites
18
24
  import com.audiobrowser.util.TrackFactory
19
25
  import com.google.common.collect.ImmutableList
20
26
  import com.google.common.util.concurrent.Futures
21
27
  import com.google.common.util.concurrent.ListenableFuture
28
+ import com.margelo.nitro.audiobrowser.FormattedNavigationError
22
29
  import com.margelo.nitro.audiobrowser.Gate
23
30
  import com.margelo.nitro.audiobrowser.GateEvent
24
31
  import com.margelo.nitro.audiobrowser.GateReason
25
32
  import com.margelo.nitro.audiobrowser.MediaReference
26
33
  import com.margelo.nitro.audiobrowser.NativeGateRequest
27
- import com.margelo.nitro.audiobrowser.NotificationButtonLayout
34
+ import com.margelo.nitro.audiobrowser.NavigationError
35
+ import com.margelo.nitro.audiobrowser.NavigationErrorType
28
36
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
37
+ import com.margelo.nitro.audiobrowser.RemoteButtonLayout
29
38
  import com.margelo.nitro.audiobrowser.SearchParams
39
+ import com.margelo.nitro.audiobrowser.Section
30
40
  import com.margelo.nitro.audiobrowser.Track
31
41
  import kotlinx.coroutines.CancellationException
32
42
  import kotlinx.coroutines.CoroutineScope
@@ -40,8 +50,12 @@ import kotlinx.coroutines.withContext
40
50
  import timber.log.Timber
41
51
 
42
52
  /**
43
- * MediaLibrarySession callback that handles all media session interactions. All logic is handled
44
- * directly by the AudioBrowser.
53
+ * Everything the MediaLibrarySession serves except transport: browse, search, item lookup, queue
54
+ * building, playback resumption, and the favorite and rating commands. Transport does not arrive
55
+ * here — it reaches the player through [InterceptingPlayer], and Assistant's play-from-search
56
+ * arrives as an intent handled in the service. The callbacks that answer through [scope] run on IO,
57
+ * so anything touching the ExoPlayer instance hops back to [Dispatchers.Main]; the rest answer
58
+ * synchronously on the Media3 application thread.
45
59
  */
46
60
  class MediaSessionCallback(private val player: Player) :
47
61
  MediaLibraryService.MediaLibrarySession.Callback {
@@ -53,6 +67,8 @@ class MediaSessionCallback(private val player: Player) :
53
67
  // threads (network observer, JS-triggered notifies) — guard all access with synchronized(this).
54
68
  private val parentIdSubscriptions =
55
69
  mutableMapOf<String, MutableSet<MediaSession.ControllerInfo>>()
70
+ private val failedSearch = FailedSearchSlot()
71
+ private val offlineSearches = OfflineSearchRegistry()
56
72
  private var mediaLibrarySession: MediaLibraryService.MediaLibrarySession? = null
57
73
 
58
74
  /**
@@ -64,14 +80,15 @@ class MediaSessionCallback(private val player: Player) :
64
80
  private set
65
81
 
66
82
  init {
67
- // Observe network state changes and notify subscribers
68
- player.networkMonitor.observeOnline(scope) { _ -> notifySubscribedChildrenChanged() }
83
+ player.networkMonitor.observeOnline(scope) { online ->
84
+ notifySubscribedChildrenChanged()
85
+ // A browse level is re-queried through its subscription; a search has none, so its offline
86
+ // tile has to be taken off the screen by re-running the query.
87
+ if (online) scope.launch { reannounceOfflineSearches() }
88
+ }
69
89
  }
70
90
 
71
- /**
72
- * Apply pagination to a list of items. If pageSize is 0 or MAX_VALUE (Android Auto default),
73
- * returns the full list.
74
- */
91
+ /** Media3's legacy browse bridge asks for a whole list with pageSize [Int.MAX_VALUE]. */
75
92
  private fun <T> List<T>.paginate(page: Int, pageSize: Int): List<T> {
76
93
  return if (pageSize in 1 until Int.MAX_VALUE) {
77
94
  this.drop(page * pageSize).take(pageSize)
@@ -80,45 +97,6 @@ class MediaSessionCallback(private val player: Player) :
80
97
  }
81
98
  }
82
99
 
83
- /** Creates an offline error MediaItem. */
84
- private fun createOfflineMediaItem(): MediaItem =
85
- createErrorMediaItem(
86
- mediaId = BrowserPathHelper.OFFLINE_PATH,
87
- title = player.context.getString(com.audiobrowser.R.string.audio_browser_offline_error),
88
- subtitle =
89
- player.context.getString(com.audiobrowser.R.string.audio_browser_offline_error_subtitle),
90
- )
91
-
92
- /**
93
- * Builds a non-browsable, non-playable [MediaItem] used to surface an error inside an Android
94
- * Auto / AAOS browse list. Rendered as a greyed-out, non-interactive tile, which is the only
95
- * side-effect-free way to communicate a browse failure to legacy controllers (Media3 drops
96
- * [LibraryResult.ofError] on the legacy browse bridge, leaving an empty "No items" screen).
97
- */
98
- private fun createErrorMediaItem(mediaId: String, title: String, subtitle: String): MediaItem =
99
- MediaItem.Builder()
100
- .setMediaId(mediaId)
101
- .setMediaMetadata(
102
- MediaMetadata.Builder()
103
- .setTitle(title)
104
- .setSubtitle(subtitle)
105
- // Non-browsable, non-playable. Android Auto ignores these flags and still drills into a
106
- // tapped tile, but onGetChildren returns an empty list for the sentinel paths so it's a
107
- // harmless "No Items" dead-end rather than an endless stack of error pages.
108
- .setIsBrowsable(false)
109
- .setIsPlayable(false)
110
- .build()
111
- )
112
- .build()
113
-
114
- /**
115
- * Builds the gate tile from a per-request chrome: while a gate is active, tabs stay visible but a
116
- * gated browse/search level serves this single tile (see the gate checks in [onGetChildren] /
117
- * [onGetSearchResult]). Same shape as the error tiles — non-browsable, non-playable — and
118
- * deliberately NOT accompanied by a SessionError: a gate is deliberate app state, not a failure.
119
- * The message renders as the tile's subtitle (newlines collapse to spaces — list rows are
120
- * single-line).
121
- */
122
100
  /**
123
101
  * Wraps a raw external-search query string into the structured [SearchParams] the gate request
124
102
  * carries. The car search surfaces only give a free-text query, so the other fields stay null —
@@ -136,40 +114,34 @@ class MediaSessionCallback(private val player: Player) :
136
114
  reference = MediaReference.UNKNOWN,
137
115
  )
138
116
 
139
- private fun createGateMediaItem(gate: Gate): MediaItem =
140
- createErrorMediaItem(
141
- mediaId = BrowserPathHelper.GATE_PATH,
142
- title = gate.title,
143
- subtitle = gate.message?.replace('\n', ' ') ?: "",
144
- )
145
-
146
117
  /**
147
- * Builds a generic "something went wrong" error tile for a browse failure. The true offline case
148
- * is handled separately by the [networkMonitor] guard in [onGetChildren]; anything reaching the
149
- * catch block is an online-but-failed request (e.g. server down, bad status), so it must NOT be
150
- * labelled "no internet connection".
118
+ * Builds the gate tile from a per-request chrome: while a gate is active, tabs stay visible but a
119
+ * gated browse/search level serves this single tile (see the gate checks in [onGetChildren] /
120
+ * [onGetSearchResult]). Same shape as the error tiles — non-browsable, non-playable — and
121
+ * deliberately NOT accompanied by a SessionError: a gate is deliberate app state, not a failure.
151
122
  */
152
- private fun createBrowseErrorMediaItem(): MediaItem =
123
+ private fun createGateMediaItem(gate: Gate): MediaItem =
153
124
  createErrorMediaItem(
154
- mediaId = BrowserPathHelper.ERROR_PATH,
155
- title = player.context.getString(com.audiobrowser.R.string.audio_browser_browse_error),
156
- subtitle =
157
- player.context.getString(com.audiobrowser.R.string.audio_browser_browse_error_subtitle),
125
+ BrowserPathHelper.GATE_PATH,
126
+ FormattedNavigationError(gate.title, gate.message),
158
127
  )
159
128
 
160
129
  fun updateMediaSession(
161
130
  mediaSession: MediaSession,
162
131
  capabilities: PlayerCapabilities,
163
- notificationButtons: NotificationButtonLayout?,
132
+ remoteButtonLayout: RemoteButtonLayout?,
164
133
  searchAvailable: Boolean,
134
+ forwardJumpInterval: Double,
135
+ backwardJumpInterval: Double,
165
136
  ) {
166
- // Store as MediaLibrarySession for notifyChildrenChanged support
167
137
  this.mediaLibrarySession = mediaSession as? MediaLibraryService.MediaLibrarySession
168
138
  commandManager.updateMediaSession(
169
139
  mediaSession,
170
140
  capabilities,
171
- notificationButtons,
141
+ remoteButtonLayout,
172
142
  searchAvailable,
143
+ forwardJumpInterval,
144
+ backwardJumpInterval,
173
145
  )
174
146
  }
175
147
 
@@ -181,18 +153,34 @@ class MediaSessionCallback(private val player: Player) :
181
153
  return commandManager.buildConnectionResult(session)
182
154
  }
183
155
 
156
+ override fun onDisconnected(session: MediaSession, controller: MediaSession.ControllerInfo) {
157
+ Timber.Forest.d("MediaSession disconnect: ${controller.packageName}")
158
+ // A controller that leaves without unsubscribing (Android Auto's normal exit) would otherwise
159
+ // be notified — and held — forever.
160
+ offlineSearches.forget(controller)
161
+ synchronized(this) {
162
+ parentIdSubscriptions.values.forEach { it.remove(controller) }
163
+ parentIdSubscriptions.entries.removeAll { (_, browsers) -> browsers.isEmpty() }
164
+ }
165
+ super.onDisconnected(session, controller)
166
+ }
167
+
184
168
  override fun onCustomCommand(
185
169
  session: MediaSession,
186
170
  controller: MediaSession.ControllerInfo,
187
171
  command: SessionCommand,
188
172
  args: Bundle,
189
173
  ): ListenableFuture<SessionResult> {
190
- // Handle favorite button tap
174
+ // A success here makes the controller flip its heart optimistically, so a no-op (no current
175
+ // track) must report INVALID_STATE instead of success-then-revert.
191
176
  if (command.customAction == MediaSessionCommandManager.CUSTOM_ACTION_FAVORITE) {
192
- val currentFavorited = player.currentTrack?.favorited ?: false
193
- Timber.d("Favorite button tapped - toggling from $currentFavorited to ${!currentFavorited}")
194
- player.setActiveTrackFavorited(!currentFavorited)
195
- return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
177
+ val applied = player.toggleActiveTrackFavorited()
178
+ Timber.d("Favorite button tapped - toggle applied=$applied")
179
+ return Futures.immediateFuture(
180
+ SessionResult(
181
+ if (applied) SessionResult.RESULT_SUCCESS else SessionResult.RESULT_ERROR_INVALID_STATE
182
+ )
183
+ )
196
184
  }
197
185
 
198
186
  if (commandManager.handleCustomCommand(command, player)) {
@@ -208,8 +196,16 @@ class MediaSessionCallback(private val player: Player) :
208
196
  ): ListenableFuture<SessionResult> {
209
197
  // A heart rating from a controller (e.g. Google Assistant "I like this") toggles the
210
198
  // now-playing favorite. setActiveTrackFavorited fires onFavoriteChanged so the consumer
211
- // persists it — the same path as the notification / CarPlay heart button.
212
- RatingFavorites.favoritedFor(rating)?.let { player.setActiveTrackFavorited(it) }
199
+ // persists it — the same path as the notification / CarPlay heart button. Report an honest
200
+ // result (see onCustomCommand): INVALID_STATE when there is no current track to favorite.
201
+ RatingFavorites.favoritedFor(rating)?.let { favorited ->
202
+ val applied = player.setActiveTrackFavorited(favorited)
203
+ return Futures.immediateFuture(
204
+ SessionResult(
205
+ if (applied) SessionResult.RESULT_SUCCESS else SessionResult.RESULT_ERROR_INVALID_STATE
206
+ )
207
+ )
208
+ }
213
209
  return super.onSetRating(session, controller, rating)
214
210
  }
215
211
 
@@ -219,13 +215,6 @@ class MediaSessionCallback(private val player: Player) :
219
215
  browser: MediaSession.ControllerInfo,
220
216
  params: MediaLibraryService.LibraryParams?,
221
217
  ): ListenableFuture<LibraryResult<MediaItem>> {
222
- // Extract artwork size hint from root hints (e.g., from Android Auto)
223
- // TODO: Also consider these other root hints in the future:
224
- // - KEY_ROOT_HINT_MEDIA_HOST_VERSION
225
- // - KEY_ROOT_HINT_MEDIA_SESSION_API
226
- // - BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT
227
- // - BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT
228
- // - KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED
229
218
  params?.extras?.getInt(MediaConstants.EXTRAS_KEY_MEDIA_ART_SIZE_PIXELS, 0)?.let { size ->
230
219
  if (size > 0) {
231
220
  artworkSizeHintPixels = size
@@ -234,13 +223,11 @@ class MediaSessionCallback(private val player: Player) :
234
223
  }
235
224
 
236
225
  if (params?.isRecent == true) {
237
- // TODO: support recent queries through something like onRecent and return a MediaItem with
238
- // .setMediaId("__RECENT__") here - when onRecent is not configured we can keep returning an
239
- // error here:
240
-
241
- // The service currently does not support playback resumption. Tell System UI by returning
242
- // an error of type 'RESULT_ERROR_NOT_SUPPORTED' for a `params.isRecent` request. See
243
- // https://github.com/androidx/media/issues/355
226
+ // Playback resumption is served by media3 itself: it intercepts System UI's EXTRA_RECENT
227
+ // root request internally and answers through onPlaybackResumption, so that request never
228
+ // reaches this callback. Any other browser asking for a recent root gets
229
+ // ERROR_NOT_SUPPORTED — there is no browsable recents tree; recents are ordinary consumer
230
+ // content (a tab or route).
244
231
  return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED))
245
232
  }
246
233
 
@@ -267,10 +254,9 @@ class MediaSessionCallback(private val player: Player) :
267
254
  params: MediaLibraryService.LibraryParams?,
268
255
  ): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
269
256
  Timber.d(
270
- "onGetChildren: {parentId: $parentId, page: $page, pageSize: $pageSize, isSearchPath: ${BrowserPathHelper.isSpecialPath(parentId)} }"
257
+ "onGetChildren: {parentId: $parentId, page: $page, pageSize: $pageSize, isSpecialPath: ${BrowserPathHelper.isSpecialPath(parentId)} }"
271
258
  )
272
259
  return scope.future {
273
- // Wait for browser to be registered if it's not available yet
274
260
  val audioBrowser =
275
261
  player.awaitBrowser().also { Timber.d("Browser ready, proceeding with onGetChildren") }
276
262
  val browserManager = audioBrowser.browserManager
@@ -297,54 +283,56 @@ class MediaSessionCallback(private val player: Player) :
297
283
  }
298
284
  }
299
285
 
300
- // The error / offline tiles are dead-ends. Some controllers (e.g. Android Auto when online)
301
- // treat a non-browsable tile as tappable and subscribe to its mediaId anyway; returning the
302
- // error tile again here would push an endless stack of error pages. Return nothing instead.
303
- // For the offline tile, re-send the alert when still offline so a tap surfaces the
304
- // explanation again — but only then, since these paths are also re-queried by
305
- // notifyChildrenChanged (e.g. when connectivity returns) without any user action.
306
- if (parentId == BrowserPathHelper.OFFLINE_PATH || parentId == BrowserPathHelper.ERROR_PATH) {
286
+ // A drill-in on the error / offline / empty tile returns nothing (see isDeadEndPath). For
287
+ // the offline tile, re-send the alert when still offline so a tap surfaces the explanation
288
+ // again — but only then, since these paths are also re-queried by notifyChildrenChanged
289
+ // (e.g. when connectivity returns) without any user action.
290
+ if (BrowserPathHelper.isDeadEndPath(parentId)) {
307
291
  if (parentId == BrowserPathHelper.OFFLINE_PATH && !player.networkMonitor.isOnline.value) {
308
292
  sendBrowseError(session, browser, offline = true)
309
293
  }
310
294
  return@future LibraryResult.ofItemList(ImmutableList.of<MediaItem>(), params)
311
295
  }
312
296
 
313
- // Show offline error when offline:
314
297
  if (
315
298
  !player.networkMonitor.isOnline.value && browserManager.config.androidControllerOfflineError
316
299
  ) {
317
300
  Timber.w("Network offline - returning error message for: $parentId")
318
301
  sendBrowseError(session, browser, offline = true)
319
- return@future LibraryResult.ofItemList(ImmutableList.of(createOfflineMediaItem()), params)
302
+ return@future LibraryResult.ofItemList(
303
+ ImmutableList.of(
304
+ createOfflineMediaItem(audioBrowser.resolveFormattedError(offlineError(), parentId))
305
+ ),
306
+ params,
307
+ )
320
308
  }
321
309
 
322
310
  try {
323
311
  val children =
324
- if (parentId == BrowserPathHelper.RECENT_PATH) {
325
- // TODO: implement recent media items
326
- emptyList<MediaItem>()
327
- } else if (parentId == BrowserPathHelper.ROOT_PATH) {
328
- // Return tabs as root children (limited to 4 for automotive platform compatibility)
329
- // TODO: Check what Android Auto does with empty tabs list - may need to return error?
330
- val tabs = browserManager.queryTabs()
312
+ if (parentId == BrowserPathHelper.ROOT_PATH) {
313
+ // Disabled tabs hide (Track.disabled) — filtered before the four-tab cap so an
314
+ // unavailable tab never costs an available one its slot.
315
+ val tabs = browserManager.queryTabs().filterNot { it.disabled == true }
331
316
  if (tabs.size > 4) {
332
317
  Timber.w(
333
318
  "Root has ${tabs.size} tabs; dropping ${tabs.size - 4} (Android Auto root limit)"
334
319
  )
335
320
  }
336
- toMediaItems(tabs.take(4))
321
+ toFlatMediaItems(tabs.take(4))
337
322
  } else {
338
- // Resolve the specific path and get its children
323
+ // Section styles arrive page-folded (`section ?? page`, ADR 0011).
339
324
  val resolvedTrack = browserManager.resolve(parentId)
340
-
341
- // Convert children to MediaItems (url is already set to contextual URL)
342
- val trackChildren =
343
- resolvedTrack.children
344
- ?: throw IllegalStateException(
345
- "Expected browsed ResolvedTrack to have a children array"
346
- )
347
- toMediaItems(trackChildren.toList())
325
+ val sections =
326
+ resolvedTrack.styleResolvedSections()
327
+ ?: throw IllegalStateException("Expected browsed ResolvedTrack to have sections")
328
+ browserManager.warnIfGridPageLacksPromise(parentId, sections)
329
+ // A page with no children resolved fine, so it raises no SessionError and no event —
330
+ // notifyChildrenChanged re-queries these paths without any user action. It serves the
331
+ // app's path-aware `empty-content` copy as one inert tile (ADR 0001) instead of
332
+ // Android Auto's bare "No items".
333
+ browseLevelItems(toMediaItems(sections)) {
334
+ audioBrowser.resolveFormattedError(emptyContentError(), parentId)
335
+ }
348
336
  }
349
337
 
350
338
  LibraryResult.ofItemList(ImmutableList.copyOf(children.paginate(page, pageSize)), params)
@@ -353,18 +341,17 @@ class MediaSessionCallback(private val player: Player) :
353
341
  // error tile. awaitBrowser's TimeoutCancellationException IS a real failure, keep that.
354
342
  if (e is CancellationException && e !is TimeoutCancellationException) throw e
355
343
  Timber.e(e, "Error getting children for parentId: $parentId")
356
- // Surface an error tile instead of a bare ofError(): Media3 drops the error message on the
357
- // legacy browse bridge, which would otherwise leave an empty "No items" screen in Android
358
- // Auto. See https://github.com/androidx/media/issues/2901
359
- //
360
- // Verified on a head unit (2026-06): the AA browse list NEVER renders error text — not for
361
- // transient sendError, sticky non-fatal replication, or fatal replication
362
- // (setLibraryErrorReplicationMode). Fatal replication does show the message on the
363
- // playback screen, but presents the session as STATE_ERROR with no actions, hiding the
364
- // now-playing item and transport controls — not worth it. Tiles remain the only in-browse
365
- // signal we control.
344
+ // A tile rather than a bare ofError() — see createErrorMediaItem and
345
+ // https://github.com/androidx/media/issues/2901
366
346
  sendBrowseError(session, browser, offline = false)
367
- LibraryResult.ofItemList(ImmutableList.of(createBrowseErrorMediaItem()), params)
347
+ LibraryResult.ofItemList(
348
+ ImmutableList.of(
349
+ createBrowseErrorMediaItem(
350
+ audioBrowser.resolveFormattedError(navigationErrorFor(e), parentId)
351
+ )
352
+ ),
353
+ params,
354
+ )
368
355
  }
369
356
  }
370
357
  }
@@ -375,8 +362,8 @@ class MediaSessionCallback(private val player: Player) :
375
362
  * Media3 transiently attaches the error code/message to the platform session's playback state
376
363
  * without entering STATE_ERROR.
377
364
  *
378
- * NOTE: current Android Auto renders NOTHING for this (verified on a head unit — see
379
- * androidx/media#2901; the transient state is cleared microseconds after being set). It is kept
365
+ * NOTE: current Android Auto renders nothing for this — its transient state is cleared
366
+ * microseconds after being set (see [createErrorMediaItem], androidx/media#2901). It is kept
380
367
  * because it is the correct Media3-API error signal, costs nothing, and becomes user-visible if
381
368
  * Android Auto moves to consuming SessionError via the Media3 controller API.
382
369
  *
@@ -398,19 +385,45 @@ class MediaSessionCallback(private val player: Player) :
398
385
  }
399
386
 
400
387
  /**
401
- * Converts tracks to MediaItems for browse delivery, routing http(s) artwork through the
402
- * content:// provider so Android Auto can load it via the ArtworkContentProvider. Image-row
403
- * tracks (a CarPlay-only rendering) are expanded into their items as regular grouped rows
404
- * first — see [TrackFactory.expandImageRows].
388
+ * Flattens a page's sections to MediaItems for browse delivery — the Media3 boundary where
389
+ * sections die (ADR 0010): the MediaBrowser protocol has no section node, so each child is
390
+ * stamped with its owning section's title/style hints. A section with a `path` gains a browsable
391
+ * "view more" row under the same header — at every `display`, because no Android Auto header is
392
+ * tappable at any of them, so the row is the section's only "view all" affordance here. Http(s)
393
+ * artwork routes through the content:// provider so Android Auto can load it via the
394
+ * ArtworkContentProvider.
405
395
  */
406
- private fun toMediaItems(tracks: List<Track>): List<MediaItem> {
396
+ private suspend fun toMediaItems(sections: List<Section>): List<MediaItem> {
407
397
  val registry = player.browseArtworkRegistry
408
398
  val authority = com.audiobrowser.util.ArtworkUris.authorityFor(player.context.packageName)
409
- return TrackFactory.expandImageRows(tracks).map {
410
- TrackFactory.toBrowseMediaItem(it, registry, authority)
399
+ return sections.flatMap { section ->
400
+ // Android Auto has no disabled affordance, so an unavailable track hides
401
+ // — never a normal-looking dead row (Track.disabled's rendering ladder).
402
+ val children = section.children.filter { it.disabled != true }
403
+ val items = children.map { TrackFactory.toBrowseMediaItem(it, registry, authority, section) }
404
+ // No "view more" under an empty section — CarPlay skips empty sections
405
+ // outright, and a lone navigation tile under a header is a dead end.
406
+ if (section.path == null || children.isEmpty()) return@flatMap items
407
+ // The browser is already up at every call site, so awaiting it here is a field read, and
408
+ // viewMoreTitle is memoized per content generation — both are free after the first row.
409
+ val title = player.awaitBrowser().browserManager.viewMoreTitle()
410
+ items +
411
+ TrackFactory.toBrowseMediaItem(
412
+ TrackFactory.navigationTrack(section, title),
413
+ registry,
414
+ authority,
415
+ section,
416
+ )
411
417
  }
412
418
  }
413
419
 
420
+ /**
421
+ * Flat lists (tabs, search results) convert as one untitled list section — which contributes no
422
+ * group or style hints — so exactly one Track→MediaItem path exists at the Media3 boundary.
423
+ */
424
+ private suspend fun toFlatMediaItems(tracks: List<Track>): List<MediaItem> =
425
+ toMediaItems(listOf(untitledSection(tracks.toTypedArray())))
426
+
414
427
  override fun onGetItem(
415
428
  session: MediaLibraryService.MediaLibrarySession,
416
429
  browser: MediaSession.ControllerInfo,
@@ -418,15 +431,56 @@ class MediaSessionCallback(private val player: Player) :
418
431
  ): ListenableFuture<LibraryResult<MediaItem>> {
419
432
  Timber.Forest.d("onGetItem: ${browser.packageName}, mediaId = $mediaId")
420
433
  return scope.future {
421
- // Handle special paths first (these don't need browser)
434
+ // Only the root answers ahead of awaitBrowser: it needs no browser. Every tile sentinel
435
+ // below does — for the formatter hop, and the gate one for gateDecision.
436
+ if (mediaId == BrowserPathHelper.ROOT_PATH) {
437
+ return@future LibraryResult.ofItem(
438
+ MediaItem.Builder()
439
+ .setMediaId(mediaId)
440
+ .setMediaMetadata(
441
+ MediaMetadata.Builder().setIsBrowsable(true).setIsPlayable(false).build()
442
+ )
443
+ .build(),
444
+ null,
445
+ )
446
+ }
447
+
422
448
  if (mediaId == BrowserPathHelper.OFFLINE_PATH) {
423
- return@future LibraryResult.ofItem(createOfflineMediaItem(), null)
449
+ return@future LibraryResult.ofItem(
450
+ createOfflineMediaItem(
451
+ player
452
+ .awaitBrowser()
453
+ .resolveFormattedError(offlineError(), BrowserPathHelper.OFFLINE_PATH)
454
+ ),
455
+ null,
456
+ )
424
457
  }
425
458
 
426
- // Return the error tile as a non-browsable item so tapping it can't re-browse into a
427
- // failing path (which would push another error page onto the stack).
428
459
  if (mediaId == BrowserPathHelper.ERROR_PATH) {
429
- return@future LibraryResult.ofItem(createBrowseErrorMediaItem(), null)
460
+ // The exception that produced the tile is gone by now — the sentinel is all the controller
461
+ // hands back — so the copy formats for the sentinel under the generic failure code.
462
+ return@future LibraryResult.ofItem(
463
+ createBrowseErrorMediaItem(
464
+ player
465
+ .awaitBrowser()
466
+ .resolveFormattedError(browseFailureError(), BrowserPathHelper.ERROR_PATH)
467
+ ),
468
+ null,
469
+ )
470
+ }
471
+
472
+ if (mediaId == BrowserPathHelper.EMPTY_PATH) {
473
+ // A controller re-reading the tile it was served. The page's path is gone by now — the
474
+ // sentinel is all it hands back — so the copy formats for the sentinel and lands on the
475
+ // app's generic empty wording, or the default.
476
+ return@future LibraryResult.ofItem(
477
+ createEmptyMediaItem(
478
+ player
479
+ .awaitBrowser()
480
+ .resolveFormattedError(emptyContentError(), BrowserPathHelper.EMPTY_PATH)
481
+ ),
482
+ null,
483
+ )
430
484
  }
431
485
 
432
486
  if (mediaId == BrowserPathHelper.GATE_PATH) {
@@ -447,22 +501,9 @@ class MediaSessionCallback(private val player: Player) :
447
501
  return@future LibraryResult.ofItem(createGateMediaItem(outcome.chrome!!), null)
448
502
  }
449
503
 
450
- if (mediaId == BrowserPathHelper.ROOT_PATH || mediaId == BrowserPathHelper.RECENT_PATH) {
451
- return@future LibraryResult.ofItem(
452
- MediaItem.Builder()
453
- .setMediaId(mediaId)
454
- .setMediaMetadata(
455
- MediaMetadata.Builder().setIsBrowsable(true).setIsPlayable(false).build()
456
- )
457
- .build(),
458
- null,
459
- )
460
- }
461
-
462
- // Wait for browser to be registered if it's not available yet
463
504
  val browserManager = player.awaitBrowser().browserManager
464
505
 
465
- // Serve tracks from the track cache first (keyed by url and src). Besides avoiding an HTTP
506
+ // Serve tracks from the track cache first (keyed by path and src). Besides avoiding an HTTP
466
507
  // resolve, this keeps item identity correct for contextual mediaIds: resolve() strips
467
508
  // __trackId and would return the *parent container's* metadata as the item.
468
509
  val browseAuthority =
@@ -534,9 +575,6 @@ class MediaSessionCallback(private val player: Player) :
534
575
  }
535
576
 
536
577
  /**
537
- * Notifies all subscribed controllers to refresh their content. Called internally when network
538
- * state changes to refresh all subscribed paths.
539
- *
540
578
  * Safe to call from any thread: snapshots the subscribed paths and dispatches the session calls
541
579
  * to the main (Media3 application) thread.
542
580
  */
@@ -549,10 +587,8 @@ class MediaSessionCallback(private val player: Player) :
549
587
  }
550
588
 
551
589
  /**
552
- * Notifies external controllers that content at the given path has changed. Controllers
553
- * subscribed to this path will refresh their UI. Safe to call from any thread.
554
- *
555
- * @param path The path where content has changed
590
+ * Notifies external controllers that content at the given path has changed, so controllers
591
+ * subscribed to it refresh their UI. Safe to call from any thread.
556
592
  */
557
593
  fun notifyContentChanged(path: String) {
558
594
  Timber.d("Notifying content changed for path: $path")
@@ -570,10 +606,6 @@ class MediaSessionCallback(private val player: Player) :
570
606
  notifySubscribedChildrenChanged()
571
607
  }
572
608
 
573
- /**
574
- * Called when the browser becomes available after a cold start. Notifies all subscribed
575
- * controllers to refresh their content.
576
- */
577
609
  fun notifyBrowserReady() {
578
610
  Timber.d("Browser ready - notifying all subscribed paths")
579
611
  notifySubscribedChildrenChanged()
@@ -592,8 +624,6 @@ class MediaSessionCallback(private val player: Player) :
592
624
  ): ListenableFuture<LibraryResult<Void>> {
593
625
  Timber.d("onSearch: ${browser.packageName}, query = $query")
594
626
  return scope.future {
595
- // Wait for browser registration like onGetChildren does, so a cold-start voice search
596
- // doesn't fail before JS has configured the browser.
597
627
  val audioBrowser =
598
628
  try {
599
629
  player.awaitBrowser()
@@ -615,28 +645,83 @@ class MediaSessionCallback(private val player: Player) :
615
645
  return@future LibraryResult.ofVoid()
616
646
  }
617
647
 
618
- // Check if search is configured
648
+ // Before the offline branch inside announceSearch: with no search source configured the
649
+ // answer is not-supported, online or not.
619
650
  if (!browserManager.config.hasSearch) {
620
651
  Timber.w("Search requested but no search source configured")
621
652
  return@future LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED)
622
653
  }
623
654
 
624
- try {
625
- // Execute search (automatically caches results at /__search?q=query)
626
- val searchResults = browserManager.search(query)
627
- val resultCount = searchResults.children?.size ?: 0
628
-
629
- Timber.d("Search completed: $resultCount results for query '$query'")
655
+ announceSearch(session, browser, browserManager, query, params)
656
+ LibraryResult.ofVoid()
657
+ }
658
+ }
630
659
 
631
- // Notify Media3 of search results
632
- session.notifySearchResultChanged(browser, query, resultCount, params)
660
+ /**
661
+ * Runs [query] and tells [browser] how many items [onGetSearchResult] will serve for it: the
662
+ * search's own hits, or the single tile standing in for them — a controller told zero never asks
663
+ * for items. Offline keeps the query for [reannounceOfflineSearches]; every other outcome drops
664
+ * it, so one controller holds at most its latest unanswered search.
665
+ */
666
+ private suspend fun announceSearch(
667
+ session: MediaLibraryService.MediaLibrarySession,
668
+ browser: MediaSession.ControllerInfo,
669
+ browserManager: BrowserManager,
670
+ query: String,
671
+ params: MediaLibraryService.LibraryParams?,
672
+ ) {
673
+ if (
674
+ !player.networkMonitor.isOnline.value && browserManager.config.androidControllerOfflineError
675
+ ) {
676
+ Timber.w("Network offline - announcing the offline tile for query: $query")
677
+ offlineSearches.record(browser, query, params)
678
+ session.notifySearchResultChanged(browser, query, 1, params)
679
+ return
680
+ }
681
+ offlineSearches.forget(browser)
682
+
683
+ try {
684
+ // Caches the result set under the query, which onGetSearchResult reads back through
685
+ // getCachedSearchResults.
686
+ val searchResults = browserManager.search(query)
687
+ failedSearch.clear()
688
+ val resultCount = announcedSearchCount(searchResults.normalizedSections)
689
+
690
+ Timber.d("Search announced $resultCount item(s) for query '$query'")
691
+
692
+ session.notifySearchResultChanged(browser, query, resultCount, params)
693
+ } catch (e: Exception) {
694
+ if (e is CancellationException && e !is TimeoutCancellationException) throw e
695
+ Timber.e(e, "Error during search for query: $query")
696
+ // A failed search announces its one item — the browse-error tile onGetSearchResult
697
+ // serves — rather than an ofError the browse list renders as nothing at all. Without
698
+ // the slot the failure is indistinguishable from an empty page by the time that call
699
+ // arrives, and a server error would read as the app's "no results" copy.
700
+ failedSearch.record(query, navigationErrorFor(e))
701
+ session.notifySearchResultChanged(browser, query, 1, params)
702
+ }
703
+ }
633
704
 
634
- LibraryResult.ofVoid()
635
- } catch (e: Exception) {
636
- if (e is CancellationException && e !is TimeoutCancellationException) throw e
637
- Timber.e(e, "Error during search for query: $query")
638
- LibraryResult.ofError(SessionError.ERROR_UNKNOWN)
705
+ /**
706
+ * Re-runs every search that was served the offline tile, now that the network is back. Announcing
707
+ * what the fresh search found — rather than a bare "one item" — keeps the announced count equal
708
+ * to what [onGetSearchResult] will then serve from the cache this fills.
709
+ */
710
+ private suspend fun reannounceOfflineSearches() {
711
+ val pending = offlineSearches.drain()
712
+ if (pending.isEmpty()) return
713
+ val session = mediaLibrarySession ?: return
714
+ val browserManager =
715
+ try {
716
+ player.awaitBrowser().browserManager
717
+ } catch (e: TimeoutCancellationException) {
718
+ Timber.w("Timed out waiting for browser - offline searches not re-announced")
719
+ return
639
720
  }
721
+
722
+ pending.forEach { search ->
723
+ Timber.d("Network back - re-running search: ${search.query}")
724
+ announceSearch(session, search.browser, browserManager, search.query, search.params)
640
725
  }
641
726
  }
642
727
 
@@ -674,26 +759,57 @@ class MediaSessionCallback(private val player: Player) :
674
759
  )
675
760
  }
676
761
 
677
- try {
678
- // Get cached search results from BrowserManager
679
- browserManager.getCachedSearchResults(query)?.let { tracks ->
680
- // Convert to MediaItems
681
- val searchAuthority =
682
- com.audiobrowser.util.ArtworkUris.authorityFor(player.context.packageName)
683
- val mediaItems =
684
- tracks.map { track ->
685
- Timber.d("Search result: ${track.title} (url=${track.url}, src=${track.src})")
686
- TrackFactory.toBrowseMediaItem(track, player.browseArtworkRegistry, searchAuthority)
687
- }
762
+ // Checked before the failure slot, as in onGetChildren: a search that never reached the
763
+ // network is not a server failure, and connectivity returning is what fixes it.
764
+ if (
765
+ !player.networkMonitor.isOnline.value && browserManager.config.androidControllerOfflineError
766
+ ) {
767
+ Timber.w("Network offline - returning the offline tile for query: $query")
768
+ sendBrowseError(session, browser, offline = true)
769
+ return@future LibraryResult.ofItemList(
770
+ ImmutableList.of(
771
+ createOfflineMediaItem(
772
+ audioBrowser.resolveFormattedError(
773
+ offlineError(),
774
+ BrowserPathHelper.createSearchPath(query),
775
+ )
776
+ )
777
+ ),
778
+ params,
779
+ )
780
+ }
688
781
 
689
- val paginatedItems = mediaItems.paginate(page, pageSize)
690
- Timber.d("Returning ${paginatedItems.size} search results")
691
- LibraryResult.ofItemList(ImmutableList.copyOf(paginatedItems), params)
692
- }
693
- ?: run {
694
- Timber.w("No cached search results for query: $query")
695
- LibraryResult.ofItemList(ImmutableList.of(), params)
782
+ // The search itself failed, so this is a failure, not an empty page — same tile and same
783
+ // SessionError the browse catch sends, worded for the error its exception mapped to.
784
+ failedSearch.errorFor(query)?.let { error ->
785
+ sendBrowseError(session, browser, offline = false)
786
+ return@future LibraryResult.ofItemList(
787
+ ImmutableList.of(
788
+ createBrowseErrorMediaItem(
789
+ audioBrowser.resolveFormattedError(error, BrowserPathHelper.createSearchPath(query))
790
+ )
791
+ ),
792
+ params,
793
+ )
794
+ }
795
+
796
+ try {
797
+ val tracks = browserManager.getCachedSearchResults(query)
798
+ if (tracks == null) Timber.w("No cached search results for query: $query")
799
+ // A search with nothing servable — no hits, every hit disabled, or nothing cached under
800
+ // this query — serves the app's copy for the search path instead of the car's bare "No
801
+ // items" (ADR 0001). Something must always come back: onSearch announced one item.
802
+ val mediaItems =
803
+ browseLevelItems(toFlatMediaItems(tracks?.toList().orEmpty())) {
804
+ audioBrowser.resolveFormattedError(
805
+ emptyContentError(),
806
+ BrowserPathHelper.createSearchPath(query),
807
+ )
696
808
  }
809
+
810
+ val paginatedItems = mediaItems.paginate(page, pageSize)
811
+ Timber.d("Returning ${paginatedItems.size} search results")
812
+ LibraryResult.ofItemList(ImmutableList.copyOf(paginatedItems), params)
697
813
  } catch (e: Exception) {
698
814
  if (e is CancellationException && e !is TimeoutCancellationException) throw e
699
815
  Timber.e(e, "Error getting search results for query: $query")
@@ -722,8 +838,9 @@ class MediaSessionCallback(private val player: Player) :
722
838
  return scope.future {
723
839
  val audioBrowser = player.awaitBrowser()
724
840
 
725
- // Helper: returns the current player state unchanged so Media3 doesn't modify playback.
726
- // ExoPlayer is main-thread confined; this future runs on IO, so hop to Main for the reads.
841
+ // The "leave playback alone" answer for handleTrackLoad: returning the current state
842
+ // unchanged is how Media3 is told to do nothing. ExoPlayer is main-thread confined and this
843
+ // future runs on IO, so the reads hop to Main.
727
844
  suspend fun currentPlayerState(): MediaSession.MediaItemsWithStartPosition =
728
845
  withContext(Dispatchers.Main) {
729
846
  val currentItems = player.tracks.map { TrackFactory.toMedia3(it) }
@@ -733,26 +850,26 @@ class MediaSessionCallback(private val player: Player) :
733
850
 
734
851
  val browserManager = audioBrowser.browserManager
735
852
 
736
- // A single tapped item resolves to the contextual url of the list it was
853
+ // A single tapped item resolves to the contextual path of the list it was
737
854
  // tapped in: directly for contextual mediaIds, via the track cache for
738
- // stable-id mediaIds (see BrowserManager.contextualUrlFor). A search
855
+ // stable-id mediaIds (see BrowserManager.contextualPathFor). A search
739
856
  // selection is not a list tap — its queue comes from the search results
740
857
  // (resolveMediaItemsForPlayback), never from a browsed container.
741
- val singleContextualUrl =
858
+ val singleContextualPath =
742
859
  mediaItems
743
860
  .singleOrNull()
744
861
  ?.takeIf { it.requestMetadata.searchQuery == null }
745
- ?.let { browserManager.contextualUrlFor(it.mediaId) }
862
+ ?.let { browserManager.contextualPathFor(it.mediaId) }
746
863
 
747
- // Check if this is a single item from the current queue source
748
- if (singleContextualUrl != null) {
749
- val parentPath = BrowserPathHelper.stripTrackId(singleContextualUrl)
750
- val trackId = BrowserPathHelper.extractTrackId(singleContextualUrl)
864
+ if (singleContextualPath != null) {
865
+ val parentPath = BrowserPathHelper.stripTrackId(singleContextualPath)
866
+ val trackId = BrowserPathHelper.extractTrackId(singleContextualPath)
751
867
 
752
- // Check if queue already came from this parent path - just skip to the track
868
+ // A tap inside the queue's own source list skips rather than rebuilds (exact path first,
869
+ // identity for index-less paths — see indexOfTappedTrack).
753
870
  if (trackId != null && parentPath == player.queueSourcePath) {
754
871
  val queueTracks = withContext(Dispatchers.Main) { player.tracks }
755
- val index = queueTracks.indexOfFirst { it.src == trackId }
872
+ val index = queueTracks.indexOfTappedTrack(singleContextualPath, trackId)
756
873
  if (index >= 0) {
757
874
  Timber.d("Queue already from $parentPath, skipping to index $index")
758
875
  val track = queueTracks[index]
@@ -763,7 +880,6 @@ class MediaSessionCallback(private val player: Player) :
763
880
  index.toDouble(),
764
881
  ::currentPlayerState,
765
882
  ) {
766
- // Return the existing queue items with the new start index
767
883
  val existingItems = queueTracks.map { TrackFactory.toMedia3(it) }
768
884
  MediaSession.MediaItemsWithStartPosition(existingItems, index, startPositionMs)
769
885
  }
@@ -784,9 +900,10 @@ class MediaSessionCallback(private val player: Player) :
784
900
  result.startIndex.toDouble(),
785
901
  ::currentPlayerState,
786
902
  ) {
787
- // If this was a contextual URL expansion, track the source path (only for default behavior)
788
- if (singleContextualUrl != null) {
789
- val parentPath = BrowserPathHelper.stripTrackId(singleContextualUrl)
903
+ // Inside handleTrackLoad's default branch: a consumer that handles the load itself owns
904
+ // the queue, so queueSourcePath must not claim this path on its behalf.
905
+ if (singleContextualPath != null) {
906
+ val parentPath = BrowserPathHelper.stripTrackId(singleContextualPath)
790
907
  withContext(Dispatchers.Main) { player.queueSourcePath = parentPath }
791
908
  }
792
909
  result
@@ -802,8 +919,6 @@ class MediaSessionCallback(private val player: Player) :
802
919
  * using the browse callback. This enables seamless resumption after app restart with the same
803
920
  * player settings (repeat mode, shuffle, playback speed).
804
921
  *
805
- * @param mediaSession The media session
806
- * @param controller The controller requesting resumption
807
922
  * @param isForPlayback True if this should start playback; false if just gathering info for the
808
923
  * boot-time resumption notification (no network; local metadata only).
809
924
  * @see https://developer.android.com/media/media3/session/background-playback#resumption
@@ -825,9 +940,17 @@ class MediaSessionCallback(private val player: Player) :
825
940
  throw IllegalStateException("No playback state to resume")
826
941
  }
827
942
 
828
- // isForPlayback == false is the device-boot-time notification case: network may be
829
- // unavailable and we must not start playback. Return just the locally-stored track with its
830
- // already-local metadata; skip the settings restore and network queue expansion below.
943
+ // The persisted track can carry `disabled` — it became unavailable after
944
+ // it was saved. A disabled track never plays, resumption included
945
+ // (Track.disabled); failing the future refuses without touching the
946
+ // player.
947
+ if (state.track.disabled == true) {
948
+ Timber.w("Refusing resumption of disabled track: ${state.track.title}")
949
+ throw IllegalStateException("Persisted track is disabled")
950
+ }
951
+
952
+ // Info-gathering returns before the settings restore and the queue expansion below: both
953
+ // touch the live player or the network, and neither is permitted here.
831
954
  if (!isForPlayback) {
832
955
  Timber.d("Info-gathering resumption (boot-time); returning stored track without expansion")
833
956
  return@future MediaSession.MediaItemsWithStartPosition(
@@ -840,14 +963,12 @@ class MediaSessionCallback(private val player: Player) :
840
963
  // restore() sets player properties which must happen on main thread
841
964
  withContext(Dispatchers.Main) { player.playbackStateStore.restore() }
842
965
 
843
- val url = state.track.url
844
- Timber.d("Resuming from url=$url, positionMs=${state.positionMs}")
966
+ val path = state.track.path
967
+ Timber.d("Resuming from path=$path, positionMs=${state.positionMs}")
845
968
 
846
- // Wait for browser to be available (JS needs to have configured it)
847
969
  val browserManager = player.awaitBrowser().browserManager
848
970
 
849
- // Try to expand the URL into a full queue
850
- val expanded = url?.let { browserManager.expandQueueFromContextualUrl(it) }
971
+ val expanded = path?.let { browserManager.expandQueueFromContextualPath(it) }
851
972
 
852
973
  if (expanded != null) {
853
974
  val (tracks, selectedIndex) = expanded
@@ -855,9 +976,8 @@ class MediaSessionCallback(private val player: Player) :
855
976
  "Restored ${tracks.size} tracks, starting at index $selectedIndex at ${state.positionMs}ms"
856
977
  )
857
978
 
858
- // Track the source path if this was a contextual URL expansion
859
- if (BrowserPathHelper.isContextual(url)) {
860
- val parentPath = BrowserPathHelper.stripTrackId(url)
979
+ if (BrowserPathHelper.isContextual(path)) {
980
+ val parentPath = BrowserPathHelper.stripTrackId(path)
861
981
  withContext(Dispatchers.Main) { player.queueSourcePath = parentPath }
862
982
  }
863
983
 
@@ -867,7 +987,6 @@ class MediaSessionCallback(private val player: Player) :
867
987
  state.positionMs,
868
988
  )
869
989
  } else {
870
- // Fallback: play just the stored track
871
990
  Timber.d("Queue expansion failed, using stored track: ${state.track.title}")
872
991
  MediaSession.MediaItemsWithStartPosition(
873
992
  ImmutableList.of(TrackFactory.toMedia3(state.track)),
@@ -878,3 +997,163 @@ class MediaSessionCallback(private val player: Player) :
878
997
  }
879
998
  }
880
999
  }
1000
+
1001
+ /**
1002
+ * Builds a non-browsable, non-playable [MediaItem] used to surface an error inside an Android Auto
1003
+ * / AAOS browse list. Rendered as a greyed-out, non-interactive tile, which is the only
1004
+ * side-effect-free way to communicate a browse failure to legacy controllers (Media3 drops
1005
+ * [LibraryResult.ofError] on the legacy browse bridge, leaving an empty "No items" screen).
1006
+ *
1007
+ * Tiles are the only in-browse error signal that exists. Verified on a head unit (2026-06): the
1008
+ * Android Auto browse list renders no error text for a transient [SessionError], for sticky
1009
+ * non-fatal replication, or for fatal replication (setLibraryErrorReplicationMode). Fatal
1010
+ * replication does surface the message on the playback screen, but at the cost of presenting the
1011
+ * session as STATE_ERROR with no actions, hiding the now-playing item and transport controls.
1012
+ */
1013
+ internal fun createErrorMediaItem(mediaId: String, title: String, subtitle: String): MediaItem =
1014
+ MediaItem.Builder()
1015
+ .setMediaId(mediaId)
1016
+ .setMediaMetadata(
1017
+ MediaMetadata.Builder()
1018
+ .setTitle(title)
1019
+ .setSubtitle(subtitle)
1020
+ // Android Auto ignores these flags and drills into a tapped tile anyway; onGetChildren
1021
+ // returns an empty list for the sentinel paths, so it dead-ends at "No Items" rather
1022
+ // than stacking error pages.
1023
+ .setIsBrowsable(false)
1024
+ .setIsPlayable(false)
1025
+ .build()
1026
+ )
1027
+ .build()
1028
+
1029
+ /**
1030
+ * The formatted form every tile is built from: the message renders as the subtitle, with newlines
1031
+ * collapsed to spaces — list rows are single-line.
1032
+ */
1033
+ private fun createErrorMediaItem(mediaId: String, formatted: FormattedNavigationError): MediaItem =
1034
+ createErrorMediaItem(
1035
+ mediaId = mediaId,
1036
+ title = formatted.title,
1037
+ subtitle = formatted.message?.replace('\n', ' ') ?: "",
1038
+ )
1039
+
1040
+ /**
1041
+ * Builds the empty tile from the app's formatted `empty-content` copy for the page's path, so an
1042
+ * empty Favorites tab and an empty search can be worded apart (ADR 0001). Same shape as the error
1043
+ * tiles — non-browsable, non-playable — and deliberately NOT accompanied by a SessionError: an
1044
+ * empty page is a successful resolve.
1045
+ */
1046
+ internal fun createEmptyMediaItem(formatted: FormattedNavigationError): MediaItem =
1047
+ createErrorMediaItem(BrowserPathHelper.EMPTY_PATH, formatted)
1048
+
1049
+ /**
1050
+ * Builds the offline tile from the app's formatted `network-error` copy for the path being served,
1051
+ * so one browse list never mixes the app's locale with the device's.
1052
+ */
1053
+ internal fun createOfflineMediaItem(formatted: FormattedNavigationError): MediaItem =
1054
+ createErrorMediaItem(BrowserPathHelper.OFFLINE_PATH, formatted)
1055
+
1056
+ /**
1057
+ * Builds the tile a failed browse or search serves, from the formatted copy for the error the
1058
+ * request's exception mapped to. The true offline case is handled separately by the networkMonitor
1059
+ * guards; anything reaching a catch block is an online-but-failed request (e.g. server down, bad
1060
+ * status), so it must NOT be labelled "no internet connection".
1061
+ */
1062
+ internal fun createBrowseErrorMediaItem(formatted: FormattedNavigationError): MediaItem =
1063
+ createErrorMediaItem(BrowserPathHelper.ERROR_PATH, formatted)
1064
+
1065
+ /**
1066
+ * What a non-root browse level or a search serves: its own items, or the lone empty tile when it
1067
+ * came back with none. [formatted] is a JS hop, so it runs only when there is nothing to show. The
1068
+ * root is not served through here — an empty tab list is a different condition.
1069
+ */
1070
+ internal suspend fun browseLevelItems(
1071
+ children: List<MediaItem>,
1072
+ formatted: suspend () -> FormattedNavigationError,
1073
+ ): List<MediaItem> = children.ifEmpty { listOf(createEmptyMediaItem(formatted())) }
1074
+
1075
+ /** The navigation error an empty page raises: no message of its own, the path carries the copy. */
1076
+ internal fun emptyContentError(): NavigationError =
1077
+ NavigationError(NavigationErrorType.EMPTY_CONTENT, "", null, null)
1078
+
1079
+ /** The navigation error a browse or search served while offline raises. */
1080
+ internal fun offlineError(): NavigationError =
1081
+ NavigationError(NavigationErrorType.NETWORK_ERROR, "", null, null)
1082
+
1083
+ /** The failure a re-read of the browse-error tile stands on, its own exception long gone. */
1084
+ internal fun browseFailureError(): NavigationError =
1085
+ NavigationError(NavigationErrorType.UNKNOWN_ERROR, "", null, null)
1086
+
1087
+ /**
1088
+ * The item count [MediaSessionCallback.onSearch] announces for a search that ran. Disabled tracks
1089
+ * hide on Android Auto (Track.disabled), so they never inflate it; and a search with nothing
1090
+ * servable announces one — the empty tile [MediaSessionCallback.onGetSearchResult] serves in their
1091
+ * place — because a controller told zero never asks for items. The gate and failure branches
1092
+ * announce their own single tile by the same rule.
1093
+ */
1094
+ internal fun announcedSearchCount(sections: List<Section>?): Int =
1095
+ (sections?.sumOf { section -> section.children.count { it.disabled != true } } ?: 0)
1096
+ .coerceAtLeast(1)
1097
+
1098
+ /**
1099
+ * The search each controller was last served the offline tile for. Media3 addresses a search result
1100
+ * by (controller, query) and keeps no record of its own, and a search has no subscription the way a
1101
+ * browse level does — so without this the offline tile stays on screen until the user searches
1102
+ * again. Only an offline-served query is kept: a search that ran found what it found, and one that
1103
+ * failed did so with a network.
1104
+ *
1105
+ * Written from the callback's IO scope and read from the connectivity observer, so every accessor
1106
+ * is synchronized.
1107
+ */
1108
+ internal class OfflineSearchRegistry {
1109
+ private val searches = mutableMapOf<MediaSession.ControllerInfo, OfflineSearch>()
1110
+
1111
+ @Synchronized
1112
+ fun record(
1113
+ browser: MediaSession.ControllerInfo,
1114
+ query: String,
1115
+ params: MediaLibraryService.LibraryParams?,
1116
+ ) {
1117
+ searches[browser] = OfflineSearch(browser, query, params)
1118
+ }
1119
+
1120
+ @Synchronized
1121
+ fun forget(browser: MediaSession.ControllerInfo) {
1122
+ searches.remove(browser)
1123
+ }
1124
+
1125
+ /** Takes every record: a re-run that finds the network gone again records its own. */
1126
+ @Synchronized
1127
+ fun drain(): List<OfflineSearch> = searches.values.toList().also { searches.clear() }
1128
+ }
1129
+
1130
+ /** One controller's unanswered search: everything announcing it again needs. */
1131
+ internal data class OfflineSearch(
1132
+ val browser: MediaSession.ControllerInfo,
1133
+ val query: String,
1134
+ val params: MediaLibraryService.LibraryParams?,
1135
+ )
1136
+
1137
+ /**
1138
+ * Remembers the query whose search threw, so [MediaSessionCallback.onGetSearchResult] serves the
1139
+ * browse-error tile instead of the empty one — by then the failure is otherwise indistinguishable
1140
+ * from a page that found nothing. Written and read from the callback's IO scope.
1141
+ *
1142
+ * One slot, matching BrowserManager's single-entry result cache: two failures in a row leave only
1143
+ * the later query recorded, so a late fetch of the earlier one serves the empty tile instead of the
1144
+ * error tile. Acceptable while only the most recent search's results are retrievable at all.
1145
+ */
1146
+ internal class FailedSearchSlot {
1147
+ @Volatile private var failed: Pair<String, NavigationError>? = null
1148
+
1149
+ fun record(query: String, error: NavigationError) {
1150
+ failed = query to error
1151
+ }
1152
+
1153
+ fun clear() {
1154
+ failed = null
1155
+ }
1156
+
1157
+ /** The error [query] failed with, or null when it is not the query that failed. */
1158
+ fun errorFor(query: String): NavigationError? = failed?.takeIf { it.first == query }?.second
1159
+ }