react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (660) hide show
  1. package/README.md +35 -21
  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 +389 -164
  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 +48 -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 +24 -5
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -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 +56 -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 +108 -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 +87 -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 +38 -0
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -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 +147 -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 +154 -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 +41 -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/Browser/BrowserConfig.swift +102 -5
  76. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  77. package/ios/Browser/BrowserManager.swift +230 -205
  78. package/ios/Browser/BrowserPathHelper.swift +44 -44
  79. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  80. package/ios/Browser/JsonModels.swift +264 -106
  81. package/ios/Browser/SearchDrillIn.swift +7 -7
  82. package/ios/Browser/SectionPresentation.swift +64 -0
  83. package/ios/Browser/SectionScope.swift +58 -24
  84. package/ios/Browser/StyleResolver.swift +58 -0
  85. package/ios/Browser/TabBarEntries.swift +9 -3
  86. package/ios/Browser/TrackIdTemplate.swift +37 -0
  87. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  88. package/ios/CarPlay/CarPlayController.swift +227 -85
  89. package/ios/CarPlay/CarPlayImageLoader.swift +42 -20
  90. package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
  91. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  92. package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
  93. package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
  94. package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
  95. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  96. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  97. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  98. package/ios/Extension/Section+Copying.swift +29 -0
  99. package/ios/Extension/String+IcyCharset.swift +21 -0
  100. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  101. package/ios/Extension/Track+Copying.swift +8 -12
  102. package/ios/Extension/Track+Identity.swift +18 -0
  103. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  104. package/ios/Http/HttpClient.swift +8 -2
  105. package/ios/HybridAudioBrowser.swift +253 -100
  106. package/ios/Model/NitroTypeStubs.swift +125 -23
  107. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  108. package/ios/Model/RemoteCommand.swift +18 -0
  109. package/ios/Model/TrackPlayerError.swift +2 -2
  110. package/ios/Option/SessionCategories.swift +3 -2
  111. package/ios/Player/MediaIntentCriteria.swift +17 -9
  112. package/ios/Player/MediaLoader.swift +33 -9
  113. package/ios/Player/NowPlayingUpdater.swift +54 -6
  114. package/ios/Player/PlaybackCoordinator.swift +13 -6
  115. package/ios/Player/PlaybackErrorHandler.swift +4 -4
  116. package/ios/Player/PlaybackTimer.swift +8 -2
  117. package/ios/Player/QueueManager.swift +46 -13
  118. package/ios/Player/RetryManager.swift +80 -18
  119. package/ios/Player/SeekCompletionHandler.swift +3 -1
  120. package/ios/Player/ShuffleOrder.swift +33 -0
  121. package/ios/Player/SleepTimerHandling.swift +13 -0
  122. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  123. package/ios/RemoteCommand/RemoteCommandController.swift +62 -24
  124. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  125. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  126. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  127. package/ios/Tests/JsonModelsTests.swift +150 -1
  128. package/ios/Tests/LRUCacheTests.swift +111 -0
  129. package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
  130. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  131. package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
  132. package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
  133. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  134. package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
  135. package/ios/Tests/QueueManagerTests.swift +122 -10
  136. package/ios/Tests/RemoteCommandControllerTests.swift +172 -0
  137. package/ios/Tests/RemoteCommandTests.swift +67 -0
  138. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  139. package/ios/Tests/RetryManagerTests.swift +89 -0
  140. package/ios/Tests/SearchDrillInTests.swift +16 -16
  141. package/ios/Tests/SectionPresentationTests.swift +90 -0
  142. package/ios/Tests/SectionScopeTests.swift +87 -55
  143. package/ios/Tests/StyleResolverTests.swift +127 -0
  144. package/ios/Tests/Support/TestTypes.swift +8 -1
  145. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  146. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  147. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  148. package/ios/Tests/TrackIdentityTests.swift +20 -0
  149. package/ios/Tests/TrackSelectorTests.swift +168 -33
  150. package/ios/TrackPlayer.swift +63 -35
  151. package/ios/TrackPlayerCallbacks.swift +5 -38
  152. package/ios/TrackSelector.swift +45 -30
  153. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  154. package/ios/Util/LRUCache.swift +4 -1
  155. package/ios/Util/NetworkMonitor.swift +43 -26
  156. package/ios/Util/SFSymbolRenderer.swift +24 -2
  157. package/ios/Util/SVGProcessor.swift +9 -2
  158. package/lib/commonjs/AudioBrowser.js +8 -8
  159. package/lib/commonjs/AudioBrowser.js.map +1 -1
  160. package/lib/commonjs/CLAUDE.md +52 -0
  161. package/lib/commonjs/features/browser-config.js +12 -8
  162. package/lib/commonjs/features/browser-config.js.map +1 -1
  163. package/lib/commonjs/features/browser.js +11 -3
  164. package/lib/commonjs/features/browser.js.map +1 -1
  165. package/lib/commonjs/features/carConnection.js +1 -1
  166. package/lib/commonjs/features/equalizer.js +1 -1
  167. package/lib/commonjs/features/errors.js +3 -3
  168. package/lib/commonjs/features/favorites.js +14 -11
  169. package/lib/commonjs/features/favorites.js.map +1 -1
  170. package/lib/commonjs/features/gate.js +16 -1
  171. package/lib/commonjs/features/gate.js.map +1 -1
  172. package/lib/commonjs/features/metadata.js +20 -3
  173. package/lib/commonjs/features/metadata.js.map +1 -1
  174. package/lib/commonjs/features/network.js +1 -1
  175. package/lib/commonjs/features/nowPlaying.js +1 -1
  176. package/lib/commonjs/features/output.js +1 -1
  177. package/lib/commonjs/features/playback/controls.js +4 -2
  178. package/lib/commonjs/features/playback/controls.js.map +1 -1
  179. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  180. package/lib/commonjs/features/playback/playing.js +1 -1
  181. package/lib/commonjs/features/playback/progress.js +3 -2
  182. package/lib/commonjs/features/playback/progress.js.map +1 -1
  183. package/lib/commonjs/features/playback/rate.js +8 -1
  184. package/lib/commonjs/features/playback/rate.js.map +1 -1
  185. package/lib/commonjs/features/playback/state.js +3 -4
  186. package/lib/commonjs/features/playback/state.js.map +1 -1
  187. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  188. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  189. package/lib/commonjs/features/playback/volume.js +1 -1
  190. package/lib/commonjs/features/player/options.js +48 -34
  191. package/lib/commonjs/features/player/options.js.map +1 -1
  192. package/lib/commonjs/features/player/setup.js +18 -9
  193. package/lib/commonjs/features/player/setup.js.map +1 -1
  194. package/lib/commonjs/features/player/validateOptions.js +23 -0
  195. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  196. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  197. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  198. package/lib/commonjs/features/queue/queue.js +13 -6
  199. package/lib/commonjs/features/queue/queue.js.map +1 -1
  200. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  201. package/lib/commonjs/features/queue/shuffle.js +1 -1
  202. package/lib/commonjs/features/remoteControls.js +13 -92
  203. package/lib/commonjs/features/remoteControls.js.map +1 -1
  204. package/lib/commonjs/features/sleepTimer.js +13 -1
  205. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  206. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  207. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  208. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  209. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  210. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  211. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  212. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  213. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  214. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  215. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  216. package/lib/commonjs/web/NativeAudioBrowser.js +200 -75
  217. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  218. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  219. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  220. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  221. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  222. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  223. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  224. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  225. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  226. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  227. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  228. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  229. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  230. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  231. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  232. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  233. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  234. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  235. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  236. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  237. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  238. package/lib/commonjs/web/browser/sections.js +89 -0
  239. package/lib/commonjs/web/browser/sections.js.map +1 -0
  240. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  241. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  242. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  243. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  244. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  245. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  246. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  247. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  248. package/lib/module/AudioBrowser.js +2 -1
  249. package/lib/module/AudioBrowser.js.map +1 -1
  250. package/lib/module/CLAUDE.md +52 -0
  251. package/lib/module/features/browser-config.js +12 -8
  252. package/lib/module/features/browser-config.js.map +1 -1
  253. package/lib/module/features/browser.js +11 -3
  254. package/lib/module/features/browser.js.map +1 -1
  255. package/lib/module/features/carConnection.js +1 -1
  256. package/lib/module/features/equalizer.js +1 -1
  257. package/lib/module/features/errors.js +3 -3
  258. package/lib/module/features/favorites.js +14 -11
  259. package/lib/module/features/favorites.js.map +1 -1
  260. package/lib/module/features/gate.js +16 -1
  261. package/lib/module/features/gate.js.map +1 -1
  262. package/lib/module/features/metadata.js +20 -3
  263. package/lib/module/features/metadata.js.map +1 -1
  264. package/lib/module/features/network.js +1 -1
  265. package/lib/module/features/nowPlaying.js +1 -1
  266. package/lib/module/features/output.js +1 -1
  267. package/lib/module/features/playback/controls.js +4 -2
  268. package/lib/module/features/playback/controls.js.map +1 -1
  269. package/lib/module/features/playback/playWhenReady.js +1 -1
  270. package/lib/module/features/playback/playing.js +1 -1
  271. package/lib/module/features/playback/progress.js +3 -2
  272. package/lib/module/features/playback/progress.js.map +1 -1
  273. package/lib/module/features/playback/rate.js +8 -1
  274. package/lib/module/features/playback/rate.js.map +1 -1
  275. package/lib/module/features/playback/state.js +3 -4
  276. package/lib/module/features/playback/state.js.map +1 -1
  277. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  278. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  279. package/lib/module/features/playback/volume.js +1 -1
  280. package/lib/module/features/player/options.js +47 -32
  281. package/lib/module/features/player/options.js.map +1 -1
  282. package/lib/module/features/player/setup.js +17 -8
  283. package/lib/module/features/player/setup.js.map +1 -1
  284. package/lib/module/features/player/validateOptions.js +19 -0
  285. package/lib/module/features/player/validateOptions.js.map +1 -0
  286. package/lib/module/features/queue/activeTrack.js +15 -4
  287. package/lib/module/features/queue/activeTrack.js.map +1 -1
  288. package/lib/module/features/queue/queue.js +13 -6
  289. package/lib/module/features/queue/queue.js.map +1 -1
  290. package/lib/module/features/queue/repeatMode.js +1 -1
  291. package/lib/module/features/queue/shuffle.js +1 -1
  292. package/lib/module/features/remoteControls.js +13 -92
  293. package/lib/module/features/remoteControls.js.map +1 -1
  294. package/lib/module/features/sleepTimer.js +13 -1
  295. package/lib/module/features/sleepTimer.js.map +1 -1
  296. package/lib/module/test-utils/reactNativeStub.js +58 -0
  297. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  298. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  299. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  300. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  301. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  302. package/lib/module/utils/getTrackIdentity.js +20 -0
  303. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  304. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  305. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  306. package/lib/module/web/NativeAudioBrowser.js +200 -75
  307. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  308. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  309. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  310. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  311. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  312. package/lib/module/web/TrackPlayer/Player.js +254 -39
  313. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  314. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  315. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  316. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  317. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  318. package/lib/module/web/browser/BrowserManager.js +175 -100
  319. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  320. package/lib/module/web/browser/FavoriteManager.js +31 -21
  321. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  322. package/lib/module/web/browser/SearchManager.js +18 -23
  323. package/lib/module/web/browser/SearchManager.js.map +1 -1
  324. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  325. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  326. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  327. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  328. package/lib/module/web/browser/sections.js +83 -0
  329. package/lib/module/web/browser/sections.js.map +1 -0
  330. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  331. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  332. package/lib/module/web/http/followMediaRedirect.js +215 -0
  333. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  334. package/lib/module/web/player/NowPlayingManager.js +4 -3
  335. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  336. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  337. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  338. package/lib/typescript/package.json +1 -0
  339. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  340. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  341. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  342. package/lib/typescript/src/features/browser.d.ts +10 -2
  343. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  344. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  345. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  346. package/lib/typescript/src/features/errors.d.ts +3 -3
  347. package/lib/typescript/src/features/favorites.d.ts +14 -11
  348. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  349. package/lib/typescript/src/features/gate.d.ts +18 -2
  350. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  351. package/lib/typescript/src/features/metadata.d.ts +20 -3
  352. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  353. package/lib/typescript/src/features/network.d.ts +1 -1
  354. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  355. package/lib/typescript/src/features/output.d.ts +1 -1
  356. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  357. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  358. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  359. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  360. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  361. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  362. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  363. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  364. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  365. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  366. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  367. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  368. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  369. package/lib/typescript/src/features/player/options.d.ts +108 -56
  370. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  371. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  372. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  373. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  374. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  375. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  376. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  377. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  378. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  379. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  380. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  381. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  382. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  383. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  384. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  385. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  386. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  387. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  388. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  389. package/lib/typescript/src/types/browser-nodes.d.ts +380 -126
  390. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  391. package/lib/typescript/src/types/browser.d.ts +82 -57
  392. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  393. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  394. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  395. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  396. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  397. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  398. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  399. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  400. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  401. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  402. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
  403. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
  404. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  405. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
  406. package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
  407. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  408. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  409. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
  410. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
  411. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
  412. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
  413. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
  414. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
  415. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
  416. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
  417. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  418. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
  419. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
  420. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +7 -10
  421. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
  422. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
  423. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
  424. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  425. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +45 -25
  426. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
  427. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  428. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  429. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
  430. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +36 -11
  431. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
  432. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +15 -12
  433. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +7 -10
  434. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
  435. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  436. package/nitrogen/generated/android/c++/JOptions.hpp +7 -10
  437. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
  438. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  439. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  440. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  441. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +57 -49
  442. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  443. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  444. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  445. package/nitrogen/generated/android/c++/JTrack.hpp +25 -46
  446. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
  447. package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
  448. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  449. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  450. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
  451. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
  452. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  453. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  454. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  455. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
  456. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  457. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
  458. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  459. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  460. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +8 -5
  461. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
  462. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  463. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  464. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  465. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
  466. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -15
  467. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +15 -6
  468. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
  469. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  470. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -12
  471. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
  472. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
  473. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
  474. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +244 -133
  475. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +31 -16
  476. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +33 -18
  477. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  478. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  479. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  480. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  481. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  482. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  483. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  484. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  485. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  486. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  487. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  488. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  489. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  490. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  491. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  492. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  493. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  494. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  495. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  496. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  497. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  498. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  499. package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
  500. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  501. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  502. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  503. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  504. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  505. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  506. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  507. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  508. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  509. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  510. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  511. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  512. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  513. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  514. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  515. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  516. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  517. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  518. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  519. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  520. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  521. package/package.json +24 -11
  522. package/src/AudioBrowser.ts +2 -1
  523. package/src/CLAUDE.md +52 -0
  524. package/src/features/battery.test.ts +79 -0
  525. package/src/features/browser-config.test.ts +21 -6
  526. package/src/features/browser-config.ts +12 -8
  527. package/src/features/browser.test.ts +1 -1
  528. package/src/features/browser.ts +11 -3
  529. package/src/features/carConnection.ts +1 -1
  530. package/src/features/equalizer.ts +1 -1
  531. package/src/features/errors.ts +3 -3
  532. package/src/features/favorites.ts +14 -11
  533. package/src/features/gate.ts +16 -1
  534. package/src/features/hook-wiring.test.ts +302 -0
  535. package/src/features/metadata.ts +20 -3
  536. package/src/features/network.ts +1 -1
  537. package/src/features/nowPlaying.ts +1 -1
  538. package/src/features/output.ts +1 -1
  539. package/src/features/playback/controls.ts +4 -2
  540. package/src/features/playback/playWhenReady.ts +1 -1
  541. package/src/features/playback/playing.ts +1 -1
  542. package/src/features/playback/progress.test.ts +169 -0
  543. package/src/features/playback/progress.ts +3 -2
  544. package/src/features/playback/rate.ts +8 -1
  545. package/src/features/playback/state.ts +3 -4
  546. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  547. package/src/features/playback/trackPlaybackTime.ts +1 -6
  548. package/src/features/playback/volume.ts +1 -1
  549. package/src/features/player/options.ts +126 -76
  550. package/src/features/player/setup.test.ts +24 -7
  551. package/src/features/player/setup.ts +22 -10
  552. package/src/features/player/validateOptions.ts +21 -0
  553. package/src/features/queue/activeTrack.ts +15 -4
  554. package/src/features/queue/queue.ts +13 -6
  555. package/src/features/queue/repeatMode.ts +1 -1
  556. package/src/features/queue/shuffle.ts +1 -1
  557. package/src/features/remoteControls.ts +13 -92
  558. package/src/features/sleepTimer.test.ts +172 -0
  559. package/src/features/sleepTimer.ts +9 -1
  560. package/src/nitro-stub-drift.test.ts +277 -0
  561. package/src/public-surface.test.ts +147 -0
  562. package/src/specs/audio-browser.nitro.ts +5 -4
  563. package/src/style-resolution.test.ts +210 -0
  564. package/src/test-utils/reactNativeStub.ts +60 -0
  565. package/src/types/browser-native.ts +7 -3
  566. package/src/types/browser-nodes.ts +396 -134
  567. package/src/types/browser.ts +85 -59
  568. package/src/utils/LazyNativeEmitter.ts +28 -15
  569. package/src/utils/NativeUpdatedValue.ts +29 -13
  570. package/src/utils/getTrackIdentity.ts +20 -0
  571. package/src/utils/useDebug.test.ts +182 -0
  572. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  573. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  574. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  575. package/src/web/NativeAudioBrowser.ts +239 -82
  576. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  577. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  578. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  579. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  580. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  581. package/src/web/TrackPlayer/Player.ts +284 -37
  582. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  583. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  584. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  585. package/src/web/__tests__/lifecycle.test.ts +345 -0
  586. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  587. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  588. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  589. package/src/web/__tests__/retry.test.ts +169 -0
  590. package/src/web/browser/BrowserManager.ts +209 -111
  591. package/src/web/browser/FavoriteManager.test.ts +84 -0
  592. package/src/web/browser/FavoriteManager.ts +28 -23
  593. package/src/web/browser/SearchManager.test.ts +75 -0
  594. package/src/web/browser/SearchManager.ts +25 -28
  595. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  596. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  597. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  598. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  599. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  600. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  601. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  602. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  603. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  604. package/src/web/browser/sections.test.ts +91 -0
  605. package/src/web/browser/sections.ts +72 -0
  606. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  607. package/src/web/http/RequestConfigBuilder.ts +140 -33
  608. package/src/web/http/followMediaRedirect.test.ts +289 -0
  609. package/src/web/http/followMediaRedirect.ts +253 -0
  610. package/src/web/player/NowPlayingManager.ts +4 -3
  611. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  612. package/src/web/util/BrowserPathHelper.ts +61 -13
  613. package/website/guide/android-auto.md +114 -0
  614. package/website/guide/android-certificates.md +409 -0
  615. package/website/guide/artwork.md +255 -0
  616. package/website/guide/audio-output.md +135 -0
  617. package/website/guide/automotive.md +86 -0
  618. package/website/guide/basic-usage.md +151 -0
  619. package/website/guide/battery.md +115 -0
  620. package/website/guide/browser.md +581 -0
  621. package/website/guide/carplay.md +620 -0
  622. package/website/guide/configuration.md +173 -0
  623. package/website/guide/equalizer.md +231 -0
  624. package/website/guide/errors.md +310 -0
  625. package/website/guide/favorites.md +154 -0
  626. package/website/guide/gate.md +143 -0
  627. package/website/guide/getting-started.md +46 -0
  628. package/website/guide/hooks.md +238 -0
  629. package/website/guide/live-streams.md +125 -0
  630. package/website/guide/metadata.md +239 -0
  631. package/website/guide/migrating-from-track-player.md +244 -0
  632. package/website/guide/native-callback-fetch.md +127 -0
  633. package/website/guide/network.md +86 -0
  634. package/website/guide/now-playing.md +175 -0
  635. package/website/guide/playback.md +328 -0
  636. package/website/guide/queue.md +300 -0
  637. package/website/guide/remote-controls.md +256 -0
  638. package/website/guide/search.md +255 -0
  639. package/website/guide/sleep-timer.md +268 -0
  640. package/website/guide/track.md +247 -0
  641. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  642. package/ios/Model/MediaURL.swift +0 -67
  643. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  644. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  645. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  646. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  647. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  648. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  649. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  653. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  654. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  655. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  656. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  657. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  658. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  659. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  660. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -0,0 +1,49 @@
1
+ package com.audiobrowser.model
2
+
3
+ import java.nio.charset.Charset
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Test
6
+
7
+ class IcyTitleTest {
8
+ /** What media3's ISO-8859-1 fallback yields for [text] sent as [charset]. */
9
+ private fun latin1Shaped(text: String, charset: String) =
10
+ String(text.toByteArray(Charset.forName(charset)), Charsets.ISO_8859_1)
11
+
12
+ @Test
13
+ fun `re-decodes a legacy code page title`() {
14
+ val title = latin1Shaped("Артист - Песня", "windows-1251")
15
+ assertEquals("Артист - Песня", IcyTitle.redecode(title, "windows-1251"))
16
+ }
17
+
18
+ @Test
19
+ fun `re-decodes thai`() {
20
+ val title = latin1Shaped("ศิลปิน - เพลง", "windows-874")
21
+ assertEquals("ศิลปิน - เพลง", IcyTitle.redecode(title, "windows-874"))
22
+ }
23
+
24
+ @Test
25
+ fun `re-decodes a title the station utf-8 encoded from legacy bytes`() {
26
+ // media3 decodes the valid UTF-8 to the same Latin-1-shaped string the fallback would give.
27
+ val doubleEncoded = latin1Shaped("Artist - เพลง", "windows-874").toByteArray(Charsets.UTF_8)
28
+ assertEquals(
29
+ "Artist - เพลง",
30
+ IcyTitle.redecode(String(doubleEncoded, Charsets.UTF_8), "windows-874"),
31
+ )
32
+ }
33
+
34
+ @Test
35
+ fun `leaves a title already in another script`() {
36
+ assertEquals("Песня", IcyTitle.redecode("Песня", "windows-1251"))
37
+ }
38
+
39
+ @Test
40
+ fun `leaves ascii`() {
41
+ assertEquals("Plain Title", IcyTitle.redecode("Plain Title", "windows-1251"))
42
+ }
43
+
44
+ @Test
45
+ fun `ignores a charset the runtime cannot decode with`() {
46
+ val title = latin1Shaped("Песня", "windows-1251")
47
+ assertEquals(title, IcyTitle.redecode(title, "no-such-charset"))
48
+ }
49
+ }
@@ -0,0 +1,385 @@
1
+ package com.audiobrowser.player
2
+
3
+ import android.os.Bundle
4
+ import androidx.media3.common.MediaItem
5
+ import androidx.media3.common.MediaMetadata
6
+ import androidx.media3.session.MediaLibraryService
7
+ import androidx.media3.session.MediaSession
8
+ import com.audiobrowser.NavigationErrorException
9
+ import com.audiobrowser.TestFixtures
10
+ import com.audiobrowser.browser.HttpStatusException
11
+ import com.audiobrowser.browser.NetworkException
12
+ import com.audiobrowser.defaultFormattedError
13
+ import com.audiobrowser.formattedOrDefault
14
+ import com.audiobrowser.navigationErrorFor
15
+ import com.audiobrowser.util.BrowserPathHelper
16
+ import com.margelo.nitro.audiobrowser.FormattedNavigationError
17
+ import com.margelo.nitro.audiobrowser.NavigationError
18
+ import com.margelo.nitro.audiobrowser.NavigationErrorType
19
+ import kotlin.coroutines.Continuation
20
+ import kotlin.coroutines.resume
21
+ import kotlin.coroutines.suspendCoroutine
22
+ import kotlinx.coroutines.test.runTest
23
+ import org.junit.Assert.assertEquals
24
+ import org.junit.Assert.assertFalse
25
+ import org.junit.Assert.assertNotEquals
26
+ import org.junit.Assert.assertNotNull
27
+ import org.junit.Assert.assertNull
28
+ import org.junit.Assert.assertSame
29
+ import org.junit.Assert.assertTrue
30
+ import org.junit.Test
31
+ import org.junit.runner.RunWith
32
+ import org.robolectric.RobolectricTestRunner
33
+
34
+ /**
35
+ * Tests the tiles an Android Auto browse level serves in place of a list: the empty tile of a page
36
+ * that resolved with zero children, and the offline and failure tiles, all worded by the app's
37
+ * `formatNavigationError` (ADR 0001). MediaSessionCallback itself needs a live MediaLibrarySession,
38
+ * so the pieces under test are the ones carved out of it: [browseLevelItems] (the empty-branch
39
+ * decision), the tile builders, [BrowserPathHelper.isDeadEndPath] (the drill-in guard),
40
+ * [announcedSearchCount] (what onSearch tells the controller to ask for), [FailedSearchSlot] (a
41
+ * failed search, which must not read as an empty one), [OfflineSearchRegistry] (the searches
42
+ * connectivity returning re-runs), [navigationErrorFor] (the one exception mapping browse, search
43
+ * and the JS-facing `search` rejection share) and [formattedOrDefault] (the formatter hop's
44
+ * fallback rule, which sits off the JNI-backed Promise edge for exactly this reason).
45
+ */
46
+ @RunWith(RobolectricTestRunner::class)
47
+ class BrowseTilesTest {
48
+
49
+ private fun controller(packageName: String, uid: Int): MediaSession.ControllerInfo =
50
+ MediaSession.ControllerInfo.createTestOnlyControllerInfo(
51
+ packageName,
52
+ /* pid= */ 0,
53
+ uid,
54
+ /* libraryVersion= */ 2,
55
+ /* interfaceVersion= */ 2,
56
+ /* trusted= */ true,
57
+ Bundle.EMPTY,
58
+ /* isPackageNameVerified= */ true,
59
+ )
60
+
61
+ private fun track(title: String): MediaItem =
62
+ MediaItem.Builder()
63
+ .setMediaId("/station/$title")
64
+ .setMediaMetadata(MediaMetadata.Builder().setTitle(title).build())
65
+ .build()
66
+
67
+ /**
68
+ * Stands in for an app's `formatNavigationError`: different copy per error and per path, the
69
+ * point of ADR 0001. The search branches match the prefix the library builds car search paths
70
+ * from.
71
+ */
72
+ private fun appCopy(error: NavigationError, path: String): FormattedNavigationError {
73
+ val isSearch = path.startsWith(BrowserPathHelper.SEARCH_PATH_PREFIX)
74
+ return when {
75
+ error.code == NavigationErrorType.NETWORK_ERROR && isSearch ->
76
+ FormattedNavigationError("Search needs a connection", "Reconnect and try again.")
77
+ error.code == NavigationErrorType.NETWORK_ERROR ->
78
+ FormattedNavigationError("You're offline", "Check your connection.")
79
+ error.code != NavigationErrorType.EMPTY_CONTENT ->
80
+ FormattedNavigationError("We couldn't load that", "Try again in a moment.")
81
+ path == "/favorites" ->
82
+ FormattedNavigationError("No favorites yet", "Tap the heart on a station to add it here.")
83
+ isSearch -> FormattedNavigationError("No results found", "Try a different search.")
84
+ else -> FormattedNavigationError("Nothing here", null)
85
+ }
86
+ }
87
+
88
+ @Test
89
+ fun `an empty favorites page serves one tile carrying the app's copy for that path`() = runTest {
90
+ val items = browseLevelItems(emptyList()) { appCopy(emptyContentError(), "/favorites") }
91
+
92
+ assertEquals(1, items.size)
93
+ val tile = items.single()
94
+ assertEquals(BrowserPathHelper.EMPTY_PATH, tile.mediaId)
95
+ assertEquals("No favorites yet", tile.mediaMetadata.title)
96
+ assertEquals("Tap the heart on a station to add it here.", tile.mediaMetadata.subtitle)
97
+ assertFalse(tile.mediaMetadata.isBrowsable!!)
98
+ assertFalse(tile.mediaMetadata.isPlayable!!)
99
+ }
100
+
101
+ @Test
102
+ fun `with no formatter the tile carries the neutral default`() = runTest {
103
+ val default = defaultFormattedError(emptyContentError())
104
+ assertEquals("Nothing here", default.title)
105
+ assertNull(default.message)
106
+
107
+ val tile = browseLevelItems(emptyList()) { default }.single()
108
+ assertEquals("Nothing here", tile.mediaMetadata.title)
109
+ assertEquals("", tile.mediaMetadata.subtitle)
110
+ }
111
+
112
+ @Test
113
+ fun `a multi-line message collapses to the single-line subtitle`() = runTest {
114
+ val tile =
115
+ browseLevelItems(emptyList()) { FormattedNavigationError("Empty", "One\nTwo") }.single()
116
+ assertEquals("One Two", tile.mediaMetadata.subtitle)
117
+ }
118
+
119
+ @Test
120
+ fun `a page with children is served unchanged and never asks for copy`() = runTest {
121
+ val children = listOf(track("a"), track("b"))
122
+
123
+ val items = browseLevelItems(children) { error("formatter must not run for a full page") }
124
+
125
+ assertSame(children, items)
126
+ }
127
+
128
+ @Test
129
+ fun `a drill-in on the empty tile dead-ends`() {
130
+ assertTrue(BrowserPathHelper.isDeadEndPath(BrowserPathHelper.EMPTY_PATH))
131
+ assertFalse(BrowserPathHelper.isDeadEndPath("/favorites"))
132
+ // The gate tile re-serves its message instead of dead-ending.
133
+ assertFalse(BrowserPathHelper.isDeadEndPath(BrowserPathHelper.GATE_PATH))
134
+ }
135
+
136
+ @Test
137
+ fun `the formatter hop keeps the app's answer`() = runTest {
138
+ val custom = FormattedNavigationError("No favorites yet", "Tap the heart.")
139
+ assertEquals(
140
+ custom,
141
+ formattedOrDefault(backgroundScope, FormattedNavigationError("Nothing here", null)) { custom },
142
+ )
143
+ }
144
+
145
+ @Test
146
+ fun `the formatter hop falls back to the default on null and on a throw`() = runTest {
147
+ val default = FormattedNavigationError("Nothing here", null)
148
+
149
+ assertEquals(default, formattedOrDefault(backgroundScope, default) { null })
150
+ assertEquals(
151
+ default,
152
+ formattedOrDefault(backgroundScope, default) { throw IllegalStateException("boom") },
153
+ )
154
+ }
155
+
156
+ @Test
157
+ fun `an empty search serves one tile carrying the app's copy for the search path`() = runTest {
158
+ val searchPath = BrowserPathHelper.createSearchPath("jazz")
159
+
160
+ val items = browseLevelItems(emptyList()) { appCopy(emptyContentError(), searchPath) }
161
+
162
+ assertEquals(1, items.size)
163
+ val tile = items.single()
164
+ assertEquals(BrowserPathHelper.EMPTY_PATH, tile.mediaId)
165
+ assertEquals("No results found", tile.mediaMetadata.title)
166
+ assertEquals("Try a different search.", tile.mediaMetadata.subtitle)
167
+ assertFalse(tile.mediaMetadata.isBrowsable!!)
168
+ assertFalse(tile.mediaMetadata.isPlayable!!)
169
+ }
170
+
171
+ @Test
172
+ fun `a search with hits is served unchanged and never asks for copy`() = runTest {
173
+ val hits = listOf(track("Jazz FM"), track("Jazz24"))
174
+
175
+ val items = browseLevelItems(hits) { error("formatter must not run for a search with hits") }
176
+
177
+ assertSame(hits, items)
178
+ }
179
+
180
+ @Test
181
+ fun `a search whose every hit is disabled counts as empty and announces the tile`() {
182
+ val disabledOnly =
183
+ listOf(
184
+ TestFixtures.section(
185
+ children =
186
+ arrayOf(
187
+ TestFixtures.track(title = "Gone", disabled = true),
188
+ TestFixtures.track(title = "Also gone", disabled = true),
189
+ )
190
+ )
191
+ )
192
+
193
+ // Nothing servable, so the announced count is the tile's — a controller told zero would never
194
+ // come back for it.
195
+ assertEquals(1, announcedSearchCount(disabledOnly))
196
+ }
197
+
198
+ @Test
199
+ fun `a search announces only its servable hits`() {
200
+ val mixed =
201
+ listOf(
202
+ TestFixtures.section(
203
+ children =
204
+ arrayOf(
205
+ TestFixtures.track(title = "Playable"),
206
+ TestFixtures.track(title = "Gone", disabled = true),
207
+ )
208
+ ),
209
+ TestFixtures.section(children = arrayOf(TestFixtures.track(title = "Another"))),
210
+ )
211
+
212
+ assertEquals(2, announcedSearchCount(mixed))
213
+ assertEquals(1, announcedSearchCount(null))
214
+ }
215
+
216
+ @Test
217
+ fun `an offline search serves the offline tile with the app's copy for the search path`() {
218
+ val searchPath = BrowserPathHelper.createSearchPath("jazz")
219
+ val slot = FailedSearchSlot()
220
+
221
+ val tile = createOfflineMediaItem(appCopy(offlineError(), searchPath))
222
+
223
+ assertEquals(BrowserPathHelper.OFFLINE_PATH, tile.mediaId)
224
+ assertEquals("Search needs a connection", tile.mediaMetadata.title)
225
+ assertEquals("Reconnect and try again.", tile.mediaMetadata.subtitle)
226
+ assertFalse(tile.mediaMetadata.isBrowsable!!)
227
+ assertFalse(tile.mediaMetadata.isPlayable!!)
228
+ assertTrue(BrowserPathHelper.isDeadEndPath(BrowserPathHelper.OFFLINE_PATH))
229
+ // Offline is not a search that failed: connectivity returning is what serves results again,
230
+ // so the slot stays empty and a later re-query of the query is not a failure.
231
+ assertNull(slot.errorFor("jazz"))
232
+ }
233
+
234
+ @Test
235
+ fun `the offline tile is worded per path, and title-only by default`() {
236
+ val browseTile = createOfflineMediaItem(appCopy(offlineError(), "/favorites"))
237
+ assertEquals("You're offline", browseTile.mediaMetadata.title)
238
+ assertEquals("Check your connection.", browseTile.mediaMetadata.subtitle)
239
+
240
+ val default = defaultFormattedError(offlineError())
241
+ assertEquals("Network Error", default.title)
242
+ assertNull(default.message)
243
+ assertEquals("", createOfflineMediaItem(default).mediaMetadata.subtitle)
244
+ }
245
+
246
+ @Test
247
+ fun `the browse-error tile is worded by the error its exception mapped to`() {
248
+ val failure = navigationErrorFor(HttpStatusException(503, "Service Unavailable"))
249
+
250
+ val appTile = createBrowseErrorMediaItem(appCopy(failure, "/favorites"))
251
+ assertEquals(BrowserPathHelper.ERROR_PATH, appTile.mediaId)
252
+ assertEquals("We couldn't load that", appTile.mediaMetadata.title)
253
+ assertEquals("Try again in a moment.", appTile.mediaMetadata.subtitle)
254
+
255
+ // Without a formatter, the status text the in-app error already shows.
256
+ val defaultTile = createBrowseErrorMediaItem(defaultFormattedError(failure))
257
+ assertEquals("Service Unavailable", defaultTile.mediaMetadata.title)
258
+
259
+ // A re-read of the tile has no exception left to map, so it stands on the generic failure.
260
+ assertEquals("Error", defaultFormattedError(browseFailureError()).title)
261
+ }
262
+
263
+ @Test
264
+ fun `a recorded search failure carries its error for that query only, until a success clears it`() {
265
+ val slot = FailedSearchSlot()
266
+ val failure = navigationErrorFor(HttpStatusException(500, "Internal Server Error"))
267
+ assertNull(slot.errorFor("jazz"))
268
+
269
+ slot.record("jazz", failure)
270
+ assertEquals(failure, slot.errorFor("jazz"))
271
+ assertNull(slot.errorFor("blues"))
272
+
273
+ slot.clear()
274
+ assertNull(slot.errorFor("jazz"))
275
+ }
276
+
277
+ @Test
278
+ fun `a failed search rejects with the navigation error a failed browse would raise`() {
279
+ // Nitro's Promise is JNI-backed (its constructor calls initHybrid), so the JS-facing
280
+ // AudioBrowser.search cannot be built here; what it rejects with is this.
281
+ val rejection =
282
+ NavigationErrorException(navigationErrorFor(HttpStatusException(503, "Service Unavailable")))
283
+ assertEquals(NavigationErrorType.HTTP_ERROR, rejection.error.code)
284
+ assertEquals(503.0, rejection.error.statusCode!!, 0.0)
285
+ assertEquals(false, rejection.error.statusCodeSuccess)
286
+ assertEquals("Service Unavailable", rejection.message)
287
+
288
+ assertEquals(
289
+ NavigationErrorType.NETWORK_ERROR,
290
+ navigationErrorFor(NetworkException("Network request failed")).code,
291
+ )
292
+
293
+ val unknown = navigationErrorFor(IllegalStateException())
294
+ assertEquals(NavigationErrorType.UNKNOWN_ERROR, unknown.code)
295
+ assertEquals("An unexpected error occurred", unknown.message)
296
+ }
297
+
298
+ @Test
299
+ fun `an offline search is remembered per controller, latest query only`() {
300
+ val registry = OfflineSearchRegistry()
301
+ val auto = controller("com.google.android.projection.gearhead", uid = 10)
302
+ val assistant = controller("com.google.android.googlequicksearchbox", uid = 11)
303
+
304
+ registry.record(auto, "jazz", null)
305
+ registry.record(assistant, "blues", null)
306
+ registry.record(auto, "jazz fm", null)
307
+
308
+ val pending = registry.drain()
309
+ assertEquals(2, pending.size)
310
+ assertEquals("jazz fm", pending.single { it.browser == auto }.query)
311
+ assertEquals("blues", pending.single { it.browser == assistant }.query)
312
+ // Drained, so a second connectivity change does not re-run the same searches again.
313
+ assertTrue(registry.drain().isEmpty())
314
+ }
315
+
316
+ @Test
317
+ fun `a re-run search is announced by its hits, an offline one by its tile`() {
318
+ val hits =
319
+ listOf(
320
+ TestFixtures.section(
321
+ children =
322
+ arrayOf(TestFixtures.track(title = "Jazz FM"), TestFixtures.track(title = "Jazz24"))
323
+ )
324
+ )
325
+
326
+ // The re-run goes through the same count as the first, online search, so what a controller is
327
+ // told to fetch is what onGetSearchResult then serves from the cache that search filled.
328
+ assertEquals(2, announcedSearchCount(hits))
329
+ // While offline there is nothing to count: the one tile is announced instead.
330
+ assertEquals(1, announcedSearchCount(null))
331
+ }
332
+
333
+ @Test
334
+ fun `a disconnected controller's offline search is forgotten`() {
335
+ val registry = OfflineSearchRegistry()
336
+ val auto = controller("com.google.android.projection.gearhead", uid = 10)
337
+ val assistant = controller("com.google.android.googlequicksearchbox", uid = 11)
338
+ registry.record(auto, "jazz", null)
339
+ registry.record(assistant, "blues", null)
340
+
341
+ registry.forget(auto)
342
+
343
+ assertEquals(listOf("blues"), registry.drain().map { it.query })
344
+ }
345
+
346
+ @Test
347
+ fun `a remembered search keeps the params its controller asked with`() {
348
+ val registry = OfflineSearchRegistry()
349
+ val auto = controller("com.google.android.projection.gearhead", uid = 10)
350
+ val params =
351
+ MediaLibraryService.LibraryParams.Builder()
352
+ .setExtras(Bundle().apply { putInt("page", 3) })
353
+ .build()
354
+
355
+ registry.record(auto, "jazz", params)
356
+
357
+ assertEquals(3, registry.drain().single().params?.extras?.getInt("page"))
358
+ }
359
+
360
+ @Test
361
+ fun `the failure sentinel is distinct from the empty one and dead-ends too`() {
362
+ assertNotEquals(BrowserPathHelper.EMPTY_PATH, BrowserPathHelper.ERROR_PATH)
363
+ assertTrue(BrowserPathHelper.isDeadEndPath(BrowserPathHelper.ERROR_PATH))
364
+ }
365
+
366
+ @Test
367
+ fun `a formatter parked in a non-cancellable wait falls back without hanging`() = runTest {
368
+ val default = FormattedNavigationError("Nothing here", null)
369
+ var parked: Continuation<FormattedNavigationError?>? = null
370
+
371
+ try {
372
+ // Nitro's Promise.await parks exactly like this — suspendCoroutine, so cancelling the
373
+ // coroutine never unparks it. The hop must be abandoned, not waited on.
374
+ val formatted =
375
+ formattedOrDefault(backgroundScope, default) {
376
+ suspendCoroutine { continuation -> parked = continuation }
377
+ }
378
+
379
+ assertNotNull(parked)
380
+ assertEquals(default, formatted)
381
+ } finally {
382
+ parked?.resume(null)
383
+ }
384
+ }
385
+ }
@@ -1,9 +1,8 @@
1
1
  package com.audiobrowser.player
2
2
 
3
- import com.margelo.nitro.audiobrowser.NotificationButton
4
- import com.margelo.nitro.audiobrowser.NotificationButtonLayout
5
3
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
6
- import com.margelo.nitro.audiobrowser.Variant_Boolean_FavoriteConfig
4
+ import com.margelo.nitro.audiobrowser.RemoteButton
5
+ import com.margelo.nitro.audiobrowser.RemoteButtonLayout
7
6
  import org.junit.Assert.assertEquals
8
7
  import org.junit.Assert.assertFalse
9
8
  import org.junit.Assert.assertTrue
@@ -12,8 +11,7 @@ import org.junit.Test
12
11
  /**
13
12
  * Truth tables for the pure Capability decision core: [PlayerCapabilities.isEnabled] (one rule for
14
13
  * "available by default, only false disables; favorite is opt-in; play/pause is one control") and
15
- * [deriveNotificationSlots] (the single notification slot derivation that both the button layout
16
- * and the notification player commands consume).
14
+ * [deriveButtonSlots] (the single slot derivation every Media3 assembly consumes).
17
15
  */
18
16
  class CapabilityControlsTest {
19
17
 
@@ -37,123 +35,168 @@ class CapabilityControlsTest {
37
35
  skipToPrevious = skipToPrevious,
38
36
  jumpForward = jumpForward,
39
37
  jumpBackward = jumpBackward,
40
- favorite = favorite?.let { Variant_Boolean_FavoriteConfig.First(it) },
38
+ favorite = favorite,
41
39
  shuffleMode = null,
42
40
  repeatMode = null,
43
41
  playbackRate = null,
44
42
  )
45
43
 
44
+ private fun layout(
45
+ back: RemoteButton? = null,
46
+ forward: RemoteButton? = null,
47
+ overflow: Array<RemoteButton> = emptyArray(),
48
+ ) = RemoteButtonLayout(back = back, forward = forward, overflow = overflow)
49
+
46
50
  private fun slots(
47
51
  capabilities: PlayerCapabilities,
48
- layout: NotificationButtonLayout? = null,
49
- ): List<Pair<NotificationButton, NotificationSlot>> =
50
- deriveNotificationSlots(capabilities, layout).map { it.button to it.slot }
52
+ layout: RemoteButtonLayout? = null,
53
+ ): List<Pair<RemoteButton, ButtonSlot>> =
54
+ deriveButtonSlots(capabilities, layout).map { it.button to it.slot }
51
55
 
52
56
  // MARK: isEnabled
53
57
 
54
58
  @Test
55
59
  fun `everything but favorite is enabled by default`() {
56
60
  val defaults = capabilities()
57
- for (control in Control.entries) {
58
- assertEquals("$control", control != Control.FAVORITE, defaults.isEnabled(control))
61
+ for (control in Capability.entries) {
62
+ assertEquals("$control", control != Capability.FAVORITE, defaults.isEnabled(control))
59
63
  }
60
64
  }
61
65
 
62
66
  @Test
63
67
  fun `only an explicit false disables a control`() {
64
68
  val caps = capabilities(skipToNext = false, jumpBackward = false)
65
- assertFalse(caps.isEnabled(Control.SKIP_TO_NEXT))
66
- assertFalse(caps.isEnabled(Control.JUMP_BACKWARD))
67
- assertTrue(caps.isEnabled(Control.SKIP_TO_PREVIOUS))
68
- assertTrue(caps.isEnabled(Control.JUMP_FORWARD))
69
+ assertFalse(caps.isEnabled(Capability.SKIP_TO_NEXT))
70
+ assertFalse(caps.isEnabled(Capability.JUMP_BACKWARD))
71
+ assertTrue(caps.isEnabled(Capability.SKIP_TO_PREVIOUS))
72
+ assertTrue(caps.isEnabled(Capability.JUMP_FORWARD))
69
73
  }
70
74
 
71
75
  @Test
72
76
  fun `play pause is one control - it survives a single half being disabled`() {
73
- assertTrue(capabilities(play = false).isEnabled(Control.PLAY_PAUSE))
74
- assertTrue(capabilities(pause = false).isEnabled(Control.PLAY_PAUSE))
75
- assertFalse(capabilities(play = false, pause = false).isEnabled(Control.PLAY_PAUSE))
77
+ assertTrue(capabilities(play = false).isEnabled(Capability.PLAY_PAUSE))
78
+ assertTrue(capabilities(pause = false).isEnabled(Capability.PLAY_PAUSE))
79
+ assertFalse(capabilities(play = false, pause = false).isEnabled(Capability.PLAY_PAUSE))
76
80
  }
77
81
 
78
82
  @Test
79
83
  fun `favorite is opt-in`() {
80
- assertFalse(capabilities().isEnabled(Control.FAVORITE))
81
- assertFalse(capabilities(favorite = false).isEnabled(Control.FAVORITE))
82
- assertTrue(capabilities(favorite = true).isEnabled(Control.FAVORITE))
84
+ assertFalse(capabilities().isEnabled(Capability.FAVORITE))
85
+ assertFalse(capabilities(favorite = false).isEnabled(Capability.FAVORITE))
86
+ assertTrue(capabilities(favorite = true).isEnabled(Capability.FAVORITE))
83
87
  }
84
88
 
85
- // MARK: deriveNotificationSlots — capability defaults
89
+ // MARK: deriveButtonSlots — capability defaults
86
90
 
87
91
  @Test
88
- fun `default layout puts skip on the primary slots, jump on the secondary, favorite in overflow`() {
92
+ fun `default layout puts skip on the primary positions, jump and favorite in overflow`() {
89
93
  val derived = slots(capabilities(favorite = true))
90
94
  assertEquals(
91
95
  listOf(
92
- NotificationButton.SKIP_TO_PREVIOUS to NotificationSlot.BACK,
93
- NotificationButton.SKIP_TO_NEXT to NotificationSlot.FORWARD,
94
- NotificationButton.JUMP_BACKWARD to NotificationSlot.BACK_SECONDARY,
95
- NotificationButton.JUMP_FORWARD to NotificationSlot.FORWARD_SECONDARY,
96
- NotificationButton.FAVORITE to NotificationSlot.OVERFLOW,
96
+ RemoteButton.SKIP_TO_PREVIOUS to ButtonSlot.BACK,
97
+ RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD,
98
+ RemoteButton.JUMP_BACKWARD to ButtonSlot.OVERFLOW,
99
+ RemoteButton.JUMP_FORWARD to ButtonSlot.OVERFLOW,
100
+ RemoteButton.FAVORITE to ButtonSlot.OVERFLOW,
97
101
  ),
98
102
  derived,
99
103
  )
100
104
  }
101
105
 
102
106
  @Test
103
- fun `jump falls back to the primary slot when skip is disabled`() {
107
+ fun `jump falls back to a primary position when skip is disabled`() {
104
108
  val derived = slots(capabilities(skipToPrevious = false, skipToNext = false))
105
109
  assertEquals(
106
110
  listOf(
107
- NotificationButton.JUMP_BACKWARD to NotificationSlot.BACK,
108
- NotificationButton.JUMP_FORWARD to NotificationSlot.FORWARD,
111
+ RemoteButton.JUMP_BACKWARD to ButtonSlot.BACK,
112
+ RemoteButton.JUMP_FORWARD to ButtonSlot.FORWARD,
109
113
  ),
110
114
  derived,
111
115
  )
112
116
  }
113
117
 
114
118
  @Test
115
- fun `a fully disabled side leaves its slot empty`() {
119
+ fun `a fully disabled side leaves its position empty`() {
116
120
  val derived =
117
121
  slots(capabilities(skipToPrevious = false, jumpBackward = false, jumpForward = false))
118
- assertEquals(listOf(NotificationButton.SKIP_TO_NEXT to NotificationSlot.FORWARD), derived)
122
+ assertEquals(listOf(RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD), derived)
119
123
  }
120
124
 
121
- // MARK: deriveNotificationSlots — explicit layout
125
+ // MARK: deriveButtonSlots — explicit layout
122
126
 
123
127
  @Test
124
- fun `explicit layout maps every slot and keeps overflow order`() {
125
- val layout =
126
- NotificationButtonLayout(
127
- back = NotificationButton.JUMP_BACKWARD,
128
- forward = NotificationButton.JUMP_FORWARD,
129
- backSecondary = null,
130
- forwardSecondary = null,
131
- overflow = arrayOf(NotificationButton.SKIP_TO_PREVIOUS, NotificationButton.SKIP_TO_NEXT),
128
+ fun `an explicit layout maps every field and keeps overflow order`() {
129
+ val derived =
130
+ slots(
131
+ capabilities(),
132
+ layout(
133
+ back = RemoteButton.JUMP_BACKWARD,
134
+ forward = RemoteButton.JUMP_FORWARD,
135
+ overflow = arrayOf(RemoteButton.SKIP_TO_PREVIOUS, RemoteButton.SKIP_TO_NEXT),
136
+ ),
132
137
  )
133
- val derived = slots(capabilities(), layout)
134
138
  assertEquals(
135
139
  listOf(
136
- NotificationButton.JUMP_BACKWARD to NotificationSlot.BACK,
137
- NotificationButton.JUMP_FORWARD to NotificationSlot.FORWARD,
138
- NotificationButton.SKIP_TO_PREVIOUS to NotificationSlot.OVERFLOW,
139
- NotificationButton.SKIP_TO_NEXT to NotificationSlot.OVERFLOW,
140
+ RemoteButton.JUMP_BACKWARD to ButtonSlot.BACK,
141
+ RemoteButton.JUMP_FORWARD to ButtonSlot.FORWARD,
142
+ RemoteButton.SKIP_TO_PREVIOUS to ButtonSlot.OVERFLOW,
143
+ RemoteButton.SKIP_TO_NEXT to ButtonSlot.OVERFLOW,
140
144
  ),
141
145
  derived,
142
146
  )
143
147
  }
144
148
 
145
149
  @Test
146
- fun `explicit layout entries are dropped when their capability is disabled`() {
147
- val layout =
148
- NotificationButtonLayout(
149
- back = NotificationButton.SKIP_TO_PREVIOUS,
150
- forward = NotificationButton.SKIP_TO_NEXT,
151
- backSecondary = null,
152
- forwardSecondary = null,
153
- overflow = arrayOf(NotificationButton.FAVORITE),
150
+ fun `a null field leaves that position empty`() {
151
+ val derived = slots(capabilities(), layout(back = null, forward = RemoteButton.SKIP_TO_NEXT))
152
+ assertEquals(listOf(RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD), derived)
153
+ }
154
+
155
+ @Test
156
+ fun `a layout is never merged with the capability defaults`() {
157
+ // Nitro collapses an omitted and a null enum field to the same Kotlin null, so a per-field
158
+ // merge is not expressible: an empty position must stay empty rather than falling back to the
159
+ // capability default. Every capability here is enabled, so a merge would add skip and jump.
160
+ val derived =
161
+ slots(capabilities(favorite = true), layout(overflow = arrayOf(RemoteButton.FAVORITE)))
162
+ assertEquals(listOf(RemoteButton.FAVORITE to ButtonSlot.OVERFLOW), derived)
163
+ }
164
+
165
+ @Test
166
+ fun `a disabled entry drops out without promoting anything into its position`() {
167
+ // favorite unset (= disabled), skipToPrevious explicitly off. skip-to-next must stay on
168
+ // FORWARD rather than sliding into the vacated BACK position.
169
+ val derived =
170
+ slots(
171
+ capabilities(skipToPrevious = false),
172
+ layout(
173
+ back = RemoteButton.SKIP_TO_PREVIOUS,
174
+ forward = RemoteButton.SKIP_TO_NEXT,
175
+ overflow = arrayOf(RemoteButton.FAVORITE),
176
+ ),
154
177
  )
155
- // favorite unset (= disabled), skipToPrevious explicitly off
156
- val derived = slots(capabilities(skipToPrevious = false), layout)
157
- assertEquals(listOf(NotificationButton.SKIP_TO_NEXT to NotificationSlot.FORWARD), derived)
178
+ assertEquals(listOf(RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD), derived)
179
+ }
180
+
181
+ // MARK: sameAs
182
+
183
+ @Test
184
+ fun `sameAs compares overflow by content, not by array identity`() {
185
+ val a = layout(back = RemoteButton.JUMP_BACKWARD, overflow = arrayOf(RemoteButton.FAVORITE))
186
+ val b = layout(back = RemoteButton.JUMP_BACKWARD, overflow = arrayOf(RemoteButton.FAVORITE))
187
+ // Distinct Array instances — data-class `==` would call these different and republish the
188
+ // layout to Android Auto on every unrelated options update.
189
+ assertTrue(a.sameAs(b))
190
+ assertFalse(a.sameAs(layout(back = RemoteButton.JUMP_BACKWARD)))
191
+ assertFalse(
192
+ a.sameAs(layout(back = RemoteButton.JUMP_FORWARD, overflow = arrayOf(RemoteButton.FAVORITE)))
193
+ )
194
+ }
195
+
196
+ @Test
197
+ fun `sameAs treats null as its own layout`() {
198
+ assertTrue(null.sameAs(null))
199
+ assertFalse(layout(back = RemoteButton.SKIP_TO_NEXT).sameAs(null))
200
+ assertFalse(null.sameAs(layout(back = RemoteButton.SKIP_TO_NEXT)))
158
201
  }
159
202
  }