12

I'm using a Samsung A3, Android 5.0.2. I'm using this setup to compile apps, i.e. Android 4.1 Jelly Bean (API 16) target.

I precisely know the path of the external removable microSD card, it is /mnt/extSdCard/ (see also Note #7 below).

Problem: I notice that

File myDir = new File("/mnt/extSdCard/test");
myDir.mkdirs();

doesn't work: no directory is created.

Also:

File file = new File("/mnt/extSdCard/books/test.txt");   // the folder "books" already exists on the external microSD card, has been created from computer with USB connection
FileOutputStream fos = new FileOutputStream(file);

produces this error:

java.io.FileNotFoundException: /mnt/extSdCard/books/test.txt: open failed: EACCES (Permission denied) at libcore.io.IoBridge.open(...

How to force read+write access to external removable microSD card?

Notes:

  1. Environment.getExternalStorageDirectory().toString() gives /storage/emulated/0 which is my phone internal storage, i.e. not what I want.

  2. getExternalFilesDir(null) gives /storage/emulated/0/Android/data/com.blahblah.appname/files/ i.e. not what I want. Note that I can't use getExternalFilesDirs with a final s because this is not available in API16. Also runtime permissions are not available in API16 neither.

  3. I already have <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />, and also READ_EXTERNAL_STORAGE.

  4. I read lots of topics like this one or this one, in fact probably twenty similar questions, but finally it seems very complex and everything and its contrary is said. That's my I'm looking for a solution specific to this situation.

  5. I don't want ACTION_OPEN_DOCUMENT and ACTION_CREATE_DOCUMENT, in fact I don't want any GUI solution.

  6. Some apps I have (Sync Resilio) are allowed to modify /mnt/extSdCard/music/ successfully, to create new files there, etc.

  7. By the way, ls -la /mnt/extSdCard/ gives

    drwxrwx--x root     sdcard_r          2017-10-15 01:21 Android
    drwxrwx--- root     sdcard_r          2017-10-14 00:59 LOST.DIR
    drwxrwx--- root     sdcard_r          2017-12-05 16:44 books
    drwxrwx--- root     sdcard_r          2017-11-21 22:55 music
    
Basj
  • 41,386
  • 99
  • 383
  • 673
  • You forgot to mention runtime permission. – greenapps Dec 10 '17 at 11:27
  • Have a look at getExternalFilesDirs(). If you are lucky it returns two paths. The second one would then be a writable one on the micro SD card. – greenapps Dec 10 '17 at 11:30
  • You could google for runtime permissions. But for access to the micro SD card they are of no use. – greenapps Dec 10 '17 at 11:44
  • @greenapps In API16, `getExternalFilesDirs` with plural (Dirs) is not available. Also runtime permission is not available either, see note 2. Any other idea? – Basj Dec 10 '17 at 23:23
  • This seems to be relevant. https://stackoverflow.com/questions/40068984/universal-way-to-write-to-external-sd-card-on-android – Parth Sane Dec 12 '17 at 02:06
  • Disable file transfer from device to computer. If Enabled, the app wont be able to access the SD card. Maybe this is the problem as it seems you tried all known methods – Rainmaker Dec 12 '17 at 20:28
  • @Basj ""but I don't see which point can help"", start trying each one in the accepted answer. – petey Dec 12 '17 at 23:46
  • @petey Many of the points don't apply because API is higher or because I tried, and they didn't work (see my Notes 1, 2, 3, etc.). – Basj Dec 12 '17 at 23:55
  • 3
    You need to use `DocumentFile`. There isn't a public API to get the path to a removable SD card (as far as I know). This should help: https://stackoverflow.com/a/35175460/1048340 Why can't you use internal storage instead of a removable SD card? – Jared Rummler Dec 12 '17 at 23:57
  • @JaredRummler I have 100+ GB on my microSD card for music library, I don't have this space on internal storage (probably only 8 or 16GB on internal storage). Do you think `DocumentFile` might work with target=API16, and would work also on a Android 5.0.2? – Basj Dec 13 '17 at 00:07
  • @Basj I have added an answer for this question. – Jorgesys Dec 18 '17 at 15:27
  • Thanks @JaredRummler. Would you have a minimal code example that would create at least a file `/books/hello.txt` (and not `/Android/data/...`), compilable with API16 (Android 4.1), and runnable with an unrooted Android 5.0 phone? – Basj Dec 18 '17 at 16:29

3 Answers3

1

bear in mind that some android devices will have a different path for the SD Card and some doesn´t have removable SD Card.

You don´t have to set the path directly!

File myDir = new File("/mnt/extSdCard/test");
myDir.mkdirs();

You can check first if your device has mounted a removable SD Card:

public static boolean isSDCardAvailable(Context context) {
    File[] storages = ContextCompat.getExternalFilesDirs(context, null);
    if (storages.length > 1 && storages[0] != null && storages[1] != null)
        return true;
    else
        return false;
}

Why get External Directories > 1, well because most of all the android devices has external storage as a primary directory and removable SD Card as second directory:

introducir la descripción de la imagen aquí

But you can use a method to get the real path of your removable microSD card:

public static String getRemovableSDCardPath(Context context) {
    File[] storages = ContextCompat.getExternalFilesDirs(context, null);
    if (storages.length > 1 && storages[0] != null && storages[1] != null)
        return storages[1].toString();
    else
        return "";
}

Then just do this:

File myDir = new File(getRemovableSDCardPath(getApplicationContext()),"test");
if(myDir.mkdirs()){
  Log.i(TAG, "Directory was succesfully create!");
}else{
  Log.i(TAG, "Error creating directory!");
}

For example using the method:

   String pathSDCard = getRemovableSDCardPath(getApplicationContext());

I have as a result the path of my removable SD Card (if i wouldn´t have a removable SD Card my path would be "", so you can implemente a validation to avoid the creation of the folder):

/storage/extSdCard/Android/data/com.jorgesys.myapplication/files

Now creating a new folder inside :

    File myDir = new File(getRemovableSDCardPath(getApplicationContext()),"test");
    if(myDir.mkdirs()){
        Log.i(TAG, "Directory was succesfully create!");
    }else{
        Log.i(TAG, "Error creating directory!");
    }

now i have the directory /test created:

enter image description here

Jorgesys
  • 124,308
  • 23
  • 334
  • 268
  • Please for the -1 add a comment for the reasons, i´m open to improve my answers the give a better help for the OP. – Jorgesys Dec 18 '17 at 15:25
  • Thanks for your answer, but please see Note 2: `getExternalFilesDirs()` is [not accessible when using target API 16 (added only in API level 19)](https://developer.android.com/reference/android/content/Context.html#getExternalFilesDirs(java.lang.String)). Also I don't want to access `/SDcard/Android/data/com.package.example/files/...` but rather a root foler: `/SDcard/mp3/` (i.e. i don't want to be limited to `/SDCard/Android/data/...` subfolders). Use case: imagine you have a 50 GB MP3 library, you don't want it to be limited to app X, but you also want to use it with app Y or app Z. – Basj Dec 18 '17 at 22:51
  • Question was to get Permissions – Umair Iqbal Apr 13 '20 at 02:36
1

As I struggled a lot with the same problem I'll share my bit. I have taken help from these resources big thanks to them as well:

DocumentFile Android Docs and From Here

I have tested the code on 5.1.1 and 6.0.1 not on rest of the device I have not tested it but it should work fine.

On 5.0.2 to write on the external device you will have to ask user's permission.

Using below code and before asking this permission you need to instruct the user to select the root sd card so that you will have the access to the entire external storage.

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, 25);

now in the onActivityResult save the UriTree return by the API as you will need it later.

 @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == 25&&resultCode == RESULT_OK) {
            getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    } 
    super.onActivityResult(requestCode, resultCode, data);
}

Once you have the root UriTree you can create modify delete files or directories from the external storage for that you will have get the DocumentFile from the UriTree.

To get the Document UriTree used the below code.

public static DocumentFile getDocumentFile(final File file) {
        String baseFolder = getExtSdCardFolder(file);
        String relativePath = null;

        if (baseFolder == null) {
            return null;
        }

        try {
            String fullPath = file.getCanonicalPath();
            relativePath = fullPath.substring(baseFolder.length() + 1);
        } catch (IOException e) {
            Logger.log(e.getMessage());
            return null;
        }
        Uri treeUri = Common.getInstance().getContentResolver().getPersistedUriPermissions().get(0).getUri();

        if (treeUri == null) {
            return null;
        }

        // start with root of SD card and then parse through document tree.
        DocumentFile document = DocumentFile.fromTreeUri(Common.getInstance(), treeUri);

        String[] parts = relativePath.split("\\/");

        for (String part : parts) {
            DocumentFile nextDocument = document.findFile(part);
            if (nextDocument != null) {
                document = nextDocument;
            }
        }

        return document;
    }


    public static String getExtSdCardFolder(File file) {
        String[] extSdPaths = getExtSdCardPaths();
        try {
            for (int i = 0; i < extSdPaths.length; i++) {
                if (file.getCanonicalPath().startsWith(extSdPaths[i])) {
                    return extSdPaths[i];
                }
            }
        } catch (IOException e) {
            return null;
        }
        return null;
    }


@TargetApi(Build.VERSION_CODES.KITKAT)
public static String[] getExtSdCardPaths() {
    List<String> paths = new ArrayList<>();
    for (File file : Common.getInstance().getExternalFilesDirs("external")) {

        if (file != null && !file.equals(Common.getInstance().getExternalFilesDir("external"))) {
            int index = file.getAbsolutePath().lastIndexOf("/Android/data");
            if (index < 0) {
                Log.w("asd", "Unexpected external file dir: " + file.getAbsolutePath());
            } else {
                String path = file.getAbsolutePath().substring(0, index);
                try {
                    path = new File(path).getCanonicalPath();
                } catch (IOException e) {
                    // Keep non-canonical path.
                }
                paths.add(path);
            }
        }
    }
    return paths.toArray(new String[paths.size()]);
}

Above code will return you the DocumentFile version of any file using that you can perform the desired operation.

If you want to see this code in action check out I have used this in my open source project to modify the mp3 files present on the external storage.

Hope it helps in case of doubts let me know.

Forgot to tell I had asked the same question a while ago Here is that question.

Edit: Using this code you can check if the user has given the permission on not

public static boolean hasPermission() {
    List<UriPermission> uriPermission = Common.getInstance().getContentResolver().getPersistedUriPermissions();
    return uriPermission != null && uriPermission.size() > 0;
}

if the permission is revoked then there will be no UriTree so you will have to ask for the permission again.

Reyansh Mishra
  • 1,879
  • 1
  • 14
  • 26
  • It looks wonderful @ReyanshMishra, I'll try this as soon as possible! Two little questions: 1) do you think it will compile with target API16 (Android 4.1) or will it absolutely need a newer version? – Basj Dec 18 '17 at 19:52
  • 2) Will the user have to select the root SD card in a `Intent.ACTION_OPEN_DOCUMENT_TREE` dialog each time? One after each phone reboot? Or once for all (the first time we open the app)? Thank you again! – Basj Dec 18 '17 at 19:53
  • yes, I should compile on the API 16 and for the second question, I'll update the answer. – Reyansh Mishra Dec 18 '17 at 20:16
  • Thanks again @ReyanshMishra. If the user doesn't do anything, for how long will the permission stay? Let's say I give permission today, and I reboot the phone a few times, and I reopen the app in January 2018, will I need to re-give permission, or will it stay, by default? – Basj Dec 18 '17 at 20:35
  • In only two cases the permission will be removed 1. when the user removes the sdcard 2. when used clears the cache and data of the app from the application setting. – Reyansh Mishra Dec 19 '17 at 04:14
  • Whoever is downvoting without explanation is really not helping the community, Please be a good community member and explain the downvotes. Thanks – Reyansh Mishra Dec 19 '17 at 04:16
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/161497/discussion-between-reyansh-mishra-and-basj). – Reyansh Mishra Dec 19 '17 at 07:44
  • Thanks again for your answer, I'll test this in detail. (PS: I didn't downvote of course, I upvoted) – Basj Dec 19 '17 at 16:45
  • As you gave me the points I assume it worked and It should. For more detailed info you can always my open source library code which I have linked in the answer and I am talking about the people who are downvoting just like that with no explanation. – Reyansh Mishra Dec 19 '17 at 17:13
0

Based on Doomsknight's answer and mine, and Dave Smith and Mark Murphy blog posts: 1, 2, 3:

  1. Ideally, use the Storage Access Framework and DocumentFile as Jared Rummler pointed. Or:
  2. Use your app specific path/storage/extSdCard/Android/data/com.myapp.example/files.
  3. Add read/write permission to manifest for pre-KitKat, no permission required later for this path.
  4. Try to use point2 directory and Doomsknight's methods considering KitKat and Samsung case.
  5. Filter and use getStorageDirectories point 2 path and read/write permissions prior to KitKat.
  6. ContextCompat.getExternalFilesDirs since KitKat remembering Samsung returns internal first:

Write My Package’s Android Data Directory is ok for primary and secondary storages:

In KitKat, full ownership of the app-specific data directories is given to the app’s unique user ID. This means that, going forward, no permission is necessary for an app to read/write to its specific directories on external storage...

The android.permission.WRITE_EXTERNAL_STORAGE permission now grants membership in sdcard_r AND sdcard_rw...

No permissions are necessary to use any external storage volume for application-specific files.

NOTE: In ls -la /mnt/extSdCard/... listings, the sdcard_r group has full +rwx permissions, which obviously is not true in practice... because the FUSE daemon is an active participant in modifying the permissions applied to applications at runtime.

Samsung: A Case Study

In Android 4.3, Samsung emulated the primary external storage volume on the device’s internal flash (NOT the removable SD card). The SD card had always internally been marked as the secondary external storage medium...

In Android 4.4, the primary external storage volume is STILL on internal flash.

Samsung made the choice to include an SD card slot but not mark that as the primary external storage medium.


Update:

As explained here and answering your comment about use root path to share files:

You can prior to Kikat using Doomsknight's method 1 and if/else code based on target version, or building multiple APKs.

Since KitKat, Third-party applications just can’t add more files of their own in random locations...

Why Now?

The answer, in an acronym, is CTS.

Rules stating that secondary storage volumes should not be writable by applications have also been in the document since 4.2

However, new tests were added in CTS for 4.4 that validate whether or not secondary storage has the proper read-only permissions in non app-specific directories, presumably because of the new APIs to finally expose those paths to application developers. As soon as CTS includes these rules, OEMs have to support them to keep shipping devices with GMS (Google Play, etc.) on-board.

What About Sharing Files?

It’s a valid question. What if an application needs to share the files it has created on a secondary external storage volume? Google’s answer seems to be that those applications who actively decide to go beyond primary external storage to write content should also expose a secure way of sharing it, either using a content provider or the new Storage Access Framework.

As I explained on the question you requested my help, I never tried this, but my opinion is that theorically you can use root path prior to KitKat using method 1, and these alternatives later.

albodelu
  • 7,931
  • 7
  • 41
  • 84
  • Thank you very much @albodelu. In addition to all the links (I'm a bit lost, because I don't which exact version to use), to make it a full answer, would you have some minimal code example of how to make this work: a minimal code that would create a file `/books/hello.txt`? (that would compile with target API 16 = Android 4.1, and would work on a unrooted Android 5.0 phone). Thanks one billion times in advance (and probably more, because I spent so much time without any success, like money other people probably :) ) – Basj Dec 18 '17 at 16:34
  • You are welcome. The idea is to use [this code](https://github.com/futuresimple/android-support-v4/blob/master/src/java/android/support/v4/content/ContextCompat.java#L223) replacing `if`section by `ContextCompat.getExternalFilesDirs`and `else` section by Doomsknight's method1 or 5. Theoretically, it will work on API 16. My legacy devices are rooted to upgrade them to KitKat, I can not try it. If you link to a demo/test project in Github, I can try to help you next week. Anyway, shared links already contain the relevant code. About the path, I expect other developers respect the Kitkat limits. – albodelu Dec 18 '17 at 22:27
  • The reason to replace `else` section is Samsung devices return a wrong path using `getExternalFilesDir`. You really don't need to use root path, this one `/storage/extSdCard/Android/data/com.myapp.example/files`solves your space issue. – albodelu Dec 18 '17 at 22:36
  • No @albodelu, `/storage/extSdCard/Android/data/com.myapp.example/files` doesn't solve the issue: imagine a 50GB MP3 library on your microSD card. You want `MP3Player` app to access it, but also another app `MP3_DJ` to also access it. So the folder *can't be app-specific*. That's why I wanted to access (read/write) **the root** of the micro SD card `/sdcard/mp3/` for example. Before I do lots of tests relevant to all the links you provided, does your solution work for root folders or not? – Basj Dec 18 '17 at 22:39
  • [This question](https://stackoverflow.com/q/26744842/1009132) already has two answers with theory and code about the SAF alternative. – albodelu Dec 18 '17 at 22:40
  • >`ContextCompat.getExternalFilesDirs` [...] Theoretically, it will work on API 16: [It seems that it starts on API 19 (added in API 19 level)](https://developer.android.com/reference/android/content/Context.html#getExternalFilesDirs(java.lang.String)) – Basj Dec 18 '17 at 22:43
  • I thought it would work @albodelu, but oops, I see [the answer you mentioned](https://stackoverflow.com/a/26765884/1422096) does not work in KitKat: `Is this available on KitKat? No, we can't retroactively add new functionality to older versions of the platform.` so it probably won't work for API 16 either. – Basj Dec 18 '17 at 22:47
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/161545/discussion-between-basj-and-albodelu). – Basj Dec 19 '17 at 16:45