Sibainu Relax Room

愛犬の柴犬とともに過ごす部屋

Android(Kotlin) WebサーバーにPOSTでUpload と フチベニベンケイ

今日の朝の散歩です。だれかお友達が来ないか一生懸命目を凝らしている柴犬です。

近くの公園の桜の木です。この木は毎年3月の10日頃咲きますので、今の時期は蕾ですが来週はちらほら綺麗な花が咲き始めていると思います。

来週、また様子を見に行ってきます。この時も柴犬も一緒でしたがアングル的無理でした。

概要

プログラム言語「Kotlin」を使ってWEBサーバーにHTTPのPOSTを使ってアップロードを考えてみました。

Android Studioの「logcat」で次の画像の結果まで達成できましたので記録することにします。

1冊のみでは理解に苦しいところがあるので更に新しく本を購入しました。

WEBのみでは断片的で覚えにくいので最初に購入した Kotlin の本です。

AndroidアプリのWEBの記事も Kotlin が過半数となっているのかなと感じます。CameraX の記事に至っては、私の捜査が悪いこともありますが、これまでJAVAで動作する記事に私は遭遇したことがありません。

フチベニベンケイ

1月27日から1カ月後の様子を記録しました。

赤5は相変わらず葉っぱのままです。芽が出てくる様子はありません。

ほかのものは順調に成長しています。

AndroidManifest.xml

ネットに接続しますので次の permission の追記が必要です。

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

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

ネットワークの security の警告が AndroidStudio の logcat に出ましたので次のコードを追加しました。

android:networkSecurityConfig="@xml/network_security_config"

全体は次のようになりました。

copy

<?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" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Jsonuploadkotlin"
        android:networkSecurityConfig="@xml/network_security_config"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

build.gradle.kts(:app)

viewBinding を使いますので次のコードを追記します。

    buildFeatures {

        viewBinding = true

    }

JSON を扱いますので次の依存関係を追加します。

implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")

全体は次のようになりました。

copy

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "org.sibainu.relax.room.jsonuploadkotlin"
    compileSdk = 34

    defaultConfig {
        applicationId = "org.sibainu.relax.room.jsonuploadkotlin"
        minSdk = 26
        targetSdk = 33
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        viewBinding = true
    }
}

dependencies {

    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}

res/layout/activity_main.xml

ガイドラインを使用しましたので文字が大分多くなりました。

copy

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/upload_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp"
        android:text="Button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2" />

    <TextView
        android:id="@+id/textView1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="TextView"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline3" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="0dp"
        android:layout_height="19dp"
        android:text="textView2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="@+id/guideline3"
        app:layout_constraintVertical_bias="0.0" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.3" />

    <androidx.constraintlayout.widget.Barrier
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="top" />

    <androidx.constraintlayout.widget.Barrier
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="left" />

    <androidx.constraintlayout.widget.Barrier
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="top" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.3" />

</androidx.constraintlayout.widget.ConstraintLayout>

こんな感じのレイアウトになります。

res/xml/network_security_config.xml

ネットワークの security の警告が AndroidStudio の logcat に出ましたので次のコードを追加しました。

AndroidManifest.xml にこのファイルの参照コードを追記します。

copy

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">https://サーバー</domain>
    </domain-config>
</network-security-config>

MainActivity.kt

関数、クラスをブロック毎に掲示して最後に全体のコードを掲載しています。

fun onCreate

クラス ClickListener を作成して登録しています。

copy

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        findViewById<Button>(R.id.upload_button).setOnClickListener(ClickListener())

    }

fun receiveInfo

ボタンをクリックして呼ばれる関数です。

copy

    @UiThread
    private fun receiveInfo(postUrl: String, bodyData: String) {

        val backgroundReceiver = InfoBackground(postUrl, bodyData)
        val executeService = Executors.newSingleThreadExecutor()
        Log.d(TAG, "receiveInfo executeService :" + bodyData)
        // サービスを作成して送信を実行、結果を受けます。
        val future = executeService.submit(backgroundReceiver)
        Log.d(TAG, "receiveInfo future :" + bodyData)
        // 結果を取得します。
        val result = future.get()
        if (result != "") {
            showInfo(result)
        } else {
            // 失敗のときの JSON を作成します。
            showInfo("{\"username\":\"失敗しました\"}")
        }
    }

fun showInfo

copy

    @UiThread
    private fun showInfo(result: String) {
        Log.d(TAG, "showInfo :" + result)
        // レスポンスJSONオブジェクトを生成。
        val rootJSON = JSONObject(result)
        // try catch で処理すべきかも。
        rootJSON?.apply {
            val userName = rootJSON.getString("username")
            Log.d(TAG, "ユーザー名 :" + userName)
            val telop = "ユーザー名:${userName}"
            val tvTelop = findViewById<TextView>(R.id.textView2)
            tvTelop.text = telop
        }
    }

fun requestBodyData

HTTP の POST 送信のリクエストのボディ部分を作成する関数です。

正しい形式が分かりませんでしたので取り合えずという状態です。使うとなれば現実に合わせて修正しなければなりません。

copy

    private fun requestBodyData(username: String,
                        userid: String,
                        access: String,
                        key: String,
                        jsondata: Map<Any?,Any?>): String {
        val postmap: MutableMap<Any?, Any?> = mutableMapOf("username" to username)
        postmap.put("userid", userid)
        postmap.put("access", access)
        postmap.put("key", key)
        postmap.put("data", jsondata)
        return JSONObject(postmap).toString()
    }

inner class InfoBackground

copy

    private inner class InfoBackground(postUrl: String, bodyData: String): Callable<String> {
        // 引数 postData、bodyData を call の中で直接使えないので
        // メンバーを作ります。
        val _postUrl = postUrl
        val _bodyData = bodyData

        @WorkerThread
        override fun call(): String {
            // POSTデータ。
            val postData = _bodyData
            // 送信結果が格納される。
            var result = ""
            // URLオブジェクトを生成。
            val url = URL(_postUrl)
            // URLオブジェクトからHttpURLConnectionオブジェクトを取得。
            val con = url.openConnection() as HttpURLConnection
            // 接続に使ってもよい時間を設定。
            con.connectTimeout = 1000
            // データ取得に使ってもよい時間。
            con.readTimeout = 1000
            // HTTP接続メソッドをPOSTに設定。
            con.requestMethod = "POST"
            con.doOutput = true
            // データのタイプを設定。
            con.setRequestProperty("Content-type", "application/json; charset=utf-8")
            // キャッシュは使いません。
            con.useCaches = false
            Log.d(TAG, "InfoBackground" + _bodyData)

            try {
                // 接続。
                con.connect()
                Log.d(TAG, "connect :" + _bodyData)

                // アップロード
                val os = con.outputStream
                os?.run {
                    // 送信するデータをバイト配列に変換
                    val postDataBytes = postData.toByteArray(charset("UTF-8"))
                    // アップロードの実行
                    write(postDataBytes)
                    // OnputStreamオブジェクトを解放。
                    flush()
                    close()
                    // レスポンスコードを取得。
                    val statusCode = con.responseCode
                    Log.d(TAG, "statuscode :" + statusCode.toString())
                    if (statusCode == HttpURLConnection.HTTP_OK) {
                        // レスポンスデータであるInputStreamオブジェクトを文字列に変換。
                        val stream = con.inputStream
                        result = is2String(stream)
                    }
                }
                Log.d(TAG, "inputstream :" + _bodyData)
            }
            catch(ex: SocketTimeoutException) {
                Log.w(TAG, "通信タイムアウト", ex)

            }
            finally {
                con.disconnect()
            }
            Log.d(TAG, "result :" + result)
            // 成功なら JSON 文字列を返し、失敗のときは空文字。
            return result
        }

        private fun is2String(stream: InputStream): String {
            val sb = StringBuilder()
            val reader = BufferedReader(InputStreamReader(stream, StandardCharsets.UTF_8))
            var line = reader.readLine()
            while(line != null) {
                sb.append(line)
                line = reader.readLine()
            }
            reader.close()
            return sb.toString()
        }
    }

companion object

Kotlinでは、クラス内の static な定数は companion object の中に定義するのが一般的のようですので、その例に習いました。

copy

    companion object {
        private const val TAG = "AsyncSample"
        private const val POSTURL = "https://サーバー/script.php"
        private const val USERNAME = "name"
        private const val USERID = "userid"
        private const val ACCESS = "access"
        private const val KEY = "key"
    }

inner class ClickListener

共通の Listener のクラスを作成しました。View のIDで場合分けをしています。

copy

    private inner class ClickListener: View.OnClickListener {
        override fun onClick(v: View?) {
            when (v?.id) {
                R.id.upload_button -> {
                    val dataMap: MutableMap<Any?, Any?> = mutableMapOf("field1" to "hoge hoge")
                    dataMap.put("field2", "1234567890")
                    dataMap.put("field3", "root")
                    dataMap.put("field4", "abc")
                    dataMap.put("field5", null)
                    val bodyData = requestBodyData(USERNAME, USERID, ACCESS, KEY, dataMap)
                    bodyData?.let {
                        Log.d(TAG, "click :" + it)
                        receiveInfo(POSTURL, it)
                    }
                }
                else -> {
                    Log.d(TAG, "設定がありません")
                }
            }
        }
    }

全体

このようなコードでエミュレートを実行した結果が概要のような結果になりました。

copy

package org.sibainu.relax.room.jsonuploadkotlin

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONObject
import org.sibainu.relax.room.jsonuploadkotlin.databinding.ActivityMainBinding
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.concurrent.Callable
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {

    private lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        findViewById<Button>(R.id.upload_button).setOnClickListener(ClickListener())

    }

    @UiThread
    private fun receiveInfo(postUrl: String, bodyData: String) {

        val backgroundReceiver = InfoBackground(postUrl, bodyData)
        val executeService = Executors.newSingleThreadExecutor()
        Log.d(TAG, "receiveInfo executeService :" + bodyData)
        val future = executeService.submit(backgroundReceiver)
        Log.d(TAG, "receiveInfo future :" + bodyData)
        val result = future.get()
        if (result != "") {
            showInfo(result)
        } else {
            showInfo("{\"username\":\"失敗しました\"}")
        }
    }

    @UiThread
    private fun showInfo(result: String) {
        Log.d(TAG, "showInfo :" + result)
        //レスポンスJSONオブジェクトを生成。
        val rootJSON = JSONObject(result)
        rootJSON?.apply {
            val userName = rootJSON.getString("username")
            Log.d(TAG, "ユーザー名 :" + userName)
            val telop = "ユーザー名:${userName}"
            val tvTelop = findViewById<TextView>(R.id.textView2)
            tvTelop.text = telop
        }
    }

    private fun requestBodyData(username: String,
                        userid: String,
                        access: String,
                        key: String,
                        jsondata: Map<Any?,Any?>): String {
        val postmap: MutableMap<Any?, Any?> = mutableMapOf("username" to username)
        postmap.put("userid", userid)
        postmap.put("access", access)
        postmap.put("key", key)
        postmap.put("data", jsondata)
        return JSONObject(postmap).toString()
    }

    private inner class InfoBackground(postUrl: String, bodyData: String): Callable<String> {

        val _postUrl = postUrl
        val _bodyData = bodyData

        @WorkerThread
        override fun call(): String {
            // POSTデータ。
            val postData = _bodyData
            // 送信結果が格納される。
            var result = ""
            // URLオブジェクトを生成。
            val url = URL(_postUrl)
            // URLオブジェクトからHttpURLConnectionオブジェクトを取得。
            val con = url.openConnection() as HttpURLConnection
            // 接続に使ってもよい時間を設定。
            con.connectTimeout = 1000
            // データ取得に使ってもよい時間。
            con.readTimeout = 1000
            // HTTP接続メソッドをPOSTに設定。
            con.requestMethod = "POST"
            con.doOutput = true
            con.setRequestProperty("Content-type", "application/json; charset=utf-8")
            con.useCaches = false
            Log.d(TAG, "InfoBackground" + _bodyData)
            try {
                // 接続。
                con.connect()
                Log.d(TAG, "connect :" + _bodyData)

                // アップロード
                val os = con.outputStream
                os?.run {
                    // 送信するデータをバイト配列に変換
                    val postDataBytes = postData.toByteArray(charset("UTF-8"))
                    // アップロードの実行
                    write(postDataBytes)
                    // OnputStreamオブジェクトを解放。
                    flush()
                    close()
                    val statusCode = con.responseCode
                    Log.d(TAG, "statuscode :" + statusCode.toString())
                    if (statusCode == HttpURLConnection.HTTP_OK) {
                        // レスポンスデータであるInputStreamオブジェクトを文字列に変換。
                        val stream = con.inputStream
                        result = is2String(stream)
                    }
                }
                Log.d(TAG, "inputstream :" + _bodyData)
            }
            catch(ex: SocketTimeoutException) {
                Log.w(TAG, "通信タイムアウト", ex)
            }
            finally {
                con.disconnect()
            }
            Log.d(TAG, "result :" + result)
            return result
        }

        private fun is2String(stream: InputStream): String {
            val sb = StringBuilder()
            val reader = BufferedReader(InputStreamReader(stream, StandardCharsets.UTF_8))
            var line = reader.readLine()
            while(line != null) {
                sb.append(line)
                line = reader.readLine()
            }
            reader.close()
            return sb.toString()
        }
    }

    companion object {
        private const val TAG = "AsyncSample"
        private const val POSTURL = "https://サーバー/script.php"
        private const val USERNAME = "name"
        private const val USERID = "userid"
        private const val ACCESS = "access"
        private const val KEY = "key"
    }

    private inner class ClickListener: View.OnClickListener {
        override fun onClick(v: View?) {
            when (v?.id) {
                R.id.upload_button -> {
                    val dataMap: MutableMap<Any?, Any?> = mutableMapOf("field1" to "hoge hoge")
                    dataMap.put("field2", "1234567890")
                    dataMap.put("field3", "root")
                    dataMap.put("field4", "abc")
                    dataMap.put("field5", null)
                    val bodyData = requestBodyData(USERNAME, USERID, ACCESS, KEY, dataMap)
                    bodyData?.let {
                        Log.d(TAG, "click :" + it)
                        receiveInfo(POSTURL, it)
                    }
                }
                else -> {
                    Log.d(TAG, "設定がありません")
                }
            }
        }
    }
}

最後にスマホでの実行状況を載せます。

左が起動時で、右がボタンをクリックした結果です。エミュレートのそのままです。

今回はここまでとします。