12
3

More than 3 years have passed since last update.

WebエンジニアがAndroidの改修をやってみた〜画像を保存したいだけなのに〜

Last updated at Posted at 2021-06-18

普段はLaravel/Vueを使ってるWebエンジニアです。
そんな自分にAndroidの改修依頼が舞い込んできました。

依頼タスク

「このAndroidアプリ、非推奨メソッド使ってるんだけどサポートいつ切れるかわからないから新しいのに変えたいんだよねー」
「なる。りょ。」

最初はメソッド置き換えればいいんでしょ?なんて軽く考えていた自分・・・

問題となった非推奨メソッド

getExternalStorageDirectory()
確かにThis method was deprecated in API level 29.って書いてる。
どうやら外部ストレージへのパスを取得する関数らしい。

今回改修するアプリは素材画像ダウンロードの際にこのメソッドが使われていた。

代替手段1

一応 android:requestLegacyExternalStorage="true" の設定を追加することで
延命措置をとることはできるらしい。

AndroidManifest.xml
<manifest>
  <application android:requestLegacyExternalStorage="true">
  </application>
</manifest>

ただこれもいつまで使えるかわからないし、根本的な解決になっていないのでナシと判断。

代替手段2

似たような代わりのメソッドとして、以下のような関数がある。
getExternalFilesDir(String)
ただし、これはアプリ固有の領域へのファイルパスで、アプリが削除されるとアプリ固有のフォルダも削除される。

アプリを削除してもダウンロードした素材は残っていて欲しい。
そのためこの代替手段はナシと判断。

代替手段3

MediaStoreというライブラリを利用する。
これを利用するとアプリを削除してもダウンロードしたデータが消されることはない。

ただこのMediaStore、「ユーザーのプライバシーを守るため」という理由でパスを開発者に意識させずにデータ保存する代物らしい。

・・・既存コード、めちゃくちゃパスを取得する前提で書いてる・・・。
ダウンロード処理部分まるっと改修やん。

まぁでもデータ保存するだけだし大丈夫っしょ。
とりあえず公式のMediaStoreのやり方コピって真似して書いて...ん?

ContentResolver resolver = getContext().getContentResolver();

Uri imageCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);

ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "Test.gif");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/gif");
values.put(MediaStore.Images.Media.IS_PENDING, 1);

Uri item = resolver.insert(imageCollection, values);

try (ParcelFileDescriptor pfd =
        resolver.openFileDescriptor(item, "w", null)) {
    // Write data into the pending file.
}

values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(item, values, null, null);

立ちはだかる壁

// Write data into the pending file.

ってなんやねん!!
ここに何をどうやって書けばいいの?

ちなみに上記のコードを実行すると0byteの画像データが保存される。

調べると参考になりそうなKotlinの記事が出てきた。

うーんわからん

仕方ないのでわからん部分を一個ずつ潰していく

そもそもAndroidのストレージってどうなってるの?

公式の説明に、外部ストレージ/内部ストレージ/永続ストレージ/共有ストレージなど色々出てきてこんがらがってきたので記事をまとめながら理解した。

inputStream/outputStreamってなに?

色々見たけどこの記事が一番分かりやすかった。
ストリームはデータをバイト単位で扱い、バイナリデータの読み書きで使われる。
入力ストリームを利用してデータの読み出しを行ない、出力ストリームを利用してデータの書き込みを行なう。
いろんな派生クラスがあるが、基本はinputStream/outputStreamクラスを継承しているので変換可能。

Androidにおける画像の取扱い方は?

調べてるとBitmapやらUriやら色々なクラスが出てきたのでそれぞれの特色を理解した。

FileDescriptorってなに?

意味としてはファイルを操作する際、対象のファイルを識別するために割り当てられる番号のことらしい。
そもそもこれってContentResolver resolverから取得したものだから、こいつについて理解しないといけない。

FileDescriptorの具体的な使い方は以下のサイトを参考にした。

ContentResolverってなに?

  • コンテンツプロパイダ...データを扱うためのAndroidの標準的なインターフェース
  • コンテンツリゾルバ...コンテンツプロバイダのデータにアクセスするためのもの

Kotlinについて

Kotlinの文法自体はJavaScriptのES6的な書き方でなんとなくわかりやすい。
しかもJavaと互換性が高く、一部のみKotlin採用とかもできるらしい。
改修しながらちょっとずつKotlin取り入れていくのもアリかも?

画像データの書き込み

これをこうして既存コードも参考にして・・・

try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(item, "w", null)) {
  FileDescriptor fd = pfd.getFileDescriptor();

  InputStream input = //保存したいデータ
  FileOutputStream output = new FileOutputStream(fd);

  BufferedInputStream bis = new BufferedInputStream(input);
  BufferedOutputStream bos = new BufferedOutputStream(output);

  final int BUFF_SIZE = 1024;
  byte[] buffer = new byte[BUFF_SIZE];
  int len = 0;
  while ((len = bis.read(buffer, 0, BUFF_SIZE)) >= 0) {
    bos.write(buffer, 0, len);
  }

  bos.flush();
  bis.close();
  bos.close();
}

画像保存できた!!

・・・と思ったらファイルの実態はzipっぽい。

今回のケースではinputStreamにzipデータで渡されてたので解凍してから画像データを取り出さないといけない。

これにともないzipに複数の画像ファイルが保存されてる場合にも対応した。
簡単にいうと、inputをZipInputStreamに変換して受け取り、getNextEntry()で中身のデータを一つずつ取り出して保存処理を行なった。
力尽きたのでこの辺の処理の話は割愛。

やりたいこと達成!

最後に

このタスクやってる時に見ためちゃくちゃ共感したツイート

でもこの対応したことによってだいぶAndroidが分かるようになってきた。
今度からAndroidできますって言おう(調子に乗るな)

それにしてもWebと比べてAndroidの情報って少なくてしんどい。
Javaの低レイヤー的な操作苦手だし・・・。
やっぱもうやりたくないからできませんって言おう(おい)

記事に間違いなどあったらご指摘ください〜
おしまい

12
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
3