仕事でハマった事例があって、同じことで悩んでいる人がいそうだと感じたので書き記しておこうと思う。
最初に、事象を説明しておこう。ここで意味が分からない人は、この記事を読んでも意味が無いと思う。
・Cordova / Phonegap でスマホ用のコンテンツを作っている。
・<input type="file"> を使ってスマホ内のファイルを選択し、Javascript でその内容を得る。
・iOS とブラウザでは動くのだけど、Android では環境により、ファイル選択できたり、できなかったりする。
まずは、上にある事象を「乗り越えた」。
上の事象で困っている人もいるかもしれないから後で解説するけど、乗り越えたうえでさらに問題がある。
・ファイル選択は出来たが、得られるものが違う。
iOS と同じ動作にさせるのに、どうしたらいいのかわからない。
原因は、Android ではセキュリティの問題で WebView からローカルファイルが扱えないこと。
ただし、これは途中のバージョンからの仕様なので、「過去との互換性」を気にして、扱えるようにしてある端末もある。
だから、端末によっては動くのに、別の端末では動かないことがある。
原因が特定しづらい、最初のハマりポイントだ。
この問題を「乗り越える」には、Android では file chooser の plugin を使う。
プラグイン自体は多くの人が作っていて、ほぼ同じ動作なので好きなものを使えばよい。
僕はdon / cordova-filechooserを使った。
他のものに比べ、制御用のオプション指定が無い。制御が不要であれば軽量で使いやすい。
HTML の Content-Security-Policy に file: blob: cdvfile: を追加すること。
そうしないと、Javascript からローカルファイルを扱えない。
file: はローカルファイルを意味する。blob: はローカルファイルの中身をデータとして扱う指定だ。
cdvfile: は、Cordova 独自の file 拡張機能を扱うことを意味する。
さて、これでファイル選択ができるようになっても、得られるものが HTML5 で得られるものと違う。
この違いを乗り越えるのが結構ややこしいことになる。
HTML5 の input file で得られるのは、Javascript の「file object」だ。
ファイル名やサイズ、更新日などと共に、ファイルの中身を読み取れるようになっている。
android の fileChooser で得られるのは、ファイルの「コルドバ拡張の」パスだ。
これは cdvfile: スキームで表現される。
特殊なスキームだけど、気にすることはない。
例えば、img タグにこのスキームを渡せば、そのまま画像ファイルを表示できる。
まぁ、ファイルを選択するという目的にはかなっているのだけど、得られるものが違う。
HTML5 / iOS で動くプログラムでは、file object を得られているのに、cordova では file path しか得られないのだ。
僕の場合、file object をそのまま受け渡すライブラリを使っていたので、できれば file object が欲しかった。
window.resolveLocalFileSystemURL を使うと、ファイルのパス名を fileEntry オブジェクトに変換できる。
fileEntry には file メソッドがあり、file object を取り出せる。
これで目的の file object を得ることができた。
// iOS / HTML5 の file タグ
$("input:[type=file]").on("change",function(e){
getFile(e.target.files[0]);
});
// 上と等価な Android の fileChooser
function fail(){alert("error");};
fileChooser.open(function(path){
window.resolveLocalFileSystemURL(path,function(fileEntry){
fileEntry.file(function(file){
getFile(file);
})
},fail);
},fail);
getFile に file object が渡されればよい、というプログラムであれば、こんな感じかな。
ただし、まだ問題がある。
cordova の File API 関連は拡張されている。
というのも、アプリを作成するのであれば、自由なファイルアクセスや、ファイル書き込みも欲しいから。
Javascript では、セキュリティのためにファイルアクセスは基本的に禁じられているし、書き込みもできない。
これらを実現するために、cordova では File API 関数群は全部互換品に置き換えられている。
しかし、ここで問題が生じる。
Android で得られる file object は、cordova による「互換品」であり、HTML 5 の file object とは違うものだ。
HTML5 には、Blob がある。
Blob っていうのは捉えどころのない塊の意味合いがあるのだけど、プログラムの用語としては binary の塊のことだ。
旧来の Javascript では、バイナリは「文字列」として扱ったのだけど、HTML5 ではより効率よく扱える Blob という概念ができた。
file object は blob object から派生したものになっていて、バイナリの塊と共に、ファイル名やサイズ、更新日付などを持っている。
そして、blob も file も、「とらえどころのない塊」なので、そのままではデータとして扱えない。
必ず、データを取り出したり変換したりするメソッドや関数を使用して扱う。
さて、cordova では、こうした「関数」レベルで互換性を保っている。
ところが、僕が使っていたライブラリでは、blob を渡されたかどうかを、データの型を確認して確かめていた。
file は blob から派生したものだから、blob として認識される。でも、それ以外のものを渡されると、エラー終了する。
まぁ、当然の処理なのだけど、cordova の file object は、HTML の blob の派生ではなく、独自の構造体に過ぎなかった。
だから、cordova の file object が得られたからと言って、iOS の file object と同じように処理を行っても、動かない。
ここは非常に厄介なハマりポイントだった。
結局のところ、Android では「そのライブラリを使わない」という決断をせざるを得なかった。
ライブラリを改造しても良かったのだけど、ある程度の規模のものを改造するのであれば、改めて「動作検証」をしないといけないし、今後ライブラリがバージョンアップされるたびにそれを繰り返す必要がある。
そうした手間はかけたくなかったんだ。
幸い、そのライブラリは「各実行環境での差異」を吸収するためのものだったので、Android だけなら独自に書いてもそれほど難しくはなかった。
別の手段として、自前で blob を用意してライブラリに渡す、という方法も考えた。
file object は当然データを得られる。これは cordova でも一緒だし、そうでないと意味がない。
そして、データがあれば blob を生成できる。
ところが、調べてみるとこちらにも問題があった。
blob は元々「ファイルや、ネットから得たデータなどを上手に扱う」ためのもので、入力を前提としていた。
生成する方法も後から策定されたのだけど、ドラフト時点と正式決定時点で方法が違っている。
このため、Android のバージョンにより、blob を生成する方法に差異がある。
Android 4 までと、5以降で実装されている方法も違う…「らしい」。
らしいというのは、僕自身が実機確認できていないから。
確認できない不安があるのであれば、比較的古くて安定動作が望める仕様を使ってプログラムを作るしかない。
最終結論としては、Android の file 周りは、iOS / HTML5 と「共通化しづらい」ことになる。
iOS や HTML 5 では、ファイルを選択した結果 file object が得られる。
しかし Android では file path しか得られず、fileEntry に変換したうえで file object を得ないといけない。
そうやって得た object は、できるだけ互換を保つ努力はされているが、完全互換ではない。
file を受け取るライブラリ等を使用している場合、そのライブラリは動かないかもしれない。
Android で blob を作り出せれば問題は解決しそうなのだが、Android のバージョン間で blob の生成方法には違いがある。
そのため、この方法を使うのは注意が必要だ。
しかし、問題は所在が分かれば解決できる。
解決方法は、そのプログラムごとに違うだろうが、ここに書いたことがヒントになれば幸いだと思う。
同じテーマの日記(最近の一覧)
別年同日の日記
申し訳ありませんが、現在意見投稿をできない状態にしています。 |