Phase 1: minimal Kotlin Android app shows Hub /health
Scaffold the Android Gradle project under android/ mirroring the proven internet_use_control toolchain: Gradle wrapper 8.11.1, AGP 8.7.3, Kotlin 2.0.21, compileSdk/targetSdk 35, Java/Kotlin 17, minSdk 29 (AGENTS.md §9). One Activity calls GET http://192.168.20.13:8788/health off the main thread and renders ok, service, version and every checks value plus the raw body. INTERNET permission and usesCleartextTraffic are declared for the LAN Hub. A failed or non-2xx call renders an explicit error line and never reports success (AGENTS.md §10 rule 4). applicationId au.audasmedia.hub.phone, versionName 0.1.0-phase1. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
43
android/app/build.gradle.kts
Normal file
43
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,43 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "au.audasmedia.hub.phone"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "au.audasmedia.hub.phone"
|
||||
// AGENTS.md §9: API 29 (Android 10) is the suggested minimum. The app
|
||||
// targets API 35 but runs on Android 10 and up.
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "0.1.0-phase1"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 1 keeps the dependency set empty on purpose: the health check uses
|
||||
// HttpURLConnection (JDK/Android) and org.json (Android platform). No appcompat,
|
||||
// no okhttp, no gson.
|
||||
dependencies {
|
||||
}
|
||||
1
android/app/proguard-rules.pro
vendored
Normal file
1
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# Phase 1 keeps no custom ProGuard rules. The release build is not minified.
|
||||
33
android/app/src/main/AndroidManifest.xml
Normal file
33
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Kotlin Phone App — Phase 1 (AGENTS.md §11 Phase 1).
|
||||
|
||||
This is the smallest possible proof that the Android build environment works
|
||||
end to end: one Activity that calls the Hub `GET /health` and shows the reply.
|
||||
|
||||
INTERNET is required to reach the Hub. usesCleartextTraffic is true because
|
||||
the Hub speaks plain HTTP on the LAN and Android blocks cleartext by default
|
||||
on API 28+ (AGENTS.md §11 Phase 1, step 3).
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:label="Hub Phone (Phase 1)"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Material.Light"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="Hub Phone (Phase 1)">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,152 @@
|
||||
package au.audasmedia.hub.phone
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import org.json.JSONObject
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* Phase 1 proof screen.
|
||||
*
|
||||
* Calls the Hub `GET /health` off the main thread and renders the reply. A
|
||||
* failed call shows an explicit error line and never reports success
|
||||
* (AGENTS.md §10 rule 4).
|
||||
*/
|
||||
class MainActivity : Activity() {
|
||||
|
||||
private lateinit var statusView: TextView
|
||||
private lateinit var resultView: TextView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val pad = dp(20)
|
||||
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(pad, pad, pad, pad)
|
||||
setBackgroundColor(Color.parseColor("#f6f5f4"))
|
||||
}
|
||||
|
||||
val title = TextView(this).apply {
|
||||
text = "Kotlin Phone App — Phase 1"
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 22f)
|
||||
setTypeface(typeface, Typeface.BOLD)
|
||||
setTextColor(Color.parseColor("#171717"))
|
||||
}
|
||||
root.addView(title)
|
||||
|
||||
val subtitle = TextView(this).apply {
|
||||
text = "Hub health check — no auth"
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
|
||||
setTextColor(Color.parseColor("#6b6b6b"))
|
||||
setPadding(0, dp(4), 0, dp(4))
|
||||
}
|
||||
root.addView(subtitle)
|
||||
|
||||
statusView = TextView(this).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||
setTextColor(Color.parseColor("#0075de"))
|
||||
text = "Calling $HEALTH_URL …"
|
||||
setPadding(0, dp(12), 0, dp(12))
|
||||
}
|
||||
root.addView(statusView)
|
||||
|
||||
resultView = TextView(this).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
|
||||
setTextColor(Color.parseColor("#171717"))
|
||||
typeface = Typeface.MONOSPACE
|
||||
}
|
||||
root.addView(resultView)
|
||||
|
||||
setContentView(ScrollView(this).apply { addView(root) })
|
||||
|
||||
fetchHealth()
|
||||
}
|
||||
|
||||
private fun fetchHealth() {
|
||||
Thread {
|
||||
try {
|
||||
val conn = (URL(HEALTH_URL).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = 8000
|
||||
readTimeout = 8000
|
||||
setRequestProperty("Accept", "application/json")
|
||||
}
|
||||
val code = conn.responseCode
|
||||
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
|
||||
val body = stream?.bufferedReader()?.use { it.readText() } ?: ""
|
||||
conn.disconnect()
|
||||
runOnUiThread { render(code, body) }
|
||||
} catch (e: Exception) {
|
||||
val reason = "${e.javaClass.simpleName}: ${e.message}"
|
||||
runOnUiThread { renderError(reason) }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun render(code: Int, body: String) {
|
||||
if (code !in 200..299) {
|
||||
renderError("HTTP $code from $HEALTH_URL\n$body")
|
||||
return
|
||||
}
|
||||
|
||||
val json = try {
|
||||
JSONObject(body)
|
||||
} catch (e: Exception) {
|
||||
renderError("Unparseable JSON from $HEALTH_URL\n$body")
|
||||
return
|
||||
}
|
||||
|
||||
if (!json.optBoolean("ok", false)) {
|
||||
renderError("Hub reported ok=false\n$body")
|
||||
return
|
||||
}
|
||||
|
||||
val out = StringBuilder()
|
||||
out.append("HTTP $code — HEALTHY\n\n")
|
||||
out.append("ok : ${json.optBoolean("ok")}\n")
|
||||
out.append("service : ${json.optString("service")}\n")
|
||||
out.append("version : ${json.optString("version")}\n")
|
||||
out.append("uptime_s : ${json.optLong("uptime_s")}\n")
|
||||
out.append("\nchecks:\n")
|
||||
val checks = json.optJSONObject("checks")
|
||||
if (checks != null && checks.length() > 0) {
|
||||
for (key in checks.keys()) {
|
||||
out.append(" $key : ${checks.optString(key)}\n")
|
||||
}
|
||||
} else {
|
||||
out.append(" (none)\n")
|
||||
}
|
||||
out.append("\nraw:\n$body")
|
||||
|
||||
statusView.text = "Hub reachable"
|
||||
statusView.setTextColor(Color.parseColor("#1a7f37"))
|
||||
resultView.setTextColor(Color.parseColor("#171717"))
|
||||
resultView.text = out.toString()
|
||||
}
|
||||
|
||||
private fun renderError(message: String) {
|
||||
statusView.text = "ERROR — Hub call failed"
|
||||
statusView.setTextColor(Color.parseColor("#c01c28"))
|
||||
resultView.setTextColor(Color.parseColor("#c01c28"))
|
||||
resultView.text = "$message\n\nURL: $HEALTH_URL"
|
||||
}
|
||||
|
||||
private fun dp(value: Int): Int = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
value.toFloat(),
|
||||
resources.displayMetrics,
|
||||
).toInt()
|
||||
|
||||
companion object {
|
||||
private const val HEALTH_URL = "http://192.168.20.13:8788/health"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user