expo-media-library 18.0.0-canary-20250722-599a28f → 18.0.0-canary-20250811-5c940c0
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 +2 -0
- package/android/build.gradle +3 -2
- package/android/src/main/java/expo/modules/medialibrary/Exceptions.kt +12 -0
- package/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt +124 -234
- package/android/src/main/java/expo/modules/medialibrary/MediaLibraryUtils.kt +26 -12
- package/android/src/main/java/expo/modules/medialibrary/albums/AddAssetsToAlbum.kt +35 -31
- package/android/src/main/java/expo/modules/medialibrary/albums/AlbumUtils.kt +22 -22
- package/android/src/main/java/expo/modules/medialibrary/albums/CreateAlbum.kt +45 -67
- package/android/src/main/java/expo/modules/medialibrary/albums/DeleteAlbums.kt +5 -26
- package/android/src/main/java/expo/modules/medialibrary/albums/GetAlbum.kt +6 -12
- package/android/src/main/java/expo/modules/medialibrary/albums/GetAlbums.kt +51 -56
- package/android/src/main/java/expo/modules/medialibrary/albums/RemoveAssetsFromAlbum.kt +4 -13
- package/android/src/main/java/expo/modules/medialibrary/albums/migration/CheckIfAlbumShouldBeMigrated.kt +16 -16
- package/android/src/main/java/expo/modules/medialibrary/albums/migration/MigrateAlbum.kt +32 -35
- package/android/src/main/java/expo/modules/medialibrary/assets/AssetUtils.kt +18 -21
- package/android/src/main/java/expo/modules/medialibrary/assets/CreateAsset.kt +40 -51
- package/android/src/main/java/expo/modules/medialibrary/assets/DeleteAssets.kt +4 -11
- package/android/src/main/java/expo/modules/medialibrary/assets/GetAssetInfo.kt +5 -12
- package/android/src/main/java/expo/modules/medialibrary/assets/GetAssets.kt +40 -52
- package/android/src/main/java/expo/modules/medialibrary/contracts/DeleteContract.kt +38 -0
- package/android/src/main/java/expo/modules/medialibrary/contracts/WriteContract.kt +38 -0
- package/expo-module.config.json +1 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0-sources.jar +0 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0-sources.jar.md5 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0-sources.jar.sha1 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0-sources.jar.sha256 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0-sources.jar.sha512 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.aar +0 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.aar.md5 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.aar.sha1 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.aar.sha256 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.aar.sha512 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/{18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.module → 18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.module} +29 -22
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.module.md5 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.module.sha1 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.module.sha256 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.module.sha512 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/{18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.pom → 18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.pom} +7 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.pom.md5 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.pom.sha1 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.pom.sha256 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250811-5c940c0/expo.modules.medialibrary-18.0.0-canary-20250811-5c940c0.pom.sha512 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml +4 -4
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.md5 +1 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.sha1 +1 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.sha256 +1 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/maven-metadata.xml.sha512 +1 -1
- package/package.json +3 -3
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f-sources.jar +0 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f-sources.jar.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f-sources.jar.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f-sources.jar.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f-sources.jar.sha512 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.aar +0 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.aar.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.aar.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.aar.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.aar.sha512 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.module.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.module.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.module.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.module.sha512 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.pom.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.pom.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.pom.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.medialibrary/18.0.0-canary-20250722-599a28f/expo.modules.medialibrary-18.0.0-canary-20250722-599a28f.pom.sha512 +0 -1
package/CHANGELOG.md
CHANGED
package/android/build.gradle
CHANGED
|
@@ -4,19 +4,20 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'host.exp.exponent'
|
|
7
|
-
version = '18.0.0-canary-
|
|
7
|
+
version = '18.0.0-canary-20250811-5c940c0'
|
|
8
8
|
|
|
9
9
|
android {
|
|
10
10
|
namespace "expo.modules.medialibrary"
|
|
11
11
|
defaultConfig {
|
|
12
12
|
versionCode 37
|
|
13
|
-
versionName "18.0.0-canary-
|
|
13
|
+
versionName "18.0.0-canary-20250811-5c940c0"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
dependencies {
|
|
18
18
|
implementation "androidx.annotation:annotation:1.2.0"
|
|
19
19
|
api "androidx.exifinterface:exifinterface:1.3.3"
|
|
20
|
+
implementation 'androidx.activity:activity-ktx:1.10.1'
|
|
20
21
|
|
|
21
22
|
if (project.findProject(':expo-modules-test-core')) {
|
|
22
23
|
testImplementation project(':expo-modules-test-core')
|
|
@@ -31,3 +31,15 @@ class ContentEntryException :
|
|
|
31
31
|
|
|
32
32
|
class AssetFileException(message: String) :
|
|
33
33
|
CodedException(message)
|
|
34
|
+
|
|
35
|
+
class UnableToLoadPermissionException(message: String, cause: Throwable? = null) :
|
|
36
|
+
CodedException(message, cause)
|
|
37
|
+
|
|
38
|
+
class UnableToLoadException(message: String, cause: Throwable? = null) :
|
|
39
|
+
CodedException(message, cause)
|
|
40
|
+
|
|
41
|
+
class UnableToDeleteException(message: String, cause: Throwable? = null) :
|
|
42
|
+
CodedException(message, cause)
|
|
43
|
+
|
|
44
|
+
class UnableToSaveException(message: String, cause: Throwable? = null) :
|
|
45
|
+
CodedException(message, cause)
|
|
@@ -8,10 +8,8 @@ import android.Manifest.permission.READ_MEDIA_VIDEO
|
|
|
8
8
|
import android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
|
|
9
9
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
10
10
|
import android.annotation.SuppressLint
|
|
11
|
-
import android.app.Activity
|
|
12
11
|
import android.content.Context
|
|
13
12
|
import android.content.Intent
|
|
14
|
-
import android.content.IntentSender.SendIntentException
|
|
15
13
|
import android.content.pm.PackageManager
|
|
16
14
|
import android.database.ContentObserver
|
|
17
15
|
import android.net.Uri
|
|
@@ -21,44 +19,42 @@ import android.os.Bundle
|
|
|
21
19
|
import android.os.Handler
|
|
22
20
|
import android.os.Looper
|
|
23
21
|
import android.provider.MediaStore
|
|
24
|
-
import android.util.Log
|
|
25
22
|
import androidx.annotation.RequiresApi
|
|
26
|
-
import expo.modules.core.errors.ModuleDestroyedException
|
|
27
23
|
import expo.modules.interfaces.permissions.Permissions.askForPermissionsWithPermissionsManager
|
|
28
24
|
import expo.modules.interfaces.permissions.Permissions.getPermissionsWithPermissionsManager
|
|
29
25
|
import expo.modules.kotlin.Promise
|
|
30
|
-
import expo.modules.kotlin.
|
|
26
|
+
import expo.modules.kotlin.activityresult.AppContextActivityResultLauncher
|
|
31
27
|
import expo.modules.kotlin.exception.Exceptions
|
|
28
|
+
import expo.modules.kotlin.functions.Coroutine
|
|
32
29
|
import expo.modules.kotlin.modules.Module
|
|
33
30
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
34
|
-
import expo.modules.medialibrary.
|
|
35
|
-
import expo.modules.medialibrary.albums.
|
|
36
|
-
import expo.modules.medialibrary.albums.
|
|
37
|
-
import expo.modules.medialibrary.albums.
|
|
38
|
-
import expo.modules.medialibrary.albums.
|
|
39
|
-
import expo.modules.medialibrary.albums.
|
|
40
|
-
import expo.modules.medialibrary.albums.GetAlbums
|
|
41
|
-
import expo.modules.medialibrary.albums.RemoveAssetsFromAlbum
|
|
31
|
+
import expo.modules.medialibrary.albums.addAssetsToAlbum
|
|
32
|
+
import expo.modules.medialibrary.albums.createAlbum
|
|
33
|
+
import expo.modules.medialibrary.albums.createAlbumWithInitialFileUri
|
|
34
|
+
import expo.modules.medialibrary.albums.deleteAlbums
|
|
35
|
+
import expo.modules.medialibrary.albums.getAlbum
|
|
36
|
+
import expo.modules.medialibrary.albums.getAlbums
|
|
42
37
|
import expo.modules.medialibrary.albums.getAssetsInAlbums
|
|
43
|
-
import expo.modules.medialibrary.albums.migration.
|
|
44
|
-
import expo.modules.medialibrary.albums.migration.
|
|
45
|
-
import expo.modules.medialibrary.
|
|
46
|
-
import expo.modules.medialibrary.assets.
|
|
47
|
-
import expo.modules.medialibrary.assets.
|
|
48
|
-
import expo.modules.medialibrary.assets.
|
|
49
|
-
import
|
|
50
|
-
import
|
|
51
|
-
import
|
|
52
|
-
import
|
|
38
|
+
import expo.modules.medialibrary.albums.migration.checkIfAlbumShouldBeMigrated
|
|
39
|
+
import expo.modules.medialibrary.albums.migration.migrateAlbum
|
|
40
|
+
import expo.modules.medialibrary.albums.removeAssetsFromAlbum
|
|
41
|
+
import expo.modules.medialibrary.assets.createAssetWithAlbumId
|
|
42
|
+
import expo.modules.medialibrary.assets.deleteAssets
|
|
43
|
+
import expo.modules.medialibrary.assets.getAssetInfo
|
|
44
|
+
import expo.modules.medialibrary.assets.getAssets
|
|
45
|
+
import expo.modules.medialibrary.contracts.DeleteContract
|
|
46
|
+
import expo.modules.medialibrary.contracts.DeleteContractInput
|
|
47
|
+
import expo.modules.medialibrary.contracts.WriteContract
|
|
48
|
+
import expo.modules.medialibrary.contracts.WriteContractInput
|
|
53
49
|
import java.lang.ref.WeakReference
|
|
54
50
|
|
|
55
51
|
class MediaLibraryModule : Module() {
|
|
56
52
|
private val context: Context
|
|
57
53
|
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
58
|
-
private val moduleCoroutineScope = CoroutineScope(Dispatchers.IO)
|
|
59
54
|
private var imagesObserver: MediaStoreContentObserver? = null
|
|
60
55
|
private var videosObserver: MediaStoreContentObserver? = null
|
|
61
|
-
private var
|
|
56
|
+
private lateinit var deleteLauncher: AppContextActivityResultLauncher<DeleteContractInput, Boolean>
|
|
57
|
+
private lateinit var writeLauncher: AppContextActivityResultLauncher<WriteContractInput, Boolean>
|
|
62
58
|
private val isExpoGo by lazy {
|
|
63
59
|
context.resources.getString(R.string.is_expo_go).toBoolean()
|
|
64
60
|
}
|
|
@@ -103,134 +99,84 @@ class MediaLibraryModule : Module() {
|
|
|
103
99
|
)
|
|
104
100
|
}
|
|
105
101
|
|
|
106
|
-
AsyncFunction("saveToLibraryAsync") { localUri: String
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
CreateAssetWithAlbumId(context, localUri, promise, false)
|
|
110
|
-
.execute()
|
|
111
|
-
}
|
|
112
|
-
}
|
|
102
|
+
AsyncFunction("saveToLibraryAsync") Coroutine { localUri: String ->
|
|
103
|
+
requireSystemPermissions()
|
|
104
|
+
return@Coroutine createAssetWithAlbumId(context, localUri, false)
|
|
113
105
|
}
|
|
114
106
|
|
|
115
|
-
AsyncFunction("createAssetAsync") { localUri: String, albumId: String
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
CreateAssetWithAlbumId(context, localUri, promise, true, albumId)
|
|
119
|
-
.execute()
|
|
120
|
-
}
|
|
121
|
-
}
|
|
107
|
+
AsyncFunction("createAssetAsync") Coroutine { localUri: String, albumId: String? ->
|
|
108
|
+
requireSystemPermissions()
|
|
109
|
+
return@Coroutine createAssetWithAlbumId(context, localUri, true, albumId)
|
|
122
110
|
}
|
|
123
111
|
|
|
124
|
-
AsyncFunction("addAssetsToAlbumAsync") { assetsId:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
AddAssetsToAlbum(context, assetsId.toTypedArray(), albumId, copyToAlbum, promise)
|
|
129
|
-
.execute()
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
runActionWithPermissions(if (copyToAlbum) emptyList() else assetsId, action)
|
|
133
|
-
}
|
|
112
|
+
AsyncFunction("addAssetsToAlbumAsync") Coroutine { assetsId: Array<String>, albumId: String, copyToAlbum: Boolean ->
|
|
113
|
+
requireSystemPermissions()
|
|
114
|
+
requestMediaLibraryActionPermission(if (copyToAlbum) emptyArray() else assetsId)
|
|
115
|
+
return@Coroutine addAssetsToAlbum(context, assetsId, albumId, copyToAlbum)
|
|
134
116
|
}
|
|
135
117
|
|
|
136
|
-
AsyncFunction("removeAssetsFromAlbumAsync") { assetsId:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
RemoveAssetsFromAlbum(context, assetsId.toTypedArray(), albumId, promise)
|
|
141
|
-
.execute()
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
runActionWithPermissions(assetsId, action)
|
|
145
|
-
}
|
|
118
|
+
AsyncFunction("removeAssetsFromAlbumAsync") Coroutine { assetsId: Array<String>, albumId: String ->
|
|
119
|
+
requireSystemPermissions()
|
|
120
|
+
requestMediaLibraryActionPermission(assetsId)
|
|
121
|
+
return@Coroutine removeAssetsFromAlbum(context, assetsId, albumId)
|
|
146
122
|
}
|
|
147
123
|
|
|
148
|
-
AsyncFunction("deleteAssetsAsync") { assetsId:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
DeleteAssets(context, assetsId.toTypedArray(), promise)
|
|
153
|
-
.execute()
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
runActionWithPermissions(assetsId, action, useDeletePermission = true)
|
|
157
|
-
}
|
|
124
|
+
AsyncFunction("deleteAssetsAsync") Coroutine { assetsId: Array<String> ->
|
|
125
|
+
requireSystemPermissions()
|
|
126
|
+
requestMediaLibraryActionPermission(assetsId, needsDeletePermission = true)
|
|
127
|
+
return@Coroutine deleteAssets(context, assetsId)
|
|
158
128
|
}
|
|
159
129
|
|
|
160
|
-
AsyncFunction("getAssetInfoAsync") { assetId: String, _: Map<String, Any?>?/* unused on android atm
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
GetAssetInfo(context, assetId, promise).execute()
|
|
164
|
-
}
|
|
165
|
-
}
|
|
130
|
+
AsyncFunction("getAssetInfoAsync") Coroutine { assetId: String, _: Map<String, Any?>?/* unused on android atm */ ->
|
|
131
|
+
requireSystemPermissions(false)
|
|
132
|
+
return@Coroutine getAssetInfo(context, assetId)
|
|
166
133
|
}
|
|
167
134
|
|
|
168
|
-
AsyncFunction("getAlbumsAsync") { _: Map<String, Any?>?/* unused on android atm
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
GetAlbums(context, promise).execute()
|
|
172
|
-
}
|
|
173
|
-
}
|
|
135
|
+
AsyncFunction("getAlbumsAsync") Coroutine { _: Map<String, Any?>?/* unused on android atm */ ->
|
|
136
|
+
requireSystemPermissions(false)
|
|
137
|
+
return@Coroutine getAlbums(context)
|
|
174
138
|
}
|
|
175
139
|
|
|
176
|
-
AsyncFunction("getAlbumAsync") { albumName: String
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
GetAlbum(context, albumName, promise)
|
|
180
|
-
.execute()
|
|
181
|
-
}
|
|
182
|
-
}
|
|
140
|
+
AsyncFunction("getAlbumAsync") Coroutine { albumName: String ->
|
|
141
|
+
requireSystemPermissions(false)
|
|
142
|
+
return@Coroutine getAlbum(context, albumName)
|
|
183
143
|
}
|
|
184
144
|
|
|
185
|
-
AsyncFunction("createAlbumAsync") { albumName: String, assetId: String?, copyAsset: Boolean, initialAssetUri: Uri
|
|
186
|
-
|
|
187
|
-
val action = actionIfUserGrantedPermission(promise) {
|
|
188
|
-
withModuleScope(promise) {
|
|
189
|
-
assetId?.let {
|
|
190
|
-
CreateAlbum(context, albumName, assetId, copyAsset, promise)
|
|
191
|
-
.execute()
|
|
192
|
-
}
|
|
145
|
+
AsyncFunction("createAlbumAsync") Coroutine { albumName: String, assetId: String?, copyAsset: Boolean, initialAssetUri: Uri? ->
|
|
146
|
+
requireSystemPermissions()
|
|
193
147
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
val assetIdList = if (!copyAsset && assetId != null) {
|
|
201
|
-
listOf(assetId)
|
|
202
|
-
} else {
|
|
203
|
-
emptyList()
|
|
204
|
-
}
|
|
205
|
-
runActionWithPermissions(assetIdList, action)
|
|
148
|
+
val assetIdArray = if (!copyAsset && assetId != null) {
|
|
149
|
+
arrayOf(assetId)
|
|
150
|
+
} else {
|
|
151
|
+
emptyArray()
|
|
206
152
|
}
|
|
207
|
-
}
|
|
208
153
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
val assetIds = getAssetsInAlbums(context, *albumIds.toTypedArray())
|
|
218
|
-
runActionWithPermissions(assetIds, action)
|
|
154
|
+
requestMediaLibraryActionPermission(assetIdArray)
|
|
155
|
+
|
|
156
|
+
return@Coroutine if (assetId != null) {
|
|
157
|
+
createAlbum(context, albumName, assetId, copyAsset)
|
|
158
|
+
} else if (initialAssetUri != null) {
|
|
159
|
+
createAlbumWithInitialFileUri(context, albumName, initialAssetUri)
|
|
160
|
+
} else {
|
|
161
|
+
throw AlbumException("Could not create the album")
|
|
219
162
|
}
|
|
220
163
|
}
|
|
221
164
|
|
|
222
|
-
AsyncFunction("
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
}
|
|
165
|
+
AsyncFunction("deleteAlbumsAsync") Coroutine { albumIds: Array<String> ->
|
|
166
|
+
requireSystemPermissions()
|
|
167
|
+
val assetIds = getAssetsInAlbums(context, *albumIds).toTypedArray()
|
|
168
|
+
requestMediaLibraryActionPermission(assetIds)
|
|
169
|
+
return@Coroutine deleteAlbums(context, albumIds)
|
|
229
170
|
}
|
|
230
171
|
|
|
231
|
-
AsyncFunction("
|
|
172
|
+
AsyncFunction("getAssetsAsync") Coroutine { assetOptions: AssetsOptions ->
|
|
173
|
+
requireSystemPermissions(false)
|
|
174
|
+
return@Coroutine getAssets(context, assetOptions)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
AsyncFunction("migrateAlbumIfNeededAsync") Coroutine { albumId: String ->
|
|
232
178
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
233
|
-
return@
|
|
179
|
+
return@Coroutine
|
|
234
180
|
}
|
|
235
181
|
|
|
236
182
|
val assetsIds = getAssetsInAlbums(context, albumId)
|
|
@@ -238,7 +184,7 @@ class MediaLibraryModule : Module() {
|
|
|
238
184
|
.toTypedArray()
|
|
239
185
|
// The album is empty, nothing to migrate
|
|
240
186
|
if (assetsIds.isEmpty()) {
|
|
241
|
-
return@
|
|
187
|
+
return@Coroutine
|
|
242
188
|
}
|
|
243
189
|
|
|
244
190
|
val assets = MediaLibraryUtils.getAssetsById(
|
|
@@ -259,33 +205,20 @@ class MediaLibraryModule : Module() {
|
|
|
259
205
|
|
|
260
206
|
val albumDir = assets[0].parentFile ?: throw AlbumPathException()
|
|
261
207
|
if (albumDir.canWrite()) {
|
|
262
|
-
return@
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
val action = actionIfUserGrantedPermission(promise) {
|
|
266
|
-
moduleCoroutineScope.launch {
|
|
267
|
-
MigrateAlbum(context, assets, albumDir.name, promise)
|
|
268
|
-
.execute()
|
|
269
|
-
}
|
|
208
|
+
return@Coroutine
|
|
270
209
|
}
|
|
271
210
|
|
|
272
|
-
val
|
|
273
|
-
|
|
211
|
+
val idsOfAssets = assets.map { it.assetId }.toTypedArray()
|
|
212
|
+
requestMediaLibraryActionPermission(idsOfAssets)
|
|
213
|
+
return@Coroutine migrateAlbum(context, assets, albumDir.name)
|
|
274
214
|
}
|
|
275
215
|
|
|
276
|
-
AsyncFunction("albumNeedsMigrationAsync") { albumId: String
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
.execute()
|
|
283
|
-
} catch (e: CodedException) {
|
|
284
|
-
promise.reject(e)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
promise.resolve(false)
|
|
216
|
+
AsyncFunction("albumNeedsMigrationAsync") Coroutine { albumId: String ->
|
|
217
|
+
requireSystemPermissions(false)
|
|
218
|
+
return@Coroutine if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
219
|
+
checkIfAlbumShouldBeMigrated(context, albumId)
|
|
220
|
+
} else {
|
|
221
|
+
false
|
|
289
222
|
}
|
|
290
223
|
}
|
|
291
224
|
|
|
@@ -333,33 +266,14 @@ class MediaLibraryModule : Module() {
|
|
|
333
266
|
}
|
|
334
267
|
}
|
|
335
268
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
OnDestroy {
|
|
344
|
-
try {
|
|
345
|
-
moduleCoroutineScope.cancel(ModuleDestroyedException())
|
|
346
|
-
} catch (e: IllegalStateException) {
|
|
347
|
-
Log.e(TAG, "The scope does not have a job in it")
|
|
348
|
-
}
|
|
269
|
+
RegisterActivityContracts {
|
|
270
|
+
deleteLauncher =
|
|
271
|
+
registerForActivityResult(DeleteContract(this@MediaLibraryModule))
|
|
272
|
+
writeLauncher =
|
|
273
|
+
registerForActivityResult(WriteContract(this@MediaLibraryModule))
|
|
349
274
|
}
|
|
350
275
|
}
|
|
351
276
|
|
|
352
|
-
private inline fun withModuleScope(promise: Promise, crossinline block: () -> Unit) =
|
|
353
|
-
moduleCoroutineScope.launch {
|
|
354
|
-
try {
|
|
355
|
-
block()
|
|
356
|
-
} catch (e: CodedException) {
|
|
357
|
-
promise.reject(e)
|
|
358
|
-
} catch (e: ModuleDestroyedException) {
|
|
359
|
-
promise.reject(TAG, "MediaLibrary module destroyed", e)
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
277
|
private val isMissingPermissions: Boolean
|
|
364
278
|
get() = hasReadPermissions()
|
|
365
279
|
|
|
@@ -430,19 +344,14 @@ class MediaLibraryModule : Module() {
|
|
|
430
344
|
return granularPermissions
|
|
431
345
|
}
|
|
432
346
|
|
|
433
|
-
private
|
|
347
|
+
private fun requireSystemPermissions(isWritePermissionRequired: Boolean = true) {
|
|
434
348
|
val missingPermissionsCondition =
|
|
435
|
-
if (
|
|
349
|
+
if (isWritePermissionRequired) isMissingWritePermission else isMissingPermissions
|
|
436
350
|
if (missingPermissionsCondition) {
|
|
437
351
|
val missingPermissionsMessage =
|
|
438
|
-
if (
|
|
352
|
+
if (isWritePermissionRequired) ERROR_NO_WRITE_PERMISSION_MESSAGE else ERROR_NO_PERMISSIONS_MESSAGE
|
|
439
353
|
throw PermissionsException(missingPermissionsMessage)
|
|
440
354
|
}
|
|
441
|
-
block()
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
private fun interface Action {
|
|
445
|
-
fun runWithPermissions(permissionsWereGranted: Boolean)
|
|
446
355
|
}
|
|
447
356
|
|
|
448
357
|
private fun hasReadPermissions(): Boolean {
|
|
@@ -481,56 +390,43 @@ class MediaLibraryModule : Module() {
|
|
|
481
390
|
?.not() ?: false
|
|
482
391
|
}
|
|
483
392
|
|
|
484
|
-
private fun
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
Binder.getCallingPid(),
|
|
491
|
-
Binder.getCallingUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
492
|
-
) != PackageManager.PERMISSION_GRANTED
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (pathsWithoutPermissions.isNotEmpty()) {
|
|
496
|
-
val request = if (useDeletePermission) {
|
|
497
|
-
MediaStore.createDeleteRequest(context.contentResolver, pathsWithoutPermissions)
|
|
498
|
-
} else {
|
|
499
|
-
MediaStore.createWriteRequest(context.contentResolver, pathsWithoutPermissions)
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
try {
|
|
503
|
-
awaitingAction = action
|
|
504
|
-
appContext.throwingActivity.startIntentSenderForResult(
|
|
505
|
-
request.intentSender,
|
|
506
|
-
if (useDeletePermission) DELETE_REQUEST_CODE else WRITE_REQUEST_CODE,
|
|
507
|
-
null,
|
|
508
|
-
0,
|
|
509
|
-
0,
|
|
510
|
-
0
|
|
511
|
-
)
|
|
512
|
-
} catch (e: SendIntentException) {
|
|
513
|
-
awaitingAction = null
|
|
514
|
-
throw e
|
|
515
|
-
}
|
|
516
|
-
// the action will be called when permissions are granted
|
|
517
|
-
return
|
|
518
|
-
}
|
|
393
|
+
private suspend fun requestMediaLibraryActionPermission(
|
|
394
|
+
assetIds: Array<String>,
|
|
395
|
+
needsDeletePermission: Boolean = false
|
|
396
|
+
) {
|
|
397
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
398
|
+
return
|
|
519
399
|
}
|
|
520
|
-
action.runWithPermissions(true)
|
|
521
|
-
}
|
|
522
400
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
401
|
+
val uris = MediaLibraryUtils.getAssetsUris(context, assetIds)
|
|
402
|
+
val urisWithoutPermission = uris.filterNot { uri ->
|
|
403
|
+
hasWritePermissionForUri(uri)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (urisWithoutPermission.isEmpty()) {
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
val granted = if (needsDeletePermission) {
|
|
411
|
+
deleteLauncher.launch(DeleteContractInput(uris = urisWithoutPermission))
|
|
529
412
|
} else {
|
|
530
|
-
|
|
413
|
+
writeLauncher.launch(WriteContractInput(uris = urisWithoutPermission))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!granted) {
|
|
417
|
+
throw PermissionsException(ERROR_USER_DID_NOT_GRANT_WRITE_PERMISSIONS_MESSAGE)
|
|
531
418
|
}
|
|
532
419
|
}
|
|
533
420
|
|
|
421
|
+
private fun hasWritePermissionForUri(uri: Uri): Boolean {
|
|
422
|
+
return context.checkUriPermission(
|
|
423
|
+
uri,
|
|
424
|
+
Binder.getCallingPid(),
|
|
425
|
+
Binder.getCallingUid(),
|
|
426
|
+
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
427
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
428
|
+
}
|
|
429
|
+
|
|
534
430
|
private inner class MediaStoreContentObserver(handler: Handler, private val mMediaType: Int) :
|
|
535
431
|
ContentObserver(handler) {
|
|
536
432
|
|
|
@@ -560,10 +456,4 @@ class MediaLibraryModule : Module() {
|
|
|
560
456
|
null
|
|
561
457
|
).use { countCursor -> countCursor?.count ?: 0 }
|
|
562
458
|
}
|
|
563
|
-
|
|
564
|
-
companion object {
|
|
565
|
-
private const val WRITE_REQUEST_CODE = 7463
|
|
566
|
-
private const val DELETE_REQUEST_CODE = 7464
|
|
567
|
-
internal val TAG = MediaLibraryModule::class.java.simpleName
|
|
568
|
-
}
|
|
569
459
|
}
|
|
@@ -4,6 +4,7 @@ import android.content.ContentResolver
|
|
|
4
4
|
import android.content.ContentUris
|
|
5
5
|
import android.content.Context
|
|
6
6
|
import android.content.pm.PackageManager
|
|
7
|
+
import android.media.MediaScannerConnection
|
|
7
8
|
import android.net.Uri
|
|
8
9
|
import android.os.Build
|
|
9
10
|
import android.os.Environment
|
|
@@ -11,11 +12,15 @@ import android.provider.MediaStore
|
|
|
11
12
|
import android.text.TextUtils
|
|
12
13
|
import android.util.Log
|
|
13
14
|
import android.webkit.MimeTypeMap
|
|
14
|
-
import
|
|
15
|
+
import kotlinx.coroutines.Dispatchers
|
|
16
|
+
import kotlinx.coroutines.ensureActive
|
|
17
|
+
import kotlinx.coroutines.withContext
|
|
15
18
|
import java.io.File
|
|
16
19
|
import java.io.FileInputStream
|
|
17
20
|
import java.io.FileOutputStream
|
|
18
21
|
import java.io.IOException
|
|
22
|
+
import kotlin.coroutines.resume
|
|
23
|
+
import kotlin.coroutines.suspendCoroutine
|
|
19
24
|
|
|
20
25
|
object MediaLibraryUtils {
|
|
21
26
|
class AssetFile(pathname: String, val assetId: String, val mimeType: String) : File(pathname)
|
|
@@ -75,7 +80,11 @@ object MediaLibraryUtils {
|
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
fun deleteAssets(
|
|
83
|
+
suspend fun deleteAssets(
|
|
84
|
+
context: Context,
|
|
85
|
+
selection: String?,
|
|
86
|
+
selectionArgs: Array<out String?>?
|
|
87
|
+
): Boolean = withContext(Dispatchers.IO) {
|
|
79
88
|
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
|
80
89
|
try {
|
|
81
90
|
context.contentResolver.query(
|
|
@@ -89,6 +98,9 @@ object MediaLibraryUtils {
|
|
|
89
98
|
throw AssetFileException("Could not delete assets. Cursor is null.")
|
|
90
99
|
} else {
|
|
91
100
|
while (filesToDelete.moveToNext()) {
|
|
101
|
+
// Interrupting file deletion when scope is closed is desired, as
|
|
102
|
+
// user might want to stop this process in the meantime
|
|
103
|
+
coroutineContext.ensureActive()
|
|
92
104
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
93
105
|
val columnId = filesToDelete.getColumnIndex(MediaStore.MediaColumns._ID)
|
|
94
106
|
val id = filesToDelete.getLong(columnId)
|
|
@@ -112,18 +124,13 @@ object MediaLibraryUtils {
|
|
|
112
124
|
}
|
|
113
125
|
}
|
|
114
126
|
}
|
|
115
|
-
promise.resolve(true)
|
|
116
127
|
}
|
|
117
128
|
}
|
|
129
|
+
return@withContext true
|
|
118
130
|
} catch (e: SecurityException) {
|
|
119
|
-
|
|
120
|
-
ERROR_UNABLE_TO_SAVE_PERMISSION,
|
|
121
|
-
"Could not delete asset: need WRITE_EXTERNAL_STORAGE permission.",
|
|
122
|
-
e
|
|
123
|
-
)
|
|
131
|
+
throw UnableToDeleteException("Could not delete asset: need WRITE_EXTERNAL_STORAGE permission.", e)
|
|
124
132
|
} catch (e: Exception) {
|
|
125
|
-
e.
|
|
126
|
-
promise.reject(ERROR_UNABLE_TO_DELETE, "Could not delete file.", e)
|
|
133
|
+
throw UnableToDeleteException("Could not delete file: ${e.message}", e)
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
|
|
@@ -188,9 +195,9 @@ object MediaLibraryUtils {
|
|
|
188
195
|
fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? =
|
|
189
196
|
contentResolver.getType(uri) ?: getMimeTypeFromFileUrl(uri.toString())
|
|
190
197
|
|
|
191
|
-
fun getAssetsUris(context: Context, assetsId:
|
|
198
|
+
fun getAssetsUris(context: Context, assetsId: Array<String>): List<Uri> {
|
|
192
199
|
val result = mutableListOf<Uri>()
|
|
193
|
-
val selection = MediaStore.MediaColumns._ID + " IN (" + TextUtils.join(",", assetsId
|
|
200
|
+
val selection = MediaStore.MediaColumns._ID + " IN (" + TextUtils.join(",", assetsId) + " )"
|
|
194
201
|
val selectionArgs: Array<String>? = null
|
|
195
202
|
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.MIME_TYPE)
|
|
196
203
|
context.contentResolver.query(
|
|
@@ -258,4 +265,11 @@ object MediaLibraryUtils {
|
|
|
258
265
|
*/
|
|
259
266
|
fun hasManifestPermission(context: Context, permission: String): Boolean =
|
|
260
267
|
getManifestPermissions(context).contains(permission)
|
|
268
|
+
|
|
269
|
+
suspend fun scanFile(context: Context, paths: Array<String>, mimeTypes: Array<String>?) =
|
|
270
|
+
suspendCoroutine { complete ->
|
|
271
|
+
MediaScannerConnection.scanFile(context, paths, mimeTypes) { path: String, uri: Uri? ->
|
|
272
|
+
complete.resume(Pair(path, uri))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
261
275
|
}
|