ネットワーク上から保存した JPG 画像の画質を比較 & Android の DownloadManager を使用して罠にハマってみた件

# 第一部 〜ネットワーク上から保存した JPG 画像の画質を比較〜

## 概要

Android アプリ開発中に、ネットワーク上から保存した JPG 画像の画質に微妙な差があることに気付いたので、同一の JPG 画像をダウンロードしてサイズの比較を行ってみます。

## 手順

:one: [Google+](https://plus.google.com/) に検証用の JPG 画像を添付して投稿
:two: 投稿した JPG 画像を自作の Android アプリから URL を指定してダウンロード
:three: 端末内に保存された JPG 画像のサイズを確認します

## [Google+](https://plus.google.com/) に投稿する写真（元画像）

![オリジナル.jpg](https://qiita-image-store.s3.amazonaws.com/0/6881/3e69f4f1-bf13-c091-1bd7-bd517687224d.jpeg)

| | 値 |
|:------|:------------:|
| サイズ | 172,109 バイト（176KB） |
| 大きさ | 458 × 458 |

検証では大きさの 458 x 458 が変わることはありませんでしたが、サイズの値は変化しました。

## 画像の取得方法

`java.net.URL` を用いて画像を取得します（「[Android Studio : インターネット上の画像を取得して、Bitmap か Drawable で ImageView に表示する](http://logicalerror.seesaa.net/article/419965567.html)」を参考にしました）。

## 検証パターン

取得した画像を `InputStream` と [`Bitmap`](https://developer.android.com/reference/android/graphics/Bitmap.html) の状態で保存してみます。

※ `AndroidManifest.xml` に `<uses-permission android:name="android.permission.INTERNET" />` と
 `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />` を記述するのをお忘れなく

### :one: `InputStream` の状態で保存する

「[画像の保存→表示](http://d.hatena.ne.jp/tak87/20091015/1255598465)」を参考にしました。
![InputStreamから.jpg](https://qiita-image-store.s3.amazonaws.com/0/6881/87dbc6fa-95f6-63c1-f478-16f82705918e.jpeg)

| | 値 |
|:------|:------------:|
| サイズ | 172,207 バイト（176KB） |
| 大きさ | 458 × 458 |

見た感じはオリジナル画像と変わらない気がします。しかしながらサイズが微増しています :worried:

### :two: [`Bitmap`](https://developer.android.com/reference/android/graphics/Bitmap.html) の状態から保存する
「[Androidで非同期で画像をサーバからダウンロードしてSDカードに保存する](http://matty-studio.jp/post-203/)」と「[Bitmap画像をsdに保存](https://sites.google.com/a/gclue.jp/android-1000-tips/bitmap-hua-xiangwosdni-bao-cun)」を参考にしました。両方とも同じ結果になりました。

![20151024_192344.jpg](https://qiita-image-store.s3.amazonaws.com/0/6881/206e1cb3-ee4e-d778-2a7f-3d587c949b96.jpeg)

| | 値 |
|:------|:------------:|
| サイズ | 186,378 バイト（188KB） |
| 大きさ | 458 × 458 |

JPG 画像の白色部分にガサガサが見えるようになりました。そして 176 KB → 188 KB とオリジナル画像よりサイズが大きくなっています :disappointed_relieved:
`InputStream` が生データなのに対して [`Bitmap`](https://developer.android.com/reference/android/graphics/Bitmap.html) は一度加工しているため画質に差が生じています。

### :three: Android の [Google+](https://play.google.com/store/apps/details?id=com.google.android.apps.plus) アプリでダウンロードする

![AndroidのGoogle+アプリのダウンロード.jpg](https://qiita-image-store.s3.amazonaws.com/0/6881/58ae4065-4758-dea0-c3ac-9406287a2fb9.jpeg)

| | 値 |
|:------|:------------:|
| サイズ | 172,109 バイト（176KB） |
| 大きさ | 458 × 458 |

オリジナル画像と同じサイズの画像を保存することができました。一体どのようにして保存しているのでしょうか（？）

※ 現在の [Google+](https://play.google.com/store/apps/details?id=com.google.android.apps.plus) アプリからは画像を直接保存する機能が無くなっているようでした。画像詳細画面 >「シェア」ボタンをタップ >「フォトにアップロード」を選択すれば [Google フォト](https://play.google.com/store/apps/details?id=com.google.android.apps.photos) に画像を保存することができました :inbox_tray:

# 第二部 〜[DownloadManager](http://developer.android.com/intl/ja/reference/android/app/DownloadManager.html) との邂逅〜

## [この記事](https://qiita.com/84d010m08/items/541c67b1e9b8deb4051f)を公開した後

@KanaSakaguchi さんから「[DownloadManager](http://developer.android.com/intl/ja/reference/android/app/DownloadManager.html) を使用すればオリジナルと同様のサイズで画像を保存できるのではないか」という旨の[コメント](https://qiita.com/84d010m08/items/541c67b1e9b8deb4051f#comment-6f94bd840e0acb8b3786)を頂きました。

## [DownloadManager](http://developer.android.com/intl/ja/reference/android/app/DownloadManager.html) とは？

ネットワーク上にある `.jpg` `.png` などの画像ファイルや `.mp4` などの動画ファイル、音声ファイルなどを保存することができます。

## [DownloadManager](http://developer.android.com/intl/ja/reference/android/app/DownloadManager.html) を使用してみる

「[JavaのDownloadManagerを使ってファイルをダウンロード](http://foonyan.sakura.ne.jp/wisteriahill/java_downloadmanager/index.html)」を参考にしました。

![dummy-1.jpg](https://qiita-image-store.s3.amazonaws.com/0/6881/7994700a-a9b5-5559-e3b4-b408befd1a48.jpeg)

| | 値 |
|:------|:------------:|
| サイズ | 172,109 バイト（176KB） |
| 大きさ | 458 × 458 |

Android 端末に保存された画像を Mac に転送してサイズを確認してみたところ、オリジナルと同じサイズで保存できていることが分かりました :o:

## [DownloadManager](http://developer.android.com/intl/ja/reference/android/app/DownloadManager.html) に書き換えてアプリをリリースしてみた結果

バグが３件見つかりました :scream:

### １件目の不具合 :bug:

[Crashlytics](https://try.crashlytics.com/) にクラッシュレポートが送られていました。

```
Fatal Exception: java.lang.IllegalArgumentException
Unknown URL content://downloads/my_downloads
```

エラー文で検索したところ [StackOverflow](https://stackoverflow.com/) の「[How to Enable Android Download Manager](http://stackoverflow.com/questions/30671548/unknown-url-content-downloads-my-downloads)」に辿り着きました。

:gear: 設定 > [アプリケーション] > すべて(システムのアプリを表示) > ダウンロードマネージャー が無効になっている状態で `DownloadManager.enqueue()` するとアプリが落ちるので対策が必要になります。

### ２件目の不具合 :bug:

[Crashlytics](https://try.crashlytics.com/) にレポートが届いていました。

```
Fatal Exception: java.lang.IllegalStateException
Unable to create directory:
```

[N-05D](http://www.n-keitai.com/n-05d/) (Android 4.0.4) などのプライマリストレージが SD カードになっているスマホからプライマリストレージの SD カードを取り除いた状態にして  [setDestinationInExternalPublicDir (String dirType, String subPath)](http://developer.android.com/reference/android/app/DownloadManager.Request.html#setDestinationInExternalPublicDir(java.lang.String, java.lang.String)) を実行するとアプリが落ちます。
[Nexus 5](https://store.google.com/product/nexus_5_white_16gb?playredirect=true) や [Nexus 7 (2013)](https://www.asus.com/jp/Tablets/Nexus_7_2013/) などの新機種はプライマリストレージが内蔵ストレージになっており SD カードのように取り外しができないため上記エラーは発生しないと思われます（[Crashlytics](https://try.crashlytics.com/) のレポートが送信されてきたのは Android 4.4.4 以下の古い端末でしたので）。

※ 参考にしたサイト :arrow_right: [Android におけるストレージまとめ](http://blog.lciel.jp/blog/2014/02/08/android-about-storage/)

### ３件目の不具合 :bug:

[Google Play ストア](https://play.google.com/store)のレビューやカスタマーサポートに「画像が保存できない」というクレームが届くようになりました。端末は [N-04E](http://www.n-keitai.com/n-04e/) (Android 4.1.2) で発生するとのこと。
原因を調べていると :gear: 設定 > [アプリ] > すべて(システムのアプリを表示) > ダウンロードマネージャー のデータ使用量が増えている状態では Response Headers に ETag の付いていない画像を保存できなくなるということが分かりました（※ ネットワーク上の画像の Response Headers に ETag が付いているかは [Google Chrome Developer Tools](https://developers.google.com/web/tools/chrome-devtools/) を使用して調べました）。
[N-04E](http://www.n-keitai.com/n-04e/) 端末内の標準ブラウザも [DownloadManager](http://developer.android.com/intl/ja/reference/android/app/DownloadManager.html) を使用しているため同様の現象が再現します。
対策としては [How to Enable Android Download Manager](http://stackoverflow.com/questions/30671548/unknown-url-content-downloads-my-downloads) を参考に [DownloadManager](http://developer.android.com/intl/ja/reference/android/app/DownloadManager.html) での保存に失敗した場合にダウンロードマネージャーの設定に誘導して「データを消去」ボタンを押してもらうようにするか、自社サーバーのファイルのみダウンロードするアプリの場合は Response Headers に ETag を設定するというのが考えられます。
この不具合は Android 4.1 系周辺バージョンのみで発生している気がするので、下位バージョンをサポートしていないアプリでは特に対策を考える必要は無いかも知れません。

#### それでも ETag 無しの画像を保存したい場合は

[Jake Wharton](https://github.com/JakeWharton) 様の [Picasso 2 OkHttp 3 Downloader](https://github.com/JakeWharton/picasso2-okhttp3-downloader) と [OkHttp](https://github.com/square/okhttp) を使用すれば画質劣化ゼロでネットワーク上の画像を保存することができました。

``` ImageLoadHelper.java
public class ImageLoadHelper {

    private static OkHttp3Downloader downloader;

    private synchronized static OkHttp3Downloader getOkHttp3DownloaderInstance() {
        if (downloader == null) {
           downloader = new OkHttp3Downloader(new OkHttpClient());
        }
        return downloader;
    }

    /**
      * 画像データの InputStream を返す
      *
      * @param url
      * @return
      * @throws IOException
      */
    public static InputStream getInputStream(String url) throws IOException {
        Downloader.Response response = getOkHttp3DownloaderInstance().load(Uri.parse(url), 1);
        return response.getInputStream();
    }
}
```

#### 手順

:one: [RxJava](https://github.com/ReactiveX/RxJava) の非同期処理中に上記の `getInputStream()`を実施
:two: 取得した画像の `InputStream` を端末内に保存する

# 番外編 〜Android の [Google Chrome](https://play.google.com/store/apps/details?id=com.android.chrome) アプリで画像保存してみる〜

![3e69f4f1-bf13-c091-1bd7-bd517687224d.jpeg](https://qiita-image-store.s3.amazonaws.com/0/6881/29c9b064-f016-a882-f60f-3e94487eae16.jpeg)

| | 値 |
|:------|:------------:|
| サイズ | 172,109 バイト（176KB） |
| 大きさ | 458 × 458 |

Android 端末に保存された画像を Mac に転送してサイズを確認したところ、オリジナルと同じサイズの画像を保存できていることが分かりました。
設定からダウンロードマネージャーを無効にした後 [Google Chrome](https://play.google.com/store/apps/details?id=com.android.chrome) から画像を保存すると「失敗しました」と [Snackbar](https://developer.android.com/reference/android/support/design/widget/Snackbar.html) 表示されるのでダウンロードマネージャーが使用されていることが分かります。
他にも Google Play や [Google Drive](https://play.google.com/store/apps/details?id=com.google.android.apps.docs), [Slack](https://play.google.com/store/apps/details?id=com.Slack) アプリもダウンロードマネージャーを使用していることが確認できました。