@javascriptcommon/react-native-track-player 1.2.9 → 1.2.24
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/android/build.gradle +61 -4
- package/android/src/main/AndroidManifest.xml +2 -4
- package/android/src/main/ic_home-playstore.png +0 -0
- package/android/src/main/ic_repeat-playstore.png +0 -0
- package/android/src/main/ic_repeat_50-playstore.png +0 -0
- package/android/src/main/ic_shuffle-playstore.png +0 -0
- package/android/src/main/ic_shuffle_50-playstore.png +0 -0
- package/android/src/main/ic_shuffle_sm-playstore.png +0 -0
- package/android/src/main/ic_stop-playstore.png +0 -0
- package/android/src/main/ic_test-playstore.png +0 -0
- package/android/src/main/java/com/guichaguri/trackplayer/{service/HeadlessJsMediaService.java → HeadlessJsMediaService.java} +83 -32
- package/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.kt +25 -0
- package/android/src/main/java/com/guichaguri/trackplayer/extensions/AudioPlayerStateExt.kt +19 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/event/EventHolder.kt +30 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/event/NotificationEventHolder.kt +20 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/event/PlayerEventHolder.kt +111 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AAMediaSessionCallback.kt +10 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioContentType.kt +10 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioItem.kt +66 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioItemTransitionReason.kt +33 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioPlayerState.kt +30 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/BufferConfig.kt +8 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/CacheConfig.kt +17 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/Capability.kt +19 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/FocusChangeData.kt +3 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/MediaSessionCallback.kt +17 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/NotificationConfig.kt +43 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/NotificationMetadata.kt +8 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/NotificationState.kt +8 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlayWhenReadyChangeData.kt +5 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlaybackEndedReason.kt +5 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlaybackError.kt +6 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlaybackMetadata.kt +200 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlayerConfig.kt +33 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlayerOptions.kt +9 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PositionChangedReason.kt +39 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/QueuedPlayerOptions.kt +49 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/notification/NotificationManager.kt +678 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/AudioPlayer.kt +10 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/BaseAudioPlayer.kt +864 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/QueuedAudioPlayer.kt +269 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/components/MediaSourceExt.kt +35 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/components/PlayerCache.kt +26 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/utils/Utils.kt +12 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/MetadataAdapter.kt +224 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/State.kt +13 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/Track.kt +120 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/TrackAudioItem.kt +19 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/TrackType.kt +11 -0
- package/android/src/main/java/com/guichaguri/trackplayer/module/AutoConnectionDetector.kt +151 -0
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.kt +66 -0
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.kt +1192 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/BundleUtils.kt +117 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.kt +31 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.kt +347 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicService.kt +1268 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/Utils.kt +228 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.kt +141 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.kt +396 -0
- package/android/src/main/res/drawable-hdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-mdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-xhdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-xxhdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-xxxhdpi/ic_home.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/raw/silent_5_seconds.mp3 +0 -0
- package/android/src/main/res/strings.xml +6 -0
- package/android/src/main/res/values/strings.xml +6 -0
- package/index.d.ts +62 -1
- package/lib/index.js +10 -9
- package/package.json +1 -1
- package/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.java +0 -28
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.java +0 -55
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.java +0 -298
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.java +0 -47
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.java +0 -383
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicService.java +0 -271
- package/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java +0 -243
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.java +0 -148
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.java +0 -379
- package/android/src/main/java/com/guichaguri/trackplayer/service/models/Track.java +0 -141
- package/android/src/main/java/com/guichaguri/trackplayer/service/models/TrackType.java +0 -35
- package/android/src/main/res/drawable-hdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-mdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-xhdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-xxhdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-xxxhdpi/ic_logo.png +0 -0
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
package com.guichaguri.trackplayer.service
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.ActivityManager
|
|
5
|
+
import android.app.ForegroundServiceStartNotAllowedException
|
|
6
|
+
import android.app.Notification
|
|
7
|
+
import android.app.NotificationChannel
|
|
8
|
+
import android.app.NotificationManager
|
|
9
|
+
import android.app.PendingIntent
|
|
10
|
+
import android.content.Context
|
|
11
|
+
import android.content.Intent
|
|
12
|
+
import android.content.pm.ServiceInfo
|
|
13
|
+
import android.os.Build
|
|
14
|
+
import android.os.Bundle
|
|
15
|
+
import android.os.Handler
|
|
16
|
+
import android.os.IBinder
|
|
17
|
+
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
|
18
|
+
import android.support.v4.media.RatingCompat
|
|
19
|
+
import android.support.v4.media.session.MediaSessionCompat
|
|
20
|
+
import android.support.v4.media.session.PlaybackStateCompat
|
|
21
|
+
import android.view.KeyEvent
|
|
22
|
+
import android.view.KeyEvent.KEYCODE_MEDIA_STOP
|
|
23
|
+
import androidx.annotation.MainThread
|
|
24
|
+
import androidx.core.app.NotificationCompat
|
|
25
|
+
import androidx.media.session.MediaButtonReceiver
|
|
26
|
+
import androidx.media.utils.MediaConstants
|
|
27
|
+
import com.facebook.react.ReactHost
|
|
28
|
+
import com.facebook.react.ReactInstanceEventListener
|
|
29
|
+
import com.facebook.react.ReactInstanceManager
|
|
30
|
+
import com.facebook.react.bridge.Arguments
|
|
31
|
+
import com.facebook.react.bridge.ReactContext
|
|
32
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
33
|
+
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.bridgelessEnabled
|
|
34
|
+
import com.facebook.react.jstasks.HeadlessJsTaskConfig
|
|
35
|
+
import com.facebook.react.modules.appregistry.AppRegistry
|
|
36
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
37
|
+
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
|
|
38
|
+
import com.google.android.exoplayer2.source.MediaSource
|
|
39
|
+
import com.guichaguri.trackplayer.HeadlessJsMediaService
|
|
40
|
+
import com.guichaguri.trackplayer.R
|
|
41
|
+
import com.guichaguri.trackplayer.extensions.asLibState
|
|
42
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.AAMediaSessionCallBack
|
|
43
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.AudioContentType
|
|
44
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.AudioItemTransitionReason
|
|
45
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.AudioPlayerState
|
|
46
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.BufferConfig
|
|
47
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.CacheConfig
|
|
48
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.Capability
|
|
49
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.MediaSessionCallback
|
|
50
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.NotificationButton
|
|
51
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.NotificationButton.CUSTOM_ACTION
|
|
52
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.NotificationButton.NEXT
|
|
53
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.NotificationButton.PLAY_PAUSE
|
|
54
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.NotificationButton.PREVIOUS
|
|
55
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.NotificationButton.SEEK_TO
|
|
56
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.NotificationButton.STOP
|
|
57
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.NotificationConfig
|
|
58
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.NotificationState
|
|
59
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.PlayerConfig
|
|
60
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.RepeatMode
|
|
61
|
+
import com.guichaguri.trackplayer.kotlinaudio.players.QueuedAudioPlayer
|
|
62
|
+
import com.guichaguri.trackplayer.model.Track
|
|
63
|
+
import com.guichaguri.trackplayer.model.TrackAudioItem
|
|
64
|
+
import com.guichaguri.trackplayer.module.AutoConnectionDetector
|
|
65
|
+
import com.guichaguri.trackplayer.module.MusicEvents
|
|
66
|
+
import com.guichaguri.trackplayer.module.MusicModule
|
|
67
|
+
import com.guichaguri.trackplayer.module.MusicModule.Companion.autoConnectionDetector
|
|
68
|
+
import com.guichaguri.trackplayer.service.Utils.setRating
|
|
69
|
+
import kotlinx.coroutines.Dispatchers
|
|
70
|
+
import kotlinx.coroutines.Job
|
|
71
|
+
import kotlinx.coroutines.MainScope
|
|
72
|
+
import kotlinx.coroutines.delay
|
|
73
|
+
import kotlinx.coroutines.flow.flow
|
|
74
|
+
import kotlinx.coroutines.launch
|
|
75
|
+
import kotlinx.coroutines.withContext
|
|
76
|
+
import java.util.concurrent.TimeUnit
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @author Guichaguri
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
class MusicService : HeadlessJsMediaService() {
|
|
83
|
+
private var player: QueuedAudioPlayer? = null
|
|
84
|
+
var manager: MusicManager? = null
|
|
85
|
+
var handler: Handler? = null
|
|
86
|
+
private var intentToStop = false
|
|
87
|
+
var mediaTree: MutableMap<String, MutableList<MediaItem>> = HashMap()
|
|
88
|
+
var toUpdateMediaItems: MutableMap<String, MutableList<MediaItem>> = HashMap()
|
|
89
|
+
var loadingChildrenParentMediaId: String? = null
|
|
90
|
+
var searchQuery: String? = null
|
|
91
|
+
var searchResult: Result<MutableList<MediaItem>>? = null
|
|
92
|
+
|
|
93
|
+
private val scope = MainScope()
|
|
94
|
+
|
|
95
|
+
val tracks: List<Track>
|
|
96
|
+
get() = player?.items?.map { (it as TrackAudioItem).track } ?: emptyList()
|
|
97
|
+
|
|
98
|
+
val state
|
|
99
|
+
get() = player?.playerState
|
|
100
|
+
|
|
101
|
+
var ratingType: Int
|
|
102
|
+
get() = player?.ratingType ?: RatingCompat.RATING_NONE
|
|
103
|
+
set(value) {
|
|
104
|
+
player?.ratingType = value
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
val playbackError
|
|
108
|
+
get() = player?.playbackError
|
|
109
|
+
|
|
110
|
+
val event
|
|
111
|
+
get() = player?.event
|
|
112
|
+
|
|
113
|
+
var playWhenReady: Boolean
|
|
114
|
+
get() = player?.playWhenReady ?: false
|
|
115
|
+
set(value) {
|
|
116
|
+
player?.playWhenReady = value
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private var latestOptions: Bundle? = null
|
|
120
|
+
|
|
121
|
+
private var mediaSession: MediaSessionCompat? = null
|
|
122
|
+
private var stateBuilder: PlaybackStateCompat.Builder? = null
|
|
123
|
+
|
|
124
|
+
override fun getTaskConfig(intent: Intent): HeadlessJsTaskConfig {
|
|
125
|
+
return HeadlessJsTaskConfig("TrackPlayer", Arguments.createMap(), 0, true)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override fun onHeadlessJsTaskFinish(taskId: Int) {
|
|
129
|
+
// Overridden to prevent the service from being terminated
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@SuppressLint("VisibleForTests")
|
|
133
|
+
@MainThread
|
|
134
|
+
fun emit(event: String, data: Bundle? = null) {
|
|
135
|
+
val currentReactContext = if (bridgelessEnabled) reactHost.currentReactContext else reactNativeHost.reactInstanceManager.currentReactContext
|
|
136
|
+
currentReactContext
|
|
137
|
+
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
138
|
+
?.emit(event, data?.let { Arguments.fromBundle(it) })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fun destroy(intentToStop: Boolean) {
|
|
142
|
+
try {
|
|
143
|
+
if (handler != null) {
|
|
144
|
+
handler!!.removeMessages(0)
|
|
145
|
+
handler = null
|
|
146
|
+
}
|
|
147
|
+
if (manager != null) {
|
|
148
|
+
manager!!.destroy(intentToStop)
|
|
149
|
+
manager = null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (autoConnectionDetector?.isCarConnected != true) {
|
|
153
|
+
player?.destroy()
|
|
154
|
+
}
|
|
155
|
+
} catch (_: Exception) {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@SuppressLint("VisibleForTests")
|
|
159
|
+
private fun onStartForeground() {
|
|
160
|
+
var serviceForeground = false
|
|
161
|
+
if (manager != null) {
|
|
162
|
+
// The session is only active when the service is on foreground
|
|
163
|
+
serviceForeground = manager!!.metadata.session.isActive
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!serviceForeground) {
|
|
167
|
+
val currentReactContext = if (bridgelessEnabled) reactHost.currentReactContext else reactNativeHost.reactInstanceManager.currentReactContext
|
|
168
|
+
|
|
169
|
+
// Checks whether there is a React activity
|
|
170
|
+
if (currentReactContext == null || !currentReactContext.hasCurrentActivity()) {
|
|
171
|
+
val channel = Utils.getNotificationChannel(this as Context)
|
|
172
|
+
|
|
173
|
+
// Sets the service to foreground with an empty notification
|
|
174
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
175
|
+
startForeground(
|
|
176
|
+
EMPTY_NOTIFICATION_ID,
|
|
177
|
+
NotificationCompat.Builder(this, channel).build(),
|
|
178
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
|
179
|
+
)
|
|
180
|
+
} else {
|
|
181
|
+
startForeground(EMPTY_NOTIFICATION_ID, NotificationCompat.Builder(this, channel).build())
|
|
182
|
+
}
|
|
183
|
+
// Stops the service right after
|
|
184
|
+
stopSelf()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
override fun onBind(intent: Intent): IBinder? {
|
|
190
|
+
/* if(Utils.CONNECT_INTENT.equals(intent.getAction())) {
|
|
191
|
+
return new MusicBinder(this, manager);
|
|
192
|
+
}
|
|
193
|
+
return super.onBind(intent); */
|
|
194
|
+
|
|
195
|
+
return if (SERVICE_INTERFACE == intent.action) {
|
|
196
|
+
super.onBind(intent)
|
|
197
|
+
} else MusicBinder(this, manager!!)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fun invokeStartTask(reactContext: ReactContext, restart: Boolean = false) {
|
|
201
|
+
try {
|
|
202
|
+
val catalystInstance = reactContext.catalystInstance
|
|
203
|
+
val jsAppModuleName = "AndroidAuto"
|
|
204
|
+
val appParams = WritableNativeMap()
|
|
205
|
+
appParams.putDouble("rootTag", 1.0)
|
|
206
|
+
appParams.putBoolean("restart", restart)
|
|
207
|
+
val appProperties = Bundle.EMPTY
|
|
208
|
+
if (appProperties != null) {
|
|
209
|
+
appParams.putMap("initialProps", Arguments.fromBundle(appProperties))
|
|
210
|
+
}
|
|
211
|
+
val jsModule = catalystInstance.getJSModule(AppRegistry::class.java)
|
|
212
|
+
if (jsModule !== null) jsModule.runApplication(jsAppModuleName, appParams)
|
|
213
|
+
} catch (e: Exception) {
|
|
214
|
+
e.printStackTrace()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaItem>>) {
|
|
219
|
+
searchQuery = query
|
|
220
|
+
searchResult = result
|
|
221
|
+
result.detach()
|
|
222
|
+
emit(MusicEvents.SEARCH, Bundle().apply {
|
|
223
|
+
putString("query", query)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
override fun onGetRoot(
|
|
228
|
+
clientPackageName: String,
|
|
229
|
+
clientUid: Int,
|
|
230
|
+
rootHints: Bundle?
|
|
231
|
+
): BrowserRoot {
|
|
232
|
+
if (clientPackageName == "com.google.android.projection.gearhead") {
|
|
233
|
+
val currentReactContext = if (bridgelessEnabled) reactHost.currentReactContext else reactNativeHost.reactInstanceManager.currentReactContext
|
|
234
|
+
|
|
235
|
+
if (currentReactContext == null) {
|
|
236
|
+
if (bridgelessEnabled) { // new arch
|
|
237
|
+
val reactHost: ReactHost = reactHost
|
|
238
|
+
reactHost.addReactInstanceEventListener(
|
|
239
|
+
object : ReactInstanceEventListener {
|
|
240
|
+
override fun onReactContextInitialized(context: ReactContext) {
|
|
241
|
+
invokeStartTask(context)
|
|
242
|
+
reactHost.removeReactInstanceEventListener(this)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
reactHost.start()
|
|
247
|
+
} else { // old arch
|
|
248
|
+
val reactInstanceManager =
|
|
249
|
+
reactNativeHost.reactInstanceManager
|
|
250
|
+
reactInstanceManager.addReactInstanceEventListener(object :
|
|
251
|
+
@Suppress("DEPRECATION")
|
|
252
|
+
ReactInstanceManager.ReactInstanceEventListener {
|
|
253
|
+
override fun onReactContextInitialized(context: ReactContext) {
|
|
254
|
+
invokeStartTask(context)
|
|
255
|
+
reactInstanceManager.removeReactInstanceEventListener(this)
|
|
256
|
+
|
|
257
|
+
autoConnectionDetector?.isCarConnected = true
|
|
258
|
+
|
|
259
|
+
val params = Arguments.createMap()
|
|
260
|
+
params.putBoolean("connected", true)
|
|
261
|
+
|
|
262
|
+
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
263
|
+
?.emit(
|
|
264
|
+
"car-connection-update", params
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
reactInstanceManager.createReactContextInBackground()
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
if (MusicModule.isAppOpen) {
|
|
272
|
+
autoConnectionDetector?.isCarConnected = true
|
|
273
|
+
|
|
274
|
+
val params = Arguments.createMap()
|
|
275
|
+
params.putBoolean("connected", true)
|
|
276
|
+
|
|
277
|
+
currentReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(
|
|
278
|
+
"car-connection-update", params
|
|
279
|
+
)
|
|
280
|
+
} else {
|
|
281
|
+
if (bridgelessEnabled) { // new arch
|
|
282
|
+
val reactHost: ReactHost = reactHost
|
|
283
|
+
reactHost.destroy("track-player", null)
|
|
284
|
+
reactHost.addReactInstanceEventListener(object :
|
|
285
|
+
@Suppress("DEPRECATION")
|
|
286
|
+
ReactInstanceEventListener {
|
|
287
|
+
override fun onReactContextInitialized(context: ReactContext) {
|
|
288
|
+
invokeStartTask(context)
|
|
289
|
+
reactHost.removeReactInstanceEventListener(
|
|
290
|
+
this
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
autoConnectionDetector?.isCarConnected = true
|
|
294
|
+
|
|
295
|
+
val params = Arguments.createMap()
|
|
296
|
+
params.putBoolean("connected", true)
|
|
297
|
+
|
|
298
|
+
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
299
|
+
?.emit(
|
|
300
|
+
"car-connection-update", params
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
reactHost.start()
|
|
305
|
+
} else { // old arch
|
|
306
|
+
reactNativeHost.reactInstanceManager.destroy()
|
|
307
|
+
reactNativeHost.reactInstanceManager.addReactInstanceEventListener(object :
|
|
308
|
+
@Suppress("DEPRECATION")
|
|
309
|
+
ReactInstanceManager.ReactInstanceEventListener {
|
|
310
|
+
override fun onReactContextInitialized(context: ReactContext) {
|
|
311
|
+
invokeStartTask(context)
|
|
312
|
+
reactNativeHost.reactInstanceManager.removeReactInstanceEventListener(
|
|
313
|
+
this
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
autoConnectionDetector?.isCarConnected = true
|
|
317
|
+
|
|
318
|
+
val params = Arguments.createMap()
|
|
319
|
+
params.putBoolean("connected", true)
|
|
320
|
+
|
|
321
|
+
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
322
|
+
?.emit(
|
|
323
|
+
"car-connection-update", params
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
reactNativeHost.reactInstanceManager.createReactContextInBackground()
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
val extras = Bundle()
|
|
334
|
+
|
|
335
|
+
extras.putBoolean(
|
|
336
|
+
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
|
|
337
|
+
|
|
338
|
+
return BrowserRoot("/", extras)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@MainThread
|
|
342
|
+
fun add(track: Track) {
|
|
343
|
+
add(listOf(track))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@MainThread
|
|
347
|
+
fun add(tracks: List<Track>) {
|
|
348
|
+
val items = tracks.map {
|
|
349
|
+
val x = it
|
|
350
|
+
x.toAudioItem()
|
|
351
|
+
}
|
|
352
|
+
player?.add(items)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@MainThread
|
|
356
|
+
fun add(tracks: List<Track>, atIndex: Int) {
|
|
357
|
+
if (player != null && (atIndex <= (player?.getQueueSize() ?: -1))) {
|
|
358
|
+
val items = tracks.map {
|
|
359
|
+
val x = it
|
|
360
|
+
if (x.url.isEmpty()) {
|
|
361
|
+
val rawId = R.raw.silent_5_seconds
|
|
362
|
+
x.url = "android.resource://${applicationContext.packageName}/$rawId"
|
|
363
|
+
x.duration = 10
|
|
364
|
+
}
|
|
365
|
+
x.toAudioItem()
|
|
366
|
+
}
|
|
367
|
+
player?.add(items, atIndex)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@MainThread
|
|
372
|
+
fun load(track: Track) {
|
|
373
|
+
val audioItem = track.toAudioItem()
|
|
374
|
+
player?.load(audioItem)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@MainThread
|
|
378
|
+
fun move(fromIndex: Int, toIndex: Int) {
|
|
379
|
+
player?.move(fromIndex, toIndex);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@MainThread
|
|
383
|
+
fun remove(index: Int) {
|
|
384
|
+
remove(listOf(index))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@MainThread
|
|
388
|
+
fun remove(indexes: List<Int>) {
|
|
389
|
+
player?.remove(indexes)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
@MainThread
|
|
393
|
+
fun clear() {
|
|
394
|
+
player?.clear()
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@MainThread
|
|
398
|
+
fun play() {
|
|
399
|
+
MusicModule.firstPlayDone = true
|
|
400
|
+
player?.play()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@MainThread
|
|
404
|
+
fun pause() {
|
|
405
|
+
player?.pause()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
@MainThread
|
|
409
|
+
fun stop() {
|
|
410
|
+
player?.stop()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
@MainThread
|
|
414
|
+
fun removeUpcomingTracks() {
|
|
415
|
+
player?.removeUpcomingItems()
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@MainThread
|
|
419
|
+
fun removePreviousTracks() {
|
|
420
|
+
player?.removePreviousItems()
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
@MainThread
|
|
424
|
+
fun skip(index: Int) {
|
|
425
|
+
player?.jumpToItem(index)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@MainThread
|
|
429
|
+
fun skipToNext() {
|
|
430
|
+
player?.next()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
@MainThread
|
|
434
|
+
fun skipToPrevious() {
|
|
435
|
+
player?.previous()
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@MainThread
|
|
439
|
+
fun seekTo(seconds: Float) {
|
|
440
|
+
player?.seek((seconds * 1000).toLong(), TimeUnit.MILLISECONDS)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
@MainThread
|
|
444
|
+
fun seekBy(offset: Float) {
|
|
445
|
+
player?.seekBy((offset.toLong()), TimeUnit.SECONDS)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@MainThread
|
|
449
|
+
fun retry() {
|
|
450
|
+
player?.prepare()
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
@MainThread
|
|
454
|
+
fun getCurrentTrackIndex(): Int {
|
|
455
|
+
// return player?.currentIndex ?: 0
|
|
456
|
+
return 0
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@MainThread
|
|
460
|
+
fun getRate(): Float {
|
|
461
|
+
return player?.playbackSpeed ?: 1f
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
@MainThread
|
|
465
|
+
fun setRate(value: Float) {
|
|
466
|
+
if (player != null) {
|
|
467
|
+
player!!.playbackSpeed = value
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
@MainThread
|
|
472
|
+
fun getRepeatMode(): RepeatMode {
|
|
473
|
+
return player?.playerOptions?.repeatMode ?: RepeatMode.OFF
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
@MainThread
|
|
477
|
+
fun setRepeatMode(value: RepeatMode) {
|
|
478
|
+
if (player != null) {
|
|
479
|
+
player!!.playerOptions.repeatMode = value
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
@MainThread
|
|
484
|
+
fun getVolume(): Float {
|
|
485
|
+
return player?.volume ?: 0f
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@MainThread
|
|
489
|
+
fun setVolume(value: Float) {
|
|
490
|
+
if (player != null) {
|
|
491
|
+
player!!.volume = value
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
@MainThread
|
|
496
|
+
fun getDurationInSeconds(): Double {
|
|
497
|
+
return ((player?.duration ?: 0) / 1000).toDouble()
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
@MainThread
|
|
501
|
+
fun getPositionInSeconds(): Double {
|
|
502
|
+
return ((player?.position ?: 0) / 1000).toDouble()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@MainThread
|
|
506
|
+
fun getBufferedPositionInSeconds(): Double {
|
|
507
|
+
return ((player?.bufferedPosition ?: 0) / 1000).toDouble()
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
@MainThread
|
|
511
|
+
fun getPlayerStateBundle(state: AudioPlayerState): Bundle {
|
|
512
|
+
val bundle = Bundle()
|
|
513
|
+
bundle.putString(STATE_KEY, state.asLibState.state)
|
|
514
|
+
if (state == AudioPlayerState.ERROR) {
|
|
515
|
+
bundle.putBundle(ERROR_KEY, getPlaybackErrorBundle())
|
|
516
|
+
}
|
|
517
|
+
return bundle
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
@MainThread
|
|
521
|
+
fun updateMetadataForTrack(index: Int, track: Track) {
|
|
522
|
+
player?.replaceItem(index, track.toAudioItem())
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
@MainThread
|
|
526
|
+
fun getPlayerQueueHead(): MediaSource? {
|
|
527
|
+
return player?.getQueueHead()
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// @MainThread
|
|
531
|
+
// fun updateNowPlayingMetadata(track: Track) {
|
|
532
|
+
// val audioItem = track.toAudioItem()
|
|
533
|
+
// player.notificationManager.notificationMetadata = NotificationMetadata(audioItem?.title, audioItem?.artist, audioItem?.artwork, audioItem?.duration)
|
|
534
|
+
// }
|
|
535
|
+
|
|
536
|
+
@MainThread
|
|
537
|
+
fun clearNotificationMetadata() {
|
|
538
|
+
player?.notificationManager?.hideNotification()
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
//
|
|
542
|
+
// FORCED CURRENT INDEX TO 0
|
|
543
|
+
//
|
|
544
|
+
private fun emitPlaybackTrackChangedEvents(
|
|
545
|
+
// index: Int?,
|
|
546
|
+
previousIndex: Int?,
|
|
547
|
+
oldPosition: Double
|
|
548
|
+
) {
|
|
549
|
+
if (player != null) {
|
|
550
|
+
val a = Bundle()
|
|
551
|
+
a.putDouble(POSITION_KEY, oldPosition)
|
|
552
|
+
//if (index != null) {
|
|
553
|
+
a.putInt(NEXT_TRACK_KEY, 0)
|
|
554
|
+
//}
|
|
555
|
+
|
|
556
|
+
if (previousIndex != null) {
|
|
557
|
+
a.putInt(TRACK_KEY, previousIndex)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
emit(MusicEvents.PLAYBACK_TRACK_CHANGED, a)
|
|
561
|
+
|
|
562
|
+
val b = Bundle()
|
|
563
|
+
b.putDouble("lastPosition", oldPosition)
|
|
564
|
+
if (tracks.isNotEmpty()) {
|
|
565
|
+
// b.putInt("index", player.currentIndex)
|
|
566
|
+
// b.putBundle("track", tracks[player.currentIndex].originalItem)
|
|
567
|
+
b.putInt("index", 0)
|
|
568
|
+
b.putBundle("track", tracks[0].originalItem)
|
|
569
|
+
if (previousIndex != null) {
|
|
570
|
+
b.putInt("lastIndex", previousIndex)
|
|
571
|
+
b.putBundle("lastTrack", tracks[previousIndex].originalItem)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
emit(MusicEvents.PLAYBACK_ACTIVE_TRACK_CHANGED, b)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
override fun onLoadChildren(
|
|
579
|
+
parentMediaId: String,
|
|
580
|
+
result: Result<List<MediaItem>>
|
|
581
|
+
) {
|
|
582
|
+
try {
|
|
583
|
+
val mediaIdParts = parentMediaId.split("-/-")
|
|
584
|
+
val itemType = mediaIdParts.getOrNull(1)
|
|
585
|
+
|
|
586
|
+
if (itemType == "empty") {
|
|
587
|
+
result.sendResult(emptyList())
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (mediaTree.keys.contains(parentMediaId) || parentMediaId == "/" || parentMediaId.contains(
|
|
591
|
+
"tab"
|
|
592
|
+
)
|
|
593
|
+
) {
|
|
594
|
+
result.sendResult(mediaTree[parentMediaId])
|
|
595
|
+
} else if (parentMediaId != loadingChildrenParentMediaId) {
|
|
596
|
+
loadingChildrenParentMediaId = parentMediaId
|
|
597
|
+
emit(MusicEvents.BUTTON_BROWSE, Bundle().apply {
|
|
598
|
+
putString("mediaId", parentMediaId)
|
|
599
|
+
})
|
|
600
|
+
result.sendResult(mediaTree["placeholder"])
|
|
601
|
+
}
|
|
602
|
+
} catch (_: Exception) {}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
override fun onLoadItem(itemId: String, result: Result<MediaItem>) {}
|
|
606
|
+
|
|
607
|
+
@SuppressLint("RestrictedApi")
|
|
608
|
+
override fun onCreate() {
|
|
609
|
+
super.onCreate()
|
|
610
|
+
|
|
611
|
+
// Create a MediaSessionCompat
|
|
612
|
+
// mediaSession = MediaSessionCompat(baseContext, PackageManagerCompat.LOG_TAG)
|
|
613
|
+
|
|
614
|
+
// Enable callbacks from MediaButtons and TransportControls
|
|
615
|
+
// @Suppress("DEPRECATION")
|
|
616
|
+
// mediaSession!!.setFlags(
|
|
617
|
+
// MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
|
|
618
|
+
// MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
|
|
619
|
+
// )
|
|
620
|
+
|
|
621
|
+
// Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
|
|
622
|
+
// stateBuilder = PlaybackStateCompat.Builder()
|
|
623
|
+
// .setActions(
|
|
624
|
+
// PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
|
625
|
+
// )
|
|
626
|
+
|
|
627
|
+
// if (stateBuilder != null) {
|
|
628
|
+
// mediaSession!!.setPlaybackState(stateBuilder!!.build())
|
|
629
|
+
// }
|
|
630
|
+
|
|
631
|
+
// MySessionCallback() has methods that handle callbacks from a media controller
|
|
632
|
+
// mediaSession.setCallback(new MySessionCallback());
|
|
633
|
+
|
|
634
|
+
// Set the session's token so that client activities can communicate with it.
|
|
635
|
+
|
|
636
|
+
// remove comment if not using player
|
|
637
|
+
//sessionToken = mediaSession!!.sessionToken
|
|
638
|
+
|
|
639
|
+
//onStartForeground();
|
|
640
|
+
if (manager == null) manager = MusicManager(this)
|
|
641
|
+
@Suppress("DEPRECATION")
|
|
642
|
+
if (handler == null) handler = Handler()
|
|
643
|
+
val channel = Utils.getNotificationChannel(this as Context)
|
|
644
|
+
|
|
645
|
+
// Sets the service to foreground with an empty notification
|
|
646
|
+
try {
|
|
647
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
648
|
+
startForeground(
|
|
649
|
+
EMPTY_NOTIFICATION_ID,
|
|
650
|
+
NotificationCompat.Builder(this, channel).build(),
|
|
651
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
|
652
|
+
)
|
|
653
|
+
} else {
|
|
654
|
+
startForeground(EMPTY_NOTIFICATION_ID, NotificationCompat.Builder(this, channel).build())
|
|
655
|
+
}
|
|
656
|
+
} catch (_: Exception) { }
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
override fun onCustomAction(action: String, extras: Bundle?, result: Result<Bundle>) {}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Workaround for the "Context.startForegroundService() did not then call Service.startForeground()"
|
|
663
|
+
* within 5s" ANR and crash by creating an empty notification and stopping it right after. For more
|
|
664
|
+
* information see https://github.com/doublesymmetry/react-native-track-player/issues/1666
|
|
665
|
+
*/
|
|
666
|
+
private fun startAndStopEmptyNotificationToAvoidANR() {
|
|
667
|
+
val notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
|
668
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
669
|
+
notificationManager.createNotificationChannel(
|
|
670
|
+
NotificationChannel("Playback", "Playback", NotificationManager.IMPORTANCE_LOW)
|
|
671
|
+
)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
val resId = applicationContext.resources.getIdentifier("track_player_logo", "drawable", applicationContext.packageName)
|
|
675
|
+
val logo = if (resId != 0) resId else R.drawable.ic_play
|
|
676
|
+
val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(
|
|
677
|
+
this, "Playback"
|
|
678
|
+
)
|
|
679
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
680
|
+
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
|
681
|
+
.setSmallIcon(logo)
|
|
682
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
683
|
+
notificationBuilder.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
|
684
|
+
}
|
|
685
|
+
val notification = notificationBuilder.build()
|
|
686
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
687
|
+
startForeground(EMPTY_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
|
688
|
+
} else {
|
|
689
|
+
startForeground(EMPTY_NOTIFICATION_ID, notification)
|
|
690
|
+
}
|
|
691
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
692
|
+
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
693
|
+
} else {
|
|
694
|
+
@Suppress("DEPRECATION")
|
|
695
|
+
stopForeground(true)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
700
|
+
|
|
701
|
+
if (intent == null) {
|
|
702
|
+
return START_NOT_STICKY
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (Intent.ACTION_MEDIA_BUTTON == intent.action) {
|
|
706
|
+
onStartForeground()
|
|
707
|
+
|
|
708
|
+
if (Build.VERSION.SDK_INT >= 33) {
|
|
709
|
+
try {
|
|
710
|
+
startAndStopEmptyNotificationToAvoidANR()
|
|
711
|
+
} catch (_: java.lang.Exception) {}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
@Suppress("DEPRECATION")
|
|
715
|
+
val intentExtra: KeyEvent? = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
|
|
716
|
+
if (intentExtra!!.keyCode == KEYCODE_MEDIA_STOP) {
|
|
717
|
+
intentToStop = true
|
|
718
|
+
startServiceOreoAndAbove()
|
|
719
|
+
stopSelf()
|
|
720
|
+
} else {
|
|
721
|
+
intentToStop = false
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (manager != null) {
|
|
725
|
+
MediaButtonReceiver.handleIntent(manager!!.metadata.session, intent)
|
|
726
|
+
return START_NOT_STICKY
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (manager == null) manager = MusicManager(this)
|
|
731
|
+
|
|
732
|
+
@Suppress("DEPRECATION")
|
|
733
|
+
if (handler == null) handler = Handler()
|
|
734
|
+
|
|
735
|
+
super.onStartCommand(intent, flags, startId)
|
|
736
|
+
return START_NOT_STICKY
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private fun startServiceOreoAndAbove() {
|
|
740
|
+
// Needed to prevent crash when dismissing notification
|
|
741
|
+
// https://stackoverflow.com/questions/47609261/bound-service-crash-with-context-startforegroundservice-did-not-then-call-ser?rq=1
|
|
742
|
+
if (Build.VERSION.SDK_INT >= 26) {
|
|
743
|
+
val channelId = Utils.NOTIFICATION_CHANNEL
|
|
744
|
+
val channelName = "Playback"
|
|
745
|
+
val channel = NotificationChannel(
|
|
746
|
+
channelId,
|
|
747
|
+
channelName,
|
|
748
|
+
NotificationManager.IMPORTANCE_DEFAULT
|
|
749
|
+
)
|
|
750
|
+
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(
|
|
751
|
+
channel
|
|
752
|
+
)
|
|
753
|
+
val resId = applicationContext.resources.getIdentifier("track_player_logo", "drawable", applicationContext.packageName)
|
|
754
|
+
val logo = if (resId != 0) resId else R.drawable.ic_play
|
|
755
|
+
val notification = NotificationCompat.Builder(this, channelId)
|
|
756
|
+
.setCategory(Notification.CATEGORY_SERVICE).setSmallIcon(logo)
|
|
757
|
+
.setPriority(
|
|
758
|
+
NotificationCompat.PRIORITY_MIN
|
|
759
|
+
).build()
|
|
760
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
761
|
+
startForeground(EMPTY_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
|
762
|
+
} else {
|
|
763
|
+
startForeground(EMPTY_NOTIFICATION_ID, notification)
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
override fun onDestroy() {
|
|
769
|
+
super.onDestroy()
|
|
770
|
+
if (manager != null) {
|
|
771
|
+
manager!!.destroy(true)
|
|
772
|
+
manager = null
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
override fun onTaskRemoved(rootIntent: Intent) {
|
|
777
|
+
super.onTaskRemoved(rootIntent)
|
|
778
|
+
if (manager == null || manager!!.shouldStopWithApp()) {
|
|
779
|
+
destroy(true)
|
|
780
|
+
stopSelf()
|
|
781
|
+
}
|
|
782
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
783
|
+
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
784
|
+
} else {
|
|
785
|
+
@Suppress("DEPRECATION")
|
|
786
|
+
stopForeground(true)
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
@MainThread
|
|
791
|
+
fun setupPlayer(playerOptions: Bundle?) {
|
|
792
|
+
if (player != null) {
|
|
793
|
+
print("Player was initialized. Prevent re-initializing again")
|
|
794
|
+
return
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
val bufferConfig = BufferConfig(
|
|
798
|
+
playerOptions?.getDouble(MIN_BUFFER_KEY)?.toInt(),
|
|
799
|
+
playerOptions?.getDouble(MAX_BUFFER_KEY)?.toInt(),
|
|
800
|
+
playerOptions?.getDouble(PLAY_BUFFER_KEY)?.toInt(),
|
|
801
|
+
playerOptions?.getDouble(BACK_BUFFER_KEY)?.toInt(),
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
val cacheConfig = CacheConfig(playerOptions?.getDouble(MAX_CACHE_SIZE_KEY)?.toLong())
|
|
805
|
+
val playerConfig = PlayerConfig(
|
|
806
|
+
interceptPlayerActionsTriggeredExternally = true,
|
|
807
|
+
handleAudioBecomingNoisy = true,
|
|
808
|
+
handleAudioFocus = true,
|
|
809
|
+
audioContentType = AudioContentType.MUSIC
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
val automaticallyUpdateNotificationMetadata = playerOptions?.getBoolean(AUTO_UPDATE_METADATA, true) ?: true
|
|
813
|
+
val mediaSessionCallback = object: AAMediaSessionCallBack {
|
|
814
|
+
override fun handleCustomActions(action: String?, extras: Bundle?) {}
|
|
815
|
+
|
|
816
|
+
override fun handlePlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
|
817
|
+
val emitBundle = extras ?: Bundle()
|
|
818
|
+
emit(MusicEvents.BUTTON_PLAY_FROM_ID, emitBundle.apply {
|
|
819
|
+
putString("mediaId", mediaId)
|
|
820
|
+
})
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
override fun handlePlayFromSearch(query: String?, extras: Bundle?) {
|
|
824
|
+
val emitBundle = extras ?: Bundle()
|
|
825
|
+
emit(MusicEvents.BUTTON_PLAY_FROM_SEARCH, emitBundle.apply {
|
|
826
|
+
putString("query", query)
|
|
827
|
+
})
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
override fun handleSkipToQueueItem(id: Long) {
|
|
831
|
+
val emitBundle = Bundle()
|
|
832
|
+
emit(MusicEvents.BUTTON_PLAY_FROM_QUEUE, emitBundle.apply {
|
|
833
|
+
putInt("index", id.toInt())
|
|
834
|
+
})
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
player = QueuedAudioPlayer(this@MusicService, playerConfig, bufferConfig, cacheConfig, mediaSessionCallback)
|
|
838
|
+
if (MusicModule.isAndroidTv) {
|
|
839
|
+
player!!.volume = 0f
|
|
840
|
+
}
|
|
841
|
+
player!!.automaticallyUpdateNotificationMetadata = automaticallyUpdateNotificationMetadata
|
|
842
|
+
sessionToken = player!!.getMediaSessionToken()
|
|
843
|
+
|
|
844
|
+
observeEvents()
|
|
845
|
+
setupForegrounding()
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
private fun getPlaybackErrorBundle(): Bundle {
|
|
849
|
+
val bundle = Bundle()
|
|
850
|
+
val error = playbackError
|
|
851
|
+
if (error?.message != null) {
|
|
852
|
+
bundle.putString("message", error.message)
|
|
853
|
+
}
|
|
854
|
+
if (error?.code != null) {
|
|
855
|
+
bundle.putString("code", "android-" + error.code)
|
|
856
|
+
}
|
|
857
|
+
return bundle
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
private fun emitQueueEndedEvent() {
|
|
861
|
+
if (player != null) {
|
|
862
|
+
val bundle = Bundle()
|
|
863
|
+
bundle.putInt(TRACK_KEY, player!!.currentIndex)
|
|
864
|
+
bundle.putDouble(POSITION_KEY, (player!!.position / 1000).toDouble())
|
|
865
|
+
emit(MusicEvents.PLAYBACK_QUEUE_ENDED, bundle)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private var capabilities: List<Capability> = emptyList()
|
|
870
|
+
private var notificationCapabilities: List<Capability> = emptyList()
|
|
871
|
+
private var compactCapabilities: List<Capability> = emptyList()
|
|
872
|
+
private var progressUpdateJob: Job? = null
|
|
873
|
+
|
|
874
|
+
private fun isCompact(capability: Capability): Boolean {
|
|
875
|
+
return compactCapabilities.contains(capability)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private fun getPendingIntentFlags(): Int {
|
|
879
|
+
return PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private fun getIcon(options: Bundle, propertyName: String, defaultIcon: Int): Int {
|
|
883
|
+
if (!options.containsKey(propertyName)) return defaultIcon
|
|
884
|
+
val bundle = options.getBundle(propertyName) ?: return defaultIcon
|
|
885
|
+
val helper = ResourceDrawableIdHelper.getInstance()
|
|
886
|
+
val icon = helper.getResourceDrawableId(this, bundle.getString("uri"))
|
|
887
|
+
return if (icon == 0) defaultIcon else icon
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
@MainThread
|
|
891
|
+
fun updateOptions(options: Bundle) {
|
|
892
|
+
latestOptions = options
|
|
893
|
+
val androidOptions = options.getBundle(ANDROID_OPTIONS_KEY)
|
|
894
|
+
|
|
895
|
+
ratingType = BundleUtils.getInt(options, "ratingType", RatingCompat.RATING_NONE)
|
|
896
|
+
|
|
897
|
+
if (player != null) {
|
|
898
|
+
player!!.playerOptions.alwaysPauseOnInterruption =
|
|
899
|
+
androidOptions?.getBoolean(PAUSE_ON_INTERRUPTION_KEY) ?: false
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
capabilities = options.getIntegerArrayList("capabilities")?.map {
|
|
903
|
+
Capability.entries[it] } ?: emptyList()
|
|
904
|
+
notificationCapabilities = options.getIntegerArrayList("notificationCapabilities")?.map { Capability.entries[it] } ?: emptyList()
|
|
905
|
+
compactCapabilities = options.getIntegerArrayList("compactCapabilities")?.map { Capability.entries[it] } ?: emptyList()
|
|
906
|
+
val customActions = options.getStringArrayList(CUSTOM_ACTIONS_KEY)
|
|
907
|
+
if (notificationCapabilities.isEmpty()) notificationCapabilities = capabilities
|
|
908
|
+
|
|
909
|
+
fun customIcon(customAction: String): Int {
|
|
910
|
+
return when (customAction) {
|
|
911
|
+
"shuffle-on" -> R.mipmap.ic_shuffle_on_foreground
|
|
912
|
+
"shuffle-off" -> R.mipmap.ic_shuffle_off_foreground
|
|
913
|
+
"repeat-on" -> R.mipmap.ic_repeat_on_foreground
|
|
914
|
+
"repeat-off" -> R.mipmap.ic_repeat_off_foreground
|
|
915
|
+
"heart" -> R.mipmap.ic_heart_foreground
|
|
916
|
+
"heart-outlined" -> R.mipmap.ic_heart_outlined_foreground
|
|
917
|
+
"clock" -> R.mipmap.ic_clock_now_foreground
|
|
918
|
+
"arrow-down-circle" -> R.mipmap.ic_arrow_down_circle_foreground
|
|
919
|
+
"md-close" -> R.mipmap.ic_close_foreground
|
|
920
|
+
else -> R.mipmap.ic_heart_outlined_foreground
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
val buttonsList = mutableListOf<NotificationButton>()
|
|
925
|
+
|
|
926
|
+
notificationCapabilities.forEach { capability ->
|
|
927
|
+
when (capability) {
|
|
928
|
+
Capability.PLAY, Capability.PAUSE -> {
|
|
929
|
+
val playIcon = getIcon(options, "playIcon", R.drawable.ic_play)
|
|
930
|
+
val pauseIcon = getIcon(options, "pauseIcon", R.drawable.ic_pause)
|
|
931
|
+
buttonsList.addAll(listOf(PLAY_PAUSE(playIcon = playIcon, pauseIcon = pauseIcon)))
|
|
932
|
+
}
|
|
933
|
+
Capability.STOP -> {
|
|
934
|
+
val stopIcon = getIcon(options, "stopIcon", R.drawable.ic_stop)
|
|
935
|
+
buttonsList.addAll(listOf(STOP(icon = stopIcon)))
|
|
936
|
+
}
|
|
937
|
+
Capability.SKIP_TO_NEXT -> {
|
|
938
|
+
val nextIcon = getIcon(options, "nextIcon", R.drawable.ic_next)
|
|
939
|
+
buttonsList.addAll(listOf(NEXT(icon = nextIcon, isCompact = isCompact(capability))))
|
|
940
|
+
}
|
|
941
|
+
Capability.SKIP_TO_PREVIOUS -> {
|
|
942
|
+
val previousIcon = getIcon(options, "previousIcon", R.drawable.ic_previous)
|
|
943
|
+
buttonsList.addAll(listOf(PREVIOUS(icon = previousIcon, isCompact = isCompact(capability))))
|
|
944
|
+
}
|
|
945
|
+
// Capability.JUMP_FORWARD -> {
|
|
946
|
+
// val forwardIcon = BundleUtils.getIconOrNull(this, options, "forwardIcon")
|
|
947
|
+
// buttonsList.addAll(listOf(FORWARD(icon = forwardIcon, isCompact = isCompact(capability))))
|
|
948
|
+
// }
|
|
949
|
+
// Capability.JUMP_BACKWARD -> {
|
|
950
|
+
// val backwardIcon = BundleUtils.getIconOrNull(this, options, "rewindIcon")
|
|
951
|
+
// buttonsList.addAll(listOf(BACKWARD(icon = backwardIcon, isCompact = isCompact(capability))))
|
|
952
|
+
// }
|
|
953
|
+
Capability.SEEK_TO -> {
|
|
954
|
+
buttonsList.addAll(listOf(SEEK_TO))
|
|
955
|
+
}
|
|
956
|
+
else -> {}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (customActions != null) {
|
|
961
|
+
for (customAction in customActions) {
|
|
962
|
+
val customIcon = customIcon(customAction)
|
|
963
|
+
buttonsList.add(CUSTOM_ACTION(icon=customIcon, customAction = customAction, isCompact = false))
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
val openAppIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
|
|
968
|
+
// flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
969
|
+
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
970
|
+
action = Intent.ACTION_VIEW
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
val accentColor = BundleUtils.getIntOrNull(options, "color")
|
|
974
|
+
val resId = applicationContext.resources.getIdentifier("track_player_logo", "drawable", applicationContext.packageName)
|
|
975
|
+
val smallIcon = if (resId != 0) resId else R.drawable.ic_play
|
|
976
|
+
val pendingIntent = PendingIntent.getActivity(this, 0, openAppIntent, getPendingIntentFlags())
|
|
977
|
+
val notificationConfig = NotificationConfig(buttonsList, accentColor, smallIcon, pendingIntent)
|
|
978
|
+
|
|
979
|
+
// player.notificationManager.destroy()
|
|
980
|
+
player?.notificationManager?.createNotification(notificationConfig)
|
|
981
|
+
|
|
982
|
+
// setup progress update events if configured
|
|
983
|
+
progressUpdateJob?.cancel()
|
|
984
|
+
val updateInterval = BundleUtils.getIntOrNull(options, PROGRESS_UPDATE_EVENT_INTERVAL_KEY)
|
|
985
|
+
if (updateInterval != null && updateInterval > 0) {
|
|
986
|
+
progressUpdateJob = scope.launch {
|
|
987
|
+
progressUpdateEventFlow(updateInterval.toDouble()).collect { emit(MusicEvents.PLAYBACK_PROGRESS_UPDATED, it) }
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
@MainThread
|
|
993
|
+
private fun progressUpdateEventFlow(interval: Double) = flow {
|
|
994
|
+
while (true) {
|
|
995
|
+
if (player?.isPlaying == true) {
|
|
996
|
+
val bundle = progressUpdateEvent()
|
|
997
|
+
emit(bundle)
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
delay((interval * 1000).toLong())
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
@MainThread
|
|
1005
|
+
private suspend fun progressUpdateEvent(): Bundle {
|
|
1006
|
+
return withContext(Dispatchers.Main) {
|
|
1007
|
+
player?.let {
|
|
1008
|
+
Bundle().apply {
|
|
1009
|
+
putDouble(POSITION_KEY, (it.position / 1000).toDouble())
|
|
1010
|
+
putDouble(DURATION_KEY, (it.duration / 1000).toDouble())
|
|
1011
|
+
putDouble(BUFFERED_POSITION_KEY, (it.bufferedPosition / 1000).toDouble())
|
|
1012
|
+
putInt(TRACK_KEY, it.currentIndex)
|
|
1013
|
+
}
|
|
1014
|
+
} ?: Bundle()
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
@MainThread
|
|
1020
|
+
private fun observeEvents() {
|
|
1021
|
+
scope.launch {
|
|
1022
|
+
event?.audioItemTransition?.collect {
|
|
1023
|
+
if (it !is AudioItemTransitionReason.REPEAT && player != null) {
|
|
1024
|
+
// emitPlaybackTrackChangedEvents(
|
|
1025
|
+
// player.currentIndex,
|
|
1026
|
+
// player.previousIndex,
|
|
1027
|
+
// (it?.oldPosition ?: 0).toDouble()
|
|
1028
|
+
// )
|
|
1029
|
+
//
|
|
1030
|
+
// FORCED CURRENT INDEX TO 0
|
|
1031
|
+
//
|
|
1032
|
+
emitPlaybackTrackChangedEvents(
|
|
1033
|
+
player!!.previousIndex,
|
|
1034
|
+
(it?.oldPosition ?: 0).toDouble()
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
scope.launch {
|
|
1041
|
+
event?.onAudioFocusChanged?.collect {
|
|
1042
|
+
Bundle().apply {
|
|
1043
|
+
putBoolean(IS_FOCUS_LOSS_PERMANENT_KEY, it.isFocusLostPermanently)
|
|
1044
|
+
putBoolean(IS_PAUSED_KEY, it.isPaused)
|
|
1045
|
+
emit(MusicEvents.BUTTON_DUCK, this)
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
scope.launch {
|
|
1052
|
+
event?.onPlayerActionTriggeredExternally?.collect {
|
|
1053
|
+
when (it) {
|
|
1054
|
+
is MediaSessionCallback.RATING -> {
|
|
1055
|
+
Bundle().apply {
|
|
1056
|
+
setRating(this, "rating", it.rating)
|
|
1057
|
+
emit(MusicEvents.BUTTON_SET_RATING, this)
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
is MediaSessionCallback.SEEK -> {
|
|
1062
|
+
Bundle().apply {
|
|
1063
|
+
putDouble("position", (it.positionMs / 1000).toDouble())
|
|
1064
|
+
emit(MusicEvents.BUTTON_SEEK_TO, this)
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
MediaSessionCallback.PLAY -> emit(MusicEvents.BUTTON_PLAY)
|
|
1069
|
+
MediaSessionCallback.PAUSE -> emit(MusicEvents.BUTTON_PAUSE)
|
|
1070
|
+
MediaSessionCallback.NEXT -> emit(MusicEvents.BUTTON_SKIP_NEXT)
|
|
1071
|
+
MediaSessionCallback.PREVIOUS -> emit(MusicEvents.BUTTON_SKIP_PREVIOUS)
|
|
1072
|
+
MediaSessionCallback.STOP -> emit(MusicEvents.BUTTON_STOP)
|
|
1073
|
+
MediaSessionCallback.FORWARD -> {
|
|
1074
|
+
// Bundle().apply {
|
|
1075
|
+
// val interval = latestOptions?.getDouble(FORWARD_JUMP_INTERVAL_KEY, DEFAULT_JUMP_INTERVAL) ?: DEFAULT_JUMP_INTERVAL
|
|
1076
|
+
// putInt("interval", interval.toInt())
|
|
1077
|
+
// emit(MusicEvents.BUTTON_JUMP_FORWARD, this)
|
|
1078
|
+
// }
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
MediaSessionCallback.REWIND -> {
|
|
1082
|
+
// Bundle().apply {
|
|
1083
|
+
// val interval = latestOptions?.getDouble(BACKWARD_JUMP_INTERVAL_KEY, DEFAULT_JUMP_INTERVAL) ?: DEFAULT_JUMP_INTERVAL
|
|
1084
|
+
// putInt("interval", interval.toInt())
|
|
1085
|
+
// emit(MusicEvents.BUTTON_JUMP_BACKWARD, this)
|
|
1086
|
+
// }
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
is MediaSessionCallback.CUSTOMACTION -> {
|
|
1090
|
+
Bundle().apply {
|
|
1091
|
+
if (it.customAction == "shuffle-on" || it.customAction == "shuffle-off") {
|
|
1092
|
+
emit(MusicEvents.BUTTON_SHUFFLE, this)
|
|
1093
|
+
} else if (it.customAction == "repeat-on" || it.customAction == "repeat-off") {
|
|
1094
|
+
emit(MusicEvents.BUTTON_REPEAT, this)
|
|
1095
|
+
} else if (it.customAction == "heart" || it.customAction == "heart-outlined" || it.customAction == "clock" || it.customAction == "arrow-down-circle" || it.customAction == "md-close") {
|
|
1096
|
+
emit(MusicEvents.BUTTON_TRACK_STATUS, this)
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
@Suppress("DEPRECATION")
|
|
1106
|
+
fun isForegroundService(): Boolean {
|
|
1107
|
+
val manager = baseContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
1108
|
+
for (service in manager.getRunningServices(Int.MAX_VALUE)) {
|
|
1109
|
+
if (MusicService::class.java.name == service.service.className) {
|
|
1110
|
+
return service.foreground
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return false
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
private var stopForegroundGracePeriod: Int = DEFAULT_STOP_FOREGROUND_GRACE_PERIOD
|
|
1117
|
+
|
|
1118
|
+
@MainThread
|
|
1119
|
+
private fun setupForegrounding() {
|
|
1120
|
+
// Implementation based on https://github.com/Automattic/pocket-casts-android/blob/ee8da0c095560ef64a82d3a31464491b8d713104/modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/playback/PlaybackService.kt#L218
|
|
1121
|
+
var notificationId: Int? = null
|
|
1122
|
+
var notification: Notification? = null
|
|
1123
|
+
var stopForegroundWhenNotOngoing = false
|
|
1124
|
+
var removeNotificationWhenNotOngoing = false
|
|
1125
|
+
|
|
1126
|
+
fun startForegroundIfNecessary() {
|
|
1127
|
+
if (isForegroundService()) {
|
|
1128
|
+
return
|
|
1129
|
+
}
|
|
1130
|
+
if (notification == null) {
|
|
1131
|
+
return
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
1135
|
+
startForeground(
|
|
1136
|
+
notificationId!!,
|
|
1137
|
+
notification!!,
|
|
1138
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
|
1139
|
+
)
|
|
1140
|
+
} else {
|
|
1141
|
+
startForeground(notificationId!!, notification)
|
|
1142
|
+
}
|
|
1143
|
+
} catch (error: Exception) {
|
|
1144
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
|
1145
|
+
error is ForegroundServiceStartNotAllowedException
|
|
1146
|
+
) {
|
|
1147
|
+
emit(MusicEvents.PLAYER_ERROR, Bundle().apply {
|
|
1148
|
+
putString("message", error.message)
|
|
1149
|
+
putString("code", "android-foreground-service-start-not-allowed")
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
scope.launch {
|
|
1156
|
+
val BACKGROUNDABLE_STATES = listOf(
|
|
1157
|
+
AudioPlayerState.IDLE,
|
|
1158
|
+
AudioPlayerState.ENDED,
|
|
1159
|
+
AudioPlayerState.STOPPED,
|
|
1160
|
+
AudioPlayerState.ERROR,
|
|
1161
|
+
AudioPlayerState.PAUSED
|
|
1162
|
+
)
|
|
1163
|
+
val REMOVABLE_STATES = listOf(
|
|
1164
|
+
AudioPlayerState.IDLE,
|
|
1165
|
+
AudioPlayerState.STOPPED,
|
|
1166
|
+
AudioPlayerState.ERROR
|
|
1167
|
+
)
|
|
1168
|
+
val LOADING_STATES = listOf(
|
|
1169
|
+
AudioPlayerState.LOADING,
|
|
1170
|
+
AudioPlayerState.READY,
|
|
1171
|
+
AudioPlayerState.BUFFERING
|
|
1172
|
+
)
|
|
1173
|
+
var stateCount = 0
|
|
1174
|
+
event?.stateChange?.collect {
|
|
1175
|
+
stateCount++
|
|
1176
|
+
if (it in LOADING_STATES) return@collect;
|
|
1177
|
+
// Skip initial idle state, since we are only interested when
|
|
1178
|
+
// state becomes idle after not being idle
|
|
1179
|
+
stopForegroundWhenNotOngoing = stateCount > 1 && it in BACKGROUNDABLE_STATES
|
|
1180
|
+
removeNotificationWhenNotOngoing = stopForegroundWhenNotOngoing && it in REMOVABLE_STATES
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
fun shouldStopForeground(): Boolean {
|
|
1185
|
+
return stopForegroundWhenNotOngoing && (removeNotificationWhenNotOngoing || isForegroundService())
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
scope.launch {
|
|
1189
|
+
event?.notificationStateChange?.collect {
|
|
1190
|
+
when (it) {
|
|
1191
|
+
is NotificationState.POSTED -> {
|
|
1192
|
+
notificationId = it.notificationId;
|
|
1193
|
+
notification = it.notification;
|
|
1194
|
+
if (it.ongoing) {
|
|
1195
|
+
if (player?.playWhenReady == true) {
|
|
1196
|
+
if (sWakeLock?.isHeld != true) {
|
|
1197
|
+
sWakeLock?.acquire()
|
|
1198
|
+
}
|
|
1199
|
+
startForegroundIfNecessary()
|
|
1200
|
+
}
|
|
1201
|
+
} else if (shouldStopForeground()) {
|
|
1202
|
+
// Allow the application a grace period to complete any actions
|
|
1203
|
+
// that may necessitate keeping the service in a foreground state.
|
|
1204
|
+
// For instance, queuing new media (e.g., related music) after the
|
|
1205
|
+
// user's queue is complete. This prevents the service from potentially
|
|
1206
|
+
// being immediately destroyed once the player finishes playing media.
|
|
1207
|
+
scope.launch {
|
|
1208
|
+
if (sWakeLock?.isHeld == true) {
|
|
1209
|
+
sWakeLock?.release()
|
|
1210
|
+
}
|
|
1211
|
+
// delay(stopForegroundGracePeriod.toLong() * 1000)
|
|
1212
|
+
// if (shouldStopForeground()) {
|
|
1213
|
+
// @Suppress("DEPRECATION")
|
|
1214
|
+
// stopForeground(removeNotificationWhenNotOngoing)
|
|
1215
|
+
// }
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
else -> {}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
companion object {
|
|
1227
|
+
const val EMPTY_NOTIFICATION_ID = 1
|
|
1228
|
+
const val STATE_KEY = "state"
|
|
1229
|
+
const val ERROR_KEY = "error"
|
|
1230
|
+
const val EVENT_KEY = "event"
|
|
1231
|
+
const val DATA_KEY = "data"
|
|
1232
|
+
const val TRACK_KEY = "track"
|
|
1233
|
+
const val NEXT_TRACK_KEY = "nextTrack"
|
|
1234
|
+
const val POSITION_KEY = "position"
|
|
1235
|
+
const val DURATION_KEY = "duration"
|
|
1236
|
+
const val BUFFERED_POSITION_KEY = "buffered"
|
|
1237
|
+
|
|
1238
|
+
const val TASK_KEY = "TrackPlayer"
|
|
1239
|
+
|
|
1240
|
+
const val MIN_BUFFER_KEY = "minBuffer"
|
|
1241
|
+
const val MAX_BUFFER_KEY = "maxBuffer"
|
|
1242
|
+
const val PLAY_BUFFER_KEY = "playBuffer"
|
|
1243
|
+
const val BACK_BUFFER_KEY = "backBuffer"
|
|
1244
|
+
|
|
1245
|
+
const val FORWARD_JUMP_INTERVAL_KEY = "forwardJumpInterval"
|
|
1246
|
+
const val BACKWARD_JUMP_INTERVAL_KEY = "backwardJumpInterval"
|
|
1247
|
+
const val PROGRESS_UPDATE_EVENT_INTERVAL_KEY = "progressUpdateEventInterval"
|
|
1248
|
+
|
|
1249
|
+
const val MAX_CACHE_SIZE_KEY = "maxCacheSize"
|
|
1250
|
+
|
|
1251
|
+
const val ANDROID_OPTIONS_KEY = "android"
|
|
1252
|
+
|
|
1253
|
+
const val STOPPING_APP_PAUSES_PLAYBACK_KEY = "stoppingAppPausesPlayback"
|
|
1254
|
+
const val APP_KILLED_PLAYBACK_BEHAVIOR_KEY = "appKilledPlaybackBehavior"
|
|
1255
|
+
const val STOP_FOREGROUND_GRACE_PERIOD_KEY = "stopForegroundGracePeriod"
|
|
1256
|
+
const val PAUSE_ON_INTERRUPTION_KEY = "alwaysPauseOnInterruption"
|
|
1257
|
+
const val AUTO_UPDATE_METADATA = "autoUpdateMetadata"
|
|
1258
|
+
const val AUTO_HANDLE_INTERRUPTIONS = "autoHandleInterruptions"
|
|
1259
|
+
const val ANDROID_AUDIO_CONTENT_TYPE = "androidAudioContentType"
|
|
1260
|
+
const val IS_FOCUS_LOSS_PERMANENT_KEY = "permanent"
|
|
1261
|
+
const val IS_PAUSED_KEY = "paused"
|
|
1262
|
+
|
|
1263
|
+
const val DEFAULT_JUMP_INTERVAL = 15.0
|
|
1264
|
+
const val DEFAULT_STOP_FOREGROUND_GRACE_PERIOD = 5
|
|
1265
|
+
|
|
1266
|
+
const val CUSTOM_ACTIONS_KEY = "customActions"
|
|
1267
|
+
}
|
|
1268
|
+
}
|