Closed rohinton-collins closed 2 months ago
This issue is stale because it has been open for 7 days with no activity.
Related issue on iOS: https://github.com/miguelpruivo/flutter_file_picker/issues/1568
Hi @rohinton-collins , I am developing an app for sharing files. Sadly getDirectoryPath() does not work the way we expected, only files owns by the app can be accessed although the full permission for the folder is asked. Your code would be helpful to us
@Fallenhh iOS code was very simple. You just need to call start/stopAccessingSecurityScopedResource() on the picked folder. You need to implement a platform channel or fork this repo to do it. Platform channel code can be found here: https://github.com/miguelpruivo/flutter_file_picker/issues/1568#issuecomment-2269483847
Android is a bit trickier as I had to implement a Kotlin/Dart platform channel for iterating folder contents and streaming files using the Scoped Storage API. I built a DocumentPickerActivity:
DocumentPickerActivity.kt
package com.yourCompany
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
class DirectoryPickerActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
resultLauncher.launch(intent)
}
private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
val returnIntent = Intent()
if (result.resultCode == RESULT_OK) {
val uri: Uri? = result.data?.data
if (uri != null) {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
returnIntent.putExtra("getDirectoryUri", uri.toString())
}
}
setResult(RESULT_OK, returnIntent)
finish()
}
}
Make sure you register the new activity in the AndroidManifest:
<activity android:name=".DirectoryPickerActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>
My MainActivity looks like this:
MainActivity.kt
package com.yourCompany
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val channel = "com.yourCompany.app/documents"
private lateinit var result: MethodChannel.Result
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel)
.setMethodCallHandler { call, result ->
when (call.method) {
"getDirectoryPath" -> {
this.result = result
val intent = Intent(this, DirectoryPickerActivity::class.java)
startActivityForResult(intent, PICK_DIRECTORY_REQUEST_CODE)
}
"listDirectories" -> {
val uriString = call.argument<String>("uri")!!
val uri = Uri.parse(uriString)
val directories = listDirectories(uri)
result.success(directories)
}
"listFiles" -> {
val uriString = call.argument<String>("uri")!!
val uri = Uri.parse(uriString)
val files = listFiles(uri)
result.success(files)
}
"getFileChunk" -> {
val uriString = call.argument<String>("uri")!!
val offset = call.argument<Int>("offset")!!
val chunkSize = call.argument<Int>("chunkSize")!!
val uri = Uri.parse(uriString)
val chunk = readFileChunk(uri, offset, chunkSize)
if (chunk != null) {
result.success(chunk)
} else {
result.error("READ_ERROR", "Failed to read file chunk", null)
}
}
else -> result.notImplemented()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == PICK_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
val pickedDirectoryUri = data?.getStringExtra("getDirectoryUri")
if (pickedDirectoryUri != null) {
result.success(pickedDirectoryUri)
} else {
result.error("NO_DIRECTORY_URI", "No directory URI found", null)
}
}
}
private fun listDirectories(uri: Uri): List<String> {
val directory = DocumentFile.fromTreeUri(this, uri)
val directories = mutableListOf<String>()
if (directory != null && directory.isDirectory) {
for (item in directory.listFiles()) {
if (item.isDirectory) {
directories.add(item.uri.toString())
}
}
}
return directories
}
private fun listFiles(uri: Uri): List<Map<String, Any>> {
val directory = DocumentFile.fromTreeUri(this, uri)
val files = mutableListOf<Map<String, Any>>()
if (directory != null && directory.isDirectory) {
for (item in directory.listFiles()) {
if (item.isFile) {
val fileInfo: Map<String, Any> = mapOf(
"uri" to item.uri.toString(),
"name" to (item.name ?: "Unknown"),
"size" to item.length(),
"modified" to item.lastModified()
)
files.add(fileInfo)
}
}
}
return files
}
private fun readFileChunk(uri: Uri, offset: Int, chunkSize: Int): ByteArray? {
return try {
contentResolver.openInputStream(uri)?.use { inputStream ->
inputStream.skip(offset.toLong())
val buffer = ByteArray(chunkSize)
val bytesRead = inputStream.read(buffer, 0, chunkSize)
if (bytesRead != -1) {
buffer.copyOf(bytesRead)
} else {
null
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
companion object {
const val PICK_DIRECTORY_REQUEST_CODE = 1
}
}
My Dart code is spread across several files:
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:yourApp/model/platform/platform_file.dart';
/// Android-specific. uses a platform channel which calls Scoped Storage APIs
/// so that we can list directory contents and read files from a selected folder
class AndroidPlatformFile implements PlatformFileInterface {
...
@override Stream<List<int>> openRead() async* {
const int chunkSize = 1024 * 1024; // 1MB
int offset = 0;
bool moreData = true;
while (moreData) {
try {
final Map<String, dynamic> arguments = {
'uri': uri,
'offset': offset,
'chunkSize': chunkSize,
};
final List<int>? chunk = await methodChannel.invokeMethod('getFileChunk', arguments);
if (chunk == null || chunk.isEmpty) {
moreData = false;
} else {
yield Uint8List.fromList(chunk);
offset += chunk.length;
}
} on PlatformException catch (e) {
if (kDebugMode) print("Failed to get file chunk: ${e.message}");
moreData = false;
}
}
}
}
import 'dart:io';
import 'package:file_picker/file_picker.dart' as fp;
import 'package:flutter/services.dart';
class UploadFiles implements UploadFilesInterface {
...
static const methodChannel = MethodChannel('com.yourCompany.app/documents');
@override
Future<List<PlatformFileInterface>?> selectFiles() async {
fp.FilePickerResult? result = await fp.FilePicker.platform.pickFiles(allowMultiple: true, compressionQuality: 0);
if (result == null) return null;
return result.paths
.map((path) => PlatformFile(File(path!)))
.toList();
}
@override
Future<PlatformFolderInterface?> selectFolder() async {
if (platform.isMobile()) {
// use Scoped Storage API for Android / startAccessingSecurityScopedResource() for iOS
final uri = await methodChannel.invokeMethod('getDirectoryPath');
if (uri == null) throw UserCancelled();
if (uri == '/') throw ZCloudError.protectedFolderLocation; // may not apply here
return Platform.isAndroid ? PlatformFolder(uri: uri) : PlatformFolder(path: uri);
} else {
final path = await fp.FilePicker.platform.getDirectoryPath();
if (path == null) throw UserCancelled();
if (path == '/') throw ZCloudError.protectedFolderLocation;
return PlatformFolder(path: path);
}
}
}
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
class PlatformFolder implements PlatformFolderInterface {
...
static const methodChannel = MethodChannel('com.yourCompany.app/documents');
@override dispose() {
if (Platform.isIOS) {
methodChannel.invokeMethod('stopAccessingSecurityScopedResource');
}
}
@override
Future<List<PlatformFileInterface>> files() async {
if (path != null) {
final directory = Directory(path!);
return (await directory.list().toList())
.whereType<File>()
.map((file) => PlatformFile(file))
.toList();
} else {
// Android
return (await methodChannel.invokeMethod('listFiles', { 'uri': uri! }))
.map((file) => AndroidPlatformFile(uri: file['uri'], name: file['name'], size: file['size'], modifiedDate: DateTime.fromMillisecondsSinceEpoch(file['modified'])))
.cast<PlatformFileInterface>()
.toList();
}
}
@override
Future<List<PlatformFolderInterface>> folders() async {
if (path != null) {
final directory = Directory(path!);
return (await directory.list().toList())
.whereType<Directory>()
.map((directory) => PlatformFolder(path: directory.path))
.toList();
} else {
// Android
return (await methodChannel.invokeMethod('listDirectories', { 'uri': uri! }))
.map((folder) => PlatformFolder(uri: folder))
.cast<PlatformFolderInterface>()
.toList();
}
}
@override String get name {
if (path != null) return p.basename(path!);
return Uri.decodeFull(uri!).split(RegExp(r'[/:]')).last; // Android
}
}
Hope that helps someone. Happy to answer questions.
This issue is stale because it has been open for 7 days with no activity.
This issue was closed because it has been inactive for 14 days since being marked as stale.
Hi, @rohintonc, currently facing the same issue, thanks a lot for the code! Do you by any chance have a fork for this packages with this functionality so its a bit easier and cleaner to use? Especially because I want to use this in quite a few projects. I would try doing it myself but I'm not really that familiar with Kotlin. Thanks either way!
@azeem-swr Unfortunately not. However, I am also not a Kotlin developer, but didn't have much problem integrating this solution. Good luck.
maybe you need to use the permission_handler package to request permissions,just like this.
import 'package:permission_handler/permission_handler.dart';
if (await Permission.manageExternalStorage.request().isDenied) {
return;
}
// file operation
and put <uses-permission android:name=“android.permission.MANAGE_EXTERNAL_STORAGE” />
into AndroidManifest.xml
The Problem Calling getDirectoryPath() will invoke a system Folder Picker, and will even ask the user for permission for access to the selected folder. But the returned path cannot be used with file:io to list directory contents or read files in Android 12 and up where Scoped Storage is now mandatory. Directory listing and reading files is possible on Android versions prior to Android 12 if the requestLegacyExternalStorage attribute is used in the build.gradle file.
The Solution I have a fix for this. I have implemented a method channel which uses the Scoped Storage API to:
The API does not use paths but instead uses content URIs which can be converted to Strings when passed back to the Flutter layer (for subsequent calls across the method channel).
I have this working on Android 9-14 and have removed the requestLegacyExternalStorage attribute given that I am now using the Scoped Storage API for this functionality.
If anyone needs the code please send a message here. If I get time and there is a call for it I will submit a PR to this repository with the code which supports the above functionality.