expo-image-picker 10.2.3 → 11.0.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/CHANGELOG.md +17 -6
- package/android/build.gradle +6 -13
- package/android/src/main/java/expo/modules/imagepicker/ImagePickerConstants.kt +7 -2
- package/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt +66 -24
- package/android/src/main/java/expo/modules/imagepicker/ImagePickerOptions.kt +1 -1
- package/android/src/main/java/expo/modules/imagepicker/ImagePickerPackage.kt +1 -1
- package/android/src/main/java/expo/modules/imagepicker/ImagePickerUtils.kt +29 -1
- package/android/src/main/java/expo/modules/imagepicker/PendingPromise.kt +9 -7
- package/android/src/main/java/expo/modules/imagepicker/fileproviders/CacheFileProvider.kt +1 -1
- package/android/src/main/java/expo/modules/imagepicker/tasks/ImageResultTask.kt +73 -31
- package/android/src/main/java/expo/modules/imagepicker/tasks/VideoResultTask.kt +52 -25
- package/build/ExponentImagePicker.d.ts +1 -1
- package/build/ExponentImagePicker.js +1 -1
- package/build/ExponentImagePicker.js.map +1 -1
- package/build/ExponentImagePicker.web.js +2 -3
- package/build/ExponentImagePicker.web.js.map +1 -1
- package/build/ImagePicker.d.ts +24 -2
- package/build/ImagePicker.js +30 -2
- package/build/ImagePicker.js.map +1 -1
- package/build/ImagePicker.types.d.ts +12 -0
- package/build/ImagePicker.types.js +12 -0
- package/build/ImagePicker.types.js.map +1 -1
- package/ios/EXImagePicker/EXImagePicker.h +3 -3
- package/ios/EXImagePicker/EXImagePicker.m +52 -56
- package/ios/EXImagePicker/Permissions/EXImagePickerCameraPermissionRequester.m +5 -5
- package/ios/EXImagePicker/Permissions/EXImagePickerMediaLibraryPermissionRequester.m +3 -3
- package/ios/EXImagePicker.podspec +2 -2
- package/package.json +5 -8
- package/plugin/build/withImagePicker.js +1 -1
- package/plugin/src/withImagePicker.ts +2 -2
- package/src/ExponentImagePicker.ts +1 -1
- package/src/ExponentImagePicker.web.ts +2 -3
- package/src/ImagePicker.ts +43 -2
- package/src/ImagePicker.types.ts +19 -7
package/CHANGELOG.md
CHANGED
|
@@ -10,17 +10,26 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 11.0.0 — 2021-09-28
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
### 🛠 Breaking changes
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
- Dropped support for iOS 11.0 ([#14383](https://github.com/expo/expo/pull/14383) by [@cruzach](https://github.com/cruzach))
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
### 🎉 New features
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
- Add useCameraPermissions and useMediaLibraryPermissions hooks from modules factory. ([#13859](https://github.com/expo/expo/pull/13859) by [@bycedric](https://github.com/bycedric))
|
|
22
|
+
- Add possibility to change presentation style on iOS. ([#14068](https://github.com/expo/expo/pull/14068) by [@mstach60161](https://github.com/mstach60161))
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
### 🐛 Bug fixes
|
|
25
|
+
|
|
26
|
+
- Add missing `GPSHPositioningError` exif parameter on Android. ([#13998](https://github.com/expo/expo/pull/13998) by [@mstach60161](https://github.com/mstach60161))
|
|
27
|
+
- Fix promise not resolving when the app is moved to the background on Android. ([#13975](https://github.com/expo/expo/pull/13975) by [@mstach60161](https://github.com/mstach60161))
|
|
28
|
+
- Fix building errors from use_frameworks! in Podfile. ([#14523](https://github.com/expo/expo/pull/14523) by [@kudo](https://github.com/kudo))
|
|
29
|
+
|
|
30
|
+
### 💡 Others
|
|
31
|
+
|
|
32
|
+
- Updated `@expo/config-plugins` ([#14443](https://github.com/expo/expo/pull/14443) by [@EvanBacon](https://github.com/EvanBacon))
|
|
24
33
|
|
|
25
34
|
## 10.2.0 — 2021-06-16
|
|
26
35
|
|
|
@@ -29,10 +38,12 @@ _This version does not introduce any user-facing changes._
|
|
|
29
38
|
- Fixed `base64` return on web. ([#12529](https://github.com/expo/expo/pull/12529) by [@simonezuccala](https://github.com/simonezuccala) and [@misterdev](https://github.com/misterdev))
|
|
30
39
|
- Enable kotlin in all modules. ([#12716](https://github.com/expo/expo/pull/12716) by [@wschurman](https://github.com/wschurman))
|
|
31
40
|
- Fixed cases where Picker & Camera would be transparent on iOS 14.5. ([#12897](https://github.com/expo/expo/pull/12897) by [@cruzach](https://github.com/cruzach))
|
|
41
|
+
- Add missing exif data to cropped image on Android. ([#14038](https://github.com/expo/expo/pull/14038) by [@mstach60161](https://github.com/mstach60161))
|
|
32
42
|
|
|
33
43
|
### 💡 Others
|
|
34
44
|
|
|
35
45
|
- Migrated from `unimodules-file-system-interface` and `unimodules-permissions-interface` to `expo-modules-core`. ([#12961](https://github.com/expo/expo/pull/12961) by [@tsapeta](https://github.com/tsapeta))
|
|
46
|
+
- Migrated from `AsyncTask` to Kotlin concurrency utilities. ([#13800](https://github.com/expo/expo/pull/13800) by [@m1st4ke](https://github.com/m1st4ke))
|
|
36
47
|
|
|
37
48
|
## 10.1.3 — 2021-04-13
|
|
38
49
|
|
package/android/build.gradle
CHANGED
|
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
|
|
|
3
3
|
apply plugin: 'maven'
|
|
4
4
|
|
|
5
5
|
group = 'host.exp.exponent'
|
|
6
|
-
version = '
|
|
6
|
+
version = '11.0.0'
|
|
7
7
|
|
|
8
8
|
buildscript {
|
|
9
9
|
// Simple helper that allows the root project to override versions declared by this library.
|
|
@@ -58,29 +58,22 @@ android {
|
|
|
58
58
|
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
59
59
|
targetSdkVersion safeExtGet("targetSdkVersion", 30)
|
|
60
60
|
versionCode 22
|
|
61
|
-
versionName "
|
|
61
|
+
versionName "11.0.0"
|
|
62
62
|
}
|
|
63
63
|
lintOptions {
|
|
64
64
|
abortOnError false
|
|
65
65
|
}
|
|
66
66
|
kotlinOptions {
|
|
67
|
-
jvmTarget =
|
|
67
|
+
jvmTarget = JavaVersion.VERSION_1_8
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
if (new File(rootProject.projectDir.parentFile, 'package.json').exists()) {
|
|
72
|
-
apply from: project(":unimodules-core").file("../unimodules-core.gradle")
|
|
73
|
-
} else {
|
|
74
|
-
throw new GradleException(
|
|
75
|
-
"'unimodules-core.gradle' was not found in the usual React Native dependency location. " +
|
|
76
|
-
"This package can only be used in such projects. Are you sure you've installed the dependencies properly?")
|
|
77
|
-
}
|
|
78
|
-
|
|
79
71
|
dependencies {
|
|
80
|
-
|
|
81
|
-
unimodule 'expo-modules-core'
|
|
72
|
+
implementation project(':expo-modules-core')
|
|
82
73
|
|
|
83
74
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${safeExtGet('kotlinVersion', '1.4.21')}"
|
|
75
|
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3")
|
|
76
|
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1")
|
|
84
77
|
api 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
|
85
78
|
api "androidx.legacy:legacy-support-v4:1.0.0"
|
|
86
79
|
api 'commons-codec:commons-codec:1.10'
|
|
@@ -23,6 +23,9 @@ object ImagePickerConstants {
|
|
|
23
23
|
const val MISSING_URL_MESSAGE = "Intent doesn't contain `url`."
|
|
24
24
|
const val ERR_CAN_NOT_OPEN_CROP = "ERR_CAN_NOT_OPEN_CROP"
|
|
25
25
|
const val CAN_NOT_OPEN_CROP_MESSAGE = "Can not open the crop tool."
|
|
26
|
+
const val COROUTINE_CANCELED = "Coroutine canceled by module destruction."
|
|
27
|
+
const val PROMISES_CANCELED = "Module destroyed, all promises canceled."
|
|
28
|
+
const val UNKNOWN_EXCEPTION = "Unknown exception."
|
|
26
29
|
|
|
27
30
|
const val OPTION_QUALITY = "quality"
|
|
28
31
|
const val OPTION_ALLOWS_EDITING = "allowsEditing"
|
|
@@ -32,8 +35,6 @@ object ImagePickerConstants {
|
|
|
32
35
|
const val OPTION_EXIF = "exif"
|
|
33
36
|
const val OPTION_VIDEO_MAX_DURATION = "videoMaxDuration"
|
|
34
37
|
|
|
35
|
-
// We need to explicitly get latitude, longitude, altitude with their specific accessor functions
|
|
36
|
-
// separately so we skip them in this list.
|
|
37
38
|
val exifTags = arrayOf(
|
|
38
39
|
arrayOf("string", ExifInterface.TAG_ARTIST),
|
|
39
40
|
arrayOf("int", ExifInterface.TAG_BITS_PER_SAMPLE),
|
|
@@ -123,6 +124,7 @@ object ImagePickerConstants {
|
|
|
123
124
|
arrayOf("int", ExifInterface.TAG_SUBJECT_LOCATION),
|
|
124
125
|
arrayOf("string", ExifInterface.TAG_USER_COMMENT),
|
|
125
126
|
arrayOf("int", ExifInterface.TAG_WHITE_BALANCE),
|
|
127
|
+
arrayOf("double", ExifInterface.TAG_GPS_ALTITUDE),
|
|
126
128
|
arrayOf("int", ExifInterface.TAG_GPS_ALTITUDE_REF),
|
|
127
129
|
arrayOf("string", ExifInterface.TAG_GPS_AREA_INFORMATION),
|
|
128
130
|
arrayOf("double", ExifInterface.TAG_GPS_DOP),
|
|
@@ -136,9 +138,12 @@ object ImagePickerConstants {
|
|
|
136
138
|
arrayOf("double", ExifInterface.TAG_GPS_DEST_LONGITUDE),
|
|
137
139
|
arrayOf("string", ExifInterface.TAG_GPS_DEST_LONGITUDE_REF),
|
|
138
140
|
arrayOf("int", ExifInterface.TAG_GPS_DIFFERENTIAL),
|
|
141
|
+
arrayOf("string", ExifInterface.TAG_GPS_H_POSITIONING_ERROR),
|
|
139
142
|
arrayOf("double", ExifInterface.TAG_GPS_IMG_DIRECTION),
|
|
140
143
|
arrayOf("string", ExifInterface.TAG_GPS_IMG_DIRECTION_REF),
|
|
144
|
+
arrayOf("double", ExifInterface.TAG_GPS_LATITUDE),
|
|
141
145
|
arrayOf("string", ExifInterface.TAG_GPS_LATITUDE_REF),
|
|
146
|
+
arrayOf("double", ExifInterface.TAG_GPS_LONGITUDE),
|
|
142
147
|
arrayOf("string", ExifInterface.TAG_GPS_LONGITUDE_REF),
|
|
143
148
|
arrayOf("string", ExifInterface.TAG_GPS_MAP_DATUM),
|
|
144
149
|
arrayOf("string", ExifInterface.TAG_GPS_MEASURE_MODE),
|
|
@@ -11,6 +11,17 @@ import android.os.Bundle
|
|
|
11
11
|
import android.provider.MediaStore
|
|
12
12
|
import android.util.Log
|
|
13
13
|
import com.theartofdev.edmodo.cropper.CropImage
|
|
14
|
+
import expo.modules.core.ExportedModule
|
|
15
|
+
import expo.modules.core.ModuleRegistry
|
|
16
|
+
import expo.modules.core.ModuleRegistryDelegate
|
|
17
|
+
import expo.modules.core.Promise
|
|
18
|
+
import expo.modules.core.errors.ModuleDestroyedException
|
|
19
|
+
import expo.modules.core.interfaces.ActivityEventListener
|
|
20
|
+
import expo.modules.core.interfaces.ActivityProvider
|
|
21
|
+
import expo.modules.core.interfaces.ExpoMethod
|
|
22
|
+
import expo.modules.core.interfaces.LifecycleEventListener
|
|
23
|
+
import expo.modules.core.interfaces.services.UIManager
|
|
24
|
+
import expo.modules.core.utilities.FileUtilities.generateOutputPath
|
|
14
25
|
import expo.modules.imagepicker.ImagePickerOptions.Companion.optionsFromMap
|
|
15
26
|
import expo.modules.imagepicker.exporters.CompressionImageExporter
|
|
16
27
|
import expo.modules.imagepicker.exporters.CropImageExporter
|
|
@@ -25,16 +36,9 @@ import expo.modules.interfaces.permissions.Permissions
|
|
|
25
36
|
import expo.modules.interfaces.permissions.PermissionsResponse
|
|
26
37
|
import expo.modules.interfaces.permissions.PermissionsResponseListener
|
|
27
38
|
import expo.modules.interfaces.permissions.PermissionsStatus
|
|
28
|
-
import
|
|
29
|
-
import
|
|
30
|
-
import
|
|
31
|
-
import org.unimodules.core.Promise
|
|
32
|
-
import org.unimodules.core.interfaces.ActivityEventListener
|
|
33
|
-
import org.unimodules.core.interfaces.ActivityProvider
|
|
34
|
-
import org.unimodules.core.interfaces.ExpoMethod
|
|
35
|
-
import org.unimodules.core.interfaces.LifecycleEventListener
|
|
36
|
-
import org.unimodules.core.interfaces.services.UIManager
|
|
37
|
-
import org.unimodules.core.utilities.FileUtilities.generateOutputPath
|
|
39
|
+
import kotlinx.coroutines.CoroutineScope
|
|
40
|
+
import kotlinx.coroutines.Dispatchers
|
|
41
|
+
import kotlinx.coroutines.cancel
|
|
38
42
|
import java.io.IOException
|
|
39
43
|
import java.lang.ref.WeakReference
|
|
40
44
|
|
|
@@ -47,13 +51,24 @@ class ImagePickerModule(
|
|
|
47
51
|
private var mCameraCaptureURI: Uri? = null
|
|
48
52
|
private var mPromise: Promise? = null
|
|
49
53
|
private var mPickerOptions: ImagePickerOptions? = null
|
|
54
|
+
private val moduleCoroutineScope = CoroutineScope(Dispatchers.IO)
|
|
55
|
+
private var exifDataHandler: ExifDataHandler? = null
|
|
56
|
+
|
|
57
|
+
override fun onDestroy() {
|
|
58
|
+
try {
|
|
59
|
+
mUIManager.unregisterLifecycleEventListener(this)
|
|
60
|
+
moduleCoroutineScope.cancel(ModuleDestroyedException(ImagePickerConstants.PROMISES_CANCELED))
|
|
61
|
+
} catch (e: IllegalStateException) {
|
|
62
|
+
Log.e(ImagePickerConstants.TAG, "The scope does not have a job in it")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
50
65
|
|
|
51
66
|
/**
|
|
52
67
|
* Android system sometimes kills the `MainActivity` after the `ImagePicker` finishes.
|
|
53
68
|
* Moreover, the react context will be reloaded again in such a case. We need to handle this situation.
|
|
54
69
|
* To do it we track if the current activity was destroyed.
|
|
55
70
|
*/
|
|
56
|
-
private var
|
|
71
|
+
private var mWasHostDestroyed = false
|
|
57
72
|
|
|
58
73
|
private val mImageLoader: ImageLoaderInterface by moduleRegistry()
|
|
59
74
|
private val mUIManager: UIManager by moduleRegistry()
|
|
@@ -126,7 +141,8 @@ class ImagePickerModule(
|
|
|
126
141
|
|
|
127
142
|
val permissionsResponseHandler = PermissionsResponseListener { permissionsResponse: Map<String, PermissionsResponse> ->
|
|
128
143
|
if (permissionsResponse[Manifest.permission.WRITE_EXTERNAL_STORAGE]?.status == PermissionsStatus.GRANTED &&
|
|
129
|
-
permissionsResponse[Manifest.permission.CAMERA]?.status == PermissionsStatus.GRANTED
|
|
144
|
+
permissionsResponse[Manifest.permission.CAMERA]?.status == PermissionsStatus.GRANTED
|
|
145
|
+
) {
|
|
130
146
|
launchCameraWithPermissionsGranted(promise, cameraIntent, pickerOptions)
|
|
131
147
|
} else {
|
|
132
148
|
promise.reject(SecurityException("User rejected permissions"))
|
|
@@ -255,7 +271,7 @@ class ImagePickerModule(
|
|
|
255
271
|
setOutputCompressFormat(compressFormat)
|
|
256
272
|
setOutputCompressQuality(pickerOptions.quality)
|
|
257
273
|
}
|
|
258
|
-
|
|
274
|
+
exifDataHandler = ExifDataHandler(uri)
|
|
259
275
|
startActivityOnResult(cropImageBuilder.getIntent(context), CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE, promise, pickerOptions)
|
|
260
276
|
}
|
|
261
277
|
|
|
@@ -270,7 +286,7 @@ class ImagePickerModule(
|
|
|
270
286
|
mUIManager.unregisterActivityEventListener(this)
|
|
271
287
|
|
|
272
288
|
var pickerOptions = mPickerOptions!!
|
|
273
|
-
val promise = if (
|
|
289
|
+
val promise = if (mWasHostDestroyed && mPromise !is PendingPromise) {
|
|
274
290
|
if (pickerOptions.isBase64) {
|
|
275
291
|
// we know that the activity was killed and we don't want to store
|
|
276
292
|
// base64 into `SharedPreferences`...
|
|
@@ -323,14 +339,16 @@ class ImagePickerModule(
|
|
|
323
339
|
mPickerOptions != null &&
|
|
324
340
|
// When we launched the crop tool and the android kills current activity, the references can be different.
|
|
325
341
|
// So, we fallback to the requestCode in this case.
|
|
326
|
-
(activity === experienceActivity ||
|
|
342
|
+
(activity === experienceActivity || mWasHostDestroyed && requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE)
|
|
327
343
|
}
|
|
328
344
|
|
|
329
345
|
private fun handleOnActivityResult(promise: Promise, activity: Activity, requestCode: Int, resultCode: Int, intent: Intent?, pickerOptions: ImagePickerOptions) {
|
|
330
346
|
if (resultCode != Activity.RESULT_OK) {
|
|
331
|
-
promise.resolve(
|
|
332
|
-
|
|
333
|
-
|
|
347
|
+
promise.resolve(
|
|
348
|
+
Bundle().apply {
|
|
349
|
+
putBoolean("cancelled", true)
|
|
350
|
+
}
|
|
351
|
+
)
|
|
334
352
|
return
|
|
335
353
|
}
|
|
336
354
|
|
|
@@ -339,7 +357,17 @@ class ImagePickerModule(
|
|
|
339
357
|
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
|
|
340
358
|
val result = CropImage.getActivityResult(intent)
|
|
341
359
|
val exporter = CropImageExporter(result.rotation, result.cropRect, pickerOptions.isBase64)
|
|
342
|
-
ImageResultTask(
|
|
360
|
+
ImageResultTask(
|
|
361
|
+
promise,
|
|
362
|
+
result.uri,
|
|
363
|
+
contentResolver,
|
|
364
|
+
CropFileProvider(result.uri),
|
|
365
|
+
pickerOptions.isAllowsEditing,
|
|
366
|
+
pickerOptions.isExif,
|
|
367
|
+
exporter,
|
|
368
|
+
exifDataHandler,
|
|
369
|
+
moduleCoroutineScope
|
|
370
|
+
).execute()
|
|
343
371
|
return
|
|
344
372
|
}
|
|
345
373
|
|
|
@@ -368,7 +396,17 @@ class ImagePickerModule(
|
|
|
368
396
|
CompressionImageExporter(mImageLoader, pickerOptions.quality, pickerOptions.isBase64)
|
|
369
397
|
}
|
|
370
398
|
|
|
371
|
-
ImageResultTask(
|
|
399
|
+
ImageResultTask(
|
|
400
|
+
promise,
|
|
401
|
+
uri,
|
|
402
|
+
contentResolver,
|
|
403
|
+
CacheFileProvider(mContext.cacheDir, deduceExtension(type)),
|
|
404
|
+
pickerOptions.isAllowsEditing,
|
|
405
|
+
pickerOptions.isExif,
|
|
406
|
+
exporter,
|
|
407
|
+
exifDataHandler,
|
|
408
|
+
moduleCoroutineScope
|
|
409
|
+
).execute()
|
|
372
410
|
return
|
|
373
411
|
}
|
|
374
412
|
|
|
@@ -376,7 +414,7 @@ class ImagePickerModule(
|
|
|
376
414
|
val metadataRetriever = MediaMetadataRetriever().apply {
|
|
377
415
|
setDataSource(mContext, uri)
|
|
378
416
|
}
|
|
379
|
-
VideoResultTask(promise, uri, contentResolver, CacheFileProvider(mContext.cacheDir, ".mp4"), metadataRetriever).execute()
|
|
417
|
+
VideoResultTask(promise, uri, contentResolver, CacheFileProvider(mContext.cacheDir, ".mp4"), metadataRetriever, moduleCoroutineScope).execute()
|
|
380
418
|
} catch (e: RuntimeException) {
|
|
381
419
|
e.printStackTrace()
|
|
382
420
|
promise.reject(ImagePickerConstants.ERR_CAN_NOT_EXTRACT_METADATA, ImagePickerConstants.CAN_NOT_EXTRACT_METADATA_MESSAGE, e)
|
|
@@ -389,11 +427,15 @@ class ImagePickerModule(
|
|
|
389
427
|
//region LifecycleEventListener
|
|
390
428
|
|
|
391
429
|
override fun onHostDestroy() {
|
|
392
|
-
|
|
393
|
-
mUIManager.unregisterLifecycleEventListener(this)
|
|
430
|
+
mWasHostDestroyed = true
|
|
394
431
|
}
|
|
395
432
|
|
|
396
|
-
override fun onHostResume()
|
|
433
|
+
override fun onHostResume() {
|
|
434
|
+
if (mWasHostDestroyed) {
|
|
435
|
+
_experienceActivity = WeakReference(mActivityProvider.currentActivity)
|
|
436
|
+
mWasHostDestroyed = false
|
|
437
|
+
}
|
|
438
|
+
}
|
|
397
439
|
override fun onHostPause() = Unit
|
|
398
440
|
|
|
399
441
|
//endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
package expo.modules.imagepicker
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
-
import
|
|
4
|
+
import expo.modules.core.BasePackage
|
|
5
5
|
|
|
6
6
|
class ImagePickerPackage : BasePackage() {
|
|
7
7
|
override fun createExportedModules(context: Context) = listOf(ImagePickerModule(context))
|
|
@@ -6,7 +6,8 @@ import android.net.Uri
|
|
|
6
6
|
import android.util.Log
|
|
7
7
|
import android.webkit.MimeTypeMap
|
|
8
8
|
import androidx.core.content.FileProvider
|
|
9
|
-
import
|
|
9
|
+
import androidx.exifinterface.media.ExifInterface
|
|
10
|
+
import expo.modules.core.utilities.FileUtilities.generateOutputPath
|
|
10
11
|
import java.io.File
|
|
11
12
|
import java.io.IOException
|
|
12
13
|
|
|
@@ -63,3 +64,30 @@ fun deduceExtension(type: String): String = when {
|
|
|
63
64
|
}
|
|
64
65
|
else -> ".jpg"
|
|
65
66
|
}
|
|
67
|
+
|
|
68
|
+
class ExifDataHandler(private val uri: Uri) {
|
|
69
|
+
fun copyExifData(newUri: Uri, contentResolver: ContentResolver) {
|
|
70
|
+
if (uri == newUri) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
contentResolver.openInputStream(uri)?.use { input ->
|
|
74
|
+
val oldExif = ExifInterface(input)
|
|
75
|
+
newUri.path?.let {
|
|
76
|
+
val newExif = ExifInterface(it)
|
|
77
|
+
for ((_, exifTag) in ImagePickerConstants.exifTags) {
|
|
78
|
+
val value = oldExif.getAttribute(exifTag)
|
|
79
|
+
if (value != null &&
|
|
80
|
+
exifTag != ExifInterface.TAG_IMAGE_LENGTH &&
|
|
81
|
+
exifTag != ExifInterface.TAG_IMAGE_WIDTH &&
|
|
82
|
+
exifTag != ExifInterface.TAG_PIXEL_X_DIMENSION &&
|
|
83
|
+
exifTag != ExifInterface.TAG_PIXEL_Y_DIMENSION &&
|
|
84
|
+
exifTag != ExifInterface.TAG_ORIENTATION
|
|
85
|
+
) {
|
|
86
|
+
newExif.setAttribute(exifTag, value)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
newExif.saveAttributes()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
package expo.modules.imagepicker
|
|
2
2
|
|
|
3
3
|
import android.os.Bundle
|
|
4
|
-
import
|
|
4
|
+
import expo.modules.core.Promise
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Class that represents a promise, which will not resolve immediately but saves results to the [PickerResultsStore].
|
|
@@ -25,12 +25,14 @@ class PendingPromise(
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
override fun reject(code: String, message: String, e: Throwable?) {
|
|
28
|
-
pickerResultsStore.addPendingResult(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
pickerResultsStore.addPendingResult(
|
|
29
|
+
Bundle().apply {
|
|
30
|
+
putString("code", code)
|
|
31
|
+
putString("message", message)
|
|
32
|
+
e?.let {
|
|
33
|
+
putString("exception", it.toString())
|
|
34
|
+
}
|
|
33
35
|
}
|
|
34
|
-
|
|
36
|
+
)
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -2,64 +2,106 @@ package expo.modules.imagepicker.tasks
|
|
|
2
2
|
|
|
3
3
|
import android.content.ContentResolver
|
|
4
4
|
import android.net.Uri
|
|
5
|
-
import android.os.AsyncTask
|
|
6
5
|
import android.os.Bundle
|
|
7
6
|
import android.util.Base64
|
|
7
|
+
import android.util.Log
|
|
8
8
|
import androidx.exifinterface.media.ExifInterface
|
|
9
|
+
import expo.modules.core.Promise
|
|
10
|
+
import expo.modules.core.errors.ModuleDestroyedException
|
|
11
|
+
import expo.modules.imagepicker.ExifDataHandler
|
|
9
12
|
import expo.modules.imagepicker.ImagePickerConstants
|
|
10
13
|
import expo.modules.imagepicker.ImagePickerConstants.exifTags
|
|
11
14
|
import expo.modules.imagepicker.exporters.ImageExporter
|
|
12
15
|
import expo.modules.imagepicker.exporters.ImageExporter.Listener
|
|
13
16
|
import expo.modules.imagepicker.fileproviders.FileProvider
|
|
14
|
-
import
|
|
17
|
+
import kotlinx.coroutines.CoroutineScope
|
|
18
|
+
import kotlinx.coroutines.launch
|
|
19
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
15
20
|
import java.io.ByteArrayOutputStream
|
|
21
|
+
import java.io.File
|
|
16
22
|
import java.io.IOException
|
|
23
|
+
import kotlin.coroutines.resume
|
|
24
|
+
import kotlin.coroutines.resumeWithException
|
|
17
25
|
|
|
18
26
|
open class ImageResultTask(
|
|
19
27
|
private val promise: Promise,
|
|
20
28
|
private val uri: Uri,
|
|
21
29
|
private val contentResolver: ContentResolver,
|
|
22
30
|
private val fileProvider: FileProvider,
|
|
31
|
+
private val isEdited: Boolean,
|
|
23
32
|
private val withExifData: Boolean,
|
|
24
|
-
private val imageExporter: ImageExporter
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
private val imageExporter: ImageExporter,
|
|
34
|
+
private var exifDataHandler: ExifDataHandler?,
|
|
35
|
+
private val coroutineScope: CoroutineScope
|
|
36
|
+
) {
|
|
37
|
+
/**
|
|
38
|
+
* We need to make coroutine wait till the file is generated, while the underlying
|
|
39
|
+
* thread is free to continue executing other coroutines.
|
|
40
|
+
*/
|
|
41
|
+
private suspend fun getFile(): File = suspendCancellableCoroutine { cancellableContinuation ->
|
|
29
42
|
try {
|
|
30
43
|
val outputFile = fileProvider.generateFile()
|
|
31
|
-
|
|
44
|
+
cancellableContinuation.resume(outputFile)
|
|
45
|
+
} catch (e: Exception) {
|
|
46
|
+
cancellableContinuation.resumeWithException(e)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
/**
|
|
51
|
+
* We need to make coroutine wait till the exif data is being read, while the underlying
|
|
52
|
+
* thread is free to continue executing other coroutines.
|
|
53
|
+
*/
|
|
54
|
+
private suspend fun getExifData(): Bundle? = suspendCancellableCoroutine { cancellableContinuation ->
|
|
55
|
+
try {
|
|
56
|
+
val exif = if (withExifData) readExif() else null
|
|
57
|
+
cancellableContinuation.resume(exif)
|
|
58
|
+
} catch (e: Exception) {
|
|
59
|
+
cancellableContinuation.resumeWithException(e)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
fun execute() {
|
|
64
|
+
coroutineScope.launch {
|
|
65
|
+
try {
|
|
66
|
+
val outputFile = getFile()
|
|
67
|
+
if (isEdited) {
|
|
68
|
+
exifDataHandler?.copyExifData(uri, contentResolver)
|
|
69
|
+
}
|
|
70
|
+
val exif = getExifData()
|
|
71
|
+
val imageExporterHandler = object : Listener {
|
|
72
|
+
override fun onResult(out: ByteArrayOutputStream?, width: Int, height: Int) {
|
|
73
|
+
val response = Bundle().apply {
|
|
74
|
+
putString("uri", Uri.fromFile(outputFile).toString())
|
|
75
|
+
putInt("width", width)
|
|
76
|
+
putInt("height", height)
|
|
77
|
+
putBoolean("cancelled", false)
|
|
78
|
+
putString("type", "image")
|
|
79
|
+
|
|
80
|
+
out?.let {
|
|
81
|
+
putString("base64", Base64.encodeToString(it.toByteArray(), Base64.NO_WRAP))
|
|
82
|
+
}
|
|
83
|
+
exif?.let {
|
|
84
|
+
putBundle("exif", it)
|
|
85
|
+
}
|
|
47
86
|
}
|
|
87
|
+
promise.resolve(response)
|
|
48
88
|
}
|
|
49
|
-
promise.resolve(response)
|
|
50
|
-
}
|
|
51
89
|
|
|
52
|
-
|
|
53
|
-
|
|
90
|
+
override fun onFailure(cause: Throwable?) {
|
|
91
|
+
promise.reject(ImagePickerConstants.ERR_CAN_NOT_SAVE_RESULT, ImagePickerConstants.CAN_NOT_SAVE_RESULT_MESSAGE, cause)
|
|
92
|
+
}
|
|
54
93
|
}
|
|
94
|
+
imageExporter.export(uri, outputFile, imageExporterHandler)
|
|
95
|
+
} catch (e: ModuleDestroyedException) {
|
|
96
|
+
Log.i(ImagePickerConstants.TAG, ImagePickerConstants.COROUTINE_CANCELED, e)
|
|
97
|
+
promise.reject(ImagePickerConstants.COROUTINE_CANCELED, e)
|
|
98
|
+
} catch (e: IOException) {
|
|
99
|
+
promise.reject(ImagePickerConstants.ERR_CAN_NOT_EXTRACT_METADATA, ImagePickerConstants.CAN_NOT_EXTRACT_METADATA_MESSAGE, e)
|
|
100
|
+
} catch (e: Exception) {
|
|
101
|
+
Log.e(ImagePickerConstants.TAG, ImagePickerConstants.UNKNOWN_EXCEPTION, e)
|
|
102
|
+
promise.reject(ImagePickerConstants.UNKNOWN_EXCEPTION, e)
|
|
55
103
|
}
|
|
56
|
-
|
|
57
|
-
imageExporter.export(uri, outputFile, imageExporterHandler)
|
|
58
|
-
} catch (e: IOException) {
|
|
59
|
-
promise.reject(ImagePickerConstants.ERR_CAN_NOT_EXTRACT_METADATA, ImagePickerConstants.CAN_NOT_EXTRACT_METADATA_MESSAGE, e)
|
|
60
104
|
}
|
|
61
|
-
|
|
62
|
-
return null
|
|
63
105
|
}
|
|
64
106
|
|
|
65
107
|
@Throws(IOException::class)
|
|
@@ -3,49 +3,76 @@ package expo.modules.imagepicker.tasks
|
|
|
3
3
|
import android.content.ContentResolver
|
|
4
4
|
import android.media.MediaMetadataRetriever
|
|
5
5
|
import android.net.Uri
|
|
6
|
-
import android.os.AsyncTask
|
|
7
6
|
import android.os.Bundle
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import expo.modules.core.Promise
|
|
9
|
+
import expo.modules.core.errors.ModuleDestroyedException
|
|
8
10
|
import expo.modules.imagepicker.ImagePickerConstants
|
|
9
11
|
import expo.modules.imagepicker.fileproviders.FileProvider
|
|
10
|
-
import
|
|
12
|
+
import kotlinx.coroutines.CoroutineScope
|
|
13
|
+
import kotlinx.coroutines.launch
|
|
14
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
11
15
|
import java.io.File
|
|
12
16
|
import java.io.FileOutputStream
|
|
13
17
|
import java.io.IOException
|
|
14
|
-
import
|
|
18
|
+
import kotlin.coroutines.resume
|
|
19
|
+
import kotlin.coroutines.resumeWithException
|
|
15
20
|
|
|
16
21
|
class VideoResultTask(
|
|
17
22
|
private val promise: Promise,
|
|
18
23
|
private val uri: Uri,
|
|
19
24
|
private val contentResolver: ContentResolver,
|
|
20
25
|
private val fileProvider: FileProvider,
|
|
21
|
-
private val mediaMetadataRetriever: MediaMetadataRetriever
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
private val mediaMetadataRetriever: MediaMetadataRetriever,
|
|
27
|
+
private val coroutineScope: CoroutineScope
|
|
28
|
+
) {
|
|
29
|
+
private fun extractMediaMetadata(key: Int): Int =
|
|
30
|
+
mediaMetadataRetriever.extractMetadata(key)!!.toInt()
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
/**
|
|
33
|
+
* We need to make coroutine wait till the video is saved, while the underlying
|
|
34
|
+
* thread is free to continue executing other coroutines.
|
|
35
|
+
*/
|
|
36
|
+
private suspend fun getFile(): File = suspendCancellableCoroutine { cancellableContinuation ->
|
|
26
37
|
try {
|
|
27
38
|
val outputFile = fileProvider.generateFile()
|
|
28
39
|
saveVideo(outputFile)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
cancellableContinuation.resume(outputFile)
|
|
41
|
+
} catch (e: Exception) {
|
|
42
|
+
cancellableContinuation.resumeWithException(e)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun execute() {
|
|
47
|
+
coroutineScope.launch {
|
|
48
|
+
try {
|
|
49
|
+
val outputFile = getFile()
|
|
50
|
+
val response = Bundle().apply {
|
|
51
|
+
putString("uri", Uri.fromFile(outputFile).toString())
|
|
52
|
+
putBoolean("cancelled", false)
|
|
53
|
+
putString("type", "video")
|
|
54
|
+
putInt("width", extractMediaMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH))
|
|
55
|
+
putInt("height", extractMediaMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT))
|
|
56
|
+
putInt("rotation", extractMediaMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION))
|
|
57
|
+
putInt("duration", extractMediaMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION))
|
|
58
|
+
}
|
|
59
|
+
promise.resolve(response)
|
|
60
|
+
} catch (e: ModuleDestroyedException) {
|
|
61
|
+
Log.d(ImagePickerConstants.TAG, ImagePickerConstants.COROUTINE_CANCELED, e)
|
|
62
|
+
promise.reject(e)
|
|
63
|
+
} catch (e: NullPointerException) {
|
|
64
|
+
promise.reject(ImagePickerConstants.ERR_CAN_NOT_EXTRACT_METADATA, ImagePickerConstants.CAN_NOT_EXTRACT_METADATA_MESSAGE, e)
|
|
65
|
+
} catch (e: IllegalArgumentException) {
|
|
66
|
+
promise.reject(ImagePickerConstants.ERR_CAN_NOT_EXTRACT_METADATA, ImagePickerConstants.CAN_NOT_EXTRACT_METADATA_MESSAGE, e)
|
|
67
|
+
} catch (e: SecurityException) {
|
|
68
|
+
promise.reject(ImagePickerConstants.ERR_CAN_NOT_EXTRACT_METADATA, ImagePickerConstants.CAN_NOT_EXTRACT_METADATA_MESSAGE, e)
|
|
69
|
+
} catch (e: IOException) {
|
|
70
|
+
promise.reject(ImagePickerConstants.ERR_CAN_NOT_SAVE_RESULT, ImagePickerConstants.CAN_NOT_SAVE_RESULT_MESSAGE, e)
|
|
71
|
+
} catch (e: Exception) {
|
|
72
|
+
Log.e(ImagePickerConstants.TAG, ImagePickerConstants.UNKNOWN_EXCEPTION, e)
|
|
73
|
+
promise.reject(ImagePickerConstants.UNKNOWN_EXCEPTION, e)
|
|
37
74
|
}
|
|
38
|
-
promise.resolve(response)
|
|
39
|
-
} catch (e: NullPointerException) {
|
|
40
|
-
promise.reject(ImagePickerConstants.ERR_CAN_NOT_EXTRACT_METADATA, ImagePickerConstants.CAN_NOT_EXTRACT_METADATA_MESSAGE, e)
|
|
41
|
-
} catch (e: IllegalArgumentException) {
|
|
42
|
-
promise.reject(ImagePickerConstants.ERR_CAN_NOT_EXTRACT_METADATA, ImagePickerConstants.CAN_NOT_EXTRACT_METADATA_MESSAGE, e)
|
|
43
|
-
} catch (e: SecurityException) {
|
|
44
|
-
promise.reject(ImagePickerConstants.ERR_CAN_NOT_EXTRACT_METADATA, ImagePickerConstants.CAN_NOT_EXTRACT_METADATA_MESSAGE, e)
|
|
45
|
-
} catch (e: IOException) {
|
|
46
|
-
promise.reject(ImagePickerConstants.ERR_CAN_NOT_SAVE_RESULT, ImagePickerConstants.CAN_NOT_SAVE_RESULT_MESSAGE, e)
|
|
47
75
|
}
|
|
48
|
-
return null
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
@Throws(IOException::class)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("expo-modules-core").ProxyNativeModule;
|
|
2
2
|
export default _default;
|