react-native-audio-api 0.11.0-nightly-b30bac9-20260114 → 0.11.0
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/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +3 -10
- package/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +0 -4
- package/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +4 -83
- package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +14 -29
- package/android/src/main/java/com/swmansion/audioapi/system/ForegroundServiceManager.kt +10 -9
- package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +10 -51
- package/android/src/main/java/com/swmansion/audioapi/system/notification/BaseNotification.kt +6 -14
- package/android/src/main/java/com/swmansion/audioapi/system/notification/NotificationRegistry.kt +79 -60
- package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt +249 -411
- package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt +8 -3
- package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt +240 -222
- package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotificationReceiver.kt +11 -22
- package/android/src/main/java/com/swmansion/audioapi/system/notification/state/RecordingNotificationState.kt +24 -0
- package/android/src/main/res/layout/btn_round_ripple.xml +9 -0
- package/android/src/main/res/layout/notification_collapsed.xml +45 -0
- package/android/src/main/res/layout/notification_expanded.xml +44 -0
- package/android/src/oldarch/NativeAudioAPIModuleSpec.java +1 -13
- package/common/cpp/audioapi/core/utils/AudioFileWriter.cpp +1 -1
- package/ios/audioapi/ios/AudioAPIModule.mm +5 -48
- package/ios/audioapi/ios/system/notification/BaseNotification.h +0 -7
- package/ios/audioapi/ios/system/notification/NotificationRegistry.h +5 -25
- package/ios/audioapi/ios/system/notification/NotificationRegistry.mm +19 -64
- package/ios/audioapi/ios/system/notification/PlaybackNotification.mm +4 -15
- package/lib/commonjs/AudioAPIModule/AudioAPIModule.js +2 -1
- package/lib/commonjs/AudioAPIModule/AudioAPIModule.js.map +1 -1
- package/lib/commonjs/api.js +1 -29
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/core/AudioDecoder.js +42 -16
- package/lib/commonjs/core/AudioDecoder.js.map +1 -1
- package/lib/commonjs/core/AudioRecorder.js +2 -1
- package/lib/commonjs/core/AudioRecorder.js.map +1 -1
- package/lib/commonjs/core/AudioStretcher.js +2 -1
- package/lib/commonjs/core/AudioStretcher.js.map +1 -1
- package/lib/commonjs/core/BaseAudioContext.js +2 -5
- package/lib/commonjs/core/BaseAudioContext.js.map +1 -1
- package/lib/commonjs/errors/AudioApiError.js +14 -0
- package/lib/commonjs/errors/AudioApiError.js.map +1 -0
- package/lib/commonjs/errors/index.js +7 -0
- package/lib/commonjs/errors/index.js.map +1 -1
- package/lib/commonjs/specs/NativeAudioAPIModule.js.map +1 -1
- package/lib/commonjs/specs/NativeAudioAPIModule.web.js +0 -9
- package/lib/commonjs/specs/NativeAudioAPIModule.web.js.map +1 -1
- package/lib/commonjs/system/notification/PlaybackNotificationManager.js +40 -85
- package/lib/commonjs/system/notification/PlaybackNotificationManager.js.map +1 -1
- package/lib/commonjs/system/notification/RecordingNotificationManager.ios.js +51 -0
- package/lib/commonjs/system/notification/RecordingNotificationManager.ios.js.map +1 -0
- package/lib/commonjs/system/notification/RecordingNotificationManager.js +30 -144
- package/lib/commonjs/system/notification/RecordingNotificationManager.js.map +1 -1
- package/lib/commonjs/system/notification/index.js +1 -9
- package/lib/commonjs/system/notification/index.js.map +1 -1
- package/lib/commonjs/utils/index.js +3 -2
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/commonjs/utils/paths.js +18 -0
- package/lib/commonjs/utils/paths.js.map +1 -0
- package/lib/commonjs/web-core/AudioContext.js +20 -11
- package/lib/commonjs/web-core/AudioContext.js.map +1 -1
- package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js +0 -1
- package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js.map +1 -1
- package/lib/commonjs/web-system/notification/RecordingNotificationManager.js +1 -6
- package/lib/commonjs/web-system/notification/RecordingNotificationManager.js.map +1 -1
- package/lib/module/AudioAPIModule/AudioAPIModule.js +2 -1
- package/lib/module/AudioAPIModule/AudioAPIModule.js.map +1 -1
- package/lib/module/api.js +3 -2
- package/lib/module/api.js.map +1 -1
- package/lib/module/core/AudioDecoder.js +42 -16
- package/lib/module/core/AudioDecoder.js.map +1 -1
- package/lib/module/core/AudioRecorder.js +3 -1
- package/lib/module/core/AudioRecorder.js.map +1 -1
- package/lib/module/core/AudioStretcher.js +2 -1
- package/lib/module/core/AudioStretcher.js.map +1 -1
- package/lib/module/core/BaseAudioContext.js +2 -5
- package/lib/module/core/BaseAudioContext.js.map +1 -1
- package/lib/module/errors/AudioApiError.js +10 -0
- package/lib/module/errors/AudioApiError.js.map +1 -0
- package/lib/module/errors/index.js +1 -0
- package/lib/module/errors/index.js.map +1 -1
- package/lib/module/specs/NativeAudioAPIModule.js.map +1 -1
- package/lib/module/specs/NativeAudioAPIModule.web.js +0 -9
- package/lib/module/specs/NativeAudioAPIModule.web.js.map +1 -1
- package/lib/module/system/notification/PlaybackNotificationManager.js +40 -85
- package/lib/module/system/notification/PlaybackNotificationManager.js.map +1 -1
- package/lib/module/system/notification/RecordingNotificationManager.ios.js +47 -0
- package/lib/module/system/notification/RecordingNotificationManager.ios.js.map +1 -0
- package/lib/module/system/notification/RecordingNotificationManager.js +30 -144
- package/lib/module/system/notification/RecordingNotificationManager.js.map +1 -1
- package/lib/module/system/notification/index.js +1 -2
- package/lib/module/system/notification/index.js.map +1 -1
- package/lib/module/utils/index.js +3 -2
- package/lib/module/utils/index.js.map +1 -1
- package/lib/module/utils/paths.js +12 -0
- package/lib/module/utils/paths.js.map +1 -0
- package/lib/module/web-core/AudioContext.js +20 -11
- package/lib/module/web-core/AudioContext.js.map +1 -1
- package/lib/module/web-system/notification/PlaybackNotificationManager.js +0 -1
- package/lib/module/web-system/notification/PlaybackNotificationManager.js.map +1 -1
- package/lib/module/web-system/notification/RecordingNotificationManager.js +1 -6
- package/lib/module/web-system/notification/RecordingNotificationManager.js.map +1 -1
- package/lib/typescript/AudioAPIModule/AudioAPIModule.d.ts.map +1 -1
- package/lib/typescript/api.d.ts +3 -2
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/core/AudioDecoder.d.ts +2 -1
- package/lib/typescript/core/AudioDecoder.d.ts.map +1 -1
- package/lib/typescript/core/AudioRecorder.d.ts.map +1 -1
- package/lib/typescript/core/AudioStretcher.d.ts.map +1 -1
- package/lib/typescript/core/BaseAudioContext.d.ts +2 -2
- package/lib/typescript/core/BaseAudioContext.d.ts.map +1 -1
- package/lib/typescript/errors/AudioApiError.d.ts +5 -0
- package/lib/typescript/errors/AudioApiError.d.ts.map +1 -0
- package/lib/typescript/errors/index.d.ts +1 -0
- package/lib/typescript/errors/index.d.ts.map +1 -1
- package/lib/typescript/interfaces.d.ts +1 -1
- package/lib/typescript/interfaces.d.ts.map +1 -1
- package/lib/typescript/specs/NativeAudioAPIModule.d.ts +2 -5
- package/lib/typescript/specs/NativeAudioAPIModule.d.ts.map +1 -1
- package/lib/typescript/specs/NativeAudioAPIModule.web.d.ts +1 -4
- package/lib/typescript/specs/NativeAudioAPIModule.web.d.ts.map +1 -1
- package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts +32 -9
- package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts.map +1 -1
- package/lib/typescript/system/notification/RecordingNotificationManager.d.ts +26 -13
- package/lib/typescript/system/notification/RecordingNotificationManager.d.ts.map +1 -1
- package/lib/typescript/system/notification/RecordingNotificationManager.ios.d.ts +36 -0
- package/lib/typescript/system/notification/RecordingNotificationManager.ios.d.ts.map +1 -0
- package/lib/typescript/system/notification/index.d.ts +0 -1
- package/lib/typescript/system/notification/index.d.ts.map +1 -1
- package/lib/typescript/system/notification/types.d.ts +12 -22
- package/lib/typescript/system/notification/types.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +1 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/utils/index.d.ts.map +1 -1
- package/lib/typescript/utils/paths.d.ts +4 -0
- package/lib/typescript/utils/paths.d.ts.map +1 -0
- package/lib/typescript/web-core/AudioContext.d.ts +8 -9
- package/lib/typescript/web-core/AudioContext.d.ts.map +1 -1
- package/lib/typescript/web-core/BaseAudioContext.d.ts +6 -7
- package/lib/typescript/web-core/BaseAudioContext.d.ts.map +1 -1
- package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts +1 -2
- package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts.map +1 -1
- package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts +2 -7
- package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AudioAPIModule/AudioAPIModule.ts +2 -1
- package/src/api.ts +2 -8
- package/src/core/AudioDecoder.ts +91 -21
- package/src/core/AudioRecorder.ts +2 -1
- package/src/core/AudioStretcher.ts +2 -1
- package/src/core/BaseAudioContext.ts +4 -6
- package/src/errors/AudioApiError.ts +8 -0
- package/src/errors/index.ts +1 -0
- package/src/interfaces.ts +1 -1
- package/src/specs/NativeAudioAPIModule.ts +5 -15
- package/src/specs/NativeAudioAPIModule.web.ts +1 -12
- package/src/system/notification/PlaybackNotificationManager.ts +42 -117
- package/src/system/notification/RecordingNotificationManager.ios.ts +65 -0
- package/src/system/notification/RecordingNotificationManager.ts +33 -183
- package/src/system/notification/index.ts +0 -1
- package/src/system/notification/types.ts +15 -37
- package/src/types.ts +2 -0
- package/src/utils/index.ts +3 -2
- package/src/utils/paths.ts +11 -0
- package/src/web-core/AudioContext.tsx +34 -19
- package/src/web-core/BaseAudioContext.tsx +9 -7
- package/src/web-system/notification/PlaybackNotificationManager.ts +1 -7
- package/src/web-system/notification/RecordingNotificationManager.ts +1 -16
- package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioPlayer.kt +0 -26
- package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioRecorder.kt +0 -26
- package/android/src/main/java/com/swmansion/audioapi/system/notification/SimpleNotification.kt +0 -119
- package/lib/commonjs/system/notification/SimpleNotificationManager.js +0 -125
- package/lib/commonjs/system/notification/SimpleNotificationManager.js.map +0 -1
- package/lib/module/system/notification/SimpleNotificationManager.js +0 -121
- package/lib/module/system/notification/SimpleNotificationManager.js.map +0 -1
- package/lib/typescript/system/notification/SimpleNotificationManager.d.ts +0 -21
- package/lib/typescript/system/notification/SimpleNotificationManager.d.ts.map +0 -1
- package/src/system/notification/SimpleNotificationManager.ts +0 -175
package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt
CHANGED
|
@@ -8,19 +8,17 @@ import android.graphics.Bitmap
|
|
|
8
8
|
import android.graphics.BitmapFactory
|
|
9
9
|
import android.graphics.drawable.BitmapDrawable
|
|
10
10
|
import android.os.Build
|
|
11
|
-
import android.provider.ContactsContract
|
|
12
11
|
import android.support.v4.media.MediaMetadataCompat
|
|
13
12
|
import android.support.v4.media.session.MediaSessionCompat
|
|
14
13
|
import android.support.v4.media.session.PlaybackStateCompat
|
|
15
|
-
import android.util.Log
|
|
16
14
|
import android.view.KeyEvent
|
|
17
15
|
import androidx.core.app.NotificationCompat
|
|
18
|
-
import androidx.core.graphics.drawable.IconCompat
|
|
19
16
|
import androidx.media.app.NotificationCompat.MediaStyle
|
|
20
17
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
21
18
|
import com.facebook.react.bridge.ReadableMap
|
|
22
19
|
import com.facebook.react.bridge.ReadableType
|
|
23
20
|
import com.swmansion.audioapi.AudioAPIModule
|
|
21
|
+
import com.swmansion.audioapi.R
|
|
24
22
|
import java.io.IOException
|
|
25
23
|
import java.lang.ref.WeakReference
|
|
26
24
|
import java.net.URL
|
|
@@ -42,104 +40,94 @@ class PlaybackNotification(
|
|
|
42
40
|
private val channelId: String,
|
|
43
41
|
) : BaseNotification {
|
|
44
42
|
companion object {
|
|
45
|
-
private const val TAG = "PlaybackNotification"
|
|
46
43
|
const val MEDIA_BUTTON = "playback_notification_media_button"
|
|
47
|
-
const val
|
|
44
|
+
const val ACTION_SKIP_FORWARD = "com.swmansion.audioapi.ACTION_SKIP_FORWARD"
|
|
45
|
+
const val ACTION_SKIP_BACKWARD = "com.swmansion.audioapi.ACTION_SKIP_BACKWARD"
|
|
46
|
+
const val ID = 100
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
private var mediaSession: MediaSessionCompat? = null
|
|
51
50
|
private var notificationBuilder: NotificationCompat.Builder? = null
|
|
52
|
-
private var
|
|
53
|
-
private var
|
|
54
|
-
private var
|
|
51
|
+
private var pb: PlaybackStateCompat.Builder = PlaybackStateCompat.Builder()
|
|
52
|
+
private var state: PlaybackStateCompat = pb.build()
|
|
53
|
+
private var controls: Long = 0
|
|
55
54
|
|
|
56
|
-
private var enabledControls: Long = 0
|
|
57
55
|
private var isPlaying: Boolean = false
|
|
56
|
+
private var isInitialized = false
|
|
58
57
|
|
|
59
58
|
// Metadata
|
|
60
59
|
private var title: String? = null
|
|
61
60
|
private var artist: String? = null
|
|
62
61
|
private var album: String? = null
|
|
63
62
|
private var artwork: Bitmap? = null
|
|
64
|
-
private var smallIcon: IconCompat? = null
|
|
65
63
|
private var duration: Long = 0L
|
|
66
64
|
private var elapsedTime: Long = 0L
|
|
67
65
|
private var speed: Float = 1.0F
|
|
68
|
-
|
|
69
|
-
// Actions
|
|
70
|
-
private var playAction: NotificationCompat.Action? = null
|
|
71
|
-
private var pauseAction: NotificationCompat.Action? = null
|
|
72
|
-
private var nextAction: NotificationCompat.Action? = null
|
|
73
|
-
private var previousAction: NotificationCompat.Action? = null
|
|
74
|
-
private var skipForwardAction: NotificationCompat.Action? = null
|
|
75
|
-
private var skipBackwardAction: NotificationCompat.Action? = null
|
|
66
|
+
private var playbackStateVal: Int = PlaybackStateCompat.STATE_PAUSED
|
|
76
67
|
|
|
77
68
|
private var artworkThread: Thread? = null
|
|
78
|
-
private var smallIconThread: Thread? = null
|
|
79
69
|
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
private fun initializeIfNeeded() {
|
|
71
|
+
if (isInitialized) return
|
|
72
|
+
val context = reactContext.get() ?: return
|
|
82
73
|
|
|
83
|
-
// Create notification channel first
|
|
84
74
|
createNotificationChannel()
|
|
85
75
|
|
|
86
|
-
// Create MediaSession
|
|
87
76
|
mediaSession = MediaSessionCompat(context, "PlaybackNotification")
|
|
88
|
-
mediaSession?.isActive = true
|
|
89
77
|
|
|
90
|
-
// Set up media session callbacks
|
|
91
78
|
mediaSession?.setCallback(
|
|
92
79
|
object : MediaSessionCompat.Callback() {
|
|
93
80
|
override fun onPlay() {
|
|
94
|
-
Log.d(TAG, "MediaSession: onPlay")
|
|
95
81
|
audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationPlay", mapOf())
|
|
96
82
|
}
|
|
97
83
|
|
|
98
84
|
override fun onPause() {
|
|
99
|
-
Log.d(TAG, "MediaSession: onPause")
|
|
100
85
|
audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationPause", mapOf())
|
|
101
86
|
}
|
|
102
87
|
|
|
103
88
|
override fun onSkipToNext() {
|
|
104
|
-
Log.d(TAG, "MediaSession: onSkipToNext")
|
|
105
89
|
audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationNext", mapOf())
|
|
106
90
|
}
|
|
107
91
|
|
|
108
92
|
override fun onSkipToPrevious() {
|
|
109
|
-
Log.d(TAG, "MediaSession: onSkipToPrevious")
|
|
110
93
|
audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationPrevious", mapOf())
|
|
111
94
|
}
|
|
112
95
|
|
|
113
96
|
override fun onFastForward() {
|
|
114
|
-
Log.d(TAG, "MediaSession: onFastForward")
|
|
115
97
|
val body = HashMap<String, Any>().apply { put("value", 15) }
|
|
116
98
|
audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationSkipForward", body)
|
|
117
99
|
}
|
|
118
100
|
|
|
119
101
|
override fun onRewind() {
|
|
120
|
-
Log.d(TAG, "MediaSession: onRewind")
|
|
121
102
|
val body = HashMap<String, Any>().apply { put("value", 15) }
|
|
122
103
|
audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationSkipBackward", body)
|
|
123
104
|
}
|
|
124
105
|
|
|
125
106
|
override fun onSeekTo(pos: Long) {
|
|
126
|
-
|
|
127
|
-
val body = HashMap<String, Any>().apply { put("value", pos / 1000.0) } // Convert to seconds
|
|
107
|
+
val body = HashMap<String, Any>().apply { put("value", pos / 1000.0) }
|
|
128
108
|
audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationSeekTo", body)
|
|
129
109
|
}
|
|
110
|
+
|
|
111
|
+
override fun onCustomAction(
|
|
112
|
+
action: String?,
|
|
113
|
+
extras: android.os.Bundle?,
|
|
114
|
+
) {
|
|
115
|
+
if (action == "SkipForward") {
|
|
116
|
+
onFastForward()
|
|
117
|
+
} else if (action == "SkipBackward") {
|
|
118
|
+
onRewind()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
130
121
|
},
|
|
131
122
|
)
|
|
132
123
|
|
|
133
|
-
// Create notification builder
|
|
134
124
|
notificationBuilder =
|
|
135
125
|
NotificationCompat
|
|
136
126
|
.Builder(context, channelId)
|
|
137
127
|
.setSmallIcon(android.R.drawable.ic_media_play)
|
|
138
|
-
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
139
128
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
140
|
-
.
|
|
129
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
141
130
|
|
|
142
|
-
// Set content intent to open app
|
|
143
131
|
val packageName = context.packageName
|
|
144
132
|
val openAppIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
145
133
|
if (openAppIntent != null) {
|
|
@@ -153,7 +141,6 @@ class PlaybackNotification(
|
|
|
153
141
|
notificationBuilder?.setContentIntent(pendingIntent)
|
|
154
142
|
}
|
|
155
143
|
|
|
156
|
-
// Set delete intent to handle dismissal
|
|
157
144
|
val deleteIntent = Intent(PlaybackNotificationReceiver.ACTION_NOTIFICATION_DISMISSED)
|
|
158
145
|
deleteIntent.setPackage(context.packageName)
|
|
159
146
|
val deletePendingIntent =
|
|
@@ -165,468 +152,325 @@ class PlaybackNotification(
|
|
|
165
152
|
)
|
|
166
153
|
notificationBuilder?.setDeleteIntent(deletePendingIntent)
|
|
167
154
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
enableControl("pause", true)
|
|
171
|
-
enableControl("next", true)
|
|
172
|
-
enableControl("previous", true)
|
|
173
|
-
enableControl("seekTo", true)
|
|
155
|
+
pb.setActions(controls)
|
|
156
|
+
mediaSession?.isActive = true
|
|
174
157
|
|
|
175
|
-
|
|
176
|
-
|
|
158
|
+
isInitialized = true
|
|
159
|
+
}
|
|
177
160
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
161
|
+
override fun show(options: ReadableMap?): Notification {
|
|
162
|
+
initializeIfNeeded()
|
|
163
|
+
if (options != null) {
|
|
164
|
+
updateInternal(options)
|
|
181
165
|
}
|
|
182
|
-
|
|
183
166
|
return buildNotification()
|
|
184
167
|
}
|
|
185
168
|
|
|
186
|
-
override fun
|
|
187
|
-
|
|
188
|
-
artworkThread?.interrupt()
|
|
189
|
-
artworkThread = null
|
|
190
|
-
smallIconThread?.interrupt()
|
|
191
|
-
smallIconThread = null
|
|
192
|
-
|
|
193
|
-
// Reset metadata
|
|
194
|
-
title = null
|
|
195
|
-
artist = null
|
|
196
|
-
album = null
|
|
197
|
-
artwork = null
|
|
198
|
-
smallIcon = null
|
|
199
|
-
duration = 0L
|
|
200
|
-
elapsedTime = 0L
|
|
201
|
-
speed = 1.0F
|
|
202
|
-
isPlaying = false
|
|
169
|
+
override fun hide() {
|
|
170
|
+
if (!isInitialized) return
|
|
203
171
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
172
|
+
if (artworkThread != null && artworkThread!!.isAlive) {
|
|
173
|
+
artworkThread!!.interrupt()
|
|
174
|
+
}
|
|
175
|
+
artworkThread = null
|
|
207
176
|
|
|
208
|
-
playbackState =
|
|
209
|
-
playbackStateBuilder
|
|
210
|
-
.setState(PlaybackStateCompat.STATE_NONE, 0, 0f)
|
|
211
|
-
.setActions(enabledControls)
|
|
212
|
-
.build()
|
|
213
|
-
mediaSession?.setPlaybackState(playbackState)
|
|
214
177
|
mediaSession?.isActive = false
|
|
215
178
|
mediaSession?.release()
|
|
216
179
|
mediaSession = null
|
|
180
|
+
notificationBuilder = null
|
|
181
|
+
isInitialized = false
|
|
182
|
+
|
|
183
|
+
controls = 0
|
|
184
|
+
isPlaying = false
|
|
185
|
+
artwork = null
|
|
217
186
|
}
|
|
218
187
|
|
|
219
188
|
override fun getNotificationId(): Int = notificationId
|
|
220
189
|
|
|
221
190
|
override fun getChannelId(): String = channelId
|
|
222
191
|
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
192
|
+
private fun updateInternal(info: ReadableMap) {
|
|
193
|
+
if (info.hasKey("control") && info.hasKey("enabled")) {
|
|
194
|
+
enableControl(info.getString("control"), info.getBoolean("enabled"))
|
|
226
195
|
}
|
|
227
196
|
|
|
228
|
-
|
|
229
|
-
if (options.hasKey("control") && options.hasKey("enabled")) {
|
|
230
|
-
val control = options.getString("control")
|
|
231
|
-
val enabled = options.getBoolean("enabled")
|
|
232
|
-
if (control != null) {
|
|
233
|
-
enableControl(control, enabled)
|
|
234
|
-
}
|
|
235
|
-
return buildNotification()
|
|
236
|
-
}
|
|
197
|
+
val md = MediaMetadataCompat.Builder()
|
|
237
198
|
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
199
|
+
if (info.hasKey("title")) title = info.getString("title")
|
|
200
|
+
if (info.hasKey("artist")) artist = info.getString("artist")
|
|
201
|
+
if (info.hasKey("album")) album = info.getString("album")
|
|
202
|
+
if (info.hasKey("duration")) duration = (info.getDouble("duration") * 1000).toLong()
|
|
242
203
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
204
|
+
md.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
|
205
|
+
md.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
|
206
|
+
md.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
|
|
207
|
+
md.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
|
|
246
208
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
209
|
+
notificationBuilder?.setContentTitle(title)
|
|
210
|
+
notificationBuilder?.setContentText(artist)
|
|
211
|
+
notificationBuilder?.setContentInfo(album)
|
|
250
212
|
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (options.hasKey("elapsedTime")) {
|
|
256
|
-
elapsedTime = (options.getDouble("elapsedTime") * 1000).toLong()
|
|
257
|
-
} else {
|
|
258
|
-
// Use the current position from the media session controller (live calculated position)
|
|
259
|
-
val controllerPosition = mediaSession?.controller?.playbackState?.position
|
|
260
|
-
if (controllerPosition != null && controllerPosition > 0) {
|
|
261
|
-
elapsedTime = controllerPosition
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (options.hasKey("speed")) {
|
|
266
|
-
speed = options.getDouble("speed").toFloat()
|
|
267
|
-
} else {
|
|
268
|
-
// Use the current speed from the media session controller
|
|
269
|
-
val controllerSpeed = mediaSession?.controller?.playbackState?.playbackSpeed
|
|
270
|
-
if (controllerSpeed != null && controllerSpeed > 0) {
|
|
271
|
-
speed = controllerSpeed
|
|
213
|
+
if (info.hasKey("artwork")) {
|
|
214
|
+
if (artworkThread != null && artworkThread!!.isAlive) {
|
|
215
|
+
artworkThread!!.interrupt()
|
|
272
216
|
}
|
|
273
|
-
}
|
|
274
217
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
when (options.getString("state")) {
|
|
283
|
-
"playing" -> {
|
|
284
|
-
playbackPlayingState = PlaybackStateCompat.STATE_PLAYING
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
"paused" -> {
|
|
288
|
-
playbackPlayingState = PlaybackStateCompat.STATE_PAUSED
|
|
218
|
+
var localArtwork = false
|
|
219
|
+
val artworkUri =
|
|
220
|
+
if (info.getType("artwork") == ReadableType.Map) {
|
|
221
|
+
localArtwork = true
|
|
222
|
+
info.getMap("artwork")?.getString("uri")
|
|
223
|
+
} else {
|
|
224
|
+
info.getString("artwork")
|
|
289
225
|
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Build MediaMetadata
|
|
294
|
-
val metadataBuilder =
|
|
295
|
-
MediaMetadataCompat
|
|
296
|
-
.Builder()
|
|
297
|
-
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
|
298
|
-
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
|
299
|
-
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
|
|
300
|
-
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
|
|
301
|
-
|
|
302
|
-
// Update notification builder
|
|
303
|
-
notificationBuilder
|
|
304
|
-
?.setContentTitle(title)
|
|
305
|
-
?.setContentText(artist)
|
|
306
|
-
|
|
307
|
-
// Handle artwork (large icon)
|
|
308
|
-
if (options.hasKey("artwork")) {
|
|
309
|
-
artworkThread?.interrupt()
|
|
310
|
-
|
|
311
|
-
val artworkUrl: String?
|
|
312
|
-
val isLocal: Boolean
|
|
313
|
-
|
|
314
|
-
if (options.getType("artwork") == ReadableType.Map) {
|
|
315
|
-
artworkUrl = options.getMap("artwork")?.getString("uri")
|
|
316
|
-
isLocal = true
|
|
317
|
-
} else {
|
|
318
|
-
artworkUrl = options.getString("artwork")
|
|
319
|
-
isLocal = false
|
|
320
|
-
}
|
|
321
226
|
|
|
322
|
-
if (
|
|
227
|
+
if (artworkUri != null) {
|
|
323
228
|
artworkThread =
|
|
324
229
|
Thread {
|
|
325
230
|
try {
|
|
326
|
-
val bitmap = loadArtwork(
|
|
231
|
+
val bitmap = loadArtwork(artworkUri, localArtwork)
|
|
327
232
|
if (bitmap != null) {
|
|
328
|
-
|
|
233
|
+
artwork = bitmap
|
|
329
234
|
val context = reactContext.get()
|
|
330
235
|
context?.runOnUiQueueThread {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
mediaSession?.setMetadata(updatedBuilder.build())
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Refresh the notification on main thread
|
|
344
|
-
val notificationManager =
|
|
345
|
-
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
|
346
|
-
notificationManager.notify(notificationId, buildNotification())
|
|
347
|
-
} catch (e: Exception) {
|
|
348
|
-
Log.e(TAG, "Error updating notification with artwork: ${e.message}", e)
|
|
349
|
-
}
|
|
236
|
+
notificationBuilder?.setLargeIcon(bitmap)
|
|
237
|
+
|
|
238
|
+
val currentMetadata = mediaSession?.controller?.metadata
|
|
239
|
+
val newBuilder = MediaMetadataCompat.Builder(currentMetadata ?: MediaMetadataCompat.Builder().build())
|
|
240
|
+
mediaSession?.setMetadata(newBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap).build())
|
|
241
|
+
|
|
242
|
+
// Trigger update
|
|
243
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
|
244
|
+
notificationManager.notify(notificationId, buildNotification())
|
|
350
245
|
}
|
|
351
246
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
Log.e(TAG, "Error loading artwork: ${e.message}", e)
|
|
355
|
-
artworkThread = null
|
|
247
|
+
} catch (ex: Exception) {
|
|
248
|
+
ex.printStackTrace()
|
|
356
249
|
}
|
|
357
250
|
}
|
|
358
|
-
artworkThread
|
|
251
|
+
artworkThread!!.start()
|
|
359
252
|
}
|
|
360
253
|
}
|
|
361
254
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
255
|
+
if (info.hasKey("speed")) {
|
|
256
|
+
speed = info.getDouble("speed").toFloat()
|
|
257
|
+
}
|
|
365
258
|
|
|
366
|
-
|
|
367
|
-
|
|
259
|
+
if (isPlaying && speed == 0F) {
|
|
260
|
+
speed = 1F
|
|
261
|
+
}
|
|
368
262
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
isLocal = false
|
|
263
|
+
if (info.hasKey("elapsedTime")) {
|
|
264
|
+
elapsedTime = (info.getDouble("elapsedTime") * 1000).toLong()
|
|
265
|
+
} else {
|
|
266
|
+
if (state.position != PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN) {
|
|
267
|
+
elapsedTime = state.position
|
|
375
268
|
}
|
|
269
|
+
}
|
|
376
270
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
val bitmap = loadArtwork(smallIconUrl, isLocal)
|
|
382
|
-
if (bitmap != null) {
|
|
383
|
-
// Post UI updates to main thread for thread safety
|
|
384
|
-
val context = reactContext.get()
|
|
385
|
-
context?.runOnUiQueueThread {
|
|
386
|
-
try {
|
|
387
|
-
val icon = IconCompat.createWithBitmap(bitmap)
|
|
388
|
-
smallIcon = icon
|
|
389
|
-
notificationBuilder?.setSmallIcon(icon)
|
|
390
|
-
|
|
391
|
-
// Refresh the notification on main thread
|
|
392
|
-
val notificationManager =
|
|
393
|
-
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
|
394
|
-
notificationManager.notify(notificationId, buildNotification())
|
|
395
|
-
} catch (e: Exception) {
|
|
396
|
-
Log.e(TAG, "Error updating notification with small icon: ${e.message}", e)
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
smallIconThread = null
|
|
401
|
-
} catch (e: Exception) {
|
|
402
|
-
Log.e(TAG, "Error loading small icon: ${e.message}", e)
|
|
403
|
-
smallIconThread = null
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
smallIconThread?.start()
|
|
271
|
+
if (info.hasKey("state")) {
|
|
272
|
+
when (info.getString("state")) {
|
|
273
|
+
"playing", "state_playing" -> playbackStateVal = PlaybackStateCompat.STATE_PLAYING
|
|
274
|
+
"paused", "state_paused" -> playbackStateVal = PlaybackStateCompat.STATE_PAUSED
|
|
407
275
|
}
|
|
408
276
|
}
|
|
409
277
|
|
|
410
|
-
updatePlaybackState(
|
|
411
|
-
mediaSession?.setMetadata(metadataBuilder.build())
|
|
412
|
-
mediaSession?.isActive = true
|
|
278
|
+
updatePlaybackState(playbackStateVal)
|
|
413
279
|
|
|
414
|
-
|
|
415
|
-
|
|
280
|
+
if (artwork != null) {
|
|
281
|
+
md.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, artwork)
|
|
282
|
+
}
|
|
283
|
+
mediaSession?.setMetadata(md.build())
|
|
416
284
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
?: throw IllegalStateException("Notification not initialized. Call init() first.")
|
|
285
|
+
updateNotificationsActions()
|
|
286
|
+
}
|
|
420
287
|
|
|
421
|
-
/**
|
|
422
|
-
* Enable or disable a specific control action.
|
|
423
|
-
*/
|
|
424
288
|
private fun enableControl(
|
|
425
|
-
name: String
|
|
289
|
+
name: String?,
|
|
426
290
|
enabled: Boolean,
|
|
427
291
|
) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
292
|
+
if (name == null) return
|
|
293
|
+
var controlValue = 0L
|
|
294
|
+
when (name) {
|
|
295
|
+
"play", "remotePlay" -> controlValue = PlaybackStateCompat.ACTION_PLAY
|
|
296
|
+
"pause", "remotePause" -> controlValue = PlaybackStateCompat.ACTION_PAUSE
|
|
297
|
+
"stop", "remoteStop" -> controlValue = PlaybackStateCompat.ACTION_STOP
|
|
298
|
+
"togglePlayPause", "remoteTogglePlayPause" -> controlValue = PlaybackStateCompat.ACTION_PLAY_PAUSE
|
|
299
|
+
"next", "remoteNextTrack" -> controlValue = PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
|
300
|
+
"previous", "remotePreviousTrack" -> controlValue = PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
|
301
|
+
"skipForward", "remoteSkipForward" -> controlValue = PlaybackStateCompat.ACTION_FAST_FORWARD
|
|
302
|
+
"skipBackward", "remoteSkipBackward" -> controlValue = PlaybackStateCompat.ACTION_REWIND
|
|
303
|
+
"seekTo", "remoteChangePlaybackPosition" -> controlValue = PlaybackStateCompat.ACTION_SEEK_TO
|
|
304
|
+
}
|
|
441
305
|
|
|
442
|
-
|
|
306
|
+
controls =
|
|
443
307
|
if (enabled) {
|
|
444
|
-
|
|
308
|
+
controls or controlValue
|
|
445
309
|
} else {
|
|
446
|
-
|
|
310
|
+
controls and controlValue.inv()
|
|
447
311
|
}
|
|
448
312
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
updateMediaStyle()
|
|
452
|
-
|
|
453
|
-
// Update playback state with new controls
|
|
454
|
-
playbackState =
|
|
455
|
-
playbackStateBuilder
|
|
456
|
-
.setActions(enabledControls)
|
|
457
|
-
.build()
|
|
458
|
-
mediaSession?.setPlaybackState(playbackState)
|
|
313
|
+
updatePlaybackActionState()
|
|
314
|
+
updateNotificationsActions()
|
|
459
315
|
}
|
|
460
316
|
|
|
461
|
-
private fun
|
|
462
|
-
val
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
pauseAction =
|
|
474
|
-
createAction(
|
|
475
|
-
"pause",
|
|
476
|
-
"Pause",
|
|
477
|
-
android.R.drawable.ic_media_pause,
|
|
478
|
-
PlaybackStateCompat.ACTION_PAUSE,
|
|
317
|
+
private fun updatePlaybackActionState() {
|
|
318
|
+
val builder = PlaybackStateCompat.Builder()
|
|
319
|
+
builder.setActions(controls)
|
|
320
|
+
|
|
321
|
+
if (hasControl(PlaybackStateCompat.ACTION_REWIND)) {
|
|
322
|
+
builder.addCustomAction(
|
|
323
|
+
PlaybackStateCompat.CustomAction
|
|
324
|
+
.Builder(
|
|
325
|
+
"SkipBackward",
|
|
326
|
+
"Skip Backward",
|
|
327
|
+
R.drawable.skip_backward_15,
|
|
328
|
+
).build(),
|
|
479
329
|
)
|
|
480
|
-
|
|
481
|
-
nextAction =
|
|
482
|
-
createAction(
|
|
483
|
-
"next",
|
|
484
|
-
"Next",
|
|
485
|
-
android.R.drawable.ic_media_next,
|
|
486
|
-
PlaybackStateCompat.ACTION_SKIP_TO_NEXT,
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
previousAction =
|
|
490
|
-
createAction(
|
|
491
|
-
"previous",
|
|
492
|
-
"Previous",
|
|
493
|
-
android.R.drawable.ic_media_previous,
|
|
494
|
-
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS,
|
|
495
|
-
)
|
|
496
|
-
|
|
497
|
-
skipForwardAction =
|
|
498
|
-
createAction(
|
|
499
|
-
"skip_forward",
|
|
500
|
-
"Skip Forward",
|
|
501
|
-
android.R.drawable.ic_media_ff,
|
|
502
|
-
PlaybackStateCompat.ACTION_FAST_FORWARD,
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
skipBackwardAction =
|
|
506
|
-
createAction(
|
|
507
|
-
"skip_backward",
|
|
508
|
-
"Skip Backward",
|
|
509
|
-
android.R.drawable.ic_media_rew,
|
|
510
|
-
PlaybackStateCompat.ACTION_REWIND,
|
|
511
|
-
)
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
private fun createAction(
|
|
515
|
-
name: String,
|
|
516
|
-
title: String,
|
|
517
|
-
icon: Int,
|
|
518
|
-
action: Long,
|
|
519
|
-
): NotificationCompat.Action? {
|
|
520
|
-
val context = reactContext.get() ?: return null
|
|
521
|
-
|
|
522
|
-
if ((enabledControls and action) == 0L) {
|
|
523
|
-
return null
|
|
524
330
|
}
|
|
525
331
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
keyCode,
|
|
535
|
-
intent,
|
|
536
|
-
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
332
|
+
if (hasControl(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
|
|
333
|
+
builder.addCustomAction(
|
|
334
|
+
PlaybackStateCompat.CustomAction
|
|
335
|
+
.Builder(
|
|
336
|
+
"SkipForward",
|
|
337
|
+
"Skip Forward",
|
|
338
|
+
R.drawable.skip_forward_15,
|
|
339
|
+
).build(),
|
|
537
340
|
)
|
|
341
|
+
}
|
|
538
342
|
|
|
539
|
-
|
|
343
|
+
pb = builder
|
|
540
344
|
}
|
|
541
345
|
|
|
542
|
-
private fun updatePlaybackState(
|
|
543
|
-
isPlaying =
|
|
346
|
+
private fun updatePlaybackState(playbackStateCode: Int) {
|
|
347
|
+
isPlaying = playbackStateCode == PlaybackStateCompat.STATE_PLAYING
|
|
544
348
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
.setActions(enabledControls)
|
|
549
|
-
.build()
|
|
550
|
-
if (mediaSession != null) {
|
|
551
|
-
Log.d(TAG, "mediaSession is not null")
|
|
552
|
-
} else {
|
|
553
|
-
Log.d(TAG, "mediaSession is null")
|
|
554
|
-
}
|
|
555
|
-
mediaSession?.setPlaybackState(playbackState)
|
|
349
|
+
pb.setState(playbackStateCode, elapsedTime, speed)
|
|
350
|
+
state = pb.build()
|
|
351
|
+
mediaSession?.setPlaybackState(state)
|
|
556
352
|
|
|
557
|
-
// Update ongoing state - only persistent when playing
|
|
558
353
|
notificationBuilder?.setOngoing(isPlaying)
|
|
559
354
|
}
|
|
560
355
|
|
|
561
|
-
private fun
|
|
356
|
+
private fun updateNotificationsActions() {
|
|
357
|
+
notificationBuilder?.clearActions()
|
|
358
|
+
|
|
562
359
|
val style = MediaStyle()
|
|
563
360
|
style.setMediaSession(mediaSession?.sessionToken)
|
|
564
361
|
|
|
565
|
-
|
|
566
|
-
notificationBuilder?.clearActions()
|
|
362
|
+
val context = reactContext.get() ?: return
|
|
567
363
|
|
|
568
|
-
|
|
569
|
-
val
|
|
570
|
-
var actionIndex = 0
|
|
364
|
+
var index = 0
|
|
365
|
+
val actionsList = mutableListOf<Int>()
|
|
571
366
|
|
|
572
|
-
if (
|
|
573
|
-
notificationBuilder?.addAction(
|
|
574
|
-
|
|
367
|
+
if (hasControl(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
|
|
368
|
+
notificationBuilder?.addAction(
|
|
369
|
+
createAction("previous", "Previous", android.R.drawable.ic_media_previous, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS),
|
|
370
|
+
)
|
|
371
|
+
actionsList.add(index++)
|
|
575
372
|
}
|
|
576
373
|
|
|
577
|
-
if (
|
|
578
|
-
notificationBuilder?.addAction(
|
|
579
|
-
|
|
374
|
+
if (hasControl(PlaybackStateCompat.ACTION_REWIND)) {
|
|
375
|
+
notificationBuilder?.addAction(
|
|
376
|
+
createAction("skip_backward", "Skip Backward", R.drawable.skip_backward_15, PlaybackStateCompat.ACTION_REWIND),
|
|
377
|
+
)
|
|
378
|
+
actionsList.add(index++)
|
|
580
379
|
}
|
|
581
380
|
|
|
582
|
-
if (
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
381
|
+
if (isPlaying) {
|
|
382
|
+
if (hasControl(PlaybackStateCompat.ACTION_PAUSE)) {
|
|
383
|
+
notificationBuilder?.addAction(
|
|
384
|
+
createAction("pause", "Pause", android.R.drawable.ic_media_pause, PlaybackStateCompat.ACTION_PAUSE),
|
|
385
|
+
)
|
|
386
|
+
actionsList.add(index++)
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
if (hasControl(PlaybackStateCompat.ACTION_PLAY)) {
|
|
390
|
+
notificationBuilder?.addAction(createAction("play", "Play", android.R.drawable.ic_media_play, PlaybackStateCompat.ACTION_PLAY))
|
|
391
|
+
actionsList.add(index++)
|
|
392
|
+
}
|
|
586
393
|
}
|
|
587
394
|
|
|
588
|
-
if (
|
|
589
|
-
notificationBuilder?.addAction(
|
|
590
|
-
|
|
591
|
-
|
|
395
|
+
if (hasControl(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
|
|
396
|
+
notificationBuilder?.addAction(
|
|
397
|
+
createAction("skip_forward", "Skip Forward", R.drawable.skip_forward_15, PlaybackStateCompat.ACTION_FAST_FORWARD),
|
|
398
|
+
)
|
|
399
|
+
actionsList.add(index++)
|
|
592
400
|
}
|
|
593
401
|
|
|
594
|
-
if (
|
|
595
|
-
notificationBuilder?.addAction(
|
|
596
|
-
|
|
402
|
+
if (hasControl(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
|
|
403
|
+
notificationBuilder?.addAction(
|
|
404
|
+
createAction("next", "Next", android.R.drawable.ic_media_next, PlaybackStateCompat.ACTION_SKIP_TO_NEXT),
|
|
405
|
+
)
|
|
406
|
+
actionsList.add(index++)
|
|
597
407
|
}
|
|
598
408
|
|
|
599
|
-
if (
|
|
600
|
-
|
|
601
|
-
|
|
409
|
+
if (actionsList.size > 3) {
|
|
410
|
+
style.setShowActionsInCompactView(actionsList[0], actionsList[1], actionsList[2])
|
|
411
|
+
} else {
|
|
412
|
+
style.setShowActionsInCompactView(*actionsList.toIntArray())
|
|
602
413
|
}
|
|
603
414
|
|
|
604
|
-
// Show up to 3 actions in compact view
|
|
605
|
-
style.setShowActionsInCompactView(*compactActions.take(3).toIntArray())
|
|
606
415
|
notificationBuilder?.setStyle(style)
|
|
607
416
|
}
|
|
608
417
|
|
|
418
|
+
private fun createAction(
|
|
419
|
+
name: String,
|
|
420
|
+
title: String,
|
|
421
|
+
icon: Int,
|
|
422
|
+
mediaAction: Long,
|
|
423
|
+
): NotificationCompat.Action {
|
|
424
|
+
val context = reactContext.get()!!
|
|
425
|
+
val pendingIntent: PendingIntent
|
|
426
|
+
|
|
427
|
+
if (name == "skip_forward" || name == "skip_backward") {
|
|
428
|
+
val customActionName = if (name == "skip_forward") ACTION_SKIP_FORWARD else ACTION_SKIP_BACKWARD
|
|
429
|
+
val intent = Intent(customActionName)
|
|
430
|
+
intent.setPackage(context.packageName)
|
|
431
|
+
pendingIntent =
|
|
432
|
+
PendingIntent.getBroadcast(
|
|
433
|
+
context,
|
|
434
|
+
if (name == "skip_forward") 1001 else 1002,
|
|
435
|
+
intent,
|
|
436
|
+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
437
|
+
)
|
|
438
|
+
} else {
|
|
439
|
+
val keyCode = PlaybackStateCompat.toKeyCode(mediaAction)
|
|
440
|
+
val intent = Intent(MEDIA_BUTTON)
|
|
441
|
+
intent.setPackage(context.packageName)
|
|
442
|
+
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
|
|
443
|
+
pendingIntent =
|
|
444
|
+
PendingIntent.getBroadcast(
|
|
445
|
+
context,
|
|
446
|
+
keyCode,
|
|
447
|
+
intent,
|
|
448
|
+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
return NotificationCompat.Action(icon, title, pendingIntent)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private fun hasControl(control: Long): Boolean = (controls and control) == control
|
|
455
|
+
|
|
609
456
|
private fun loadArtwork(
|
|
610
457
|
url: String,
|
|
611
|
-
|
|
458
|
+
local: Boolean,
|
|
612
459
|
): Bitmap? {
|
|
613
460
|
val context = reactContext.get() ?: return null
|
|
614
461
|
|
|
615
462
|
return try {
|
|
616
|
-
if (
|
|
617
|
-
// Load local resource
|
|
463
|
+
if (local && !url.startsWith("http")) {
|
|
618
464
|
val helper =
|
|
619
465
|
com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
|
|
620
466
|
.getInstance()
|
|
621
467
|
val drawable = helper.getResourceDrawable(context, url)
|
|
622
|
-
|
|
623
468
|
if (drawable is BitmapDrawable) {
|
|
624
469
|
drawable.bitmap
|
|
625
470
|
} else {
|
|
626
471
|
BitmapFactory.decodeFile(url)
|
|
627
472
|
}
|
|
628
473
|
} else {
|
|
629
|
-
// Load from URL
|
|
630
474
|
val connection = URL(url).openConnection()
|
|
631
475
|
connection.connect()
|
|
632
476
|
val inputStream = connection.getInputStream()
|
|
@@ -635,10 +479,8 @@ class PlaybackNotification(
|
|
|
635
479
|
bitmap
|
|
636
480
|
}
|
|
637
481
|
} catch (e: IOException) {
|
|
638
|
-
Log.e(TAG, "Failed to load artwork: ${e.message}", e)
|
|
639
482
|
null
|
|
640
483
|
} catch (e: Exception) {
|
|
641
|
-
Log.e(TAG, "Error loading artwork: ${e.message}", e)
|
|
642
484
|
null
|
|
643
485
|
}
|
|
644
486
|
}
|
|
@@ -646,24 +488,20 @@ class PlaybackNotification(
|
|
|
646
488
|
private fun createNotificationChannel() {
|
|
647
489
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
648
490
|
val context = reactContext.get() ?: return
|
|
649
|
-
|
|
491
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
|
650
492
|
val channel =
|
|
651
|
-
android.app
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
val notificationManager =
|
|
663
|
-
context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
|
664
|
-
notificationManager.createNotificationChannel(channel)
|
|
665
|
-
|
|
666
|
-
Log.d(TAG, "Notification channel created: $channelId")
|
|
493
|
+
android.app.NotificationChannel(
|
|
494
|
+
channelId,
|
|
495
|
+
"Media Playback",
|
|
496
|
+
android.app.NotificationManager.IMPORTANCE_LOW,
|
|
497
|
+
)
|
|
498
|
+
channel.description = "Media playback controls"
|
|
499
|
+
channel.setShowBadge(false)
|
|
500
|
+
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
501
|
+
manager.createNotificationChannel(channel)
|
|
667
502
|
}
|
|
668
503
|
}
|
|
504
|
+
|
|
505
|
+
private fun buildNotification(): Notification =
|
|
506
|
+
notificationBuilder?.build() ?: throw IllegalStateException("Notification not initialized")
|
|
669
507
|
}
|