Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
devfemibadmus opened this issue Feb 11, 2024 · 23 comments
Closed
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. library-io

Comments

@devfemibadmus
Copy link

devfemibadmus commented Feb 11, 2024

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 [];
    }
  }
}
devfemibadmus added a commit to devfemibadmus/folderpermission that referenced this issue Feb 11, 2024
devfemibadmus added a commit to devfemibadmus/folderpermission that referenced this issue Feb 11, 2024
@devfemibadmus
Copy link
Author

@stuartmorgan

@stuartmorgan
Copy link
Contributor

stuartmorgan commented Feb 11, 2024

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.

@devfemibadmus
Copy link
Author

devfemibadmus commented Feb 11, 2024

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 ?

@devfemibadmus
Copy link
Author

devfemibadmus commented Feb 11, 2024

just that there are no better if these feature is not available

only ways are to render bytes, file copy, folder moved

@lrhn lrhn added area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. library-io labels Feb 11, 2024
@brianquinlan
Copy link
Contributor

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?

@devfemibadmus
Copy link
Author

@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());

@stuartmorgan
Copy link
Contributor

You've shown a file: URI, not a content: URI. As I noted above, those have very different semantics.

@brianquinlan
Copy link
Contributor

@devfemibadmus In your example, does converting the content: URI to a file path result in Image.file working?

@devfemibadmus
Copy link
Author

You've shown a file: URI, not a content: 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 in Image.file working?

nah, Oh yeah It's ContentProviderAPI

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?

You're right it uses ContentProvider API, but still since its lead to a file destination, should be called through File?

@stuartmorgan
Copy link
Contributor

but still since its lead to a file destination

It doesn't though, it resolved via an interprocess system-meditated data transfer.

@devfemibadmus
Copy link
Author

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?

@devfemibadmus
Copy link
Author

@lrhn
Copy link
Member

lrhn commented Feb 24, 2024

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.)

@brianquinlan
Copy link
Contributor

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.

@devfemibadmus
Copy link
Author

devfemibadmus commented Feb 26, 2024

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 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?

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

@devfemibadmus
Copy link
Author

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.)

YES! YES!! PLEASE!!!

@sachinkhatripro
Copy link

sachinkhatripro commented Apr 18, 2024

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.

@devfemibadmus
Copy link
Author

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

@sachinkhatripro
Copy link

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.

@devfemibadmus
Copy link
Author

devfemibadmus commented Apr 19, 2024

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

@lrhn
Copy link
Member

lrhn commented Apr 19, 2024

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.

@devfemibadmus
Copy link
Author

devfemibadmus commented Apr 19, 2024

Damn.............this gonna be another limitation for flutter even thou its android?

@sachinkhatripro
Copy link

I have opened a feature request on Flutter repository. flutter/flutter#147037 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. library-io
Projects
None yet
Development

No branches or pull requests

5 participants