sdk
sdk copied to clipboard
Request for Support of `content://` URI in Flutter's File Class for Android
Use case
Request support for android content:// uri in the File class there is similar closed #25659. The conclusion of the #25659 is to use a plugin https://pub.dev/packages/flutter_absolute_path which i dont think that will be Good bcus this plugin copy the files temporary to another location then returns its absolute dir imagine working on large project where u work with many files, user will suffer for storage or sometimes not even enough storage to copy single file
I convert the content:// uri to get string absolute path but will end up in permission denied even thou permission been given successfully meaning converting doesn't work we can only access through content:// uri bcus we got that from https://developer.android.com/training/data-storage/shared/documents-files
this issue is all derived from this simple flutter project https://github.com/devfemibadmus/whatsapp-status-saver
which we use https://developer.android.com/training/data-storage/shared/documents-files to get permission to folder, the code in android works fine bcus we can access content:// uri in adnroid but wont in flutter bcus we can't access content:// uri in File and with this, this simple flutter project is limited.
Using manage_external_storage permission works fine but security issue app reject from playstore by the way not really recommended bcus of security please flutter request for feature Android support for content:// uri in the File class
Proposal
Another exception was thrown: PathNotFoundException: Cannot retrieve length of file, path =
'content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia%2Fcom.whatsapp%2FWhatsApp%2FMedia%2F.Statuses/document/primary%3AAndroid%2Fmedia%2Fcom.w
hatsapp%2FWhatsApp%2FMedia%2F.Statuses%2F21ffcc43b1e141efaef73cd5a099ef0f.jpg' (OS Error: No such file or directory, errno = 2)
Sample code
https://github.com/devfemibadmus/folderpermission
kotlin MainActivity.kt
package com.blackstackhub.folderpicker
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.util.Log
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
import android.content.Intent
import androidx.documentfile.provider.DocumentFile
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.blackstackhub.folderpicker"
private val PERMISSIONS = arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE)
private val TAG = "MainActivity"
private val PICK_DIRECTORY_REQUEST_CODE = 123
private var STATUS_DIRECTORY: DocumentFile? = null
private val BASE_DIRECTORY: Uri = Uri.fromFile(File("/storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/.Statuses/"))
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"isPermissionGranted" -> {
result.success(isPermissionGranted())
}
"requestSpecificFolderAccess" -> {
result.success(requestSpecificFolderAccess())
}
"fetchFilesFromDirectory" -> {
result.success(fetchFilesFromDirectory())
}
else -> {
result.notImplemented()
}
}
}
}
private fun isPermissionGranted(): Boolean {
Log.d(TAG, "isPermissionGranted: $STATUS_DIRECTORY")
return STATUS_DIRECTORY != null && STATUS_DIRECTORY!!.canWrite() && STATUS_DIRECTORY!!.canRead()
}
private fun requestSpecificFolderAccess(): Boolean {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, BASE_DIRECTORY)
startActivityForResult(intent, PICK_DIRECTORY_REQUEST_CODE)
return true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (requestCode == PICK_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
val treeUri: Uri? = resultData?.data
treeUri?.let {
contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
STATUS_DIRECTORY = DocumentFile.fromTreeUri(this, it)
}
}
}
private fun fetchFilesFromDirectory(): List<String> {
val statusFileNames = mutableListOf<String>()
Log.d(TAG, "STATUS_DIRECTORY: $STATUS_DIRECTORY")
STATUS_DIRECTORY?.let { rootDirectory ->
rootDirectory.listFiles()?.forEach { file ->
if (file.isFile && file.canRead()) {
statusFileNames.add(file.uri.toString())
}
}
}
return statusFileNames
}
}
Flutter main.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Status Downloader',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isPermissionGranted = false;
List<String> _files = [];
@override
void initState() {
super.initState();
_checkPermission();
}
Future<void> _checkPermission() async {
bool isGranted = await FolderPicker.isPermissionGranted();
setState(() {
_isPermissionGranted = isGranted;
});
if (_isPermissionGranted) {
_fetchFiles();
}
}
Future<void> _requestPermission() async {
await FolderPicker.requestPermission();
_checkPermission();
}
Future<void> _fetchFiles() async {
List<String> files = await FolderPicker.fetchFilesFromDirectory();
setState(() {
_files = files;
});
}
/*
String convertContentUriToFilePath(String contentUri) {
String prefix = "primary:";
String newPathPrefix = "/storage/emulated/0/";
String newPath = contentUri.replaceAll("%2F", "/");
newPath = newPath.replaceAll("%3A", ":");
newPath = newPath.replaceAll("%2E", ".");
//newPath = newPath.replaceAll(prefix, "");
newPath = newPath.substring(newPath.indexOf('document/') + 9);
//newPath = newPath.substring(newPath.indexOf(':') + 1);
newPath = newPath.replaceAll(prefix, newPathPrefix);
return newPath;
}
*/
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Status Downloader'),
),
body: Center(
child: _isPermissionGranted
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Permission Granted'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _fetchFiles,
child: const Text('Fetch Files'),
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: _files.length,
itemBuilder: (context, index) {
return _files[index].endsWith(".jpg")
? Image.file(File(_files[
index])) //try convertContentUriToFilePath(_files[index])
: ListTile(
title: Text(_files[index]),
);
},
),
),
],
)
: ElevatedButton(
onPressed: _requestPermission,
child: const Text('Request Permission'),
),
),
);
}
}
class FolderPicker {
static const MethodChannel _channel =
MethodChannel('com.blackstackhub.folderpicker');
static Future<bool> isPermissionGranted() async {
try {
final bool result = await _channel.invokeMethod('isPermissionGranted');
return result;
} on PlatformException catch (e) {
print("Failed to check permission: '${e.message}'.");
return false;
}
}
static Future<void> requestPermission() async {
try {
await _channel.invokeMethod('requestSpecificFolderAccess');
} on PlatformException catch (e) {
print("Failed to request permission: '${e.message}'.");
}
}
static Future<List<String>> fetchFilesFromDirectory() async {
try {
final List<dynamic> result =
await _channel.invokeMethod('fetchFilesFromDirectory');
print(result);
print(result.length);
return result.cast<String>();
} on PlatformException catch (e) {
print("Failed to fetch files: '${e.message}'.");
return [];
}
}
}
@stuartmorgan
As I said in the referenced issue, this is likely a wontfix, since:
it's not at all clear how many of the APIs in File would work. content URIs aren't files, they are their own distinct concept; that's why they have a different scheme in the first place.
As to this:
The conclusion of the #25659 is to use a plugin https://pub.dev/packages/flutter_absolute_path which i dont think that will be Good bcus this plugin copy the files temporary to another location
That was not "the conclusion" it was a suggestion from another user for one possible solution. There are better options that don't involve File support, such as a plugin that specifically provide an interface for the kinds of things that can be done with content:// URIs, such as reading their data, without copying them, but also without suggesting that they can be, e.g., moved to a different path, which they can't.
For instance, there's been some discussion of reworking cross_file to allow for arbitrary implementations, including content:// support.
Correct
such as a plugin that specifically provide an interface for the kinds of things that can be done with content:// URIs, such as reading their data, without copying them, but also without suggesting that they can be, e.g., moved to a different path, which they can't.
Good
there's been some discussion of reworking cross_file to allow for arbitrary implementations, including content:// support.`
inconclusion i should await the reworking ?
just that there are no better if these feature is not available
only ways are to render bytes, file copy, folder moved
My initial thought is that this doesn't belong in File unless the API for accessing these URIs is identical to how files are accessed i.e. through open, read, etc. POSIX system calls.
From what @stuartmorgan wrote, that seems to not be the case - it uses the ContentProvider API, right?
@brianquinlan nah doesn't use ContentProviderAPI its just like a normal path in android but instead of usual str /path/to/file its Uri instead
Uri uri = Uri.parse("file:///path/to/your/file");
File file = new File(uri.getPath());
You've shown a file: URI, not a content: URI. As I noted above, those have very different semantics.
@devfemibadmus In your example, does converting the content: URI to a file path result in Image.file working?
You've shown a
file:URI, not acontent:URI. As I noted above, those have very different semantics.
Thats just an example
@devfemibadmus In your example, does converting the
content:URI to a file path result inImage.fileworking?
nah, Oh yeah It's ContentProviderAPI
My initial thought is that this doesn't belong in
Fileunless the API for accessing these URIs is identical to how files are accessed i.e. throughopen,read, etc. POSIX system calls.From what @stuartmorgan wrote, that seems to not be the case - it uses the
ContentProviderAPI, right?
You're right it uses ContentProvider API, but still since its lead to a file destination, should be called through File?
but still since its lead to a file destination
It doesn't though, it resolved via an interprocess system-meditated data transfer.
but still since its lead to a file destination
It doesn't though, it resolved via an interprocess system-meditated data transfer.
so what do we use? File?
@stuartmorgan @lrhn @brianquinlan
We should not use theFile class for any URI scheme other than file:.
The File class represents a POSIX file, or its path really, which is why it can be converted to and from a File: URI.
This is something else. It should be represented by something else.
If the problem is that some existing code accepts only File objects, for a use whether a "content" object could also be accepted, then we may need to introduce a supertype for "readable resources".
(I'd consider introducing a new abstraction instead of using the platform File class, because this sounds like something slightly different. A ReadableResource with FileResource and ContentResource subtypes, perhaps.)
so what do we use? File?
I think that using File is the wrong solution. This seems like an Android-specific problem that, as @stuartmorgan said, can be solved with a Flutter plugin.
I'm closing this issue because I think that it is out-of-scope for Dart. If you disagree, please reopen with your reasoning.
can be solved with a Flutter plugin.
and using FIle is the way we can access that which is from Dart, we have file
**In Dart, there is a built-in class called File which represents a file on the filesystem. **
final file = File(path);
// here path should be /Document/path/to/file/
nah, Oh yeah It's ContentProviderAPI
My initial thought is that this doesn't belong in
Fileunless the API for accessing these URIs is identical to how files are accessed i.e. throughopen,read, etc. POSIX system calls. From what @stuartmorgan wrote, that seems to not be the case - it uses theContentProviderAPI, right?You're right it uses ContentProvider API, but still since its lead to a file destination, should be called through
File?
So if you say this should be solved with flutter plugin, i will like you to expatiate maybe i can do that
We should not use the
Fileclass for any URI scheme other thanfile:.The
Fileclass represents a POSIX file, or its path really, which is why it can be converted to and from aFile:URI.This is something else. It should be represented by something else.
If the problem is that some existing code accepts only
Fileobjects, for a use whether a "content" object could also be accepted, then we may need to introduce a supertype for "readable resources". (I'd consider introducing a new abstraction instead of using the platformFileclass, because this sounds like something slightly different. AReadableResourcewithFileResourceandContentResourcesubtypes, perhaps.)
YES! YES!! PLEASE!!!
Content Uri are now the standard way of accessing files in android. So, its frustrating that there is no direct support in Flutter/Dart for it. A third party plugin is not the best solution for this as it may or may not be maintained by the developer and may be buggy.
Content Uri are now the standard way of accessing files in android. So, its frustrating that there is no direct support in Flutter/Dart for it. A third party plugin is not the best solution for this as it may or may not be maintained by the developer and may be buggy.
finally someone is here for me
Content Uri are now the standard way of accessing files in android. So, its frustrating that there is no direct support in Flutter/Dart for it. A third party plugin is not the best solution for this as it may or may not be maintained by the developer and may be buggy.
finally someone is here for me
I believe you can reopen the request. This requirement is a must have feature.
I believe you can re-open the request. This requirement is a must have feature.
you cannot re-open your own issues if a repo collaborator closed them @brianquinlan @stuartmorgan @lrhn
Does sound like you want a ContentReader API that accepts URIs and can read content through the corresponding Android API.
That's not the File class.
It may not be a class that can even exist outside of Android.
Maybe the behavior is close enough to File that it can pretend to implement File. More likely, because the path getter won't even work, it will be a separate API which can maybe work with file URIs on all native platforms, and also with content URIs on Android.
But it's not the File class.
Not sure what it is, if not a complete implementation of ContentResolver.
Damn.............this gonna be another limitation for flutter even thou its android?
I have opened a feature request on Flutter repository. https://github.com/flutter/flutter/issues/147037#issue-2252357646
update, we getting the features
https://github.com/flutter/website/pull/10472
https://github.com/flutter/packages/pull/6625
https://docs.google.com/document/d/12pJDtl0yubyc68UqKo2hQ7XJwv86_ixCjNemQ7ENCBQ/edit#heading=h.ebkwt1wlot63
we getting the features
@devfemibadmus
I am not sure we are getting them that soon, cross_file would first need to be federated until some new features are merged, when this will be done (anybody can do this! If you feel you are capable, try to make it work and to get it merged), I'll probably do another proposal/PR once cross_file will be federated. I did https://github.com/flutter/packages/pull/6625 mainly to get things moving with some code and ideas we can iterate on.