Xamarin and Android - KitKat and Your External SD Card

November 20, 2014

This is an update on my previous blog post about locating and using your external, removable SD card on versions of Android based on Jellybean (and below).

Android KitKat (version 4.4) introduced some extra security around writing to your external SD card, and in a nutshell, it really just comes down to not being able to place files where you like any more. You can still write to it, but there is a rather nasty caveat which I’ll get to later on.

permissions... permissions everywhere...

Pre-KitKat, you could still create directories and files at the “root” of the mount point of the SD card.

But not any more. You can still do this on the INTERNAL disk. I don’t know if this will change in future, but I am going to assume it will.

So:

  • You can still write to the external, removable SD card, just not in the root.
  • On the external SD card, you have to place your files in a folder based off:

/storage/(external_sd_mount)/Android/data/(your.package.name)/

(you have full access rights in here, you can do pretty much anything you need to)

  • You can write to the internal disk exactly as you’ve always been able to (as far as I’ve been able to test it). This part hasn’t changed.
  • You don’t appear to need the WRITE_EXTERNAL_STORAGE permission any more either, if you’re targeting only KitKat and above. Best to leave it in though, if targeting devices lower on the food chain.

So in my spare time, I’ve done some fiddling, some research, built and broke a bunch of things, and coerced a couple of people (@dfrencham and @meligy) who have KitKat devices into helping me out. Thanks guys! :)

If you’ve been struggling with this, yourself, then I’ll save you some time, and show you how to go about it.

The key to accessing the external, removable SD card was there all along (or at least since the SDK version 19 for KitKat was released). Why did I have so much trouble getting it to work? Well, I was overthinking it, and trying to force the solution down the proc/mounts path I’d had to choose before. A one-size-fits-all approach struck me as more elegant, but it was not to be…

Things have actually become easier though…

Google introduced a new API method call in KitKat called GetExternalFilesDirs. This returns an array of all ‘permanently’ mounted ‘external’ paths to where you can write data. I use the term ‘external’ in quotes because I think it’s a bit of a misnomer (and I had a bit of a grumble about it in the earlier post).

You’ll have noticed the similarity of GetExternalFilesDirs to GetExternalFilesDir (added in level 8). One letter difference, and probably why I missed it.

You’ll be pleased to know that GetExternalFilesDirs does pretty much the same thing. The very first item in the list of paths it returns is exactly the same path as would have been returned by GetExternalFilesDir.

However, any paths in the list after that will be paths for externally mounted media, like your external, removable SD card.

If I make this call on an LG G3 running KitKat, I get:

File[] externalFilesDirs = Android.App.Application.Context.GetExternalFilesDirs(null);

// Array.ForEach(externalFilesDirs, efd => Log.Debug("ESH", "Path: {0} - Mount State: {1}", efd.AbsolutePath, Android.OS.Environment.GetStorageState(efd)));
// D/ESH(31949): Path: /storage/emulated/0/Android/data/TestExternalSSD.TestExternalSSD/files - Mount State: mounted
// D/ESH(31949): Path: /storage/external_SD/Android/data/TestExternalSSD.TestExternalSSD/files - Mount State: mounted <-- YES!!! :)

Hooray!

Note that drives which are mounted “transiently”, such as those which might be connected via a USB cable (or something else) are not listed. It appears that the disk needs to be physically attached to the device, for instance in an SD card slot, inside the case.

Does this mean you can simply switch out GetExternalFilesDir for GetExternalFilesDirs, and just use the first item for your internal disk, as a one-call-to-rule-them-all method? Not quite. Because the API was only introduced with KitKat, you have to do an Android version check, to see which version of Android you’re running, or it’ll crash.

Viva fragmentation.

The check is easily done tho. All we have to do is something along the lines of:

// for jellybean and below
if (Android.OS.Build.VERSION.SdkInt <= BuildVersionCodes.JellyBeanMr2)
{
  // and call exactly what we did before
  _path = ExternalSdStorageHelper.GetExternalSdCardPath();
}
else // for kitkat and above
{
  // I made a new method for legibility's sake 
  _path = ExternalSdStorageHelper.GetExternalSdCardPathEx();
}

And what does ExternalSdStorageHelper.GetExternalSdCardPathEx() do?

public static string GetExternalSdCardPathEx()
{
  File[] externalFilesDirs = Android.App.Application.Context.GetExternalFilesDirs(null);

  // Array.ForEach(externalFilesDirs, efd => Log.Debug("ExternalSDStorageHelper", "Path: {0} - Mount State: {1}", efd.AbsolutePath, Android.OS.Environment.GetStorageState(efd)));
  // Path: /storage/emulated/0/Android/data/TestExternalSSD.TestExternalSSD/files - Mount State: mounted
  // Path: /storage/external_SD/Android/data/TestExternalSSD.TestExternalSSD/files - Mount State: mounted <-- this is the one we want!

  if (externalFilesDirs.Any())
  {
    string externalSdPath = externalFilesDirs.Length > 1 ? externalFilesDirs[1].AbsolutePath : string.Empty; // we only want the external drive, otherwise nothing!

    // note that in the case of an SD card, ONLY the path it returns is writeable. You can 
    // drop back to the "root" as we did with the internal one above, but that's readonly.
    return externalSdPath;
  }
  return string.Empty;
}

There’s more info and comments in the code file in the repo, I took some of it out brevity’s sake. You can also take a look at the ContextCompat shim which Google provides for some backward compatibility tweaking, but this adds a hefty chunk to your final APK, which may be an issue for you.

And that’s pretty much all there is to it.

Note that there is also another API method call introduced in Android Lollipop (level 21), named GetExternalMediaDirs, but I haven’t got around to upgrading any of my devices to give this a try (yet!). And no, I don’t trust the emulators for anything hardware related.

Warning! Data loss!

Back to the caveat I mentioned at the start of this post. If you decide to store your information on the external, removable SD card in KitKat (or above), the only location you can write to will be removed if the application is uninstalled! There will be no warning, that data will simply be clobbered. So you may want to give your users the option of either backing up their files or moving them to somewhere else (e.g. on the internal) before uninstalling. Application updates are fine, everything is left as is, but if your user uninstalls your app, their data is history!

If you are a developer, and using this SD card location, then every time you relaunch your app using the debugger will also wipe everything out, since the debugger uninstalls the old version and installs the new one. Ask me how I know :)

The newest version of the testing app I built before is now up on GitHub, with the additional functionality built in. About the only other tweak I made to it was to explicitly call the IsWritable method after getting the path, instead of making calling it inside the path discovery routine.

References

Some references I found useful:

GetExternalFilesDirs

GetExternalFilesDir

GetExternalMediaDirs

ContextCompat

Everything in here is released as OSS under the MIT license, so feel free to use it any way you like.


comments powered by Disqus