ストレージにある画像をWindowsにアップロードする(Android)続き

1つ前の記事で画像をアップロードできたものの、同じファイルを上書きしてしまう(使えねぇ!!)なので、ファイル名もアップロードするデータに追加して、PHPサーバー側で取り出して、別々のファイルに保存するように改造をします。

まず、Androidアプリから画像を受け取るphpファイル(Windowsにあるよ)を改造します。tomcatをC:\tomcatにインストールしている場合は、 C:\tomcat\htdocs\index.phpというファイルにします。

<?php
$data = file_get_contents("php://input");
$bytes = strlen($data);
$name_part = substr($data, 0, 80);
$name_part_str = "";
foreach (str_split($name_part) as $chr) {
  if ($chr != "\0") {
    $name_part_str .= $chr;
  }
}
$image = substr($data, 80, $bytes - 80);
$fname = "C:/temp/" . $name_part_str . ".jpg";
$fp = fopen($fname, 'wb');
fwrite($fp, $image);
fclose($fp);
echo "Image Upload Finish.\n";
?>

受け取ったデータをsubstr()で先頭の80byteを$name_partに取り出します。$dataはbyte配列なのに、なぜかsubstr()で切れて、結果は文字列になります(ややこしい)。このままファイル名に使えると便利なのですが、null文字が含まれていて、エラーになってしまいます。

str_split()で文字列をbyte配列に戻して(ややこしい)foreachで1つずつ取り出して$chrに保存。null文字を除く文字を$name_part_str .= $chr;でくっつけます。文字結合は+=じゃなくて.=なのです。

画像の本体は、substr($data, 80, $bytes - 80); で80byte目から、最後までを$imageに取り出します。

ファイル名は、$fname = "C:/temp/" . $name_part_str . ".jpg"; で作ります。文字結合は + じゃなくて . なのです。

後は、1つ前の記事と同じで、fopen()でファイルを開いてfwrite()で画像データを保存します。

続いてandroidアプリを改造。前回は送信するデータは画像だけでしたが、データの先頭80byteにファイル名を入れます。ファイル名は、ディレクトリと拡張子を除いた部分 "/storage/emulated/なんたらかんたら/hoge.png"だったら"hoge"のところだけ取り出します。では、PostBmpAsyncHttpRequest.kt の全ソースコード

package com.hatenablog.kuukaix.simpleapp

import android.app.Activity
import android.graphics.Bitmap
import android.os.AsyncTask
import android.view.View
import android.widget.TextView
import java.io.*
import java.net.HttpURLConnection
import java.net.URL

class Param(var uri: String, var bmp: Bitmap, var name: String)

class PostBmpAsyncHttpRequest(private val mActivity: Activity) : AsyncTask<Param?, Void?, String>() {
    override fun doInBackground(vararg params: Param?): String {
        val param: Param = params[0]!!
        var connection: HttpURLConnection? = null
        val sb = StringBuilder()
        try {
            // 画像をjpeg形式でstreamに保存
            val jpg = ByteArrayOutputStream()
            param.bmp.compress(Bitmap.CompressFormat.JPEG, 100, jpg)
            val url = URL(param.uri)
            connection = url.openConnection() as HttpURLConnection
            connection.setConnectTimeout(3000) //接続タイムアウトを設定する。
            connection.setReadTimeout(3000) //レスポンスデータ読み取りタイムアウトを設定する。
            connection.setRequestMethod("POST") //HTTPのメソッドをPOSTに設定する。
            //ヘッダーを設定する
            connection.setRequestProperty("User-Agent", "Android")
            connection.setRequestProperty("Content-Type", "application/octet-stream")
            connection.setDoInput(true) //リクエストのボディ送信を許可する
            connection.setDoOutput(true) //レスポンスのボディ受信を許可する
            connection.setUseCaches(false) //キャッシュを使用しない
            connection.connect()

            // データを投げる
            val out: OutputStream = BufferedOutputStream(connection.getOutputStream())
            val filename = basename(param.name)
            out.write(filename.toByteArray())
            out.write(ByteArray(80 - filename.length))
            out.write(jpg.toByteArray())
            out.flush()

            // データを受け取る
            val istream: InputStream = connection.getInputStream()
            val reader = BufferedReader(InputStreamReader(istream, "UTF-8"))
            var line: String? = ""
            while (reader.readLine().also { line = it } != null) sb.append(line)
            istream.close()
        } catch (e: IOException) {
            e.printStackTrace()
        } finally {
            connection!!.disconnect()
        }
        return sb.toString()
    }

    public override fun onPostExecute(string: String) {
        // 戻り値をViewにセット
        val textView = mActivity.findViewById<View>(R.id.text_view) as TextView
        textView.text = "" + textView.text + "\n" + string
    }

    private fun basename(path: String): String {
        val file = File(path)
        return file.nameWithoutExtension
    }
}

前回から改造したのは

class Param(var uri: String, var bmp: Bitmap, var name: String)

MainActivityからデータを受け取る部分。nameでファイル名を受け取ります。

そしてデータを送信する部分

 // データを投げる
            val out: OutputStream = BufferedOutputStream(connection.getOutputStream())
            val filename = basename(param.name)
            out.write(filename.toByteArray())
            out.write(ByteArray(80 - filename.length))
            out.write(jpg.toByteArray())
            out.flush()
//と
    private fun basename(path: String): String {
        val file = File(path)
        return file.nameWithoutExtension
    }

basename()でファイルを拡張子を除く部分を取り出して、bytearrayにして、out.write()に出力します。 で、これだと80byteにならないので、残りの80 - filename.lengthの長さをnull文字で埋めます。 ByteArray(数)で、数の分だけnull文字が作れます。最後にjpg本体を書き込みます。

今回は、

    public override fun onPostExecute(string: String) {
        // 戻り値をViewにセット
        val textView = mActivity.findViewById<View>(R.id.text_view) as TextView
        textView.text = "" + textView.text + "\n" + string
    }

も追加します。Windowsサーバー側で何らかのエラーが返ると、それをAndroidの画面に出すためです。kotlinでは文字の結合は "."ではなくて"+"なのよね(ややこしい)

で、最後にMainActivityからファイル名をPostBmpAsyncHttpRequest()に渡すよう改造します。

package com.hatenablog.kuukaix.simpleapp

import android.content.ContentUris
import android.graphics.Bitmap
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.button).setOnClickListener {
            // ボタンが押されたら実行される
            Toast.makeText(this, "button clicked", Toast.LENGTH_LONG).show()
            //レコードの取得
            try {
                val contentResolver = contentResolver
                val query = contentResolver.query(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null
                )
                query?.use { cursor ->
                    val str = String.format(
                            "MediaStore.Images = %s\n\n", cursor.count)
                    val sb = StringBuilder(str)
                    while (cursor.moveToNext()) {
                        sb.append("ID: ")
                        sb.append(cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media._ID)));
                        val id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID))
                        sb.append("\n")
                        sb.append("Title: ")
                        sb.append(cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media.TITLE)))
                        sb.append("\n")
                        sb.append("Path: ")
                        val path_name = cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media.DATA))
                        sb.append(path_name)
                        sb.append("\n")
                        sb.append("Uri: ")
                        val uri = ContentUris.withAppendedId(
                                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                                id)
                        sb.append(uri)
                        sb.append("\n\n")
                        try {
                            val bmp: Bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri)
                            PostBmpAsyncHttpRequest(this).execute(Param("http://192.168.1.12/index.php", bmp, path_name))
                        } catch (e: Exception) {
                            e.printStackTrace()
                            Toast.makeText(this, "なんかエラー", Toast.LENGTH_SHORT).show()
                        }
                    }
                    val textView = findViewById<TextView>(R.id.text_view)
                    textView.setText(sb);
                }
            } catch (e: Exception) {
                e.printStackTrace()
                Toast.makeText(this,
                        "例外が発生、ストレージPermissionを許可していますか?", Toast.LENGTH_SHORT).show()
            }
        }
    }
}
                        val path_name = cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media.DATA))

でファイルパスを取り出して、

                            PostBmpAsyncHttpRequest(this).execute(Param("http://192.168.1.12/index.php", bmp, path_name))

Param()の第3引数にpath_nameを追加して、PostBmpAsyncHttpRequest()に渡します。

これで完成。

Androidアプリを実行してボタンを押すとC:\tempにjpgファイルが作られます。なお、エミュレータでは画像ファイルがないので、画像ファイルの入っている実機androidで試してください。