diff --git a/.all-contributorsrc b/.all-contributorsrc index 84cba1a..aedaff3 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -65,7 +65,9 @@ "avatar_url": "https://avatars.githubusercontent.com/u/758047?v=4", "profile": "https://eternityforest.com", "contributions": [ - "bug" + "bug", + "code", + "doc" ] } ], diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d69bb5..7dbd4e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## 0.4.0 + +Fix the current behavior of `listFiles` and `openDocumentFile` API. + +### Improvements + +- It's now possible to list contents of all subfolders of a granted Uri opened from `openDocumentTree` ([@EternityForest](https://github.com/EternityForest)). +- Now `ACTION_VIEW` intent builder through `openDocumentFile` API was fixed. So it's now possible to open any file of any kind in third party apps without needing specify the mime type. + +### Breaking changes + +- Removed Android specific APIs: + - `DocumentFile.listFiles` (Now it's only available globally). + - `buildDocumentUriUsingTree` removed due high coupling with Android API (Android specific API that are not useful on any other platforms). + - `buildDocumentUri` removed due high coupling with Android API (Android specific API that are not useful on any other platforms). + - `buildTreeDocumentUri` removed due high coupling with Android API (Android specific API that are not useful on any other platforms). +- `getDocumentThumbnail` now receives only the `uri` param instead of a `rootUri` and a `documentId`. +- `rootUri` field from `QueryMetadata` was removed due API ambiguity: there's no such concept in the Android API and this is not required by it to work well. + ## 0.3.1 Minor improvements and bug fixes: diff --git a/README.md b/README.md index 363751c..17cf1c6 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,14 @@ ## Documentation -#### See the website for [documentation](https://lakscastro.github.io/shared-storage) +See the website for [documentation](https://lakscastro.github.io/shared-storage). All documentation is also available under `/docs` to each released version which is the data source of the website. You can contribute to the documentation by just editing these files through the GitHub web editor! +To check all ways you can contribute to this package see [Contributing/Ways to contribute](https://lakscastro.github.io/shared-storage/Contributing/Ways%20to%20contribute/). + Latest changes are available on `master` branch and the actual latest published package version lives under `release` branch. All other branches are derivated from issues, new features or bug fixes. @@ -46,7 +48,7 @@ These are the brilliant minds behind the development of this plugin!
www.bibliotecaortodoxa.ro

💻 🐛 🤔
dangilbert

💻 🐛
dhaval-k-simformsolutions

🐛 🤔 -
Daniel Dunn

🐛 +
Daniel Dunn

🐛 💻 📖 diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt index 0806146..af90e89 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt @@ -14,13 +14,13 @@ import io.lakscastro.sharedstorage.ROOT_CHANNEL import io.lakscastro.sharedstorage.SharedStoragePlugin import io.lakscastro.sharedstorage.plugin.* import io.lakscastro.sharedstorage.storageaccessframework.lib.* +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.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch /** * Aimed to implement strictly only the APIs already available from the native and original @@ -75,57 +75,73 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : ) } PERSISTED_URI_PERMISSIONS -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - persistedUriPermissions(result) - } + persistedUriPermissions(result) RELEASE_PERSISTABLE_URI_PERMISSION -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - releasePersistableUriPermission(result, call.argument("uri") as String) - } + releasePersistableUriPermission( + result, + call.argument("uri") as String + ) FROM_TREE_URI -> - if (Build.VERSION.SDK_INT >= API_21) { - result.success( - createDocumentFileMap( - documentFromUri(plugin.context, call.argument("uri") as String) - ) + if (Build.VERSION.SDK_INT >= API_21) { + result.success( + createDocumentFileMap( + documentFromUri( + plugin.context, + call.argument("uri") as String + ) ) - } + ) + } CAN_WRITE -> - if (Build.VERSION.SDK_INT >= API_21) { - result.success( - documentFromUri(plugin.context, call.argument("uri") as String)?.canWrite() - ) - } + if (Build.VERSION.SDK_INT >= API_21) { + result.success( + documentFromUri( + plugin.context, + call.argument("uri") as String + )?.canWrite() + ) + } CAN_READ -> - if (Build.VERSION.SDK_INT >= API_21) { - val uri = call.argument("uri") as String + if (Build.VERSION.SDK_INT >= API_21) { + val uri = call.argument("uri") as String - result.success(documentFromUri(plugin.context, uri)?.canRead()) - } + 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() + 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() + 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() + documentFromUri( + plugin.context, + call.argument("uri") as String + )?.delete() ) } LAST_MODIFIED -> if (Build.VERSION.SDK_INT >= API_21) { - result.success( - documentFromUri(plugin.context, call.argument("uri") as String) - ?.lastModified() + val document = documentFromUri( + plugin.context, + call.argument("uri") as String ) + + result.success(document?.lastModified()) } CREATE_DIRECTORY -> { if (Build.VERSION.SDK_INT >= API_21) { @@ -146,7 +162,12 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : val displayName = call.argument("displayName") as String result.success( - createDocumentFileMap(documentFromUri(plugin.context, uri)?.findFile(displayName)) + createDocumentFileMap( + documentFromUri( + plugin.context, + uri + )?.findFile(displayName) + ) ) } } @@ -165,9 +186,9 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : } } else { result.notSupported( - RENAME_TO, - API_21, - mapOf("uri" to "$uri", "destination" to "$destination") + RENAME_TO, + API_21, + mapOf("uri" to "$uri", "destination" to "$destination") ) } } @@ -180,8 +201,13 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : val success = renameTo(displayName) result.success( - if (success) createDocumentFileMap(documentFromUri(plugin.context, this.uri)!!) - else null + if (success) createDocumentFileMap( + documentFromUri( + plugin.context, + this.uri + )!! + ) + else null ) } } else { @@ -235,9 +261,9 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : 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) DocumentsContract.EXTRA_INITIAL_URI + else DOCUMENTS_CONTRACT_EXTRA_INITIAL_URI, + tree?.uri ) } } @@ -380,7 +406,9 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : eventSink = events when (args["event"]) { - LIST_FILES -> listFilesEvent(eventSink, args) + LIST_FILES -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + listFilesEvent(eventSink, args) + } } } @@ -393,28 +421,27 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : private fun listFilesEvent(eventSink: EventChannel.EventSink?, args: Map<*, *>) { if (eventSink == null) return - val document = - if (Build.VERSION.SDK_INT >= API_24) { - documentFromUri(plugin.context, args["uri"] as String) ?: return - } else { - null - } + val columns = args["columns"] as List<*> + val uri = Uri.parse(args["uri"] as String) + val document = DocumentFile.fromTreeUri(plugin.context, uri) if (document == null) { eventSink.error( - EXCEPTION_NOT_SUPPORTED, - "Android SDK must be greater or equal than [Build.VERSION_CODES.N]", - "Got (Build.VERSION.SDK_INT): ${Build.VERSION.SDK_INT}" + EXCEPTION_NOT_SUPPORTED, + "Android SDK must be greater or equal than [Build.VERSION_CODES.N]", + "Got (Build.VERSION.SDK_INT): ${Build.VERSION.SDK_INT}" ) } else { - val columns = args["columns"] as List<*> - if (!document.canRead()) { val error = "You cannot read a URI that you don't have read permissions" Log.d("NO PERMISSION!!!", error) - eventSink.error(EXCEPTION_MISSING_PERMISSIONS, error, mapOf("uri" to args["uri"])) + eventSink.error( + EXCEPTION_MISSING_PERMISSIONS, + error, + mapOf("uri" to args["uri"]) + ) eventSink.endOfStream() } else { @@ -422,13 +449,13 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : CoroutineScope(Dispatchers.IO).launch { try { traverseDirectoryEntries( - plugin.context.contentResolver, - rootOnly = true, - rootUri = document.uri, - columns = - columns - .map { parseDocumentFileColumn(parseDocumentFileColumn(it as String)!!) } - .toTypedArray() + plugin.context.contentResolver, + rootOnly = true, + targetUri = document.uri, + columns = + columns + .map { parseDocumentFileColumn(parseDocumentFileColumn(it as String)!!) } + .toTypedArray() ) { data, _ -> launch(Dispatchers.Main) { eventSink.success(data) } } } finally { launch(Dispatchers.Main) { eventSink.endOfStream() } diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt index 71382a3..e25edd7 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileHelperApi.kt @@ -1,27 +1,15 @@ package io.lakscastro.sharedstorage.storageaccessframework import android.content.ActivityNotFoundException -import android.content.ContentUris -import android.content.Context import android.content.Intent -import android.database.Cursor import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.DocumentsContract -import android.provider.MediaStore import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.content.ContextCompat -import androidx.core.content.PermissionChecker 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.API_19 import io.lakscastro.sharedstorage.plugin.ActivityListener import io.lakscastro.sharedstorage.plugin.Listenable -import io.lakscastro.sharedstorage.plugin.notSupported import io.lakscastro.sharedstorage.storageaccessframework.lib.* /** @@ -53,40 +41,18 @@ internal class DocumentFileHelperApi(private val plugin: SharedStoragePlugin) : override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { OPEN_DOCUMENT_FILE -> openDocumentFile(call, result) - GET_REAL_PATH_FROM_URI -> getRealPathFromUri(call, result) else -> result.notImplemented() } } - private fun getRealPathFromUri(call: MethodCall, result: MethodChannel.Result) { - val uri = Uri.parse(call.argument("uri")!!) - if (Build.VERSION.SDK_INT >= API_19) { - result.success(getPath(plugin.context, uri)) - } else { - result.notSupported(GET_REAL_PATH_FROM_URI, API_19, mapOf("uri" to "$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) val intent = Intent(Intent.ACTION_VIEW).apply { - addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - addCategory(Intent.CATEGORY_DEFAULT) - - val uriWithProviderScheme = - Uri.Builder().let { - it.scheme(uri.scheme) - it.path(uri.path) - it.query(uri.query) - it.authority(uri.authority) - it.build() - } - - setDataAndType(uriWithProviderScheme, type) + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + data = uri } try { @@ -116,128 +82,6 @@ internal class DocumentFileHelperApi(private val plugin: SharedStoragePlugin) : } } - private fun hasPermission(permission: String): Boolean { - return ContextCompat.checkSelfPermission(plugin.binding!!.activity, permission) == - PermissionChecker.PERMISSION_GRANTED - } - - /** - * Get a file path from a Uri. This will get the the path for Storage Access Framework Documents, - * as well as the _data field for the MediaStore and other file-based ContentProviders. - * - * @param context The context. - * @param uri The Uri to query. - * @author paulburke - */ - @RequiresApi(Build.VERSION_CODES.KITKAT) - fun getPath(context: Context, uri: Uri): String? { - val isKitKat: Boolean = Build.VERSION.SDK_INT >= API_19 - - // DocumentProvider - if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { - // ExternalStorageProvider - if (isExternalStorageDocument(uri)) { - val docId: String = DocumentsContract.getDocumentId(uri) - val split = docId.split(":").toTypedArray() - val type = split[0] - if ("primary".equals(type, ignoreCase = true)) { - return Environment.getExternalStorageDirectory().toString() + "/" + split[1] - } - - // TODO handle non-primary volumes - } else if (isDownloadsDocument(uri)) { - val id: String = DocumentsContract.getDocumentId(uri) - val contentUri: Uri = - ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), - java.lang.Long.valueOf(id) - ) - - return getDataColumn(context, contentUri, null, null) - } else if (isMediaDocument(uri)) { - val docId: String = DocumentsContract.getDocumentId(uri) - val split = docId.split(":").toTypedArray() - - val contentUri: Uri? = - when (split[0]) { - "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> null - } - - val selection = "_id=?" - val selectionArgs = arrayOf(split[1]) - - contentUri?.let { - return getDataColumn(context, it, selection, selectionArgs) - } - } - } else if ("content".equals(uri.scheme, ignoreCase = true)) { - return getDataColumn(context, uri, null, null) - } else if ("file".equals(uri.scheme, ignoreCase = true)) { - return uri.path - } - - return null - } - - /** - * Get the value of the data column for this Uri. This is useful for MediaStore Uris, and other - * file-based ContentProviders. - * - * @param context The context. - * @param uri The Uri to query. - * @param selection (Optional) Filter used in the query. - * @param selectionArgs (Optional) Selection arguments used in the query. - * @return The value of the _data column, which is typically a file path. - */ - private fun getDataColumn( - context: Context, - uri: Uri, - selection: String?, - selectionArgs: Array? - ): String? { - var cursor: Cursor? = null - val column = "_data" - val projection = arrayOf(column) - try { - cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) - if (cursor != null && cursor.moveToFirst()) { - val columnIndex: Int = cursor.getColumnIndexOrThrow(column) - - return cursor.getString(columnIndex) - } - } finally { - cursor?.close() - } - return null - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is ExternalStorageProvider. - */ - private fun isExternalStorageDocument(uri: Uri): Boolean { - return "com.android.externalstorage.documents" == uri.authority - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is DownloadsProvider. - */ - fun isDownloadsDocument(uri: Uri): Boolean { - return "com.android.providers.downloads.documents" == uri.authority - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is MediaProvider. - */ - private fun isMediaDocument(uri: Uri): Boolean { - return "com.android.providers.media.documents" == uri.authority - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { when (requestCode) { /** TODO(@lakscastro): Implement if required */ diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentsContractApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentsContractApi.kt index fb14347..aca2a1b 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentsContractApi.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentsContractApi.kt @@ -13,7 +13,8 @@ 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.* +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 @@ -30,13 +31,10 @@ internal class DocumentsContractApi(private val plugin: SharedStoragePlugin) : when (call.method) { GET_DOCUMENT_THUMBNAIL -> { if (Build.VERSION.SDK_INT >= API_21) { - val rootUri = Uri.parse(call.argument("rootUri")) - val documentId = call.argument("documentId") + val uri = Uri.parse(call.argument("uri")) val width = call.argument("width")!! val height = call.argument("height")!! - val uri = DocumentsContract.buildDocumentUriUsingTree(rootUri, documentId) - val bitmap = DocumentsContract.getDocumentThumbnail( plugin.context.contentResolver, @@ -51,12 +49,12 @@ internal class DocumentsContractApi(private val plugin: SharedStoragePlugin) : 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 + "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) } @@ -66,42 +64,6 @@ internal class DocumentsContractApi(private val plugin: SharedStoragePlugin) : result.notSupported(call.method, API_21) } } - BUILD_DOCUMENT_URI_USING_TREE -> { - val treeUri = Uri.parse(call.argument("treeUri")) - val documentId = call.argument("documentId") - - if (Build.VERSION.SDK_INT >= API_21) { - val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId) - - result.success("$documentUri") - } else { - result.notImplemented() - } - } - BUILD_DOCUMENT_URI -> { - val authority = call.argument("authority") - val documentId = call.argument("documentId") - - if (Build.VERSION.SDK_INT >= API_21) { - val documentUri = DocumentsContract.buildDocumentUri(authority, documentId) - - result.success("$documentUri") - } else { - result.notSupported(call.method, API_21) - } - } - BUILD_TREE_DOCUMENT_URI -> { - val authority = call.argument("authority") - val documentId = call.argument("documentId") - - if (Build.VERSION.SDK_INT >= API_21) { - val treeDocumentUri = DocumentsContract.buildTreeDocumentUri(authority, documentId) - - result.success("$treeDocumentUri") - } else { - result.notSupported(call.method, API_21) - } - } } } diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentCommon.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentCommon.kt index 8e49462..d072e48 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentCommon.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/DocumentCommon.kt @@ -15,34 +15,6 @@ import io.lakscastro.sharedstorage.plugin.API_24 import java.io.ByteArrayOutputStream import java.io.Closeable -/** - * Helper class to make more easy to handle callbacks using Kotlin syntax - */ -data class CallbackHandler( - var onSuccess: (T.() -> Unit)? = null, - var onEnd: (() -> Unit)? = null -) - -/** - * Generate the `DocumentFile` reference from string `uri` (Single `DocumentFile`) - */ -@RequiresApi(API_21) -fun documentFromSingleUri(context: Context, uri: String): DocumentFile? = - documentFromSingleUri(context, Uri.parse(uri)) - -/** - * Generate the `DocumentFile` reference from string `uri` (Single `DocumentFile`) - */ -@RequiresApi(API_21) -fun documentFromSingleUri(context: Context, uri: Uri): DocumentFile? { - val documentUri = DocumentsContract.buildDocumentUri( - uri.authority, - DocumentsContract.getDocumentId(uri) - ) - - return DocumentFile.fromSingleUri(context, documentUri) -} - /** * Generate the `DocumentFile` reference from string `uri` */ @@ -65,6 +37,7 @@ fun documentFromUri( } } + /** * Standard map encoding of a `DocumentFile` and must be used before returning any `DocumentFile` * from plugin results, like: @@ -110,7 +83,6 @@ fun createDocumentFileMap(documentFile: DocumentFile?): Map? { * ``` */ fun createCursorRowMap( - rootUri: Uri, parentUri: Uri, uri: Uri, data: Map, @@ -132,7 +104,6 @@ fun createCursorRowMap( "data" to formattedData, "metadata" to mapOf( "parentUri" to "$parentUri", - "rootUri" to "$rootUri", "isDirectory" to isDirectory, "uri" to "$uri" ) @@ -156,18 +127,29 @@ fun closeQuietly(closeable: Closeable?) { @RequiresApi(API_21) fun traverseDirectoryEntries( contentResolver: ContentResolver, - rootUri: Uri, + targetUri: Uri, columns: Array, rootOnly: Boolean, block: (data: Map, isLast: Boolean) -> Unit ): Boolean { + val documentId = try { + DocumentsContract.getDocumentId(targetUri) + } catch(e: IllegalArgumentException) { + DocumentsContract.getTreeDocumentId(targetUri) + } + val treeDocumentId = DocumentsContract.getTreeDocumentId(targetUri) + + val rootUri = DocumentsContract.buildTreeDocumentUri( + targetUri.authority, + treeDocumentId + ) val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( rootUri, - DocumentsContract.getTreeDocumentId(rootUri) + documentId ) - /// Keep track of our directory hierarchy - val dirNodes = mutableListOf>(Pair(rootUri, childrenUri)) + // Keep track of our directory hierarchy + val dirNodes = mutableListOf(Pair(targetUri, childrenUri)) while (dirNodes.isNotEmpty()) { val (parent, children) = dirNodes.removeAt(0) @@ -187,7 +169,7 @@ fun traverseDirectoryEntries( val cursor = contentResolver.query( children, projection, - /// TODO: Add support for `selection`, `selectionArgs` and `sortOrder` + // TODO: Add support for `selection`, `selectionArgs` and `sortOrder` null, null, null @@ -217,7 +199,7 @@ fun traverseDirectoryEntries( val isDirectory = if (mimeType != null) isDirectory(mimeType) else null val uri = DocumentsContract.buildDocumentUriUsingTree( - parent, + rootUri, DocumentsContract.getDocumentId( DocumentsContract.buildDocumentUri(parent.authority, id) ) @@ -225,7 +207,7 @@ fun traverseDirectoryEntries( if (isDirectory == true && !rootOnly) { val nextChildren = - DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, id) + DocumentsContract.buildChildDocumentsUriUsingTree(targetUri, id) val nextNode = Pair(uri, nextChildren) @@ -234,7 +216,6 @@ fun traverseDirectoryEntries( block( createCursorRowMap( - rootUri, parent, uri, data, @@ -267,7 +248,7 @@ fun bitmapToBase64(bitmap: Bitmap): String { } /** - * Trick to verify if is a tree URi even not in API 26+ + * Trick to verify if is a tree URI even not in API 26+ */ fun isTreeUri(uri: Uri): Boolean { if (Build.VERSION.SDK_INT >= API_24) { diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt index 05e3c59..d1ed988 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt @@ -3,8 +3,6 @@ package io.lakscastro.sharedstorage.storageaccessframework.lib /** * Exceptions */ -const val EXCEPTION_PARENT_DOCUMENT_MUST_BE_DIRECTORY = - "EXCEPTION_PARENT_DOCUMENT_MUST_BE_DIRECTORY" const val EXCEPTION_MISSING_PERMISSIONS = "EXCEPTION_MISSING_PERMISSIONS" const val EXCEPTION_CANT_OPEN_DOCUMENT_FILE = "EXCEPTION_CANT_OPEN_DOCUMENT_FILE" @@ -18,9 +16,6 @@ const val EXCEPTION_CANT_OPEN_FILE_DUE_SECURITY_POLICY = const val DOCUMENTS_CONTRACT_EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI" -const val PLUGIN_FILE_PROVIDER_PACKAGE_NAME = - "fileprovider.io.lakscastro.sharedstorage.storageaccessframework.lib" - /** * Available DocumentFile Method Channel APIs */ @@ -41,16 +36,12 @@ const val FIND_FILE = "findFile" const val COPY = "copy" const val LAST_MODIFIED = "lastModified" const val GET_DOCUMENT_THUMBNAIL = "getDocumentThumbnail" -const val BUILD_DOCUMENT_URI_USING_TREE = "buildDocumentUriUsingTree" -const val BUILD_DOCUMENT_URI = "buildDocumentUri" -const val BUILD_TREE_DOCUMENT_URI = "buildTreeDocumentUri" const val CHILD = "child" /** * Available DocumentFileHelper Method Channel APIs */ const val OPEN_DOCUMENT_FILE = "openDocumentFile" -const val GET_REAL_PATH_FROM_URI = "getRealPathFromUri" /** * Available Event Channels APIs diff --git a/docs/Usage/Storage Access Framework.md b/docs/Usage/Storage Access Framework.md index 41cd5d7..34f2fdd 100644 --- a/docs/Usage/Storage Access Framework.md +++ b/docs/Usage/Storage Access Framework.md @@ -71,7 +71,7 @@ This method list files lazily **over a granted uri:** > **Note** `DocumentFileColumn.id` is optional. It is required to fetch the file list from native API. So it is enabled regardless if you include this column or not. And this applies only to this API (`listFiles`). ```dart -/// *Must* be a granted uri from `openDocumentTree` +/// *Must* be a granted uri from `openDocumentTree`, or a URI representing a child under such a granted uri. final Uri myGrantedUri = ... final DocumentFile? documentFileOfMyGrantedUri = await myGrantedUri.toDocumentFile(); @@ -321,56 +321,6 @@ const List columns = [ final Stream onNewFileLoaded = documentFileOfMyGrantedUri.listFiles(columns); ``` -### buildDocumentUriUsingTree - -Mirror of [`DocumentsContract.buildDocumentUriUsingTree`]() - -This is typically used to access documents under a user-selected directory tree, since it doesn't require the user to separately confirm each new document access. - -For details refer to the [original docs](). - -```dart -final Uri treeUri = ... - -final PartialDocumentFile partialDocumentFile = ... - -final String documentId = partialFile.data![DocumentFileColumn.id]!; - -final Uri? documentUri = await buildDocumentUriUsingTree(treeUri, documentId); -``` - -### buildDocumentUri - -Mirror of [`DocumentsContract.buildDocumentUri`]() - -Less common method. Use it when you need to (e.g some SAF tutorial/API point to this method or you want a custom authority when building the document file). - -For details refer to the [original docs](). - -```dart -final String customAuthority = ... -final String documentId = ... - -final Uri? documentUri = await buildDocumentUri(customAuthority, documentId); -``` - -### buildTreeDocumentUri - -Mirror of [`DocumentsContract.buildTreeDocumentUri`]() - -Less common method. Use it when you need to (e.g some SAF tutorial/API point to this method or you want a custom authority when building the document file). - -Build URI representing access to descendant documents of the given [`Document#COLUMN_DOCUMENT_ID`](https://developer.android.com/reference/android/provider/DocumentsContract.Document#COLUMN_DOCUMENT_ID). - -For details refer to the [original docs](). - -```dart -final String customAuthority = ... -final String documentId = ... - -final Uri? documentUri = await buildDocumentUri(customAuthority, documentId); -``` - ### delete Mirror of [`DocumentFile.delete`]() diff --git a/docs/index.md b/docs/index.md index 6b04301..62b25fe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -79,7 +79,7 @@ These are the brilliant minds behind the development of this plugin!
www.bibliotecaortodoxa.ro

💻 🐛 🤔
dangilbert

💻 🐛
dhaval-k-simformsolutions

🐛 🤔 -
Daniel Dunn

🐛 +
Daniel Dunn

🐛 💻 📖 diff --git a/example/lib/screens/folder_files/folder_file_card.dart b/example/lib/screens/file_explorer/file_explorer_card.dart similarity index 85% rename from example/lib/screens/folder_files/folder_file_card.dart rename to example/lib/screens/file_explorer/file_explorer_card.dart index d970b09..874762e 100644 --- a/example/lib/screens/folder_files/folder_file_card.dart +++ b/example/lib/screens/file_explorer/file_explorer_card.dart @@ -9,10 +9,10 @@ import '../../theme/spacing.dart'; import '../../widgets/buttons.dart'; import '../../widgets/key_value_text.dart'; import '../../widgets/simple_card.dart'; -import 'folder_file_list.dart'; +import 'file_explorer_page.dart'; -class FolderFileCard extends StatefulWidget { - const FolderFileCard({ +class FileExplorerCard extends StatefulWidget { + const FileExplorerCard({ Key? key, required this.partialFile, required this.didUpdateDocument, @@ -22,10 +22,10 @@ class FolderFileCard extends StatefulWidget { final void Function(PartialDocumentFile?) didUpdateDocument; @override - _FolderFileCardState createState() => _FolderFileCardState(); + _FileExplorerCardState createState() => _FileExplorerCardState(); } -class _FolderFileCardState extends State { +class _FileExplorerCardState extends State { PartialDocumentFile get file => widget.partialFile; static const _size = Size.square(150); @@ -33,14 +33,12 @@ class _FolderFileCardState extends State { Uint8List? imageBytes; Future _loadThumbnailIfAvailable() async { - final rootUri = file.metadata?.rootUri; - final documentId = file.data?[DocumentFileColumn.id] as String?; + final uri = file.metadata?.uri; - if (rootUri == null || documentId == null) return; + if (uri == null) return; final bitmap = await getDocumentThumbnail( - rootUri: rootUri, - documentId: documentId, + uri: uri, width: _size.width, height: _size.height, ); @@ -60,7 +58,7 @@ class _FolderFileCardState extends State { } @override - void didUpdateWidget(covariant FolderFileCard oldWidget) { + void didUpdateWidget(covariant FileExplorerCard oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.partialFile.data?[DocumentFileColumn.id] != @@ -78,7 +76,7 @@ class _FolderFileCardState extends State { void _openFolderFileListPage(Uri uri) { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => FolderFileList(uri: uri), + builder: (context) => FileExplorerPage(uri: uri), ), ); } @@ -153,7 +151,6 @@ class _FolderFileCardState extends State { 'summary': '${file.data?[DocumentFileColumn.summary]}', 'id': '${file.data?[DocumentFileColumn.id]}', 'parentUri': '${file.metadata?.parentUri}', - 'rootUri': '${file.metadata?.rootUri}', 'uri': '${file.metadata?.uri}', }, ), @@ -164,12 +161,9 @@ class _FolderFileCardState extends State { 'Open Directory', onTap: () async { if (_isDirectory) { - final uri = await buildTreeDocumentUri( - file.metadata!.rootUri!.authority, - file.data![DocumentFileColumn.id] as String, + _openFolderFileListPage( + file.metadata!.uri!, ); - - _openFolderFileListPage(uri!); } }, ), diff --git a/example/lib/screens/folder_files/folder_file_list.dart b/example/lib/screens/file_explorer/file_explorer_page.dart similarity index 87% rename from example/lib/screens/folder_files/folder_file_list.dart rename to example/lib/screens/file_explorer/file_explorer_page.dart index 292e35f..91fc1e1 100644 --- a/example/lib/screens/folder_files/folder_file_list.dart +++ b/example/lib/screens/file_explorer/file_explorer_page.dart @@ -7,18 +7,21 @@ import '../../theme/spacing.dart'; import '../../widgets/buttons.dart'; import '../../widgets/light_text.dart'; import '../../widgets/simple_card.dart'; -import 'folder_file_card.dart'; +import 'file_explorer_card.dart'; -class FolderFileList extends StatefulWidget { - const FolderFileList({Key? key, required this.uri}) : super(key: key); +class FileExplorerPage extends StatefulWidget { + const FileExplorerPage({ + Key? key, + required this.uri, + }) : super(key: key); final Uri uri; @override - _FolderFileListState createState() => _FolderFileListState(); + _FileExplorerPageState createState() => _FileExplorerPageState(); } -class _FolderFileListState extends State { +class _FileExplorerPageState extends State { List? _files; late bool _hasPermission; @@ -88,7 +91,7 @@ class _FolderFileListState extends State { (context, index) { final file = _files![index]; - return FolderFileCard( + return FileExplorerCard( partialFile: file, didUpdateDocument: (document) { if (document == null) { @@ -153,18 +156,22 @@ class _FolderFileListState extends State { return setState(() => _files = []); } - final documentUri = await widget.uri.toDocumentFile(); + final folderUri = widget.uri; const columns = [ DocumentFileColumn.displayName, DocumentFileColumn.size, DocumentFileColumn.lastModified, - // Optional column (this can't be removed because it's required to list files) - DocumentFileColumn.id, DocumentFileColumn.mimeType, + // The column below is a optional column + // you can wether include or not here and + // it will be always available on the results + DocumentFileColumn.id, ]; - _listener = documentUri?.listFiles(columns).listen( + final fileListStream = listFiles(folderUri, columns: columns); + + _listener = fileListStream.listen( (file) { /// Append new files to the current file list _files = [...?_files, file]; diff --git a/example/lib/screens/persisted_uris/persisted_uri_card.dart b/example/lib/screens/granted_uris/granted_uri_card.dart similarity index 92% rename from example/lib/screens/persisted_uris/persisted_uri_card.dart rename to example/lib/screens/granted_uris/granted_uri_card.dart index 465b6e1..6887dce 100644 --- a/example/lib/screens/persisted_uris/persisted_uri_card.dart +++ b/example/lib/screens/granted_uris/granted_uri_card.dart @@ -7,8 +7,8 @@ import '../../widgets/key_value_text.dart'; import '../../widgets/simple_card.dart'; import '../folder_files/folder_file_list.dart'; -class PersistedUriCard extends StatefulWidget { - const PersistedUriCard({ +class GrantedUriCard extends StatefulWidget { + const GrantedUriCard({ Key? key, required this.permissionUri, required this.onChange, @@ -18,10 +18,10 @@ class PersistedUriCard extends StatefulWidget { final VoidCallback onChange; @override - _PersistedUriCardState createState() => _PersistedUriCardState(); + _GrantedUriCardState createState() => _GrantedUriCardState(); } -class _PersistedUriCardState extends State { +class _GrantedUriCardState extends State { Future _appendSampleFile(Uri parentUri) async { /// Create a new file inside the `parentUri` final documentFile = await parentUri.toDocumentFile(); diff --git a/example/lib/screens/persisted_uris/persisted_uri_list.dart b/example/lib/screens/granted_uris/granted_uris_page.dart similarity index 89% rename from example/lib/screens/persisted_uris/persisted_uri_list.dart rename to example/lib/screens/granted_uris/granted_uris_page.dart index 76c9547..fa9ea92 100644 --- a/example/lib/screens/persisted_uris/persisted_uri_list.dart +++ b/example/lib/screens/granted_uris/granted_uris_page.dart @@ -4,16 +4,16 @@ import 'package:shared_storage/saf.dart'; import '../../theme/spacing.dart'; import '../../widgets/light_text.dart'; -import 'persisted_uri_card.dart'; +import 'granted_uri_card.dart'; -class PersistedUriList extends StatefulWidget { - const PersistedUriList({Key? key}) : super(key: key); +class GrantedUrisPage extends StatefulWidget { + const GrantedUrisPage({Key? key}) : super(key: key); @override - _PersistedUriListState createState() => _PersistedUriListState(); + _GrantedUrisPageState createState() => _GrantedUrisPageState(); } -class _PersistedUriListState extends State { +class _GrantedUrisPageState extends State { List? persistedPermissionUris; @override @@ -81,7 +81,7 @@ class _PersistedUriListState extends State { _buildNoFolderAllowedYetWarning() else for (final permissionUri in persistedPermissionUris!) - PersistedUriCard( + GrantedUriCard( permissionUri: permissionUri, onChange: _loadPersistedUriPermissions, ) diff --git a/lib/src/saf/document_file.dart b/lib/src/saf/document_file.dart index 3298001..e072d20 100644 --- a/lib/src/saf/document_file.dart +++ b/lib/src/saf/document_file.dart @@ -1,8 +1,6 @@ import 'dart:typed_data'; import '../common/functional_extender.dart'; -import 'document_file_column.dart'; -import 'partial_document_file.dart'; import 'saf.dart' as saf; extension UriDocumentFileUtils on Uri { @@ -97,10 +95,6 @@ class DocumentFile { /// {@macro sharedstorage.saf.canWrite} Future canWrite() async => saf.canWrite(uri); - /// {@macro sharedstorage.saf.listFiles} - Stream listFiles(List columns) => - saf.listFiles(uri, columns: columns); - /// {@macro sharedstorage.saf.exists} Future exists() => saf.exists(uri); diff --git a/lib/src/saf/partial_document_file.dart b/lib/src/saf/partial_document_file.dart index d22662a..0efdc4a 100644 --- a/lib/src/saf/partial_document_file.dart +++ b/lib/src/saf/partial_document_file.dart @@ -52,7 +52,6 @@ class PartialDocumentFile { class QueryMetadata { const QueryMetadata._({ required this.parentUri, - required this.rootUri, required this.isDirectory, required this.uri, }); @@ -60,14 +59,12 @@ class QueryMetadata { factory QueryMetadata.fromMap(Map map) { return QueryMetadata._( parentUri: _parseUri(map['parentUri'] as String?), - rootUri: _parseUri(map['rootUri'] as String?), isDirectory: map['isDirectory'] as bool?, uri: _parseUri(map['uri'] as String?), ); } final Uri? parentUri; - final Uri? rootUri; final bool? isDirectory; final Uri? uri; @@ -80,7 +77,6 @@ class QueryMetadata { Map toMap() { return { 'parentUri': '$parentUri', - 'rootUri': '$rootUri', 'isDirectory': isDirectory, 'uri': uri, }; @@ -91,11 +87,10 @@ class QueryMetadata { if (other is! QueryMetadata) return false; return other.parentUri == parentUri && - other.rootUri == rootUri && other.isDirectory == isDirectory && other.uri == uri; } @override - int get hashCode => Object.hash(parentUri, rootUri, isDirectory, uri); + int get hashCode => Object.hash(parentUri, isDirectory, uri); } diff --git a/lib/src/saf/saf.dart b/lib/src/saf/saf.dart index a63ecad..3b79293 100644 --- a/lib/src/saf/saf.dart +++ b/lib/src/saf/saf.dart @@ -102,14 +102,12 @@ Future canWrite(Uri uri) async => kDocumentFileChannel /// [Refer to details](https://developer.android.com/reference/android/provider/DocumentsContract#getDocumentThumbnail(android.content.ContentResolver,%20android.net.Uri,%20android.graphics.Point,%20android.os.CancellationSignal)). /// {@endtemplate} Future getDocumentThumbnail({ - required Uri rootUri, - required String documentId, + required Uri uri, required double width, required double height, }) async { final args = { - 'rootUri': '$rootUri', - 'documentId': documentId, + 'uri': '$uri', 'width': width, 'height': height, }; @@ -173,63 +171,6 @@ Stream listFiles( Future exists(Uri uri) async => kDocumentFileChannel .invokeMethod('exists', {'uri': '$uri'}); -/// {@template sharedstorage.saf.buildDocumentUriUsingTree} -/// Equivalent to `DocumentsContract.buildDocumentUriUsingTree`. -/// -/// [Refer to details](https://developer.android.com/reference/android/provider/DocumentsContract#buildDocumentUriUsingTree%28android.net.Uri,%20java.lang.String%29). -/// {@endtemplate} -Future buildDocumentUriUsingTree(Uri treeUri, String documentId) async { - final args = { - 'treeUri': '$treeUri', - 'documentId': documentId, - }; - - final uri = await kDocumentsContractChannel.invokeMethod( - 'buildDocumentUriUsingTree', - args, - ); - - return uri?.apply((u) => Uri.parse(u)); -} - -/// {@template sharedstorage.saf.buildDocumentUri} -/// Equivalent to `DocumentsContract.buildDocumentUri`. -/// -/// [Refer to details](https://developer.android.com/reference/android/provider/DocumentsContract#buildDocumentUri%28java.lang.String,%20java.lang.String%29). -/// {@endtemplate} -Future buildDocumentUri(String authority, String documentId) async { - final args = { - 'authority': authority, - 'documentId': documentId, - }; - - final uri = await kDocumentsContractChannel.invokeMethod( - 'buildDocumentUri', - args, - ); - - return uri?.apply((u) => Uri.parse(u)); -} - -/// {@template sharedstorage.saf.buildDocumentUri} -/// Equivalent to `DocumentsContract.buildDocumentUri`. -/// -/// [Refer to details](https://developer.android.com/reference/android/provider/DocumentsContract#buildDocumentUri%28java.lang.String,%20java.lang.String%29). -/// {@endtemplate} -Future buildTreeDocumentUri(String authority, String documentId) async { - final args = { - 'authority': authority, - 'documentId': documentId, - }; - - final uri = await kDocumentsContractChannel.invokeMethod( - 'buildTreeDocumentUri', - args, - ); - - return uri?.apply((u) => Uri.parse(u)); -} - /// {@template sharedstorage.saf.delete} /// Equivalent to `DocumentFile.delete`. /// diff --git a/pubspec.yaml b/pubspec.yaml index ccb99ff..4ab815f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: shared_storage description: "Flutter plugin to get Android shared folders like DCIM, Downloads, Video, Audio. Works with Android 4.1 (API Level 16+)" -version: 0.3.1 +version: 0.4.0 homepage: https://github.com/lakscastro/shared-storage repository: https://github.com/lakscastro/shared-storage issue_tracker: https://github.com/lakscastro/shared-storage/issues