react-native-audio-browser 0.2.0-next.11 → 0.2.0-next.13

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 (416) hide show
  1. package/README.md +14 -4
  2. package/android/build.gradle +20 -1
  3. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +115 -26
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +262 -188
  5. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
  6. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
  7. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +95 -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 +60 -0
  10. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +19 -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/model/PlayerUpdateOptions.kt +1 -1
  14. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
  15. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
  16. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +109 -74
  17. package/android/src/main/java/com/audiobrowser/player/Player.kt +51 -30
  18. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  19. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  20. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
  21. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  22. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
  23. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +52 -53
  24. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +74 -41
  25. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  26. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +229 -50
  27. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  28. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  29. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
  30. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
  31. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
  32. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
  33. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  34. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  35. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
  36. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  37. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  38. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  39. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  40. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
  41. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  42. package/ios/Browser/BrowserConfig.swift +102 -5
  43. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  44. package/ios/Browser/BrowserManager.swift +229 -180
  45. package/ios/Browser/BrowserPathHelper.swift +44 -44
  46. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  47. package/ios/Browser/JsonModels.swift +258 -106
  48. package/ios/Browser/SearchDrillIn.swift +6 -6
  49. package/ios/Browser/SectionPresentation.swift +64 -0
  50. package/ios/Browser/SectionScope.swift +58 -24
  51. package/ios/Browser/StyleResolver.swift +58 -0
  52. package/ios/Browser/TabBarEntries.swift +9 -3
  53. package/ios/Browser/TrackIdTemplate.swift +37 -0
  54. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  55. package/ios/CarPlay/CarPlayController.swift +227 -85
  56. package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
  57. package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
  58. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  59. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  60. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  61. package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
  62. package/ios/Extension/Section+Copying.swift +31 -0
  63. package/ios/Extension/Track+Copying.swift +6 -12
  64. package/ios/Extension/Track+Identity.swift +18 -0
  65. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  66. package/ios/Http/HttpClient.swift +8 -2
  67. package/ios/HybridAudioBrowser.swift +166 -45
  68. package/ios/Model/NitroTypeStubs.swift +106 -21
  69. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  70. package/ios/Player/MediaIntentCriteria.swift +13 -5
  71. package/ios/Player/MediaLoader.swift +33 -9
  72. package/ios/Player/NowPlayingUpdater.swift +54 -6
  73. package/ios/Player/PlaybackCoordinator.swift +13 -6
  74. package/ios/Player/PlaybackTimer.swift +8 -2
  75. package/ios/Player/QueueManager.swift +36 -10
  76. package/ios/Player/RetryManager.swift +4 -3
  77. package/ios/Player/ShuffleOrder.swift +23 -0
  78. package/ios/Player/SleepTimerHandling.swift +13 -0
  79. package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
  80. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  81. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  82. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  83. package/ios/Tests/JsonModelsTests.swift +150 -1
  84. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  85. package/ios/Tests/QueueManagerTests.swift +72 -6
  86. package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
  87. package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
  88. package/ios/Tests/SearchDrillInTests.swift +16 -16
  89. package/ios/Tests/SectionPresentationTests.swift +90 -0
  90. package/ios/Tests/SectionScopeTests.swift +87 -55
  91. package/ios/Tests/StyleResolverTests.swift +127 -0
  92. package/ios/Tests/Support/TestTypes.swift +7 -0
  93. package/ios/Tests/TabBarEntriesTests.swift +18 -18
  94. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  95. package/ios/Tests/TrackIdentityTests.swift +20 -0
  96. package/ios/Tests/TrackSelectorTests.swift +168 -33
  97. package/ios/TrackPlayer.swift +57 -34
  98. package/ios/TrackSelector.swift +45 -30
  99. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  100. package/ios/Util/NetworkMonitor.swift +43 -26
  101. package/ios/Util/SFSymbolRenderer.swift +24 -2
  102. package/ios/Util/SVGProcessor.swift +9 -2
  103. package/lib/commonjs/AudioBrowser.js +11 -0
  104. package/lib/commonjs/AudioBrowser.js.map +1 -1
  105. package/lib/commonjs/features/browser-config.js +11 -8
  106. package/lib/commonjs/features/browser-config.js.map +1 -1
  107. package/lib/commonjs/features/browser.js +2 -2
  108. package/lib/commonjs/features/favorites.js +13 -10
  109. package/lib/commonjs/features/favorites.js.map +1 -1
  110. package/lib/commonjs/features/playback/controls.js +4 -2
  111. package/lib/commonjs/features/playback/controls.js.map +1 -1
  112. package/lib/commonjs/features/playback/progress.js +2 -1
  113. package/lib/commonjs/features/playback/progress.js.map +1 -1
  114. package/lib/commonjs/features/playback/rate.js +8 -1
  115. package/lib/commonjs/features/playback/rate.js.map +1 -1
  116. package/lib/commonjs/features/playback/state.js +2 -3
  117. package/lib/commonjs/features/playback/state.js.map +1 -1
  118. package/lib/commonjs/features/player/options.js.map +1 -1
  119. package/lib/commonjs/features/player/setup.js +3 -1
  120. package/lib/commonjs/features/player/setup.js.map +1 -1
  121. package/lib/commonjs/features/player/validateOptions.js +2 -0
  122. package/lib/commonjs/features/player/validateOptions.js.map +1 -1
  123. package/lib/commonjs/features/queue/activeTrack.js +14 -3
  124. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  125. package/lib/commonjs/features/queue/queue.js +11 -4
  126. package/lib/commonjs/features/queue/queue.js.map +1 -1
  127. package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
  128. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  129. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  130. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  131. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  132. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  133. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  134. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  135. package/lib/commonjs/web/NativeAudioBrowser.js +32 -24
  136. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  137. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  138. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  139. package/lib/commonjs/web/browser/BrowserManager.js +144 -87
  140. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  141. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  142. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  143. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  144. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  145. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  146. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  147. package/lib/commonjs/web/browser/sections.js +89 -0
  148. package/lib/commonjs/web/browser/sections.js.map +1 -0
  149. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  150. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  151. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  152. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  153. package/lib/module/AudioBrowser.js +1 -0
  154. package/lib/module/AudioBrowser.js.map +1 -1
  155. package/lib/module/features/browser-config.js +11 -8
  156. package/lib/module/features/browser-config.js.map +1 -1
  157. package/lib/module/features/browser.js +2 -2
  158. package/lib/module/features/favorites.js +13 -10
  159. package/lib/module/features/favorites.js.map +1 -1
  160. package/lib/module/features/playback/controls.js +4 -2
  161. package/lib/module/features/playback/controls.js.map +1 -1
  162. package/lib/module/features/playback/progress.js +2 -1
  163. package/lib/module/features/playback/progress.js.map +1 -1
  164. package/lib/module/features/playback/rate.js +8 -1
  165. package/lib/module/features/playback/rate.js.map +1 -1
  166. package/lib/module/features/playback/state.js +2 -3
  167. package/lib/module/features/playback/state.js.map +1 -1
  168. package/lib/module/features/player/options.js.map +1 -1
  169. package/lib/module/features/player/setup.js +3 -1
  170. package/lib/module/features/player/setup.js.map +1 -1
  171. package/lib/module/features/player/validateOptions.js +2 -0
  172. package/lib/module/features/player/validateOptions.js.map +1 -1
  173. package/lib/module/features/queue/activeTrack.js +14 -3
  174. package/lib/module/features/queue/activeTrack.js.map +1 -1
  175. package/lib/module/features/queue/queue.js +11 -4
  176. package/lib/module/features/queue/queue.js.map +1 -1
  177. package/lib/module/utils/LazyNativeEmitter.js +21 -10
  178. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  179. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  180. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  181. package/lib/module/utils/getTrackIdentity.js +20 -0
  182. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  183. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  184. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  185. package/lib/module/web/NativeAudioBrowser.js +32 -24
  186. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  187. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  188. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  189. package/lib/module/web/browser/BrowserManager.js +144 -87
  190. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  191. package/lib/module/web/browser/FavoriteManager.js +31 -21
  192. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  193. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  194. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  195. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  196. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  197. package/lib/module/web/browser/sections.js +83 -0
  198. package/lib/module/web/browser/sections.js.map +1 -0
  199. package/lib/module/web/player/NowPlayingManager.js +4 -3
  200. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  201. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  202. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  203. package/lib/typescript/src/AudioBrowser.d.ts +1 -0
  204. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  205. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  206. package/lib/typescript/src/features/browser.d.ts +2 -2
  207. package/lib/typescript/src/features/favorites.d.ts +13 -10
  208. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  209. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  210. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  211. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  212. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  213. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  214. package/lib/typescript/src/features/playback/state.d.ts +2 -3
  215. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  216. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  217. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  218. package/lib/typescript/src/features/player/options.d.ts +21 -19
  219. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  220. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  221. package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
  222. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
  223. package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
  224. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  225. package/lib/typescript/src/features/queue/queue.d.ts +5 -2
  226. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  227. package/lib/typescript/src/types/browser-native.d.ts +2 -2
  228. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  229. package/lib/typescript/src/types/browser-nodes.d.ts +361 -126
  230. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  231. package/lib/typescript/src/types/browser.d.ts +33 -53
  232. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  233. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
  234. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  235. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  236. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  237. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  238. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  239. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  240. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  241. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
  242. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  243. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
  244. package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
  245. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  246. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  247. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
  248. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
  249. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
  250. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
  251. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
  252. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
  253. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
  254. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
  255. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
  256. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
  257. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
  258. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
  259. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
  260. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
  261. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  262. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +33 -14
  263. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  264. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  265. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
  266. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +22 -10
  267. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
  268. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +10 -7
  269. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
  270. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  271. package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
  272. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
  273. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  274. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +55 -51
  275. package/nitrogen/generated/android/c++/JSection.hpp +120 -0
  276. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  277. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  278. package/nitrogen/generated/android/c++/JTrack.hpp +23 -48
  279. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
  280. package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
  281. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
  282. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  283. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  284. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  285. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +18 -6
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
  297. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +158 -82
  298. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +25 -10
  299. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +25 -10
  300. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  301. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  302. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  303. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  304. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  305. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  306. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +5 -5
  307. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  308. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  309. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
  310. package/nitrogen/generated/ios/swift/Section.swift +100 -0
  311. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  312. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  313. package/nitrogen/generated/ios/swift/Track.swift +20 -73
  314. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  315. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  316. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  317. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  318. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  319. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  320. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  321. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
  322. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  323. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  324. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
  325. package/nitrogen/generated/shared/c++/Section.hpp +106 -0
  326. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  327. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  328. package/nitrogen/generated/shared/c++/Track.hpp +15 -31
  329. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  330. package/package.json +7 -2
  331. package/src/AudioBrowser.ts +1 -0
  332. package/src/features/browser-config.test.ts +21 -6
  333. package/src/features/browser-config.ts +11 -8
  334. package/src/features/browser.test.ts +1 -1
  335. package/src/features/browser.ts +2 -2
  336. package/src/features/favorites.ts +13 -10
  337. package/src/features/hook-wiring.test.ts +63 -15
  338. package/src/features/playback/controls.ts +4 -2
  339. package/src/features/playback/progress.ts +2 -1
  340. package/src/features/playback/rate.ts +8 -1
  341. package/src/features/playback/state.ts +2 -3
  342. package/src/features/playback/trackPlaybackTime.ts +1 -1
  343. package/src/features/player/options.ts +27 -19
  344. package/src/features/player/setup.test.ts +12 -3
  345. package/src/features/player/setup.ts +3 -1
  346. package/src/features/player/validateOptions.ts +2 -0
  347. package/src/features/queue/activeTrack.ts +14 -3
  348. package/src/features/queue/queue.ts +11 -4
  349. package/src/nitro-stub-drift.test.ts +40 -0
  350. package/src/public-surface.test.ts +40 -0
  351. package/src/specs/audio-browser.nitro.ts +4 -3
  352. package/src/style-resolution.test.ts +210 -0
  353. package/src/types/browser-native.ts +2 -2
  354. package/src/types/browser-nodes.ts +377 -135
  355. package/src/types/browser.ts +34 -55
  356. package/src/utils/LazyNativeEmitter.ts +20 -10
  357. package/src/utils/NativeUpdatedValue.ts +29 -13
  358. package/src/utils/getTrackIdentity.ts +20 -0
  359. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  360. package/src/web/NativeAudioBrowser.ts +37 -22
  361. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  362. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  363. package/src/web/browser/BrowserManager.ts +176 -95
  364. package/src/web/browser/FavoriteManager.test.ts +84 -0
  365. package/src/web/browser/FavoriteManager.ts +28 -23
  366. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  367. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  368. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  369. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  370. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  371. package/src/web/browser/sections.test.ts +91 -0
  372. package/src/web/browser/sections.ts +72 -0
  373. package/src/web/player/NowPlayingManager.ts +4 -3
  374. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  375. package/src/web/util/BrowserPathHelper.ts +61 -13
  376. package/website/guide/android-auto.md +114 -0
  377. package/website/guide/android-certificates.md +409 -0
  378. package/website/guide/artwork.md +255 -0
  379. package/website/guide/audio-output.md +135 -0
  380. package/website/guide/automotive.md +86 -0
  381. package/website/guide/basic-usage.md +151 -0
  382. package/website/guide/battery.md +115 -0
  383. package/website/guide/browser.md +581 -0
  384. package/website/guide/carplay.md +620 -0
  385. package/website/guide/configuration.md +173 -0
  386. package/website/guide/equalizer.md +231 -0
  387. package/website/guide/errors.md +310 -0
  388. package/website/guide/favorites.md +154 -0
  389. package/website/guide/gate.md +143 -0
  390. package/website/guide/getting-started.md +46 -0
  391. package/website/guide/hooks.md +238 -0
  392. package/website/guide/live-streams.md +125 -0
  393. package/website/guide/metadata.md +239 -0
  394. package/website/guide/migrating-from-track-player.md +244 -0
  395. package/website/guide/native-callback-fetch.md +127 -0
  396. package/website/guide/network.md +86 -0
  397. package/website/guide/now-playing.md +175 -0
  398. package/website/guide/playback.md +328 -0
  399. package/website/guide/queue.md +300 -0
  400. package/website/guide/remote-controls.md +256 -0
  401. package/website/guide/search.md +255 -0
  402. package/website/guide/sleep-timer.md +268 -0
  403. package/website/guide/track.md +246 -0
  404. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  405. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  406. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  407. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  408. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  409. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  410. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  411. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  412. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  413. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  414. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  415. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  416. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
@@ -1,42 +1,84 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
- import com.margelo.nitro.audiobrowser.ImageRowItem
3
+ import com.audiobrowser.extension.identity
4
+ import com.margelo.nitro.audiobrowser.ResolvedTrack
5
+ import com.margelo.nitro.audiobrowser.Section
4
6
  import com.margelo.nitro.audiobrowser.Track
5
7
 
6
8
  /**
7
9
  * Queue scope is the tapped section, not the whole page (ADR 0006): the playback context a listener
8
10
  * expects is the list they tapped in — a page aggregating several sections must not leak
9
- * next/previous across them.
11
+ * next/previous across them. Sections are structural (ADR 0010), so scoping is a lookup, not a
12
+ * groupTitle-run derivation.
10
13
  */
11
14
  object SectionScope {
12
- sealed class Section {
13
- /** The tapped id lives inside an image row's items. */
14
- data class ImageRow(val items: List<ImageRowItem>) : Section()
15
-
16
- /**
17
- * The contiguous `groupTitle` run around the tapped child (items with no group title form runs
18
- * of their own).
19
- */
20
- data class Run(val tracks: List<Track>) : Section()
21
- }
15
+ /**
16
+ * The section containing the tapped identity, plus the tapped child's offset within it — non-null
17
+ * only when the stamped flat index pinned the exact copy.
18
+ */
19
+ data class Scoped(val section: Section, val tappedOffset: Int?)
22
20
 
23
21
  /**
24
- * The section of [children] containing the playable [trackId], or null when the id is not found.
22
+ * The section of [sections] containing the playable [trackId] (a track identity — id when
23
+ * non-blank, else src), or null when not found.
24
+ *
25
+ * [tappedIndex] — the flat page position stamped into the contextual URL (children concatenated
26
+ * in section order) — is a tie-breaker, never an identifier: when the child at that position
27
+ * still carries the tapped identity, it pins which section (and which copy) was tapped; when it
28
+ * doesn't (the list shifted), resolution falls back to the first section containing the identity.
29
+ * A stale index can therefore never select a different track — at worst a different copy of the
30
+ * same one.
25
31
  */
26
- fun section(children: List<Track>, trackId: String): Section? {
27
- for (child in children) {
28
- val items = child.imageRow
29
- if (items != null && items.any { it.src == trackId }) {
30
- return Section.ImageRow(items.toList())
32
+ fun scoped(sections: List<Section>, trackId: String, tappedIndex: Int? = null): Scoped? {
33
+ if (tappedIndex != null && tappedIndex >= 0) {
34
+ var start = 0
35
+ for (section in sections) {
36
+ val offset = tappedIndex - start
37
+ if (offset < section.children.size) {
38
+ if (section.children[offset].identity == trackId) {
39
+ return Scoped(section, offset)
40
+ }
41
+ break
42
+ }
43
+ start += section.children.size
44
+ }
45
+ }
46
+ for (section in sections) {
47
+ if (section.children.any { it.identity == trackId }) {
48
+ return Scoped(section, null)
31
49
  }
32
50
  }
33
- val index = children.indexOfFirst { it.src == trackId }
34
- if (index < 0) return null
35
- val group = children[index].groupTitle
36
- var start = index
37
- while (start > 0 && children[start - 1].groupTitle == group) start--
38
- var end = index
39
- while (end + 1 < children.size && children[end + 1].groupTitle == group) end++
40
- return Section.Run(children.subList(start, end + 1).toList())
51
+ return null
41
52
  }
42
53
  }
54
+
55
+ // Page shape helpers (ADR 0010)
56
+
57
+ /**
58
+ * One untitled section — the canonical wrap of a flat track list (`children` authoring sugar,
59
+ * search results — ADR 0010).
60
+ */
61
+ fun untitledSection(children: Array<Track>): Section =
62
+ Section(title = null, subtitle = null, style = null, path = null, children = children)
63
+
64
+ /**
65
+ * The canonical sectioned shape: `sections` wins when present; plain `children` is authoring sugar
66
+ * for one untitled section.
67
+ */
68
+ val ResolvedTrack.normalizedSections: List<Section>?
69
+ get() = sections?.toList() ?: children?.let { listOf(untitledSection(it)) }
70
+
71
+ /**
72
+ * [normalizedSections] with the page block folded in: each section's style resolves `section ??
73
+ * page` per property ([StyleResolver], ADR 0011), so everything downstream sees only effective
74
+ * section blocks — a page is a Track that is also the container of its sections.
75
+ */
76
+ fun ResolvedTrack.styleResolvedSections(): List<Section>? =
77
+ normalizedSections?.map { it.copy(style = StyleResolver.sectionStyle(it.style, style)) }
78
+
79
+ /**
80
+ * The page's children concatenated in section order — the flattening that defines contextual
81
+ * `__index` positions and the flat views (tabs, search) of a sectioned page.
82
+ */
83
+ val ResolvedTrack.flattenedChildren: List<Track>?
84
+ get() = normalizedSections?.flatMap { it.children.asList() }
@@ -0,0 +1,60 @@
1
+ package com.audiobrowser.browser
2
+
3
+ import com.margelo.nitro.audiobrowser.SectionStyle
4
+ import com.margelo.nitro.audiobrowser.TrackStyle
5
+
6
+ /**
7
+ * The one place style declarations resolve to effective values (ADR 0011).
8
+ *
9
+ * Two mechanisms, deliberately distinct:
10
+ * - **Inherited item properties** resolve `track ?? section ?? page`, per property — a track's own
11
+ * declaration wins over the blocks that contain it.
12
+ * - **Container properties and the positional `display`** resolve by scope override: `section ??
13
+ * page` — the page declares for its whole scope, a section overrides for its own children.
14
+ * `display` is never inherited item-to-container: each holder describes its own children, and a
15
+ * track's `display` is the promise for the page *it* opens, not its rendering inside this
16
+ * section.
17
+ *
18
+ * Every key of `SectionStyle` must be read by [sectionStyle] and every inherited key of
19
+ * `TrackStyle` by [trackStyle] — Nitro flattens the spec's `extends`, so
20
+ * `src/style-resolution.test.ts` enforces this completeness, not the type system.
21
+ */
22
+ object StyleResolver {
23
+ /**
24
+ * Folds the page block into a section's block: every `SectionStyle` key resolves `section ??
25
+ * page`. The result is the section's effective block; item resolution then only needs `track ??
26
+ * section`.
27
+ */
28
+ fun sectionStyle(section: SectionStyle?, page: SectionStyle?): SectionStyle =
29
+ SectionStyle(
30
+ display = section?.display ?: page?.display,
31
+ artworkRendering = section?.artworkRendering ?: page?.artworkRendering,
32
+ imageShape = section?.imageShape ?: page?.imageShape,
33
+ accessorySymbol = section?.accessorySymbol ?: page?.accessorySymbol,
34
+ cardTint = section?.cardTint ?: page?.cardTint,
35
+ cardImage = section?.cardImage ?: page?.cardImage,
36
+ gridWrap = section?.gridWrap ?: page?.gridWrap,
37
+ gridTile = section?.gridTile ?: page?.gridTile,
38
+ )
39
+
40
+ /**
41
+ * Resolves a track's effective item properties against its (page-folded) section block.
42
+ *
43
+ * No Android renderer consumes the result yet — today's only inherited property
44
+ * (`artworkRendering`) is CarPlay-rendered — but the resolver ships on both platforms so the
45
+ * completeness test can hold every future inherited key to the same rule from day one.
46
+ */
47
+ fun trackStyle(track: TrackStyle?, section: SectionStyle?): TrackStyle =
48
+ TrackStyle(
49
+ // Positional deny-list: `display` is never inherited onto an item — resolved null, so no
50
+ // renderer can mistake the handle's page promise for this item's own layout.
51
+ display = null,
52
+ artworkRendering = track?.artworkRendering ?: section?.artworkRendering,
53
+ imageShape = track?.imageShape ?: section?.imageShape,
54
+ // 'none' resolves like any value — a renderer treats it as "no accessory, derived
55
+ // behavior" (the inheritance escape). No Android surface draws these properties today.
56
+ accessorySymbol = track?.accessorySymbol ?: section?.accessorySymbol,
57
+ cardTint = track?.cardTint ?: section?.cardTint,
58
+ cardImage = track?.cardImage ?: section?.cardImage,
59
+ )
60
+ }
@@ -2,6 +2,7 @@ package com.audiobrowser.extension
2
2
 
3
3
  import com.margelo.nitro.audiobrowser.ResolvedTrack
4
4
  import com.margelo.nitro.audiobrowser.Track
5
+ import com.margelo.nitro.audiobrowser.TrackStyle
5
6
 
6
7
  /**
7
8
  * The Track view of a ResolvedTrack: every display/playback field carried over, the browse-only
@@ -12,24 +13,35 @@ import com.margelo.nitro.audiobrowser.Track
12
13
  fun ResolvedTrack.toTrack(): Track =
13
14
  Track(
14
15
  id = id,
15
- url = url,
16
+ path = path,
16
17
  src = src,
17
18
  artwork = artwork,
18
19
  artworkSource = artworkSource,
19
20
  request = request,
20
- artworkCarPlayTinted = artworkCarPlayTinted,
21
21
  title = title,
22
22
  subtitle = subtitle,
23
23
  artist = artist,
24
- albumUrl = albumUrl,
24
+ albumPath = albumPath,
25
25
  album = album,
26
26
  description = description,
27
27
  genre = genre,
28
28
  duration = duration,
29
- style = style,
30
- childrenStyle = childrenStyle,
29
+ // An explicit SectionStyle → TrackStyle projection: the page's container
30
+ // properties (gridWrap) have no meaning on a plain Track, so they drop —
31
+ // Nitro flattens the spec's `extends`, making this narrowing a hand-written
32
+ // field list rather than an upcast (ADR 0011).
33
+ style =
34
+ style?.let {
35
+ TrackStyle(
36
+ display = it.display,
37
+ artworkRendering = it.artworkRendering,
38
+ imageShape = it.imageShape,
39
+ accessorySymbol = it.accessorySymbol,
40
+ cardTint = it.cardTint,
41
+ cardImage = it.cardImage,
42
+ )
43
+ },
44
+ disabled = disabled,
31
45
  favorited = favorited,
32
- groupTitle = groupTitle,
33
46
  live = live,
34
- imageRow = imageRow,
35
47
  )
@@ -0,0 +1,20 @@
1
+ package com.audiobrowser.extension
2
+
3
+ import com.audiobrowser.util.BrowserPathHelper
4
+ import com.margelo.nitro.audiobrowser.Track
5
+
6
+ /**
7
+ * The queue position of the tapped surface, for skip-in-place. Exact-surface match first: a
8
+ * contextual path carries the tapped page position (`__index`), so path equality pins the exact
9
+ * copy when the page holds the same identity more than once. The identity match remains for
10
+ * index-less paths (e.g. pre-stamp persisted state); an index-stamped path with no exact match
11
+ * returns -1, and the caller falls through to expansion, which re-scopes the queue to the tapped
12
+ * section (ADR 0009).
13
+ */
14
+ fun Array<Track>.indexOfTappedTrack(contextualPath: String, trackId: String?): Int {
15
+ val index = indexOfFirst { it.path == contextualPath }
16
+ if (index >= 0 || BrowserPathHelper.extractIndex(contextualPath) != null || trackId == null) {
17
+ return index
18
+ }
19
+ return indexOfFirst { it.identity == trackId }
20
+ }
@@ -0,0 +1,15 @@
1
+ package com.audiobrowser.extension
2
+
3
+ import com.margelo.nitro.audiobrowser.Track
4
+
5
+ /**
6
+ * A track's identity: the opaque `id` when present (non-blank), else the playable `src`. Two tracks
7
+ * refer to the same item iff their identities are equal. Browsable-only tracks (neither `id` nor
8
+ * `src`) have no identity — they are addressed by `path` instead.
9
+ *
10
+ * This is THE comparison rule for favorites matching, section scoping, skip-in-place, the car
11
+ * now-playing row indicator, and the contextual `__trackId` — see ADR 0008. Mirrors the TS
12
+ * `trackIdentity` helper.
13
+ */
14
+ val Track.identity: String?
15
+ get() = id?.takeUnless { it.isBlank() } ?: src
@@ -20,7 +20,7 @@ data class PlayerUpdateOptions(
20
20
  var progressUpdateEventInterval: Double? = null,
21
21
 
22
22
  // Player capabilities - most enabled by default, only false values disable
23
- // Exceptions: bookmark, jumpForward, jumpBackward default to false
23
+ // Exceptions: jumpForward, jumpBackward default to false
24
24
  var capabilities: PlayerCapabilities =
25
25
  PlayerCapabilities(
26
26
  play = null,
@@ -12,6 +12,10 @@ import androidx.media3.session.SessionCommand
12
12
  import androidx.media3.session.SessionError
13
13
  import androidx.media3.session.SessionResult
14
14
  import com.audiobrowser.browser.handleTrackLoad
15
+ import com.audiobrowser.browser.normalizedSections
16
+ import com.audiobrowser.browser.styleResolvedSections
17
+ import com.audiobrowser.browser.untitledSection
18
+ import com.audiobrowser.extension.indexOfTappedTrack
15
19
  import com.audiobrowser.extension.toTrack
16
20
  import com.audiobrowser.util.BrowserPathHelper
17
21
  import com.audiobrowser.util.RatingFavorites
@@ -27,6 +31,8 @@ import com.margelo.nitro.audiobrowser.NativeGateRequest
27
31
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
28
32
  import com.margelo.nitro.audiobrowser.RemoteButtonLayout
29
33
  import com.margelo.nitro.audiobrowser.SearchParams
34
+ import com.margelo.nitro.audiobrowser.Section
35
+ import com.margelo.nitro.audiobrowser.StyleDisplay
30
36
  import com.margelo.nitro.audiobrowser.Track
31
37
  import kotlinx.coroutines.CancellationException
32
38
  import kotlinx.coroutines.CoroutineScope
@@ -191,12 +197,17 @@ class MediaSessionCallback(private val player: Player) :
191
197
  command: SessionCommand,
192
198
  args: Bundle,
193
199
  ): ListenableFuture<SessionResult> {
194
- // Handle favorite button tap
200
+ // Handle favorite button tap. Report an honest result: a success here makes
201
+ // the controller flip its heart optimistically, so a no-op (no current
202
+ // track) must say INVALID_STATE instead of success-then-revert.
195
203
  if (command.customAction == MediaSessionCommandManager.CUSTOM_ACTION_FAVORITE) {
196
- val currentFavorited = player.currentTrack?.favorited ?: false
197
- Timber.d("Favorite button tapped - toggling from $currentFavorited to ${!currentFavorited}")
198
- player.setActiveTrackFavorited(!currentFavorited)
199
- return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
204
+ val applied = player.toggleActiveTrackFavorited()
205
+ Timber.d("Favorite button tapped - toggle applied=$applied")
206
+ return Futures.immediateFuture(
207
+ SessionResult(
208
+ if (applied) SessionResult.RESULT_SUCCESS else SessionResult.RESULT_ERROR_INVALID_STATE
209
+ )
210
+ )
200
211
  }
201
212
 
202
213
  if (commandManager.handleCustomCommand(command, player)) {
@@ -212,8 +223,16 @@ class MediaSessionCallback(private val player: Player) :
212
223
  ): ListenableFuture<SessionResult> {
213
224
  // A heart rating from a controller (e.g. Google Assistant "I like this") toggles the
214
225
  // now-playing favorite. setActiveTrackFavorited fires onFavoriteChanged so the consumer
215
- // persists it — the same path as the notification / CarPlay heart button.
216
- RatingFavorites.favoritedFor(rating)?.let { player.setActiveTrackFavorited(it) }
226
+ // persists it — the same path as the notification / CarPlay heart button. Report an honest
227
+ // result (see onCustomCommand): INVALID_STATE when there is no current track to favorite.
228
+ RatingFavorites.favoritedFor(rating)?.let { favorited ->
229
+ val applied = player.setActiveTrackFavorited(favorited)
230
+ return Futures.immediateFuture(
231
+ SessionResult(
232
+ if (applied) SessionResult.RESULT_SUCCESS else SessionResult.RESULT_ERROR_INVALID_STATE
233
+ )
234
+ )
235
+ }
217
236
  return super.onSetRating(session, controller, rating)
218
237
  }
219
238
 
@@ -238,13 +257,11 @@ class MediaSessionCallback(private val player: Player) :
238
257
  }
239
258
 
240
259
  if (params?.isRecent == true) {
241
- // TODO: support recent queries through something like onRecent and return a MediaItem with
242
- // .setMediaId("__RECENT__") here - when onRecent is not configured we can keep returning an
243
- // error here:
244
-
245
- // The service currently does not support playback resumption. Tell System UI by returning
246
- // an error of type 'RESULT_ERROR_NOT_SUPPORTED' for a `params.isRecent` request. See
247
- // https://github.com/androidx/media/issues/355
260
+ // Playback resumption is served by media3 itself: it intercepts System UI's EXTRA_RECENT
261
+ // root request internally and answers through onPlaybackResumption, so that request never
262
+ // reaches this callback. Any other browser asking for a recent root gets
263
+ // ERROR_NOT_SUPPORTED — there is no browsable recents tree; recents are ordinary consumer
264
+ // content (a tab or route).
248
265
  return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED))
249
266
  }
250
267
 
@@ -271,7 +288,7 @@ class MediaSessionCallback(private val player: Player) :
271
288
  params: MediaLibraryService.LibraryParams?,
272
289
  ): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
273
290
  Timber.d(
274
- "onGetChildren: {parentId: $parentId, page: $page, pageSize: $pageSize, isSearchPath: ${BrowserPathHelper.isSpecialPath(parentId)} }"
291
+ "onGetChildren: {parentId: $parentId, page: $page, pageSize: $pageSize, isSpecialPath: ${BrowserPathHelper.isSpecialPath(parentId)} }"
275
292
  )
276
293
  return scope.future {
277
294
  // Wait for browser to be registered if it's not available yet
@@ -325,30 +342,28 @@ class MediaSessionCallback(private val player: Player) :
325
342
 
326
343
  try {
327
344
  val children =
328
- if (parentId == BrowserPathHelper.RECENT_PATH) {
329
- // TODO: implement recent media items
330
- emptyList<MediaItem>()
331
- } else if (parentId == BrowserPathHelper.ROOT_PATH) {
345
+ if (parentId == BrowserPathHelper.ROOT_PATH) {
332
346
  // Return tabs as root children (limited to 4 for automotive platform compatibility)
333
347
  // TODO: Check what Android Auto does with empty tabs list - may need to return error?
334
- val tabs = browserManager.queryTabs()
348
+ // Disabled tabs hide (Track.disabled) — filtered before the cap so
349
+ // an unavailable tab never costs an available one its slot.
350
+ val tabs = browserManager.queryTabs().filterNot { it.disabled == true }
335
351
  if (tabs.size > 4) {
336
352
  Timber.w(
337
353
  "Root has ${tabs.size} tabs; dropping ${tabs.size - 4} (Android Auto root limit)"
338
354
  )
339
355
  }
340
- toMediaItems(tabs.take(4))
356
+ toFlatMediaItems(tabs.take(4))
341
357
  } else {
342
- // Resolve the specific path and get its children
358
+ // Resolve the specific path and flatten its sections to MediaItems
359
+ // (paths are already set to contextual paths). Section styles come
360
+ // page-folded (`section ?? page`, ADR 0011).
343
361
  val resolvedTrack = browserManager.resolve(parentId)
344
-
345
- // Convert children to MediaItems (url is already set to contextual URL)
346
- val trackChildren =
347
- resolvedTrack.children
348
- ?: throw IllegalStateException(
349
- "Expected browsed ResolvedTrack to have a children array"
350
- )
351
- toMediaItems(trackChildren.toList())
362
+ val sections =
363
+ resolvedTrack.styleResolvedSections()
364
+ ?: throw IllegalStateException("Expected browsed ResolvedTrack to have sections")
365
+ browserManager.warnIfGridPageLacksPromise(parentId, sections)
366
+ toMediaItems(sections)
352
367
  }
353
368
 
354
369
  LibraryResult.ofItemList(ImmutableList.copyOf(children.paginate(page, pageSize)), params)
@@ -402,19 +417,45 @@ class MediaSessionCallback(private val player: Player) :
402
417
  }
403
418
 
404
419
  /**
405
- * Converts tracks to MediaItems for browse delivery, routing http(s) artwork through the
406
- * content:// provider so Android Auto can load it via the ArtworkContentProvider. Image-row
407
- * tracks (a CarPlay-only rendering) are expanded into their items as regular grouped rows first —
408
- * see [TrackFactory.expandImageRows].
420
+ * Flattens a page's sections to MediaItems for browse delivery — the Media3 boundary where
421
+ * sections die (ADR 0010): the MediaBrowser protocol has no section node, so each child is
422
+ * stamped with its owning section's title/style hints. A grid-displayed section with a `path`
423
+ * gains a browsable "view all" link under the same header. Http(s) artwork routes through the
424
+ * content:// provider so Android Auto can load it via the ArtworkContentProvider.
409
425
  */
410
- private fun toMediaItems(tracks: List<Track>): List<MediaItem> {
426
+ private fun toMediaItems(sections: List<Section>): List<MediaItem> {
411
427
  val registry = player.browseArtworkRegistry
412
428
  val authority = com.audiobrowser.util.ArtworkUris.authorityFor(player.context.packageName)
413
- return TrackFactory.expandImageRows(tracks).map {
414
- TrackFactory.toBrowseMediaItem(it, registry, authority)
429
+ return sections.flatMap { section ->
430
+ // Android Auto has no disabled affordance, so an unavailable track hides
431
+ // — never a normal-looking dead row (Track.disabled's rendering ladder).
432
+ val visibleChildren = section.children.filter { it.disabled != true }
433
+ val items =
434
+ visibleChildren.map { TrackFactory.toBrowseMediaItem(it, registry, authority, section) }
435
+ val isTileSection = section.style?.display == StyleDisplay.GRID
436
+ // No "view all" under an empty section — CarPlay skips empty sections
437
+ // outright, and a lone navigation tile under a header is a dead end.
438
+ if (isTileSection && section.path != null && visibleChildren.isNotEmpty()) {
439
+ items +
440
+ TrackFactory.toBrowseMediaItem(
441
+ TrackFactory.navigationTrack(section),
442
+ registry,
443
+ authority,
444
+ section,
445
+ )
446
+ } else {
447
+ items
448
+ }
415
449
  }
416
450
  }
417
451
 
452
+ /**
453
+ * Flat lists (tabs, search results) convert as one untitled list section — which contributes no
454
+ * group or style hints — so exactly one Track→MediaItem path exists at the Media3 boundary.
455
+ */
456
+ private fun toFlatMediaItems(tracks: List<Track>): List<MediaItem> =
457
+ toMediaItems(listOf(untitledSection(tracks.toTypedArray())))
458
+
418
459
  override fun onGetItem(
419
460
  session: MediaLibraryService.MediaLibrarySession,
420
461
  browser: MediaSession.ControllerInfo,
@@ -451,7 +492,7 @@ class MediaSessionCallback(private val player: Player) :
451
492
  return@future LibraryResult.ofItem(createGateMediaItem(outcome.chrome!!), null)
452
493
  }
453
494
 
454
- if (mediaId == BrowserPathHelper.ROOT_PATH || mediaId == BrowserPathHelper.RECENT_PATH) {
495
+ if (mediaId == BrowserPathHelper.ROOT_PATH) {
455
496
  return@future LibraryResult.ofItem(
456
497
  MediaItem.Builder()
457
498
  .setMediaId(mediaId)
@@ -466,7 +507,7 @@ class MediaSessionCallback(private val player: Player) :
466
507
  // Wait for browser to be registered if it's not available yet
467
508
  val browserManager = player.awaitBrowser().browserManager
468
509
 
469
- // Serve tracks from the track cache first (keyed by url and src). Besides avoiding an HTTP
510
+ // Serve tracks from the track cache first (keyed by path and src). Besides avoiding an HTTP
470
511
  // resolve, this keeps item identity correct for contextual mediaIds: resolve() strips
471
512
  // __trackId and would return the *parent container's* metadata as the item.
472
513
  val browseAuthority =
@@ -628,7 +669,12 @@ class MediaSessionCallback(private val player: Player) :
628
669
  try {
629
670
  // Execute search (automatically caches results at /__search?q=query)
630
671
  val searchResults = browserManager.search(query)
631
- val resultCount = searchResults.children?.size ?: 0
672
+ // Count what onGetSearchResult will actually serve: disabled tracks
673
+ // hide on Android Auto, so they must not inflate the announced count.
674
+ val resultCount =
675
+ searchResults.normalizedSections?.sumOf { section ->
676
+ section.children.count { it.disabled != true }
677
+ } ?: 0
632
678
 
633
679
  Timber.d("Search completed: $resultCount results for query '$query'")
634
680
 
@@ -681,14 +727,9 @@ class MediaSessionCallback(private val player: Player) :
681
727
  try {
682
728
  // Get cached search results from BrowserManager
683
729
  browserManager.getCachedSearchResults(query)?.let { tracks ->
684
- // Convert to MediaItems
685
- val searchAuthority =
686
- com.audiobrowser.util.ArtworkUris.authorityFor(player.context.packageName)
687
- val mediaItems =
688
- tracks.map { track ->
689
- Timber.d("Search result: ${track.title} (url=${track.url}, src=${track.src})")
690
- TrackFactory.toBrowseMediaItem(track, player.browseArtworkRegistry, searchAuthority)
691
- }
730
+ // Through the shared browse conversion, so search results get the
731
+ // same artwork routing as browse rows.
732
+ val mediaItems = toFlatMediaItems(tracks.toList())
692
733
 
693
734
  val paginatedItems = mediaItems.paginate(page, pageSize)
694
735
  Timber.d("Returning ${paginatedItems.size} search results")
@@ -737,26 +778,28 @@ class MediaSessionCallback(private val player: Player) :
737
778
 
738
779
  val browserManager = audioBrowser.browserManager
739
780
 
740
- // A single tapped item resolves to the contextual url of the list it was
781
+ // A single tapped item resolves to the contextual path of the list it was
741
782
  // tapped in: directly for contextual mediaIds, via the track cache for
742
- // stable-id mediaIds (see BrowserManager.contextualUrlFor). A search
783
+ // stable-id mediaIds (see BrowserManager.contextualPathFor). A search
743
784
  // selection is not a list tap — its queue comes from the search results
744
785
  // (resolveMediaItemsForPlayback), never from a browsed container.
745
- val singleContextualUrl =
786
+ val singleContextualPath =
746
787
  mediaItems
747
788
  .singleOrNull()
748
789
  ?.takeIf { it.requestMetadata.searchQuery == null }
749
- ?.let { browserManager.contextualUrlFor(it.mediaId) }
790
+ ?.let { browserManager.contextualPathFor(it.mediaId) }
750
791
 
751
792
  // Check if this is a single item from the current queue source
752
- if (singleContextualUrl != null) {
753
- val parentPath = BrowserPathHelper.stripTrackId(singleContextualUrl)
754
- val trackId = BrowserPathHelper.extractTrackId(singleContextualUrl)
793
+ if (singleContextualPath != null) {
794
+ val parentPath = BrowserPathHelper.stripTrackId(singleContextualPath)
795
+ val trackId = BrowserPathHelper.extractTrackId(singleContextualPath)
755
796
 
756
- // Check if queue already came from this parent path - just skip to the track
797
+ // Check if queue already came from this parent path - just skip to the
798
+ // tapped surface (exact path first, identity for index-less paths —
799
+ // see indexOfTappedTrack)
757
800
  if (trackId != null && parentPath == player.queueSourcePath) {
758
801
  val queueTracks = withContext(Dispatchers.Main) { player.tracks }
759
- val index = queueTracks.indexOfFirst { it.src == trackId }
802
+ val index = queueTracks.indexOfTappedTrack(singleContextualPath, trackId)
760
803
  if (index >= 0) {
761
804
  Timber.d("Queue already from $parentPath, skipping to index $index")
762
805
  val track = queueTracks[index]
@@ -788,9 +831,10 @@ class MediaSessionCallback(private val player: Player) :
788
831
  result.startIndex.toDouble(),
789
832
  ::currentPlayerState,
790
833
  ) {
791
- // If this was a contextual URL expansion, track the source path (only for default behavior)
792
- if (singleContextualUrl != null) {
793
- val parentPath = BrowserPathHelper.stripTrackId(singleContextualUrl)
834
+ // If this was a contextual path expansion, track the source path (only for default
835
+ // behavior)
836
+ if (singleContextualPath != null) {
837
+ val parentPath = BrowserPathHelper.stripTrackId(singleContextualPath)
794
838
  withContext(Dispatchers.Main) { player.queueSourcePath = parentPath }
795
839
  }
796
840
  result
@@ -829,6 +873,15 @@ class MediaSessionCallback(private val player: Player) :
829
873
  throw IllegalStateException("No playback state to resume")
830
874
  }
831
875
 
876
+ // The persisted track can carry `disabled` — it became unavailable after
877
+ // it was saved. A disabled track never plays, resumption included
878
+ // (Track.disabled); failing the future refuses without touching the
879
+ // player.
880
+ if (state.track.disabled == true) {
881
+ Timber.w("Refusing resumption of disabled track: ${state.track.title}")
882
+ throw IllegalStateException("Persisted track is disabled")
883
+ }
884
+
832
885
  // isForPlayback == false is the device-boot-time notification case: network may be
833
886
  // unavailable and we must not start playback. Return just the locally-stored track with its
834
887
  // already-local metadata; skip the settings restore and network queue expansion below.
@@ -844,14 +897,14 @@ class MediaSessionCallback(private val player: Player) :
844
897
  // restore() sets player properties which must happen on main thread
845
898
  withContext(Dispatchers.Main) { player.playbackStateStore.restore() }
846
899
 
847
- val url = state.track.url
848
- Timber.d("Resuming from url=$url, positionMs=${state.positionMs}")
900
+ val path = state.track.path
901
+ Timber.d("Resuming from path=$path, positionMs=${state.positionMs}")
849
902
 
850
903
  // Wait for browser to be available (JS needs to have configured it)
851
904
  val browserManager = player.awaitBrowser().browserManager
852
905
 
853
- // Try to expand the URL into a full queue
854
- val expanded = url?.let { browserManager.expandQueueFromContextualUrl(it) }
906
+ // Try to expand the path into a full queue
907
+ val expanded = path?.let { browserManager.expandQueueFromContextualPath(it) }
855
908
 
856
909
  if (expanded != null) {
857
910
  val (tracks, selectedIndex) = expanded
@@ -859,9 +912,9 @@ class MediaSessionCallback(private val player: Player) :
859
912
  "Restored ${tracks.size} tracks, starting at index $selectedIndex at ${state.positionMs}ms"
860
913
  )
861
914
 
862
- // Track the source path if this was a contextual URL expansion
863
- if (BrowserPathHelper.isContextual(url)) {
864
- val parentPath = BrowserPathHelper.stripTrackId(url)
915
+ // Track the source path if this was a contextual path expansion
916
+ if (BrowserPathHelper.isContextual(path)) {
917
+ val parentPath = BrowserPathHelper.stripTrackId(path)
865
918
  withContext(Dispatchers.Main) { player.queueSourcePath = parentPath }
866
919
  }
867
920