Skip to content

Request for Support of content:// URI in Flutter's File Class for Android #54878

Closed
@devfemibadmus

Description

@devfemibadmus

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

and here #bug-flutter-fileimagevideo-not-working-with-android-action_open_document_tree-but-works-fine-in-kotlin

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-core-librarySDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries.library-io

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions