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