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
@@ -8,39 +8,69 @@ import java.net.Socket
8
8
  import java.net.URL
9
9
  import java.security.cert.CertificateFactory
10
10
  import java.security.cert.X509Certificate
11
- import java.util.concurrent.ConcurrentHashMap
11
+ import java.util.concurrent.atomic.AtomicBoolean
12
12
  import javax.net.ssl.HttpsURLConnection
13
13
  import javax.net.ssl.SSLContext
14
+ import javax.net.ssl.SSLSocket
14
15
  import javax.net.ssl.SSLSocketFactory
16
+ import kotlin.concurrent.thread
15
17
  import timber.log.Timber
16
18
 
17
19
  /**
18
20
  * Fetches and caches issuer certificates from AIA "CA Issuers" URLs. Used by
19
21
  * [AiaChasingTrustManager] to fill in intermediates a server failed to send.
20
22
  *
21
- * Runs on the TLS handshake thread, so it uses short timeouts and never throws — a failed or slow
22
- * fetch resolves to an empty list (the handshake then fails exactly as it would have without AIA
23
- * chasing). Only non-empty results are cached, so the (typically `http`) round-trip happens at most
24
- * once per intermediate for the process lifetime while transient failures stay retryable.
23
+ * Runs on the TLS handshake thread, so it is bounded on every axis and never throws — a failed,
24
+ * oversized or slow fetch resolves to an empty list, and the handshake then fails exactly as it
25
+ * would have without AIA chasing. Every input here is chosen by a server we are already failing to
26
+ * validate: the AIA extension names the host, port and path, and then that host chooses the
27
+ * response. So:
28
+ * - the URL is rejected unless it is free of control characters ([isSafeUrl]),
29
+ * - a response is capped at [MAX_RESPONSE_BYTES] and [MAX_RESPONSE_MILLIS],
30
+ * - at most [MAX_REDIRECTS] redirects are followed,
31
+ * - the cache holds at most [MAX_CACHE_ENTRIES], and only successful fetches, so a round trip
32
+ * happens at most once per intermediate while transient failures stay retryable,
33
+ * - the whole call — every connect, handshake, read and redirect hop — ends at the caller's
34
+ * `deadlineNanos`: each per-operation timeout is clipped to the time remaining, connect and read
35
+ * by their socket timeouts and the TLS handshake plus the exchange by a watchdog that closes the
36
+ * underlying transport, and once the deadline has passed nothing further is even started.
25
37
  *
26
- * Responses are capped at [MAX_RESPONSE_BYTES]. The URL comes from a server-presented certificate,
27
- * so the host, port and path are chosen by whoever we are failing to validate; the read timeout is
28
- * per-read and would not stop a slow drip of unbounded length. A certificate is a few kilobytes and
29
- * even a PKCS#7 bundle a few dozen, so the cap is far above anything legitimate.
38
+ * The one step no deadline reaches is DNS: `InetSocketAddress` resolves synchronously and Java
39
+ * exposes no timeout for it, so a lookup begun just inside the deadline can overrun it by its own
40
+ * duration. A lookup is never *started* past the deadline, and [AiaCertChaser.MAX_URLS_PER_CERT]
41
+ * keeps the count down — a certificate can only make us resolve a few names, not a few hundred.
30
42
  *
31
- * `http` fetches go over a raw [Socket] rather than `HttpURLConnection`. CA "CA Issuers" URLs are
32
- * virtually always plain `http` (the CA/Browser Forum Baseline Requirements mandate it, to avoid a
33
- * chicken-and-egg TLS dependency when fetching the cert needed to complete a TLS chain), but
34
- * Android blocks cleartext `HttpURLConnection` traffic by default on apps targeting API 28+. A raw
35
- * socket is not subject to `NetworkSecurityPolicy`, so the fetch succeeds regardless of the host
36
- * app's cleartext setting — safely, because the fetched certificate is still cryptographically
37
- * verified to be the real issuer before [AiaChasingTrustManager] trusts it.
43
+ * Both schemes go over a raw [Socket] rather than `HttpURLConnection`. For `http` that is because
44
+ * CA "CA Issuers" URLs are virtually always plain `http` — the CA/Browser Forum Baseline
45
+ * Requirements mandate it, to avoid a chicken-and-egg TLS dependency when fetching the certificate
46
+ * needed to complete a TLS chain — while Android blocks cleartext `HttpURLConnection` traffic by
47
+ * default on apps targeting API 28+. A raw socket is not subject to `NetworkSecurityPolicy`, so the
48
+ * fetch succeeds regardless of the host app's cleartext setting; safely, because the fetched
49
+ * certificate is still cryptographically verified to be the real issuer before
50
+ * [AiaChasingTrustManager] trusts it.
51
+ *
52
+ * For `https` it is because `HttpURLConnection.getInputStream()` performs the connect, request and
53
+ * response *headers* before returning a stream, so a read cap applied to that stream bounds only
54
+ * the body: a server dripping header bytes just inside the per-read timeout would hold the
55
+ * handshake thread for as long as it liked. Reading the headers ourselves puts them inside the same
56
+ * budget as the body. It also keeps one redirect policy — the platform stack would otherwise follow
57
+ * its own, ignoring [MAX_REDIRECTS].
38
58
  */
39
59
  class CachingCertificateFetcher(
40
60
  private val connectTimeoutMs: Int = 5_000,
41
61
  private val readTimeoutMs: Int = 5_000,
62
+ private val nowNanos: () -> Long = System::nanoTime,
42
63
  ) {
43
- private val cache = ConcurrentHashMap<String, List<X509Certificate>>()
64
+ /**
65
+ * Bounded, least-recently-used. An attacker can name any URL and serve any parseable certificate
66
+ * from it, so an unbounded map would retain a megabyte per hostile handshake for the process
67
+ * lifetime. A real app needs a handful of intermediates.
68
+ */
69
+ private val cache =
70
+ object : LinkedHashMap<String, List<X509Certificate>>(16, 0.75f, true) {
71
+ override fun removeEldestEntry(eldest: Map.Entry<String, List<X509Certificate>>) =
72
+ size > MAX_CACHE_ENTRIES
73
+ }
44
74
 
45
75
  /**
46
76
  * A plain system-default socket factory for `https` AIA fetches. Without it the fetch would use
@@ -51,98 +81,243 @@ class CachingCertificateFetcher(
51
81
  SSLContext.getInstance("TLS").apply { init(null, null, null) }.socketFactory
52
82
  }
53
83
 
54
- fun fetch(url: String): List<X509Certificate> {
55
- cache[url]?.let {
56
- return it
57
- }
58
- val certs = download(url)?.let { parseCertificates(it) }.orEmpty()
59
- if (certs.isNotEmpty()) cache[url] = certs
84
+ /**
85
+ * Fetches the certificates served at [url], giving up no later than [deadlineNanos] — absolute,
86
+ * on [nowNanos]'s origin. [AiaCertChaser.completeChain] passes its whole-walk deadline here, so
87
+ * one budget bounds the sum of every fetch it makes. The default covers a standalone call.
88
+ *
89
+ * A cache hit is answered even past the deadline — it costs no I/O.
90
+ */
91
+ fun fetch(
92
+ url: String,
93
+ deadlineNanos: Long = nowNanos() + AiaCertChaser.MAX_CHASE_MILLIS * 1_000_000,
94
+ ): List<X509Certificate> {
95
+ synchronized(cache) { cache[url] }
96
+ ?.let {
97
+ return it
98
+ }
99
+ val certs = download(url, deadlineNanos)?.let { parseCertificates(it) }.orEmpty()
100
+ if (certs.isNotEmpty()) synchronized(cache) { cache[url] = certs }
60
101
  return certs
61
102
  }
62
103
 
63
- private fun download(url: String, redirectsLeft: Int = MAX_REDIRECTS): ByteArray? =
104
+ /** Milliseconds until [deadlineNanos]; zero or negative once it has passed. */
105
+ private fun remainingMs(deadlineNanos: Long): Long = (deadlineNanos - nowNanos()) / 1_000_000
106
+
107
+ private fun download(
108
+ url: String,
109
+ deadlineNanos: Long,
110
+ redirectsLeft: Int = MAX_REDIRECTS,
111
+ ): ByteArray? =
64
112
  try {
113
+ // Checked before anything is started — DNS included, since it is the one step below that
114
+ // no timeout reaches. Redirects recurse through here, so every hop re-checks.
115
+ require(remainingMs(deadlineNanos) > 0) { "AIA chase budget exhausted" }
65
116
  require(isSafeUrl(url)) { "AIA URL contains characters that are not valid in a URL" }
66
117
  val parsed = URL(url)
67
118
  when (parsed.protocol) {
68
- "http" -> downloadCleartext(parsed, redirectsLeft)
69
- "https" -> downloadHttps(parsed)
70
- else -> null // restrict to http/https — file:, ftp: etc. are never valid CA-issuer sources
119
+ // restrict to http/https — file:, ftp: etc. are never valid CA-issuer sources
120
+ "http" -> fetchOverSocket(parsed, deadlineNanos, redirectsLeft, secure = false)
121
+ "https" -> fetchOverSocket(parsed, deadlineNanos, redirectsLeft, secure = true)
122
+ else -> null
71
123
  }
72
124
  } catch (e: Exception) {
73
- Timber.w(e, "AIA CA-issuer fetch failed: %s", url)
125
+ // The URL is attacker-supplied text and this is the one place it survives the safety
126
+ // check, so it is escaped rather than handed to the log verbatim.
127
+ Timber.w(e, "AIA CA-issuer fetch failed: %s", sanitizeForLog(url))
74
128
  null
75
129
  }
76
130
 
77
131
  /**
78
- * `https` AIA fetch over the platform HTTP stack. `https` is never subject to Android's cleartext
79
- * policy, so `HttpsURLConnection` is fine here; the plain socket factory keeps the fetch's own
80
- * handshake from recursing into AIA chasing.
132
+ * A connected socket to speak HTTP over, plus the plain TCP transport underneath it. They differ
133
+ * for `https`: closing an `SSLSocket` writes a `close_notify` alert, which can itself block
134
+ * against a peer that stopped reading — so anything that needs to *unblock* a stalled call must
135
+ * close [transport], whose close is immediate and fails every read layered above it.
81
136
  */
82
- private fun downloadHttps(url: URL): ByteArray? {
83
- val connection = url.openConnection() as? HttpsURLConnection ?: return null
84
- connection.connectTimeout = connectTimeoutMs
85
- connection.readTimeout = readTimeoutMs
86
- connection.sslSocketFactory = plainSslSocketFactory
87
- return connection.inputStream.use { readCapped(it) }
88
- }
137
+ private class Connection(val socket: Socket, val transport: Socket)
89
138
 
90
- /**
91
- * `http` AIA fetch over a raw socket — see the class doc for why this bypasses
92
- * `HttpURLConnection`.
93
- */
94
- private fun downloadCleartext(url: URL, redirectsLeft: Int): ByteArray? {
139
+ /** One HTTP/1.0 exchange over a socket, TLS-wrapped when [secure], then redirects. */
140
+ private fun fetchOverSocket(
141
+ url: URL,
142
+ deadlineNanos: Long,
143
+ redirectsLeft: Int,
144
+ secure: Boolean,
145
+ ): ByteArray? {
95
146
  val host = url.host
96
- val port = if (url.port != -1) url.port else 80
147
+ val port = if (url.port != -1) url.port else if (secure) 443 else 80
97
148
  val path = url.file.ifEmpty { "/" } // URL.file is path + query
149
+ val connection = connect(host, port, secure, deadlineNanos)
98
150
  val raw =
99
- Socket().use { socket ->
100
- socket.connect(InetSocketAddress(host, port), connectTimeoutMs)
101
- socket.soTimeout = readTimeoutMs
102
- // HTTP/1.0 + `Connection: close` so the server closes after the body and we can read to EOF
103
- // without parsing Content-Length.
104
- val request =
105
- "GET $path HTTP/1.0\r\n" +
106
- "Host: $host\r\n" +
107
- "Connection: close\r\n" +
108
- "User-Agent: react-native-audio-browser\r\n" +
109
- "Accept: */*\r\n\r\n"
110
- socket.getOutputStream().apply {
111
- write(request.toByteArray(Charsets.ISO_8859_1))
112
- flush()
151
+ try {
152
+ withWatchdog(
153
+ minOf(remainingMs(deadlineNanos), connectTimeoutMs + MAX_RESPONSE_MILLIS),
154
+ abort = { connection.transport.close() },
155
+ ) {
156
+ val socket = connection.socket
157
+ socket.soTimeout = timeoutMs(deadlineNanos, readTimeoutMs)
158
+ // HTTP/1.0 + `Connection: close` so the server closes after the body and we can read
159
+ // to EOF without parsing Content-Length. A non-default port belongs in Host, or a
160
+ // name-based virtual host answers for the wrong site.
161
+ val hostHeader = if (port == (if (secure) 443 else 80)) host else "$host:$port"
162
+ val request =
163
+ "GET $path HTTP/1.0\r\n" +
164
+ "Host: $hostHeader\r\n" +
165
+ "Connection: close\r\n" +
166
+ "User-Agent: react-native-audio-browser\r\n" +
167
+ "Accept: */*\r\n\r\n"
168
+ socket.getOutputStream().apply {
169
+ write(request.toByteArray(Charsets.ISO_8859_1))
170
+ flush()
171
+ }
172
+ socket.getInputStream().use {
173
+ readCapped(
174
+ it,
175
+ budgetMs = minOf(remainingMs(deadlineNanos), MAX_RESPONSE_MILLIS),
176
+ nowNanos = nowNanos,
177
+ )
178
+ }
113
179
  }
114
- socket.getInputStream().use { readCapped(it) }
180
+ } finally {
181
+ // Transport first: its close never blocks and unblocks anything above it, so the
182
+ // SSLSocket's close_notify attempt afterwards fails fast instead of stalling.
183
+ runCatching { connection.transport.close() }
184
+ runCatching { connection.socket.close() }
115
185
  } ?: return null
116
186
  val response = parseHttpResponse(raw) ?: return null
117
187
  return when {
118
188
  response.status in 200..299 -> response.body
119
189
  response.status in 300..399 && response.location != null && redirectsLeft > 0 ->
120
- // Follow CA-issuer redirects (rare). A redirect to `https` falls through to the https path.
121
- download(URL(url, response.location).toString(), redirectsLeft - 1)
190
+ // Follow CA-issuer redirects (rare). `download` is the recursion point, so the target
191
+ // goes through the same URL check and the same deadline.
192
+ download(URL(url, response.location).toString(), deadlineNanos, redirectsLeft - 1)
122
193
  else -> null
123
194
  }
124
195
  }
125
196
 
197
+ /**
198
+ * Connects to [host]:[port], wrapping in TLS when [secure]. Both the connect and the TLS
199
+ * handshake are clipped to the caller's deadline.
200
+ *
201
+ * The TLS socket verifies the hostname explicitly. A raw `SSLSocket` does not do it on its own,
202
+ * and `SSLParameters.setEndpointIdentificationAlgorithm` is API 24+, so the platform's default
203
+ * verifier is applied against the negotiated session instead — which works on every supported
204
+ * level and is the same check `HttpsURLConnection` would have made.
205
+ */
206
+ private fun connect(host: String, port: Int, secure: Boolean, deadlineNanos: Long): Connection {
207
+ val plain = Socket()
208
+ // Everything from here on closes `plain` if it throws. Android's Socket.connect, unlike
209
+ // modern OpenJDK's, does not close itself on failure once the descriptor is allocated, so
210
+ // every unreachable AIA host would otherwise leak one until finalization.
211
+ var wrapper: SSLSocket? = null
212
+ return try {
213
+ plain.connect(InetSocketAddress(host, port), timeoutMs(deadlineNanos, connectTimeoutMs))
214
+ if (!secure) return Connection(plain, plain)
215
+ // The handshake is bounded by a watchdog, not by soTimeout: soTimeout is per-read, so a
216
+ // server drip-feeding handshake bytes would stretch it indefinitely. The watchdog closes
217
+ // `plain`, the transport — closing the SSLSocket instead would write a close_notify that
218
+ // can block against the very peer that caused the stall.
219
+ withWatchdog(
220
+ minOf(remainingMs(deadlineNanos), connectTimeoutMs + MAX_RESPONSE_MILLIS),
221
+ abort = { plain.close() },
222
+ ) {
223
+ val ssl = plainSslSocketFactory.createSocket(plain, host, port, true) as SSLSocket
224
+ wrapper = ssl
225
+ ssl.soTimeout = timeoutMs(deadlineNanos, readTimeoutMs)
226
+ ssl.startHandshake()
227
+ require(HttpsURLConnection.getDefaultHostnameVerifier().verify(host, ssl.session)) {
228
+ "AIA host $host does not match its certificate"
229
+ }
230
+ Connection(ssl, plain)
231
+ }
232
+ } catch (e: Exception) {
233
+ runCatching { plain.close() }
234
+ // The orphaned TLS wrapper too — with the transport already gone its close_notify attempt
235
+ // fails fast, and this releases the wrapper's own buffers rather than waiting on the GC.
236
+ runCatching { wrapper?.close() }
237
+ throw e
238
+ }
239
+ }
240
+
241
+ /**
242
+ * A per-operation socket timeout: [perOperationMs] clipped to the deadline, and never 0 ("no
243
+ * timeout" to a socket) — an already-expired deadline still gets a 1 ms timeout and fails at
244
+ * once.
245
+ */
246
+ private fun timeoutMs(deadlineNanos: Long, perOperationMs: Int): Int =
247
+ minOf(remainingMs(deadlineNanos), perOperationMs.toLong())
248
+ .coerceIn(1, Int.MAX_VALUE.toLong())
249
+ .toInt()
250
+
251
+ /**
252
+ * Runs [body] with a watchdog thread that invokes [abort] once [budgetMs] elapses — unless the
253
+ * body has already finished, which the done flag makes visible to a watchdog waking at just that
254
+ * moment, so a successful exchange cannot have its socket closed out from under it.
255
+ *
256
+ * The socket timeouts are per-operation and so cannot bound a peer that keeps every individual
257
+ * read just inside them; [abort] — closing the underlying transport — is what actually ends a
258
+ * stalled handshake or exchange: the blocked call throws, and a failed fetch is already handled
259
+ * as "no AIA".
260
+ */
261
+ private inline fun <T> withWatchdog(
262
+ budgetMs: Long,
263
+ crossinline abort: () -> Unit,
264
+ body: () -> T,
265
+ ): T {
266
+ val done = AtomicBoolean(false)
267
+ val watchdog =
268
+ thread(isDaemon = true, name = "aia-fetch-watchdog") {
269
+ runCatching { Thread.sleep(budgetMs.coerceAtLeast(0)) }
270
+ .onSuccess { if (!done.get()) runCatching { abort() } }
271
+ }
272
+ try {
273
+ return body()
274
+ } finally {
275
+ done.set(true)
276
+ watchdog.interrupt()
277
+ }
278
+ }
279
+
126
280
  companion object {
127
281
  private const val MAX_REDIRECTS = 5
128
282
 
283
+ /** Ceiling on how many fetched issuer certificates are retained. */
284
+ const val MAX_CACHE_ENTRIES = 32
285
+
286
+ /** Ceiling on a single AIA response, headers included. */
287
+ const val MAX_RESPONSE_BYTES = 1 shl 20 // 1 MiB
288
+
289
+ /** Ceiling on the wall-clock time spent reading one AIA response. */
290
+ const val MAX_RESPONSE_MILLIS = 10_000L
291
+
129
292
  /**
130
293
  * Whether [url] is free of characters that have no business in a URL.
131
294
  *
132
- * The cleartext fetch writes the path into a request line on a raw socket, and the URL is an
133
- * IA5String copied verbatim out of a certificate a hostile server presented. `java.net.URL`
134
- * preserves control characters — `URL("http://host:6379/\r\nSET foo bar")` parses to that host
135
- * and port with the CRLF intact in `file` — so without this check a certificate could inject
136
- * request lines of its own, to any host and port the device can reach. Space is excluded too:
137
- * it would split the request line's target from its HTTP version.
295
+ * The fetch writes the path into a request line on a raw socket, and the URL is an IA5String
296
+ * copied verbatim out of a certificate a hostile server presented. `java.net.URL` preserves
297
+ * control characters — `URL("http://host:6379/\r\nSET foo bar")` parses to that host and port
298
+ * with the CRLF intact in `file` — so without this check a certificate could inject request
299
+ * lines of its own, to any host and port the device can reach. Space is excluded too: it would
300
+ * split the request line's target from its HTTP version.
138
301
  */
139
302
  fun isSafeUrl(url: String): Boolean = url.none { it.code <= 0x20 || it.code == 0x7F }
140
303
 
141
- /** Ceiling on a single AIA response body. */
142
- const val MAX_RESPONSE_BYTES = 1 shl 20 // 1 MiB
304
+ /** [text] with anything non-printable replaced, so a log line cannot be forged. */
305
+ fun sanitizeForLog(text: String): String =
306
+ text.map { if (it.code <= 0x20 || it.code == 0x7F) '?' else it }.joinToString("")
143
307
 
144
- /** Ceiling on the wall-clock time spent reading one AIA response. */
145
- const val MAX_RESPONSE_MILLIS = 10_000L
308
+ /**
309
+ * Parses every certificate from the bytes a CA-Issuers URL serves — a single DER certificate
310
+ * (e.g. Let's Encrypt), a PEM file, or a PKCS#7 "certs-only" bundle. Empty if the bytes are not
311
+ * parseable certificates.
312
+ */
313
+ fun parseCertificates(bytes: ByteArray): List<X509Certificate> =
314
+ try {
315
+ CertificateFactory.getInstance("X.509")
316
+ .generateCertificates(ByteArrayInputStream(bytes))
317
+ .filterIsInstance<X509Certificate>()
318
+ } catch (e: Exception) {
319
+ emptyList()
320
+ }
146
321
 
147
322
  /**
148
323
  * Reads [input] to EOF, or returns null once it exceeds [limit] bytes or [budgetMs] of
@@ -175,20 +350,6 @@ class CachingCertificateFetcher(
175
350
  }
176
351
  }
177
352
 
178
- /**
179
- * Parses every certificate from the bytes a CA-Issuers URL serves — a single DER certificate
180
- * (e.g. Let's Encrypt), a PEM file, or a PKCS#7 "certs-only" bundle. Empty if the bytes are not
181
- * parseable certificates.
182
- */
183
- fun parseCertificates(bytes: ByteArray): List<X509Certificate> =
184
- try {
185
- CertificateFactory.getInstance("X.509")
186
- .generateCertificates(ByteArrayInputStream(bytes))
187
- .filterIsInstance<X509Certificate>()
188
- } catch (e: Exception) {
189
- emptyList()
190
- }
191
-
192
353
  /** Status code, `Location` header (if any), and decoded body of a raw HTTP response. */
193
354
  data class HttpResponse(val status: Int, val location: String?, val body: ByteArray)
194
355
 
@@ -231,7 +392,9 @@ class CachingCertificateFetcher(
231
392
  .substringBefore(';')
232
393
  .trim()
233
394
  .toIntOrNull(16) ?: break
234
- if (size == 0) break // last chunk
395
+ // `<= 0`, not `== 0`: toIntOrNull accepts a leading `-`, and a negative size would move
396
+ // `pos` backwards — the same rewinding-cursor loop the DER reader had.
397
+ if (size <= 0) break // last chunk, or malformed
235
398
  val dataStart = eol + 2
236
399
  if (dataStart + size > body.size) break
237
400
  out.write(body, dataStart, size)
@@ -8,14 +8,19 @@ import java.net.URLEncoder
8
8
  * Utility for handling browser paths and contextual URLs in the media browser system.
9
9
  *
10
10
  * Handles two types of special paths:
11
- * 1. System paths (prefixed with `/__`): Root, recent, and search paths
11
+ * 1. System paths (prefixed with `/__`): root, search, offline, error, and gate paths
12
12
  * 2. Contextual URLs: Embed parent context in track identifiers for Media3 integration
13
13
  *
14
- * Contextual URL format: `{parentPath}?__trackId={trackSrc}` Example:
15
- * "/library/radio?__trackId=song.mp3"
14
+ * Contextual URL format: `{parentPath}?__trackId={trackIdentity}&__index={childIndex}` (the
15
+ * identity is the track's `id` when non-blank, else its `src`) Example:
16
+ * "/library/radio?__trackId=song.mp3&__index=2"
17
+ *
18
+ * `__trackId` is the identity check; `__index` (the child's position on the page at stamp time) is
19
+ * only a tie-breaker between surfaces that carry the same identity — a stale index never selects a
20
+ * different track.
16
21
  *
17
22
  * This allows:
18
- * - Media3 to reference playable-only tracks (tracks with `src` but no `url`)
23
+ * - Media3 to reference playable-only tracks (tracks with `src` but no `path`)
19
24
  * - Cache lookup to work consistently
20
25
  * - Parent context to be preserved for queue restoration
21
26
  */
@@ -23,9 +28,6 @@ object BrowserPathHelper {
23
28
  /** Root path for media browsing */
24
29
  const val ROOT_PATH = "/__root"
25
30
 
26
- /** Recent media path for playback resumption */
27
- const val RECENT_PATH = "/__recent"
28
-
29
31
  /** Search path prefix (full path is /__search?q=query) */
30
32
  const val SEARCH_PATH_PREFIX = "/__search"
31
33
 
@@ -41,9 +43,12 @@ object BrowserPathHelper {
41
43
  // Query parameter name for contextual track identifiers
42
44
  private const val CONTEXTUAL_TRACK_PARAM = "__trackId"
43
45
 
46
+ // Query parameter name for the tapped child's page position (tie-breaker)
47
+ private const val CONTEXTUAL_INDEX_PARAM = "__index"
48
+
44
49
  /** Check if a path is a special system path (not a regular navigation path) */
45
50
  fun isSpecialPath(path: String): Boolean {
46
- return path == ROOT_PATH || path == RECENT_PATH || path.startsWith("$SEARCH_PATH_PREFIX?")
51
+ return path == ROOT_PATH || path.startsWith("$SEARCH_PATH_PREFIX?")
47
52
  }
48
53
 
49
54
  /** Create a search path for a given query */
@@ -63,13 +68,13 @@ object BrowserPathHelper {
63
68
  }
64
69
 
65
70
  /**
66
- * Strips the __trackId parameter from a contextual URL to get the parent path. If the URL is not
67
- * contextual, returns it unchanged.
71
+ * Strips the contextual parameters (__trackId and __index) from a contextual URL to get the
72
+ * parent path. If the URL is not contextual, returns it unchanged.
68
73
  *
69
74
  * @param url The URL to process
70
- * @return The URL without the __trackId parameter
75
+ * @return The URL without the contextual parameters
71
76
  *
72
- * Example: "/library/radio?__trackId=song.mp3" → "/library/radio" Example:
77
+ * Example: "/library/radio?__trackId=song.mp3&__index=2" → "/library/radio" Example:
73
78
  * "/search?q=jazz&__trackId=song.mp3" → "/search?q=jazz"
74
79
  */
75
80
  fun stripTrackId(url: String): String {
@@ -79,13 +84,13 @@ object BrowserPathHelper {
79
84
 
80
85
  val uri = url.toUri()
81
86
 
82
- // Build URL preserving everything except __trackId parameter
87
+ // Build URL preserving everything except the contextual parameters
83
88
  val builder = uri.buildUpon()
84
89
  builder.clearQuery()
85
90
 
86
- // Re-add all query params except __trackId
91
+ // Re-add all query params except __trackId and __index
87
92
  uri.queryParameterNames.forEach { paramName ->
88
- if (paramName != CONTEXTUAL_TRACK_PARAM) {
93
+ if (paramName != CONTEXTUAL_TRACK_PARAM && paramName != CONTEXTUAL_INDEX_PARAM) {
89
94
  uri.getQueryParameters(paramName).forEach { value ->
90
95
  builder.appendQueryParameter(paramName, value)
91
96
  }
@@ -96,19 +101,21 @@ object BrowserPathHelper {
96
101
  }
97
102
 
98
103
  /**
99
- * Builds a contextual URL by appending a track identifier to a parent path. Handles existing
100
- * query parameters correctly.
104
+ * Builds a contextual URL by appending a track identifier — and optionally the tapped child's
105
+ * page position — to a parent path. Handles existing query parameters correctly.
101
106
  *
102
107
  * @param parentPath The parent container path
103
- * @param trackId The track identifier (typically the `src` value)
104
- * @return A contextual URL combining parent path and track ID
108
+ * @param trackId The track identity (`id` when non-blank, else `src` — see Track.identity)
109
+ * @param index The child's position on the page at stamp time (tie-breaker)
110
+ * @return A contextual URL combining parent path, track ID, and index
105
111
  *
106
- * Example: build("/library", "song.mp3") → "/library?__trackId=song.mp3" Example:
112
+ * Example: build("/library", "song.mp3", 2) → "/library?__trackId=song.mp3&__index=2" Example:
107
113
  * build("/search?q=jazz", "song.mp3") → "/search?q=jazz&__trackId=song.mp3"
108
114
  */
109
- fun build(parentPath: String, trackId: String): String {
115
+ fun build(parentPath: String, trackId: String, index: Int? = null): String {
110
116
  val separator = if (parentPath.contains('?')) '&' else '?'
111
- return "$parentPath$separator$CONTEXTUAL_TRACK_PARAM=${Uri.encode(trackId)}"
117
+ val indexParam = if (index != null) "&$CONTEXTUAL_INDEX_PARAM=$index" else ""
118
+ return "$parentPath$separator$CONTEXTUAL_TRACK_PARAM=${Uri.encode(trackId)}$indexParam"
112
119
  }
113
120
 
114
121
  /**
@@ -129,6 +136,21 @@ object BrowserPathHelper {
129
136
  return uri.getQueryParameter(CONTEXTUAL_TRACK_PARAM)
130
137
  }
131
138
 
139
+ /**
140
+ * Extracts the stamped page index from a contextual URL, or null when the URL is not contextual
141
+ * or carries no (valid, non-negative) index.
142
+ *
143
+ * Example: "/library/radio?__trackId=song.mp3&__index=2" → 2
144
+ */
145
+ fun extractIndex(path: String): Int? {
146
+ if (!isContextual(path)) {
147
+ return null
148
+ }
149
+
150
+ val raw = Uri.parse(path).getQueryParameter(CONTEXTUAL_INDEX_PARAM) ?: return null
151
+ return raw.toIntOrNull()?.takeIf { it >= 0 }
152
+ }
153
+
132
154
  /**
133
155
  * Combines a base URL with a path, ensuring proper slash handling.
134
156
  *
@@ -158,22 +180,4 @@ object BrowserPathHelper {
158
180
  val normalizedPath = path.trimStart('/')
159
181
  return "$normalizedBase$normalizedPath"
160
182
  }
161
-
162
- /**
163
- * True if [segment] appears in [path] as a complete path segment — bounded on the left by the
164
- * string start or `/`, and on the right by the string end or one of `/`, `?`, `#`.
165
- */
166
- fun containsSegment(path: String, segment: String): Boolean {
167
- if (segment.isEmpty()) return false
168
- var from = 0
169
- while (true) {
170
- val idx = path.indexOf(segment, from)
171
- if (idx < 0) return false
172
- val beforeOk = idx == 0 || path[idx - 1] == '/'
173
- val end = idx + segment.length
174
- val afterOk = end == path.length || path[end] == '/' || path[end] == '?' || path[end] == '#'
175
- if (beforeOk && afterOk) return true
176
- from = idx + 1
177
- }
178
- }
179
183
  }