Files
astra-mobile/patches/react-native-track-player+4.1.2.patch
2026-07-29 19:25:54 -04:00

570 lines
21 KiB
Diff

diff --git a/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt b/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
index b2409a0..119caaf 100644
--- a/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
+++ b/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
@@ -18,9 +18,11 @@ import com.doublesymmetry.trackplayer.utils.RejectionException
import com.facebook.react.bridge.*
import com.google.android.exoplayer2.DefaultLoadControl.*
import com.google.android.exoplayer2.Player
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.*
import javax.annotation.Nonnull
@@ -169,8 +171,12 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
return
}
+ val bundledData = Arguments.toBundle(data)
+ val androidOptions = bundledData?.getBundle(MusicService.ANDROID_OPTIONS_KEY)
+ val allowBackgroundSetup = androidOptions?.getBoolean("allowBackgroundSetup") ?: false
+
// prevent crash Fatal Exception: android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && AppForegroundTracker.backgrounded) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && AppForegroundTracker.backgrounded && !allowBackgroundSetup) {
promise.reject(
"android_cannot_setup_player_in_background",
"On Android the app must be in the foreground when setting up the player."
@@ -179,7 +185,6 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
// Validate buffer keys.
- val bundledData = Arguments.toBundle(data)
val minBuffer =
bundledData?.getDouble(MusicService.MIN_BUFFER_KEY)?.toMilliseconds()?.toInt()
?: DEFAULT_MIN_BUFFER_MS
@@ -251,7 +256,7 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun updateOptions(data: ReadableMap?, callback: Promise) = scope.launch {
+ fun updateOptions(data: ReadableMap?, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
val options = Arguments.toBundle(data)
@@ -262,19 +267,24 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
callback.resolve(null)
}
+ }
@ReactMethod
- fun add(data: ReadableArray?, insertBeforeIndex: Int, callback: Promise) = scope.launch {
+ fun add(data: ReadableArray?, insertBeforeIndex: Int, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
try {
- val tracks = readableArrayToTrackList(data);
+ // Track conversion is O(queue) work (Bundle parsing, Uri resolution) —
+ // keep it off the main thread so long queues don't freeze the UI/ANR.
+ val tracks = withContext(Dispatchers.Default) {
+ readableArrayToTrackList(data).map { it.toAudioItem() }
+ };
if (insertBeforeIndex < -1 || insertBeforeIndex > musicService.tracks.size) {
callback.reject("index_out_of_bounds", "The track index is out of bounds")
return@launch
}
val index = if (insertBeforeIndex == -1) musicService.tracks.size else insertBeforeIndex
- musicService.add(
+ musicService.addPrepared(
tracks,
index
)
@@ -283,9 +293,10 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
rejectWithException(callback, exception)
}
}
+ }
@ReactMethod
- fun load(data: ReadableMap?, callback: Promise) = scope.launch {
+ fun load(data: ReadableMap?, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
if (data == null) {
callback.resolve(null)
@@ -299,16 +310,18 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
callback.reject("invalid_track_object", "Track was not a dictionary type")
}
}
+ }
@ReactMethod
- fun move(fromIndex: Int, toIndex: Int, callback: Promise) = scope.launch {
+ fun move(fromIndex: Int, toIndex: Int, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.move(fromIndex, toIndex)
callback.resolve(null)
}
+ }
@ReactMethod
- fun remove(data: ReadableArray?, callback: Promise) = scope.launch {
+ fun remove(data: ReadableArray?, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
val inputIndexes = Arguments.toList(data)
if (inputIndexes != null) {
@@ -329,9 +342,10 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
callback.resolve(null)
}
+ }
@ReactMethod
- fun updateMetadataForTrack(index: Int, map: ReadableMap?, callback: Promise) =
+ fun updateMetadataForTrack(index: Int, map: ReadableMap?, callback: Promise) {
scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
@@ -346,9 +360,10 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
callback.resolve(null)
}
}
+ }
@ReactMethod
- fun updateNowPlayingMetadata(map: ReadableMap?, callback: Promise) = scope.launch {
+ fun updateNowPlayingMetadata(map: ReadableMap?, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
if (musicService.tracks.isEmpty())
@@ -362,9 +377,10 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
callback.resolve(null)
}
+ }
@ReactMethod
- fun clearNowPlayingMetadata(callback: Promise) = scope.launch {
+ fun clearNowPlayingMetadata(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
if (musicService.tracks.isEmpty())
@@ -373,17 +389,19 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
musicService.clearNotificationMetadata()
callback.resolve(null)
}
+ }
@ReactMethod
- fun removeUpcomingTracks(callback: Promise) = scope.launch {
+ fun removeUpcomingTracks(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.removeUpcomingTracks()
callback.resolve(null)
}
+ }
@ReactMethod
- fun skip(index: Int, initialTime: Float, callback: Promise) = scope.launch {
+ fun skip(index: Int, initialTime: Float, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.skip(index)
@@ -394,9 +412,10 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
callback.resolve(null)
}
+ }
@ReactMethod
- fun skipToNext(initialTime: Float, callback: Promise) = scope.launch {
+ fun skipToNext(initialTime: Float, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.skipToNext()
@@ -407,9 +426,10 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
callback.resolve(null)
}
+ }
@ReactMethod
- fun skipToPrevious(initialTime: Float, callback: Promise) = scope.launch {
+ fun skipToPrevious(initialTime: Float, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.skipToPrevious()
@@ -420,9 +440,10 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
callback.resolve(null)
}
+ }
@ReactMethod
- fun reset(callback: Promise) = scope.launch {
+ fun reset(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.stop()
@@ -431,188 +452,224 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
callback.resolve(null)
}
+ }
@ReactMethod
- fun play(callback: Promise) = scope.launch {
+ fun play(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.play()
callback.resolve(null)
}
+ }
@ReactMethod
- fun pause(callback: Promise) = scope.launch {
+ fun pause(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.pause()
callback.resolve(null)
}
+ }
+
+ @ReactMethod
+ fun setPauseAtEndOfItem(enabled: Boolean, callback: Promise) { scope.launch {
+ if (verifyServiceBoundOrReject(callback)) return@launch
+
+ musicService.setPauseAtEndOfItem(enabled)
+ callback.resolve(null)
+ }
+ }
@ReactMethod
- fun stop(callback: Promise) = scope.launch {
+ fun stop(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.stop()
callback.resolve(null)
}
+ }
@ReactMethod
- fun seekTo(seconds: Float, callback: Promise) = scope.launch {
+ fun seekTo(seconds: Float, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.seekTo(seconds)
callback.resolve(null)
}
+ }
@ReactMethod
- fun seekBy(offset: Float, callback: Promise) = scope.launch {
+ fun seekBy(offset: Float, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.seekBy(offset)
callback.resolve(null)
}
+ }
@ReactMethod
- fun retry(callback: Promise) = scope.launch {
+ fun retry(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.retry()
callback.resolve(null)
}
+ }
@ReactMethod
- fun setVolume(volume: Float, callback: Promise) = scope.launch {
+ fun setVolume(volume: Float, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.setVolume(volume)
callback.resolve(null)
}
+ }
@ReactMethod
- fun getVolume(callback: Promise) = scope.launch {
+ fun getVolume(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(musicService.getVolume())
}
+ }
@ReactMethod
- fun setRate(rate: Float, callback: Promise) = scope.launch {
+ fun setRate(rate: Float, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.setRate(rate)
callback.resolve(null)
}
+ }
@ReactMethod
- fun getRate(callback: Promise) = scope.launch {
+ fun getRate(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(musicService.getRate())
}
+ }
@ReactMethod
- fun setRepeatMode(mode: Int, callback: Promise) = scope.launch {
+ fun setRepeatMode(mode: Int, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.setRepeatMode(RepeatMode.fromOrdinal(mode))
callback.resolve(null)
}
+ }
@ReactMethod
- fun getRepeatMode(callback: Promise) = scope.launch {
+ fun getRepeatMode(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(musicService.getRepeatMode().ordinal)
}
+ }
@ReactMethod
- fun setPlayWhenReady(playWhenReady: Boolean, callback: Promise) = scope.launch {
+ fun setPlayWhenReady(playWhenReady: Boolean, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
musicService.playWhenReady = playWhenReady
callback.resolve(null)
}
+ }
@ReactMethod
- fun getPlayWhenReady(callback: Promise) = scope.launch {
+ fun getPlayWhenReady(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(musicService.playWhenReady)
}
+ }
@ReactMethod
- fun getTrack(index: Int, callback: Promise) = scope.launch {
+ fun getTrack(index: Int, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
if (index >= 0 && index < musicService.tracks.size) {
- callback.resolve(Arguments.fromBundle(musicService.tracks[index].originalItem))
+ callback.resolve(musicService.tracks[index].originalItem?.let { Arguments.fromBundle(it) })
} else {
callback.resolve(null)
}
}
+ }
@ReactMethod
- fun getQueue(callback: Promise) = scope.launch {
+ fun getQueue(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
- callback.resolve(Arguments.fromList(musicService.tracks.map { it.originalItem }))
+ // Clone on main (the service mutates originalItem there), marshal off-main.
+ val bundles = musicService.tracks.map { track -> track.originalItem?.let(::Bundle) }
+ callback.resolve(withContext(Dispatchers.Default) { Arguments.fromList(bundles) })
+ }
}
@ReactMethod
- fun setQueue(data: ReadableArray?, callback: Promise) = scope.launch {
+ fun setQueue(data: ReadableArray?, callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
try {
+ val tracks = withContext(Dispatchers.Default) {
+ readableArrayToTrackList(data).map { it.toAudioItem() }
+ }
musicService.clear()
- musicService.add(readableArrayToTrackList(data))
+ musicService.addPrepared(tracks, 0)
callback.resolve(null)
} catch (exception: Exception) {
rejectWithException(callback, exception)
}
}
+ }
@ReactMethod
- fun getActiveTrackIndex(callback: Promise) = scope.launch {
+ fun getActiveTrackIndex(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(
if (musicService.tracks.isEmpty()) null else musicService.getCurrentTrackIndex()
)
}
+ }
@ReactMethod
- fun getActiveTrack(callback: Promise) = scope.launch {
+ fun getActiveTrack(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(
if (musicService.tracks.isEmpty()) null
- else Arguments.fromBundle(
- musicService.tracks[musicService.getCurrentTrackIndex()].originalItem
- )
+ else musicService.tracks[musicService.getCurrentTrackIndex()].originalItem
+ ?.let { Arguments.fromBundle(it) }
)
}
+ }
@ReactMethod
- fun getDuration(callback: Promise) = scope.launch {
+ fun getDuration(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(musicService.getDurationInSeconds())
}
+ }
@ReactMethod
- fun getBufferedPosition(callback: Promise) = scope.launch {
+ fun getBufferedPosition(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(musicService.getBufferedPositionInSeconds())
}
+ }
@ReactMethod
- fun getPosition(callback: Promise) = scope.launch {
+ fun getPosition(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(musicService.getPositionInSeconds())
}
+ }
@ReactMethod
- fun getProgress(callback: Promise) = scope.launch {
+ fun getProgress(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
var bundle = Bundle()
bundle.putDouble("duration", musicService.getDurationInSeconds());
@@ -620,10 +677,12 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
bundle.putDouble("buffered", musicService.getBufferedPositionInSeconds());
callback.resolve(Arguments.fromBundle(bundle))
}
+ }
@ReactMethod
- fun getPlaybackState(callback: Promise) = scope.launch {
+ fun getPlaybackState(callback: Promise) { scope.launch {
if (verifyServiceBoundOrReject(callback)) return@launch
callback.resolve(Arguments.fromBundle(musicService.getPlayerStateBundle(musicService.state)))
}
+ }
}
diff --git a/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt b/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
index afa6b0f..87af67c 100644
--- a/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
+++ b/node_modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
@@ -9,6 +9,7 @@ import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
+import android.os.Trace
import android.support.v4.media.RatingCompat
import androidx.annotation.MainThread
import androidx.core.app.NotificationCompat
@@ -300,6 +301,18 @@ class MusicService : HeadlessJsTaskService() {
player.add(items, atIndex)
}
+ /**
+ * Track -> AudioItem construction is safe off the player looper and can be
+ * noticeable even for a bounded window. MusicModule prepares these on
+ * Dispatchers.Default; only the actual Media3 playlist mutation stays here.
+ */
+ @MainThread
+ fun addPrepared(items: List<TrackAudioItem>, atIndex: Int) {
+ tracePlaylistMutation("AstraQueue.rntpAdd") {
+ player.add(items, atIndex)
+ }
+ }
+
@MainThread
fun load(track: Track) {
player.load(track.toAudioItem())
@@ -307,7 +320,9 @@ class MusicService : HeadlessJsTaskService() {
@MainThread
fun move(fromIndex: Int, toIndex: Int) {
- player.move(fromIndex, toIndex);
+ tracePlaylistMutation("AstraQueue.rntpMove") {
+ player.move(fromIndex, toIndex);
+ }
}
@MainThread
@@ -317,12 +332,25 @@ class MusicService : HeadlessJsTaskService() {
@MainThread
fun remove(indexes: List<Int>) {
- player.remove(indexes)
+ tracePlaylistMutation("AstraQueue.rntpRemove") {
+ player.remove(indexes)
+ }
}
@MainThread
fun clear() {
- player.clear()
+ tracePlaylistMutation("AstraQueue.rntpClear") {
+ player.clear()
+ }
+ }
+
+ private inline fun <T> tracePlaylistMutation(name: String, block: () -> T): T {
+ Trace.beginSection(name)
+ return try {
+ block()
+ } finally {
+ Trace.endSection()
+ }
}
@MainThread
@@ -335,6 +363,10 @@ class MusicService : HeadlessJsTaskService() {
player.pause()
}
+ fun setPauseAtEndOfItem(enabled: Boolean) {
+ player.setPauseAtEndOfItem(enabled)
+ }
+
@MainThread
fun stop() {
player.stop()
@@ -741,7 +773,7 @@ class MusicService : HeadlessJsTaskService() {
@MainThread
private fun emit(event: String, data: Bundle? = null) {
- reactNativeHost.reactInstanceManager.currentReactContext
+ (applicationContext as? com.facebook.react.ReactApplication)?.reactHost?.currentReactContext
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
?.emit(event, data?.let { Arguments.fromBundle(it) })
}
@@ -751,7 +783,7 @@ class MusicService : HeadlessJsTaskService() {
val payload = Arguments.createArray()
data.forEach { payload.pushMap(Arguments.fromBundle(it)) }
- reactNativeHost.reactInstanceManager.currentReactContext
+ (applicationContext as? com.facebook.react.ReactApplication)?.reactHost?.currentReactContext
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
?.emit(event, payload)
}