param([Parameter(Mandatory)][int[]]$Contacts, [int]$Port=8554) $ErrorActionPreference='Stop' if ($Contacts.Count -eq 0 -or $Contacts.Count % 4 -ne 0) { throw 'Contacts must contain repeated x,y,id,pressure quadruples.' } [AppContext]::SetSwitch('System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport', $true) $discovery=Get-ChildItem -LiteralPath (Join-Path $env:LOCALAPPDATA 'Temp/avd/running') -Filter 'pid_*.ini' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 $tokenLine=Get-Content -LiteralPath $discovery.FullName | Where-Object { $_ -match '^grpc.token=' } | Select-Object -First 1 if (!$tokenLine) { throw 'Authenticated emulator discovery is required; tokens are never logged.' } function Varint([int]$Value) { [uint32]$number=$Value; $bytes=[Collections.Generic.List[byte]]::new() while ($number -ge 128) { $bytes.Add([byte](($number -band 127) -bor 128)); $number=$number -shr 7 } $bytes.Add([byte]$number); return ,$bytes.ToArray() } $message=[Collections.Generic.List[byte]]::new() for ($offset=0; $offset -lt $Contacts.Count; $offset+=4) { # AVD touch injection uses portrait physical coordinates even when the app is landscape-left. $fields=@((1080-$Contacts[$offset+1]),$Contacts[$offset],$Contacts[$offset+2],$Contacts[$offset+3]) $touch=[Collections.Generic.List[byte]]::new() for ($field=0; $field -lt 4; $field++) { $touch.Add([byte](8*($field+1))); $touch.AddRange([byte[]](Varint $fields[$field])) } $message.Add(10); $message.AddRange([byte[]](Varint $touch.Count)); $message.AddRange($touch.ToArray()) } $frame=[byte[]]::new($message.Count+5); $frame[1]=($message.Count -shr 24) -band 255; $frame[2]=($message.Count -shr 16) -band 255 $frame[3]=($message.Count -shr 8) -band 255; $frame[4]=$message.Count -band 255; $message.ToArray().CopyTo($frame,5) $client=[Net.Http.HttpClient]::new(); $request=[Net.Http.HttpRequestMessage]::new([Net.Http.HttpMethod]::Post,"http://127.0.0.1:$Port/android.emulation.control.EmulatorController/sendTouch") $request.Version=[Version]::new(2,0); $request.VersionPolicy=[Net.Http.HttpVersionPolicy]::RequestVersionExact $request.Headers.Add('TE','trailers'); $request.Headers.Authorization=[Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer',$tokenLine.Substring('grpc.token='.Length)) $request.Content=[Net.Http.ByteArrayContent]::new($frame); $request.Content.Headers.ContentType=[Net.Http.Headers.MediaTypeHeaderValue]::new('application/grpc') try { $response=$client.SendAsync($request).GetAwaiter().GetResult(); $response.EnsureSuccessStatusCode() | Out-Null $status=if ($response.TrailingHeaders.Contains('grpc-status')) { ($response.TrailingHeaders.GetValues('grpc-status') -join '') } else { '0' } if ($status -ne '0') { throw "Emulator sendTouch grpc-status=$status" } } finally { $request.Dispose(); $client.Dispose() }