-1

I am trying to write to a file that is located in the SDCard, I found out that I need special permission for removable storage something that is not found in any known permission handler plugin for flutter (i tried simple_permission and permission_handler with no use).

I tried to acquire those permissions using the android side of things, so I wrote a simple function that would show the dialog and the user would allow the app to modify the content of the SDCard.

even after acquiring the rights to the SDCARD, I still get the same permissions denied error when trying to save files to the SDCard when using File.writeAsStringSync method.

I want to know if there is any known way/hack/workaround to save files in SDCards in flutter.

The android code i used is the same from this answer : https://stackoverflow.com/a/55024683/6641693

NOTE : I am targetting android 7 and beyond but not android 11.

Kaki Master Of Time
  • 1,428
  • 1
  • 21
  • 39
  • 1
    Which Android version runs on used device? The removable micro sd card is read only since kitkat. Except for app specific directory. On Android 11 you have write access again to some directories. – blackapps Sep 21 '20 at 17:46
  • 1
    Using SAF you have write acces to whole card. Unless under Android 11. – blackapps Sep 21 '20 at 17:47
  • You can save a file to any file path provided the app has permissions to save a file there. No hack needed. – Abion47 Sep 21 '20 at 18:01
  • @Abion47 go ahead try it out, try to save a file in sdCard with storage permissions, it won't work, u need sdcard permissions on top of that. there is a whole issue with it here : https://github.com/flutter/flutter/issues/40504 – Kaki Master Of Time Sep 21 '20 at 18:11
  • As said before: the removable micro sd card is read only for Android 7.1 also. Flutter will not have permissions which the rest of the world has not. – blackapps Sep 21 '20 at 18:19
  • @blackapps okay, but I can use my file explorer on my device to add files to my sdCard. it means i can get permissions to write on sdCard, which is the point of my post, HOW do I get those permissions for my flutter app. – Kaki Master Of Time Sep 21 '20 at 19:25
  • 1
    @KakiMasterOfTime That thread is about including a built-in path generator in Flutter to the directory where the SD card is mounted. As the issue itself states, the location of the SD card directory varies from vendor to vendor, so there is no one way to determine what the path is going to be, hence why the issue has been all but abandoned. You have no choice really but to check all the common paths to see if any of them exist. – Abion47 Sep 21 '20 at 19:39
  • @Abion47 ah yes, i figured that too so i used a native side method to get the SDCard location. my problem now is getting the permissions to write to that directory. – Kaki Master Of Time Sep 21 '20 at 19:41
  • @KakiMasterOfTime But that is irrelevant to my point, though. Once you have the path, writing to the SD card is no different than writing anywhere else. If you have the permissions, write the file with `File.writeAsStringSync` (or similar). If that call fails, you don't have permissions to write there. And as blackapps keeps pointing out, you are targeting an Android API where the SD card is read-only, and there is no black magic that Flutter can do to magically change that, so this entire conversation is moot anyway. – Abion47 Sep 21 '20 at 19:41
  • @Abion47 like i said to blackapps, on my android 7 phone i can the permissions to write on my SdCard, and.i do receive a dialog asking me to allow the file app to do that or not. i was however able to get there and gave my app that permission, but i still get the same permissions error from `File.writeAsStringSync`. I know that the SDcard is readOnly but clearly there is a way to make it not. – Kaki Master Of Time Sep 21 '20 at 19:44
  • @KakiMasterOfTime I think you should examine your permission-granting code again. You can show as many dialogs and get user input as much as you want, that won't change the restrictions of the underlying system itself. Just because the user pressed an OK button on a popup window doesn't mean you have been granted permissions that, as has been made explicitly clear, you are not going to get. This problem has nothing to do with Flutter and everything to do with Android 7. The bottom line is this, pure and simple: if you want write access to the SD card, target a newer version of Android. – Abion47 Sep 21 '20 at 19:48
  • I have seen it done by other apps. Thank you for your care. – Kaki Master Of Time Sep 21 '20 at 19:49
  • Then look at how those apps did it, and if those apps were actually running on Android 7, or if they had some kind of elevated status to be able to get those permissions. – Abion47 Sep 21 '20 at 19:51
  • my phone runs on android 7 and I can change files on my sdCard after getting permission for that from a built-in dialog and I can literally do it as we speak. Thanks again. – Kaki Master Of Time Sep 21 '20 at 19:53
  • Which apps? And how did they do it? Why dont you tell. System apps have special privilleges. Specially the inbuild file manager. And every app, also yours, can use SAF to write to sd card. So now tell how they did it. – blackapps Sep 21 '20 at 19:54
  • Further: any app can write to its app specific directory on a removable micro sd card. No permissions needed. – blackapps Sep 21 '20 at 20:05
  • I am running a music app that reads your entire phone for music files and will let you change the tags on each file and save them. I need to be able to save files whenever they are. – Kaki Master Of Time Sep 21 '20 at 20:07

1 Answers1

0

I solved This, by ditching the dart file saving and using the android SAF. First, what I did was try to get the sdCard modification permissions. After that, I get to save the files I need.

here is the code I used to get the permissions ( aka the "allow this app to modify content on your sdCard" dialog )

public void takeCardUriPermission(String sdCardRootPath) {
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
      File sdCard = new File(sdCardRootPath);
      StorageManager storageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
      StorageVolume storageVolume = storageManager.getStorageVolume(sdCard);
      Intent intent = storageVolume.createAccessIntent(null);
      try {
        startActivityForResult(intent, 4010);
      } catch (ActivityNotFoundException e) {
        Log.e("TUNE-IN ANDROID", "takeCardUriPermission: "+e);
      }
    }
  }

  protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == 4010) {

      Uri uri = data.getData();

      grantUriPermission(getPackageName(), uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
              Intent.FLAG_GRANT_READ_URI_PERMISSION);

      final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
              Intent.FLAG_GRANT_READ_URI_PERMISSION);

      getContentResolver().takePersistableUriPermission(uri, takeFlags);
      methodChannel.invokeMethod("resolveWithSDCardUri",getUri().toString());
    }
  }

  public Uri getUri() {
    List<UriPermission> persistedUriPermissions = getContentResolver().getPersistedUriPermissions();
    if (persistedUriPermissions.size() > 0) {
      UriPermission uriPermission = persistedUriPermissions.get(0);
      return uriPermission.getUri();
    }
    return null;
  }

So in order to start the whole permissions acquiring process, you have to first call takeCardUriPermission and passing the URI of the sdCard path.

Note: on my FlutterActivity, i am able to get the sdCardPath directly using getExternalCacheDirs()[1].toString()

After calling takeCardUriPermission and once the allow button is pressed (or the decline) an activity result event will be called and the onActivtyResult method will be called. the requestCode check is useful when you have multiple events and you need to filter this one out.

The activity result code will give the app permissions to modify the files on the sdCard.

The getUri method is the one that we will be using afterwards when trying to save bytes to a file, it returns the URI of the SDCard that we selected (you can have multiple sdCards).

Saving Files

What I used to save a file is a straightforward method. First we need to get the URI of the sdCard and create a Documentfile out of it, then we go through the hierarchy of that directory (DocumentFile can reference files and directories) to find the needed file based on it's URI.

We do this search by splitting the file URI into parts and then navigating the hierarchy by testing if each part exists or not. Once we test all the parts we would have reached our file, if it exists, or we were stuck at the last directory we got to.

the resulting of this iteration is a DocumentFile that we can execute operations on and with.

the following is the full file saving code :

String filepath = (String) arguments.get("filepath");
          final byte[] bytes = methodCall.argument("bytes");

          try{
            if(filepath==null || bytes==null)throw new Exception("Arguments Not found");
            DocumentFile documentFile = DocumentFile.fromTreeUri(getApplicationContext(), getUri());
            String[] parts = filepath.split("/");
            for (int i = 0; i < parts.length; i++) {
              DocumentFile nextfile = documentFile.findFile(parts[i]); 
              if(nextfile!=null){
                documentFile=nextfile;
              }
            }
            if(documentFile!=null && documentFile.isFile()){
              OutputStream out = getContentResolver().openOutputStream(documentFile.getUri());
              out.write(bytes);
              out.close();
            }else{
              throw new Exception("File Not Found");
            }
          }catch (Exception e){
            result.error("400",e.getMessage(),e);
            return;
          }
          result.success(true);

Note: in my code, I am calling this under the MethodChannel's MethodCallHandler which will give me the argument I need: filePath which is the String URI of the file I want to write to and the bytes byte array representing the data I want to save. The same can be said for the result.success

The file writing code is simple: open the file, write the data and close the file.

Kaki Master Of Time
  • 1,428
  • 1
  • 21
  • 39