From 9292ce751aa90739089feafd826220b9d336bdde Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Mon, 7 Nov 2022 21:06:29 -0300 Subject: [PATCH 01/11] Fix `README.md` typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7965e49..0e50eef 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ All other branches are derivated from issues, new features or bug fixes. ## Contributors -- [jfaltis](https://github.com/jfaltis) fixed a memory leak and implemented an API to override existing files, thanks for you contribution! +- [jfaltis](https://github.com/jfaltis) fixed a memory leak and implemented an API to override existing files, thanks for your contribution! - [EternityForest](https://github.com/EternityForest) did fix a severe crash when the ID column was not provided and implemented a new feature to list all subfolders, thanks man! - Thanks [dhaval-k-simformsolutions](https://github.com/dhaval-k-simformsolutions) for taking time to submit bug reports related to duplicated file entries! - [dangilbert](https://github.com/dangilbert) pointed and fixed bug when the user doesn't select a folder, thanks man! From f10d9229b238b01a628a22035f4bfffe3e6b96e6 Mon Sep 17 00:00:00 2001 From: honjow Date: Fri, 11 Nov 2022 23:58:06 +0800 Subject: [PATCH 02/11] add openDocument --- .../storageaccessframework/DocumentFileApi.kt | 51 +++++++++++++++++++ .../lib/StorageAccessFrameworkConstant.kt | 2 + lib/src/saf/saf.dart | 20 ++++++++ 3 files changed, 73 insertions(+) diff --git a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt index 5f1bc30..895b93d 100644 --- a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt +++ b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt @@ -60,6 +60,10 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : result.notSupported(call.method, API_21) } } + OPEN_DOCUMENT -> + if (Build.VERSION.SDK_INT >= API_21) { + openDocument(call, result) + } OPEN_DOCUMENT_TREE -> if (Build.VERSION.SDK_INT >= API_21) { openDocumentTree(call, result) @@ -250,6 +254,33 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : } } + @RequiresApi(API_21) + private fun openDocument(call: MethodCall, result: MethodChannel.Result) { + + val initialUri = call.argument("initialUri") + + val intent = + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + + if (initialUri != null) { + val tree = DocumentFile.fromTreeUri(plugin.context, Uri.parse(initialUri)) + if (Build.VERSION.SDK_INT >= API_26) { + putExtra(DocumentsContract.EXTRA_INITIAL_URI, tree?.uri) + } + } + + type = call.argument("mimeType") ?: "*/*" + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, call.argument("multiple") ?: false) + } + + if (pendingResults[OPEN_DOCUMENT_CODE] != null) return + + pendingResults[OPEN_DOCUMENT_CODE] = Pair(call, result) + + plugin.binding?.activity?.startActivityForResult(intent, OPEN_DOCUMENT_CODE) + } + @RequiresApi(API_21) private fun openDocumentTree(call: MethodCall, result: MethodChannel.Result) { val grantWritePermission = call.argument("grantWritePermission")!! @@ -394,6 +425,26 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : pendingResults.remove(OPEN_DOCUMENT_TREE_CODE) } } + OPEN_DOCUMENT_CODE -> { + val pendingResult = pendingResults[OPEN_DOCUMENT_CODE] ?: return false + + try { + // if data.clipData not null, uriList from data.clipData, else uriList is data.data + val uriList = data?.clipData?.let { + (0 until it.itemCount).map { i -> it.getItemAt(i).uri } + } ?: data?.data?.let { listOf(it) } + + if (uriList != null) { + pendingResult.second.success(uriList.map { "$it" }) + + return true + } + + pendingResult.second.success(null) + } finally { + pendingResults.remove(OPEN_DOCUMENT_CODE) + } + } } return false diff --git a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt index 41d1de4..ad61abf 100644 --- a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt +++ b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt @@ -19,6 +19,7 @@ const val DOCUMENTS_CONTRACT_EXTRA_INITIAL_URI = /** * Available DocumentFile Method Channel APIs */ +const val OPEN_DOCUMENT = "openDocument" const val OPEN_DOCUMENT_TREE = "openDocumentTree" const val PERSISTED_URI_PERMISSIONS = "persistedUriPermissions" const val RELEASE_PERSISTABLE_URI_PERMISSION = "releasePersistableUriPermission" @@ -54,3 +55,4 @@ const val GET_DOCUMENT_CONTENT = "getDocumentContent" * Intent Request Codes */ const val OPEN_DOCUMENT_TREE_CODE = 10 +const val OPEN_DOCUMENT_CODE = 11 diff --git a/lib/src/saf/saf.dart b/lib/src/saf/saf.dart index 7357c21..3d7accd 100644 --- a/lib/src/saf/saf.dart +++ b/lib/src/saf/saf.dart @@ -35,6 +35,26 @@ Future openDocumentTree({ return selectedDirectoryUri?.apply((e) => Uri.parse(e)); } +/// [Refer to details](https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT). +Future?> openDocument({ + Uri? initialUri, + String mimeType = '*/*', + bool multiple = false, +}) async { + const kOpenDocument = 'openDocument'; + + final args = { + if (initialUri != null) 'initialUri': '$initialUri', + 'mimeType': mimeType, + 'multiple': multiple, + }; + + final selectedUriList = + await kDocumentFileChannel.invokeListMethod(kOpenDocument, args); + + return selectedUriList?.apply((e) => e.map((e) => Uri.parse(e as String)).toList()); +} + /// {@template sharedstorage.saf.persistedUriPermissions} /// Returns an `List` with all persisted [Uri] /// From 00be06730d0952ccc1943a043f8f0ffd89203b67 Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Fri, 11 Nov 2022 15:29:10 -0300 Subject: [PATCH 03/11] (#110) Update `v0.7.0` --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index c83f734..69bcc78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: shared_storage description: "Flutter plugin to work with external storage and privacy-friendly APIs." -version: 0.6.0 +version: 0.7.0 homepage: https://github.com/alexrintt/shared-storage repository: https://github.com/alexrintt/shared-storage issue_tracker: https://github.com/alexrintt/shared-storage/issues From 3ad5b54dd3fb39d7aada87dece853353661da418 Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Fri, 11 Nov 2022 15:31:01 -0300 Subject: [PATCH 04/11] (#110) Add `persistablePermission` and `grantWritePermission` to `openDocumentTree` and `openDocument` --- .../storageaccessframework/DocumentFileApi.kt | 25 +++++++++++++--- lib/src/saf/saf.dart | 9 +++++- lib/src/saf/uri_permission.dart | 30 +++++++++++++++---- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt index 895b93d..ee42f84 100644 --- a/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt +++ b/android/src/main/kotlin/io/alexrintt/sharedstorage/storageaccessframework/DocumentFileApi.kt @@ -7,6 +7,7 @@ import android.provider.DocumentsContract import android.util.Log import androidx.annotation.RequiresApi import androidx.documentfile.provider.DocumentFile +import com.anggrayudi.storage.extension.isTreeDocumentFile import com.anggrayudi.storage.file.child import io.flutter.plugin.common.* import io.flutter.plugin.common.EventChannel.StreamHandler @@ -256,7 +257,6 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : @RequiresApi(API_21) private fun openDocument(call: MethodCall, result: MethodChannel.Result) { - val initialUri = call.argument("initialUri") val intent = @@ -380,7 +380,8 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : "isReadPermission" to it.isReadPermission, "isWritePermission" to it.isWritePermission, "persistedTime" to it.persistedTime, - "uri" to "${it.uri}" + "uri" to "${it.uri}", + "isTreeDocumentFile" to it.uri.isTreeDocumentFile ) } .toList() @@ -404,16 +405,19 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : val pendingResult = pendingResults[OPEN_DOCUMENT_TREE_CODE] ?: return false val grantWritePermission = pendingResult.first.argument("grantWritePermission")!! + val persistablePermission = pendingResult.first.argument("persistablePermission")!! try { val uri = data?.data if (uri != null) { - plugin.context.contentResolver.takePersistableUriPermission( + if (persistablePermission) { + plugin.context.contentResolver.takePersistableUriPermission( uri, if (grantWritePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION else Intent.FLAG_GRANT_READ_URI_PERMISSION - ) + ) + } pendingResult.second.success("$uri") @@ -428,6 +432,9 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : OPEN_DOCUMENT_CODE -> { val pendingResult = pendingResults[OPEN_DOCUMENT_CODE] ?: return false + val grantWritePermission = pendingResult.first.argument("grantWritePermission")!! + val persistablePermission = pendingResult.first.argument("persistablePermission")!! + try { // if data.clipData not null, uriList from data.clipData, else uriList is data.data val uriList = data?.clipData?.let { @@ -435,6 +442,16 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : } ?: data?.data?.let { listOf(it) } if (uriList != null) { + if (persistablePermission) { + for (uri in uriList) { + plugin.context.contentResolver.takePersistableUriPermission( + uri, + if (grantWritePermission) Intent.FLAG_GRANT_WRITE_URI_PERMISSION + else Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + } + pendingResult.second.success(uriList.map { "$it" }) return true diff --git a/lib/src/saf/saf.dart b/lib/src/saf/saf.dart index 3d7accd..b505bf4 100644 --- a/lib/src/saf/saf.dart +++ b/lib/src/saf/saf.dart @@ -20,12 +20,14 @@ import 'common.dart'; /// {@endtemplate} Future openDocumentTree({ bool grantWritePermission = true, + bool persistablePermission = true, Uri? initialUri, }) async { const kOpenDocumentTree = 'openDocumentTree'; final args = { 'grantWritePermission': grantWritePermission, + 'persistablePermission': persistablePermission, if (initialUri != null) 'initialUri': '$initialUri', }; @@ -38,6 +40,8 @@ Future openDocumentTree({ /// [Refer to details](https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT). Future?> openDocument({ Uri? initialUri, + bool grantWritePermission = true, + bool persistablePermission = true, String mimeType = '*/*', bool multiple = false, }) async { @@ -45,6 +49,8 @@ Future?> openDocument({ final args = { if (initialUri != null) 'initialUri': '$initialUri', + 'grantWritePermission': grantWritePermission, + 'persistablePermission': persistablePermission, 'mimeType': mimeType, 'multiple': multiple, }; @@ -52,7 +58,8 @@ Future?> openDocument({ final selectedUriList = await kDocumentFileChannel.invokeListMethod(kOpenDocument, args); - return selectedUriList?.apply((e) => e.map((e) => Uri.parse(e as String)).toList()); + return selectedUriList + ?.apply((e) => e.map((e) => Uri.parse(e as String)).toList()); } /// {@template sharedstorage.saf.persistedUriPermissions} diff --git a/lib/src/saf/uri_permission.dart b/lib/src/saf/uri_permission.dart index d0327ac..5ec93d9 100644 --- a/lib/src/saf/uri_permission.dart +++ b/lib/src/saf/uri_permission.dart @@ -2,7 +2,7 @@ /// This grants may have been created via `Intent#FLAG_GRANT_READ_URI_PERMISSION`, /// etc when sending an `Intent`, or explicitly through `Context#grantUriPermission(String, android.net.Uri, int)`. /// -/// [Refer to details](https://developer.android.com/reference/android/content/UriPermission) +/// [Refer to details](https://developer.android.com/reference/android/content/UriPermission). class UriPermission { /// Even we allow create instances of this class avoid it and use /// `persistedUriPermissions` API instead @@ -11,6 +11,7 @@ class UriPermission { required this.isWritePermission, required this.persistedTime, required this.uri, + required this.isTreeDocumentFile, }); factory UriPermission.fromMap(Map map) { @@ -19,6 +20,7 @@ class UriPermission { isWritePermission: map['isWritePermission'] as bool, persistedTime: map['persistedTime'] as int, uri: Uri.parse(map['uri'] as String), + isTreeDocumentFile: map['isTreeDocumentFile'] as bool, ); } @@ -40,17 +42,33 @@ class UriPermission { /// [Refer to details](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/UriPermission.java#56) final Uri uri; + /// Whether or not a tree document file. + /// + /// Tree document files are granted through [openDocumentTree] method, that is, when the user select a folder-like tree document file. + /// Document files are granted through [openDocument] method, that is, when the user select (a) specific(s) document files. + /// + /// Roughly you may consider it as a property to verify if [this] permission is over a folder or a single-file. + final bool isTreeDocumentFile; + @override bool operator ==(Object other) => other is UriPermission && isReadPermission == other.isReadPermission && isWritePermission == other.isWritePermission && persistedTime == other.persistedTime && - uri == other.uri; + uri == other.uri && + isTreeDocumentFile == other.isTreeDocumentFile; @override - int get hashCode => - Object.hash(isReadPermission, isWritePermission, persistedTime, uri); + int get hashCode => Object.hashAll( + [ + isReadPermission, + isWritePermission, + persistedTime, + uri, + isTreeDocumentFile, + ], + ); Map toMap() { return { @@ -58,6 +76,7 @@ class UriPermission { 'isWritePermission': isWritePermission, 'persistedTime': persistedTime, 'uri': '$uri', + 'isTreeDocumentFile': isTreeDocumentFile, }; } @@ -66,5 +85,6 @@ class UriPermission { 'isReadPermission: $isReadPermission, ' 'isWritePermission: $isWritePermission, ' 'persistedTime: $persistedTime, ' - 'uri: $uri)'; + 'uri: $uri, ' + 'isTreeDocumentFile: $isTreeDocumentFile)'; } From bb9ea89b7e66665b6a05563fdff701e3bab81835 Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Fri, 11 Nov 2022 15:31:25 -0300 Subject: [PATCH 05/11] (#110) Update example project to cover `openDocument` API --- .../file_explorer/file_explorer_card.dart | 80 +-------------- .../granted_uris/granted_uri_card.dart | 99 ++++++++++++++++--- .../granted_uris/granted_uris_page.dart | 56 +++++++++-- example/lib/utils/document_file_utils.dart | 90 +++++++++++++++++ 4 files changed, 228 insertions(+), 97 deletions(-) create mode 100644 example/lib/utils/document_file_utils.dart diff --git a/example/lib/screens/file_explorer/file_explorer_card.dart b/example/lib/screens/file_explorer/file_explorer_card.dart index 3e14a06..1654a49 100644 --- a/example/lib/screens/file_explorer/file_explorer_card.dart +++ b/example/lib/screens/file_explorer/file_explorer_card.dart @@ -3,15 +3,13 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; -import 'package:fl_toast/fl_toast.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:shared_storage/shared_storage.dart'; -import '../../theme/spacing.dart'; import '../../utils/apply_if_not_null.dart'; import '../../utils/confirm_decorator.dart'; import '../../utils/disabled_text_style.dart'; +import '../../utils/document_file_utils.dart'; import '../../utils/format_bytes.dart'; import '../../utils/inline_span.dart'; import '../../utils/mime_types.dart'; @@ -241,7 +239,7 @@ class _FileExplorerCardState extends State { } Widget _buildOpenWithButton() => - Button('Open with', onTap: _openFileWithExternalApp); + Button('Open with', onTap: _currentUri.openWithExternalApp); Widget _buildDocumentSimplifiedTile() { return ListTile( @@ -319,60 +317,6 @@ class _FileExplorerCardState extends State { String get _mimeTypeOrEmpty => _file.type ?? ''; - Future _showFileContents() async { - if (_isDirectory) return; - - const k10mb = 1024 * 1024 * 10; - - if (!_mimeTypeOrEmpty.startsWith(kTextMime) && - !_mimeTypeOrEmpty.startsWith(kImageMime)) { - if (_mimeTypeOrEmpty == kApkMime) { - return showTextToast( - text: - 'Requesting to install a package (.apk) is not currently supported, to request this feature open an issue at github.com/alexrintt/shared-storage/issues', - context: context, - ); - } - - return _openFileWithExternalApp(); - } - - // Too long, will take too much time to read - if (_sizeInBytes > k10mb) { - return showTextToast( - text: 'File too long to open', - context: context, - ); - } - - content = await getDocumentContent(_file.uri); - - if (content != null) { - final isImage = _mimeTypeOrEmpty.startsWith(kImageMime); - - await showModalBottomSheet( - context: context, - builder: (context) { - if (isImage) { - return Image.memory(content!); - } - - final contentAsString = String.fromCharCodes(content!); - - final fileIsEmpty = contentAsString.isEmpty; - - return Container( - padding: k8dp.all, - child: Text( - fileIsEmpty ? 'This file is empty' : contentAsString, - style: fileIsEmpty ? disabledTextStyle() : null, - ), - ); - }, - ); - } - } - Future _deleteDocument() async { final deleted = await delete(_currentUri); @@ -436,24 +380,6 @@ class _FileExplorerCardState extends State { } } - Future _openFileWithExternalApp() async { - final uri = _currentUri; - - try { - final launched = await openDocumentFile(uri); - - if (launched ?? false) { - print('Successfully opened $uri'); - } else { - print('Failed to launch $uri'); - } - } on PlatformException { - print( - "There's no activity associated with the file type of this Uri: $uri", - ); - } - } - Future _openDirectory() async { if (_isDirectory) { _openFolderFileListPage(_file.uri); @@ -463,7 +389,7 @@ class _FileExplorerCardState extends State { @override Widget build(BuildContext context) { return SimpleCard( - onTap: _isDirectory ? _openDirectory : _showFileContents, + onTap: _isDirectory ? _openDirectory : () => _file.showContents(context), children: [ if (_expanded) ...[ _buildThumbnail(size: 50), diff --git a/example/lib/screens/granted_uris/granted_uri_card.dart b/example/lib/screens/granted_uris/granted_uri_card.dart index d3e25f9..39c362a 100644 --- a/example/lib/screens/granted_uris/granted_uri_card.dart +++ b/example/lib/screens/granted_uris/granted_uri_card.dart @@ -1,10 +1,14 @@ +import 'package:fl_toast/fl_toast.dart'; import 'package:flutter/material.dart'; import 'package:shared_storage/shared_storage.dart'; import '../../theme/spacing.dart'; +import '../../utils/disabled_text_style.dart'; +import '../../utils/document_file_utils.dart'; import '../../widgets/buttons.dart'; import '../../widgets/key_value_text.dart'; import '../../widgets/simple_card.dart'; +import '../file_explorer/file_explorer_card.dart'; import '../file_explorer/file_explorer_page.dart'; class GrantedUriCard extends StatefulWidget { @@ -55,19 +59,65 @@ class _GrantedUriCardState extends State { ); } + List _getTreeAvailableOptions() { + return [ + ActionButton( + 'Create sample file', + onTap: () => _appendSampleFile( + widget.permissionUri.uri, + ), + ), + ActionButton( + 'Open tree here', + onTap: () => openDocumentTree(initialUri: widget.permissionUri.uri), + ) + ]; + } + + DocumentFile? documentFile; + bool loading = false; + String? error; + + Future _loadDocumentFile() async { + loading = true; + setState(() {}); + + documentFile = await widget.permissionUri.uri.toDocumentFile(); + loading = false; + + if (mounted) setState(() {}); + } + + Future _showDocumentFileContents() async { + try { + final documentFile = await widget.permissionUri.uri.toDocumentFile(); + + if (mounted) documentFile?.showContents(context); + } catch (e) { + error = e.toString(); + } + } + + List _getDocumentAvailableOptions() { + return [ + ActionButton( + 'Open document', + onTap: _showDocumentFileContents, + ), + ActionButton( + 'Load extra document data linked to this permission', + onTap: _loadDocumentFile, + ), + ]; + } + Widget _buildAvailableActions() { return Wrap( children: [ - ActionButton( - 'Create Sample File', - onTap: () => _appendSampleFile( - widget.permissionUri.uri, - ), - ), - ActionButton( - 'Open Tree Here', - onTap: () => openDocumentTree(initialUri: widget.permissionUri.uri), - ), + if (widget.permissionUri.isTreeDocumentFile) + ..._getTreeAvailableOptions() + else + ..._getDocumentAvailableOptions(), Padding(padding: k2dp.all), DangerButton( 'Revoke', @@ -86,6 +136,7 @@ class _GrantedUriCardState extends State { 'isReadPermission': '${widget.permissionUri.isReadPermission}', 'persistedTime': '${widget.permissionUri.persistedTime}', 'uri': Uri.decodeFull('${widget.permissionUri.uri}'), + 'isTreeDocumentFile': '${widget.permissionUri.isTreeDocumentFile}', }, ); } @@ -93,10 +144,36 @@ class _GrantedUriCardState extends State { @override Widget build(BuildContext context) { return SimpleCard( - onTap: _openListFilesPage, + onTap: widget.permissionUri.isTreeDocumentFile + ? _openListFilesPage + : _showDocumentFileContents, children: [ + Padding( + padding: k2dp.all.copyWith(top: k8dp, bottom: k8dp), + child: Icon( + widget.permissionUri.isTreeDocumentFile + ? Icons.folder + : Icons.file_copy_sharp, + color: disabledColor(), + ), + ), _buildGrantedUriMetadata(), _buildAvailableActions(), + if (loading) + const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + else if (error != null) + Text('Error was thrown: $error') + else if (documentFile != null) + FileExplorerCard( + documentFile: documentFile!, + didUpdateDocument: (updatedDocumentFile) { + documentFile = updatedDocumentFile; + }, + ) ], ); } diff --git a/example/lib/screens/granted_uris/granted_uris_page.dart b/example/lib/screens/granted_uris/granted_uris_page.dart index 91eaeba..baa7978 100644 --- a/example/lib/screens/granted_uris/granted_uris_page.dart +++ b/example/lib/screens/granted_uris/granted_uris_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shared_storage/shared_storage.dart'; import '../../theme/spacing.dart'; +import '../../utils/disabled_text_style.dart'; import '../../widgets/light_text.dart'; import 'granted_uri_card.dart'; @@ -13,7 +14,13 @@ class GrantedUrisPage extends StatefulWidget { } class _GrantedUrisPageState extends State { - List? persistedPermissionUris; + List? __persistedPermissionUris; + List? get _persistedPermissionUris { + if (__persistedPermissionUris == null) return null; + + return List.from(__persistedPermissionUris!) + ..sort((a, z) => z.persistedTime - a.persistedTime); + } @override void initState() { @@ -23,7 +30,7 @@ class _GrantedUrisPageState extends State { } Future _loadPersistedUriPermissions() async { - persistedPermissionUris = await persistedUriPermissions(); + __persistedPermissionUris = await persistedUriPermissions(); if (mounted) setState(() => {}); } @@ -41,6 +48,20 @@ class _GrantedUrisPageState extends State { await _loadPersistedUriPermissions(); } + Future _openDocument() async { + const kDownloadsFolder = + 'content://com.android.externalstorage.documents/tree/primary%3ADownloads/document/primary%3ADownloads'; + + final List? selectedDocumentUris = await openDocument( + initialUri: Uri.parse(kDownloadsFolder), + multiple: true, + ); + + if (selectedDocumentUris == null) return; + + await _loadPersistedUriPermissions(); + } + Widget _buildNoFolderAllowedYetWarning() { return Padding( padding: k8dp.all, @@ -66,22 +87,39 @@ class _GrantedUrisPageState extends State { delegate: SliverChildListDelegate( [ Center( - child: TextButton( - onPressed: _openDocumentTree, - child: const Text('New allowed folder'), + child: Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + TextButton( + onPressed: _openDocumentTree, + child: const Text('New allowed folder'), + ), + const Padding(padding: EdgeInsets.all(k2dp)), + TextButton( + onPressed: _openDocument, + child: const Text('New allowed files'), + ), + ], ), ), - if (persistedPermissionUris != null) - if (persistedPermissionUris!.isEmpty) + if (_persistedPermissionUris != null) + if (_persistedPermissionUris!.isEmpty) _buildNoFolderAllowedYetWarning() else - for (final permissionUri in persistedPermissionUris!) + for (final permissionUri in _persistedPermissionUris!) GrantedUriCard( permissionUri: permissionUri, onChange: _loadPersistedUriPermissions, ) else - const Text('Loading...'), + Center( + child: Text( + 'Loading...', + style: disabledTextStyle(), + ), + ), ], ), ), diff --git a/example/lib/utils/document_file_utils.dart b/example/lib/utils/document_file_utils.dart new file mode 100644 index 0000000..0167d85 --- /dev/null +++ b/example/lib/utils/document_file_utils.dart @@ -0,0 +1,90 @@ +import 'package:fl_toast/fl_toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:shared_storage/saf.dart'; + +import '../theme/spacing.dart'; +import 'disabled_text_style.dart'; +import 'mime_types.dart'; + +extension ShowText on BuildContext { + Future showToast(String text, {Duration? duration}) { + return showTextToast( + text: text, + context: this, + duration: const Duration(seconds: 5), + ); + } +} + +extension OpenUriWithExternalApp on Uri { + Future openWithExternalApp() async { + final uri = this; + + try { + final launched = await openDocumentFile(uri); + + if (launched ?? false) { + print('Successfully opened $uri'); + } else { + print('Failed to launch $uri'); + } + } on PlatformException { + print( + "There's no activity associated with the file type of this Uri: $uri", + ); + } + } +} + +extension ShowDocumentFileContents on DocumentFile { + Future showContents(BuildContext context) async { + final mimeTypeOrEmpty = type ?? ''; + final sizeInBytes = size ?? 0; + + const k10mb = 1024 * 1024 * 10; + + if (!mimeTypeOrEmpty.startsWith(kTextMime) && + !mimeTypeOrEmpty.startsWith(kImageMime)) { + if (mimeTypeOrEmpty == kApkMime) { + return context.showToast( + 'Requesting to install a package (.apk) is not currently supported, to request this feature open an issue at github.com/alexrintt/shared-storage/issues', + ); + } + + return uri.openWithExternalApp(); + } + + // Too long, will take too much time to read + if (sizeInBytes > k10mb) { + return context.showToast('File too long to open'); + } + + final content = await getDocumentContent(uri); + + if (content != null) { + final isImage = mimeTypeOrEmpty.startsWith(kImageMime); + + await showModalBottomSheet( + context: context, + builder: (context) { + if (isImage) { + return Image.memory(content); + } + + final contentAsString = String.fromCharCodes(content); + + final fileIsEmpty = contentAsString.isEmpty; + + return Container( + padding: k8dp.all, + child: Text( + fileIsEmpty ? 'This file is empty' : contentAsString, + style: fileIsEmpty ? disabledTextStyle() : null, + ), + ); + }, + ); + } + } +} From c6813763c973c661a386cd40cef68c805f58fd77 Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Fri, 11 Nov 2022 15:36:09 -0300 Subject: [PATCH 06/11] (#110) Remove unused import from example project --- example/lib/screens/granted_uris/granted_uri_card.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/example/lib/screens/granted_uris/granted_uri_card.dart b/example/lib/screens/granted_uris/granted_uri_card.dart index 39c362a..dd57ee5 100644 --- a/example/lib/screens/granted_uris/granted_uri_card.dart +++ b/example/lib/screens/granted_uris/granted_uri_card.dart @@ -1,4 +1,3 @@ -import 'package:fl_toast/fl_toast.dart'; import 'package:flutter/material.dart'; import 'package:shared_storage/shared_storage.dart'; From 1aa10eb82c548ac9ef8ff836204801ab0b03cf2a Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Tue, 15 Nov 2022 18:51:08 -0300 Subject: [PATCH 07/11] Create FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6ce78cc --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [alexrintt] From eed796efde47f6095bd69c0e8b2953e53bc035d9 Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Tue, 15 Nov 2022 18:54:22 -0300 Subject: [PATCH 08/11] (#111) Customize `README.md` for `v0.7.0` --- README.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 489d2d7..4fe52ae 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

-

Install It

+

Install It

## Documentation @@ -34,15 +34,20 @@ Latest changes are available on `master` branch and the actual latest published All other branches are derivated from issues, new features or bug fixes. +## Supporters + +- [aplicatii-romanesti](https://www.bibliotecaortodoxa.ro/) who bought me a whole month of caffeine! + ## Contributors -- [clragon](https://github.com/clragon) submitted a severe bug report and opened discussions around package architecture, thanks! -- [jfaltis](https://github.com/jfaltis) fixed a memory leak and implemented an API to override existing files, thanks for your contribution! -- [EternityForest](https://github.com/EternityForest) did fix a severe crash when the ID column was not provided and implemented a new feature to list all subfolders, thanks man! -- Thanks [dhaval-k-simformsolutions](https://github.com/dhaval-k-simformsolutions) for taking time to submit bug reports related to duplicated file entries! -- [dangilbert](https://github.com/dangilbert) pointed and fixed bug when the user doesn't select a folder, thanks man! -- A huge thanks to [aplicatii-romanesti](https://www.bibliotecaortodoxa.ro/) for taking time to submit device specific issues and for supporting the project! -- I would thanks [ankitparmar007](https://github.com/ankitparmar007) for discussing and requesting create file related APIs! +- [honjow](https://github.com/honjow) contributed by [implementing `openDocument` Android API #110](https://github.com/alexrintt/shared-storage/pull/110) to pick single or multiple file URIs. Really helpful, thanks! +- [clragon](https://github.com/clragon) submitted a severe [bug report #107](https://github.com/alexrintt/shared-storage/issues/107) and opened [discussions around package architecture #108](https://github.com/alexrintt/shared-storage/discussions/108), thanks! +- [jfaltis](https://github.com/jfaltis) fixed [a memory leak #86](https://github.com/alexrintt/shared-storage/pull/86) and implemented an API to [override existing files #85](https://github.com/alexrintt/shared-storage/pull/85), thanks for your contribution! +- [EternityForest](https://github.com/EternityForest) did [report a severe crash #50](https://github.com/alexrintt/shared-storage/issues/50) when the column ID was not provided and [implemented a new feature to list all subfolders #59](https://github.com/alexrintt/shared-storage/pull/59), thanks man! +- Thanks [dhaval-k-simformsolutions](https://github.com/dhaval-k-simformsolutions) for taking time to submit [bug reports](https://github.com/alexrintt/shared-storage/issues?q=is%3Aissue+author%3Adhaval-k-simformsolutions) related to duplicated file entries! +- [dangilbert](https://github.com/dangilbert) pointed and [fixed a bug #14](https://github.com/alexrintt/shared-storage/pull/14) when the user doesn't select a folder, thanks man! +- A huge thanks to [aplicatii-romanesti](https://www.bibliotecaortodoxa.ro/) for taking time to submit [device specific issues](https://github.com/alexrintt/shared-storage/issues?q=author%3Aaplicatii-romanesti)! +- I would thanks [ankitparmar007](https://github.com/ankitparmar007) for [discussing and requesting create file related APIs #20](https://github.com/alexrintt/shared-storage/issues/10)!
From 522fc072bb06439406205a1279fcbee3f792d5f3 Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Tue, 15 Nov 2022 18:55:16 -0300 Subject: [PATCH 09/11] (#111) Minor bug fixes on `/example` project --- .../file_explorer/file_explorer_card.dart | 2 +- .../granted_uris/granted_uri_card.dart | 30 +++++++++++++------ .../granted_uris/granted_uris_page.dart | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/example/lib/screens/file_explorer/file_explorer_card.dart b/example/lib/screens/file_explorer/file_explorer_card.dart index 1654a49..a290fb3 100644 --- a/example/lib/screens/file_explorer/file_explorer_card.dart +++ b/example/lib/screens/file_explorer/file_explorer_card.dart @@ -113,7 +113,7 @@ class _FileExplorerCardState extends State { } Widget _buildMimeTypeIconThumbnail(String mimeType, {double? size}) { - if (mimeType == kDirectoryMime) { + if (_isDirectory) { return Icon(Icons.folder, size: size, color: Colors.blueGrey); } diff --git a/example/lib/screens/granted_uris/granted_uri_card.dart b/example/lib/screens/granted_uris/granted_uri_card.dart index dd57ee5..b374e0f 100644 --- a/example/lib/screens/granted_uris/granted_uri_card.dart +++ b/example/lib/screens/granted_uris/granted_uri_card.dart @@ -67,12 +67,21 @@ class _GrantedUriCardState extends State { ), ), ActionButton( - 'Open tree here', + 'Open file picker here', onTap: () => openDocumentTree(initialUri: widget.permissionUri.uri), ) ]; } + @override + void didUpdateWidget(covariant GrantedUriCard oldWidget) { + super.didUpdateWidget(oldWidget); + + documentFile = null; + loading = false; + error = null; + } + DocumentFile? documentFile; bool loading = false; String? error; @@ -97,11 +106,17 @@ class _GrantedUriCardState extends State { } } + VoidCallback get _onTapHandler => widget.permissionUri.isTreeDocumentFile + ? _openListFilesPage + : _showDocumentFileContents; + List _getDocumentAvailableOptions() { return [ ActionButton( - 'Open document', - onTap: _showDocumentFileContents, + widget.permissionUri.isTreeDocumentFile + ? 'Open folder' + : 'Open document', + onTap: _onTapHandler, ), ActionButton( 'Load extra document data linked to this permission', @@ -114,9 +129,8 @@ class _GrantedUriCardState extends State { return Wrap( children: [ if (widget.permissionUri.isTreeDocumentFile) - ..._getTreeAvailableOptions() - else - ..._getDocumentAvailableOptions(), + ..._getTreeAvailableOptions(), + ..._getDocumentAvailableOptions(), Padding(padding: k2dp.all), DangerButton( 'Revoke', @@ -143,9 +157,7 @@ class _GrantedUriCardState extends State { @override Widget build(BuildContext context) { return SimpleCard( - onTap: widget.permissionUri.isTreeDocumentFile - ? _openListFilesPage - : _showDocumentFileContents, + onTap: _onTapHandler, children: [ Padding( padding: k2dp.all.copyWith(top: k8dp, bottom: k8dp), diff --git a/example/lib/screens/granted_uris/granted_uris_page.dart b/example/lib/screens/granted_uris/granted_uris_page.dart index baa7978..133759c 100644 --- a/example/lib/screens/granted_uris/granted_uris_page.dart +++ b/example/lib/screens/granted_uris/granted_uris_page.dart @@ -66,7 +66,7 @@ class _GrantedUrisPageState extends State { return Padding( padding: k8dp.all, child: const Center( - child: LightText('No folders allowed yet'), + child: LightText('No folders or files allowed yet'), ), ); } From fb653abfd6f4126733800ec9d01486caaa9496d4 Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Tue, 15 Nov 2022 18:57:51 -0300 Subject: [PATCH 10/11] (#111) Deprecate non SAF members --- lib/src/environment/environment.dart | 15 +++++++++++ .../environment/environment_directory.dart | 27 +++++++++++++++++++ lib/src/media_store/media_store.dart | 3 +++ .../media_store/media_store_collection.dart | 15 +++++++++++ 4 files changed, 60 insertions(+) diff --git a/lib/src/environment/environment.dart b/lib/src/environment/environment.dart index c22698a..e36df37 100644 --- a/lib/src/environment/environment.dart +++ b/lib/src/environment/environment.dart @@ -7,6 +7,9 @@ import 'environment_directory.dart'; /// Equivalent to `Environment.getRootDirectory` /// /// [Refer to details](https://developer.android.com/reference/android/os/Environment#getRootDirectory%28%29) +@Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', +) Future getRootDirectory() async { const kGetRootDirectory = 'getRootDirectory'; @@ -49,6 +52,9 @@ Future getExternalStoragePublicDirectory( /// Equivalent to `Environment.getExternalStorageDirectory` /// /// [Refer to details](https://developer.android.com/reference/android/os/Environment#getExternalStorageDirectory%28%29) +@Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', +) Future getExternalStorageDirectory() async { const kGetExternalStorageDirectory = 'getExternalStorageDirectory'; @@ -58,6 +64,9 @@ Future getExternalStorageDirectory() async { /// Equivalent to `Environment.getDataDirectory` /// /// [Refer to details](https://developer.android.com/reference/android/os/Environment#getDataDirectory%28%29) +@Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', +) Future getDataDirectory() async { const kGetDataDirectory = 'getDataDirectory'; @@ -67,6 +76,9 @@ Future getDataDirectory() async { /// Equivalent to `Environment.getDataDirectory` /// /// [Refer to details](https://developer.android.com/reference/android/os/Environment#getDownloadCacheDirectory%28%29) +@Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', +) Future getDownloadCacheDirectory() async { const kGetDownloadCacheDirectory = 'getDownloadCacheDirectory'; @@ -76,6 +88,9 @@ Future getDownloadCacheDirectory() async { /// Equivalent to `Environment.getStorageDirectory` /// /// [Refer to details](https://developer.android.com/reference/android/os/Environment#getStorageDirectory%28%29) +@Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', +) Future getStorageDirectory() { const kGetStorageDirectory = 'getStorageDirectory'; diff --git a/lib/src/environment/environment_directory.dart b/lib/src/environment/environment_directory.dart index 2b900f4..2e1657c 100644 --- a/lib/src/environment/environment_directory.dart +++ b/lib/src/environment/environment_directory.dart @@ -20,49 +20,76 @@ class EnvironmentDirectory { /// Available for Android [4.1 to 9.0] /// /// Equivalent to [Environment.DIRECTORY_ALARMS] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const alarms = EnvironmentDirectory._('$_kPrefix.Alarms'); /// Available for Android [4.1 to 9] /// /// Equivalent to: /// - [Environment.DIRECTORY_DCIM] on Android [4.1 to 9] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const dcim = EnvironmentDirectory._('$_kPrefix.DCIM'); /// Available for Android [4.1 to 9] /// /// Equivalent to: /// - [Environment.DIRECTORY_DOWNLOADS] on Android [4.1 to 9] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const downloads = EnvironmentDirectory._('$_kPrefix.Downloads'); /// Available for Android [4.1 to 9] /// /// - [Environment.DIRECTORY_MOVIES] on Android [4.1 to 9] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const movies = EnvironmentDirectory._('$_kPrefix.Movies'); /// Available for Android [4.1 to 9] /// /// - [Environment.DIRECTORY_MUSIC] on Android [4.1 to 9] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const music = EnvironmentDirectory._('$_kPrefix.Music'); /// Available for Android [4.1 to 9] /// /// - [Environment.DIRECTORY_NOTIFICATIONS] on Android [4.1 to 9] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const notifications = EnvironmentDirectory._('$_kPrefix.Notifications'); /// Available for Android [4.1 to 9] /// /// - [Environment.DIRECTORY_PICTURES] on Android [4.1 to 9] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const pictures = EnvironmentDirectory._('$_kPrefix.Pictures'); /// Available for Android [4.1 to 9] /// /// - [Environment.DIRECTORY_PODCASTS] on Android [4.1 to 9] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const podcasts = EnvironmentDirectory._('$_kPrefix.Podcasts'); /// Available for Android [4.1 to 9] /// /// - [Environment.DIRECTORY_RINGTONES] on Android [4.1 to 9] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const ringtones = EnvironmentDirectory._('$_kPrefix.Ringtones'); @override diff --git a/lib/src/media_store/media_store.dart b/lib/src/media_store/media_store.dart index 2a7b425..08a12df 100644 --- a/lib/src/media_store/media_store.dart +++ b/lib/src/media_store/media_store.dart @@ -6,6 +6,9 @@ import 'media_store_collection.dart'; /// Get the directory of a given [MediaStoreCollection] /// /// [Refer to details](https://developer.android.com/reference/android/provider/MediaStore#summary) +@Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', +) Future getMediaStoreContentDirectory( MediaStoreCollection collection, ) async { diff --git a/lib/src/media_store/media_store_collection.dart b/lib/src/media_store/media_store_collection.dart index 6f571e3..ed694cc 100644 --- a/lib/src/media_store/media_store_collection.dart +++ b/lib/src/media_store/media_store_collection.dart @@ -12,24 +12,36 @@ class MediaStoreCollection { /// /// Equivalent to: /// - [MediaStore.Audio] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const audio = MediaStoreCollection._('$_kPrefix.Audio'); /// Available for Android [10 to 12] /// /// Equivalent to: /// - [MediaStore.Downloads] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const downloads = MediaStoreCollection._('$_kPrefix.Downloads'); /// Available for Android [10 to 12] /// /// Equivalent to: /// - [MediaStore.Images] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const images = MediaStoreCollection._('$_kPrefix.Images'); /// Available for Android [10 to 12] /// /// Equivalent to: /// - [MediaStore.Video] + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) static const video = MediaStoreCollection._('$_kPrefix.Video'); @override @@ -41,5 +53,8 @@ class MediaStoreCollection { int get hashCode => id.hashCode; @override + @Deprecated( + 'Android specific APIs will be removed soon in order to be replaced with a new set of original cross-platform APIs.', + ) String toString() => id; } From b806fbd8ce52a64a5a3d071c0a9e3bc0783f0cbd Mon Sep 17 00:00:00 2001 From: Alex Rintt Date: Wed, 16 Nov 2022 11:53:30 -0300 Subject: [PATCH 11/11] (#111) Add `CHANGELOG.md` and docs for new and deprecated APIs --- CHANGELOG.md | 22 ++++++++++++-- docs/Migrate notes/Migrate to v0.7.0.md | 12 ++++++++ docs/Usage/Environment.md | 2 ++ docs/Usage/Media Store.md | 2 ++ docs/Usage/Storage Access Framework.md | 40 +++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 docs/Migrate notes/Migrate to v0.7.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f94715..2b5efe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,28 @@ +## 0.7.0 + +- New APIs and options. +- There's no major breaking changes when updating to `v0.7.0` but there are deprecation notices over Media Store and Environment API. + +### New + +- `openDocument` API with single and multiple files support @honjow. +- `openDocumentTree` it now also supports `persistablePermission` option which flags an one-time operation to avoid unused permission issues. + +### Deprecation notices + +- All non SAF APIs are deprecated (Media Store and Environment APIs), if you are using them, let us know by [opening an issue](https://github.com/alexrintt/shared-storage/issues/new) with your use-case so we can implement a new compatible API using a cross-platform approach. + +### Example project + +- Added a new button that implements `openDocument` API. + ## 0.6.0 This release contains a severe API fixes and some minor doc changes: ### Breaking changes -- Unused arguments in `DocumentFile.getContent` and `DocumentFile.getContentAsString`. [#107](https://github.com/alexrintt/shared-storage/issues/107). +- Unused arguments in `DocumentFile.getContent` and `DocumentFile.getContentAsString`. [#107](https://github.com/alexrintt/shared-storage/issues/107) @clragon. - Package import it's now done through a single import. ## 0.5.0 @@ -170,7 +188,7 @@ See the label [reference here](/docs/Usage/API%20Labeling.md). - Mirror `getStorageDirectory` from [`Environment.getStorageDirectory`](https://developer.android.com/reference/android/os/Environment#getStorageDirectory%28%29). -### Deprecation Notices +### Deprecation notices - `getExternalStoragePublicDirectory` was marked as deprecated and should be replaced with an equivalent API depending on your use-case, see [how to migrate `getExternalStoragePublicDirectory`](https://stackoverflow.com/questions/56468539/getexternalstoragepublicdirectory-deprecated-in-android-q). This deprecation is originated from official Android documentation and not by the plugin itself. diff --git a/docs/Migrate notes/Migrate to v0.7.0.md b/docs/Migrate notes/Migrate to v0.7.0.md new file mode 100644 index 0000000..c25fe64 --- /dev/null +++ b/docs/Migrate notes/Migrate to v0.7.0.md @@ -0,0 +1,12 @@ +There's no major breaking changes when updating to `v0.7.0` but there are deprecation notices if you are using Media Store and Environment API. + +Update your `pubspec.yaml`: + +```yaml +dependencies: + shared_storage: ^0.7.0 +``` + +## Deprecation notices + +All non SAF APIs are deprecated, if you are using them, let us know by [opening an issue](https://github.com/alexrintt/shared-storage/issues/new) with your use-case so we can implement a new compatible API using a cross-platform approach. diff --git a/docs/Usage/Environment.md b/docs/Usage/Environment.md index cc519f2..3ce904a 100644 --- a/docs/Usage/Environment.md +++ b/docs/Usage/Environment.md @@ -1,3 +1,5 @@ +> **WARNING** This API is deprecated and will be removed soon. If you need it, please open an issue with your use-case to include in the next release as part of the new original cross-platform API. + ## Import package ```dart diff --git a/docs/Usage/Media Store.md b/docs/Usage/Media Store.md index 50f76e8..b5476c0 100644 --- a/docs/Usage/Media Store.md +++ b/docs/Usage/Media Store.md @@ -1,3 +1,5 @@ +> **WARNING** This API is deprecated and will be removed soon. If you need it, please open an issue with your use-case to include in the next release as part of the new original cross-platform API. + ## Import package ```dart diff --git a/docs/Usage/Storage Access Framework.md b/docs/Usage/Storage Access Framework.md index 7814827..a89cf0e 100644 --- a/docs/Usage/Storage Access Framework.md +++ b/docs/Usage/Storage Access Framework.md @@ -64,6 +64,46 @@ if (grantedUri != null) { } ``` +### openDocument + +Same as `openDocumentTree` but for file URIs, you can request user to select a file and filter by: + +- Single or multiple files. +- Mime type. + +You can also specify if you want a one-time operation (`persistablePermission` = false) and if you don't need write access (`grantWritePermission` = false). + +```dart +const kDownloadsFolder = + 'content://com.android.externalstorage.documents/tree/primary%3ADownloads/document/primary%3ADownloads'; + +final List? selectedDocumentUris = await openDocument( + // if you have a previously saved URI, + // you can use the specify the tree you user will see at startup of the file picker. + initialUri: Uri.parse(kDownloadsFolder), + + // whether or not allow the user select multiple files. + multiple: true, + + // whether or not the selected URIs should be persisted across app and device reboots. + persistablePermission: true, + + // whether or not grant write permission required to edit file metadata (name) and it's contents. + grantWritePermission: true, + + // whether or not filter by mime type. + mimeType: 'image/*' // default '*/*' +); + +if (selectedDocumentUris == null) { + return print('User cancelled the operation.'); +} + +// If [selectedDocumentUris] are [persistablePermission]s then it will be returned by this function +// along with any another URIs you've got permission over. +final List persistedUris = await persistedUriPermissions(); +``` + ### listFiles This method list files lazily **over a granted uri:**