expo-image-picker 13.2.0 → 13.3.1
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 +23 -2
- package/android/build.gradle +8 -6
- package/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt +22 -14
- package/android/src/main/java/expo/modules/imagepicker/ImagePickerOptions.kt +3 -0
- package/android/src/main/java/expo/modules/imagepicker/ImagePickerResponse.kt +33 -23
- package/android/src/main/java/expo/modules/imagepicker/ImagePickerUtils.kt +108 -0
- package/android/src/main/java/expo/modules/imagepicker/MediaHandler.kt +20 -8
- package/android/src/main/java/expo/modules/imagepicker/contracts/CameraContract.kt +10 -2
- package/android/src/main/java/expo/modules/imagepicker/contracts/ContractsUtils.kt +1 -7
- package/android/src/main/java/expo/modules/imagepicker/contracts/CropImageContract.kt +20 -25
- package/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt +18 -4
- package/build/ImagePicker.d.ts +1 -1
- package/build/ImagePicker.js +1 -1
- package/build/ImagePicker.js.map +1 -1
- package/build/ImagePicker.types.d.ts +44 -0
- package/build/ImagePicker.types.d.ts.map +1 -1
- package/build/ImagePicker.types.js.map +1 -1
- package/ios/ImagePickerExceptions.swift +18 -0
- package/ios/ImagePickerModule.swift +7 -3
- package/ios/ImagePickerOptions.swift +7 -0
- package/ios/ImagePickerResponse.swift +13 -1
- package/ios/MediaHandler.swift +145 -38
- package/package.json +2 -2
- package/src/ImagePicker.ts +1 -1
- package/src/ImagePicker.types.ts +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,11 +10,32 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 13.3.1 — 2022-07-25
|
|
14
|
+
|
|
15
|
+
_This version does not introduce any user-facing changes._
|
|
16
|
+
|
|
17
|
+
## 13.3.0 — 2022-07-16
|
|
18
|
+
|
|
19
|
+
### 🎉 New features
|
|
20
|
+
|
|
21
|
+
- On iOS 14+ added support for selection limit and on iOS 15+ for ordered selection. ([#18142](https://github.com/expo/expo/pull/18142), [#18143](https://github.com/expo/expo/pull/18143) by [@barthap](https://github.com/barthap))
|
|
22
|
+
- The picker now resolves media library asset ID. ([#18236](https://github.com/expo/expo/pull/18236), [#18185](https://github.com/expo/expo/pull/18185) by [@barthap](https://github.com/barthap))
|
|
23
|
+
- On iOS, the picker now resolves file name and size and media library asset ID. ([#18179](https://github.com/expo/expo/pull/18179) by [@barthap](https://github.com/barthap))
|
|
24
|
+
- On Android added support for selecting multiple images/videos. ([#18161](https://github.com/expo/expo/pull/18161) by [@bbarthec](https://github.com/bbarthec))
|
|
25
|
+
|
|
26
|
+
### 🐛 Bug fixes
|
|
27
|
+
|
|
28
|
+
- On Android restored support for `allowsEditing` option that was disabled when migrating to `registerForActivityResult` mechanism. ([#17963](https://github.com/expo/expo/pull/17963) by [@bbarthec](https://github.com/bbarthec))
|
|
29
|
+
|
|
30
|
+
## 13.2.1 — 2022-07-11
|
|
31
|
+
|
|
32
|
+
_This version does not introduce any user-facing changes._
|
|
33
|
+
|
|
13
34
|
## 13.2.0 — 2022-07-07
|
|
14
35
|
|
|
15
36
|
### 🎉 New features
|
|
16
37
|
|
|
17
|
-
- On iOS 14+ added support for selecting multiple images/videos. ([#18102](https://github.com/expo/expo/pull/18102) by [@barthap](https://github.com/barthap))
|
|
38
|
+
- On iOS 14+ added support for selecting multiple images/videos. ([#18102](https://github.com/expo/expo/pull/18102), [#18138](https://github.com/expo/expo/pull/18138), [#18145](https://github.com/expo/expo/pull/18145) by [@barthap](https://github.com/barthap))
|
|
18
39
|
|
|
19
40
|
### 🐛 Bug fixes
|
|
20
41
|
|
|
@@ -22,7 +43,7 @@
|
|
|
22
43
|
|
|
23
44
|
### 💡 Others
|
|
24
45
|
|
|
25
|
-
- On Android migrated to the new `registerForActivityResult` mechanism. ([#17671](https://github.com/expo/expo/pull/17671), ([#17987](https://github.com/expo/expo/pull/17987) by [@bbarthec](https://github.com/bbarthec))
|
|
46
|
+
- On Android migrated to the new `registerForActivityResult` mechanism. This migration disables `allowsEditing` option. ([#17671](https://github.com/expo/expo/pull/17671), ([#17987](https://github.com/expo/expo/pull/17987) by [@bbarthec](https://github.com/bbarthec))
|
|
26
47
|
- Native module on Android is now written in Kotlin using [Sweet API](https://docs.expo.dev/modules/module-api). ([#17668](https://github.com/expo/expo/pull/17668) by [@bbarthec](https://github.com/bbarthec))
|
|
27
48
|
- Migrated Expo modules definitions to the new naming convention. ([#17193](https://github.com/expo/expo/pull/17193) by [@tsapeta](https://github.com/tsapeta))
|
|
28
49
|
|
package/android/build.gradle
CHANGED
|
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
|
|
|
3
3
|
apply plugin: 'maven-publish'
|
|
4
4
|
|
|
5
5
|
group = 'host.exp.exponent'
|
|
6
|
-
version = '13.
|
|
6
|
+
version = '13.3.1'
|
|
7
7
|
|
|
8
8
|
buildscript {
|
|
9
9
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
@@ -74,7 +74,7 @@ android {
|
|
|
74
74
|
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
75
75
|
targetSdkVersion safeExtGet("targetSdkVersion", 31)
|
|
76
76
|
versionCode 22
|
|
77
|
-
versionName "13.
|
|
77
|
+
versionName "13.3.1"
|
|
78
78
|
}
|
|
79
79
|
lintOptions {
|
|
80
80
|
abortOnError false
|
|
@@ -84,9 +84,11 @@ android {
|
|
|
84
84
|
dependencies {
|
|
85
85
|
implementation project(':expo-modules-core')
|
|
86
86
|
|
|
87
|
-
implementation "
|
|
88
|
-
implementation
|
|
89
|
-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3")
|
|
90
|
-
implementation "com.github.CanHub:Android-Image-Cropper:1.1.1"
|
|
87
|
+
implementation "androidx.activity:activity-ktx:1.4.0"
|
|
88
|
+
implementation "androidx.appcompat:appcompat:1.4.2"
|
|
91
89
|
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
|
90
|
+
implementation "com.github.CanHub:Android-Image-Cropper:4.3.0"
|
|
91
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
92
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3"
|
|
93
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3"
|
|
92
94
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
@file:OptIn(ExperimentalContracts::class)
|
|
2
|
-
|
|
3
1
|
package expo.modules.imagepicker
|
|
4
2
|
|
|
5
3
|
import android.Manifest
|
|
@@ -10,6 +8,8 @@ import expo.modules.core.errors.ModuleNotFoundException
|
|
|
10
8
|
import android.os.OperationCanceledException
|
|
11
9
|
import expo.modules.imagepicker.contracts.CameraContract
|
|
12
10
|
import expo.modules.imagepicker.contracts.CameraContractOptions
|
|
11
|
+
import expo.modules.imagepicker.contracts.CropImageContract
|
|
12
|
+
import expo.modules.imagepicker.contracts.CropImageContractOptions
|
|
13
13
|
import expo.modules.imagepicker.contracts.ImageLibraryContract
|
|
14
14
|
import expo.modules.imagepicker.contracts.ImageLibraryContractOptions
|
|
15
15
|
import expo.modules.imagepicker.contracts.ImagePickerContractResult
|
|
@@ -24,7 +24,6 @@ import kotlinx.coroutines.Dispatchers
|
|
|
24
24
|
import kotlinx.coroutines.launch
|
|
25
25
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
26
26
|
import kotlinx.coroutines.withContext
|
|
27
|
-
import kotlin.contracts.ExperimentalContracts
|
|
28
27
|
import kotlin.coroutines.resume
|
|
29
28
|
import kotlin.coroutines.resumeWithException
|
|
30
29
|
|
|
@@ -71,7 +70,7 @@ class ImagePickerModule : Module() {
|
|
|
71
70
|
launchContract({ imageLibraryLauncher.launch(contractOptions) }, options)
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
AsyncFunction("getPendingResultAsync") Coroutine {
|
|
73
|
+
AsyncFunction("getPendingResultAsync") Coroutine { ->
|
|
75
74
|
val (bareResult, options) = pendingMediaPickingResult ?: return@Coroutine null
|
|
76
75
|
|
|
77
76
|
pendingMediaPickingResult = null
|
|
@@ -90,6 +89,9 @@ class ImagePickerModule : Module() {
|
|
|
90
89
|
imageLibraryLauncher = appContext.registerForActivityResult(
|
|
91
90
|
ImageLibraryContract(this@ImagePickerModule),
|
|
92
91
|
) { input, result -> handleResultUponActivityDestruction(result, input.options) }
|
|
92
|
+
cropImageLauncher = appContext.registerForActivityResult(
|
|
93
|
+
CropImageContract(this@ImagePickerModule),
|
|
94
|
+
) { input, result -> handleResultUponActivityDestruction(result, input.options) }
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
}
|
|
@@ -106,6 +108,7 @@ class ImagePickerModule : Module() {
|
|
|
106
108
|
|
|
107
109
|
private lateinit var cameraLauncher: AppContextActivityResultLauncher<CameraContractOptions, ImagePickerContractResult>
|
|
108
110
|
private lateinit var imageLibraryLauncher: AppContextActivityResultLauncher<ImageLibraryContractOptions, ImagePickerContractResult>
|
|
111
|
+
private lateinit var cropImageLauncher: AppContextActivityResultLauncher<CropImageContractOptions, ImagePickerContractResult>
|
|
109
112
|
|
|
110
113
|
/**
|
|
111
114
|
* Stores result for an operation that has been interrupted by the activity destruction.
|
|
@@ -123,8 +126,18 @@ class ImagePickerModule : Module() {
|
|
|
123
126
|
options: ImagePickerOptions,
|
|
124
127
|
): Any {
|
|
125
128
|
return try {
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
var result = launchPicker(pickerLauncher)
|
|
130
|
+
if (
|
|
131
|
+
!options.allowsMultipleSelection &&
|
|
132
|
+
options.allowsEditing &&
|
|
133
|
+
result.data.size == 1 &&
|
|
134
|
+
result.data[0].first == MediaType.IMAGE
|
|
135
|
+
) {
|
|
136
|
+
result = launchPicker {
|
|
137
|
+
cropImageLauncher.launch(CropImageContractOptions(result.data[0].second, options))
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
mediaHandler.readExtras(result.data, options)
|
|
128
141
|
} catch (cause: OperationCanceledException) {
|
|
129
142
|
ImagePickerCancelledResponse()
|
|
130
143
|
}
|
|
@@ -145,9 +158,9 @@ class ImagePickerModule : Module() {
|
|
|
145
158
|
*/
|
|
146
159
|
private suspend fun launchPicker(
|
|
147
160
|
pickerLauncher: suspend () -> ImagePickerContractResult,
|
|
148
|
-
):
|
|
161
|
+
): ImagePickerContractResult.Success = withContext(Dispatchers.Main) {
|
|
149
162
|
when (val pickingResult = pickerLauncher()) {
|
|
150
|
-
is ImagePickerContractResult.Success -> pickingResult
|
|
163
|
+
is ImagePickerContractResult.Success -> pickingResult
|
|
151
164
|
is ImagePickerContractResult.Cancelled -> throw OperationCanceledException()
|
|
152
165
|
}
|
|
153
166
|
}
|
|
@@ -191,15 +204,10 @@ class ImagePickerModule : Module() {
|
|
|
191
204
|
// endregion
|
|
192
205
|
}
|
|
193
206
|
|
|
194
|
-
internal enum class PickingSource {
|
|
195
|
-
CAMERA,
|
|
196
|
-
IMAGE_LIBRARY
|
|
197
|
-
}
|
|
198
|
-
|
|
199
207
|
/**
|
|
200
208
|
* Simple data structure to hold the data that has to be preserved after the Activity is destroyed.
|
|
201
209
|
*/
|
|
202
210
|
internal data class PendingMediaPickingResult(
|
|
203
|
-
val data: Pair<MediaType, Uri
|
|
211
|
+
val data: List<Pair<MediaType, Uri>>,
|
|
204
212
|
val options: ImagePickerOptions
|
|
205
213
|
)
|
|
@@ -9,32 +9,42 @@ internal class ImagePickerCancelledResponse : Record {
|
|
|
9
9
|
val cancelled: Boolean = true
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
internal sealed class
|
|
13
|
-
@Field val type: MediaType,
|
|
14
|
-
@Field val uri: String,
|
|
15
|
-
@Field val width: Int,
|
|
16
|
-
@Field val height: Int,
|
|
17
|
-
) : Record {
|
|
12
|
+
internal sealed class ImagePickerResponse : Record {
|
|
18
13
|
@Field
|
|
19
14
|
val cancelled: Boolean = false
|
|
20
15
|
|
|
21
|
-
class
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@Field val
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
16
|
+
sealed class Single(
|
|
17
|
+
@Field val type: MediaType,
|
|
18
|
+
@Field val uri: String,
|
|
19
|
+
@Field val width: Int,
|
|
20
|
+
@Field val height: Int,
|
|
21
|
+
@Field val assetId: String?,
|
|
22
|
+
) : ImagePickerResponse() {
|
|
23
|
+
class Image(
|
|
24
|
+
uri: String,
|
|
25
|
+
width: Int,
|
|
26
|
+
height: Int,
|
|
27
|
+
assetId: String?,
|
|
28
|
+
|
|
29
|
+
@Field val base64: String?,
|
|
30
|
+
@Field val exif: Bundle?
|
|
31
|
+
) : Single(MediaType.IMAGE, uri, width, height, assetId)
|
|
32
|
+
|
|
33
|
+
class Video(
|
|
34
|
+
uri: String,
|
|
35
|
+
width: Int,
|
|
36
|
+
height: Int,
|
|
37
|
+
assetId: String?,
|
|
38
|
+
|
|
39
|
+
@Field val duration: Int,
|
|
40
|
+
@Field val rotation: Int
|
|
41
|
+
) : Single(MediaType.VIDEO, uri, width, height, assetId)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class Multiple(
|
|
45
|
+
@Field
|
|
46
|
+
val selected: List<Single>
|
|
47
|
+
) : ImagePickerResponse()
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
enum class MediaType(val value: String) {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
package expo.modules.imagepicker
|
|
2
2
|
|
|
3
|
+
import android.content.ClipData
|
|
3
4
|
import android.content.ContentResolver
|
|
4
5
|
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
5
7
|
import android.graphics.Bitmap
|
|
6
8
|
import android.media.MediaMetadataRetriever
|
|
7
9
|
import android.net.Uri
|
|
10
|
+
import android.provider.DocumentsContract
|
|
8
11
|
import android.util.Log
|
|
9
12
|
import android.webkit.MimeTypeMap
|
|
10
13
|
import androidx.core.content.FileProvider
|
|
@@ -100,6 +103,42 @@ internal fun MediaMetadataRetriever.extractInt(key: Int): Int {
|
|
|
100
103
|
return this.extractMetadata(key)?.toInt() ?: throw FailedToExtractVideoMetadataException()
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* [Iterable] implementation for [ClipData] items
|
|
108
|
+
*/
|
|
109
|
+
val ClipData.items: Iterable<ClipData.Item>
|
|
110
|
+
get() = object : Iterable<ClipData.Item> {
|
|
111
|
+
override fun iterator() = object : Iterator<ClipData.Item> {
|
|
112
|
+
var index = 0
|
|
113
|
+
val count = itemCount
|
|
114
|
+
|
|
115
|
+
override fun hasNext(): Boolean = index < count
|
|
116
|
+
|
|
117
|
+
override fun next(): ClipData.Item = getItemAt(index++)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Gets all data that is associated with this [Intent].
|
|
123
|
+
* Original data order is preserved.
|
|
124
|
+
*
|
|
125
|
+
* Adapted from [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents.getClipDataUris]
|
|
126
|
+
*/
|
|
127
|
+
internal fun Intent.getAllDataUris(): List<Uri> {
|
|
128
|
+
// Use a LinkedHashSet to maintain any ordering that may be present in the ClipData
|
|
129
|
+
val resultSet = LinkedHashSet<Uri>()
|
|
130
|
+
|
|
131
|
+
data
|
|
132
|
+
?.let { resultSet.add(it) }
|
|
133
|
+
|
|
134
|
+
clipData
|
|
135
|
+
?.items
|
|
136
|
+
?.map { it.uri }
|
|
137
|
+
?.let { resultSet.addAll(it) }
|
|
138
|
+
|
|
139
|
+
return resultSet.toList()
|
|
140
|
+
}
|
|
141
|
+
|
|
103
142
|
/**
|
|
104
143
|
* Copy the media file from `sourceUri` to `destinationUri`.
|
|
105
144
|
*
|
|
@@ -168,3 +207,72 @@ internal suspend fun copyExifData(
|
|
|
168
207
|
throw FailedToWriteFileException(targetFile, cause)
|
|
169
208
|
}
|
|
170
209
|
}
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
Getting asset ID (and metadata) on Android is not that obvious. When getting a `content://` URI
|
|
213
|
+
using `ACTION_GET_CONTENT` or `ACTION_OPEN_DOCUMENT` intents, there are 3 possible ways:
|
|
214
|
+
|
|
215
|
+
1. When the user selects a photo from **Images** section of the picker (on the left drawer)
|
|
216
|
+
In this case we get a URI from `com.android.providers.media.MediaDocumentsProvider`,
|
|
217
|
+
that inherits from `DocumentsProvider`. The URI looks like this:
|
|
218
|
+
```
|
|
219
|
+
com.android.providers.media.documents/document/image:56
|
|
220
|
+
```
|
|
221
|
+
In this case, the `56` is the ID we're looking for.
|
|
222
|
+
|
|
223
|
+
2. When the user selects a photo from **Downloads** section, another content provider is used:
|
|
224
|
+
`DownloadStorageProvider` which is a bit different, and also differs depending on Android version:
|
|
225
|
+
- On API 29+ it also inherits from `com.android.providers.downloads.DocumentsProvider`
|
|
226
|
+
and the URI looks like this:
|
|
227
|
+
```
|
|
228
|
+
com.android.providers.downloads.documents/document/msf:56
|
|
229
|
+
```
|
|
230
|
+
Where "msf" is abbr. of "media store file" and 56 is our asset ID
|
|
231
|
+
- On API <29 it looks similar:
|
|
232
|
+
```
|
|
233
|
+
com.android.providers.downloads.documents/document/128
|
|
234
|
+
```
|
|
235
|
+
but the 128 is an internal ID of downloads provider, unrelated to media store asset ID.
|
|
236
|
+
|
|
237
|
+
3. When the user selects a photo by browsing the filesystem, the URI looks like this:
|
|
238
|
+
```
|
|
239
|
+
com.android.externalstorage.documents/document/primary:Download:filename.jpg
|
|
240
|
+
```
|
|
241
|
+
No ID in this case
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Checks whether this [Uri] is a `com.android.providers.media.documents` provider uri
|
|
246
|
+
*/
|
|
247
|
+
internal val Uri.isMediaProviderUri
|
|
248
|
+
get() = this.authority == "com.android.providers.media.documents"
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Checks whether this [Uri] is a `com.android.providers.downloads.documents` provider uri
|
|
252
|
+
*/
|
|
253
|
+
internal val Uri.isDownloadsProviderUri
|
|
254
|
+
get() = this.authority == "com.android.providers.downloads.documents"
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Checks whether asset represented by this [Uri] can be queried in the media store
|
|
258
|
+
*/
|
|
259
|
+
internal val Uri.isMediaStoreAssetUri
|
|
260
|
+
get() = isMediaProviderUri || (
|
|
261
|
+
isDownloadsProviderUri &&
|
|
262
|
+
DocumentsContract
|
|
263
|
+
.getDocumentId(this)
|
|
264
|
+
.startsWith("msf:")
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* If the URI represents a media store asset, this returns its ID. Otherwise, returns `null`.
|
|
269
|
+
*
|
|
270
|
+
* See the detailed explanation above in this file (ImagePickerUtils.kt).
|
|
271
|
+
*/
|
|
272
|
+
internal fun Uri.getMediaStoreAssetId(): String? {
|
|
273
|
+
if (isMediaStoreAssetUri) {
|
|
274
|
+
val rawId = DocumentsContract.getDocumentId(this)
|
|
275
|
+
return if (rawId.contains(':')) rawId.split(':')[1] else rawId
|
|
276
|
+
}
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
@@ -17,17 +17,27 @@ internal class MediaHandler(
|
|
|
17
17
|
get() = requireNotNull(appContextProvider.appContext.reactContext) { "React Application Context is null" }
|
|
18
18
|
|
|
19
19
|
internal suspend fun readExtras(
|
|
20
|
-
bareResult: Pair<MediaType, Uri
|
|
20
|
+
bareResult: List<Pair<MediaType, Uri>>,
|
|
21
21
|
options: ImagePickerOptions,
|
|
22
|
-
):
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
): ImagePickerResponse {
|
|
23
|
+
val results = bareResult.map { (mediaType, uri) ->
|
|
24
|
+
when (mediaType) {
|
|
25
|
+
MediaType.VIDEO -> handleVideo(uri)
|
|
26
|
+
MediaType.IMAGE -> handleImage(uri, options)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return if (results.size == 1) {
|
|
31
|
+
results[0]
|
|
32
|
+
} else {
|
|
33
|
+
ImagePickerResponse.Multiple(results)
|
|
34
|
+
}
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
private suspend fun handleImage(
|
|
28
38
|
sourceUri: Uri,
|
|
29
39
|
options: ImagePickerOptions,
|
|
30
|
-
):
|
|
40
|
+
): ImagePickerResponse.Single.Image {
|
|
31
41
|
val exporter: ImageExporter = if (options.quality == ImagePickerConstants.MAXIMUM_QUALITY) {
|
|
32
42
|
RawImageExporter()
|
|
33
43
|
} else {
|
|
@@ -43,18 +53,19 @@ internal class MediaHandler(
|
|
|
43
53
|
val exif = options.exif.takeIf { it }
|
|
44
54
|
?.let { exportedImage.exif(context.contentResolver) }
|
|
45
55
|
|
|
46
|
-
return
|
|
56
|
+
return ImagePickerResponse.Single.Image(
|
|
47
57
|
uri = Uri.fromFile(outputFile).toString(),
|
|
48
58
|
width = exportedImage.width,
|
|
49
59
|
height = exportedImage.height,
|
|
50
60
|
base64 = base64,
|
|
51
61
|
exif = exif,
|
|
62
|
+
assetId = sourceUri.getMediaStoreAssetId()
|
|
52
63
|
)
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
private suspend fun handleVideo(
|
|
56
67
|
sourceUri: Uri,
|
|
57
|
-
):
|
|
68
|
+
): ImagePickerResponse.Single.Video {
|
|
58
69
|
val outputFile = createOutputFile(context.cacheDir, ".mp4")
|
|
59
70
|
copyFile(sourceUri, outputFile, context.contentResolver)
|
|
60
71
|
val outputUri = outputFile.toUri()
|
|
@@ -64,12 +75,13 @@ internal class MediaHandler(
|
|
|
64
75
|
setDataSource(context, outputUri)
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
ImagePickerResponse.Single.Video(
|
|
68
79
|
uri = outputUri.toString(),
|
|
69
80
|
width = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH),
|
|
70
81
|
height = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT),
|
|
71
82
|
duration = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_DURATION),
|
|
72
83
|
rotation = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION),
|
|
84
|
+
assetId = sourceUri.getMediaStoreAssetId()
|
|
73
85
|
)
|
|
74
86
|
} catch (cause: FailedToExtractVideoMetadataException) {
|
|
75
87
|
throw FailedToExtractVideoMetadataException(outputFile, cause)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package expo.modules.imagepicker.contracts
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
+
import android.content.ContentResolver
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.content.Intent
|
|
6
7
|
import android.net.Uri
|
|
@@ -21,6 +22,11 @@ import java.io.Serializable
|
|
|
21
22
|
internal class CameraContract(
|
|
22
23
|
private val appContextProvider: AppContextProvider,
|
|
23
24
|
) : AppContextActivityResultContract<CameraContractOptions, ImagePickerContractResult> {
|
|
25
|
+
val contentResolver: ContentResolver
|
|
26
|
+
get() = requireNotNull(appContextProvider.appContext.reactContext) {
|
|
27
|
+
"React Application Context is null"
|
|
28
|
+
}.contentResolver
|
|
29
|
+
|
|
24
30
|
override fun createIntent(context: Context, input: CameraContractOptions): Intent =
|
|
25
31
|
Intent(input.options.mediaTypes.toCameraIntentAction())
|
|
26
32
|
.putExtra(MediaStore.EXTRA_OUTPUT, input.uri)
|
|
@@ -34,14 +40,16 @@ internal class CameraContract(
|
|
|
34
40
|
if (resultCode == Activity.RESULT_CANCELED) {
|
|
35
41
|
ImagePickerContractResult.Cancelled()
|
|
36
42
|
} else {
|
|
37
|
-
val contentResolver = requireNotNull(appContextProvider.appContext.reactContext) { "React Application Context is null. " }.contentResolver
|
|
38
43
|
val uri = input.uri
|
|
39
44
|
val type = uri.toMediaType(contentResolver)
|
|
40
|
-
ImagePickerContractResult.Success(type to uri)
|
|
45
|
+
ImagePickerContractResult.Success(listOf(type to uri))
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
internal data class CameraContractOptions(
|
|
50
|
+
/**
|
|
51
|
+
* Destination file in a form of content-[Uri] to save results coming from camera to.
|
|
52
|
+
*/
|
|
45
53
|
val uri: Uri,
|
|
46
54
|
val options: ImagePickerOptions,
|
|
47
55
|
) : Serializable
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
package expo.modules.imagepicker.contracts
|
|
2
2
|
|
|
3
3
|
import android.net.Uri
|
|
4
|
-
import androidx.activity.result.contract.ActivityResultContract
|
|
5
4
|
import expo.modules.imagepicker.MediaType
|
|
6
5
|
|
|
7
|
-
/**
|
|
8
|
-
* Alias for [androidx.activity.result.contract.ActivityResultContract] with required generic types already specified
|
|
9
|
-
*/
|
|
10
|
-
internal typealias ImagePickerContract = ActivityResultContract<Any?, ImagePickerContractResult>
|
|
11
|
-
|
|
12
6
|
/**
|
|
13
7
|
* Data required to be returned upon successful contract completion
|
|
14
8
|
*/
|
|
15
9
|
internal sealed class ImagePickerContractResult private constructor() {
|
|
16
10
|
class Cancelled : ImagePickerContractResult()
|
|
17
|
-
class Success(val data: Pair<MediaType, Uri
|
|
11
|
+
class Success(val data: List<Pair<MediaType, Uri>>) : ImagePickerContractResult()
|
|
18
12
|
}
|
|
@@ -12,44 +12,34 @@ import com.canhub.cropper.CropImageActivity
|
|
|
12
12
|
import com.canhub.cropper.CropImageOptions
|
|
13
13
|
import expo.modules.imagepicker.ImagePickerOptions
|
|
14
14
|
import expo.modules.imagepicker.MediaType
|
|
15
|
-
import expo.modules.imagepicker.PickingSource
|
|
16
15
|
import expo.modules.imagepicker.copyExifData
|
|
17
16
|
import expo.modules.imagepicker.createOutputFile
|
|
18
17
|
import expo.modules.imagepicker.toBitmapCompressFormat
|
|
19
18
|
import expo.modules.imagepicker.toImageFileExtension
|
|
19
|
+
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
|
|
20
20
|
import expo.modules.kotlin.providers.AppContextProvider
|
|
21
21
|
import kotlinx.coroutines.runBlocking
|
|
22
|
+
import java.io.Serializable
|
|
22
23
|
|
|
23
24
|
internal class CropImageContract(
|
|
24
25
|
private val appContextProvider: AppContextProvider,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
) : ImagePickerContract() {
|
|
29
|
-
override fun createIntent(context: Context, input: Any?) = Intent(context, CropImageActivity::class.java).apply {
|
|
30
|
-
// for [IMAGE_LIBRARY] we need to create a new file as up to this point we've been operating on the original media asset
|
|
31
|
-
// for [CAMERA] we do not have to do it as it's already been created at the beginning of the picking process
|
|
32
|
-
val needCreateNewFile = pickingSource == PickingSource.IMAGE_LIBRARY
|
|
33
|
-
val mediaType = expo.modules.imagepicker.getType(context.contentResolver, sourceUri)
|
|
34
|
-
|
|
26
|
+
) : AppContextActivityResultContract<CropImageContractOptions, ImagePickerContractResult> {
|
|
27
|
+
override fun createIntent(context: Context, input: CropImageContractOptions) = Intent(context, CropImageActivity::class.java).apply {
|
|
28
|
+
val mediaType = expo.modules.imagepicker.getType(context.contentResolver, input.sourceUri)
|
|
35
29
|
val compressFormat = mediaType.toBitmapCompressFormat()
|
|
36
|
-
|
|
37
|
-
val outputUri: Uri = if (needCreateNewFile) {
|
|
38
|
-
createOutputFile(context.cacheDir, compressFormat.toImageFileExtension()).toUri()
|
|
39
|
-
} else {
|
|
40
|
-
sourceUri
|
|
41
|
-
}
|
|
30
|
+
val outputUri = createOutputFile(context.cacheDir, compressFormat.toImageFileExtension()).toUri()
|
|
42
31
|
|
|
43
32
|
putExtra(
|
|
44
33
|
CropImage.CROP_IMAGE_EXTRA_BUNDLE,
|
|
45
34
|
bundleOf(
|
|
46
|
-
CropImage.CROP_IMAGE_EXTRA_SOURCE to sourceUri,
|
|
35
|
+
CropImage.CROP_IMAGE_EXTRA_SOURCE to input.sourceUri,
|
|
47
36
|
CropImage.CROP_IMAGE_EXTRA_OPTIONS to CropImageOptions().apply {
|
|
48
37
|
outputCompressFormat = compressFormat
|
|
49
|
-
outputCompressQuality = (
|
|
50
|
-
this.outputUri = outputUri
|
|
38
|
+
outputCompressQuality = (input.options.quality * 100).toInt()
|
|
51
39
|
|
|
52
|
-
this
|
|
40
|
+
this.customOutputUri = outputUri
|
|
41
|
+
|
|
42
|
+
input.options.aspect?.let { (x, y) ->
|
|
53
43
|
aspectRatioX = x
|
|
54
44
|
aspectRatioY = y
|
|
55
45
|
fixAspectRatio = true
|
|
@@ -62,14 +52,19 @@ internal class CropImageContract(
|
|
|
62
52
|
)
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
override fun parseResult(resultCode: Int, intent: Intent?): ImagePickerContractResult {
|
|
55
|
+
override fun parseResult(input: CropImageContractOptions, resultCode: Int, intent: Intent?): ImagePickerContractResult {
|
|
66
56
|
val result = intent?.getParcelableExtra<CropImage.ActivityResult?>(CropImage.CROP_IMAGE_EXTRA_RESULT)
|
|
67
57
|
if (resultCode == Activity.RESULT_CANCELED || result == null) {
|
|
68
58
|
return ImagePickerContractResult.Cancelled()
|
|
69
59
|
}
|
|
70
|
-
val targetUri = requireNotNull(result.
|
|
60
|
+
val targetUri = requireNotNull(result.uriContent)
|
|
71
61
|
val contentResolver = requireNotNull(appContextProvider.appContext.reactContext) { "React Application Context is null" }.contentResolver
|
|
72
|
-
runBlocking { copyExifData(sourceUri, targetUri.toFile(), contentResolver) }
|
|
73
|
-
return ImagePickerContractResult.Success(MediaType.IMAGE to targetUri)
|
|
62
|
+
runBlocking { copyExifData(input.sourceUri, targetUri.toFile(), contentResolver) }
|
|
63
|
+
return ImagePickerContractResult.Success(listOf(MediaType.IMAGE to targetUri))
|
|
74
64
|
}
|
|
75
65
|
}
|
|
66
|
+
|
|
67
|
+
internal data class CropImageContractOptions(
|
|
68
|
+
val sourceUri: Uri,
|
|
69
|
+
val options: ImagePickerOptions,
|
|
70
|
+
) : Serializable
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
package expo.modules.imagepicker.contracts
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
+
import android.content.ContentResolver
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.content.Intent
|
|
6
7
|
import android.net.Uri
|
|
7
8
|
import expo.modules.imagepicker.ImagePickerOptions
|
|
9
|
+
import expo.modules.imagepicker.getAllDataUris
|
|
8
10
|
import expo.modules.imagepicker.toMediaType
|
|
9
11
|
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
|
|
10
12
|
import expo.modules.kotlin.providers.AppContextProvider
|
|
@@ -14,25 +16,37 @@ import java.io.Serializable
|
|
|
14
16
|
* An [androidx.activity.result.contract.ActivityResultContract] to prompt the user to pick single or multiple image(s) or/and video(s),
|
|
15
17
|
* receiving a `content://` [Uri] for each piece of content.
|
|
16
18
|
*
|
|
17
|
-
* @see [androidx.activity.result.contract.ActivityResultContracts.GetContent]
|
|
19
|
+
* @see [androidx.activity.result.contract.ActivityResultContracts.GetContent],
|
|
20
|
+
* @see [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents]
|
|
18
21
|
*/
|
|
19
22
|
internal class ImageLibraryContract(
|
|
20
23
|
private val appContextProvider: AppContextProvider,
|
|
21
24
|
) : AppContextActivityResultContract<ImageLibraryContractOptions, ImagePickerContractResult> {
|
|
25
|
+
val contentResolver: ContentResolver
|
|
26
|
+
get() = requireNotNull(appContextProvider.appContext.reactContext) {
|
|
27
|
+
"React Application Context is null"
|
|
28
|
+
}.contentResolver
|
|
29
|
+
|
|
22
30
|
override fun createIntent(context: Context, input: ImageLibraryContractOptions) =
|
|
23
31
|
Intent(Intent.ACTION_GET_CONTENT)
|
|
24
32
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
|
25
33
|
.setType(input.options.mediaTypes.toMimeType())
|
|
34
|
+
.apply {
|
|
35
|
+
if (input.options.allowsMultipleSelection) {
|
|
36
|
+
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
26
39
|
|
|
27
40
|
override fun parseResult(input: ImageLibraryContractOptions, resultCode: Int, intent: Intent?) =
|
|
28
41
|
if (resultCode == Activity.RESULT_CANCELED) {
|
|
29
42
|
ImagePickerContractResult.Cancelled()
|
|
43
|
+
} else if (input.options.allowsMultipleSelection) {
|
|
44
|
+
val uris = requireNotNull(intent).getAllDataUris()
|
|
45
|
+
ImagePickerContractResult.Success(uris.map { uri -> uri.toMediaType(contentResolver) to uri })
|
|
30
46
|
} else {
|
|
31
47
|
val uri = requireNotNull(requireNotNull(intent).data)
|
|
32
|
-
val contentResolver = requireNotNull(appContextProvider.appContext.reactContext) { "React Application Context is null. " }.contentResolver
|
|
33
48
|
val type = uri.toMediaType(contentResolver)
|
|
34
|
-
|
|
35
|
-
ImagePickerContractResult.Success(type to uri)
|
|
49
|
+
ImagePickerContractResult.Success(listOf(type to uri))
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
|
package/build/ImagePicker.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare function getPendingResultAsync(): Promise<(ImagePickerResult | Im
|
|
|
68
68
|
/**
|
|
69
69
|
* Display the system UI for taking a photo with the camera. Requires `Permissions.CAMERA`.
|
|
70
70
|
* On Android and iOS 10 `Permissions.CAMERA_ROLL` is also required. On mobile web, this must be
|
|
71
|
-
* called immediately in a user interaction like a button press, otherwise the browser will
|
|
71
|
+
* called immediately in a user interaction like a button press, otherwise the browser will block
|
|
72
72
|
* the request without a warning.
|
|
73
73
|
* > **Note:** Make sure that you handle `MainActivity` destruction on **Android**. See [ImagePicker.getPendingResultAsync](#imagepickergetpendingresultasync).
|
|
74
74
|
* > **Notes for Web:** The system UI can only be shown after user activation (e.g. a `Button` press).
|
package/build/ImagePicker.js
CHANGED
|
@@ -118,7 +118,7 @@ export async function getPendingResultAsync() {
|
|
|
118
118
|
/**
|
|
119
119
|
* Display the system UI for taking a photo with the camera. Requires `Permissions.CAMERA`.
|
|
120
120
|
* On Android and iOS 10 `Permissions.CAMERA_ROLL` is also required. On mobile web, this must be
|
|
121
|
-
* called immediately in a user interaction like a button press, otherwise the browser will
|
|
121
|
+
* called immediately in a user interaction like a button press, otherwise the browser will block
|
|
122
122
|
* the request without a warning.
|
|
123
123
|
* > **Note:** Make sure that you handle `MainActivity` destruction on **Android**. See [ImagePicker.getPendingResultAsync](#imagepickergetpendingresultasync).
|
|
124
124
|
* > **Notes for Web:** The system UI can only be shown after user activation (e.g. a `Button` press).
|