expo-clipboard 2.0.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +31 -4
  2. package/README.md +1 -1
  3. package/android/build.gradle +47 -28
  4. package/android/src/main/AndroidManifest.xml +12 -1
  5. package/android/src/main/java/expo/modules/clipboard/ClipboardExceptions.kt +23 -0
  6. package/android/src/main/java/expo/modules/clipboard/ClipboardFileProvider.kt +360 -0
  7. package/android/src/main/java/expo/modules/clipboard/ClipboardImage.kt +163 -0
  8. package/android/src/main/java/expo/modules/clipboard/ClipboardModule.kt +246 -24
  9. package/android/src/main/java/expo/modules/clipboard/ClipboardOptions.kt +44 -0
  10. package/android/src/main/res/xml/clipboard_provider_paths.xml +5 -0
  11. package/build/Clipboard.d.ts +98 -7
  12. package/build/Clipboard.d.ts.map +1 -0
  13. package/build/Clipboard.js +150 -11
  14. package/build/Clipboard.js.map +1 -1
  15. package/build/Clipboard.types.d.ts +74 -0
  16. package/build/Clipboard.types.d.ts.map +1 -0
  17. package/build/Clipboard.types.js +22 -0
  18. package/build/Clipboard.types.js.map +1 -0
  19. package/build/ExpoClipboard.d.ts +1 -0
  20. package/build/ExpoClipboard.d.ts.map +1 -0
  21. package/build/ExpoClipboard.web.d.ts +2 -8
  22. package/build/ExpoClipboard.web.d.ts.map +1 -0
  23. package/build/ExpoClipboard.web.js +1 -38
  24. package/build/ExpoClipboard.web.js.map +1 -1
  25. package/build/web/ClipboardModule.d.ts +15 -0
  26. package/build/web/ClipboardModule.d.ts.map +1 -0
  27. package/build/web/ClipboardModule.js +177 -0
  28. package/build/web/ClipboardModule.js.map +1 -0
  29. package/build/web/Exceptions.d.ts +14 -0
  30. package/build/web/Exceptions.d.ts.map +1 -0
  31. package/build/web/Exceptions.js +22 -0
  32. package/build/web/Exceptions.js.map +1 -0
  33. package/build/web/Utils.d.ts +18 -0
  34. package/build/web/Utils.d.ts.map +1 -0
  35. package/build/web/Utils.js +79 -0
  36. package/build/web/Utils.js.map +1 -0
  37. package/expo-module.config.json +10 -0
  38. package/ios/ClipboardExceptions.swift +15 -0
  39. package/ios/ClipboardModule.swift +136 -0
  40. package/ios/ClipboardOptions.swift +40 -0
  41. package/ios/{EXClipboard.podspec → ExpoClipboard.podspec} +10 -6
  42. package/ios/NSAttributedString+utilities.swift +34 -0
  43. package/ios/UIPasteboard+html.swift +47 -0
  44. package/package.json +6 -5
  45. package/src/Clipboard.ts +176 -12
  46. package/src/Clipboard.types.ts +80 -0
  47. package/src/ExpoClipboard.web.ts +1 -35
  48. package/src/web/ClipboardModule.ts +198 -0
  49. package/src/web/Exceptions.ts +25 -0
  50. package/src/web/Utils.ts +87 -0
  51. package/android/src/main/java/expo/modules/clipboard/ClipboardEventEmitter.kt +0 -49
  52. package/android/src/main/java/expo/modules/clipboard/ClipboardPackage.kt +0 -12
  53. package/ios/EXClipboard/EXClipboardModule.h +0 -8
  54. package/ios/EXClipboard/EXClipboardModule.m +0 -88
  55. package/unimodule.json +0 -4
package/CHANGELOG.md CHANGED
@@ -10,13 +10,40 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
- ## 2.0.32021-10-21
13
+ ## 3.0.02022-04-18
14
14
 
15
- _This version does not introduce any user-facing changes._
15
+ ### 🛠 Breaking changes
16
16
 
17
- ## 2.0.2 2021-10-15
17
+ - The `content` property of the clipboard event listener is now deprecated and always returns empty string and logs a warning message to the console. Use `getStringAsync()` instead.
18
18
 
19
- _This version does not introduce any user-facing changes._
19
+ ### 🎉 New features
20
+
21
+ - Native module on Android is now written in Kotlin using the new API. ([#16269](https://github.com/expo/expo/pull/16269) by [@barthap](https://github.com/barthap))
22
+ - Added support for setting and getting images (`setImageAsync`, `hasImageAsync`, `getImageAsync`). ([#16391](https://github.com/expo/expo/pull/16391), [#16413](https://github.com/expo/expo/pull/16413), [#16481](https://github.com/expo/expo/pull/16481) by [@barthap](https://github.com/barthap))
23
+ - On iOS added support for setting and getting URLs (`setUrlAsync`, `hasUrlAsync`, `getUrlAsync`). ([#16391](https://github.com/expo/expo/pull/16391) by [@graszka22](https://github.com/graszka22), [@barthap](https://github.com/barthap))
24
+ - Added new method `hasStringAsync` that checks whether clipboard has text content. ([#16524](https://github.com/expo/expo/pull/16524) by [@barthap](https://github.com/barthap))
25
+ - Added support for HTML content in `getStringAsync` and `setStringAsync`. ([#16551](https://github.com/expo/expo/pull/16551), [#16687](https://github.com/expo/expo/pull/16687) by [@barthap](https://github.com/barthap))
26
+ - Added new property `contentTypes` to the clipboard event listener describing contents of the clipboard. ([#16787](https://github.com/expo/expo/pull/16787) by [@barthap](https://github.com/barthap))
27
+
28
+ ### ⚠ Notices
29
+
30
+ - Deprecated `setString`. Use `setStringAsync` instead. ([#16320](https://github.com/expo/expo/pull/16320) by [@barthap](https://github.com/barthap))
31
+
32
+ ### ⚠️ Notices
33
+
34
+ - On Android bump `compileSdkVersion` to `31`, `targetSdkVersion` to `31` and `Java` version to `11`. ([#16941](https://github.com/expo/expo/pull/16941) by [@bbarthec](https://github.com/bbarthec))
35
+
36
+ ## 2.1.1 - 2022-02-01
37
+
38
+ ### 🐛 Bug fixes
39
+
40
+ - Fix `Plugin with id 'maven' not found` build error from Android Gradle 7. ([#16080](https://github.com/expo/expo/pull/16080) by [@kudo](https://github.com/kudo))
41
+
42
+ ## 2.1.0 — 2021-12-03
43
+
44
+ ### 🎉 New features
45
+
46
+ - Native module on iOS is now written in Swift using [Sweet API](https://blog.expo.dev/a-peek-into-the-upcoming-sweet-expo-module-api-6de6b9aca492). ([#14959](https://github.com/expo/expo/pull/14959) by [@tsapeta](https://github.com/tsapeta))
20
47
 
21
48
  ## 2.0.1 — 2021-10-01
22
49
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## API documentation
6
6
 
7
- Please refer to the [API documentation for the latest stable release](https://docs.expo.io/versions/latest/sdk/clipboard/).
7
+ Please refer to the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/clipboard/).
8
8
 
9
9
  ## Installation in managed Expo projects
10
10
 
@@ -1,63 +1,80 @@
1
1
  apply plugin: 'com.android.library'
2
2
  apply plugin: 'kotlin-android'
3
- apply plugin: 'maven'
3
+ apply plugin: 'maven-publish'
4
4
 
5
5
  group = 'host.exp.exponent'
6
- version = '2.0.3'
6
+ version = '3.0.0'
7
7
 
8
8
  buildscript {
9
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10
+ if (expoModulesCorePlugin.exists()) {
11
+ apply from: expoModulesCorePlugin
12
+ applyKotlinExpoModulesCorePlugin()
13
+ }
14
+
9
15
  // Simple helper that allows the root project to override versions declared by this library.
10
16
  ext.safeExtGet = { prop, fallback ->
11
17
  rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
12
18
  }
13
19
 
20
+ // Ensures backward compatibility
21
+ ext.getKotlinVersion = {
22
+ if (ext.has("kotlinVersion")) {
23
+ ext.kotlinVersion()
24
+ } else {
25
+ ext.safeExtGet("kotlinVersion", "1.6.10")
26
+ }
27
+ }
28
+
14
29
  repositories {
15
30
  mavenCentral()
16
31
  }
17
32
 
18
33
  dependencies {
19
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.4.21')}")
34
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
20
35
  }
21
36
  }
22
37
 
23
- // Upload android library to maven with javadoc and android sources
24
- configurations {
25
- deployerJars
26
- }
27
-
28
38
  // Creating sources with comments
29
39
  task androidSourcesJar(type: Jar) {
30
40
  classifier = 'sources'
31
41
  from android.sourceSets.main.java.srcDirs
32
42
  }
33
43
 
34
- // Put the androidSources and javadoc to the artifacts
35
- artifacts {
36
- archives androidSourcesJar
37
- }
38
-
39
- uploadArchives {
40
- repositories {
41
- mavenDeployer {
42
- configuration = configurations.deployerJars
43
- repository(url: mavenLocal().url)
44
+ afterEvaluate {
45
+ publishing {
46
+ publications {
47
+ release(MavenPublication) {
48
+ from components.release
49
+ // Add additional sourcesJar to artifacts
50
+ artifact(androidSourcesJar)
51
+ }
52
+ }
53
+ repositories {
54
+ maven {
55
+ url = mavenLocal().url
56
+ }
44
57
  }
45
58
  }
46
59
  }
47
60
 
48
61
  android {
49
- compileSdkVersion rootProject.ext.compileSdkVersion
62
+ compileSdkVersion safeExtGet("compileSdkVersion", 31)
50
63
 
51
64
  compileOptions {
52
- sourceCompatibility JavaVersion.VERSION_1_8
53
- targetCompatibility JavaVersion.VERSION_1_8
65
+ sourceCompatibility JavaVersion.VERSION_11
66
+ targetCompatibility JavaVersion.VERSION_11
67
+ }
68
+
69
+ kotlinOptions {
70
+ jvmTarget = JavaVersion.VERSION_11.majorVersion
54
71
  }
55
72
 
56
73
  defaultConfig {
57
- minSdkVersion rootProject.ext.minSdkVersion
58
- targetSdkVersion rootProject.ext.targetSdkVersion
74
+ minSdkVersion safeExtGet("minSdkVersion", 21)
75
+ targetSdkVersion safeExtGet("targetSdkVersion", 31)
59
76
  versionCode 3
60
- versionName '2.0.3'
77
+ versionName '3.0.0'
61
78
  }
62
79
  lintOptions {
63
80
  abortOnError false
@@ -70,10 +87,12 @@ repositories {
70
87
 
71
88
  dependencies {
72
89
  implementation project(':expo-modules-core')
73
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${safeExtGet('kotlinVersion', '1.4.21')}"
90
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
91
+ implementation "androidx.core:core-ktx:1.6.0"
92
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
93
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
74
94
 
75
- if (project.findProject(':unimodules-test-core')) {
76
- testImplementation project(':unimodules-test-core')
95
+ if (project.findProject(':expo-modules-test-core')) {
96
+ testImplementation project(':expo-modules-test-core')
77
97
  }
78
- testImplementation "org.robolectric:robolectric:4.3.1"
79
98
  }
@@ -1,2 +1,13 @@
1
- <manifest package="expo.modules.clipboard">
1
+ <manifest package="expo.modules.clipboard"
2
+ xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <application>
4
+ <provider
5
+ android:name=".ClipboardFileProvider"
6
+ android:authorities="${applicationId}.ClipboardFileProvider"
7
+ android:exported="true">
8
+ <meta-data
9
+ android:name="expo.modules.clipboard.CLIPBOARD_FILE_PROVIDER_PATHS"
10
+ android:resource="@xml/clipboard_provider_paths"/>
11
+ </provider>
12
+ </application>
2
13
  </manifest>
@@ -0,0 +1,23 @@
1
+ package expo.modules.clipboard
2
+
3
+ import expo.modules.kotlin.exception.CodedException
4
+
5
+ internal class ClipboardUnavailableException :
6
+ CodedException("'CLIPBOARD_SERVICE' is unavailable on this device", null)
7
+
8
+ internal class NoPermissionException(cause: SecurityException?) :
9
+ CodedException("App has no permission to read this clipboard item", cause)
10
+
11
+ internal class PasteFailureException(cause: Throwable?, kind: String = "item") :
12
+ CodedException("Failed to get $kind from clipboard", cause)
13
+
14
+ internal class CopyFailureException(cause: Throwable?, kind: String = "item") :
15
+ CodedException("Failed to save $kind into clipboard", cause)
16
+
17
+ internal class InvalidImageException(image: String, cause: Throwable?) :
18
+ CodedException(
19
+ "Invalid base64 image: ${
20
+ image.run { substring(0, minOf(length, 32)) + if (length > 32) "..." else ""}
21
+ }",
22
+ cause
23
+ )
@@ -0,0 +1,360 @@
1
+ package expo.modules.clipboard
2
+
3
+ import android.content.ContentProvider
4
+ import android.content.ContentValues
5
+ import android.content.Context
6
+ import android.content.pm.PackageManager
7
+ import android.content.pm.ProviderInfo
8
+ import android.database.Cursor
9
+ import android.database.MatrixCursor
10
+ import android.net.Uri
11
+ import android.os.Build
12
+ import android.os.Environment
13
+ import android.os.ParcelFileDescriptor
14
+ import android.provider.OpenableColumns
15
+ import android.text.TextUtils
16
+ import android.webkit.MimeTypeMap
17
+ import androidx.annotation.RequiresApi
18
+ import androidx.core.content.FileProvider
19
+ import org.xmlpull.v1.XmlPullParser.END_DOCUMENT
20
+ import org.xmlpull.v1.XmlPullParser.START_TAG
21
+ import org.xmlpull.v1.XmlPullParserException
22
+ import java.io.File
23
+ import java.io.FileNotFoundException
24
+ import java.io.IOException
25
+
26
+ /**
27
+ * This is a modified version of [FileProvider]
28
+ * that facilitates exposing files associated with an app by creating
29
+ * a `content://` uri without using Androids URI permission mechanism.
30
+ * In contrast to [FileProvider], this provider _must_ be exported.
31
+ *
32
+ * The difference is that [FileProvider] forbids provider to be _exported_
33
+ * which means it cannot easily grant access to any app installed
34
+ * on the device. This becomes even more problematic with API 31, when
35
+ * [PackageManager.getInstalledApplications] doesn't return all
36
+ * installed apps, so we cannot iterate and use [Context.grantUriPermission]
37
+ * easily
38
+ *
39
+ * For usage details, see [FileProvider] documentation
40
+ */
41
+ @RequiresApi(Build.VERSION_CODES.KITKAT)
42
+ class ClipboardFileProvider : ContentProvider() {
43
+ private val defaultProjectionColumns = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
44
+
45
+ private lateinit var strategy: PathStrategy
46
+
47
+ override fun onCreate() = true
48
+
49
+ /**
50
+ * After the [ClipboardFileProvider] is instantiated, this method is called to provide the system with
51
+ * information about the provider.
52
+ *
53
+ * @param context A [Context] for the current component.
54
+ * @param info A [ProviderInfo] for the new provider.
55
+ */
56
+ override fun attachInfo(context: Context, info: ProviderInfo) {
57
+ super.attachInfo(context, info)
58
+
59
+ if (!info.exported) {
60
+ throw AssertionError("ClipboardFileProvider must be exported")
61
+ }
62
+
63
+ strategy = getPathStrategy(context, info.authority)
64
+ }
65
+
66
+ /**
67
+ * Returns the MIME type of a content URI returned by
68
+ * [getUriForFile()][getUriForFile].
69
+ *
70
+ * @param uri A content URI returned by
71
+ * [getUriForFile()][getUriForFile].
72
+ * @return If the associated file has an extension, the MIME type associated with that
73
+ * extension; otherwise `application/octet-stream`.
74
+ */
75
+ override fun getType(uri: Uri): String {
76
+ val file: File = strategy.getFileForUri(uri)
77
+ val lastDot = file.name.lastIndexOf('.')
78
+ if (lastDot >= 0) {
79
+ val extension = file.name.substring(lastDot + 1)
80
+ MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)?.let {
81
+ return it
82
+ }
83
+ }
84
+ return "application/octet-stream"
85
+ }
86
+
87
+ override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
88
+ val projection = projection ?: defaultProjectionColumns
89
+ val file: File = strategy.getFileForUri(uri)
90
+ var columns = arrayOfNulls<String>(projection.size)
91
+ var values = arrayOfNulls<Any>(projection.size)
92
+ var i = 0
93
+ for (column in projection) {
94
+ when (column) {
95
+ OpenableColumns.DISPLAY_NAME -> {
96
+ columns[i] = OpenableColumns.DISPLAY_NAME
97
+ values[i++] = file.name
98
+ }
99
+ OpenableColumns.SIZE -> {
100
+ columns[i] = OpenableColumns.SIZE
101
+ values[i++] = file.length()
102
+ }
103
+ }
104
+ }
105
+ columns = columns.copyOf(i)
106
+ values = values.copyOf(i)
107
+ return MatrixCursor(columns, 1).apply {
108
+ addRow(values)
109
+ }
110
+ }
111
+
112
+ override fun insert(uri: Uri, values: ContentValues?): Uri =
113
+ throw UnsupportedOperationException("This is a read-only provider")
114
+
115
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int =
116
+ throw UnsupportedOperationException("This is a read-only provider")
117
+
118
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
119
+ throw UnsupportedOperationException("This is a read-only provider")
120
+
121
+ @Throws(FileNotFoundException::class)
122
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
123
+ require("r" == mode) { "mode must be \"r\"" }
124
+ val file = strategy.getFileForUri(uri)
125
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
126
+ }
127
+
128
+ companion object {
129
+ private const val META_DATA_FILE_PROVIDER_PATHS =
130
+ "expo.modules.clipboard.CLIPBOARD_FILE_PROVIDER_PATHS"
131
+
132
+ private const val TAG_ROOT_PATH = "root-path"
133
+ private const val TAG_FILES_PATH = "files-path"
134
+ private const val TAG_CACHE_PATH = "cache-path"
135
+ private const val TAG_EXTERNAL = "external-path"
136
+ private const val TAG_EXTERNAL_FILES = "external-files-path"
137
+ private const val TAG_EXTERNAL_CACHE = "external-cache-path"
138
+
139
+ private const val ATTR_NAME = "name"
140
+ private const val ATTR_PATH = "path"
141
+
142
+ private val DEVICE_ROOT = File("/")
143
+
144
+ private val cache: HashMap<String, PathStrategy> = HashMap()
145
+
146
+ /**
147
+ * Return a content URI for a given [File]. A PublicFileProvider can only return a
148
+ * `content` [Uri] for file paths defined in their `<paths>`
149
+ * meta-data element. See the Class Overview for more information.
150
+ *
151
+ * @param context A [Context] for the current component.
152
+ * @param authority The authority of a [ClipboardFileProvider] defined in a
153
+ * `<provider>` element in your app's manifest.
154
+ * @param file A [File] pointing to the filename for which you want a
155
+ * `content` [Uri].
156
+ * @return A content URI for the file.
157
+ * @throws IllegalArgumentException When the given [File] is outside
158
+ * the paths supported by the provider.
159
+ */
160
+ fun getUriForFile(context: Context, authority: String, file: File): Uri? {
161
+ val strategy = getPathStrategy(context, authority)
162
+ return strategy.getUriForFile(file)
163
+ }
164
+
165
+ /**
166
+ * Return {@link PathStrategy} for given authority, either by parsing or
167
+ * returning from cache.
168
+ */
169
+ internal fun getPathStrategy(context: Context, authority: String): PathStrategy {
170
+ var pathStrategy: PathStrategy
171
+ synchronized(cache) {
172
+ pathStrategy = cache[authority] ?: run {
173
+ try {
174
+ pathStrategy = parsePathStrategy(context, authority)
175
+ } catch (e: IOException) {
176
+ throw IllegalArgumentException(
177
+ "Failed to parse $META_DATA_FILE_PROVIDER_PATHS meta-data", e
178
+ )
179
+ } catch (e: XmlPullParserException) {
180
+ throw IllegalArgumentException(
181
+ "Failed to parse $META_DATA_FILE_PROVIDER_PATHS meta-data", e
182
+ )
183
+ }
184
+ cache[authority] = pathStrategy
185
+ pathStrategy
186
+ }
187
+ }
188
+ return pathStrategy
189
+ }
190
+
191
+ /**
192
+ * Parse and return [PathStrategy] for given authority as defined in
193
+ * [META_DATA_FILE_PROVIDER_PATHS] `<meta-data>`.
194
+ *
195
+ * @see .getPathStrategy
196
+ */
197
+ @Throws(IOException::class, XmlPullParserException::class)
198
+ private fun parsePathStrategy(context: Context, authority: String): PathStrategy {
199
+ val pathStrategy = SimplePathStrategy(authority)
200
+ val packageManager = context.packageManager
201
+ val info = packageManager.resolveContentProvider(authority, PackageManager.GET_META_DATA)
202
+ ?: throw IllegalArgumentException("Couldn't find meta-data for provider with authority $authority")
203
+ val parser = info.loadXmlMetaData(packageManager, META_DATA_FILE_PROVIDER_PATHS)
204
+ ?: throw IllegalArgumentException("Missing $META_DATA_FILE_PROVIDER_PATHS meta-data")
205
+ var type: Int
206
+ while (parser.next().also { type = it } != END_DOCUMENT) {
207
+ if (type != START_TAG) continue
208
+
209
+ val tag = parser.name
210
+ val target: File? = targetFileFromTag(tag, context)
211
+ target?.let {
212
+ val name = parser.getAttributeValue(null, ATTR_NAME)
213
+ val path = parser.getAttributeValue(null, ATTR_PATH)
214
+ pathStrategy.addRoot(name, buildPath(it, path))
215
+ }
216
+ }
217
+ return pathStrategy
218
+ }
219
+
220
+ private fun targetFileFromTag(tag: String, context: Context): File? = when (tag) {
221
+ TAG_ROOT_PATH -> DEVICE_ROOT
222
+ TAG_FILES_PATH -> context.filesDir
223
+ TAG_CACHE_PATH -> context.cacheDir
224
+ TAG_EXTERNAL -> Environment.getExternalStorageDirectory()
225
+ TAG_EXTERNAL_FILES -> {
226
+ val externalFilesDirs: Array<File> = context.getExternalFilesDirs(null)
227
+ externalFilesDirs.takeIf { it.isNotEmpty() }?.let { it[0] }
228
+ }
229
+ TAG_EXTERNAL_CACHE -> {
230
+ val externalCacheDirs: Array<File> = context.externalCacheDirs
231
+ externalCacheDirs.takeIf { it.isNotEmpty() }?.let { it[0] }
232
+ }
233
+ else -> null
234
+ }
235
+
236
+ private fun buildPath(base: File, vararg segments: String?): File {
237
+ var cur = base
238
+ for (segment in segments) {
239
+ if (segment != null) {
240
+ cur = File(cur, segment)
241
+ }
242
+ }
243
+ return cur
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Strategy for mapping between [File] and [Uri].
249
+ *
250
+ *
251
+ * Strategies must be symmetric so that mapping a [File] to a
252
+ * [Uri] and then back to a [File] points at the original
253
+ * target.
254
+ *
255
+ *
256
+ * Strategies must remain consistent across app launches, and not rely on
257
+ * dynamic state. This ensures that any generated [Uri] can still be
258
+ * resolved if your process is killed and later restarted.
259
+ *
260
+ * @see SimplePathStrategy
261
+ */
262
+ internal interface PathStrategy {
263
+ /**
264
+ * Return a [Uri] that represents the given [File].
265
+ */
266
+ fun getUriForFile(file: File): Uri?
267
+
268
+ /**
269
+ * Return a [File] that represents the given [Uri].
270
+ */
271
+ fun getFileForUri(uri: Uri): File
272
+ }
273
+
274
+ /**
275
+ * Strategy that provides access to files living under a narrow whitelist of
276
+ * filesystem roots. It will throw [SecurityException] if callers try
277
+ * accessing files outside the configured roots.
278
+ *
279
+ *
280
+ * For example, if configured with
281
+ * `addRoot("myfiles", context.getFilesDir())`, then
282
+ * `context.getFileStreamPath("foo.txt")` would map to
283
+ * `content://myauthority/myfiles/foo.txt`.
284
+ */
285
+ internal class SimplePathStrategy(private val authority: String) : PathStrategy {
286
+ private val roots: HashMap<String, File> = HashMap()
287
+
288
+ /**
289
+ * Add a mapping from a name to a filesystem root. The provider only offers
290
+ * access to files that live under configured roots.
291
+ */
292
+ fun addRoot(name: String?, root: File) {
293
+ require(name != null && !TextUtils.isEmpty(name)) { "Name must not be empty" }
294
+ val newRoot = try {
295
+ // Resolve to canonical path to keep path checking fast
296
+ root.canonicalFile
297
+ } catch (e: IOException) {
298
+ throw IllegalArgumentException("Failed to resolve canonical path for $root", e)
299
+ }
300
+ roots[name] = newRoot
301
+ }
302
+
303
+ override fun getUriForFile(file: File): Uri? {
304
+ var path: String = try {
305
+ file.canonicalPath
306
+ } catch (e: IOException) {
307
+ throw java.lang.IllegalArgumentException("Failed to resolve canonical path for $file")
308
+ }
309
+
310
+ // Find the most-specific root path
311
+ var mostSpecific: Map.Entry<String, File>? = null
312
+ for (root in roots.entries) {
313
+ val rootPath = root.value.path
314
+ if (path.startsWith(rootPath) && (
315
+ mostSpecific == null ||
316
+ rootPath.length > mostSpecific.value.path.length
317
+ )
318
+ ) {
319
+ mostSpecific = root
320
+ }
321
+ }
322
+ requireNotNull(mostSpecific) { "Failed to find configured root that contains $path" }
323
+
324
+ // Start at first char of path under root
325
+ val rootPath = mostSpecific.value.path
326
+ path = if (rootPath.endsWith("/")) {
327
+ path.substring(rootPath.length)
328
+ } else {
329
+ path.substring(rootPath.length + 1)
330
+ }
331
+
332
+ // Encode the tag and path separately
333
+ path = Uri.encode(mostSpecific.key) + '/' + Uri.encode(path, "/")
334
+ return Uri.Builder()
335
+ .scheme("content")
336
+ .authority(authority)
337
+ .encodedPath(path)
338
+ .build()
339
+ }
340
+
341
+ override fun getFileForUri(uri: Uri): File {
342
+ var path = uri.encodedPath!!
343
+ val splitIndex = path.indexOf('/', 1)
344
+ val tag = Uri.decode(path.substring(1, splitIndex))
345
+ path = Uri.decode(path.substring(splitIndex + 1))
346
+ val root = roots[tag]
347
+ ?: throw java.lang.IllegalArgumentException("Unable to find configured root for $uri")
348
+ var file = File(root, path)
349
+ file = try {
350
+ file.canonicalFile
351
+ } catch (e: IOException) {
352
+ throw java.lang.IllegalArgumentException("Failed to resolve canonical path for $file")
353
+ }
354
+ if (!file.path.startsWith(root.path)) {
355
+ throw SecurityException("Resolved path jumped beyond configured root")
356
+ }
357
+ return file
358
+ }
359
+ }
360
+ }