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

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

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

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

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

[getExternalStorageDirectory()](https://developer.android.com/reference/android/os/Environment#getExternalStorageDirectory())
確かに`This method was deprecated in API level 29.`って書いてる。
どうやら外部ストレージへのパスを取得する関数らしい。

https://nagare-it.com/115/

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

## 代替手段1

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

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

https://stackoverflow.com/questions/57116335/environment-getexternalstoragedirectory-deprecated-in-api-level-29-java

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

## 代替手段2
似たような代わりのメソッドとして、以下のような関数がある。
[getExternalFilesDir(String)](https://developer.android.com/reference/android/content/Context?hl=ja#getExternalFilesDir(java.lang.String))
ただし、これはアプリ固有の領域へのファイルパスで、アプリが削除されるとアプリ固有のフォルダも削除される。

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

## 代替手段3
[MediaStore](https://developer.android.com/reference/android/provider/MediaStore?hl=ja)というライブラリを利用する。
これを利用するとアプリを削除してもダウンロードしたデータが消されることはない。

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

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

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

https://developer.android.com/training/data-storage/shared/media?hl=ja#add-item

```java
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);
```

## 立ちはだかる壁

```java
// Write data into the pending file.
```

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140953/125b5f6e-1de9-9d06-e722-c0f474f01ade.png" width=50%>

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

https://codechacha.com/ja/android-mediastore-insert-media-files/

**うーんわからん**

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

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

https://qiita.com/mgmgmogumi/items/fa2bba333899e39e4ac3

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

### inputStream/outputStreamってなに？

https://zenn.dev/kawakawaryuryu/articles/8924849b88590cda4e22

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

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

https://qiita.com/wakamesoba98/items/98b79bdfde19612d12b0

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

### FileDescriptorってなに？

https://e-words.jp/w/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%82%BF.html

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

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

https://www.codota.com/code/java/methods/android.content.ContentResolver/openFileDescriptor

### ContentResolverってなに？

https://developer.android.com/guide/topics/providers/content-provider-basics?hl=ja

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

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


## 画像データの書き込み

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

```java
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();
}
```

画像保存できた！！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140953/dc9973cf-eb95-52df-ad8d-dddec54747d1.png" width=50%>

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

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

https://flytech.work/blog/14899/

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140953/0a55162b-db88-c8ac-914b-da802ad3c37b.png" width=50%>

やりたいこと達成！

## 最後に

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

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">知らん分野学ぼうとすると、「&lt;知らん言葉&gt;は&lt;知らん言葉&gt;を&lt;知らん言葉&gt;するために作られた&lt;知らん言葉&gt;です」みたいになっていて泣いちゃうね</p>&mdash; きひろちゃん（9歳幼女） (@aki33524) <a href="https://twitter.com/aki33524/status/1404691159859953664?ref_src=twsrc%5Etfw">June 15, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

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

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

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