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だけを適用範囲としてみます。
ではどのように検出するのか?以下の手順を踏みます。
- 環境変数の一覧を取得する
- 環境変数の中から、EXTERNALで始まる環境変数を抽出する
- Environment.getExternalStorageDirectory().getPath()で外部ストレージのパスを取得する
- getExternalStorageDirectory()で取得したパスと違うパスが2で抽出したパスに存在するか確認する
- もしなければ、その端末には二つのドライブは存在しないとみなす
- もしあれば、そのパスが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コマンドです。AndroidもUnix (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) {} } }
これでいけそうです♪