diff --git a/.github/skills/android-build/SKILL.md b/.github/skills/android-build/SKILL.md index f250f31..5344776 100644 --- a/.github/skills/android-build/SKILL.md +++ b/.github/skills/android-build/SKILL.md @@ -36,6 +36,9 @@ Verwende **immer** das `android-dev.ps1`-Skript statt roher Gradle-Aufrufe: # Nur clean & ".github/skills/android-build/android-dev.ps1" -Action clean + +# Hot Reload: Build + Install + Relaunch auf laufendem Emulator (ohne Neustart) +& ".github/skills/android-build/android-dev.ps1" -Action hot-reload ``` ### Direkter Gradle-Aufruf (Fallback) diff --git a/.github/skills/android-build/android-dev.ps1 b/.github/skills/android-build/android-dev.ps1 index c497544..076ce57 100644 --- a/.github/skills/android-build/android-dev.ps1 +++ b/.github/skills/android-build/android-dev.ps1 @@ -16,11 +16,12 @@ install-emulator - APK auf Emulator installieren install-device - APK auf physisches Gerät installieren launch - App starten (auf dem aktiven Ziel) + hot-reload - Build + Install + Relaunch auf laufendem Ziel (ohne Emulator-Neustart) deploy-emulator - Build + Install + Launch auf Emulator deploy-device - Build + Install + Launch auf physisches Gerät logcat - App-Logcat anzeigen (Ctrl+C zum Beenden) devices - Verbundene Geräte auflisten - screenshot - Screenshot vom aktiven Gerät speichern + (Screenshot: separates Skript screenshot.ps1) .PARAMETER Target Zielgerät: 'emulator' (Standard) oder 'device'. @@ -36,9 +37,9 @@ param( 'build', 'clean', 'clean-build', 'emulator-start', 'emulator-stop', 'install-emulator', 'install-device', - 'launch', + 'launch', 'hot-reload', 'deploy-emulator', 'deploy-device', - 'logcat', 'devices', 'screenshot' + 'logcat', 'devices' )] [string]$Action, @@ -301,6 +302,30 @@ switch ($Action) { exit ([int](-not $ok)) } + 'hot-reload' { + # Build + Install + Relaunch auf bereits laufendem Emulator/Gerät. + # Kein Emulator-Start, kein Snapshot-Laden — spart 60-90s. + $targetType = $Target + if (-not (Test-DeviceConnected $targetType)) { + Write-Err "Kein $targetType verbunden. Starte zuerst mit: -Action emulator-start" + exit 1 + } + + $ok = Invoke-Gradle @('assembleDebug') + if (-not $ok) { exit 1 } + + # App stoppen bevor Install (verhindert 'Activity not started' Race Condition) + Write-Step "App stoppen" + $flags = Get-AdbTarget + & $ADB $flags shell am force-stop $PACKAGE 2>$null + + $ok = Install-Apk $targetType + if (-not $ok) { exit 1 } + + $ok = Start-App + exit ([int](-not $ok)) + } + 'deploy-emulator' { $Target = 'emulator' $ok = Start-Emulator @@ -350,22 +375,4 @@ switch ($Action) { Write-Step "Verbundene Geräte" & $ADB devices -l } - - 'screenshot' { - $flags = Get-AdbTarget - $tmpDir = "$PROJECT_DIR\tmp" - if (-not (Test-Path $tmpDir)) { New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null } - $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" - $filename = "screenshot-$timestamp.png" - $screenshotPath = "$tmpDir\$filename" - Write-Step "Screenshot: $filename" - & $ADB $flags exec-out screencap -p > $screenshotPath - if ((Test-Path $screenshotPath) -and (Get-Item $screenshotPath).Length -gt 0) { - $sizeKB = [math]::Round((Get-Item $screenshotPath).Length / 1KB) - Write-Ok "Gespeichert: tmp\$filename (${sizeKB} KB)" - } - else { - Write-Err "Screenshot fehlgeschlagen" - } - } } diff --git a/.github/skills/android-build/screenshot.ps1 b/.github/skills/android-build/screenshot.ps1 new file mode 100644 index 0000000..1b086a7 --- /dev/null +++ b/.github/skills/android-build/screenshot.ps1 @@ -0,0 +1,202 @@ +<# +.SYNOPSIS + Robuster Screenshot vom Android-Emulator/Gerät. + +.DESCRIPTION + Umgeht PowerShells UTF-16-Encoding-Problem bei Binärausgaben. + Nutzt adb pull statt exec-out-Pipe, validiert den PNG-Header, + und liefert als Fallback eine Text-Beschreibung der UI via uiautomator. + +.PARAMETER OutputPath + Zielpfad für den Screenshot. Standard: tmp/screenshot-.png + +.PARAMETER Target + 'emulator' (Standard) oder 'device'. + +.PARAMETER UiDump + Zusätzlich UI-Hierarchie als Text ausgeben (nützlich für automatische Verifikation). + +.EXAMPLE + & ".github/skills/android-build/screenshot.ps1" + & ".github/skills/android-build/screenshot.ps1" -UiDump + & ".github/skills/android-build/screenshot.ps1" -OutputPath "my-screenshot.png" +#> +param( + [string]$OutputPath, + [ValidateSet('emulator', 'device')] + [string]$Target = 'emulator', + [switch]$UiDump +) + +$ErrorActionPreference = 'Stop' + +# --- Konfiguration --- +$SDK_ROOT = "C:\Users\JensR\AppData\Local\Android\Sdk" +$ADB = "$SDK_ROOT\platform-tools\adb.exe" +$PROJECT_DIR = $PSScriptRoot | Split-Path | Split-Path | Split-Path +$ADB_TIMEOUT = 15 # Sekunden pro ADB-Kommando +$DEVICE_TMP = "/sdcard/screenshot-tmp.png" + +# --- Hilfsfunktionen --- +function Write-Step { param([string]$msg) Write-Host ">> $msg" -ForegroundColor Cyan } +function Write-Ok { param([string]$msg) Write-Host "OK $msg" -ForegroundColor Green } +function Write-Err { param([string]$msg) Write-Host "ERR $msg" -ForegroundColor Red } + +function Get-AdbFlag { + if ($Target -eq 'device') { return '-d' } + return '-e' +} + +function Invoke-AdbWithTimeout { + param( + [string[]]$Arguments, + [int]$TimeoutSec = $ADB_TIMEOUT + ) + $psi = [System.Diagnostics.ProcessStartInfo]::new($ADB) + $psi.Arguments = $Arguments -join ' ' + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + + $proc = [System.Diagnostics.Process]::Start($psi) + $stdout = $proc.StandardOutput.ReadToEnd() + $stderr = $proc.StandardError.ReadToEnd() + $exited = $proc.WaitForExit($TimeoutSec * 1000) + + if (-not $exited) { + $proc.Kill() + throw "ADB-Timeout nach ${TimeoutSec}s: $($Arguments -join ' ')" + } + + return @{ + ExitCode = $proc.ExitCode + Stdout = $stdout + Stderr = $stderr + } +} + +# --- Verbindung prüfen --- +$flag = Get-AdbFlag +Write-Step "Prüfe $Target-Verbindung..." +try { + $check = Invoke-AdbWithTimeout @($flag, 'get-state') -TimeoutSec 5 + if ($check.Stdout.Trim() -ne 'device') { + Write-Err "$Target nicht verbunden (Status: $($check.Stdout.Trim()))" + exit 1 + } +} +catch { + Write-Err "$Target nicht erreichbar: $_" + exit 1 +} +Write-Ok "$Target verbunden" + +# --- Zielpfad bestimmen --- +$tmpDir = Join-Path $PROJECT_DIR "tmp" +if (-not (Test-Path $tmpDir)) { New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null } + +if (-not $OutputPath) { + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $OutputPath = Join-Path $tmpDir "screenshot-$timestamp.png" +} +elseif (-not [System.IO.Path]::IsPathRooted($OutputPath)) { + $OutputPath = Join-Path $PROJECT_DIR $OutputPath +} + +# --- Screenshot aufnehmen --- +Write-Step "Screenshot aufnehmen..." + +# Schritt 1: Screenshot auf dem Gerät erstellen +try { + $cap = Invoke-AdbWithTimeout @($flag, 'shell', 'screencap', '-p', $DEVICE_TMP) -TimeoutSec $ADB_TIMEOUT + if ($cap.ExitCode -ne 0) { + Write-Err "screencap fehlgeschlagen: $($cap.Stderr)" + exit 1 + } +} +catch { + Write-Err "screencap abgebrochen: $_" + exit 1 +} + +# Schritt 2: Datei vom Gerät holen (adb pull = binärsicher) +try { + $pull = Invoke-AdbWithTimeout @($flag, 'pull', $DEVICE_TMP, $OutputPath) -TimeoutSec $ADB_TIMEOUT + if ($pull.ExitCode -ne 0) { + Write-Err "adb pull fehlgeschlagen: $($pull.Stderr)" + exit 1 + } +} +catch { + Write-Err "adb pull abgebrochen: $_" + exit 1 +} + +# Schritt 3: Temp-Datei auf dem Gerät löschen +try { + Invoke-AdbWithTimeout @($flag, 'shell', 'rm', '-f', $DEVICE_TMP) -TimeoutSec 5 | Out-Null +} +catch { + # Nicht kritisch +} + +# --- PNG validieren --- +if (-not (Test-Path $OutputPath)) { + Write-Err "Screenshot-Datei nicht erstellt: $OutputPath" + exit 1 +} + +$fileInfo = Get-Item $OutputPath +if ($fileInfo.Length -eq 0) { + Remove-Item $OutputPath -Force + Write-Err "Screenshot-Datei ist leer (0 Bytes)" + exit 1 +} + +$bytes = [System.IO.File]::ReadAllBytes($OutputPath) +$pngMagic = @(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A) +$headerValid = $true +for ($i = 0; $i -lt 8; $i++) { + if ($bytes[$i] -ne $pngMagic[$i]) { $headerValid = $false; break } +} + +if (-not $headerValid) { + $hexHeader = ($bytes[0..7] | ForEach-Object { "{0:X2}" -f $_ }) -join " " + Write-Err "Ungültiger PNG-Header: $hexHeader (erwartet: 89 50 4E 47 0D 0A 1A 0A)" + Write-Err "Mögliche Ursache: PowerShell UTF-16-Encoding oder Geräte-Fehler" + Remove-Item $OutputPath -Force + exit 1 +} + +$sizeKB = [math]::Round($fileInfo.Length / 1KB) +$relativePath = $OutputPath.Replace("$PROJECT_DIR\", "") +Write-Ok "Screenshot gespeichert: $relativePath (${sizeKB} KB, PNG validiert)" + +# --- UI-Dump (optional) --- +if ($UiDump) { + Write-Step "UI-Hierarchie auslesen..." + try { + Invoke-AdbWithTimeout @($flag, 'shell', 'uiautomator', 'dump', '/sdcard/ui-dump.xml') -TimeoutSec 10 | Out-Null + $xmlResult = Invoke-AdbWithTimeout @($flag, 'shell', 'cat', '/sdcard/ui-dump.xml') -TimeoutSec 5 + Invoke-AdbWithTimeout @($flag, 'shell', 'rm', '-f', '/sdcard/ui-dump.xml') -TimeoutSec 5 | Out-Null + + # Sichtbare Texte extrahieren + $texts = [regex]::Matches($xmlResult.Stdout, 'text="([^"]+)"') | + ForEach-Object { $_.Groups[1].Value } | + Where-Object { $_ -ne "" } + + if ($texts.Count -gt 0) { + Write-Step "Sichtbare Texte auf dem Bildschirm:" + $texts | ForEach-Object { Write-Host " · $_" -ForegroundColor White } + } + else { + Write-Host " (keine Texte gefunden)" -ForegroundColor DarkGray + } + } + catch { + Write-Err "UI-Dump fehlgeschlagen: $_" + } +} + +exit 0 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 88162d5..451e53b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { applicationId = "de.krisenvorrat.app" minSdk = 26 targetSdk = 35 - versionCode = 1 - versionName = "1.0" + versionCode = 2 + versionName = "1.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }