expo-media-library 18.3.0-canary-20251015-a6a1272 → 18.3.0-canary-20251031-b135dff

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/medialibrary/next/MediaLibraryNextModule.kt +4 -26
  4. package/android/src/main/java/expo/modules/medialibrary/next/exceptions/PermissionExceptions.kt +1 -2
  5. package/android/src/main/java/expo/modules/medialibrary/next/extensions/ContextExtensions.kt +16 -0
  6. package/android/src/main/java/expo/modules/medialibrary/next/extensions/resolver/QueryOne.kt +1 -2
  7. package/android/src/main/java/expo/modules/medialibrary/next/extensions/resolver/SafeQuery.kt +20 -0
  8. package/android/src/main/java/expo/modules/medialibrary/next/objects/album/factories/AlbumModernFactory.kt +3 -0
  9. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/delegates/AssetLegacyDelegate.kt +12 -6
  10. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/delegates/AssetModernDelegate.kt +8 -7
  11. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/deleters/AssetLegacyDeleter.kt +7 -2
  12. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/factories/AssetLegacyFactory.kt +29 -14
  13. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/factories/AssetModernFactory.kt +3 -0
  14. package/android/src/main/java/expo/modules/medialibrary/next/objects/query/builder/QueryLegacyExecutor.kt +2 -1
  15. package/android/src/main/java/expo/modules/medialibrary/next/permissions/MediaStorePermissionsDelegate.kt +5 -11
  16. package/android/src/main/java/expo/modules/medialibrary/next/permissions/SystemPermissionsDelegate.kt +16 -39
  17. package/build/MediaLibrary.d.ts +4 -2
  18. package/build/MediaLibrary.d.ts.map +1 -1
  19. package/build/MediaLibrary.js.map +1 -1
  20. package/expo-module.config.json +1 -1
  21. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/{18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272-sources.jar → 18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff-sources.jar} +0 -0
  22. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff-sources.jar.md5 +1 -0
  23. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff-sources.jar.sha1 +1 -0
  24. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff-sources.jar.sha256 +1 -0
  25. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff-sources.jar.sha512 +1 -0
  26. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.aar +0 -0
  27. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.aar.md5 +1 -0
  28. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.aar.sha1 +1 -0
  29. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.aar.sha256 +1 -0
  30. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.aar.sha512 +1 -0
  31. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/{18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.module → 18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.module} +22 -22
  32. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.module.md5 +1 -0
  33. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.module.sha1 +1 -0
  34. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.module.sha256 +1 -0
  35. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.module.sha512 +1 -0
  36. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/{18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.pom → 18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.pom} +1 -1
  37. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.pom.md5 +1 -0
  38. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.pom.sha1 +1 -0
  39. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.pom.sha256 +1 -0
  40. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251031-b135dff/expo.modules.medialibrary-18.3.0-canary-20251031-b135dff.pom.sha512 +1 -0
  41. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml +4 -4
  42. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.md5 +1 -1
  43. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.sha1 +1 -1
  44. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.sha256 +1 -1
  45. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.sha512 +1 -1
  46. package/package.json +3 -3
  47. package/src/MediaLibrary.ts +4 -2
  48. package/ios/Tests/MediaLibrarySpec.swift +0 -116
  49. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272-sources.jar.md5 +0 -1
  50. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272-sources.jar.sha1 +0 -1
  51. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272-sources.jar.sha256 +0 -1
  52. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272-sources.jar.sha512 +0 -1
  53. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.aar +0 -0
  54. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.aar.md5 +0 -1
  55. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.aar.sha1 +0 -1
  56. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.aar.sha256 +0 -1
  57. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.aar.sha512 +0 -1
  58. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.module.md5 +0 -1
  59. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.module.sha1 +0 -1
  60. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.module.sha256 +0 -1
  61. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.module.sha512 +0 -1
  62. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.pom.md5 +0 -1
  63. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.pom.sha1 +0 -1
  64. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.pom.sha256 +0 -1
  65. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.3.0-canary-20251015-a6a1272/expo.modules.medialibrary-18.3.0-canary-20251015-a6a1272.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  ### 🐛 Bug fixes
13
13
 
14
+ - Fix `endCursor` description in the documentation. ([#39120](https://github.com/expo/expo/pull/39120) by [@Wenszel](https://github.com/Wenszel))
14
15
  - [next][iOS] Convert `id` to URI format ([#39920](https://github.com/expo/expo/pull/39920) by [@Wenszel](https://github.com/Wenszel))
15
16
  - [next][android] Fix `delete()` throwing security exception ([#39914](https://github.com/expo/expo/pull/39914) by [@Wenszel](https://github.com/Wenszel))
16
17
  - [next][android] Change default root directory to Pictures ([#39716](https://github.com/expo/expo/pull/39716) by [@Wenszel](https://github.com/Wenszel))
@@ -18,8 +19,10 @@
18
19
 
19
20
  ### 💡 Others
20
21
 
22
+ - [next][android] Refactor permissions ([#40076](https://github.com/expo/expo/pull/40076) by [@Wenszel](https://github.com/Wenszel))
21
23
  - [next] Add test screens ([#39951](https://github.com/expo/expo/pull/39951) by [@Wenszel](https://github.com/Wenszel))
22
24
  - [next] Add documentation ([#39754](https://github.com/expo/expo/pull/39754) by [@Wenszel](https://github.com/Wenszel))
25
+ - Remove tests related files from the published package content. ([#39551](https://github.com/expo/expo/pull/39551) by [@Simek](https://github.com/Simek))
23
26
 
24
27
  ## 18.2.0 — 2025-09-16
25
28
 
@@ -4,13 +4,13 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'host.exp.exponent'
7
- version = '18.3.0-canary-20251015-a6a1272'
7
+ version = '18.3.0-canary-20251031-b135dff'
8
8
 
9
9
  android {
10
10
  namespace "expo.modules.medialibrary"
11
11
  defaultConfig {
12
12
  versionCode 37
13
- versionName "18.3.0-canary-20251015-a6a1272"
13
+ versionName "18.3.0-canary-20251031-b135dff"
14
14
  }
15
15
  }
16
16
 
@@ -47,7 +47,7 @@ class MediaLibraryNextModule : Module() {
47
47
 
48
48
  private val albumFactory by lazy {
49
49
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
50
- AlbumModernFactory(assetFactory, assetDeleter, context)
50
+ AlbumModernFactory(assetFactory, assetDeleter, mediaStorePermissionsDelegate, context)
51
51
  } else {
52
52
  AlbumLegacyFactory(assetFactory, assetDeleter, context)
53
53
  }
@@ -55,9 +55,9 @@ class MediaLibraryNextModule : Module() {
55
55
 
56
56
  private val assetFactory by lazy {
57
57
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
58
- AssetModernFactory(assetDeleter, context)
58
+ AssetModernFactory(assetDeleter, mediaStorePermissionsDelegate, context)
59
59
  } else {
60
- AssetLegacyFactory(assetDeleter, context)
60
+ AssetLegacyFactory(assetDeleter, systemPermissionsDelegate, context)
61
61
  }
62
62
  }
63
63
 
@@ -65,7 +65,7 @@ class MediaLibraryNextModule : Module() {
65
65
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
66
66
  AssetModernDeleter(mediaStorePermissionsDelegate)
67
67
  } else {
68
- AssetLegacyDeleter(context)
68
+ AssetLegacyDeleter(systemPermissionsDelegate, context)
69
69
  }
70
70
  }
71
71
 
@@ -78,62 +78,50 @@ class MediaLibraryNextModule : Module() {
78
78
  }
79
79
 
80
80
  Property("id") { self: Asset ->
81
- systemPermissionsDelegate.requireSystemPermissions(false)
82
81
  self.contentUri
83
82
  }
84
83
 
85
84
  AsyncFunction("getCreationTime") Coroutine { self: Asset ->
86
- systemPermissionsDelegate.requireSystemPermissions(false)
87
85
  self.getCreationTime()
88
86
  }
89
87
 
90
88
  AsyncFunction("getDuration") Coroutine { self: Asset ->
91
- systemPermissionsDelegate.requireSystemPermissions(false)
92
89
  self.getDuration()
93
90
  }
94
91
 
95
92
  AsyncFunction("getExif") Coroutine { self: Asset ->
96
- systemPermissionsDelegate.requireSystemPermissions(false)
97
93
  self.getExif()
98
94
  }
99
95
 
100
96
  AsyncFunction("getLocation") Coroutine { self: Asset ->
101
- systemPermissionsDelegate.requireSystemPermissions(false)
102
97
  self.getLocation()
103
98
  }
104
99
 
105
100
  AsyncFunction("getFilename") Coroutine { self: Asset ->
106
- systemPermissionsDelegate.requireSystemPermissions(false)
107
101
  self.getFilename()
108
102
  }
109
103
 
110
104
  AsyncFunction("getHeight") Coroutine { self: Asset ->
111
- systemPermissionsDelegate.requireSystemPermissions(false)
112
105
  self.getHeight()
113
106
  }
114
107
 
115
108
  AsyncFunction("getMediaType") Coroutine { self: Asset ->
116
- systemPermissionsDelegate.requireSystemPermissions(false)
117
109
  self.getMediaType()
118
110
  }
119
111
 
120
112
  AsyncFunction("getModificationTime") Coroutine { self: Asset ->
121
- systemPermissionsDelegate.requireSystemPermissions(false)
122
113
  self.getModificationTime()
123
114
  }
124
115
 
125
116
  AsyncFunction("getUri") Coroutine { self: Asset ->
126
- systemPermissionsDelegate.requireSystemPermissions(false)
127
117
  self.getUri()
128
118
  }
129
119
 
130
120
  AsyncFunction("getWidth") Coroutine { self: Asset ->
131
- systemPermissionsDelegate.requireSystemPermissions(false)
132
121
  self.getWidth()
133
122
  }
134
123
 
135
124
  AsyncFunction("delete") Coroutine { self: Asset ->
136
- systemPermissionsDelegate.requireSystemPermissions(true)
137
125
  self.delete()
138
126
  }
139
127
  }
@@ -148,23 +136,18 @@ class MediaLibraryNextModule : Module() {
148
136
  }
149
137
 
150
138
  AsyncFunction("getTitle") Coroutine { self: Album ->
151
- systemPermissionsDelegate.requireSystemPermissions(false)
152
139
  self.getTitle()
153
140
  }
154
141
 
155
142
  AsyncFunction("getAssets") Coroutine { self: Album ->
156
- systemPermissionsDelegate.requireSystemPermissions(false)
157
143
  self.getAssets()
158
144
  }
159
145
 
160
146
  AsyncFunction("add") Coroutine { self: Album, asset: Asset ->
161
- systemPermissionsDelegate.requireSystemPermissions(true)
162
- mediaStorePermissionsDelegate.requestMediaLibraryWritePermission(listOf(asset.contentUri))
163
147
  self.add(asset)
164
148
  }
165
149
 
166
150
  AsyncFunction("delete") Coroutine { self: Album ->
167
- systemPermissionsDelegate.requireSystemPermissions(true)
168
151
  self.delete()
169
152
  }
170
153
  }
@@ -227,13 +210,11 @@ class MediaLibraryNextModule : Module() {
227
210
  }
228
211
 
229
212
  AsyncFunction("createAsset") Coroutine { filePath: Uri, album: Album? ->
230
- systemPermissionsDelegate.requireSystemPermissions(true)
231
213
  return@Coroutine assetFactory.create(filePath, album?.getRelativePath())
232
214
  }
233
215
 
234
216
  @OptIn(EitherType::class)
235
217
  AsyncFunction("createAlbum") Coroutine { name: String, assetRefs: Either<List<Asset>, List<Uri>>, move: Boolean ->
236
- systemPermissionsDelegate.requireSystemPermissions(true)
237
218
  val assetListKClass = toKClass<List<Asset>>()
238
219
  if (assetRefs.`is`(assetListKClass)) {
239
220
  val assetList = assetRefs.get(assetListKClass)
@@ -244,12 +225,10 @@ class MediaLibraryNextModule : Module() {
244
225
  }
245
226
 
246
227
  AsyncFunction("getAlbum") Coroutine { title: String ->
247
- systemPermissionsDelegate.requireSystemPermissions(false)
248
228
  albumQuery.getAlbum(title)
249
229
  }
250
230
 
251
231
  AsyncFunction("deleteAlbums") Coroutine { albums: List<Album> ->
252
- systemPermissionsDelegate.requireSystemPermissions(true)
253
232
  val contentUris = albums
254
233
  .map { it.getAssets() }
255
234
  .flatten()
@@ -258,7 +237,6 @@ class MediaLibraryNextModule : Module() {
258
237
  }
259
238
 
260
239
  AsyncFunction("deleteAssets") Coroutine { assets: List<Asset> ->
261
- systemPermissionsDelegate.requireSystemPermissions(true)
262
240
  assetDeleter.delete(assets.map { it.contentUri })
263
241
  }
264
242
 
@@ -2,5 +2,4 @@ package expo.modules.medialibrary.next.exceptions
2
2
 
3
3
  import expo.modules.kotlin.exception.CodedException
4
4
 
5
- class PermissionException(message: String) :
6
- CodedException(message)
5
+ class PermissionException(message: String, cause: Throwable? = null) : CodedException(message, cause)
@@ -0,0 +1,16 @@
1
+ package expo.modules.medialibrary.next.extensions
2
+
3
+ import android.content.Context
4
+ import android.media.MediaScannerConnection
5
+ import android.net.Uri
6
+ import kotlin.coroutines.resume
7
+ import kotlin.coroutines.suspendCoroutine
8
+
9
+ suspend fun Context.scanFile(
10
+ path: String,
11
+ mimeType: String? = null
12
+ ): Pair<String, Uri?> = suspendCoroutine { continuation ->
13
+ MediaScannerConnection.scanFile(this, arrayOf(path), arrayOf(mimeType)) { path, uri ->
14
+ continuation.resume(Pair(path, uri))
15
+ }
16
+ }
@@ -16,7 +16,7 @@ suspend fun <T> ContentResolver.queryOne(
16
16
  sortOrder: String? = null
17
17
  ): T? = withContext(Dispatchers.IO) {
18
18
  val projection = arrayOf(column)
19
- query(contentUri, projection, selection, selectionArgs, sortOrder)?.use { cursor ->
19
+ safeQuery(contentUri, projection, selection, selectionArgs, sortOrder)?.use { cursor ->
20
20
  ensureActive()
21
21
  val index = cursor.getColumnIndexOrThrow(column)
22
22
  return@withContext if (cursor.moveToFirst()) {
@@ -25,5 +25,4 @@ suspend fun <T> ContentResolver.queryOne(
25
25
  null
26
26
  }
27
27
  }
28
- return@withContext null
29
28
  }
@@ -0,0 +1,20 @@
1
+ package expo.modules.medialibrary.next.extensions.resolver
2
+
3
+ import android.content.ContentResolver
4
+ import android.database.Cursor
5
+ import android.net.Uri
6
+ import expo.modules.medialibrary.next.exceptions.PermissionException
7
+
8
+ fun ContentResolver.safeQuery(
9
+ uri: Uri,
10
+ projection: Array<String>,
11
+ selection: String? = null,
12
+ selectionArgs: Array<String>? = null,
13
+ sortOrder: String? = null
14
+ ): Cursor? {
15
+ return try {
16
+ query(uri, projection, selection, selectionArgs, sortOrder)
17
+ } catch (e: SecurityException) {
18
+ throw PermissionException("Missing required system permissions", e)
19
+ }
20
+ }
@@ -14,6 +14,7 @@ import expo.modules.medialibrary.next.objects.asset.Asset
14
14
  import expo.modules.medialibrary.next.objects.asset.deleters.AssetDeleter
15
15
  import expo.modules.medialibrary.next.objects.wrappers.MimeType
16
16
  import expo.modules.medialibrary.next.objects.asset.factories.AssetFactory
17
+ import expo.modules.medialibrary.next.permissions.MediaStorePermissionsDelegate
17
18
  import java.io.IOException
18
19
  import java.lang.ref.WeakReference
19
20
 
@@ -21,6 +22,7 @@ import java.lang.ref.WeakReference
21
22
  class AlbumModernFactory(
22
23
  private val assetFactory: AssetFactory,
23
24
  private val assetDeleter: AssetDeleter,
25
+ private val mediaStorePermissionsDelegate: MediaStorePermissionsDelegate,
24
26
  context: Context
25
27
  ) : AlbumFactory {
26
28
  private val contextRef = WeakReference(context)
@@ -61,6 +63,7 @@ class AlbumModernFactory(
61
63
 
62
64
  private suspend fun processAssetsLocation(assets: List<Asset>, relativePath: RelativePath, deleteOriginalAssets: Boolean) {
63
65
  if (deleteOriginalAssets) {
66
+ mediaStorePermissionsDelegate.requestMediaLibraryWritePermission(assets.map { it.contentUri })
64
67
  assets.map { it.move(relativePath) }
65
68
  } else {
66
69
  assets.map { it.copy(relativePath) }
@@ -8,7 +8,6 @@ import android.os.Bundle
8
8
  import androidx.annotation.DeprecatedSinceApi
9
9
  import androidx.core.net.toUri
10
10
  import androidx.exifinterface.media.ExifInterface
11
- import expo.modules.medialibrary.MediaLibraryUtils
12
11
  import expo.modules.medialibrary.next.exceptions.AssetCouldNotBeCreated
13
12
  import expo.modules.medialibrary.next.exceptions.AssetPropertyNotFoundException
14
13
  import expo.modules.medialibrary.next.exceptions.ContentResolverNotObtainedException
@@ -23,12 +22,14 @@ import expo.modules.medialibrary.next.extensions.resolver.queryAssetWidth
23
22
  import expo.modules.medialibrary.next.extensions.resolver.queryAssetCreationTime
24
23
  import expo.modules.medialibrary.next.extensions.safeCopy
25
24
  import expo.modules.medialibrary.next.extensions.safeMove
25
+ import expo.modules.medialibrary.next.extensions.scanFile
26
26
  import expo.modules.medialibrary.next.objects.wrappers.RelativePath
27
27
  import expo.modules.medialibrary.next.objects.asset.Asset
28
28
  import expo.modules.medialibrary.next.objects.asset.EXIF_TAGS
29
29
  import expo.modules.medialibrary.next.objects.asset.deleters.AssetDeleter
30
30
  import expo.modules.medialibrary.next.objects.wrappers.MediaType
31
31
  import expo.modules.medialibrary.next.objects.wrappers.MimeType
32
+ import expo.modules.medialibrary.next.permissions.SystemPermissionsDelegate
32
33
  import expo.modules.medialibrary.next.records.Location
33
34
  import kotlinx.coroutines.Dispatchers
34
35
  import kotlinx.coroutines.ensureActive
@@ -44,6 +45,7 @@ import kotlin.time.toDuration
44
45
  class AssetLegacyDelegate(
45
46
  contentUri: Uri,
46
47
  val assetDeleter: AssetDeleter,
48
+ val systemPermissionsDelegate: SystemPermissionsDelegate,
47
49
  context: Context
48
50
  ) : AssetDelegate {
49
51
  private val contextRef = WeakReference(context)
@@ -121,14 +123,17 @@ class AssetLegacyDelegate(
121
123
  ?: MimeType.from(getUri())
122
124
  }
123
125
 
124
- override suspend fun getLocation(): Location? =
125
- contentResolver.openInputStream(contentUri)?.use { stream ->
126
+ override suspend fun getLocation(): Location? {
127
+ systemPermissionsDelegate.requireReadPermissions()
128
+ return contentResolver.openInputStream(contentUri)?.use { stream ->
126
129
  ExifInterface(stream)
127
130
  .latLong
128
131
  ?.let { (lat, long) -> Location(lat, long) }
129
132
  }
133
+ }
130
134
 
131
135
  override suspend fun getExif(): Bundle = withContext(Dispatchers.IO) {
136
+ systemPermissionsDelegate.requireReadPermissions()
132
137
  if (getMediaType() != MediaType.IMAGE) {
133
138
  return@withContext Bundle()
134
139
  }
@@ -163,11 +168,12 @@ class AssetLegacyDelegate(
163
168
  }
164
169
 
165
170
  override suspend fun move(relativePath: RelativePath) = withContext(Dispatchers.IO) {
171
+ systemPermissionsDelegate.requireWritePermissions()
166
172
  val path = contentResolver.queryAssetPath(contentUri)
167
173
  ?: throw AssetPropertyNotFoundException("Asset path")
168
174
  val newFile = File(path).safeMove(File(relativePath.toFilePath()))
169
175
  contentResolver.deleteBy(path)
170
- val (_, uri) = MediaLibraryUtils.scanFile(contextRef.getOrThrow(), arrayOf(newFile.path), null)
176
+ val (_, uri) = contextRef.getOrThrow().scanFile(newFile.path, null)
171
177
  this@AssetLegacyDelegate.contentUri = uri
172
178
  ?: throw AssetCouldNotBeCreated("Could not create a new asset while moving the old one")
173
179
  }
@@ -176,11 +182,11 @@ class AssetLegacyDelegate(
176
182
  val path = contentResolver.queryAssetPath(contentUri)
177
183
  ?: throw AssetPropertyNotFoundException("Asset path")
178
184
  val newFile = File(path).safeCopy(File(relativePath.toFilePath()))
179
- val (_, uri) = MediaLibraryUtils.scanFile(contextRef.getOrThrow(), arrayOf(newFile.path), null)
185
+ val (_, uri) = contextRef.getOrThrow().scanFile(newFile.path, null)
180
186
  if (uri == null) {
181
187
  throw AssetCouldNotBeCreated("Could not create a new asset while copying the old one")
182
188
  }
183
- val newAssetDelegate = AssetLegacyDelegate(contentUri, assetDeleter, contextRef.getOrThrow())
189
+ val newAssetDelegate = AssetLegacyDelegate(contentUri, assetDeleter, systemPermissionsDelegate, contextRef.getOrThrow())
184
190
  return@withContext Asset(newAssetDelegate)
185
191
  }
186
192
  }
@@ -28,6 +28,7 @@ import expo.modules.medialibrary.next.objects.asset.EXIF_TAGS
28
28
  import expo.modules.medialibrary.next.objects.asset.deleters.AssetDeleter
29
29
  import expo.modules.medialibrary.next.objects.wrappers.MediaType
30
30
  import expo.modules.medialibrary.next.objects.wrappers.MimeType
31
+ import expo.modules.medialibrary.next.permissions.MediaStorePermissionsDelegate
31
32
  import expo.modules.medialibrary.next.records.Location
32
33
  import kotlinx.coroutines.Dispatchers
33
34
  import kotlinx.coroutines.ensureActive
@@ -44,6 +45,7 @@ import kotlin.time.toDuration
44
45
  class AssetModernDelegate(
45
46
  override val contentUri: Uri,
46
47
  val assetDeleter: AssetDeleter,
48
+ val mediaStorePermissionsDelegate: MediaStorePermissionsDelegate,
47
49
  context: Context
48
50
  ) : AssetDelegate {
49
51
  private val contextRef = WeakReference(context)
@@ -75,21 +77,19 @@ class AssetModernDelegate(
75
77
 
76
78
  override suspend fun getHeight(): Int {
77
79
  val height = contentResolver.queryAssetHeight(contentUri)
78
- ?: throw AssetPropertyNotFoundException("Height")
79
80
  // If height is not saved to the database
80
- if (getMediaType() == MediaType.IMAGE && height <= 0) {
81
+ if (getMediaType() == MediaType.IMAGE && height != null && height <= 0) {
81
82
  return downloadBitmapAndGet { it.outHeight }
82
83
  }
83
- return height
84
+ return height ?: throw AssetPropertyNotFoundException("Height")
84
85
  }
85
86
 
86
87
  override suspend fun getWidth(): Int {
87
88
  val width = contentResolver.queryAssetWidth(contentUri)
88
- ?: throw AssetPropertyNotFoundException("Width")
89
- if (getMediaType() == MediaType.IMAGE && width <= 0) {
89
+ if (getMediaType() == MediaType.IMAGE && width != null && width <= 0) {
90
90
  return downloadBitmapAndGet { it.outWidth }
91
91
  }
92
- return width
92
+ return width ?: throw AssetPropertyNotFoundException("Width")
93
93
  }
94
94
 
95
95
  private suspend fun downloadBitmapAndGet(extract: (BitmapFactory.Options) -> Int): Int {
@@ -155,6 +155,7 @@ class AssetModernDelegate(
155
155
  }
156
156
 
157
157
  override suspend fun move(relativePath: RelativePath) {
158
+ mediaStorePermissionsDelegate.requestMediaLibraryWritePermission(listOf(contentUri))
158
159
  contentResolver.updateRelativePath(contentUri, relativePath)
159
160
  }
160
161
 
@@ -162,7 +163,7 @@ class AssetModernDelegate(
162
163
  val newAssetUri = contentResolver.insertPendingAsset(getFilename(), getMimeType(), relativePath)
163
164
  contentResolver.copyUriContent(contentUri, newAssetUri)
164
165
  contentResolver.publishPendingAsset(newAssetUri)
165
- val newAssetDelegate = AssetModernDelegate(newAssetUri, assetDeleter, contextRef.getOrThrow())
166
+ val newAssetDelegate = AssetModernDelegate(newAssetUri, assetDeleter, mediaStorePermissionsDelegate, contextRef.getOrThrow())
166
167
  return@withContext Asset(newAssetDelegate)
167
168
  }
168
169
  }
@@ -9,6 +9,7 @@ import expo.modules.medialibrary.next.exceptions.AssetPropertyNotFoundException
9
9
  import expo.modules.medialibrary.next.exceptions.ContentResolverNotObtainedException
10
10
  import expo.modules.medialibrary.next.extensions.getOrThrow
11
11
  import expo.modules.medialibrary.next.extensions.resolver.queryAssetPath
12
+ import expo.modules.medialibrary.next.permissions.SystemPermissionsDelegate
12
13
  import kotlinx.coroutines.Dispatchers
13
14
  import kotlinx.coroutines.async
14
15
  import kotlinx.coroutines.awaitAll
@@ -16,8 +17,11 @@ import kotlinx.coroutines.withContext
16
17
  import java.io.File
17
18
  import java.lang.ref.WeakReference
18
19
 
19
- @DeprecatedSinceApi(Build.VERSION_CODES.Q)
20
- class AssetLegacyDeleter(context: Context) : AssetDeleter {
20
+ @DeprecatedSinceApi(Build.VERSION_CODES.R)
21
+ class AssetLegacyDeleter(
22
+ val systemPermissionsDelegate: SystemPermissionsDelegate,
23
+ context: Context
24
+ ) : AssetDeleter {
21
25
  private val contextRef = WeakReference(context)
22
26
 
23
27
  private val contentResolver
@@ -26,6 +30,7 @@ class AssetLegacyDeleter(context: Context) : AssetDeleter {
26
30
  .contentResolver ?: throw ContentResolverNotObtainedException()
27
31
 
28
32
  override suspend fun delete(contentUri: Uri): Unit = withContext(Dispatchers.IO) {
33
+ systemPermissionsDelegate.requireWritePermissions()
29
34
  val path = contentResolver.queryAssetPath(contentUri)
30
35
  ?: throw AssetPropertyNotFoundException("Uri")
31
36
  if (!File(path).delete()) {
@@ -1,11 +1,11 @@
1
1
  package expo.modules.medialibrary.next.objects.asset.factories
2
2
 
3
3
  import android.content.Context
4
+ import android.media.MediaScannerConnection
4
5
  import android.net.Uri
5
6
  import android.os.Build
6
7
  import androidx.annotation.DeprecatedSinceApi
7
8
  import androidx.core.net.toFile
8
- import expo.modules.medialibrary.MediaLibraryUtils
9
9
  import expo.modules.medialibrary.next.exceptions.AssetCouldNotBeCreated
10
10
  import expo.modules.medialibrary.next.exceptions.ContentResolverNotObtainedException
11
11
  import expo.modules.medialibrary.next.extensions.getOrThrow
@@ -16,15 +16,19 @@ import expo.modules.medialibrary.next.objects.asset.delegates.AssetDelegate
16
16
  import expo.modules.medialibrary.next.objects.asset.delegates.AssetLegacyDelegate
17
17
  import expo.modules.medialibrary.next.objects.asset.deleters.AssetDeleter
18
18
  import expo.modules.medialibrary.next.objects.wrappers.MimeType
19
+ import expo.modules.medialibrary.next.permissions.SystemPermissionsDelegate
19
20
  import kotlinx.coroutines.Dispatchers
20
21
  import kotlinx.coroutines.ensureActive
21
22
  import kotlinx.coroutines.withContext
22
23
  import java.io.File
23
24
  import java.lang.ref.WeakReference
25
+ import kotlin.coroutines.resume
26
+ import kotlin.coroutines.suspendCoroutine
24
27
 
25
28
  @DeprecatedSinceApi(Build.VERSION_CODES.R)
26
29
  class AssetLegacyFactory(
27
30
  val assetDeleter: AssetDeleter,
31
+ val systemPermissionsDelegate: SystemPermissionsDelegate,
28
32
  context: Context
29
33
  ) : AssetFactory {
30
34
  private val contextRef = WeakReference(context)
@@ -35,7 +39,7 @@ class AssetLegacyFactory(
35
39
  .contentResolver ?: throw ContentResolverNotObtainedException()
36
40
 
37
41
  private fun createAssetDelegate(contentUri: Uri): AssetDelegate {
38
- return AssetLegacyDelegate(contentUri, assetDeleter, contextRef.getOrThrow())
42
+ return AssetLegacyDelegate(contentUri, assetDeleter, systemPermissionsDelegate, contextRef.getOrThrow())
39
43
  }
40
44
 
41
45
  override fun create(contentUri: Uri): Asset {
@@ -44,24 +48,35 @@ class AssetLegacyFactory(
44
48
  }
45
49
 
46
50
  override suspend fun create(filePath: Uri, relativePath: RelativePath?): Asset = withContext(Dispatchers.IO) {
47
- val mimeType = contentResolver.getType(filePath)?.let { MimeType(it) }
48
- ?: MimeType.from(filePath)
51
+ systemPermissionsDelegate.requireWritePermissions()
52
+ val destinationDirectory = createDestinationDirectory(filePath, relativePath)
53
+ val destinationFile = filePath
54
+ .toFile()
55
+ .safeCopy(destinationDirectory)
56
+ val (_, uri) = scanFile(contextRef.getOrThrow(), arrayOf(destinationFile.toString()), null)
57
+ ensureActive()
58
+ if (uri == null) {
59
+ throw AssetCouldNotBeCreated("Failed to create asset: could not add asset to MediaStore")
60
+ }
61
+ return@withContext create(uri)
62
+ }
63
+
64
+ private fun createDestinationDirectory(filePath: Uri, relativePath: RelativePath?): File {
49
65
  val destinationDirectory = if (relativePath != null) {
50
66
  File(relativePath.toFilePath())
51
67
  } else {
68
+ val mimeType = contentResolver.getType(filePath)?.let { MimeType(it) }
69
+ ?: MimeType.from(filePath)
52
70
  mimeType.externalStorageAssetDirectory()
53
71
  }
54
72
  destinationDirectory.mkdirs()
73
+ return destinationDirectory
74
+ }
55
75
 
56
- val destFile = filePath
57
- .toFile()
58
- .safeCopy(destinationDirectory)
59
- val (_, uri) = MediaLibraryUtils.scanFile(contextRef.getOrThrow(), arrayOf(destFile.toString()), null)
60
- coroutineContext.ensureActive()
61
-
62
- if (uri == null) {
63
- throw AssetCouldNotBeCreated("Failed to create asset: could not add asset to MediaStore")
76
+ private suspend fun scanFile(context: Context, paths: Array<String>, mimeTypes: Array<String>?) =
77
+ suspendCoroutine { complete ->
78
+ MediaScannerConnection.scanFile(context, paths, mimeTypes) { path: String, uri: Uri? ->
79
+ complete.resume(Pair(path, uri))
80
+ }
64
81
  }
65
- return@withContext create(uri)
66
- }
67
82
  }
@@ -15,6 +15,7 @@ import expo.modules.medialibrary.next.objects.asset.delegates.AssetDelegate
15
15
  import expo.modules.medialibrary.next.objects.asset.delegates.AssetModernDelegate
16
16
  import expo.modules.medialibrary.next.objects.asset.deleters.AssetDeleter
17
17
  import expo.modules.medialibrary.next.objects.wrappers.MimeType
18
+ import expo.modules.medialibrary.next.permissions.MediaStorePermissionsDelegate
18
19
  import kotlinx.coroutines.Dispatchers
19
20
  import kotlinx.coroutines.ensureActive
20
21
  import kotlinx.coroutines.withContext
@@ -23,6 +24,7 @@ import java.lang.ref.WeakReference
23
24
  @RequiresApi(Build.VERSION_CODES.R)
24
25
  class AssetModernFactory(
25
26
  val assetDeleter: AssetDeleter,
27
+ val mediaStorePermissionsDelegate: MediaStorePermissionsDelegate,
26
28
  context: Context
27
29
  ) : AssetFactory {
28
30
  private val contextRef = WeakReference(context)
@@ -36,6 +38,7 @@ class AssetModernFactory(
36
38
  return AssetModernDelegate(
37
39
  contentUri,
38
40
  assetDeleter,
41
+ mediaStorePermissionsDelegate,
39
42
  contextRef.getOrThrow()
40
43
  )
41
44
  }
@@ -7,6 +7,7 @@ import android.provider.MediaStore
7
7
  import androidx.annotation.DeprecatedSinceApi
8
8
  import expo.modules.medialibrary.next.exceptions.QueryCouldNotBeExecuted
9
9
  import expo.modules.medialibrary.next.extensions.resolver.EXTERNAL_CONTENT_URI
10
+ import expo.modules.medialibrary.next.extensions.resolver.safeQuery
10
11
  import expo.modules.medialibrary.next.records.SortDescriptor
11
12
  import kotlinx.coroutines.Dispatchers
12
13
  import kotlinx.coroutines.withContext
@@ -26,7 +27,7 @@ class QueryLegacyExecutor(
26
27
  val selection = buildSelection()
27
28
  val sortOrder = buildSortOrder()
28
29
  val selectionArgs = args.toTypedArray()
29
- return@withContext contentResolver.query(EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, sortOrder)
30
+ return@withContext contentResolver.safeQuery(EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, sortOrder)
30
31
  ?: throw QueryCouldNotBeExecuted("Cursor is null")
31
32
  }
32
33
 
@@ -1,10 +1,7 @@
1
1
  package expo.modules.medialibrary.next.permissions
2
2
 
3
3
  import android.content.Context
4
- import android.content.Intent
5
- import android.content.pm.PackageManager
6
4
  import android.net.Uri
7
- import android.os.Binder
8
5
  import android.os.Build
9
6
  import androidx.annotation.RequiresApi
10
7
  import expo.modules.kotlin.AppContext
@@ -56,12 +53,9 @@ class MediaStorePermissionsDelegate(val appContext: AppContext) {
56
53
  writeLauncher = registerForActivityResult(WriteContract(appContextProvider))
57
54
  }
58
55
 
59
- private fun hasWritePermissionForUri(uri: Uri): Boolean {
60
- return context.checkUriPermission(
61
- uri,
62
- Binder.getCallingPid(),
63
- Binder.getCallingUid(),
64
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION
65
- ) == PackageManager.PERMISSION_GRANTED
66
- }
56
+ private fun hasWritePermissionForUri(uri: Uri): Boolean =
57
+ runCatching {
58
+ context.contentResolver.openOutputStream(uri, "rw")?.close()
59
+ return true
60
+ }.getOrDefault(false)
67
61
  }