2019/02/06
sponsored link
罠と化したgetExternalStorageDirectory()
androidアプリを作っていて、SDカードにデータを保存する機能を実装したい時の話。
androidは全て把握できないくらい様々な機種があり、残念なことにSDカードのマウント先(ディレクトリのpath)は、機種によって微妙な差があるらしいです。なので、プログラムの中で
File file = new File("mnt/sdcard", "data");
みたいにそのpathを絶対的に指定することはできません。なんとかしてその機種固有のpathを取得する必要があります。
その為に用意されていたのが、
Environment.getExternalStorageDirectory()
というクラスメソッドです。このメソッドを使えば、外部ストレージ(SDカード)のマウント先を簡単に取得することができていました。
しかし、いつの頃からか、androidのとんでもない仕様変更により、この、名前からしていかにも外部ストレージのディレクトリをゲットしてくれそうな「Environment.getExternalStorageDirectory()」が、とんでもないトラップになってしまいました。それも結構悪質な。
どんな悪質な罠か、結論からと申しますと、このメソッドで取得できるのは外部ストレージ(SDカード)のpathではなく、内部ストレージのpathなんです。
androidの内部ストレージというのは、なぜかディレクトリ名が「../sdcard」となっています。外部がsdcardなのは分かるけど、なんで内部がsdcardなん?と思わずにいられないのですが、一体どういうことなんでしょう??
しかも、外部ストレージのディレクトリを取得するはずの「Environment.getExternalStorageDirectory()」で、内部ストレージのpath「../sdcard」を取得してしまうんです。なので、もちろん「Environment.getExternalStorageDirectory()」で取得したディレクトリにファイルを作ってデータを保存しても、SDカードには一切何も書き込まれません。トラップ以外の何者でもないですよね・・。
そこで、本物のSDカードのpathを取得するにはどうすればいいのかを、解説しまっす。
解説なんて言うと偉そうですが、実は私自身このトラップにハマって困っていたところ、「外部ストレージのパスを取得する(Android 2.2~?) – 戌印-INUJIRUSHI- 」こちらのサイトの心優しい管理人様にいろいろ教えて頂いて(その節はありがとうございました)、解決することができたのでその方法を防備録として書いておきます。
システム設定ファイル(vold.fstab)をまさぐる
androidには、システム設定ファイル(/system/etc/vold.fstab)というのがあります。そのファイルの中に、(その機種の)SDカードのマウント先が書いてあるようなので、そこからゲットします。
具体的にシステム設定ファイルの中身がどうなっているか、ご自分のandroidの中の/system/etc/vold.fstabをテキストエディタで開いて見てもらえるとわかると思いますが、一応、僕の持っているHTC J oneの中にあるシステム設定ファイルの中身を載せておきます。
## Vold 2.0 fstab for cardhu ####################### ## Regular device mount ## ## Format: dev_mount
こんな風になっています。この中の赤字の部分「/storage/ext_sd」が、取得するべきSDカードのマウント先(path)です。
「dev_mount」で始まる行の半角スペース区切りで3番目のやつが、マウント先になるようです。ただし、見ての通り、sdcardのマウント先だけでなく、usbのマウント先?もあります。しかもこのシステム設定ファイルに載っている情報は機種によって違うので、他の機種はどんな情報が載っているのかよく分かりません。が、「dev_mount」(機種によっては「fuse_mount」)で始まる行の半角スペース区切りで3番目のやつがpathを示すというのはどうやら共通の仕様のようです。
その中から、sdcardのpathだけを取得する為に、以下のメソッド(2つ)を定義します。2つ目のisMounted()は、1つ目のgetMount_sd()内で使っています。
// SDカードのマウント先をゲットするメソッド @TargetApi(9) private String getMount_sd() { List<String> mountList = new ArrayList<String>(); String mount_sdcard = null; Scanner scanner = null; try { // システム設定ファイルにアクセス File vold_fstab = new File("/system/etc/vold.fstab"); scanner = new Scanner(new FileInputStream(vold_fstab)); // 一行ずつ読み込む while (scanner.hasNextLine()) { String line = scanner.nextLine(); // dev_mountまたはfuse_mountで始まる行の if (line.startsWith("dev_mount") || line.startsWith("fuse_mount")) { // 半角スペースではなくタブで区切られている機種もあるらしいので修正して // 半角スペース区切り3つめ(path)を取得 String path = line.replaceAll("\t", " ").split(" ")[2]; // 取得したpathを重複しないようにリストに登録 if (!mountList.contains(path)){ mountList.add(path); } } } } catch (FileNotFoundException e) { throw new RuntimeException(e); } finally { if (scanner != null) { scanner.close(); } } // Environment.isExternalStorageRemovable()はGINGERBREAD以降しか使えない if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD){ // getExternalStorageDirectory()が罠であれば、そのpathをリストから除外 if (!Environment.isExternalStorageRemovable()) { // 注1 mountList.remove(Environment.getExternalStorageDirectory().getPath()); } } // マウントされていないpathは除外 for (int i = 0; i < mountList.size(); i++) { if (!isMounted(mountList.get(i))){ mountList.remove(i--); } } // 除外されずに残ったものがSDカードのマウント先 if(mountList.size() > 0){ mount_sdcard = mountList.get(0); } // マウント先をreturn(全て除外された場合はnullをreturn) return mount_sdcard; } // 引数に渡したpathがマウントされているかどうかチェックするメソッド public boolean isMounted(String path) { boolean isMounted = false; Scanner scanner = null; try { // マウントポイントを取得する File mounts = new File("/proc/mounts"); // 注2 scanner = new Scanner(new FileInputStream(mounts)); // マウントポイントに該当するパスがあるかチェックする while (scanner.hasNextLine()) { if (scanner.nextLine().contains(path)) { // 該当するパスがあればマウントされているってこと isMounted = true; break; } } } catch (FileNotFoundException e) { throw new RuntimeException(e); } finally { if (scanner != null) { scanner.close(); } } // マウント状態をreturn return isMounted; }
※コメントが多すぎて見づらかったらごめんなさい。
注1)isExternalStorageRemovable()は、getExternalStorageDirectory()で取得できるpathが、取り外し可能な外部ストレージ(つまりSDカード)ならばtrueを返し、内部ストレージ(上記の罠に嵌っている状態)ならば、falseを返します。
注2)/proc/mountsの中に現在マウント中のストレージ?のpathが書いてあります。やたらズラズラと書いてあるので正直、何を書いてあるのか理解不能でしたが、確かにSDカードをマウントしている状態だとそのpathが書いてあり、マウントしていない状態で見てみると書いていませんでした。
SDカードのpathを取得する
上記の2つのメソッドを定義しておけば、
if(getMount_sd() != null){ File file = new File(getMount_sd(), "data"); // SDカードに保存する処理 }else{ // SDカードのマウント先が見つからない場合の処理 }
こんな風にして、SDカード内のファイルにアクセスできると思います。もし、SDカードが刺さっていなかったらgetMount_sd( )はnullを返すので、「SDカードが見つかりません」とかのメッセージをToastで出すようにしたらいいと思います。
ただし、このシステム設定ファイルなんですが、GalaxyNexusや一部のカスタムROMには存在しないそうです。どないせー言うねん。。けど、 – 戌印-INUJIRUSHI- さん曰く、この方法が一番多くの機種に対して使えるらしいので、この方法でダメな機種は、僕は切り捨てることにしましたw
androidのそういうとこ困る・・。
今だけ→転職できなければ全額返金の「エンジニア転職保証コース」
絶対エンジニアになる!→テックエキスパート
フリーランスエンジニアの収入例を見てみる→レバテックフリーランス
コメント
はじめまして、外部SDの読み込みについて調べていたらこのサイトを見つけました!
かなり参考になりました!ありがとうございます!
一つ。つたない追加ですが、自分の手元にあった「SC-01(GaraxyTab初代)」では、「/mnt/sdcard/external_sd」ではなく「/mnt/sdcard」が出てきました。
原因はシステム設定ファイルに両方書き込まれていることが原因で、リストの最初を拾ったためこの結果になったようです(つまり3番目ではなく4番目に外部SDのパスがあるようです)なので、ディレクトリパスをStringに入れる前に、「/mnt/sdcard」のパスをリストから除外する処理を追加して回避しました。
役に立つか判りませんが一応。
今後も参考にさせて頂きます!
多分失礼しました。
by ミケ猫 2013/10/25 12:04
ミケ猫さん、コメントありがとうございます。
お役に立ててよかったです。
そして貴重な情報ありがとうございます。
実は私もいろいろ困ってまして・・。
GaraxyTab初代では、システム設定ファイルに、
『dev_mount sdcard /mnt/sdcard /mnt/sdcard/external_sd』
って言う風に書いてるってことですかね?
by nobuo 2013/10/25 12:25
[…] 「スマホのSDカードパスを取得、機種依存対策」という情報によりますと、本当の外部にあるストレージ、SDカードのパスを取得する Android API は用意されていないと書いてあります。また、「SDカードのpathを取得する方法」には、SDカードを取得する方法をJavaで書いた複雑なコードが紹介されていますが、素人には簡単に利用することはできません。 […]
by [15] SDカードのpath | 老輩のAndroid Studio奮闘記 2016/04/25 19:35
[…] http://nobuo-create.net/sdcard-2/ こういうサイトみても実は私には解決できなかった・・・ […]
by fireタブレット内のClipboxのデータを外部ストレージのsdカードに保存する方法 | 自己投資図書館 2016/05/07 23:29
本当にありがとうございました
by 秋男 2019/02/15 17:54
解決することができたのでその方法を防備録として書いておきます。
↓
備忘録
by 通りすがり 2023/11/05 13:49