feat(android-agent): Meilenstein 1 — verbinden + in Diagnostic sichtbar
Nativer Kotlin-Agent (host-agent/android/): Gradle-Projekt, AndroidManifest,
RVS-WebSocket-Client (OkHttp) mit host_hello/host_ping/host_command->host_result,
Foreground-Service (Weg A, Auto-Reconnect, BootReceiver), Connect-UI (QR-Scan via
ZXing ODER manuell: host/port/token/name/TLS/Steuerung-Schalter). QR-Format =
{host,port,token,tls} wie die ARIA-App -> derselbe QR nutzbar.
M1-Aktion: info (Modell/Android/Akku). screenshot/ui_* liefern 'kommt in M2/M3'.
Gate: CONTROL_ENABLED-Schalter in der App. Build: Docker (Android-SDK+Gradle) ->
dist/aria-android-agent.apk (Debug, auto-signiert). Kein Google.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'de.hackersoft.ariaagent'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'de.hackersoft.ariaagent'
|
||||
minSdk 26
|
||||
targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34
|
||||
versionCode 1
|
||||
versionName '0.1.0'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0' // RVS-WebSocket
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0' // QR-Scan (FOSS, kein Google-Dienst)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.AppCompat.DayNight">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".AgentService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.hackersoft.ariaagent
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
/** Verbindungs-/Agent-Einstellungen (in SharedPreferences persistiert). */
|
||||
data class AgentConfig(
|
||||
var host: String = "",
|
||||
var port: Int = 443,
|
||||
var tls: Boolean = true,
|
||||
var token: String = "",
|
||||
var name: String = "",
|
||||
var controlEnabled: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
private const val PREFS = "aria_agent"
|
||||
|
||||
fun load(ctx: Context): AgentConfig {
|
||||
val p = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
return AgentConfig(
|
||||
host = p.getString("host", "") ?: "",
|
||||
port = p.getInt("port", 443),
|
||||
tls = p.getBoolean("tls", true),
|
||||
token = p.getString("token", "") ?: "",
|
||||
name = p.getString("name", "") ?: "",
|
||||
controlEnabled = p.getBoolean("control", false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun save(ctx: Context) {
|
||||
ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().apply {
|
||||
putString("host", host)
|
||||
putInt("port", port)
|
||||
putBoolean("tls", tls)
|
||||
putString("token", token)
|
||||
putString("name", name)
|
||||
putBoolean("control", controlEnabled)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun isValid(): Boolean = host.isNotBlank() && token.isNotBlank()
|
||||
|
||||
fun displayName(): String = if (name.isNotBlank()) name else Build.MODEL
|
||||
|
||||
/** Stabile, technische Host-ID (a-z0-9_-), wie beim Desktop-Agent. */
|
||||
fun hostId(): String {
|
||||
val base = displayName().lowercase()
|
||||
.replace(Regex("[^a-z0-9_-]+"), "-").trim('-')
|
||||
return base.ifBlank { "android" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package de.hackersoft.ariaagent
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
/**
|
||||
* Foreground-Service (Weg A): haelt die RVS-Verbindung dauerhaft, damit ARIA
|
||||
* den Agenten jederzeit erreicht. Persistente Notification + Auto-Reconnect.
|
||||
*/
|
||||
class AgentService : Service() {
|
||||
|
||||
private var rvs: RvsClient? = null
|
||||
|
||||
companion object {
|
||||
private const val CH = "aria_agent"
|
||||
private const val NOTIF_ID = 1
|
||||
const val ACTION_STATUS = "de.hackersoft.ariaagent.STATUS"
|
||||
|
||||
@Volatile var status: String = "gestoppt"
|
||||
@Volatile var connected: Boolean = false
|
||||
|
||||
fun start(ctx: Context) {
|
||||
val i = Intent(ctx, AgentService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= 26) ctx.startForegroundService(i) else ctx.startService(i)
|
||||
}
|
||||
|
||||
fun stop(ctx: Context) {
|
||||
ctx.stopService(Intent(ctx, AgentService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createChannel()
|
||||
startForeground(NOTIF_ID, buildNotification("startet …"))
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val cfg = AgentConfig.load(this)
|
||||
if (!cfg.isValid()) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
rvs?.stop()
|
||||
rvs = RvsClient(applicationContext, cfg) { conn, msg ->
|
||||
connected = conn
|
||||
status = msg
|
||||
updateNotification(msg)
|
||||
sendBroadcast(Intent(ACTION_STATUS).setPackage(packageName))
|
||||
}
|
||||
rvs?.start()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
rvs?.stop()
|
||||
connected = false
|
||||
status = "gestoppt"
|
||||
sendBroadcast(Intent(ACTION_STATUS).setPackage(packageName))
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
private fun createChannel() {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
val ch = NotificationChannel(CH, "ARIA Agent", NotificationManager.IMPORTANCE_LOW)
|
||||
ch.setShowBadge(false)
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
|
||||
.createNotificationChannel(ch)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(text: String): Notification {
|
||||
val pi = PendingIntent.getActivity(
|
||||
this, 0, Intent(this, MainActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
return NotificationCompat.Builder(this, CH)
|
||||
.setContentTitle("ARIA Host-Agent")
|
||||
.setContentText(text)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(pi)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun updateNotification(text: String) {
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
|
||||
.notify(NOTIF_ID, buildNotification(text))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.hackersoft.ariaagent
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
/** Startet den Agent-Service nach dem Booten wieder (wenn konfiguriert). */
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
if (AgentConfig.load(ctx).isValid()) {
|
||||
AgentService.start(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package de.hackersoft.ariaagent
|
||||
|
||||
import android.Manifest
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import org.json.JSONObject
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var host: EditText
|
||||
private lateinit var port: EditText
|
||||
private lateinit var token: EditText
|
||||
private lateinit var name: EditText
|
||||
private lateinit var tls: SwitchCompat
|
||||
private lateinit var control: SwitchCompat
|
||||
private lateinit var statusView: TextView
|
||||
|
||||
private val scan = registerForActivityResult(ScanContract()) { res ->
|
||||
res.contents?.let { applyQr(it) }
|
||||
}
|
||||
private val camPerm = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
if (granted) launchScan() else toast("Kamera-Berechtigung nötig für QR-Scan")
|
||||
}
|
||||
private val notifPerm = registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
|
||||
|
||||
private val statusReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(c: Context?, i: Intent?) = refreshStatus()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
host = findViewById(R.id.host)
|
||||
port = findViewById(R.id.port)
|
||||
token = findViewById(R.id.token)
|
||||
name = findViewById(R.id.name)
|
||||
tls = findViewById(R.id.tls)
|
||||
control = findViewById(R.id.control)
|
||||
statusView = findViewById(R.id.status)
|
||||
|
||||
findViewById<Button>(R.id.btnScan).setOnClickListener {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
||||
== PackageManager.PERMISSION_GRANTED) launchScan()
|
||||
else camPerm.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
findViewById<Button>(R.id.btnConnect).setOnClickListener { saveAndConnect() }
|
||||
findViewById<Button>(R.id.btnStop).setOnClickListener {
|
||||
AgentService.stop(this)
|
||||
refreshStatus()
|
||||
}
|
||||
|
||||
loadIntoUi(AgentConfig.load(this))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33 &&
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
notifPerm.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val filter = IntentFilter(AgentService.ACTION_STATUS)
|
||||
ContextCompat.registerReceiver(this, statusReceiver, filter,
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
refreshStatus()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
try { unregisterReceiver(statusReceiver) } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun launchScan() {
|
||||
val o = ScanOptions()
|
||||
.setBeepEnabled(false)
|
||||
.setOrientationLocked(false)
|
||||
.setPrompt("ARIA-Verbindungs-QR scannen")
|
||||
scan.launch(o)
|
||||
}
|
||||
|
||||
private fun applyQr(content: String) {
|
||||
try {
|
||||
val j = JSONObject(content)
|
||||
host.setText(j.optString("host"))
|
||||
port.setText((if (j.has("port")) j.optInt("port", 443) else 443).toString())
|
||||
token.setText(j.optString("token"))
|
||||
if (j.has("tls")) tls.isChecked = j.optBoolean("tls", true)
|
||||
toast("QR übernommen — jetzt 'Speichern & Verbinden'")
|
||||
} catch (_: Exception) {
|
||||
toast("Das ist kein ARIA-Verbindungs-QR")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadIntoUi(c: AgentConfig) {
|
||||
host.setText(c.host)
|
||||
port.setText(c.port.toString())
|
||||
token.setText(c.token)
|
||||
name.setText(c.name)
|
||||
tls.isChecked = c.tls
|
||||
control.isChecked = c.controlEnabled
|
||||
}
|
||||
|
||||
private fun saveAndConnect() {
|
||||
val c = AgentConfig(
|
||||
host = host.text.toString().trim(),
|
||||
port = port.text.toString().trim().toIntOrNull() ?: 443,
|
||||
tls = tls.isChecked,
|
||||
token = token.text.toString().trim(),
|
||||
name = name.text.toString().trim(),
|
||||
controlEnabled = control.isChecked,
|
||||
)
|
||||
if (!c.isValid()) {
|
||||
toast("Host und Token sind Pflicht")
|
||||
return
|
||||
}
|
||||
c.save(this)
|
||||
AgentService.start(this)
|
||||
toast("Agent gestartet")
|
||||
refreshStatus()
|
||||
}
|
||||
|
||||
private fun refreshStatus() {
|
||||
val dot = if (AgentService.connected) "🟢" else "🔴"
|
||||
statusView.text = "Status: ${AgentService.status} $dot"
|
||||
}
|
||||
|
||||
private fun toast(m: String) = Toast.makeText(this, m, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package de.hackersoft.ariaagent
|
||||
|
||||
import android.content.Context
|
||||
import android.os.BatteryManager
|
||||
import android.os.Build
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* RVS-WebSocket-Client (Meilenstein 1). Spricht dasselbe Protokoll wie der
|
||||
* Desktop-Agent: meldet sich per host_hello, haelt sich per host_ping frisch,
|
||||
* beantwortet host_command -> host_result.
|
||||
*
|
||||
* M1-Aktionen: nur `info`. screenshot/ui_* folgen in M2/M3.
|
||||
*/
|
||||
class RvsClient(
|
||||
private val appCtx: Context,
|
||||
private val config: AgentConfig,
|
||||
private val onStatus: (connected: Boolean, msg: String) -> Unit,
|
||||
) {
|
||||
private val client = OkHttpClient.Builder()
|
||||
.pingInterval(20, TimeUnit.SECONDS)
|
||||
.readTimeout(0, TimeUnit.MILLISECONDS) // Server-Push: nie lesen-timeouten
|
||||
.build()
|
||||
|
||||
private var ws: WebSocket? = null
|
||||
@Volatile private var running = false
|
||||
private var pingThread: Thread? = null
|
||||
|
||||
private val caps = listOf("info") // waechst in M2/M3
|
||||
|
||||
fun start() {
|
||||
running = true
|
||||
connect()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
running = false
|
||||
pingThread?.interrupt()
|
||||
try { ws?.close(1000, "bye") } catch (_: Exception) {}
|
||||
ws = null
|
||||
}
|
||||
|
||||
private fun url(): String {
|
||||
val proto = if (config.tls) "wss" else "ws"
|
||||
return "$proto://${config.host}:${config.port}?token=${config.token}"
|
||||
}
|
||||
|
||||
private fun connect() {
|
||||
if (!running) return
|
||||
onStatus(false, "verbinde …")
|
||||
val req = Request.Builder().url(url()).build()
|
||||
ws = client.newWebSocket(req, object : WebSocketListener() {
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
onStatus(true, "verbunden")
|
||||
sendHello(webSocket)
|
||||
startPing(webSocket)
|
||||
}
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
handle(webSocket, text)
|
||||
}
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
onStatus(false, "getrennt: ${t.message ?: "?"}")
|
||||
reconnectLater()
|
||||
}
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
onStatus(false, "geschlossen")
|
||||
reconnectLater()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun reconnectLater() {
|
||||
pingThread?.interrupt()
|
||||
if (!running) return
|
||||
Thread {
|
||||
try { Thread.sleep(3000) } catch (_: InterruptedException) { return@Thread }
|
||||
connect()
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun send(webSocket: WebSocket, type: String, payload: JSONObject) {
|
||||
val o = JSONObject()
|
||||
o.put("type", type)
|
||||
o.put("payload", payload)
|
||||
o.put("timestamp", System.currentTimeMillis())
|
||||
try { webSocket.send(o.toString()) } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun sendHello(webSocket: WebSocket) {
|
||||
val p = JSONObject()
|
||||
p.put("hostId", config.hostId())
|
||||
p.put("name", config.displayName())
|
||||
p.put("os", "Android ${Build.VERSION.RELEASE} (${Build.MODEL})")
|
||||
p.put("caps", JSONArray(caps))
|
||||
p.put("control", config.controlEnabled)
|
||||
send(webSocket, "host_hello", p)
|
||||
}
|
||||
|
||||
private fun startPing(webSocket: WebSocket) {
|
||||
pingThread?.interrupt()
|
||||
pingThread = Thread {
|
||||
while (running && !Thread.currentThread().isInterrupted) {
|
||||
try { Thread.sleep(25000) } catch (_: InterruptedException) { break }
|
||||
val p = JSONObject().put("hostId", config.hostId())
|
||||
send(webSocket, "host_ping", p)
|
||||
sendHello(webSocket) // Re-announce (RVS replayt hellos nicht)
|
||||
}
|
||||
}.also { it.start() }
|
||||
}
|
||||
|
||||
private fun handle(webSocket: WebSocket, text: String) {
|
||||
val msg = try { JSONObject(text) } catch (_: Exception) { return }
|
||||
if (msg.optString("type") != "host_command") return
|
||||
val payload = msg.optJSONObject("payload") ?: JSONObject()
|
||||
|
||||
val target = payload.optString("host").ifBlank { payload.optString("hostId") }
|
||||
if (target.isNotBlank()
|
||||
&& !target.equals(config.hostId(), true)
|
||||
&& !target.equals(config.displayName(), true)) return
|
||||
|
||||
val action = payload.optString("action")
|
||||
val result: JSONObject = when {
|
||||
!config.controlEnabled ->
|
||||
err("Steuerung ist in der Agent-App deaktiviert (Schalter 'Steuerung erlauben').")
|
||||
action == "info" -> doInfo()
|
||||
action in listOf("screenshot", "ui_dump", "ui_tap", "ui_text",
|
||||
"ui_swipe", "ui_key", "app_launch", "app_list", "notify") ->
|
||||
err("Aktion '$action' kommt in Meilenstein 2/3 (noch nicht implementiert).")
|
||||
else -> err("Aktion '$action' unbekannt.")
|
||||
}
|
||||
result.put("requestId", payload.optString("requestId"))
|
||||
result.put("hostId", config.hostId())
|
||||
result.put("action", action)
|
||||
send(webSocket, "host_result", result)
|
||||
}
|
||||
|
||||
private fun err(m: String): JSONObject = JSONObject().put("ok", false).put("error", m)
|
||||
|
||||
private fun doInfo(): JSONObject {
|
||||
val res = JSONObject()
|
||||
res.put("host", config.displayName())
|
||||
res.put("model", Build.MODEL)
|
||||
res.put("manufacturer", Build.MANUFACTURER)
|
||||
res.put("android", Build.VERSION.RELEASE)
|
||||
res.put("sdk", Build.VERSION.SDK_INT)
|
||||
try {
|
||||
val bm = appCtx.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
|
||||
res.put("battery_percent", bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY))
|
||||
} catch (_: Exception) {}
|
||||
return JSONObject().put("ok", true).put("result", res)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#0D0D1A"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#3FFF9F"
|
||||
android:pathData="M54,26 m-26,0 a26,26 0 1,0 52,0 a26,26 0 1,0 -52,0 Z M54,26 m-13,0 a13,13 0 1,1 26,0 a13,13 0 1,1 -26,0 Z" />
|
||||
<path
|
||||
android:fillColor="#3FFF9F"
|
||||
android:pathData="M52,58 h4 v24 h-4 z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,100 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Status: —"
|
||||
android:paddingBottom="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnScan"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="QR-Code scannen" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="— oder manuell —"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/host"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="RVS-Host (z.B. rvs.example.com)"
|
||||
android:inputType="textUri" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/port"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Port"
|
||||
android:text="443"
|
||||
android:inputType="number" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/token"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="RVS-Token"
|
||||
android:inputType="textNoSuggestions" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Anzeigename (optional, z.B. 'Stefans Handy')"
|
||||
android:inputType="text" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/tls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TLS (wss)"
|
||||
android:checked="true"
|
||||
android:paddingTop="12dp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/control"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Steuerung erlauben (Aktionen ausführen)"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnConnect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Speichern & Verbinden" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnStop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Agent stoppen"
|
||||
android:paddingTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">ARIA Host-Agent</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user