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

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 (284) hide show
  1. package/README.md +22 -18
  2. package/android/build.gradle +7 -5
  3. package/android/gradle.properties +1 -1
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +76 -21
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +1 -2
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +1 -2
  11. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +6 -7
  12. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  13. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +11 -11
  14. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  15. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +9 -5
  16. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  17. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +0 -1
  18. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -4
  19. package/android/src/main/java/com/audiobrowser/player/Player.kt +30 -8
  20. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  21. package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +2 -1
  22. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +21 -6
  23. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +109 -0
  24. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +16 -2
  25. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +59 -3
  26. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  27. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -7
  28. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
  29. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +0 -1
  30. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +2 -1
  31. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +2 -4
  32. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +100 -56
  33. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  34. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  35. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  36. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +46 -0
  37. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +113 -0
  38. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  39. package/ios/Browser/BrowserManager.swift +2 -27
  40. package/ios/Browser/SearchDrillIn.swift +1 -1
  41. package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
  42. package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
  43. package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
  44. package/ios/HybridAudioBrowser.swift +87 -56
  45. package/ios/Model/RemoteCommand.swift +18 -0
  46. package/ios/Model/TrackPlayerError.swift +2 -2
  47. package/ios/Option/SessionCategories.swift +3 -2
  48. package/ios/Player/MediaIntentCriteria.swift +4 -4
  49. package/ios/Player/MediaLoader.swift +3 -1
  50. package/ios/Player/PlaybackCoordinator.swift +4 -1
  51. package/ios/Player/PlaybackErrorHandler.swift +4 -4
  52. package/ios/Player/QueueManager.swift +10 -3
  53. package/ios/Player/RetryManager.swift +79 -18
  54. package/ios/Player/ShuffleOrder.swift +10 -0
  55. package/ios/Player/SleepTimerManager.swift +43 -15
  56. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  57. package/ios/RemoteCommand/RemoteCommandController.swift +9 -6
  58. package/ios/Tests/LRUCacheTests.swift +111 -0
  59. package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
  60. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  61. package/ios/Tests/MediaLoaderTests.swift +10 -13
  62. package/ios/Tests/PlaybackCoordinatorTests.swift +10 -7
  63. package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
  64. package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
  65. package/ios/Tests/QueueManagerTests.swift +46 -0
  66. package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
  67. package/ios/Tests/RemoteCommandTests.swift +67 -0
  68. package/ios/Tests/RetryManagerTests.swift +89 -0
  69. package/ios/Tests/SectionScopeTests.swift +2 -2
  70. package/ios/Tests/SleepTimerManagerTests.swift +22 -19
  71. package/ios/Tests/Support/ManualSleepTimerScheduler.swift +47 -0
  72. package/ios/Tests/Support/TestTypes.swift +1 -1
  73. package/ios/Tests/TabBarEntriesTests.swift +7 -7
  74. package/ios/TrackPlayer.swift +6 -1
  75. package/ios/TrackPlayerCallbacks.swift +5 -38
  76. package/ios/Util/LRUCache.swift +4 -1
  77. package/lib/commonjs/AudioBrowser.js +0 -11
  78. package/lib/commonjs/AudioBrowser.js.map +1 -1
  79. package/lib/commonjs/CLAUDE.md +52 -0
  80. package/lib/commonjs/features/browser.js +1 -1
  81. package/lib/commonjs/features/browser.js.map +1 -1
  82. package/lib/commonjs/features/carConnection.js +1 -1
  83. package/lib/commonjs/features/equalizer.js +1 -1
  84. package/lib/commonjs/features/errors.js +3 -3
  85. package/lib/commonjs/features/favorites.js +1 -1
  86. package/lib/commonjs/features/gate.js +16 -1
  87. package/lib/commonjs/features/gate.js.map +1 -1
  88. package/lib/commonjs/features/metadata.js +20 -3
  89. package/lib/commonjs/features/metadata.js.map +1 -1
  90. package/lib/commonjs/features/network.js +1 -1
  91. package/lib/commonjs/features/nowPlaying.js +1 -1
  92. package/lib/commonjs/features/output.js +1 -1
  93. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  94. package/lib/commonjs/features/playback/playing.js +1 -1
  95. package/lib/commonjs/features/playback/progress.js +1 -1
  96. package/lib/commonjs/features/playback/state.js +1 -1
  97. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  98. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  99. package/lib/commonjs/features/playback/volume.js +1 -1
  100. package/lib/commonjs/features/player/options.js +48 -34
  101. package/lib/commonjs/features/player/options.js.map +1 -1
  102. package/lib/commonjs/features/player/setup.js +15 -8
  103. package/lib/commonjs/features/player/setup.js.map +1 -1
  104. package/lib/commonjs/features/player/validateOptions.js +21 -0
  105. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  106. package/lib/commonjs/features/queue/activeTrack.js +1 -1
  107. package/lib/commonjs/features/queue/queue.js +2 -2
  108. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  109. package/lib/commonjs/features/queue/shuffle.js +1 -1
  110. package/lib/commonjs/features/remoteControls.js +13 -92
  111. package/lib/commonjs/features/remoteControls.js.map +1 -1
  112. package/lib/commonjs/features/sleepTimer.js +13 -1
  113. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  114. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  115. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  116. package/lib/commonjs/utils/LazyNativeEmitter.js +8 -5
  117. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  118. package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
  119. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  120. package/lib/commonjs/web/browser/BrowserManager.js +1 -1
  121. package/lib/module/AudioBrowser.js +1 -1
  122. package/lib/module/AudioBrowser.js.map +1 -1
  123. package/lib/module/CLAUDE.md +52 -0
  124. package/lib/module/features/browser.js +1 -1
  125. package/lib/module/features/browser.js.map +1 -1
  126. package/lib/module/features/carConnection.js +1 -1
  127. package/lib/module/features/equalizer.js +1 -1
  128. package/lib/module/features/errors.js +3 -3
  129. package/lib/module/features/favorites.js +1 -1
  130. package/lib/module/features/gate.js +16 -1
  131. package/lib/module/features/gate.js.map +1 -1
  132. package/lib/module/features/metadata.js +20 -3
  133. package/lib/module/features/metadata.js.map +1 -1
  134. package/lib/module/features/network.js +1 -1
  135. package/lib/module/features/nowPlaying.js +1 -1
  136. package/lib/module/features/output.js +1 -1
  137. package/lib/module/features/playback/playWhenReady.js +1 -1
  138. package/lib/module/features/playback/playing.js +1 -1
  139. package/lib/module/features/playback/progress.js +1 -1
  140. package/lib/module/features/playback/state.js +1 -1
  141. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  142. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  143. package/lib/module/features/playback/volume.js +1 -1
  144. package/lib/module/features/player/options.js +47 -32
  145. package/lib/module/features/player/options.js.map +1 -1
  146. package/lib/module/features/player/setup.js +14 -7
  147. package/lib/module/features/player/setup.js.map +1 -1
  148. package/lib/module/features/player/validateOptions.js +17 -0
  149. package/lib/module/features/player/validateOptions.js.map +1 -0
  150. package/lib/module/features/queue/activeTrack.js +1 -1
  151. package/lib/module/features/queue/queue.js +2 -2
  152. package/lib/module/features/queue/repeatMode.js +1 -1
  153. package/lib/module/features/queue/shuffle.js +1 -1
  154. package/lib/module/features/remoteControls.js +13 -92
  155. package/lib/module/features/remoteControls.js.map +1 -1
  156. package/lib/module/features/sleepTimer.js +13 -1
  157. package/lib/module/features/sleepTimer.js.map +1 -1
  158. package/lib/module/test-utils/reactNativeStub.js +58 -0
  159. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  160. package/lib/module/utils/LazyNativeEmitter.js +8 -5
  161. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  162. package/lib/module/web/NativeAudioBrowser.js +1 -1
  163. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  164. package/lib/module/web/browser/BrowserManager.js +1 -1
  165. package/lib/typescript/src/AudioBrowser.d.ts +0 -1
  166. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  167. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  168. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  169. package/lib/typescript/src/features/errors.d.ts +3 -3
  170. package/lib/typescript/src/features/favorites.d.ts +1 -1
  171. package/lib/typescript/src/features/gate.d.ts +18 -2
  172. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  173. package/lib/typescript/src/features/metadata.d.ts +20 -3
  174. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  175. package/lib/typescript/src/features/network.d.ts +1 -1
  176. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  177. package/lib/typescript/src/features/output.d.ts +1 -1
  178. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  179. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  180. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  181. package/lib/typescript/src/features/playback/state.d.ts +1 -1
  182. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  183. package/lib/typescript/src/features/player/options.d.ts +87 -37
  184. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  185. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  186. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  187. package/lib/typescript/src/features/player/validateOptions.d.ts +9 -0
  188. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  189. package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
  190. package/lib/typescript/src/features/queue/queue.d.ts +2 -2
  191. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  192. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  193. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  194. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  195. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  196. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  197. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  198. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  199. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -4
  200. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  201. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -1
  202. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
  203. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +5 -5
  204. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +11 -11
  205. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
  206. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +5 -5
  207. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +5 -5
  208. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
  209. package/nitrogen/generated/android/c++/JOptions.hpp +5 -5
  210. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  211. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  212. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  213. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  214. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
  215. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
  216. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
  217. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  218. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
  219. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
  220. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +36 -51
  221. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +6 -6
  222. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +8 -8
  223. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  224. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  225. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  226. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  227. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  228. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  229. package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
  230. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  231. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  232. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  233. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  234. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  235. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  236. package/package.json +10 -3
  237. package/src/AudioBrowser.ts +1 -1
  238. package/src/CLAUDE.md +52 -0
  239. package/src/features/battery.test.ts +79 -0
  240. package/src/features/browser.ts +1 -1
  241. package/src/features/carConnection.ts +1 -1
  242. package/src/features/equalizer.ts +1 -1
  243. package/src/features/errors.ts +3 -3
  244. package/src/features/favorites.ts +1 -1
  245. package/src/features/gate.ts +16 -1
  246. package/src/features/hook-wiring.test.ts +254 -0
  247. package/src/features/metadata.ts +20 -3
  248. package/src/features/network.ts +1 -1
  249. package/src/features/nowPlaying.ts +1 -1
  250. package/src/features/output.ts +1 -1
  251. package/src/features/playback/playWhenReady.ts +1 -1
  252. package/src/features/playback/playing.ts +1 -1
  253. package/src/features/playback/progress.test.ts +169 -0
  254. package/src/features/playback/progress.ts +1 -1
  255. package/src/features/playback/state.ts +1 -1
  256. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  257. package/src/features/playback/trackPlaybackTime.ts +0 -5
  258. package/src/features/playback/volume.ts +1 -1
  259. package/src/features/player/options.ts +99 -57
  260. package/src/features/player/setup.test.ts +12 -4
  261. package/src/features/player/setup.ts +19 -9
  262. package/src/features/player/validateOptions.ts +19 -0
  263. package/src/features/queue/activeTrack.ts +1 -1
  264. package/src/features/queue/queue.ts +2 -2
  265. package/src/features/queue/repeatMode.ts +1 -1
  266. package/src/features/queue/shuffle.ts +1 -1
  267. package/src/features/remoteControls.ts +13 -92
  268. package/src/features/sleepTimer.test.ts +172 -0
  269. package/src/features/sleepTimer.ts +9 -1
  270. package/src/public-surface.test.ts +107 -0
  271. package/src/specs/audio-browser.nitro.ts +1 -1
  272. package/src/test-utils/reactNativeStub.ts +60 -0
  273. package/src/utils/LazyNativeEmitter.ts +9 -6
  274. package/src/utils/useDebug.test.ts +182 -0
  275. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  276. package/src/web/NativeAudioBrowser.ts +1 -1
  277. package/src/web/browser/BrowserManager.ts +1 -1
  278. package/ios/Model/MediaURL.swift +0 -67
  279. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  280. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  281. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  282. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  283. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  284. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
package/README.md CHANGED
@@ -66,8 +66,8 @@ AudioBrowser.configureBrowser({
66
66
  tabs: [{ title: 'Browse', url: '/browse' }],
67
67
  routes: {
68
68
  // Every path under /browse is fetched from your API, on demand.
69
- '/browse/**': { baseUrl: 'https://api.example.com' },
70
- },
69
+ '/browse/**': { baseUrl: 'https://api.example.com' }
70
+ }
71
71
  })
72
72
  ```
73
73
 
@@ -79,7 +79,7 @@ Each route returns a page of children — a child with a `url` is browsable (ope
79
79
  "url": "/browse/jazz",
80
80
  "title": "Jazz",
81
81
  "children": [
82
- { "title": "Smooth Floret FM", "url": "/browse/jazz/floret-fm" }, // url → open for children
82
+ { "title": "Smooth Floret FM", "url": "/browse/jazz/floret-fm" }, // url → open for children
83
83
  { "title": "The Stalk 88.5", "src": "https://stream.example.com/stalk.mp3" } // src → play this stream
84
84
  ]
85
85
  }
@@ -91,7 +91,9 @@ The player works standalone, too — no browse tree required:
91
91
  import AudioBrowser from 'react-native-audio-browser'
92
92
 
93
93
  await AudioBrowser.setupPlayer()
94
- AudioBrowser.setQueue([{ title: 'Track', artist: 'Artist', src: 'https://example.com/track.mp3' }])
94
+ AudioBrowser.setQueue([
95
+ { title: 'Track', artist: 'Artist', src: 'https://example.com/track.mp3' }
96
+ ])
95
97
  AudioBrowser.play()
96
98
  ```
97
99
 
@@ -99,16 +101,16 @@ AudioBrowser.play()
99
101
 
100
102
  Reactive hooks for your in-app UI — they update automatically as playback, the queue, and browse state change. Call `setupPlayer()` once at startup, before rendering any component that uses them.
101
103
 
102
- | Hook | Returns |
103
- | --- | --- |
104
- | `usePlayingState()` | `{ playing, buffering }` |
105
- | `useProgress()` | `{ position, duration, buffered }` (seconds) |
106
- | `useActiveTrack()` | the current `Track` |
107
- | `useNowPlaying()` | now-playing metadata |
108
- | `useQueue()` | the current queue |
109
- | `useRepeatMode()` / `useShuffle()` | repeat / shuffle state |
110
- | `useSleepTimer()` | sleep-timer state |
111
- | `useCarConnected()` | whether a car is connected |
104
+ | Hook | Returns |
105
+ | ------------------------------------------ | -------------------------------------------------------------------- |
106
+ | `usePlayingState()` | `{ playing, buffering }` |
107
+ | `useProgress()` | `{ position, duration, buffered }` (seconds) |
108
+ | `useActiveTrack()` | the current `Track` |
109
+ | `useNowPlaying()` | now-playing metadata |
110
+ | `useQueue()` | the current queue |
111
+ | `useRepeatMode()` / `useShuffle()` | repeat / shuffle state |
112
+ | `useSleepTimer()` | sleep-timer state |
113
+ | `useCarConnected()` | whether a car is connected |
112
114
  | `usePath()` / `useTabs()` / `useContent()` | current browse path, tabs, and the resolved page at the current path |
113
115
 
114
116
  …and more — see the [Hooks guide](https://audiobrowser.dev/guide/hooks) and the [API Reference](https://audiobrowser.dev/api/).
@@ -118,18 +120,20 @@ import { View, Text, Button } from 'react-native'
118
120
  import AudioBrowser, {
119
121
  usePlayingState,
120
122
  useProgress,
121
- useActiveTrack,
123
+ useActiveTrack
122
124
  } from 'react-native-audio-browser'
123
125
 
124
126
  function PlayerBar() {
125
- const { playing } = usePlayingState() // { playing, buffering }
127
+ const { playing } = usePlayingState() // { playing, buffering }
126
128
  const { position, duration } = useProgress() // in seconds
127
- const track = useActiveTrack() // Track | undefined
129
+ const track = useActiveTrack() // Track | undefined
128
130
 
129
131
  return (
130
132
  <View>
131
133
  <Text>{track?.title}</Text>
132
- <Text>{Math.floor(position)} / {Math.floor(duration)}s</Text>
134
+ <Text>
135
+ {Math.floor(position)} / {Math.floor(duration)}s
136
+ </Text>
133
137
  <Button
134
138
  title={playing ? 'Pause' : 'Play'}
135
139
  onPress={() => AudioBrowser.togglePlayback()}
@@ -33,13 +33,19 @@ if (isNewArchitectureEnabled()) {
33
33
  apply plugin: "com.facebook.react"
34
34
  }
35
35
 
36
- ktfmt { googleStyle()
36
+ ktfmt {
37
+ googleStyle()
37
38
  }
38
39
 
39
40
  tasks.register("format") {
40
41
  dependsOn "ktfmtFormat"
41
42
  }
42
43
 
44
+ // Fails instead of rewriting — what CI runs.
45
+ tasks.register("formatCheck") {
46
+ dependsOn "ktfmtCheck"
47
+ }
48
+
43
49
  def getExtOrDefault(name) {
44
50
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["AudioBrowser_" + name]
45
51
  }
@@ -168,14 +174,10 @@ dependencies {
168
174
  implementation "androidx.core:core-ktx:1.13.1"
169
175
  // CarConnection: the documented Android Auto / Automotive connection signal
170
176
  implementation "androidx.car.app:app:1.4.0"
171
- implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0"
172
177
  implementation "androidx.lifecycle:lifecycle-process:2.8.6"
173
178
  implementation 'androidx.media:media:1.7.0'
174
179
 
175
180
  implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6")
176
- implementation("androidx.activity:activity-compose:1.9.3")
177
- implementation("androidx.compose.ui:ui")
178
- implementation("androidx.compose.ui:ui-graphics")
179
181
 
180
182
  // Media3 dependencies for audio playback.
181
183
  // Version is configurable via rootProject.ext.media3Version or AudioBrowser_media3Version
@@ -1,6 +1,6 @@
1
1
  AudioBrowser_kotlinVersion=2.1.20
2
2
  AudioBrowser_minSdkVersion=23
3
3
  AudioBrowser_targetSdkVersion=35
4
- AudioBrowser_compileSdkVersion=34
4
+ AudioBrowser_compileSdkVersion=35
5
5
  AudioBrowser_ndkVersion=27.1.12297006
6
6
  AudioBrowser_media3Version=1.10.1
@@ -23,7 +23,9 @@
23
23
  <action android:name="androidx.media3.session.MediaSessionService" />
24
24
  </intent-filter>
25
25
 
26
- <!-- For Android Auto support -->
26
+ <!-- Voice "play <query>". Any app can send it, but that is a subset of what an
27
+ unauthenticated MediaController already has: onConnect admits everyone.
28
+ Restrict connections there, not here. -->
27
29
  <intent-filter>
28
30
  <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
29
31
  <category android:name="android.intent.category.DEFAULT" />
@@ -84,14 +84,15 @@ import com.margelo.nitro.audiobrowser.Track
84
84
  import com.margelo.nitro.audiobrowser.TrackMetadata
85
85
  import com.margelo.nitro.core.Promise
86
86
  import java.util.concurrent.TimeUnit
87
+ import java.util.concurrent.atomic.AtomicReference
87
88
  import kotlin.coroutines.resume
88
89
  import kotlin.coroutines.resumeWithException
89
90
  import kotlin.coroutines.suspendCoroutine
90
91
  import kotlinx.coroutines.CancellationException
91
92
  import kotlinx.coroutines.Job
92
93
  import kotlinx.coroutines.MainScope
94
+ import kotlinx.coroutines.cancel
93
95
  import kotlinx.coroutines.cancelChildren
94
- import kotlinx.coroutines.delay
95
96
  import kotlinx.coroutines.launch
96
97
  import kotlinx.coroutines.runBlocking
97
98
  import timber.log.Timber
@@ -100,10 +101,54 @@ import timber.log.Timber
100
101
  @DoNotStrip
101
102
  class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
102
103
 
103
- /** Called by Nitro when the JS object is destroyed (incl. JS runtime reloads). */
104
+ /**
105
+ * Nitro calls this only when JS calls `dispose()` explicitly — never on runtime teardown, which
106
+ * is what [AudioBrowserLifecycleModule] is for. Both routes land here.
107
+ *
108
+ * The Service outlives this object, so everything registered in `init` or on connect has to be
109
+ * undone. Left alone, the running [Player] goes on invoking callbacks into a dead JSI runtime,
110
+ * and every reload strands another ServiceConnection and lifecycle observer, each retaining a
111
+ * BrowserManager.
112
+ *
113
+ * Runs the teardown on main: the state below is written from main-thread coroutines
114
+ * ([onServiceConnected], [onServiceDisconnected]), while dispose itself arrives on the JS thread
115
+ * or the teardown callback's thread.
116
+ */
104
117
  override fun dispose() {
118
+ current.compareAndSet(this, null)
105
119
  systemVolumeMonitor.destroy()
106
120
  outputMonitor.destroy()
121
+ // Only when still ours: on a reload the replacement may already own the slot.
122
+ carConnectionTarget.compareAndSet(onCarConnectedChanged, null)
123
+
124
+ handler.post {
125
+ connectedService?.let { service ->
126
+ // Identity-guarded: both instances bind to the same singleton Service, so a late dispose
127
+ // must not strip the wiring a replacement has already installed.
128
+ if (service.player.getCallbacks() === callbacks) service.player.setCallbacks(null)
129
+ if (service.player.browser === this) {
130
+ service.player.browser = null
131
+ service.player.forgetBrowserRegistration()
132
+ }
133
+ if (service.onBatteryWarningPendingChanged === onBatteryWarningPending) {
134
+ service.onBatteryWarningPendingChanged = null
135
+ }
136
+ }
137
+ connectedService = null
138
+
139
+ // Dropping the reference disconnects nothing — the controller stays in the session's
140
+ // connected list, holding a binder and a listener.
141
+ mediaBrowserFuture?.let { MediaBrowser.releaseFuture(it) }
142
+ mediaBrowserFuture = null
143
+ setupPromise = null
144
+
145
+ serviceBinding.unbind(this)
146
+
147
+ ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
148
+ // After the observer removal, so this post is not the one dropped.
149
+ handler.removeCallbacksAndMessages(null)
150
+ mainScope.cancel()
151
+ }
107
152
  super.dispose()
108
153
  }
109
154
 
@@ -150,6 +195,13 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
150
195
  private var mediaBrowserFuture: ListenableFuture<MediaBrowser>? = null
151
196
  private var setupOptions = PlayerSetupOptions()
152
197
  private var connectedService: Service? = null
198
+
199
+ private val serviceBinding = ServiceBinding(context)
200
+
201
+ /** Held rather than inlined so [dispose] can tell its own wiring from a replacement's. */
202
+ private val onBatteryWarningPending: (Boolean) -> Unit = { pending ->
203
+ post { onBatteryWarningPendingChanged(BatteryWarningPendingChangedEvent(pending)) }
204
+ }
153
205
  private var setupPromise: ((Unit) -> Unit)? = null
154
206
 
155
207
  // Initial player state staged before the player exists — from setup options or the imperative
@@ -239,12 +291,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
239
291
  }
240
292
 
241
293
  init {
294
+ current.set(this)
295
+
242
296
  // Auto-bind to service if it's already running
243
297
  launchInScope {
244
298
  try {
245
299
  Timber.d("Attempting to auto-bind to existing AudioBrowserService from AudioBrowser")
246
300
  val intent = Intent(context, Service::class.java)
247
- val bound = context.bindService(intent, this@AudioBrowser, Context.BIND_AUTO_CREATE)
301
+ val bound = serviceBinding.bind(intent, this@AudioBrowser)
248
302
  Timber.d("Auto-bind result: $bound")
249
303
 
250
304
  if (!bound) {
@@ -659,7 +713,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
659
713
  }
660
714
  }
661
715
 
662
- override fun onSearch(query: String): Promise<Array<Track>> {
716
+ override fun search(query: String): Promise<Array<Track>> {
663
717
  return Promise.async(mainScope) {
664
718
  Timber.d("Searching for: $query")
665
719
  val searchResults = browserManager.search(query)
@@ -781,7 +835,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
781
835
  override var onCarConnectedChanged: (Boolean) -> Unit = {}
782
836
  set(value) {
783
837
  field = value
784
- carConnectionTarget = value
838
+ carConnectionTarget.set(value)
785
839
  // Immediately notify current state (the car may have connected before
786
840
  // this JS runtime subscribed) — mirrors the iOS behavior.
787
841
  value(carConnected)
@@ -811,12 +865,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
811
865
  // Service not connected yet, bind to service
812
866
  suspendCoroutine<Unit> { continuation ->
813
867
  Timber.d("Binding to AudioBrowserService")
814
- val bound =
815
- context.bindService(
816
- Intent(context, Service::class.java),
817
- this@AudioBrowser,
818
- Context.BIND_AUTO_CREATE,
819
- )
868
+ val bound = serviceBinding.bind(Intent(context, Service::class.java), this@AudioBrowser)
820
869
 
821
870
  if (!bound) {
822
871
  continuation.resumeWithException(
@@ -852,7 +901,6 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
852
901
 
853
902
  override fun reset() = runBlockingOnMain {
854
903
  player.stop()
855
- delay(300) // Allow playback to stop
856
904
  player.clear()
857
905
  // clear() while IDLE emits no engine event, so the state machine never
858
906
  // leaves STOPPED — a reset queue is "nothing loaded".
@@ -1175,13 +1223,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1175
1223
  }
1176
1224
 
1177
1225
  // Wire up battery warning callback from service
1178
- connectedService?.onBatteryWarningPendingChanged = { pending ->
1179
- post {
1180
- this@AudioBrowser.onBatteryWarningPendingChanged(
1181
- BatteryWarningPendingChangedEvent(pending)
1182
- )
1183
- }
1184
- }
1226
+ connectedService?.onBatteryWarningPendingChanged = onBatteryWarningPending
1185
1227
 
1186
1228
  val sessionToken = SessionToken(context, ComponentName(context, Service::class.java))
1187
1229
  mediaBrowserFuture = MediaBrowser.Builder(context, sessionToken).buildAsync()
@@ -1436,9 +1478,22 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1436
1478
  // callback registers as the single notification target (mirrors the iOS
1437
1479
  // static + shared-instance pattern).
1438
1480
  @Volatile private var carConnected = false
1439
- @Volatile private var carConnectionTarget: ((Boolean) -> Unit)? = null
1481
+ // Compare-and-set, not @Volatile: a disposing instance clears the slot only if it still owns
1482
+ // it, and a replacement may be claiming it at the same moment.
1483
+ private val carConnectionTarget = AtomicReference<((Boolean) -> Unit)?>(null)
1440
1484
  private var carConnectionObserverStarted = false
1441
1485
 
1486
+ /**
1487
+ * The instance the current JS runtime is using. Nitro hands out no registry, and the teardown
1488
+ * callback has nothing to dispose without one.
1489
+ */
1490
+ private val current = AtomicReference<AudioBrowser?>(null)
1491
+
1492
+ /** Called by [AudioBrowserLifecycleModule] when the React instance is torn down. */
1493
+ internal fun disposeCurrent() {
1494
+ current.getAndSet(null)?.dispose()
1495
+ }
1496
+
1442
1497
  private fun startCarConnectionObserver(context: Context, handler: Handler) {
1443
1498
  // Posted to main: LiveData observation is main-thread only, and the
1444
1499
  // single-start guard is then only ever touched from one thread.
@@ -1449,7 +1504,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1449
1504
  val connected = type != CarConnection.CONNECTION_TYPE_NOT_CONNECTED
1450
1505
  if (connected == carConnected) return@observeForever
1451
1506
  carConnected = connected
1452
- carConnectionTarget?.invoke(connected)
1507
+ carConnectionTarget.get()?.invoke(connected)
1453
1508
  }
1454
1509
  }
1455
1510
  }
@@ -0,0 +1,38 @@
1
+ package com.audiobrowser
2
+
3
+ import com.facebook.react.bridge.BaseJavaModule
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.turbomodule.core.interfaces.TurboModule
6
+ import timber.log.Timber
7
+
8
+ /**
9
+ * Tears the live [AudioBrowser] down when the React instance goes away.
10
+ *
11
+ * Nitro's `dispose()` is reachable only from an explicit `dispose()` call in JS — it is *not* run
12
+ * when the runtime is destroyed. Without this, a reload leaves the previous instance registered
13
+ * with the Service, the process lifecycle and the car-connection observer, so it stays reachable
14
+ * (and keeps receiving player callbacks over a dead JSI runtime) forever.
15
+ *
16
+ * [invalidate] is what React Native calls on instance teardown, so it is the hook that actually
17
+ * fires. The module exists only for that callback and exposes nothing to JS, so it has to be
18
+ * created without JS ever asking for it: `TurboModuleManager.invalidate` only invalidates modules
19
+ * in its holder map, and under bridgeless a module reaches that map eagerly only if it is *both*
20
+ * `needsEagerInit` and `isTurboModule`
21
+ * (`ReactPackageTurboModuleManagerDelegate.getEagerInitModuleNames`). Hence the marker interface —
22
+ * a plain legacy module is created solely on request from JS, which for this one never comes.
23
+ */
24
+ internal class AudioBrowserLifecycleModule(reactContext: ReactApplicationContext) :
25
+ BaseJavaModule(reactContext), TurboModule {
26
+
27
+ override fun getName(): String = NAME
28
+
29
+ override fun invalidate() {
30
+ Timber.d("React instance going away — disposing the live AudioBrowser")
31
+ AudioBrowser.disposeCurrent()
32
+ super.invalidate()
33
+ }
34
+
35
+ companion object {
36
+ const val NAME: String = "AudioBrowserLifecycle"
37
+ }
38
+ }
@@ -3,14 +3,30 @@ package com.audiobrowser
3
3
  import com.facebook.react.TurboReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
5
5
  import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
6
7
  import com.facebook.react.module.model.ReactModuleInfoProvider
7
8
  import com.margelo.nitro.audiobrowser.AudioBrowserOnLoad
8
9
 
9
10
  public class AudioBrowserPackage : TurboReactPackage() {
10
- override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
12
+ if (name == AudioBrowserLifecycleModule.NAME) AudioBrowserLifecycleModule(reactContext)
13
+ else null
11
14
 
12
15
  override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
13
- emptyMap()
16
+ mapOf(
17
+ AudioBrowserLifecycleModule.NAME to
18
+ ReactModuleInfo(
19
+ AudioBrowserLifecycleModule.NAME,
20
+ AudioBrowserLifecycleModule::class.java.name,
21
+ false, // canOverrideExistingModule
22
+ // Eager + TurboModule: nothing in JS imports this module, and its only job is the
23
+ // teardown callback, which never arrives if the module is never created. Both flags are
24
+ // required for that — see the note on the module.
25
+ true, // needsEagerInit
26
+ false, // isCxxModule
27
+ ReactModuleInfo.classIsTurboModule(AudioBrowserLifecycleModule::class.java),
28
+ )
29
+ )
14
30
  }
15
31
 
16
32
  companion object {
@@ -0,0 +1,48 @@
1
+ package com.audiobrowser
2
+
3
+ import android.content.Context
4
+ import android.content.Intent
5
+ import android.content.ServiceConnection
6
+ import timber.log.Timber
7
+
8
+ /**
9
+ * Exactly-once bookkeeping for a [ServiceConnection].
10
+ *
11
+ * `bindService` may be called from more than one place — an auto-bind at construction and again
12
+ * from setup — but the binding must be released exactly once, and only if one was ever taken:
13
+ * [Context.unbindService] throws when the connection was never registered. Kept apart from the
14
+ * caller so the rule is one object's job, and so it can be tested without a JSI runtime.
15
+ */
16
+ internal class ServiceBinding(private val context: Context) {
17
+
18
+ @Volatile
19
+ var isBound: Boolean = false
20
+ private set
21
+
22
+ /**
23
+ * Binds if not already bound, and returns whether a binding is held afterwards. A second call
24
+ * while bound is a no-op: Android would otherwise count the extra bind and expect a matching
25
+ * unbind.
26
+ */
27
+ fun bind(intent: Intent, connection: ServiceConnection): Boolean {
28
+ if (isBound) return true
29
+ // Marked bound whatever the result: Android registers the connection before it asks the
30
+ // ActivityManager, so a false return still leaves one to release — "regardless of the return
31
+ // value, you should later call unbindService()".
32
+ isBound = true
33
+ return context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
34
+ }
35
+
36
+ /**
37
+ * Releases the binding if one is held. Safe to call repeatedly, and safe when the service has
38
+ * already died — [onServiceDisconnected][ServiceConnection.onServiceDisconnected] means the
39
+ * service went away, not that the binding did, so the unbind is still required.
40
+ */
41
+ fun unbind(connection: ServiceConnection) {
42
+ if (!isBound) return
43
+ isBound = false
44
+ // Throws if the binding is somehow already gone, which is the outcome we wanted anyway.
45
+ runCatching { context.unbindService(connection) }
46
+ .onFailure { Timber.w(it, "unbindService failed") }
47
+ }
48
+ }
@@ -1,7 +1,5 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
- import com.audiobrowser.util.artworkOf
4
-
5
3
  import android.util.LruCache
6
4
  import androidx.media3.common.MediaItem
7
5
  import androidx.media3.session.MediaSession
@@ -9,6 +7,7 @@ import com.audiobrowser.http.HttpClient
9
7
  import com.audiobrowser.http.RequestConfigBuilder
10
8
  import com.audiobrowser.util.BrowserPathHelper
11
9
  import com.audiobrowser.util.TrackFactory
10
+ import com.audiobrowser.util.artworkOf
12
11
  import com.margelo.nitro.audiobrowser.ArtworkRequestConfig
13
12
  import com.margelo.nitro.audiobrowser.BrowserSourceCallbackParam
14
13
  import com.margelo.nitro.audiobrowser.FavoritesMatchMode
@@ -1,8 +1,7 @@
1
1
  package com.audiobrowser.browser
2
2
 
3
- import com.audiobrowser.util.url
4
-
5
3
  import com.audiobrowser.http.RequestConfigBuilder
4
+ import com.audiobrowser.util.url
6
5
  import com.margelo.nitro.audiobrowser.ArtworkRequestConfig
7
6
  import com.margelo.nitro.audiobrowser.ImageContext
8
7
  import com.margelo.nitro.audiobrowser.ImageQueryParams
@@ -4,9 +4,9 @@ import com.margelo.nitro.audiobrowser.ImageRowItem
4
4
  import com.margelo.nitro.audiobrowser.Track
5
5
 
6
6
  /**
7
- * Queue scope is the tapped section, not the whole page (ADR 0006): the
8
- * playback context a listener expects is the list they tapped in — a page
9
- * aggregating several sections must not leak next/previous across them.
7
+ * Queue scope is the tapped section, not the whole page (ADR 0006): the playback context a listener
8
+ * expects is the list they tapped in — a page aggregating several sections must not leak
9
+ * next/previous across them.
10
10
  */
11
11
  object SectionScope {
12
12
  sealed class Section {
@@ -14,15 +14,14 @@ object SectionScope {
14
14
  data class ImageRow(val items: List<ImageRowItem>) : Section()
15
15
 
16
16
  /**
17
- * The contiguous `groupTitle` run around the tapped child (items with no
18
- * group title form runs of their own).
17
+ * The contiguous `groupTitle` run around the tapped child (items with no group title form runs
18
+ * of their own).
19
19
  */
20
20
  data class Run(val tracks: List<Track>) : Section()
21
21
  }
22
22
 
23
23
  /**
24
- * The section of [children] containing the playable [trackId], or null
25
- * when the id is not found.
24
+ * The section of [children] containing the playable [trackId], or null when the id is not found.
26
25
  */
27
26
  fun section(children: List<Track>, trackId: String): Section? {
28
27
  for (child in children) {
@@ -1,5 +1,6 @@
1
1
  package com.audiobrowser.http
2
2
 
3
+ import com.audiobrowser.BuildConfig
3
4
  import java.io.IOException
4
5
  import java.util.concurrent.TimeUnit
5
6
  import kotlinx.coroutines.Dispatchers
@@ -27,13 +28,29 @@ class HttpClient {
27
28
  isLenient = true
28
29
  }
29
30
 
30
- private val loggingInterceptor =
31
+ /**
32
+ * Full request/response logging, for debug builds only.
33
+ *
34
+ * Timber is exposed as `api`, so whatever this logs reaches the host app's tree — a Crashlytics
35
+ * or Sentry breadcrumb tree included. That means credentials attached by the request-transform
36
+ * layer and every response body, so the interceptor is not installed in release at all: `BODY`
37
+ * also buffers and stringifies each response whether or not anything reads the log.
38
+ *
39
+ * The redactions apply in debug too. Logcat is shared in bug reports and screen shares.
40
+ */
41
+ private fun loggingInterceptor() =
31
42
  HttpLoggingInterceptor { message -> Timber.d(message) }
32
- .apply { level = HttpLoggingInterceptor.Level.BODY }
43
+ .apply {
44
+ level = HttpLoggingInterceptor.Level.BODY
45
+ redactHeader("Authorization")
46
+ redactHeader("Proxy-Authorization")
47
+ redactHeader("Cookie")
48
+ redactHeader("Set-Cookie")
49
+ }
33
50
 
34
51
  private val client =
35
52
  OkHttpClient.Builder()
36
- .addInterceptor(loggingInterceptor)
53
+ .apply { if (BuildConfig.DEBUG) addInterceptor(loggingInterceptor()) }
37
54
  .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
38
55
  .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
39
56
  .writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@@ -3,11 +3,11 @@ package com.audiobrowser.model
3
3
  import com.margelo.nitro.audiobrowser.AndroidOptions
4
4
  import com.margelo.nitro.audiobrowser.AppKilledPlaybackBehavior
5
5
  import com.margelo.nitro.audiobrowser.NativeUpdateOptions
6
- import com.margelo.nitro.audiobrowser.NotificationButtonLayout
7
6
  import com.margelo.nitro.audiobrowser.Options
8
7
  import com.margelo.nitro.audiobrowser.PlayerCapabilities
8
+ import com.margelo.nitro.audiobrowser.RemoteButtonLayout
9
9
  import com.margelo.nitro.audiobrowser.Variant_NullType_Double
10
- import com.margelo.nitro.audiobrowser.Variant_NullType_NotificationButtonLayout
10
+ import com.margelo.nitro.audiobrowser.Variant_NullType_RemoteButtonLayout
11
11
 
12
12
  /**
13
13
  * Update options for the AudioBrowser that can be changed at runtime. These options control player
@@ -37,8 +37,8 @@ data class PlayerUpdateOptions(
37
37
  playbackRate = null,
38
38
  ),
39
39
 
40
- // Notification button layout (null = derive from capabilities)
41
- var notificationButtons: NotificationButtonLayout? = null,
40
+ // Ordered button layout (null = derive from capabilities)
41
+ var remoteButtonLayout: RemoteButtonLayout? = null,
42
42
 
43
43
  // Android-specific runtime options (all under android.* in JS)
44
44
  var appKilledPlaybackBehavior: AppKilledPlaybackBehavior =
@@ -66,12 +66,12 @@ data class PlayerUpdateOptions(
66
66
  // Update boolean options
67
67
  androidOptions.skipSilence?.let { skipSilence = it }
68
68
 
69
- // Handle notificationButtons - variant allows distinguishing undefined from null
70
- androidOptions.notificationButtons?.let { variant ->
71
- notificationButtons =
69
+ // Handle remoteButtonLayout - variant allows distinguishing undefined from null
70
+ androidOptions.remoteButtonLayout?.let { variant ->
71
+ remoteButtonLayout =
72
72
  when (variant) {
73
- is Variant_NullType_NotificationButtonLayout.First -> null
74
- is Variant_NullType_NotificationButtonLayout.Second -> variant.value
73
+ is Variant_NullType_RemoteButtonLayout.First -> null
74
+ is Variant_NullType_RemoteButtonLayout.Second -> variant.value
75
75
  }
76
76
  }
77
77
  }
@@ -84,8 +84,8 @@ data class PlayerUpdateOptions(
84
84
  AndroidOptions(
85
85
  appKilledPlaybackBehavior = appKilledPlaybackBehavior,
86
86
  skipSilence = skipSilence,
87
- notificationButtons =
88
- notificationButtons?.let { Variant_NullType_NotificationButtonLayout.create(it) },
87
+ remoteButtonLayout =
88
+ remoteButtonLayout?.let { Variant_NullType_RemoteButtonLayout.create(it) },
89
89
  ),
90
90
  forwardJumpInterval = forwardJumpInterval,
91
91
  backwardJumpInterval = backwardJumpInterval,