Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Appium Wizard/Android Wireless.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,18 @@ await Task.Run(() => {
else
{
Dictionary<string, string> deviceInfo = new Dictionary<string, string>();
await Task.Run(() => {
deviceInfo = AndroidAsyncMethods.GetInstance().GetDeviceInformation(connectIPAddress);
});
// Right after connecting, the device can briefly be offline and return no properties.
const int maxAttempts = 8;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
deviceInfo = await Task.Run(() => AndroidAsyncMethods.GetInstance().GetDeviceInformation(connectIPAddress));
if (deviceInfo.ContainsKey("ro.serialno") || attempt == maxAttempts)
{
break;
}
commonProgress.UpdateStepLabel("Android Wireless device", "Waiting for the device to respond (" + attempt + "/" + maxAttempts + ")...", 50 + attempt * 3);
await Task.Delay(1000);
}
commonProgress.UpdateStepLabel("Android Wireless device", "Connecting to Android device over Wi-Fi...", 75);
DeviceInformation deviceInformation = new DeviceInformation(mainScreen);
if (deviceInfo.ContainsKey("ro.serialno"))
Expand Down Expand Up @@ -244,14 +253,15 @@ await Task.Run(() => {
else
{
commonProgress.Close();
MessageBox.Show("No Android Device available.\nPlease check if the device is on the same network as this PC.\nGo to Developer options > Wireless debugging > Pair device with pairing code > Find Devices again.", "Add Android Device Over Wi-Fi", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Show("No Android Device available.\n1.Turn Wireless debugging off and on again in Developer options, then try again.\n2.Check if the device is on the same network as this PC.", "Add Android Device Over Wi-Fi", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}

private void AndroidWireless_Shown(object sender, EventArgs e)
{
GoogleAnalytics.SendEvent("AndroidWireless_Shown");
ActiveControl = QRButton;
}

private void QRButton_Click(object sender, EventArgs e)
Expand Down
163 changes: 160 additions & 3 deletions Appium Wizard/AndroidMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,162 @@
return ExecuteCommand(" connect " + connectAddress);
}

// Android picks a new Wireless debugging port every time the phone locks or Wi-Fi sleeps, so the
// IP:port saved for a device goes stale. This maps the saved address to the current one and is
// applied to every adb command (see ResolveAddress).
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, string> wirelessAddressAlias = new();
private static readonly object wirelessReconnectLock = new object();

public static string ResolveAddress(string udid)
{
return udid != null && wirelessAddressAlias.TryGetValue(udid, out string current) ? current : udid;
}

// Rewrites "-s <savedAddress>" in an adb argument string to the device's current address.
internal static string ResolveArguments(string arguments)
{
if (arguments == null || wirelessAddressAlias.IsEmpty)
{
return arguments;
}
foreach (var pair in wirelessAddressAlias)
{
arguments = arguments.Replace("-s " + pair.Key + " ", "-s " + pair.Value + " ");
if (arguments.EndsWith("-s " + pair.Key))
{
arguments = arguments.Substring(0, arguments.Length - pair.Key.Length) + pair.Value;
}
}
return arguments;
}

// Makes sure a wireless (IP:port) device is online, rediscovering its port if it changed.
// Returns the address adb currently knows the device by, or the original one if it can't be found.
public string EnsureWirelessDeviceConnected(string udid, int attempts = 3, Action<string> onStatus = null)

Check warning on line 444 in Appium Wizard/AndroidMethods.cs

View workflow job for this annotation

GitHub Actions / build (Release)

Cannot convert null literal to non-nullable reference type.
{
if (string.IsNullOrEmpty(udid) || !udid.Contains(':'))
{
return udid;
}
// Fast path: already online, so no need to queue behind a reconnect in progress.
string resolved = ResolveAddress(udid);
if (GetListOfDevicesUDID().Contains(resolved))
{
return resolved;
}
onStatus?.Invoke("Wi-Fi connection was lost. Reconnecting to the device, this can take up to 20 seconds...");
lock (wirelessReconnectLock)
{
string ip = udid.Split(':')[0];
for (int i = 0; i < attempts; i++)
{
string current = ResolveAddress(udid);
if (GetListOfDevicesUDID().Contains(current))
{
return current;
}
Logger.Info("Wireless device " + udid + " (current " + current + ") is not online, looking for it again. Attempt " + (i + 1));

// adb may already be connected on a new port (e.g. auto-reconnect through mDNS).
string found = FindConnectedAddressByIp(ip);
if (string.IsNullOrEmpty(found))
{
// Stale "offline" entries for this phone confuse adb, so drop them first.
DisconnectOfflineEntries(ip);
// Closed ports refuse instantly, so scanning is far cheaper than trying
// stale mDNS/saved addresses (each refused "adb connect" takes seconds).
found = ScanForConnectAddress(ip);
}
if (!string.IsNullOrEmpty(found))
{
Thread.Sleep(1000);
if (GetListOfDevicesUDID().Contains(found))
{
if (!found.Equals(udid))
{
Logger.Info("Wireless device " + udid + " moved to " + found);
wirelessAddressAlias[udid] = found;
}
else
{
wirelessAddressAlias.TryRemove(udid, out _);
}
return found;
}
}
Thread.Sleep(1500);
}
return ResolveAddress(udid);
}
}

private void DisconnectOfflineEntries(string ip)
{
try
{
string output = ExecuteCommand("devices");
foreach (Match match in Regex.Matches(output, Regex.Escape(ip) + @":\d+(?=\s+offline)"))
{
ExecuteCommand(" disconnect " + match.Value, true, 5000);
}
}
catch (Exception ex)
{
Logger.Error(ex, "Failed to clear offline wireless entries");
}
}

// Ports that were open but refused to act as adb, such as 39391 on this phone.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<int, byte> nonAdbPorts = new();

private static async Task<List<int>> ScanOpenPortsAsync(string ip, int startPort, int endPort)
{
var open = new System.Collections.Concurrent.ConcurrentBag<int>();
using var gate = new SemaphoreSlim(1000);
var tasks = Enumerable.Range(startPort, endPort - startPort + 1).Select(async port =>
{
await gate.WaitAsync();
try
{
using var client = new System.Net.Sockets.TcpClient();
using var timeout = new CancellationTokenSource(300);
await client.ConnectAsync(ip, port, timeout.Token);
open.Add(port);
}
catch { }
finally { gate.Release(); }
});
await Task.WhenAll(tasks);
return open.OrderBy(p => p).ToList();
}

private string ScanForConnectAddress(string ip)
{
try
{
var sw = Stopwatch.StartNew();
var open = ScanOpenPortsAsync(ip, 30000, 50000).GetAwaiter().GetResult();
Logger.Info("Open ports on " + ip + " : " + string.Join(",", open) + " (scan took " + sw.ElapsedMilliseconds + " ms)");
// Ports that were open but never accepted an adb connection go last.
foreach (int port in open.OrderBy(p => nonAdbPorts.ContainsKey(p)).ThenBy(p => p))
{
string candidate = ip + ":" + port;
string output = ExecuteCommand(" connect " + candidate, true, 4000);
Logger.Info("adb connect " + candidate + " : " + output.Trim());
if (output.Contains("connected to"))
{
return candidate;
}
nonAdbPorts[port] = 0;
}
}
catch (Exception ex)
{
Logger.Error(ex, "Port scan for wireless device failed");
}
return string.Empty;
}

public void ConnectToAndroidWirelessly(string udid, string IPAddress, string Port)
{
ExecuteCommand("-s " + udid + " connect " + IPAddress + ":" + Port);
Expand Down Expand Up @@ -647,6 +803,7 @@
{
try
{
arguments = ResolveArguments(arguments);
adbProcess.StartInfo.Arguments = arguments;
adbProcess.Start();
bool processExited = false;
Expand Down Expand Up @@ -784,7 +941,7 @@
var processStartInfo = new ProcessStartInfo
{
FileName = adbFilePath,
Arguments = "-s " + udid + " forward --list",
Arguments = "-s " + ResolveAddress(udid) + " forward --list",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
Expand Down Expand Up @@ -911,7 +1068,7 @@
{
try
{
adbAsyncProcess.StartInfo.Arguments = arguments;
adbAsyncProcess.StartInfo.Arguments = AndroidMethods.ResolveArguments(arguments);
adbAsyncProcess.Start();
}
catch (Exception ex)
Expand Down Expand Up @@ -951,7 +1108,7 @@
{
try
{
adbAsyncProcess.StartInfo.Arguments = arguments;
adbAsyncProcess.StartInfo.Arguments = AndroidMethods.ResolveArguments(arguments);
StringBuilder outputBuilder = new StringBuilder();
StringBuilder errorBuilder = new StringBuilder();

Expand Down Expand Up @@ -1191,7 +1348,7 @@
var request = new RestRequest("/session/" + sessionId + "/source", Method.Get);
RestResponse response = client.Execute(request);

if (!response.IsSuccessful || response.Content.Contains("invalid session id"))

Check warning on line 1351 in Appium Wizard/AndroidMethods.cs

View workflow job for this annotation

GitHub Actions / build (Release)

Dereference of a possibly null reference.
{
// Check if the response indicates an invalid session
return "Invalid session";
Expand Down
3 changes: 3 additions & 0 deletions Appium Wizard/OpenDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,9 @@ await Task.Run(() =>
string sessionIdAvailableForAutomation = string.Empty;
try
{
// A locked wireless device may have dropped its adb connection; reconnect before setup.
AndroidMethods.GetInstance().EnsureWirelessDeviceConnected(udid, 3,
message => commonProgress.UpdateStepLabel(title, message, 3));
commonProgress.UpdateStepLabel(title, "Getting screen density...", 5);
if (!MainScreen.useScrcpy)
{
Expand Down
50 changes: 47 additions & 3 deletions Appium Wizard/ScrcpyEmbedder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
private System.Windows.Forms.Timer processMonitorTimer;
private string currentUdid;
private int restartAttempts = 0;
private const int MaxRestartAttempts = 5;
private const int MaxRestartAttempts = 15;
private bool isRestarting = false;

/// <summary>
Expand Down Expand Up @@ -117,6 +117,41 @@ public ScrcpyEmbedder(string scrcpyExePath)
//Dock = DockStyle.Fill,
BackColor = Color.Black
};

statusLabel = new Label
{
Dock = DockStyle.Fill,
ForeColor = Color.White,
BackColor = Color.Black,
TextAlign = ContentAlignment.MiddleCenter,
Font = new Font("Segoe UI", 11f),
Visible = false
};
hostPanel.Controls.Add(statusLabel);
}

private Label statusLabel;

// Shows (or hides, when message is null) a message over the panel while scrcpy is reconnecting.
private void SetStatusMessage(string message)
{
if (hostPanel == null || hostPanel.IsDisposed || statusLabel == null)
return;
if (hostPanel.InvokeRequired)
{
hostPanel.BeginInvoke(new Action(() => SetStatusMessage(message)));
return;
}
if (message == null)
{
statusLabel.Visible = false;
}
else
{
statusLabel.Text = message;
statusLabel.Visible = true;
statusLabel.BringToFront();
}
}

public async Task<bool> StartAsync(string udid)
Expand All @@ -129,7 +164,10 @@ public async Task<bool> StartAsync(string udid)
// Store the udid for potential restarts
currentUdid = udid;

ScrcpyArguments = ScrcpyArguments + " -s "+udid;
// Wireless devices may have dropped the adb link (e.g. after the screen was locked).
string resolvedUdid = await Task.Run(() => AndroidMethods.GetInstance().EnsureWirelessDeviceConnected(udid));

ScrcpyArguments = ScrcpyArguments + " -s " + resolvedUdid;

ProcessStartInfo psi = new ProcessStartInfo
{
Expand Down Expand Up @@ -277,6 +315,7 @@ private async Task RestartScrcpyAsync()
try
{
Logger.Info("Restarting scrcpy...");
SetStatusMessage("Connection to the device was lost.\nReconnecting, please wait...\n\nThis can take up to 20 seconds.");

// Stop the monitoring timer temporarily
processMonitorTimer?.Stop();
Expand All @@ -295,9 +334,13 @@ private async Task RestartScrcpyAsync()
// Wait a bit before restarting
await Task.Delay(1000);

// Bring a dropped wireless adb connection back before relaunching scrcpy
string udidToReconnect = currentUdid;
string resolvedUdid = await Task.Run(() => AndroidMethods.GetInstance().EnsureWirelessDeviceConnected(udidToReconnect, 5));

// Rebuild the arguments (clear any previous appended udid)
string baseArgs = "--max-size 1080 --window-title=\"scrcpy_embed\" --stay-awake --disable-screensaver --window-x=-2000 --window-y=-2000";
ScrcpyArguments = baseArgs + " -s " + currentUdid;
ScrcpyArguments = baseArgs + " -s " + resolvedUdid;

// Start new scrcpy process
ProcessStartInfo psi = new ProcessStartInfo
Expand Down Expand Up @@ -367,6 +410,7 @@ private async Task RestartScrcpyAsync()
finally
{
isRestarting = false;
SetStatusMessage(null);
}
}

Expand Down
Loading
Loading