Description
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 [];
}
}
}