読者です 読者をやめる 読者になる 読者になる

woshidan's blog

そんなことよりコードにダイブ。

Androidで外部サーバからファイルをダウンロードする

Android Retrofit Streaming

通信周りでRetrofitを使っているとレスポンスがJSONばかりを想定していて、ファイルのダウンロードする場合はどうしたらいいのか、そもそも私はファイル入出力をまともに扱ったことがあったのかと、少し途方にくれてしまったのでメモ。

内容

  • 処理の流れとクラスについてのメモ
  • Fileクラスを利用して、保存先のファイルが入るディレクトリとファイル名を用意する
  • サーバへファイルをDLするためのリクエストを送り、そのレスポンスをInputStreamのインスタンスが取得できるクラスで受け取る
  • FileOutputStreamクラスを利用して、2のInputStreamのインスタンスから得られるバイト列を、1のファイル名のパスのファイルを作成して書き込む
  • 作成したファイルを他のアプリ(例えばギャラリー等)と共有したい場合は、ContentResolverを使ってコンテンツプロバイダへのデータの挿入を行う

処理の流れとクラスについてのメモ

ファイルをAndroidのストレージにダウンロードしてくるとき、処理の流れとしては大きく3つに分かれていて、

  1. Fileクラスを利用して、保存先のファイルが入るディレクトリとファイル名を用意する
  2. サーバへファイルをDLするためのリクエストを送り、そのレスポンスをInputStreamのインスタンスが取得できるクラスで受け取る
  3. FileOutputStreamクラスを利用して、2のInputStreamのインスタンスから得られるバイト列を、1のファイル名のパスのファイルを作成して書き込む
  4. 作成したファイルを他のアプリ(例えばギャラリー等)と共有したい場合は、ContentResolverを使ってコンテンツプロバイダへのデータの挿入を行う

という流れになります。

File

コンストラクタで与えた文字列のパスに対して、ファイルの権限を設定したり、ディレクトリを作ったりといったファイルシステムに関する処理を担当するクラスです*1

InputStream

一度に読み書きすると重すぎるような場合、少しずつ小出しに出来るような形で入力内容のバイト列を持っているイメージのクラスで、今回の場合では、#readメソッドでいくらか読み出してはバッファ用のbyte配列へ書き込むというのを繰り返させる、みたいな使い方をします*2

FileOutputStream

画像データなどの raw バイトのストリームを書き込むときに使用する、ファイルへ書き込むバイト列を貯めていくクラスです*3*4コンストラクタにFileオブジェクトやファイルのパスを渡して、最後に貯めた出力バイトを書き込むファイルを指定します。

ContentResolver

コンテンツプロバイダ*5に、作成したファイルのデータを登録するのに使います。

Fileクラスを利用して、保存先のファイルが入るディレクトリとファイル名を用意する

参考: http://www.ipentec.com/document/document.aspx?page=android-http-file-download-async

用意するのはディレクトリとファイル名(あるいはFileインスタンス)です。

最初にディレクトリを作成して、次に、そのディレクトリのパスに適当にファイル名を足して、ファイルの絶対パスを作ります。

    val downloadDir = File(Environment.getExternalStorageDirectory().path + "/DOWNLOAD_DIR_PATH/")

    try {
        // 保存先のディレクトリが無ければ作成する
        if (!downloadDir.exists()) {
            downloadDir.mkdir()
        }
    } catch (error : SecurityException) {
        // ファイルに書き込み用のパーミッションが無い場合など
        error.printStackTrace()
    } catch (error: IOException) {
        // 何らかの原因で誤ってディレクトリを2回作成してしまった場合など
        // http://stackoverflow.com/questions/22675796/mkdir-java-keeps-throwing-ioexception
        error.printStackTrace()
    } catch (error: Exception) {
        error.printStackTrace()
    }

    val fileName = "sample.jpg"
    val absoluteFilePath = downloadDir.absolutePath + "/" + fileName

サーバへファイルをDLするためのリクエストを送り、そのレスポンスをInputStreamのインスタンスが取得できるクラスで受け取る

参考: https://github.com/square/retrofit/issues/1228

今回はこれをRetrofitを使って行います。

RetrofitではファイルなどをDLする場合にInputStreamのインスタンスが取得できるレスポンスが欲しいときは、@Streamingアノテーションを用い、また、RetrofitのResponseクラスをジェネリックの型に指定するようです。

interface DownloadAdapter {
  @Streaming
  @GET("/files/{id}")
  fun downloadFile(
        @Path("fileId") id: int)
  ): Observable<Response>
}

// レスポンスからInputStreamのオブジェクトを取得する時は下記のような感じになります
var inputStream = response.body.`in`()

OkHttpについては、 http://stackoverflow.com/questions/25462523/retrofit-api-to-retrieve-a-png-image の回答などみたいな感じになります、というかこちらの方が調べて出てくる件数は多いような気がします。

FileOutputStreamクラスを利用して、2のInputStreamのインスタンスから得られるバイト列を、1のファイル名のパスのファイルを作って書き込む

var inputStream : InputStream? = null
var outputStream : FileOutputStream? = null

try {
    if (response.status == 200) {
        inputStream = response.body.`in`()
        var buff = kotlin.ByteArray(4096)
        val fileSize = response.body.length()
        var readSize = 0L
        var writtenSize = 0L

        outputStream = FileOutputStream(absoluteFilePath)

        // 全部書き込む(writtenSize == fileSize)か
        // もうInputStreamから読み込めなくなるか(inputStream.read(buff) != -1)まで繰り返し
        while(writtenSize < fileSize && (readSize = inputStream.read(buff)) != -1) {
            // inputStreamから#readで読み込んだ分のバイト列(buff)をoutputStream
            // の出力ストリームに書き込み
            writtenSize += readSize
            outputStream.write(buff)
        }
        // 読み込みが終ったら、出力ストリームに貯めた出力バイトをファイルへ一気に書き込みます
        outputStream.flush()
    } else {
      // サーバーからパラメータが違う等エラーが返ってきた場合か
      Log.e(TAG, response.toString())
    }
} catch (error: IOException) {
    // ファイルのパスがおかしい場合など
    error.printStackTrace()
} finally {
    // 例外が発生した場合でも使ったストリームは閉じるようにします
    inputStream?.close()
    outputStream?.close()
}

作成したファイルを他のアプリ(例えばギャラリー等)と共有したい場合は、ContentResolverを使ってコンテンツプロバイダへのデータの挿入を行う

参考: http://developer.android.com/intl/ja/guide/topics/providers/content-providers.html http://www.adakoda.com/adakoda/2010/08/android-34.html

// この場合は画像
var values = ContentValues()
val contentResolver = context.contentResolver
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
values.put(MediaStore.MediaColumns.TITLE, fileName)
values.put("_data", absoluteFilePath)
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

ここまで書いてて疲れてメモにまとめるくらいしか気力が無くなったけど、ファイルにバイト列を全部書き込んだらExifとか作成日時とか入っていて、地味に興奮しました。ファイルの中のバイト列以外のどこにファイルの情報が入っているのですか?と聞かれたら分からないんだけど、興奮した!!

*1:https://docs.oracle.com/javase/jp/6/api/java/io/File.html

*2:https://docs.oracle.com/javase/jp/6/api/java/io/InputStream.html

*3:https://docs.oracle.com/javase/jp/6/api/java/io/FileOutputStream.html

*4:テキストデータを書き込んでいく場合はFileWriterを使うそうです

*5:他のアプリへファイルの情報を共有してアクセスできるようにしてくれるデータベースのようなもの http://developer.android.com/intl/ja/guide/topics/providers/content-provider-basics.html