diff --git a/.github/skills/admin-message-e2ee/__pycache__/generaltest_admin_message.cpython-313.pyc b/.github/skills/admin-message-e2ee/__pycache__/generaltest_admin_message.cpython-313.pyc new file mode 100644 index 0000000..98c1a00 Binary files /dev/null and b/.github/skills/admin-message-e2ee/__pycache__/generaltest_admin_message.cpython-313.pyc differ diff --git a/.github/skills/admin-message-e2ee/__pycache__/receive_admin_messages.cpython-313.pyc b/.github/skills/admin-message-e2ee/__pycache__/receive_admin_messages.cpython-313.pyc new file mode 100644 index 0000000..381219e Binary files /dev/null and b/.github/skills/admin-message-e2ee/__pycache__/receive_admin_messages.cpython-313.pyc differ diff --git a/.github/skills/admin-message-e2ee/__pycache__/send_admin_messages.cpython-313.pyc b/.github/skills/admin-message-e2ee/__pycache__/send_admin_messages.cpython-313.pyc new file mode 100644 index 0000000..11d3deb Binary files /dev/null and b/.github/skills/admin-message-e2ee/__pycache__/send_admin_messages.cpython-313.pyc differ diff --git a/.github/skills/android-device/SKILL.md b/.github/skills/android-device/SKILL.md index 42dfe65..743f119 100644 --- a/.github/skills/android-device/SKILL.md +++ b/.github/skills/android-device/SKILL.md @@ -88,6 +88,28 @@ $adb = "C:\Users\JensR\AppData\Local\Android\Sdk\platform-tools\adb.exe" **Hinweis:** Die Skript-Aktionen `deploy-device` und `install-device` verwenden `adb -d` und funktionieren nur über USB. Bei Wireless ADB die manuellen Kommandos verwenden. +### Robustes Python-Deployment (empfohlen) + +Neues Skript: `.github/skills/android-device/deploy-device.py` + +```powershell +# Standard: Build + Device erkennen + Install + Launch +python ".github/skills/android-device/deploy-device.py" --repo-root "X:\bollwerk" + +# Explizites Wireless-Geraet +python ".github/skills/android-device/deploy-device.py" --repo-root "X:\bollwerk" --serial "192.168.68.107:42539" + +# Ohne Build (wenn APK schon gebaut), nur Install +python ".github/skills/android-device/deploy-device.py" --repo-root "X:\bollwerk" --skip-build --no-launch +``` + +Funktionen: + +- ADB-Autodetect ueber `--adb`, `ANDROID_SDK_ROOT`/`ANDROID_HOME` oder `local.properties` +- Robuste Device-Erkennung (USB oder Wireless) +- Install-Retries mit klaren Fehlern +- Definierte Exitcodes (`0` Erfolg, `1` Fehler) + ```powershell # App bauen + auf Gerät installieren + starten (nur USB) & ".github/skills/android-build/android-dev.ps1" -Action deploy-device diff --git a/.github/skills/android-device/deploy-device.py b/.github/skills/android-device/deploy-device.py new file mode 100644 index 0000000..5190073 --- /dev/null +++ b/.github/skills/android-device/deploy-device.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Robustes Deployment der Debug-APK auf ein physisches Android-Geraet. + +Features: +- ADB automatisch finden (Argument, ENV, local.properties) +- USB oder Wireless-Device robust erkennen +- Optional Build via Gradle +- Install mit Retry +- Optional App-Start +- Klare Exitcodes und kompakte Logs +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +import time +from pathlib import Path +from typing import List, Optional, Tuple + +PACKAGE_NAME = "de.bollwerk.app" +MAIN_ACTIVITY = "de.bollwerk.app/.MainActivity" +DEFAULT_APK_REL = Path("app/build/outputs/apk/debug/app-debug.apk") +DEFAULT_GRADLEW = Path("gradlew.bat") + + +def log(msg: str) -> None: + print(msg, flush=True) + + +def run_cmd(cmd: List[str], cwd: Optional[Path] = None, timeout: Optional[int] = None) -> subprocess.CompletedProcess: + return subprocess.run( + cmd, + cwd=str(cwd) if cwd else None, + text=True, + capture_output=True, + timeout=timeout, + check=False, + ) + + +def normalize_sdk_dir(raw: str) -> str: + # local.properties encodes ':' as '\:' on Windows. + val = raw.strip() + val = val.replace("\\:", ":") + val = val.replace("\\\\", "\\") + return val + + +def read_sdk_from_local_properties(repo_root: Path) -> Optional[Path]: + local_props = repo_root / "local.properties" + if not local_props.exists(): + return None + for line in local_props.read_text(encoding="utf-8").splitlines(): + if line.strip().startswith("sdk.dir="): + _, value = line.split("=", 1) + sdk = normalize_sdk_dir(value) + p = Path(sdk) + return p if p.exists() else None + return None + + +def find_adb(repo_root: Path, explicit_adb: Optional[str]) -> Path: + candidates: List[Path] = [] + + if explicit_adb: + candidates.append(Path(explicit_adb)) + + env_android_sdk = os.environ.get("ANDROID_SDK_ROOT") or os.environ.get("ANDROID_HOME") + if env_android_sdk: + candidates.append(Path(env_android_sdk) / "platform-tools" / "adb.exe") + candidates.append(Path(env_android_sdk) / "platform-tools" / "adb") + + sdk_from_local = read_sdk_from_local_properties(repo_root) + if sdk_from_local: + candidates.append(sdk_from_local / "platform-tools" / "adb.exe") + candidates.append(sdk_from_local / "platform-tools" / "adb") + + # Last fallback: rely on PATH. + path_adb = shutil_which("adb") + if path_adb: + candidates.append(Path(path_adb)) + + for candidate in candidates: + if candidate.exists(): + return candidate + + raise FileNotFoundError( + "ADB nicht gefunden. Setze --adb oder ANDROID_SDK_ROOT/ANDROID_HOME oder local.properties sdk.dir." + ) + + +def shutil_which(binary: str) -> Optional[str]: + for folder in os.environ.get("PATH", "").split(os.pathsep): + p = Path(folder) / binary + if p.exists(): + return str(p) + if os.name == "nt": + p_exe = Path(folder) / f"{binary}.exe" + if p_exe.exists(): + return str(p_exe) + return None + + +def parse_adb_devices(output: str) -> List[Tuple[str, str]]: + devices: List[Tuple[str, str]] = [] + for line in output.splitlines(): + line = line.strip() + if not line or line.startswith("List of devices"): + continue + # serial state [extra] + parts = re.split(r"\s+", line) + if len(parts) >= 2: + devices.append((parts[0], parts[1])) + return devices + + +def resolve_target_serial(adb: Path, prefer_serial: Optional[str], prefer_usb: bool, timeout_sec: int) -> str: + start = time.time() + while True: + cp = run_cmd([str(adb), "devices", "-l"]) + if cp.returncode == 0: + devices = parse_adb_devices(cp.stdout) + + if prefer_serial: + for serial, state in devices: + if serial == prefer_serial and state == "device": + return serial + else: + live = [(s, st) for s, st in devices if st == "device"] + if prefer_usb: + usb = [s for s, _ in live if not re.match(r"\d+\.\d+\.\d+\.\d+:\d+", s)] + if usb: + return usb[0] + if live: + return live[0][0] + + if (time.time() - start) >= timeout_sec: + break + time.sleep(2) + + raise RuntimeError("Kein geeignetes ADB-Geraet gefunden (USB/Wireless, Debugging, Autorisierung pruefen).") + + +def ensure_apk(repo_root: Path, apk_path: Path, gradlew_path: Path, skip_build: bool) -> None: + if skip_build and apk_path.exists(): + return + + if not gradlew_path.exists(): + raise FileNotFoundError(f"Gradle Wrapper nicht gefunden: {gradlew_path}") + + log("[1/4] Build Debug-APK ...") + cp = run_cmd([str(gradlew_path), "assembleDebug"], cwd=repo_root) + if cp.returncode != 0: + tail = "\n".join((cp.stdout + "\n" + cp.stderr).splitlines()[-40:]) + raise RuntimeError(f"Gradle Build fehlgeschlagen.\n{tail}") + + if not apk_path.exists(): + raise FileNotFoundError(f"APK nach Build nicht gefunden: {apk_path}") + + +def adb_install(adb: Path, serial: str, apk_path: Path, retries: int) -> None: + last_err = "" + for attempt in range(1, retries + 1): + log(f"[3/4] Installiere APK (Versuch {attempt}/{retries}) ...") + cp = run_cmd([str(adb), "-s", serial, "install", "-r", str(apk_path)], timeout=180) + out = (cp.stdout + "\n" + cp.stderr).strip() + if cp.returncode == 0 and "Success" in out: + log("Install OK") + return + last_err = out + time.sleep(2) + + raise RuntimeError(f"APK-Installation fehlgeschlagen.\n{last_err}") + + +def adb_launch(adb: Path, serial: str) -> None: + cp = run_cmd([str(adb), "-s", serial, "shell", "am", "start", "-n", MAIN_ACTIVITY], timeout=60) + out = (cp.stdout + "\n" + cp.stderr).strip() + if cp.returncode != 0 or "Error" in out or "Exception" in out: + raise RuntimeError(f"App-Start fehlgeschlagen.\n{out}") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Deploy app-debug.apk robust auf ein physisches Android-Geraet") + parser.add_argument("--repo-root", default=".", help="Pfad zum Repo-Root (Standard: aktuelles Verzeichnis)") + parser.add_argument("--adb", default=None, help="Expliziter Pfad zu adb(.exe)") + parser.add_argument("--serial", default=None, help="Geraete-Serial explizit (z. B. USB-Serial oder 192.168.x.x:port)") + parser.add_argument("--prefer-usb", action="store_true", help="Bevorzuge USB-Geraet, wenn --serial nicht gesetzt") + parser.add_argument("--timeout-device", type=int, default=30, help="Maximale Wartezeit auf Device-Erkennung in Sekunden") + parser.add_argument("--retries-install", type=int, default=3, help="Install-Retries") + parser.add_argument("--skip-build", action="store_true", help="Build ueberspringen, wenn APK bereits existiert") + parser.add_argument("--no-launch", action="store_true", help="App nach Installation nicht starten") + args = parser.parse_args() + + repo_root = Path(args.repo_root).resolve() + apk_path = (repo_root / DEFAULT_APK_REL).resolve() + gradlew_path = (repo_root / DEFAULT_GRADLEW).resolve() + + try: + adb = find_adb(repo_root, args.adb) + log(f"ADB: {adb}") + + ensure_apk(repo_root, apk_path, gradlew_path, skip_build=args.skip_build) + + log("[2/4] Suche Device ...") + serial = resolve_target_serial(adb, args.serial, args.prefer_usb, args.timeout_device) + log(f"Device: {serial}") + + adb_install(adb, serial, apk_path, retries=args.retries_install) + + if not args.no_launch: + log("[4/4] Starte App ...") + adb_launch(adb, serial) + log("Launch OK") + + log("DEPLOY SUCCESS") + return 0 + except Exception as exc: + log(f"DEPLOY FAILED: {exc}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/skills/publish/__pycache__/publish-apk.cpython-313.pyc b/.github/skills/publish/__pycache__/publish-apk.cpython-313.pyc new file mode 100644 index 0000000..e1d4fce Binary files /dev/null and b/.github/skills/publish/__pycache__/publish-apk.cpython-313.pyc differ diff --git a/app/src/main/java/de/bollwerk/app/ui/settings/SettingsScreen.kt b/app/src/main/java/de/bollwerk/app/ui/settings/SettingsScreen.kt index 8205053..da2d48b 100644 --- a/app/src/main/java/de/bollwerk/app/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/bollwerk/app/ui/settings/SettingsScreen.kt @@ -438,7 +438,7 @@ internal fun SettingsScreen( style = MaterialTheme.typography.bodyMedium ) Text( - text = "Version ${BuildConfig.VERSION_NAME}.${BuildConfig.VERSION_CODE}", + text = "Version ${BuildConfig.VERSION_NAME}", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/de/bollwerk/app/ui/update/UpdateBanner.kt b/app/src/main/java/de/bollwerk/app/ui/update/UpdateBanner.kt index 470787e..20f021b 100644 --- a/app/src/main/java/de/bollwerk/app/ui/update/UpdateBanner.kt +++ b/app/src/main/java/de/bollwerk/app/ui/update/UpdateBanner.kt @@ -47,7 +47,7 @@ internal fun UpdateBanner( ) { when (status) { is UpdateStatus.Available -> AvailableBanner( - versionName = "${status.versionName}.${status.versionCode}", + versionName = status.versionName, onDownloadClick = onDownloadClick, onDismiss = onDismiss ) @@ -55,7 +55,7 @@ internal fun UpdateBanner( progress = status.progress ) is UpdateStatus.ReadyToInstall -> ReadyBanner( - versionName = "${status.versionName}.${status.versionCode}", + versionName = status.versionName, onDismiss = onDismiss ) is UpdateStatus.Error -> ErrorBanner(