react-native-audio-browser 0.2.0-next.1 → 0.2.0-next.11
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.
- package/README.md +22 -18
- package/android/build.gradle +13 -10
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +76 -21
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +1 -2
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +1 -2
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +6 -7
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +11 -11
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +9 -5
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +0 -1
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -4
- package/android/src/main/java/com/audiobrowser/player/Player.kt +30 -8
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +2 -1
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -7
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +52 -1
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +167 -8
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +2 -1
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +2 -4
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +100 -56
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/ios/Browser/BrowserManager.swift +2 -27
- package/ios/Browser/SearchDrillIn.swift +1 -1
- package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
- package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
- package/ios/HybridAudioBrowser.swift +87 -56
- package/ios/Model/NitroTypeStubs.swift +18 -2
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/Option/SessionCategories.swift +3 -2
- package/ios/Player/MediaIntentCriteria.swift +4 -4
- package/ios/Player/MediaLoader.swift +3 -1
- package/ios/Player/PlaybackCoordinator.swift +4 -1
- package/ios/Player/PlaybackErrorHandler.swift +4 -4
- package/ios/Player/QueueManager.swift +10 -3
- package/ios/Player/RetryManager.swift +79 -18
- package/ios/Player/ShuffleOrder.swift +10 -0
- package/ios/Player/SleepTimerManager.swift +43 -15
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +9 -6
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/MediaLoaderTests.swift +10 -13
- package/ios/Tests/PlaybackCoordinatorTests.swift +10 -7
- package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/QueueManagerTests.swift +50 -4
- package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SectionScopeTests.swift +2 -2
- package/ios/Tests/SleepTimerManagerTests.swift +22 -19
- package/ios/Tests/Support/ManualSleepTimerScheduler.swift +47 -0
- package/ios/Tests/Support/TestTypes.swift +1 -1
- package/ios/Tests/TabBarEntriesTests.swift +7 -7
- package/ios/TrackPlayer.swift +6 -1
- package/ios/TrackPlayerCallbacks.swift +5 -38
- package/ios/Util/LRUCache.swift +4 -1
- package/lib/commonjs/AudioBrowser.js +0 -11
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser.js +1 -1
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +1 -1
- package/lib/commonjs/features/playback/state.js +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +15 -8
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +21 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +1 -1
- package/lib/commonjs/features/queue/queue.js +2 -2
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +8 -5
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +1 -1
- package/lib/module/AudioBrowser.js +1 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser.js +1 -1
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +1 -1
- package/lib/module/features/playback/state.js +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +14 -7
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +17 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +1 -1
- package/lib/module/features/queue/queue.js +2 -2
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +8 -5
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/web/NativeAudioBrowser.js +1 -1
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts +0 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +87 -37
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +9 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +2 -2
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -4
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +5 -5
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +11 -11
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +36 -51
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +6 -6
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +8 -8
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/package.json +12 -4
- package/src/AudioBrowser.ts +1 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser.ts +1 -1
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +1 -1
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +254 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +1 -1
- package/src/features/playback/state.ts +1 -1
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +0 -5
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +99 -57
- package/src/features/player/setup.test.ts +12 -4
- package/src/features/player/setup.ts +19 -9
- package/src/features/player/validateOptions.ts +19 -0
- package/src/features/queue/activeTrack.ts +1 -1
- package/src/features/queue/queue.ts +2 -2
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +237 -0
- package/src/public-surface.test.ts +107 -0
- package/src/specs/audio-browser.nitro.ts +1 -1
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/utils/LazyNativeEmitter.ts +9 -6
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/web/NativeAudioBrowser.ts +1 -1
- package/src/web/browser/BrowserManager.ts +1 -1
- package/ios/Model/MediaURL.swift +0 -67
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- 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" },
|
|
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([
|
|
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
|
|
103
|
-
|
|
|
104
|
-
| `usePlayingState()`
|
|
105
|
-
| `useProgress()`
|
|
106
|
-
| `useActiveTrack()`
|
|
107
|
-
| `useNowPlaying()`
|
|
108
|
-
| `useQueue()`
|
|
109
|
-
| `useRepeatMode()` / `useShuffle()`
|
|
110
|
-
| `useSleepTimer()`
|
|
111
|
-
| `useCarConnected()`
|
|
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()
|
|
127
|
+
const { playing } = usePlayingState() // { playing, buffering }
|
|
126
128
|
const { position, duration } = useProgress() // in seconds
|
|
127
|
-
const track = useActiveTrack()
|
|
129
|
+
const track = useActiveTrack() // Track | undefined
|
|
128
130
|
|
|
129
131
|
return (
|
|
130
132
|
<View>
|
|
131
133
|
<Text>{track?.title}</Text>
|
|
132
|
-
<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()}
|
package/android/build.gradle
CHANGED
|
@@ -33,13 +33,19 @@ if (isNewArchitectureEnabled()) {
|
|
|
33
33
|
apply plugin: "com.facebook.react"
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
ktfmt {
|
|
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
|
}
|
|
@@ -56,12 +62,13 @@ android {
|
|
|
56
62
|
ndkVersion getExtOrDefault("ndkVersion")
|
|
57
63
|
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
58
64
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
65
|
+
// Deliberately left off (the AGP default). Turning it on is global: every un-mocked
|
|
66
|
+
// android.* call in every unit test silently returns 0/false/null instead of throwing,
|
|
67
|
+
// so a test can pass while the code under test never ran. Tests that need real framework
|
|
68
|
+
// behaviour run under Robolectric instead — see BrowserManagerLayerResolutionTest, whose
|
|
69
|
+
// android.util.LruCache.evictAll() is what this flag used to paper over.
|
|
63
70
|
testOptions {
|
|
64
|
-
unitTests.returnDefaultValues =
|
|
71
|
+
unitTests.returnDefaultValues = false
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
defaultConfig {
|
|
@@ -168,14 +175,10 @@ dependencies {
|
|
|
168
175
|
implementation "androidx.core:core-ktx:1.13.1"
|
|
169
176
|
// CarConnection: the documented Android Auto / Automotive connection signal
|
|
170
177
|
implementation "androidx.car.app:app:1.4.0"
|
|
171
|
-
implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0"
|
|
172
178
|
implementation "androidx.lifecycle:lifecycle-process:2.8.6"
|
|
173
179
|
implementation 'androidx.media:media:1.7.0'
|
|
174
180
|
|
|
175
181
|
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
182
|
|
|
180
183
|
// Media3 dependencies for audio playback.
|
|
181
184
|
// 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=
|
|
4
|
+
AudioBrowser_compileSdkVersion=35
|
|
5
5
|
AudioBrowser_ndkVersion=27.1.12297006
|
|
6
|
-
AudioBrowser_media3Version=1.
|
|
6
|
+
AudioBrowser_media3Version=1.11.0
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
<action android:name="androidx.media3.session.MediaSessionService" />
|
|
24
24
|
</intent-filter>
|
|
25
25
|
|
|
26
|
-
<!--
|
|
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
|
-
/**
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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? =
|
|
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
|
-
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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
|
-
//
|
|
41
|
-
var
|
|
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
|
|
70
|
-
androidOptions.
|
|
71
|
-
|
|
69
|
+
// Handle remoteButtonLayout - variant allows distinguishing undefined from null
|
|
70
|
+
androidOptions.remoteButtonLayout?.let { variant ->
|
|
71
|
+
remoteButtonLayout =
|
|
72
72
|
when (variant) {
|
|
73
|
-
is
|
|
74
|
-
is
|
|
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
|
-
|
|
88
|
-
|
|
87
|
+
remoteButtonLayout =
|
|
88
|
+
remoteButtonLayout?.let { Variant_NullType_RemoteButtonLayout.create(it) },
|
|
89
89
|
),
|
|
90
90
|
forwardJumpInterval = forwardJumpInterval,
|
|
91
91
|
backwardJumpInterval = backwardJumpInterval,
|