param([Parameter(Mandatory)][string]$ApkPath,[Parameter(Mandatory)][string]$EvidenceDir,[Parameter(Mandatory)][string]$Name,[ValidateSet('Phone','Tablet')][string]$Viewport='Phone',[int]$ObserveSeconds=0,[string]$Serial='emulator-5554',[string]$ApkSourceCommit) $ErrorActionPreference='Stop' if($Serial-ne'emulator-5554'){throw 'Only the authorized emulator-5554 may be observed'} if($Name-notmatch'^[a-zA-Z0-9_-]+$'){throw 'Observation name must be a simple artifact label'} New-Item -ItemType Directory -Path $EvidenceDir -Force|Out-Null $adb=Join-Path $env:LOCALAPPDATA 'Android/Sdk/platform-tools/adb.exe';$package='com.magnetismheroes.game' $workspaceCommit=(& git rev-parse HEAD);if(!$ApkSourceCommit){$ApkSourceCommit=$workspaceCommit} $commands=[Collections.Generic.List[object]]::new();$result=[ordered]@{status='failed';scope='Actual AVD capture/resource observation only; direct visual judgement and game behavior are separate. APK source attribution must be linked to build or baseline-product-equivalence evidence by the observation owner.';name=$Name;serial=$Serial;viewport=$Viewport;sourceCommit=$ApkSourceCommit;workspaceCommit=$workspaceCommit;sourceDirty=[bool](& git status --porcelain);apkSha256=(Get-FileHash -LiteralPath $ApkPath).Hash;startedUtc=[DateTime]::UtcNow.ToString('o')} function Adb([string[]]$Arguments){$output=& $adb -s $Serial @Arguments 2>&1;$code=$LASTEXITCODE;$commands.Add(@{command='adb -s '+$Serial+' '+($Arguments-join' ');exitCode=$code;utc=[DateTime]::UtcNow.ToString('o');output=$output-join"`n"});if($code-ne0){throw "ADB observation exit $code"};return ($output-join"`n").Trim()} $sizeBefore=$null;$statsEnabled=$false try{ if($result.sourceDirty){throw 'Observation source is dirty'} if((Adb @('emu','avd','name'))-notmatch'^Medium_Phone_API_37\.0'){throw 'Unexpected AVD'} $remoteApk=(Adb @('shell','pm','path',$package)).Replace('package:','').Split("`n")[0].Trim() $installed=Join-Path $EvidenceDir "$Name-installed-base.apk";Adb @('pull',$remoteApk,$installed)|Out-Null if((Get-FileHash -LiteralPath $installed).Hash-ne$result.apkSha256){throw 'Installed APK differs from supplied observation APK'} $sizeBefore=Adb @('shell','wm','size');$result.sizeBefore=$sizeBefore if($Viewport-eq'Tablet'){Adb @('shell','wm','size','1000x1600')|Out-Null}else{Adb @('shell','wm','size','reset')|Out-Null} Start-Sleep -Milliseconds 1500 $png=Join-Path $EvidenceDir "$Name.png";Adb @('shell','screencap','-p',"/sdcard/$Name.png")|Out-Null;Adb @('pull',"/sdcard/$Name.png",$png)|Out-Null Add-Type -AssemblyName System.Drawing;$image=[Drawing.Bitmap]::new($png) try{$result.width=$image.Width;$result.height=$image.Height}finally{$image.Dispose()} $expected=if($Viewport-eq'Tablet'){@(1600,1000)}else{@(2400,1080)} if($result.width-ne$expected[0]-or$result.height-ne$expected[1]){throw 'Actual PNG dimensions differ from requested landscape viewport'} $result.pngSha256=(Get-FileHash -LiteralPath $png).Hash $save=Join-Path $EvidenceDir "$Name-progress.json";Adb @('pull',"/sdcard/Android/data/$package/files/progress.json",$save)|Out-Null;$result.progressSha256=(Get-FileHash -LiteralPath $save).Hash if($ObserveSeconds-gt0){ if($ObserveSeconds-lt5-or$ObserveSeconds-gt60){throw 'Resource sample must be between 5 and 60 seconds'} Adb @('shell','dumpsys','SurfaceFlinger','--timestats','-clear','-enable')|Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-timestats-start.txt");$statsEnabled=$true Start-Sleep -Seconds $ObserveSeconds Adb @('shell','dumpsys','SurfaceFlinger','--timestats','-dump')|Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-timestats.txt") Adb @('shell','dumpsys','meminfo',$package)|Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-meminfo.txt") $result.observeSeconds=$ObserveSeconds;$result.resourceMethod='Actual same-AVD SurfaceView presentToPresent histograms and total process PSS raw dumps; root must compare matching camera/viewport/board/load order/warmup/sample duration. AVD observation is not real-device FPS.' } $result.status='passed' }catch{$result.error=$_.Exception.Message} finally{ try{ if($statsEnabled){Adb @('shell','dumpsys','SurfaceFlinger','--timestats','-disable')|Out-Null} if($sizeBefore){if($sizeBefore-match'Override size: (\d+x\d+)'){Adb @('shell','wm','size',$Matches[1])|Out-Null}else{Adb @('shell','wm','size','reset')|Out-Null};$result.sizeAfter=Adb @('shell','wm','size');if($result.sizeAfter-ne$sizeBefore){throw 'Original display size was not restored'}} }catch{$result.status='failed';$result.restoreError=$_.Exception.Message} $result.commands=$commands.ToArray();$result.finishedUtc=[DateTime]::UtcNow.ToString('o');$result|ConvertTo-Json -Depth 8|Set-Content -LiteralPath (Join-Path $EvidenceDir "$Name-observation.json") -Encoding utf8 } if($result.status-ne'passed'){Write-Host $result.error;exit 1};exit 0