rns-mediapicker 0.0.4 → 0.0.6
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/README.md
CHANGED
|
@@ -52,6 +52,8 @@ pick(useCamera, mediaType, env)
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
response-object
|
|
55
|
+
```
|
|
56
|
+
```js
|
|
55
57
|
{
|
|
56
58
|
uri: string; // path to the processed file
|
|
57
59
|
width: number; // media width
|
|
@@ -59,8 +61,11 @@ response-object
|
|
|
59
61
|
type: string; // 'image' or 'video'
|
|
60
62
|
isVideo: boolean; // helper flag
|
|
61
63
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
```
|
|
65
|
+
```bash
|
|
66
|
+
features-included
|
|
67
|
+
```
|
|
68
|
+
```js
|
|
64
69
|
1. Android 11+ Intent Queries: Automatically handled via plugin.
|
|
65
70
|
2. FileProvider: Safe URI sharing handled internally.
|
|
66
71
|
3. iOS Permissions: Usage descriptions injected into Info.plist.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.rnsmediapicker
|
|
2
2
|
|
|
3
|
+
import android.Manifest
|
|
3
4
|
import android.app.Activity
|
|
4
5
|
import android.content.Intent
|
|
5
6
|
import android.graphics.*
|
|
@@ -27,10 +28,12 @@ class FastMediaPickerModule(
|
|
|
27
28
|
private var pickerPromise: Promise? = null
|
|
28
29
|
private var cameraCaptureUri: Uri? = null
|
|
29
30
|
private var pendingFrontCamera = false
|
|
31
|
+
private var pendingUseCamera = false
|
|
32
|
+
private var pendingMediaType: String? = null
|
|
30
33
|
|
|
31
34
|
companion object {
|
|
32
35
|
private const val PICKER_REQUEST_CODE = 4123
|
|
33
|
-
private const val
|
|
36
|
+
private const val PERMISSION_REQUEST = 9123
|
|
34
37
|
private const val MAX_IMAGE_SIZE = 1200
|
|
35
38
|
private const val JPEG_QUALITY = 85
|
|
36
39
|
}
|
|
@@ -41,31 +44,13 @@ class FastMediaPickerModule(
|
|
|
41
44
|
|
|
42
45
|
override fun getName(): String = "FastMediaPicker"
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
@ReactMethod fun addListener(eventName: String) {}
|
|
45
48
|
|
|
46
|
-
@ReactMethod
|
|
47
|
-
fun addListener(eventName: String) {
|
|
48
|
-
// Required for RN event emitter (even if unused)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
@ReactMethod
|
|
52
|
-
fun removeListeners(count: Int) {
|
|
53
|
-
// Required for RN event emitter (even if unused)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private fun sendLog(message: String) {
|
|
57
|
-
android.util.Log.d("FastMediaPicker", message)
|
|
58
|
-
try {
|
|
59
|
-
reactContext
|
|
60
|
-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
61
|
-
.emit("onPickerLog", message)
|
|
62
|
-
} catch (_: Exception) {
|
|
63
|
-
}
|
|
64
|
-
}
|
|
49
|
+
@ReactMethod fun removeListeners(count: Int) {}
|
|
65
50
|
|
|
66
51
|
// -------------------- PUBLIC API --------------------
|
|
67
52
|
|
|
68
|
-
|
|
53
|
+
@ReactMethod
|
|
69
54
|
fun pick(
|
|
70
55
|
useCamera: Boolean,
|
|
71
56
|
mediaType: String,
|
|
@@ -74,43 +59,126 @@ class FastMediaPickerModule(
|
|
|
74
59
|
) {
|
|
75
60
|
val activity =
|
|
76
61
|
currentActivity ?: run {
|
|
77
|
-
promise.reject("
|
|
62
|
+
promise.reject("E_NO_ACTIVITY", "Activity not found")
|
|
78
63
|
return
|
|
79
64
|
}
|
|
80
65
|
|
|
81
66
|
cleanup()
|
|
82
67
|
pickerPromise = promise
|
|
68
|
+
pendingUseCamera = useCamera
|
|
69
|
+
pendingMediaType = mediaType
|
|
83
70
|
pendingFrontCamera = env?.lowercase() == "front"
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
72
|
+
val permissions = mutableListOf<String>()
|
|
73
|
+
|
|
74
|
+
if (useCamera) {
|
|
75
|
+
// Camera always needs permission
|
|
76
|
+
permissions.add(Manifest.permission.CAMERA)
|
|
77
|
+
} else {
|
|
78
|
+
// GALLERY LOGIC:
|
|
79
|
+
// Android 13 (API 33) and above use the Photo Picker which REQUIRES NO PERMISSIONS.
|
|
80
|
+
// We only request storage permissions for older devices (API 32 and below).
|
|
81
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
82
|
+
permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Filter out permissions that are already granted
|
|
87
|
+
val missing = permissions.filter {
|
|
88
|
+
activity.checkSelfPermission(it) != android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (missing.isNotEmpty()) {
|
|
92
|
+
val pa = activity as? PermissionAwareActivity ?: run {
|
|
93
|
+
promise.reject("E_NOT_PERMISSION_AWARE", "Activity not PermissionAware")
|
|
94
|
+
cleanup()
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pa.requestPermissions(
|
|
99
|
+
missing.toTypedArray(),
|
|
100
|
+
PERMISSION_REQUEST,
|
|
101
|
+
this,
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// All permissions granted (or none needed), launch the picker/camera
|
|
107
|
+
launch(activity)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Android 14 (API 34+) introduces Partial Access
|
|
111
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
112
|
+
permissions.add(Manifest.permission.READ_MEDIA_IMAGES)
|
|
113
|
+
permissions.add(Manifest.permission.READ_MEDIA_VIDEO)
|
|
114
|
+
permissions.add(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
|
|
115
|
+
}
|
|
116
|
+
// Android 13 (API 33)
|
|
117
|
+
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
118
|
+
if (mediaType == "video") {
|
|
119
|
+
permissions.add(Manifest.permission.READ_MEDIA_VIDEO)
|
|
120
|
+
} else if (mediaType == "image") {
|
|
121
|
+
permissions.add(Manifest.permission.READ_MEDIA_IMAGES)
|
|
107
122
|
} else {
|
|
108
|
-
|
|
123
|
+
permissions.add(Manifest.permission.READ_MEDIA_IMAGES)
|
|
124
|
+
permissions.add(Manifest.permission.READ_MEDIA_VIDEO)
|
|
109
125
|
}
|
|
110
|
-
}
|
|
111
|
-
|
|
126
|
+
}
|
|
127
|
+
// Android 12 and below
|
|
128
|
+
else {
|
|
129
|
+
permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
val missing =
|
|
133
|
+
permissions.filter {
|
|
134
|
+
activity.checkSelfPermission(it) !=
|
|
135
|
+
android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (missing.isNotEmpty()) {
|
|
139
|
+
val pa =
|
|
140
|
+
activity as? PermissionAwareActivity ?: run {
|
|
141
|
+
promise.reject("E_NOT_PERMISSION_AWARE", "Activity not PermissionAware")
|
|
142
|
+
cleanup()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pa.requestPermissions(
|
|
147
|
+
missing.toTypedArray(),
|
|
148
|
+
PERMISSION_REQUEST,
|
|
149
|
+
this,
|
|
150
|
+
)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
launch(activity)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun launch(activity: Activity) {
|
|
158
|
+
if (pendingUseCamera) {
|
|
159
|
+
launchCamera(activity, pendingFrontCamera)
|
|
160
|
+
} else {
|
|
161
|
+
launchPicker(activity, pendingMediaType ?: "image")
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// -------------------- PERMISSIONS CALLBACK --------------------
|
|
166
|
+
|
|
167
|
+
override fun onRequestPermissionsResult(
|
|
168
|
+
requestCode: Int,
|
|
169
|
+
permissions: Array<String>,
|
|
170
|
+
grantResults: IntArray,
|
|
171
|
+
): Boolean {
|
|
172
|
+
if (requestCode != PERMISSION_REQUEST) return false
|
|
173
|
+
|
|
174
|
+
if (grantResults.any { it != android.content.pm.PackageManager.PERMISSION_GRANTED }) {
|
|
175
|
+
pickerPromise?.reject("E_PERMISSION_DENIED", "Required permission denied")
|
|
112
176
|
cleanup()
|
|
177
|
+
return true
|
|
113
178
|
}
|
|
179
|
+
|
|
180
|
+
currentActivity?.let { launch(it) }
|
|
181
|
+
return true
|
|
114
182
|
}
|
|
115
183
|
|
|
116
184
|
// -------------------- PICKER / CAMERA --------------------
|
|
@@ -119,51 +187,60 @@ class FastMediaPickerModule(
|
|
|
119
187
|
activity: Activity,
|
|
120
188
|
type: String,
|
|
121
189
|
) {
|
|
190
|
+
// 1. Try the Modern Photo Picker (Android 13+ or backported to 11+)
|
|
191
|
+
// We check for the action string directly to handle backported devices
|
|
122
192
|
val intent =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
this.type =
|
|
127
|
-
|
|
128
|
-
"image" -> "image/*"
|
|
129
|
-
"video" -> "video/*"
|
|
130
|
-
else -> "*/*"
|
|
131
|
-
}
|
|
193
|
+
Intent("android.provider.action.PICK_IMAGES").apply {
|
|
194
|
+
when (type) {
|
|
195
|
+
"video" -> this.type = "video/*"
|
|
196
|
+
"image" -> this.type = "image/*"
|
|
197
|
+
// For 'both', we leave the type null or specify multiple if needed
|
|
132
198
|
}
|
|
133
|
-
}
|
|
134
|
-
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
activity.startActivityForResult(intent, PICKER_REQUEST_CODE)
|
|
203
|
+
} catch (e: Exception) {
|
|
204
|
+
// 2. Fallback: Legacy ACTION_GET_CONTENT (Android 10 and below, or failed 13+)
|
|
205
|
+
val fallbackIntent =
|
|
206
|
+
Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
135
207
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
136
|
-
addFlags(
|
|
137
|
-
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
|
138
|
-
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
|
|
139
|
-
)
|
|
140
208
|
this.type =
|
|
141
209
|
when (type) {
|
|
142
|
-
"image" -> "image/*"
|
|
143
210
|
"video" -> "video/*"
|
|
144
|
-
|
|
211
|
+
"image" -> "image/*"
|
|
212
|
+
else -> "*/*" // 'both' case
|
|
145
213
|
}
|
|
214
|
+
// Allow both images and videos in the legacy picker if 'both' is selected
|
|
215
|
+
if (type == "both") {
|
|
216
|
+
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
|
|
217
|
+
}
|
|
146
218
|
}
|
|
147
|
-
}
|
|
148
219
|
|
|
149
|
-
|
|
220
|
+
try {
|
|
221
|
+
activity.startActivityForResult(
|
|
222
|
+
Intent.createChooser(fallbackIntent, "Select Media"),
|
|
223
|
+
PICKER_REQUEST_CODE,
|
|
224
|
+
)
|
|
225
|
+
} catch (fallbackEx: Exception) {
|
|
226
|
+
pickerPromise?.reject("E_NO_PICKER", "No app available to pick media")
|
|
227
|
+
cleanup()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
150
230
|
}
|
|
151
231
|
|
|
152
232
|
private fun launchCamera(
|
|
153
233
|
activity: Activity,
|
|
154
234
|
useFrontCamera: Boolean,
|
|
155
235
|
) {
|
|
156
|
-
val
|
|
236
|
+
val file = createTempFile("jpg")
|
|
157
237
|
val authority = "${reactContext.packageName}.provider"
|
|
158
|
-
cameraCaptureUri = FileProvider.getUriForFile(reactContext, authority,
|
|
238
|
+
cameraCaptureUri = FileProvider.getUriForFile(reactContext, authority, file)
|
|
159
239
|
|
|
160
240
|
val intent =
|
|
161
241
|
Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
|
162
242
|
putExtra(MediaStore.EXTRA_OUTPUT, cameraCaptureUri)
|
|
163
|
-
addFlags(
|
|
164
|
-
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
|
165
|
-
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
|
|
166
|
-
)
|
|
243
|
+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
167
244
|
if (useFrontCamera) {
|
|
168
245
|
putExtra("android.intent.extras.CAMERA_FACING", 1)
|
|
169
246
|
putExtra("android.intent.extra.USE_FRONT_CAMERA", true)
|
|
@@ -173,7 +250,7 @@ class FastMediaPickerModule(
|
|
|
173
250
|
activity.startActivityForResult(intent, PICKER_REQUEST_CODE)
|
|
174
251
|
}
|
|
175
252
|
|
|
176
|
-
// -------------------- ACTIVITY
|
|
253
|
+
// -------------------- ACTIVITY RESULT --------------------
|
|
177
254
|
|
|
178
255
|
override fun onActivityResult(
|
|
179
256
|
activity: Activity?,
|
|
@@ -183,184 +260,67 @@ class FastMediaPickerModule(
|
|
|
183
260
|
) {
|
|
184
261
|
if (requestCode != PICKER_REQUEST_CODE || pickerPromise == null) return
|
|
185
262
|
|
|
186
|
-
if (resultCode
|
|
263
|
+
if (resultCode != Activity.RESULT_OK) {
|
|
187
264
|
pickerPromise?.reject("E_CANCELLED", "User cancelled")
|
|
188
265
|
cleanup()
|
|
189
266
|
return
|
|
190
267
|
}
|
|
191
268
|
|
|
192
269
|
val uri = data?.data ?: cameraCaptureUri
|
|
193
|
-
|
|
194
|
-
if (uri != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
195
|
-
try {
|
|
196
|
-
reactContext.contentResolver.takePersistableUriPermission(
|
|
197
|
-
uri,
|
|
198
|
-
Intent.FLAG_GRANT_READ_URI_PERMISSION,
|
|
199
|
-
)
|
|
200
|
-
} catch (_: Exception) {
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
270
|
if (uri != null) {
|
|
205
271
|
processMedia(uri)
|
|
206
272
|
} else {
|
|
207
|
-
pickerPromise?.reject("E_NO_URI", "No media
|
|
208
|
-
cleanup()
|
|
273
|
+
pickerPromise?.reject("E_NO_URI", "No media")
|
|
209
274
|
}
|
|
210
275
|
}
|
|
211
276
|
|
|
212
|
-
|
|
213
|
-
requestCode: Int,
|
|
214
|
-
permissions: Array<String>,
|
|
215
|
-
grantResults: IntArray,
|
|
216
|
-
): Boolean {
|
|
217
|
-
if (requestCode == CAMERA_PERMISSION_REQUEST) {
|
|
218
|
-
if (grantResults.isNotEmpty() &&
|
|
219
|
-
grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
220
|
-
) {
|
|
221
|
-
currentActivity?.let {
|
|
222
|
-
launchCamera(it, pendingFrontCamera)
|
|
223
|
-
}
|
|
224
|
-
} else {
|
|
225
|
-
pickerPromise?.reject("E_CAMERA_PERMISSION", "Camera permission denied")
|
|
226
|
-
cleanup()
|
|
227
|
-
}
|
|
228
|
-
return true
|
|
229
|
-
}
|
|
230
|
-
return false
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// -------------------- MEDIA PROCESSING --------------------
|
|
277
|
+
// -------------------- MEDIA --------------------
|
|
234
278
|
|
|
235
279
|
private fun processMedia(uri: Uri) {
|
|
236
280
|
val resolver = reactContext.contentResolver
|
|
237
|
-
val
|
|
238
|
-
val isVideo =
|
|
281
|
+
val mime = resolver.getType(uri) ?: "image/jpeg"
|
|
282
|
+
val isVideo = mime.startsWith("video")
|
|
239
283
|
|
|
240
284
|
try {
|
|
241
285
|
if (isVideo) {
|
|
242
|
-
val
|
|
243
|
-
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: "mp4"
|
|
244
|
-
val tempFile = createTempFile(ext)
|
|
245
|
-
|
|
286
|
+
val file = createTempFile("mp4")
|
|
246
287
|
resolver.openInputStream(uri)?.use { input ->
|
|
247
|
-
FileOutputStream(
|
|
288
|
+
FileOutputStream(file).use { input.copyTo(it) }
|
|
248
289
|
}
|
|
249
|
-
|
|
250
|
-
val (w, h) = getVideoDimensions(tempFile)
|
|
251
|
-
resolveResult(Uri.fromFile(tempFile).toString(), true, w, h)
|
|
290
|
+
resolveResult(Uri.fromFile(file).toString(), true, 0, 0)
|
|
252
291
|
} else {
|
|
253
|
-
val
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
)
|
|
292
|
+
val file = createTempFile("jpg")
|
|
293
|
+
resolver.openInputStream(uri)?.use { input ->
|
|
294
|
+
BitmapFactory.decodeStream(input)?.compress(
|
|
295
|
+
Bitmap.CompressFormat.JPEG,
|
|
296
|
+
JPEG_QUALITY,
|
|
297
|
+
FileOutputStream(file),
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
resolveResult(Uri.fromFile(file).toString(), false, 0, 0)
|
|
262
301
|
}
|
|
263
302
|
} catch (e: Exception) {
|
|
264
|
-
pickerPromise?.reject("
|
|
303
|
+
pickerPromise?.reject("E_PROCESS", e.message)
|
|
265
304
|
} finally {
|
|
266
305
|
cleanup()
|
|
267
306
|
}
|
|
268
307
|
}
|
|
269
308
|
|
|
270
|
-
private fun getVideoDimensions(file: File): Pair<Int, Int> {
|
|
271
|
-
val retriever = MediaMetadataRetriever()
|
|
272
|
-
return try {
|
|
273
|
-
retriever.setDataSource(file.absolutePath)
|
|
274
|
-
val w =
|
|
275
|
-
retriever
|
|
276
|
-
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
|
277
|
-
?.toInt() ?: 0
|
|
278
|
-
val h =
|
|
279
|
-
retriever
|
|
280
|
-
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
|
281
|
-
?.toInt() ?: 0
|
|
282
|
-
val rot =
|
|
283
|
-
retriever
|
|
284
|
-
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
|
285
|
-
?.toInt() ?: 0
|
|
286
|
-
if (rot == 90 || rot == 270) h to w else w to h
|
|
287
|
-
} finally {
|
|
288
|
-
retriever.release()
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private fun processAndSaveImage(uri: Uri): File {
|
|
293
|
-
val tempFile = createTempFile("jpg")
|
|
294
|
-
val resolver = reactContext.contentResolver
|
|
295
|
-
|
|
296
|
-
val opts =
|
|
297
|
-
BitmapFactory.Options().apply {
|
|
298
|
-
inJustDecodeBounds = true
|
|
299
|
-
resolver.openInputStream(uri)?.use {
|
|
300
|
-
BitmapFactory.decodeStream(it, null, this)
|
|
301
|
-
}
|
|
302
|
-
inSampleSize =
|
|
303
|
-
calculateInSampleSize(outWidth, outHeight, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
|
|
304
|
-
inJustDecodeBounds = false
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
val original =
|
|
308
|
-
resolver.openInputStream(uri)?.use {
|
|
309
|
-
BitmapFactory.decodeStream(it, null, opts)
|
|
310
|
-
} ?: throw RuntimeException("Decode failed")
|
|
311
|
-
|
|
312
|
-
val result =
|
|
313
|
-
Bitmap.createBitmap(original.width, original.height, Bitmap.Config.ARGB_8888)
|
|
314
|
-
|
|
315
|
-
Canvas(result).apply {
|
|
316
|
-
drawColor(Color.WHITE)
|
|
317
|
-
drawBitmap(original, 0f, 0f, null)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
FileOutputStream(tempFile).use {
|
|
321
|
-
result.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, it)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
original.recycle()
|
|
325
|
-
result.recycle()
|
|
326
|
-
return tempFile
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private fun calculateInSampleSize(
|
|
330
|
-
w: Int,
|
|
331
|
-
h: Int,
|
|
332
|
-
reqW: Int,
|
|
333
|
-
reqH: Int,
|
|
334
|
-
): Int {
|
|
335
|
-
var size = 1
|
|
336
|
-
if (h > reqH || w > reqW) {
|
|
337
|
-
val halfH = h / 2
|
|
338
|
-
val halfW = w / 2
|
|
339
|
-
while (halfH / size >= reqH && halfW / size >= reqW) {
|
|
340
|
-
size *= 2
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return size
|
|
344
|
-
}
|
|
345
|
-
|
|
346
309
|
private fun resolveResult(
|
|
347
310
|
uri: String,
|
|
348
311
|
isVideo: Boolean,
|
|
349
312
|
w: Int,
|
|
350
313
|
h: Int,
|
|
351
314
|
) {
|
|
352
|
-
|
|
315
|
+
pickerPromise?.resolve(
|
|
353
316
|
Arguments.createMap().apply {
|
|
354
317
|
putString("uri", uri)
|
|
355
318
|
putBoolean("isVideo", isVideo)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
putDouble("height", h.toDouble())
|
|
359
|
-
}
|
|
360
|
-
pickerPromise?.resolve(map)
|
|
319
|
+
},
|
|
320
|
+
)
|
|
361
321
|
}
|
|
362
322
|
|
|
363
|
-
private fun createTempFile(ext: String)
|
|
323
|
+
private fun createTempFile(ext: String) = File(reactContext.cacheDir, "fast-${UUID.randomUUID()}.$ext")
|
|
364
324
|
|
|
365
325
|
private fun cleanup() {
|
|
366
326
|
pickerPromise = null
|
package/package.json
CHANGED
package/withMediaPicker.js
CHANGED
|
@@ -3,74 +3,98 @@ const { withAndroidManifest, withInfoPlist, AndroidConfig } = require('@expo/con
|
|
|
3
3
|
const { getMainApplicationOrThrow } = AndroidConfig.Manifest;
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* ANDROID CONFIG
|
|
7
7
|
*/
|
|
8
8
|
const withAndroidConfig = (config) => {
|
|
9
9
|
return withAndroidManifest(config, (config) => {
|
|
10
10
|
const androidManifest = config.modResults;
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'android.permission.READ_EXTERNAL_STORAGE',
|
|
16
|
-
'android.permission.WRITE_EXTERNAL_STORAGE',
|
|
17
|
-
'android.permission.READ_MEDIA_IMAGES',
|
|
18
|
-
'android.permission.READ_MEDIA_VIDEO',
|
|
19
|
-
];
|
|
12
|
+
// -------------------------
|
|
13
|
+
// 1. PERMISSIONS
|
|
14
|
+
// -------------------------
|
|
20
15
|
|
|
21
16
|
if (!androidManifest.manifest['uses-permission']) {
|
|
22
17
|
androidManifest.manifest['uses-permission'] = [];
|
|
23
18
|
}
|
|
24
19
|
|
|
20
|
+
const permissions = [
|
|
21
|
+
{ name: 'android.permission.CAMERA' },
|
|
22
|
+
{ name: 'android.permission.READ_MEDIA_IMAGES' },
|
|
23
|
+
{ name: 'android.permission.READ_MEDIA_VIDEO' },
|
|
24
|
+
{ name: 'android.permission.READ_EXTERNAL_STORAGE', maxSdkVersion: '32' },
|
|
25
|
+
];
|
|
26
|
+
|
|
25
27
|
permissions.forEach((perm) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
const exists = androidManifest.manifest['uses-permission'].some(
|
|
29
|
+
(p) => p.$['android:name'] === perm.name
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (!exists) {
|
|
33
|
+
androidManifest.manifest['uses-permission'].push({
|
|
34
|
+
$: {
|
|
35
|
+
'android:name': perm.name,
|
|
36
|
+
...(perm.maxSdkVersion
|
|
37
|
+
? { 'android:maxSdkVersion': perm.maxSdkVersion }
|
|
38
|
+
: {}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
28
41
|
}
|
|
29
42
|
});
|
|
30
43
|
|
|
31
|
-
//
|
|
44
|
+
// -------------------------
|
|
45
|
+
// 2. QUERIES (Android 11+)
|
|
46
|
+
// -------------------------
|
|
47
|
+
|
|
32
48
|
if (!androidManifest.manifest.queries) {
|
|
33
49
|
androidManifest.manifest.queries = [{}];
|
|
34
50
|
}
|
|
51
|
+
|
|
35
52
|
const queries = androidManifest.manifest.queries[0];
|
|
36
53
|
if (!queries.intent) queries.intent = [];
|
|
37
54
|
|
|
38
|
-
const
|
|
55
|
+
const intents = [
|
|
39
56
|
{ action: [{ $: { 'android:name': 'android.media.action.IMAGE_CAPTURE' } }] },
|
|
40
57
|
{ action: [{ $: { 'android:name': 'android.provider.action.PICK_IMAGES' } }] },
|
|
41
58
|
{
|
|
42
59
|
action: [{ $: { 'android:name': 'android.intent.action.OPEN_DOCUMENT' } }],
|
|
43
|
-
data: [{ $: { 'android:mimeType': 'image/*' } }]
|
|
60
|
+
data: [{ $: { 'android:mimeType': 'image/*' } }],
|
|
44
61
|
},
|
|
45
62
|
{
|
|
46
63
|
action: [{ $: { 'android:name': 'android.intent.action.OPEN_DOCUMENT' } }],
|
|
47
|
-
data: [{ $: { 'android:mimeType': 'video/*' } }]
|
|
48
|
-
}
|
|
64
|
+
data: [{ $: { 'android:mimeType': 'video/*' } }],
|
|
65
|
+
},
|
|
49
66
|
];
|
|
50
67
|
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const exists = queries.intent.
|
|
68
|
+
intents.forEach((intent) => {
|
|
69
|
+
const actionName = intent.action[0].$['android:name'];
|
|
70
|
+
const exists = queries.intent.some(
|
|
71
|
+
(i) => i.action?.[0]?.$['android:name'] === actionName
|
|
72
|
+
);
|
|
54
73
|
if (!exists) queries.intent.push(intent);
|
|
55
74
|
});
|
|
56
75
|
|
|
57
|
-
//
|
|
76
|
+
// -------------------------
|
|
77
|
+
// 3. FILE PROVIDER
|
|
78
|
+
// -------------------------
|
|
79
|
+
|
|
58
80
|
const mainApplication = getMainApplicationOrThrow(androidManifest);
|
|
81
|
+
|
|
59
82
|
if (!mainApplication.provider) {
|
|
60
83
|
mainApplication.provider = [];
|
|
61
84
|
}
|
|
62
85
|
|
|
63
|
-
const providerClassName = 'androidx.core.content.FileProvider';
|
|
64
86
|
const providerAuthority = '${applicationId}.provider';
|
|
65
87
|
|
|
66
|
-
const hasProvider = mainApplication.provider.
|
|
67
|
-
(p) =>
|
|
88
|
+
const hasProvider = mainApplication.provider.some(
|
|
89
|
+
(p) =>
|
|
90
|
+
p.$['android:name'] === 'androidx.core.content.FileProvider' ||
|
|
91
|
+
p.$['android:authorities'] === providerAuthority
|
|
68
92
|
);
|
|
69
93
|
|
|
70
94
|
if (!hasProvider) {
|
|
71
95
|
mainApplication.provider.push({
|
|
72
96
|
$: {
|
|
73
|
-
'android:name':
|
|
97
|
+
'android:name': 'androidx.core.content.FileProvider',
|
|
74
98
|
'android:authorities': providerAuthority,
|
|
75
99
|
'android:exported': 'false',
|
|
76
100
|
'android:grantUriPermissions': 'true',
|
|
@@ -91,26 +115,33 @@ const withAndroidConfig = (config) => {
|
|
|
91
115
|
};
|
|
92
116
|
|
|
93
117
|
/**
|
|
94
|
-
*
|
|
118
|
+
* IOS CONFIG
|
|
95
119
|
*/
|
|
96
120
|
const withIosConfig = (config) => {
|
|
97
121
|
return withInfoPlist(config, (config) => {
|
|
98
|
-
config.modResults
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
const plist = config.modResults;
|
|
123
|
+
|
|
124
|
+
plist.NSCameraUsageDescription =
|
|
125
|
+
plist.NSCameraUsageDescription ||
|
|
126
|
+
'Allow access to your camera to take photos and videos.';
|
|
127
|
+
|
|
128
|
+
plist.NSPhotoLibraryUsageDescription =
|
|
129
|
+
plist.NSPhotoLibraryUsageDescription ||
|
|
130
|
+
'Allow access to your photo library to select media.';
|
|
131
|
+
|
|
132
|
+
plist.NSMicrophoneUsageDescription =
|
|
133
|
+
plist.NSMicrophoneUsageDescription ||
|
|
134
|
+
'Allow access to your microphone when recording videos.';
|
|
104
135
|
|
|
105
136
|
return config;
|
|
106
137
|
});
|
|
107
138
|
};
|
|
108
139
|
|
|
109
140
|
/**
|
|
110
|
-
*
|
|
141
|
+
* EXPORT PLUGIN
|
|
111
142
|
*/
|
|
112
143
|
module.exports = function withMediaPicker(config) {
|
|
113
144
|
config = withAndroidConfig(config);
|
|
114
145
|
config = withIosConfig(config);
|
|
115
146
|
return config;
|
|
116
|
-
};
|
|
147
|
+
};
|