diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/SharedStoragePlugin.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/SharedStoragePlugin.kt index e79656e..e036bbf 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/SharedStoragePlugin.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/SharedStoragePlugin.kt @@ -6,22 +6,14 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.lakscastro.sharedstorage.environment.EnvironmentApi -import io.lakscastro.sharedstorage.mediastore.MediaStoreApi -import io.lakscastro.sharedstorage.storageaccessframework.StorageAccessFrameworkApi +import io.lakscastro.sharedstorage.plugin.SharedStorageApi const val ROOT_CHANNEL = "io.lakscastro.plugins/sharedstorage" /** Flutter plugin Kotlin implementation `SharedStoragePlugin` */ class SharedStoragePlugin : FlutterPlugin, ActivityAware { - /** `Environment` API channel */ - private val environmentApi = EnvironmentApi(this) - - /** `MediaStore` API channel */ - private val mediaStoreApi = MediaStoreApi(this) - /** `DocumentFile` API channel */ - private val storageAccessFrameworkApi = StorageAccessFrameworkApi(this) + private val sharedStorageApi = SharedStorageApi(this) lateinit var context: Context var binding: ActivityPluginBinding? = null @@ -30,25 +22,21 @@ class SharedStoragePlugin : FlutterPlugin, ActivityAware { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPluginBinding) { context = flutterPluginBinding.applicationContext - environmentApi.startListening(flutterPluginBinding.binaryMessenger) - mediaStoreApi.startListening(flutterPluginBinding.binaryMessenger) - storageAccessFrameworkApi.startListening(flutterPluginBinding.binaryMessenger) + sharedStorageApi.startListening(flutterPluginBinding.binaryMessenger) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { this.binding = binding - storageAccessFrameworkApi.startListeningToActivity() + sharedStorageApi.startListeningToActivity() } override fun onDetachedFromEngine(@NonNull binding: FlutterPluginBinding) { - environmentApi.stopListening() - mediaStoreApi.stopListening() - storageAccessFrameworkApi.stopListening() + sharedStorageApi.stopListening() } override fun onDetachedFromActivityForConfigChanges() { - storageAccessFrameworkApi.stopListeningToActivity() + sharedStorageApi.stopListeningToActivity() } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/ActivityListener.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/common/ActivityListener.kt similarity index 85% rename from android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/ActivityListener.kt rename to android/src/main/kotlin/io/lakscastro/sharedstorage/common/ActivityListener.kt index 3169e75..1f6deb9 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/ActivityListener.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/common/ActivityListener.kt @@ -1,4 +1,4 @@ -package io.lakscastro.sharedstorage.plugin +package io.lakscastro.sharedstorage.common /** * Interface shared across API classes to make intuitive and clean [init] and [dispose] plugin diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/Common.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/common/Common.kt similarity index 94% rename from android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/Common.kt rename to android/src/main/kotlin/io/lakscastro/sharedstorage/common/Common.kt index 589e3f8..0b28418 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/Common.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/common/Common.kt @@ -1,4 +1,4 @@ -package io.lakscastro.sharedstorage.plugin +package io.lakscastro.sharedstorage.common import android.os.Build import io.flutter.plugin.common.MethodChannel diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/Listenable.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/common/Listenable.kt similarity index 87% rename from android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/Listenable.kt rename to android/src/main/kotlin/io/lakscastro/sharedstorage/common/Listenable.kt index 2918297..add9587 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/Listenable.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/common/Listenable.kt @@ -1,4 +1,4 @@ -package io.lakscastro.sharedstorage.plugin +package io.lakscastro.sharedstorage.common import io.flutter.plugin.common.BinaryMessenger diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/PluginConstant.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/common/PluginConstant.kt similarity index 93% rename from android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/PluginConstant.kt rename to android/src/main/kotlin/io/lakscastro/sharedstorage/common/PluginConstant.kt index c220284..1bb6122 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/PluginConstant.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/common/PluginConstant.kt @@ -1,4 +1,4 @@ -package io.lakscastro.sharedstorage.plugin +package io.lakscastro.sharedstorage.common import android.os.Build diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/environment/EnvironmentApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/environment/EnvironmentApi.kt deleted file mode 100644 index 013e09b..0000000 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/environment/EnvironmentApi.kt +++ /dev/null @@ -1,100 +0,0 @@ -package io.lakscastro.sharedstorage.environment - -import android.os.Build -import android.os.Environment -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.lakscastro.sharedstorage.ROOT_CHANNEL -import io.lakscastro.sharedstorage.SharedStoragePlugin -import io.lakscastro.sharedstorage.plugin.API_30 -import io.lakscastro.sharedstorage.plugin.Listenable -import io.lakscastro.sharedstorage.plugin.notSupported -import java.io.File - -class EnvironmentApi(val plugin: SharedStoragePlugin) : - MethodChannel.MethodCallHandler, Listenable { - private var channel: MethodChannel? = null - - companion object { - const val GET_EXTERNAL_STORAGE_PUBLIC_DIRECTORY = "getExternalStoragePublicDirectory" - const val GET_ROOT_DIRECTORY = "getRootDirectory" - const val GET_EXTERNAL_STORAGE_DIRECTORY = "getExternalStorageDirectory" - const val GET_DATA_DIRECTORY = "getDataDirectory" - const val GET_DOWNLOAD_CACHE_DIRECTORY = "getDownloadCacheDirectory" - const val GET_STORAGE_DIRECTORY = "getStorageDirectory" - - const val CHANNEL = "environment" - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - GET_EXTERNAL_STORAGE_PUBLIC_DIRECTORY -> - getExternalStoragePublicDirectory(result, call.argument("directory") as String) - GET_EXTERNAL_STORAGE_DIRECTORY -> getExternalStorageDirectory(result) - GET_ROOT_DIRECTORY -> getRootDirectory(result) - GET_DATA_DIRECTORY -> getDataDirectory(result) - GET_STORAGE_DIRECTORY -> getStorageDirectory(result) - GET_DOWNLOAD_CACHE_DIRECTORY -> getDownloadCacheDirectory(result) - else -> result.notImplemented() - } - } - - /** Deprecated Android API, use only if you know exactly what you need */ - private fun getExternalStoragePublicDirectory(result: MethodChannel.Result, directory: String) = - result.success(environmentDirectoryOf(directory).absolutePath) - - private fun getDataDirectory(result: MethodChannel.Result) = - result.success(Environment.getDataDirectory().absolutePath) - - private fun getStorageDirectory(result: MethodChannel.Result) { - if (Build.VERSION.SDK_INT >= API_30) { - result.success(Environment.getStorageDirectory().absolutePath) - } else { - result.notSupported("getStorageDirectory", API_30) - } - } - - private fun getDownloadCacheDirectory(result: MethodChannel.Result) = - result.success(Environment.getDownloadCacheDirectory().absolutePath) - - /** Deprecated Android API, use only if you know exactly what you need */ - private fun getExternalStorageDirectory(result: MethodChannel.Result) = - result.success(Environment.getExternalStorageDirectory().absolutePath) - - private fun getRootDirectory(result: MethodChannel.Result) = - result.success(Environment.getRootDirectory().absolutePath) - - private fun environmentDirectoryOf(directory: String): File { - val mapper = - mapOf( - "EnvironmentDirectory.Alarms" to Environment.DIRECTORY_ALARMS, - "EnvironmentDirectory.DCIM" to Environment.DIRECTORY_DCIM, - "EnvironmentDirectory.Downloads" to Environment.DIRECTORY_DOWNLOADS, - "EnvironmentDirectory.Movies" to Environment.DIRECTORY_MOVIES, - "EnvironmentDirectory.Music" to Environment.DIRECTORY_MUSIC, - "EnvironmentDirectory.Notifications" to Environment.DIRECTORY_NOTIFICATIONS, - "EnvironmentDirectory.Pictures" to Environment.DIRECTORY_PICTURES, - "EnvironmentDirectory.Podcasts" to Environment.DIRECTORY_PODCASTS, - "EnvironmentDirectory.Ringtones" to Environment.DIRECTORY_RINGTONES - ) - - return Environment.getExternalStoragePublicDirectory(mapper[directory] ?: directory) - } - - override fun startListening(binaryMessenger: BinaryMessenger) { - if (channel != null) { - stopListening() - } - - channel = MethodChannel(binaryMessenger, "$ROOT_CHANNEL/$CHANNEL") - channel?.setMethodCallHandler(this) - } - - override fun stopListening() { - if (channel == null) return - - channel?.setMethodCallHandler(null) - channel = null - } -} diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/mediastore/MediaStoreApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/mediastore/MediaStoreApi.kt deleted file mode 100644 index 75e6044..0000000 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/mediastore/MediaStoreApi.kt +++ /dev/null @@ -1,70 +0,0 @@ -package io.lakscastro.sharedstorage.mediastore - -import android.os.Build -import android.provider.MediaStore -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.lakscastro.sharedstorage.ROOT_CHANNEL -import io.lakscastro.sharedstorage.SharedStoragePlugin -import io.lakscastro.sharedstorage.plugin.API_29 -import io.lakscastro.sharedstorage.plugin.Listenable - -class MediaStoreApi(val plugin: SharedStoragePlugin) : MethodChannel.MethodCallHandler, Listenable { - private var channel: MethodChannel? = null - - companion object { - private const val GET_MEDIA_STORE_CONTENT_DIRECTORY = "getMediaStoreContentDirectory" - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - GET_MEDIA_STORE_CONTENT_DIRECTORY -> { - getMediaStoreContentDirectory( - result, - call.argument("collection") as String - ) - } - else -> result.notImplemented() - } - } - - private fun getMediaStoreContentDirectory( - result: MethodChannel.Result, - collection: String - ) = result.success(mediaStoreOf(collection)) - - /** - * Returns the [EXTERNAL_CONTENT_URI] of [MediaStore.] equivalent to [collection] - */ - private fun mediaStoreOf(collection: String): String? { - val mapper = mutableMapOf( - "MediaStoreCollection.Audio" to MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.path, - "MediaStoreCollection.Video" to MediaStore.Video.Media.EXTERNAL_CONTENT_URI.path, - "MediaStoreCollection.Images" to MediaStore.Images.Media.EXTERNAL_CONTENT_URI.path - ) - - if (Build.VERSION.SDK_INT >= API_29) { - mapper["MediaStoreCollection.Downloads"] = - MediaStore.Downloads.EXTERNAL_CONTENT_URI.path - } - - return mapper[collection] - } - - override fun startListening(binaryMessenger: BinaryMessenger) { - if (channel != null) { - stopListening() - } - - channel = MethodChannel(binaryMessenger, "$ROOT_CHANNEL/mediastore") - channel?.setMethodCallHandler(this) - } - - override fun stopListening() { - if (channel == null) return - - channel?.setMethodCallHandler(null) - channel = null - } -} diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/SharedStorageApi.kt similarity index 68% rename from android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt rename to android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/SharedStorageApi.kt index af90e89..baa262e 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/SharedStorageApi.kt @@ -1,6 +1,8 @@ -package io.lakscastro.sharedstorage.storageaccessframework +package io.lakscastro.sharedstorage.plugin +import android.content.ActivityNotFoundException import android.content.Intent +import android.graphics.Point import android.net.Uri import android.os.Build import android.provider.DocumentsContract @@ -9,11 +11,10 @@ import androidx.annotation.RequiresApi import androidx.documentfile.provider.DocumentFile import com.anggrayudi.storage.file.child import io.flutter.plugin.common.* -import io.flutter.plugin.common.EventChannel.StreamHandler import io.lakscastro.sharedstorage.ROOT_CHANNEL import io.lakscastro.sharedstorage.SharedStoragePlugin -import io.lakscastro.sharedstorage.plugin.* -import io.lakscastro.sharedstorage.storageaccessframework.lib.* +import io.lakscastro.sharedstorage.common.* +import io.lakscastro.sharedstorage.plugin.lib.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -23,20 +24,16 @@ import java.io.InputStream import java.io.OutputStream /** - * Aimed to implement strictly only the APIs already available from the native and original - * `DocumentFile` API - * - * Basically, this is just an adapter of the native `DocumentFile` class to a Flutter Plugin class, - * without any modifications or abstractions + * APIs implementations are contained in this class and used by [SharedStoragePlugin] */ -internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : - MethodChannel.MethodCallHandler, - PluginRegistry.ActivityResultListener, - Listenable, - ActivityListener, - StreamHandler { +class SharedStorageApi(val plugin: SharedStoragePlugin) : + MethodChannel.MethodCallHandler, + PluginRegistry.ActivityResultListener, + Listenable, + ActivityListener, + EventChannel.StreamHandler { private val pendingResults: MutableMap> = - mutableMapOf() + mutableMapOf() private var channel: MethodChannel? = null private var eventChannel: EventChannel? = null private var eventSink: EventChannel.EventSink? = null @@ -47,6 +44,41 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { + GET_DOCUMENT_THUMBNAIL -> { + if (Build.VERSION.SDK_INT >= API_21) { + 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 + ) + + CoroutineScope(Dispatchers.Default).launch { + if (bitmap != null) { + 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) } + } + } + } else { + result.notSupported(call.method, API_21) + } + } GET_DOCUMENT_CONTENT -> { val uri = Uri.parse(call.argument("uri")!!) @@ -61,9 +93,9 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : } } OPEN_DOCUMENT_TREE -> - if (Build.VERSION.SDK_INT >= API_21) { - openDocumentTree(call, result) - } + if (Build.VERSION.SDK_INT >= API_21) { + openDocumentTree(call, result) + } CREATE_FILE -> if (Build.VERSION.SDK_INT >= API_21) { createFile( @@ -74,6 +106,13 @@ 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 -> @@ -108,48 +147,48 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : result.success(documentFromUri(plugin.context, uri)?.canRead()) } LENGTH -> - if (Build.VERSION.SDK_INT >= API_21) { - result.success( - documentFromUri( - plugin.context, - call.argument("uri") as String - )?.length() - ) - } + if (Build.VERSION.SDK_INT >= API_21) { + result.success( + documentFromUri( + plugin.context, + call.argument("uri") as String + )?.length() + ) + } EXISTS -> - if (Build.VERSION.SDK_INT >= API_21) { - result.success( - documentFromUri( - plugin.context, - call.argument("uri") as String - )?.exists() - ) - } + if (Build.VERSION.SDK_INT >= API_21) { + result.success( + documentFromUri( + plugin.context, + call.argument("uri") as String + )?.exists() + ) + } DELETE -> - if (Build.VERSION.SDK_INT >= API_21) { - result.success( - documentFromUri( - plugin.context, - call.argument("uri") as String - )?.delete() - ) - } - LAST_MODIFIED -> - if (Build.VERSION.SDK_INT >= API_21) { - val document = documentFromUri( + if (Build.VERSION.SDK_INT >= API_21) { + result.success( + documentFromUri( plugin.context, call.argument("uri") as String - ) + )?.delete() + ) + } + LAST_MODIFIED -> + if (Build.VERSION.SDK_INT >= API_21) { + val document = documentFromUri( + plugin.context, + call.argument("uri") as String + ) - result.success(document?.lastModified()) - } + result.success(document?.lastModified()) + } CREATE_DIRECTORY -> { if (Build.VERSION.SDK_INT >= API_21) { val uri = call.argument("uri") as String val displayName = call.argument("displayName") as String val createdDirectory = - documentFromUri(plugin.context, uri)?.createDirectory(displayName) ?: return + documentFromUri(plugin.context, uri)?.createDirectory(displayName) ?: return result.success(createDocumentFileMap(createdDirectory)) } else { @@ -232,17 +271,59 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : if (Build.VERSION.SDK_INT >= API_21) { val document = documentFromUri(plugin.context, uri) - val childDocument = document?.child(plugin.context, path, requiresWriteAccess) + val childDocument = + document?.child(plugin.context, path, requiresWriteAccess) result.success(createDocumentFileMap(childDocument)) } else { result.notSupported(CHILD, API_21, mapOf("uri" to uri)) } } + OPEN_DOCUMENT_FILE -> openDocumentFile(call, result) else -> result.notImplemented() } } + 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 + ) + + val intent = + Intent(Intent.ACTION_VIEW).apply { + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + data = uri + } + + try { + plugin.binding?.activity?.startActivity(intent, null) + + Log.d("sharedstorage", "Successfully launched uri $uri ") + + 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) + ) + } 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") + ) + } 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") + ) + } + } + @RequiresApi(API_21) private fun openDocumentTree(call: MethodCall, result: MethodChannel.Result) { val grantWritePermission = call.argument("grantWritePermission")!! @@ -250,24 +331,24 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : val initialUri = call.argument("initialUri") val intent = - Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { - addFlags( - if (grantWritePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION - else Intent.FLAG_GRANT_READ_URI_PERMISSION - ) + Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { + addFlags( + if (grantWritePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION + else Intent.FLAG_GRANT_READ_URI_PERMISSION + ) - if (initialUri != null) { - val tree = DocumentFile.fromTreeUri(plugin.context, Uri.parse(initialUri)) + if (initialUri != null) { + val tree = DocumentFile.fromTreeUri(plugin.context, Uri.parse(initialUri)) - if (Build.VERSION.SDK_INT >= API_26) { - putExtra( - if (Build.VERSION.SDK_INT >= API_26) DocumentsContract.EXTRA_INITIAL_URI - else DOCUMENTS_CONTRACT_EXTRA_INITIAL_URI, - tree?.uri - ) - } + if (Build.VERSION.SDK_INT >= API_26) { + putExtra( + if (Build.VERSION.SDK_INT >= API_26) DocumentsContract.EXTRA_INITIAL_URI + else DOCUMENTS_CONTRACT_EXTRA_INITIAL_URI, + tree?.uri + ) } } + } if (pendingResults[OPEN_DOCUMENT_TREE_CODE] != null) return @@ -278,11 +359,11 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : @RequiresApi(API_21) private fun createFile( - result: MethodChannel.Result, - mimeType: String, - displayName: String, - directory: String, - content: ByteArray + result: MethodChannel.Result, + mimeType: String, + displayName: String, + directory: String, + content: ByteArray ) { createFile(Uri.parse(directory), mimeType, displayName, content) { result.success(createDocumentFileMap(this)) @@ -291,11 +372,11 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun createFile( - treeUri: Uri, - mimeType: String, - displayName: String, - content: ByteArray, - block: DocumentFile?.() -> Unit + treeUri: Uri, + mimeType: String, + displayName: String, + content: ByteArray, + block: DocumentFile?.() -> Unit ) { val createdFile = documentFromUri(plugin.context, treeUri)!!.createFile(mimeType, displayName) @@ -311,29 +392,48 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : } } + private fun writeToFile( + result: MethodChannel.Result, + uri: String, + content: ByteArray, + mode: String + ) { + try { + plugin.context.contentResolver.openOutputStream(Uri.parse(uri), mode)?.apply { + write(content) + flush() + close() + + result.success(true) + } + } catch (e: Exception) { + result.success(false) + } + } + @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}" - ) - } - .toList() + persistedUriPermissions + .map { + mapOf( + "isReadPermission" to it.isReadPermission, + "isWritePermission" to it.isWritePermission, + "persistedTime" to it.persistedTime, + "uri" to "${it.uri}" + ) + } + .toList() ) } @RequiresApi(API_19) private fun releasePersistableUriPermission(result: MethodChannel.Result, directoryUri: String) { plugin.context.contentResolver.releasePersistableUriPermission( - Uri.parse(directoryUri), - Intent.FLAG_GRANT_WRITE_URI_PERMISSION + Uri.parse(directoryUri), + Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) result.success(null) @@ -352,9 +452,9 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : if (uri != null) { plugin.context.contentResolver.takePersistableUriPermission( - uri, - if (grantWritePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION - else Intent.FLAG_GRANT_READ_URI_PERMISSION + uri, + if (grantWritePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION + else Intent.FLAG_GRANT_READ_URI_PERMISSION ) pendingResult.second.success("$uri") diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentCommon.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/lib/DocumentCommon.kt similarity index 96% rename from android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentCommon.kt rename to android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/lib/DocumentCommon.kt index d072e48..ca84af5 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentCommon.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/lib/DocumentCommon.kt @@ -1,4 +1,4 @@ -package io.lakscastro.sharedstorage.storageaccessframework.lib +package io.lakscastro.sharedstorage.plugin.lib import android.content.ContentResolver import android.content.Context @@ -9,9 +9,9 @@ import android.provider.DocumentsContract import android.util.Base64 import androidx.annotation.RequiresApi import androidx.documentfile.provider.DocumentFile -import io.lakscastro.sharedstorage.plugin.API_19 -import io.lakscastro.sharedstorage.plugin.API_21 -import io.lakscastro.sharedstorage.plugin.API_24 +import io.lakscastro.sharedstorage.common.API_19 +import io.lakscastro.sharedstorage.common.API_21 +import io.lakscastro.sharedstorage.common.API_24 import java.io.ByteArrayOutputStream import java.io.Closeable diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentFileColumn.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/lib/DocumentFileColumn.kt similarity index 97% rename from android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentFileColumn.kt rename to android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/lib/DocumentFileColumn.kt index 6a178ff..12c130b 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentFileColumn.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/lib/DocumentFileColumn.kt @@ -1,4 +1,4 @@ -package io.lakscastro.sharedstorage.storageaccessframework.lib +package io.lakscastro.sharedstorage.plugin.lib import android.database.Cursor import android.provider.DocumentsContract diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/lib/StorageAccessFrameworkConstant.kt similarity index 94% rename from android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt rename to android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/lib/StorageAccessFrameworkConstant.kt index d1ed988..6423f36 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/plugin/lib/StorageAccessFrameworkConstant.kt @@ -1,4 +1,4 @@ -package io.lakscastro.sharedstorage.storageaccessframework.lib +package io.lakscastro.sharedstorage.plugin.lib /** * Exceptions @@ -23,6 +23,7 @@ const val OPEN_DOCUMENT_TREE = "openDocumentTree" const val PERSISTED_URI_PERMISSIONS = "persistedUriPermissions" const val RELEASE_PERSISTABLE_URI_PERMISSION = "releasePersistableUriPermission" const val CREATE_FILE = "createFile" +const val WRITE_TO_FILE = "writeToFile" const val FROM_TREE_URI = "fromTreeUri" const val CAN_WRITE = "canWrite" const val CAN_READ = "canRead" diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt deleted file mode 100644 index e25edd7..0000000 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt +++ /dev/null @@ -1,135 +0,0 @@ -package io.lakscastro.sharedstorage.storageaccessframework - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.net.Uri -import android.util.Log -import io.flutter.plugin.common.* -import io.flutter.plugin.common.EventChannel.StreamHandler -import io.lakscastro.sharedstorage.ROOT_CHANNEL -import io.lakscastro.sharedstorage.SharedStoragePlugin -import io.lakscastro.sharedstorage.plugin.ActivityListener -import io.lakscastro.sharedstorage.plugin.Listenable -import io.lakscastro.sharedstorage.storageaccessframework.lib.* - -/** - * Aimed to be a class which takes the `DocumentFile` API and implement some APIs not supported - * natively by Android. - * - * This is why it is separated from the original and raw `DocumentFileApi` which is the class that - * only exposes the APIs without modifying them (Mirror API). - * - * Then here is where we can implement the main abstractions/use-cases which would be available - * globally without modifying the strict APIs. - */ -internal class DocumentFileHelperApi(private val plugin: SharedStoragePlugin) : - MethodChannel.MethodCallHandler, - PluginRegistry.ActivityResultListener, - Listenable, - ActivityListener, - StreamHandler { - private val pendingResults: MutableMap> = - mutableMapOf() - private var channel: MethodChannel? = null - private var eventChannel: EventChannel? = null - private var eventSink: EventChannel.EventSink? = null - - companion object { - private const val CHANNEL = "documentfilehelper" - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - OPEN_DOCUMENT_FILE -> openDocumentFile(call, result) - else -> result.notImplemented() - } - } - - 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) - - val intent = - Intent(Intent.ACTION_VIEW).apply { - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - data = uri - } - - try { - plugin.binding?.activity?.startActivity(intent, null) - - Log.d("sharedstorage", "Successfully launched uri $uri ") - - 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) - ) - } 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") - ) - } 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") - ) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - when (requestCode) { - /** TODO(@lakscastro): Implement if required */ - else -> return true - } - - return false - } - - override fun startListening(binaryMessenger: BinaryMessenger) { - if (channel != null) stopListening() - - channel = MethodChannel(binaryMessenger, "$ROOT_CHANNEL/$CHANNEL") - channel?.setMethodCallHandler(this) - - eventChannel = EventChannel(binaryMessenger, "$ROOT_CHANNEL/event/$CHANNEL") - eventChannel?.setStreamHandler(this) - } - - override fun stopListening() { - if (channel == null) return - - channel?.setMethodCallHandler(null) - channel = null - - eventChannel?.setStreamHandler(null) - eventChannel = null - } - - override fun startListeningToActivity() { - plugin.binding?.addActivityResultListener(this) - } - - override fun stopListeningToActivity() { - plugin.binding?.removeActivityResultListener(this) - } - - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - val args = arguments as Map<*, *> - - eventSink = events - - when (args["event"]) { - /** TODO(@lakscastro): Implement if required */ - } - } - - override fun onCancel(arguments: Any?) { - eventSink = null - } -} diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentsContractApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentsContractApi.kt deleted file mode 100644 index aca2a1b..0000000 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentsContractApi.kt +++ /dev/null @@ -1,91 +0,0 @@ -package io.lakscastro.sharedstorage.storageaccessframework - -import android.graphics.Point -import android.net.Uri -import android.os.Build -import android.provider.DocumentsContract -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.lakscastro.sharedstorage.ROOT_CHANNEL -import io.lakscastro.sharedstorage.SharedStoragePlugin -import io.lakscastro.sharedstorage.plugin.API_21 -import io.lakscastro.sharedstorage.plugin.ActivityListener -import io.lakscastro.sharedstorage.plugin.Listenable -import io.lakscastro.sharedstorage.plugin.notSupported -import io.lakscastro.sharedstorage.storageaccessframework.lib.GET_DOCUMENT_THUMBNAIL -import io.lakscastro.sharedstorage.storageaccessframework.lib.bitmapToBase64 -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -internal class DocumentsContractApi(private val plugin: SharedStoragePlugin) : - MethodChannel.MethodCallHandler, Listenable, ActivityListener { - private var channel: MethodChannel? = null - - companion object { - private const val CHANNEL = "documentscontract" - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - GET_DOCUMENT_THUMBNAIL -> { - if (Build.VERSION.SDK_INT >= API_21) { - 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 - ) - - CoroutineScope(Dispatchers.Default).launch { - if (bitmap != null) { - 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) } - } - } - } else { - result.notSupported(call.method, API_21) - } - } - } - } - - override fun startListening(binaryMessenger: BinaryMessenger) { - if (channel != null) stopListening() - - channel = MethodChannel(binaryMessenger, "$ROOT_CHANNEL/$CHANNEL") - channel?.setMethodCallHandler(this) - } - - override fun stopListening() { - if (channel == null) return - - channel?.setMethodCallHandler(null) - channel = null - } - - override fun startListeningToActivity() { - /** Implement if needed */ - } - - override fun stopListeningToActivity() { - /** Implement if needed */ - } -} diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/StorageAccessFrameworkApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/StorageAccessFrameworkApi.kt deleted file mode 100644 index a172714..0000000 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/StorageAccessFrameworkApi.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.lakscastro.sharedstorage.storageaccessframework - -import io.flutter.plugin.common.* -import io.lakscastro.sharedstorage.SharedStoragePlugin -import io.lakscastro.sharedstorage.plugin.ActivityListener -import io.lakscastro.sharedstorage.plugin.Listenable - -class StorageAccessFrameworkApi(plugin: SharedStoragePlugin) : Listenable, ActivityListener { - private val documentFileApi = DocumentFileApi(plugin) - private val documentsContractApi = DocumentsContractApi(plugin) - private val documentFileHelperApi = DocumentFileHelperApi(plugin) - - override fun startListening(binaryMessenger: BinaryMessenger) { - documentFileApi.startListening(binaryMessenger) - documentsContractApi.startListening(binaryMessenger) - documentFileHelperApi.startListening(binaryMessenger) - } - - override fun stopListening() { - documentFileApi.stopListening() - documentsContractApi.stopListening() - documentFileHelperApi.stopListening() - } - - override fun startListeningToActivity() { - documentFileApi.startListeningToActivity() - documentsContractApi.startListeningToActivity() - documentFileHelperApi.startListeningToActivity() - } - - override fun stopListeningToActivity() { - documentFileApi.stopListeningToActivity() - documentsContractApi.stopListeningToActivity() - documentFileHelperApi.stopListeningToActivity() - } -} diff --git a/docs/Usage/Environment.md b/docs/Usage/Environment.md index 3bfb925..925c3ee 100644 --- a/docs/Usage/Environment.md +++ b/docs/Usage/Environment.md @@ -1,22 +1,8 @@ ## Import package -```dart -import 'package:shared_storage/environment.dart' as environment; -``` - -> **Note** Be aware that if you import the package `import '...' as environment;` (strongly recommended) you should prefix all method calls with `environment`, example: +This module is no longer available after `v0.5.0`, if you are using this consider filling a issue with your use-case. -```dart -environment.getRootDirectory(...); -environment.getExternalStoragePublicDirectory(...); -``` - -But if you import without alias `import '...';` (Not recommeded because can conflict with other method/package names) you should use directly as functions: - -```dart -getRootDirectory(...); -getExternalStoragePublicDirectory(...); -``` +This was removed since it was not compliant with latest Android privacy APIs (Android 9+). ## Mirror methods diff --git a/docs/Usage/Media Store.md b/docs/Usage/Media Store.md index fcd30dd..4ed5b9c 100644 --- a/docs/Usage/Media Store.md +++ b/docs/Usage/Media Store.md @@ -1,41 +1,15 @@ ## Import package -```dart -import 'package:shared_storage/media_store.dart' as mediastore; -``` +This module is no longer available after `v0.5.0`, if you are using this consider filling a issue with your use-case. -> **Note** Be aware that if you import the package `import '...' as mediastore;` (strongly recommended) you should prefix all method calls with `mediastore`, example: - -```dart -mediastore.getMediaStoreContentDirectory(...); -``` - -But if you import without alias `import '...';` (Not recommeded because can conflict with other method/package names) you should use directly as functions: - -```dart -getMediaStoreContentDirectory(...); -``` - -## API reference - -Original API. These methods exists only in this package. - -Because methods are an abstraction from native API, for example: `getMediaStoreContentDirectory` is an abstraction because there's no such method in native Android, there you can access these directories synchronously and directly from the `MediaStore` nested classes which is not the goal of this package (re-create all Android APIs) but provide a powerful fully-configurable API to call these APIs. +This was removed since this package is no longer exporting native APIs, see [#56](https://github.com/lakscastro/shared-storage/issues/56) for details. ### getMediaStoreContentDirectory -Get the **directory** of a given Media Store Collection. - -The directory follows the **Uri** format +Removed since `v0.5.0`. -To see all available collections see `MediaStoreCollection` class - -```dart -final Uri directory = getMediaStoreContentDirectory(MediaStoreCollection.downloads); -``` +This was removed since this package is no longer exporting native APIs, see [#56](https://github.com/lakscastro/shared-storage/issues/56) for details. ## Android Official Documentation -The **Media Store** [official documentation is available here.](https://developer.android.com/reference/android/provider/MediaStore) - -All the APIs listed in this plugin module are derivated from the official docs. +You can still read about the **Media Store** [official documentation is available here.](https://developer.android.com/reference/android/provider/MediaStore). diff --git a/docs/Usage/Storage Access Framework.md b/docs/Usage/Storage Access Framework.md index 34f2fdd..5a1dc14 100644 --- a/docs/Usage/Storage Access Framework.md +++ b/docs/Usage/Storage Access Framework.md @@ -1,7 +1,7 @@ ## Import package ```dart -import 'package:shared_storage/saf.dart' as saf; +import 'package:shared_storage/shared_storage.dart'; ``` > **Note** Be aware that if you import the package `import '...' as saf;` (strongly recommended) you should prefix all method calls with `saf`, example: @@ -126,17 +126,7 @@ print(String.fromCharCodes(fileContent)); ### getRealPathFromUri -Helper method to generate the file path of the given `uri`. This returns the real path to work with native old `File` API instead Uris, be aware this approach is no longer supported on Android 10+ (API 29+) and though new, this API is **marked as deprecated** and should be migrated to a _scoped-storage_ approach. - -See [Get real path from URI, Android KitKat new storage access framework](https://stackoverflow.com/questions/20067508/get-real-path-from-uri-android-kitkat-new-storage-access-framework/20559175#20559175) for details. - -```dart -final Uri uri = ...; - -final String? filePath = await getRealPathFromUri(myUri); - -final File file = File(filePath); -``` +Removed since `v0.5.0`, this is not compliant with latest privacy-friendly APIs so it was removed. ## Mirror methods @@ -217,6 +207,33 @@ final DocumentFile? createdFile = createFileAsBytes( ); ``` +### writeToFileAsBytes + +Write to a file using raw bytes `Uint8List`. + +Given the document uri, opens the file in the specified `mode` and writes the `bytes` to it. + +`mode` represents the mode in which the file will be opened for writing. Use `FileMode.write` for truncating (overwrite) and `FileMode.append` for appending to the file. + +```dart +final Uri documentUri = ... +final String fileContent = 'My File Content'; + +/// Write to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFileAsBytes( + documentUri, + bytes: Uint8List.fromList(fileContent.codeUnits), + mode: FileMode.write, +); + +/// Append to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFileAsBytes( + documentUri, + bytes: Uint8List.fromList(fileContent.codeUnits), + mode: FileMode.write, +); +``` + ### canRead Mirror of [`DocumentFile.canRead`]() @@ -485,6 +502,31 @@ final DocumentFile? createdFile = createFileAsString( ); ``` +### writeToFileAsString + +Alias for `writeToFileAsBytes` + +Convenient method to write to a file using `content` as `String` instead `Uint8List`. + +```dart +final Uri documentUri = ... +final String fileContent = 'My File Content'; + +/// Write to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFileAsString( + documentUri, + content: fileContent, + mode: FileMode.write, +); + +/// Append to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFileAsBytes( + documentUri, + content: fileContent, + mode: FileMode.write, +); +``` + ### createFile Alias for `createFileAsBytes` and `createFileAsString` @@ -514,6 +556,49 @@ final DocumentFile? createdFile = createFile( ); ``` +### writeToFile + +Alias for `writeToFileAsBytes` and `writeToFileAsString` + +Convenient method to write to a file using `content` as `String` **or** `bytes` as `Uint8List`. + +You should provide either `content` or `bytes`, if both `bytes` will be used. + +`mode` represents the mode in which the file will be opened for writing. Use `FileMode.write` for truncating and `FileMode.append` for appending to the file. + +```dart +final Uri documentUri = ... +final String fileContent = 'My File Content'; + +/// Write to a file using a [String] as file contents [content] +final bool? success = writeToFile( + documentUri, + content: fileContent, + mode: FileMode.write, +); + +/// Append to a file using a [String] as file contents [content] +final bool? success = writeToFile( + documentUri, + content: fileContent, + mode: FileMode.append, +); + +/// Write to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFile( + documentUri, + content: Uint8List.fromList(fileContent.codeUnits), + mode: FileMode.write, +); + +/// Append to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFile( + documentUri, + content: Uint8List.fromList(fileContent.codeUnits), + mode: FileMode.append, +); +``` + ## External APIs (deprecated) These APIs are from external Android libraries. diff --git a/example/lib/screens/file_explorer/file_explorer_card.dart b/example/lib/screens/file_explorer/file_explorer_card.dart index 874762e..8c0d829 100644 --- a/example/lib/screens/file_explorer/file_explorer_card.dart +++ b/example/lib/screens/file_explorer/file_explorer_card.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:shared_storage/saf.dart'; +import 'package:shared_storage/shared_storage.dart'; import '../../theme/spacing.dart'; import '../../widgets/buttons.dart'; @@ -85,6 +87,12 @@ class _FileExplorerCardState extends State { bool get _isDirectory => file.metadata?.isDirectory ?? false; + int _generateLuckNumber() { + final random = Random(); + + return random.nextInt(1000); + } + @override Widget build(BuildContext context) { return SimpleCard( @@ -105,9 +113,19 @@ class _FileExplorerCardState extends State { return Image.memory(content!); } + final contentAsString = String.fromCharCodes(content!); + + final fileIsEmpty = contentAsString.isEmpty; + return Container( padding: k8dp.all, - child: Text(String.fromCharCodes(content!)), + child: Text( + fileIsEmpty ? 'This file is empty' : contentAsString, + style: TextStyle( + color: fileIsEmpty ? Colors.black26 : null, + fontStyle: fileIsEmpty ? FontStyle.italic : null, + ), + ), ); }, ); @@ -173,7 +191,6 @@ class _FileExplorerCardState extends State { final uri = widget.partialFile.metadata!.uri!; try { - // OpenFile.open('/sdcard/example.txt'); final launched = await openDocumentFile(uri); if (launched ?? false) { @@ -198,6 +215,46 @@ class _FileExplorerCardState extends State { } }, ), + if (!_isDirectory) ...[ + DangerButton( + 'Write to File', + onTap: () async { + await writeToFile( + widget.partialFile.metadata!.uri!, + content: + 'Hello World! Your luck number is: ${_generateLuckNumber()}', + mode: FileMode.write, + ); + }, + ), + DangerButton( + 'Append to file', + onTap: () async { + final contents = await getDocumentContentAsString( + widget.partialFile.metadata!.uri!, + ); + + final prependWithNewLine = contents?.isNotEmpty ?? true; + + await writeToFile( + widget.partialFile.metadata!.uri!, + content: + "${prependWithNewLine ? '\n' : ''}You file got bigger! Here's your luck number: ${_generateLuckNumber()}", + mode: FileMode.append, + ); + }, + ), + DangerButton( + 'Erase file content', + onTap: () async { + await writeToFile( + widget.partialFile.metadata!.uri!, + content: '', + mode: FileMode.write, + ); + }, + ), + ], ], ), ], diff --git a/example/lib/screens/file_explorer/file_explorer_page.dart b/example/lib/screens/file_explorer/file_explorer_page.dart index 91fc1e1..d71d9ea 100644 --- a/example/lib/screens/file_explorer/file_explorer_page.dart +++ b/example/lib/screens/file_explorer/file_explorer_page.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:shared_storage/saf.dart'; +import 'package:shared_storage/shared_storage.dart'; import '../../theme/spacing.dart'; import '../../widgets/buttons.dart'; diff --git a/example/lib/screens/granted_uris/granted_uri_card.dart b/example/lib/screens/granted_uris/granted_uri_card.dart index 55c3aa4..c8167be 100644 --- a/example/lib/screens/granted_uris/granted_uri_card.dart +++ b/example/lib/screens/granted_uris/granted_uri_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:shared_storage/saf.dart'; +import 'package:shared_storage/shared_storage.dart'; import '../../theme/spacing.dart'; import '../../widgets/buttons.dart'; diff --git a/example/lib/screens/granted_uris/granted_uris_page.dart b/example/lib/screens/granted_uris/granted_uris_page.dart index fa9ea92..4d077a8 100644 --- a/example/lib/screens/granted_uris/granted_uris_page.dart +++ b/example/lib/screens/granted_uris/granted_uris_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:shared_storage/saf.dart'; +import 'package:shared_storage/shared_storage.dart'; import '../../theme/spacing.dart'; import '../../widgets/light_text.dart'; diff --git a/lib/environment.dart b/lib/environment.dart deleted file mode 100644 index f0d0b93..0000000 --- a/lib/environment.dart +++ /dev/null @@ -1,4 +0,0 @@ -library shared_storage; - -export './src/environment/environment.dart'; -export './src/environment/environment_directory.dart'; diff --git a/lib/media_store.dart b/lib/media_store.dart deleted file mode 100644 index 0bddfe9..0000000 --- a/lib/media_store.dart +++ /dev/null @@ -1,4 +0,0 @@ -library shared_storage; - -export './src/media_store/media_store.dart'; -export './src/media_store/media_store_collection.dart'; diff --git a/lib/saf.dart b/lib/saf.dart deleted file mode 100644 index 1f9768e..0000000 --- a/lib/saf.dart +++ /dev/null @@ -1,8 +0,0 @@ -library shared_storage; - -export './src/saf/document_bitmap.dart'; -export './src/saf/document_file.dart'; -export './src/saf/document_file_column.dart'; -export './src/saf/partial_document_file.dart'; -export './src/saf/saf.dart'; -export './src/saf/uri_permission.dart'; diff --git a/lib/shared_storage.dart b/lib/shared_storage.dart index 7551f71..501ea0f 100644 --- a/lib/shared_storage.dart +++ b/lib/shared_storage.dart @@ -1,3 +1,8 @@ -export './environment.dart'; -export './media_store.dart'; -export './saf.dart'; +library shared_storage; + +export './src/api/document_bitmap.dart'; +export './src/api/document_file.dart'; +export './src/api/document_file_column.dart'; +export './src/api/partial_document_file.dart'; +export './src/api/saf.dart'; +export './src/api/uri_permission.dart'; diff --git a/lib/src/saf/document_bitmap.dart b/lib/src/api/document_bitmap.dart similarity index 100% rename from lib/src/saf/document_bitmap.dart rename to lib/src/api/document_bitmap.dart diff --git a/lib/src/saf/document_file.dart b/lib/src/api/document_file.dart similarity index 89% rename from lib/src/saf/document_file.dart rename to lib/src/api/document_file.dart index e072d20..16416ce 100644 --- a/lib/src/saf/document_file.dart +++ b/lib/src/api/document_file.dart @@ -1,6 +1,7 @@ +import 'dart:io'; import 'dart:typed_data'; -import '../common/functional_extender.dart'; +import '../common/annotation.dart'; import 'saf.dart' as saf; extension UriDocumentFileUtils on Uri { @@ -74,7 +75,7 @@ class DocumentFile { static Future fromTreeUri(Uri uri) => saf.fromTreeUri(uri); /// {@macro sharedstorage.saf.child} - @willbemovedsoon + @deprecated Future child( String path, { bool requiresWriteAccess = false, @@ -155,6 +156,41 @@ class DocumentFile { displayName: displayName, content: content, ); + + /// {@macro sharedstorage.saf.writeToFileAsBytes} + Future writeToFileAsBytes({ + required Uint8List bytes, + FileMode? mode, + }) => + saf.writeToFileAsBytes( + uri, + bytes: bytes, + mode: mode, + ); + + /// {@macro sharedstorage.saf.writeToFile} + Future writeToFile({ + String? content, + Uint8List? bytes, + FileMode? mode, + }) => + saf.writeToFile( + uri, + content: content, + bytes: bytes, + mode: mode, + ); + + /// Alias for [writeToFile] with [content] param + Future writeToFileAsString({ + required String content, + FileMode? mode, + }) => + saf.writeToFile( + uri, + content: content, + mode: mode, + ); /// {@macro sharedstorage.saf.length} Future get length => saf.documentLength(uri); diff --git a/lib/src/saf/document_file_column.dart b/lib/src/api/document_file_column.dart similarity index 100% rename from lib/src/saf/document_file_column.dart rename to lib/src/api/document_file_column.dart diff --git a/lib/src/saf/partial_document_file.dart b/lib/src/api/partial_document_file.dart similarity index 98% rename from lib/src/saf/partial_document_file.dart rename to lib/src/api/partial_document_file.dart index 0efdc4a..3c65b98 100644 --- a/lib/src/saf/partial_document_file.dart +++ b/lib/src/api/partial_document_file.dart @@ -1,4 +1,4 @@ -import '../../saf.dart'; +import 'document_file_column.dart'; /// Represent the same entity as `DocumentFile` but will be lazily loaded /// by `listFiles` method with dynamic diff --git a/lib/src/saf/saf.dart b/lib/src/api/saf.dart similarity index 85% rename from lib/src/saf/saf.dart rename to lib/src/api/saf.dart index 3b79293..b243112 100644 --- a/lib/src/saf/saf.dart +++ b/lib/src/api/saf.dart @@ -1,10 +1,16 @@ import 'dart:async'; +import 'dart:io'; import 'dart:typed_data'; -import '../../saf.dart'; import '../channels.dart'; +import '../common/annotation.dart'; import '../common/functional_extender.dart'; -import 'common.dart'; +import '../common/invoke_map_method.dart'; +import 'document_bitmap.dart'; +import 'document_file.dart'; +import 'document_file_column.dart'; +import 'partial_document_file.dart'; +import 'uri_permission.dart'; /// {@template sharedstorage.saf.openDocumentTree} /// Start Activity Action: Allow the user to pick a directory subtree. @@ -112,8 +118,10 @@ Future getDocumentThumbnail({ 'height': height, }; - final bitmap = await kDocumentsContractChannel - .invokeMapMethod('getDocumentThumbnail', args); + final bitmap = await kDocumentFileChannel.invokeMapMethod( + 'getDocumentThumbnail', + args, + ); return bitmap?.apply((b) => DocumentBitmap.fromMap(b)); } @@ -201,7 +209,7 @@ Future createDirectory(Uri parentUri, String displayName) async { } /// {@template sharedstorage.saf.createFile} -/// Convenient method to create files using either String or raw bytes. +/// Convenient method to create files using either [String] or raw bytes [Uint8List]. /// /// Under the hood this method calls `createFileAsString` or `createFileAsBytes` /// depending on which argument is passed. @@ -281,6 +289,79 @@ Future createFileAsString( ); } +/// {@template sharedstorage.saf.writeToFile} +/// Convenient method to write to a file using either [String] or raw bytes [Uint8List]. +/// +/// Under the hood this method calls `writeToFileAsString` or `writeToFileAsBytes` +/// depending on which argument is passed. +/// +/// If both (bytes and content) are passed, the bytes will be used and the content will be ignored. +/// {@endtemplate} +Future writeToFile( + Uri uri, { + Uint8List? bytes, + String? content, + FileMode? mode, +}) { + assert( + bytes != null || content != null, + '''Either [bytes] or [content] should be provided''', + ); + + return bytes != null + ? writeToFileAsBytes( + uri, + bytes: bytes, + mode: mode, + ) + : writeToFileAsString( + uri, + content: content!, + mode: mode, + ); +} + +/// {@template sharedstorage.saf.writeToFileAsBytes} +/// Write to a file. +/// - `uri` is the URI of the file. +/// - `bytes` is the content of the document as a list of bytes `Uint8List`. +/// - `mode` is the mode in which the file will be opened for writing. Use `FileMode.write` for truncating and `FileMode.append` for appending to the file. +/// +/// Returns `true` if the file was successfully written to. +/// {@endtemplate} +Future writeToFileAsBytes( + Uri uri, { + required Uint8List bytes, + FileMode? mode, +}) async { + final writeMode = + mode == FileMode.append || mode == FileMode.writeOnlyAppend ? 'wa' : 'wt'; + + final args = { + 'uri': '$uri', + 'content': bytes, + 'mode': writeMode, + }; + + return kDocumentFileChannel.invokeMethod('writeToFile', args); +} + +/// {@template sharedstorage.saf.writeToFileAsString} +/// Convenient method to write to a file. +/// using `content` as [String] instead [Uint8List]. +/// {@endtemplate} +Future writeToFileAsString( + Uri uri, { + required String content, + FileMode? mode, +}) { + return writeToFileAsBytes( + uri, + bytes: Uint8List.fromList(content.codeUnits), + mode: mode, + ); +} + /// {@template sharedstorage.saf.length} /// Equivalent to `DocumentFile.length`. /// @@ -361,7 +442,7 @@ Future fromTreeUri(Uri uri) async => /// /// [Refer to details](https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile#fromTreeUri%28android.content.Context,%20android.net.Uri%29) /// {@endtemplate} -@willbemovedsoon +@deprecated Future child( Uri uri, String path, { @@ -388,8 +469,7 @@ Future child( /// Returns `true` if launched successfully otherwise `false`. /// {@endtemplate} Future openDocumentFile(Uri uri) async { - final successfullyLaunched = - await kDocumentFileHelperChannel.invokeMethod( + final successfullyLaunched = await kDocumentFileChannel.invokeMethod( 'openDocumentFile', {'uri': '$uri'}, ); @@ -443,12 +523,3 @@ Future getDocumentContentAsString( return bytes?.apply((a) => String.fromCharCodes(a)); } - -/// {@template sharedstorage.saf.getDocumentContentAsString} -/// Helper method to generate the file path of the given `uri` -/// -/// See [Get real path from URI, Android KitKat new storage access framework](https://stackoverflow.com/questions/20067508/get-real-path-from-uri-android-kitkat-new-storage-access-framework/20559175#20559175) -/// for details. -/// {@endtemplate} -Future getRealPathFromUri(Uri uri) async => kDocumentFileHelperChannel - .invokeMethod('getRealPathFromUri', {'uri': '$uri'}); diff --git a/lib/src/saf/uri_permission.dart b/lib/src/api/uri_permission.dart similarity index 100% rename from lib/src/saf/uri_permission.dart rename to lib/src/api/uri_permission.dart diff --git a/lib/src/channels.dart b/lib/src/channels.dart index 14bcfe9..1658fa9 100644 --- a/lib/src/channels.dart +++ b/lib/src/channels.dart @@ -4,23 +4,9 @@ const kRootChannel = 'io.lakscastro.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'); - -/// Target [MediaStore] Android API -const kMediaStoreChannel = MethodChannel('$kRootChannel/mediastore'); - /// Target [DocumentFile] from `SAF` Android API (New Android APIs use it) const kDocumentFileChannel = MethodChannel('$kRootChannel/documentfile'); -/// Target [DocumentsContract] from `SAF` Android API (New Android APIs use it) -const kDocumentsContractChannel = - MethodChannel('$kRootChannel/documentscontract'); - -/// Target [DocumentFileHelper] Shared Storage plugin class (SAF Based) -const 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) diff --git a/lib/src/common/annotation.dart b/lib/src/common/annotation.dart new file mode 100644 index 0000000..bc244bc --- /dev/null +++ b/lib/src/common/annotation.dart @@ -0,0 +1,4 @@ +const deprecated = Deprecated( + 'This method should no longer be used, and will be removed in a near release' + '\nRefer to the documentation https://lakscastro.github.io/shared-storage to see how to migrate', +); 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/saf/common.dart b/lib/src/common/invoke_map_method.dart similarity index 92% rename from lib/src/saf/common.dart rename to lib/src/common/invoke_map_method.dart index 5e48099..b63db11 100644 --- a/lib/src/saf/common.dart +++ b/lib/src/common/invoke_map_method.dart @@ -1,5 +1,5 @@ +import '../api/document_file.dart'; import '../channels.dart'; -import 'document_file.dart'; /// Helper method to invoke a native SAF method and return a document file /// if not null, shouldn't be called directly from non-package code diff --git a/lib/src/environment/common.dart b/lib/src/environment/common.dart deleted file mode 100644 index 6dca670..0000000 --- a/lib/src/environment/common.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:io'; - -import '../channels.dart'; - -/// Util method to call a given `Environment.` method without arguments -Future invokeVoidEnvironmentMethod(String method) async { - final directory = await kEnvironmentChannel.invokeMethod(method); - - if (directory == null) return null; - - return Directory(directory); -} diff --git a/lib/src/environment/environment.dart b/lib/src/environment/environment.dart deleted file mode 100644 index c22698a..0000000 --- a/lib/src/environment/environment.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:io'; - -import '../channels.dart'; -import 'common.dart'; -import 'environment_directory.dart'; - -/// Equivalent to `Environment.getRootDirectory` -/// -/// [Refer to details](https://developer.android.com/reference/android/os/Environment#getRootDirectory%28%29) -Future getRootDirectory() async { - const kGetRootDirectory = 'getRootDirectory'; - - return invokeVoidEnvironmentMethod(kGetRootDirectory); -} - -/// Equivalent to `Environment.getExternalStoragePublicDirectory`. -/// -/// _Added in API level 8_. -/// -/// _Deprecated in API level 29_. -/// -/// See [EnvironmentDirectory] to see all available directories. -/// -/// See [how to migrate](https://stackoverflow.com/questions/56468539/getexternalstoragepublicdirectory-deprecated-in-android-q) from this deprecated API. -/// -/// [Refer to details](https://developer.android.com/reference/android/os/Environment#getExternalStoragePublicDirectory%28java.lang.String%29) -@Deprecated( - '''Deprecated in API level 29 (Android 10+) and was deprecated in this package after v0.3.0.\nSee how to migrate: https://stackoverflow.com/questions/56468539/getexternalstoragepublicdirectory-deprecated-in-android-q''', -) -Future getExternalStoragePublicDirectory( - EnvironmentDirectory directory, -) async { - const kGetExternalStoragePublicDirectory = - 'getExternalStoragePublicDirectory'; - const kDirectoryArg = 'directory'; - - final args = {kDirectoryArg: '$directory'}; - - final publicDir = await kEnvironmentChannel.invokeMethod( - kGetExternalStoragePublicDirectory, - args, - ); - - if (publicDir == null) return null; - - return Directory(publicDir); -} - -/// Equivalent to `Environment.getExternalStorageDirectory` -/// -/// [Refer to details](https://developer.android.com/reference/android/os/Environment#getExternalStorageDirectory%28%29) -Future getExternalStorageDirectory() async { - const kGetExternalStorageDirectory = 'getExternalStorageDirectory'; - - return invokeVoidEnvironmentMethod(kGetExternalStorageDirectory); -} - -/// Equivalent to `Environment.getDataDirectory` -/// -/// [Refer to details](https://developer.android.com/reference/android/os/Environment#getDataDirectory%28%29) -Future getDataDirectory() async { - const kGetDataDirectory = 'getDataDirectory'; - - return invokeVoidEnvironmentMethod(kGetDataDirectory); -} - -/// Equivalent to `Environment.getDataDirectory` -/// -/// [Refer to details](https://developer.android.com/reference/android/os/Environment#getDownloadCacheDirectory%28%29) -Future getDownloadCacheDirectory() async { - const kGetDownloadCacheDirectory = 'getDownloadCacheDirectory'; - - return invokeVoidEnvironmentMethod(kGetDownloadCacheDirectory); -} - -/// Equivalent to `Environment.getStorageDirectory` -/// -/// [Refer to details](https://developer.android.com/reference/android/os/Environment#getStorageDirectory%28%29) -Future getStorageDirectory() { - const kGetStorageDirectory = 'getStorageDirectory'; - - return invokeVoidEnvironmentMethod(kGetStorageDirectory); -} diff --git a/lib/src/environment/environment_directory.dart b/lib/src/environment/environment_directory.dart deleted file mode 100644 index 2b900f4..0000000 --- a/lib/src/environment/environment_directory.dart +++ /dev/null @@ -1,78 +0,0 @@ -/// Enumeration to all fields of [android.os.Environment] -/// class available to API level 16 or higher -/// -/// - You can also create a custom [EnvironmentDirectory] -/// by using [custom] constructor -/// -/// - This generally returns a directory pointing to `/storage/emulated/0/[this]` -/// -/// [Refer to details](https://developer.android.com/reference/android/os/Environment#fields_1) -class EnvironmentDirectory { - const EnvironmentDirectory._(this.id); - - /// Define a custom [folder] - const EnvironmentDirectory.custom(String folder) : id = folder; - - final String id; - - static const _kPrefix = 'EnvironmentDirectory'; - - /// Available for Android [4.1 to 9.0] - /// - /// Equivalent to [Environment.DIRECTORY_ALARMS] - static const alarms = EnvironmentDirectory._('$_kPrefix.Alarms'); - - /// Available for Android [4.1 to 9] - /// - /// Equivalent to: - /// - [Environment.DIRECTORY_DCIM] on Android [4.1 to 9] - static const dcim = EnvironmentDirectory._('$_kPrefix.DCIM'); - - /// Available for Android [4.1 to 9] - /// - /// Equivalent to: - /// - [Environment.DIRECTORY_DOWNLOADS] on Android [4.1 to 9] - static const downloads = EnvironmentDirectory._('$_kPrefix.Downloads'); - - /// Available for Android [4.1 to 9] - /// - /// - [Environment.DIRECTORY_MOVIES] on Android [4.1 to 9] - static const movies = EnvironmentDirectory._('$_kPrefix.Movies'); - - /// Available for Android [4.1 to 9] - /// - /// - [Environment.DIRECTORY_MUSIC] on Android [4.1 to 9] - static const music = EnvironmentDirectory._('$_kPrefix.Music'); - - /// Available for Android [4.1 to 9] - /// - /// - [Environment.DIRECTORY_NOTIFICATIONS] on Android [4.1 to 9] - static const notifications = - EnvironmentDirectory._('$_kPrefix.Notifications'); - - /// Available for Android [4.1 to 9] - /// - /// - [Environment.DIRECTORY_PICTURES] on Android [4.1 to 9] - static const pictures = EnvironmentDirectory._('$_kPrefix.Pictures'); - - /// Available for Android [4.1 to 9] - /// - /// - [Environment.DIRECTORY_PODCASTS] on Android [4.1 to 9] - static const podcasts = EnvironmentDirectory._('$_kPrefix.Podcasts'); - - /// Available for Android [4.1 to 9] - /// - /// - [Environment.DIRECTORY_RINGTONES] on Android [4.1 to 9] - static const ringtones = EnvironmentDirectory._('$_kPrefix.Ringtones'); - - @override - bool operator ==(Object other) { - return other is EnvironmentDirectory && other.id == id; - } - - @override - int get hashCode => id.hashCode; - - @override - String toString() => id; -} diff --git a/lib/src/media_store/media_store.dart b/lib/src/media_store/media_store.dart deleted file mode 100644 index 2a7b425..0000000 --- a/lib/src/media_store/media_store.dart +++ /dev/null @@ -1,25 +0,0 @@ -import '../channels.dart'; -import 'media_store_collection.dart'; - -/// The contract between the media provider and applications. -/// -/// Get the directory of a given [MediaStoreCollection] -/// -/// [Refer to details](https://developer.android.com/reference/android/provider/MediaStore#summary) -Future getMediaStoreContentDirectory( - MediaStoreCollection collection, -) async { - const kGetMediaStoreContentDirectory = 'getMediaStoreContentDirectory'; - const kCollectionArg = 'collection'; - - final args = {kCollectionArg: '$collection'}; - - final publicDir = await kMediaStoreChannel.invokeMethod( - kGetMediaStoreContentDirectory, - args, - ); - - if (publicDir == null) return null; - - return Uri.parse(publicDir); -} diff --git a/lib/src/media_store/media_store_collection.dart b/lib/src/media_store/media_store_collection.dart deleted file mode 100644 index 6f571e3..0000000 --- a/lib/src/media_store/media_store_collection.dart +++ /dev/null @@ -1,45 +0,0 @@ -/// Representation of the [android.provider.MediaStore] Android SDK -/// -/// [Refer to details](https://developer.android.com/reference/android/provider/MediaStore#summary) -class MediaStoreCollection { - const MediaStoreCollection._(this.id); - - final String id; - - static const _kPrefix = 'MediaStoreCollection'; - - /// Available for Android [10 to 12] - /// - /// Equivalent to: - /// - [MediaStore.Audio] - static const audio = MediaStoreCollection._('$_kPrefix.Audio'); - - /// Available for Android [10 to 12] - /// - /// Equivalent to: - /// - [MediaStore.Downloads] - static const downloads = MediaStoreCollection._('$_kPrefix.Downloads'); - - /// Available for Android [10 to 12] - /// - /// Equivalent to: - /// - [MediaStore.Images] - static const images = MediaStoreCollection._('$_kPrefix.Images'); - - /// Available for Android [10 to 12] - /// - /// Equivalent to: - /// - [MediaStore.Video] - static const video = MediaStoreCollection._('$_kPrefix.Video'); - - @override - bool operator ==(Object other) { - return other is MediaStoreCollection && other.id == id; - } - - @override - int get hashCode => id.hashCode; - - @override - String toString() => id; -} diff --git a/pubspec.yaml b/pubspec.yaml index 5f4f0de..f6ffe66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: shared_storage -description: "Flutter plugin to work with external storage." -version: 0.4.1 +description: "Flutter plugin to work with external storage with privacy-friendly APIs." +version: 0.6.0 homepage: https://github.com/lakscastro/shared-storage repository: https://github.com/lakscastro/shared-storage issue_tracker: https://github.com/lakscastro/shared-storage/issues