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

Androidアプリで画像をアップロードしたいぞぉ!ということで、1つ前の記事でphpをhttpサーバーで動くようにしたので、次は、Androidアプリを作成します。

が、その前にAndroidアプリで画像を受け取るphpファイルをWindowsに作っておきます。tomcatをC:\tomcatにインストールしている場合は、 C:\tomcat\htdocs\index.phpというファイルに

<?php
$data = file_get_contents("php://input");
$fp = fopen("C:/temp/data.jpg", 'wb');
fwrite($fp, $data);
fclose($fp);
echo "Image Upload Finish.\n";
?>

を作ります。でアップロードしたファイルは C:/temp/data.jpg に作るので、C:\tempフォルダーを作っておきます。ここで気が付いたと思いますが、アップロードした画像は全部C:/temp/data.jpgに置かれてしまうので、古い画像は上書きされてしまいます(使えねぇ!!)

2つほど前に記事で作成したボタン付きのSampleAppを改造します。

MainActivityを以下のように改造

"http://192.168.1.21/index.php"のところは、サーバーになるWindows10のPCのローカルアドレスに変更すること。確認するには、Windowsキーを押す→設定→ネットワークとインターネット→プロパティ→IPv4 アドレス

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: ")
                        sb.append(cursor.getString(cursor.getColumnIndex(
                                MediaStore.Images.Media.DATA)));
                        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.21/index.php", bmp))
                        } 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()
            }
        }
    }
}

Android Studioでapp→manifests→AndroidManifest.xmlをダブルクリックして開く。 下の方に

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

の2行を追加。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hatenablog.kuukaix.simpleapp">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SimpleApp">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>

最後にMainAcitivityのところで右クリックしてNew→Kotolin Class/File を選んで、ファイル名にPostBmpAsyncHttpRequestと入力。

中身は

package com.hatenablog.kuukaix.simpleapp

import android.app.Activity
import android.graphics.Bitmap
import android.os.AsyncTask
import java.io.*
import java.net.HttpURLConnection
import java.net.URL

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

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())
            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 = string
    }

}

実行(エミュレータでは画像ファイルがないので正しく動きません。画像が入っている実機で動作させます)すると、画面は

f:id:kuukaix:20210429155156j:plain

で、同じです。「ボタン」を押すと、「例外が発生、ストレージPermissionを許可していますか?」が出ます。 ストレージへアクセス許可しないといけません。Androidの方で「設定」→「アプリ」→「SimpleApp」→「許可」→「ストレージ」のスライドを動かして許可する。permission.INTERNETも追加したのに出ないのは、初めから許可されているからです。(でもAndroidManifest.xmlには書かないといけません)

もう一度アプリを起動して、「ボタン」を押すとMediaStorageに入っている画像データの一覧が出ます。このときhttpのPost機能を使って、画像データをサーバーに転送しています。

Windowsに戻って、C:\temp\data.jpgを見ると最後にアップロードしたファイルが見えます。