expo-media-library 18.0.5 → 18.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt +10 -6
  4. package/android/src/main/java/expo/modules/medialibrary/next/MediaLibraryNextModule.kt +187 -0
  5. package/android/src/main/java/expo/modules/medialibrary/next/exceptions/AlbumExceptions.kt +15 -0
  6. package/android/src/main/java/expo/modules/medialibrary/next/exceptions/AssetExceptions.kt +21 -0
  7. package/android/src/main/java/expo/modules/medialibrary/next/exceptions/ContentResolverExceptions.kt +6 -0
  8. package/android/src/main/java/expo/modules/medialibrary/next/exceptions/PermissionExceptions.kt +6 -0
  9. package/android/src/main/java/expo/modules/medialibrary/next/extensions/CursorExtensions.kt +28 -0
  10. package/android/src/main/java/expo/modules/medialibrary/next/extensions/FileExtensions.kt +41 -0
  11. package/android/src/main/java/expo/modules/medialibrary/next/extensions/WeakReferenceExtensions.kt +9 -0
  12. package/android/src/main/java/expo/modules/medialibrary/next/extensions/resolver/AlbumExtensions.kt +68 -0
  13. package/android/src/main/java/expo/modules/medialibrary/next/extensions/resolver/AssetExtensions.kt +89 -0
  14. package/android/src/main/java/expo/modules/medialibrary/next/extensions/resolver/CursorExtensions.kt +18 -0
  15. package/android/src/main/java/expo/modules/medialibrary/next/extensions/resolver/QueryOne.kt +29 -0
  16. package/android/src/main/java/expo/modules/medialibrary/next/extensions/resolver/TransferExtensions.kt +15 -0
  17. package/android/src/main/java/expo/modules/medialibrary/next/objects/album/Album.kt +71 -0
  18. package/android/src/main/java/expo/modules/medialibrary/next/objects/album/factories/AlbumFactory.kt +10 -0
  19. package/android/src/main/java/expo/modules/medialibrary/next/objects/album/factories/AlbumLegacyFactory.kt +76 -0
  20. package/android/src/main/java/expo/modules/medialibrary/next/objects/album/factories/AlbumModernFactory.kt +60 -0
  21. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/Asset.kt +68 -0
  22. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/delegates/AssetDelegate.kt +22 -0
  23. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/delegates/AssetLegacyDelegate.kt +146 -0
  24. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/delegates/AssetModernDelegate.kt +121 -0
  25. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/factories/AssetFactory.kt +9 -0
  26. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/factories/AssetLegacyFactory.kt +52 -0
  27. package/android/src/main/java/expo/modules/medialibrary/next/objects/asset/factories/AssetModernFactory.kt +42 -0
  28. package/android/src/main/java/expo/modules/medialibrary/next/objects/wrappers/MimeType.kt +54 -0
  29. package/android/src/main/java/expo/modules/medialibrary/next/objects/wrappers/RelativePath.kt +31 -0
  30. package/android/src/main/java/expo/modules/medialibrary/next/permissions/MediaLibraryPermissionPromiseWrapper.kt +78 -0
  31. package/android/src/main/java/expo/modules/medialibrary/next/permissions/MediaStorePermissionsDelegate.kt +61 -0
  32. package/android/src/main/java/expo/modules/medialibrary/next/permissions/SystemPermissionsDelegate.kt +182 -0
  33. package/android/src/main/java/expo/modules/medialibrary/next/permissions/contracts/DeleteContract.kt +38 -0
  34. package/android/src/main/java/expo/modules/medialibrary/next/permissions/contracts/WriteContract.kt +38 -0
  35. package/android/src/main/java/expo/modules/medialibrary/next/permissions/enums/AccessPrivileges.kt +7 -0
  36. package/android/src/main/java/expo/modules/medialibrary/next/permissions/enums/GranularPermission.kt +18 -0
  37. package/build/next/ExpoMediaLibraryNext.d.ts +9 -0
  38. package/build/next/ExpoMediaLibraryNext.d.ts.map +1 -0
  39. package/build/next/ExpoMediaLibraryNext.js +3 -0
  40. package/build/next/ExpoMediaLibraryNext.js.map +1 -0
  41. package/build/next/MediaLibraryNext.types.d.ts +3 -0
  42. package/build/next/MediaLibraryNext.types.d.ts.map +1 -0
  43. package/build/next/MediaLibraryNext.types.js +3 -0
  44. package/build/next/MediaLibraryNext.types.js.map +1 -0
  45. package/build/next/index.d.ts +14 -0
  46. package/build/next/index.d.ts.map +1 -0
  47. package/build/next/index.js +40 -0
  48. package/build/next/index.js.map +1 -0
  49. package/build/next/types/Album.d.ts +91 -0
  50. package/build/next/types/Album.d.ts.map +1 -0
  51. package/build/next/types/Album.js +2 -0
  52. package/build/next/types/Album.js.map +1 -0
  53. package/build/next/types/Asset.d.ts +91 -0
  54. package/build/next/types/Asset.d.ts.map +1 -0
  55. package/build/next/types/Asset.js +2 -0
  56. package/build/next/types/Asset.js.map +1 -0
  57. package/expo-module.config.json +6 -3
  58. package/ios/MediaLibraryModule.swift +22 -18
  59. package/ios/next/MediaLibraryNextModule.swift +180 -0
  60. package/ios/next/exceptions/Exceptions.swift +55 -0
  61. package/ios/next/extensions/PHAssetExtensions.swift +16 -0
  62. package/ios/next/extensions/PHImageManagerExtensions.swift +15 -0
  63. package/ios/next/extensions/PHPhotoLibraryExtensions.swift +34 -0
  64. package/ios/next/objects/album/Album.swift +72 -0
  65. package/ios/next/objects/asset/Asset.swift +122 -0
  66. package/ios/next/repository/AssetCollectionRepository.swift +80 -0
  67. package/ios/next/repository/AssetRepository.swift +111 -0
  68. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0-sources.jar +0 -0
  69. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0-sources.jar.md5 +1 -0
  70. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0-sources.jar.sha1 +1 -0
  71. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0-sources.jar.sha256 +1 -0
  72. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0-sources.jar.sha512 +1 -0
  73. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.aar +0 -0
  74. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.aar.md5 +1 -0
  75. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.aar.sha1 +1 -0
  76. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.aar.sha256 +1 -0
  77. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.aar.sha512 +1 -0
  78. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/{18.0.5/expo.modules.medialibrary-18.0.5.module → 18.1.0/expo.modules.medialibrary-18.1.0.module} +22 -22
  79. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.module.md5 +1 -0
  80. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.module.sha1 +1 -0
  81. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.module.sha256 +1 -0
  82. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.module.sha512 +1 -0
  83. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/{18.0.5/expo.modules.medialibrary-18.0.5.pom → 18.1.0/expo.modules.medialibrary-18.1.0.pom} +1 -1
  84. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.pom.md5 +1 -0
  85. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.pom.sha1 +1 -0
  86. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.pom.sha256 +1 -0
  87. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.1.0/expo.modules.medialibrary-18.1.0.pom.sha512 +1 -0
  88. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml +4 -4
  89. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.md5 +1 -1
  90. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.sha1 +1 -1
  91. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.sha256 +1 -1
  92. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.sha512 +1 -1
  93. package/next.ts +1 -0
  94. package/package.json +3 -3
  95. package/src/next/ExpoMediaLibraryNext.ts +10 -0
  96. package/src/next/MediaLibraryNext.types.ts +2 -0
  97. package/src/next/index.ts +51 -0
  98. package/src/next/types/Album.ts +102 -0
  99. package/src/next/types/Asset.ts +102 -0
  100. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5-sources.jar +0 -0
  101. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5-sources.jar.md5 +0 -1
  102. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5-sources.jar.sha1 +0 -1
  103. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5-sources.jar.sha256 +0 -1
  104. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5-sources.jar.sha512 +0 -1
  105. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.aar +0 -0
  106. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.aar.md5 +0 -1
  107. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.aar.sha1 +0 -1
  108. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.aar.sha256 +0 -1
  109. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.aar.sha512 +0 -1
  110. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.module.md5 +0 -1
  111. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.module.sha1 +0 -1
  112. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.module.sha256 +0 -1
  113. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.module.sha512 +0 -1
  114. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.pom.md5 +0 -1
  115. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.pom.sha1 +0 -1
  116. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.pom.sha256 +0 -1
  117. package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.5/expo.modules.medialibrary-18.0.5.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,18 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 18.1.0 — 2025-09-08
14
+
15
+ ### 🎉 New features
16
+
17
+ - Add MediaLibrary@Next. ([#38835](https://github.com/expo/expo/pull/38835) by [@Wenszel](https://github.com/wenszel))
18
+
19
+ ## 18.0.6 — 2025-09-02
20
+
21
+ ### 💡 Others
22
+
23
+ - Change Constants to Constant/Property. ([#38926](https://github.com/expo/expo/pull/38926) by [@jakex7](https://github.com/jakex7))
24
+
13
25
  ## 18.0.5 — 2025-08-31
14
26
 
15
27
  _This version does not introduce any user-facing changes._
@@ -4,13 +4,13 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'host.exp.exponent'
7
- version = '18.0.5'
7
+ version = '18.1.0'
8
8
 
9
9
  android {
10
10
  namespace "expo.modules.medialibrary"
11
11
  defaultConfig {
12
12
  versionCode 37
13
- versionName "18.0.5"
13
+ versionName "18.1.0"
14
14
  }
15
15
  }
16
16
 
@@ -69,12 +69,16 @@ class MediaLibraryModule : Module() {
69
69
  override fun definition() = ModuleDefinition {
70
70
  Name("ExpoMediaLibrary")
71
71
 
72
- Constants {
73
- return@Constants mapOf(
74
- "MediaType" to MediaType.getConstants(),
75
- "SortBy" to SortBy.getConstants(),
76
- "CHANGE_LISTENER_NAME" to LIBRARY_DID_CHANGE_EVENT
77
- )
72
+ Constant("MediaType") {
73
+ MediaType.getConstants()
74
+ }
75
+
76
+ Constant("SortBy") {
77
+ SortBy.getConstants()
78
+ }
79
+
80
+ Constant("CHANGE_LISTENER_NAME") {
81
+ LIBRARY_DID_CHANGE_EVENT
78
82
  }
79
83
 
80
84
  Events(LIBRARY_DID_CHANGE_EVENT)
@@ -0,0 +1,187 @@
1
+ package expo.modules.medialibrary.next
2
+
3
+ import android.net.Uri
4
+ import android.os.Build
5
+ import expo.modules.kotlin.Promise
6
+ import expo.modules.kotlin.apifeatures.EitherType
7
+ import expo.modules.kotlin.exception.Exceptions
8
+ import expo.modules.kotlin.functions.Coroutine
9
+ import expo.modules.kotlin.modules.Module
10
+ import expo.modules.kotlin.modules.ModuleDefinition
11
+ import expo.modules.kotlin.types.Either
12
+ import expo.modules.kotlin.types.toKClass
13
+ import expo.modules.medialibrary.next.objects.album.Album
14
+ import expo.modules.medialibrary.next.objects.asset.Asset
15
+ import expo.modules.medialibrary.next.objects.album.factories.AlbumModernFactory
16
+ import expo.modules.medialibrary.next.objects.album.factories.AlbumLegacyFactory
17
+ import expo.modules.medialibrary.next.objects.asset.factories.AssetModernFactory
18
+ import expo.modules.medialibrary.next.objects.asset.factories.AssetLegacyFactory
19
+ import expo.modules.medialibrary.next.permissions.MediaStorePermissionsDelegate
20
+ import expo.modules.medialibrary.next.permissions.SystemPermissionsDelegate
21
+ import expo.modules.medialibrary.next.permissions.enums.GranularPermission
22
+
23
+ class MediaLibraryNextModule : Module() {
24
+ private val context
25
+ get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
26
+
27
+ private val systemPermissionsDelegate by lazy {
28
+ SystemPermissionsDelegate(appContext)
29
+ }
30
+
31
+ private val mediaStorePermissionsDelegate by lazy {
32
+ MediaStorePermissionsDelegate(appContext)
33
+ }
34
+
35
+ private val albumFactory by lazy {
36
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
37
+ AlbumModernFactory(assetFactory, context)
38
+ } else {
39
+ AlbumLegacyFactory(assetFactory, context)
40
+ }
41
+ }
42
+
43
+ private val assetFactory by lazy {
44
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
45
+ AssetModernFactory(context)
46
+ } else {
47
+ AssetLegacyFactory(context)
48
+ }
49
+ }
50
+
51
+ override fun definition() = ModuleDefinition {
52
+ Name("ExpoMediaLibraryNext")
53
+
54
+ Class(Asset::class) {
55
+ Constructor { contentUri: Uri ->
56
+ Asset(contentUri, context)
57
+ }
58
+
59
+ Property("id") { self: Asset ->
60
+ systemPermissionsDelegate.requireSystemPermissions(false)
61
+ self.contentUri
62
+ }
63
+
64
+ AsyncFunction("getCreationTime") Coroutine { self: Asset ->
65
+ systemPermissionsDelegate.requireSystemPermissions(false)
66
+ self.getCreationTime()
67
+ }
68
+
69
+ AsyncFunction("getDuration") Coroutine { self: Asset ->
70
+ systemPermissionsDelegate.requireSystemPermissions(false)
71
+ self.getDuration()
72
+ }
73
+
74
+ AsyncFunction("getFilename") Coroutine { self: Asset ->
75
+ systemPermissionsDelegate.requireSystemPermissions(false)
76
+ self.getFilename()
77
+ }
78
+
79
+ AsyncFunction("getHeight") Coroutine { self: Asset ->
80
+ systemPermissionsDelegate.requireSystemPermissions(false)
81
+ self.getHeight()
82
+ }
83
+
84
+ AsyncFunction("getMediaType") Coroutine { self: Asset ->
85
+ systemPermissionsDelegate.requireSystemPermissions(false)
86
+ self.getMediaType()
87
+ }
88
+
89
+ AsyncFunction("getModificationTime") Coroutine { self: Asset ->
90
+ systemPermissionsDelegate.requireSystemPermissions(false)
91
+ self.getModificationTime()
92
+ }
93
+
94
+ AsyncFunction("getUri") Coroutine { self: Asset ->
95
+ systemPermissionsDelegate.requireSystemPermissions(false)
96
+ self.getUri()
97
+ }
98
+
99
+ AsyncFunction("getWidth") Coroutine { self: Asset ->
100
+ systemPermissionsDelegate.requireSystemPermissions(false)
101
+ self.getWidth()
102
+ }
103
+
104
+ AsyncFunction("delete") Coroutine { self: Asset ->
105
+ systemPermissionsDelegate.requireSystemPermissions(true)
106
+ mediaStorePermissionsDelegate.requestMediaLibraryActionPermission(listOf(self.contentUri), needsDeletePermission = true)
107
+ self.delete()
108
+ }
109
+ }
110
+
111
+ Class(Album::class) {
112
+ Constructor { id: String ->
113
+ Album(id, context)
114
+ }
115
+
116
+ Property("id") { self: Album ->
117
+ self.id
118
+ }
119
+
120
+ AsyncFunction("getTitle") Coroutine { self: Album ->
121
+ systemPermissionsDelegate.requireSystemPermissions(false)
122
+ self.getTitle()
123
+ }
124
+
125
+ AsyncFunction("getAssets") Coroutine { self: Album ->
126
+ systemPermissionsDelegate.requireSystemPermissions(false)
127
+ self.getAssets()
128
+ }
129
+
130
+ AsyncFunction("add") Coroutine { self: Album, asset: Asset ->
131
+ systemPermissionsDelegate.requireSystemPermissions(true)
132
+ mediaStorePermissionsDelegate.requestMediaLibraryActionPermission(listOf(asset.contentUri))
133
+ self.add(asset)
134
+ }
135
+
136
+ AsyncFunction("delete") Coroutine { self: Album ->
137
+ systemPermissionsDelegate.requireSystemPermissions(true)
138
+ val assetIdsToDelete = self.getAssets().map { it.contentUri }
139
+ mediaStorePermissionsDelegate.requestMediaLibraryActionPermission(assetIdsToDelete, needsDeletePermission = true)
140
+ self.delete()
141
+ }
142
+ }
143
+
144
+ AsyncFunction("createAsset") Coroutine { filePath: Uri, album: Album? ->
145
+ systemPermissionsDelegate.requireSystemPermissions(true)
146
+ return@Coroutine assetFactory.create(filePath, album?.getRelativePath())
147
+ }
148
+
149
+ @OptIn(EitherType::class)
150
+ AsyncFunction("createAlbum") Coroutine { name: String, assetRefs: Either<List<Asset>, List<Uri>>, move: Boolean ->
151
+ systemPermissionsDelegate.requireSystemPermissions(true)
152
+ val assetListKClass = toKClass<List<Asset>>()
153
+ if (assetRefs.`is`(assetListKClass)) {
154
+ val assetList = assetRefs.get(assetListKClass)
155
+ return@Coroutine albumFactory.createFromAssets(name, assetList, move)
156
+ }
157
+ val assetPaths = assetRefs.get(toKClass<List<Uri>>())
158
+ return@Coroutine albumFactory.createFromFilePaths(name, assetPaths)
159
+ }
160
+
161
+ AsyncFunction("deleteAlbums") Coroutine { albums: List<Album> ->
162
+ systemPermissionsDelegate.requireSystemPermissions(true)
163
+ albums.forEach { album -> album.delete() }
164
+ }
165
+
166
+ AsyncFunction("deleteAssets") Coroutine { assets: List<Asset> ->
167
+ systemPermissionsDelegate.requireSystemPermissions(true)
168
+ val assetIdsToDelete = assets.map { it.contentUri }
169
+ mediaStorePermissionsDelegate.requestMediaLibraryActionPermission(assetIdsToDelete, needsDeletePermission = true)
170
+ assets.forEach { asset -> asset.delete() }
171
+ }
172
+
173
+ AsyncFunction("requestPermissionsAsync") { writeOnly: Boolean, permissions: List<GranularPermission>?, promise: Promise ->
174
+ systemPermissionsDelegate.requestPermissions(writeOnly, permissions, promise)
175
+ }
176
+
177
+ AsyncFunction("getPermissionsAsync") { writeOnly: Boolean, permissions: List<GranularPermission>?, promise: Promise ->
178
+ systemPermissionsDelegate.getPermissions(writeOnly, permissions, promise)
179
+ }
180
+
181
+ RegisterActivityContracts {
182
+ with(mediaStorePermissionsDelegate) {
183
+ registerMediaStoreContracts(this@MediaLibraryNextModule)
184
+ }
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,15 @@
1
+ package expo.modules.medialibrary.next.exceptions
2
+
3
+ import expo.modules.kotlin.exception.CodedException
4
+
5
+ class AlbumNotFoundException(message: String, cause: Throwable? = null) :
6
+ CodedException(message, cause)
7
+
8
+ class AlbumAlreadyExistsException(message: String, cause: Throwable? = null) :
9
+ CodedException(message, cause)
10
+
11
+ class AlbumPropertyNotFoundException(message: String, cause: Throwable? = null) :
12
+ CodedException(message, cause)
13
+
14
+ class AlbumCouldNotBeCreated(message: String, cause: Throwable? = null) :
15
+ CodedException(message, cause)
@@ -0,0 +1,21 @@
1
+ package expo.modules.medialibrary.next.exceptions
2
+
3
+ import expo.modules.kotlin.exception.CodedException
4
+
5
+ class PermissionsException(message: String, cause: Throwable? = null) :
6
+ CodedException(message, cause)
7
+
8
+ class UnableToSaveException(message: String) :
9
+ CodedException(message)
10
+
11
+ class AssetNotFoundException(message: String, cause: Throwable? = null) :
12
+ CodedException(message, cause)
13
+
14
+ class AssetPropertyNotFoundException(propertyName: String, cause: Throwable? = null) :
15
+ CodedException("$propertyName not found. The asset may have been deleted or is no longer accessible.", cause)
16
+
17
+ class AssetCouldNotBeCreated(message: String, cause: Throwable? = null) :
18
+ CodedException(message, cause)
19
+
20
+ class AssetInitializationException(message: String, cause: Throwable? = null) :
21
+ CodedException(message, cause)
@@ -0,0 +1,6 @@
1
+ package expo.modules.medialibrary.next.exceptions
2
+
3
+ import expo.modules.kotlin.exception.CodedException
4
+
5
+ class ContentResolverNotObtainedException(cause: Throwable? = null) :
6
+ CodedException("Could not obtain the content resolver", cause)
@@ -0,0 +1,6 @@
1
+ package expo.modules.medialibrary.next.exceptions
2
+
3
+ import expo.modules.kotlin.exception.CodedException
4
+
5
+ class PermissionException(message: String) :
6
+ CodedException(message)
@@ -0,0 +1,28 @@
1
+ package expo.modules.medialibrary.next.extensions
2
+
3
+ import android.database.Cursor
4
+
5
+ fun Cursor.asIterable(): Iterable<Cursor> {
6
+ return object : Iterable<Cursor> {
7
+ override fun iterator(): Iterator<Cursor> = object : Iterator<Cursor> {
8
+ private var hasNextCalled = false
9
+ private var hasNextCache = false
10
+
11
+ override fun hasNext(): Boolean {
12
+ if (!hasNextCalled) {
13
+ hasNextCache = moveToNext()
14
+ hasNextCalled = true
15
+ }
16
+ return hasNextCache
17
+ }
18
+
19
+ override fun next(): Cursor {
20
+ if (!hasNextCalled) {
21
+ if (!moveToNext()) throw NoSuchElementException()
22
+ }
23
+ hasNextCalled = false
24
+ return this@asIterable
25
+ }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,41 @@
1
+ package expo.modules.medialibrary.next.extensions
2
+
3
+ import expo.modules.medialibrary.MediaLibraryUtils.getFileNameAndExtension
4
+ import java.io.File
5
+ import java.io.FileInputStream
6
+ import java.io.FileOutputStream
7
+ import java.io.IOException
8
+
9
+ fun File.safeMove(destinationDirectory: File): File =
10
+ safeCopy(destinationDirectory).also {
11
+ delete()
12
+ }
13
+
14
+ fun File.safeCopy(destinationDirectory: File): File {
15
+ val newFile = createUniqueFileIn(destinationDirectory, name)
16
+ FileInputStream(this).channel.use { input ->
17
+ FileOutputStream(newFile).channel.use { output ->
18
+ val transferred = input.transferTo(0, input.size(), output)
19
+ if (transferred != input.size()) {
20
+ newFile.delete()
21
+ throw IOException("Could not save file to $destinationDirectory Not enough space.")
22
+ }
23
+ return newFile
24
+ }
25
+ }
26
+ }
27
+
28
+ private fun createUniqueFileIn(directory: File, newFileName: String): File {
29
+ var newFile = File(directory, newFileName)
30
+ var suffix = 2
31
+ val (filename, extension) = getFileNameAndExtension(newFileName)
32
+ val suffixLimit = Short.MAX_VALUE.toInt()
33
+ while (newFile.exists()) {
34
+ newFile = File(directory, filename + "_" + suffix + extension)
35
+ suffix++
36
+ if (suffix > suffixLimit) {
37
+ throw IOException("File name suffix limit reached ($suffixLimit)")
38
+ }
39
+ }
40
+ return newFile
41
+ }
@@ -0,0 +1,9 @@
1
+ package expo.modules.medialibrary.next.extensions
2
+
3
+ import android.content.Context
4
+ import expo.modules.kotlin.exception.Exceptions
5
+ import java.lang.ref.WeakReference
6
+
7
+ fun WeakReference<Context>.getOrThrow(): Context {
8
+ return get() ?: throw Exceptions.ReactContextLost()
9
+ }
@@ -0,0 +1,68 @@
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 android.provider.MediaStore
7
+ import expo.modules.medialibrary.next.extensions.asIterable
8
+ import expo.modules.medialibrary.next.objects.wrappers.RelativePath
9
+ import kotlinx.coroutines.Dispatchers
10
+ import kotlinx.coroutines.ensureActive
11
+ import kotlinx.coroutines.withContext
12
+
13
+ val EXTERNAL_CONTENT_URI: Uri = MediaStore.Files.getContentUri("external")
14
+
15
+ suspend fun ContentResolver.queryAlbumTitle(bucketId: String): String? =
16
+ queryOne(
17
+ EXTERNAL_CONTENT_URI,
18
+ MediaStore.MediaColumns.BUCKET_DISPLAY_NAME,
19
+ Cursor::getString,
20
+ "${MediaStore.MediaColumns.BUCKET_ID} = ?",
21
+ arrayOf(bucketId)
22
+ )
23
+
24
+ suspend fun ContentResolver.queryAlbumRelativePath(bucketId: String): RelativePath? =
25
+ queryOne(
26
+ EXTERNAL_CONTENT_URI,
27
+ MediaStore.MediaColumns.RELATIVE_PATH,
28
+ { index -> RelativePath(getString(index)) },
29
+ "${MediaStore.MediaColumns.BUCKET_ID} = ?",
30
+ arrayOf(bucketId)
31
+ )
32
+
33
+ suspend fun ContentResolver.queryAlbumFilepath(bucketId: String): String? =
34
+ queryOne(
35
+ EXTERNAL_CONTENT_URI,
36
+ MediaStore.MediaColumns.DATA,
37
+ Cursor::getString,
38
+ "${MediaStore.Files.FileColumns.BUCKET_ID} = ?",
39
+ arrayOf(bucketId)
40
+ )
41
+
42
+ suspend fun ContentResolver.queryAlbumId(relativePath: RelativePath): String? =
43
+ queryOne(
44
+ EXTERNAL_CONTENT_URI,
45
+ MediaStore.Files.FileColumns.BUCKET_ID,
46
+ Cursor::getString,
47
+ "${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?",
48
+ arrayOf(relativePath.value)
49
+ )
50
+
51
+ suspend fun ContentResolver.queryAlbumAssetsContentUris(bucketId: String): List<Uri> =
52
+ withContext(Dispatchers.IO) {
53
+ val projection = arrayOf(
54
+ MediaStore.Files.FileColumns._ID,
55
+ MediaStore.Files.FileColumns.MEDIA_TYPE
56
+ )
57
+ val selection = "${MediaStore.Files.FileColumns.BUCKET_ID} = ?"
58
+
59
+ query(EXTERNAL_CONTENT_URI, projection, selection, arrayOf(bucketId), null)?.use { cursor ->
60
+ ensureActive()
61
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
62
+ val typeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
63
+ cursor
64
+ .asIterable()
65
+ .map { it.extractAssetContentUri(idColumn, typeColumn) }
66
+ .toList()
67
+ } ?: emptyList()
68
+ }
@@ -0,0 +1,89 @@
1
+ package expo.modules.medialibrary.next.extensions.resolver
2
+
3
+ import android.content.ContentResolver
4
+ import android.content.ContentValues
5
+ import android.database.Cursor
6
+ import android.net.Uri
7
+ import android.os.Build
8
+ import android.provider.MediaStore
9
+ import androidx.annotation.RequiresApi
10
+ import expo.modules.medialibrary.EXTERNAL_CONTENT_URI
11
+ import expo.modules.medialibrary.next.exceptions.AssetCouldNotBeCreated
12
+ import expo.modules.medialibrary.next.objects.wrappers.RelativePath
13
+ import expo.modules.medialibrary.next.objects.wrappers.MimeType
14
+ import kotlinx.coroutines.Dispatchers
15
+ import kotlinx.coroutines.withContext
16
+
17
+ suspend fun ContentResolver.queryAssetDisplayName(contentUri: Uri): String? =
18
+ queryOne(contentUri, MediaStore.MediaColumns.DISPLAY_NAME, Cursor::getString)
19
+
20
+ suspend fun ContentResolver.queryGetCreationTime(contentUri: Uri): Long? =
21
+ queryOne(contentUri, MediaStore.Images.Media.DATE_TAKEN, Cursor::getLong)
22
+
23
+ suspend fun ContentResolver.queryAssetModificationTime(contentUri: Uri): Long? =
24
+ queryOne(contentUri, MediaStore.Images.Media.DATE_MODIFIED, Cursor::getLong)
25
+
26
+ suspend fun ContentResolver.queryAssetDuration(contentUri: Uri): Long? =
27
+ queryOne(contentUri, MediaStore.Video.VideoColumns.DURATION, Cursor::getLong)
28
+
29
+ suspend fun ContentResolver.queryAssetWidth(contentUri: Uri): Int? =
30
+ queryOne(contentUri, MediaStore.Images.Media.WIDTH, Cursor::getInt)
31
+
32
+ suspend fun ContentResolver.queryAssetHeight(contentUri: Uri): Int? =
33
+ queryOne(contentUri, MediaStore.Images.Media.HEIGHT, Cursor::getInt)
34
+
35
+ suspend fun ContentResolver.queryAssetPath(contentUri: Uri): String? =
36
+ queryOne(contentUri, MediaStore.Files.FileColumns.DATA, Cursor::getString)
37
+
38
+ suspend fun ContentResolver.queryAssetBucketId(contentUri: Uri): Int? =
39
+ queryOne(contentUri, MediaStore.Images.Media.BUCKET_ID, Cursor::getInt)
40
+
41
+ suspend fun ContentResolver.insertPendingAsset(
42
+ displayName: String,
43
+ mimeType: MimeType,
44
+ relativePath: RelativePath
45
+ ): Uri = withContext(Dispatchers.IO) {
46
+ val contentValues = ContentValues().apply {
47
+ put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
48
+ put(MediaStore.MediaColumns.MIME_TYPE, mimeType.value)
49
+ put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath.value)
50
+ put(MediaStore.MediaColumns.IS_PENDING, 1)
51
+ }
52
+ val collectionUri = mimeType.mediaCollectionUri()
53
+ return@withContext insert(collectionUri, contentValues)
54
+ ?: throw AssetCouldNotBeCreated("Failed to create asset: contentResolver.insert() returned null.")
55
+ }
56
+
57
+ @RequiresApi(Build.VERSION_CODES.Q)
58
+ fun ContentResolver.publishPendingAsset(uri: Uri) {
59
+ val contentValues = ContentValues().apply {
60
+ put(MediaStore.MediaColumns.IS_PENDING, 0)
61
+ }
62
+ safeUpdate(uri, contentValues)
63
+ }
64
+
65
+ fun ContentResolver.safeUpdate(
66
+ uri: Uri,
67
+ values: ContentValues
68
+ ): Int {
69
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
70
+ update(uri, values, null)
71
+ } else {
72
+ update(uri, values, null, null)
73
+ }
74
+ }
75
+
76
+ fun ContentResolver.updateRelativePath(contentUri: Uri, newRelativePath: RelativePath) {
77
+ val contentValues = ContentValues().apply {
78
+ put(MediaStore.MediaColumns.RELATIVE_PATH, newRelativePath.value)
79
+ }
80
+ update(contentUri, contentValues, null, null)
81
+ }
82
+
83
+ fun ContentResolver.deleteBy(assetPath: String) {
84
+ delete(
85
+ EXTERNAL_CONTENT_URI,
86
+ "${MediaStore.MediaColumns.DATA}=?",
87
+ arrayOf(assetPath)
88
+ )
89
+ }
@@ -0,0 +1,18 @@
1
+ package expo.modules.medialibrary.next.extensions.resolver
2
+
3
+ import android.content.ContentUris
4
+ import android.database.Cursor
5
+ import android.net.Uri
6
+ import android.provider.MediaStore
7
+
8
+ fun Cursor.extractAssetContentUri(idColumn: Int, typeColumn: Int): Uri {
9
+ val id = getLong(idColumn)
10
+ val mediaType = getInt(typeColumn)
11
+ val baseUri = when (mediaType) {
12
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
13
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
14
+ MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
15
+ else -> EXTERNAL_CONTENT_URI
16
+ }
17
+ return ContentUris.withAppendedId(baseUri, id)
18
+ }
@@ -0,0 +1,29 @@
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 kotlinx.coroutines.Dispatchers
7
+ import kotlinx.coroutines.ensureActive
8
+ import kotlinx.coroutines.withContext
9
+
10
+ suspend fun <T> ContentResolver.queryOne(
11
+ contentUri: Uri,
12
+ column: String,
13
+ extractor: Cursor.(index: Int) -> T,
14
+ selection: String? = null,
15
+ selectionArgs: Array<String>? = null,
16
+ sortOrder: String? = null
17
+ ): T? = withContext(Dispatchers.IO) {
18
+ val projection = arrayOf(column)
19
+ query(contentUri, projection, selection, selectionArgs, sortOrder)?.use { cursor ->
20
+ ensureActive()
21
+ val index = cursor.getColumnIndexOrThrow(column)
22
+ return@withContext if (cursor.moveToFirst()) {
23
+ extractor(cursor, index)
24
+ } else {
25
+ null
26
+ }
27
+ }
28
+ return@withContext null
29
+ }
@@ -0,0 +1,15 @@
1
+ package expo.modules.medialibrary.next.extensions.resolver
2
+
3
+ import android.content.ContentResolver
4
+ import android.net.Uri
5
+ import kotlin.io.copyTo
6
+
7
+ fun ContentResolver.copyUriContent(from: Uri, to: Uri) {
8
+ openInputStream(from).use { input ->
9
+ openOutputStream(to).use { output ->
10
+ if (input != null && output != null) {
11
+ input.copyTo(output)
12
+ }
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,71 @@
1
+ package expo.modules.medialibrary.next.objects.album
2
+
3
+ import android.content.Context
4
+ import android.os.Build
5
+ import android.os.Environment
6
+ import expo.modules.kotlin.sharedobjects.SharedObject
7
+ import expo.modules.medialibrary.next.exceptions.AlbumPropertyNotFoundException
8
+ import expo.modules.medialibrary.next.exceptions.ContentResolverNotObtainedException
9
+ import expo.modules.medialibrary.next.extensions.getOrThrow
10
+ import expo.modules.medialibrary.next.extensions.resolver.queryAlbumAssetsContentUris
11
+ import expo.modules.medialibrary.next.extensions.resolver.queryAlbumFilepath
12
+ import expo.modules.medialibrary.next.extensions.resolver.queryAlbumRelativePath
13
+ import expo.modules.medialibrary.next.extensions.resolver.queryAlbumTitle
14
+ import expo.modules.medialibrary.next.objects.asset.Asset
15
+ import expo.modules.medialibrary.next.objects.wrappers.RelativePath
16
+ import kotlinx.coroutines.async
17
+ import kotlinx.coroutines.awaitAll
18
+ import kotlinx.coroutines.coroutineScope
19
+ import java.io.File
20
+ import java.lang.ref.WeakReference
21
+
22
+ class Album(val id: String, context: Context) : SharedObject() {
23
+ private val contextRef = WeakReference(context)
24
+
25
+ private val contentResolver
26
+ get() = contextRef
27
+ .getOrThrow()
28
+ .contentResolver ?: throw ContentResolverNotObtainedException()
29
+
30
+ suspend fun getTitle(): String {
31
+ return contentResolver.queryAlbumTitle(id)
32
+ ?: throw AlbumPropertyNotFoundException("Album with ID=$id does not exist in MediaStore")
33
+ }
34
+
35
+ suspend fun getRelativePath(): RelativePath {
36
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
37
+ contentResolver.queryAlbumRelativePath(id)
38
+ ?: throw AlbumPropertyNotFoundException("Album with ID=$id does not exist in MediaStore")
39
+ } else {
40
+ val filePath = contentResolver.queryAlbumFilepath(id)
41
+ ?: throw AlbumPropertyNotFoundException("Album with ID=$id does not exist in MediaStore")
42
+ createRelativePathFrom(filePath)
43
+ }
44
+ }
45
+
46
+ private fun createRelativePathFrom(filePath: String): RelativePath {
47
+ val albumDir = File(filePath).parent
48
+ ?: throw AlbumPropertyNotFoundException("Could get a relative path for the album")
49
+ val externalRoot = Environment.getExternalStorageDirectory().absolutePath
50
+ val relative = albumDir.removePrefix(externalRoot).trimStart('/').plus('/')
51
+ return RelativePath(relative)
52
+ }
53
+
54
+ suspend fun getAssets(): List<Asset> {
55
+ return contentResolver
56
+ .queryAlbumAssetsContentUris(id)
57
+ .map { contentUri -> Asset(contentUri, contextRef.getOrThrow()) }
58
+ }
59
+
60
+ suspend fun delete() = coroutineScope {
61
+ getAssets().map { asset ->
62
+ async {
63
+ asset.delete()
64
+ }
65
+ }.awaitAll()
66
+ }
67
+
68
+ suspend fun add(asset: Asset) {
69
+ asset.move(getRelativePath())
70
+ }
71
+ }