This commit is contained in:
Matthew Leibowitz 2020-10-20 16:38:51 +02:00
Родитель c0e5600a11
Коммит de64e0276c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: ECDB25CC0E22FC46
27 изменённых файлов: 670 добавлений и 165 удалений

Просмотреть файл

@ -25,8 +25,10 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AndroidLinkMode>None</AndroidLinkMode>
<AndroidSupportedAbis />
<AndroidSupportedAbis>armeabi-v7a;x86;x86_64;arm64-v8a</AndroidSupportedAbis>
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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();

Просмотреть файл

@ -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<IEnumerable<string>> 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<IEnumerable<string>> 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<string> 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<IEnumerable<VirtualDevice>> 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);
}

Просмотреть файл

@ -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*(?<ns>[^=]+)=(?<url>.*)$");
private readonly static Regex xmltreeElementRegex = new Regex(@"^E:\s*((?<ns>[^:]+):)?(?<name>.*) \(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<AndroidManifest> GetAndroidManifestAsync(string apk, CancellationToken cancellationToken = default)

Просмотреть файл

@ -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<ConnectedDevice?> 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<ConnectedDevice> 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<IEnumerable<ConnectedDevice>> GetDevicesAsync(CancellationToken cancellationToken = default)
{
logger?.LogInformation("Searching for conected devices...");
return await GetDevicesNoLoggingAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<ConnectedDevice?> 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<string?> 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<ProcessResult> 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<ProcessOutput, bool>? Wrap(Action<ProcessOutput>? 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<string> 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<bool> 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<ProcessResult> 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<ProcessResult> 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<bool> 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<IEnumerable<ConnectedDevice>> 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<ConnectedDevice>();
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;
}
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -0,0 +1,9 @@
namespace DotNetDevices.Android
{
public enum ConnectedDeviceState
{
Unknown,
Disconnected,
Connected,
}
}

Просмотреть файл

@ -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<int> BootVirtualDeviceAsync(string name, BootVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
public async Task<int> 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())
{

Просмотреть файл

@ -0,0 +1,7 @@
namespace DotNetDevices.Android
{
public class InstallAppOptions
{
public bool SkipSharedRuntimeValidation { get; set; } = false;
}
}

Просмотреть файл

@ -0,0 +1,12 @@
using DotNetDevices.Processes;
using System;
namespace DotNetDevices.Android
{
public class LogcatOptions
{
public bool DumpOnly { get; set; }
public Action<ProcessOutput>? HandleOutput { get; set; }
}
}

Просмотреть файл

@ -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

Просмотреть файл

@ -14,13 +14,16 @@ namespace DotNetDevices.Android
private readonly string configPath;
private readonly ILogger? logger;
private readonly string? avdPath;
public Dictionary<string, string>? 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<IReadOnlyDictionary<string, string>> 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<string, string> props, out VirtualDeviceType value)

Просмотреть файл

@ -7,8 +7,6 @@ namespace DotNetDevices.Apple
{
public bool CaptureOutput { get; set; } = false;
public bool BootSimulator { get; set; } = false;
public Action<ProcessOutput>? HandleOutput { get; set; }
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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.");

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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