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.
- package/android/build.gradle +111 -0
- package/android/src/main/AndroidManifest.xml +44 -0
- package/android/src/main/java/com/reactnativemp3/Mp3Package.kt +23 -0
- package/android/src/main/java/com/reactnativemp3/Mp3TurboModule.kt +43 -0
- package/android/src/main/java/com/reactnativemp3/database/MusicDatabase.kt +48 -0
- package/android/src/main/java/com/reactnativemp3/database/dao/SongDao.kt +72 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/PlaylistEntity.kt +58 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/SongEntity.kt +104 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/ThumbnailCacheEntity.kt +43 -0
- package/android/src/main/java/com/reactnativemp3/managers/CacheManager.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/managers/EqualizerManager.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/modules/MetadataModule.kt +330 -0
- package/android/src/main/java/com/reactnativemp3/modules/NotificationModule.kt +236 -0
- package/android/src/main/java/com/reactnativemp3/modules/PlayerModule.kt +710 -0
- package/android/src/main/java/com/reactnativemp3/modules/ScannerModule.kt +640 -0
- package/android/src/main/java/com/reactnativemp3/services/AudioFocusService.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/services/FileObserverService.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/services/MusicService.kt +309 -0
- package/android/src/main/java/com/reactnativemp3/utils/MediaStoreUtils.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/utils/PermissionUtils.kt +0 -0
- package/android/src/main/jni/Mp3TurboModule.cpp +29 -0
- package/android/src/main/res/drawable/ic_music_note.xml +11 -0
- package/android/src/main/res/drawable/ic_pause.xml +11 -0
- package/android/src/main/res/drawable/ic_play.xml +11 -0
- package/android/src/main/res/drawable/ic_skip_next.xml +11 -0
- package/android/src/main/res/drawable/ic_skip_previous.xml +11 -0
- package/android/src/main/res/drawable/ic_stop.xml +11 -0
- package/lib/components/MusicList.d.ts +0 -0
- package/lib/components/MusicList.js +1 -0
- package/lib/components/MusicPlayerUI.d.ts +0 -0
- package/lib/components/MusicPlayerUI.js +1 -0
- package/lib/hooks/useMusicPlayer.d.ts +38 -0
- package/lib/hooks/useMusicPlayer.js +242 -0
- package/lib/hooks/useMusicScanner.d.ts +27 -0
- package/lib/hooks/useMusicScanner.js +217 -0
- package/lib/hooks/usePermissions.d.ts +9 -0
- package/lib/hooks/usePermissions.js +55 -0
- package/lib/index.d.ts +144 -0
- package/lib/index.js +148 -0
- package/lib/types/common.types.d.ts +79 -0
- package/lib/types/common.types.js +2 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +2 -0
- package/lib/types/player.types.d.ts +35 -0
- package/lib/types/player.types.js +2 -0
- package/lib/types/scanner.types.d.ts +29 -0
- package/lib/types/scanner.types.js +2 -0
- package/lib/utils/constants.d.ts +31 -0
- package/lib/utils/constants.js +55 -0
- package/lib/utils/events.d.ts +0 -0
- package/lib/utils/events.js +1 -0
- package/package.json +62 -0
- package/src/components/MusicList.tsx +0 -0
- package/src/components/MusicPlayerUI.tsx +0 -0
- package/src/hooks/useMusicPlayer.ts +358 -0
- package/src/hooks/useMusicScanner.ts +286 -0
- package/src/hooks/usePermissions.ts +64 -0
- package/src/index.ts +214 -0
- package/src/types/common.types.ts +86 -0
- package/src/types/index.ts +4 -0
- package/src/types/player.types.ts +37 -0
- package/src/types/scanner.types.ts +31 -0
- package/src/utils/constants.ts +56 -0
- 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
|
+
}
|