feat(skills): add hot-reload action and robust screenshot script
android-dev.ps1: - Added 'hot-reload' action: build + force-stop + install + launch on a running emulator/device without restart (saves 60-90s vs deploy-emulator) - Removed 'screenshot' action (replaced by standalone script) screenshot.ps1 (new): - Uses adb pull instead of exec-out pipe to avoid PowerShell's UTF-16 CRLF corruption of binary data (root cause of all broken screenshots) - Validates PNG magic bytes after capture - ADB commands wrapped with configurable timeout (prevents hangs) - Optional -UiDump flag extracts visible text via uiautomator for automated verification without image viewing SKILL.md: - Documented hot-reload action app/build.gradle.kts: - Version bump 1.0 → 1.1 (versionCode 1 → 2)
This commit is contained in:
parent
a9a999fd1e
commit
6603016369
4 changed files with 235 additions and 23 deletions
3
.github/skills/android-build/SKILL.md
vendored
3
.github/skills/android-build/SKILL.md
vendored
|
|
@ -36,6 +36,9 @@ Verwende **immer** das `android-dev.ps1`-Skript statt roher Gradle-Aufrufe:
|
||||||
|
|
||||||
# Nur clean
|
# Nur clean
|
||||||
& ".github/skills/android-build/android-dev.ps1" -Action 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)
|
### Direkter Gradle-Aufruf (Fallback)
|
||||||
|
|
|
||||||
49
.github/skills/android-build/android-dev.ps1
vendored
49
.github/skills/android-build/android-dev.ps1
vendored
|
|
@ -16,11 +16,12 @@
|
||||||
install-emulator - APK auf Emulator installieren
|
install-emulator - APK auf Emulator installieren
|
||||||
install-device - APK auf physisches Gerät installieren
|
install-device - APK auf physisches Gerät installieren
|
||||||
launch - App starten (auf dem aktiven Ziel)
|
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-emulator - Build + Install + Launch auf Emulator
|
||||||
deploy-device - Build + Install + Launch auf physisches Gerät
|
deploy-device - Build + Install + Launch auf physisches Gerät
|
||||||
logcat - App-Logcat anzeigen (Ctrl+C zum Beenden)
|
logcat - App-Logcat anzeigen (Ctrl+C zum Beenden)
|
||||||
devices - Verbundene Geräte auflisten
|
devices - Verbundene Geräte auflisten
|
||||||
screenshot - Screenshot vom aktiven Gerät speichern
|
(Screenshot: separates Skript screenshot.ps1)
|
||||||
|
|
||||||
.PARAMETER Target
|
.PARAMETER Target
|
||||||
Zielgerät: 'emulator' (Standard) oder 'device'.
|
Zielgerät: 'emulator' (Standard) oder 'device'.
|
||||||
|
|
@ -36,9 +37,9 @@ param(
|
||||||
'build', 'clean', 'clean-build',
|
'build', 'clean', 'clean-build',
|
||||||
'emulator-start', 'emulator-stop',
|
'emulator-start', 'emulator-stop',
|
||||||
'install-emulator', 'install-device',
|
'install-emulator', 'install-device',
|
||||||
'launch',
|
'launch', 'hot-reload',
|
||||||
'deploy-emulator', 'deploy-device',
|
'deploy-emulator', 'deploy-device',
|
||||||
'logcat', 'devices', 'screenshot'
|
'logcat', 'devices'
|
||||||
)]
|
)]
|
||||||
[string]$Action,
|
[string]$Action,
|
||||||
|
|
||||||
|
|
@ -301,6 +302,30 @@ switch ($Action) {
|
||||||
exit ([int](-not $ok))
|
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' {
|
'deploy-emulator' {
|
||||||
$Target = 'emulator'
|
$Target = 'emulator'
|
||||||
$ok = Start-Emulator
|
$ok = Start-Emulator
|
||||||
|
|
@ -350,22 +375,4 @@ switch ($Action) {
|
||||||
Write-Step "Verbundene Geräte"
|
Write-Step "Verbundene Geräte"
|
||||||
& $ADB devices -l
|
& $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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
202
.github/skills/android-build/screenshot.ps1
vendored
Normal file
202
.github/skills/android-build/screenshot.ps1
vendored
Normal file
|
|
@ -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-<timestamp>.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
|
||||||
|
|
@ -15,8 +15,8 @@ android {
|
||||||
applicationId = "de.krisenvorrat.app"
|
applicationId = "de.krisenvorrat.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "1.0"
|
versionName = "1.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue