diff --git a/CHANGELOG.md b/CHANGELOG.md index 6efc703..512f012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.9.0 + +### New + +- Calling [openDocumentFile] on apk files triggers the installation. +- [getDocumentThumbnail] it's now supports decoding apk file icons. +- [shareUri] is a new API to trigger share intent using Uris from SAF (files through File class are also supported). + ## 0.8.0 New SAF API and Gradle version upgrade. diff --git a/LICENSE b/LICENSE index d7e44f7..7d8e012 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Alex Rintt +Copyright (c) 2021-2023 Alex Rintt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1395f3e..aab84ff 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ All other branches are derivated from issues, new features or bug fixes. ## Contributors +- [kent](https://github.com/ken-tn) fixed documentation typo. - [iamcosmin](https://github.com/iamcosmin), [limshengli](https://github.com/limshengli) reported a issue with Gradle and Kotlin version [#124](https://github.com/alexrintt/shared-storage/issues/124). - [mx1up](https://github.com/mx1up) reported a issue with `openDocumentFile` API [#121](https://github.com/alexrintt/shared-storage/issues/121). - [Tamerlanchiques](https://github.com/Tamerlanchiques) reported a bug which the persisted URI wasn't being properly persisted across device reboots [#118](https://github.com/alexrintt/shared-storage/issues/118). diff --git a/analysis_options.yaml b/analysis_options.yaml index d6312c6..f40ab3a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -19,3 +19,5 @@ linter: always_use_package_imports: false avoid_relative_lib_imports: false avoid_print: false + always_specify_types: true + avoid_classes_with_only_static_members: false diff --git a/android/build.gradle b/android/build.gradle index d9dd1c5..4dd5e0f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -27,7 +27,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt index eedfab9..ac88afe 100644 --- a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt +++ b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt @@ -13,15 +13,17 @@ import io.flutter.plugin.common.* import io.flutter.plugin.common.EventChannel.StreamHandler import io.alexrintt.sharedstorage.ROOT_CHANNEL import io.alexrintt.sharedstorage.SharedStoragePlugin +import io.alexrintt.sharedstorage.deprecated.lib.* import io.alexrintt.sharedstorage.plugin.* +import io.alexrintt.sharedstorage.storageaccessframework.* import io.alexrintt.sharedstorage.storageaccessframework.lib.* +import io.alexrintt.sharedstorage.plugin.ActivityListener +import io.alexrintt.sharedstorage.plugin.Listenable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.io.FileNotFoundException -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream +import kotlinx.coroutines.withContext +import java.io.* /** * Aimed to implement strictly only the APIs already available from the native and original @@ -58,12 +60,15 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : result.notSupported(call.method, API_21) } } + OPEN_DOCUMENT -> if (Build.VERSION.SDK_INT >= API_21) { openDocument(call, result) } + OPEN_DOCUMENT_TREE -> if (Build.VERSION.SDK_INT >= API_21) { openDocumentTree(call, result) } + CREATE_FILE -> if (Build.VERSION.SDK_INT >= API_21) { createFile( result, @@ -73,16 +78,20 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : call.argument("content")!! ) } + WRITE_TO_FILE -> writeToFile( result, call.argument("uri")!!, call.argument("content")!!, call.argument("mode")!! ) + PERSISTED_URI_PERMISSIONS -> persistedUriPermissions(result) + RELEASE_PERSISTABLE_URI_PERMISSION -> releasePersistableUriPermission( result, call.argument("uri") as String ) + FROM_TREE_URI -> if (Build.VERSION.SDK_INT >= API_21) { result.success( createDocumentFileMap( @@ -92,6 +101,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : ) ) } + CAN_WRITE -> if (Build.VERSION.SDK_INT >= API_21) { result.success( documentFromUri( @@ -99,11 +109,13 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : )?.canWrite() ) } + CAN_READ -> if (Build.VERSION.SDK_INT >= API_21) { val uri = call.argument("uri") as String result.success(documentFromUri(plugin.context, uri)?.canRead()) } + LENGTH -> if (Build.VERSION.SDK_INT >= API_21) { result.success( documentFromUri( @@ -111,6 +123,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : )?.length() ) } + EXISTS -> if (Build.VERSION.SDK_INT >= API_21) { result.success( documentFromUri( @@ -118,13 +131,36 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : )?.exists() ) } + DELETE -> if (Build.VERSION.SDK_INT >= API_21) { - result.success( - documentFromUri( - plugin.context, call.argument("uri") as String - )?.delete() - ) + try { + result.success( + documentFromUri( + plugin.context, call.argument("uri") as String + )?.delete() + ) + } catch (e: FileNotFoundException) { + // File is already deleted. + result.success(null) + } catch (e: IllegalStateException) { + // File is already deleted. + result.success(null) + } catch (e: IllegalArgumentException) { + // File is already deleted. + result.success(null) + } catch (e: IOException) { + // Unknown, can be anything. + result.success(null) + } catch (e: Throwable) { + Log.d( + "sharedstorage", + "Unknown error when calling [delete] method with [uri]." + ) + // Unknown, can be anything. + result.success(null) + } } + LAST_MODIFIED -> if (Build.VERSION.SDK_INT >= API_21) { val document = documentFromUri( plugin.context, call.argument("uri") as String @@ -132,6 +168,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : result.success(document?.lastModified()) } + CREATE_DIRECTORY -> { if (Build.VERSION.SDK_INT >= API_21) { val uri = call.argument("uri") as String @@ -146,6 +183,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : result.notSupported(call.method, API_21) } } + FIND_FILE -> { if (Build.VERSION.SDK_INT >= API_21) { val uri = call.argument("uri") as String @@ -160,20 +198,27 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : ) } } + COPY -> { val uri = Uri.parse(call.argument("uri")!!) val destination = Uri.parse(call.argument("destination")!!) if (Build.VERSION.SDK_INT >= API_21) { - if (Build.VERSION.SDK_INT >= API_24) { - DocumentsContract.copyDocument( - plugin.context.contentResolver, uri, destination - ) - } else { - val inputStream = openInputStream(uri) - val outputStream = openOutputStream(destination) + CoroutineScope(Dispatchers.IO).launch { + withContext(Dispatchers.IO) { + val inputStream = openInputStream(uri) + val outputStream = openOutputStream(destination) + + // TODO: Implement progress indicator by re-writing the [copyTo] impl with an optional callback fn. + outputStream?.let { inputStream?.copyTo(it) } - outputStream?.let { inputStream?.copyTo(it) } + inputStream?.close() + outputStream?.close() + } + + launch(Dispatchers.Main) { + result.success(null) + } } } else { result.notSupported( @@ -183,6 +228,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : ) } } + RENAME_TO -> { val uri = call.argument("uri") as String val displayName = call.argument("displayName") as String @@ -206,6 +252,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : ) } } + PARENT_FILE -> { val uri = call.argument("uri")!! @@ -217,6 +264,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : result.notSupported(PARENT_FILE, API_21, mapOf("uri" to uri)) } } + CHILD -> { val uri = call.argument("uri")!! val path = call.argument("path")!! @@ -233,6 +281,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : result.notSupported(CHILD, API_21, mapOf("uri" to uri)) } } + else -> result.notImplemented() } } @@ -344,20 +393,25 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : content: ByteArray, block: DocumentFile?.() -> Unit ) { - val createdFile = documentFromUri(plugin.context, treeUri)!!.createFile( - mimeType, displayName - ) + CoroutineScope(Dispatchers.IO).launch { + val createdFile = documentFromUri(plugin.context, treeUri)!!.createFile( + mimeType, displayName + ) - createdFile?.uri?.apply { - plugin.context.contentResolver.openOutputStream(this)?.apply { - write(content) - flush() - close() + createdFile?.uri?.apply { + kotlin.runCatching { + plugin.context.contentResolver.openOutputStream(this)?.use { + it.write(content) + it.flush() - val createdFileDocument = - documentFromUri(plugin.context, createdFile.uri) + val createdFileDocument = + documentFromUri(plugin.context, createdFile.uri) - block(createdFileDocument) + launch(Dispatchers.Main) { + block(createdFileDocument) + } + } + } } } } @@ -379,23 +433,21 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : } } - @RequiresApi(API_19) private fun persistedUriPermissions(result: MethodChannel.Result) { val persistedUriPermissions = plugin.context.contentResolver.persistedUriPermissions result.success(persistedUriPermissions.map { - mapOf( - "isReadPermission" to it.isReadPermission, - "isWritePermission" to it.isWritePermission, - "persistedTime" to it.persistedTime, - "uri" to "${it.uri}", - "isTreeDocumentFile" to it.uri.isTreeDocumentFile - ) - }.toList()) + mapOf( + "isReadPermission" to it.isReadPermission, + "isWritePermission" to it.isWritePermission, + "persistedTime" to it.persistedTime, + "uri" to "${it.uri}", + "isTreeDocumentFile" to it.uri.isTreeDocumentFile + ) + }.toList()) } - @RequiresApi(API_19) private fun releasePersistableUriPermission( result: MethodChannel.Result, directoryUri: String ) { @@ -417,8 +469,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : } plugin.context.contentResolver.releasePersistableUriPermission( - targetUri, - flags + targetUri, flags ) } } @@ -426,7 +477,6 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : result.success(null) } - @RequiresApi(API_19) override fun onActivityResult( requestCode: Int, resultCode: Int, resultIntent: Intent? ): Boolean { @@ -467,6 +517,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : pendingResults.remove(OPEN_DOCUMENT_TREE_CODE) } } + OPEN_DOCUMENT_CODE -> { val pendingResult = pendingResults[OPEN_DOCUMENT_CODE] ?: return false @@ -579,7 +630,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : ) { if (eventSink == null) return - val columns = args["columns"] as List<*> + val userProvidedColumns = args["columns"] as List<*> val uri = Uri.parse(args["uri"] as String) val document = DocumentFile.fromTreeUri(plugin.context, uri) @@ -589,6 +640,7 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : "Android SDK must be greater or equal than [Build.VERSION_CODES.N]", "Got (Build.VERSION.SDK_INT): ${Build.VERSION.SDK_INT}" ) + eventSink.endOfStream() } else { if (!document.canRead()) { val error = "You cannot read a URI that you don't have read permissions" @@ -604,14 +656,15 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { CoroutineScope(Dispatchers.IO).launch { try { - traverseDirectoryEntries( - plugin.context.contentResolver, + traverseDirectoryEntries(plugin.context.contentResolver, rootOnly = true, targetUri = document.uri, - columns = columns.map { - parseDocumentFileColumn(parseDocumentFileColumn(it as String)!!) - }.toTypedArray() - ) { data, _ -> + columns = userProvidedColumns.map { + // Convert the user provided column string to documentscontract column ID. + documentFileColumnToActualDocumentsContractEnumString( + deserializeDocumentFileColumn(it as String)!! + ) + }.toTypedArray()) { data, _ -> launch(Dispatchers.Main) { eventSink.success( data @@ -622,6 +675,8 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : launch(Dispatchers.Main) { eventSink.endOfStream() } } } + } else { + eventSink.endOfStream() } } } @@ -649,13 +704,22 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : bytes } catch (e: FileNotFoundException) { + // Probably the file was already deleted and now you are trying to read. null } catch (e: IOException) { + // Unknown, can be anything. + null + } catch (e: IllegalArgumentException) { + // Probably the file was already deleted and now you are trying to read. + null + } catch (e: IllegalStateException) { + // Probably you ran [delete] and [readDocumentContent] at the same time. null } } override fun onCancel(arguments: Any?) { + eventSink?.endOfStream() eventSink = null } } diff --git a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt index f7b23f7..c04861e 100644 --- a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt +++ b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt @@ -3,14 +3,22 @@ package io.alexrintt.sharedstorage.storageaccessframework import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri +import android.os.Build import android.util.Log -import io.flutter.plugin.common.* -import io.flutter.plugin.common.EventChannel.StreamHandler +import androidx.annotation.RequiresApi +import androidx.core.app.ShareCompat +import com.anggrayudi.storage.file.isTreeDocumentFile +import com.anggrayudi.storage.file.mimeType import io.alexrintt.sharedstorage.ROOT_CHANNEL import io.alexrintt.sharedstorage.SharedStoragePlugin +import io.alexrintt.sharedstorage.deprecated.lib.documentFromUri import io.alexrintt.sharedstorage.plugin.ActivityListener import io.alexrintt.sharedstorage.plugin.Listenable import io.alexrintt.sharedstorage.storageaccessframework.lib.* +import io.flutter.plugin.common.* +import io.flutter.plugin.common.EventChannel.StreamHandler +import java.net.URLConnection + /** * Aimed to be a class which takes the `DocumentFile` API and implement some APIs not supported @@ -23,13 +31,13 @@ import io.alexrintt.sharedstorage.storageaccessframework.lib.* * globally without modifying the strict APIs. */ internal class DocumentFileHelperApi(private val plugin: SharedStoragePlugin) : - MethodChannel.MethodCallHandler, - PluginRegistry.ActivityResultListener, - Listenable, - ActivityListener, - StreamHandler { + MethodChannel.MethodCallHandler, + PluginRegistry.ActivityResultListener, + Listenable, + ActivityListener, + StreamHandler { private val pendingResults: MutableMap> = - mutableMapOf() + mutableMapOf() private var channel: MethodChannel? = null private var eventChannel: EventChannel? = null private var eventSink: EventChannel.EventSink? = null @@ -41,48 +49,157 @@ internal class DocumentFileHelperApi(private val plugin: SharedStoragePlugin) : override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { OPEN_DOCUMENT_FILE -> openDocumentFile(call, result) + SHARE_URI -> shareUri(call, result) else -> result.notImplemented() } } - private fun openDocumentFile(call: MethodCall, result: MethodChannel.Result) { + private fun shareUri(call: MethodCall, result: MethodChannel.Result) { val uri = Uri.parse(call.argument("uri")!!) - val type = call.argument("type") ?: plugin.context.contentResolver.getType(uri) - - val intent = - Intent(Intent.ACTION_VIEW).apply { - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - data = uri + val type = + call.argument("type") + ?: try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + documentFromUri(plugin.context, uri)?.mimeType + } else { + null + } + } catch (e: Throwable) { + null } + ?: plugin.binding!!.activity.contentResolver.getType(uri) + ?: URLConnection.guessContentTypeFromName(uri.lastPathSegment) + ?: "application/octet-stream" try { - plugin.binding?.activity?.startActivity(intent, null) + Log.d("sharedstorage", "Trying to share uri $uri with type $type") - Log.d("sharedstorage", "Successfully launched uri $uri ") + ShareCompat + .IntentBuilder(plugin.binding!!.activity) + .setChooserTitle("Share") + .setType(type) + .setStream(uri) + .startChooser() - result.success(true) + Log.d("sharedstorage", "Successfully shared uri $uri of type $type.") + + result.success(null) } catch (e: ActivityNotFoundException) { result.error( - EXCEPTION_ACTIVITY_NOT_FOUND, - "There's no activity handler that can process the uri $uri of type $type", - mapOf("uri" to "$uri", "type" to type) + EXCEPTION_ACTIVITY_NOT_FOUND, + "There's no activity handler that can process the uri $uri of type $type, error: $e.", + mapOf("uri" to "$uri", "type" to type) ) } catch (e: SecurityException) { result.error( - EXCEPTION_CANT_OPEN_FILE_DUE_SECURITY_POLICY, - "Missing read and write permissions for uri $uri of type $type to launch ACTION_VIEW activity", - mapOf("uri" to "$uri", "type" to "$type") + EXCEPTION_CANT_OPEN_FILE_DUE_SECURITY_POLICY, + "Missing read and write permissions for uri $uri of type $type to launch ACTION_VIEW activity, error: $e.", + mapOf("uri" to "$uri", "type" to type) ) } catch (e: Throwable) { result.error( - EXCEPTION_CANT_OPEN_DOCUMENT_FILE, - "Couldn't start activity to open document file for uri: $uri", - mapOf("uri" to "$uri") + EXCEPTION_CANT_OPEN_DOCUMENT_FILE, + "Couldn't start activity to open document file for uri: $uri, error: $e.", + mapOf("uri" to "$uri") + ) + } + } + + private fun openDocumentAsSimpleFile(uri: Uri, type: String?) { + Log.d("sharedstorage", "Trying to open uri $uri with type $type") + + val intent = + Intent(Intent.ACTION_VIEW).apply { + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + + setDataAndType(uri, type) + setFlags(flags) + } + + plugin.binding?.activity?.startActivity(intent, null) + + Log.d( + "sharedstorage", + "Successfully launched uri $uri as single|file uri." + ) + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private fun openDocumentAsTree(uri: Uri) { + val file = documentFromUri(plugin.context, uri) + if (file?.isTreeDocumentFile == true) { + val intent = Intent(Intent.ACTION_VIEW) + + intent.setDataAndType(uri, "vnd.android.document/root") + + plugin.binding?.activity?.startActivity(intent, null) + + Log.d( + "sharedstorage", + "Successfully launched uri $uri as tree uri." + ) + } else { + throw Exception("Not a document tree URI") + } + } + + private fun openDocumentFile(call: MethodCall, result: MethodChannel.Result) { + val uri = Uri.parse(call.argument("uri")!!) + val type = + call.argument("type") ?: plugin.context.contentResolver.getType( + uri + ) + + fun successfullyOpenedUri() { + result.success(true) + } + + try { + openDocumentAsSimpleFile(uri, type) + return successfullyOpenedUri() + } catch (e: ActivityNotFoundException) { + Log.d( + "sharedstorage", + "No activity is defined to handle $uri, trying to recover from error and interpret as tree." + ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + openDocumentAsTree(uri) + return successfullyOpenedUri() + } catch (e: Throwable) { + Log.d( + "sharedstorage", + "Tried to recover from missing activity exception but did not work, exception: $e" + ) + } + } + + return result.error( + EXCEPTION_ACTIVITY_NOT_FOUND, + "There's no activity handler that can process the uri $uri of type $type", + mapOf("uri" to "$uri", "type" to type) + ) + } catch (e: SecurityException) { + return result.error( + EXCEPTION_CANT_OPEN_FILE_DUE_SECURITY_POLICY, + "Missing read and write permissions for uri $uri of type $type to launch ACTION_VIEW activity", + mapOf("uri" to "$uri", "type" to "$type") + ) + } catch (e: Throwable) { + return result.error( + EXCEPTION_CANT_OPEN_DOCUMENT_FILE, + "Couldn't start activity to open document file for uri: $uri", + mapOf("uri" to "$uri") ) } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + override fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent? + ): Boolean { when (requestCode) { /** TODO(@alexrintt): Implement if required */ else -> return true @@ -125,7 +242,7 @@ internal class DocumentFileHelperApi(private val plugin: SharedStoragePlugin) : eventSink = events when (args["event"]) { - /** TODO(@alexrintt): Implement if required */ + /** TODO(@alexrintt): Implement if required */ } } diff --git a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentsContractApi.kt b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentsContractApi.kt index 9b8848b..38eaad4 100644 --- a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentsContractApi.kt +++ b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentsContractApi.kt @@ -1,73 +1,177 @@ package io.alexrintt.sharedstorage.storageaccessframework +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Canvas import android.graphics.Point +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import android.provider.DocumentsContract +import android.util.Log +import io.alexrintt.sharedstorage.ROOT_CHANNEL +import io.alexrintt.sharedstorage.SharedStoragePlugin +import io.alexrintt.sharedstorage.plugin.* +import io.alexrintt.sharedstorage.storageaccessframework.* +import io.alexrintt.sharedstorage.storageaccessframework.lib.* import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import io.alexrintt.sharedstorage.ROOT_CHANNEL -import io.alexrintt.sharedstorage.SharedStoragePlugin -import io.alexrintt.sharedstorage.plugin.API_21 -import io.alexrintt.sharedstorage.plugin.ActivityListener -import io.alexrintt.sharedstorage.plugin.Listenable -import io.alexrintt.sharedstorage.plugin.notSupported -import io.alexrintt.sharedstorage.storageaccessframework.lib.GET_DOCUMENT_THUMBNAIL -import io.alexrintt.sharedstorage.storageaccessframework.lib.bitmapToBase64 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.InputStream +import java.io.Serializable +import java.util.* + + +const val APK_MIME_TYPE = "application/vnd.android.package-archive" internal class DocumentsContractApi(private val plugin: SharedStoragePlugin) : - MethodChannel.MethodCallHandler, Listenable, ActivityListener { + MethodChannel.MethodCallHandler, Listenable, ActivityListener { private var channel: MethodChannel? = null companion object { private const val CHANNEL = "documentscontract" } + private fun createTempUriFile(sourceUri: Uri, callback: (File?) -> Unit) { + try { + val destinationFilename: String = UUID.randomUUID().toString() + + val tempDestinationFile = + File(plugin.context.cacheDir.path, destinationFilename) + + plugin.context.contentResolver.openInputStream(sourceUri)?.use { + createFileFromStream(it, tempDestinationFile) + } + callback(tempDestinationFile) + } catch (_: FileNotFoundException) { + callback(null) + } + } + + private fun createFileFromStream(ins: InputStream, destination: File?) { + FileOutputStream(destination).use { fileOutputStream -> + val buffer = ByteArray(4096) + var length: Int + while (ins.read(buffer).also { length = it } > 0) { + fileOutputStream.write(buffer, 0, length) + } + fileOutputStream.flush() + } + } + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { GET_DOCUMENT_THUMBNAIL -> { - if (Build.VERSION.SDK_INT >= API_21) { - try { - val uri = Uri.parse(call.argument("uri")) - val width = call.argument("width")!! - val height = call.argument("height")!! - - val bitmap = DocumentsContract.getDocumentThumbnail( - plugin.context.contentResolver, - uri, - Point(width, height), - null - ) - - if (bitmap != null) { - CoroutineScope(Dispatchers.Default).launch { - val base64 = bitmapToBase64(bitmap) - - val data = - mapOf( - "base64" to base64, - "uri" to "$uri", - "width" to bitmap.width, - "height" to bitmap.height, - "byteCount" to bitmap.byteCount, - "density" to bitmap.density - ) - - launch(Dispatchers.Main) { result.success(data) } - } + val uri = Uri.parse(call.argument("uri")) + val mimeType: String? = plugin.context.contentResolver.getType(uri) + + if (mimeType == APK_MIME_TYPE) { + getThumbnailForApkFile(call, result, uri) + } else { + if (Build.VERSION.SDK_INT >= API_21) { + getThumbnailForApi24(call, result) + } else { + result.notSupported(call.method, API_21) + } + } + } + } + } + + private fun getThumbnailForApkFile( + call: MethodCall, + result: MethodChannel.Result, + uri: Uri + ) { + CoroutineScope(Dispatchers.IO).launch { + createTempUriFile(uri) { + if (it == null) { + launch(Dispatchers.Main) { result.success(null) } + return@createTempUriFile + } + + kotlin.runCatching { + val packageManager: PackageManager = + plugin.context.packageManager + val packageInfo: PackageInfo? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackageArchiveInfo( + it.path, + PackageManager.PackageInfoFlags.of(0) + ) } else { - result.success(null) + @Suppress("DEPRECATION") + packageManager.getPackageArchiveInfo( + it.path, + 0 + ) } - } catch(e: IllegalArgumentException) { - // Tried to load thumbnail of a folder. - result.success(null) + + if (packageInfo == null) { + if (it.exists()) it.delete() + return@createTempUriFile result.success(null) } + + // Parse the apk and to get the icon later on + packageInfo.applicationInfo.sourceDir = it.path + packageInfo.applicationInfo.publicSourceDir = it.path + + val apkIcon: Drawable = + packageInfo.applicationInfo.loadIcon(packageManager) + + val bitmap: Bitmap = drawableToBitmap(apkIcon) + + val data = bitmap.generateSerializableBitmapData(uri) + + if (it.exists()) it.delete() + + launch(Dispatchers.Main) { result.success(data) } + } + + try { + } catch (e: FileNotFoundException) { + // The target file apk is invalid + launch(Dispatchers.Main) { result.success(null) } + } + } + } + } + + private fun getThumbnailForApi24( + call: MethodCall, + result: MethodChannel.Result + ) { + CoroutineScope(Dispatchers.IO).launch { + val uri = Uri.parse(call.argument("uri")) + val width = call.argument("width")!! + val height = call.argument("height")!! + + // run catching because [DocumentsContract.getDocumentThumbnail] + // can throw a [FileNotFoundException]. + kotlin.runCatching { + val bitmap = DocumentsContract.getDocumentThumbnail( + plugin.context.contentResolver, + uri, + Point(width, height), + null + ) + + if (bitmap != null) { + val data = bitmap.generateSerializableBitmapData(uri) + + launch(Dispatchers.Main) { result.success(data) } } else { - result.notSupported(call.method, API_21) + Log.d("GET_DOCUMENT_THUMBNAIL", "bitmap is null") + launch(Dispatchers.Main) { result.success(null) } } } } @@ -95,3 +199,68 @@ internal class DocumentsContractApi(private val plugin: SharedStoragePlugin) : /** Implement if needed */ } } + +fun drawableToBitmap(drawable: Drawable): Bitmap { + if (drawable is BitmapDrawable) { + val bitmapDrawable: BitmapDrawable = drawable + if (bitmapDrawable.bitmap != null) { + return bitmapDrawable.bitmap + } + } + val bitmap: Bitmap = + if (drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) { + Bitmap.createBitmap( + 1, + 1, + Bitmap.Config.ARGB_8888 + ) // Single color bitmap will be created of 1x1 pixel + } else { + Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + } + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap +} + +/** + * Convert bitmap to byte array using ByteBuffer. + * + * This method calls [Bitmap.recycle] so this function will make the bitmap unusable after that. + */ +fun Bitmap.convertToByteArray(): ByteArray { + val stream = ByteArrayOutputStream() + + // Very important, see https://stackoverflow.com/questions/51516310/sending-bitmap-to-flutter-from-android-platform + // Without compressing the raw bitmap, Flutter Image widget cannot decode it correctly and will throw a error. + this.compress(Bitmap.CompressFormat.PNG, 100, stream) + + val byteArray = stream.toByteArray() + + this.recycle() + + return byteArray +} + +fun Bitmap.generateSerializableBitmapData( + uri: Uri, + additional: Map = mapOf() +): Map { + val metadata = mapOf( + "uri" to "$uri", + "width" to this.width, + "height" to this.height, + "byteCount" to this.byteCount, + "density" to this.density + ) + + val bytes: ByteArray = this.convertToByteArray() + + return metadata + additional + mapOf( + "bytes" to bytes + ) +} diff --git a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/DocumentCommon.kt b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/DocumentCommon.kt index 00ddd6a..8a6419a 100644 --- a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/DocumentCommon.kt +++ b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/DocumentCommon.kt @@ -1,4 +1,4 @@ -package io.alexrintt.sharedstorage.storageaccessframework.lib +package io.alexrintt.sharedstorage.deprecated.lib import android.content.ContentResolver import android.content.Context @@ -9,9 +9,8 @@ import android.provider.DocumentsContract import android.util.Base64 import androidx.annotation.RequiresApi import androidx.documentfile.provider.DocumentFile -import io.alexrintt.sharedstorage.plugin.API_19 -import io.alexrintt.sharedstorage.plugin.API_21 -import io.alexrintt.sharedstorage.plugin.API_24 +import io.alexrintt.sharedstorage.plugin.* +import io.alexrintt.sharedstorage.storageaccessframework.* import java.io.ByteArrayOutputStream import java.io.Closeable @@ -168,13 +167,15 @@ fun traverseDirectoryEntries( } while (cursor.moveToNext()) { - val data = mutableMapOf() + val data = mutableMapOf() for (column in projection) { - data[column] = cursorHandlerOf(typeOfColumn(column)!!)( + val columnValue: Any? = cursorHandlerOf(typeOfColumn(column)!!)( cursor, cursor.getColumnIndexOrThrow(column) ) + + data[column] = columnValue } val mimeType = @@ -230,7 +231,6 @@ fun traverseDirectoryEntries( return true } -@RequiresApi(API_19) private fun isDirectory(mimeType: String): Boolean { return DocumentsContract.Document.MIME_TYPE_DIR == mimeType } diff --git a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/DocumentFileColumn.kt b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/DocumentFileColumn.kt index a1b7523..c473bbc 100644 --- a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/DocumentFileColumn.kt +++ b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/DocumentFileColumn.kt @@ -1,7 +1,8 @@ -package io.alexrintt.sharedstorage.storageaccessframework.lib +package io.alexrintt.sharedstorage.deprecated.lib import android.database.Cursor import android.provider.DocumentsContract +import java.lang.NullPointerException private const val PREFIX = "DocumentFileColumn" @@ -20,7 +21,7 @@ enum class DocumentFileColumnType { INT } -fun parseDocumentFileColumn(column: String): DocumentFileColumn? { +fun deserializeDocumentFileColumn(column: String): DocumentFileColumn? { val values = mapOf( "$PREFIX.COLUMN_DOCUMENT_ID" to DocumentFileColumn.ID, "$PREFIX.COLUMN_DISPLAY_NAME" to DocumentFileColumn.DISPLAY_NAME, @@ -33,7 +34,7 @@ fun parseDocumentFileColumn(column: String): DocumentFileColumn? { return values[column] } -fun documentFileColumnToRawString(column: DocumentFileColumn): String? { +fun serializeDocumentFileColumn(column: DocumentFileColumn): String? { val values = mapOf( DocumentFileColumn.ID to "$PREFIX.COLUMN_DOCUMENT_ID", DocumentFileColumn.DISPLAY_NAME to "$PREFIX.COLUMN_DISPLAY_NAME", @@ -46,7 +47,7 @@ fun documentFileColumnToRawString(column: DocumentFileColumn): String? { return values[column] } -fun parseDocumentFileColumn(column: DocumentFileColumn): String { +fun documentFileColumnToActualDocumentsContractEnumString(column: DocumentFileColumn): String { val values = mapOf( DocumentFileColumn.ID to DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentFileColumn.DISPLAY_NAME to DocumentsContract.Document.COLUMN_DISPLAY_NAME, @@ -74,10 +75,34 @@ fun typeOfColumn(column: String): DocumentFileColumnType? { return values[column] } -fun cursorHandlerOf(type: DocumentFileColumnType): (Cursor, Int) -> Any { - when(type) { - DocumentFileColumnType.LONG -> { return { cursor, index -> cursor.getLong(index) } } - DocumentFileColumnType.STRING -> { return { cursor, index -> cursor.getString(index) } } - DocumentFileColumnType.INT -> { return { cursor, index -> cursor.getInt(index) } } +fun cursorHandlerOf(type: DocumentFileColumnType): (Cursor, Int) -> Any? { + when (type) { + DocumentFileColumnType.LONG -> { + return { cursor, index -> + try { + cursor.getLong(index) + } catch (e: NullPointerException) { + null + } + } + } + DocumentFileColumnType.STRING -> { + return { cursor, index -> + try { + cursor.getString(index) + } catch (e: NullPointerException) { + null + } + } + } + DocumentFileColumnType.INT -> { + return { cursor, index -> + try { + cursor.getInt(index) + } catch (e: NullPointerException) { + null + } + } + } } } diff --git a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt index ad61abf..bc52e8f 100644 --- a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt +++ b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt @@ -44,6 +44,7 @@ const val CHILD = "child" * Available DocumentFileHelper Method Channel APIs */ const val OPEN_DOCUMENT_FILE = "openDocumentFile" +const val SHARE_URI = "shareUri" /** * Available Event Channels APIs diff --git a/docs/Usage/Storage Access Framework.md b/docs/Usage/Storage Access Framework.md index f64855b..c882050 100644 --- a/docs/Usage/Storage Access Framework.md +++ b/docs/Usage/Storage Access Framework.md @@ -219,7 +219,7 @@ Basically this allow get the **granted** `Uri`s permissions after the app restar ```dart final List? grantedUris = await persistedUriPermissions(); -if (grantedUris != null) { +if (grantedUris == null) { print('There is no granted Uris'); } else { print('My granted Uris: $grantedUris'); diff --git a/docs/index.md b/docs/index.md index ffc4537..b24d9d6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,6 +30,21 @@ Import: import 'package:shared_storage/shared_storage.dart' as shared_storage; ``` +## Permissions (optional) + +The following APIs require the `REQUEST_INSTALL_PACKAGES` permission in order to prompt the user to install arbitrary APKs: + +- `openDocumentFile` when trying to open APK files. + +If your want to display APK files inside your app and let users install it, then you need this permission, if that's not the case then you can just skip this step. + +```xml + +``` + +> Warning! In some cases the app can become ineligible in the Play Store when using this permission, be sure you need it. Most cases where you think you don't need it you are goddamn right. + + ## Plugin This plugin include **partial** support for the following APIs: @@ -54,14 +69,8 @@ All these APIs are module based, which means they are implemented separadely and ## Support -If you have ideas to share, bugs to report or need support, you can either open an issue or join our [Discord server](https://discord.gg/86GDERXZNS). - -## Android APIs - -Most Flutter plugins use Android API's under the hood. So this plugin does the same, and to call native Android storage APIs the following API's are being used: - -[`đŸ”—android.os.Environment`](https://developer.android.com/reference/android/os/Environment#summary) [`đŸ”—android.provider.MediaStore`](https://developer.android.com/reference/android/provider/MediaStore#summary) [`đŸ”—android.provider.DocumentsProvider`](https://developer.android.com/guide/topics/providers/document-provider) +If you have ideas to share, bugs to report or need support, you can either open an issue or join our [Discord server](https://discord.alexrintt.io). --- -Thanks to all [contributors](https://github.com/alexrintt/shared-storage/tree/release#contributors). \ No newline at end of file +Last but not least, [thanks to all contributors](https://github.com/alexrintt/shared-storage/tree/release#contributors) that makes this plugin a better tool. \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index e9d9fce..180d76f 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: "kotlin-android" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 33 sourceSets { main.java.srcDirs += "src/main/kotlin" diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 9b5789e..d8efeeb 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,19 +1,26 @@ - + + - + - + - + diff --git a/example/lib/screens/file_explorer/file_explorer_card.dart b/example/lib/screens/file_explorer/file_explorer_card.dart index baf2f03..319c003 100644 --- a/example/lib/screens/file_explorer/file_explorer_card.dart +++ b/example/lib/screens/file_explorer/file_explorer_card.dart @@ -36,7 +36,7 @@ class FileExplorerCard extends StatefulWidget { class _FileExplorerCardState extends State { DocumentFile get _file => widget.documentFile; - static const _expandedThumbnailSize = Size.square(150); + static const _kExpandedThumbnailSize = Size.square(150); Uint8List? _thumbnailImageBytes; Size? _thumbnailSize; @@ -51,8 +51,8 @@ class _FileExplorerCardState extends State { final bitmap = await getDocumentThumbnail( uri: uri, - width: _expandedThumbnailSize.width, - height: _expandedThumbnailSize.height, + width: _kExpandedThumbnailSize.width, + height: _kExpandedThumbnailSize.height, ); if (bitmap == null) { @@ -177,7 +177,7 @@ class _FileExplorerCardState extends State { return random.nextInt(1000); } - Widget _buildThumbnail({double? size}) { + Widget _buildThumbnailImage({double? size}) { late Widget thumbnail; if (_thumbnailImageBytes == null) { @@ -207,6 +207,12 @@ class _FileExplorerCardState extends State { } } + return thumbnail; + } + + Widget _buildThumbnail({double? size}) { + final Widget thumbnail = _buildThumbnailImage(size: size); + List children; if (_expanded) { @@ -289,6 +295,30 @@ class _FileExplorerCardState extends State { ); } + Future _shareDocument() async { + await widget.documentFile.share(); + } + + Future _copyTo() async { + final Uri? parentUri = await openDocumentTree(persistablePermission: false); + + if (parentUri != null) { + final DocumentFile? parentDocumentFile = await parentUri.toDocumentFile(); + + if (widget.documentFile.type != null && + widget.documentFile.name != null) { + final DocumentFile? recipient = await parentDocumentFile?.createFile( + mimeType: widget.documentFile.type!, + displayName: widget.documentFile.name!, + ); + + if (recipient != null) { + widget.documentFile.copy(recipient.uri); + } + } + } + } + Widget _buildAvailableActions() { return Wrap( children: [ @@ -309,6 +339,14 @@ class _FileExplorerCardState extends State { : _fileConfirmation('Delete', _deleteDocument), ), if (!_isDirectory) ...[ + ActionButton( + 'Copy to', + onTap: _copyTo, + ), + ActionButton( + 'Share Document', + onTap: _shareDocument, + ), DangerButton( 'Write to File', onTap: _fileConfirmation('Overwite', _overwriteFileContents), diff --git a/example/lib/utils/document_file_utils.dart b/example/lib/utils/document_file_utils.dart index ee27d72..179f3c9 100644 --- a/example/lib/utils/document_file_utils.dart +++ b/example/lib/utils/document_file_utils.dart @@ -46,12 +46,6 @@ extension ShowDocumentFileContents on DocumentFile { if (!mimeTypeOrEmpty.startsWith(kTextMime) && !mimeTypeOrEmpty.startsWith(kImageMime)) { - if (mimeTypeOrEmpty == kApkMime) { - return context.showToast( - 'Requesting to install a package (.apk) is not currently supported, to request this feature open an issue at github.com/alexrintt/shared-storage/issues', - ); - } - return uri.openWithExternalApp(); } diff --git a/lib/src/channels.dart b/lib/src/channels.dart index 2c29515..10263b5 100644 --- a/lib/src/channels.dart +++ b/lib/src/channels.dart @@ -1,28 +1,31 @@ import 'package:flutter/services.dart'; -const kRootChannel = 'io.alexrintt.plugins/sharedstorage'; +const String kRootChannel = 'io.alexrintt.plugins/sharedstorage'; /// `MethodChannels` of this plugin. Flutter use this to communicate with native Android /// Target [Environment] Android API (Legacy and you should avoid it) -const kEnvironmentChannel = MethodChannel('$kRootChannel/environment'); +const MethodChannel kEnvironmentChannel = + MethodChannel('$kRootChannel/environment'); /// Target [MediaStore] Android API -const kMediaStoreChannel = MethodChannel('$kRootChannel/mediastore'); +const MethodChannel kMediaStoreChannel = + MethodChannel('$kRootChannel/mediastore'); /// Target [DocumentFile] from `SAF` Android API (New Android APIs use it) -const kDocumentFileChannel = MethodChannel('$kRootChannel/documentfile'); +const MethodChannel kDocumentFileChannel = + MethodChannel('$kRootChannel/documentfile'); /// Target [DocumentsContract] from `SAF` Android API (New Android APIs use it) -const kDocumentsContractChannel = +const MethodChannel kDocumentsContractChannel = MethodChannel('$kRootChannel/documentscontract'); /// Target [DocumentFileHelper] Shared Storage plugin class (SAF Based) -const kDocumentFileHelperChannel = +const MethodChannel kDocumentFileHelperChannel = MethodChannel('$kRootChannel/documentfilehelper'); /// `EventChannels` of this plugin. Flutter use this to communicate with native Android /// Target [DocumentFile] from `SAF` Android API (New Android APIs use it) -const kDocumentFileEventChannel = +const EventChannel kDocumentFileEventChannel = EventChannel('$kRootChannel/event/documentfile'); diff --git a/lib/src/common/functional_extender.dart b/lib/src/common/functional_extender.dart index 91b1f70..734ef78 100644 --- a/lib/src/common/functional_extender.dart +++ b/lib/src/common/functional_extender.dart @@ -28,7 +28,3 @@ extension FunctionalExtender on T? { return self != null && f(self) ? self : null; } } - -const willbemovedsoon = Deprecated( - 'This method will be moved to another package in a next release.\nBe aware this method will not be removed but moved to another module outside of [saf].', -); diff --git a/lib/src/environment/common.dart b/lib/src/environment/common.dart index 6dca670..7e9f4f0 100644 --- a/lib/src/environment/common.dart +++ b/lib/src/environment/common.dart @@ -4,7 +4,8 @@ import '../channels.dart'; /// Util method to call a given `Environment.` method without arguments Future invokeVoidEnvironmentMethod(String method) async { - final directory = await kEnvironmentChannel.invokeMethod(method); + final String? directory = + await kEnvironmentChannel.invokeMethod(method); if (directory == null) return null; diff --git a/lib/src/environment/environment.dart b/lib/src/environment/environment.dart index e36df37..8152414 100644 --- a/lib/src/environment/environment.dart +++ b/lib/src/environment/environment.dart @@ -11,7 +11,7 @@ import 'environment_directory.dart'; 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) Future getRootDirectory() async { - const kGetRootDirectory = 'getRootDirectory'; + const String kGetRootDirectory = 'getRootDirectory'; return invokeVoidEnvironmentMethod(kGetRootDirectory); } @@ -33,13 +33,15 @@ Future getRootDirectory() async { Future getExternalStoragePublicDirectory( EnvironmentDirectory directory, ) async { - const kGetExternalStoragePublicDirectory = + const String kGetExternalStoragePublicDirectory = 'getExternalStoragePublicDirectory'; - const kDirectoryArg = 'directory'; + const String kDirectoryArg = 'directory'; - final args = {kDirectoryArg: '$directory'}; + final Map args = { + kDirectoryArg: '$directory' + }; - final publicDir = await kEnvironmentChannel.invokeMethod( + final String? publicDir = await kEnvironmentChannel.invokeMethod( kGetExternalStoragePublicDirectory, args, ); @@ -56,7 +58,7 @@ Future getExternalStoragePublicDirectory( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) Future getExternalStorageDirectory() async { - const kGetExternalStorageDirectory = 'getExternalStorageDirectory'; + const String kGetExternalStorageDirectory = 'getExternalStorageDirectory'; return invokeVoidEnvironmentMethod(kGetExternalStorageDirectory); } @@ -68,7 +70,7 @@ Future getExternalStorageDirectory() async { 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) Future getDataDirectory() async { - const kGetDataDirectory = 'getDataDirectory'; + const String kGetDataDirectory = 'getDataDirectory'; return invokeVoidEnvironmentMethod(kGetDataDirectory); } @@ -80,7 +82,7 @@ Future getDataDirectory() async { 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) Future getDownloadCacheDirectory() async { - const kGetDownloadCacheDirectory = 'getDownloadCacheDirectory'; + const String kGetDownloadCacheDirectory = 'getDownloadCacheDirectory'; return invokeVoidEnvironmentMethod(kGetDownloadCacheDirectory); } @@ -92,7 +94,7 @@ Future getDownloadCacheDirectory() async { 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) Future getStorageDirectory() { - const kGetStorageDirectory = 'getStorageDirectory'; + const String kGetStorageDirectory = 'getStorageDirectory'; return invokeVoidEnvironmentMethod(kGetStorageDirectory); } diff --git a/lib/src/environment/environment_directory.dart b/lib/src/environment/environment_directory.dart index 2e1657c..9f27d16 100644 --- a/lib/src/environment/environment_directory.dart +++ b/lib/src/environment/environment_directory.dart @@ -15,7 +15,7 @@ class EnvironmentDirectory { final String id; - static const _kPrefix = 'EnvironmentDirectory'; + static const String _kPrefix = 'EnvironmentDirectory'; /// Available for Android [4.1 to 9.0] /// @@ -23,7 +23,8 @@ class EnvironmentDirectory { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const alarms = EnvironmentDirectory._('$_kPrefix.Alarms'); + static const EnvironmentDirectory alarms = + EnvironmentDirectory._('$_kPrefix.Alarms'); /// Available for Android [4.1 to 9] /// @@ -32,7 +33,8 @@ class EnvironmentDirectory { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const dcim = EnvironmentDirectory._('$_kPrefix.DCIM'); + static const EnvironmentDirectory dcim = + EnvironmentDirectory._('$_kPrefix.DCIM'); /// Available for Android [4.1 to 9] /// @@ -41,7 +43,8 @@ class EnvironmentDirectory { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const downloads = EnvironmentDirectory._('$_kPrefix.Downloads'); + static const EnvironmentDirectory downloads = + EnvironmentDirectory._('$_kPrefix.Downloads'); /// Available for Android [4.1 to 9] /// @@ -49,7 +52,8 @@ class EnvironmentDirectory { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const movies = EnvironmentDirectory._('$_kPrefix.Movies'); + static const EnvironmentDirectory movies = + EnvironmentDirectory._('$_kPrefix.Movies'); /// Available for Android [4.1 to 9] /// @@ -57,7 +61,8 @@ class EnvironmentDirectory { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const music = EnvironmentDirectory._('$_kPrefix.Music'); + static const EnvironmentDirectory music = + EnvironmentDirectory._('$_kPrefix.Music'); /// Available for Android [4.1 to 9] /// @@ -65,7 +70,7 @@ class EnvironmentDirectory { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const notifications = + static const EnvironmentDirectory notifications = EnvironmentDirectory._('$_kPrefix.Notifications'); /// Available for Android [4.1 to 9] @@ -74,7 +79,8 @@ class EnvironmentDirectory { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const pictures = EnvironmentDirectory._('$_kPrefix.Pictures'); + static const EnvironmentDirectory pictures = + EnvironmentDirectory._('$_kPrefix.Pictures'); /// Available for Android [4.1 to 9] /// @@ -82,7 +88,8 @@ class EnvironmentDirectory { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const podcasts = EnvironmentDirectory._('$_kPrefix.Podcasts'); + static const EnvironmentDirectory podcasts = + EnvironmentDirectory._('$_kPrefix.Podcasts'); /// Available for Android [4.1 to 9] /// @@ -90,7 +97,8 @@ class EnvironmentDirectory { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const ringtones = EnvironmentDirectory._('$_kPrefix.Ringtones'); + static const EnvironmentDirectory ringtones = + EnvironmentDirectory._('$_kPrefix.Ringtones'); @override bool operator ==(Object other) { diff --git a/lib/src/media_store/media_store.dart b/lib/src/media_store/media_store.dart index 08a12df..4d5a107 100644 --- a/lib/src/media_store/media_store.dart +++ b/lib/src/media_store/media_store.dart @@ -12,12 +12,14 @@ import 'media_store_collection.dart'; Future getMediaStoreContentDirectory( MediaStoreCollection collection, ) async { - const kGetMediaStoreContentDirectory = 'getMediaStoreContentDirectory'; - const kCollectionArg = 'collection'; + const String kGetMediaStoreContentDirectory = 'getMediaStoreContentDirectory'; + const String kCollectionArg = 'collection'; - final args = {kCollectionArg: '$collection'}; + final Map args = { + kCollectionArg: '$collection' + }; - final publicDir = await kMediaStoreChannel.invokeMethod( + final String? publicDir = await kMediaStoreChannel.invokeMethod( kGetMediaStoreContentDirectory, args, ); diff --git a/lib/src/media_store/media_store_collection.dart b/lib/src/media_store/media_store_collection.dart index ed694cc..c960009 100644 --- a/lib/src/media_store/media_store_collection.dart +++ b/lib/src/media_store/media_store_collection.dart @@ -6,7 +6,7 @@ class MediaStoreCollection { final String id; - static const _kPrefix = 'MediaStoreCollection'; + static const String _kPrefix = 'MediaStoreCollection'; /// Available for Android [10 to 12] /// @@ -15,7 +15,8 @@ class MediaStoreCollection { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const audio = MediaStoreCollection._('$_kPrefix.Audio'); + static const MediaStoreCollection audio = + MediaStoreCollection._('$_kPrefix.Audio'); /// Available for Android [10 to 12] /// @@ -24,7 +25,8 @@ class MediaStoreCollection { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const downloads = MediaStoreCollection._('$_kPrefix.Downloads'); + static const MediaStoreCollection downloads = + MediaStoreCollection._('$_kPrefix.Downloads'); /// Available for Android [10 to 12] /// @@ -33,7 +35,8 @@ class MediaStoreCollection { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const images = MediaStoreCollection._('$_kPrefix.Images'); + static const MediaStoreCollection images = + MediaStoreCollection._('$_kPrefix.Images'); /// Available for Android [10 to 12] /// @@ -42,7 +45,8 @@ class MediaStoreCollection { @Deprecated( 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', ) - static const video = MediaStoreCollection._('$_kPrefix.Video'); + static const MediaStoreCollection video = + MediaStoreCollection._('$_kPrefix.Video'); @override bool operator ==(Object other) { diff --git a/lib/src/saf/api/barrel.dart b/lib/src/saf/api/barrel.dart index 1bf0c16..275d5f7 100644 --- a/lib/src/saf/api/barrel.dart +++ b/lib/src/saf/api/barrel.dart @@ -8,6 +8,7 @@ export './open.dart'; export './persisted.dart'; export './rename.dart'; export './search.dart'; +export './share.dart'; export './tree.dart'; export './utility.dart'; export './write.dart'; diff --git a/lib/src/saf/api/content.dart b/lib/src/saf/api/content.dart index 15e9f10..338b2c3 100644 --- a/lib/src/saf/api/content.dart +++ b/lib/src/saf/api/content.dart @@ -12,7 +12,7 @@ Future getDocumentContentAsString( Uri uri, { bool throwIfError = false, }) async { - final bytes = await getDocumentContent(uri); + final Uint8List? bytes = await getDocumentContent(uri); if (bytes == null) return null; @@ -42,14 +42,14 @@ Future getDocumentThumbnail({ required double width, required double height, }) async { - final args = { + final Map args = { 'uri': '$uri', 'width': width, 'height': height, }; - final bitmap = await kDocumentsContractChannel + final Map? bitmap = await kDocumentsContractChannel .invokeMapMethod('getDocumentThumbnail', args); - return bitmap?.apply((b) => DocumentBitmap.fromMap(b)); + return bitmap?.apply((Map b) => DocumentBitmap.fromMap(b)); } diff --git a/lib/src/saf/api/copy.dart b/lib/src/saf/api/copy.dart index 830abc6..527cc18 100644 --- a/lib/src/saf/api/copy.dart +++ b/lib/src/saf/api/copy.dart @@ -7,7 +7,10 @@ import '../models/barrel.dart'; /// This API uses the `createFile` and `getDocumentContent` API's behind the scenes. /// {@endtemplate} Future copy(Uri uri, Uri destination) async { - final args = {'uri': '$uri', 'destination': '$destination'}; + final Map args = { + 'uri': '$uri', + 'destination': '$destination' + }; return invokeMapMethod('copy', args); } diff --git a/lib/src/saf/api/create.dart b/lib/src/saf/api/create.dart index 4ec1638..64d09a6 100644 --- a/lib/src/saf/api/create.dart +++ b/lib/src/saf/api/create.dart @@ -13,15 +13,16 @@ import '../models/barrel.dart'; /// [Refer to details](https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile#createDirectory%28java.lang.String%29). /// {@endtemplate} Future createDirectory(Uri parentUri, String displayName) async { - final args = { + final Map args = { 'uri': '$parentUri', 'displayName': displayName, }; - final createdDocumentFile = await kDocumentFileChannel + final Map? createdDocumentFile = await kDocumentFileChannel .invokeMapMethod('createDirectory', args); - return createdDocumentFile?.apply((c) => DocumentFile.fromMap(c)); + return createdDocumentFile + ?.apply((Map c) => DocumentFile.fromMap(c)); } /// {@template sharedstorage.saf.createFile} @@ -70,9 +71,9 @@ Future createFileAsBytes( required String displayName, required Uint8List bytes, }) async { - final directoryUri = '$parentUri'; + final String directoryUri = '$parentUri'; - final args = { + final Map args = { 'mimeType': mimeType, 'content': bytes, 'displayName': displayName, diff --git a/lib/src/saf/api/grant.dart b/lib/src/saf/api/grant.dart index db7b559..6efb59c 100644 --- a/lib/src/saf/api/grant.dart +++ b/lib/src/saf/api/grant.dart @@ -17,18 +17,18 @@ Future openDocumentTree({ bool persistablePermission = true, Uri? initialUri, }) async { - const kOpenDocumentTree = 'openDocumentTree'; + const String kOpenDocumentTree = 'openDocumentTree'; - final args = { + final Map args = { 'grantWritePermission': grantWritePermission, 'persistablePermission': persistablePermission, if (initialUri != null) 'initialUri': '$initialUri', }; - final selectedDirectoryUri = + final String? selectedDirectoryUri = await kDocumentFileChannel.invokeMethod(kOpenDocumentTree, args); - return selectedDirectoryUri?.apply((e) => Uri.parse(e)); + return selectedDirectoryUri?.apply((String e) => Uri.parse(e)); } /// [Refer to details](https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT). @@ -39,9 +39,9 @@ Future?> openDocument({ String mimeType = '*/*', bool multiple = false, }) async { - const kOpenDocument = 'openDocument'; + const String kOpenDocument = 'openDocument'; - final args = { + final Map args = { if (initialUri != null) 'initialUri': '$initialUri', 'grantWritePermission': grantWritePermission, 'persistablePermission': persistablePermission, @@ -49,9 +49,11 @@ Future?> openDocument({ 'multiple': multiple, }; - final selectedUriList = + final List? selectedUriList = await kDocumentFileChannel.invokeListMethod(kOpenDocument, args); - return selectedUriList - ?.apply((e) => e.map((e) => Uri.parse(e as String)).toList()); + return selectedUriList?.apply( + (List list) => + list.map((dynamic e) => Uri.parse(e as String)).toList(), + ); } diff --git a/lib/src/saf/api/info.dart b/lib/src/saf/api/info.dart index 00535c9..1da646c 100644 --- a/lib/src/saf/api/info.dart +++ b/lib/src/saf/api/info.dart @@ -17,14 +17,14 @@ Future documentLength(Uri uri) async => kDocumentFileChannel /// [Refer to details](https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile#lastModified%28%29). /// {@endtemplate} Future lastModified(Uri uri) async { - const kLastModified = 'lastModified'; + const String kLastModified = 'lastModified'; - final inMillisecondsSinceEpoch = await kDocumentFileChannel + final int? inMillisecondsSinceEpoch = await kDocumentFileChannel .invokeMethod(kLastModified, {'uri': '$uri'}); return inMillisecondsSinceEpoch - ?.takeIf((i) => i > 0) - ?.apply((i) => DateTime.fromMillisecondsSinceEpoch(i)); + ?.takeIf((int i) => i > 0) + ?.apply((int i) => DateTime.fromMillisecondsSinceEpoch(i)); } /// {@template sharedstorage.saf.canRead} diff --git a/lib/src/saf/api/persisted.dart b/lib/src/saf/api/persisted.dart index e3a62d1..f055203 100644 --- a/lib/src/saf/api/persisted.dart +++ b/lib/src/saf/api/persisted.dart @@ -10,11 +10,17 @@ import '../models/barrel.dart'; /// To remove an persisted [Uri] call `releasePersistableUriPermission`. /// {@endtemplate} Future?> persistedUriPermissions() async { - final persistedUriPermissions = - await kDocumentFileChannel.invokeListMethod('persistedUriPermissions'); + final List? persistedUriPermissions = await kDocumentFileChannel + .invokeListMethod('persistedUriPermissions'); return persistedUriPermissions?.apply( - (p) => p.map((e) => UriPermission.fromMap(Map.from(e as Map))).toList(), + (List p) => p + .map( + (dynamic e) => UriPermission.fromMap( + Map.from(e as Map), + ), + ) + .toList(), ); } @@ -26,9 +32,11 @@ Future?> persistedUriPermissions() async { /// of allowed [Uri]s then will verify if the [uri] is included in. /// {@endtemplate} Future isPersistedUri(Uri uri) async { - final persistedUris = await persistedUriPermissions(); + final List? persistedUris = await persistedUriPermissions(); - return persistedUris?.any((persistedUri) => persistedUri.uri == uri) ?? false; + return persistedUris + ?.any((UriPermission persistedUri) => persistedUri.uri == uri) ?? + false; } /// {@template sharedstorage.saf.releasePersistableUriPermission} diff --git a/lib/src/saf/api/search.dart b/lib/src/saf/api/search.dart index 397408f..09f963e 100644 --- a/lib/src/saf/api/search.dart +++ b/lib/src/saf/api/search.dart @@ -9,7 +9,7 @@ import '../common/barrel.dart'; /// [Refer to details](https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile#findFile%28java.lang.String%29). /// {@endtemplate} Future findFile(Uri directoryUri, String displayName) async { - final args = { + final Map args = { 'uri': '$directoryUri', 'displayName': displayName, }; diff --git a/lib/src/saf/api/share.dart b/lib/src/saf/api/share.dart new file mode 100644 index 0000000..dd04c4f --- /dev/null +++ b/lib/src/saf/api/share.dart @@ -0,0 +1,81 @@ +import 'dart:io'; + +import '../../channels.dart'; + +/// {@template sharedstorage.saf.share} +/// Start share intent for the given [uri]. +/// +/// To share a file, use [Uri.parse] passing the file absolute path as argument. +/// +/// Note that this method can only share files that your app has permission over, +/// either by being in your app domain (e.g file from your app cache) or that is granted by [openDocumentTree]. +/// +/// Usage: +/// +/// ```dart +/// try { +/// await shareUriOrFile( +/// uri: uri, +/// filePath: path, +/// file: file, +/// ); +/// } on PlatformException catch (e) { +/// // The user clicked twice too fast, which created 2 share requests and the second one failed. +/// // Unhandled Exception: PlatformException(Share callback error, prior share-sheet did not call back, did you await it? Maybe use non-result variant, null, null). +/// log('Error when calling [shareFile]: $e'); +/// return; +/// } +/// ``` +/// {@endtemplate} +Future shareUri( + Uri uri, { + String? type, +}) { + final Map args = { + 'uri': '$uri', + 'type': type, + }; + + return kDocumentFileHelperChannel.invokeMethod('shareUri', args); +} + +/// Alias for [shareUri]. +Future shareFile({File? file, String? path}) { + return shareUriOrFile(filePath: path, file: file); +} + +/// Alias for [shareUri]. +Future shareUriOrFile({String? filePath, File? file, Uri? uri}) { + return shareUri( + _getShareableUriFrom(file: file, filePath: filePath, uri: uri), + ); +} + +/// Helper function to get the shareable URI from [file], [filePath] or the [uri] itself. +/// +/// Usage: +/// +/// ```dart +/// shareUri(getShareableUri(...)); +/// ``` +Uri _getShareableUriFrom({String? filePath, File? file, Uri? uri}) { + if (filePath == null && file == null && uri == null) { + throw ArgumentError.value( + null, + 'getShareableUriFrom', + 'Tried to call [getShareableUriFrom] or with all arguments ({String? filePath, File? file, Uri? uri}) set to [null].', + ); + } + + late Uri target; + + if (uri != null) { + target = uri; + } else if (filePath != null) { + target = Uri.parse(filePath); + } else if (file != null) { + target = Uri.parse(file.absolute.path); + } + + return target; +} diff --git a/lib/src/saf/api/tree.dart b/lib/src/saf/api/tree.dart index 4624f78..c7c12e8 100644 --- a/lib/src/saf/api/tree.dart +++ b/lib/src/saf/api/tree.dart @@ -1,5 +1,4 @@ import '../../channels.dart'; -import '../../common/functional_extender.dart'; import '../common/barrel.dart'; import '../models/barrel.dart'; @@ -32,16 +31,22 @@ Stream listFiles( Uri uri, { required List columns, }) { - final args = { + final Map args = { 'uri': '$uri', 'event': 'listFiles', - 'columns': columns.map((e) => '$e').toList(), + 'columns': columns.map((DocumentFileColumn e) => '$e').toList(), }; - final onCursorRowResult = + final Stream onCursorRowResult = kDocumentFileEventChannel.receiveBroadcastStream(args); - return onCursorRowResult.map((e) => DocumentFile.fromMap(Map.from(e as Map))); + return onCursorRowResult.map( + (dynamic e) => DocumentFile.fromMap( + Map.from( + e as Map, + ), + ), + ); } /// {@template sharedstorage.saf.child} @@ -54,13 +59,12 @@ Stream listFiles( /// /// [Refer to details](https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile#fromTreeUri%28android.content.Context,%20android.net.Uri%29) /// {@endtemplate} -@willbemovedsoon Future child( Uri uri, String path, { bool requiresWriteAccess = false, }) async { - final args = { + final Map args = { 'uri': '$uri', 'path': path, 'requiresWriteAccess': requiresWriteAccess, diff --git a/lib/src/saf/api/write.dart b/lib/src/saf/api/write.dart index 1c801ec..c4b3e00 100644 --- a/lib/src/saf/api/write.dart +++ b/lib/src/saf/api/write.dart @@ -16,10 +16,10 @@ Future writeToFileAsBytes( required Uint8List bytes, FileMode? mode, }) async { - final writeMode = + final String writeMode = mode == FileMode.append || mode == FileMode.writeOnlyAppend ? 'wa' : 'wt'; - final args = { + final Map args = { 'uri': '$uri', 'content': bytes, 'mode': writeMode, diff --git a/lib/src/saf/common/method_channel_helper.dart b/lib/src/saf/common/method_channel_helper.dart index 7fd2511..f0f68f8 100644 --- a/lib/src/saf/common/method_channel_helper.dart +++ b/lib/src/saf/common/method_channel_helper.dart @@ -7,7 +7,7 @@ Future invokeMapMethod( String method, Map args, ) async { - final documentMap = + final Map? documentMap = await kDocumentFileChannel.invokeMapMethod(method, args); if (documentMap == null) return null; diff --git a/lib/src/saf/models/document_bitmap.dart b/lib/src/saf/models/document_bitmap.dart index 40aec2c..b798551 100644 --- a/lib/src/saf/models/document_bitmap.dart +++ b/lib/src/saf/models/document_bitmap.dart @@ -1,29 +1,28 @@ -import 'dart:convert'; import 'dart:typed_data'; /// Represent the bitmap/image of a document. /// /// Usually the thumbnail of the document. /// -/// The bitmap is represented as a base64 string. +/// The bitmap is represented as a byte array [Uint8List]. /// /// Should be used to show a list/grid preview of a file list. /// /// See also [getDocumentThumbnail]. class DocumentBitmap { const DocumentBitmap({ - required this.base64, required this.uri, required this.width, required this.height, required this.byteCount, required this.density, + required this.bytes, }); factory DocumentBitmap.fromMap(Map map) { return DocumentBitmap( uri: (() { - final uri = map['uri'] as String?; + final String? uri = map['uri'] as String?; if (uri == null) return null; @@ -31,38 +30,30 @@ class DocumentBitmap { })(), width: map['width'] as int?, height: map['height'] as int?, - base64: map['base64'] as String?, + bytes: map['bytes'] as Uint8List?, byteCount: map['byteCount'] as int?, density: map['density'] as int?, ); } - final String? base64; final Uri? uri; final int? width; final int? height; final int? byteCount; final int? density; + final Uint8List? bytes; Map toMap() { return { 'uri': '$uri', 'width': width, 'height': height, - 'base64': base64, + 'bytes': bytes, 'byteCount': byteCount, 'density': density, }; } - Uint8List? get bytes { - if (base64 == null) return null; - - const codec = Base64Codec(); - - return codec.decode(base64!); - } - @override bool operator ==(Object other) { if (other is! DocumentBitmap) return false; @@ -72,10 +63,10 @@ class DocumentBitmap { other.height == height && other.uri == uri && other.density == density && - other.base64 == base64; + other.bytes == bytes; } @override int get hashCode => - Object.hash(width, height, uri, density, byteCount, base64); + Object.hash(width, height, uri, density, byteCount, bytes); } diff --git a/lib/src/saf/models/document_file.dart b/lib/src/saf/models/document_file.dart index 3304c26..4e3ba7e 100644 --- a/lib/src/saf/models/document_file.dart +++ b/lib/src/saf/models/document_file.dart @@ -31,7 +31,8 @@ class DocumentFile { factory DocumentFile.fromMap(Map map) { return DocumentFile( - parentUri: (map['parentUri'] as String?)?.apply((p) => Uri.parse(p)), + parentUri: + (map['parentUri'] as String?)?.apply((String p) => Uri.parse(p)), id: map['id'] as String?, isDirectory: map['isDirectory'] as bool?, isFile: map['isFile'] as bool?, @@ -41,7 +42,7 @@ class DocumentFile { uri: Uri.parse(map['uri'] as String), size: map['size'] as int?, lastModified: (map['lastModified'] as int?) - ?.apply((l) => DateTime.fromMillisecondsSinceEpoch(l)), + ?.apply((int l) => DateTime.fromMillisecondsSinceEpoch(l)), ); } @@ -95,7 +96,6 @@ class DocumentFile { static Future fromTreeUri(Uri uri) => saf.fromTreeUri(uri); /// {@macro sharedstorage.saf.child} - @willbemovedsoon Future child( String path, { bool requiresWriteAccess = false, @@ -113,6 +113,9 @@ class DocumentFile { /// {@macro sharedstorage.saf.canRead} Future canRead() async => saf.canRead(uri); + /// {@macro sharedstorage.saf.share} + Future share() async => saf.shareUri(uri); + /// {@macro sharedstorage.saf.canWrite} Future canWrite() async => saf.canWrite(uri); @@ -226,7 +229,7 @@ class DocumentFile { Future parentFile() => saf.parentFile(uri); Map toMap() { - return { + return { 'id': id, 'uri': '$uri', 'parentUri': '$parentUri', diff --git a/lib/src/saf/models/document_file_column.dart b/lib/src/saf/models/document_file_column.dart index e939c86..2acbe20 100644 --- a/lib/src/saf/models/document_file_column.dart +++ b/lib/src/saf/models/document_file_column.dart @@ -22,7 +22,7 @@ enum DocumentFileColumn { const DocumentFileColumn(this.androidEnumItemName); - static const _kAndroidEnumTypeName = 'DocumentFileColumn'; + static const String _kAndroidEnumTypeName = 'DocumentFileColumn'; final String androidEnumItemName; diff --git a/lib/src/saf/models/uri_permission.dart b/lib/src/saf/models/uri_permission.dart index 5ec93d9..9cfd1e2 100644 --- a/lib/src/saf/models/uri_permission.dart +++ b/lib/src/saf/models/uri_permission.dart @@ -61,7 +61,7 @@ class UriPermission { @override int get hashCode => Object.hashAll( - [ + [ isReadPermission, isWritePermission, persistedTime, diff --git a/pubspec.yaml b/pubspec.yaml index d332239..075afe0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: shared_storage description: "Flutter plugin to work with external storage and privacy-friendly APIs." -version: 0.8.0 +version: 0.9.0 homepage: https://github.com/alexrintt/shared-storage repository: https://github.com/alexrintt/shared-storage issue_tracker: https://github.com/alexrintt/shared-storage/issues