Xamarin and Android - How to use your external removable SD card

September 28, 2014

This quick post demonstrates how to how to access the external, REMOVABLE SD card plugged into your Android device.

The code example is done in Xamarin/C#, but exactly the same principles apply in Java, so it should be extremely easy for an Android Java dev to understand exactly what’s going on, and apply the same methods.

Background

There’s been a lot of questions on StackOverflow (and other places) about how to access the ‘External Storage’ on an Android device. Most people seem to want to know how to read from (and write to) the SD card the’ve plugged into the SD card slot in their device.

This sample project shows you how to do just that.

Sadly, due to the gamification and ‘first post!’ mentality of some SO posts, a lot of developers go charging off, half-cocked, and answer the question they think is being asked, instead of the one actually being asked. That’s why there’s so many duplicates of this type of question. And so most of the answers appear to get the concept of ‘External’ and ‘Removable’ mixed up, and then start preaching about how the ‘External Storage’ isn’t actually EXTERNAL storage, it’s just storage separate from the inaccessible, INTERNAL storage of the device. And sometimes that maybe the questioner is being a moron for getting the two concepts confused, and this may be a duplicate of that question over there. So most of those answers are essentially identical in implementation, and completely have the wrong end of the stick. Or the wrong stick.

Just about all developers who build for Android understand that the ‘External’ storage does NOT refer to the removable SD card, even tho the file system sometimes refers to it as such. For the purposes of this post, I am going to refer to this as the ‘removable’ storage, and it will refer to a removable SD card, which a person has physically plugged into the device.

Android is Linux-based

The operating system underneath the Dalvik VM is a Linux based one. I hope that didn’t come as a shock to anyone.

That means that even if there’s not a direct Xamarin or Android call to do what you need to do, there may sometimes be a more Linux-y way of doing it.

The first question we’ve got is: how do we determine whether we even have an removable SD card plugged into the device? This is both simultaneously easy to answer, but harder to solve. Easy, because we can make a single call to see what file systems we have mounted. And hard, because those damned device manufacturers haven’t seen fit to settle on a standardised naming convention for a removable SD card which is plugged into the device.

To make matters worse, some manufacturers have hijacked the name of the internal storage, and often called it something stupid like sdcard0.

On most Linux systems, there’s a ‘file’ in the /proc/ directory called mounts, which contains a (sometimes very long) list of all the file systems mounted on the device.

It can be treated like a a text file. And it doesn’t require any special user permissions or device rooting to read it (though writing to it is another matter!).

Here’s an example, from my ye anciente Samsung Galaxy S2, which has an SD card slot in it, and a micro-SD card plugged into it (the list is shortened substantially):

rootfs / rootfs ro,relatime 0 0
tmpfs /dev tmpfs rw,nosuid,relatime,mode=755 0 0
devpts /dev/pts devpts rw,relatime,mode=600 0 0
blah blah more mounted stuff blah
:
/dev/block/mmcblk0p10 /data ext4 rw,nosuid,nodev,noatime,barrier=1,
/dev/block/mmcblk0p4 /mnt/.lfs j4fs rw,relatime 0 0
/dev/block/vold/259:3 /storage/sdcard0 vfat rw,dirsync,nosuid,nodev ...
/dev/block/vold/179:9 /storage/extSdCard vfat rw,dirsync,nosuid,nodev ...
:

Notice the two last lines there. Both of those are tagged as ‘sdcard’, and we know one of them has to be our removable one. My money is on extSdCard

Can you see where we’re going with this? Good.

Reading the File System

Remember to turn on READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions in your Manifest.xml, or you won’t get the results you expect.

Now we can do a simple call to System.IO.File.ReadAllText("/proc/mounts") to read the text out of this file, and store it in a string. We can then parse the string to look for things like storage, sdcard, vfat, ext and anything else we think would be a good indicator. And if we can find all of that on one file-system line, then the file system mounted there is very likely a good candidate for our removable SD card.

So, after some hacky string parsing (because quick-and-dirty-hacks are how we roll) from the example I’ve put together at this repo:

var candidateProcMountEntries = procMounts.Split('\n', '\r').ToList();
candidateProcMountEntries.RemoveAll(s => s.IndexOf("storage", StringComparison.OrdinalIgnoreCase) < 0);
var bestCandidate = candidateProcMountEntries
  .FirstOrDefault(s => s.IndexOf("ext", StringComparison.OrdinalIgnoreCase) >= 0
                       && s.IndexOf("sd", StringComparison.OrdinalIgnoreCase) >= 0
                       && s.IndexOf("vfat", StringComparison.OrdinalIgnoreCase) >= 0);

// e.g. /dev/block/vold/179:9 /storage/extSdCard vfat rw,dirsync,nosuid, blah
if (!string.IsNullOrWhiteSpace(bestCandidate))
{
  var sdCardEntries = bestCandidate.Split(' ');
  sdCardEntry = sdCardEntries.FirstOrDefault(s => s.IndexOf("/storage/", System.StringComparison.OrdinalIgnoreCase) >= 0);
  return !string.IsNullOrWhiteSpace(sdCardEntry) ? string.Format("{0}", sdCardEntry) : string.Empty;
}

…we end up with /storage/extSdCard.

It’s quite likely that this may not be quite good enough, because the manufacturers of an HTC or Xiaomi phone might have called it something different (for example if they’d mounted it at /storage/bob we’d be pretty much screwed). But that being said, they can’t be too free-spirited with it, or the Android OS wouldn’t be able to identify it either, and you wouldn’t be able to copy your music or movies to it, and then play them.

OK, so now we’ve established how to locate the SD card (assuming we have one plugged in!).

Can We Write To It?

How do we know if it’s writeable? Well, let’s just try and write to it…

public static bool IsWriteable(string pathToTest)
{
  bool result = false;
  const string someTestText = "some test text";
  try
  {
    string testFile = string.Format("{0}/{1}.txt", pathToTest, Guid.NewGuid());
    System.IO.File.WriteAllText(testFile, someTestText);
    System.IO.File.Delete(testFile);
    result = true;
  }
  catch (Exception ex) // argh! did we do something stupid? no? then it's not writeable
  {
    Log.Error("ExternalSDStorageHelper", string.Format("Exception: {0}\r\nMessage: {1}\r\nStack Trace: {2}", ex, ex.Message, ex.StackTrace));
  }
  return result;
}

You can create directories off this path to the SD card now too, now that you have it. Exactly the same way you programmatically create them everywhere else

OK, that works. But this is on an Android 4.1.2 device (not a KitKat one). And it works all the way up to, and including, Android 4.3 (level 18) We’ll get to the KitKat part later. For now, it’s OK, we have a removable SD card, and we can write to it. Hooray!

How Much Space Have We Got?

We’re not quite done yet though. The standard Xamarin/Android calls for determining how much total/available/usable/free space is available are limited to only those mount points that Android would let you access. Like the inappropriately and stupidly named ExternalStorageDirectory, which isn’t what we thought it was, and isn’t where we want.

Back to Linux we go. There’s a statvfs() call we can make to get some basic info about the mounted file system, and luckily for us, there’s a handy wrapper object called Android.OS.StatFS, which does all that heavy lifting for us. Makes it really easy to get to!

I’m interested in total space, available space and free space (the last two aren’t always interchangeable), so I made an object called FileSystemBlockInfo, which I’ll be using to store these bits of information

  public class FileSystemBlockInfo
  {
    /// <summary>
    /// The path you asked to check file allocation blocks for
    /// </summary>
    public string Path { get; set; }

    /// <summary>
    /// The file system block size, in bytes, for the given path
    /// </summary>
    public double BlockSizeBytes { get; set; }

    /// <summary>
    /// Total size of the file system at the given path
    /// </summary>
    public double TotalSizeBytes { get; set; }

    /// <summary>
    /// Available size of the file system at the given path
    /// </summary>
    public double AvailableSizeBytes { get; set; }

    /// <summary>
    /// Total free size of the file system at the given path
    /// </summary>
    public double FreeSizeBytes { get; set; }
  }

Nothing special. Moving on…

Using the StatFS object in ExternalSdStorageHelper.GetFileSystemBlockInfo()

    public static FileSystemBlockInfo GetFileSystemBlockInfo(string path)
    {
      var statFs = new StatFs(path);
      var fsbi = new FileSystemBlockInfo();
      if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.JellyBeanMr2)
      {
        fsbi.Path = path;
        fsbi.BlockSizeBytes = statFs.BlockSizeLong;
        fsbi.TotalSizeBytes = statFs.BlockCountLong*statFs.BlockSizeLong;
        fsbi.AvailableSizeBytes = statFs.AvailableBlocksLong*statFs.BlockSizeLong;
        fsbi.FreeSizeBytes = statFs.FreeBlocksLong*statFs.BlockSizeLong;
      }
      else // this was deprecated in API level 18 (Android 4.3), so if your device is below level 18, this is what will be used instead.
      {
        fsbi.Path = path;
        // you may want to disable warning about obsoletes, earlier versions of Android are using the deprecated versions
        fsbi.BlockSizeBytes = (long)statFs.BlockSize;
        fsbi.TotalSizeBytes = (long)statFs.BlockCount * (long)statFs.BlockSize;
        fsbi.FreeSizeBytes = (long)statFs.FreeBlocks * (long)statFs.BlockSize;
        fsbi.AvailableSizeBytes = (long) statFs.AvailableBlocks * (long)statFs.BlockSize;
      }
      return fsbi;
    }

We have to do the check for the Android version, because the OS calls being made have been deprecated for older versions. The ‘old style’, pre Android level 18 didn’t use the Long suffixes, so if you try and call use those on anything below Android 4.3, it’ll crash on you, telling you that that those methods are unavailable. Viva fragmentation!

And that gives us the info we need to be able to track how much space we have, and have available to play with.

The example solution in this repo ties this all together, with a simple UI to show the mount point of a removable SD card (as pertains to my SGS I, SGS II and a couple of other devices I’ve tried).

screenshot

I hope it’s of use to someone. And if you find any funky-named removable SD card mounts which the hacky parsing we did above wouldn’t have caught, please add it, and send through pull request! :)

Google: y u no allow access to sd card?!

Google Y U No Allow Access To SD Cards in KitKat?!

One caveat: Android KitKat (aka 4.4, aka level 19, and presumably higher) won’t let you write to wherever you like on this removable disk. Ironically, earlier, more primitive versions of Android do.

I’ve written a new blog post about this and how to get to the SD card, so take a look if you’re interested.

The general consensus seems to be that this was done for security reasons. Basically to stop rogue apps from writing to places they don’t own. There are some patches and apps which can fix this for you, but they all appear to require rooting your device. KitKat doesn’t block you completely from writing to the SD card, but it means you have to make sure your data is stored somewhere “private” to the app. This will be someplace like [externalSdpath]/Android/data/your.package.name/files/yourFolderName.

There’s some more information on Google, some apps here and here which may help you figure out what you’ll need to do to make it work. And there’s some explanations of what other app developers have done to ‘fix’ their apps over here at BeyondPod and doubleTwist.

Note: I’m not affiliated with any of these devs or apps

Unfortunately, it looks like your users will now also lose whatever data your app has generated, if you put your data in these sandboxed folders, and then they uninstall the app, the data gets removed as well. Though apparently app upgrades are fine.

Opinions differ as well, since it seems that you may be able to create directories elsewhere too, with the proviso that your app is the only one that can write to those directories… I guess I need to do a little more testing…

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