Primary Memory vs Removable Memory

先日会社でこんなことが話題に上がりました。

たとえばGalaxy Sはユーザが利用可能なメモリとして、内蔵メモリと外付けSDカードスロットが付いています。このような状況はGalaxy Sだけでなく、いくつかのAndroid端末にも当てはまります。メモリをお手軽に拡張出来るので、とっても便利です。

そこで話題になったのが、こちら。

この二つのメモリのうち、外付けSDカードにアプリケーションのデータを出力することが出来ないか?


この問題、簡単そうで実は難しいのです。難しさの想像が付かない方はこちらをご覧ください。


【教えてください】Galaxy SのSDカード保存先について
外部メモリの取り扱い
SDカードを示すディレクトリ


何が難しいって、SDカードのパス (マウントポイント) がデバイスによって異なる点です。たとえば、こんな感じ。

バイス SDカードのパス
Galaxy S /mnt/sdcard/external_sd
SMT-i9100 /mnt/sdcard/external_sd
IdeaPad Tablet A1 /mnt/sdcard/removable_sd
Sony Tablet S /mnt/sdcard2
XOOM /mnt/sdcard-ext


なんか全然違うのね。このパスを何とかして動的に導き出す、というのがまず第一のお題目です。

そんなの無理だよ〜、と思いながらこれらのパスを眺めていると、二つのグループに分けることが出来ることに気付きます。

  • グループ1

  • グループ2


グループ1は内蔵メモリのフォルダとしてSDカードの中身を見ることが出きるようになっています。これ、結構面白くて、SDカードがない場合、対応するフォルダはあるんですが、中身は空という状態になっています。IdeaPadで試したところ、驚くことにremovable_sdフォルダ内にファイルを作ることが出来ました。一方、SDカードがささっている場合はあたかも内蔵メモリの中の1フォルダのように見ることが出来ます。これによりユーザはあたかも一つのメモリを見ていることになるのです。


グループ2はこれとは対称的に内蔵メモリとSDカードをまったく別物 (同等) とみなしています。
また、全部調べているわけではありませんが、Sony Tablet Sに関してはどうもSDカードの扱いが特殊になっているようです。なぜならSDカードを抜き差ししても


ACTION_MEDIA_MOUNTED
ACTION_MEDIA_UNMOUNTED


などというAndroid標準のIntentが飛んでこないからです。その代わりSony独自のIntentが飛んでいるのがLogCatから見て取れます。
一方IdeaPadでは上記のようなAndroid標準のIntentが飛び交っていました。


このうちのグループ2のSDカードのパスを特定するのがとても難しいのです。ここは欲張らず、グループ1だけを適用範囲としてみます。

ではどのように検出するのか?以下の手順を踏みます。

  1. 環境変数の一覧を取得する
  2. 環境変数の中から、EXTERNALで始まる環境変数を抽出する
  3. Environment.getExternalStorageDirectory().getPath()で外部ストレージのパスを取得する
  4. getExternalStorageDirectory()で取得したパスと違うパスが2で抽出したパスに存在するか確認する
  5. もしなければ、その端末には二つのドライブは存在しないとみなす
  6. もしあれば、そのパスがSDカードのマウントポイントを示すパスの候補とする


なお、環境変数の一覧はこんな感じに取得出来ます。

Map<String, String> envs = System.getenv();


ちなみにHT-03Aで試したところ、こんな環境変数がとれました。

# printenv
ANDROID_ROOT=/system
LD_LIBRARY_PATH=/system/lib
PATH=/sbin:/system/sbin:/system/bin:/system/xbin
SD_EXT_DIRECTORY=/sd-ext
ASEC_MOUNTPOINT=/mnt/asec
ANDROID_CACHE=/cache
BOOTCLASSPATH=/system/framework/core.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/services.jar
ANDROID_BOOTLOGO=1
ANDROID_ASSETS=/system/app
EXTERNAL_STORAGE=/mnt/sdcard
ANDROID_DATA=/data
TERMINFO=/system/etc/terminfo
ANDROID_PROPERTY_WORKSPACE=10,32768

※ SD_EXT_DIRECTORYが気になりますが、見て見ぬふり・・・。


これでSDカードのパスを取得することが出来そうです。
でも若干危険な匂いがします。こんなところが匂います。

SDカードのマウントポイントはEXTERNALで始まる環境変数に設定されているってホント?


なので検算的なチェックをします。SDカードが脱着された時に飛ばされるIntentを受信するのです。たとえば上でも書きましたが、こんなところ。


ACTION_MEDIA_MOUNTED
ACTION_MEDIA_UNMOUNTED


このIntentのmDataの中にマウントポイントのパスが入っているので、これを使って確認し、もし間違っていれば修正するわけです。


一つ目の難関はなんとか突破出来そうです。次の難関は、SDカードがささっているか否かを検出することです。一度SDカードの抜き差しがされれば、Intentを受信してマウント状態を確認することが出来ますが、SDカードの抜き差しがない状態ではどうでしょうか?


ここではかなり強引にも思えますが、信頼性は非常に高い方法をとります。それはmountコマンドを使う方法です。mountコマンドは言わずと知れたUnixコマンドです。AndroidUnix (Linux) ですから一応mountコマンドを持っています (断言は出来ませんが) 。そこでmountコマンドの出力結果を見てみましょう。

# mount
rootfs on / type rootfs (ro,relatime)
tmpfs on /dev type tmpfs (rw,relatime,mode=755)
devpts on /dev/pts type devpts (rw,relatime,mode=600)
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
none on /acct type cgroup (rw,relatime,cpuacct)
tmpfs on /mnt/asec type tmpfs (rw,relatime,mode=755,gid=1000)
none on /dev/cpuctl type cgroup (rw,relatime,cpu)
/dev/block/mtdblock3 on /system type yaffs2 (ro,relatime)
/dev/block/mtdblock5 on /data type yaffs2 (rw,nosuid,nodev,relatime)
/dev/block/loop0 on /system/xbin type squashfs (ro,relatime)
/dev/block/mtdblock4 on /cache type yaffs2 (rw,nosuid,nodev,relatime)
/dev/block/vold/179:1 on /mnt/sdcard type vfat (rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0702,dmask=0702,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro)
/dev/block/vold/179:1 on /mnt/secure/asec type vfat (rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0702,dmask=0702,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro)
tmpfs on /mnt/sdcard/.android_secure type tmpfs (ro,relatime,size=0k,mode=000)


よく見て下さい。「/mnt/sdcard」というパスがあるのにお気づきでしょうか?
mountコマンドはマウントポイントをリストアップすることが出来ます。これは使わない手はないでしょう。


ちなみにAndroid上からコマンドを実行し、その出力を得る方法はこちら。

// externalPathにSDカードマウントポイントのパスが格納されているとします
BufferedReader reader = null;
try {
    Process process = Runtime.getRuntime().exec("mount");
    reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

    String line;
    while((line = reader.readLine()) != null) {
        if(line.contains(externalPath)) {
            // SDカードがマウントされてる♪
            break;
        }
    }

    process.waitFor();

} catch (IOException e) {
    throw new RuntimeException(e);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
} finally {
    if(reader != null) {
        try {
            reader.close();
        catch (IOException e) {}
    }
}


これでいけそうです♪