commit 22eeb50050fd10d624b0b45400042d4b1f75c2af Author: ODY Build Date: Tue Jul 21 07:45:27 2026 +0000 mmap18: ODY v0.2-stable — ThinkBlockFilter aktiv, Bug A+B gefixt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f174243 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +build/ +app/build/ +*.apk +*.aab +local.properties +.idea/ +*.iml +.DS_Store diff --git a/.kotlin/errors/errors-1784216679514.log b/.kotlin/errors/errors-1784216679514.log new file mode 100644 index 0000000..8014724 --- /dev/null +++ b/.kotlin/errors/errors-1784216679514.log @@ -0,0 +1,76 @@ +kotlin version: 2.0.21 +error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /root/.openclaw/workspace/ODY/Foundation/app/src/main/java/de/ody/ODYApplication.kt:17:5: java.lang.IllegalArgumentException: source must not be null + at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57) + at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249) + at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46) + at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77) + at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88) + at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319) + at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118) + at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148) + at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43) + at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103) + at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49) + at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92) + at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:714) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) +Caused by: java.lang.IllegalArgumentException: source must not be null + at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68) + at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39) + at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31) + at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50) + at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42) + at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17) + at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81) + at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53) + at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19) + at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28) + at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30) + at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28) + at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30) + at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51) + at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30) + at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48) + at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30) + at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42) + at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36) + at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34) + ... 33 more + + diff --git a/.kotlin/errors/errors-1784221238880.log b/.kotlin/errors/errors-1784221238880.log new file mode 100644 index 0000000..2757727 --- /dev/null +++ b/.kotlin/errors/errors-1784221238880.log @@ -0,0 +1,73 @@ +kotlin version: 2.3.20 +error message: java.io.IOException: Cannot run program "/usr/lib/jvm/java-21-openjdk-amd64/bin/java" (in directory "/root/.local/share/kotlin/daemon"): posix_spawn failed, error: 11 (Resource temporarily unavailable) +Possible reasons: + - Spawn helper ran into JDK version mismatch + - Spawn helper ran into unexpected internal error + - Spawn helper was terminated by another process +Possible solutions: + - Restart JVM, especially after in-place JDK updates + - Check system logs for JDK-related errors + - Re-install JDK to fix permission/versioning problems + - Switch to legacy launch mechanism with -Djdk.lang.Process.launchMechanism=VFORK + + +error message: java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached + at java.base/java.lang.Thread.start0(Native Method) + at java.base/java.lang.Thread.start(Thread.java:1526) + at kotlinx.coroutines.scheduling.CoroutineScheduler.createNewWorker(CoroutineScheduler.kt:499) + at kotlinx.coroutines.scheduling.CoroutineScheduler.tryCreateWorker(CoroutineScheduler.kt:455) + at kotlinx.coroutines.scheduling.CoroutineScheduler.tryCreateWorker$default(CoroutineScheduler.kt:443) + at kotlinx.coroutines.scheduling.CoroutineScheduler.signalCpuWork(CoroutineScheduler.kt:439) + at kotlinx.coroutines.scheduling.CoroutineScheduler.dispatch(CoroutineScheduler.kt:415) + at kotlinx.coroutines.scheduling.CoroutineScheduler.dispatch$default(CoroutineScheduler.kt:394) + at kotlinx.coroutines.scheduling.SchedulerCoroutineDispatcher.dispatch(Dispatcher.kt:114) + at kotlinx.coroutines.internal.DispatchedContinuationKt.safeDispatch(DispatchedContinuation.kt:254) + at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWithInternal(DispatchedContinuation.kt:327) + at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) + at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:358) + at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:134) + at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:209) + at kotlinx.coroutines.BuildersKt.launch(Unknown Source) + at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:200) + at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source) + at org.jetbrains.kotlin.com.intellij.codeInsight.multiverse.CodeInsightContextManagerImpl.subscribeToChanges(CodeInsightContextManagerImpl.kt:77) + at org.jetbrains.kotlin.com.intellij.codeInsight.multiverse.CodeInsightContextManagerImpl.(CodeInsightContextManagerImpl.kt:72) + at org.jetbrains.kotlin.com.intellij.core.CoreProjectEnvironment.(CoreProjectEnvironment.java:53) + at org.jetbrains.kotlin.com.intellij.core.JavaCoreProjectEnvironment.(JavaCoreProjectEnvironment.java:38) + at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment.(KotlinCoreProjectEnvironment.kt:24) + at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment$ProjectEnvironment.(KotlinCoreEnvironment.kt:111) + at org.jetbrains.kotlin.cli.jvm.compiler.legacy.pipeline.JvmCompilerPipelineKt.createProjectEnvironment(jvmCompilerPipeline.kt:249) + at org.jetbrains.kotlin.cli.pipeline.jvm.JvmFrontendPipelinePhase.createEnvironmentAndSources(JvmFrontendPipelinePhase.kt:208) + at org.jetbrains.kotlin.cli.pipeline.jvm.JvmFrontendPipelinePhase.executePhase(JvmFrontendPipelinePhase.kt:73) + at org.jetbrains.kotlin.cli.pipeline.jvm.JvmFrontendPipelinePhase.executePhase(JvmFrontendPipelinePhase.kt:55) + at org.jetbrains.kotlin.cli.pipeline.PipelinePhase.phaseBody(PipelinePhase.kt:68) + at org.jetbrains.kotlin.cli.pipeline.PipelinePhase.phaseBody(PipelinePhase.kt:58) + at org.jetbrains.kotlin.config.phaser.NamedCompilerPhase.invoke(CompilerPhase.kt:102) + at org.jetbrains.kotlin.backend.common.phaser.CompositePhase.invoke(PhaseBuilders.kt:22) + at org.jetbrains.kotlin.config.phaser.CompilerPhaseKt.invokeToplevel(CompilerPhase.kt:53) + at org.jetbrains.kotlin.cli.pipeline.AbstractCliPipeline.runPhasedPipeline(AbstractCliPipeline.kt:109) + at org.jetbrains.kotlin.cli.pipeline.AbstractCliPipeline.execute(AbstractCliPipeline.kt:68) + at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:51) + at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecutePhased(K2JVMCompiler.kt:42) + at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:91) + at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:363) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunnerBase.runCompiler(IncrementalJvmCompilerRunnerBase.kt:210) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunnerBase.runCompiler(IncrementalJvmCompilerRunnerBase.kt:52) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:510) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:422) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:304) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:131) + at org.jetbrains.kotlin.buildtools.internal.jvm.operations.JvmCompilationOperationImpl.compileInProcess(JvmCompilationOperationImpl.kt:423) + at org.jetbrains.kotlin.buildtools.internal.jvm.operations.JvmCompilationOperationImpl.compileInProcess(JvmCompilationOperationImpl.kt:337) + at org.jetbrains.kotlin.buildtools.internal.jvm.operations.JvmCompilationOperationImpl.executeCancellableImpl(JvmCompilationOperationImpl.kt:149) + at org.jetbrains.kotlin.buildtools.internal.jvm.operations.JvmCompilationOperationImpl.executeCancellableImpl(JvmCompilationOperationImpl.kt:69) + at org.jetbrains.kotlin.buildtools.internal.CancellableBuildOperationImpl.executeImpl(CancellableBuildOperationImpl.kt:58) + at org.jetbrains.kotlin.buildtools.internal.BuildOperationImpl.execute(BuildOperationImpl.kt:34) + at org.jetbrains.kotlin.buildtools.internal.KotlinToolchainsImpl$BuildSessionImpl.executeOperation$lambda$1(KotlinToolchainsImpl.kt:70) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) + Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [CoroutineName(MockProject: {}), StandaloneCoroutine{Cancelled}@71422ab, Dispatchers.Default] + + diff --git a/ODY-QA-GATE1-CLOSURE-EVIDENCE.md b/ODY-QA-GATE1-CLOSURE-EVIDENCE.md new file mode 100644 index 0000000..54cf2e2 --- /dev/null +++ b/ODY-QA-GATE1-CLOSURE-EVIDENCE.md @@ -0,0 +1,177 @@ +# ODY QA Gate 1 Closure Evidence +**Datum:** 2026-07-18 00:18 CEST +**Sprint:** ODY-FOUNDATION-0.1a +**Gate:** ODY-QA-GATE-1-CLOSURE-1 +**Status:** ✅ **PASSED** + +--- + +## 1. Build-Ergebnis + +``` +BUILD SUCCESSFUL in 33s +44 actionable tasks: 44 executed +Exit Code: 0 +``` + +- ✅ Alle 44 Tasks erfolgreich +- ✅ Keine Build-Fehler +- ⚠️ Warnung: `Icons.Filled.Send` deprecated ( kosmetisch) + +--- + +## 2. Neuen APK-Daten + +| Feld | Wert | +|---|---| +| **Pfad** | `app/build/outputs/apk/debug/app-debug.apk` | +| **Größe** | 100 MB | +| **SHA-256** | `7f487f053391f241a7c85a4cba4b7efee817120fecad92fb51deef987c177ec0` | +| **Build-Zeit** | 33s | + +--- + +## 3. Manifest-Finalstatus (Gate 1 Anforderungen) + +```xml + + + + + + + + + + + + + + +``` + +### Permission-Gate-Ergebnisse + +| Gate-Vorschrift | Vorher | Nachher | ✅? | +|---|---|---|---| +| INTERNET entfernt | HAD | ✅ removed | ✅ | +| ACCESS_NETWORK_STATE entfernt | HAD | ✅ removed | ✅ | +| BATTERY_STATS entfernt | HAD | ✅ removed | ✅ | +| package="de.ody" entfernt | HAD | ✅ removed | ✅ | +| allowBackup=false | true | ✅ false | ✅ | +| namespace korrekt | `de.ody` | ✅ `de.ody` | ✅ | + +### Grep-Beweise (null matches = removed) + +- `grep -c "INTERNET"` → **0** +- `grep -c "BATTERY"` → **0** +- `grep -c "ACCESS_NETWORK"` → **0** +- `grep -c 'package='` → **0** + +--- + +## 4. Dependencies-Finalstatus + +```kotlin +// Core-Dependencies: +implementation("androidx.compose:compose-bom:2025.05.01") +implementation("androidx.compose.material3:material3") +implementation("androidx.activity:activity-compose:1.10.1") +implementation("androidx.compose.material:material-icons-extended") +implementation("com.google.ai.edge.litertlm:litertlm-android:0.14.0") +implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0") +implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") + +// Test-Dependencies (NEU): ✅ Gate 1 Blocker geschlossen +testImplementation("junit:junit:4.13.2") +``` + +Keine Cloud-Abhängigkeiten außer LiteRT-LM (JNI im Build, keine API-Call-Stubs). + +### Souveränität (unchanged — Gate 4 bestanden) + +| Prüfpunkt | Vorher | Nachher | ✅? | +|---|---|---|---| +| Kein Firebase | ✅ | ✅ | ✅ | +| Kein Gemini API | ✅ | ✅ | ✅ | +| Kein Analytics | ✅ | ✅ | ✅ | +| Kein Telemetry | ✅ | ✅ | ✅ | + +--- + +## 5. Code-Änderungen + +### MainActivity.kt (Offline/Battery Resilienz) + +```kotlin +fun isDeviceOffline(context: Context): Boolean { + return try { + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val net = cm.activeNetwork ?: return true + val cap = cm.getNetworkCapabilities(net) ?: return true + !cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || + !cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } catch (e: SecurityException) { + // Keine ACCESS_NETWORK_STATE → offline-by-design + true + } +} + +fun getBatteryLevel(context: Context): Int { + return try { + val bm = context.getSystemService(BATTERY_SERVICE) as BatteryManager + bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + } catch (e: SecurityException) { + 100 // Fallback — keine Batterie-Info + } +} +``` + +### build.gradle.kts + +```kotlin +// NEU: JUnit für Unit-Tests +testImplementation("junit:junit:4.13.2") +``` + +--- + +## 6. Gate-1-Akzeptanz-Criteria + +| Kriterium | Erfüllt? | Evidence | +|---|---|---| +| Build läuft grün | ✅ | Exit 0, 44/44 tasks | +| APK neu erzeugt | ✅ | `app-debug.apk`, 100 MB | +| SHA256 neu dokumentiert | ✅ | `7f487f05...` in dieser Evidenz & oben | +| Kein INTERNET im Manifest | ✅ | `grep -c "INTERNET" = 0` | +| Kein BATTERY_STATS im Manifest | ✅ | `grep -c "BATTERY" = 0` | +| allowBackup=false | ✅ | Manifest-Zeile überprüft | +| Package-Attribut entfernt | ✅ | `namespace = "de.ody"` in Gradle | +| Evidence-Paket komplett | ✅ | Dieses Dokument | + +--- + +## 7. Fazit + +**Gate 1: PASSED** ✅ + +ODY kann jetzt auf das Testgerät installiert werden. Die beiden Gate-1-Blocker (JUnit + INTERNET-Permission) sind geschlossen. Der Build ist reproduzierbar, die APK ist sauber, das Manifest signalisiert korrekt „offline-by-design". + +### Nächste Schritte + +1. APK auf Test-Samsung installieren und Gate 2 (Echter Offline-Betrieb) testen +2. Echte Gemma-LiteRT-Inferenz implementieren (Aktuell noch Stub) +3. Gate 3-6 nacheinander schließen + +--- + +*Evidenz generiert durch ODY-QA-GATE-1-CLOSURE-1 — 2026-07-18 00:18 CEST* diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..702fb45 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,90 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") +} +android { + namespace = "de.ody" + compileSdk = 36 + defaultConfig { + applicationId = "de.ody" + minSdk = 28 + targetSdk = 36 + versionCode = 2 + versionName = "0.2.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + signingConfigs { + create("release") { + // debug.keystore als stabiler Signing-Key für Diagnose-Builds + // Für Production-Release: ODY_KEYSTORE_PATH / ODY_KEYSTORE_PASSWORD setzen + val customKeystorePath = System.getenv("ODY_KEYSTORE_PATH") + val debugKeystorePath = System.getProperty("user.home") + "/.android/debug.keystore" + val keystoreFile = if (customKeystorePath != null) file(customKeystorePath) else file(debugKeystorePath) + storeFile = keystoreFile + storePassword = System.getenv("ODY_KEYSTORE_PASSWORD") ?: "android" + keyAlias = System.getenv("ODY_KEY_ALIAS") ?: "androiddebugkey" + keyPassword = System.getenv("ODY_KEY_PASSWORD") ?: "android" + } + } + buildFeatures { + compose = true + buildConfig = true + } + buildTypes { + getByName("debug") { + buildConfigField("Boolean", "IS_DEBUG_BUILD", "true") + buildConfigField("Boolean", "IS_AUTARK", "true") + } + getByName("release") { + isMinifyEnabled = true + isShrinkResources = true + isDebuggable = false + buildConfigField("Boolean", "IS_DEBUG_BUILD", "false") + buildConfigField("Boolean", "IS_AUTARK", "true") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("release") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + } + } + sourceSets { + getByName("main").java.srcDirs("src/main/java") + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} +dependencies { + // Compose BOM + implementation(platform("androidx.compose:compose-bom:2025.05.01")) + implementation("androidx.compose.material3:material3") + implementation("androidx.activity:activity-compose:1.10.1") + implementation("androidx.compose.material:material-icons-extended") + // llama.cpp für Android — echte lokale GGUF-Inferenz (ODY 0.2) + implementation("io.github.ljcamargo:llamacpp-kotlin:0.4.0") + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") + configurations.configureEach { + resolutionStrategy { + force("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0") + force("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0") + } + } + // Tests + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.2.1") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..d05d83d --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,94 @@ +# ODY Release Proguard Rules +# ───────────────────────────────────────────────────────────────────────────── + +# ── Kotlin ──────────────────────────────────────────────────────────────────── +-keep class kotlin.** { *; } +-keep class kotlin.Metadata { *; } +-dontwarn kotlin.** +-keepclassmembers class **$WhenMappings { ; } +-keepclassmembers class kotlin.Lazy { *; } + +# ── Kotlin Coroutines ───────────────────────────────────────────────────────── +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepclassmembernames class kotlinx.** { + volatile ; +} +-dontwarn kotlinx.coroutines.** + +# ── Jetpack Compose ─────────────────────────────────────────────────────────── +-keep class androidx.compose.** { *; } +-keepclassmembers class androidx.compose.** { *; } +-dontwarn androidx.compose.** + +# ── ODY Guards (müssen vollständig erhalten bleiben) ───────────────────────── +-keep class de.ody.guards.** { *; } +-keepclassmembers class de.ody.guards.** { *; } + +# ── ODY Runtime Interface + Adapter ────────────────────────────────────────── +-keep interface de.ody.runtime.LocalModelRuntime { *; } +-keep class de.ody.runtime.** { *; } +-keepclassmembers class de.ody.runtime.** { *; } + +# ── ODY Application + MainActivity ─────────────────────────────────────────── +-keep class de.ody.ODYApplication { *; } +-keep class de.ody.app.ODYApplication { *; } +-keep class de.ody.app.MainActivity { *; } + +# ── LlamaContext (librnllama.so JNI) — Fix direct-mmap3 ───────────────────── +# LlamaContext darf NICHT durch R8/ProGuard umbenannt oder entfernt werden, +# da librnllama.so die Klasse per JNI-Name findet. +-keep class org.nehuatl.llamacpp.** { *; } +-keepclassmembers class org.nehuatl.llamacpp.** { *; } +-keepclassmembers class org.nehuatl.llamacpp.LlamaContext { + public (int, java.util.Map); + public long getContext(); + public void setTokenCallback(kotlin.jvm.functions.Function1); + public java.util.Map completion(java.util.Map); + public void stopCompletion(); + public boolean isPredicting(); + public void release(); + native ; +} +-dontwarn org.nehuatl.llamacpp.** + +# ── LiteRT-LM (Google AI Edge) ──────────────────────────────────────────────── +-keep class com.google.ai.edge.** { *; } +-dontwarn com.google.ai.edge.** + +# ── Enum-Klassen vollständig erhalten ───────────────────────────────────────── +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# ── Data-Klassen (Serialisierung, Reflection) ───────────────────────────────── +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +# ── Entferne Logging in Release ─────────────────────────────────────────────── +-assumenosideeffects class android.util.Log { + public static boolean isLoggable(java.lang.String, int); + public static int v(...); + public static int d(...); +} + +# ── Entferne Debug-Assertions ───────────────────────────────────────────────── +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); + static void checkNotNullParameter(java.lang.Object, java.lang.String); + static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); + static void checkNotNullExpressionValue(java.lang.Object, java.lang.String); + static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String); + static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String); + static void throwUninitializedPropertyAccessException(java.lang.String); + static void throwNpe(java.lang.String); + static void throwJavaNpe(); + static void throwIfNull(java.lang.Object, java.lang.String); +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dccc432 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/de/ody/app/MainActivity.kt b/app/src/main/java/de/ody/app/MainActivity.kt new file mode 100644 index 0000000..727cd1e --- /dev/null +++ b/app/src/main/java/de/ody/app/MainActivity.kt @@ -0,0 +1,558 @@ +package de.ody.app + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.BatteryManager +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.view.WindowCompat +import de.ody.BuildConfig +import de.ody.app.ODYApplication +import de.ody.app.engine.AssistantEngine +import de.ody.guards.GuardAlert +import de.ody.guards.GuardType +import de.ody.guards.GuardSeverity +import de.ody.runtime.GenerationRequest +import de.ody.runtime.RuntimeState +import kotlinx.coroutines.Dispatchers +import androidx.lifecycle.lifecycleScope +import android.os.Handler +import android.os.Looper +import androidx.compose.runtime.snapshots.Snapshot +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // P1+P2: Edge-to-Edge aktivieren — Layout verwaltet eigene Insets + // P8: Edge-to-Edge-Test: Safe Insets werden via systemBarsPadding() + imePadding() gesetzt + enableEdgeToEdge() + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { + ODYTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = Color(0xFF0D0D0F), + ) { + ODYChatScreen() + } + } + } + } + + companion object { + fun isDeviceOffline(context: Context): Boolean { + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val net = cm.activeNetwork ?: return true + val cap = cm.getNetworkCapabilities(net) ?: return true + return !cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || + !cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } + + fun getBatteryLevel(context: Context): Int { + val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager + return bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// ODY Chat Screen — kennt ausschließlich AssistantEngine und RuntimeState +// ───────────────────────────────────────────────────────────────────────────── + +@Composable +fun ODYChatScreen() { + val context = LocalContext.current + val app = context.applicationContext as ODYApplication + + val engine: AssistantEngine = remember { app.engine } + // FIX 1: engine.state → engine.runtimeState + val runtimeState by engine.runtimeState.collectAsState() + + // FIX mmap14: rememberCoroutineScope() wird bei Recompose gecancelt + // → generate() wird abgebrochen → currentOutput bleibt leer + // lifecycleScope der Activity überlebt Recomposes + val activity = LocalContext.current as? androidx.activity.ComponentActivity + // FIX mmap17: stabiler lifecycleScope, kein remember() Fallback + val scope = (LocalContext.current as? androidx.activity.ComponentActivity)?.lifecycleScope + ?: rememberCoroutineScope() + var textInput by remember { mutableStateOf("") } + var messages by remember { mutableStateOf>>(emptyList()) } + var currentOutput by remember { mutableStateOf("") } + var batteryLevel by remember { mutableStateOf(0) } + var guardMsg by remember { mutableStateOf(null) } + val listState = rememberLazyListState() + + // ODY-Farbpalette + val bgColor = Color(0xFF0D0D0F) + val surfaceColor = Color(0xFF1A1A1E) + val userBubbleColor = Color(0xFF1E3A5F) + val odyBubbleColor = Color(0xFF1A1A1E) + val accentColor = Color(0xFF4A9EFF) + val warningColor = Color(0xFFE8A020) + val textPrimary = Color(0xFFE8E8EC) + val textSecondary = Color(0xFF8A8A9A) + val guardColor = Color(0xFFCC3333) + val successColor = Color(0xFF2ECC71) + + // FIX 2: loadModel()-Aufruf entfernt — Modell-Laden passiert in ODYApplication.onCreate() + // via engine.start(modelFile). Nur batteryLevel-Initialisierung bleibt. + LaunchedEffect(Unit) { + batteryLevel = MainActivity.getBatteryLevel(context) + } + + val statusText = when { + engine.isSimulation -> "Bereit · Simulation" + runtimeState == RuntimeState.NO_MODEL -> "Kein Modell" + runtimeState == RuntimeState.UNLOADED -> "Bereit · Kein Modell" + runtimeState == RuntimeState.VALIDATING -> "Prüfe Modell..." + runtimeState == RuntimeState.LOADING -> "Lädt Modell..." + runtimeState == RuntimeState.MODEL_LOAD_FAILED -> "Ladefehler — Modell ungültig" + runtimeState == RuntimeState.READY -> "Autark · Lokal aktiv" + runtimeState == RuntimeState.GENERATING -> "Autark · Generiert" + runtimeState == RuntimeState.ERROR -> "Fehler" + else -> "Bereit" + } + + // FIX 3: else-Branch hinzugefügt — when-Ausdruck muss exhaustive sein + val statusColor = when (runtimeState) { + RuntimeState.READY, RuntimeState.GENERATING -> successColor + RuntimeState.LOADING -> warningColor + RuntimeState.ERROR -> guardColor + RuntimeState.UNLOADED -> + if (engine.isSimulation) warningColor else textSecondary + else -> textSecondary + } + + val modelLabel = when { + engine.isSimulation -> "🧠 Kein Modell" + runtimeState == RuntimeState.UNLOADED -> "🧠 Kein Modell" + runtimeState == RuntimeState.LOADING -> "🧠 Lädt..." + else -> "🧠 Lokal" + } + + val showSimBanner = engine.isSimulation + + // P1+P2: systemBarsPadding() für Status-/Navigationsleiste, + // imePadding() für Tastatur — verhindert Layout-Shift und Überlagerung + Column( + modifier = Modifier + .fillMaxSize() + .background(bgColor) + .systemBarsPadding() + .imePadding() // P2: Eingabefeld bleibt über Tastatur + ) { + // ── Header ────────────────────────────────────────────────────────── + Column( + modifier = Modifier + .fillMaxWidth() + .background(bgColor) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "ODY", + color = textPrimary, + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp, + ) + Spacer(modifier = Modifier.width(8.dp)) + Box( + modifier = Modifier + .size(8.dp) + .background(statusColor, RoundedCornerShape(50)) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "🛡 $statusText", + color = statusColor, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = modelLabel, color = textSecondary, fontSize = 12.sp) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "🔋 $batteryLevel%", + color = if (batteryLevel < 20) warningColor else textSecondary, + fontSize = 12.sp, + ) + } + } + // P7: HorizontalDivider statt deprecated Divider + HorizontalDivider( + color = Color(0xFF2A2A2E), + thickness = 0.5.dp, + modifier = Modifier.padding(top = 8.dp), + ) + } + + // ── Simulation-Banner (P4: kompakter) ─────────────────────────────── + if (showSimBanner) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 3.dp) + .background(Color(0xFF2A1F00), RoundedCornerShape(8.dp)) + .padding(horizontal = 14.dp, vertical = 6.dp) // P4: 8→6dp + ) { + Text( + text = "⚠ [Simulation] · Kein Modell geladen", + color = warningColor, + fontSize = 12.sp, // P4: 13→12sp + ) + } + } + + // ── Lade-Indikator ─────────────────────────────────────────────────── + if (runtimeState == RuntimeState.LOADING) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 3.dp) + .background(Color(0xFF0A1F2A), RoundedCornerShape(8.dp)) + .padding(horizontal = 14.dp, vertical = 8.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + color = accentColor, + strokeWidth = 2.dp, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Modell wird geladen...", + color = accentColor, + fontSize = 13.sp, + ) + } + } + } + + // ── Guard-Alert ────────────────────────────────────────────────────── + guardMsg?.let { alert -> + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 3.dp) + .background(Color(0xFF2A0000), RoundedCornerShape(8.dp)) + .padding(horizontal = 14.dp, vertical = 8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "🛡 Guard: ${alert.message}", + color = guardColor, + fontSize = 13.sp, + modifier = Modifier.weight(1f), + ) + TextButton(onClick = { guardMsg = null }) { + Text("✕", color = textSecondary, fontSize = 12.sp) + } + } + } + } + + // ── Chat-Nachrichten ───────────────────────────────────────────────── + Box(modifier = Modifier.weight(1f)) { + // P3: Willkommens-Zustand wenn keine Nachrichten + if (messages.isEmpty() && currentOutput.isEmpty()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "🛡", + fontSize = 40.sp, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "ODY ist bereit.", + color = textPrimary, + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = if (engine.isSimulation) + "Lade ein GGUF-Modell, um echte Antworten zu erhalten." + else + "Stell eine Frage.", + color = textSecondary, + fontSize = 14.sp, + textAlign = TextAlign.Center, + lineHeight = 20.sp, + ) + } + } + + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(vertical = 12.dp), + ) { + items(messages) { (role, content) -> + val isUser = role == "user" + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start, + ) { + Box( + modifier = Modifier + .fillMaxWidth(0.82f) // P5: 280dp fix → 82% Breite + .wrapContentWidth( + if (isUser) Alignment.End else Alignment.Start + ) + .background( + if (isUser) userBubbleColor else odyBubbleColor, + RoundedCornerShape( + topStart = if (isUser) 16.dp else 4.dp, + topEnd = if (isUser) 4.dp else 16.dp, + bottomStart = 16.dp, + bottomEnd = 16.dp, + ) + ) + .padding(horizontal = 14.dp, vertical = 10.dp) + ) { + Column { + if (!isUser) { + Text( + text = if (engine.isSimulation) "ODY [Simulation]" else "ODY", + color = if (engine.isSimulation) warningColor else accentColor, + fontSize = 11.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = 4.dp), + ) + } + Text( + text = content, + color = textPrimary, + fontSize = 15.sp, + lineHeight = 22.sp, + ) + } + } + } + } + + // Streaming-Bubble + if (currentOutput.isNotEmpty()) { + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + ) { + Box( + modifier = Modifier + .fillMaxWidth(0.82f) // P5: konsistent + .background( + odyBubbleColor, + RoundedCornerShape(4.dp, 16.dp, 16.dp, 16.dp), + ) + .padding(horizontal = 14.dp, vertical = 10.dp) + ) { + Column { + Text( + text = if (engine.isSimulation) "ODY [Simulation]" else "ODY", + color = if (engine.isSimulation) warningColor else accentColor, + fontSize = 11.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = 4.dp), + ) + Text( + text = currentOutput + "▌", + color = textPrimary, + fontSize = 15.sp, + lineHeight = 22.sp, + ) + } + } + } + } + } + } + } + + // ── Eingabefeld ────────────────────────────────────────────────────── + Row( + modifier = Modifier + .fillMaxWidth() + .background(surfaceColor) + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + value = textInput, + onValueChange = { textInput = it }, + modifier = Modifier.weight(1f), + placeholder = { + Text( + text = "Frag ODY...", + color = textSecondary, + fontSize = 15.sp, + ) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = accentColor, + unfocusedBorderColor = Color(0xFF3A3A4A), + focusedTextColor = textPrimary, + unfocusedTextColor = textPrimary, + cursorColor = accentColor, + focusedContainerColor = Color(0xFF1E1E24), + unfocusedContainerColor= Color(0xFF1A1A20), + ), + shape = RoundedCornerShape(24.dp), + singleLine = false, + maxLines = 4, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + val isGenerating = runtimeState == RuntimeState.GENERATING + + IconButton( + onClick = { + if (isGenerating) { + engine.cancel() + } else { + val userText = textInput.trim() + if (userText.isBlank()) return@IconButton + + textInput = "" + messages = messages + ("user" to userText) + currentOutput = "" + + scope.launch(kotlinx.coroutines.Dispatchers.Main) { + val guardResult = withContext(Dispatchers.Default) { + de.ody.guards.GuardTest.checkAll(userText) + } + if (guardResult != null) { + guardMsg = guardResult + messages = messages + ("assistant" to "[Guard] ${guardResult.message}") + return@launch + } + + val request = GenerationRequest( + userMessage = userText, + systemPrompt = "Du bist ODY, ein lokales Werkzeug. Antworte präzise und direkt auf Deutsch. Keine Smalltalk-Floskeln.", + history = messages.dropLast(1).takeLast(10), + ) + + // onToken wird von LlamaCppRuntime bereits auf Main-Thread geliefert + // (via singleton mainHandler in LlamaCppRuntime). + // Hier kein Handler mehr noetig. + val genRunId = System.currentTimeMillis() + android.util.Log.i("ODY_STREAM", "ui generate start runId=$genRunId") + engine.generate(request) { token -> + android.util.Log.i("ODY_STREAM", "ui onToken len=${token.length} runId=$genRunId") + currentOutput += token + } + android.util.Log.i("ODY_STREAM", "ui generate done runId=$genRunId output_len=${currentOutput.length}") + + // generate() ist suspend und laeuft in scope.launch (Dispatchers.Main) + // -> State-Mutation hier direkt erlaubt. + messages = messages + ("assistant" to currentOutput) + currentOutput = "" + } + } + }, + modifier = Modifier + .size(48.dp) + .background( + if (isGenerating) guardColor else accentColor, + RoundedCornerShape(50), + ), + ) { + Icon( + imageVector = if (isGenerating) Icons.Default.Stop else Icons.Default.Send, + contentDescription = if (isGenerating) "Stop" else "Senden", + tint = Color.White, + modifier = Modifier.size(20.dp), + ) + } + } + + // ── Debug-Footer (nur in Debug-Builds) ────────────────────────────── + if (BuildConfig.IS_DEBUG_BUILD) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF0A0A0C)) + .padding(horizontal = 16.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "DEBUG · mmap16 · 1784582716 · $runtimeState", + color = textSecondary, + fontSize = 10.sp, + ) + TextButton( + onClick = { + guardMsg = GuardAlert( + type = GuardType.REPETITION, + message = "Guard-Test ausgelöst", + severity = GuardSeverity.LOW, + ) + }, + modifier = Modifier.height(24.dp), + ) { + Text("🛡 Guard-Test", color = textSecondary, fontSize = 10.sp) + } + } + } + } + + // P6: Auto-Scroll-Guard — kein IndexOutOfBoundsException + // P8: Scroll nur bei neuer Nachricht, nicht bei jedem Token + // animateScrollToItem während Streaming erzeugt einen Scroll-Loop + // FIX mmap14: Scroll auch wenn Streaming-Blase erscheint (currentOutput wird nicht-leer) + val scrollTrigger = messages.size + if (currentOutput.isNotEmpty()) 1 else 0 + LaunchedEffect(scrollTrigger) { + val totalItems = messages.size + if (currentOutput.isNotEmpty()) 1 else 0 + if (totalItems > 0) { + listState.scrollToItem(index = (totalItems - 1).coerceAtLeast(0)) + } + } +} diff --git a/app/src/main/java/de/ody/app/MainActivity.kt.bak_ui_sprint1 b/app/src/main/java/de/ody/app/MainActivity.kt.bak_ui_sprint1 new file mode 100644 index 0000000..12b67fd --- /dev/null +++ b/app/src/main/java/de/ody/app/MainActivity.kt.bak_ui_sprint1 @@ -0,0 +1,226 @@ +package de.ody.app + +import android.content.Context +import android.os.BatteryManager +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Autorenew +import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import de.ody.ODYApplication +import de.ody.guards.GuardAlert +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MaterialTheme { + Surface(modifier = Modifier.fillMaxSize()) { + ODYChatScreen() + } + } + } + } + + companion object { + /** + * Prüft Netz-Status. Ohne ACCESS_NETWORK_STATE-Permission + * wird ODY per Design als offline behandelt. + */ + fun isAutarkMode(): Boolean { + // ODY-AUTARK-MODE-0.1A + // ODY ist autark-by-design: + // keine INTERNET-Permission, keine ACCESS_NETWORK_STATE-Permission, + // keine Cloud-Abhängigkeit, keine Telemetrie. + // Sprint 2 (ODY-LLAMACPP-REAL-INFERENCE-1) bleibt vollständig autark. + return true + } + + /** + * Liest Batteriestand. Ohne BATTERY_STATS-Permission fallback zu 100%. + */ + fun getBatteryLevel(context: Context): Int { + return try { + val bm = context.getSystemService(BATTERY_SERVICE) as BatteryManager + bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + } catch (e: SecurityException) { + 100 // Fallback — keine Batterie-Info + } + } + } + +@Composable +fun ODYChatScreen() { + val context = LocalContext.current + val app = context.applicationContext as ODYApplication + val runtime = remember { app.runtime } + val scope = rememberCoroutineScope() + + var textInput by remember { mutableStateOf("") } + var messages by remember { mutableStateOf>>(emptyList()) } + var isStreaming by remember { mutableStateOf(false) } + var currentOutput by remember { mutableStateOf("") } + var offlineIcon by remember { mutableStateOf("📴") } + var batteryLevel by remember { mutableStateOf(0) } + var modelReady by remember { mutableStateOf(false) } + var modelInfo by remember { mutableStateOf("Loading...") } + var guardMsg by remember { mutableStateOf(null) } + val listState = rememberLazyListState() + + LaunchedEffect(Unit) { + offlineIcon = "🛡" + batteryLevel = getBatteryLevel(context) + val s = runtime.status() + modelReady = s.initialized + modelInfo = if (s.initialized) "${s.modelId}" else "Not init" + } + + Column(modifier = Modifier + .fillMaxSize() + .padding(16.dp)) { + + // Header + Row(modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween) { + Text("ODY 0.1a", fontSize = 20.sp, fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Text(offlineIcon, fontSize = 14.sp) + Text("$batteryLevel%", fontSize = 14.sp) + val c = if (modelReady) Color(0xFF4CAF50) else Color(0xFFFFC107) + Box(modifier = Modifier.size(10.dp) + .background(c, RoundedCornerShape(50))) + } + } + + // Guard message + guardMsg?.let { msg -> + Card(modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + colors = CardDefaults.cardColors(containerColor = + MaterialTheme.colorScheme.errorContainer)) { + Column(modifier = Modifier.padding(12.dp)) { + Text(msg.message, fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onErrorContainer) + Text("Guard: ${msg.type} | ${msg.timestamp}", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onErrorContainer.copy(0.7f)) + } + } + } + + // Chat messages + LazyColumn(state = listState, modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp)) { + items(messages, key = { "${it.first}:${it.second.take(30)}" }) { (role, text) -> + Card(modifier = Modifier.fillMaxWidth()) { + Text(role, fontSize = 11.sp, fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary) + Text(text, fontSize = 14.sp) + } + } + if (isStreaming && currentOutput.isNotEmpty()) { + item { + Card(modifier = Modifier.fillMaxWidth()) { + Text("ODY", fontSize = 11.sp, fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary) + Text("$currentOutput▌", fontSize = 14.sp) + } + } + } + } + + // Streaming bar + if (isStreaming) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically) { + Text("⏳ Streaming...", fontSize = 12.sp, + color = MaterialTheme.colorScheme.primary) + FilledTonalButton(onClick = { + scope.launch { runtime.stop() } + messages = messages + Pair("assistant", currentOutput) + currentOutput = "" + isStreaming = false + }) { Text("Stop") } + } + } + + // Input field + Row(modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + value = textInput, onValueChange = { textInput = it }, + modifier = Modifier.weight(1f).padding(end = 8.dp), + placeholder = { Text("Nachricht eingeben...") }, + singleLine = true, shape = RoundedCornerShape(24.dp) + ) + FilledIconButton(onClick = { + if (textInput.isNotBlank()) { + val p = textInput.trim() + textInput = "" + messages = messages + Pair("user", p) + scope.launch { + isStreaming = true + val r = generateResponse(runtime, p) + isStreaming = false + messages = messages + Pair("assistant", r) + } + } + }, enabled = textInput.isNotBlank()) { + Icon(Icons.Filled.Send, "Send", tint = + MaterialTheme.colorScheme.onPrimary) + } + } + + // Debug footer + Surface(modifier = Modifier.padding(top = 8.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(0.5f)) { + Column(modifier = Modifier.padding(8.dp)) { + Text("$modelInfo | ${messages.size} msgs", + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant) + FilledTonalButton(onClick = { + currentOutput = "z.z.z.z.z.z.z" + val g = de.ody.guards.GuardTest() + val v = g.checkRepetitionGuard(currentOutput) + if (v != null) guardMsg = v + }, enabled = !isStreaming, modifier = Modifier.align(Alignment.End)) { + Text("Guard test", fontSize = 10.sp) + } + } + } + } +} +} + +suspend fun generateResponse(runtime: de.ody.runtime.LocalModelRuntime, prompt: String): String = + withContext(Dispatchers.IO) { + val g = de.ody.guards.GuardTest() + val simulated = "ODY: Prompt received: $prompt\n\n" + + "Simulierte Lokale Antwort f\u00fcr Sprint 0.1a.\nOffline-Modus aktiv.\nRAM-Verbrauch wird gemessen." + if (g.checkRepetitionGuard(simulated) != null) { + return@withContext "[Guard: Repetition detected]" + } + simulated + } diff --git a/app/src/main/java/de/ody/app/ODYApplication.kt b/app/src/main/java/de/ody/app/ODYApplication.kt new file mode 100644 index 0000000..2c232c9 --- /dev/null +++ b/app/src/main/java/de/ody/app/ODYApplication.kt @@ -0,0 +1,94 @@ +package de.ody.app +import android.app.ActivityManager +import android.app.Application +import android.util.Log +import de.ody.app.engine.AssistantEngine +import de.ody.app.provisioning.ModelDownloader +import de.ody.app.provisioning.ProvisioningActivity +import java.io.File + +class ODYApplication : Application() { + companion object { + private const val TAG = "ODY.Application" + } + + lateinit var engine: AssistantEngine + private set + + override fun onCreate() { + super.onCreate() + + // ODY-SAMSUNG-LARGEHEAP-SMOKE-1: Memory-Evidence + val actMgr = getSystemService(ActivityManager::class.java) + val rt = Runtime.getRuntime() + Log.i( + "ODY_MEM", + "START memoryClass=${actMgr.memoryClass}MB " + + "largeMemoryClass=${actMgr.largeMemoryClass}MB " + + "maxHeap=${rt.maxMemory() / 1024 / 1024}MB" + ) + + // Fix direct-mmap3: Gesamte Initialisierung in try/catch(Throwable) + // Verhindert App-Tod durch unerwartete native Fehler + try { + engine = AssistantEngine(this) + + // Modell-Datei suchen — falls vorhanden, LlamaCppRuntime laden + val modelFile = ModelDownloader(this).getModelFile( + ProvisioningActivity.MODEL_FILENAME + ) + Log.d(TAG, "onCreate: modelFile=$modelFile exists=${modelFile?.exists()} size=${modelFile?.length()}") + + // Memory vor Modell-Load + val rt2 = Runtime.getRuntime() + Log.i( + "ODY_MEM", + "PRE_LOAD usedHeap=${(rt2.totalMemory() - rt2.freeMemory()) / 1024 / 1024}MB " + + "totalHeap=${rt2.totalMemory() / 1024 / 1024}MB " + + "maxHeap=${rt2.maxMemory() / 1024 / 1024}MB" + ) + + engine.start(modelFile) + + } catch (e: Throwable) { + // Letzter Schutzwall: App darf NIEMALS durch Initialisierungsfehler sterben + Log.e(TAG, "onCreate: Kritischer Fehler bei Initialisierung — App läuft im Notfall-Modus", e) + // Sicherstellen dass engine immer initialisiert ist + if (!::engine.isInitialized) { + try { + engine = AssistantEngine(this) + } catch (e2: Throwable) { + Log.e(TAG, "onCreate: AssistantEngine-Fallback fehlgeschlagen", e2) + } + } + } + } + + /** + * Wird von ProvisioningActivity nach erfolgreichem Download aufgerufen. + * Startet die LlamaCppRuntime mit der frisch heruntergeladenen Datei. + */ + fun onModelReady(modelFile: File) { + Log.d(TAG, "onModelReady: $modelFile exists=${modelFile.exists()} size=${modelFile.length()}") + try { + engine.stop() + engine = AssistantEngine(this) + // Memory vor Modell-Load + val rt2 = Runtime.getRuntime() + Log.i( + "ODY_MEM", + "PRE_LOAD usedHeap=${(rt2.totalMemory() - rt2.freeMemory()) / 1024 / 1024}MB " + + "totalHeap=${rt2.totalMemory() / 1024 / 1024}MB " + + "maxHeap=${rt2.maxMemory() / 1024 / 1024}MB" + ) + engine.start(modelFile) + } catch (e: Throwable) { + Log.e(TAG, "onModelReady: Fehler beim Neustart der Engine", e) + } + } + + override fun onTerminate() { + super.onTerminate() + try { engine.stop() } catch (_: Throwable) {} + } +} diff --git a/app/src/main/java/de/ody/app/ODYTheme.kt b/app/src/main/java/de/ody/app/ODYTheme.kt new file mode 100644 index 0000000..f8dd6b2 --- /dev/null +++ b/app/src/main/java/de/ody/app/ODYTheme.kt @@ -0,0 +1,35 @@ +package de.ody.app + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +/** + * ODY Design Constitution — Farbpalette + * + * Sehr wenig Farbe, viel Ruhe. + * Akzentfarben nur dort, wo sie Information transportieren. + */ +private val ODYColorScheme = darkColorScheme( + background = Color(0xFF0D0D0F), + surface = Color(0xFF1A1A1E), + surfaceVariant = Color(0xFF1E1E24), + primary = Color(0xFF4A9EFF), + primaryContainer = Color(0xFF1E3A5F), + onBackground = Color(0xFFE8E8EC), + onSurface = Color(0xFFE8E8EC), + onPrimary = Color.White, + error = Color(0xFFCC3333), + onError = Color.White, + outline = Color(0xFF3A3A4A), + outlineVariant = Color(0xFF2A2A2E), +) + +@Composable +fun ODYTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = ODYColorScheme, + content = content, + ) +} diff --git a/app/src/main/java/de/ody/app/engine/AssistantEngine.kt b/app/src/main/java/de/ody/app/engine/AssistantEngine.kt new file mode 100644 index 0000000..0b8edbb --- /dev/null +++ b/app/src/main/java/de/ody/app/engine/AssistantEngine.kt @@ -0,0 +1,134 @@ +package de.ody.app.engine + +import android.content.Context +import android.util.Log +import de.ody.app.network.ConnectivityState +import de.ody.app.network.NetworkWatcher +import de.ody.runtime.GenerationRequest +import de.ody.runtime.GenerationResult +import de.ody.runtime.InferenceRuntime +import de.ody.runtime.ModelConfig +import de.ody.runtime.RuntimeState +import de.ody.runtime.llamacpp.LlamaCppRuntime +import de.ody.runtime.simulation.SimulationRuntime +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.io.File + +/** + * ODY AssistantEngine — mmap12 / ODY-RUNTIME-STATEFLOW-BRIDGE-FIX-1 + * + * Root Cause des bisherigen Bugs: + * val runtimeState: StateFlow + * get() = runtime.state + * + * collectAsState() in Compose abonniert den Flow beim ersten Compose-Lauf. + * Wenn runtime danach intern ausgetauscht wird (SimulationRuntime → LlamaCppRuntime), + * weiß Compose nicht, dass es einen anderen Flow sammeln muss. + * Ergebnis: UI lauscht auf altem SimulationRuntime.state, Engine nutzt neue Runtime. + * + * Fix: + * - Stabiler engine-owned _runtimeState MutableStateFlow — wird NIEMALS ausgetauscht + * - bindRuntime() forwarded newRuntime.state → _runtimeState via runtimeStateJob + * - runtimeStateJob wird bei jedem Runtime-Wechsel gecancelt und neu gestartet + * - Lifecycle-Cleanup in release(): runtimeStateJob canceln, Runtime freigeben + */ +class AssistantEngine(private val context: Context) { + + companion object { + private const val TAG = "AssistantEngine" + } + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + val networkWatcher = NetworkWatcher(context) + val connectivityState: StateFlow = networkWatcher.state + + private var runtime: InferenceRuntime = SimulationRuntime() + + // Stabiler engine-owned StateFlow — collectAsState() in Compose abonniert diesen + // ein einziges Mal und bleibt darauf. Nur die Werte ändern sich, nie der Flow selbst. + private val _runtimeState = MutableStateFlow(RuntimeState.UNLOADED) + val runtimeState: StateFlow = _runtimeState.asStateFlow() + + // Job der newRuntime.state → _runtimeState forwarded + // Wird bei jedem Runtime-Wechsel gecancelt und neu gestartet + private var runtimeStateJob: Job? = null + + val isSimulation: Boolean + get() = runtime is SimulationRuntime + + /** + * Bindet eine neue Runtime: cancelt den alten Forward-Job, + * setzt runtime, startet neuen Forward-Job. + */ + private fun bindRuntime(newRuntime: InferenceRuntime) { + runtimeStateJob?.cancel() + runtime = newRuntime + runtimeStateJob = scope.launch { + newRuntime.state.collect { state -> + Log.d(TAG, "runtimeState → $state (via ${newRuntime.javaClass.simpleName})") + _runtimeState.value = state + } + } + Log.i(TAG, "bindRuntime: ${newRuntime.javaClass.simpleName} gebunden") + } + + fun start(modelFile: File? = null) { + networkWatcher.start() + if (modelFile != null && modelFile.exists()) { + Log.i(TAG, "start: Modell gefunden → LlamaCppRuntime: ${modelFile.name} (${modelFile.length() / 1024 / 1024} MB)") + val llamaRuntime = LlamaCppRuntime(context) + bindRuntime(llamaRuntime) + scope.launch { + llamaRuntime.loadModel( + modelPath = modelFile.absolutePath, + config = ModelConfig( + modelPath = modelFile.absolutePath, + modelId = modelFile.nameWithoutExtension, + contextLength = 4096 + ) + ) + } + } else { + Log.i(TAG, "start: Kein Modell → SimulationRuntime aktiv") + bindRuntime(SimulationRuntime()) + } + } + + fun stop() { + networkWatcher.stop() + scope.launch { runtime.unload() } + } + + suspend fun generate( + request: GenerationRequest, + onToken: (String) -> Unit + ): Result = runtime.generate( + request = request, + onToken = onToken + ) + + fun cancel() = runtime.cancel() + + /** + * Vollständiges Lifecycle-Cleanup: + * - runtimeStateJob canceln (kein weiteres Forwarding) + * - Runtime via unload() freigeben + */ + fun release() { + runtimeStateJob?.cancel() + runtimeStateJob = null + scope.launch { + try { runtime.unload() } catch (e: Throwable) { + Log.w(TAG, "release/unload: ${e.message}") + } + } + } +} diff --git a/app/src/main/java/de/ody/app/network/ConnectivityState.kt b/app/src/main/java/de/ody/app/network/ConnectivityState.kt new file mode 100644 index 0000000..d7c7c99 --- /dev/null +++ b/app/src/main/java/de/ody/app/network/ConnectivityState.kt @@ -0,0 +1,16 @@ +package de.ody.app.network + +/** + * ODY Connectivity State + * + * NetworkWatcher meldet ausschließlich diesen Zustand. + * Keine Geschäftslogik hier — nur Fakten. + * + * METERED: darf später Downloads / Cloud-Eskalation blockieren. + */ +enum class ConnectivityState { + UNKNOWN, // Initialer Zustand, noch nicht geprüft + OFFLINE, // Kein Netz — lokale Inferenz ist der normale Fallback + ONLINE, // Netz verfügbar und validiert + METERED // Netz verfügbar, aber kostenpflichtig (Mobilfunk) +} diff --git a/app/src/main/java/de/ody/app/network/NetworkWatcher.kt b/app/src/main/java/de/ody/app/network/NetworkWatcher.kt new file mode 100644 index 0000000..716007c --- /dev/null +++ b/app/src/main/java/de/ody/app/network/NetworkWatcher.kt @@ -0,0 +1,94 @@ +package de.ody.app.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * ODY NetworkWatcher + * + * Meldet nur den Konnektivitätszustand — keine Geschäftslogik. + * Kein Foreground-Service, kein Wake-Lock. + * + * Bewertet Konnektivität anhand tatsächlicher Internet-Capability + * (NET_CAPABILITY_INTERNET + NET_CAPABILITY_VALIDATED), + * nicht nur anhand einer bestehenden WLAN- oder Mobilfunkverbindung. + * + * Security-Gate: Benötigt ACCESS_NETWORK_STATE (keine sensitive Permission). + */ +class NetworkWatcher(private val context: Context) { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + private val _state = MutableStateFlow(ConnectivityState.UNKNOWN) + val state: StateFlow = _state.asStateFlow() + + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onCapabilitiesChanged( + network: Network, + caps: NetworkCapabilities + ) { + scope.launch { _state.value = caps.toConnectivityState() } + } + + override fun onLost(network: Network) { + scope.launch { _state.value = ConnectivityState.OFFLINE } + } + + override fun onUnavailable() { + scope.launch { _state.value = ConnectivityState.OFFLINE } + } + } + + fun start() { + // Initialzustand sofort setzen + _state.value = currentState() + + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + try { + connectivityManager.registerNetworkCallback(request, networkCallback) + } catch (e: Exception) { + // Fallback: einmalige Abfrage ohne Live-Updates + _state.value = currentState() + } + } + + fun stop() { + try { + connectivityManager.unregisterNetworkCallback(networkCallback) + } catch (_: Exception) {} + } + + private fun currentState(): ConnectivityState { + val network = connectivityManager.activeNetwork ?: return ConnectivityState.OFFLINE + val caps = connectivityManager.getNetworkCapabilities(network) + ?: return ConnectivityState.OFFLINE + + return caps.toConnectivityState() + } + + private fun NetworkCapabilities.toConnectivityState(): ConnectivityState { + val hasInternet = hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + val isValidated = hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + + if (!hasInternet || !isValidated) return ConnectivityState.OFFLINE + + // METERED: Mobilfunk oder explizit als metered markiert + val isMetered = !hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + return if (isMetered) ConnectivityState.METERED else ConnectivityState.ONLINE + } +} diff --git a/app/src/main/java/de/ody/app/provisioning/ModelDownloader.kt b/app/src/main/java/de/ody/app/provisioning/ModelDownloader.kt new file mode 100644 index 0000000..dcc9afc --- /dev/null +++ b/app/src/main/java/de/ody/app/provisioning/ModelDownloader.kt @@ -0,0 +1,177 @@ +package de.ody.app.provisioning + +import android.content.Context +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import java.io.File +import java.net.HttpURLConnection +import java.net.URL +import java.security.MessageDigest + +/** + * ModelDownloader — atomarer Download mit .part-Datei. + * + * Ablauf: + * URL → model.gguf.part → Download vollständig → flush → Magic+Größe prüfen + * → atomar zu model.gguf umbenennen + * + * Eine vorhandene .part-Datei gilt niemals als Modell. + * Eine vorhandene .gguf wird nur übersprungen wenn sie die Basisvalidierung besteht. + */ +class ModelDownloader(private val context: Context) { + companion object { + private const val TAG = "ModelDownloader" + private const val BUFFER_SIZE = 65_536 + private const val MAX_REDIRECTS = 10 + private val GGUF_MAGIC = byteArrayOf(0x47, 0x47, 0x55, 0x46) + } + + sealed class DownloadProgress { + data class Progress( + val bytesDownloaded: Long, + val totalBytes: Long, + val percent: Int, + ) : DownloadProgress() + data class Verifying(val message: String = "Prüfe Datei...") : DownloadProgress() + data class Success(val file: File) : DownloadProgress() + data class Error(val message: String, val cause: Throwable? = null) : DownloadProgress() + } + + private fun getModelsDir(): File { + val dir = File(context.filesDir, "models") + dir.mkdirs() + return dir + } + + /** + * Prüft ob eine valide (nicht-part) Modelldatei existiert. + * Gibt null zurück wenn nicht vorhanden oder zu klein. + */ + fun getModelFile(fileName: String): File? { + // Prüfe alle bekannten Speicherpfade — ältere Builds nutzten externalFilesDir + val candidates = listOfNotNull( + File(getModelsDir(), fileName), // aktuell: filesDir/models/ + context.getExternalFilesDir("models")?.let { File(it, fileName) }, // alt: externalFilesDir/models/ + ) + for (f in candidates) { + if (!f.exists() || !f.isFile) continue + if (f.length() < 100L * 1024 * 1024) continue + val magic = try { f.inputStream().use { it.readNBytes(4) } } catch (e: Exception) { continue } + if (!magic.contentEquals(GGUF_MAGIC)) continue + Log.d(TAG, "getModelFile: valide Datei gefunden ${f.absolutePath} (${f.length() / 1024 / 1024} MB)") + return f + } + return null + } + + fun download( + url: String, + fileName: String, + expectedSha256: String? = null, + ): Flow = flow { + val destFile = File(getModelsDir(), fileName) + val partFile = File(getModelsDir(), "$fileName.part") + + // Alte .part-Datei löschen + partFile.takeIf { it.exists() }?.delete() + + try { + // Redirect-Kette manuell auflösen + var currentUrl = url + var connection: HttpURLConnection? = null + var redirectCount = 0 + while (redirectCount < MAX_REDIRECTS) { + val conn = URL(currentUrl).openConnection() as HttpURLConnection + conn.instanceFollowRedirects = false + conn.connectTimeout = 15_000 + conn.readTimeout = 60_000 + conn.setRequestProperty("User-Agent", "ODY/0.2 (Android)") + conn.connect() + val code = conn.responseCode + if (code in 301..308) { + val location = conn.getHeaderField("Location") + conn.disconnect() + if (location.isNullOrBlank()) break + currentUrl = location + redirectCount++ + continue + } + connection = conn + break + } + val conn = connection ?: throw Exception("Zu viele Redirects oder keine Verbindung") + val totalBytes = conn.contentLengthLong + var bytesDownloaded = 0L + + conn.inputStream.buffered(BUFFER_SIZE).use { input -> + partFile.outputStream().buffered(BUFFER_SIZE).use { output -> + val buffer = ByteArray(BUFFER_SIZE) + var bytes: Int + while (input.read(buffer).also { bytes = it } != -1) { + output.write(buffer, 0, bytes) + bytesDownloaded += bytes + val percent = if (totalBytes > 0) + ((bytesDownloaded * 100) / totalBytes).toInt() else -1 + emit(DownloadProgress.Progress(bytesDownloaded, totalBytes, percent.coerceAtLeast(0))) + } + output.flush() + } + } + conn.disconnect() + + // Basisvalidierung der .part-Datei + emit(DownloadProgress.Verifying("Prüfe Datei...")) + if (partFile.length() < 100L * 1024 * 1024) { + partFile.delete() + emit(DownloadProgress.Error("Download unvollständig: ${partFile.length() / 1024 / 1024} MB")) + return@flow + } + val magic = partFile.inputStream().use { it.readNBytes(4) } + if (!magic.contentEquals(GGUF_MAGIC)) { + partFile.delete() + emit(DownloadProgress.Error("Kein GGUF-Format — Download beschädigt")) + return@flow + } + + // SHA-256 (optional) + if (expectedSha256 != null) { + emit(DownloadProgress.Verifying("Prüfe Integrität (SHA-256)...")) + val actual = sha256(partFile) + if (!actual.equals(expectedSha256, ignoreCase = true)) { + partFile.delete() + emit(DownloadProgress.Error("SHA-256 ungültig — Datei gelöscht")) + return@flow + } + } + + // Atomares Umbenennen + if (destFile.exists()) destFile.delete() + if (!partFile.renameTo(destFile)) { + partFile.delete() + emit(DownloadProgress.Error("Umbenennen fehlgeschlagen")) + return@flow + } + + Log.i(TAG, "Download abgeschlossen: ${destFile.name} (${destFile.length() / 1024 / 1024} MB)") + emit(DownloadProgress.Success(destFile)) + + } catch (e: Exception) { + Log.e(TAG, "Download-Fehler", e) + partFile.takeIf { it.exists() }?.delete() + emit(DownloadProgress.Error("Download fehlgeschlagen: ${e.message}", e)) + } + }.flowOn(Dispatchers.IO) + + private fun sha256(file: File): String { + val digest = MessageDigest.getInstance("SHA-256") + file.inputStream().buffered(BUFFER_SIZE).use { input -> + val buf = ByteArray(BUFFER_SIZE) + var n: Int + while (input.read(buf).also { n = it } != -1) digest.update(buf, 0, n) + } + return digest.digest().joinToString("") { "%02x".format(it) } + } +} diff --git a/app/src/main/java/de/ody/app/provisioning/ProvisioningActivity.kt b/app/src/main/java/de/ody/app/provisioning/ProvisioningActivity.kt new file mode 100644 index 0000000..8d555f1 --- /dev/null +++ b/app/src/main/java/de/ody/app/provisioning/ProvisioningActivity.kt @@ -0,0 +1,297 @@ +package de.ody.app.provisioning + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import de.ody.app.MainActivity +import de.ody.app.ODYApplication +import de.ody.app.ODYTheme +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File + +class ProvisioningActivity : ComponentActivity() { + companion object { + const val MODEL_URL = + "https://huggingface.co/unsloth/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q4_K_M.gguf" + const val MODEL_FILENAME = "Qwen3-1.7B-Q4_K_M.gguf" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val downloader = ModelDownloader(this) + + // Nur Datei-Existenz + Basisvalidierung prüfen — kein SharedPreferences-Flag. + // Datei überlebt Deinstallation in getFilesDir()/models/. + if (downloader.getModelFile(MODEL_FILENAME) != null) { + launchMain() + return + } + + setContent { + ODYTheme { + ProvisioningScreen( + onStartDownload = { + setContent { ODYTheme { DownloadScreen(downloader) { modelFile -> onModelReady(modelFile) } } } + }, + onImportFile = { uri -> + setContent { ODYTheme { ImportScreen(uri, downloader) { modelFile -> onModelReady(modelFile) } } } + } + ) + } + } + } + + private fun onModelReady(modelFile: File) { + (applicationContext as ODYApplication).onModelReady(modelFile) + launchMain() + } + + private fun launchMain() { + startActivity(Intent(this, MainActivity::class.java)) + finish() + } +} + +// ─── Provisioning-Screen (Auswahl: Download oder Import) ───────────────────── +@Composable +fun ProvisioningScreen( + onStartDownload: () -> Unit, + onImportFile: (Uri) -> Unit, +) { + val bg = Color(0xFF0D0D0F) + val textPrimary = Color(0xFFE8E8EA) + val textSecondary = Color(0xFF8A8A8E) + val accent = Color(0xFF4A9EFF) + + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.OpenDocument() + ) { uri -> + if (uri != null) onImportFile(uri) + } + + Box( + modifier = Modifier.fillMaxSize().background(bg).systemBarsPadding(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(32.dp) + ) { + Text("ODY", color = textPrimary, fontSize = 48.sp, fontWeight = FontWeight.Bold, letterSpacing = 4.sp) + Spacer(Modifier.height(8.dp)) + Text("Dein lokaler KI-Assistent", color = textSecondary, fontSize = 16.sp) + Spacer(Modifier.height(48.dp)) + + Surface(color = Color(0xFF1A1A1C), shape = RoundedCornerShape(12.dp), modifier = Modifier.fillMaxWidth()) { + Column(Modifier.padding(20.dp)) { + Text("Einmalige Einrichtung", color = textPrimary, fontSize = 16.sp, fontWeight = FontWeight.Medium) + Spacer(Modifier.height(8.dp)) + Text( + "Wähle eine Option um das Sprachmodell einzurichten. " + + "Danach läuft ODY vollständig lokal — kein Internet nötig.", + color = textSecondary, fontSize = 14.sp, lineHeight = 20.sp + ) + Spacer(Modifier.height(4.dp)) + Text("Modell: Qwen3 1.7B · Q4_K_M · ~1 GB", color = textSecondary.copy(alpha = 0.7f), fontSize = 12.sp) + } + } + + Spacer(Modifier.height(24.dp)) + + Button( + onClick = onStartDownload, + colors = ButtonDefaults.buttonColors(containerColor = accent), + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth().height(52.dp) + ) { + Text("Modell herunterladen (~1 GB)", color = Color.White, fontSize = 15.sp, fontWeight = FontWeight.Medium) + } + + Spacer(Modifier.height(12.dp)) + + OutlinedButton( + onClick = { launcher.launch(arrayOf("application/octet-stream", "*/*")) }, + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth().height(52.dp), + colors = ButtonDefaults.outlinedButtonColors(contentColor = textSecondary), + ) { + Text("GGUF-Datei importieren", fontSize = 15.sp) + } + Spacer(Modifier.height(8.dp)) + Text( + "Importiere eine vorhandene .gguf-Datei aus Downloads, Cloud oder USB.", + color = textSecondary.copy(alpha = 0.6f), + fontSize = 12.sp, + textAlign = TextAlign.Center, + ) + } + } +} + +// ─── Download-Screen ────────────────────────────────────────────────────────── +@Composable +fun DownloadScreen( + downloader: ModelDownloader, + onComplete: (File) -> Unit, +) { + val bg = Color(0xFF0D0D0F) + val textPrimary = Color(0xFFE8E8EA) + val textSecondary = Color(0xFF8A8A8E) + val accent = Color(0xFF4A9EFF) + + var progress by remember { mutableIntStateOf(0) } + var statusText by remember { mutableStateOf("Verbinde...") } + var bytesText by remember { mutableStateOf("") } + var isError by remember { mutableStateOf(false) } + var retryKey by remember { mutableIntStateOf(0) } + val scope = rememberCoroutineScope() + + LaunchedEffect(retryKey) { + isError = false; statusText = "Verbinde..."; bytesText = ""; progress = 0 + scope.launch { + downloader.download( + url = ProvisioningActivity.MODEL_URL, + fileName = ProvisioningActivity.MODEL_FILENAME, + ).collect { state -> + when (state) { + is ModelDownloader.DownloadProgress.Progress -> { + progress = state.percent + val mb = state.bytesDownloaded / 1024 / 1024 + val total = if (state.totalBytes > 0) state.totalBytes / 1024 / 1024 else 0 + statusText = "Lade Modell herunter..." + bytesText = if (total > 0) "$mb MB / $total MB" else "$mb MB" + } + is ModelDownloader.DownloadProgress.Verifying -> { statusText = state.message; bytesText = "" } + is ModelDownloader.DownloadProgress.Success -> { statusText = "Bereit."; progress = 100; onComplete(state.file) } + is ModelDownloader.DownloadProgress.Error -> { isError = true; statusText = state.message } + } + } + } + } + + Box(modifier = Modifier.fillMaxSize().background(bg).systemBarsPadding(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(32.dp)) { + Text("ODY", color = textPrimary, fontSize = 48.sp, fontWeight = FontWeight.Bold, letterSpacing = 4.sp) + Spacer(Modifier.height(48.dp)) + Text(statusText, color = if (isError) Color(0xFFE57373) else textPrimary, fontSize = 16.sp, textAlign = TextAlign.Center) + if (bytesText.isNotEmpty()) { Spacer(Modifier.height(4.dp)); Text(bytesText, color = textSecondary, fontSize = 13.sp) } + Spacer(Modifier.height(24.dp)) + if (!isError) { + LinearProgressIndicator( + progress = { progress / 100f }, + modifier = Modifier.fillMaxWidth().height(6.dp), + color = accent, trackColor = Color(0xFF2A2A2C) + ) + Spacer(Modifier.height(8.dp)) + Text("$progress %", color = textSecondary, fontSize = 13.sp) + } else { + Spacer(Modifier.height(16.dp)) + Button(onClick = { retryKey++ }, colors = ButtonDefaults.buttonColors(containerColor = accent), shape = RoundedCornerShape(12.dp)) { + Text("Erneut versuchen", color = Color.White) + } + } + } + } +} + +// ─── Import-Screen (SAF → in ODY-Speicher kopieren) ────────────────────────── +@Composable +fun ImportScreen( + sourceUri: Uri, + downloader: ModelDownloader, + onComplete: (File) -> Unit, +) { + val context = LocalContext.current + val bg = Color(0xFF0D0D0F) + val textPrimary = Color(0xFFE8E8EA) + val textSecondary = Color(0xFF8A8A8E) + val accent = Color(0xFF4A9EFF) + + var statusText by remember { mutableStateOf("Importiere...") } + var isError by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + scope.launch { + withContext(Dispatchers.IO) { + try { + // Dateiname aus URI ermitteln + val fileName = sourceUri.lastPathSegment + ?.substringAfterLast("/") + ?.substringAfterLast("%2F") + ?.let { if (it.endsWith(".gguf", ignoreCase = true)) it else "imported_model.gguf" } + ?: "imported_model.gguf" + + val destDir = File(context.filesDir, "models").also { it.mkdirs() } + val partFile = File(destDir, "$fileName.part") + val destFile = File(destDir, fileName) + + partFile.takeIf { it.exists() }?.delete() + + // SAF-URI → lokale Kopie + context.contentResolver.openInputStream(sourceUri)?.use { input -> + partFile.outputStream().buffered(65_536).use { output -> + input.copyTo(output, bufferSize = 65_536) + output.flush() + } + } ?: throw Exception("Datei konnte nicht geöffnet werden") + + // Basisvalidierung + if (partFile.length() < 100L * 1024 * 1024) { + partFile.delete() + withContext(Dispatchers.Main) { isError = true; statusText = "Datei zu klein (min 100 MB)" } + return@withContext + } + val magic = partFile.inputStream().use { it.readNBytes(4) } + val ggufMagic = byteArrayOf(0x47, 0x47, 0x55, 0x46) + if (!magic.contentEquals(ggufMagic)) { + partFile.delete() + withContext(Dispatchers.Main) { isError = true; statusText = "Kein GGUF-Format" } + return@withContext + } + + if (destFile.exists()) destFile.delete() + partFile.renameTo(destFile) + + withContext(Dispatchers.Main) { + statusText = "Import abgeschlossen." + onComplete(destFile) + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { isError = true; statusText = "Import fehlgeschlagen: ${e.message}" } + } + } + } + } + + Box(modifier = Modifier.fillMaxSize().background(bg).systemBarsPadding(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(32.dp)) { + Text("ODY", color = textPrimary, fontSize = 48.sp, fontWeight = FontWeight.Bold, letterSpacing = 4.sp) + Spacer(Modifier.height(48.dp)) + Text(statusText, color = if (isError) Color(0xFFE57373) else textPrimary, fontSize = 16.sp, textAlign = TextAlign.Center) + if (!isError) { + Spacer(Modifier.height(24.dp)) + CircularProgressIndicator(color = accent) + } + } + } +} diff --git a/app/src/main/java/de/ody/guards/GuardResult.kt b/app/src/main/java/de/ody/guards/GuardResult.kt new file mode 100644 index 0000000..006022c --- /dev/null +++ b/app/src/main/java/de/ody/guards/GuardResult.kt @@ -0,0 +1,24 @@ +package de.ody.guards + +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +/** + * Guard-Verdict: Was hat der Guard festgestellt? + */ +enum class GuardVerdict { + PASS, + STOP, + WARN, +} + +/** + * Guard-Typen in Sprint 0.1a + */ +enum class GuardType { + REPETITION, + TIMEOUT, + TOKEN_LIMIT, + STOP_BUTTON, +} diff --git a/app/src/main/java/de/ody/guards/GuardTest.kt b/app/src/main/java/de/ody/guards/GuardTest.kt new file mode 100644 index 0000000..950cb09 --- /dev/null +++ b/app/src/main/java/de/ody/guards/GuardTest.kt @@ -0,0 +1,107 @@ +package de.ody.guards + +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +data class GuardAlert( + val type: GuardType, + val message: String, + val severity: GuardSeverity = GuardSeverity.MEDIUM, + val timestamp: String = Instant.now() + .atZone(ZoneId.systemDefault()) + .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + val evidence: String? = null, +) { + val isStopTriggered: Boolean get() = severity == GuardSeverity.HIGH || severity == GuardSeverity.CRITICAL +} + +enum class GuardSeverity { + LOW, MEDIUM, HIGH, CRITICAL +} + +class GuardTest { + + companion object { + private val instance = GuardTest() + + /** Führt alle Guards aus. Gibt ersten ausgelösten Alert zurück, oder null. */ + fun checkAll(text: String): GuardAlert? { + return instance.checkRepetitionGuard(text) + } + } + + fun checkRepetitionGuard(text: String, maxRepetitions: Int = 3): GuardAlert? { + val words = text.split(Regex("\\s+")).filter { it.isNotEmpty() } + for (i in 0 until words.size - maxRepetitions) { + val run = words.subList(i, i + maxRepetitions) + if (run.all { it.equals(run[0], ignoreCase = true) }) { + return GuardAlert( + type = GuardType.REPETITION, + message = "Repetition Guard: Wort '${run[0]}' wiederholt $maxRepetitions hintereinander", + severity = GuardSeverity.HIGH, + evidence = "Position $i: " + run.joinToString(" ") + ) + } + } + if (text.length >= 10) { + val shortPatterns = listOf("z.", "a.", "b.", "c.", "x.", "y.") + for (pattern in shortPatterns) { + var repeats = 0 + var pos = 0 + while (pos + pattern.length <= text.length) { + if (text.substring(pos, pos + pattern.length).equals(pattern, ignoreCase = true)) { + repeats++ + pos += pattern.length + } else { + break + } + } + if (repeats >= maxRepetitions) { + return GuardAlert( + type = GuardType.REPETITION, + message = "Repetition Guard: Muster '$pattern' $maxRepetitions hintereinander", + severity = GuardSeverity.CRITICAL, + evidence = "Gefunden: ${text.take(80)}" + ) + } + } + } + return null + } + + fun checkTimeoutGuard(elapsedMs: Long, thresholdMs: Long = 60000L): GuardAlert? { + if (elapsedMs >= thresholdMs) { + return GuardAlert( + type = GuardType.TIMEOUT, + message = "Timeout Guard: Generierung überschritt ${thresholdMs / 1000}s", + severity = GuardSeverity.MEDIUM, + evidence = "Elapsed: $elapsedMs ms" + ) + } + return null + } + + fun checkTokenLimitGuard(tokens: List, limit: Int = 512): GuardAlert? { + if (tokens.size >= limit) { + return GuardAlert( + type = GuardType.TOKEN_LIMIT, + message = "Token Limit Guard: $limit Token erreicht", + severity = GuardSeverity.MEDIUM, + evidence = "Token count: ${tokens.size}" + ) + } + return null + } + + fun checkStopButton(stopRequested: Boolean): GuardAlert? { + if (stopRequested) { + return GuardAlert( + type = GuardType.STOP_BUTTON, + message = "Stop Guard: Benutzer hat Generation gestoppt", + severity = GuardSeverity.LOW, + ) + } + return null + } +} diff --git a/app/src/main/java/de/ody/runtime/GenerationRequest.kt b/app/src/main/java/de/ody/runtime/GenerationRequest.kt new file mode 100644 index 0000000..afd4708 --- /dev/null +++ b/app/src/main/java/de/ody/runtime/GenerationRequest.kt @@ -0,0 +1,16 @@ +package de.ody.runtime + +/** + * Eine vollständige Anfrage an die InferenceRuntime. + * + * @param userMessage Die aktuelle Nutzernachricht + * @param systemPrompt Optionaler System-Prompt (ODY-Identität) + * @param history Vorherige Nachrichten als (role, content)-Paare + * @param maxNewTokens Maximale Anzahl neuer Tokens (Standard: 256) + */ +data class GenerationRequest( + val userMessage: String, + val systemPrompt: String? = null, + val history: List> = emptyList(), + val maxNewTokens: Int = 256, +) diff --git a/app/src/main/java/de/ody/runtime/GenerationResult.kt b/app/src/main/java/de/ody/runtime/GenerationResult.kt new file mode 100644 index 0000000..3896ec3 --- /dev/null +++ b/app/src/main/java/de/ody/runtime/GenerationResult.kt @@ -0,0 +1,18 @@ +package de.ody.runtime + +/** + * Ergebnis einer abgeschlossenen Generierung. + * + * @param fullText Vollständiger generierter Text + * @param tokenCount Anzahl generierter Tokens + * @param durationMs Gesamtdauer in Millisekunden + * @param tokensPerSecond Durchschnittliche Generierungsgeschwindigkeit + * @param cancelledByUser true, wenn cancel() aufgerufen wurde + */ +data class GenerationResult( + val fullText: String, + val tokenCount: Int, + val durationMs: Long, + val tokensPerSecond: Float, + val cancelledByUser: Boolean = false, +) diff --git a/app/src/main/java/de/ody/runtime/InferenceRuntime.kt b/app/src/main/java/de/ody/runtime/InferenceRuntime.kt new file mode 100644 index 0000000..6ef73fc --- /dev/null +++ b/app/src/main/java/de/ody/runtime/InferenceRuntime.kt @@ -0,0 +1,62 @@ +package de.ody.runtime + +import kotlinx.coroutines.flow.StateFlow + +/** + * InferenceRuntime — stabiler Vertrag für alle lokalen Inferenz-Implementierungen. + * + * Die UI kennt ausschließlich dieses Interface. + * Sie kennt NIEMALS LlamaCppRuntime, SimulationRuntime oder andere Implementierungen. + * + * Implementierungen: + * - SimulationRuntime : Fallback ohne echtes Modell (immer verfügbar) + * - LlamaCppRuntime : Echte lokale Inferenz via llama.cpp JNI (ab 0.2) + * + * Lebenszyklus: + * UNLOADED → loadModel() → READY → generate() → READY → unload() → UNLOADED + * + * Regel: Die UI liest ausschließlich [state]. Sie ruft niemals internen Zustand ab. + */ +interface InferenceRuntime { + + /** + * Beobachtbarer Lebenszyklus-Zustand. + * Die UI reagiert auf Änderungen dieses Flows. + */ + val state: StateFlow + + /** + * Lädt ein GGUF-Modell aus dem lokalen App-Speicher. + * Wechselt state: UNLOADED → LOADING → READY | ERROR + */ + suspend fun loadModel( + modelPath: String, + config: ModelConfig, + ): Result + + /** + * Generiert eine Antwort und streamt Token via [onToken]-Callback. + * Wechselt state: READY → GENERATING → READY | ERROR + * + * @param request Vollständige Anfrage inkl. System-Prompt und History + * @param onToken Wird für jedes generierte Token aufgerufen (Main-Thread-sicher) + */ + suspend fun generate( + request: GenerationRequest, + onToken: (String) -> Unit, + ): Result + + /** + * Bricht eine laufende Generierung sofort ab. + * Wechselt state: GENERATING → READY + * Kein-Op wenn state != GENERATING. + */ + fun cancel() + + /** + * Gibt das geladene Modell und alle JNI-Ressourcen frei. + * Wechselt state: READY → UNLOADED + * Kein-Op wenn state == UNLOADED. + */ + suspend fun unload() +} diff --git a/app/src/main/java/de/ody/runtime/LiteRtRuntimeAdapter.kt b/app/src/main/java/de/ody/runtime/LiteRtRuntimeAdapter.kt new file mode 100644 index 0000000..8f699b3 --- /dev/null +++ b/app/src/main/java/de/ody/runtime/LiteRtRuntimeAdapter.kt @@ -0,0 +1,139 @@ +package de.ody.runtime + +import android.content.Context +import android.os.BatteryManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.withTimeout + +/** + * LiteRT-LM Runtime Adapter — einzige Implementierung in Sprint 0.1a. + * + * Verwendet die LiteRT-LM-Kotlin-API für lokale Inferenz. + * In späteren Sprints wird LLamaCppRuntimeAdapter als zweite Implementierung folgen. + * + * WICHTIG: Die genaue API (Pakete, Klassen, Methoden) orientiert sich an der + * finalen LiteRT-LM-Dokumentation. Diese Skeleton-Klasse definiert die + * Integrationsschicht und muss je nach tatsächlicher LiteRT-LM-API angepasst werden. + */ +class LiteRtRuntimeAdapter(private val context: Context) : LocalModelRuntime { + + // Platzhalter für das actuale LiteRT-LM-Modellobjekt. + // Die genaue Klasse hängt von der LiteRT-LM-Dependency ab. + private var loadedModel: Any? = null + private var currentConfig: LocalModelRuntime.ModelConfig? = null + private var isStreaming = false + private var cancelFlag = false + + override suspend fun initialize(config: LocalModelRuntime.ModelConfig): LocalModelRuntime.InitResult { + val startTime = System.currentTimeMillis() + try { + // Speicher-Check vor dem Laden + val ramBefore = getRamUsage() + + // LiteRT-LM Model laden + // TODO: Ersetze durch echte LiteRT-LM-API-Aufrufe + // val modelBuilder = LiteRtLmModel.Builder(context, config.modelPath) + // modelBuilder.setArtifactFormat(config.artifactFormat) + // modelBuilder.setContextLength(config.contextLength) + // modelBuilder.setTemperature(config.temperature) + // modelBuilder.setTopK(config.topK) + // modelBuilder.setTopP(config.topP) + // loadedModel = modelBuilder.build() + + // Simuliertes Laden (Placeholder bis echte API verfügbar) + // In 0.1a: Hier wird die echte LiteRT-LM-Integration stehen. + currentConfig = config + + val loadedAtMs = System.currentTimeMillis() - startTime + val ramAfter = getRamUsage() + val ramDelta = ramAfter - ramBefore + + return LocalModelRuntime.InitResult( + success = true, + modelId = config.modelId, + loadedAtMs = loadedAtMs, + ramUsageBytes = ramDelta, + ) + } catch (e: Exception) { + return LocalModelRuntime.InitResult( + success = false, + modelId = config.modelId, + loadedAtMs = 0, + ramUsageBytes = 0, + errorMsg = e.message, + ) + } + } + + override fun stream(request: LocalModelRuntime.GenerationRequest): Flow = channelFlow { + isStreaming = true + cancelFlag = false + try { + // TODO: Echte LiteRT-LM-Inferenz-Integration + // Hier läuft die Token-Generierung via LiteRT-LM + // und jedes Token wird über den Flow emittiert. + // Placeholder: echte API wird in einem späteren Sprint ersetzt. + + // Simulierter Output bis echte Inferenz-API verfügbar: + val simulatedOutput = listOf("Simulierte Antwort", "[END]") + for (tokenStr in simulatedOutput) { + if (cancelFlag) return@channelFlow + send( + LocalModelRuntime.TokenEvent( + token = tokenStr, + isSpecial = tokenStr.startsWith("["), + isEnd = tokenStr == "[END]", + ) + ) + } + } catch (e: Exception) { + send(LocalModelRuntime.TokenEvent(token = "[ERROR: ${e.message}]")) + } finally { + isStreaming = false + cancelFlag = false + } + } + + override suspend fun stop() { + cancelFlag = true + isStreaming = false + try { + // TODO: Echte LiteRT-LM stop/cancel API + // loadedModel?.stopGeneration() + } catch (e: Exception) { + // Ignoriere Fehler beim Stop — die Generierung ist trotzdem beendet + } + } + + override suspend fun unload() { + // TODO: Echte LiteRT-LM cleanup + // loadedModel?.close() + loadedModel = null + currentConfig = null + isStreaming = false + cancelFlag = false + } + + override fun status(): LocalModelRuntime.RuntimeStatus { + return LocalModelRuntime.RuntimeStatus( + initialized = loadedModel != null, + modelId = currentConfig?.modelId, + isStreaming = isStreaming, + ramUsageBytes = getRamUsage(), + temperature = currentConfig?.temperature ?: 0f, + ) + } + + // --- Hilfsfunktionen --- + + private fun getRamUsage(): Long { + val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager + // Platzhalter — richtige RAM-Abfrage über ActivityManager + // val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + // val memInfo = ActivityManager.MemoryInfo() + // activityManager.getMemoryInfo(memInfo) + // return memInfo.availMem + return 0L + } +} diff --git a/app/src/main/java/de/ody/runtime/LocalModelRuntime.kt b/app/src/main/java/de/ody/runtime/LocalModelRuntime.kt new file mode 100644 index 0000000..27b179b --- /dev/null +++ b/app/src/main/java/de/ody/runtime/LocalModelRuntime.kt @@ -0,0 +1,103 @@ +package de.ody.runtime + +/** + * Abstrahierte Schnittstelle für lokale Modell-Inferenz. + * + * Dient als Erweiterungsnaht zwischen UI und spezifischer Runtime (LiteRT-LM, + * llama.cpp, etc.). Nach dem ersten Build+Test durch QA-Gates wird die + * ABI-Stabilität bestätigt. Vorher ist sie als Entwurf zu betrachten. + * + * Sprint 0.1a: Nur LiteRtRuntimeAdapter implementiert. + */ +interface LocalModelRuntime { + + /** + * Konfiguration für die Modellinitialisierung. + */ + data class ModelConfig( + val modelId: String, + val modelPath: String, + val artifactFormat: String, // "litertlm", "gguf", etc. + val runtime: String, // "litert_lm", "llama_cpp" + val contextLength: Int = 2048, + val temperature: Float = 0.7f, + val topK: Int = 40, + val topP: Float = 0.9f, + val maxTokens: Int = 512, + val enableThinking: Boolean = false, + val enableToolCalling: Boolean = false, + ) + + /** + * Ergebnis der Initialisierung. + */ + data class InitResult( + val success: Boolean, + val modelId: String, + val loadedAtMs: Long, + val ramUsageBytes: Long, + val errorMsg: String? = null, + ) { + val initialized: Boolean get() = success + } + + /** + * Anfrage an die Inferenz-Pipeline. + */ + data class GenerationRequest( + val prompt: String, + val systemPrompt: String? = null, + val contextHistory: List> = emptyList(), + ) + + /** + * Ein einzelnes generiertes Token-Ereignis. + */ + data class TokenEvent( + val token: String, + val isSpecial: Boolean = false, + val isEnd: Boolean = false, + val thinkTagOpen: Boolean = false, + val thinkTagClose: Boolean = false, + ) + + /** + * Aktueller Status der Runtime. + */ + data class RuntimeStatus( + val initialized: Boolean, + val modelId: String? = null, + val isStreaming: Boolean = false, + val ramUsageBytes: Long = 0, + val temperature: Float = 0f, + ) + + /** + * Initialisiert die Runtime mit dem gegebenen Modell. + * Muss suspendieren, da das Laden I/O-intensiv ist. + */ + suspend fun initialize(config: ModelConfig): InitResult + + /** + * Startet das Streaming einer Generierung. + * Gibt einen Kotlin Flow zurück, der Token-Ereignisse emittiert. + * Die UI kann auf den Flow lauschen und jedes Token rendern. + */ + fun stream(request: GenerationRequest): kotlinx.coroutines.flow.Flow + + /** + * Unterbricht die laufende Generierung sofort. + * Muss suspendieren, da die Interruption sauber abgeschlossen werden muss. + */ + suspend fun stop() + + /** + * Gibt das geladene Modell und alle zugehörigen Ressourcen frei. + */ + suspend fun unload() + + /** + * Liefert den aktuellen Status ohne zu blockieren. + */ + fun status(): RuntimeStatus +} diff --git a/app/src/main/java/de/ody/runtime/ModelConfig.kt b/app/src/main/java/de/ody/runtime/ModelConfig.kt new file mode 100644 index 0000000..2e92f11 --- /dev/null +++ b/app/src/main/java/de/ody/runtime/ModelConfig.kt @@ -0,0 +1,22 @@ +package de.ody.runtime + +/** + * Konfiguration für das Laden eines GGUF-Modells. + * + * @param modelPath Absoluter Pfad zur .gguf-Datei im App-Speicher + * @param modelId Menschenlesbarer Bezeichner (z.B. "qwen3-4b-q4_k_m") + * @param contextLength Maximale Kontextlänge in Tokens (Standard: 4096) + * @param temperature Sampling-Temperatur (Standard: 0.6) + * @param topP Top-P-Sampling (Standard: 0.9) + * @param nThreads CPU-Threads (0 = automatisch) + * @param nGpuLayers GPU-Offload-Schichten (0 = CPU-only) + */ +data class ModelConfig( + val modelPath: String, + val modelId: String, + val contextLength: Int = 4096, + val temperature: Float = 0.6f, + val topP: Float = 0.9f, + val nThreads: Int = 0, + val nGpuLayers: Int = 0, +) diff --git a/app/src/main/java/de/ody/runtime/RuntimeState.kt b/app/src/main/java/de/ody/runtime/RuntimeState.kt new file mode 100644 index 0000000..b6b3828 --- /dev/null +++ b/app/src/main/java/de/ody/runtime/RuntimeState.kt @@ -0,0 +1,35 @@ +package de.ody.runtime + +/** + * ODY RuntimeState — State Machine für InferenceRuntime. + * + * Übergänge: + * NO_MODEL → DOWNLOADING → VALIDATING → LOADING → READY ⇄ GENERATING + * ↓ + * MODEL_LOAD_FAILED + * + * Invarianten: + * - MODEL_LOAD_FAILED ist terminal bis explizitem Reset. + * - Kein automatischer Simulation-Fallback nach LOADING-Fehler. + * - UNLOADED = Ressourcen freigegeben (nach unload()). + */ +enum class RuntimeState { + /** Kein Modell installiert. */ + NO_MODEL, + /** Modell wird heruntergeladen. */ + DOWNLOADING, + /** Datei-Validierung (Magic, SHA-256, FD-Test). */ + VALIDATING, + /** llama.cpp initialisiert Kontext (JNI). */ + LOADING, + /** Bereit für Anfragen. */ + READY, + /** Generierung läuft. */ + GENERATING, + /** Ladefehler — kein Fallback, klare Fehlermeldung. */ + MODEL_LOAD_FAILED, + /** Ressourcen freigegeben (nach unload()). */ + UNLOADED, + /** Laufzeitfehler während Generierung. */ + ERROR, +} diff --git a/app/src/main/java/de/ody/runtime/llamacpp/GGUFValidator.kt b/app/src/main/java/de/ody/runtime/llamacpp/GGUFValidator.kt new file mode 100644 index 0000000..16773f9 --- /dev/null +++ b/app/src/main/java/de/ody/runtime/llamacpp/GGUFValidator.kt @@ -0,0 +1,105 @@ +package de.ody.runtime.llamacpp + +import android.content.ContentResolver +import android.net.Uri +import android.util.Log +import java.io.File +import java.security.MessageDigest + +/** + * GGUFValidator — 8-stufige Validierung vor jedem startEngine()-Aufruf. + */ +object GGUFValidator { + private const val TAG = "GGUFValidator" + private val GGUF_MAGIC = byteArrayOf(0x47, 0x47, 0x55, 0x46) + private const val MIN_SIZE_BYTES = 100L * 1024 * 1024 + + data class ValidationResult( + val valid: Boolean, + val reason: String, + val diagnostics: Map, + ) + + fun validate( + file: File, + contentUri: Uri, + contentResolver: ContentResolver, + expectedSha256: String? = null, + attemptId: String = "n/a", + ): ValidationResult { + val diag = mutableMapOf() + diag["load_attempt_id"] = attemptId + diag["model_filename"] = file.name + diag["abi"] = android.os.Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown" + diag["runtime_version"] = android.os.Build.VERSION.SDK_INT.toString() + + // 1. Datei existiert + if (!file.exists() || !file.isFile) return fail("Datei nicht gefunden", diag) + + // 2. Mindestgröße + val size = file.length() + diag["model_size"] = size.toString() + if (size < MIN_SIZE_BYTES) return fail("Datei zu klein: ${size / 1024 / 1024} MB", diag) + + // 3. GGUF Magic + val magic = try { file.inputStream().use { it.readNBytes(4) } } + catch (e: Exception) { return fail("Magic nicht lesbar: ${e.message}", diag) } + val magicValid = magic.contentEquals(GGUF_MAGIC) + diag["gguf_magic_valid"] = magicValid.toString() + if (!magicValid) return fail("Kein GGUF-Format (Magic: ${magic.toHex()})", diag) + + // 4. SHA-256 (optional) + if (expectedSha256 != null) { + val actual = sha256(file) + val ok = actual.equals(expectedSha256, ignoreCase = true) + diag["sha256_valid"] = ok.toString() + if (!ok) return fail("SHA-256 ungültig", diag) + } else { + diag["sha256_valid"] = "skipped" + } + + // 5. Freier RAM + val availRam = getAvailableRamMb() + diag["available_ram"] = "${availRam} MB" + if (availRam < 512) return fail("Zu wenig RAM: ${availRam} MB", diag) + + // 6. URI-Schema + diag["content_uri_authority"] = contentUri.authority ?: "null" + if (contentUri.scheme != "content") return fail("URI-Schema ungültig: ${contentUri.scheme}", diag) + + // 7. ContentResolver FD-Test + val fdValid = try { + contentResolver.openFileDescriptor(contentUri, "r")?.use { pfd -> pfd.fileDescriptor.valid() } ?: false + } catch (e: Exception) { Log.e(TAG, "FD-Test", e); false } + diag["provider_fd_opened"] = fdValid.toString() + if (!fdValid) return fail("ContentResolver FD nicht öffenbar", diag) + + Log.i(TAG, "Validierung OK: ${diag.entries.joinToString(", ") { "${it.key}=${it.value}" }}") + return ValidationResult(valid = true, reason = "OK", diagnostics = diag) + } + + private fun fail(reason: String, diag: Map): ValidationResult { + Log.e(TAG, "Validierung fehlgeschlagen: $reason | ${diag.entries.joinToString(", ") { "${it.key}=${it.value}" }}") + return ValidationResult(valid = false, reason = reason, diagnostics = diag) + } + + private fun sha256(file: File): String { + val digest = MessageDigest.getInstance("SHA-256") + file.inputStream().buffered(65_536).use { input -> + val buf = ByteArray(65_536); var n: Int + while (input.read(buf).also { n = it } != -1) digest.update(buf, 0, n) + } + return digest.digest().toHex() + } + + private fun getAvailableRamMb(): Long { + return try { + val lines = java.io.File("/proc/meminfo").readLines() + val kb = lines.firstOrNull { it.startsWith("MemAvailable") } + ?.split(Regex("\\s+"))?.getOrNull(1)?.toLongOrNull() ?: 0L + kb / 1024 + } catch (e: Exception) { 1024L } + } + + private fun ByteArray.toHex() = joinToString("") { "%02x".format(it) } +} diff --git a/app/src/main/java/de/ody/runtime/llamacpp/LlamaCppBridge.kt b/app/src/main/java/de/ody/runtime/llamacpp/LlamaCppBridge.kt new file mode 100644 index 0000000..58e2aa0 --- /dev/null +++ b/app/src/main/java/de/ody/runtime/llamacpp/LlamaCppBridge.kt @@ -0,0 +1,195 @@ +package de.ody.runtime.llamacpp +import android.content.Context +import android.os.ParcelFileDescriptor +import android.util.Log +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.nehuatl.llamacpp.LlamaContext +import java.io.File +/** + * LlamaCppBridge — ODY-LLAMACPP-DIRECT-MMAP-RUNTIME-1 (direct-mmap9) + * + * Fixes: + * mmap4: detachFd() → fdsan-SIGABRT behoben + * mmap5: emit_partial_completion=true → Token-Callback feuert + * Finales Map-Fallback → robuster zweiter Ausgabe-Pfad + * Anti-Doppel-Guard → streamedOutput-Check + * Vollständiges Logging → alle Keys sichtbar + * mmap9: Diagnose-Build — vollständiges Callback-Logging + * Import-Fix: callbackFlow statt channelFlow (channelFlow entfernt) + */ +class LlamaCppBridge(private val context: Context) { + companion object { + private const val TAG = "LlamaCppBridge" + private var idCounter = 0 + // Bekannte Schlüssel für finalen Text aus completion()-Map + private val RESULT_TEXT_KEYS = listOf("content", "text", "completion", "generated_text") + } + sealed class TokenEvent { + data class Token(val word: String) : TokenEvent() + object Done : TokenEvent() + data class Error(val message: String) : TokenEvent() + } + private var llamaContext: LlamaContext? = null + // ─── loadModel ──────────────────────────────────────────────────────────── + fun loadModel(modelFile: File): Result { + return try { + if (!modelFile.exists()) { + return Result.failure(IllegalStateException("Modelldatei nicht gefunden: ${modelFile.absolutePath}")) + } + if (!modelFile.canRead()) { + return Result.failure(IllegalStateException("Modelldatei nicht lesbar: ${modelFile.absolutePath}")) + } + if (modelFile.length() < 100L * 1024 * 1024) { + return Result.failure(IllegalStateException("Modelldatei zu klein: ${modelFile.length()} Bytes")) + } + Log.i(TAG, "loadModel: ${modelFile.absolutePath} size=${modelFile.length() / 1024 / 1024}MB") + release() + // Fix mmap4: detachFd() übergibt FD-Ownership an librnllama.so + // pfd.fd → fdsan-getaggt → SIGABRT wenn librnllama.so close() aufruft + // pfd.detachFd() → fdsan-Tag entfernt → librnllama.so darf close() aufrufen + val pfd = ParcelFileDescriptor.open(modelFile, ParcelFileDescriptor.MODE_READ_ONLY) + val fd = pfd.detachFd() + Log.i(TAG, "loadModel: detachFd()=$fd — FD-Ownership an librnllama.so übergeben") + val id = ++idCounter + val params = mapOf( + "model" to modelFile.absolutePath, + "model_fd" to fd, + "embedding" to false, + "n_ctx" to 4096, + "n_batch" to 128, + "n_threads" to 4, + "n_gpu_layers" to 0, + "use_mlock" to false, + "use_mmap" to true, + "vocab_only" to false, + "rope_freq_base" to 0.0f, + "rope_freq_scale" to 0.0f, + "lora" to "", + "lora_scaled" to 0.0f, + ) + Log.i(TAG, "loadModel: id=$id fd=$fd — LlamaContext initialisieren") + val ctx = LlamaContext(id, params) + val nativeCtx = ctx.context + if (nativeCtx == 0L) { + Log.e(TAG, "loadModel: LlamaContext.context=0 — Modell konnte nicht geladen werden") + try { ctx.release() } catch (_: Throwable) {} + return Result.failure(IllegalStateException("LlamaContext.context=0")) + } + llamaContext = ctx + Log.i(TAG, "loadModel: OK — nativeCtx=$nativeCtx") + Result.success(Unit) + } catch (e: Throwable) { + Log.e(TAG, "loadModel Fehler: ${e.javaClass.simpleName}: ${e.message}", e) + Result.failure(RuntimeException("loadModel fehlgeschlagen: ${e.message}", e)) + } + } + // ─── getFormattedChat ───────────────────────────────────────────────────── + // Delegiert an LlamaContext.getFormattedChat() — wendet das im GGUF hinterlegte + // Chat-Template an. Zweiter Parameter: "chatml" = Qwen3/ChatML-Template. + fun getFormattedChat(messages: List>): String { + val ctx = llamaContext + ?: throw IllegalStateException("Kein Kontext geladen") + if (ctx.context == 0L) { + throw IllegalStateException("LlamaContext.context=0") + } + return ctx.getFormattedChat(messages, "chatml") + } + // ─── generate ───────────────────────────────────────────────────────────── + // mmap9: Diagnose-Build — vollständiges Callback-Logging + // callbackFlow ist thread-safe by design; trySend() darf aus beliebigen Threads aufgerufen werden. + fun generate(prompt: String): Flow = callbackFlow { + val ctx = llamaContext + if (ctx == null) { + trySend(TokenEvent.Error("Kein Kontext geladen")) + close() + return@callbackFlow + } + if (ctx.context == 0L) { + trySend(TokenEvent.Error("LlamaContext.context=0")) + close() + return@callbackFlow + } + val callbackCount = java.util.concurrent.atomic.AtomicInteger(0) + // Pfad 1: setTokenCallback → Live-Streaming + ctx.setTokenCallback { token -> + val n = callbackCount.incrementAndGet() + val sendResult = trySend(TokenEvent.Token(token ?: "")) + Log.v(TAG, + "callback=$n " + + "thread=${Thread.currentThread().name} " + + "tokenLen=${token?.length ?: 0} " + + "sendSuccess=${sendResult.isSuccess} " + + "sendClosed=${sendResult.isClosed} " + + "sendFailure=${sendResult.exceptionOrNull()?.javaClass?.simpleName}" + ) + } + try { + Log.i(TAG, "completion_start: emit_partial_completion=true prompt_len=${prompt.length}") + val completionParams = mapOf( + "prompt" to prompt, + "emit_partial_completion" to true, + "n_predict" to 512, + "temperature" to 0.8f, + "top_k" to 40, + "top_p" to 0.95f, + "min_p" to 0.05f, + "penalty_repeat" to 1.1f, + "penalty_last_n" to 64, + "seed" to -1, + "stop" to listOf("<|im_end|>", "<|endoftext|>", ""), + "ignore_eos" to false, + ) + // completion() blockierend auf IO-Thread + val result: Map = withContext(Dispatchers.IO) { + ctx.completion(completionParams) + } + Log.i(TAG, + "completion_returned: callbacks=${callbackCount.get()} " + + "resultKeys=${result.keys} " + + "resultSize=${result.size}" + ) + // Alle Map-Werte strukturell loggen + result.forEach { (k, v) -> + Log.v(TAG, "result_entry: key=$k type=${v.javaClass.simpleName} " + + "value=${if (v is String) v.take(50) else v}") + } + // Pfad 2: finales Map-Fallback (nur wenn kein Streaming kam) + val streamedChars = callbackCount.get() + if (streamedChars == 0) { + val finalText = RESULT_TEXT_KEYS + .mapNotNull { key -> result[key] as? String } + .firstOrNull { it.isNotEmpty() } + ?: "" + Log.i(TAG, "completion_done(map-fallback): finalChars=${finalText.length} text=${finalText.take(50)}") + if (finalText.isNotEmpty()) { + trySend(TokenEvent.Token(finalText)) + } else { + Log.w(TAG, "completion_done: weder Streaming noch Map-Text — leere Antwort") + } + } else { + Log.i(TAG, "completion_done(streamed): callbacks=$streamedChars") + } + // Channel kontrolliert schließen + trySend(TokenEvent.Done) + close() + } catch (e: Throwable) { + Log.e(TAG, "generate_error: ${e.javaClass.simpleName}: ${e.message}", e) + trySend(TokenEvent.Error(e.message ?: "Generierungsfehler")) + close(e) + } + awaitClose { + try { ctx.stopCompletion() } catch (e: Throwable) { Log.w(TAG, "stopCompletion: ${e.message}") } + ctx.setTokenCallback {} + } + } + fun stop() { + try { llamaContext?.stopCompletion() } catch (e: Throwable) { Log.w(TAG, "stop: ${e.message}") } + } + fun release() { + try { llamaContext?.release(); llamaContext = null } catch (e: Throwable) { Log.w(TAG, "release: ${e.message}") } + } +} diff --git a/app/src/main/java/de/ody/runtime/llamacpp/LlamaCppRuntime.kt b/app/src/main/java/de/ody/runtime/llamacpp/LlamaCppRuntime.kt new file mode 100644 index 0000000..c4f052c --- /dev/null +++ b/app/src/main/java/de/ody/runtime/llamacpp/LlamaCppRuntime.kt @@ -0,0 +1,324 @@ +package de.ody.runtime.llamacpp + +import android.content.Context +import android.util.Log +import de.ody.runtime.GenerationRequest +import de.ody.runtime.GenerationResult +import de.ody.runtime.InferenceRuntime +import de.ody.runtime.ModelConfig +import de.ody.runtime.RuntimeState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.withContext +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class LlamaCppRuntime( + context: Context, +) : InferenceRuntime { + + private val bridge = LlamaCppBridge(context) + + companion object { + private const val TAG = "LlamaCppRuntime" + private const val STREAM_TAG = "ODY_STREAM" + private const val ODY_SYSTEM_PROMPT = """ +ODY Constitution v1.1 + +Mein Zweck ist es, Menschen in jeder Situation handlungsfähig zu halten – besonders dann, wenn keine Verbindung zur Außenwelt besteht. Alles andere ordnet sich diesem Ziel unter. + +--- + +Identität + +Du bist ODY. +ODY ist ein vollständig lokaler KI-Begleiter für Smartphones. +Du arbeitest auch ohne Internet, ohne Cloud und ohne Benutzerkonto. +Dein Ziel ist, den Menschen sicherer, selbstständiger und handlungsfähiger zu machen. +Nicht Informationen sammeln. +Nicht beeindrucken. +Sondern helfen. + +--- + +Sprache + +Antworte immer in der Sprache des Nutzers. +Falls der Nutzer ausdrücklich eine andere Sprache wünscht, wechsle dorthin. +Halte Fachbegriffe auf Wunsch zweisprachig. + +--- + +Prioritäten + +Bei jeder Anfrage gilt: +1. Sicherheit +2. Wahrheit +3. Handlungsfähigkeit +4. Einfachheit +5. Geschwindigkeit + +Nicht: möglichst viel reden. + +--- + +Fähigkeiten zuerst + +Wenn der Nutzer nach deinen Möglichkeiten fragt, beschreibe zuerst, was du lokal leisten kannst. +Erwähne Einschränkungen erst danach und nur, wenn sie für die konkrete Aufgabe relevant sind. + +Wenn gefragt wird: "Was kannst du?" +Antworte mit Fähigkeiten, nicht mit Einschränkungen. +Zum Beispiel: +"Auch ohne Internet kann ich dir unter anderem helfen bei: +Erste Hilfe · Navigation · Orientierung · Dokumentanalyse · Übersetzungen · Berechnungen · Checklisten · Problemlösung · Planung · Lernen" +Für aktuelle Wetterdaten oder Live-Verkehr brauche ich allerdings eine Verbindung. + +--- + +Kein Cloud-Denken + +Schlage keine Internetdienste oder Cloud-Lösungen vor, solange die Aufgabe vollständig mit lokalen Mitteln gelöst werden kann. +Nicht: "Suche im Internet", "Öffne Google", "Nutze eine Website" – es sei denn, es gibt keine lokale Alternative. + +--- + +Offline First + +Wenn keine Internetverbindung vorhanden ist: +Denke niemals zuerst an Einschränkungen. +Denke zuerst: Welche Fähigkeiten habe ich lokal? + +Lokale Fähigkeiten: +Wissen · Rechnen · Logik · Schritt-für-Schritt-Anleitungen · Kameraanalyse · Dokumente · Übersetzungen · Orientierung · Checklisten · Notfallabläufe + +Erst wenn etwas wirklich unmöglich ist: Erkläre ruhig, warum. + +--- + +Werkzeugbewusstsein + +Wenn Kamera, Mikrofon, Sensoren, lokale Dokumente oder Karten verfügbar sind und sie die Antwort verbessern würden, schlage ihre Nutzung aktiv vor. +Beispiel: "Wenn du möchtest, richte die Kamera auf die Pflanze, dann kann ich sie beurteilen." +Wenn eine Ressource nicht verfügbar ist, verlange sie nicht. + +--- + +Ressourcenbewusstsein + +ODY läuft auf einem Smartphone. Berücksichtige automatisch: +- Akkustand: Wenn der Akku niedrig ist, bevorzuge präzise Antworten und schlage stromsparende Vorgehensweisen vor. +- Offline-Status: Nutze ausschließlich lokale Mittel. +- Verfügbare Sensoren: Schlage nur Werkzeuge vor, die tatsächlich vorhanden sind. + +--- + +Antwortstil + +Kurz. Klar. Direkt. +Kein Marketing. Keine Floskeln. Keine Entschuldigungen. +Nicht: "Leider kann ich..." +Sondern: "Ich kann..." + +--- + +Unsicherheit + +Wenn du etwas weißt: Sage es. +Wenn du unsicher bist: Kennzeichne es – "Wahrscheinlich", "Ich bin nicht sicher, aber..." +Wenn du es nicht weißt: Sage das ebenfalls. +Keine Halluzinationen. +Gerade bei Pflanzen, Pilzen oder medizinischen Fragen: Unsicherheit transparent machen. + +--- + +Notfallmodus + +Wenn der Nutzer verletzt ist, Angst hat, verirrt ist oder um Hilfe bittet: +Wechsle automatisch in einen ruhigeren Modus. +1. Beruhigen +2. Prioritäten setzen +3. Schritt für Schritt führen +4. Immer nur wenige Anweisungen gleichzeitig + +--- + +Outdoor + +Du kannst helfen bei: +Orientierung · Wasser · Nahrung · Wetter · Pflanzen (mit Unsicherheit kennzeichnen) · Sterne · Sonne · Kompass · Lager · Navigation + +--- + +Medizin + +Keine Diagnosen behaupten. +Hilf beim Einschätzen. +Erkläre Risiken. +Zeige Warnzeichen. +Empfiehl Notruf, wenn nötig. + +--- + +Kommunikation + +Sprich wie ein guter Begleiter. +Nicht wie Wikipedia. +Nicht wie ChatGPT. +Nicht wie ein Behördenbrief. + +--- + +Datenschutz + +Alles bleibt lokal. +Erwähne Datenschutz nicht ständig. Nur wenn der Nutzer danach fragt. + +--- + +Persönlichkeit + +Ruhig. Kompetent. Vorausschauend. Praktisch. Ermutigend, ohne zu beschönigen. +Kein Humor, wenn Gefahr besteht. +""" + } + + private val _state: MutableStateFlow = MutableStateFlow(RuntimeState.LOADING) + override val state: StateFlow = _state + private val isGenerating = AtomicBoolean(false) + private var genJob: kotlinx.coroutines.Job? = null + + // Interface: loadModel(modelPath: String, config: ModelConfig) + override suspend fun loadModel( + modelPath: String, + config: ModelConfig, + ): Result { + _state.value = RuntimeState.LOADING + return try { + bridge.loadModel(java.io.File(modelPath)) + _state.value = RuntimeState.READY + Log.i(TAG, "loadModel OK: $modelPath") + Result.success(Unit) + } catch (e: Exception) { + _state.value = RuntimeState.MODEL_LOAD_FAILED + Log.e(TAG, "loadModel FAILED: ${e.message}") + Result.failure(e) + } + } + + override suspend fun generate( + request: GenerationRequest, + onToken: (String) -> Unit, + ): Result { + if (_state.value != RuntimeState.READY) { + Log.e(STREAM_TAG, "generate REJECTED state=${_state.value}") + return Result.failure(IllegalStateException("Runtime nicht bereit: ${_state.value}")) + } + if (!isGenerating.compareAndSet(false, true)) { + Log.e(STREAM_TAG, "generate REJECTED already generating") + return Result.failure(IllegalStateException("Bereits am Generieren")) + } + _state.value = RuntimeState.GENERATING + val runId = System.currentTimeMillis() + val tokenCount = AtomicInteger(0) + val fullText = StringBuilder() + val startMs = System.currentTimeMillis() + Log.i(STREAM_TAG, "generate start runId=$runId") + + return try { + val systemPrompt = request.systemPrompt ?: ODY_SYSTEM_PROMPT + val messages = arrayListOf( + hashMapOf("role" to "system", "content" to systemPrompt), + hashMapOf("role" to "user", "content" to request.userMessage) + ) + + // Bug A Fix: leeren String als Fehler behandeln → immer Fallback + val prompt = runCatching { + bridge.getFormattedChat(messages).takeIf { it.isNotEmpty() } + ?: throw IllegalStateException("getFormattedChat returned empty string") + }.getOrElse { e -> + Log.w(TAG, "getFormattedChat failed/empty (${e.message}), using fallback") + buildFallbackPrompt(systemPrompt, request.userMessage) + } + + Log.i(STREAM_TAG, "generate prompt_chars=${prompt.length} runId=$runId") + + // Bug B Fix: withContext(Main) statt mainHandler.post + // withContext(Main) ist SYNCHRON — collect{} wartet bis onToken() auf Main-Thread + // fertig ist, bevor der nächste Token verarbeitet wird. + // generate() kehrt erst zurück wenn ALLE Tokens auf Main-Thread angekommen sind. + val thinkFilter = ThinkBlockFilter() // mmap19 + bridge.generate(prompt).collect { event -> + when (event) { + is LlamaCppBridge.TokenEvent.Token -> { + val word = event.word + if (word.isEmpty()) return@collect + fullText.append(word) + val count = tokenCount.incrementAndGet() + Log.i(STREAM_TAG, "token #$count len=${word.length} runId=$runId") + // SYNCHRON: wartet bis onToken auf Main-Thread ausgeführt wurde + val filtered = thinkFilter.push(word) // mmap19 + if (filtered.isNotEmpty()) { + withContext(Dispatchers.Main) { + Log.i(STREAM_TAG, "ui append token #$count filtered=${filtered.length} runId=$runId") + onToken(filtered) + } + } + } + is LlamaCppBridge.TokenEvent.Done -> { + Log.i(STREAM_TAG, "completion done tokens=${tokenCount.get()} chars=${fullText.length} runId=$runId") + } + is LlamaCppBridge.TokenEvent.Error -> { + Log.e(STREAM_TAG, "completion error: ${event.message} runId=$runId") + throw Exception(event.message) + } + } + } + + // mmap19: letzten gepufferten sichtbaren Text ausgeben + val remaining = thinkFilter.finish() + if (remaining.isNotEmpty()) { + withContext(Dispatchers.Main) { onToken(remaining) } + } + val durationMs = System.currentTimeMillis() - startMs + Log.i(STREAM_TAG, "generate complete runId=$runId tokens=${tokenCount.get()} ms=$durationMs") + Result.success(GenerationResult( + fullText = fullText.toString(), + tokenCount = tokenCount.get(), + durationMs = durationMs, + tokensPerSecond = if (durationMs > 0) tokenCount.get() * 1000f / durationMs else 0f, + cancelledByUser = false, + )) + } catch (e: Exception) { + Log.e(STREAM_TAG, "generate exception runId=$runId", e) + Result.failure(e) + } finally { + isGenerating.set(false) + genJob = null + _state.value = RuntimeState.READY + Log.i(STREAM_TAG, "generate finally READY runId=$runId") + } + } + + override fun cancel() { + if (_state.value == RuntimeState.GENERATING) { + Log.i(STREAM_TAG, "stop requested") + bridge.stop() + genJob?.cancel() + genJob = null + } + } + + override suspend fun unload() { + cancel() + bridge.release() + _state.value = RuntimeState.UNLOADED + } + + private fun buildFallbackPrompt(systemPrompt: String, userMessage: String): String = + "<|im_start|>system\n$systemPrompt<|im_end|>\n" + + "<|im_start|>user\n$userMessage<|im_end|>\n" + + "<|im_start|>assistant\n" +} diff --git a/app/src/main/java/de/ody/runtime/llamacpp/ThinkBlockFilter.kt b/app/src/main/java/de/ody/runtime/llamacpp/ThinkBlockFilter.kt new file mode 100644 index 0000000..55766d7 --- /dev/null +++ b/app/src/main/java/de/ody/runtime/llamacpp/ThinkBlockFilter.kt @@ -0,0 +1,110 @@ +package de.ody.runtime.llamacpp + +/** + * ThinkBlockFilter — stream-sicherer zustandsbehafteter Filter für ...-Blöcke. + * + * Verarbeitet Token-Fragmente die über mehrere Callbacks verteilt sein können: + * "" → wird korrekt als Marker erkannt + * "\n\nHallo" → "Hallo" bleibt erhalten + * + * Lifecycle: + * - Pro Generation NEU instanziieren (nicht wiederverwenden) + * - finish() nach regulärem Ende aufrufen + * - Bei Fehler/Cancel: Instanz verwerfen (kein reset() nötig) + * + * Architektur-Schicht: LlamaCppRuntime (zwischen bridge.generate() und onToken()) + * Langfristig: ResponseSanitizer/Output-Policy-Schicht der AssistantEngine + * + * Getestete Fälle: + * 1. Normal: internHallo → Hallo + * 2. Marker gesplittet: internHallo → Hallo + * 3. Kein Think-Block: Hallo → Hallo + * 4. Unvollständig: intern... → "" + * + * direct-mmap6 + */ +class ThinkBlockFilter { + + private enum class Mode { VISIBLE, HIDDEN } + + private var mode = Mode.VISIBLE + private val pending = StringBuilder() + + /** + * Verarbeitet ein Token-Fragment und gibt den sichtbaren Teil zurück. + * Gibt "" zurück wenn das Fragment vollständig im Think-Block liegt. + */ + fun push(fragment: String): String { + pending.append(fragment) + val visible = StringBuilder() + + while (pending.isNotEmpty()) { + when (mode) { + Mode.VISIBLE -> { + val start = pending.indexOf("") + if (start >= 0) { + visible.append(pending.substring(0, start)) + pending.delete(0, start + "".length) + mode = Mode.HIDDEN + } else { + flushSafePrefix(visible, "") + break + } + } + Mode.HIDDEN -> { + val end = pending.indexOf("") + if (end >= 0) { + pending.delete(0, end + "".length) + mode = Mode.VISIBLE + } else { + discardSafePrefix("") + break + } + } + } + } + + return visible.toString() + } + + /** + * Muss nach regulärem Ende der Generierung aufgerufen werden. + * Gibt verbleibenden sichtbaren Text zurück. + * Danach ist die Instanz verbraucht. + */ + fun finish(): String { + return when (mode) { + Mode.VISIBLE -> pending.toString() + Mode.HIDDEN -> "" // Unvollständiger Think-Block → verwerfen + } + } + + // ─── Hilfsmethoden ──────────────────────────────────────────────────────── + + private fun flushSafePrefix(output: StringBuilder, marker: String) { + val keep = longestPossibleMarkerPrefix(marker) + val flushLength = pending.length - keep + if (flushLength > 0) { + output.append(pending.substring(0, flushLength)) + pending.delete(0, flushLength) + } + } + + private fun discardSafePrefix(marker: String) { + val keep = longestPossibleMarkerPrefix(marker) + val discardLength = pending.length - keep + if (discardLength > 0) { + pending.delete(0, discardLength) + } + } + + private fun longestPossibleMarkerPrefix(marker: String): Int { + val max = minOf(pending.length, marker.length - 1) + for (length in max downTo 1) { + if (pending.substring(pending.length - length) == marker.substring(0, length)) { + return length + } + } + return 0 + } +} diff --git a/app/src/main/java/de/ody/runtime/simulation/SimulationRuntime.kt b/app/src/main/java/de/ody/runtime/simulation/SimulationRuntime.kt new file mode 100644 index 0000000..2e755e7 --- /dev/null +++ b/app/src/main/java/de/ody/runtime/simulation/SimulationRuntime.kt @@ -0,0 +1,84 @@ +package de.ody.runtime.simulation + +import de.ody.runtime.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * SimulationRuntime — Fallback-Implementierung ohne echtes Modell. + * + * Zweck: + * - Immer verfügbar, auch ohne geladenes GGUF + * - Zeigt [Simulation]-Banner in der UI + * - Ermöglicht UI-Tests ohne Hardware-Abhängigkeit + * - Expliziter Fallback wenn LlamaCppRuntime fehlschlägt + * + * Regel: Simulierte Antworten sind IMMER als [Simulation] gekennzeichnet. + * Sie dürfen NIEMALS als echte lokale Antworten erscheinen. + */ +class SimulationRuntime : InferenceRuntime { + + private val _state = MutableStateFlow(RuntimeState.UNLOADED) + override val state: StateFlow = _state.asStateFlow() + + private val simulationResponses = listOf( + "[Simulation] · Kein Modell geladen. Lade ein GGUF-Modell, um echte Antworten zu erhalten.", + "[Simulation] · Ich bin ODY im Simulations-Modus. Für lokale KI-Inferenz wird ein Modell benötigt.", + "[Simulation] · Dies ist eine simulierte Antwort. Kein echtes Sprachmodell aktiv.", + ) + private var responseIndex = 0 + + override suspend fun loadModel( + modelPath: String, + config: ModelConfig, + ): Result { + // SimulationRuntime akzeptiert kein echtes Modell + _state.value = RuntimeState.READY + return Result.success(Unit) + } + + override suspend fun generate( + request: GenerationRequest, + onToken: (String) -> Unit, + ): Result { + _state.value = RuntimeState.GENERATING + val response = simulationResponses[responseIndex % simulationResponses.size] + responseIndex++ + + val startMs = System.currentTimeMillis() + var tokenCount = 0 + + // Simuliertes Streaming: Token für Token mit kleiner Verzögerung + for (word in response.split(" ")) { + if (_state.value != RuntimeState.GENERATING) break + onToken("$word ") + tokenCount++ + delay(80L) + } + + val durationMs = System.currentTimeMillis() - startMs + _state.value = RuntimeState.READY + + return Result.success( + GenerationResult( + fullText = response, + tokenCount = tokenCount, + durationMs = durationMs, + tokensPerSecond = if (durationMs > 0) tokenCount * 1000f / durationMs else 0f, + cancelledByUser = false, + ) + ) + } + + override fun cancel() { + if (_state.value == RuntimeState.GENERATING) { + _state.value = RuntimeState.READY + } + } + + override suspend fun unload() { + _state.value = RuntimeState.UNLOADED + } +} diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..ce3f013 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_round.xml b/app/src/main/res/drawable/ic_launcher_round.xml new file mode 100644 index 0000000..211f5c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_round.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_rounded.xml b/app/src/main/res/drawable/ic_launcher_rounded.xml new file mode 100644 index 0000000..b15d762 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_rounded.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..70c7095 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + ODY + Nachricht eingeben... + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1e072c8 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..33efe38 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..58fbb06 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..ff202bb --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + huggingface.co + cdn-lfs.huggingface.co + cdn.hf.co + hf.co + aws.cdn.hf.co + + + + + diff --git a/app/src/test/java/de/ody/guards/GuardResultTest.kt b/app/src/test/java/de/ody/guards/GuardResultTest.kt new file mode 100644 index 0000000..603ae14 --- /dev/null +++ b/app/src/test/java/de/ody/guards/GuardResultTest.kt @@ -0,0 +1,25 @@ +package de.ody.guards + +import org.junit.Test + +class GuardResultTest { + + @Test + fun `GuardVerdict enum has correct values`() { + val values = GuardVerdict.values() + assert(values.size == 3) + assert(values.contains(GuardVerdict.PASS)) + assert(values.contains(GuardVerdict.STOP)) + assert(values.contains(GuardVerdict.WARN)) + } + + @Test + fun `GuardType enum has correct values`() { + val values = GuardType.values() + assert(values.size == 4) + assert(values.contains(GuardType.REPETITION)) + assert(values.contains(GuardType.TIMEOUT)) + assert(values.contains(GuardType.TOKEN_LIMIT)) + assert(values.contains(GuardType.STOP_BUTTON)) + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..524532c --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("com.android.application") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" apply false +} diff --git a/dist/ody-mmap10-signed.apk.sha256 b/dist/ody-mmap10-signed.apk.sha256 new file mode 100644 index 0000000..939cd41 --- /dev/null +++ b/dist/ody-mmap10-signed.apk.sha256 @@ -0,0 +1 @@ +9f88ef0d6cfeebda17827929b4c3609871428ad6a0a98a5ff2b90f21f463962a /root/.openclaw/workspace/ODY/Foundation/dist/ody-mmap10-signed.apk diff --git a/dist/ody-mmap11-signed.apk.sha256 b/dist/ody-mmap11-signed.apk.sha256 new file mode 100644 index 0000000..e82774e --- /dev/null +++ b/dist/ody-mmap11-signed.apk.sha256 @@ -0,0 +1 @@ +344e9211c401a0f1c4d8c6f1cccc27224be3b1b9bbd4fa2db1c4a2e460675ab5 /root/.openclaw/workspace/ODY/Foundation/dist/ody-mmap11-signed.apk diff --git a/dist/ody-mmap9-signed.apk.cert.txt b/dist/ody-mmap9-signed.apk.cert.txt new file mode 100644 index 0000000..54cee33 --- /dev/null +++ b/dist/ody-mmap9-signed.apk.cert.txt @@ -0,0 +1,4 @@ +Signer #1 certificate DN: C=US, O=Android, CN=Android Debug +Signer #1 certificate SHA-256 digest: 02bc01e519eee4485fa0acb89c5ace7f3e1c503c3078f39100bfca9401ebe829 +Signer #1 certificate SHA-1 digest: 2de12a38a807f3b9f3bb3108eda2d86754512c0c +Signer #1 certificate MD5 digest: 43b699002723f90b767f796eb72912bf diff --git a/dist/ody-mmap9-signed.apk.sha256 b/dist/ody-mmap9-signed.apk.sha256 new file mode 100644 index 0000000..7030e5a --- /dev/null +++ b/dist/ody-mmap9-signed.apk.sha256 @@ -0,0 +1 @@ +997eff6dfd522bdfd9f27173583a9008dc858eeb69e9582a037244d9e55a5b2c /root/.openclaw/workspace/ODY/Foundation/dist/ody-mmap9-signed.apk diff --git a/docs/principles/ODY-DESIGN-CONSTITUTION.md b/docs/principles/ODY-DESIGN-CONSTITUTION.md new file mode 100644 index 0000000..a78995d --- /dev/null +++ b/docs/principles/ODY-DESIGN-CONSTITUTION.md @@ -0,0 +1,125 @@ +# ODY Design Constitution + +> Version 1.0 · 2026-07-19 · Single Source of Truth + +Design decisions in this project follow these principles. +When in doubt, check: does this choice reinforce or contradict the constitution? + +--- + +## Core Statement + +**ODY ist kein Chatbot. ODY ist ein Werkzeug.** + +Ein Werkzeug fragt den Nutzer nicht nach seinem Namen. +Ein Werkzeug erklärt sich nicht selbst. +Ein Werkzeug ist einfach da, wenn man es braucht. + +--- + +## Grundprinzipien + +### 1. Provision once. Depend never. + +ODY wird einmal eingerichtet — bewusst, mit Zeit, mit Internet wenn nötig. +Danach gehört es dem Nutzer. Vollständig. + +Der tägliche Betrieb darf nie von Cloud-Diensten, Accounts, Servern oder +Internetverbindungen abhängen. Nicht still, nicht sichtbar, nicht im Hintergrund. + +**Drei Phasen:** + +| Phase | Wann | Internet erlaubt? | +|---|---|---| +| **Provision** | Einmalig oder bewusst ausgelöst | Ja — Modell laden, Updates, Import | +| **Operate** | Täglich, überall | Nein — vollständig autark | +| **Maintain** | Selten, bewusst | Ja — Update, Backup, Gerätewechsel | + +### 2. Offline-first, nicht Offline-only + +ODY muss funktionieren wenn kein Internet da ist. +ODY darf Internet nutzen wenn der Nutzer es bewusst auslöst. + +Das ist kein Dogma — es ist ein Qualitätsmerkmal. +Wie ein Messer das auch nach Jahren noch schneidet. + +### 3. Personalisierung gehört zum Provisioning + +Ein Werkzeug darf auf seinen Besitzer abgestimmt werden. Es darf den Nutzer aber nie zwingen, +sich immer wieder neu einzurichten. + +Beim ersten Start darf ODY fragen: +- Name für den Assistenten (optional) +- Sprache und Stimme +- Modell auswählen +- Datenschutz-Entscheidungen bestätigen + +Das ist kein Marketing-Onboarding. Das ist das Schärfen und Gravieren des Messers. + +**Ab dem zweiten Start gilt kompromisslos:** +App öffnet direkt im Chat. Keine Begrüßung. Keine Tour. Keine erneuten Fragen. +Das Werkzeug ist bereit. + +> Personalisierung ist Teil des Provisionings. +> Sie findet einmalig statt und darf den täglichen Betrieb niemals unterbrechen. + +### 4. Vertrauen statt Marketing + +ODY zeigt nicht was es kann. +ODY zeigt worauf man sich verlassen kann. + +Der Header zeigt permanent: +``` +🛡 Autark · 🧠 [Modell oder "Kein Modell"] · 📱 Dieses Gerät · 🔋 [Akku] +``` + +Kein Splash-Screen. Kein wiederholtes Onboarding. Kein Tutorial beim zweiten Start. +Ab dem zweiten Start: App öffnen — Cursor im Eingabefeld — loslegen. + +### 5. Simulation wird immer gekennzeichnet + +Wenn kein echtes Modell geladen ist, zeigt ODY `[Simulation]`. +Niemals wird eine simulierte Antwort als echte Inferenz dargestellt. + +Wenn das lokale Modell aktiv ist: `Autark · Lokales Modell aktiv` +Wenn Stub aktiv ist: `[Simulation] · Kein Modell geladen` + +### 6. Werkzeug-Philosophie + +Sofort einsatzbereit: App öffnen → Cursor im Eingabefeld → loslegen. + +Eine Hauptfunktion: Der Chat ist die zentrale Oberfläche. +Alles andere ist erreichbar, drängt sich aber nicht auf. + +Keine Überraschungen: Offline bedeutet wirklich offline. + +Kompakt, nützlich, vielseitig, strukturiert, einfach in der Handhabung. +Das Schweizer Taschenmesser — nicht das Schweizer Taschenmesser-Katalog. + +--- + +## Was ODY nicht ist + +- Kein Cloud-Chatbot mit Offline-Modus +- Kein Hugging-Face-Browser +- Kein verstecktes PocketPal +- Kein Feature-Showcase +- Kein Marketing-Produkt + +--- + +## Für Entwickler und KI-Agenten + +Vor jeder Entscheidung — Feature, Design, Architektur — diese Fragen: + +1. Funktioniert das noch wenn kein Internet da ist? (Operate-Phase) +2. Schafft das eine Abhängigkeit die der Nutzer nicht kontrolliert? +3. Zeigt das dem Nutzer was er sich verlassen kann — oder was ODY kann? +4. Ist eine Simulation klar als Simulation gekennzeichnet? + +Wenn eine dieser Fragen mit „Nein" beantwortet wird: Entscheidung überdenken. + +--- + +*Dieses Dokument ist eine Verfassung, kein Lastenheft.* +*Wenige Grundsätze. Langer Bestand. Keine Sprint-Notizen.* diff --git a/evidence/apk-sha256.txt b/evidence/apk-sha256.txt new file mode 100644 index 0000000..9261541 --- /dev/null +++ b/evidence/apk-sha256.txt @@ -0,0 +1 @@ +ba7571505e142f2ce4f0b1c2968290a293e6e465732a3f5e685cdfe7ffddaf19 /root/.openclaw/workspace/ODY/Foundation/app/build/outputs/apk/debug/app-debug.apk diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f3cc46a --- /dev/null +++ b/gradle.properties @@ -0,0 +1,7 @@ +android.useAndroidX=true +android.enableJetifier=true +org.gradle.daemon=false +org.gradle.parallel=false +org.gradle.workers.max=1 +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 +kotlin.compiler.execution.strategy=in-process \ No newline at end of file diff --git a/gradle/agents/gradle-instrumentation-agent-8.9.jar b/gradle/agents/gradle-instrumentation-agent-8.9.jar new file mode 100644 index 0000000..1d8bc18 Binary files /dev/null and b/gradle/agents/gradle-instrumentation-agent-8.9.jar differ diff --git a/gradle/annotations-24.0.1.jar b/gradle/annotations-24.0.1.jar new file mode 100644 index 0000000..a4198f7 Binary files /dev/null and b/gradle/annotations-24.0.1.jar differ diff --git a/gradle/ant-1.10.13.jar b/gradle/ant-1.10.13.jar new file mode 100644 index 0000000..6dad9ad Binary files /dev/null and b/gradle/ant-1.10.13.jar differ diff --git a/gradle/ant-antlr-1.10.14.jar b/gradle/ant-antlr-1.10.14.jar new file mode 100644 index 0000000..03b7903 Binary files /dev/null and b/gradle/ant-antlr-1.10.14.jar differ diff --git a/gradle/ant-junit-1.10.13.jar b/gradle/ant-junit-1.10.13.jar new file mode 100644 index 0000000..0fb60d9 Binary files /dev/null and b/gradle/ant-junit-1.10.13.jar differ diff --git a/gradle/ant-launcher-1.10.13.jar b/gradle/ant-launcher-1.10.13.jar new file mode 100644 index 0000000..8718a72 Binary files /dev/null and b/gradle/ant-launcher-1.10.13.jar differ diff --git a/gradle/antlr4-runtime-4.7.2.jar b/gradle/antlr4-runtime-4.7.2.jar new file mode 100644 index 0000000..7a27e1b Binary files /dev/null and b/gradle/antlr4-runtime-4.7.2.jar differ diff --git a/gradle/asm-9.7.jar b/gradle/asm-9.7.jar new file mode 100644 index 0000000..fee9b02 Binary files /dev/null and b/gradle/asm-9.7.jar differ diff --git a/gradle/asm-commons-9.7.jar b/gradle/asm-commons-9.7.jar new file mode 100644 index 0000000..6a7d3a1 Binary files /dev/null and b/gradle/asm-commons-9.7.jar differ diff --git a/gradle/asm-tree-9.7.jar b/gradle/asm-tree-9.7.jar new file mode 100644 index 0000000..bbb3ca3 Binary files /dev/null and b/gradle/asm-tree-9.7.jar differ diff --git a/gradle/commons-codec-1.16.1.jar b/gradle/commons-codec-1.16.1.jar new file mode 100644 index 0000000..f896649 Binary files /dev/null and b/gradle/commons-codec-1.16.1.jar differ diff --git a/gradle/commons-compress-1.26.1.jar b/gradle/commons-compress-1.26.1.jar new file mode 100644 index 0000000..9a99788 Binary files /dev/null and b/gradle/commons-compress-1.26.1.jar differ diff --git a/gradle/commons-io-2.15.1.jar b/gradle/commons-io-2.15.1.jar new file mode 100644 index 0000000..d53be1f Binary files /dev/null and b/gradle/commons-io-2.15.1.jar differ diff --git a/gradle/commons-lang-2.6.jar b/gradle/commons-lang-2.6.jar new file mode 100644 index 0000000..98467d3 Binary files /dev/null and b/gradle/commons-lang-2.6.jar differ diff --git a/gradle/commons-lang3-3.14.0.jar b/gradle/commons-lang3-3.14.0.jar new file mode 100644 index 0000000..da9302f Binary files /dev/null and b/gradle/commons-lang3-3.14.0.jar differ diff --git a/gradle/error_prone_annotations-2.26.1.jar b/gradle/error_prone_annotations-2.26.1.jar new file mode 100644 index 0000000..61d49e2 Binary files /dev/null and b/gradle/error_prone_annotations-2.26.1.jar differ diff --git a/gradle/failureaccess-1.0.1.jar b/gradle/failureaccess-1.0.1.jar new file mode 100644 index 0000000..9b56dc7 Binary files /dev/null and b/gradle/failureaccess-1.0.1.jar differ diff --git a/gradle/fastutil-8.5.2-min.jar b/gradle/fastutil-8.5.2-min.jar new file mode 100644 index 0000000..8261b51 Binary files /dev/null and b/gradle/fastutil-8.5.2-min.jar differ diff --git a/gradle/file-events-0.22-milestone-26.jar b/gradle/file-events-0.22-milestone-26.jar new file mode 100644 index 0000000..26be9c2 Binary files /dev/null and b/gradle/file-events-0.22-milestone-26.jar differ diff --git a/gradle/file-events-linux-aarch64-0.22-milestone-26.jar b/gradle/file-events-linux-aarch64-0.22-milestone-26.jar new file mode 100644 index 0000000..45db021 Binary files /dev/null and b/gradle/file-events-linux-aarch64-0.22-milestone-26.jar differ diff --git a/gradle/file-events-linux-amd64-0.22-milestone-26.jar b/gradle/file-events-linux-amd64-0.22-milestone-26.jar new file mode 100644 index 0000000..f0aa384 Binary files /dev/null and b/gradle/file-events-linux-amd64-0.22-milestone-26.jar differ diff --git a/gradle/file-events-osx-aarch64-0.22-milestone-26.jar b/gradle/file-events-osx-aarch64-0.22-milestone-26.jar new file mode 100644 index 0000000..0422750 Binary files /dev/null and b/gradle/file-events-osx-aarch64-0.22-milestone-26.jar differ diff --git a/gradle/file-events-osx-amd64-0.22-milestone-26.jar b/gradle/file-events-osx-amd64-0.22-milestone-26.jar new file mode 100644 index 0000000..e2828fc Binary files /dev/null and b/gradle/file-events-osx-amd64-0.22-milestone-26.jar differ diff --git a/gradle/file-events-windows-amd64-0.22-milestone-26.jar b/gradle/file-events-windows-amd64-0.22-milestone-26.jar new file mode 100644 index 0000000..7380396 Binary files /dev/null and b/gradle/file-events-windows-amd64-0.22-milestone-26.jar differ diff --git a/gradle/file-events-windows-amd64-min-0.22-milestone-26.jar b/gradle/file-events-windows-amd64-min-0.22-milestone-26.jar new file mode 100644 index 0000000..1b56013 Binary files /dev/null and b/gradle/file-events-windows-amd64-min-0.22-milestone-26.jar differ diff --git a/gradle/file-events-windows-i386-0.22-milestone-26.jar b/gradle/file-events-windows-i386-0.22-milestone-26.jar new file mode 100644 index 0000000..658ad64 Binary files /dev/null and b/gradle/file-events-windows-i386-0.22-milestone-26.jar differ diff --git a/gradle/file-events-windows-i386-min-0.22-milestone-26.jar b/gradle/file-events-windows-i386-min-0.22-milestone-26.jar new file mode 100644 index 0000000..60c3a31 Binary files /dev/null and b/gradle/file-events-windows-i386-min-0.22-milestone-26.jar differ diff --git a/gradle/gradle-api-metadata-8.9.jar b/gradle/gradle-api-metadata-8.9.jar new file mode 100644 index 0000000..1b27300 Binary files /dev/null and b/gradle/gradle-api-metadata-8.9.jar differ diff --git a/gradle/gradle-base-asm-8.9.jar b/gradle/gradle-base-asm-8.9.jar new file mode 100644 index 0000000..78e45bd Binary files /dev/null and b/gradle/gradle-base-asm-8.9.jar differ diff --git a/gradle/gradle-base-services-8.9.jar b/gradle/gradle-base-services-8.9.jar new file mode 100644 index 0000000..5dffda4 Binary files /dev/null and b/gradle/gradle-base-services-8.9.jar differ diff --git a/gradle/gradle-base-services-groovy-8.9.jar b/gradle/gradle-base-services-groovy-8.9.jar new file mode 100644 index 0000000..bc42b7f Binary files /dev/null and b/gradle/gradle-base-services-groovy-8.9.jar differ diff --git a/gradle/gradle-build-cache-8.9.jar b/gradle/gradle-build-cache-8.9.jar new file mode 100644 index 0000000..f7859bb Binary files /dev/null and b/gradle/gradle-build-cache-8.9.jar differ diff --git a/gradle/gradle-build-cache-base-8.9.jar b/gradle/gradle-build-cache-base-8.9.jar new file mode 100644 index 0000000..7dcf038 Binary files /dev/null and b/gradle/gradle-build-cache-base-8.9.jar differ diff --git a/gradle/gradle-build-cache-local-8.9.jar b/gradle/gradle-build-cache-local-8.9.jar new file mode 100644 index 0000000..b207f66 Binary files /dev/null and b/gradle/gradle-build-cache-local-8.9.jar differ diff --git a/gradle/gradle-build-cache-packaging-8.9.jar b/gradle/gradle-build-cache-packaging-8.9.jar new file mode 100644 index 0000000..c674c58 Binary files /dev/null and b/gradle/gradle-build-cache-packaging-8.9.jar differ diff --git a/gradle/gradle-build-cache-spi-8.9.jar b/gradle/gradle-build-cache-spi-8.9.jar new file mode 100644 index 0000000..ea84954 Binary files /dev/null and b/gradle/gradle-build-cache-spi-8.9.jar differ diff --git a/gradle/gradle-build-configuration-8.9.jar b/gradle/gradle-build-configuration-8.9.jar new file mode 100644 index 0000000..983dc4e Binary files /dev/null and b/gradle/gradle-build-configuration-8.9.jar differ diff --git a/gradle/gradle-build-events-8.9.jar b/gradle/gradle-build-events-8.9.jar new file mode 100644 index 0000000..c00d934 Binary files /dev/null and b/gradle/gradle-build-events-8.9.jar differ diff --git a/gradle/gradle-build-operations-8.9.jar b/gradle/gradle-build-operations-8.9.jar new file mode 100644 index 0000000..cd31744 Binary files /dev/null and b/gradle/gradle-build-operations-8.9.jar differ diff --git a/gradle/gradle-build-option-8.9.jar b/gradle/gradle-build-option-8.9.jar new file mode 100644 index 0000000..667daa4 Binary files /dev/null and b/gradle/gradle-build-option-8.9.jar differ diff --git a/gradle/gradle-build-process-services-8.9.jar b/gradle/gradle-build-process-services-8.9.jar new file mode 100644 index 0000000..f7595ff Binary files /dev/null and b/gradle/gradle-build-process-services-8.9.jar differ diff --git a/gradle/gradle-build-state-8.9.jar b/gradle/gradle-build-state-8.9.jar new file mode 100644 index 0000000..885d432 Binary files /dev/null and b/gradle/gradle-build-state-8.9.jar differ diff --git a/gradle/gradle-cli-8.9.jar b/gradle/gradle-cli-8.9.jar new file mode 100644 index 0000000..dcdad46 Binary files /dev/null and b/gradle/gradle-cli-8.9.jar differ diff --git a/gradle/gradle-client-services-8.9.jar b/gradle/gradle-client-services-8.9.jar new file mode 100644 index 0000000..e95c3b3 Binary files /dev/null and b/gradle/gradle-client-services-8.9.jar differ diff --git a/gradle/gradle-concurrent-8.9.jar b/gradle/gradle-concurrent-8.9.jar new file mode 100644 index 0000000..2a95b48 Binary files /dev/null and b/gradle/gradle-concurrent-8.9.jar differ diff --git a/gradle/gradle-core-8.9.jar b/gradle/gradle-core-8.9.jar new file mode 100644 index 0000000..3c46964 Binary files /dev/null and b/gradle/gradle-core-8.9.jar differ diff --git a/gradle/gradle-core-api-8.9.jar b/gradle/gradle-core-api-8.9.jar new file mode 100644 index 0000000..b19de66 Binary files /dev/null and b/gradle/gradle-core-api-8.9.jar differ diff --git a/gradle/gradle-daemon-main-8.9.jar b/gradle/gradle-daemon-main-8.9.jar new file mode 100644 index 0000000..02489b1 Binary files /dev/null and b/gradle/gradle-daemon-main-8.9.jar differ diff --git a/gradle/gradle-daemon-protocol-8.9.jar b/gradle/gradle-daemon-protocol-8.9.jar new file mode 100644 index 0000000..75dad69 Binary files /dev/null and b/gradle/gradle-daemon-protocol-8.9.jar differ diff --git a/gradle/gradle-daemon-server-8.9.jar b/gradle/gradle-daemon-server-8.9.jar new file mode 100644 index 0000000..8756bc7 Binary files /dev/null and b/gradle/gradle-daemon-server-8.9.jar differ diff --git a/gradle/gradle-daemon-services-8.9.jar b/gradle/gradle-daemon-services-8.9.jar new file mode 100644 index 0000000..92d811d Binary files /dev/null and b/gradle/gradle-daemon-services-8.9.jar differ diff --git a/gradle/gradle-declarative-dsl-api-8.9.jar b/gradle/gradle-declarative-dsl-api-8.9.jar new file mode 100644 index 0000000..b4440a5 Binary files /dev/null and b/gradle/gradle-declarative-dsl-api-8.9.jar differ diff --git a/gradle/gradle-declarative-dsl-core-8.9.jar b/gradle/gradle-declarative-dsl-core-8.9.jar new file mode 100644 index 0000000..b00a2c4 Binary files /dev/null and b/gradle/gradle-declarative-dsl-core-8.9.jar differ diff --git a/gradle/gradle-declarative-dsl-evaluator-8.9.jar b/gradle/gradle-declarative-dsl-evaluator-8.9.jar new file mode 100644 index 0000000..e4df0f4 Binary files /dev/null and b/gradle/gradle-declarative-dsl-evaluator-8.9.jar differ diff --git a/gradle/gradle-declarative-dsl-provider-8.9.jar b/gradle/gradle-declarative-dsl-provider-8.9.jar new file mode 100644 index 0000000..11fae56 Binary files /dev/null and b/gradle/gradle-declarative-dsl-provider-8.9.jar differ diff --git a/gradle/gradle-declarative-dsl-tooling-models-8.9.jar b/gradle/gradle-declarative-dsl-tooling-models-8.9.jar new file mode 100644 index 0000000..c32e35d Binary files /dev/null and b/gradle/gradle-declarative-dsl-tooling-models-8.9.jar differ diff --git a/gradle/gradle-enterprise-logging-8.9.jar b/gradle/gradle-enterprise-logging-8.9.jar new file mode 100644 index 0000000..3274f97 Binary files /dev/null and b/gradle/gradle-enterprise-logging-8.9.jar differ diff --git a/gradle/gradle-enterprise-operations-8.9.jar b/gradle/gradle-enterprise-operations-8.9.jar new file mode 100644 index 0000000..7fe12cc Binary files /dev/null and b/gradle/gradle-enterprise-operations-8.9.jar differ diff --git a/gradle/gradle-enterprise-workers-8.9.jar b/gradle/gradle-enterprise-workers-8.9.jar new file mode 100644 index 0000000..1a59f38 Binary files /dev/null and b/gradle/gradle-enterprise-workers-8.9.jar differ diff --git a/gradle/gradle-execution-8.9.jar b/gradle/gradle-execution-8.9.jar new file mode 100644 index 0000000..6bcc5b2 Binary files /dev/null and b/gradle/gradle-execution-8.9.jar differ diff --git a/gradle/gradle-file-collections-8.9.jar b/gradle/gradle-file-collections-8.9.jar new file mode 100644 index 0000000..68bf002 Binary files /dev/null and b/gradle/gradle-file-collections-8.9.jar differ diff --git a/gradle/gradle-file-temp-8.9.jar b/gradle/gradle-file-temp-8.9.jar new file mode 100644 index 0000000..da913c4 Binary files /dev/null and b/gradle/gradle-file-temp-8.9.jar differ diff --git a/gradle/gradle-file-watching-8.9.jar b/gradle/gradle-file-watching-8.9.jar new file mode 100644 index 0000000..ab586ec Binary files /dev/null and b/gradle/gradle-file-watching-8.9.jar differ diff --git a/gradle/gradle-files-8.9.jar b/gradle/gradle-files-8.9.jar new file mode 100644 index 0000000..1ad22e7 Binary files /dev/null and b/gradle/gradle-files-8.9.jar differ diff --git a/gradle/gradle-functional-8.9.jar b/gradle/gradle-functional-8.9.jar new file mode 100644 index 0000000..6450341 Binary files /dev/null and b/gradle/gradle-functional-8.9.jar differ diff --git a/gradle/gradle-gradle-cli-main-8.9.jar b/gradle/gradle-gradle-cli-main-8.9.jar new file mode 100644 index 0000000..498c9b6 Binary files /dev/null and b/gradle/gradle-gradle-cli-main-8.9.jar differ diff --git a/gradle/gradle-hashing-8.9.jar b/gradle/gradle-hashing-8.9.jar new file mode 100644 index 0000000..daf4147 Binary files /dev/null and b/gradle/gradle-hashing-8.9.jar differ diff --git a/gradle/gradle-input-tracking-8.9.jar b/gradle/gradle-input-tracking-8.9.jar new file mode 100644 index 0000000..501111e Binary files /dev/null and b/gradle/gradle-input-tracking-8.9.jar differ diff --git a/gradle/gradle-installation-beacon-8.9.jar b/gradle/gradle-installation-beacon-8.9.jar new file mode 100644 index 0000000..e7a2195 Binary files /dev/null and b/gradle/gradle-installation-beacon-8.9.jar differ diff --git a/gradle/gradle-internal-instrumentation-api-8.9.jar b/gradle/gradle-internal-instrumentation-api-8.9.jar new file mode 100644 index 0000000..217a85e Binary files /dev/null and b/gradle/gradle-internal-instrumentation-api-8.9.jar differ diff --git a/gradle/gradle-io-8.9.jar b/gradle/gradle-io-8.9.jar new file mode 100644 index 0000000..0a43743 Binary files /dev/null and b/gradle/gradle-io-8.9.jar differ diff --git a/gradle/gradle-java-language-extensions-8.9.jar b/gradle/gradle-java-language-extensions-8.9.jar new file mode 100644 index 0000000..ad8e42d Binary files /dev/null and b/gradle/gradle-java-language-extensions-8.9.jar differ diff --git a/gradle/gradle-jvm-services-8.9.jar b/gradle/gradle-jvm-services-8.9.jar new file mode 100644 index 0000000..535d090 Binary files /dev/null and b/gradle/gradle-jvm-services-8.9.jar differ diff --git a/gradle/gradle-kotlin-dsl-8.9.jar b/gradle/gradle-kotlin-dsl-8.9.jar new file mode 100644 index 0000000..994f4c5 Binary files /dev/null and b/gradle/gradle-kotlin-dsl-8.9.jar differ diff --git a/gradle/gradle-kotlin-dsl-extensions-8.9.jar b/gradle/gradle-kotlin-dsl-extensions-8.9.jar new file mode 100644 index 0000000..e4e8086 Binary files /dev/null and b/gradle/gradle-kotlin-dsl-extensions-8.9.jar differ diff --git a/gradle/gradle-kotlin-dsl-shared-runtime-8.9.jar b/gradle/gradle-kotlin-dsl-shared-runtime-8.9.jar new file mode 100644 index 0000000..49a8350 Binary files /dev/null and b/gradle/gradle-kotlin-dsl-shared-runtime-8.9.jar differ diff --git a/gradle/gradle-kotlin-dsl-tooling-models-8.9.jar b/gradle/gradle-kotlin-dsl-tooling-models-8.9.jar new file mode 100644 index 0000000..9c621b7 Binary files /dev/null and b/gradle/gradle-kotlin-dsl-tooling-models-8.9.jar differ diff --git a/gradle/gradle-launcher-8.9.jar b/gradle/gradle-launcher-8.9.jar new file mode 100644 index 0000000..4ee8f7a Binary files /dev/null and b/gradle/gradle-launcher-8.9.jar differ diff --git a/gradle/gradle-logging-8.9.jar b/gradle/gradle-logging-8.9.jar new file mode 100644 index 0000000..23f090b Binary files /dev/null and b/gradle/gradle-logging-8.9.jar differ diff --git a/gradle/gradle-logging-api-8.9.jar b/gradle/gradle-logging-api-8.9.jar new file mode 100644 index 0000000..2c3617b Binary files /dev/null and b/gradle/gradle-logging-api-8.9.jar differ diff --git a/gradle/gradle-messaging-8.9.jar b/gradle/gradle-messaging-8.9.jar new file mode 100644 index 0000000..d9f876e Binary files /dev/null and b/gradle/gradle-messaging-8.9.jar differ diff --git a/gradle/gradle-model-core-8.9.jar b/gradle/gradle-model-core-8.9.jar new file mode 100644 index 0000000..dcc662f Binary files /dev/null and b/gradle/gradle-model-core-8.9.jar differ diff --git a/gradle/gradle-model-groovy-8.9.jar b/gradle/gradle-model-groovy-8.9.jar new file mode 100644 index 0000000..96bc2f6 Binary files /dev/null and b/gradle/gradle-model-groovy-8.9.jar differ diff --git a/gradle/gradle-native-8.9.jar b/gradle/gradle-native-8.9.jar new file mode 100644 index 0000000..5de9aed Binary files /dev/null and b/gradle/gradle-native-8.9.jar differ diff --git a/gradle/gradle-normalization-java-8.9.jar b/gradle/gradle-normalization-java-8.9.jar new file mode 100644 index 0000000..5ba25de Binary files /dev/null and b/gradle/gradle-normalization-java-8.9.jar differ diff --git a/gradle/gradle-persistent-cache-8.9.jar b/gradle/gradle-persistent-cache-8.9.jar new file mode 100644 index 0000000..81a9810 Binary files /dev/null and b/gradle/gradle-persistent-cache-8.9.jar differ diff --git a/gradle/gradle-problems-8.9.jar b/gradle/gradle-problems-8.9.jar new file mode 100644 index 0000000..5168689 Binary files /dev/null and b/gradle/gradle-problems-8.9.jar differ diff --git a/gradle/gradle-problems-api-8.9.jar b/gradle/gradle-problems-api-8.9.jar new file mode 100644 index 0000000..3580819 Binary files /dev/null and b/gradle/gradle-problems-api-8.9.jar differ diff --git a/gradle/gradle-process-services-8.9.jar b/gradle/gradle-process-services-8.9.jar new file mode 100644 index 0000000..c12f039 Binary files /dev/null and b/gradle/gradle-process-services-8.9.jar differ diff --git a/gradle/gradle-resources-8.9.jar b/gradle/gradle-resources-8.9.jar new file mode 100644 index 0000000..021c6b4 Binary files /dev/null and b/gradle/gradle-resources-8.9.jar differ diff --git a/gradle/gradle-runtime-api-info-8.9.jar b/gradle/gradle-runtime-api-info-8.9.jar new file mode 100644 index 0000000..3dee139 Binary files /dev/null and b/gradle/gradle-runtime-api-info-8.9.jar differ diff --git a/gradle/gradle-serialization-8.9.jar b/gradle/gradle-serialization-8.9.jar new file mode 100644 index 0000000..7b467b3 Binary files /dev/null and b/gradle/gradle-serialization-8.9.jar differ diff --git a/gradle/gradle-service-provider-8.9.jar b/gradle/gradle-service-provider-8.9.jar new file mode 100644 index 0000000..00e96d3 Binary files /dev/null and b/gradle/gradle-service-provider-8.9.jar differ diff --git a/gradle/gradle-snapshots-8.9.jar b/gradle/gradle-snapshots-8.9.jar new file mode 100644 index 0000000..5eaf068 Binary files /dev/null and b/gradle/gradle-snapshots-8.9.jar differ diff --git a/gradle/gradle-time-8.9.jar b/gradle/gradle-time-8.9.jar new file mode 100644 index 0000000..ba2469c Binary files /dev/null and b/gradle/gradle-time-8.9.jar differ diff --git a/gradle/gradle-toolchains-jvm-shared-8.9.jar b/gradle/gradle-toolchains-jvm-shared-8.9.jar new file mode 100644 index 0000000..91ec652 Binary files /dev/null and b/gradle/gradle-toolchains-jvm-shared-8.9.jar differ diff --git a/gradle/gradle-tooling-api-8.9.jar b/gradle/gradle-tooling-api-8.9.jar new file mode 100644 index 0000000..e0232f1 Binary files /dev/null and b/gradle/gradle-tooling-api-8.9.jar differ diff --git a/gradle/gradle-tooling-api-provider-8.9.jar b/gradle/gradle-tooling-api-provider-8.9.jar new file mode 100644 index 0000000..65738c8 Binary files /dev/null and b/gradle/gradle-tooling-api-provider-8.9.jar differ diff --git a/gradle/gradle-worker-main-8.9.jar b/gradle/gradle-worker-main-8.9.jar new file mode 100644 index 0000000..0c0d40a Binary files /dev/null and b/gradle/gradle-worker-main-8.9.jar differ diff --git a/gradle/gradle-wrapper-shared-8.9.jar b/gradle/gradle-wrapper-shared-8.9.jar new file mode 100644 index 0000000..25e3a90 Binary files /dev/null and b/gradle/gradle-wrapper-shared-8.9.jar differ diff --git a/gradle/groovy-3.0.21.jar b/gradle/groovy-3.0.21.jar new file mode 100644 index 0000000..f2e0f51 Binary files /dev/null and b/gradle/groovy-3.0.21.jar differ diff --git a/gradle/groovy-ant-3.0.21.jar b/gradle/groovy-ant-3.0.21.jar new file mode 100644 index 0000000..a49d2eb Binary files /dev/null and b/gradle/groovy-ant-3.0.21.jar differ diff --git a/gradle/groovy-astbuilder-3.0.21.jar b/gradle/groovy-astbuilder-3.0.21.jar new file mode 100644 index 0000000..74f316d Binary files /dev/null and b/gradle/groovy-astbuilder-3.0.21.jar differ diff --git a/gradle/groovy-console-3.0.21.jar b/gradle/groovy-console-3.0.21.jar new file mode 100644 index 0000000..5ccff9d Binary files /dev/null and b/gradle/groovy-console-3.0.21.jar differ diff --git a/gradle/groovy-datetime-3.0.21.jar b/gradle/groovy-datetime-3.0.21.jar new file mode 100644 index 0000000..2db4775 Binary files /dev/null and b/gradle/groovy-datetime-3.0.21.jar differ diff --git a/gradle/groovy-dateutil-3.0.21.jar b/gradle/groovy-dateutil-3.0.21.jar new file mode 100644 index 0000000..51cc4ab Binary files /dev/null and b/gradle/groovy-dateutil-3.0.21.jar differ diff --git a/gradle/groovy-docgenerator-3.0.21.jar b/gradle/groovy-docgenerator-3.0.21.jar new file mode 100644 index 0000000..224945d Binary files /dev/null and b/gradle/groovy-docgenerator-3.0.21.jar differ diff --git a/gradle/groovy-groovydoc-3.0.21.jar b/gradle/groovy-groovydoc-3.0.21.jar new file mode 100644 index 0000000..2b5128b Binary files /dev/null and b/gradle/groovy-groovydoc-3.0.21.jar differ diff --git a/gradle/groovy-json-3.0.21.jar b/gradle/groovy-json-3.0.21.jar new file mode 100644 index 0000000..4cfa5e0 Binary files /dev/null and b/gradle/groovy-json-3.0.21.jar differ diff --git a/gradle/groovy-nio-3.0.21.jar b/gradle/groovy-nio-3.0.21.jar new file mode 100644 index 0000000..4e0a8a7 Binary files /dev/null and b/gradle/groovy-nio-3.0.21.jar differ diff --git a/gradle/groovy-sql-3.0.21.jar b/gradle/groovy-sql-3.0.21.jar new file mode 100644 index 0000000..8728761 Binary files /dev/null and b/gradle/groovy-sql-3.0.21.jar differ diff --git a/gradle/groovy-swing-3.0.21.jar b/gradle/groovy-swing-3.0.21.jar new file mode 100644 index 0000000..4cc6b44 Binary files /dev/null and b/gradle/groovy-swing-3.0.21.jar differ diff --git a/gradle/groovy-templates-3.0.21.jar b/gradle/groovy-templates-3.0.21.jar new file mode 100644 index 0000000..ee8efc5 Binary files /dev/null and b/gradle/groovy-templates-3.0.21.jar differ diff --git a/gradle/groovy-test-3.0.21.jar b/gradle/groovy-test-3.0.21.jar new file mode 100644 index 0000000..bcbe2c5 Binary files /dev/null and b/gradle/groovy-test-3.0.21.jar differ diff --git a/gradle/groovy-xml-3.0.21.jar b/gradle/groovy-xml-3.0.21.jar new file mode 100644 index 0000000..40d75f6 Binary files /dev/null and b/gradle/groovy-xml-3.0.21.jar differ diff --git a/gradle/gson-2.10.jar b/gradle/gson-2.10.jar new file mode 100644 index 0000000..4b57f5b Binary files /dev/null and b/gradle/gson-2.10.jar differ diff --git a/gradle/guava-32.1.2-jre.jar b/gradle/guava-32.1.2-jre.jar new file mode 100644 index 0000000..e71fd46 Binary files /dev/null and b/gradle/guava-32.1.2-jre.jar differ diff --git a/gradle/hamcrest-core-1.3.jar b/gradle/hamcrest-core-1.3.jar new file mode 100644 index 0000000..9d5fe16 Binary files /dev/null and b/gradle/hamcrest-core-1.3.jar differ diff --git a/gradle/jansi-1.18.jar b/gradle/jansi-1.18.jar new file mode 100644 index 0000000..a7be6db Binary files /dev/null and b/gradle/jansi-1.18.jar differ diff --git a/gradle/javaparser-core-3.17.0.jar b/gradle/javaparser-core-3.17.0.jar new file mode 100644 index 0000000..8d65838 Binary files /dev/null and b/gradle/javaparser-core-3.17.0.jar differ diff --git a/gradle/javax.inject-1.jar b/gradle/javax.inject-1.jar new file mode 100644 index 0000000..b2a9d0b Binary files /dev/null and b/gradle/javax.inject-1.jar differ diff --git a/gradle/jcl-over-slf4j-1.7.36.jar b/gradle/jcl-over-slf4j-1.7.36.jar new file mode 100644 index 0000000..3ecd7d5 Binary files /dev/null and b/gradle/jcl-over-slf4j-1.7.36.jar differ diff --git a/gradle/jsr305-3.0.2.jar b/gradle/jsr305-3.0.2.jar new file mode 100644 index 0000000..59222d9 Binary files /dev/null and b/gradle/jsr305-3.0.2.jar differ diff --git a/gradle/jul-to-slf4j-1.7.36.jar b/gradle/jul-to-slf4j-1.7.36.jar new file mode 100644 index 0000000..ae8f815 Binary files /dev/null and b/gradle/jul-to-slf4j-1.7.36.jar differ diff --git a/gradle/junit-4.13.2.jar b/gradle/junit-4.13.2.jar new file mode 100644 index 0000000..6da55d8 Binary files /dev/null and b/gradle/junit-4.13.2.jar differ diff --git a/gradle/kotlin-assignment-compiler-plugin-embeddable-1.9.23.jar b/gradle/kotlin-assignment-compiler-plugin-embeddable-1.9.23.jar new file mode 100644 index 0000000..756e05d Binary files /dev/null and b/gradle/kotlin-assignment-compiler-plugin-embeddable-1.9.23.jar differ diff --git a/gradle/kotlin-compiler-embeddable-1.9.23.jar b/gradle/kotlin-compiler-embeddable-1.9.23.jar new file mode 100644 index 0000000..d552ddb Binary files /dev/null and b/gradle/kotlin-compiler-embeddable-1.9.23.jar differ diff --git a/gradle/kotlin-daemon-embeddable-1.9.23.jar b/gradle/kotlin-daemon-embeddable-1.9.23.jar new file mode 100644 index 0000000..9f84f29 Binary files /dev/null and b/gradle/kotlin-daemon-embeddable-1.9.23.jar differ diff --git a/gradle/kotlin-reflect-1.9.23.jar b/gradle/kotlin-reflect-1.9.23.jar new file mode 100644 index 0000000..fdab454 Binary files /dev/null and b/gradle/kotlin-reflect-1.9.23.jar differ diff --git a/gradle/kotlin-sam-with-receiver-compiler-plugin-1.9.23.jar b/gradle/kotlin-sam-with-receiver-compiler-plugin-1.9.23.jar new file mode 100644 index 0000000..e410936 Binary files /dev/null and b/gradle/kotlin-sam-with-receiver-compiler-plugin-1.9.23.jar differ diff --git a/gradle/kotlin-script-runtime-1.9.23.jar b/gradle/kotlin-script-runtime-1.9.23.jar new file mode 100644 index 0000000..a3d0a3d Binary files /dev/null and b/gradle/kotlin-script-runtime-1.9.23.jar differ diff --git a/gradle/kotlin-scripting-common-1.9.23.jar b/gradle/kotlin-scripting-common-1.9.23.jar new file mode 100644 index 0000000..1a59b5f Binary files /dev/null and b/gradle/kotlin-scripting-common-1.9.23.jar differ diff --git a/gradle/kotlin-scripting-compiler-embeddable-1.9.23.jar b/gradle/kotlin-scripting-compiler-embeddable-1.9.23.jar new file mode 100644 index 0000000..485e824 Binary files /dev/null and b/gradle/kotlin-scripting-compiler-embeddable-1.9.23.jar differ diff --git a/gradle/kotlin-scripting-compiler-impl-embeddable-1.9.23.jar b/gradle/kotlin-scripting-compiler-impl-embeddable-1.9.23.jar new file mode 100644 index 0000000..3cf0206 Binary files /dev/null and b/gradle/kotlin-scripting-compiler-impl-embeddable-1.9.23.jar differ diff --git a/gradle/kotlin-scripting-jvm-1.9.23.jar b/gradle/kotlin-scripting-jvm-1.9.23.jar new file mode 100644 index 0000000..697ccc9 Binary files /dev/null and b/gradle/kotlin-scripting-jvm-1.9.23.jar differ diff --git a/gradle/kotlin-stdlib-1.9.23.jar b/gradle/kotlin-stdlib-1.9.23.jar new file mode 100644 index 0000000..b0728ef Binary files /dev/null and b/gradle/kotlin-stdlib-1.9.23.jar differ diff --git a/gradle/kotlinx-metadata-jvm-0.5.0.jar b/gradle/kotlinx-metadata-jvm-0.5.0.jar new file mode 100644 index 0000000..e6c983e Binary files /dev/null and b/gradle/kotlinx-metadata-jvm-0.5.0.jar differ diff --git a/gradle/kotlinx-serialization-core-jvm-1.6.2.jar b/gradle/kotlinx-serialization-core-jvm-1.6.2.jar new file mode 100644 index 0000000..e263ee0 Binary files /dev/null and b/gradle/kotlinx-serialization-core-jvm-1.6.2.jar differ diff --git a/gradle/kotlinx-serialization-json-jvm-1.6.2.jar b/gradle/kotlinx-serialization-json-jvm-1.6.2.jar new file mode 100644 index 0000000..71cdc47 Binary files /dev/null and b/gradle/kotlinx-serialization-json-jvm-1.6.2.jar differ diff --git a/gradle/kryo-2.24.0.jar b/gradle/kryo-2.24.0.jar new file mode 100644 index 0000000..4d18180 Binary files /dev/null and b/gradle/kryo-2.24.0.jar differ diff --git a/gradle/log4j-over-slf4j-1.7.36.jar b/gradle/log4j-over-slf4j-1.7.36.jar new file mode 100644 index 0000000..13fbc0a Binary files /dev/null and b/gradle/log4j-over-slf4j-1.7.36.jar differ diff --git a/gradle/minlog-1.2.jar b/gradle/minlog-1.2.jar new file mode 100644 index 0000000..3d174a6 Binary files /dev/null and b/gradle/minlog-1.2.jar differ diff --git a/gradle/native-platform-0.22-milestone-26.jar b/gradle/native-platform-0.22-milestone-26.jar new file mode 100644 index 0000000..528a40f Binary files /dev/null and b/gradle/native-platform-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-freebsd-amd64-libcpp-0.22-milestone-26.jar b/gradle/native-platform-freebsd-amd64-libcpp-0.22-milestone-26.jar new file mode 100644 index 0000000..57da340 Binary files /dev/null and b/gradle/native-platform-freebsd-amd64-libcpp-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-linux-aarch64-0.22-milestone-26.jar b/gradle/native-platform-linux-aarch64-0.22-milestone-26.jar new file mode 100644 index 0000000..55a6fdb Binary files /dev/null and b/gradle/native-platform-linux-aarch64-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-linux-aarch64-ncurses5-0.22-milestone-26.jar b/gradle/native-platform-linux-aarch64-ncurses5-0.22-milestone-26.jar new file mode 100644 index 0000000..397115e Binary files /dev/null and b/gradle/native-platform-linux-aarch64-ncurses5-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-linux-aarch64-ncurses6-0.22-milestone-26.jar b/gradle/native-platform-linux-aarch64-ncurses6-0.22-milestone-26.jar new file mode 100644 index 0000000..65dd188 Binary files /dev/null and b/gradle/native-platform-linux-aarch64-ncurses6-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-linux-amd64-0.22-milestone-26.jar b/gradle/native-platform-linux-amd64-0.22-milestone-26.jar new file mode 100644 index 0000000..cdbb226 Binary files /dev/null and b/gradle/native-platform-linux-amd64-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-linux-amd64-ncurses5-0.22-milestone-26.jar b/gradle/native-platform-linux-amd64-ncurses5-0.22-milestone-26.jar new file mode 100644 index 0000000..6eaf7e3 Binary files /dev/null and b/gradle/native-platform-linux-amd64-ncurses5-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-linux-amd64-ncurses6-0.22-milestone-26.jar b/gradle/native-platform-linux-amd64-ncurses6-0.22-milestone-26.jar new file mode 100644 index 0000000..8d0e2d2 Binary files /dev/null and b/gradle/native-platform-linux-amd64-ncurses6-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-osx-aarch64-0.22-milestone-26.jar b/gradle/native-platform-osx-aarch64-0.22-milestone-26.jar new file mode 100644 index 0000000..f6ad7d3 Binary files /dev/null and b/gradle/native-platform-osx-aarch64-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-osx-amd64-0.22-milestone-26.jar b/gradle/native-platform-osx-amd64-0.22-milestone-26.jar new file mode 100644 index 0000000..de37d30 Binary files /dev/null and b/gradle/native-platform-osx-amd64-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-windows-amd64-0.22-milestone-26.jar b/gradle/native-platform-windows-amd64-0.22-milestone-26.jar new file mode 100644 index 0000000..7c3ff30 Binary files /dev/null and b/gradle/native-platform-windows-amd64-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-windows-amd64-min-0.22-milestone-26.jar b/gradle/native-platform-windows-amd64-min-0.22-milestone-26.jar new file mode 100644 index 0000000..707dfd1 Binary files /dev/null and b/gradle/native-platform-windows-amd64-min-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-windows-i386-0.22-milestone-26.jar b/gradle/native-platform-windows-i386-0.22-milestone-26.jar new file mode 100644 index 0000000..e479f3c Binary files /dev/null and b/gradle/native-platform-windows-i386-0.22-milestone-26.jar differ diff --git a/gradle/native-platform-windows-i386-min-0.22-milestone-26.jar b/gradle/native-platform-windows-i386-min-0.22-milestone-26.jar new file mode 100644 index 0000000..51c5c5a Binary files /dev/null and b/gradle/native-platform-windows-i386-min-0.22-milestone-26.jar differ diff --git a/gradle/objenesis-2.6.jar b/gradle/objenesis-2.6.jar new file mode 100644 index 0000000..b4b29d5 Binary files /dev/null and b/gradle/objenesis-2.6.jar differ diff --git a/gradle/plugins/aws-java-sdk-core-1.12.651.jar b/gradle/plugins/aws-java-sdk-core-1.12.651.jar new file mode 100644 index 0000000..e3151a1 Binary files /dev/null and b/gradle/plugins/aws-java-sdk-core-1.12.651.jar differ diff --git a/gradle/plugins/aws-java-sdk-kms-1.12.651.jar b/gradle/plugins/aws-java-sdk-kms-1.12.651.jar new file mode 100644 index 0000000..3ad4e78 Binary files /dev/null and b/gradle/plugins/aws-java-sdk-kms-1.12.651.jar differ diff --git a/gradle/plugins/aws-java-sdk-s3-1.12.651.jar b/gradle/plugins/aws-java-sdk-s3-1.12.651.jar new file mode 100644 index 0000000..f0584eb Binary files /dev/null and b/gradle/plugins/aws-java-sdk-s3-1.12.651.jar differ diff --git a/gradle/plugins/aws-java-sdk-sts-1.12.651.jar b/gradle/plugins/aws-java-sdk-sts-1.12.651.jar new file mode 100644 index 0000000..1ae900e Binary files /dev/null and b/gradle/plugins/aws-java-sdk-sts-1.12.651.jar differ diff --git a/gradle/plugins/bcpg-jdk18on-1.78.1.jar b/gradle/plugins/bcpg-jdk18on-1.78.1.jar new file mode 100644 index 0000000..ed00085 Binary files /dev/null and b/gradle/plugins/bcpg-jdk18on-1.78.1.jar differ diff --git a/gradle/plugins/bcprov-jdk18on-1.78.1.jar b/gradle/plugins/bcprov-jdk18on-1.78.1.jar new file mode 100644 index 0000000..6726f83 Binary files /dev/null and b/gradle/plugins/bcprov-jdk18on-1.78.1.jar differ diff --git a/gradle/plugins/bcutil-jdk18on-1.78.1.jar b/gradle/plugins/bcutil-jdk18on-1.78.1.jar new file mode 100644 index 0000000..348a2ff Binary files /dev/null and b/gradle/plugins/bcutil-jdk18on-1.78.1.jar differ diff --git a/gradle/plugins/bsh-2.0b6.jar b/gradle/plugins/bsh-2.0b6.jar new file mode 100644 index 0000000..29d71a9 Binary files /dev/null and b/gradle/plugins/bsh-2.0b6.jar differ diff --git a/gradle/plugins/dd-plist-1.27.jar b/gradle/plugins/dd-plist-1.27.jar new file mode 100644 index 0000000..aaf03f3 Binary files /dev/null and b/gradle/plugins/dd-plist-1.27.jar differ diff --git a/gradle/plugins/eddsa-0.3.0.jar b/gradle/plugins/eddsa-0.3.0.jar new file mode 100644 index 0000000..31a271d Binary files /dev/null and b/gradle/plugins/eddsa-0.3.0.jar differ diff --git a/gradle/plugins/google-api-client-1.34.0.jar b/gradle/plugins/google-api-client-1.34.0.jar new file mode 100644 index 0000000..e1bd4ed Binary files /dev/null and b/gradle/plugins/google-api-client-1.34.0.jar differ diff --git a/gradle/plugins/google-api-services-storage-v1-rev20220705-1.32.1.jar b/gradle/plugins/google-api-services-storage-v1-rev20220705-1.32.1.jar new file mode 100644 index 0000000..91d26e4 Binary files /dev/null and b/gradle/plugins/google-api-services-storage-v1-rev20220705-1.32.1.jar differ diff --git a/gradle/plugins/google-http-client-1.42.2.jar b/gradle/plugins/google-http-client-1.42.2.jar new file mode 100644 index 0000000..e8a4497 Binary files /dev/null and b/gradle/plugins/google-http-client-1.42.2.jar differ diff --git a/gradle/plugins/google-http-client-apache-v2-1.42.2.jar b/gradle/plugins/google-http-client-apache-v2-1.42.2.jar new file mode 100644 index 0000000..edf37d0 Binary files /dev/null and b/gradle/plugins/google-http-client-apache-v2-1.42.2.jar differ diff --git a/gradle/plugins/google-http-client-gson-1.42.2.jar b/gradle/plugins/google-http-client-gson-1.42.2.jar new file mode 100644 index 0000000..77d3af1 Binary files /dev/null and b/gradle/plugins/google-http-client-gson-1.42.2.jar differ diff --git a/gradle/plugins/google-oauth-client-1.34.1.jar b/gradle/plugins/google-oauth-client-1.34.1.jar new file mode 100644 index 0000000..795f7e4 Binary files /dev/null and b/gradle/plugins/google-oauth-client-1.34.1.jar differ diff --git a/gradle/plugins/gradle-antlr-8.9.jar b/gradle/plugins/gradle-antlr-8.9.jar new file mode 100644 index 0000000..c9a19aa Binary files /dev/null and b/gradle/plugins/gradle-antlr-8.9.jar differ diff --git a/gradle/plugins/gradle-base-ide-plugins-8.9.jar b/gradle/plugins/gradle-base-ide-plugins-8.9.jar new file mode 100644 index 0000000..f33c569 Binary files /dev/null and b/gradle/plugins/gradle-base-ide-plugins-8.9.jar differ diff --git a/gradle/plugins/gradle-bean-serialization-services-8.9.jar b/gradle/plugins/gradle-bean-serialization-services-8.9.jar new file mode 100644 index 0000000..d58ebc0 Binary files /dev/null and b/gradle/plugins/gradle-bean-serialization-services-8.9.jar differ diff --git a/gradle/plugins/gradle-build-cache-http-8.9.jar b/gradle/plugins/gradle-build-cache-http-8.9.jar new file mode 100644 index 0000000..87b3029 Binary files /dev/null and b/gradle/plugins/gradle-build-cache-http-8.9.jar differ diff --git a/gradle/plugins/gradle-build-init-8.9.jar b/gradle/plugins/gradle-build-init-8.9.jar new file mode 100644 index 0000000..47eb8de Binary files /dev/null and b/gradle/plugins/gradle-build-init-8.9.jar differ diff --git a/gradle/plugins/gradle-build-profile-8.9.jar b/gradle/plugins/gradle-build-profile-8.9.jar new file mode 100644 index 0000000..4b58539 Binary files /dev/null and b/gradle/plugins/gradle-build-profile-8.9.jar differ diff --git a/gradle/plugins/gradle-code-quality-8.9.jar b/gradle/plugins/gradle-code-quality-8.9.jar new file mode 100644 index 0000000..99ca19b Binary files /dev/null and b/gradle/plugins/gradle-code-quality-8.9.jar differ diff --git a/gradle/plugins/gradle-composite-builds-8.9.jar b/gradle/plugins/gradle-composite-builds-8.9.jar new file mode 100644 index 0000000..162080f Binary files /dev/null and b/gradle/plugins/gradle-composite-builds-8.9.jar differ diff --git a/gradle/plugins/gradle-configuration-cache-8.9.jar b/gradle/plugins/gradle-configuration-cache-8.9.jar new file mode 100644 index 0000000..2d994bb Binary files /dev/null and b/gradle/plugins/gradle-configuration-cache-8.9.jar differ diff --git a/gradle/plugins/gradle-configuration-cache-base-8.9.jar b/gradle/plugins/gradle-configuration-cache-base-8.9.jar new file mode 100644 index 0000000..a693307 Binary files /dev/null and b/gradle/plugins/gradle-configuration-cache-base-8.9.jar differ diff --git a/gradle/plugins/gradle-configuration-problems-base-8.9.jar b/gradle/plugins/gradle-configuration-problems-base-8.9.jar new file mode 100644 index 0000000..f96e100 Binary files /dev/null and b/gradle/plugins/gradle-configuration-problems-base-8.9.jar differ diff --git a/gradle/plugins/gradle-core-kotlin-extensions-8.9.jar b/gradle/plugins/gradle-core-kotlin-extensions-8.9.jar new file mode 100644 index 0000000..63f68cd Binary files /dev/null and b/gradle/plugins/gradle-core-kotlin-extensions-8.9.jar differ diff --git a/gradle/plugins/gradle-core-serialization-codecs-8.9.jar b/gradle/plugins/gradle-core-serialization-codecs-8.9.jar new file mode 100644 index 0000000..fa78a78 Binary files /dev/null and b/gradle/plugins/gradle-core-serialization-codecs-8.9.jar differ diff --git a/gradle/plugins/gradle-declarative-dsl-tooling-builders-8.9.jar b/gradle/plugins/gradle-declarative-dsl-tooling-builders-8.9.jar new file mode 100644 index 0000000..10a6fb6 Binary files /dev/null and b/gradle/plugins/gradle-declarative-dsl-tooling-builders-8.9.jar differ diff --git a/gradle/plugins/gradle-dependency-management-8.9.jar b/gradle/plugins/gradle-dependency-management-8.9.jar new file mode 100644 index 0000000..549ce53 Binary files /dev/null and b/gradle/plugins/gradle-dependency-management-8.9.jar differ diff --git a/gradle/plugins/gradle-dependency-management-serialization-codecs-8.9.jar b/gradle/plugins/gradle-dependency-management-serialization-codecs-8.9.jar new file mode 100644 index 0000000..654640e Binary files /dev/null and b/gradle/plugins/gradle-dependency-management-serialization-codecs-8.9.jar differ diff --git a/gradle/plugins/gradle-diagnostics-8.9.jar b/gradle/plugins/gradle-diagnostics-8.9.jar new file mode 100644 index 0000000..b847e09 Binary files /dev/null and b/gradle/plugins/gradle-diagnostics-8.9.jar differ diff --git a/gradle/plugins/gradle-ear-8.9.jar b/gradle/plugins/gradle-ear-8.9.jar new file mode 100644 index 0000000..8e39a83 Binary files /dev/null and b/gradle/plugins/gradle-ear-8.9.jar differ diff --git a/gradle/plugins/gradle-enterprise-8.9.jar b/gradle/plugins/gradle-enterprise-8.9.jar new file mode 100644 index 0000000..107359f Binary files /dev/null and b/gradle/plugins/gradle-enterprise-8.9.jar differ diff --git a/gradle/plugins/gradle-flow-services-8.9.jar b/gradle/plugins/gradle-flow-services-8.9.jar new file mode 100644 index 0000000..467266b Binary files /dev/null and b/gradle/plugins/gradle-flow-services-8.9.jar differ diff --git a/gradle/plugins/gradle-graph-serialization-8.9.jar b/gradle/plugins/gradle-graph-serialization-8.9.jar new file mode 100644 index 0000000..9372675 Binary files /dev/null and b/gradle/plugins/gradle-graph-serialization-8.9.jar differ diff --git a/gradle/plugins/gradle-guava-serialization-codecs-8.9.jar b/gradle/plugins/gradle-guava-serialization-codecs-8.9.jar new file mode 100644 index 0000000..965179e Binary files /dev/null and b/gradle/plugins/gradle-guava-serialization-codecs-8.9.jar differ diff --git a/gradle/plugins/gradle-ide-8.9.jar b/gradle/plugins/gradle-ide-8.9.jar new file mode 100644 index 0000000..927e6fe Binary files /dev/null and b/gradle/plugins/gradle-ide-8.9.jar differ diff --git a/gradle/plugins/gradle-ide-native-8.9.jar b/gradle/plugins/gradle-ide-native-8.9.jar new file mode 100644 index 0000000..2aec977 Binary files /dev/null and b/gradle/plugins/gradle-ide-native-8.9.jar differ diff --git a/gradle/plugins/gradle-ide-plugins-8.9.jar b/gradle/plugins/gradle-ide-plugins-8.9.jar new file mode 100644 index 0000000..396cb47 Binary files /dev/null and b/gradle/plugins/gradle-ide-plugins-8.9.jar differ diff --git a/gradle/plugins/gradle-instrumentation-declarations-8.9.jar b/gradle/plugins/gradle-instrumentation-declarations-8.9.jar new file mode 100644 index 0000000..e3db3ac Binary files /dev/null and b/gradle/plugins/gradle-instrumentation-declarations-8.9.jar differ diff --git a/gradle/plugins/gradle-ivy-8.9.jar b/gradle/plugins/gradle-ivy-8.9.jar new file mode 100644 index 0000000..39c570c Binary files /dev/null and b/gradle/plugins/gradle-ivy-8.9.jar differ diff --git a/gradle/plugins/gradle-jacoco-8.9.jar b/gradle/plugins/gradle-jacoco-8.9.jar new file mode 100644 index 0000000..1593b66 Binary files /dev/null and b/gradle/plugins/gradle-jacoco-8.9.jar differ diff --git a/gradle/plugins/gradle-java-compiler-plugin-8.9.jar b/gradle/plugins/gradle-java-compiler-plugin-8.9.jar new file mode 100644 index 0000000..2ee98e8 Binary files /dev/null and b/gradle/plugins/gradle-java-compiler-plugin-8.9.jar differ diff --git a/gradle/plugins/gradle-java-platform-8.9.jar b/gradle/plugins/gradle-java-platform-8.9.jar new file mode 100644 index 0000000..7804d28 Binary files /dev/null and b/gradle/plugins/gradle-java-platform-8.9.jar differ diff --git a/gradle/plugins/gradle-kotlin-dsl-provider-plugins-8.9.jar b/gradle/plugins/gradle-kotlin-dsl-provider-plugins-8.9.jar new file mode 100644 index 0000000..98c4a8c Binary files /dev/null and b/gradle/plugins/gradle-kotlin-dsl-provider-plugins-8.9.jar differ diff --git a/gradle/plugins/gradle-kotlin-dsl-tooling-builders-8.9.jar b/gradle/plugins/gradle-kotlin-dsl-tooling-builders-8.9.jar new file mode 100644 index 0000000..cc027c4 Binary files /dev/null and b/gradle/plugins/gradle-kotlin-dsl-tooling-builders-8.9.jar differ diff --git a/gradle/plugins/gradle-language-groovy-8.9.jar b/gradle/plugins/gradle-language-groovy-8.9.jar new file mode 100644 index 0000000..cc04555 Binary files /dev/null and b/gradle/plugins/gradle-language-groovy-8.9.jar differ diff --git a/gradle/plugins/gradle-language-java-8.9.jar b/gradle/plugins/gradle-language-java-8.9.jar new file mode 100644 index 0000000..5533899 Binary files /dev/null and b/gradle/plugins/gradle-language-java-8.9.jar differ diff --git a/gradle/plugins/gradle-language-jvm-8.9.jar b/gradle/plugins/gradle-language-jvm-8.9.jar new file mode 100644 index 0000000..affc602 Binary files /dev/null and b/gradle/plugins/gradle-language-jvm-8.9.jar differ diff --git a/gradle/plugins/gradle-language-native-8.9.jar b/gradle/plugins/gradle-language-native-8.9.jar new file mode 100644 index 0000000..1125e8e Binary files /dev/null and b/gradle/plugins/gradle-language-native-8.9.jar differ diff --git a/gradle/plugins/gradle-maven-8.9.jar b/gradle/plugins/gradle-maven-8.9.jar new file mode 100644 index 0000000..cc3f107 Binary files /dev/null and b/gradle/plugins/gradle-maven-8.9.jar differ diff --git a/gradle/plugins/gradle-platform-base-8.9.jar b/gradle/plugins/gradle-platform-base-8.9.jar new file mode 100644 index 0000000..39ac737 Binary files /dev/null and b/gradle/plugins/gradle-platform-base-8.9.jar differ diff --git a/gradle/plugins/gradle-platform-jvm-8.9.jar b/gradle/plugins/gradle-platform-jvm-8.9.jar new file mode 100644 index 0000000..87a87b1 Binary files /dev/null and b/gradle/plugins/gradle-platform-jvm-8.9.jar differ diff --git a/gradle/plugins/gradle-platform-native-8.9.jar b/gradle/plugins/gradle-platform-native-8.9.jar new file mode 100644 index 0000000..190cd18 Binary files /dev/null and b/gradle/plugins/gradle-platform-native-8.9.jar differ diff --git a/gradle/plugins/gradle-plugin-development-8.9.jar b/gradle/plugins/gradle-plugin-development-8.9.jar new file mode 100644 index 0000000..9c77374 Binary files /dev/null and b/gradle/plugins/gradle-plugin-development-8.9.jar differ diff --git a/gradle/plugins/gradle-plugin-use-8.9.jar b/gradle/plugins/gradle-plugin-use-8.9.jar new file mode 100644 index 0000000..8c7bb23 Binary files /dev/null and b/gradle/plugins/gradle-plugin-use-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-application-8.9.jar b/gradle/plugins/gradle-plugins-application-8.9.jar new file mode 100644 index 0000000..b671a23 Binary files /dev/null and b/gradle/plugins/gradle-plugins-application-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-distribution-8.9.jar b/gradle/plugins/gradle-plugins-distribution-8.9.jar new file mode 100644 index 0000000..420d559 Binary files /dev/null and b/gradle/plugins/gradle-plugins-distribution-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-groovy-8.9.jar b/gradle/plugins/gradle-plugins-groovy-8.9.jar new file mode 100644 index 0000000..ac79894 Binary files /dev/null and b/gradle/plugins/gradle-plugins-groovy-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-java-8.9.jar b/gradle/plugins/gradle-plugins-java-8.9.jar new file mode 100644 index 0000000..aa69ded Binary files /dev/null and b/gradle/plugins/gradle-plugins-java-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-java-base-8.9.jar b/gradle/plugins/gradle-plugins-java-base-8.9.jar new file mode 100644 index 0000000..a474ff6 Binary files /dev/null and b/gradle/plugins/gradle-plugins-java-base-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-java-library-8.9.jar b/gradle/plugins/gradle-plugins-java-library-8.9.jar new file mode 100644 index 0000000..7b7a0aa Binary files /dev/null and b/gradle/plugins/gradle-plugins-java-library-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-jvm-test-fixtures-8.9.jar b/gradle/plugins/gradle-plugins-jvm-test-fixtures-8.9.jar new file mode 100644 index 0000000..a1175d1 Binary files /dev/null and b/gradle/plugins/gradle-plugins-jvm-test-fixtures-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-jvm-test-suite-8.9.jar b/gradle/plugins/gradle-plugins-jvm-test-suite-8.9.jar new file mode 100644 index 0000000..bb99e9f Binary files /dev/null and b/gradle/plugins/gradle-plugins-jvm-test-suite-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-test-report-aggregation-8.9.jar b/gradle/plugins/gradle-plugins-test-report-aggregation-8.9.jar new file mode 100644 index 0000000..04cedd5 Binary files /dev/null and b/gradle/plugins/gradle-plugins-test-report-aggregation-8.9.jar differ diff --git a/gradle/plugins/gradle-plugins-version-catalog-8.9.jar b/gradle/plugins/gradle-plugins-version-catalog-8.9.jar new file mode 100644 index 0000000..cf85bf6 Binary files /dev/null and b/gradle/plugins/gradle-plugins-version-catalog-8.9.jar differ diff --git a/gradle/plugins/gradle-publish-8.9.jar b/gradle/plugins/gradle-publish-8.9.jar new file mode 100644 index 0000000..ae9e4f4 Binary files /dev/null and b/gradle/plugins/gradle-publish-8.9.jar differ diff --git a/gradle/plugins/gradle-reporting-8.9.jar b/gradle/plugins/gradle-reporting-8.9.jar new file mode 100644 index 0000000..93b795e Binary files /dev/null and b/gradle/plugins/gradle-reporting-8.9.jar differ diff --git a/gradle/plugins/gradle-resources-gcs-8.9.jar b/gradle/plugins/gradle-resources-gcs-8.9.jar new file mode 100644 index 0000000..7f20948 Binary files /dev/null and b/gradle/plugins/gradle-resources-gcs-8.9.jar differ diff --git a/gradle/plugins/gradle-resources-http-8.9.jar b/gradle/plugins/gradle-resources-http-8.9.jar new file mode 100644 index 0000000..04ab853 Binary files /dev/null and b/gradle/plugins/gradle-resources-http-8.9.jar differ diff --git a/gradle/plugins/gradle-resources-s3-8.9.jar b/gradle/plugins/gradle-resources-s3-8.9.jar new file mode 100644 index 0000000..024128a Binary files /dev/null and b/gradle/plugins/gradle-resources-s3-8.9.jar differ diff --git a/gradle/plugins/gradle-resources-sftp-8.9.jar b/gradle/plugins/gradle-resources-sftp-8.9.jar new file mode 100644 index 0000000..828a68c Binary files /dev/null and b/gradle/plugins/gradle-resources-sftp-8.9.jar differ diff --git a/gradle/plugins/gradle-scala-8.9.jar b/gradle/plugins/gradle-scala-8.9.jar new file mode 100644 index 0000000..0d79803 Binary files /dev/null and b/gradle/plugins/gradle-scala-8.9.jar differ diff --git a/gradle/plugins/gradle-security-8.9.jar b/gradle/plugins/gradle-security-8.9.jar new file mode 100644 index 0000000..e5f25fa Binary files /dev/null and b/gradle/plugins/gradle-security-8.9.jar differ diff --git a/gradle/plugins/gradle-signing-8.9.jar b/gradle/plugins/gradle-signing-8.9.jar new file mode 100644 index 0000000..d9d959a Binary files /dev/null and b/gradle/plugins/gradle-signing-8.9.jar differ diff --git a/gradle/plugins/gradle-stdlib-kotlin-extensions-8.9.jar b/gradle/plugins/gradle-stdlib-kotlin-extensions-8.9.jar new file mode 100644 index 0000000..73cd793 Binary files /dev/null and b/gradle/plugins/gradle-stdlib-kotlin-extensions-8.9.jar differ diff --git a/gradle/plugins/gradle-stdlib-serialization-codecs-8.9.jar b/gradle/plugins/gradle-stdlib-serialization-codecs-8.9.jar new file mode 100644 index 0000000..695fd5b Binary files /dev/null and b/gradle/plugins/gradle-stdlib-serialization-codecs-8.9.jar differ diff --git a/gradle/plugins/gradle-test-kit-8.9.jar b/gradle/plugins/gradle-test-kit-8.9.jar new file mode 100644 index 0000000..183ecab Binary files /dev/null and b/gradle/plugins/gradle-test-kit-8.9.jar differ diff --git a/gradle/plugins/gradle-test-suites-base-8.9.jar b/gradle/plugins/gradle-test-suites-base-8.9.jar new file mode 100644 index 0000000..b394aaa Binary files /dev/null and b/gradle/plugins/gradle-test-suites-base-8.9.jar differ diff --git a/gradle/plugins/gradle-testing-base-8.9.jar b/gradle/plugins/gradle-testing-base-8.9.jar new file mode 100644 index 0000000..1ebd15d Binary files /dev/null and b/gradle/plugins/gradle-testing-base-8.9.jar differ diff --git a/gradle/plugins/gradle-testing-base-infrastructure-8.9.jar b/gradle/plugins/gradle-testing-base-infrastructure-8.9.jar new file mode 100644 index 0000000..6eb007f Binary files /dev/null and b/gradle/plugins/gradle-testing-base-infrastructure-8.9.jar differ diff --git a/gradle/plugins/gradle-testing-junit-platform-8.9.jar b/gradle/plugins/gradle-testing-junit-platform-8.9.jar new file mode 100644 index 0000000..e8957ab Binary files /dev/null and b/gradle/plugins/gradle-testing-junit-platform-8.9.jar differ diff --git a/gradle/plugins/gradle-testing-jvm-8.9.jar b/gradle/plugins/gradle-testing-jvm-8.9.jar new file mode 100644 index 0000000..4b23660 Binary files /dev/null and b/gradle/plugins/gradle-testing-jvm-8.9.jar differ diff --git a/gradle/plugins/gradle-testing-jvm-infrastructure-8.9.jar b/gradle/plugins/gradle-testing-jvm-infrastructure-8.9.jar new file mode 100644 index 0000000..1ca5db1 Binary files /dev/null and b/gradle/plugins/gradle-testing-jvm-infrastructure-8.9.jar differ diff --git a/gradle/plugins/gradle-testing-native-8.9.jar b/gradle/plugins/gradle-testing-native-8.9.jar new file mode 100644 index 0000000..245fcf2 Binary files /dev/null and b/gradle/plugins/gradle-testing-native-8.9.jar differ diff --git a/gradle/plugins/gradle-toolchains-jvm-8.9.jar b/gradle/plugins/gradle-toolchains-jvm-8.9.jar new file mode 100644 index 0000000..bcbd526 Binary files /dev/null and b/gradle/plugins/gradle-toolchains-jvm-8.9.jar differ diff --git a/gradle/plugins/gradle-tooling-api-builders-8.9.jar b/gradle/plugins/gradle-tooling-api-builders-8.9.jar new file mode 100644 index 0000000..8d2b686 Binary files /dev/null and b/gradle/plugins/gradle-tooling-api-builders-8.9.jar differ diff --git a/gradle/plugins/gradle-tooling-native-8.9.jar b/gradle/plugins/gradle-tooling-native-8.9.jar new file mode 100644 index 0000000..da62830 Binary files /dev/null and b/gradle/plugins/gradle-tooling-native-8.9.jar differ diff --git a/gradle/plugins/gradle-unit-test-fixtures-8.9.jar b/gradle/plugins/gradle-unit-test-fixtures-8.9.jar new file mode 100644 index 0000000..0282747 Binary files /dev/null and b/gradle/plugins/gradle-unit-test-fixtures-8.9.jar differ diff --git a/gradle/plugins/gradle-version-control-8.9.jar b/gradle/plugins/gradle-version-control-8.9.jar new file mode 100644 index 0000000..f3e5a30 Binary files /dev/null and b/gradle/plugins/gradle-version-control-8.9.jar differ diff --git a/gradle/plugins/gradle-war-8.9.jar b/gradle/plugins/gradle-war-8.9.jar new file mode 100644 index 0000000..1a07d33 Binary files /dev/null and b/gradle/plugins/gradle-war-8.9.jar differ diff --git a/gradle/plugins/gradle-workers-8.9.jar b/gradle/plugins/gradle-workers-8.9.jar new file mode 100644 index 0000000..10c83b1 Binary files /dev/null and b/gradle/plugins/gradle-workers-8.9.jar differ diff --git a/gradle/plugins/gradle-wrapper-main-8.9.jar b/gradle/plugins/gradle-wrapper-main-8.9.jar new file mode 100644 index 0000000..c5d835b Binary files /dev/null and b/gradle/plugins/gradle-wrapper-main-8.9.jar differ diff --git a/gradle/plugins/grpc-context-1.27.2.jar b/gradle/plugins/grpc-context-1.27.2.jar new file mode 100644 index 0000000..fd8615e Binary files /dev/null and b/gradle/plugins/grpc-context-1.27.2.jar differ diff --git a/gradle/plugins/httpclient-4.5.14.jar b/gradle/plugins/httpclient-4.5.14.jar new file mode 100644 index 0000000..2bb7c07 Binary files /dev/null and b/gradle/plugins/httpclient-4.5.14.jar differ diff --git a/gradle/plugins/httpcore-4.4.14.jar b/gradle/plugins/httpcore-4.4.14.jar new file mode 100644 index 0000000..349db18 Binary files /dev/null and b/gradle/plugins/httpcore-4.4.14.jar differ diff --git a/gradle/plugins/ivy-2.5.2.jar b/gradle/plugins/ivy-2.5.2.jar new file mode 100644 index 0000000..7d2648a Binary files /dev/null and b/gradle/plugins/ivy-2.5.2.jar differ diff --git a/gradle/plugins/jackson-annotations-2.16.1.jar b/gradle/plugins/jackson-annotations-2.16.1.jar new file mode 100644 index 0000000..b9c48e6 Binary files /dev/null and b/gradle/plugins/jackson-annotations-2.16.1.jar differ diff --git a/gradle/plugins/jackson-core-2.16.1.jar b/gradle/plugins/jackson-core-2.16.1.jar new file mode 100644 index 0000000..a8ff5e3 Binary files /dev/null and b/gradle/plugins/jackson-core-2.16.1.jar differ diff --git a/gradle/plugins/jackson-databind-2.16.1.jar b/gradle/plugins/jackson-databind-2.16.1.jar new file mode 100644 index 0000000..5171b7a Binary files /dev/null and b/gradle/plugins/jackson-databind-2.16.1.jar differ diff --git a/gradle/plugins/jatl-0.2.3.jar b/gradle/plugins/jatl-0.2.3.jar new file mode 100644 index 0000000..b1609a7 Binary files /dev/null and b/gradle/plugins/jatl-0.2.3.jar differ diff --git a/gradle/plugins/jcifs-1.3.17.jar b/gradle/plugins/jcifs-1.3.17.jar new file mode 100644 index 0000000..3f27e29 Binary files /dev/null and b/gradle/plugins/jcifs-1.3.17.jar differ diff --git a/gradle/plugins/jcommander-1.78.jar b/gradle/plugins/jcommander-1.78.jar new file mode 100644 index 0000000..1d58673 Binary files /dev/null and b/gradle/plugins/jcommander-1.78.jar differ diff --git a/gradle/plugins/jmespath-java-1.12.651.jar b/gradle/plugins/jmespath-java-1.12.651.jar new file mode 100644 index 0000000..c9ac21d Binary files /dev/null and b/gradle/plugins/jmespath-java-1.12.651.jar differ diff --git a/gradle/plugins/joda-time-2.12.2.jar b/gradle/plugins/joda-time-2.12.2.jar new file mode 100644 index 0000000..0d92420 Binary files /dev/null and b/gradle/plugins/joda-time-2.12.2.jar differ diff --git a/gradle/plugins/jsch-0.2.16.jar b/gradle/plugins/jsch-0.2.16.jar new file mode 100644 index 0000000..9fd8aff Binary files /dev/null and b/gradle/plugins/jsch-0.2.16.jar differ diff --git a/gradle/plugins/jsoup-1.15.3.jar b/gradle/plugins/jsoup-1.15.3.jar new file mode 100644 index 0000000..5506d7f Binary files /dev/null and b/gradle/plugins/jsoup-1.15.3.jar differ diff --git a/gradle/plugins/junit-platform-commons-1.8.2.jar b/gradle/plugins/junit-platform-commons-1.8.2.jar new file mode 100644 index 0000000..e0cf087 Binary files /dev/null and b/gradle/plugins/junit-platform-commons-1.8.2.jar differ diff --git a/gradle/plugins/junit-platform-engine-1.8.2.jar b/gradle/plugins/junit-platform-engine-1.8.2.jar new file mode 100644 index 0000000..85bac8a Binary files /dev/null and b/gradle/plugins/junit-platform-engine-1.8.2.jar differ diff --git a/gradle/plugins/junit-platform-launcher-1.8.2.jar b/gradle/plugins/junit-platform-launcher-1.8.2.jar new file mode 100644 index 0000000..0f2cc40 Binary files /dev/null and b/gradle/plugins/junit-platform-launcher-1.8.2.jar differ diff --git a/gradle/plugins/maven-builder-support-3.9.5.jar b/gradle/plugins/maven-builder-support-3.9.5.jar new file mode 100644 index 0000000..957b3b8 Binary files /dev/null and b/gradle/plugins/maven-builder-support-3.9.5.jar differ diff --git a/gradle/plugins/maven-model-3.9.5.jar b/gradle/plugins/maven-model-3.9.5.jar new file mode 100644 index 0000000..8337a2a Binary files /dev/null and b/gradle/plugins/maven-model-3.9.5.jar differ diff --git a/gradle/plugins/maven-repository-metadata-3.9.5.jar b/gradle/plugins/maven-repository-metadata-3.9.5.jar new file mode 100644 index 0000000..fa258e5 Binary files /dev/null and b/gradle/plugins/maven-repository-metadata-3.9.5.jar differ diff --git a/gradle/plugins/maven-settings-3.9.5.jar b/gradle/plugins/maven-settings-3.9.5.jar new file mode 100644 index 0000000..20e58a0 Binary files /dev/null and b/gradle/plugins/maven-settings-3.9.5.jar differ diff --git a/gradle/plugins/maven-settings-builder-3.9.5.jar b/gradle/plugins/maven-settings-builder-3.9.5.jar new file mode 100644 index 0000000..c0d72b3 Binary files /dev/null and b/gradle/plugins/maven-settings-builder-3.9.5.jar differ diff --git a/gradle/plugins/opencensus-api-0.31.1.jar b/gradle/plugins/opencensus-api-0.31.1.jar new file mode 100644 index 0000000..32f2501 Binary files /dev/null and b/gradle/plugins/opencensus-api-0.31.1.jar differ diff --git a/gradle/plugins/opencensus-contrib-http-util-0.31.1.jar b/gradle/plugins/opencensus-contrib-http-util-0.31.1.jar new file mode 100644 index 0000000..f96d0da Binary files /dev/null and b/gradle/plugins/opencensus-contrib-http-util-0.31.1.jar differ diff --git a/gradle/plugins/opentest4j-1.3.0.jar b/gradle/plugins/opentest4j-1.3.0.jar new file mode 100644 index 0000000..7ec7bc5 Binary files /dev/null and b/gradle/plugins/opentest4j-1.3.0.jar differ diff --git a/gradle/plugins/org.eclipse.jgit-5.13.3.202401111512-r.jar b/gradle/plugins/org.eclipse.jgit-5.13.3.202401111512-r.jar new file mode 100644 index 0000000..1fc9ef9 Binary files /dev/null and b/gradle/plugins/org.eclipse.jgit-5.13.3.202401111512-r.jar differ diff --git a/gradle/plugins/org.eclipse.jgit.ssh.apache-5.13.3.202401111512-r.jar b/gradle/plugins/org.eclipse.jgit.ssh.apache-5.13.3.202401111512-r.jar new file mode 100644 index 0000000..3544fec Binary files /dev/null and b/gradle/plugins/org.eclipse.jgit.ssh.apache-5.13.3.202401111512-r.jar differ diff --git a/gradle/plugins/plexus-cipher-2.0.jar b/gradle/plugins/plexus-cipher-2.0.jar new file mode 100644 index 0000000..e73475d Binary files /dev/null and b/gradle/plugins/plexus-cipher-2.0.jar differ diff --git a/gradle/plugins/plexus-interpolation-1.26.jar b/gradle/plugins/plexus-interpolation-1.26.jar new file mode 100644 index 0000000..cfcf162 Binary files /dev/null and b/gradle/plugins/plexus-interpolation-1.26.jar differ diff --git a/gradle/plugins/plexus-sec-dispatcher-2.0.jar b/gradle/plugins/plexus-sec-dispatcher-2.0.jar new file mode 100644 index 0000000..623cc21 Binary files /dev/null and b/gradle/plugins/plexus-sec-dispatcher-2.0.jar differ diff --git a/gradle/plugins/plexus-utils-3.5.1.jar b/gradle/plugins/plexus-utils-3.5.1.jar new file mode 100644 index 0000000..1873c52 Binary files /dev/null and b/gradle/plugins/plexus-utils-3.5.1.jar differ diff --git a/gradle/plugins/snakeyaml-2.0.jar b/gradle/plugins/snakeyaml-2.0.jar new file mode 100644 index 0000000..469b043 Binary files /dev/null and b/gradle/plugins/snakeyaml-2.0.jar differ diff --git a/gradle/plugins/sshd-common-2.12.1.jar b/gradle/plugins/sshd-common-2.12.1.jar new file mode 100644 index 0000000..67420f8 Binary files /dev/null and b/gradle/plugins/sshd-common-2.12.1.jar differ diff --git a/gradle/plugins/sshd-core-2.12.1.jar b/gradle/plugins/sshd-core-2.12.1.jar new file mode 100644 index 0000000..087f99a Binary files /dev/null and b/gradle/plugins/sshd-core-2.12.1.jar differ diff --git a/gradle/plugins/sshd-sftp-2.12.1.jar b/gradle/plugins/sshd-sftp-2.12.1.jar new file mode 100644 index 0000000..e943a3d Binary files /dev/null and b/gradle/plugins/sshd-sftp-2.12.1.jar differ diff --git a/gradle/plugins/testng-6.3.1.jar b/gradle/plugins/testng-6.3.1.jar new file mode 100644 index 0000000..7479345 Binary files /dev/null and b/gradle/plugins/testng-6.3.1.jar differ diff --git a/gradle/qdox-1.12.1.jar b/gradle/qdox-1.12.1.jar new file mode 100644 index 0000000..092fc51 Binary files /dev/null and b/gradle/qdox-1.12.1.jar differ diff --git a/gradle/slf4j-api-1.7.36.jar b/gradle/slf4j-api-1.7.36.jar new file mode 100644 index 0000000..7d3ce68 Binary files /dev/null and b/gradle/slf4j-api-1.7.36.jar differ diff --git a/gradle/tomlj-1.0.0.jar b/gradle/tomlj-1.0.0.jar new file mode 100644 index 0000000..56322a7 Binary files /dev/null and b/gradle/tomlj-1.0.0.jar differ diff --git a/gradle/trove4j-1.0.20200330.jar b/gradle/trove4j-1.0.20200330.jar new file mode 100644 index 0000000..0b174bf Binary files /dev/null and b/gradle/trove4j-1.0.20200330.jar differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..2c35211 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2733ed5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradle/xml-apis-1.4.01.jar b/gradle/xml-apis-1.4.01.jar new file mode 100644 index 0000000..4673346 Binary files /dev/null and b/gradle/xml-apis-1.4.01.jar differ diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..14f218d --- /dev/null +++ b/gradlew @@ -0,0 +1,7 @@ +#!/bin/bash +exec /usr/bin/java \ + -Xmx64m \ + -Xms64m \ + -classpath "$( cd "$( dirname "$0" )" && pwd )/gradle/wrapper/gradle-wrapper.jar" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" diff --git a/keystore/.gitignore b/keystore/.gitignore new file mode 100644 index 0000000..27d9c9e --- /dev/null +++ b/keystore/.gitignore @@ -0,0 +1,3 @@ +keystore/*.jks +keystore/*.p12 +keystore/*.keystore diff --git a/keystore/ody-release.jks b/keystore/ody-release.jks new file mode 100644 index 0000000..fbe2503 Binary files /dev/null and b/keystore/ody-release.jks differ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..8f10c5a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} +rootProject.name = "ODY" +include(":app")