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.
- package/README.md +22 -18
- package/android/build.gradle +7 -5
- package/android/gradle.properties +1 -1
- 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 +21 -6
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +109 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +16 -2
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +59 -3
- 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 +0 -1
- 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 +46 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +113 -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/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 +46 -0
- 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 +10 -3
- 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/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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
package com.audiobrowser.player
|
|
2
2
|
|
|
3
|
-
import com.margelo.nitro.audiobrowser.NotificationButton
|
|
4
|
-
import com.margelo.nitro.audiobrowser.NotificationButtonLayout
|
|
5
3
|
import com.margelo.nitro.audiobrowser.PlayerCapabilities
|
|
4
|
+
import com.margelo.nitro.audiobrowser.RemoteButton
|
|
5
|
+
import com.margelo.nitro.audiobrowser.RemoteButtonLayout
|
|
6
6
|
import com.margelo.nitro.audiobrowser.Variant_Boolean_FavoriteConfig
|
|
7
7
|
import org.junit.Assert.assertEquals
|
|
8
8
|
import org.junit.Assert.assertFalse
|
|
@@ -12,8 +12,7 @@ import org.junit.Test
|
|
|
12
12
|
/**
|
|
13
13
|
* Truth tables for the pure Capability decision core: [PlayerCapabilities.isEnabled] (one rule for
|
|
14
14
|
* "available by default, only false disables; favorite is opt-in; play/pause is one control") and
|
|
15
|
-
* [
|
|
16
|
-
* and the notification player commands consume).
|
|
15
|
+
* [deriveButtonSlots] (the single slot derivation every Media3 assembly consumes).
|
|
17
16
|
*/
|
|
18
17
|
class CapabilityControlsTest {
|
|
19
18
|
|
|
@@ -43,117 +42,162 @@ class CapabilityControlsTest {
|
|
|
43
42
|
playbackRate = null,
|
|
44
43
|
)
|
|
45
44
|
|
|
45
|
+
private fun layout(
|
|
46
|
+
back: RemoteButton? = null,
|
|
47
|
+
forward: RemoteButton? = null,
|
|
48
|
+
overflow: Array<RemoteButton> = emptyArray(),
|
|
49
|
+
) = RemoteButtonLayout(back = back, forward = forward, overflow = overflow)
|
|
50
|
+
|
|
46
51
|
private fun slots(
|
|
47
52
|
capabilities: PlayerCapabilities,
|
|
48
|
-
layout:
|
|
49
|
-
): List<Pair<
|
|
50
|
-
|
|
53
|
+
layout: RemoteButtonLayout? = null,
|
|
54
|
+
): List<Pair<RemoteButton, ButtonSlot>> =
|
|
55
|
+
deriveButtonSlots(capabilities, layout).map { it.button to it.slot }
|
|
51
56
|
|
|
52
57
|
// MARK: isEnabled
|
|
53
58
|
|
|
54
59
|
@Test
|
|
55
60
|
fun `everything but favorite is enabled by default`() {
|
|
56
61
|
val defaults = capabilities()
|
|
57
|
-
for (control in
|
|
58
|
-
assertEquals("$control", control !=
|
|
62
|
+
for (control in Capability.entries) {
|
|
63
|
+
assertEquals("$control", control != Capability.FAVORITE, defaults.isEnabled(control))
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
@Test
|
|
63
68
|
fun `only an explicit false disables a control`() {
|
|
64
69
|
val caps = capabilities(skipToNext = false, jumpBackward = false)
|
|
65
|
-
assertFalse(caps.isEnabled(
|
|
66
|
-
assertFalse(caps.isEnabled(
|
|
67
|
-
assertTrue(caps.isEnabled(
|
|
68
|
-
assertTrue(caps.isEnabled(
|
|
70
|
+
assertFalse(caps.isEnabled(Capability.SKIP_TO_NEXT))
|
|
71
|
+
assertFalse(caps.isEnabled(Capability.JUMP_BACKWARD))
|
|
72
|
+
assertTrue(caps.isEnabled(Capability.SKIP_TO_PREVIOUS))
|
|
73
|
+
assertTrue(caps.isEnabled(Capability.JUMP_FORWARD))
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
@Test
|
|
72
77
|
fun `play pause is one control - it survives a single half being disabled`() {
|
|
73
|
-
assertTrue(capabilities(play = false).isEnabled(
|
|
74
|
-
assertTrue(capabilities(pause = false).isEnabled(
|
|
75
|
-
assertFalse(capabilities(play = false, pause = false).isEnabled(
|
|
78
|
+
assertTrue(capabilities(play = false).isEnabled(Capability.PLAY_PAUSE))
|
|
79
|
+
assertTrue(capabilities(pause = false).isEnabled(Capability.PLAY_PAUSE))
|
|
80
|
+
assertFalse(capabilities(play = false, pause = false).isEnabled(Capability.PLAY_PAUSE))
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
@Test
|
|
79
84
|
fun `favorite is opt-in`() {
|
|
80
|
-
assertFalse(capabilities().isEnabled(
|
|
81
|
-
assertFalse(capabilities(favorite = false).isEnabled(
|
|
82
|
-
assertTrue(capabilities(favorite = true).isEnabled(
|
|
85
|
+
assertFalse(capabilities().isEnabled(Capability.FAVORITE))
|
|
86
|
+
assertFalse(capabilities(favorite = false).isEnabled(Capability.FAVORITE))
|
|
87
|
+
assertTrue(capabilities(favorite = true).isEnabled(Capability.FAVORITE))
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
// MARK:
|
|
90
|
+
// MARK: deriveButtonSlots — capability defaults
|
|
86
91
|
|
|
87
92
|
@Test
|
|
88
|
-
fun `default layout puts skip on the primary
|
|
93
|
+
fun `default layout puts skip on the primary positions, jump and favorite in overflow`() {
|
|
89
94
|
val derived = slots(capabilities(favorite = true))
|
|
90
95
|
assertEquals(
|
|
91
96
|
listOf(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
RemoteButton.SKIP_TO_PREVIOUS to ButtonSlot.BACK,
|
|
98
|
+
RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD,
|
|
99
|
+
RemoteButton.JUMP_BACKWARD to ButtonSlot.OVERFLOW,
|
|
100
|
+
RemoteButton.JUMP_FORWARD to ButtonSlot.OVERFLOW,
|
|
101
|
+
RemoteButton.FAVORITE to ButtonSlot.OVERFLOW,
|
|
97
102
|
),
|
|
98
103
|
derived,
|
|
99
104
|
)
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
@Test
|
|
103
|
-
fun `jump falls back to
|
|
108
|
+
fun `jump falls back to a primary position when skip is disabled`() {
|
|
104
109
|
val derived = slots(capabilities(skipToPrevious = false, skipToNext = false))
|
|
105
110
|
assertEquals(
|
|
106
111
|
listOf(
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
RemoteButton.JUMP_BACKWARD to ButtonSlot.BACK,
|
|
113
|
+
RemoteButton.JUMP_FORWARD to ButtonSlot.FORWARD,
|
|
109
114
|
),
|
|
110
115
|
derived,
|
|
111
116
|
)
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
@Test
|
|
115
|
-
fun `a fully disabled side leaves its
|
|
120
|
+
fun `a fully disabled side leaves its position empty`() {
|
|
116
121
|
val derived =
|
|
117
122
|
slots(capabilities(skipToPrevious = false, jumpBackward = false, jumpForward = false))
|
|
118
|
-
assertEquals(listOf(
|
|
123
|
+
assertEquals(listOf(RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD), derived)
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
// MARK:
|
|
126
|
+
// MARK: deriveButtonSlots — explicit layout
|
|
122
127
|
|
|
123
128
|
@Test
|
|
124
|
-
fun `explicit layout maps every
|
|
125
|
-
val
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
fun `an explicit layout maps every field and keeps overflow order`() {
|
|
130
|
+
val derived =
|
|
131
|
+
slots(
|
|
132
|
+
capabilities(),
|
|
133
|
+
layout(
|
|
134
|
+
back = RemoteButton.JUMP_BACKWARD,
|
|
135
|
+
forward = RemoteButton.JUMP_FORWARD,
|
|
136
|
+
overflow = arrayOf(RemoteButton.SKIP_TO_PREVIOUS, RemoteButton.SKIP_TO_NEXT),
|
|
137
|
+
),
|
|
132
138
|
)
|
|
133
|
-
val derived = slots(capabilities(), layout)
|
|
134
139
|
assertEquals(
|
|
135
140
|
listOf(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
RemoteButton.JUMP_BACKWARD to ButtonSlot.BACK,
|
|
142
|
+
RemoteButton.JUMP_FORWARD to ButtonSlot.FORWARD,
|
|
143
|
+
RemoteButton.SKIP_TO_PREVIOUS to ButtonSlot.OVERFLOW,
|
|
144
|
+
RemoteButton.SKIP_TO_NEXT to ButtonSlot.OVERFLOW,
|
|
140
145
|
),
|
|
141
146
|
derived,
|
|
142
147
|
)
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
@Test
|
|
146
|
-
fun `
|
|
147
|
-
val layout =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
fun `a null field leaves that position empty`() {
|
|
152
|
+
val derived = slots(capabilities(), layout(back = null, forward = RemoteButton.SKIP_TO_NEXT))
|
|
153
|
+
assertEquals(listOf(RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD), derived)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Test
|
|
157
|
+
fun `a layout is never merged with the capability defaults`() {
|
|
158
|
+
// Nitro collapses an omitted and a null enum field to the same Kotlin null, so a per-field
|
|
159
|
+
// merge is not expressible: an empty position must stay empty rather than falling back to the
|
|
160
|
+
// capability default. Every capability here is enabled, so a merge would add skip and jump.
|
|
161
|
+
val derived =
|
|
162
|
+
slots(capabilities(favorite = true), layout(overflow = arrayOf(RemoteButton.FAVORITE)))
|
|
163
|
+
assertEquals(listOf(RemoteButton.FAVORITE to ButtonSlot.OVERFLOW), derived)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@Test
|
|
167
|
+
fun `a disabled entry drops out without promoting anything into its position`() {
|
|
168
|
+
// favorite unset (= disabled), skipToPrevious explicitly off. skip-to-next must stay on
|
|
169
|
+
// FORWARD rather than sliding into the vacated BACK position.
|
|
170
|
+
val derived =
|
|
171
|
+
slots(
|
|
172
|
+
capabilities(skipToPrevious = false),
|
|
173
|
+
layout(
|
|
174
|
+
back = RemoteButton.SKIP_TO_PREVIOUS,
|
|
175
|
+
forward = RemoteButton.SKIP_TO_NEXT,
|
|
176
|
+
overflow = arrayOf(RemoteButton.FAVORITE),
|
|
177
|
+
),
|
|
154
178
|
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
179
|
+
assertEquals(listOf(RemoteButton.SKIP_TO_NEXT to ButtonSlot.FORWARD), derived)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// MARK: sameAs
|
|
183
|
+
|
|
184
|
+
@Test
|
|
185
|
+
fun `sameAs compares overflow by content, not by array identity`() {
|
|
186
|
+
val a = layout(back = RemoteButton.JUMP_BACKWARD, overflow = arrayOf(RemoteButton.FAVORITE))
|
|
187
|
+
val b = layout(back = RemoteButton.JUMP_BACKWARD, overflow = arrayOf(RemoteButton.FAVORITE))
|
|
188
|
+
// Distinct Array instances — data-class `==` would call these different and republish the
|
|
189
|
+
// layout to Android Auto on every unrelated options update.
|
|
190
|
+
assertTrue(a.sameAs(b))
|
|
191
|
+
assertFalse(a.sameAs(layout(back = RemoteButton.JUMP_BACKWARD)))
|
|
192
|
+
assertFalse(
|
|
193
|
+
a.sameAs(layout(back = RemoteButton.JUMP_FORWARD, overflow = arrayOf(RemoteButton.FAVORITE)))
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@Test
|
|
198
|
+
fun `sameAs treats null as its own layout`() {
|
|
199
|
+
assertTrue(null.sameAs(null))
|
|
200
|
+
assertFalse(layout(back = RemoteButton.SKIP_TO_NEXT).sameAs(null))
|
|
201
|
+
assertFalse(null.sameAs(layout(back = RemoteButton.SKIP_TO_NEXT)))
|
|
158
202
|
}
|
|
159
203
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.audiobrowser.player
|
|
2
|
+
|
|
3
|
+
import androidx.media3.session.CommandButton
|
|
4
|
+
import org.junit.Assert.assertArrayEquals
|
|
5
|
+
import org.junit.Assert.assertTrue
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Guards the slot preference lists handed to Media3.
|
|
10
|
+
*
|
|
11
|
+
* The invariant that matters is the OVERFLOW fallback: every flattening keeps a button only if it
|
|
12
|
+
* won SLOT_BACK, won SLOT_FORWARD, or names SLOT_OVERFLOW. A slot that stops naming OVERFLOW
|
|
13
|
+
* silently deletes its button from the notification and the car the moment its primary position is
|
|
14
|
+
* unavailable, so the loop below asserts the property for every slot rather than only today's
|
|
15
|
+
* three.
|
|
16
|
+
*/
|
|
17
|
+
class MediaSessionSlotMappingTest {
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
fun `primary slots are preferred with an overflow fallback`() {
|
|
21
|
+
assertArrayEquals(
|
|
22
|
+
intArrayOf(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW),
|
|
23
|
+
media3SlotsFor(ButtonSlot.BACK),
|
|
24
|
+
)
|
|
25
|
+
assertArrayEquals(
|
|
26
|
+
intArrayOf(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW),
|
|
27
|
+
media3SlotsFor(ButtonSlot.FORWARD),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun `overflow does not repeat itself`() {
|
|
33
|
+
assertArrayEquals(intArrayOf(CommandButton.SLOT_OVERFLOW), media3SlotsFor(ButtonSlot.OVERFLOW))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun `every slot names OVERFLOW so nothing can be dropped`() {
|
|
38
|
+
for (slot in ButtonSlot.entries) {
|
|
39
|
+
assertTrue(
|
|
40
|
+
"$slot must name SLOT_OVERFLOW or its button is dropped from the notification and the car",
|
|
41
|
+
media3SlotsFor(slot).contains(CommandButton.SLOT_OVERFLOW),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -132,8 +132,8 @@ class PlaybackErrorClassifierTest {
|
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Load errors are raw IOExceptions from the transport layer; the policy only retries network
|
|
135
|
-
* ones, so the advisory classification is UNREACHABLE unless the device is offline or the
|
|
136
|
-
*
|
|
135
|
+
* ones, so the advisory classification is UNREACHABLE unless the device is offline or the server
|
|
136
|
+
* actually answered.
|
|
137
137
|
*/
|
|
138
138
|
@Test
|
|
139
139
|
fun `retrying transport failure is unreachable`() {
|
|
@@ -158,8 +158,7 @@ class PlaybackErrorClassifierTest {
|
|
|
158
158
|
/** The HTTP status is dug out of the cause chain and mapped through the shared status table. */
|
|
159
159
|
@Test
|
|
160
160
|
fun `retrying HTTP failure carries the status`() {
|
|
161
|
-
val error =
|
|
162
|
-
PlaybackErrorClassifier.retryingLoadError(httpException(503), online = true)
|
|
161
|
+
val error = PlaybackErrorClassifier.retryingLoadError(httpException(503), online = true)
|
|
163
162
|
assertEquals(PlaybackErrorKind.SERVER_ERROR, error.kind)
|
|
164
163
|
assertEquals(503.0, error.statusCode!!, 0.0)
|
|
165
164
|
assertEquals(true, error.retrying)
|
|
@@ -201,9 +201,9 @@ class RetryLoadErrorHandlingPolicyBudgetTest {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
|
-
* Offline failures must not burn the first-connect budget (a station tapped in a tunnel parks
|
|
205
|
-
*
|
|
206
|
-
*
|
|
204
|
+
* Offline failures must not burn the first-connect budget (a station tapped in a tunnel parks for
|
|
205
|
+
* connectivity); the budget's clock starts at the first error observed online, so a dead station
|
|
206
|
+
* still gets a fast verdict after restoration.
|
|
207
207
|
*/
|
|
208
208
|
@Test
|
|
209
209
|
fun `offline time does not burn the first-connect budget`() {
|
|
@@ -10,9 +10,55 @@ import java.util.Date
|
|
|
10
10
|
import org.junit.Assert.assertEquals
|
|
11
11
|
import org.junit.Assert.assertThrows
|
|
12
12
|
import org.junit.Test
|
|
13
|
+
import org.mockito.Mockito.mock
|
|
14
|
+
import org.mockito.Mockito.`when`
|
|
13
15
|
|
|
14
16
|
class AiaCertChaserTest {
|
|
15
17
|
|
|
18
|
+
/** A certificate whose AIA extension value is exactly [extensionValue]. */
|
|
19
|
+
private fun certWithAia(extensionValue: ByteArray): X509Certificate =
|
|
20
|
+
mock(X509Certificate::class.java).also {
|
|
21
|
+
`when`(it.getExtensionValue("1.3.6.1.5.5.7.1.1")).thenReturn(extensionValue)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Test(timeout = 5_000)
|
|
25
|
+
fun `a negative long-form length does not hang the parser`() {
|
|
26
|
+
// OCTET STRING { SEQUENCE { tag 0x00, long-form length 0xFFFFFFFA = -6 } }.
|
|
27
|
+
// The length's four bytes shift into a negative Int, so `pos += len` rewinds the
|
|
28
|
+
// reader by exactly the six bytes it just consumed: the tag is not a SEQUENCE, the
|
|
29
|
+
// loop `continue`s, and the reader is back where it started with more input to read.
|
|
30
|
+
val aia =
|
|
31
|
+
byteArrayOf(
|
|
32
|
+
0x04,
|
|
33
|
+
0x08,
|
|
34
|
+
0x30,
|
|
35
|
+
0x06,
|
|
36
|
+
0x00,
|
|
37
|
+
0x84.toByte(),
|
|
38
|
+
0xFF.toByte(),
|
|
39
|
+
0xFF.toByte(),
|
|
40
|
+
0xFF.toByte(),
|
|
41
|
+
0xFA.toByte(),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test(timeout = 5_000)
|
|
48
|
+
fun `a length running past the buffer is treated as no AIA`() {
|
|
49
|
+
// SEQUENCE claiming 0x7F content bytes inside a 2-byte buffer.
|
|
50
|
+
val aia = byteArrayOf(0x04, 0x04, 0x30, 0x02, 0x30, 0x7F)
|
|
51
|
+
|
|
52
|
+
assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test(timeout = 5_000)
|
|
56
|
+
fun `truncated DER is treated as no AIA`() {
|
|
57
|
+
val aia = byteArrayOf(0x04, 0x08, 0x30)
|
|
58
|
+
|
|
59
|
+
assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
|
|
60
|
+
}
|
|
61
|
+
|
|
16
62
|
@Test
|
|
17
63
|
fun `extracts CA Issuers URL from real leaf certificate AIA extension`() {
|
|
18
64
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
package com.audiobrowser.tls
|
|
2
2
|
|
|
3
|
+
import java.net.Socket
|
|
3
4
|
import java.security.cert.CertificateException
|
|
4
5
|
import java.security.cert.X509Certificate
|
|
6
|
+
import javax.net.ssl.SSLEngine
|
|
7
|
+
import javax.net.ssl.X509ExtendedTrustManager
|
|
5
8
|
import javax.net.ssl.X509TrustManager
|
|
6
9
|
import org.junit.Assert.assertEquals
|
|
7
10
|
import org.junit.Assert.assertThrows
|
|
@@ -70,4 +73,114 @@ class AiaChasingTrustManagerTest {
|
|
|
70
73
|
assertEquals(0, fetches)
|
|
71
74
|
assertEquals(listOf(1), delegate.checkedChainSizes)
|
|
72
75
|
}
|
|
76
|
+
|
|
77
|
+
// -- the API 24+ variant, which is what the platform actually calls into --
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stands in for Android's `RootTrustManager` under a per-domain network security config: the
|
|
81
|
+
* hostname-aware overloads work, and the two-argument one refuses to answer at all.
|
|
82
|
+
*/
|
|
83
|
+
private class FakeDomainConfigDelegate(private val trustsChainOfSize: Int) :
|
|
84
|
+
X509ExtendedTrustManager() {
|
|
85
|
+
val hostnameAwareChecks = mutableListOf<Int>()
|
|
86
|
+
|
|
87
|
+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
88
|
+
throw CertificateException(
|
|
89
|
+
"Domain specific configurations require that hostname aware " +
|
|
90
|
+
"checkServerTrusted(X509Certificate[], String, String) is used"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
override fun checkServerTrusted(
|
|
94
|
+
chain: Array<X509Certificate>,
|
|
95
|
+
authType: String,
|
|
96
|
+
socket: Socket?,
|
|
97
|
+
) {
|
|
98
|
+
hostnameAwareChecks.add(chain.size)
|
|
99
|
+
if (chain.size != trustsChainOfSize) {
|
|
100
|
+
throw CertificateException("Trust anchor for certification path not found.")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun checkServerTrusted(
|
|
105
|
+
chain: Array<X509Certificate>,
|
|
106
|
+
authType: String,
|
|
107
|
+
engine: SSLEngine?,
|
|
108
|
+
) = checkServerTrusted(chain, authType, null as Socket?)
|
|
109
|
+
|
|
110
|
+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
|
111
|
+
|
|
112
|
+
override fun checkClientTrusted(
|
|
113
|
+
chain: Array<X509Certificate>,
|
|
114
|
+
authType: String,
|
|
115
|
+
socket: Socket?,
|
|
116
|
+
) = Unit
|
|
117
|
+
|
|
118
|
+
override fun checkClientTrusted(
|
|
119
|
+
chain: Array<X509Certificate>,
|
|
120
|
+
authType: String,
|
|
121
|
+
engine: SSLEngine?,
|
|
122
|
+
) = Unit
|
|
123
|
+
|
|
124
|
+
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun `forwards to the hostname-aware overload the platform requires`() {
|
|
129
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
130
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
131
|
+
val delegate = FakeDomainConfigDelegate(trustsChainOfSize = 2)
|
|
132
|
+
val tm =
|
|
133
|
+
AiaChasingExtendedTrustManager(delegate) {
|
|
134
|
+
if (it == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The socket overload must reach the delegate's socket overload. Routing it through the
|
|
138
|
+
// two-argument form instead would throw the domain-config refusal, which is what a plain
|
|
139
|
+
// X509TrustManager wrapper causes for every connection in the process.
|
|
140
|
+
tm.checkServerTrusted(arrayOf(leaf), "RSA", null as Socket?)
|
|
141
|
+
|
|
142
|
+
assertEquals(listOf(1, 2), delegate.hostnameAwareChecks)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Test
|
|
146
|
+
fun `the engine overload is hostname-aware too`() {
|
|
147
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
148
|
+
val delegate = FakeDomainConfigDelegate(trustsChainOfSize = 1)
|
|
149
|
+
|
|
150
|
+
AiaChasingExtendedTrustManager(delegate) { emptyList() }
|
|
151
|
+
.checkServerTrusted(arrayOf(leaf), "RSA", null as SSLEngine?)
|
|
152
|
+
|
|
153
|
+
assertEquals(listOf(1), delegate.hostnameAwareChecks)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Test
|
|
157
|
+
fun `a delegate without extended overloads still works`() {
|
|
158
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
159
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
160
|
+
val delegate = FakeDelegate(trustsChainOfSize = 2)
|
|
161
|
+
val tm =
|
|
162
|
+
AiaChasingExtendedTrustManager(delegate) {
|
|
163
|
+
if (it == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
tm.checkServerTrusted(arrayOf(leaf), "RSA", null as Socket?)
|
|
167
|
+
|
|
168
|
+
assertEquals(listOf(1, 2), delegate.checkedChainSizes)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Test
|
|
172
|
+
fun `the extended variant does not weaken trust`() {
|
|
173
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
174
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
175
|
+
// Trusts nothing, whatever the chain.
|
|
176
|
+
val delegate = FakeDomainConfigDelegate(trustsChainOfSize = -1)
|
|
177
|
+
val tm = AiaChasingExtendedTrustManager(delegate) { listOf(r13) }
|
|
178
|
+
|
|
179
|
+
assertThrows(CertificateException::class.java) {
|
|
180
|
+
tm.checkServerTrusted(arrayOf(leaf), "RSA", null as Socket?)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The completed chain was offered to the delegate and refused, rather than assumed good.
|
|
184
|
+
assertEquals(listOf(1, 2), delegate.hostnameAwareChecks)
|
|
185
|
+
}
|
|
73
186
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.audiobrowser.tls
|
|
2
2
|
|
|
3
|
+
import java.io.ByteArrayInputStream
|
|
4
|
+
import java.io.InputStream
|
|
3
5
|
import org.junit.Assert.assertArrayEquals
|
|
4
6
|
import org.junit.Assert.assertEquals
|
|
5
7
|
import org.junit.Assert.assertNull
|
|
@@ -72,4 +74,85 @@ class CachingCertificateFetcherTest {
|
|
|
72
74
|
CachingCertificateFetcher.parseHttpResponse("garbage without a header break".toByteArray())
|
|
73
75
|
)
|
|
74
76
|
}
|
|
77
|
+
|
|
78
|
+
// -- response cap (the AIA URL, so the host serving it, comes from the presented cert) --
|
|
79
|
+
|
|
80
|
+
// -- URL validation (the AIA URL is attacker-supplied text from the presented cert) --
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
fun `accepts the URL form CAs actually serve`() {
|
|
84
|
+
assertTrue(CachingCertificateFetcher.isSafeUrl("http://r13.i.lencr.org/"))
|
|
85
|
+
assertTrue(CachingCertificateFetcher.isSafeUrl("http://ca.example:8080/issuer.crt?v=2"))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
fun `rejects a URL carrying CRLF, which would inject request lines`() {
|
|
90
|
+
// java.net.URL keeps the CRLF in `file`, and the cleartext fetch writes `file` straight
|
|
91
|
+
// into "GET $path HTTP/1.0\r\n" on a raw socket.
|
|
92
|
+
assertTrue(!CachingCertificateFetcher.isSafeUrl("http://127.0.0.1:6379/\r\nSET foo bar\r\n"))
|
|
93
|
+
assertTrue(!CachingCertificateFetcher.isSafeUrl("http://ca.example/a\nX-Injected: 1"))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun `rejects a URL carrying a space, which would split the request line`() {
|
|
98
|
+
assertTrue(!CachingCertificateFetcher.isSafeUrl("http://ca.example/a b"))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Test
|
|
102
|
+
fun `reads a response that fits within the cap`() {
|
|
103
|
+
val body = ByteArray(64) { it.toByte() }
|
|
104
|
+
|
|
105
|
+
assertArrayEquals(
|
|
106
|
+
body,
|
|
107
|
+
CachingCertificateFetcher.readCapped(ByteArrayInputStream(body), limit = 128),
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Test
|
|
112
|
+
fun `returns null for a response that exceeds the cap`() {
|
|
113
|
+
val body = ByteArray(129)
|
|
114
|
+
|
|
115
|
+
assertNull(CachingCertificateFetcher.readCapped(ByteArrayInputStream(body), limit = 128))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
fun `a stream that never ends does not read without bound`() {
|
|
120
|
+
// Endless: read() always fills the buffer and never signals EOF, the shape a slow-drip
|
|
121
|
+
// server takes — the per-read timeout never fires because every read succeeds.
|
|
122
|
+
val endless =
|
|
123
|
+
object : InputStream() {
|
|
124
|
+
override fun read() = 0
|
|
125
|
+
|
|
126
|
+
override fun read(b: ByteArray, off: Int, len: Int): Int = len
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
assertNull(CachingCertificateFetcher.readCapped(endless, limit = 1 shl 16))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test
|
|
133
|
+
fun `a slow drip is cut off by the deadline, not left to exhaust the byte cap`() {
|
|
134
|
+
// One byte per read, each one landing just inside the socket's read timeout: the byte cap
|
|
135
|
+
// alone would let this run for a million reads. The clock advances 1s per read, so the
|
|
136
|
+
// 10s budget must end it after ~10 — far short of the 1 MiB cap.
|
|
137
|
+
var reads = 0
|
|
138
|
+
val drip =
|
|
139
|
+
object : InputStream() {
|
|
140
|
+
override fun read() = 0
|
|
141
|
+
|
|
142
|
+
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
|
143
|
+
reads++
|
|
144
|
+
return 1
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
var nanos = 0L
|
|
148
|
+
|
|
149
|
+
val result =
|
|
150
|
+
CachingCertificateFetcher.readCapped(drip, budgetMs = 10_000) {
|
|
151
|
+
nanos += 1_000_000_000
|
|
152
|
+
nanos
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
assertNull(result)
|
|
156
|
+
assertTrue("gave up after $reads reads", reads < 20)
|
|
157
|
+
}
|
|
75
158
|
}
|
|
@@ -64,9 +64,6 @@ final class BrowserManager {
|
|
|
64
64
|
private let router = SimpleRouter()
|
|
65
65
|
private let httpClient = HttpClient()
|
|
66
66
|
|
|
67
|
-
// LRU cache for individual tracks - keyed by both url and src for O(1) lookup
|
|
68
|
-
private let trackCache = LRUCache<String, Track>(maxSize: 3000)
|
|
69
|
-
|
|
70
67
|
// LRU cache for resolved content - keyed by path
|
|
71
68
|
private let contentCache = LRUCache<String, ResolvedTrack>(maxSize: 20)
|
|
72
69
|
|
|
@@ -223,22 +220,6 @@ final class BrowserManager {
|
|
|
223
220
|
return resolvedTrack.copying(children: hydratedChildren)
|
|
224
221
|
}
|
|
225
222
|
|
|
226
|
-
// MARK: - Track Cache
|
|
227
|
-
|
|
228
|
-
/// Cache a track by both url and src for O(1) lookup from either key.
|
|
229
|
-
private func cacheTrack(_ track: Track) {
|
|
230
|
-
if let url = track.url {
|
|
231
|
-
trackCache.set(url, value: track)
|
|
232
|
-
}
|
|
233
|
-
if let src = track.src {
|
|
234
|
-
trackCache.set(src, value: track)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private func cacheChildren(_ resolvedTrack: ResolvedTrack) {
|
|
239
|
-
resolvedTrack.children?.forEach { cacheTrack($0) }
|
|
240
|
-
}
|
|
241
|
-
|
|
242
223
|
// MARK: - Navigation
|
|
243
224
|
|
|
244
225
|
/// Main navigation method - updates path and resolves content.
|
|
@@ -276,9 +257,6 @@ final class BrowserManager {
|
|
|
276
257
|
// Cache the resolved content for future navigation
|
|
277
258
|
contentCache.set(normalizedPath, value: resolvedTrack)
|
|
278
259
|
|
|
279
|
-
// Cache children for track lookups
|
|
280
|
-
cacheChildren(resolvedTrack)
|
|
281
|
-
|
|
282
260
|
return hydrateChildren(resolvedTrack)
|
|
283
261
|
}
|
|
284
262
|
|
|
@@ -682,9 +660,6 @@ final class BrowserManager {
|
|
|
682
660
|
lastSearchQuery = query
|
|
683
661
|
lastSearchResults = hydratedResults
|
|
684
662
|
|
|
685
|
-
// Cache individual tracks
|
|
686
|
-
hydratedResults.forEach { cacheTrack($0) }
|
|
687
|
-
|
|
688
663
|
return makeSearchResult(query: query, results: hydratedResults)
|
|
689
664
|
}
|
|
690
665
|
|
|
@@ -770,9 +745,9 @@ final class BrowserManager {
|
|
|
770
745
|
// silently queueing the changed list would resume the wrong station.
|
|
771
746
|
let sectionTracks: [Track]
|
|
772
747
|
switch SectionScope.section(of: children, containing: trackId) {
|
|
773
|
-
case .imageRow(
|
|
748
|
+
case let .imageRow(items):
|
|
774
749
|
sectionTracks = items.map { $0.toTrack() }
|
|
775
|
-
case .run(
|
|
750
|
+
case let .run(tracks):
|
|
776
751
|
sectionTracks = tracks
|
|
777
752
|
case nil:
|
|
778
753
|
return nil
|
|
@@ -20,7 +20,7 @@ enum SearchDrillIn {
|
|
|
20
20
|
/// yields nothing. Returns nil when nothing is playable.
|
|
21
21
|
static func playable<Item: PlayableSearchItem>(
|
|
22
22
|
from children: [Item],
|
|
23
|
-
resolveChildren: sending (String) async throws -> [Item]
|
|
23
|
+
resolveChildren: sending (String) async throws -> [Item],
|
|
24
24
|
) async throws -> [Item]? {
|
|
25
25
|
guard !children.isEmpty else { return nil }
|
|
26
26
|
|