AndroidでVolleyを使ってMultipart/form-data形式で送信してみる

# AndroidでMultipart/form-dataを送信するには・・・？

Multipart/form-dataとはHTMLでファイルのアップロードと同時にテキストのデータも送信する方式です。一般的に、HTMLで書くと
```HTML
<html>
    <body>
        <form method="POST" action="/upload" enctype="multipart/form-data">
            <input type="text" name="message" value="Hello"/><br>
            <input type="file" name="file"/><br>
            <input type="submit" value="SUBMIT"/>
        </form>
    </body>
</html>
```
のような感じになります。このHTMLで送信するのと同じことをAndroidから発射してみようと言うわけです。ネットワーク上に流れるHTTPのデータとしてはこのようになります。
```
POST /fileUpload.php HTTP/1.1
Content-Type: multipart/form-data; boundary=FbnLKkZjJymSfRYdo2PiutUU4HrdDt1F3mk3sC; charset=Shift_JIS
User-Agent: Dalvik/2.1.0 (Linux; U; Android 11; sdk_gphone_x86_64 Build/RSR1.201211.001)
Host: 192.168.0.1:8080
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 458

--FbnLKkZjJymSfRYdo2PiutUU4HrdDt1F3mk3sC
Content-Disposition: form-data; name="upFile.txt"; filename="upFile.txt"
Content-Type: text/plain; charset=Shift_JIS
Content-Transfer-Encoding: binary

filecontentaaaaaaaaaaaaaaaa
--FbnLKkZjJymSfRYdo2PiutUU4HrdDt1F3mk3sC
Content-Disposition: form-data; name="text1"
Content-Type: text/plain; charset=Shift_JIS
Content-Transfer-Encoding: 8bit

stringggggggggggg
--FbnLKkZjJymSfRYdo2PiutUU4HrdDt1F3mk3sC--
```

## ライブラリに何を使うか

AndroidでHTTPを扱うライブラリは色々ありますが、
* Apache HTTP Client は Android 6.0 で削除されています。
* ネットワーク通信は非同期である必要がありますが、AnsyncTaskがAndroid11から非推奨になっています。
* Android9(API 28)以上はHTTPS通信を推奨され、素のHTTPで通信する場合はmanifestの修正が必要です。

以上の様に時代と共にバージョンが新しくなるに連れセキュリティ面が厳しくなっています。
今回は、本家Android develpersでも紹介しているVolleyを使ってMultipart/form-dataで送信してみます。

本家Android develpers
[Volley の概要](https://developer.android.com/training/volley?hl=ja)

VolleyはHTTP通信を非同期でやってくれるんですが、queueイングもやってくれます。Volleyを使って、普通にPOST、GETの例は沢山ありますが、Multipart/form-dataの例は少ないです。(本家Android develpersでも、Multipart/form-dataのやり方は書いてない)

# build.gradleの依存関係
ライブラリの依存関係を追加します。
```gradle:build.gradle
dependencies {
    ・・・
    // Volley
    implementation 'com.android.volley:volley:1.2.1'
    implementation 'org.apache.httpcomponents:httpcore:4.4.13'
    implementation 'org.apache.httpcomponents:httpmime:4.5.12'
}
```
また、ビルドするときにそれぞれのライブラリに入っていファイルが重複して、ビルドが失敗するので、
```gradle:build.gradle
android {
    ・・・
    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/NOTICE'
    }
}
```
を追加します。
# AndroidManifest.xml
ネットワーク通信するので、パーミッションが必要になります。また、上に書いたとおりHTTPSではなく、素のHTTPで通信する場合にはクリアテキストを許可する設定が必要になります。
```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        ・・・
        android:usesCleartextTraffic="true"
        ・・・ >
        <activity
            ・・・
        </activity>
    </application>

</manifest>
```
# VolleyのRequestクラスを継承したクラスを作る
com.android.volley.Request<T>を継承したクラスを作ります。ここで、multipart/form-dataのファイル(バイナリ)部分とテキスト部分のリクエストを表現できるようにします。
```kotlin
class MultipartRequest(
    url: String,
    private val mListener: Response.Listener<String>,
    errorListener: Response.ErrorListener,
    private val stringParts: Map<String, String>,
    private val fileParts: Map<String, File>
) : Request<String?>(Method.POST, url, errorListener) {
    private lateinit var httpEntity: HttpEntity
    private var entity = MultipartEntityBuilder.create()
        .setCharset(charset("Shift_JIS"))

    init {
        buildMultipartEntity()
        // Connection timeout, Read Timeout
        retryPolicy =
            DefaultRetryPolicy(30000, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)
    }

    private fun buildMultipartEntity() {
        //File Data
        val contentType = ContentType.create("text/plain", charset("Shift_JIS"))
        fileParts.forEach { (k, v) ->
            entity.addBinaryBody(k, v, contentType, v.name)
        }
        stringParts.forEach { (k, v) ->
            entity.addPart(k, StringBody(v, contentType))
        }
        httpEntity = entity.build()
    }

    override fun getBodyContentType(): String {
        return httpEntity.contentType?.value ?: ""
    }

    override fun getBody(): ByteArray {
        // TODO ファイルのサイズが大きくて、OutOfMemoryが起きる場合はHurlStackの実装が必要
        // @see http://fly1tkg.github.io/2014/03/volley-multipart-form-data/
        val bos = ByteArrayOutputStream()
        httpEntity.writeTo(bos)
        return bos.toByteArray()
    }

    override fun parseNetworkResponse(response: NetworkResponse?): Response<String?>? {
        return Response.success("Uploaded", cacheEntry)
    }

    override fun deliverResponse(response: String?) {
        mListener.onResponse(response)
    }
}
```
HTTPの仕様としては、送るファイル(バイナリ)部分とテキスト部分の個数に制限はないので、それぞれ複数個送れるように引数はMapの形で渡します。
この継承したRequestクラスをnewして呼ぶクラスを作成します。ここで、HTTP通信成功/失敗のリスナも定義してやります。
```kotlin
class HttpPostMultiPart(private val context: Context) {

    companion object {
        const val TAG = "HttpPost"
    }

    private var mQueue: RequestQueue = Volley.newRequestQueue(context)

    fun doUpload(url: String, fileMap: Map<String, File>, stringMap: Map<String, String>) {
        val multipartRequest = MultipartRequest(
            url,
            // Response.Listener、Upload成功
            { response ->
                Log.d(TAG, "アップロード成功:\n $response")
                Toast.makeText(context, "ファイルのアップロードが完了しました。:${url}", Toast.LENGTH_LONG).show()
            },
            // Response.ErrorListener、Upload失敗
            { e ->
                Log.d(TAG, "アップロード失敗:\n ${e.networkResponse}", e)
                Toast.makeText(context, "ファイルのアップロードに失敗しました。:${url}", Toast.LENGTH_LONG).show()
            },
            stringMap,
            fileMap
        )
        mQueue.add(multipartRequest)
    }
}
```
# MainActivity
あとはMainActivityで画面からデータを取得して、ファイルはファイルに書き込んで、テキストは取得したそのままを渡すようにしてやります。この例ではファイル(バイナリ)、テキスト共に1個づつですが、Mapなので何個でも可能です。
```kotlin:MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater).apply{
        setContentView(this.root)
    }
    // アップロードするファイル(例)
    val upFile = File(applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "upFile.txt")

    binding.sendBtn.setOnClickListener {
        val fileContent = binding.fileContent.text.toString()
        upFile.writeText(fileContent)
        val textContent = binding.textContent.text.toString()
        // Multipartのファイルの部分
        val filePart = mapOf("upFile.txt" to  upFile)
        // Multipartのテキストの部分
        val stringPart = mapOf("text1" to textContent)
        val httpMultiPart = HttpPostMultiPart(applicationContext)
        httpMultiPart.doUpload(URL, filePart, stringPart)
    }
}
```
画面はこんな画面を作ってみました
![Screenshot_20221224_162106.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/f1a59b8f-4418-a809-bfed-299d8d36f99c.png)
上側のEditTextに入力した文字がファイルに書き出されて、ファイル(バイナリ)として送信されます、下側のEditTextはテキストとしてそのまま送信されます。送信ボタンをタップすると上のHTTPの生データの例のように送信されます。

# 最後に
完成版は[gitHub](https://github.com/cozyk100/AndroidStudioProjectsPublic/tree/main/VolleyKotlin)に置きました。

更に、VolleyでCookieを有効にして使う例も書きました。
[AndroidでVolleyを使った送信でcookieを使用する場合](https://qiita.com/cozyk100/items/ce5c052224cc24e2924e)も参考にしてください。