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:
2026-09-24 20:08:54 +02:00
co-authored by Claude Opus 4.8
parent 558afee239
commit 1ad85fb687
16 changed files with 750 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
build/
.gradle/
dist/
*.apk
local.properties
.idea/
*.iml
captures/
+32
View File
@@ -0,0 +1,32 @@
# Baut die Android-Agent-APK (Debug, auto-signiert -> direkt installierbar).
# APK-Builds gehen nur unter Linux — deshalb im Container.
FROM eclipse-temurin:17-jdk-jammy
ARG GRADLE_VERSION=8.5
ARG CMDLINE_TOOLS=11076708
ENV ANDROID_SDK_ROOT=/opt/android-sdk
ENV ANDROID_HOME=/opt/android-sdk
RUN apt-get update && apt-get install -y --no-install-recommends \
unzip wget ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Gradle
RUN wget -q https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip -O /tmp/g.zip \
&& unzip -q /tmp/g.zip -d /opt && rm /tmp/g.zip
# Android cmdline-tools + SDK
RUN mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools \
&& wget -q https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS}_latest.zip -O /tmp/c.zip \
&& unzip -q /tmp/c.zip -d ${ANDROID_SDK_ROOT}/cmdline-tools && rm /tmp/c.zip \
&& mv ${ANDROID_SDK_ROOT}/cmdline-tools/cmdline-tools ${ANDROID_SDK_ROOT}/cmdline-tools/latest
ENV PATH="/opt/gradle-${GRADLE_VERSION}/bin:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools:${PATH}"
RUN yes | sdkmanager --licenses >/dev/null 2>&1 || true
RUN sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0" >/dev/null 2>&1
WORKDIR /project
COPY . /project
CMD ["bash","-lc","gradle --no-daemon assembleDebug && cp app/build/outputs/apk/debug/app-debug.apk /out/aria-android-agent.apk && echo 'OK -> /out/aria-android-agent.apk'"]
+37
View File
@@ -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 &amp; 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>
+5
View File
@@ -0,0 +1,5 @@
// Root-Build. Plugin-Versionen zentral, in den Modulen nur angewandt.
plugins {
id 'com.android.application' version '8.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
}
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Baut die Android-Agent-APK per Docker (Android-SDK + Gradle).
# ./build.sh
# Ergebnis: dist/aria-android-agent.apk -> aufs Handy kopieren + installieren
# ("Unbekannte Quellen erlauben").
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p dist
docker build -f Dockerfile.build -t aria-android-agent-build .
docker run --rm -v "$(pwd)/dist:/out" aria-android-agent-build
echo
echo "Fertig: dist/aria-android-agent.apk"
+5
View File
@@ -0,0 +1,5 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
org.gradle.caching=true
+16
View File
@@ -0,0 +1,16 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "aria-android-agent"
include(":app")