diff --git a/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj b/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj
index 9b81f49..5b1fa07 100644
--- a/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj
+++ b/DeviceTests/DeviceTests.Android/DeviceTests.Android.csproj
@@ -25,8 +25,10 @@
prompt
4
None
-
+ armeabi-v7a;x86;x86_64;arm64-v8a
1G
+ false
+ true
true
diff --git a/DeviceTests/DeviceTests.Android/MainActivity.cs b/DeviceTests/DeviceTests.Android/MainActivity.cs
index 7adb290..7c20f5f 100644
--- a/DeviceTests/DeviceTests.Android/MainActivity.cs
+++ b/DeviceTests/DeviceTests.Android/MainActivity.cs
@@ -1,31 +1,30 @@
-using System.IO;
-using System.Reflection;
-using Android.App;
-using Android.Content.PM;
-using Android.OS;
-using Android.Runtime;
-using Xunit.Runners.UI;
-using Environment = System.Environment;
-
-namespace DeviceTests.Droid
-{
- [Activity(MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
- public class MainActivity : RunnerActivity
- {
- protected override void OnCreate(Bundle bundle)
- {
- AddExecutionAssembly(Assembly.GetExecutingAssembly());
-
- AddTestAssembly(typeof(SharedTests).Assembly);
-
- var path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
-
- ResultChannel = new CombinedResultChannel(Path.Combine(path, "TestResults.trx"));
-
- AutoStart = true;
- TerminateAfterExecution = true;
-
- base.OnCreate(bundle);
- }
- }
-}
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+using System.IO;
+using System.Reflection;
+using Xunit.Runners.UI;
+using Environment = System.Environment;
+
+namespace DeviceTests.Droid
+{
+ [Activity(MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
+ public class MainActivity : RunnerActivity
+ {
+ protected override void OnCreate(Bundle bundle)
+ {
+ AddExecutionAssembly(Assembly.GetExecutingAssembly());
+
+ AddTestAssembly(typeof(SharedTests).Assembly);
+
+ var path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
+
+ ResultChannel = new CombinedResultChannel(Path.Combine(path, "TestResults.trx"));
+
+ AutoStart = true;
+ TerminateAfterExecution = true;
+
+ base.OnCreate(bundle);
+ }
+ }
+}
diff --git a/dotnet-devices.Tests/Android/AaptToolTests.cs b/dotnet-devices.Tests/Android/AaptToolTests.cs
index 5ce9709..1e5bd58 100644
--- a/dotnet-devices.Tests/Android/AaptToolTests.cs
+++ b/dotnet-devices.Tests/Android/AaptToolTests.cs
@@ -18,7 +18,7 @@ namespace DotNetDevices.Tests
{
var xmltree = File.ReadAllText(file);
- var xdoc = AaptTool.ParseXmlTree(xmltree);
+ var xdoc = Aapt.ParseXmlTree(xmltree);
Assert.NotNull(xdoc);
}
@@ -27,7 +27,7 @@ namespace DotNetDevices.Tests
public void ParseIsValid()
{
var xmltree = File.ReadAllText("TestData/Android/CompiledXmlDump.txt");
- var xdoc = AaptTool.ParseXmlTree(xmltree);
+ var xdoc = Aapt.ParseXmlTree(xmltree);
Assert.NotNull(xdoc);
var manifest = xdoc.Root;
diff --git a/dotnet-devices.Tests/Android/VirtualDeviceConfigTests.cs b/dotnet-devices.Tests/Android/VirtualDeviceConfigTests.cs
index 52209ce..4ca1700 100644
--- a/dotnet-devices.Tests/Android/VirtualDeviceConfigTests.cs
+++ b/dotnet-devices.Tests/Android/VirtualDeviceConfigTests.cs
@@ -10,8 +10,8 @@ namespace DotNetDevices.Tests
public class CreateVirtualDeviceAsync
{
[Theory]
- [InlineData("TestData/Android/AvdConfigIni_Normal.txt", "pixel_2_q_10_0_-_api_29", "Pixel 2 Q 10.0 - API 29")]
- [InlineData("TestData/Android/AvdConfigIni_Tiny.txt", "pixel_2_q_10_0_-_api_29", "pixel_2_q_10_0_-_api_29")]
+ [InlineData("TestData/Android/AvdConfigIni_Normal.avd", "pixel_2_q_10_0_-_api_29", "Pixel 2 Q 10.0 - API 29")]
+ [InlineData("TestData/Android/AvdConfigIni_Tiny.avd", "pixel_2_q_10_0_-_api_29", "pixel_2_q_10_0_-_api_29")]
public async Task CanCreateInstance(string file, string id, string name)
{
var config = new VirtualDeviceConfig(file);
@@ -26,7 +26,7 @@ namespace DotNetDevices.Tests
[Fact]
public async Task CanReadTV()
{
- var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_TV.txt");
+ var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_TV.avd");
var device = await config.CreateVirtualDeviceAsync();
@@ -39,7 +39,7 @@ namespace DotNetDevices.Tests
[Fact]
public async Task CanReadWear()
{
- var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Wear.txt");
+ var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Wear.avd");
var device = await config.CreateVirtualDeviceAsync();
@@ -52,7 +52,7 @@ namespace DotNetDevices.Tests
[Fact]
public async Task CanReadTablet()
{
- var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Tablet.txt");
+ var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Tablet.avd");
var device = await config.CreateVirtualDeviceAsync();
@@ -65,7 +65,7 @@ namespace DotNetDevices.Tests
[Fact]
public async Task CanReadGeneric()
{
- var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Generic.txt");
+ var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Generic.avd");
var device = await config.CreateVirtualDeviceAsync();
@@ -78,7 +78,7 @@ namespace DotNetDevices.Tests
[Fact]
public async Task CanReadPhone()
{
- var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Phone.txt");
+ var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Phone.avd");
var device = await config.CreateVirtualDeviceAsync();
diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Generic.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Generic.avd/config.ini
similarity index 100%
rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Generic.txt
rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Generic.avd/config.ini
diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Normal.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Normal.avd/config.ini
similarity index 100%
rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Normal.txt
rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Normal.avd/config.ini
diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Phone.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Phone.avd/config.ini
similarity index 100%
rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Phone.txt
rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Phone.avd/config.ini
diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_TV.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_TV.avd/config.ini
similarity index 100%
rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_TV.txt
rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_TV.avd/config.ini
diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tablet.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tablet.avd/config.ini
similarity index 100%
rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tablet.txt
rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tablet.avd/config.ini
diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tiny.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tiny.avd/config.ini
similarity index 100%
rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tiny.txt
rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Tiny.avd/config.ini
diff --git a/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Wear.txt b/dotnet-devices.Tests/TestData/Android/AvdConfigIni_Wear.avd/config.ini
similarity index 100%
rename from dotnet-devices.Tests/TestData/Android/AvdConfigIni_Wear.txt
rename to dotnet-devices.Tests/TestData/Android/AvdConfigIni_Wear.avd/config.ini
diff --git a/dotnet-devices/Android/AVDManager.cs b/dotnet-devices/Android/AVDManager.cs
index e8de93e..e30f6bc 100644
--- a/dotnet-devices/Android/AVDManager.cs
+++ b/dotnet-devices/Android/AVDManager.cs
@@ -11,7 +11,8 @@ namespace DotNetDevices.Android
{
public class AVDManager
{
- private static Regex virtualDevicePathRegex = new Regex(@"\s*Path\:\s*(.+)");
+ private static readonly Regex virtualDevicePathRegex = new Regex(@"\s*Path\:\s*(.+)");
+ private static readonly string[] userDataFiles = { "userdata-qemu.img", "userdata-qemu.img.qcow2" };
private readonly ProcessRunner processRunner;
private readonly ILogger? logger;
@@ -58,7 +59,21 @@ namespace DotNetDevices.Android
return targets;
}
- public async Task> GetVirtualDeviceNamesAsync(CancellationToken cancellationToken = default)
+ public async Task ResetVirtualDeviceAsync(string id, CancellationToken cancellationToken)
+ {
+ logger?.LogInformation($"Resetting virtual device '{id}'...");
+
+ var avdPath = await GetVirtualDevicePathAsync(id, cancellationToken).ConfigureAwait(false);
+
+ foreach (var file in userDataFiles)
+ {
+ var f = Path.Combine(avdPath, file);
+ if (File.Exists(f))
+ File.Delete(f);
+ }
+ }
+
+ public async Task> GetVirtualDeviceIdsAsync(CancellationToken cancellationToken = default)
{
logger?.LogInformation("Retrieving all the virtual devices...");
@@ -77,7 +92,6 @@ namespace DotNetDevices.Android
if (Directory.Exists(path))
{
var avd = Path.GetFileNameWithoutExtension(path);
-
avds.Add(avd);
}
}
@@ -86,6 +100,29 @@ namespace DotNetDevices.Android
return avds;
}
+ public async Task GetVirtualDevicePathAsync(string id, CancellationToken cancellationToken = default)
+ {
+ logger?.LogInformation("Retrieving all the virtual devices...");
+
+ var args = $"list avd";
+
+ var result = await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
+
+ foreach (var output in GetListResults(result))
+ {
+ var pathMatch = virtualDevicePathRegex.Match(output);
+ if (pathMatch.Success)
+ {
+ var path = pathMatch.Groups[1].Value;
+ var avdId = Path.GetFileNameWithoutExtension(path);
+ if (avdId.Equals(id, StringComparison.OrdinalIgnoreCase) && Directory.Exists(path))
+ return path;
+ }
+ }
+
+ throw new Exception($"Virtual device '{id}' does not exist.");
+ }
+
public async Task> GetVirtualDevicesAsync(CancellationToken cancellationToken = default)
{
logger?.LogInformation("Retrieving all the virtual devices...");
@@ -105,7 +142,7 @@ namespace DotNetDevices.Android
var configIniPath = Path.Combine(path, "config.ini");
if (Directory.Exists(path) && File.Exists(configIniPath))
{
- var config = new VirtualDeviceConfig(configIniPath, logger);
+ var config = new VirtualDeviceConfig(path, logger);
var avd = await config.CreateVirtualDeviceAsync(cancellationToken).ConfigureAwait(false);
avds.Add(avd);
@@ -116,11 +153,11 @@ namespace DotNetDevices.Android
return avds;
}
- public async Task DeleteVirtualDeviceAsync(string name, CancellationToken cancellationToken = default)
+ public async Task DeleteVirtualDeviceAsync(string id, CancellationToken cancellationToken = default)
{
- logger?.LogInformation($"Deleting virtual device '{name}'...");
+ logger?.LogInformation($"Deleting virtual device '{id}'...");
- var args = $"delete avd --name \"{name}\"";
+ var args = $"delete avd --name \"{id}\"";
try
{
@@ -133,7 +170,7 @@ namespace DotNetDevices.Android
bool WasExisting(ProcessResult result)
{
- var expected = $"Error: There is no Android Virtual Device named '{name}'.";
+ var expected = $"Error: There is no Android Virtual Device with ID '{id}'.";
foreach (var output in result.GetErrorOutput())
{
@@ -145,11 +182,11 @@ namespace DotNetDevices.Android
}
}
- public async Task CreateVirtualDeviceAsync(string name, string package, CreateVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
+ public async Task CreateVirtualDeviceAsync(string id, string package, CreateVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
{
- logger?.LogInformation($"Creating virtual device '{name}'...");
+ logger?.LogInformation($"Creating virtual device '{id}'...");
- var args = $"create avd --name \"{name}\" --package \"{package}\"";
+ var args = $"create avd --name \"{id}\" --package \"{package}\"";
if (options?.Overwrite == true)
args += " --force";
@@ -164,7 +201,7 @@ namespace DotNetDevices.Android
bool WasExisting(ProcessResult result)
{
- var expected = $"Error: Android Virtual Device '{name}' already exists.";
+ var expected = $"Error: Android Virtual Device '{id}' already exists.";
foreach (var output in result.GetErrorOutput())
{
@@ -176,20 +213,20 @@ namespace DotNetDevices.Android
}
}
- public async Task RenameVirtualDeviceAsync(string name, string newName, CancellationToken cancellationToken = default)
+ public async Task RenameVirtualDeviceAsync(string id, string newId, CancellationToken cancellationToken = default)
{
- logger?.LogInformation($"Renaming virtual device '{name}'...");
+ logger?.LogInformation($"Renaming virtual device '{id}'...");
- var args = $"move avd --name \"{name}\" --rename \"{newName}\"";
+ var args = $"move avd --name \"{id}\" --rename \"{newId}\"";
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
}
- public async Task MoveVirtualDeviceAsync(string name, string newPath, CancellationToken cancellationToken = default)
+ public async Task MoveVirtualDeviceAsync(string id, string newPath, CancellationToken cancellationToken = default)
{
- logger?.LogInformation($"Moving virtual device '{name}'...");
+ logger?.LogInformation($"Moving virtual device '{id}'...");
- var args = $"move avd --name \"{name}\" --path \"{newPath}\"";
+ var args = $"move avd --name \"{id}\" --path \"{newPath}\"";
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
}
diff --git a/dotnet-devices/Android/AaptTool.cs b/dotnet-devices/Android/Aapt.cs
similarity index 98%
rename from dotnet-devices/Android/AaptTool.cs
rename to dotnet-devices/Android/Aapt.cs
index cc866fd..25a0c69 100644
--- a/dotnet-devices/Android/AaptTool.cs
+++ b/dotnet-devices/Android/Aapt.cs
@@ -9,7 +9,7 @@ using System.Xml.Linq;
namespace DotNetDevices.Android
{
- public class AaptTool
+ public class Aapt
{
private readonly static Regex xmltreeNamespaceRegex = new Regex(@"^N:\s*(?[^=]+)=(?.*)$");
private readonly static Regex xmltreeElementRegex = new Regex(@"^E:\s*((?[^:]+):)?(?.*) \(line=\d+\)$");
@@ -19,13 +19,13 @@ namespace DotNetDevices.Android
private readonly ILogger? logger;
private readonly string aapt;
- public AaptTool(string? sdkRoot = null, ILogger? logger = null)
+ public Aapt(string? sdkRoot = null, ILogger? logger = null)
{
- processRunner = new ProcessRunner(logger);
this.logger = logger;
-
aapt = AndroidSDK.FindBuildToolPath(sdkRoot, "aapt", logger)
?? throw new ArgumentException($"Unable to locate aapt. Make sure that ANDROID_HOME or ANDROID_SDK_ROOT is set.");
+
+ processRunner = new ProcessRunner(logger);
}
public async Task GetAndroidManifestAsync(string apk, CancellationToken cancellationToken = default)
diff --git a/dotnet-devices/Android/Adb.cs b/dotnet-devices/Android/Adb.cs
new file mode 100644
index 0000000..013ec5d
--- /dev/null
+++ b/dotnet-devices/Android/Adb.cs
@@ -0,0 +1,380 @@
+using DotNetDevices.Processes;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DotNetDevices.Android
+{
+ public class Adb
+ {
+ private readonly ProcessRunner processRunner;
+ private readonly ILogger? logger;
+ private readonly string adb;
+
+ public Adb(string? sdkRoot = null, ILogger? logger = null)
+ {
+ this.logger = logger;
+ adb = AndroidSDK.FindPath(sdkRoot, "platform-tools/adb", logger)
+ ?? throw new ArgumentException($"Unable to locate adb. Make sure that ANDROID_HOME or ANDROID_SDK_ROOT is set.");
+
+ processRunner = new ProcessRunner(logger);
+ }
+
+ public async Task GetVirtualDeviceWithIdAsync(string avdId, CancellationToken cancellationToken = default)
+ {
+ var devices = await GetDevicesAsync(cancellationToken).ConfigureAwait(false);
+ foreach (var device in devices)
+ {
+ var deviceAvdId = await GetVirtualDeviceIdAsync(device.Serial, cancellationToken).ConfigureAwait(false);
+ if (deviceAvdId?.Equals(avdId, StringComparison.OrdinalIgnoreCase) == true)
+ return device;
+ }
+
+ return null;
+ }
+
+ public async IAsyncEnumerable GetVirtualDevicesWithIdAsync(string avdId, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ if (avdId == null)
+ throw new ArgumentNullException(nameof(avdId));
+
+ var devices = await GetDevicesAsync(cancellationToken).ConfigureAwait(false);
+ foreach (var device in devices)
+ {
+ var deviceAvdId = await GetVirtualDeviceIdAsync(device.Serial, cancellationToken).ConfigureAwait(false);
+ if (deviceAvdId?.Equals(avdId, StringComparison.OrdinalIgnoreCase) == true)
+ yield return device;
+ }
+ }
+
+ public async Task> GetDevicesAsync(CancellationToken cancellationToken = default)
+ {
+ logger?.LogInformation("Searching for conected devices...");
+
+ return await GetDevicesNoLoggingAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task GetDeviceAsync(string serial, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+
+ logger?.LogInformation($"Searching for conected device '{serial}'...");
+
+ var devices = await GetDevicesNoLoggingAsync(cancellationToken).ConfigureAwait(false);
+
+ return devices.FirstOrDefault(d => d.Serial.Equals(serial, StringComparison.OrdinalIgnoreCase));
+ }
+
+ public async Task GetVirtualDeviceIdAsync(string serial, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+
+ logger?.LogInformation($"Reading virtual device ID for '{serial}'...");
+
+ await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false);
+
+ var args = $"-s \"{serial}\" emu avd name";
+ var result = await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false);
+
+ var nonEmptyLines = result.GetOutput()
+ .Where(l => !string.IsNullOrWhiteSpace(l))
+ .ToArray();
+ if (nonEmptyLines.Length == 0)
+ return null;
+
+ if (nonEmptyLines.Length < 2 || !nonEmptyLines[1].Trim().Equals("OK", StringComparison.OrdinalIgnoreCase))
+ throw new Exception($"Unable to read virtual device ID.");
+
+ return nonEmptyLines[0].Trim();
+ }
+
+ public async Task LogcatAsync(string serial, LogcatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+
+ logger?.LogInformation($"Starting logcat for '{serial}'...");
+
+ await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ var dump = options?.DumpOnly == true
+ ? "-d"
+ : string.Empty;
+
+ var args = $"-s \"{serial}\" logcat {dump}";
+ return await processRunner.RunAsync(adb, args, Wrap(options?.HandleOutput), cancellationToken).ConfigureAwait(false);
+ }
+ catch (ProcessResultException ex) when (ex.InnerException is OperationCanceledException && ex.ProcessResult != null)
+ {
+ return ex.ProcessResult;
+ }
+
+ static Func? Wrap(Action? handle)
+ {
+ if (handle == null)
+ return null;
+
+ return o =>
+ {
+ handle(o);
+ return true;
+ };
+ }
+ }
+
+ public async Task ClearLogcatAsync(string serial, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+
+ logger?.LogInformation($"Clearing logcat for '{serial}'...");
+
+ await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false);
+
+ var args = $"-s \"{serial}\" logcat --clear";
+ await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task LaunchActivityAsync(string serial, string activity, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+ if (activity == null)
+ throw new ArgumentNullException(nameof(activity));
+
+ logger?.LogInformation($"Launching activity '{activity}' on device '{serial}'...");
+
+ await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false);
+
+ var args = $"-s \"{serial}\" shell am start -n \"{activity}\"";
+ await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task PullFileAsync(string serial, string packageName, string sourceFileName, string destFileName, bool overwrite, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+ if (packageName == null)
+ throw new ArgumentNullException(nameof(packageName));
+ if (sourceFileName == null)
+ throw new ArgumentNullException(nameof(sourceFileName));
+ if (destFileName == null)
+ throw new ArgumentNullException(nameof(destFileName));
+
+ logger?.LogInformation($"Pulling file '{sourceFileName}' on device '{serial}' to '{destFileName}'...");
+
+ if (File.Exists(destFileName))
+ {
+ if (!overwrite)
+ throw new IOException($"File {destFileName} already exists.");
+ File.Delete(destFileName);
+ }
+
+ var guid = Guid.NewGuid().ToString();
+
+ var command = $"cp \"{sourceFileName}\" \"/sdcard/Download/{guid}\"";
+ await RunCommandAsAppAsync(serial, packageName, command, cancellationToken).ConfigureAwait(false);
+
+ var args = $"-s \"{serial}\" pull \"/sdcard/Download/{guid}\" \"{destFileName}\"";
+ await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false);
+
+ command = $"rm \"/sdcard/Download/{guid}\"";
+ await RunCommandAsAppAsync(serial, packageName, command, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task InstallAppAsync(string serial, string appPath, InstallAppOptions? options = default, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+ if (appPath == null)
+ throw new ArgumentNullException(nameof(appPath));
+ if (!File.Exists(appPath))
+ throw new FileNotFoundException($"Unable to find the app '{appPath}'.", appPath);
+
+ logger?.LogInformation($"Installing '{appPath}' on virtual device '{serial}'...");
+
+ if (options?.SkipSharedRuntimeValidation != true && HasSharedRuntime(appPath))
+ throw new Exception("Installing apps that rely on the Mono Shared Runtime is not supported. Change the project configuration or use a Release build.");
+
+ await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false);
+
+ var args = $"-s \"{serial}\" install \"{appPath}\"";
+ await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task UninstallAppAsync(string serial, string packageName, CancellationToken cancellationToken)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+ if (packageName == null)
+ throw new ArgumentNullException(nameof(packageName));
+
+ logger?.LogInformation($"Uninstalling '{packageName}' on virtual device '{serial}'...");
+
+ await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false);
+
+ var args = $"-s \"{serial}\" uninstall \"{packageName}\"";
+ await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task ShutdownVirtualDeviceAsync(string serial, CancellationToken cancellationToken = default)
+ {
+ logger?.LogInformation($"Shutting down virtual device with serial '{serial}'...");
+
+ await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false);
+
+ var args = $"-s \"{serial}\" emu kill";
+ await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false);
+
+ await EnsureShutdownAsync(serial, cancellationToken);
+ }
+
+ public async Task GetDataDirectoryAsync(string serial, string packageName, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+ if (packageName == null)
+ throw new ArgumentNullException(nameof(packageName));
+
+ logger?.LogInformation($"Retrieving data path for app '{packageName}' on device '{serial}'...");
+
+ var command = $"pwd";
+ var result = await RunCommandAsAppAsync(serial, packageName, command, cancellationToken).ConfigureAwait(false);
+ var packageDataRoot = result.Output.Trim();
+
+ return $"{packageDataRoot}/files";
+ }
+
+ public async Task PathExistsAsync(string serial, string packageName, string path, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+ if (packageName == null)
+ throw new ArgumentNullException(nameof(packageName));
+ if (path == null)
+ throw new ArgumentNullException(nameof(path));
+
+ logger?.LogInformation($"Check for '{path}' for app '{packageName}' on device '{serial}'...");
+
+ try
+ {
+ var command = $"ls \"{path}\"";
+ var result = await RunCommandAsAppAsync(serial, packageName, command, cancellationToken).ConfigureAwait(false);
+
+ return true;
+ }
+ catch (ProcessResultException ex) when (ex.ProcessResult.OutputCount == 1 && ex.ProcessResult.Output.Contains("No such file or directory"))
+ {
+ return false;
+ }
+ }
+
+ public async Task RunCommandAsAppAsync(string serial, string packageName, string command, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+ if (packageName == null)
+ throw new ArgumentNullException(nameof(packageName));
+ if (command == null)
+ throw new ArgumentNullException(nameof(command));
+
+ logger?.LogInformation($"Running command '{command}' as app '{packageName}' on device '{serial}'...");
+
+ command = $"run-as \"{packageName}\" {command}";
+ return await RunCommandAsync(serial, command, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task RunCommandAsync(string serial, string command, CancellationToken cancellationToken = default)
+ {
+ if (serial == null)
+ throw new ArgumentNullException(nameof(serial));
+ if (command == null)
+ throw new ArgumentNullException(nameof(command));
+
+ logger?.LogInformation($"Running command '{command}' on device '{serial}'...");
+
+ await EnsureDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false);
+
+ var args = $"-s \"{serial}\" shell {command}";
+ return await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false);
+ }
+
+ private bool HasSharedRuntime(string appPath)
+ {
+ using var archive = ZipFile.OpenRead(appPath);
+
+ var entries = archive.Entries;
+
+ var hasMonodroid = entries.Any(x => x.Name.EndsWith("libmonodroid.so"));
+ var hasRuntime = entries.Any(x => x.Name.EndsWith("mscorlib.dll"));
+ var hasEnterpriseBundle = entries.Any(x => x.Name.EndsWith("libmonodroid_bundle_app.so"));
+
+ return hasMonodroid && !hasRuntime && !hasEnterpriseBundle;
+ }
+
+ private async Task EnsureShutdownAsync(string serial, CancellationToken cancellationToken)
+ {
+ while (await IsDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task EnsureDeviceVisibleAsync(string serial, CancellationToken cancellationToken)
+ {
+ var foundDevice = await IsDeviceVisibleAsync(serial, cancellationToken).ConfigureAwait(false);
+ if (!foundDevice)
+ throw new Exception($"Unable to find virtual device '{serial}'.");
+ }
+
+ private async Task IsDeviceVisibleAsync(string serial, CancellationToken cancellationToken)
+ {
+ var devices = await GetDevicesNoLoggingAsync(cancellationToken).ConfigureAwait(false);
+ return devices.Any(d => d.Serial.Equals(serial, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private async Task> GetDevicesNoLoggingAsync(CancellationToken cancellationToken)
+ {
+ logger?.LogDebug("Searching for conected devices...");
+
+ var args = $"devices";
+
+ var result = await processRunner.RunAsync(adb, args, null, cancellationToken).ConfigureAwait(false);
+
+ var devices = new List();
+
+ foreach (var line in result.GetOutput())
+ {
+ if (!line.Contains('\t'))
+ continue;
+
+ var parts = line.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length < 2)
+ continue;
+
+ var serial = parts[0].Trim();
+ var state = parts[1].Trim().ToLowerInvariant() switch
+ {
+ "device" => ConnectedDeviceState.Connected,
+ "offline" => ConnectedDeviceState.Disconnected,
+ "no device" => ConnectedDeviceState.Unknown,
+ _ => ConnectedDeviceState.Unknown
+ };
+ devices.Add(new ConnectedDevice(serial, state));
+ }
+
+ return devices;
+ }
+ }
+}
diff --git a/dotnet-devices/Android/AndroidManifest.cs b/dotnet-devices/Android/AndroidManifest.cs
index 2c8d91a..56a655c 100644
--- a/dotnet-devices/Android/AndroidManifest.cs
+++ b/dotnet-devices/Android/AndroidManifest.cs
@@ -1,10 +1,13 @@
using System;
+using System.Linq;
using System.Xml.Linq;
namespace DotNetDevices.Android
{
public class AndroidManifest
{
+ private static readonly XNamespace xmlnsAndroid = "http://schemas.android.com/apk/res/android";
+
public AndroidManifest(XDocument xdoc)
{
Document = xdoc ?? throw new ArgumentNullException(nameof(xdoc));
@@ -12,6 +15,20 @@ namespace DotNetDevices.Android
public XDocument Document { get; }
- public string? PackageName => Document.Root?.Attribute("package")?.Value;
+ public string? PackageName =>
+ Document.Root
+ ?.Attribute("package")?.Value;
+
+ public string? MainLauncherActivity =>
+ Document.Root
+ ?.Element("application")
+ ?.Elements("activity")
+ ?.FirstOrDefault(a =>
+ a?.Element("intent-filter")
+ ?.Element("action")?.Attribute(xmlnsAndroid + "name")?.Value == "android.intent.action.MAIN" &&
+ a?.Element("intent-filter")
+ ?.Element("category")?.Attribute(xmlnsAndroid + "name")?.Value == "android.intent.category.LAUNCHER")
+ ?.Attribute(xmlnsAndroid + "name")
+ ?.Value;
}
}
diff --git a/dotnet-devices/Android/ConnectedDevice.cs b/dotnet-devices/Android/ConnectedDevice.cs
new file mode 100644
index 0000000..2e94a4d
--- /dev/null
+++ b/dotnet-devices/Android/ConnectedDevice.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace DotNetDevices.Android
+{
+ public class ConnectedDevice
+ {
+ private static readonly Regex emulatorPortRegex = new Regex(@"emulator-(\d+)");
+
+ public ConnectedDevice(string serial, ConnectedDeviceState state)
+ {
+ Serial = serial ?? throw new ArgumentNullException(nameof(serial));
+ State = state;
+
+ var match = emulatorPortRegex.Match(Serial);
+ if (match.Success && int.TryParse(match.Groups[1].Value, out var newPort))
+ Port = newPort;
+ else
+ Port = -1;
+ }
+
+ public string Serial { get; }
+
+ public ConnectedDeviceState State { get; }
+
+ public int Port { get; }
+ }
+}
diff --git a/dotnet-devices/Android/ConnectedDeviceState.cs b/dotnet-devices/Android/ConnectedDeviceState.cs
new file mode 100644
index 0000000..dc9499a
--- /dev/null
+++ b/dotnet-devices/Android/ConnectedDeviceState.cs
@@ -0,0 +1,9 @@
+namespace DotNetDevices.Android
+{
+ public enum ConnectedDeviceState
+ {
+ Unknown,
+ Disconnected,
+ Connected,
+ }
+}
diff --git a/dotnet-devices/Android/EmulatorManager.cs b/dotnet-devices/Android/EmulatorManager.cs
index a95f422..5b1d20b 100644
--- a/dotnet-devices/Android/EmulatorManager.cs
+++ b/dotnet-devices/Android/EmulatorManager.cs
@@ -1,11 +1,11 @@
-using System;
+using DotNetDevices.Processes;
+using Microsoft.Extensions.Logging;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
-using DotNetDevices.Processes;
-using Microsoft.Extensions.Logging;
namespace DotNetDevices.Android
{
@@ -27,14 +27,14 @@ namespace DotNetDevices.Android
?? throw new ArgumentException($"Unable to locate the Android Emulator. Make sure that ANDROID_HOME or ANDROID_SDK_ROOT is set.");
}
- public async Task BootVirtualDeviceAsync(string name, BootVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
+ public async Task BootVirtualDeviceAsync(string avdId, BootVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
{
- if (name == null)
- throw new ArgumentNullException(nameof(name));
+ if (avdId == null)
+ throw new ArgumentNullException(nameof(avdId));
- logger?.LogInformation($"Booting virtual device '{name}'...");
+ logger?.LogInformation($"Booting virtual device '{avdId}'...");
- var args = $"-avd {name} -verbose";
+ var args = $"-avd {avdId} -verbose";
if (options?.NoWindow == true)
args += " -no-boot-anim -no-window";
if (options?.NoSnapshots == true)
@@ -77,7 +77,7 @@ namespace DotNetDevices.Android
return true;
}
- bool IsAlreadyLaunched(ProcessResultException ex)
+ static bool IsAlreadyLaunched(ProcessResultException ex)
{
foreach (var output in ex.ProcessResult.GetOutput())
{
diff --git a/dotnet-devices/Android/InstallAppOptions.cs b/dotnet-devices/Android/InstallAppOptions.cs
new file mode 100644
index 0000000..2ce50a7
--- /dev/null
+++ b/dotnet-devices/Android/InstallAppOptions.cs
@@ -0,0 +1,7 @@
+namespace DotNetDevices.Android
+{
+ public class InstallAppOptions
+ {
+ public bool SkipSharedRuntimeValidation { get; set; } = false;
+ }
+}
diff --git a/dotnet-devices/Android/LogcatOptions.cs b/dotnet-devices/Android/LogcatOptions.cs
new file mode 100644
index 0000000..7206142
--- /dev/null
+++ b/dotnet-devices/Android/LogcatOptions.cs
@@ -0,0 +1,12 @@
+using DotNetDevices.Processes;
+using System;
+
+namespace DotNetDevices.Android
+{
+ public class LogcatOptions
+ {
+ public bool DumpOnly { get; set; }
+
+ public Action? HandleOutput { get; set; }
+ }
+}
diff --git a/dotnet-devices/Android/VirtualDevice.cs b/dotnet-devices/Android/VirtualDevice.cs
index e6be798..02f6ca8 100644
--- a/dotnet-devices/Android/VirtualDevice.cs
+++ b/dotnet-devices/Android/VirtualDevice.cs
@@ -4,14 +4,14 @@ namespace DotNetDevices.Android
{
public class VirtualDevice
{
- public VirtualDevice(string id, string name, string package, VirtualDeviceType type, int apiLevel, string? configPath)
+ public VirtualDevice(string id, string name, string package, VirtualDeviceType type, int apiLevel, string? avdPath)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
Name = name ?? throw new ArgumentNullException(nameof(name));
Package = package ?? throw new ArgumentNullException(nameof(package));
Type = type;
ApiLevel = apiLevel;
- ConfigPath = configPath;
+ AvdPath = avdPath;
}
public string Id { get; }
@@ -24,7 +24,7 @@ namespace DotNetDevices.Android
public int ApiLevel { get; }
- public string? ConfigPath { get; }
+ public string? AvdPath { get; }
public Version Version =>
ApiLevel switch
diff --git a/dotnet-devices/Android/VirtualDeviceConfig.cs b/dotnet-devices/Android/VirtualDeviceConfig.cs
index ae6dc75..c6f334a 100644
--- a/dotnet-devices/Android/VirtualDeviceConfig.cs
+++ b/dotnet-devices/Android/VirtualDeviceConfig.cs
@@ -14,13 +14,16 @@ namespace DotNetDevices.Android
private readonly string configPath;
private readonly ILogger? logger;
+ private readonly string? avdPath;
public Dictionary? properties;
- public VirtualDeviceConfig(string configPath, ILogger? logger = null)
+ public VirtualDeviceConfig(string avdPath, ILogger? logger = null)
{
- this.configPath = configPath ?? throw new ArgumentNullException(nameof(configPath));
+ this.avdPath = avdPath ?? throw new ArgumentNullException(nameof(avdPath));
this.logger = logger;
+
+ configPath = Path.Combine(avdPath, "config.ini");
}
public async Task> GetPropertiesAsync(CancellationToken cancellationToken = default)
@@ -50,10 +53,7 @@ namespace DotNetDevices.Android
var props = await GetPropertiesAsync(cancellationToken).ConfigureAwait(false);
if (!props.TryGetValue("avdid", out var id))
- {
- var avdDir = Path.GetDirectoryName(configPath);
- id = Path.GetFileNameWithoutExtension(avdDir);
- }
+ id = Path.GetFileNameWithoutExtension(avdPath);
if (string.IsNullOrEmpty(id))
throw new Exception($"Invalid config.ini. Unable to find the virtual device ID.");
@@ -77,7 +77,7 @@ namespace DotNetDevices.Android
if (!TryGetType(props, out var type))
type = VirtualDeviceType.Unknown;
- return new VirtualDevice(id, name, package, type, apiLevel, configPath);
+ return new VirtualDevice(id, name, package, type, apiLevel, avdPath);
}
private static bool TryGetType(IReadOnlyDictionary props, out VirtualDeviceType value)
diff --git a/dotnet-devices/Apple/LaunchAppOptions.cs b/dotnet-devices/Apple/LaunchAppOptions.cs
index 03467d1..155c2aa 100644
--- a/dotnet-devices/Apple/LaunchAppOptions.cs
+++ b/dotnet-devices/Apple/LaunchAppOptions.cs
@@ -7,8 +7,6 @@ namespace DotNetDevices.Apple
{
public bool CaptureOutput { get; set; } = false;
- public bool BootSimulator { get; set; } = false;
-
public Action? HandleOutput { get; set; }
}
}
diff --git a/dotnet-devices/Apple/SimulatorControl.cs b/dotnet-devices/Apple/SimulatorControl.cs
index 08aead7..d3b31bf 100644
--- a/dotnet-devices/Apple/SimulatorControl.cs
+++ b/dotnet-devices/Apple/SimulatorControl.cs
@@ -1,13 +1,13 @@
-using System;
+using DotNetDevices.Processes;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
-using DotNetDevices.Processes;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
namespace DotNetDevices.Apple
{
@@ -183,7 +183,7 @@ namespace DotNetDevices.Apple
await EnsureShutdownAsync(udid, cancellationToken).ConfigureAwait(false);
}
- public async Task EraseSimulatorAsync(string udid, bool shutdown = false, CancellationToken cancellationToken = default)
+ public async Task EraseSimulatorAsync(string udid, CancellationToken cancellationToken = default)
{
if (udid == null)
throw new ArgumentNullException(nameof(udid));
@@ -194,14 +194,11 @@ namespace DotNetDevices.Apple
if (sim == null)
throw new Exception($"Unable to find simulator {udid}.");
- if (shutdown && sim.State != SimulatorState.Shutdown)
- await ShutdownSimulatorAsync(udid, cancellationToken).ConfigureAwait(false);
-
var args = $"simctl erase \"{udid}\"";
await processRunner.RunAsync(xcrun, args, null, cancellationToken).ConfigureAwait(false);
}
- public async Task InstallAppAsync(string udid, string appPath, bool boot = false, CancellationToken cancellationToken = default)
+ public async Task InstallAppAsync(string udid, string appPath, CancellationToken cancellationToken = default)
{
if (udid == null)
throw new ArgumentNullException(nameof(udid));
@@ -216,9 +213,6 @@ namespace DotNetDevices.Apple
if (sim == null)
throw new Exception($"Unable to find simulator {udid}.");
- if (boot && sim.State != SimulatorState.Booted)
- await BootSimulatorAsync(udid, cancellationToken).ConfigureAwait(false);
-
var args = $"simctl install \"{udid}\" \"{appPath}\"";
await processRunner.RunAsync(xcrun, args, null, cancellationToken).ConfigureAwait(false);
}
@@ -236,9 +230,6 @@ namespace DotNetDevices.Apple
if (sim == null)
throw new Exception($"Unable to find simulator {udid}.");
- if (options?.BootSimulator == true && sim.State != SimulatorState.Booted)
- await BootSimulatorAsync(udid, cancellationToken).ConfigureAwait(false);
-
var console = options?.CaptureOutput == true
? "--console"
: string.Empty;
@@ -289,7 +280,7 @@ namespace DotNetDevices.Apple
await processRunner.RunAsync(xcrun, args, null, cancellationToken).ConfigureAwait(false);
}
- public async Task UninstallAppAsync(string udid, string appBundleId, bool boot = false, CancellationToken cancellationToken = default)
+ public async Task UninstallAppAsync(string udid, string appBundleId, CancellationToken cancellationToken = default)
{
if (udid == null)
throw new ArgumentNullException(nameof(udid));
@@ -302,14 +293,8 @@ namespace DotNetDevices.Apple
if (sim == null)
throw new Exception($"Unable to find simulator {udid}.");
- if (boot && sim.State != SimulatorState.Booted)
- await BootSimulatorAsync(udid, cancellationToken).ConfigureAwait(false);
-
var args = $"simctl uninstall \"{udid}\" \"{appBundleId}\"";
await processRunner.RunAsync(xcrun, args, null, cancellationToken).ConfigureAwait(false);
-
- if (boot && sim.State != SimulatorState.Booted)
- await ShutdownSimulatorAsync(udid, cancellationToken).ConfigureAwait(false);
}
private async Task EnsureBootedAsync(string udid, CancellationToken cancellationToken)
@@ -317,7 +302,7 @@ namespace DotNetDevices.Apple
while ((await GetSimulatorNoLoggingAsync(udid, cancellationToken).ConfigureAwait(false))!.State != SimulatorState.Booted)
{
cancellationToken.ThrowIfCancellationRequested();
- await Task.Delay(1000).ConfigureAwait(false);
+ await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
}
}
@@ -326,7 +311,7 @@ namespace DotNetDevices.Apple
while ((await GetSimulatorNoLoggingAsync(udid, cancellationToken).ConfigureAwait(false))!.State != SimulatorState.Shutdown)
{
cancellationToken.ThrowIfCancellationRequested();
- await Task.Delay(1000).ConfigureAwait(false);
+ await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/dotnet-devices/Commands/AndroidCommand.cs b/dotnet-devices/Commands/AndroidCommand.cs
index 62832e9..7705685 100644
--- a/dotnet-devices/Commands/AndroidCommand.cs
+++ b/dotnet-devices/Commands/AndroidCommand.cs
@@ -133,7 +133,7 @@ namespace DotNetDevices.Commands
if (!replace)
{
- var devices = await avdmanager.GetVirtualDeviceNamesAsync(cancellationToken);
+ var devices = await avdmanager.GetVirtualDeviceIdsAsync(cancellationToken);
if (devices.Any(d => d.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
logger.LogInformation($"Virtual device already exists.");
@@ -160,7 +160,7 @@ namespace DotNetDevices.Commands
var avdmanager = new AVDManager(sdk, logger);
- var devices = await avdmanager.GetVirtualDeviceNamesAsync(cancellationToken);
+ var devices = await avdmanager.GetVirtualDeviceIdsAsync(cancellationToken);
if (devices.All(d => !d.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
logger.LogInformation($"Virtual device does not exist.");
diff --git a/dotnet-devices/Commands/AndroidTestCommand.cs b/dotnet-devices/Commands/AndroidTestCommand.cs
index 26e5c60..6a01b46 100644
--- a/dotnet-devices/Commands/AndroidTestCommand.cs
+++ b/dotnet-devices/Commands/AndroidTestCommand.cs
@@ -1,7 +1,9 @@
using DotNetDevices.Android;
+using DotNetDevices.Testing;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -14,7 +16,8 @@ namespace DotNetDevices.Commands
private readonly ILogger logger;
private readonly AVDManager avdmanager;
private readonly EmulatorManager emulator;
- private readonly AaptTool aapt;
+ private readonly Aapt aapt;
+ private readonly Adb adb;
public AndroidTestCommand(string? sdkRoot, ILogger logger)
{
@@ -23,7 +26,8 @@ namespace DotNetDevices.Commands
avdmanager = new AVDManager(sdkRoot, logger);
emulator = new EmulatorManager(sdkRoot, logger);
- aapt = new AaptTool(sdkRoot, logger);
+ aapt = new Aapt(sdkRoot, logger);
+ adb = new Adb(sdkRoot, logger);
}
public async Task RunTestsAsync(
@@ -44,8 +48,12 @@ namespace DotNetDevices.Commands
var packageName = androidManifest.PackageName;
if (string.IsNullOrEmpty(packageName))
throw new Exception("Unable to determine the package name for the app.");
+ var activityName = androidManifest.MainLauncherActivity;
+ if (string.IsNullOrEmpty(activityName))
+ throw new Exception("Unable to determine the main launcher activity name for the app.");
logger.LogInformation($"Running tests on '{packageName}'...");
+ logger.LogInformation($"Detected main launcher activity '{activityName}'.");
// validate requested OS
var avdRuntime = ParseDeviceRuntime(runtimeString);
@@ -61,74 +69,93 @@ namespace DotNetDevices.Commands
var avd = available.FirstOrDefault();
logger.LogInformation($"Using virtual device {avd.Name} ({avd.Runtime} {avd.Version}): {avd.Id}");
+ string? serial = null;
try
{
- // if (reset)
- // await simctl.EraseSimulatorAsync(simulator.Udid, true, cancellationToken);
+ if (reset)
+ {
+ var bootedDevice = await adb.GetVirtualDeviceWithIdAsync(avd.Id, cancellationToken);
+ if (bootedDevice != null)
+ await adb.ShutdownVirtualDeviceAsync(bootedDevice.Serial);
- // await simctl.InstallAppAsync(simulator.Udid, app, true, cancellationToken);
+ await avdmanager.ResetVirtualDeviceAsync(avd.Id, cancellationToken);
+ }
+
+ var bootOptions = new BootVirtualDeviceOptions
+ {
+ WipeData = reset,
+ };
+ var port = await emulator.BootVirtualDeviceAsync(avd.Id, bootOptions, cancellationToken);
+ if (port == -1)
+ {
+ // device was already booted, so find it
+ var bootedDevice = await adb.GetVirtualDeviceWithIdAsync(avd.Id, cancellationToken);
+ if (bootedDevice == null)
+ throw new Exception($"Virtual device '{avd.Name}' was already booted, but was not able to be found.");
+
+ serial = bootedDevice.Serial;
+ logger.LogDebug($"Virutal device was already booted, found serial '{serial}'");
+ }
+ else
+ {
+ serial = $"emulator-{port}";
+ logger.LogDebug($"Virtual device was booted to port {port}, assuming serial '{serial}'");
+ }
+
+ var installOptions = new InstallAppOptions
+ {
+ SkipSharedRuntimeValidation = false,
+ };
+ await adb.InstallAppAsync(serial, app, installOptions, cancellationToken);
try
{
- // var parser = new TestResultsParser();
+ await adb.ClearLogcatAsync(serial, cancellationToken);
- // var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ var parser = new TestResultsParser();
- // var launched = await simctl.LaunchAppAsync(simulator.Udid, bundleId, new LaunchAppOptions
- // {
- // CaptureOutput = true,
- // BootSimulator = true,
- // HandleOutput = output =>
- // {
- // parser.ParseTestOutput(
- // output,
- // line => logger?.LogWarning(line),
- // async () =>
- // {
- // try
- // {
- // // wait a few seconds before terminating
- // await Task.Delay(1000, cts.Token);
+ var logcatTask = adb.LogcatAsync(serial, new LogcatOptions
+ {
+ HandleOutput = output =>
+ {
+ parser.ParseTestOutput(
+ output,
+ line => logger?.LogWarning(line),
+ () => throw new TaskCanceledException());
+ }
+ }, cancellationToken);
- // await simctl.TerminateAppAsync(simulator.Udid, bundleId, cts.Token);
- // }
- // catch (OperationCanceledException)
- // {
- // // we expected this
- // }
- // });
- // },
- // }, cancellationToken);
+ await adb.LaunchActivityAsync(serial, $"{packageName}/{activityName}", cancellationToken);
- // cts.Cancel();
+ await logcatTask;
- // if (deviceResults != null)
- // {
- // var dest = outputResults ?? Path.GetFileName(deviceResults);
+ if (deviceResults != null)
+ {
+ var dest = outputResults ?? Path.GetFileName(deviceResults);
- // logger.LogInformation($"Copying test results from simulator to {dest}...");
+ logger.LogInformation($"Copying test results from virtual device to {dest}...");
- // var dataPath = await simctl.GetDataDirectoryAsync(simulator.Udid, bundleId, cancellationToken);
- // var results = Path.Combine(dataPath, "Documents", deviceResults);
- // if (File.Exists(results))
- // File.Copy(results, dest, true);
- // else
- // logger.LogInformation($"No test results found.");
- // }
- // else
- // {
- // logger.LogInformation($"Unable to determine the test results file.");
- // }
+ var dataPath = await adb.GetDataDirectoryAsync(serial, packageName, cancellationToken);
+ var results = Path.Combine(dataPath, deviceResults).Replace("\\", "/");
+ if (await adb.PathExistsAsync(serial, packageName, results, cancellationToken))
+ await adb.PullFileAsync(serial, packageName, results, dest, true, cancellationToken);
+ else
+ logger.LogInformation($"No test results found.");
+ }
+ else
+ {
+ logger.LogInformation($"Unable to determine the test results file.");
+ }
}
finally
{
- // await simctl.UninstallAppAsync(simulator.Udid, bundleId, false, cancellationToken);
+ await adb.UninstallAppAsync(serial, packageName, cancellationToken);
}
}
finally
{
- // if (shutdown)
- // await simctl.ShutdownSimulatorAsync(simulator.Udid, cancellationToken);
+ if (shutdown && serial != null)
+ await adb.ShutdownVirtualDeviceAsync(serial, cancellationToken);
}
}
diff --git a/dotnet-devices/Commands/AppleTestCommand.cs b/dotnet-devices/Commands/AppleTestCommand.cs
index 95756b3..df6358d 100644
--- a/dotnet-devices/Commands/AppleTestCommand.cs
+++ b/dotnet-devices/Commands/AppleTestCommand.cs
@@ -58,9 +58,14 @@ namespace DotNetDevices.Commands
try
{
if (reset)
- await simctl.EraseSimulatorAsync(simulator.Udid, true, cancellationToken);
+ {
+ await simctl.ShutdownSimulatorAsync(simulator.Udid, cancellationToken);
+ await simctl.EraseSimulatorAsync(simulator.Udid, cancellationToken);
+ }
- await simctl.InstallAppAsync(simulator.Udid, app, true, cancellationToken);
+ await simctl.BootSimulatorAsync(simulator.Udid, cancellationToken);
+
+ await simctl.InstallAppAsync(simulator.Udid, app, cancellationToken);
try
{
@@ -71,7 +76,6 @@ namespace DotNetDevices.Commands
var launched = await simctl.LaunchAppAsync(simulator.Udid, bundleId, new LaunchAppOptions
{
CaptureOutput = true,
- BootSimulator = true,
HandleOutput = output =>
{
parser.ParseTestOutput(
@@ -116,7 +120,7 @@ namespace DotNetDevices.Commands
}
finally
{
- await simctl.UninstallAppAsync(simulator.Udid, bundleId, false, cancellationToken);
+ await simctl.UninstallAppAsync(simulator.Udid, bundleId, cancellationToken);
}
}
finally