@amazon-devices/expo-file-system 2.0.1758683737

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 (111) hide show
  1. package/.eslintrc.js +2 -0
  2. package/CHANGELOG.md +317 -0
  3. package/CMakeLists.txt +52 -0
  4. package/LICENSE +7 -0
  5. package/README.kepler.md +260 -0
  6. package/README.md +46 -0
  7. package/THIRD_PARTY_NOTICES.txt +24 -0
  8. package/android/build.gradle +118 -0
  9. package/android/src/main/AndroidManifest.xml +17 -0
  10. package/android/src/main/java/expo/modules/filesystem/AppDirectoriesModule.kt +26 -0
  11. package/android/src/main/java/expo/modules/filesystem/CountingRequestBody.kt +52 -0
  12. package/android/src/main/java/expo/modules/filesystem/FilePermissionModule.kt +48 -0
  13. package/android/src/main/java/expo/modules/filesystem/FileSystemExceptions.kt +48 -0
  14. package/android/src/main/java/expo/modules/filesystem/FileSystemFileProvider.kt +5 -0
  15. package/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt +1110 -0
  16. package/android/src/main/java/expo/modules/filesystem/FileSystemPackage.kt +10 -0
  17. package/android/src/main/java/expo/modules/filesystem/FileSystemRecords.kt +92 -0
  18. package/android/src/main/res/xml/file_system_provider_paths.xml +5 -0
  19. package/app.plugin.js +1 -0
  20. package/build/ExponentFileSystem.d.ts +4 -0
  21. package/build/ExponentFileSystem.d.ts.map +1 -0
  22. package/build/ExponentFileSystem.js +5 -0
  23. package/build/ExponentFileSystem.js.map +1 -0
  24. package/build/ExponentFileSystem.kepler.d.ts +4 -0
  25. package/build/ExponentFileSystem.kepler.d.ts.map +1 -0
  26. package/build/ExponentFileSystem.kepler.js +112 -0
  27. package/build/ExponentFileSystem.kepler.js.map +1 -0
  28. package/build/ExponentFileSystem.web.d.ts +3 -0
  29. package/build/ExponentFileSystem.web.d.ts.map +1 -0
  30. package/build/ExponentFileSystem.web.js +3 -0
  31. package/build/ExponentFileSystem.web.js.map +1 -0
  32. package/build/ExponentFileSystemShim.d.ts +4 -0
  33. package/build/ExponentFileSystemShim.d.ts.map +1 -0
  34. package/build/ExponentFileSystemShim.js +21 -0
  35. package/build/ExponentFileSystemShim.js.map +1 -0
  36. package/build/FileSystem.d.ts +361 -0
  37. package/build/FileSystem.d.ts.map +1 -0
  38. package/build/FileSystem.js +616 -0
  39. package/build/FileSystem.js.map +1 -0
  40. package/build/FileSystem.types.d.ts +304 -0
  41. package/build/FileSystem.types.d.ts.map +1 -0
  42. package/build/FileSystem.types.js +47 -0
  43. package/build/FileSystem.types.js.map +1 -0
  44. package/build/index.d.ts +3 -0
  45. package/build/index.d.ts.map +1 -0
  46. package/build/index.js +3 -0
  47. package/build/index.js.map +1 -0
  48. package/build/types.d.ts +35 -0
  49. package/build/types.d.ts.map +1 -0
  50. package/build/types.js +2 -0
  51. package/build/types.js.map +1 -0
  52. package/expo-module.config.json +11 -0
  53. package/ios/EXFilePermissionModule.h +16 -0
  54. package/ios/EXFilePermissionModule.m +68 -0
  55. package/ios/EXFileSystem.h +25 -0
  56. package/ios/EXFileSystem.m +1084 -0
  57. package/ios/EXFileSystemAssetLibraryHandler.h +6 -0
  58. package/ios/EXFileSystemAssetLibraryHandler.m +142 -0
  59. package/ios/EXFileSystemHandler.h +17 -0
  60. package/ios/EXFileSystemLocalFileHandler.h +7 -0
  61. package/ios/EXFileSystemLocalFileHandler.m +79 -0
  62. package/ios/EXSessionTasks/EXSessionCancelableUploadTaskDelegate.h +16 -0
  63. package/ios/EXSessionTasks/EXSessionCancelableUploadTaskDelegate.m +55 -0
  64. package/ios/EXSessionTasks/EXSessionDownloadTaskDelegate.h +13 -0
  65. package/ios/EXSessionTasks/EXSessionDownloadTaskDelegate.m +64 -0
  66. package/ios/EXSessionTasks/EXSessionHandler.h +19 -0
  67. package/ios/EXSessionTasks/EXSessionHandler.m +49 -0
  68. package/ios/EXSessionTasks/EXSessionResumableDownloadTaskDelegate.h +18 -0
  69. package/ios/EXSessionTasks/EXSessionResumableDownloadTaskDelegate.m +66 -0
  70. package/ios/EXSessionTasks/EXSessionTaskDelegate.h +32 -0
  71. package/ios/EXSessionTasks/EXSessionTaskDelegate.m +58 -0
  72. package/ios/EXSessionTasks/EXSessionTaskDispatcher.h +19 -0
  73. package/ios/EXSessionTasks/EXSessionTaskDispatcher.m +96 -0
  74. package/ios/EXSessionTasks/EXSessionUploadTaskDelegate.h +8 -0
  75. package/ios/EXSessionTasks/EXSessionUploadTaskDelegate.m +52 -0
  76. package/ios/EXSessionTasks/EXTaskHandlersManager.h +21 -0
  77. package/ios/EXSessionTasks/EXTaskHandlersManager.m +56 -0
  78. package/ios/Encoding.swift +88 -0
  79. package/ios/ExpoFileSystem.h +1 -0
  80. package/ios/ExpoFileSystem.podspec +32 -0
  81. package/ios/FileSystemBackgroundSessionHandler.swift +25 -0
  82. package/ios/FileSystemExceptions.swift +69 -0
  83. package/ios/FileSystemHelpers.swift +66 -0
  84. package/ios/FileSystemModule.swift +263 -0
  85. package/ios/FileSystemRecords.swift +74 -0
  86. package/ios/NSData+EXFileSystem.h +9 -0
  87. package/ios/NSData+EXFileSystem.m +19 -0
  88. package/ios/NetworkingHelpers.swift +84 -0
  89. package/ios/Tests/EXFileSystemSpec.swift +55 -0
  90. package/jest.config.js +3 -0
  91. package/kepler/ExponentFileSystem.cpp +498 -0
  92. package/kepler/ExponentFileSystem.h +60 -0
  93. package/kepler/KeplerInit.cpp +28 -0
  94. package/kepler/NetworkingRequestHandler.cpp +47 -0
  95. package/kepler/test/FileSystem.cpp +425 -0
  96. package/package.json +1 -0
  97. package/plugin/build/withFileSystem.d.ts +3 -0
  98. package/plugin/build/withFileSystem.js +12 -0
  99. package/plugin/src/withFileSystem.ts +13 -0
  100. package/plugin/tsconfig.json +9 -0
  101. package/plugin/tsconfig.tsbuildinfo +1 -0
  102. package/react-native.config.js +21 -0
  103. package/src/ExponentFileSystem.kepler.ts +167 -0
  104. package/src/ExponentFileSystem.ts +7 -0
  105. package/src/ExponentFileSystem.web.ts +2 -0
  106. package/src/ExponentFileSystemShim.ts +23 -0
  107. package/src/FileSystem.ts +756 -0
  108. package/src/FileSystem.types.ts +339 -0
  109. package/src/index.ts +2 -0
  110. package/src/types.ts +34 -0
  111. package/tsconfig.json +9 -0
@@ -0,0 +1,1110 @@
1
+ package expo.modules.filesystem
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.app.Activity
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.net.Uri
8
+ import android.os.Build
9
+ import android.os.Bundle
10
+ import android.os.Environment
11
+ import android.os.StatFs
12
+ import android.provider.DocumentsContract
13
+ import android.util.Base64
14
+ import android.util.Log
15
+ import androidx.core.content.FileProvider
16
+ import androidx.documentfile.provider.DocumentFile
17
+ import expo.modules.core.errors.ModuleDestroyedException
18
+ import expo.modules.interfaces.filesystem.Permission
19
+ import expo.modules.kotlin.Promise
20
+ import expo.modules.kotlin.exception.Exceptions
21
+ import expo.modules.kotlin.modules.Module
22
+ import expo.modules.kotlin.modules.ModuleDefinition
23
+ import kotlinx.coroutines.CoroutineScope
24
+ import kotlinx.coroutines.Dispatchers
25
+ import kotlinx.coroutines.cancel
26
+ import kotlinx.coroutines.launch
27
+ import kotlinx.coroutines.withContext
28
+ import okhttp3.Call
29
+ import okhttp3.Callback
30
+ import okhttp3.Headers
31
+ import okhttp3.JavaNetCookieJar
32
+ import okhttp3.MediaType
33
+ import okhttp3.MediaType.Companion.toMediaTypeOrNull
34
+ import okhttp3.MultipartBody
35
+ import okhttp3.OkHttpClient
36
+ import okhttp3.Request
37
+ import okhttp3.RequestBody
38
+ import okhttp3.RequestBody.Companion.asRequestBody
39
+ import okhttp3.Response
40
+ import okhttp3.ResponseBody
41
+ import okio.*
42
+ import org.apache.commons.codec.binary.Hex
43
+ import org.apache.commons.codec.digest.DigestUtils
44
+ import org.apache.commons.io.FileUtils
45
+ import org.apache.commons.io.IOUtils
46
+ import java.io.BufferedInputStream
47
+ import java.io.ByteArrayOutputStream
48
+ import java.io.File
49
+ import java.io.FileInputStream
50
+ import java.io.FileNotFoundException
51
+ import java.io.FileOutputStream
52
+ import java.io.IOException
53
+ import java.io.InputStream
54
+ import java.io.OutputStream
55
+ import java.io.OutputStreamWriter
56
+ import java.math.BigInteger
57
+ import java.net.CookieHandler
58
+ import java.net.URLConnection
59
+ import java.util.*
60
+ import java.util.concurrent.TimeUnit
61
+ import java.util.regex.Pattern
62
+ import kotlin.math.pow
63
+
64
+ private val TAG = FileSystemModule::class.java.simpleName
65
+ private const val EXDownloadProgressEventName = "expo-file-system.downloadProgress"
66
+ private const val EXUploadProgressEventName = "expo-file-system.uploadProgress"
67
+ private const val MIN_EVENT_DT_MS: Long = 100
68
+ private const val DIR_PERMISSIONS_REQUEST_CODE = 5394
69
+
70
+ private fun slashifyFilePath(path: String?): String? {
71
+ return if (path == null) {
72
+ null
73
+ } else if (path.startsWith("file:///")) {
74
+ path
75
+ } else {
76
+ // Ensure leading schema with a triple slash
77
+ Pattern.compile("^file:/*").matcher(path).replaceAll("file:///")
78
+ }
79
+ }
80
+
81
+ // The class needs to be 'open', because it's inherited in expoview
82
+ open class FileSystemModule : Module() {
83
+ private val context: Context
84
+ get() = appContext.reactContext ?: throw Exceptions.AppContextLost()
85
+ private var client: OkHttpClient? = null
86
+ private var dirPermissionsRequest: Promise? = null
87
+ private val taskHandlers: MutableMap<String, TaskHandler> = HashMap()
88
+ private val moduleCoroutineScope = CoroutineScope(Dispatchers.Default)
89
+
90
+ @SuppressLint("WrongConstant", "DiscouragedApi")
91
+ override fun definition() = ModuleDefinition {
92
+ Name("ExponentFileSystem")
93
+
94
+ Constants(
95
+ "documentDirectory" to Uri.fromFile(context.filesDir).toString() + "/",
96
+ "cacheDirectory" to Uri.fromFile(context.cacheDir).toString() + "/",
97
+ "bundleDirectory" to "asset:///"
98
+ )
99
+
100
+ Events(
101
+ EXDownloadProgressEventName,
102
+ EXUploadProgressEventName
103
+ )
104
+
105
+ OnCreate {
106
+ try {
107
+ ensureDirExists(context.filesDir)
108
+ ensureDirExists(context.cacheDir)
109
+ } catch (e: Exception) {
110
+ e.printStackTrace()
111
+ }
112
+ }
113
+
114
+ AsyncFunction("getInfoAsync") { _uriStr: String, options: InfoOptions ->
115
+ var uriStr = slashifyFilePath(_uriStr)
116
+
117
+ val uri = Uri.parse(uriStr)
118
+ var absoluteUri = uri
119
+
120
+ if (uri.scheme == "file") {
121
+ uriStr = parseFileUri(uriStr as String)
122
+ absoluteUri = Uri.parse(uriStr)
123
+ }
124
+ ensurePermission(absoluteUri, Permission.READ)
125
+
126
+ if (uri.scheme == "file") {
127
+ val file = absoluteUri.toFile()
128
+ if (file.exists()) {
129
+ return@AsyncFunction Bundle().apply {
130
+ putBoolean("exists", true)
131
+ putBoolean("isDirectory", file.isDirectory)
132
+ putString("uri", Uri.fromFile(file).toString())
133
+ putDouble("size", getFileSize(file).toDouble())
134
+ putDouble("modificationTime", 0.001 * file.lastModified())
135
+ options.md5.takeIf { it == true }?.let { putString("md5", md5(file)) }
136
+ }
137
+ } else {
138
+ return@AsyncFunction Bundle().apply {
139
+ putBoolean("exists", false)
140
+ putBoolean("isDirectory", false)
141
+ }
142
+ }
143
+ } else if (uri.scheme == "content" || uri.scheme == "asset" || uri.scheme == null) {
144
+ try {
145
+ val inputStream: InputStream = when (uri.scheme) {
146
+ "content" -> context.contentResolver.openInputStream(uri)
147
+ "asset" -> openAssetInputStream(uri)
148
+ else -> openResourceInputStream(uriStr)
149
+ } ?: throw FileNotFoundException()
150
+
151
+ return@AsyncFunction Bundle().apply {
152
+ putBoolean("exists", true)
153
+ putBoolean("isDirectory", false)
154
+ putString("uri", uri.toString())
155
+ // NOTE: `.available()` is supposedly not a reliable source of size info, but it's been
156
+ // more reliable than querying `OpenableColumns.SIZE` in practice in tests ¯\_(ツ)_/¯
157
+ putDouble("size", inputStream.available().toDouble())
158
+ if (options.md5 == true) {
159
+ val md5bytes = DigestUtils.md5(inputStream)
160
+ putString("md5", String(Hex.encodeHex(md5bytes)))
161
+ }
162
+ }
163
+ } catch (e: FileNotFoundException) {
164
+ return@AsyncFunction Bundle().apply {
165
+ putBoolean("exists", false)
166
+ putBoolean("isDirectory", false)
167
+ }
168
+ }
169
+ }
170
+
171
+ throw IOException("Unsupported scheme for location '$uri'.")
172
+ }
173
+
174
+ AsyncFunction("readAsStringAsync") { uriStr: String, options: ReadingOptions ->
175
+ val uri = Uri.parse(slashifyFilePath(uriStr))
176
+ ensurePermission(uri, Permission.READ)
177
+
178
+ // TODO:Bacon: Add more encoding types to match iOS
179
+ val encoding = options.encoding
180
+ var contents: String?
181
+ if (encoding == EncodingType.BASE64) {
182
+ getInputStream(uri).use { inputStream ->
183
+ contents = if (options.length != null && options.position != null) {
184
+ val buffer = ByteArray(options.length)
185
+ inputStream.skip(options.position.toLong())
186
+ val bytesRead = inputStream.read(buffer, 0, options.length)
187
+ Base64.encodeToString(buffer, 0, bytesRead, Base64.NO_WRAP)
188
+ } else {
189
+ val inputData = getInputStreamBytes(inputStream)
190
+ Base64.encodeToString(inputData, Base64.NO_WRAP)
191
+ }
192
+ }
193
+ } else {
194
+ contents = when {
195
+ uri.scheme == "file" -> IOUtils.toString(FileInputStream(uri.toFile()))
196
+ uri.scheme == "asset" -> IOUtils.toString(openAssetInputStream(uri))
197
+ uri.scheme == null -> IOUtils.toString(openResourceInputStream(uriStr))
198
+ uri.isSAFUri -> IOUtils.toString(context.contentResolver.openInputStream(uri))
199
+ else -> throw IOException("Unsupported scheme for location '$uri'.")
200
+ }
201
+ }
202
+ return@AsyncFunction contents
203
+ }
204
+
205
+ AsyncFunction("writeAsStringAsync") { uriStr: String, contents: String, options: WritingOptions ->
206
+ val uri = Uri.parse(slashifyFilePath(uriStr))
207
+ ensurePermission(uri, Permission.WRITE)
208
+ val encoding = options.encoding
209
+ getOutputStream(uri).use { out ->
210
+ if (encoding == EncodingType.BASE64) {
211
+ val bytes = Base64.decode(contents, Base64.DEFAULT)
212
+ out.write(bytes)
213
+ } else {
214
+ OutputStreamWriter(out).use { writer -> writer.write(contents) }
215
+ }
216
+ }
217
+ }
218
+
219
+ AsyncFunction("deleteAsync") { uriStr: String, options: DeletingOptions ->
220
+ val uri = Uri.parse(slashifyFilePath(uriStr))
221
+ val appendedUri = Uri.withAppendedPath(uri, "..")
222
+ ensurePermission(appendedUri, Permission.WRITE, "Location '$uri' isn't deletable.")
223
+
224
+ if (uri.scheme == "file") {
225
+ val file = uri.toFile()
226
+ if (file.exists()) {
227
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
228
+ FileUtils.forceDelete(file)
229
+ } else {
230
+ // to be removed once Android SDK 25 support is dropped
231
+ forceDelete(file)
232
+ }
233
+ return@AsyncFunction
234
+ } else {
235
+ if (options.idempotent) {
236
+ return@AsyncFunction
237
+ } else {
238
+ throw FileSystemFileNotFoundException(uri)
239
+ }
240
+ }
241
+ } else if (uri.isSAFUri) {
242
+ val file = getNearestSAFFile(uri)
243
+ if (file != null && file.exists()) {
244
+ file.delete()
245
+ return@AsyncFunction
246
+ } else {
247
+ if (options.idempotent) {
248
+ return@AsyncFunction
249
+ } else {
250
+ throw FileSystemFileNotFoundException(uri)
251
+ }
252
+ }
253
+ } else {
254
+ throw IOException("Unsupported scheme for location '$uri'.")
255
+ }
256
+ }
257
+
258
+ AsyncFunction("moveAsync") { options: RelocatingOptions ->
259
+ val fromUri = Uri.parse(slashifyFilePath(options.from))
260
+ ensurePermission(Uri.withAppendedPath(fromUri, ".."), Permission.WRITE, "Location '$fromUri' isn't movable.")
261
+ val toUri = Uri.parse(slashifyFilePath(options.to))
262
+ ensurePermission(toUri, Permission.WRITE)
263
+
264
+ if (fromUri.scheme == "file") {
265
+ val from = fromUri.toFile()
266
+ val to = toUri.toFile()
267
+ if (from.renameTo(to)) {
268
+ return@AsyncFunction
269
+ } else {
270
+ throw FileSystemCannotMoveFileException(fromUri, toUri)
271
+ }
272
+ } else if (fromUri.isSAFUri) {
273
+ val documentFile = getNearestSAFFile(fromUri)
274
+ if (documentFile == null || !documentFile.exists()) {
275
+ throw FileSystemCannotMoveFileException(fromUri, toUri)
276
+ }
277
+
278
+ val output = toUri.toFile()
279
+ transformFilesFromSAF(documentFile, output, false)
280
+ } else {
281
+ throw IOException("Unsupported scheme for location '$fromUri'.")
282
+ }
283
+ }
284
+
285
+ AsyncFunction("copyAsync") { options: RelocatingOptions ->
286
+ val fromUri = Uri.parse(slashifyFilePath(options.from))
287
+ ensurePermission(fromUri, Permission.READ, "Location '$fromUri' isn't readable.")
288
+ val toUri = Uri.parse(slashifyFilePath(options.to))
289
+ ensurePermission(toUri, Permission.WRITE)
290
+
291
+ when {
292
+ fromUri.scheme == "file" -> {
293
+ val from = fromUri.toFile()
294
+ val to = toUri.toFile()
295
+ if (from.isDirectory) {
296
+ FileUtils.copyDirectory(from, to)
297
+ } else {
298
+ FileUtils.copyFile(from, to)
299
+ }
300
+ }
301
+
302
+ fromUri.isSAFUri -> {
303
+ val documentFile = getNearestSAFFile(fromUri)
304
+ if (documentFile == null || !documentFile.exists()) {
305
+ throw FileSystemCopyFailedException(fromUri)
306
+ }
307
+ val output = toUri.toFile()
308
+ transformFilesFromSAF(documentFile, output, true)
309
+ }
310
+
311
+ fromUri.scheme == "content" -> {
312
+ val inputStream = context.contentResolver.openInputStream(fromUri)
313
+ val out: OutputStream = FileOutputStream(toUri.toFile())
314
+ IOUtils.copy(inputStream, out)
315
+ }
316
+
317
+ fromUri.scheme == "asset" -> {
318
+ val inputStream = openAssetInputStream(fromUri)
319
+ val out: OutputStream = FileOutputStream(toUri.toFile())
320
+ IOUtils.copy(inputStream, out)
321
+ }
322
+
323
+ fromUri.scheme == null -> {
324
+ // this is probably an asset embedded by the packager in resources
325
+ val inputStream = openResourceInputStream(options.from)
326
+ val out: OutputStream = FileOutputStream(toUri.toFile())
327
+ IOUtils.copy(inputStream, out)
328
+ }
329
+
330
+ else -> {
331
+ throw IOException("Unsupported scheme for location '$fromUri'.")
332
+ }
333
+ }
334
+ }
335
+
336
+ AsyncFunction("makeDirectoryAsync") { uriStr: String, options: MakeDirectoryOptions ->
337
+ val uri = Uri.parse(slashifyFilePath(uriStr))
338
+ ensurePermission(uri, Permission.WRITE)
339
+ if (uri.scheme == "file") {
340
+ val file = uri.toFile()
341
+ val previouslyCreated = file.isDirectory
342
+ val setIntermediates = options.intermediates
343
+ val success = if (setIntermediates) file.mkdirs() else file.mkdir()
344
+ if (success || setIntermediates && previouslyCreated) {
345
+ return@AsyncFunction
346
+ } else {
347
+ throw FileSystemCannotCreateDirectoryException(uri)
348
+ }
349
+ } else {
350
+ throw IOException("Unsupported scheme for location '$uri'.")
351
+ }
352
+ }
353
+
354
+ AsyncFunction("readDirectoryAsync") { uriStr: String? ->
355
+ val uri = Uri.parse(slashifyFilePath(uriStr))
356
+ ensurePermission(uri, Permission.READ)
357
+
358
+ if (uri.scheme == "file") {
359
+ val file = uri.toFile()
360
+ val children = file.listFiles() as Array<File?>?
361
+ ?: throw FileSystemCannotReadDirectoryException(uri)
362
+
363
+ return@AsyncFunction children.map { it?.name }
364
+ } else if (uri.isSAFUri) {
365
+ throw FileSystemUnsupportedSchemeException()
366
+ } else {
367
+ throw IOException("Unsupported scheme for location '$uri'.")
368
+ }
369
+ }
370
+
371
+ AsyncFunction("getTotalDiskCapacityAsync") {
372
+ val root = StatFs(Environment.getDataDirectory().absolutePath)
373
+ val blockCount = root.blockCountLong
374
+ val blockSize = root.blockSizeLong
375
+ val capacity = BigInteger.valueOf(blockCount).multiply(BigInteger.valueOf(blockSize))
376
+ // cast down to avoid overflow
377
+ return@AsyncFunction capacity.toDouble().coerceAtMost(2.0.pow(53.0) - 1)
378
+ }
379
+
380
+ AsyncFunction("getFreeDiskStorageAsync") {
381
+ val external = StatFs(Environment.getDataDirectory().absolutePath)
382
+ val availableBlocks = external.availableBlocksLong
383
+ val blockSize = external.blockSizeLong
384
+ val storage = BigInteger.valueOf(availableBlocks).multiply(BigInteger.valueOf(blockSize))
385
+ // cast down to avoid overflow
386
+ return@AsyncFunction storage.toDouble().coerceAtMost(2.0.pow(53.0) - 1)
387
+ }
388
+
389
+ AsyncFunction("getContentUriAsync") { uri: String ->
390
+ val fileUri = Uri.parse(slashifyFilePath(uri))
391
+ ensurePermission(fileUri, Permission.WRITE)
392
+ ensurePermission(fileUri, Permission.READ)
393
+ fileUri.checkIfFileDirExists()
394
+
395
+ if (fileUri.scheme == "file") {
396
+ val file = fileUri.toFile()
397
+ return@AsyncFunction contentUriFromFile(file).toString()
398
+ } else {
399
+ throw FileSystemUnreadableDirectoryException(uri)
400
+ }
401
+ }
402
+
403
+ AsyncFunction("readSAFDirectoryAsync") { uriStr: String ->
404
+ val uri = Uri.parse(slashifyFilePath(uriStr))
405
+ ensurePermission(uri, Permission.READ)
406
+
407
+ if (uri.isSAFUri) {
408
+ val file = DocumentFile.fromTreeUri(context, uri)
409
+ if (file == null || !file.exists() || !file.isDirectory) {
410
+ throw FileSystemCannotReadDirectoryException(uri)
411
+ }
412
+ val children = file.listFiles()
413
+ return@AsyncFunction children.map { it.uri.toString() }
414
+ } else {
415
+ throw IOException("The URI '$uri' is not a Storage Access Framework URI. Try using FileSystem.readDirectoryAsync instead.")
416
+ }
417
+ }
418
+
419
+ AsyncFunction("makeSAFDirectoryAsync") { uriStr: String, dirName: String ->
420
+ val uri = Uri.parse(slashifyFilePath(uriStr))
421
+ ensurePermission(uri, Permission.WRITE)
422
+
423
+ if (!uri.isSAFUri) {
424
+ throw IOException("The URI '$uri' is not a Storage Access Framework URI. Try using FileSystem.makeDirectoryAsync instead.")
425
+ }
426
+ val dir = getNearestSAFFile(uri)
427
+ if (dir != null) {
428
+ if (!dir.isDirectory) {
429
+ throw FileSystemCannotCreateDirectoryException(uri)
430
+ }
431
+ }
432
+ val newDir = dirName.let { dir?.createDirectory(it) }
433
+ ?: throw FileSystemCannotCreateDirectoryException(null)
434
+
435
+ return@AsyncFunction newDir.uri.toString()
436
+ }
437
+
438
+ AsyncFunction("createSAFFileAsync") { uriStr: String, fileName: String, mimeType: String ->
439
+ val uri = Uri.parse(slashifyFilePath(uriStr))
440
+ ensurePermission(uri, Permission.WRITE)
441
+ if (uri.isSAFUri) {
442
+ val dir = getNearestSAFFile(uri)
443
+ if (dir == null || !dir.isDirectory) {
444
+ throw FileSystemCannotCreateFileException(uri)
445
+ }
446
+ val newFile = dir.createFile(mimeType, fileName)
447
+ ?: throw FileSystemCannotCreateFileException(null)
448
+
449
+ return@AsyncFunction newFile.uri.toString()
450
+ } else {
451
+ throw IOException("The URI '$uri' is not a Storage Access Framework URI.")
452
+ }
453
+ }
454
+
455
+ AsyncFunction("requestDirectoryPermissionsAsync") { initialFileUrl: String?, promise: Promise ->
456
+ val currentActivity = appContext.currentActivity
457
+ ?: throw Exceptions.MissingActivity()
458
+ if (dirPermissionsRequest != null) {
459
+ throw FileSystemPendingPermissionsRequestException()
460
+ }
461
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
462
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
463
+ initialFileUrl
464
+ ?.let { Uri.parse(slashifyFilePath(it)) }
465
+ ?.let { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it) }
466
+ }
467
+
468
+ dirPermissionsRequest = promise
469
+ currentActivity.startActivityForResult(intent, DIR_PERMISSIONS_REQUEST_CODE)
470
+ }
471
+
472
+ AsyncFunction("uploadAsync") { url: String, fileUriString: String, options: FileSystemUploadOptions, promise: Promise ->
473
+ val request = createUploadRequest(
474
+ url, fileUriString, options
475
+ ) { requestBody -> requestBody }
476
+
477
+ okHttpClient?.let {
478
+ it.newCall(request).enqueue(object : Callback {
479
+ override fun onFailure(call: Call, e: IOException) {
480
+ Log.e(TAG, e.message.toString())
481
+ promise.reject(TAG, e.message, e)
482
+ }
483
+
484
+ override fun onResponse(call: Call, response: Response) {
485
+ val result = Bundle().apply {
486
+ putString("body", response.body?.string())
487
+ putInt("status", response.code)
488
+ putBundle("headers", translateHeaders(response.headers))
489
+ }
490
+ response.close()
491
+ promise.resolve(result)
492
+ }
493
+ })
494
+ } ?: run {
495
+ promise.reject(FileSystemOkHttpNullException())
496
+ }
497
+ }
498
+
499
+ AsyncFunction("uploadTaskStartAsync") { url: String, fileUriString: String, uuid: String, options: FileSystemUploadOptions, promise: Promise ->
500
+ val progressListener: CountingRequestListener = object : CountingRequestListener {
501
+ private var mLastUpdate: Long = -1
502
+ override fun onProgress(bytesWritten: Long, contentLength: Long) {
503
+ val uploadProgress = Bundle()
504
+ val uploadProgressData = Bundle()
505
+ val currentTime = System.currentTimeMillis()
506
+
507
+ // Throttle events. Sending too many events will block the JS event loop.
508
+ // Make sure to send the last event when we're at 100%.
509
+ if (currentTime > mLastUpdate + MIN_EVENT_DT_MS || bytesWritten == contentLength) {
510
+ mLastUpdate = currentTime
511
+ uploadProgressData.putDouble("totalBytesSent", bytesWritten.toDouble())
512
+ uploadProgressData.putDouble("totalBytesExpectedToSend", contentLength.toDouble())
513
+ uploadProgress.putString("uuid", uuid)
514
+ uploadProgress.putBundle("data", uploadProgressData)
515
+ sendEvent(EXUploadProgressEventName, uploadProgress)
516
+ }
517
+ }
518
+ }
519
+ val request = createUploadRequest(
520
+ url,
521
+ fileUriString,
522
+ options,
523
+ ) { requestBody -> CountingRequestBody(requestBody, progressListener) }
524
+
525
+ val call = okHttpClient!!.newCall(request)
526
+ taskHandlers[uuid] = TaskHandler(call)
527
+ call.enqueue(object : Callback {
528
+ override fun onFailure(call: Call, e: IOException) {
529
+ if (call.isCanceled()) {
530
+ promise.resolve(null)
531
+ return
532
+ }
533
+ Log.e(TAG, e.message.toString())
534
+ promise.reject(TAG, e.message, e)
535
+ }
536
+
537
+ override fun onResponse(call: Call, response: Response) {
538
+ val result = Bundle()
539
+ val body = response.body
540
+ result.apply {
541
+ putString("body", body?.string())
542
+ putInt("status", response.code)
543
+ putBundle("headers", translateHeaders(response.headers))
544
+ }
545
+ response.close()
546
+ promise.resolve(result)
547
+ }
548
+ })
549
+ }
550
+
551
+ AsyncFunction("downloadAsync") { url: String, uriStr: String?, options: DownloadOptions, promise: Promise ->
552
+ val uri = Uri.parse(slashifyFilePath(uriStr))
553
+ ensurePermission(uri, Permission.WRITE)
554
+ uri.checkIfFileDirExists()
555
+
556
+ when {
557
+ url.contains(":").not() -> {
558
+ val context = context
559
+ val resources = context.resources
560
+ val packageName = context.packageName
561
+ val resourceId = resources.getIdentifier(url, "raw", packageName)
562
+ val bufferedSource = context.resources.openRawResource(resourceId).source().buffer()
563
+ val file = uri.toFile()
564
+ file.delete()
565
+ val sink = file.sink().buffer()
566
+ sink.writeAll(bufferedSource)
567
+ sink.close()
568
+ val result = Bundle()
569
+ result.putString("uri", Uri.fromFile(file).toString())
570
+ options.md5.takeIf { it }?.let { result.putString("md5", md5(file)) }
571
+ promise.resolve(result)
572
+ }
573
+
574
+ "file" == uri.scheme -> {
575
+ val requestBuilder = Request.Builder().url(url)
576
+ if (options.headers != null) {
577
+ val headers = options.headers
578
+ headers.forEach { (key, value) ->
579
+ requestBuilder.addHeader(key, value)
580
+ }
581
+ }
582
+ okHttpClient?.newCall(requestBuilder.build())?.enqueue(object : Callback {
583
+ override fun onFailure(call: Call, e: IOException) {
584
+ Log.e(TAG, e.message.toString())
585
+ promise.reject(TAG, e.message, e)
586
+ }
587
+
588
+ @Throws(IOException::class)
589
+ override fun onResponse(call: Call, response: Response) {
590
+ val file = uri.toFile()
591
+ file.delete()
592
+ val sink = file.sink().buffer()
593
+ sink.writeAll(response.body!!.source())
594
+ sink.close()
595
+ val result = Bundle().apply {
596
+ putString("uri", Uri.fromFile(file).toString())
597
+ putInt("status", response.code)
598
+ putBundle("headers", translateHeaders(response.headers))
599
+ if (options.md5) {
600
+ putString("md5", md5(file))
601
+ }
602
+ }
603
+ response.close()
604
+ promise.resolve(result)
605
+ }
606
+ }) ?: promise.reject(FileSystemOkHttpNullException())
607
+ }
608
+
609
+ else -> throw IOException("Unsupported scheme for location '$uri'.")
610
+ }
611
+ }
612
+
613
+ AsyncFunction("networkTaskCancelAsync") { uuid: String ->
614
+ val taskHandler = taskHandlers[uuid]
615
+ taskHandler?.call?.cancel()
616
+ }
617
+
618
+ AsyncFunction("downloadResumableStartAsync") { url: String, fileUriStr: String, uuid: String, options: DownloadOptions, resumeData: String?, promise: Promise ->
619
+ val fileUri = Uri.parse(slashifyFilePath(fileUriStr))
620
+ fileUri.checkIfFileDirExists()
621
+ if (fileUri.scheme != "file") {
622
+ throw IOException("Unsupported scheme for location '$fileUri'.")
623
+ }
624
+ val progressListener: ProgressListener = object : ProgressListener {
625
+ var mLastUpdate: Long = -1
626
+ override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
627
+ val downloadProgress = Bundle()
628
+ val downloadProgressData = Bundle()
629
+ val totalBytesWritten = bytesRead + (resumeData?.toLong() ?: 0)
630
+ val totalBytesExpectedToWrite = contentLength + (resumeData?.toLong() ?: 0)
631
+ val currentTime = System.currentTimeMillis()
632
+
633
+ // Throttle events. Sending too many events will block the JS event loop.
634
+ // Make sure to send the last event when we're at 100%.
635
+ if (currentTime > mLastUpdate + MIN_EVENT_DT_MS || totalBytesWritten == totalBytesExpectedToWrite) {
636
+ mLastUpdate = currentTime
637
+ downloadProgressData.putDouble("totalBytesWritten", totalBytesWritten.toDouble())
638
+ downloadProgressData.putDouble("totalBytesExpectedToWrite", totalBytesExpectedToWrite.toDouble())
639
+ downloadProgress.putString("uuid", uuid)
640
+ downloadProgress.putBundle("data", downloadProgressData)
641
+ sendEvent(EXDownloadProgressEventName, downloadProgress)
642
+ }
643
+ }
644
+ }
645
+ val client = okHttpClient?.newBuilder()
646
+ ?.addInterceptor { chain ->
647
+ val originalResponse = chain.proceed(chain.request())
648
+ originalResponse.newBuilder()
649
+ .body(ProgressResponseBody(originalResponse.body, progressListener))
650
+ .build()
651
+ }
652
+ ?.build()
653
+ if (client == null) {
654
+ promise.reject(FileSystemOkHttpNullException())
655
+ return@AsyncFunction
656
+ }
657
+ val requestBuilder = Request.Builder()
658
+ resumeData?.let {
659
+ requestBuilder.addHeader("Range", "bytes=$it-")
660
+ }
661
+ if (options.headers != null) {
662
+ val headers = options.headers
663
+ headers.forEach { (key, value) ->
664
+ requestBuilder.addHeader(key, value)
665
+ }
666
+ }
667
+
668
+ val request = requestBuilder.url(url).build()
669
+ val call = client.newCall(request)
670
+ taskHandlers[uuid] = DownloadTaskHandler(fileUri, call)
671
+ val params = DownloadResumableTaskParams(
672
+ options, call, fileUri.toFile(), resumeData != null, promise
673
+ )
674
+ moduleCoroutineScope.launch {
675
+ downloadResumableTask(params)
676
+ }
677
+ }
678
+
679
+ AsyncFunction("downloadResumablePauseAsync") { uuid: String ->
680
+ val taskHandler = taskHandlers[uuid]
681
+ ?: throw IOException("No download object available")
682
+
683
+ if (taskHandler !is DownloadTaskHandler) {
684
+ throw FileSystemCannotFindTaskException()
685
+ }
686
+
687
+ taskHandler.call.cancel()
688
+ taskHandlers.remove(uuid)
689
+ val file = taskHandler.fileUri.toFile()
690
+ val result = Bundle().apply {
691
+ putString("resumeData", file.length().toString())
692
+ }
693
+ return@AsyncFunction result
694
+ }
695
+
696
+ OnActivityResult { _, (requestCode, resultCode, data) ->
697
+ if (requestCode == DIR_PERMISSIONS_REQUEST_CODE && dirPermissionsRequest != null) {
698
+ val currentActivity =
699
+ appContext.currentActivity ?: throw Exceptions.MissingActivity()
700
+ val result = Bundle()
701
+ if (resultCode == Activity.RESULT_OK && data != null) {
702
+ val treeUri = data.data
703
+ val takeFlags = (
704
+ data.flags
705
+ and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
706
+ )
707
+ treeUri?.let {
708
+ currentActivity.contentResolver.takePersistableUriPermission(it, takeFlags)
709
+ }
710
+ result.putBoolean("granted", true)
711
+ result.putString("directoryUri", treeUri.toString())
712
+ } else {
713
+ result.putBoolean("granted", false)
714
+ }
715
+ dirPermissionsRequest?.resolve(result)
716
+ dirPermissionsRequest = null
717
+ }
718
+ }
719
+
720
+ OnDestroy {
721
+ try {
722
+ moduleCoroutineScope.cancel(ModuleDestroyedException())
723
+ } catch (e: IllegalStateException) {
724
+ Log.e(TAG, "The scope does not have a job in it")
725
+ }
726
+ }
727
+ }
728
+
729
+ @Throws(IOException::class)
730
+ private fun Uri.checkIfFileExists() {
731
+ val file = this.toFile()
732
+ if (!file.exists()) {
733
+ throw IOException("Directory for '${file.path}' doesn't exist.")
734
+ }
735
+ }
736
+
737
+ @Throws(IOException::class)
738
+ private fun Uri.checkIfFileDirExists() {
739
+ val file = this.toFile()
740
+ val dir = file.parentFile
741
+ if (dir == null || !dir.exists()) {
742
+ throw IOException("Directory for '${file.path}' doesn't exist. Please make sure directory '${file.parent}' exists before calling downloadAsync.")
743
+ }
744
+ }
745
+
746
+ @Throws(IOException::class)
747
+ private fun ensureDirExists(dir: File) {
748
+ if (!(dir.isDirectory || dir.mkdirs())) {
749
+ throw IOException("Couldn't create directory '$dir'")
750
+ }
751
+ }
752
+
753
+ private fun permissionsForPath(path: String?): EnumSet<Permission>? {
754
+ return appContext.filePermission?.getPathPermissions(context, path)
755
+ }
756
+
757
+ private fun permissionsForUri(uri: Uri) = when {
758
+ uri.isSAFUri -> permissionsForSAFUri(uri)
759
+ uri.scheme == "content" -> EnumSet.of(Permission.READ)
760
+ uri.scheme == "asset" -> EnumSet.of(Permission.READ)
761
+ uri.scheme == "file" -> permissionsForPath(uri.path)
762
+ uri.scheme == null -> EnumSet.of(Permission.READ)
763
+ else -> EnumSet.noneOf(Permission::class.java)
764
+ }
765
+
766
+ private fun permissionsForSAFUri(uri: Uri): EnumSet<Permission> {
767
+ val documentFile = getNearestSAFFile(uri)
768
+ return EnumSet.noneOf(Permission::class.java).apply {
769
+ if (documentFile != null) {
770
+ if (documentFile.canRead()) {
771
+ add(Permission.READ)
772
+ }
773
+ if (documentFile.canWrite()) {
774
+ add(Permission.WRITE)
775
+ }
776
+ }
777
+ }
778
+ }
779
+
780
+ // For now we only need to ensure one permission at a time, this allows easier error message strings,
781
+ // we can generalize this when needed later
782
+ @Throws(IOException::class)
783
+ private fun ensurePermission(uri: Uri, permission: Permission, errorMsg: String) {
784
+ if (permissionsForUri(uri)?.contains(permission) != true) {
785
+ throw IOException(errorMsg)
786
+ }
787
+ }
788
+
789
+ @Throws(IOException::class)
790
+ private fun ensurePermission(uri: Uri, permission: Permission) {
791
+ if (permission == Permission.READ) {
792
+ ensurePermission(uri, permission, "Location '$uri' isn't readable.")
793
+ }
794
+ if (permission == Permission.WRITE) {
795
+ ensurePermission(uri, permission, "Location '$uri' isn't writable.")
796
+ }
797
+ ensurePermission(uri, permission, "Location '$uri' doesn't have permission '${permission.name}'.")
798
+ }
799
+
800
+ @Throws(IOException::class)
801
+ private fun openAssetInputStream(uri: Uri): InputStream {
802
+ // AssetManager expects no leading slash.
803
+ val asset = requireNotNull(uri.path).substring(1)
804
+ return context.assets.open(asset)
805
+ }
806
+
807
+ @SuppressLint("DiscouragedApi")
808
+ @Throws(IOException::class)
809
+ private fun openResourceInputStream(resourceName: String?): InputStream {
810
+ var resourceId = context.resources.getIdentifier(resourceName, "raw", context.packageName)
811
+ if (resourceId == 0) {
812
+ // this resource doesn't exist in the raw folder, so try drawable
813
+ resourceId = context.resources.getIdentifier(resourceName, "drawable", context.packageName)
814
+ if (resourceId == 0) {
815
+ throw FileNotFoundException("No resource found with the name '$resourceName'")
816
+ }
817
+ }
818
+ return context.resources.openRawResource(resourceId)
819
+ }
820
+
821
+ @Throws(IOException::class)
822
+ private fun transformFilesFromSAF(documentFile: DocumentFile, outputDir: File, copy: Boolean) {
823
+ if (!documentFile.exists()) {
824
+ return
825
+ }
826
+ if (!outputDir.exists() && !outputDir.mkdirs()) {
827
+ throw IOException("Couldn't create folder in output dir.")
828
+ }
829
+ if (documentFile.isDirectory) {
830
+ for (file in documentFile.listFiles()) {
831
+ documentFile.name?.let {
832
+ transformFilesFromSAF(file, File(outputDir, it), copy)
833
+ }
834
+ }
835
+ if (!copy) {
836
+ documentFile.delete()
837
+ }
838
+ return
839
+ }
840
+ documentFile.name?.let {
841
+ val newFile = File(outputDir.path, it)
842
+ context.contentResolver.openInputStream(documentFile.uri).use { `in` -> FileOutputStream(newFile).use { out -> IOUtils.copy(`in`, out) } }
843
+ if (!copy) {
844
+ documentFile.delete()
845
+ }
846
+ }
847
+ }
848
+
849
+ private fun contentUriFromFile(file: File): Uri {
850
+ val currentActivity = appContext.currentActivity
851
+ ?: throw Exceptions.MissingActivity()
852
+
853
+ return FileProvider.getUriForFile(
854
+ currentActivity.application,
855
+ "${currentActivity.application.packageName}.FileSystemFileProvider",
856
+ file
857
+ )
858
+ }
859
+
860
+ @Throws(IOException::class)
861
+ private fun createUploadRequest(url: String, fileUriString: String, options: FileSystemUploadOptions, decorator: RequestBodyDecorator): Request {
862
+ val fileUri = Uri.parse(slashifyFilePath(fileUriString))
863
+ ensurePermission(fileUri, Permission.READ)
864
+ fileUri.checkIfFileExists()
865
+
866
+ val requestBuilder = Request.Builder().url(url)
867
+ options.headers?.let {
868
+ it.forEach { (key, value) -> requestBuilder.addHeader(key, value) }
869
+ }
870
+
871
+ val body = createRequestBody(options, decorator, fileUri.toFile())
872
+ return options.httpMethod.let { requestBuilder.method(it.value, body).build() }
873
+ }
874
+
875
+ private fun createRequestBody(options: FileSystemUploadOptions, decorator: RequestBodyDecorator, file: File): RequestBody {
876
+ return when (options.uploadType) {
877
+ FileSystemUploadType.BINARY_CONTENT -> {
878
+ decorator.decorate(file.asRequestBody(null))
879
+ }
880
+
881
+ FileSystemUploadType.MULTIPART -> {
882
+ val bodyBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)
883
+ options.parameters?.let {
884
+ (it as Map<String, Any>)
885
+ .forEach { (key, value) -> bodyBuilder.addFormDataPart(key, value.toString()) }
886
+ }
887
+ val mimeType: String = options.mimeType ?: URLConnection.guessContentTypeFromName(file.name)
888
+
889
+ val fieldName = options.fieldName ?: file.name
890
+ bodyBuilder.addFormDataPart(fieldName, file.name, decorator.decorate(file.asRequestBody(mimeType.toMediaTypeOrNull())))
891
+ bodyBuilder.build()
892
+ }
893
+ }
894
+ }
895
+
896
+ private data class DownloadResumableTaskParams(
897
+ val options: DownloadOptions,
898
+ val call: Call,
899
+ val file: File,
900
+ val isResume: Boolean,
901
+ val promise: Promise
902
+ )
903
+
904
+ private suspend fun downloadResumableTask(params: DownloadResumableTaskParams) = withContext(Dispatchers.IO) {
905
+ val (options, call, file, isResume, promise) = params
906
+ try {
907
+ val response = call.execute()
908
+ val responseBody = response.body
909
+ val input = BufferedInputStream(responseBody!!.byteStream())
910
+ val output = FileOutputStream(file, isResume)
911
+ val data = ByteArray(1024)
912
+ var count: Int
913
+ while (input.read(data).also { count = it } != -1) {
914
+ output.write(data, 0, count)
915
+ }
916
+ val result = Bundle().apply {
917
+ putString("uri", Uri.fromFile(file).toString())
918
+ putInt("status", response.code)
919
+ putBundle("headers", translateHeaders(response.headers))
920
+ options.md5.takeIf { it }?.let { putString("md5", md5(file)) }
921
+ }
922
+ response.close()
923
+ promise.resolve(result)
924
+ null
925
+ } catch (e: Exception) {
926
+ if (call.isCanceled()) {
927
+ promise.resolve(null)
928
+ return@withContext null
929
+ }
930
+ e.message?.let { Log.e(TAG, it) }
931
+ promise.reject(TAG, e.message, e)
932
+ null
933
+ }
934
+ }
935
+
936
+ private open class TaskHandler(val call: Call)
937
+ private class DownloadTaskHandler(val fileUri: Uri, call: Call) : TaskHandler(call)
938
+
939
+ // https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
940
+ private class ProgressResponseBody constructor(private val responseBody: ResponseBody?, private val progressListener: ProgressListener) : ResponseBody() {
941
+ private var bufferedSource: BufferedSource? = null
942
+
943
+ override fun contentType(): MediaType? = responseBody?.contentType()
944
+
945
+ override fun contentLength(): Long = responseBody?.contentLength() ?: -1
946
+
947
+ override fun source(): BufferedSource =
948
+ bufferedSource ?: source(responseBody!!.source()).buffer()
949
+
950
+ private fun source(source: Source): Source {
951
+ return object : ForwardingSource(source) {
952
+ var totalBytesRead = 0L
953
+
954
+ @Throws(IOException::class)
955
+ override fun read(sink: Buffer, byteCount: Long): Long {
956
+ val bytesRead = super.read(sink, byteCount)
957
+ // read() returns the number of bytes read, or -1 if this source is exhausted.
958
+ totalBytesRead += if (bytesRead != -1L) bytesRead else 0
959
+ progressListener.update(
960
+ totalBytesRead,
961
+ responseBody?.contentLength()
962
+ ?: -1,
963
+ bytesRead == -1L
964
+ )
965
+ return bytesRead
966
+ }
967
+ }
968
+ }
969
+ }
970
+
971
+ internal fun interface ProgressListener {
972
+ fun update(bytesRead: Long, contentLength: Long, done: Boolean)
973
+ }
974
+
975
+ @get:Synchronized
976
+ private val okHttpClient: OkHttpClient?
977
+ get() {
978
+ if (client == null) {
979
+ val builder = OkHttpClient.Builder()
980
+ .connectTimeout(60, TimeUnit.SECONDS)
981
+ .readTimeout(60, TimeUnit.SECONDS)
982
+ .writeTimeout(60, TimeUnit.SECONDS)
983
+ val cookieHandler: CookieHandler = appContext.legacyModule()
984
+ ?: throw CookieHandlerNotFoundException()
985
+
986
+ builder.cookieJar(JavaNetCookieJar(cookieHandler))
987
+ client = builder.build()
988
+ }
989
+ return client
990
+ }
991
+
992
+ @Throws(IOException::class)
993
+ private fun md5(file: File): String {
994
+ val inputStream: InputStream = FileInputStream(file)
995
+ return inputStream.use {
996
+ val md5bytes = DigestUtils.md5(it)
997
+ String(Hex.encodeHex(md5bytes))
998
+ }
999
+ }
1000
+
1001
+ /**
1002
+ * Concatenated copy of org.apache.commons.io@1.4.0#FileUtils#forceDelete
1003
+ * Newer version of commons-io uses File#toPath() under the hood that unsupported below Android SDK 26
1004
+ * See docs for reference https://commons.apache.org/proper/commons-io/javadocs/api-1.4/index.html
1005
+ */
1006
+ @Throws(IOException::class)
1007
+ private fun forceDelete(file: File) {
1008
+ if (file.isDirectory) {
1009
+ val files = file.listFiles() ?: throw IOException("Failed to list contents of $file")
1010
+ var exception: IOException? = null
1011
+ for (f in files) {
1012
+ try {
1013
+ forceDelete(f)
1014
+ } catch (ioe: IOException) {
1015
+ exception = ioe
1016
+ }
1017
+ }
1018
+ if (null != exception) {
1019
+ throw exception
1020
+ }
1021
+ if (!file.delete()) {
1022
+ throw IOException("Unable to delete directory $file.")
1023
+ }
1024
+ } else if (!file.delete()) {
1025
+ throw IOException("Unable to delete file: $file")
1026
+ }
1027
+ }
1028
+
1029
+ private fun getFileSize(file: File): Long {
1030
+ if (!file.isDirectory) {
1031
+ return file.length()
1032
+ }
1033
+ val content = file.listFiles() ?: return 0
1034
+ return content.map { getFileSize(it) }.reduceOrNull { total, itemSize -> total + itemSize }
1035
+ ?: 0
1036
+ }
1037
+
1038
+ @Throws(IOException::class)
1039
+ private fun getInputStream(uri: Uri) = when {
1040
+ uri.scheme == "file" -> FileInputStream(uri.toFile())
1041
+ uri.scheme == "asset" -> openAssetInputStream(uri)
1042
+ uri.isSAFUri -> context.contentResolver.openInputStream(uri)!!
1043
+ else -> throw IOException("Unsupported scheme for location '$uri'.")
1044
+ }
1045
+
1046
+ @Throws(IOException::class)
1047
+ private fun getOutputStream(uri: Uri) = when {
1048
+ uri.scheme == "file" -> FileOutputStream(uri.toFile())
1049
+ uri.isSAFUri -> context.contentResolver.openOutputStream(uri)!!
1050
+ else -> throw IOException("Unsupported scheme for location '$uri'.")
1051
+ }
1052
+
1053
+ private fun getNearestSAFFile(uri: Uri): DocumentFile? {
1054
+ val file = DocumentFile.fromSingleUri(context, uri)
1055
+ return if (file != null && file.isFile) {
1056
+ file
1057
+ } else DocumentFile.fromTreeUri(context, uri)
1058
+ }
1059
+
1060
+ // extension functions of Uri class
1061
+ private fun Uri.toFile() = if (this.path != null) {
1062
+ File(this.path!!)
1063
+ } else {
1064
+ throw IOException("Invalid Uri: $this")
1065
+ }
1066
+
1067
+ private val Uri.isSAFUri: Boolean
1068
+ get() = scheme == "content" && host?.startsWith("com.android.externalstorage") ?: false
1069
+
1070
+ private fun parseFileUri(uriStr: String) = uriStr.substring(uriStr.indexOf(':') + 3)
1071
+
1072
+ @Throws(IOException::class)
1073
+ private fun getInputStreamBytes(inputStream: InputStream): ByteArray {
1074
+ val bytesResult: ByteArray
1075
+ val byteBuffer = ByteArrayOutputStream()
1076
+ val bufferSize = 1024
1077
+ val buffer = ByteArray(bufferSize)
1078
+ try {
1079
+ var len: Int
1080
+ while (inputStream.read(buffer).also { len = it } != -1) {
1081
+ byteBuffer.write(buffer, 0, len)
1082
+ }
1083
+ bytesResult = byteBuffer.toByteArray()
1084
+ } finally {
1085
+ try {
1086
+ byteBuffer.close()
1087
+ } catch (ignored: IOException) {
1088
+ }
1089
+ }
1090
+ return bytesResult
1091
+ }
1092
+
1093
+ // Copied out of React Native's `NetworkingModule.java`
1094
+ private fun translateHeaders(headers: Headers): Bundle {
1095
+ val responseHeaders = Bundle()
1096
+ for (i in 0 until headers.size) {
1097
+ val headerName = headers.name(i)
1098
+ // multiple values for the same header
1099
+ if (responseHeaders.containsKey(headerName)) {
1100
+ responseHeaders.putString(
1101
+ headerName,
1102
+ responseHeaders.getString(headerName) + ", " + headers.value(i)
1103
+ )
1104
+ } else {
1105
+ responseHeaders.putString(headerName, headers.value(i))
1106
+ }
1107
+ }
1108
+ return responseHeaders
1109
+ }
1110
+ }