react-native-audio-browser 0.2.0-next.10 → 0.2.0-next.12

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 (345) hide show
  1. package/README.md +4 -4
  2. package/android/build.gradle +6 -5
  3. package/android/gradle.properties +1 -1
  4. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +110 -25
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +254 -188
  6. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  7. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +77 -58
  8. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
  9. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +49 -0
  10. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +9 -7
  11. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  12. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  13. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  14. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  15. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +93 -73
  16. package/android/src/main/java/com/audiobrowser/player/Player.kt +48 -30
  17. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  18. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
  19. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
  20. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  21. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
  22. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
  23. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  24. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  25. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +35 -0
  26. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +45 -53
  27. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +69 -30
  28. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  29. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
  30. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +345 -8
  31. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  32. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  33. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +91 -0
  34. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  35. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +87 -0
  36. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  37. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  38. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  39. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  40. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  41. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
  42. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
  43. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  44. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  45. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  46. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  47. package/ios/Browser/BrowserConfig.swift +102 -5
  48. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  49. package/ios/Browser/BrowserManager.swift +131 -143
  50. package/ios/Browser/BrowserPathHelper.swift +44 -44
  51. package/ios/Browser/JsonModels.swift +187 -106
  52. package/ios/Browser/SearchDrillIn.swift +6 -6
  53. package/ios/Browser/SectionPresentation.swift +34 -0
  54. package/ios/Browser/SectionScope.swift +58 -24
  55. package/ios/Browser/StyleResolver.swift +47 -0
  56. package/ios/Browser/TabBarEntries.swift +2 -2
  57. package/ios/Browser/TrackIdTemplate.swift +37 -0
  58. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  59. package/ios/CarPlay/CarPlayController.swift +105 -67
  60. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  61. package/ios/CarPlay/CarPlayListItemFactory.swift +217 -84
  62. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  63. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  64. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  65. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  66. package/ios/Extension/Section+Copying.swift +31 -0
  67. package/ios/Extension/Track+Copying.swift +6 -12
  68. package/ios/Extension/Track+Identity.swift +18 -0
  69. package/ios/HybridAudioBrowser.swift +113 -35
  70. package/ios/Model/NitroTypeStubs.swift +64 -23
  71. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  72. package/ios/Player/NowPlayingUpdater.swift +1 -1
  73. package/ios/Player/PlaybackCoordinator.swift +2 -2
  74. package/ios/Player/RetryManager.swift +4 -3
  75. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  76. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  77. package/ios/Tests/JsonModelsTests.swift +75 -1
  78. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  79. package/ios/Tests/QueueManagerTests.swift +4 -4
  80. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  81. package/ios/Tests/SearchDrillInTests.swift +16 -16
  82. package/ios/Tests/SectionPresentationTests.swift +61 -0
  83. package/ios/Tests/SectionScopeTests.swift +87 -55
  84. package/ios/Tests/StyleResolverTests.swift +67 -0
  85. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  86. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  87. package/ios/Tests/TrackIdentityTests.swift +20 -0
  88. package/ios/Tests/TrackSelectorTests.swift +168 -33
  89. package/ios/TrackSelector.swift +45 -30
  90. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  91. package/lib/commonjs/AudioBrowser.js +11 -0
  92. package/lib/commonjs/AudioBrowser.js.map +1 -1
  93. package/lib/commonjs/features/browser-config.js +11 -8
  94. package/lib/commonjs/features/browser-config.js.map +1 -1
  95. package/lib/commonjs/features/browser.js +2 -2
  96. package/lib/commonjs/features/favorites.js +13 -10
  97. package/lib/commonjs/features/favorites.js.map +1 -1
  98. package/lib/commonjs/features/playback/controls.js +4 -2
  99. package/lib/commonjs/features/playback/controls.js.map +1 -1
  100. package/lib/commonjs/features/playback/state.js +2 -3
  101. package/lib/commonjs/features/playback/state.js.map +1 -1
  102. package/lib/commonjs/features/player/options.js.map +1 -1
  103. package/lib/commonjs/features/player/setup.js +3 -1
  104. package/lib/commonjs/features/player/setup.js.map +1 -1
  105. package/lib/commonjs/features/player/validateOptions.js +2 -0
  106. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  107. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  108. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  109. package/lib/commonjs/features/queue/queue.js +11 -4
  110. package/lib/commonjs/features/queue/queue.js.map +1 -1
  111. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  112. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  113. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  114. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  115. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  116. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  117. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  118. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  119. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  120. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  121. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  122. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  123. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  124. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  125. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  126. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  127. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  128. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  129. package/lib/commonjs/web/browser/sections.js +89 -0
  130. package/lib/commonjs/web/browser/sections.js.map +1 -0
  131. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  132. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  133. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  134. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  135. package/lib/module/AudioBrowser.js +1 -0
  136. package/lib/module/AudioBrowser.js.map +1 -1
  137. package/lib/module/features/browser-config.js +11 -8
  138. package/lib/module/features/browser-config.js.map +1 -1
  139. package/lib/module/features/browser.js +2 -2
  140. package/lib/module/features/favorites.js +13 -10
  141. package/lib/module/features/favorites.js.map +1 -1
  142. package/lib/module/features/playback/controls.js +4 -2
  143. package/lib/module/features/playback/controls.js.map +1 -1
  144. package/lib/module/features/playback/state.js +2 -3
  145. package/lib/module/features/playback/state.js.map +1 -1
  146. package/lib/module/features/player/options.js.map +1 -1
  147. package/lib/module/features/player/setup.js +3 -1
  148. package/lib/module/features/player/setup.js.map +1 -1
  149. package/lib/module/features/player/validateOptions.js +2 -0
  150. package/lib/module/features/player/validateOptions.js.map +1 -1
  151. package/lib/module/features/queue/activeTrack.js +14 -3
  152. package/lib/module/features/queue/activeTrack.js.map +1 -1
  153. package/lib/module/features/queue/queue.js +11 -4
  154. package/lib/module/features/queue/queue.js.map +1 -1
  155. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  156. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  157. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  158. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  159. package/lib/module/utils/getTrackIdentity.js +20 -0
  160. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  161. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  162. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  163. package/lib/module/web/NativeAudioBrowser.js +32 -24
  164. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  165. package/lib/module/web/browser/BrowserManager.js +144 -87
  166. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  167. package/lib/module/web/browser/FavoriteManager.js +31 -21
  168. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  169. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  170. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  171. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  172. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  173. package/lib/module/web/browser/sections.js +83 -0
  174. package/lib/module/web/browser/sections.js.map +1 -0
  175. package/lib/module/web/player/NowPlayingManager.js +4 -3
  176. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  177. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  178. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  179. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  180. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  181. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  182. package/lib/typescript/src/features/browser.d.ts +2 -2
  183. package/lib/typescript/src/features/favorites.d.ts +13 -10
  184. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  185. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  186. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  187. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  188. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  189. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  190. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  191. package/lib/typescript/src/features/player/options.d.ts +21 -19
  192. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  193. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  194. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  195. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  196. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  197. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  198. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  199. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  200. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  201. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  202. package/lib/typescript/src/types/browser-nodes.d.ts +260 -127
  203. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  204. package/lib/typescript/src/types/browser.d.ts +33 -53
  205. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  206. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  207. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  208. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  209. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  210. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  211. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  212. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  213. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  214. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  215. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  216. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -3
  217. package/nitrogen/generated/android/c++/JBrowseResult.hpp +10 -4
  218. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  219. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -3
  220. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -3
  221. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -3
  222. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -3
  223. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -3
  224. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -3
  225. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -2
  226. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -2
  227. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +10 -4
  228. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -3
  229. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  230. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -3
  231. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +9 -3
  232. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -2
  233. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +21 -14
  234. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  235. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -3
  236. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +16 -10
  237. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +8 -2
  238. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +6 -7
  239. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  240. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  241. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  242. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -3
  243. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  244. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +49 -51
  245. package/nitrogen/generated/android/c++/JSection.hpp +114 -0
  246. package/nitrogen/generated/android/c++/JSectionStyle.hpp +69 -0
  247. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  248. package/nitrogen/generated/android/c++/JTrack.hpp +19 -48
  249. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -2
  250. package/nitrogen/generated/android/c++/JTrackStyle.hpp +25 -18
  251. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +10 -4
  252. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  253. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  254. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  255. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  256. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  257. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  258. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  259. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +50 -0
  260. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → SectionStyle.kt} +12 -6
  261. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  262. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  263. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +23 -5
  264. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -82
  265. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +16 -10
  266. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +16 -10
  267. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  268. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  269. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  270. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  271. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  272. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  273. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  274. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  275. package/nitrogen/generated/ios/swift/SectionStyle.swift +64 -0
  276. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  277. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  278. package/nitrogen/generated/ios/swift/TrackStyle.swift +28 -22
  279. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  280. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  281. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  282. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  283. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  284. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  285. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  286. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  287. package/nitrogen/generated/shared/c++/SectionStyle.hpp +96 -0
  288. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  289. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  290. package/nitrogen/generated/shared/c++/TrackStyle.hpp +50 -34
  291. package/package.json +3 -2
  292. package/src/AudioBrowser.ts +1 -0
  293. package/src/features/browser-config.test.ts +21 -6
  294. package/src/features/browser-config.ts +11 -8
  295. package/src/features/browser.test.ts +1 -1
  296. package/src/features/browser.ts +2 -2
  297. package/src/features/favorites.ts +13 -10
  298. package/src/features/hook-wiring.test.ts +63 -15
  299. package/src/features/playback/controls.ts +4 -2
  300. package/src/features/playback/state.ts +2 -3
  301. package/src/features/playback/trackPlaybackTime.ts +1 -1
  302. package/src/features/player/options.ts +27 -19
  303. package/src/features/player/setup.test.ts +12 -3
  304. package/src/features/player/setup.ts +3 -1
  305. package/src/features/player/validateOptions.ts +2 -0
  306. package/src/features/queue/activeTrack.ts +14 -3
  307. package/src/features/queue/queue.ts +11 -4
  308. package/src/nitro-stub-drift.test.ts +277 -0
  309. package/src/public-surface.test.ts +40 -0
  310. package/src/specs/audio-browser.nitro.ts +4 -3
  311. package/src/style-resolution.test.ts +139 -0
  312. package/src/types/browser-native.ts +2 -2
  313. package/src/types/browser-nodes.ts +268 -136
  314. package/src/types/browser.ts +34 -55
  315. package/src/utils/LazyNativeEmitter.ts +20 -10
  316. package/src/utils/NativeUpdatedValue.ts +29 -13
  317. package/src/utils/getTrackIdentity.ts +20 -0
  318. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  319. package/src/web/NativeAudioBrowser.ts +37 -22
  320. package/src/web/browser/BrowserManager.ts +176 -95
  321. package/src/web/browser/FavoriteManager.test.ts +84 -0
  322. package/src/web/browser/FavoriteManager.ts +28 -23
  323. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  324. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  325. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  326. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  327. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  328. package/src/web/browser/sections.test.ts +91 -0
  329. package/src/web/browser/sections.ts +72 -0
  330. package/src/web/player/NowPlayingManager.ts +4 -3
  331. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  332. package/src/web/util/BrowserPathHelper.ts +61 -13
  333. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  334. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  335. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  336. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  337. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  338. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  339. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  340. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  341. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  342. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  343. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  344. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  345. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -2,30 +2,31 @@ package com.audiobrowser.util
2
2
 
3
3
  import android.os.Bundle
4
4
  import androidx.media.utils.MediaConstants
5
- import com.margelo.nitro.audiobrowser.ResolvedTrack
5
+ import com.margelo.nitro.audiobrowser.Section
6
+ import com.margelo.nitro.audiobrowser.StyleDisplay
6
7
  import com.margelo.nitro.audiobrowser.Track
7
- import com.margelo.nitro.audiobrowser.TrackStyle
8
8
 
9
9
  /** Builds MediaMetadata extras bundle for Android Auto/AAOS content styling. */
10
10
  object MediaExtrasBuilder {
11
11
 
12
12
  /**
13
- * Maps TrackStyle to MediaConstants content style value. When artwork is an android.resource://
14
- * URI, uses CATEGORY_* variants which add margins around icons (better for small vector icons).
13
+ * Maps a display mode to the MediaConstants content style value. When artwork is an
14
+ * android.resource:// URI, uses CATEGORY_* variants which add margins around icons (better for
15
+ * small vector icons).
15
16
  *
16
17
  * @see <a href="https://developer.android.com/training/cars/media#default-content-style">Default
17
18
  * content style</a>
18
19
  */
19
- private fun TrackStyle.toContentStyleValue(artwork: String?): Int {
20
+ private fun StyleDisplay.toContentStyleValue(artwork: String?): Int {
20
21
  val isAndroidResource = artwork?.startsWith("android.resource://") == true
21
22
  return when (this) {
22
- TrackStyle.LIST ->
23
+ StyleDisplay.LIST ->
23
24
  if (isAndroidResource) {
24
25
  MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM
25
26
  } else {
26
27
  MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
27
28
  }
28
- TrackStyle.GRID ->
29
+ StyleDisplay.GRID ->
29
30
  if (isAndroidResource) {
30
31
  MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM
31
32
  } else {
@@ -34,39 +35,43 @@ object MediaExtrasBuilder {
34
35
  }
35
36
  }
36
37
 
37
- fun build(track: Track): Bundle =
38
+ /**
39
+ * Extras for a track rendered on a browse surface. The owning [section] supplies the Android Auto
40
+ * group-title header hint (grouping is a per-item advisory in the flat MediaBrowser protocol —
41
+ * the section dies here, ADR 0010) and, from its page-folded style block, the per-item display
42
+ * hint: per-item hints derive from the section resolution, never from the track's own `display`,
43
+ * which is positional (ADR 0011).
44
+ *
45
+ * The track's own declared `display` is the page-layout *promise* for the page a browsable track
46
+ * opens — emitted as the parent-level BROWSABLE/PLAYABLE hint, the only below-root hint
47
+ * AOSP-derived AAOS media UIs honor. Declared or it doesn't exist: the library never derives a
48
+ * promise from pages it hasn't resolved, and never back-fills from ones it has.
49
+ */
50
+ fun build(track: Track, section: Section? = null): Bundle =
38
51
  build(
39
- groupTitle = track.groupTitle,
40
- style = track.style,
41
- childrenStyle = track.childrenStyle,
52
+ groupTitle = section?.title,
53
+ itemDisplay = section?.style?.display,
54
+ promisedDisplay = if (track.src == null) track.style?.display else null,
42
55
  artwork = track.artwork?.url,
43
56
  )
44
57
 
45
- fun build(resolvedTrack: ResolvedTrack): Bundle =
46
- build(
47
- groupTitle = resolvedTrack.groupTitle,
48
- style = resolvedTrack.style,
49
- childrenStyle = resolvedTrack.childrenStyle,
50
- artwork = resolvedTrack.artwork?.url,
51
- )
52
-
53
58
  private fun build(
54
59
  groupTitle: String?,
55
- style: TrackStyle?,
56
- childrenStyle: TrackStyle?,
60
+ itemDisplay: StyleDisplay?,
61
+ promisedDisplay: StyleDisplay?,
57
62
  artwork: String?,
58
63
  ): Bundle =
59
64
  Bundle().apply {
60
65
  groupTitle?.let {
61
66
  putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, it)
62
67
  }
63
- style?.let {
68
+ itemDisplay?.let {
64
69
  putInt(
65
70
  MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
66
71
  it.toContentStyleValue(artwork),
67
72
  )
68
73
  }
69
- childrenStyle?.let {
74
+ promisedDisplay?.let {
70
75
  val styleValue = it.toContentStyleValue(artwork)
71
76
  putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, styleValue)
72
77
  putInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, styleValue)
@@ -0,0 +1,35 @@
1
+ package com.audiobrowser.util
2
+
3
+ import com.margelo.nitro.audiobrowser.ArtworkRendering
4
+ import com.margelo.nitro.audiobrowser.StyleDisplay
5
+
6
+ /**
7
+ * The style block's wire strings (ADR 0011) ↔ Nitro enums, shared by the browse decoder and the
8
+ * playback-state store. Inbound mapping is lenient: an unknown or empty value is "no declaration",
9
+ * never an error.
10
+ */
11
+ internal fun StyleDisplay.toWireString(): String =
12
+ when (this) {
13
+ StyleDisplay.LIST -> "list"
14
+ StyleDisplay.GRID -> "grid"
15
+ }
16
+
17
+ internal fun ArtworkRendering.toWireString(): String =
18
+ when (this) {
19
+ ArtworkRendering.ORIGINAL -> "original"
20
+ ArtworkRendering.STENCIL -> "stencil"
21
+ }
22
+
23
+ internal fun String?.toStyleDisplay(): StyleDisplay? =
24
+ when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
25
+ "list" -> StyleDisplay.LIST
26
+ "grid" -> StyleDisplay.GRID
27
+ else -> null
28
+ }
29
+
30
+ internal fun String?.toArtworkRendering(): ArtworkRendering? =
31
+ when (this?.takeIf { it.isNotEmpty() }?.lowercase()) {
32
+ "original" -> ArtworkRendering.ORIGINAL
33
+ "stencil" -> ArtworkRendering.STENCIL
34
+ else -> null
35
+ }
@@ -7,17 +7,16 @@ import androidx.media3.common.MediaItem
7
7
  import androidx.media3.common.MediaMetadata
8
8
  import com.audiobrowser.browser.BrowseArtworkRegistry
9
9
  import com.audiobrowser.browser.ResolvedArtwork
10
- import com.margelo.nitro.audiobrowser.ImageRowItem
10
+ import com.margelo.nitro.audiobrowser.Section
11
11
  import com.margelo.nitro.audiobrowser.Track
12
12
  import com.margelo.nitro.audiobrowser.TrackStyle
13
- import com.margelo.nitro.audiobrowser.Variant_String_ArtworkVariants
14
13
 
15
14
  /**
16
15
  * The single Track → Media3 [MediaItem] conversion. Owns the two easy-to-drift fallbacks: the
17
16
  * displayed artwork is the transformed `artworkSource.uri` falling back to the raw `artwork` field,
18
- * and the mediaId is the stable `id` for playable tracks, falling back to `url` then `src` (a Track
19
- * must have one of the latter two — see the `src` vs `url` note in CONTEXT.md). The list line
20
- * renders from `subtitle` (the now-playing line is re-stamped from `artist` by the now-playing
17
+ * and the mediaId is the stable `id` for playable tracks, falling back to `path` then `src` (a
18
+ * Track must have one of the latter two — see the `src` vs `path` note in CONTEXT.md). The list
19
+ * line renders from `subtitle` (the now-playing line is re-stamped from `artist` by the now-playing
21
20
  * pipeline; see the Now Playing Metadata guide).
22
21
  */
23
22
  object TrackFactory {
@@ -26,49 +25,35 @@ object TrackFactory {
26
25
  }
27
26
 
28
27
  /**
29
- * Android Auto has no image-row rendering; its closest equivalent is a grid of artwork tiles. A
30
- * track carrying `imageRow` expands into its items as grid-styled rows (the per-item
31
- * content-style hint — hosts that ignore it fall back to list rows) grouped under the row's
32
- * title, followed by the row itself as a browsable "view all" link when it has a `url` (a
33
- * url-less row is a pure preview and contributes only its items). Tracks without an `imageRow`
34
- * pass through unchanged.
28
+ * A synthetic browsable Track for a section's "view all" surface — the section has a path, title,
29
+ * and label, but no Track (ADR 0010). It carries the section's declared item block as its own,
30
+ * `display` included: the section's declaration about this content's layout is the only declared
31
+ * promise the "view all" page can ever have (there is no consumer-authored handle to declare one
32
+ * on), and without it Android Auto would always render that page as a list.
35
33
  */
36
- fun expandImageRows(tracks: List<Track>): List<Track> =
37
- tracks.flatMap { track ->
38
- val items = track.imageRow
39
- if (items.isNullOrEmpty()) return@flatMap listOf(track)
40
- val expanded = items.map { it.toTrack(groupTitle = track.title) }
41
- if (track.url != null) {
42
- expanded + track.copy(imageRow = null, groupTitle = track.title)
43
- } else {
44
- expanded
45
- }
46
- }
47
-
48
- /** The row-item equivalent of a full Track, for surfaces that render items as plain rows. */
49
- fun ImageRowItem.toTrack(groupTitle: String?): Track =
34
+ fun navigationTrack(section: Section): Track =
50
35
  Track(
51
- id = id,
52
- url = url,
53
- src = src,
54
- artwork = artwork?.let { Variant_String_ArtworkVariants.First(it) },
55
- artworkSource = artworkSource,
56
- request = request,
57
- artworkCarPlayTinted = null,
58
- title = title,
59
- subtitle = null,
60
- artist = artist,
61
- albumUrl = albumUrl,
62
- album = album,
36
+ id = null,
37
+ path = section.path,
38
+ src = null,
39
+ artwork = null,
40
+ artworkSource = null,
41
+ request = null,
42
+ title = section.title ?: "",
43
+ subtitle = section.subtitle,
44
+ artist = null,
45
+ albumPath = null,
46
+ album = null,
63
47
  description = null,
64
48
  genre = null,
65
49
  duration = null,
66
- style = TrackStyle.GRID,
67
- childrenStyle = null,
50
+ style =
51
+ section.style?.let {
52
+ TrackStyle(display = it.display, artworkRendering = it.artworkRendering)
53
+ },
54
+ disabled = null,
68
55
  favorited = null,
69
- groupTitle = groupTitle,
70
- live = live,
71
- imageRow = null,
56
+ live = null,
72
57
  )
73
58
 
74
59
  fun toMedia3(tracks: Array<Track>): List<MediaItem> {
@@ -90,10 +75,11 @@ object TrackFactory {
90
75
  track: Track,
91
76
  registry: BrowseArtworkRegistry,
92
77
  authority: String,
78
+ section: Section? = null,
93
79
  ): MediaItem {
94
80
  val rawUrl = artworkUri(track) // artworkSource.uri ?: artwork
95
81
  val scheme = rawUrl?.let { Uri.parse(it).scheme?.lowercase() }
96
- val builder = metadataBuilder(track)
82
+ val builder = metadataBuilder(track, section)
97
83
  if (rawUrl != null && (scheme == "http" || scheme == "https")) {
98
84
  val isSvg =
99
85
  SvgArtworkRenderer.isSvgUrl(rawUrl) || SvgArtworkRenderer.isSvgUrl(track.artwork?.url)
@@ -109,8 +95,11 @@ object TrackFactory {
109
95
  /** The transformed artworkSource wins over the raw artwork field. */
110
96
  private fun artworkUri(track: Track): String? = track.artworkSource?.uri ?: track.artwork?.url
111
97
 
112
- /** All metadata except artwork. */
113
- private fun metadataBuilder(track: Track): MediaMetadata.Builder =
98
+ /**
99
+ * All metadata except artwork. The owning [section] (browse surfaces only) supplies the
100
+ * group-title header hint and the per-item style default (ADR 0010).
101
+ */
102
+ private fun metadataBuilder(track: Track, section: Section? = null): MediaMetadata.Builder =
114
103
  MediaMetadata.Builder()
115
104
  .setTitle(track.title)
116
105
  .setArtist(track.subtitle)
@@ -119,26 +108,29 @@ object TrackFactory {
119
108
  .setGenre(track.genre)
120
109
  .setIsBrowsable(track.src == null)
121
110
  .setIsPlayable(track.src != null)
122
- .setExtras(MediaExtrasBuilder.build(track))
111
+ .setExtras(MediaExtrasBuilder.build(track, section))
123
112
  .apply { track.favorited?.let { setUserRating(HeartRating(it)) } }
124
113
 
125
114
  private fun buildMediaItem(track: Track, metadata: MediaMetadata): MediaItem {
126
- // A playable track's mediaId is the consumer's stable `id` when it has one.
127
- // Android Auto marks the "now playing" browse row by exact mediaId equality
128
- // between the row and the player's current item (media3 announces
129
- // `currentMediaItem.mediaId` in the legacy playback state), and a
115
+ // A playable track's mediaId is the consumer's stable `id` when it has one —
116
+ // the identity rule (id when non-blank, else src; see Track.identity /
117
+ // ADR 0008), except that an id-less playable track prefers its contextual
118
+ // `path` over `src` here, so this is deliberately NOT expressed via
119
+ // Track.identity. Android Auto marks the "now playing" browse row by exact
120
+ // mediaId equality between the row and the player's current item (media3
121
+ // announces `currentMediaItem.mediaId` in the legacy playback state), and a
130
122
  // consumer-loaded track's `src` can differ textually from the browse row's
131
123
  // for the same item (absolute vs relative, extra query params) — the id is
132
124
  // the one string both sides share. It is also context-free, so the same
133
125
  // station gets the same mediaId in every tab. Browsable-only tracks keep
134
- // `url` so navigation parentIds stay resolvable paths.
126
+ // `path` so navigation parentIds stay resolvable paths.
135
127
  val stableId = if (track.src != null) track.id?.takeUnless { it.isBlank() } else null
136
128
  val mediaId =
137
129
  stableId
138
- ?: track.url
130
+ ?: track.path
139
131
  ?: track.src
140
132
  ?: throw IllegalArgumentException(
141
- "Track must have either url or src defined. Track: title='${track.title}', artist='${track.artist}'"
133
+ "Track must have either path or src defined. Track: title='${track.title}', artist='${track.artist}'"
142
134
  )
143
135
  return MediaItem.Builder()
144
136
  .setMediaId(mediaId)
@@ -3,9 +3,12 @@ package com.audiobrowser
3
3
  import com.audiobrowser.util.artworkOf
4
4
  import com.margelo.nitro.audiobrowser.ArtworkRequestConfig
5
5
  import com.margelo.nitro.audiobrowser.ImageQueryParams
6
- import com.margelo.nitro.audiobrowser.ImageRowItem
7
6
  import com.margelo.nitro.audiobrowser.ImageSource
8
7
  import com.margelo.nitro.audiobrowser.MediaRequestConfig
8
+ import com.margelo.nitro.audiobrowser.NativeRouteEntry
9
+ import com.margelo.nitro.audiobrowser.ResolvedTrack
10
+ import com.margelo.nitro.audiobrowser.Section
11
+ import com.margelo.nitro.audiobrowser.SectionStyle
9
12
  import com.margelo.nitro.audiobrowser.Track
10
13
  import com.margelo.nitro.audiobrowser.TransformableRequestConfig
11
14
 
@@ -24,47 +27,37 @@ object TestFixtures {
24
27
  artist: String? = null,
25
28
  album: String? = null,
26
29
  favorited: Boolean? = null,
27
- groupTitle: String? = null,
28
- imageRow: Array<ImageRowItem>? = null,
30
+ disabled: Boolean? = null,
29
31
  ) =
30
32
  Track(
31
33
  id = id,
32
- url = null,
34
+ path = null,
33
35
  src = src,
34
36
  artwork = artworkOf(artwork),
35
37
  artworkSource = null,
36
38
  request = null,
37
- artworkCarPlayTinted = null,
38
39
  title = title,
39
40
  subtitle = null,
40
41
  artist = artist,
41
- albumUrl = null,
42
+ albumPath = null,
42
43
  album = album,
43
44
  description = null,
44
45
  genre = null,
45
46
  duration = null,
46
47
  style = null,
47
- childrenStyle = null,
48
+ disabled = disabled,
48
49
  favorited = favorited,
49
- groupTitle = groupTitle,
50
50
  live = null,
51
- imageRow = imageRow,
52
51
  )
53
52
 
54
- fun imageRowItem(src: String, title: String = src) =
55
- ImageRowItem(
56
- id = null,
57
- url = null,
58
- src = src,
59
- artwork = null,
60
- artworkSource = null,
61
- title = title,
62
- artist = null,
63
- album = null,
64
- albumUrl = null,
65
- live = null,
66
- request = null,
67
- )
53
+ /** A page [Section] — plain list by default. */
54
+ fun section(
55
+ children: Array<Track>,
56
+ title: String? = null,
57
+ subtitle: String? = null,
58
+ style: SectionStyle? = null,
59
+ path: String? = null,
60
+ ) = Section(title = title, subtitle = subtitle, style = style, path = path, children = children)
68
61
 
69
62
  fun transformableConfig(
70
63
  baseUrl: String? = null,
@@ -113,32 +106,78 @@ object TestFixtures {
113
106
  */
114
107
  fun browseTrack(
115
108
  title: String = "T",
116
- url: String = "https://api.example.com/channel/abc",
109
+ path: String = "https://api.example.com/channel/abc",
117
110
  artwork: String? = null,
118
111
  artworkSource: ImageSource? = null,
119
112
  ) =
120
113
  Track(
121
114
  id = null,
122
- url = url,
115
+ path = path,
123
116
  src = null, // browsable, not playable
124
117
  artwork = artworkOf(artwork),
125
118
  artworkSource = artworkSource,
126
119
  request = null,
127
- artworkCarPlayTinted = null,
128
120
  title = title,
129
121
  subtitle = null,
130
122
  artist = null,
131
- albumUrl = null,
123
+ albumPath = null,
132
124
  album = null,
133
125
  description = null,
134
126
  genre = null,
135
127
  duration = null,
136
128
  style = null,
137
- childrenStyle = null,
129
+ disabled = null,
138
130
  favorited = null,
139
- groupTitle = null,
140
131
  live = null,
141
- imageRow = null,
132
+ )
133
+
134
+ /** A container [ResolvedTrack] — what a browse route hands back for a path. */
135
+ fun resolvedTrack(
136
+ path: String = "/container",
137
+ title: String = "Container",
138
+ children: Array<Track>? = null,
139
+ sections: Array<Section>? = null,
140
+ style: SectionStyle? = null,
141
+ ) =
142
+ ResolvedTrack(
143
+ path = path,
144
+ style = style,
145
+ sections = sections,
146
+ children = children,
147
+ carPlaySiriListButton = null,
148
+ id = null,
149
+ src = null,
150
+ artwork = null,
151
+ artworkSource = null,
152
+ request = null,
153
+ title = title,
154
+ subtitle = null,
155
+ artist = null,
156
+ albumPath = null,
157
+ album = null,
158
+ description = null,
159
+ genre = null,
160
+ duration = null,
161
+ disabled = null,
162
+ favorited = null,
163
+ live = null,
164
+ )
165
+
166
+ /**
167
+ * A route serving [browseStatic] verbatim. `browseStatic` is the only one of the three browse
168
+ * arms (callback > config > static) that needs neither the JNI bridge nor HTTP, so it is how a
169
+ * unit test gives [com.audiobrowser.browser.BrowserManager] page content to resolve.
170
+ */
171
+ fun staticRoute(path: String, browseStatic: ResolvedTrack) =
172
+ NativeRouteEntry(
173
+ path = path,
174
+ browseCallback = null,
175
+ browseConfig = null,
176
+ browseStatic = browseStatic,
177
+ searchCallback = null,
178
+ searchConfig = null,
179
+ media = null,
180
+ artwork = null,
142
181
  )
143
182
 
144
183
  fun artworkConfig(
@@ -0,0 +1,153 @@
1
+ package com.audiobrowser.browser
2
+
3
+ import com.audiobrowser.TestFixtures.resolvedTrack
4
+ import com.audiobrowser.TestFixtures.staticRoute
5
+ import com.audiobrowser.TestFixtures.track
6
+ import kotlinx.coroutines.runBlocking
7
+ import org.junit.Assert.assertEquals
8
+ import org.junit.Before
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import org.robolectric.RobolectricTestRunner
12
+
13
+ /**
14
+ * Favorite hydration keyed by track identity (id when non-blank, else src — see Track.identity /
15
+ * ADR 0008). Hydration is exercised through [BrowserManager.resolve], the path every browse page
16
+ * takes.
17
+ *
18
+ * Routes serve `browseStatic` (no JNI/HTTP); Robolectric is required for `android.util.LruCache`
19
+ * and `android.net.Uri`.
20
+ */
21
+ @RunWith(RobolectricTestRunner::class)
22
+ class BrowserManagerFavoritesTest {
23
+
24
+ private lateinit var browserManager: BrowserManager
25
+
26
+ @Before
27
+ fun setup() {
28
+ browserManager = BrowserManager()
29
+ }
30
+
31
+ private fun servePage(path: String, vararg children: com.margelo.nitro.audiobrowser.Track) {
32
+ browserManager.config =
33
+ BrowserConfig(
34
+ routes =
35
+ arrayOf(staticRoute(path, resolvedTrack(path = path, children = arrayOf(*children))))
36
+ )
37
+ }
38
+
39
+ private fun favoritedByTitle(children: Array<com.margelo.nitro.audiobrowser.Track>?) =
40
+ children?.associate { it.title to it.favorited }
41
+
42
+ @Test
43
+ fun `hydrates by id when the track has one`() = runBlocking {
44
+ servePage(
45
+ "/home",
46
+ track(title = "A", id = "stable-a", src = "https://s/a.mp3"),
47
+ track(title = "B", id = "stable-b", src = "https://s/b.mp3"),
48
+ )
49
+ browserManager.setFavoriteEnabled(true)
50
+ browserManager.setFavorites(listOf("stable-b"))
51
+
52
+ val resolved = browserManager.resolve("/home")
53
+
54
+ assertEquals(
55
+ mapOf("A" to false, "B" to true),
56
+ favoritedByTitle(resolved.flattenedChildren?.toTypedArray()),
57
+ )
58
+ }
59
+
60
+ @Test
61
+ fun `falls back to src for id-less tracks`() = runBlocking {
62
+ servePage(
63
+ "/home",
64
+ track(title = "A", src = "https://s/a.mp3"),
65
+ track(title = "B", src = "https://s/b.mp3"),
66
+ )
67
+ browserManager.setFavoriteEnabled(true)
68
+ browserManager.setFavorites(listOf("https://s/b.mp3"))
69
+
70
+ val resolved = browserManager.resolve("/home")
71
+
72
+ assertEquals(
73
+ mapOf("A" to false, "B" to true),
74
+ favoritedByTitle(resolved.flattenedChildren?.toTypedArray()),
75
+ )
76
+ }
77
+
78
+ @Test
79
+ fun `a favorite stored under src does not match an id-bearing track`() = runBlocking {
80
+ // The identity is the id when present — a src-keyed entry must not light up
81
+ // a track whose identity is its id (one rule, no partial matching).
82
+ servePage("/home", track(title = "A", id = "stable-a", src = "https://s/a.mp3"))
83
+ browserManager.setFavoriteEnabled(true)
84
+ browserManager.setFavorites(listOf("https://s/a.mp3"))
85
+
86
+ val resolved = browserManager.resolve("/home")
87
+
88
+ assertEquals(mapOf("A" to false), favoritedByTitle(resolved.flattenedChildren?.toTypedArray()))
89
+ }
90
+
91
+ @Test
92
+ fun `caller-set favorited wins over hydration`() = runBlocking {
93
+ servePage(
94
+ "/home",
95
+ track(title = "A", id = "stable-a", src = "https://s/a.mp3", favorited = false),
96
+ )
97
+ browserManager.setFavoriteEnabled(true)
98
+ browserManager.setFavorites(listOf("stable-a"))
99
+
100
+ val resolved = browserManager.resolve("/home")
101
+
102
+ // API-provided value is never overwritten, even when the identity is favorited.
103
+ assertEquals(mapOf("A" to false), favoritedByTitle(resolved.flattenedChildren?.toTypedArray()))
104
+ }
105
+
106
+ @Test
107
+ fun `disabled favoriting leaves tracks untouched`() = runBlocking {
108
+ servePage("/home", track(title = "A", id = "stable-a", src = "https://s/a.mp3"))
109
+ // favoriteEnabled defaults to false
110
+ browserManager.setFavorites(listOf("stable-a"))
111
+
112
+ val resolved = browserManager.resolve("/home")
113
+
114
+ assertEquals(
115
+ mapOf<String, Boolean?>("A" to null),
116
+ favoritedByTitle(resolved.flattenedChildren?.toTypedArray()),
117
+ )
118
+ }
119
+
120
+ @Test
121
+ fun `updateFavorite adds and removes by identity`() = runBlocking {
122
+ servePage("/home", track(title = "A", id = "stable-a", src = "https://s/a.mp3"))
123
+ browserManager.setFavoriteEnabled(true)
124
+
125
+ assertEquals(
126
+ mapOf("A" to false),
127
+ favoritedByTitle(browserManager.resolve("/home").flattenedChildren?.toTypedArray()),
128
+ )
129
+
130
+ browserManager.updateFavorite("stable-a", true)
131
+ assertEquals(
132
+ mapOf("A" to true),
133
+ favoritedByTitle(browserManager.resolve("/home").flattenedChildren?.toTypedArray()),
134
+ )
135
+
136
+ browserManager.updateFavorite("stable-a", false)
137
+ assertEquals(
138
+ mapOf("A" to false),
139
+ favoritedByTitle(browserManager.resolve("/home").flattenedChildren?.toTypedArray()),
140
+ )
141
+ }
142
+
143
+ @Test
144
+ fun `getCachedTrack re-hydrates favorites by identity`() = runBlocking {
145
+ servePage("/home", track(title = "A", id = "stable-a", src = "https://s/a.mp3"))
146
+ browserManager.setFavoriteEnabled(true)
147
+ browserManager.resolve("/home")
148
+
149
+ browserManager.updateFavorite("stable-a", true)
150
+
151
+ assertEquals(true, browserManager.getCachedTrack("stable-a")?.favorited)
152
+ }
153
+ }
@@ -8,6 +8,8 @@ import org.junit.Assert.assertNull
8
8
  import org.junit.Assert.assertSame
9
9
  import org.junit.Before
10
10
  import org.junit.Test
11
+ import org.junit.runner.RunWith
12
+ import org.robolectric.RobolectricTestRunner
11
13
 
12
14
  /**
13
15
  * Tests the request/browse layer resolution + caching introduced for resolver thunks.
@@ -26,7 +28,11 @@ import org.junit.Test
26
28
  * bump, no in-flight cache) is identical for the static and resolver paths — `resolveLayer` only
27
29
  * differs in how it produces the `TransformableRequestConfig` — so the guard is fully covered here.
28
30
  * The resolver-await path is covered by the TS/web-stub tests and on iOS.
31
+ *
32
+ * Runs under Robolectric for [BrowserManager]'s `android.util.LruCache` fields: `clearContentCache`
33
+ * calls `evictAll()`, which a plain JVM test cannot execute.
29
34
  */
35
+ @RunWith(RobolectricTestRunner::class)
30
36
  class BrowserManagerLayerResolutionTest {
31
37
 
32
38
  private lateinit var browserManager: BrowserManager