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
@@ -20,6 +20,7 @@ import com.audiobrowser.browser.displayArtworkSource
20
20
  import com.audiobrowser.browser.resolveArtworkUrl
21
21
  import com.audiobrowser.browser.unattributedArtworkSource
22
22
  import com.audiobrowser.extension.NumberExt.Companion.toSeconds
23
+ import com.audiobrowser.extension.identity
23
24
  import com.audiobrowser.model.PlayerSetupOptions
24
25
  import com.audiobrowser.model.PlayerUpdateOptions
25
26
  import com.audiobrowser.util.CoilBitmapLoader
@@ -36,7 +37,6 @@ import com.margelo.nitro.audiobrowser.ImageSource
36
37
  import com.margelo.nitro.audiobrowser.NativeGateRequest
37
38
  import com.margelo.nitro.audiobrowser.NowPlayingMetadata
38
39
  import com.margelo.nitro.audiobrowser.Playback
39
- import com.margelo.nitro.audiobrowser.PlaybackActiveTrackChangedEvent
40
40
  import com.margelo.nitro.audiobrowser.PlaybackError
41
41
  import com.margelo.nitro.audiobrowser.PlaybackPlayWhenReadyChangedEvent
42
42
  import com.margelo.nitro.audiobrowser.PlaybackProgressUpdatedEvent
@@ -351,7 +351,7 @@ class Player(internal val context: Context) {
351
351
  get() = exoPlayer.nextMediaItemIndex == C.INDEX_UNSET
352
352
 
353
353
  /**
354
- * The source path from which the current queue was expanded (e.g., from a contextual URL). Used
354
+ * The source path from which the current queue was expanded (e.g., from a contextual path). Used
355
355
  * to avoid re-expanding the queue when selecting tracks from the same source.
356
356
  */
357
357
  var queueSourcePath: String? = null
@@ -657,7 +657,7 @@ class Player(internal val context: Context) {
657
657
  * @param tracks The tracks to set as the new queue.
658
658
  * @param startIndex The index to start playback from (default: 0).
659
659
  * @param startPositionMs The position in milliseconds to start from (default: 0).
660
- * @param sourcePath Optional path from which this queue was expanded (for contextual URL
660
+ * @param sourcePath Optional path from which this queue was expanded (for contextual path
661
661
  * optimization).
662
662
  */
663
663
  fun setQueue(
@@ -685,17 +685,27 @@ class Player(internal val context: Context) {
685
685
  /**
686
686
  * Sets the favorited state of the currently playing track. Updates the heart icon in media
687
687
  * controllers without interrupting playback.
688
+ *
689
+ * @return whether the gesture applied — false only when there is no current track (nothing to
690
+ * favorite), so MediaSession callers can report an invalid state instead of a success the
691
+ * controller then watches revert.
688
692
  */
689
- fun setActiveTrackFavorited(favorited: Boolean) {
690
- val currentTrack = this.currentTrack ?: return
693
+ fun setActiveTrackFavorited(favorited: Boolean): Boolean {
694
+ val currentTrack = this.currentTrack ?: return false
691
695
 
692
696
  // Persist to the native favorites cache regardless of player state — the
693
697
  // user's gesture is durable even when there's no active media item to
694
- // update (queue tear-down, between-track gaps, etc.).
695
- currentTrack.src?.let { src -> browser?.browserManager?.updateFavorite(src, favorited) }
698
+ // update (queue tear-down, between-track gaps, etc.). Keyed by the track
699
+ // identity (id when non-blank, else src).
700
+ currentTrack.identity?.let { identity ->
701
+ browser?.browserManager?.updateFavorite(identity, favorited)
702
+ }
696
703
 
704
+ // Cache written above: the gesture is durably recorded even without a media
705
+ // item to restamp (queue tear-down, between-track gaps) — that counts as
706
+ // applied; there is also no visible heart to revert in that state.
697
707
  val index = exoPlayer.currentMediaItemIndex
698
- if (index == C.INDEX_UNSET) return
708
+ if (index == C.INDEX_UNSET) return true
699
709
 
700
710
  // Create updated Track with new favorited state
701
711
  val updatedTrack = currentTrack.copy(favorited = favorited)
@@ -719,28 +729,23 @@ class Player(internal val context: Context) {
719
729
  // Update the heart button icon in notification/Android Auto
720
730
  updateFavoriteButtonState(favorited)
721
731
 
722
- // Notify JS of the favorite state change
732
+ // Notify JS of the favorite state change. This is the ONLY emit: a favorite
733
+ // toggle is an in-place mutation of the active track, not a transition —
734
+ // onPlaybackActiveTrackChanged / onPlaybackQueueChanged stay
735
+ // transition-only, and the useActiveTrack / useQueue hooks subscribe to
736
+ // onFavoriteChanged themselves.
723
737
  callbacks?.onFavoriteChanged(FavoriteChangedEvent(updatedTrack, favorited))
724
-
725
- // Emit active track changed so useActiveTrack() hook updates
726
- val activeTrackEvent =
727
- PlaybackActiveTrackChangedEvent(
728
- lastIndex = index.toDouble(),
729
- lastTrack = currentTrack,
730
- lastPosition = exoPlayer.currentPosition.toSeconds(),
731
- index = index.toDouble(),
732
- track = updatedTrack,
733
- )
734
- callbacks?.onPlaybackActiveTrackChanged(activeTrackEvent)
735
-
736
- // Emit queue changed so useQueue() hook updates
737
- callbacks?.onPlaybackQueueChanged(tracks)
738
+ return true
738
739
  }
739
740
 
740
- /** Toggles the favorited state of the currently playing track. */
741
- fun toggleActiveTrackFavorited() {
742
- val currentTrack = this.currentTrack ?: return
743
- setActiveTrackFavorited(currentTrack.favorited != true)
741
+ /**
742
+ * Toggles the favorited state of the currently playing track.
743
+ *
744
+ * @return whether the toggle applied — false when there is no current track.
745
+ */
746
+ fun toggleActiveTrackFavorited(): Boolean {
747
+ val currentTrack = this.currentTrack ?: return false
748
+ return setActiveTrackFavorited(currentTrack.favorited != true)
744
749
  }
745
750
 
746
751
  // MARK: - Now Playing Metadata
@@ -926,10 +931,23 @@ class Player(internal val context: Context) {
926
931
  }
927
932
  }
928
933
 
929
- /** Jump to the live edge (default position) of a live item; no-op otherwise. */
934
+ /**
935
+ * Jump to the live edge of the current track; no-op unless it declares `live` — the track's own
936
+ * declaration is the gate (matching iOS and the spec), not ExoPlayer's stream-derived liveness,
937
+ * which never marks progressive ICY radio live. A stream with a live window seeks to its edge;
938
+ * anything else reconnects — for a non-seekable stream a fresh connection IS the live edge. A
939
+ * stale URL recovers through the failure-armed re-resolve in [TransformingDataSource].
940
+ */
930
941
  fun seekToLiveEdge() {
942
+ if (currentTrack?.live != true) return
931
943
  if (exoPlayer.isCurrentMediaItemLive) {
932
944
  exoPlayer.seekToDefaultPosition()
945
+ } else {
946
+ // ExoPlayer.prepare() only re-prepares from STATE_IDLE, hence the raw stop() — not
947
+ // Player.stop(), which would drop the play intent this reconnect must preserve.
948
+ exoPlayer.stop()
949
+ exoPlayer.seekToDefaultPosition()
950
+ exoPlayer.prepare()
933
951
  }
934
952
  }
935
953
 
@@ -1249,8 +1267,8 @@ class Player(internal val context: Context) {
1249
1267
 
1250
1268
  if (capabilitiesChanged) {
1251
1269
  // The `favorite` capability is the single favoriting switch — propagate
1252
- // its match mode to the browser so it can hydrate row hearts.
1253
- browser?.browserManager?.setFavoriteMatch(options.capabilities.favoriteMatch)
1270
+ // it to the browser so it can hydrate row hearts.
1271
+ browser?.browserManager?.setFavoriteEnabled(options.capabilities.favoriteEnabled)
1254
1272
  }
1255
1273
 
1256
1274
  if (progressUpdateEventIntervalChanged) {
@@ -1,20 +1,7 @@
1
1
  package com.audiobrowser.player
2
2
 
3
- import com.margelo.nitro.audiobrowser.FavoritesMatchMode
4
3
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
5
4
 
6
- /**
7
- * Resolved favorite match mode, or null when favoriting is disabled.
8
- *
9
- * `false`/unset → null; `true` → EXACT; `{ match }` → that match.
10
- */
11
- val PlayerCapabilities.favoriteMatch: FavoritesMatchMode?
12
- get() =
13
- favorite?.match(
14
- first = { enabled -> if (enabled) FavoritesMatchMode.EXACT else null },
15
- second = { config -> config.match },
16
- )
17
-
18
- /** Whether the favorite/like control is enabled. */
5
+ /** Whether the favorite/like control is enabled. Opt-in: only an explicit `true` enables it. */
19
6
  val PlayerCapabilities.favoriteEnabled: Boolean
20
- get() = favoriteMatch != null
7
+ get() = favorite == true
@@ -1,6 +1,7 @@
1
1
  package com.audiobrowser.tls
2
2
 
3
3
  import java.security.cert.X509Certificate
4
+ import javax.security.auth.x500.X500Principal
4
5
 
5
6
  /**
6
7
  * Fills in missing intermediate CA certificates by following the Authority Information Access (AIA)
@@ -14,6 +15,18 @@ import java.security.cert.X509Certificate
14
15
  * found".
15
16
  */
16
17
  object AiaCertChaser {
18
+ /** How many distinct CA-Issuers URLs from one certificate are worth trying. */
19
+ const val MAX_URLS_PER_CERT = 3
20
+
21
+ /**
22
+ * Wall-clock ceiling on one chain-completion attempt, across all of its fetches. The deadline it
23
+ * implies is handed to every fetch, whose connects, handshakes, reads and redirects are all
24
+ * clipped to the time remaining — so the whole walk ends at the deadline. The one thing it cannot
25
+ * cut short is a DNS lookup already in flight (Java exposes no timeout for resolution), so the
26
+ * true bound is this budget plus at most one lookup's duration.
27
+ */
28
+ const val MAX_CHASE_MILLIS = 20_000L
29
+
17
30
  /** Authority Information Access extension. */
18
31
  private const val AIA_OID = "1.3.6.1.5.5.7.1.1"
19
32
 
@@ -69,8 +82,9 @@ object AiaCertChaser {
69
82
 
70
83
  /**
71
84
  * Walks up from the presented chain, fetching each missing issuer via its predecessor's AIA "CA
72
- * Issuers" pointer, until it reaches a self-signed certificate, an issuer that cannot be fetched,
73
- * or [maxIntermediates] additions. Returns the (possibly extended) chain.
85
+ * Issuers" pointer, until it reaches a certificate issued by one of [anchorSubjects], a
86
+ * self-signed certificate, an issuer that cannot be fetched, or [maxIntermediates] additions.
87
+ * Returns the (possibly extended) chain.
74
88
  *
75
89
  * Only ever *adds* certificates — the caller is still responsible for validating the completed
76
90
  * chain against the system trust anchors, so this can never cause an untrusted chain to be
@@ -78,25 +92,52 @@ object AiaCertChaser {
78
92
  * selected (defends against an AIA pointer to an unrelated certificate, and picks the right one
79
93
  * out of a multi-certificate PKCS#7 bundle).
80
94
  *
95
+ * @param anchorSubjects the subjects of the caller's trust anchors. A chain whose top is already
96
+ * issued by one of them is not missing anything, so nothing is fetched — chasing could not help
97
+ * and the fetch would be a blocking round trip to a host named by a certificate that just
98
+ * failed validation. This is what keeps an expired-but-complete chain, a hostname mismatch or
99
+ * any other non-path failure from triggering a chase, and it stops the genuine
100
+ * missing-intermediate case one hop early, where the chain reaches a root the caller already
101
+ * trusts. Empty means "unknown", and the walk runs to one of the other stopping conditions.
102
+ * @param maxUrlsPerCert how many of a certificate's distinct CA-Issuers URLs are tried before
103
+ * giving up on it. The extension is authored by the same server that failed validation and
104
+ * nothing limits how many URLs it may list — a leaf can carry hundreds — while each one costs a
105
+ * blocking connect on the handshake thread. Real CAs publish one. Duplicates are collapsed
106
+ * before the cap, so the cap buys distinct hosts, not repeat visits to one dead one.
107
+ * @param budgetMs wall-clock ceiling on the whole walk. It is checked before each fetch, and the
108
+ * deadline it implies is passed to [fetch], which is expected to clip its own connects,
109
+ * handshakes, reads and redirects to the time remaining — so the budget bounds the fetches
110
+ * themselves, not just how many of them start.
81
111
  * @param fetch resolves a CA-Issuers URL to its candidate certificates (e.g. every certificate in
82
- * a PKCS#7 bundle); empty if nothing could be retrieved.
112
+ * a PKCS#7 bundle), giving up no later than `deadlineNanos` (absolute, [nowNanos] origin);
113
+ * empty if nothing could be retrieved.
83
114
  */
84
115
  fun completeChain(
85
116
  presented: List<X509Certificate>,
117
+ anchorSubjects: Set<X500Principal> = emptySet(),
86
118
  maxIntermediates: Int = 5,
87
- fetch: (url: String) -> List<X509Certificate>,
119
+ maxUrlsPerCert: Int = MAX_URLS_PER_CERT,
120
+ budgetMs: Long = MAX_CHASE_MILLIS,
121
+ nowNanos: () -> Long = System::nanoTime,
122
+ fetch: (url: String, deadlineNanos: Long) -> List<X509Certificate>,
88
123
  ): List<X509Certificate> {
89
124
  if (presented.isEmpty()) return presented
90
125
  val chain = presented.toMutableList()
91
126
  val seen = HashSet(chain)
127
+ val deadline = nowNanos() + budgetMs * 1_000_000
92
128
  var added = 0
93
129
  while (added < maxIntermediates) {
94
130
  val last = chain.last()
95
131
  if (last.subjectX500Principal == last.issuerX500Principal) break // self-signed root
132
+ if (last.issuerX500Principal in anchorSubjects) break // already reaches a trust anchor
96
133
  val issuer =
97
134
  extractCaIssuerUrls(last)
98
135
  .asSequence()
99
- .flatMap { url -> runCatching { fetch(url) }.getOrElse { emptyList() } }
136
+ .distinct()
137
+ .take(maxUrlsPerCert)
138
+ // Subtraction, not `>`: overflow-safe across nanoTime's arbitrary origin.
139
+ .takeWhile { nowNanos() - deadline < 0 }
140
+ .flatMap { url -> runCatching { fetch(url, deadline) }.getOrElse { emptyList() } }
100
141
  .firstOrNull { it.subjectX500Principal == last.issuerX500Principal } ?: break
101
142
  if (!seen.add(issuer)) break // cycle guard
102
143
  chain.add(issuer)
@@ -1,11 +1,16 @@
1
1
  package com.audiobrowser.tls
2
2
 
3
+ import android.os.Build
4
+ import androidx.annotation.RequiresApi
5
+ import java.lang.reflect.InvocationTargetException
6
+ import java.lang.reflect.Method
3
7
  import java.net.Socket
4
8
  import java.security.cert.CertificateException
5
9
  import java.security.cert.X509Certificate
6
10
  import javax.net.ssl.SSLEngine
7
11
  import javax.net.ssl.X509ExtendedTrustManager
8
12
  import javax.net.ssl.X509TrustManager
13
+ import javax.security.auth.x500.X500Principal
9
14
 
10
15
  /**
11
16
  * The [AiaChasingTrustManager] behaviour, as an [X509ExtendedTrustManager].
@@ -18,9 +23,8 @@ import javax.net.ssl.X509TrustManager
18
23
  * > checkServerTrusted(X509Certificate[], String, String) is used
19
24
  *
20
25
  * Wrapping it in a plain [X509TrustManager] hides those overloads, so conscrypt falls through to
21
- * the two-argument form and *every* HTTPS connection in the process fails — and, because the
22
- * wrapper treats a [CertificateException] as its cue to chase AIA, fails slowly. The socket
23
- * overload is also where endpoint identification happens for a raw `SSLSocket` configured with
26
+ * the two-argument form and *every* HTTPS connection in the process fails. The socket overload is
27
+ * also where endpoint identification happens for a raw `SSLSocket` configured with
24
28
  * `endpointIdentificationAlgorithm = "HTTPS"`; skipping it skips hostname verification.
25
29
  *
26
30
  * [X509ExtendedTrustManager] is API 24+, hence the split from [AiaChasingTrustManager] — see
@@ -28,25 +32,49 @@ import javax.net.ssl.X509TrustManager
28
32
  * themselves API 24+, so the plain variant on 23 loses nothing.
29
33
  *
30
34
  * @param delegate the wrapped trust manager, normally the platform default.
31
- * @param fetch resolves a CA-Issuers URL to its candidate certificates.
35
+ * @param fetch resolves a CA-Issuers URL to its candidate certificates, giving up no later than
36
+ * `deadlineNanos`.
32
37
  */
38
+ @RequiresApi(Build.VERSION_CODES.N)
33
39
  class AiaChasingExtendedTrustManager(
34
40
  private val delegate: X509TrustManager,
35
- private val fetch: (url: String) -> List<X509Certificate>,
41
+ private val fetch: (url: String, deadlineNanos: Long) -> List<X509Certificate>,
36
42
  ) : X509ExtendedTrustManager() {
37
43
 
38
44
  /** The delegate's own extended overloads, when it has them. */
39
45
  private val extended = delegate as? X509ExtendedTrustManager
40
46
 
47
+ private val anchorSubjects: Set<X500Principal> by lazy { delegate.anchorSubjects() }
48
+
49
+ /**
50
+ * The delegate's Android-specific hostname-aware check, if it has one.
51
+ *
52
+ * `android.net.http.X509TrustManagerExtensions` — how an HTTP client reaches per-host certificate
53
+ * pinning — locates this method reflectively and refuses a trust manager that lacks it. It is not
54
+ * part of [X509ExtendedTrustManager], and `RootTrustManager` is not a public type, so it is
55
+ * forwarded reflectively rather than overridden.
56
+ */
57
+ private val hostAwareCheck: Method? by lazy {
58
+ runCatching {
59
+ delegate.javaClass.getMethod(
60
+ "checkServerTrusted",
61
+ Array<X509Certificate>::class.java,
62
+ String::class.java,
63
+ String::class.java,
64
+ )
65
+ }
66
+ .getOrNull()
67
+ }
68
+
41
69
  override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
42
- withAiaRetry(chain) { delegate.checkServerTrusted(it, authType) }
70
+ withAiaRetry(chain, anchorSubjects, fetch) { delegate.checkServerTrusted(it, authType) }
43
71
 
44
72
  override fun checkServerTrusted(
45
73
  chain: Array<X509Certificate>,
46
74
  authType: String,
47
75
  socket: Socket?,
48
76
  ) =
49
- withAiaRetry(chain) {
77
+ withAiaRetry(chain, anchorSubjects, fetch) {
50
78
  // A delegate without the extended overloads cannot be hostname-aware in the first place,
51
79
  // so falling back to its two-argument form loses nothing it could have offered.
52
80
  extended?.checkServerTrusted(it, authType, socket)
@@ -58,11 +86,61 @@ class AiaChasingExtendedTrustManager(
58
86
  authType: String,
59
87
  engine: SSLEngine?,
60
88
  ) =
61
- withAiaRetry(chain) {
89
+ withAiaRetry(chain, anchorSubjects, fetch) {
62
90
  extended?.checkServerTrusted(it, authType, engine)
63
91
  ?: delegate.checkServerTrusted(it, authType)
64
92
  }
65
93
 
94
+ /**
95
+ * The signature `X509TrustManagerExtensions` looks for: validates against [host] and returns the
96
+ * chain it accepted. Kept in step with the other overloads, AIA retry included, so installing
97
+ * this wrapper does not cost a consumer its certificate pinning.
98
+ */
99
+ @Suppress("unused") // called reflectively
100
+ fun checkServerTrusted(
101
+ chain: Array<X509Certificate>,
102
+ authType: String,
103
+ host: String,
104
+ ): List<X509Certificate> {
105
+ var accepted: List<X509Certificate> = emptyList()
106
+ val method = hostAwareCheck
107
+ withAiaRetry(chain, anchorSubjects, fetch) { checked ->
108
+ // Whichever chain passed is the one to report — reporting the originally presented chain
109
+ // would hand a pinning consumer a chain missing the intermediate that made it validate.
110
+ // `ifEmpty` guards a delegate that reports nothing on success; the checked chain is what
111
+ // it accepted, so that is the answer.
112
+ accepted =
113
+ if (method != null)
114
+ invokeHostAware(method, checked, authType, host).ifEmpty { checked.toList() }
115
+ else {
116
+ // Same routing as the other overloads: an extended delegate may refuse its
117
+ // two-argument form outright (the domain-config case), so prefer its socket overload.
118
+ extended?.checkServerTrusted(checked, authType, null as Socket?)
119
+ ?: delegate.checkServerTrusted(checked, authType)
120
+ checked.toList()
121
+ }
122
+ }
123
+ return accepted
124
+ }
125
+
126
+ @Suppress("UNCHECKED_CAST")
127
+ private fun invokeHostAware(
128
+ method: Method,
129
+ chain: Array<X509Certificate>,
130
+ authType: String,
131
+ host: String,
132
+ ): List<X509Certificate> =
133
+ try {
134
+ method.invoke(delegate, chain, authType, host) as List<X509Certificate>
135
+ } catch (e: InvocationTargetException) {
136
+ // Unwrap, so a rejection still reaches withAiaRetry as the CertificateException it is.
137
+ when (val cause = e.targetException) {
138
+ is CertificateException -> throw cause
139
+ is RuntimeException -> throw cause
140
+ else -> throw CertificateException(cause)
141
+ }
142
+ }
143
+
66
144
  // Client certificates are the app's own; there is nothing to chase.
67
145
 
68
146
  override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) =
@@ -87,23 +165,4 @@ class AiaChasingExtendedTrustManager(
87
165
  }
88
166
 
89
167
  override fun getAcceptedIssuers(): Array<X509Certificate> = delegate.acceptedIssuers
90
-
91
- /**
92
- * Runs [check], and on rejection retries it once against the AIA-completed chain. The completed
93
- * chain goes through the same [check], so trust is never weakened: whatever the delegate would
94
- * have refused, it still refuses.
95
- */
96
- private inline fun withAiaRetry(
97
- chain: Array<X509Certificate>,
98
- check: (Array<X509Certificate>) -> Unit,
99
- ) {
100
- try {
101
- check(chain)
102
- } catch (original: CertificateException) {
103
- val completed = AiaCertChaser.completeChain(chain.toList(), fetch = fetch)
104
- // Nothing could be added — the original failure stands (don't re-check an unchanged chain).
105
- if (completed.size == chain.size) throw original
106
- check(completed.toTypedArray())
107
- }
108
- }
109
168
  }
@@ -3,6 +3,7 @@ package com.audiobrowser.tls
3
3
  import java.security.cert.CertificateException
4
4
  import java.security.cert.X509Certificate
5
5
  import javax.net.ssl.X509TrustManager
6
+ import javax.security.auth.x500.X500Principal
6
7
 
7
8
  /**
8
9
  * An [X509TrustManager] that retries validation after filling in missing intermediate certificates
@@ -10,28 +11,71 @@ import javax.net.ssl.X509TrustManager
10
11
  *
11
12
  * Wraps a [delegate] (typically the platform default trust manager). On the happy path the delegate
12
13
  * accepts the server-presented chain on the first try and this adds nothing. Only when the delegate
13
- * rejects the chain does it attempt AIA chasing — and the *completed* chain is still validated by
14
- * the same delegate against the system trust anchors, so trust can never be weakened: an untrusted
15
- * root still fails. See [AiaCertChaser].
14
+ * rejects the chain *and* the top of the chain is not already issued by one of the delegate's
15
+ * anchors does it attempt AIA chasing. That gate is an issuer-name test, not a diagnosis: it rules
16
+ * out the common non-path failures (an expired leaf, a hostname mismatch on a complete chain), but
17
+ * a chain topping at a root the device does not carry is still chased whatever its real problem
18
+ * was. Either way the *completed* chain is still validated by the same delegate against the system
19
+ * trust anchors, so trust can never be weakened: an untrusted root still fails. See
20
+ * [AiaCertChaser].
21
+ *
22
+ * This is the API 23 variant. On 24+ [AiaChasingExtendedTrustManager] is used instead, and must be:
23
+ * see its documentation for why a plain [X509TrustManager] is not a safe wrapper there.
16
24
  */
17
25
  class AiaChasingTrustManager(
18
26
  private val delegate: X509TrustManager,
19
- private val fetch: (url: String) -> List<X509Certificate>,
27
+ private val fetch: (url: String, deadlineNanos: Long) -> List<X509Certificate>,
20
28
  ) : X509TrustManager {
21
29
 
22
- override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
23
- try {
24
- delegate.checkServerTrusted(chain, authType)
25
- } catch (original: CertificateException) {
26
- val completed = AiaCertChaser.completeChain(chain.toList(), fetch = fetch)
27
- // Nothing could be added — the original failure stands (don't re-check an unchanged chain).
28
- if (completed.size == chain.size) throw original
29
- delegate.checkServerTrusted(completed.toTypedArray(), authType)
30
- }
31
- }
30
+ private val anchorSubjects: Set<X500Principal> by lazy { delegate.anchorSubjects() }
31
+
32
+ override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
33
+ withAiaRetry(chain, anchorSubjects, fetch) { delegate.checkServerTrusted(it, authType) }
32
34
 
33
35
  override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) =
34
36
  delegate.checkClientTrusted(chain, authType)
35
37
 
36
38
  override fun getAcceptedIssuers(): Array<X509Certificate> = delegate.acceptedIssuers
37
39
  }
40
+
41
+ /** The subjects of a trust manager's accepted issuers; empty if it will not say. */
42
+ internal fun X509TrustManager.anchorSubjects(): Set<X500Principal> =
43
+ runCatching { acceptedIssuers.mapTo(HashSet()) { it.subjectX500Principal } }
44
+ .getOrElse { emptySet() }
45
+
46
+ /**
47
+ * Runs [check], and on rejection retries it once against the AIA-completed chain.
48
+ *
49
+ * Nothing is fetched when the presented chain already reaches one of the caller's anchors — see
50
+ * [AiaCertChaser.completeChain]'s `anchorSubjects`. Without that test *every* failed handshake in
51
+ * the process, whatever the cause, would make a blocking outbound request to a host named by the
52
+ * certificate that just failed: an expired certificate, a hostname mismatch and a
53
+ * certificate-transparency refusal all arrive here as a [CertificateException] from a delegate that
54
+ * is perfectly happy with the path. (The test is on issuer names, so a chain topping at an unknown
55
+ * root is still chased even when its real failure was something else.)
56
+ *
57
+ * The completed chain goes through the same [check], so trust is never weakened: whatever the
58
+ * delegate would have refused, it still refuses.
59
+ */
60
+ internal inline fun withAiaRetry(
61
+ chain: Array<X509Certificate>,
62
+ anchorSubjects: Set<X500Principal>,
63
+ noinline fetch: (url: String, deadlineNanos: Long) -> List<X509Certificate>,
64
+ check: (Array<X509Certificate>) -> Unit,
65
+ ) {
66
+ try {
67
+ check(chain)
68
+ } catch (original: CertificateException) {
69
+ val completed = AiaCertChaser.completeChain(chain.toList(), anchorSubjects, fetch = fetch)
70
+ // Nothing could be added — the original failure stands (don't re-check an unchanged chain).
71
+ if (completed.size == chain.size) throw original
72
+ try {
73
+ check(completed.toTypedArray())
74
+ } catch (retried: CertificateException) {
75
+ // The chase can change which failure the caller sees (a missing intermediate can turn out
76
+ // to hide an expiry). Keep the first one attached rather than losing it.
77
+ retried.addSuppressed(original)
78
+ throw retried
79
+ }
80
+ }
81
+ }
@@ -5,6 +5,7 @@ import java.security.KeyStore
5
5
  import javax.net.ssl.SSLContext
6
6
  import javax.net.ssl.SSLSocketFactory
7
7
  import javax.net.ssl.TrustManagerFactory
8
+ import javax.net.ssl.X509ExtendedTrustManager
8
9
  import javax.net.ssl.X509TrustManager
9
10
 
10
11
  /**
@@ -12,11 +13,24 @@ import javax.net.ssl.X509TrustManager
12
13
  * following the leaf's Authority Information Access (AIA) "CA Issuers" pointer — the behaviour
13
14
  * Apple's Secure Transport has by default but Android's lacks.
14
15
  *
15
- * The returned components are side-effect-free: they wrap the platform default trust manager and
16
- * only ever *add* a missing intermediate before re-validating against the same system trust
17
- * anchors, so they cannot weaken trust. Installing them as a process default (e.g.
18
- * `HttpsURLConnection.setDefaultSSLSocketFactory(AiaTls.socketFactory())`) is the caller's
19
- * decision.
16
+ * The returned components wrap the platform default trust manager and only ever *add* a missing
17
+ * intermediate before re-validating against the same system trust anchors, so they cannot weaken
18
+ * trust: whatever the platform would have refused, it still refuses.
19
+ *
20
+ * What they are not is side-effect-free. Completing a chain means fetching the missing intermediate
21
+ * from a URL named by the certificate that just failed to validate — so a rejected handshake can
22
+ * make an outbound request to a host, port and path of that server's choosing, over cleartext, not
23
+ * subject to the app's `NetworkSecurityPolicy`. That is inherent to AIA chasing and is what
24
+ * browsers and Apple's Secure Transport do too, but it is a real consequence of installing this,
25
+ * and it is bounded rather than absent: see [CachingCertificateFetcher] for the per-fetch limits
26
+ * and [AiaCertChaser.MAX_CHASE_MILLIS] for the wall-clock ceiling on the whole chase. The fetch is
27
+ * skipped when the chain already reaches one of the system anchors — an issuer-name test, so it
28
+ * spares the common expired-leaf and hostname-mismatch failures, but a chain topping at a root the
29
+ * device does not carry is still chased whatever its real problem was.
30
+ *
31
+ * Installing them as a process default (e.g.
32
+ * `HttpsURLConnection.setDefaultSSLSocketFactory(AiaTls.socketFactory())`) applies that to every
33
+ * connection in the process, and is the caller's decision.
20
34
  */
21
35
  object AiaTls {
22
36
 
@@ -38,7 +52,7 @@ object AiaTls {
38
52
  */
39
53
  fun trustManager(): X509TrustManager {
40
54
  val fetcher = CachingCertificateFetcher()
41
- val fetch = { url: String -> fetcher.fetch(url) }
55
+ val fetch = { url: String, deadlineNanos: Long -> fetcher.fetch(url, deadlineNanos) }
42
56
  val system = systemTrustManager()
43
57
  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
44
58
  AiaChasingExtendedTrustManager(system, fetch)