react-native-mp3 0.1.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.
Files changed (64) hide show
  1. package/android/build.gradle +111 -0
  2. package/android/src/main/AndroidManifest.xml +44 -0
  3. package/android/src/main/java/com/reactnativemp3/Mp3Package.kt +23 -0
  4. package/android/src/main/java/com/reactnativemp3/Mp3TurboModule.kt +43 -0
  5. package/android/src/main/java/com/reactnativemp3/database/MusicDatabase.kt +48 -0
  6. package/android/src/main/java/com/reactnativemp3/database/dao/SongDao.kt +72 -0
  7. package/android/src/main/java/com/reactnativemp3/database/entities/PlaylistEntity.kt +58 -0
  8. package/android/src/main/java/com/reactnativemp3/database/entities/SongEntity.kt +104 -0
  9. package/android/src/main/java/com/reactnativemp3/database/entities/ThumbnailCacheEntity.kt +43 -0
  10. package/android/src/main/java/com/reactnativemp3/managers/CacheManager.kt +0 -0
  11. package/android/src/main/java/com/reactnativemp3/managers/EqualizerManager.kt +0 -0
  12. package/android/src/main/java/com/reactnativemp3/modules/MetadataModule.kt +330 -0
  13. package/android/src/main/java/com/reactnativemp3/modules/NotificationModule.kt +236 -0
  14. package/android/src/main/java/com/reactnativemp3/modules/PlayerModule.kt +710 -0
  15. package/android/src/main/java/com/reactnativemp3/modules/ScannerModule.kt +640 -0
  16. package/android/src/main/java/com/reactnativemp3/services/AudioFocusService.kt +0 -0
  17. package/android/src/main/java/com/reactnativemp3/services/FileObserverService.kt +0 -0
  18. package/android/src/main/java/com/reactnativemp3/services/MusicService.kt +309 -0
  19. package/android/src/main/java/com/reactnativemp3/utils/MediaStoreUtils.kt +0 -0
  20. package/android/src/main/java/com/reactnativemp3/utils/PermissionUtils.kt +0 -0
  21. package/android/src/main/jni/Mp3TurboModule.cpp +29 -0
  22. package/android/src/main/res/drawable/ic_music_note.xml +11 -0
  23. package/android/src/main/res/drawable/ic_pause.xml +11 -0
  24. package/android/src/main/res/drawable/ic_play.xml +11 -0
  25. package/android/src/main/res/drawable/ic_skip_next.xml +11 -0
  26. package/android/src/main/res/drawable/ic_skip_previous.xml +11 -0
  27. package/android/src/main/res/drawable/ic_stop.xml +11 -0
  28. package/lib/components/MusicList.d.ts +0 -0
  29. package/lib/components/MusicList.js +1 -0
  30. package/lib/components/MusicPlayerUI.d.ts +0 -0
  31. package/lib/components/MusicPlayerUI.js +1 -0
  32. package/lib/hooks/useMusicPlayer.d.ts +38 -0
  33. package/lib/hooks/useMusicPlayer.js +242 -0
  34. package/lib/hooks/useMusicScanner.d.ts +27 -0
  35. package/lib/hooks/useMusicScanner.js +217 -0
  36. package/lib/hooks/usePermissions.d.ts +9 -0
  37. package/lib/hooks/usePermissions.js +55 -0
  38. package/lib/index.d.ts +144 -0
  39. package/lib/index.js +148 -0
  40. package/lib/types/common.types.d.ts +79 -0
  41. package/lib/types/common.types.js +2 -0
  42. package/lib/types/index.d.ts +3 -0
  43. package/lib/types/index.js +2 -0
  44. package/lib/types/player.types.d.ts +35 -0
  45. package/lib/types/player.types.js +2 -0
  46. package/lib/types/scanner.types.d.ts +29 -0
  47. package/lib/types/scanner.types.js +2 -0
  48. package/lib/utils/constants.d.ts +31 -0
  49. package/lib/utils/constants.js +55 -0
  50. package/lib/utils/events.d.ts +0 -0
  51. package/lib/utils/events.js +1 -0
  52. package/package.json +62 -0
  53. package/src/components/MusicList.tsx +0 -0
  54. package/src/components/MusicPlayerUI.tsx +0 -0
  55. package/src/hooks/useMusicPlayer.ts +358 -0
  56. package/src/hooks/useMusicScanner.ts +286 -0
  57. package/src/hooks/usePermissions.ts +64 -0
  58. package/src/index.ts +214 -0
  59. package/src/types/common.types.ts +86 -0
  60. package/src/types/index.ts +4 -0
  61. package/src/types/player.types.ts +37 -0
  62. package/src/types/scanner.types.ts +31 -0
  63. package/src/utils/constants.ts +56 -0
  64. package/src/utils/events.ts +0 -0
@@ -0,0 +1,330 @@
1
+ package com.reactnativemp3.modules
2
+
3
+ import android.content.ContentResolver
4
+ import android.graphics.Bitmap
5
+ import android.graphics.BitmapFactory
6
+ import android.media.MediaMetadataRetriever
7
+ import android.net.Uri
8
+ import android.util.Base64
9
+ import com.facebook.react.bridge.*
10
+ import com.reactnativemp3.Mp3TurboModule
11
+ import kotlinx.coroutines.*
12
+ import java.io.ByteArrayOutputStream
13
+ import java.io.File
14
+
15
+ @ReactModule(name = MetadataModule.NAME)
16
+ class MetadataModule(reactContext: ReactApplicationContext) :
17
+ Mp3TurboModule(reactContext, NAME) {
18
+
19
+ companion object {
20
+ const val NAME = "MetadataModule"
21
+ }
22
+
23
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
24
+
25
+ @ReactMethod
26
+ fun extractMetadata(uri: String, promise: Promise) {
27
+ scope.launch {
28
+ try {
29
+ val metadata = extractMetadataFromUri(uri)
30
+ withContext(Dispatchers.Main) {
31
+ promise.resolve(metadata)
32
+ }
33
+ } catch (e: Exception) {
34
+ withContext(Dispatchers.Main) {
35
+ promise.reject("METADATA_ERROR", "Failed to extract metadata", e)
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ @ReactMethod
42
+ fun getAlbumArt(uri: String, size: Int, promise: Promise) {
43
+ scope.launch {
44
+ try {
45
+ val albumArt = getAlbumArtFromUri(uri, size)
46
+ withContext(Dispatchers.Main) {
47
+ promise.resolve(albumArt)
48
+ }
49
+ } catch (e: Exception) {
50
+ withContext(Dispatchers.Main) {
51
+ promise.reject("ALBUM_ART_ERROR", "Failed to get album art", e)
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ @ReactMethod
58
+ fun getLyrics(uri: String, promise: Promise) {
59
+ scope.launch {
60
+ try {
61
+ val lyrics = extractLyrics(uri)
62
+ withContext(Dispatchers.Main) {
63
+ promise.resolve(lyrics)
64
+ }
65
+ } catch (e: Exception) {
66
+ withContext(Dispatchers.Main) {
67
+ promise.reject("LYRICS_ERROR", "Failed to extract lyrics", e)
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ @ReactMethod
74
+ fun updateMetadata(uri: String, metadata: ReadableMap, promise: Promise) {
75
+ // Note: Updating metadata of audio files is complex and requires external libraries
76
+ // This is a placeholder implementation
77
+ scope.launch {
78
+ try {
79
+ // For now, just return success
80
+ withContext(Dispatchers.Main) {
81
+ promise.resolve(true)
82
+ }
83
+ } catch (e: Exception) {
84
+ withContext(Dispatchers.Main) {
85
+ promise.reject("UPDATE_ERROR", "Failed to update metadata", e)
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ private fun extractMetadataFromUri(uriString: String): WritableMap {
92
+ val metadata = Arguments.createMap()
93
+ val retriever = MediaMetadataRetriever()
94
+
95
+ try {
96
+ val uri = Uri.parse(uriString)
97
+
98
+ if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
99
+ reactApplicationContext?.contentResolver?.let { resolver ->
100
+ resolver.openFileDescriptor(uri, "r")?.use { pfd ->
101
+ retriever.setDataSource(pfd.fileDescriptor)
102
+ }
103
+ }
104
+ } else if (uri.scheme == ContentResolver.SCHEME_FILE) {
105
+ retriever.setDataSource(uri.path)
106
+ } else {
107
+ retriever.setDataSource(uriString)
108
+ }
109
+
110
+ // Extract all available metadata
111
+ extractStandardMetadata(retriever, metadata)
112
+ extractExtendedMetadata(retriever, metadata)
113
+
114
+ } finally {
115
+ retriever.release()
116
+ }
117
+
118
+ return metadata
119
+ }
120
+
121
+ private fun extractStandardMetadata(retriever: MediaMetadataRetriever, metadata: WritableMap) {
122
+ // Title
123
+ val title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
124
+ metadata.putString("title", title ?: "")
125
+
126
+ // Artist
127
+ val artist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
128
+ metadata.putString("artist", artist ?: "Unknown Artist")
129
+
130
+ // Album
131
+ val album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)
132
+ metadata.putString("album", album ?: "Unknown Album")
133
+
134
+ // Album Artist
135
+ val albumArtist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST)
136
+ metadata.putString("albumArtist", albumArtist ?: artist)
137
+
138
+ // Genre
139
+ val genre = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)
140
+ metadata.putString("genre", genre ?: "")
141
+
142
+ // Duration
143
+ val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
144
+ metadata.putDouble("duration", duration?.toLongOrNull()?.toDouble() ?: 0.0)
145
+
146
+ // Year
147
+ val year = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR)
148
+ metadata.putString("year", year ?: "")
149
+
150
+ // Track Number
151
+ val trackNumber = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER)
152
+ metadata.putString("trackNumber", trackNumber ?: "0")
153
+
154
+ // Disc Number
155
+ val discNumber = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER)
156
+ metadata.putString("discNumber", discNumber ?: "0")
157
+
158
+ // Composer
159
+ val composer = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER)
160
+ metadata.putString("composer", composer ?: "")
161
+
162
+ // Writer
163
+ val writer = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_WRITER)
164
+ metadata.putString("writer", writer ?: "")
165
+
166
+ // Bitrate
167
+ val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
168
+ metadata.putInt("bitrate", bitrate?.toIntOrNull() ?: 0)
169
+
170
+ // Sample Rate
171
+ val sampleRate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE)
172
+ metadata.putInt("sampleRate", sampleRate?.toIntOrNull() ?: 0)
173
+
174
+ // Channels
175
+ val channels = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS)
176
+ metadata.putInt("channels", channels?.toIntOrNull() ?: 2)
177
+
178
+ // MIME Type
179
+ val mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
180
+ metadata.putString("mimeType", mimeType ?: "audio/mpeg")
181
+ }
182
+
183
+ private fun extractExtendedMetadata(retriever: MediaMetadataRetriever, metadata: WritableMap) {
184
+ // Lyrics (embedded)
185
+ val lyrics = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LYRICIST)
186
+ metadata.putString("lyrics", lyrics ?: "")
187
+
188
+ // Has Album Art
189
+ val picture = retriever.embeddedPicture
190
+ metadata.putBoolean("hasEmbeddedArt", picture != null)
191
+
192
+ // Location (GPS coordinates if available in some audio formats)
193
+ val location = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
194
+ metadata.putString("location", location ?: "")
195
+
196
+ // Copyright
197
+ val copyright = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COPYRIGHT)
198
+ metadata.putString("copyright", copyright ?: "")
199
+
200
+ // Encoder
201
+ val encoder = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ENCODER)
202
+ metadata.putString("encoder", encoder ?: "")
203
+
204
+ // Date
205
+ val date = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)
206
+ metadata.putString("date", date ?: "")
207
+
208
+ // Capturing information for podcasts/audiobooks
209
+ val captureSessionId = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_SESSION_ID)
210
+ metadata.putString("captureSessionId", captureSessionId ?: "")
211
+ }
212
+
213
+ private fun getAlbumArtFromUri(uriString: String, size: Int): String {
214
+ val retriever = MediaMetadataRetriever()
215
+
216
+ try {
217
+ val uri = Uri.parse(uriString)
218
+
219
+ if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
220
+ reactApplicationContext?.contentResolver?.let { resolver ->
221
+ resolver.openFileDescriptor(uri, "r")?.use { pfd ->
222
+ retriever.setDataSource(pfd.fileDescriptor)
223
+ }
224
+ }
225
+ } else if (uri.scheme == ContentResolver.SCHEME_FILE) {
226
+ retriever.setDataSource(uri.path)
227
+ } else {
228
+ retriever.setDataSource(uriString)
229
+ }
230
+
231
+ // Try to get embedded picture
232
+ val picture = retriever.embeddedPicture
233
+ if (picture != null) {
234
+ return encodeBitmapToBase64(picture, size)
235
+ }
236
+
237
+ // If no embedded picture, try to get album art from MediaStore
238
+ val albumId = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)
239
+ if (albumId != null) {
240
+ // Try to find album art in MediaStore
241
+ // This is simplified - in real implementation you'd query MediaStore
242
+ }
243
+
244
+ } finally {
245
+ retriever.release()
246
+ }
247
+
248
+ return ""
249
+ }
250
+
251
+ private fun encodeBitmapToBase64(picture: ByteArray, targetSize: Int): String {
252
+ try {
253
+ // Decode the bitmap
254
+ val options = BitmapFactory.Options()
255
+ options.inJustDecodeBounds = true
256
+ BitmapFactory.decodeByteArray(picture, 0, picture.size, options)
257
+
258
+ // Calculate inSampleSize
259
+ options.inSampleSize = calculateInSampleSize(options, targetSize, targetSize)
260
+ options.inJustDecodeBounds = false
261
+
262
+ // Decode bitmap with sampling
263
+ val bitmap = BitmapFactory.decodeByteArray(picture, 0, picture.size, options)
264
+ ?: return ""
265
+
266
+ // Compress to JPEG
267
+ val outputStream = ByteArrayOutputStream()
268
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
269
+ val compressedBytes = outputStream.toByteArray()
270
+
271
+ // Encode to Base64
272
+ return Base64.encodeToString(compressedBytes, Base64.DEFAULT)
273
+
274
+ } catch (e: Exception) {
275
+ return ""
276
+ }
277
+ }
278
+
279
+ private fun calculateInSampleSize(
280
+ options: BitmapFactory.Options,
281
+ reqWidth: Int,
282
+ reqHeight: Int
283
+ ): Int {
284
+ val height = options.outHeight
285
+ val width = options.outWidth
286
+ var inSampleSize = 1
287
+
288
+ if (height > reqHeight || width > reqWidth) {
289
+ val halfHeight = height / 2
290
+ val halfWidth = width / 2
291
+
292
+ while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
293
+ inSampleSize *= 2
294
+ }
295
+ }
296
+
297
+ return inSampleSize
298
+ }
299
+
300
+ private fun extractLyrics(uriString: String): String? {
301
+ val retriever = MediaMetadataRetriever()
302
+
303
+ try {
304
+ val uri = Uri.parse(uriString)
305
+
306
+ if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
307
+ reactApplicationContext?.contentResolver?.let { resolver ->
308
+ resolver.openFileDescriptor(uri, "r")?.use { pfd ->
309
+ retriever.setDataSource(pfd.fileDescriptor)
310
+ }
311
+ }
312
+ } else if (uri.scheme == ContentResolver.SCHEME_FILE) {
313
+ retriever.setDataSource(uri.path)
314
+ } else {
315
+ retriever.setDataSource(uriString)
316
+ }
317
+
318
+ // Try to get lyrics from metadata
319
+ return retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LYRICIST)
320
+
321
+ } finally {
322
+ retriever.release()
323
+ }
324
+ }
325
+
326
+ override fun onCatalystInstanceDestroy() {
327
+ super.onCatalystInstanceDestroy()
328
+ scope.cancel()
329
+ }
330
+ }
@@ -0,0 +1,236 @@
1
+ package com.reactnativemp3.modules
2
+
3
+ import android.app.*
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.os.Build
9
+ import android.support.v4.media.session.MediaSessionCompat
10
+ import androidx.core.app.NotificationCompat
11
+ import androidx.media.app.NotificationCompat as MediaNotificationCompat
12
+ import com.facebook.react.bridge.*
13
+ import com.reactnativemp3.Mp3TurboModule
14
+ import com.reactnativemp3.R
15
+ import kotlinx.coroutines.*
16
+ import java.net.URL
17
+
18
+ @ReactModule(name = NotificationModule.NAME)
19
+ class NotificationModule(reactContext: ReactApplicationContext) :
20
+ Mp3TurboModule(reactContext, NAME) {
21
+
22
+ companion object {
23
+ const val NAME = "NotificationModule"
24
+ const val NOTIFICATION_CHANNEL_ID = "music_player_channel"
25
+ const val NOTIFICATION_ID = 1001
26
+ const val ACTION_PLAY = "com.reactnativemp3.ACTION_PLAY"
27
+ const val ACTION_PAUSE = "com.reactnativemp3.ACTION_PAUSE"
28
+ const val ACTION_NEXT = "com.reactnativemp3.ACTION_NEXT"
29
+ const val ACTION_PREVIOUS = "com.reactnativemp3.ACTION_PREVIOUS"
30
+ const val ACTION_STOP = "com.reactnativemp3.ACTION_STOP"
31
+ }
32
+
33
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
34
+ private lateinit var notificationManager: NotificationManager
35
+ private var mediaSession: MediaSessionCompat? = null
36
+ private var currentNotification: Notification? = null
37
+
38
+ override fun initialize() {
39
+ super.initialize()
40
+ notificationManager = reactApplicationContext
41
+ .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
42
+ setupNotificationChannel()
43
+ }
44
+
45
+ @ReactMethod
46
+ fun setupNotificationChannel() {
47
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
48
+ val channel = NotificationChannel(
49
+ NOTIFICATION_CHANNEL_ID,
50
+ "Music Player",
51
+ NotificationManager.IMPORTANCE_LOW
52
+ ).apply {
53
+ description = "Music playback controls"
54
+ setShowBadge(false)
55
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
56
+ setSound(null, null)
57
+ }
58
+
59
+ notificationManager.createNotificationChannel(channel)
60
+ }
61
+ }
62
+
63
+ @ReactMethod
64
+ fun updateNotification(metadata: ReadableMap) {
65
+ scope.launch {
66
+ try {
67
+ val notification = buildNotification(metadata)
68
+ currentNotification = notification
69
+
70
+ withContext(Dispatchers.Main) {
71
+ notificationManager.notify(NOTIFICATION_ID, notification)
72
+ }
73
+ } catch (e: Exception) {
74
+ // Log error but don't crash
75
+ e.printStackTrace()
76
+ }
77
+ }
78
+ }
79
+
80
+ @ReactMethod
81
+ fun showNotification() {
82
+ currentNotification?.let {
83
+ notificationManager.notify(NOTIFICATION_ID, it)
84
+ }
85
+ }
86
+
87
+ @ReactMethod
88
+ fun hideNotification() {
89
+ notificationManager.cancel(NOTIFICATION_ID)
90
+ }
91
+
92
+ @ReactMethod
93
+ fun setMediaSessionToken(sessionToken: String, promise: Promise) {
94
+ // This would be used to connect to MediaSession from service
95
+ promise.resolve(true)
96
+ }
97
+
98
+ private suspend fun buildNotification(metadata: ReadableMap): Notification {
99
+ val context = reactApplicationContext ?: throw IllegalStateException("Context not available")
100
+
101
+ // Create media session if not exists
102
+ if (mediaSession == null) {
103
+ mediaSession = MediaSessionCompat(context, "MusicPlayer").apply {
104
+ setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
105
+ MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
106
+ }
107
+ }
108
+
109
+ // Extract metadata
110
+ val title = metadata.getString("title") ?: "Unknown Title"
111
+ val artist = metadata.getString("artist") ?: "Unknown Artist"
112
+ val album = metadata.getString("album") ?: "Unknown Album"
113
+ val albumArtUri = metadata.getString("albumArtUri")
114
+ val isPlaying = metadata.getBoolean("isPlaying")
115
+
116
+ // Load album art
117
+ val albumArt = loadAlbumArt(albumArtUri)
118
+
119
+ // Create pending intents for actions
120
+ val playIntent = createPendingIntent(ACTION_PLAY)
121
+ val pauseIntent = createPendingIntent(ACTION_PAUSE)
122
+ val nextIntent = createPendingIntent(ACTION_NEXT)
123
+ val previousIntent = createPendingIntent(ACTION_PREVIOUS)
124
+ val stopIntent = createPendingIntent(ACTION_STOP)
125
+
126
+ // Build notification
127
+ val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
128
+ .setSmallIcon(R.drawable.ic_music_note)
129
+ .setContentTitle(title)
130
+ .setContentText(artist)
131
+ .setSubText(album)
132
+ .setLargeIcon(albumArt)
133
+ .setStyle(
134
+ androidx.media.app.NotificationCompat.MediaStyle()
135
+ .setMediaSession(mediaSession?.sessionToken)
136
+ .setShowActionsInCompactView(0, 1, 2)
137
+ )
138
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
139
+ .setPriority(NotificationCompat.PRIORITY_LOW)
140
+ .setOngoing(isPlaying)
141
+ .setShowWhen(false)
142
+ .setAutoCancel(false)
143
+ .setDeleteIntent(stopIntent)
144
+
145
+ // Add actions
146
+ if (isPlaying) {
147
+ builder.addAction(
148
+ R.drawable.ic_pause,
149
+ "Pause",
150
+ pauseIntent
151
+ )
152
+ } else {
153
+ builder.addAction(
154
+ R.drawable.ic_play,
155
+ "Play",
156
+ playIntent
157
+ )
158
+ }
159
+
160
+ builder.addAction(
161
+ R.drawable.ic_skip_previous,
162
+ "Previous",
163
+ previousIntent
164
+ ).addAction(
165
+ R.drawable.ic_skip_next,
166
+ "Next",
167
+ nextIntent
168
+ ).addAction(
169
+ R.drawable.ic_stop,
170
+ "Stop",
171
+ stopIntent
172
+ )
173
+
174
+ return builder.build()
175
+ }
176
+
177
+ private suspend fun loadAlbumArt(albumArtUri: String?): Bitmap? {
178
+ if (albumArtUri.isNullOrEmpty()) {
179
+ return null
180
+ }
181
+
182
+ return withContext(Dispatchers.IO) {
183
+ try {
184
+ when {
185
+ albumArtUri.startsWith("http://") || albumArtUri.startsWith("https://") -> {
186
+ // Load from URL
187
+ URL(albumArtUri).openStream().use { input ->
188
+ BitmapFactory.decodeStream(input)
189
+ }
190
+ }
191
+ albumArtUri.startsWith("content://") -> {
192
+ // Load from content URI
193
+ val uri = android.net.Uri.parse(albumArtUri)
194
+ reactApplicationContext?.contentResolver?.openInputStream(uri)?.use { input ->
195
+ BitmapFactory.decodeStream(input)
196
+ }
197
+ }
198
+ albumArtUri.startsWith("file://") -> {
199
+ // Load from file
200
+ BitmapFactory.decodeFile(albumArtUri.removePrefix("file://"))
201
+ }
202
+ else -> {
203
+ // Try as file path
204
+ BitmapFactory.decodeFile(albumArtUri)
205
+ }
206
+ }
207
+ } catch (e: Exception) {
208
+ // Return null if can't load
209
+ null
210
+ }
211
+ }
212
+ }
213
+
214
+ private fun createPendingIntent(action: String): PendingIntent {
215
+ val context = reactApplicationContext ?: throw IllegalStateException("Context not available")
216
+
217
+ val intent = Intent(action).apply {
218
+ setPackage(context.packageName)
219
+ }
220
+
221
+ val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
222
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
223
+ } else {
224
+ PendingIntent.FLAG_UPDATE_CURRENT
225
+ }
226
+
227
+ return PendingIntent.getBroadcast(context, action.hashCode(), intent, flags)
228
+ }
229
+
230
+ override fun onCatalystInstanceDestroy() {
231
+ super.onCatalystInstanceDestroy()
232
+ scope.cancel()
233
+ mediaSession?.release()
234
+ mediaSession = null
235
+ }
236
+ }