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
@@ -0,0 +1,322 @@
1
+ package com.audiobrowser.tls
2
+
3
+ import java.net.InetAddress
4
+ import java.net.ServerSocket
5
+ import java.security.cert.X509Certificate
6
+ import java.util.concurrent.CountDownLatch
7
+ import java.util.concurrent.TimeUnit
8
+ import java.util.concurrent.atomic.AtomicInteger
9
+ import java.util.concurrent.atomic.AtomicReference
10
+ import javax.security.auth.x500.X500Principal
11
+ import kotlin.concurrent.thread
12
+ import org.junit.After
13
+ import org.junit.Assert.assertEquals
14
+ import org.junit.Assert.assertFalse
15
+ import org.junit.Assert.assertTrue
16
+ import org.junit.Test
17
+ import org.mockito.Mockito.mock
18
+ import org.mockito.Mockito.`when`
19
+
20
+ /**
21
+ * Exercises [CachingCertificateFetcher.fetch] against a real loopback server.
22
+ *
23
+ * The safety helpers have their own unit tests, but a predicate that is never consulted protects
24
+ * nothing: these cover the *wiring*, so that removing the `isSafeUrl` guard or swapping
25
+ * `readCapped` back for a plain read-to-EOF fails a test rather than passing silently.
26
+ */
27
+ class CachingCertificateFetcherCallSiteTest {
28
+
29
+ private var server: ServerSocket? = null
30
+
31
+ @After
32
+ fun tearDown() {
33
+ server?.close()
34
+ }
35
+
36
+ /**
37
+ * Serves [response] to every connection, recording the request line of the first. [handled]
38
+ * counts accepted connections.
39
+ */
40
+ private fun startServer(
41
+ response: ByteArray = "HTTP/1.0 404 Not Found\r\n\r\n".toByteArray(),
42
+ handled: AtomicInteger = AtomicInteger(),
43
+ firstRequest: AtomicReference<String> = AtomicReference(),
44
+ ): Int {
45
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
46
+ server = socket
47
+ thread(isDaemon = true) {
48
+ while (!socket.isClosed) {
49
+ try {
50
+ socket.accept().use { client ->
51
+ handled.incrementAndGet()
52
+ val request = ByteArray(1024)
53
+ val read = client.getInputStream().read(request)
54
+ firstRequest.compareAndSet(null, String(request, 0, maxOf(read, 0)))
55
+ client.getOutputStream().apply {
56
+ write(response)
57
+ flush()
58
+ }
59
+ }
60
+ } catch (e: Exception) {
61
+ return@thread // socket closed, or the client hung up
62
+ }
63
+ }
64
+ }
65
+ return socket.localPort
66
+ }
67
+
68
+ @Test
69
+ fun `a URL carrying CRLF never reaches the network`() {
70
+ val handled = AtomicInteger()
71
+ val port = startServer(handled = handled)
72
+
73
+ // Without the isSafeUrl guard this connects and emits "SET foo bar" as its own request line.
74
+ val certs = CachingCertificateFetcher().fetch("http://127.0.0.1:$port/\r\nSET foo bar\r\n")
75
+
76
+ assertTrue(certs.isEmpty())
77
+ assertEquals("the request must not be sent at all", 0, handled.get())
78
+ }
79
+
80
+ @Test
81
+ fun `a well-formed URL does reach the network, so the guard is not just refusing everything`() {
82
+ val handled = AtomicInteger()
83
+ val request = AtomicReference<String>()
84
+ val port = startServer(handled = handled, firstRequest = request)
85
+
86
+ CachingCertificateFetcher().fetch("http://127.0.0.1:$port/issuer.crt")
87
+
88
+ assertEquals(1, handled.get())
89
+ assertTrue("request line was: ${request.get()}", request.get().startsWith("GET /issuer.crt "))
90
+ }
91
+
92
+ @Test
93
+ fun `a non-default port is carried in the Host header`() {
94
+ val request = AtomicReference<String>()
95
+ val port = startServer(firstRequest = request)
96
+
97
+ CachingCertificateFetcher().fetch("http://127.0.0.1:$port/issuer.crt")
98
+
99
+ assertTrue(
100
+ "headers were: ${request.get()}",
101
+ request.get().contains("Host: 127.0.0.1:$port\r\n"),
102
+ )
103
+ }
104
+
105
+ @Test(timeout = 30_000)
106
+ fun `an oversized response is abandoned rather than read to EOF`() {
107
+ // Asserting only that the fetch fails would prove nothing — 4 MiB crosses loopback in
108
+ // milliseconds and would fail to parse either way. What has to be true is that the client
109
+ // stopped reading, which shows up as the server's write not draining.
110
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
111
+ server = socket
112
+ val written = AtomicInteger()
113
+ val finished = CountDownLatch(1)
114
+ thread(isDaemon = true) {
115
+ runCatching {
116
+ socket.accept().use { client ->
117
+ client.getInputStream().read(ByteArray(1024))
118
+ val out = client.getOutputStream()
119
+ out.write("HTTP/1.0 200 OK\r\n\r\n".toByteArray())
120
+ val chunk = ByteArray(64 * 1024)
121
+ // 64 MiB — far past the 1 MiB cap. Once the client stops reading and closes, this
122
+ // throws a broken pipe, which is the signal we want.
123
+ repeat(1024) {
124
+ out.write(chunk)
125
+ written.addAndGet(chunk.size)
126
+ }
127
+ }
128
+ }
129
+ finished.countDown()
130
+ }
131
+
132
+ val certs = CachingCertificateFetcher().fetch("http://127.0.0.1:${socket.localPort}/big.crt")
133
+
134
+ assertTrue(certs.isEmpty())
135
+ assertTrue(finished.await(20, TimeUnit.SECONDS))
136
+ // Socket buffers mean the server gets somewhat past the cap before the write fails, but it
137
+ // must be nowhere near the 64 MiB it wanted to send.
138
+ assertTrue(
139
+ "server wrote ${written.get()} bytes; the read was not cut off",
140
+ written.get() < 16 shl 20,
141
+ )
142
+ }
143
+
144
+ @Test(timeout = 30_000)
145
+ fun `a real certificate is fetched, parsed and then served from cache`() {
146
+ // The success path, end to end over a socket: every other wire test serves a failure, so a
147
+ // broken body offset or a broken cache write would not show up anywhere.
148
+ val der = CertFixtures.bytes("letsencrypt-r13.der")
149
+ val handled = AtomicInteger()
150
+ val port =
151
+ startServer(
152
+ response =
153
+ ("HTTP/1.0 200 OK\r\nContent-Type: application/pkix-cert\r\n\r\n").toByteArray() + der,
154
+ handled = handled,
155
+ )
156
+ val fetcher = CachingCertificateFetcher()
157
+ val url = "http://127.0.0.1:$port/r13.der"
158
+
159
+ val certs = fetcher.fetch(url)
160
+
161
+ assertEquals(1, certs.size)
162
+ assertEquals("CN=R13,O=Let's Encrypt,C=US", certs.first().subjectX500Principal.name)
163
+
164
+ // Second call must be served from the cache, not the network.
165
+ assertEquals(certs, fetcher.fetch(url))
166
+ assertEquals("a successful fetch must be cached", 1, handled.get())
167
+ }
168
+
169
+ /**
170
+ * A mock certificate whose AIA extension lists [url] as its one CA-Issuers location, so
171
+ * [AiaCertChaser.completeChain] chases it with this fetcher — the real wiring, end to end.
172
+ */
173
+ private fun certPointingAt(url: String): X509Certificate {
174
+ // Short-form DER lengths only; a loopback URL is far under 128 bytes.
175
+ fun der(tag: Int, content: ByteArray) =
176
+ byteArrayOf(tag.toByte(), content.size.toByte()) + content
177
+ val caIssuersOid = byteArrayOf(0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02)
178
+ val aia =
179
+ der(
180
+ 0x04,
181
+ der(
182
+ 0x30,
183
+ der(0x30, der(0x06, caIssuersOid) + der(0x86, url.toByteArray(Charsets.US_ASCII))),
184
+ ),
185
+ )
186
+ return mock(X509Certificate::class.java).also {
187
+ `when`(it.getExtensionValue("1.3.6.1.5.5.7.1.1")).thenReturn(aia)
188
+ // Distinct principals, or the chaser reads the mock as a self-signed root and never fetches.
189
+ `when`(it.subjectX500Principal).thenReturn(X500Principal("CN=leaf"))
190
+ `when`(it.issuerX500Principal).thenReturn(X500Principal("CN=issuer"))
191
+ }
192
+ }
193
+
194
+ @Test(timeout = 30_000)
195
+ fun `a drip-feeding server is cut off by the chase deadline, not only the per-fetch caps`() {
196
+ // One byte every 100 ms: each read lands well inside the socket timeout, so per-read limits
197
+ // never fire, and the per-fetch read budget alone would let this run for its full 10 s. The
198
+ // walk's 1 s budget must cut the fetch *itself* — not merely decline to start the next one.
199
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
200
+ server = socket
201
+ thread(isDaemon = true) {
202
+ runCatching {
203
+ socket.accept().use { client ->
204
+ client.getInputStream().read(ByteArray(1024))
205
+ val out = client.getOutputStream()
206
+ out.write("HTTP/1.0 200 OK\r\n\r\n".toByteArray())
207
+ out.flush()
208
+ repeat(300) { // 30 s of drip on offer; the client must not stay for it
209
+ out.write(0)
210
+ out.flush()
211
+ Thread.sleep(100)
212
+ }
213
+ }
214
+ }
215
+ }
216
+ val fetcher = CachingCertificateFetcher()
217
+ val cert = certPointingAt("http://127.0.0.1:${socket.localPort}/drip.crt")
218
+
219
+ val started = System.nanoTime()
220
+ val completed =
221
+ AiaCertChaser.completeChain(listOf(cert), budgetMs = 1_000) { url, deadline ->
222
+ fetcher.fetch(url, deadline)
223
+ }
224
+ val elapsedMs = (System.nanoTime() - started) / 1_000_000
225
+
226
+ assertEquals(listOf(cert), completed)
227
+ // Well past the 1 s budget to absorb scheduling noise, but far short of the 10 s the
228
+ // per-fetch read budget alone would have allowed.
229
+ assertTrue("chase ran for $elapsedMs ms", elapsedMs < 8_000)
230
+ }
231
+
232
+ @Test(timeout = 30_000)
233
+ fun `the chase deadline spans redirect hops, not just fetch starts`() {
234
+ // Every hop stalls 1 s and then redirects back to itself. Each hop sits comfortably inside
235
+ // the per-operation timeouts, so only a deadline threaded through the redirect recursion
236
+ // can end the fetch — without one it walks all 5 redirects, ~6 s here.
237
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
238
+ server = socket
239
+ thread(isDaemon = true) {
240
+ while (!socket.isClosed) {
241
+ try {
242
+ socket.accept().use { client ->
243
+ client.getInputStream().read(ByteArray(1024))
244
+ Thread.sleep(1_000)
245
+ client.getOutputStream().apply {
246
+ write("HTTP/1.0 302 Found\r\nLocation: /again.crt\r\n\r\n".toByteArray())
247
+ flush()
248
+ }
249
+ }
250
+ } catch (e: Exception) {
251
+ return@thread
252
+ }
253
+ }
254
+ }
255
+ val fetcher = CachingCertificateFetcher()
256
+
257
+ val started = System.nanoTime()
258
+ val certs =
259
+ fetcher.fetch(
260
+ "http://127.0.0.1:${socket.localPort}/loop.crt",
261
+ deadlineNanos = System.nanoTime() + 1_500 * 1_000_000L,
262
+ )
263
+ val elapsedMs = (System.nanoTime() - started) / 1_000_000
264
+
265
+ assertTrue(certs.isEmpty())
266
+ assertTrue("fetch ran for $elapsedMs ms across redirect hops", elapsedMs < 4_000)
267
+ }
268
+
269
+ @Test(timeout = 30_000)
270
+ fun `a server that accepts and never answers is bounded by the read timeout`() {
271
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
272
+ server = socket
273
+ val accepted = CountDownLatch(1)
274
+ // Accept, then hold the connection open saying nothing at all.
275
+ thread(isDaemon = true) {
276
+ runCatching {
277
+ val client = socket.accept()
278
+ accepted.countDown()
279
+ Thread.sleep(60_000)
280
+ client.close()
281
+ }
282
+ }
283
+
284
+ val certs =
285
+ CachingCertificateFetcher(connectTimeoutMs = 2_000, readTimeoutMs = 1_000)
286
+ .fetch("http://127.0.0.1:${socket.localPort}/hangs.crt")
287
+
288
+ assertTrue(accepted.await(10, TimeUnit.SECONDS))
289
+ assertTrue(certs.isEmpty())
290
+ }
291
+
292
+ @Test
293
+ fun `a non-http scheme is never fetched`() {
294
+ val handled = AtomicInteger()
295
+ val port = startServer(handled = handled)
296
+
297
+ assertTrue(CachingCertificateFetcher().fetch("ftp://127.0.0.1:$port/issuer.crt").isEmpty())
298
+ assertTrue(CachingCertificateFetcher().fetch("file:///etc/hosts").isEmpty())
299
+ assertEquals(0, handled.get())
300
+ }
301
+
302
+ @Test
303
+ fun `a failed fetch is not cached, so it stays retryable`() {
304
+ val handled = AtomicInteger()
305
+ val port = startServer(handled = handled)
306
+ val fetcher = CachingCertificateFetcher()
307
+ val url = "http://127.0.0.1:$port/issuer.crt"
308
+
309
+ fetcher.fetch(url)
310
+ fetcher.fetch(url)
311
+
312
+ assertEquals("both attempts must hit the network", 2, handled.get())
313
+ }
314
+
315
+ @Test
316
+ fun `the cache is bounded`() {
317
+ // Far more distinct URLs than MAX_CACHE_ENTRIES; nothing is cached here (every fetch fails),
318
+ // but the bound is what stops an attacker-chosen URL space from growing without limit.
319
+ assertFalse(CachingCertificateFetcher.MAX_CACHE_ENTRIES <= 0)
320
+ assertTrue(CachingCertificateFetcher.MAX_CACHE_ENTRIES <= 128)
321
+ }
322
+ }
@@ -1,28 +1,53 @@
1
1
  package com.audiobrowser.util
2
2
 
3
- import org.junit.Assert.assertFalse
4
- import org.junit.Assert.assertTrue
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertNull
5
5
  import org.junit.Test
6
+ import org.junit.runner.RunWith
7
+ import org.robolectric.RobolectricTestRunner
6
8
 
9
+ @RunWith(RobolectricTestRunner::class)
7
10
  class BrowserPathHelperTest {
8
11
 
9
12
  @Test
10
- fun `containsSegment matches a uid as a full path segment`() {
11
- val path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
12
- assertTrue(BrowserPathHelper.containsSegment(path, "Gw0LGB8j"))
13
- assertTrue(BrowserPathHelper.containsSegment(path, "amsterdam-funk-channel"))
14
- assertTrue(BrowserPathHelper.containsSegment(path, "listen"))
15
- assertTrue(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j?hl=en", "Gw0LGB8j"))
16
- assertTrue(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j#frag", "Gw0LGB8j"))
13
+ fun `build round-trips a src carrying its own query params`() {
14
+ // A src carrying its own query string (signed CDN URL) must survive the
15
+ // build → extract/strip round-trip: an unescaped `&` would split the src
16
+ // into stray query params, truncating the trackId and polluting the
17
+ // parent path.
18
+ val src = "https://cdn.example.com/stream.mp3?token=abc&exp=1699999999"
19
+ val url = BrowserPathHelper.build("/library", src)
20
+ assertEquals(src, BrowserPathHelper.extractTrackId(url))
21
+ assertEquals("/library", BrowserPathHelper.stripTrackId(url))
17
22
  }
18
23
 
19
24
  @Test
20
- fun `containsSegment rejects partial-segment and missing matches`() {
21
- val path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
22
- assertFalse(BrowserPathHelper.containsSegment(path, "Gw0LGB8"))
23
- assertFalse(BrowserPathHelper.containsSegment(path, "funk"))
24
- assertFalse(BrowserPathHelper.containsSegment(path, "msterdam-funk-channe"))
25
- assertFalse(BrowserPathHelper.containsSegment(path, "NopeNope"))
26
- assertFalse(BrowserPathHelper.containsSegment(path, ""))
25
+ fun `build round-trips a src with equals and plus`() {
26
+ val src = "https://cdn.example.com/a+b.mp3?sig=x=y"
27
+ val url = BrowserPathHelper.build("/library", src)
28
+ assertEquals(src, BrowserPathHelper.extractTrackId(url))
29
+ assertEquals("/library", BrowserPathHelper.stripTrackId(url))
30
+ }
31
+
32
+ @Test
33
+ fun `build stamps the index and round-trips it`() {
34
+ val src = "https://cdn.example.com/stream.mp3?token=abc&exp=1"
35
+ val url = BrowserPathHelper.build("/library", src, 3)
36
+ assertEquals(src, BrowserPathHelper.extractTrackId(url))
37
+ assertEquals(3, BrowserPathHelper.extractIndex(url))
38
+ assertEquals("/library", BrowserPathHelper.stripTrackId(url))
39
+ }
40
+
41
+ @Test
42
+ fun `extractIndex returns null without a stamped index`() {
43
+ val url = BrowserPathHelper.build("/library", "song.mp3")
44
+ assertNull(BrowserPathHelper.extractIndex(url))
45
+ }
46
+
47
+ @Test
48
+ fun `extractIndex returns null for non-contextual or malformed-index URLs`() {
49
+ assertNull(BrowserPathHelper.extractIndex("/library"))
50
+ assertNull(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=x"))
51
+ assertNull(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=-1"))
27
52
  }
28
53
  }
@@ -0,0 +1,161 @@
1
+ package com.audiobrowser.util
2
+
3
+ import androidx.media.utils.MediaConstants
4
+ import com.audiobrowser.TestFixtures
5
+ import com.audiobrowser.browser.styleResolvedSections
6
+ import com.margelo.nitro.audiobrowser.SectionStyle
7
+ import com.margelo.nitro.audiobrowser.StyleDisplay
8
+ import com.margelo.nitro.audiobrowser.TrackStyle
9
+ import org.junit.Assert.assertEquals
10
+ import org.junit.Assert.assertFalse
11
+ import org.junit.Test
12
+ import org.junit.runner.RunWith
13
+ import org.robolectric.RobolectricTestRunner
14
+
15
+ /**
16
+ * The extras are where a section survives the flat MediaBrowser protocol (ADR 0010): the owning
17
+ * section stamps the Android Auto group-title header hint and the per-item display hint. Per-item
18
+ * hints derive from the section resolution; a track's own `display` is positional — the page
19
+ * promise a browsable track emits as the parent-level hint, never its own tile shape (ADR 0011).
20
+ */
21
+ @RunWith(RobolectricTestRunner::class)
22
+ class MediaExtrasBuilderTest {
23
+ private val track = TestFixtures.track()
24
+
25
+ private fun sectionStyle(display: StyleDisplay?) =
26
+ SectionStyle(display = display, artworkRendering = null, gridWrap = null)
27
+
28
+ @Test
29
+ fun `owning section title stamps the group-title hint`() {
30
+ val section = TestFixtures.section(arrayOf(track), title = "Popular")
31
+ val extras = MediaExtrasBuilder.build(track, section)
32
+ assertEquals(
33
+ "Popular",
34
+ extras.getString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE),
35
+ )
36
+ }
37
+
38
+ @Test
39
+ fun `an untitled section stamps no group-title hint`() {
40
+ val section = TestFixtures.section(arrayOf(track))
41
+ val extras = MediaExtrasBuilder.build(track, section)
42
+ assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE))
43
+ }
44
+
45
+ @Test
46
+ fun `grid section display stamps the grid item hint`() {
47
+ val section = TestFixtures.section(arrayOf(track), style = sectionStyle(StyleDisplay.GRID))
48
+ val extras = MediaExtrasBuilder.build(track, section)
49
+ assertEquals(
50
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM,
51
+ extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM),
52
+ )
53
+ }
54
+
55
+ @Test
56
+ fun `list section display stamps the list item hint`() {
57
+ val section = TestFixtures.section(arrayOf(track), style = sectionStyle(StyleDisplay.LIST))
58
+ val extras = MediaExtrasBuilder.build(track, section)
59
+ assertEquals(
60
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM,
61
+ extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM),
62
+ )
63
+ }
64
+
65
+ @Test
66
+ fun `a track's own display never sets its item hint - display is positional`() {
67
+ val gridPromiseTrack =
68
+ track.copy(style = TrackStyle(display = StyleDisplay.GRID, artworkRendering = null))
69
+ val section = TestFixtures.section(arrayOf(gridPromiseTrack), style = null)
70
+ val extras = MediaExtrasBuilder.build(gridPromiseTrack, section)
71
+ assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM))
72
+ }
73
+
74
+ @Test
75
+ fun `a browsable track's declared display emits the parent-level promise`() {
76
+ val handle =
77
+ TestFixtures.browseTrack()
78
+ .copy(style = TrackStyle(display = StyleDisplay.GRID, artworkRendering = null))
79
+ val extras = MediaExtrasBuilder.build(handle)
80
+ val expected = MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
81
+ assertEquals(
82
+ expected,
83
+ extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE),
84
+ )
85
+ assertEquals(
86
+ expected,
87
+ extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE),
88
+ )
89
+ }
90
+
91
+ @Test
92
+ fun `an undeclared promise emits no parent-level hint - never derived`() {
93
+ val handle = TestFixtures.browseTrack()
94
+ val extras = MediaExtrasBuilder.build(handle)
95
+ assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE))
96
+ assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE))
97
+ }
98
+
99
+ @Test
100
+ fun `a playable track's display is inert - playables open no page`() {
101
+ val playable =
102
+ track.copy(style = TrackStyle(display = StyleDisplay.GRID, artworkRendering = null))
103
+ val extras = MediaExtrasBuilder.build(playable)
104
+ assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE))
105
+ assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE))
106
+ }
107
+
108
+ @Test
109
+ fun `no style anywhere stamps no style hint`() {
110
+ val extras = MediaExtrasBuilder.build(track, TestFixtures.section(arrayOf(track)))
111
+ assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM))
112
+ }
113
+
114
+ @Test
115
+ fun `no section (queue and now-playing surfaces) stamps neither section hint`() {
116
+ val extras = MediaExtrasBuilder.build(track)
117
+ assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE))
118
+ assertFalse(extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM))
119
+ }
120
+
121
+ @Test
122
+ fun `the view-all navigation track carries the section's declared display as its promise`() {
123
+ // A "view all" page has no consumer-authored handle — the synthetic
124
+ // navigation track projects the section's declared block, the only
125
+ // declared promise that page can ever have.
126
+ val section =
127
+ TestFixtures.section(
128
+ arrayOf(track),
129
+ title = "Popular",
130
+ path = "/popular",
131
+ style = SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = false),
132
+ )
133
+ val navigation = TrackFactory.navigationTrack(section)
134
+ assertEquals(StyleDisplay.GRID, navigation.style?.display)
135
+
136
+ val extras = MediaExtrasBuilder.build(navigation)
137
+ assertEquals(
138
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM,
139
+ extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE),
140
+ )
141
+ }
142
+
143
+ @Test
144
+ fun `an unstyled section under a grid page stamps the grid hint end to end`() {
145
+ // The wire that makes ResolvedTrack.style real: page block ->
146
+ // styleResolvedSections fold -> per-item hint. Without the fold, every
147
+ // narrower unit test stays green while the page tier goes dark.
148
+ val page =
149
+ TestFixtures.resolvedTrack(
150
+ style = SectionStyle(display = StyleDisplay.GRID, artworkRendering = null, gridWrap = null),
151
+ sections = arrayOf(TestFixtures.section(arrayOf(track))),
152
+ )
153
+
154
+ val folded = page.styleResolvedSections()!!.single()
155
+ val extras = MediaExtrasBuilder.build(track, folded)
156
+ assertEquals(
157
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM,
158
+ extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM),
159
+ )
160
+ }
161
+ }
@@ -22,27 +22,27 @@ class TrackFactoryMediaIdTest {
22
22
  }
23
23
 
24
24
  @Test
25
- fun `stable id wins over the contextual url for playable tracks`() {
25
+ fun `stable id wins over the contextual path for playable tracks`() {
26
26
  val track =
27
27
  TestFixtures.track(id = "abc123", src = "/listen/abc123/channel.mp3")
28
- .copy(url = "/home?__trackId=%2Flisten%2Fabc123%2Fchannel.mp3")
28
+ .copy(path = "/home?__trackId=%2Flisten%2Fabc123%2Fchannel.mp3")
29
29
  assertEquals("abc123", TrackFactory.toMedia3(track).mediaId)
30
30
  }
31
31
 
32
32
  @Test
33
- fun `playable track without id falls back to url then src`() {
34
- val withUrl = TestFixtures.track(src = "/a.mp3").copy(url = "/home?__trackId=%2Fa.mp3")
35
- assertEquals("/home?__trackId=%2Fa.mp3", TrackFactory.toMedia3(withUrl).mediaId)
33
+ fun `playable track without id falls back to path then src`() {
34
+ val withPath = TestFixtures.track(src = "/a.mp3").copy(path = "/home?__trackId=%2Fa.mp3")
35
+ assertEquals("/home?__trackId=%2Fa.mp3", TrackFactory.toMedia3(withPath).mediaId)
36
36
 
37
37
  val srcOnly = TestFixtures.track(src = "/a.mp3")
38
38
  assertEquals("/a.mp3", TrackFactory.toMedia3(srcOnly).mediaId)
39
39
  }
40
40
 
41
41
  @Test
42
- fun `browsable-only track keeps url as mediaId even with an id`() {
42
+ fun `browsable-only track keeps path as mediaId even with an id`() {
43
43
  // Navigation parentIds must stay resolvable paths, so a container's id is
44
44
  // never its mediaId.
45
- val track = TestFixtures.browseTrack(url = "/stations").copy(id = "abc123")
45
+ val track = TestFixtures.browseTrack(path = "/stations").copy(id = "abc123")
46
46
  assertEquals("/stations", TrackFactory.toMedia3(track).mediaId)
47
47
  }
48
48