Android now can read apk files

This commit is contained in:
Matthew Leibowitz 2020-10-16 03:25:27 +02:00
Родитель 21cb3c162a
Коммит 405864895b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: ECDB25CC0E22FC46
22 изменённых файлов: 898 добавлений и 123 удалений

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

@ -0,0 +1,54 @@
using DotNetDevices.Android;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Xunit;
namespace DotNetDevices.Tests
{
public class AaptToolTests
{
public class ParseXmlTree
{
private static readonly XNamespace AndroidNamespace = "http://schemas.android.com/apk/res/android";
[Theory]
[InlineData("TestData/Android/CompiledXmlDump.txt")]
public void CanParse(string file)
{
var xmltree = File.ReadAllText(file);
var xdoc = AaptTool.ParseXmlTree(xmltree);
Assert.NotNull(xdoc);
}
[Fact]
public void ParseIsValid()
{
var xmltree = File.ReadAllText("TestData/Android/CompiledXmlDump.txt");
var xdoc = AaptTool.ParseXmlTree(xmltree);
Assert.NotNull(xdoc);
var manifest = xdoc.Root;
Assert.Equal(AndroidNamespace, manifest.GetNamespaceOfPrefix("android"));
Assert.Equal("(type 0x10)0x1", manifest.Attribute(AndroidNamespace + "versionCode").Value);
Assert.Equal("1.0.1.0", manifest.Attribute(AndroidNamespace + "versionName").Value);
Assert.Equal("10", manifest.Attribute(AndroidNamespace + "compileSdkVersionCodename").Value);
Assert.Equal("net.dot.devicetests", manifest.Attribute("package").Value);
Assert.Equal("(type 0x10)0x1d", manifest.Attribute("platformBuildVersionCode").Value);
var usessdk = manifest.Element("uses-sdk");
Assert.Equal("(type 0x10)0x13", usessdk.Attribute(AndroidNamespace + "minSdkVersion").Value);
var usespermissions = manifest.Elements("uses-permission").ToList();
Assert.Equal(2, usespermissions.Count);
Assert.Equal("android.permission.INTERNET", usespermissions[0].Attribute(AndroidNamespace + "name").Value);
var application = manifest.Element("application");
Assert.Equal("@0x7f0c001b", application.Attribute(AndroidNamespace + "label").Value);
Assert.Equal("android.app.Application", application.Attribute(AndroidNamespace + "name").Value);
}
}
}
}

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

@ -0,0 +1,26 @@
using DotNetDevices.Android;
using System.Threading.Tasks;
using Xunit;
namespace DotNetDevices.Tests
{
public class VirtualDeviceConfigTests
{
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")]
public async Task CanCreateInstance(string file, string id, string name)
{
var config = new VirtualDeviceConfig(file);
var device = await config.CreateVirtualDeviceAsync();
Assert.NotNull(device);
Assert.Equal(id, device.Id);
Assert.Equal(name, device.Name);
}
}
}
}

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

@ -0,0 +1,41 @@
AvdId=pixel_2_q_10_0_-_api_29
PlayStore.enabled=true
abi.type=x86
avd.ini.displayname=Pixel 2 Q 10.0 - API 29
avd.ini.encoding=UTF-8
disk.dataPartition.size=6442450944
fastboot.forceColdBoot=no
fastboot.forceFastBoot=yes
hw.accelerometer=yes
hw.arc=no
hw.audioInput=yes
hw.battery=yes
hw.camera.back=virtualscene
hw.camera.front=emulated
hw.cpu.arch=x86
hw.cpu.ncore=4
hw.dPad=no
hw.device.hash2=MD5:55acbc835978f326788ed66a5cd4c9a7
hw.device.manufacturer=Google
hw.device.name=pixel_2
hw.gps=yes
hw.gpu.enabled=yes
hw.gpu.mode=auto
hw.keyboard=yes
hw.lcd.density=420
hw.lcd.height=1920
hw.lcd.width=1080
hw.mainKeys=no
hw.ramSize=1536
hw.sdCard=yes
hw.sensors.orientation=yes
hw.sensors.proximity=yes
hw.trackBall=no
image.sysdir.1=system-images\android-29\google_apis_playstore\x86\
sdcard.size=512M
showDeviceFrame=yes
skin.name=pixel_2
skin.path=C:\Android\android-sdk\skins\pixel_2
tag.display=Google Play
tag.id=google_apis_playstore
vm.heapSize=256

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

@ -0,0 +1 @@
AvdId=pixel_2_q_10_0_-_api_29

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

@ -0,0 +1,51 @@
N: android=http://schemas.android.com/apk/res/android
E: manifest (line=2)
A: android:versionCode(0x0101021b)=(type 0x10)0x1
A: android:versionName(0x0101021c)="1.0.1.0" (Raw: "1.0.1.0")
A: android:installLocation(0x010102b7)=(type 0x10)0x0
A: android:compileSdkVersion(0x01010572)=(type 0x10)0x1d
A: android:compileSdkVersionCodename(0x01010573)="10" (Raw: "10")
A: package="net.dot.devicetests" (Raw: "net.dot.devicetests")
A: platformBuildVersionCode=(type 0x10)0x1d
A: platformBuildVersionName=(type 0x10)0xa
E: uses-sdk (line=3)
A: android:minSdkVersion(0x0101020c)=(type 0x10)0x13
A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1d
E: uses-permission (line=4)
A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
E: uses-permission (line=5)
A: android:name(0x01010003)="android.permission.READ_EXTERNAL_STORAGE" (Raw: "android.permission.READ_EXTERNAL_STORAGE")
E: application (line=6)
A: android:theme(0x01010000)=@0x7f0d00c7
A: android:label(0x01010001)=@0x7f0c001b
A: android:icon(0x01010002)=@0x7f07006f
A: android:name(0x01010003)="android.app.Application" (Raw: "android.app.Application")
A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
A: android:allowBackup(0x01010280)=(type 0x12)0xffffffff
A: android:appComponentFactory(0x0101057a)="androidx.core.app.CoreComponentFactory" (Raw: "androidx.core.app.CoreComponentFactory")
E: activity (line=7)
A: android:name(0x01010003)="crc645c25d208dccba52c.MainActivity" (Raw: "crc645c25d208dccba52c.MainActivity")
A: android:configChanges(0x0101001f)=(type 0x11)0x480
E: intent-filter (line=8)
E: action (line=9)
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
E: category (line=10)
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
E: service (line=13)
A: android:name(0x01010003)="crc64396a3fe5f8138e3f.KeepAliveService" (Raw: "crc64396a3fe5f8138e3f.KeepAliveService")
E: receiver (line=14)
A: android:name(0x01010003)="crc643f46942d9dd1fff9.PowerSaveModeBroadcastReceiver" (Raw: "crc643f46942d9dd1fff9.PowerSaveModeBroadcastReceiver")
A: android:enabled(0x0101000e)=(type 0x12)0xffffffff
A: android:exported(0x01010010)=(type 0x12)0x0
E: provider (line=15)
A: android:name(0x01010003)="mono.MonoRuntimeProvider" (Raw: "mono.MonoRuntimeProvider")
A: android:exported(0x01010010)=(type 0x12)0x0
A: android:authorities(0x01010018)="net.dot.devicetests.mono.MonoRuntimeProvider.__mono_init__" (Raw: "net.dot.devicetests.mono.MonoRuntimeProvider.__mono_init__")
A: android:initOrder(0x0101001a)=(type 0x10)0x773593ff
E: receiver (line=17)
A: android:name(0x01010003)="mono.android.Seppuku" (Raw: "mono.android.Seppuku")
E: intent-filter (line=18)
E: action (line=19)
A: android:name(0x01010003)="mono.android.intent.action.SEPPUKU" (Raw: "mono.android.intent.action.SEPPUKU")
E: category (line=20)
A: android:name(0x01010003)="mono.android.intent.category.SEPPUKU.net.dot.devicetests" (Raw: "mono.android.intent.category.SEPPUKU.net.dot.devicetests")

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

@ -1,14 +0,0 @@
using System;
using Xunit;
namespace DotNetDevices.Tests
{
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}
}

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

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
@ -9,12 +9,22 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\dotnet-devices\dotnet-devices.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="TestData\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

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

@ -1,16 +1,18 @@
using System;
using DotNetDevices.Processes;
using Microsoft.Extensions.Logging;
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;
namespace DotNetDevices.Android
{
public class AVDManager
{
private static Regex virtualDevicePathRegex = new Regex(@"\s*Path\:\s*(.+)");
private readonly ProcessRunner processRunner;
private readonly ILogger? logger;
private readonly string avdmanager;
@ -60,16 +62,30 @@ namespace DotNetDevices.Android
{
logger?.LogInformation("Retrieving all the virtual devices...");
var args = $"list avd -c";
var args = $"list avd";
var result = await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
var avd = new List<VirtualDevice>(result.OutputCount);
var avds = new List<VirtualDevice>();
foreach (var output in GetListResults(result))
{
avd.Add(new VirtualDevice(output));
var pathMatch = virtualDevicePathRegex.Match(output);
if (pathMatch.Success)
{
var path = pathMatch.Groups[1].Value;
var configIniPath = Path.Combine(path, "config.ini");
if (Directory.Exists(path) && File.Exists(configIniPath))
{
var config = new VirtualDeviceConfig(configIniPath, logger);
var avd = await config.CreateVirtualDeviceAsync(cancellationToken).ConfigureAwait(false);
avds.Add(avd);
}
}
}
return avd;
return avds;
}
public async Task DeleteVirtualDeviceAsync(string name, CancellationToken cancellationToken = default)
@ -81,7 +97,7 @@ namespace DotNetDevices.Android
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
}
public async Task CreateVirtualDeviceAsync(string name, string package, VirtualDeviceCreateOptions? options = null, CancellationToken cancellationToken = default)
public async Task CreateVirtualDeviceAsync(string name, string package, CreateVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
{
logger?.LogInformation($"Creating virtual device '{name}'...");
@ -110,7 +126,7 @@ namespace DotNetDevices.Android
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
}
private IEnumerable<string> GetListResults(ProcessResult result)
private static IEnumerable<string> GetListResults(ProcessResult result)
{
foreach (var output in result.GetOutput())
{
@ -135,17 +151,4 @@ namespace DotNetDevices.Android
}
}
}
public class VirtualDeviceCreateOptions
{
public string? Device { get; set; }
public bool Overwrite { get; set; }
public string? Path { get; set; }
public string? SharedSdCardPath { get; set; }
public string? NewSdCardSize { get; set; }
}
}

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

@ -0,0 +1,150 @@
using DotNetDevices.Processes;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace DotNetDevices.Android
{
public class AaptTool
{
private readonly static Regex xmltreeNamespaceRegex = new Regex(@"^N:\s*(?<ns>[^=]+)=(?<url>.*)$");
private readonly static Regex xmltreeElementRegex = new Regex(@"^E:\s*((?<ns>[^:]+):)?(?<name>.*) \(line=\d+\)$");
private readonly static Regex xmltreeAttributeRegex = new Regex(@"^A:\s*((?<ns>[^:]+):)?(?<name>[^(]+)(\(.*\))?=(?<value>.*)$");
private readonly ProcessRunner processRunner;
private readonly ILogger? logger;
private readonly string aapt;
public AaptTool(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.");
}
public async Task<AndroidManifest> GetAndroidManifestAsync(string apk, CancellationToken cancellationToken = default)
{
logger?.LogInformation("Loading AndroidManifest.xml...");
var args = $"dump xmltree \"{apk}\" AndroidManifest.xml";
var result = await processRunner.RunAsync(aapt, args, null, cancellationToken).ConfigureAwait(false);
return new AndroidManifest(ParseXmlTree(result.Output));
}
public static XDocument ParseXmlTree(string xmltree)
{
var lines = xmltree.Split(new[] { "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
var xdoc = new XDocument();
var stack = new Stack<ParsedElement>();
stack.Push(new ParsedElement(xdoc, 0));
var namespaces = new Dictionary<string, XNamespace>();
foreach (var line in lines)
{
ParseXmlTreeLine(line, stack, namespaces);
}
return xdoc;
}
private static void ParseXmlTreeLine(string line, Stack<ParsedElement> stack, Dictionary<string, XNamespace> namespaces)
{
var trimmedLine = line.TrimStart();
var indent = line.Length - trimmedLine.Length;
if (trimmedLine.StartsWith("N"))
{
var match = xmltreeNamespaceRegex.Match(trimmedLine);
if (!match.Success)
throw new Exception($"Invalid namespace: {line}");
var namespaceName = match.Groups["ns"].Value;
if (!namespaces.ContainsKey(namespaceName))
namespaces.Add(namespaceName, XNamespace.Get(match.Groups["url"].Value));
}
else if (trimmedLine.StartsWith("E"))
{
// pop out if the current line is higher than previous
while (stack.Count > 0 && stack.Peek().Indent >= indent)
stack.Pop();
var match = xmltreeElementRegex.Match(trimmedLine);
if (!match.Success)
throw new Exception($"Invalid element: {line}");
var element = new XElement(GetXName(match, namespaces, line));
// this is the first element, so add the namespaces to it
if (stack.Count == 1)
{
foreach (var pair in namespaces)
{
element.Add(new XAttribute(XNamespace.Xmlns + pair.Key, pair.Value));
}
}
stack.Peek().Container.Add(element);
stack.Push(new ParsedElement(element, indent));
}
else if (trimmedLine.StartsWith("A"))
{
var match = xmltreeAttributeRegex.Match(trimmedLine);
if (!match.Success)
throw new Exception($"Invalid attribute: {line}");
// TODO: parse the (type) and use the correct value
var value = match.Groups["value"].Value;
var strMatch = Regex.Match(value, @"\""(?<value>.*)\""\s*\(Raw:.*\)");
var xName = GetXName(match, namespaces, line);
stack.Peek().Container.Add(strMatch.Success
? new XAttribute(xName, strMatch.Groups["value"].Value)
: new XAttribute(xName, value));
}
}
static XName GetXName(Match match, Dictionary<string, XNamespace> namespaces, string line)
{
var namespaceName = match.Groups["ns"].Value;
if (!string.IsNullOrWhiteSpace(namespaceName) && !namespaces.ContainsKey(namespaceName))
throw new Exception($"Unknown xml namespace: {namespaceName}.");
XName xName;
try
{
xName = string.IsNullOrWhiteSpace(namespaceName)
? XName.Get(match.Groups["name"].Value)
: XName.Get(match.Groups["name"].Value, namespaces[namespaceName].ToString());
}
catch
{
throw new Exception($"Invalid attribute: {line}");
}
return xName;
}
class ParsedElement
{
public ParsedElement(XContainer container, int indent)
{
Container = container;
Indent = indent;
}
public XContainer Container { get; }
public int Indent { get; }
}
}
}

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

@ -0,0 +1,17 @@
using System;
using System.Xml.Linq;
namespace DotNetDevices.Android
{
public class AndroidManifest
{
public AndroidManifest(XDocument xdoc)
{
Document = xdoc ?? throw new ArgumentNullException(nameof(xdoc));
}
public XDocument Document { get; }
public string? PackageName => Document.Root?.Attribute("package")?.Value;
}
}

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

@ -63,6 +63,55 @@ namespace DotNetDevices.Android
return null;
}
public static string? FindBuildToolPath(string? sdkRoot, string tool, ILogger? logger)
{
var newSdkRoot = FindPath(sdkRoot, null, logger);
if (newSdkRoot == null)
{
logger?.LogDebug($"Unable to resolve Android SDK root directory from '{sdkRoot}'.");
return null;
}
var versions = Directory.GetDirectories(Path.Combine(newSdkRoot, "build-tools"));
if (versions.Length == 0)
{
logger?.LogDebug($"Unable to locate any build tools in '{newSdkRoot}'.");
return null;
}
else
{
logger?.LogDebug($"Found {versions.Length} build tools versions in '{newSdkRoot}'.");
}
var path = default(string);
var latestSoFar = new Version();
foreach (var versionDir in versions)
{
var v = Path.GetFileName(versionDir);
if (Version.TryParse(v, out var version) && version > latestSoFar)
{
var foundPath = FindFuzzyPath(Path.Combine(versionDir, tool));
if (foundPath != null)
{
latestSoFar = version;
path = foundPath;
}
}
else
{
logger?.LogDebug($"Found invalid build tool version: '{v}'.");
}
}
if (path == null)
logger?.LogDebug($"Unable to find any build tools in '{newSdkRoot}'.");
else
logger?.LogDebug($"Found build tool '{path}'.");
return path;
}
private static string? FindFuzzyPath(string path)
{
if (File.Exists(path))

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

@ -0,0 +1,15 @@
namespace DotNetDevices.Android
{
public class CreateVirtualDeviceOptions
{
public string? Device { get; set; }
public bool Overwrite { get; set; }
public string? Path { get; set; }
public string? SharedSdCardPath { get; set; }
public string? NewSdCardSize { get; set; }
}
}

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

@ -101,7 +101,7 @@ namespace DotNetDevices.Android
var avd = new List<VirtualDevice>(result.OutputCount);
foreach (var output in result.GetOutput())
{
avd.Add(new VirtualDevice(output));
avd.Add(new VirtualDevice(output, output));
}
return avd;
}

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

@ -0,0 +1,34 @@
using DotNetDevices.Processes;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace DotNetDevices.Android
{
public class SDKManager
{
private readonly ProcessRunner processRunner;
private readonly ILogger? logger;
private readonly string sdkmanager;
public SDKManager(string? sdkRoot = null, ILogger? logger = null)
{
processRunner = new ProcessRunner(logger);
this.logger = logger;
sdkmanager = AndroidSDK.FindPath(sdkRoot, Path.Combine("tools", "bin", "sdkmanager"), logger)
?? throw new ArgumentException($"Unable to locate the SDK Manager. Make sure that ANDROID_HOME or ANDROID_SDK_ROOT is set.");
}
public async Task InstallAsync(string package, CancellationToken cancellationToken = default)
{
logger?.LogInformation("Installing packages...");
var args = $"--install \"{package}\"";
await processRunner.RunAsync(sdkmanager, args, null, cancellationToken).ConfigureAwait(false);
}
}
}

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

@ -4,13 +4,21 @@ namespace DotNetDevices.Android
{
public class VirtualDevice
{
public VirtualDevice(string name)
private readonly string? configPath;
public VirtualDevice(string id, string name, string? configPath = null)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
Name = name ?? throw new ArgumentNullException(nameof(name));
this.configPath = configPath;
}
public string Id { get; }
public string Name { get; }
public override string ToString() => Name;
public override string ToString() =>
$"{Name}";
}
}

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

@ -0,0 +1,76 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace DotNetDevices.Android
{
public class VirtualDeviceConfig
{
private readonly string configPath;
private readonly ILogger? logger;
public Dictionary<string, string>? properties;
public VirtualDeviceConfig(string configPath, ILogger? logger = null)
{
this.configPath = configPath ?? throw new ArgumentNullException(nameof(configPath));
this.logger = logger;
}
public async Task<IReadOnlyDictionary<string, string>> GetPropertiesAsync(CancellationToken cancellationToken = default)
{
if (properties != null)
return properties;
logger?.LogDebug($"Loading config.ini {configPath}...");
var contents = await File.ReadAllTextAsync(configPath, cancellationToken);
properties = ParseConfig(contents);
return properties;
}
public async Task<string?> GetStringValueAsync(string key, CancellationToken cancellationToken = default)
{
var props = await GetPropertiesAsync(cancellationToken).ConfigureAwait(false);
props.TryGetValue(key, out var value);
return value;
}
public async Task<VirtualDevice> CreateVirtualDeviceAsync(CancellationToken cancellationToken = default)
{
var props = await GetPropertiesAsync(cancellationToken).ConfigureAwait(false);
if (!props.TryGetValue("avdid", out var id))
throw new Exception($"Invalid config.ini. Unable to find the virtual device ID.");
if (!props.TryGetValue("avd.ini.displayname", out var name))
name = id;
return new VirtualDevice(id, name, configPath);
}
private static Dictionary<string, string> ParseConfig(string contents)
{
var lines = contents.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
Dictionary<string, string> props = new Dictionary<string, string>();
foreach (var line in lines)
{
var pair = line.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
if (pair.Length == 2)
{
props[pair[0].ToLowerInvariant()] = pair[1];
}
}
return props;
}
}
}

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

@ -54,13 +54,14 @@ namespace DotNetDevices.Apple
}
}
public async Task<string> GetStringValueAsync(string key, CancellationToken cancellationToken = default)
public async Task<string?> GetStringValueAsync(string key, CancellationToken cancellationToken = default)
{
var xdoc = await GetDocumentAsync(cancellationToken).ConfigureAwait(false);
var keyElements = xdoc.Descendants("key").Where(x => x.Value == key).ToArray();
if (keyElements.Length == 0)
throw new Exception($"Unable to find key {key}.");
return null;
if (keyElements.Length > 1)
throw new Exception($"Found multiple instances of key {key}.");
@ -71,7 +72,7 @@ namespace DotNetDevices.Apple
return value.Value;
}
public Task<string> GetBundleIdentifierAsync(CancellationToken cancellationToken = default) =>
public Task<string?> GetBundleIdentifierAsync(CancellationToken cancellationToken = default) =>
GetStringValueAsync("CFBundleIdentifier", cancellationToken);
}
}

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

@ -1,17 +1,15 @@
using System;
using DotNetDevices.Android;
using DotNetDevices.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.CommandLine.Rendering;
using System.CommandLine.Rendering.Views;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DotNetDevices.Android;
using DotNetDevices.Apple;
using DotNetDevices.Logging;
using Microsoft.Extensions.Logging;
namespace DotNetDevices.Commands
{
@ -23,15 +21,8 @@ namespace DotNetDevices.Commands
{
new Command("list", "List the virtual devices.")
{
new Option<string?>(new[] { "--sdk" }, "Whether or not to only include the available simulators."),
new Option(new[] { "--available" }, "Whether or not to only include the available simulators."),
new Option(new[] { "--booted" }, "Whether or not to only include the booted simulators."),
new Option<SimulatorRuntime>(new[] { "--runtime" }, "The runtime to use when filtering."),
new Option<string?>(new[] { "--version" }, description: "The runtime version to use when filtering. This could be in either <major> or <major>.<minor> version formats.",
parseArgument: CommandLine.ParseVersion),
new Option<string?>(new[] { "--sdk" }, "The path to the Android SDK directory."),
CommandLine.CreateVerbosity(),
new Argument<string?>("TERM", "The search term to use when filtering simulators. This could be any number of properties (UDID, runtime, version, availability, or state) as well as part of the simulator name.")
{ Arity = ArgumentArity.ZeroOrOne },
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleListAsync))!)),
new Command("create", "Create a new virtual device.")
{
@ -41,22 +32,23 @@ namespace DotNetDevices.Commands
new Argument<string?>("NAME", "The name of the new virtual device."),
new Argument<string?>("PACKAGE", "The package to use for the new virtual device."),
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleCreateAsync))!)),
new Command("boot", "Boot a particular simulator.")
new Command("boot", "Boot a particular virtual device.")
{
new Option<string?>(new[] { "--sdk" }, "Whether or not to only include the available simulators."),
new Option<string?>(new[] { "--sdk" }, "The path to the Android SDK directory."),
CommandLine.CreateVerbosity(),
new Argument<string?>("NAME", "The UDID of the simulator to boot."),
new Argument<string?>("NAME", "The name of the virtual device to boot."),
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleBootAsync))!)),
new Command("install", "Download and install packages using the SDK Manager.")
{
new Option<string?>(new[] { "--sdk" }, "The path to the Android SDK directory."),
CommandLine.CreateVerbosity(),
new Argument<string?>("PACKAGE", "The package to install."),
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleInstallAsync))!)),
};
}
public static async Task HandleListAsync(
string? term = null,
string? sdk = null,
bool available = false,
bool booted = false,
SimulatorRuntime? runtime = null,
string? version = null,
string? verbosity = null,
IConsole console = null!,
CancellationToken cancellationToken = default)
@ -64,49 +56,36 @@ namespace DotNetDevices.Commands
var logger = console.CreateLogger(verbosity);
var avdmanager = new AVDManager(sdk, logger);
var devices = await avdmanager.GetDevicesAsync();
foreach (var device in devices)
{
logger?.LogInformation(" - " + device.ToString());
}
var devices = await avdmanager.GetVirtualDevicesAsync(cancellationToken);
var targets = await avdmanager.GetTargetsAsync();
foreach (var target in targets)
{
logger?.LogInformation(" - " + target.ToString());
}
var filtered = (IEnumerable<VirtualDevice>)devices;
var avds = await avdmanager.GetVirtualDevicesAsync();
foreach (var avd in avds)
{
logger?.LogInformation(" - " + avd.ToString());
}
try
{
await avdmanager.DeleteVirtualDeviceAsync("TESTING");
}
catch { }
//try
//{
// await avdmanager.DeleteVirtualDeviceAsync("TESTING");
//}
//catch { }
try
{
await avdmanager.DeleteVirtualDeviceAsync("TESTED");
}
catch { }
//try
//{
// await avdmanager.DeleteVirtualDeviceAsync("TESTED");
//}
//catch { }
await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64");
//await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64");
await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64", new VirtualDeviceCreateOptions { Overwrite = true });
//await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64", new VirtualDeviceCreateOptions { Overwrite = true });
await avdmanager.RenameVirtualDeviceAsync("TESTING", "TESTED");
await avdmanager.MoveVirtualDeviceAsync("TESTED", "/Users/matthew/.android/avd/tested.avd");
//await avdmanager.RenameVirtualDeviceAsync("TESTING", "TESTED");
//await avdmanager.MoveVirtualDeviceAsync("TESTED", "/Users/matthew/.android/avd/tested.avd");
//await avdmanager.DeleteVirtualDeviceAsync("TESTING");
//term = term?.ToLowerInvariant()?.Trim();
//var simctl = new SimulatorControl(logger);
//var simulators = await simctl.GetSimulatorsAsync(cancellationToken);
//term = term?.ToLowerInvariant()?.Trim();
//var filtered = (IEnumerable<Simulator>)simulators;
//if (!string.IsNullOrWhiteSpace(term))
@ -142,20 +121,16 @@ namespace DotNetDevices.Commands
// filtered = filtered.Where(s => s.Version.Major == versionMjor);
//}
//var all = filtered.ToList();
var all = filtered.ToList();
//logger.LogInformation($"Found {all.Count} simulator[s].");
logger.LogInformation($"Found {all.Count} virtual device[s].");
//var table = new TableView<Simulator>();
//table.AddColumn(s => s.Udid, "UDID");
//table.AddColumn(s => s.Name, "Name");
//table.AddColumn(s => s.Runtime, "Runtime");
//table.AddColumn(s => s.Version, "Version");
//table.AddColumn(s => s.Availability, "Availability");
//table.AddColumn(s => s.State, "State");
//table.Items = all;
var table = new TableView<VirtualDevice>();
table.AddColumn(s => s.Id, "Id");
table.AddColumn(s => s.Name, "Name");
table.Items = all;
//console.Append(new StackLayoutView { table });
console.Append(new StackLayoutView { table });
}
public static async Task HandleCreateAsync(
@ -171,7 +146,7 @@ namespace DotNetDevices.Commands
var avdmanager = new AVDManager(sdk, logger);
var options = new VirtualDeviceCreateOptions
var options = new CreateVirtualDeviceOptions
{
Overwrite = replace
};
@ -191,7 +166,7 @@ namespace DotNetDevices.Commands
var emulator = new EmulatorManager(sdk, logger);
var avds = await emulator.GetVirtualDevicesAsync(cancellationToken);
if (avds.All(a => !a.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
if (avds.All(a => !a.Id.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
logger.LogError($"No virtual device with name {name} was found.");
return 1;
@ -210,5 +185,21 @@ namespace DotNetDevices.Commands
return 0;
}
public static async Task<int> HandleInstallAsync(
string package,
string? sdk = null,
string? verbosity = null,
IConsole console = null!,
CancellationToken cancellationToken = default)
{
var logger = console.CreateLogger(verbosity);
var sdkmanager = new SDKManager(sdk, logger);
await sdkmanager.InstallAsync(package, cancellationToken);
return 0;
}
}
}

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

@ -0,0 +1,237 @@
using DotNetDevices.Android;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace DotNetDevices.Commands
{
internal class AndroidTestCommand
{
private readonly string? sdkRoot;
private readonly ILogger logger;
private readonly AVDManager avdmanager;
private readonly EmulatorManager emulator;
private readonly AaptTool aapt;
public AndroidTestCommand(string? sdkRoot, ILogger logger)
{
this.sdkRoot = sdkRoot;
this.logger = logger;
avdmanager = new AVDManager(sdkRoot, logger);
emulator = new EmulatorManager(sdkRoot, logger);
aapt = new AaptTool(sdkRoot, logger);
}
public async Task RunTestsAsync(
string app,
string? deviceResults = null, // "TestResults.trx"
string? outputResults = null, // "TestResults.trx"
string? runtimeString = null,
string? versionString = null,
bool latest = false,
string? deviceType = null,
string? deviceName = null,
bool reset = false,
bool shutdown = false,
CancellationToken cancellationToken = default)
{
// validate app / bundle
var androidManifest = await aapt.GetAndroidManifestAsync(app, cancellationToken);
var packageName = androidManifest.PackageName;
if (string.IsNullOrEmpty(packageName))
throw new Exception("Unable to determine the package name for the app.");
logger.LogInformation($"Running tests on '{packageName}'...");
//// validate requested OS
//var simulatorType = ParseSimulatorType(deviceType);
//var runtime = ParseSimulatorRuntime(runtimeString);
//var runtimeVersion = await ParseVersionAsync(versionString, runtime, cancellationToken);
//logger.LogInformation($"Looking for an available {simulatorType} ({runtimeVersion}) simulator...");
//var available = await GetAvailableSimulatorsAsync(simulatorType, runtime, runtimeVersion, latest, cancellationToken);
//// first look for a booted device
//var simulator = available.FirstOrDefault(s => s.State == SimulatorState.Booted) ?? available.FirstOrDefault();
//logger.LogInformation($"Using simulator {simulator.Name} ({simulator.Runtime} {simulator.Version}): {simulator.Udid}");
//try
//{
// if (reset)
// await simctl.EraseSimulatorAsync(simulator.Udid, true, cancellationToken);
// await simctl.InstallAppAsync(simulator.Udid, app, true, cancellationToken);
// try
// {
// var parser = new TestResultsParser();
// var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// 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);
// await simctl.TerminateAppAsync(simulator.Udid, bundleId, cts.Token);
// }
// catch (OperationCanceledException)
// {
// // we expected this
// }
// });
// },
// }, cancellationToken);
// cts.Cancel();
// if (deviceResults != null)
// {
// var dest = outputResults ?? Path.GetFileName(deviceResults);
// logger.LogInformation($"Copying test results from simulator 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.");
// }
// }
// finally
// {
// await simctl.UninstallAppAsync(simulator.Udid, bundleId, false, cancellationToken);
// }
//}
//finally
//{
// if (shutdown)
// await simctl.ShutdownSimulatorAsync(simulator.Udid, cancellationToken);
//}
}
//private async Task<List<Simulator>> GetAvailableSimulatorsAsync(SimulatorType type, SimulatorRuntime runtime, Version version, bool useLatest = true, CancellationToken cancellationToken = default)
//{
// // load all simulators
// var simulators = await simctl.GetSimulatorsAsync(cancellationToken);
// // find ones that can be used
// var available = simulators
// .Where(s => s.Availability == SimulatorAvailability.Available)
// .Where(s => s.Runtime == runtime)
// .Where(s => s.Type == type);
// logger.LogDebug($"Found some available simulators:");
// foreach (var sim in available)
// {
// logger.LogDebug($" {sim.Name} ({sim.Runtime} {sim.Version}): {sim.Udid}");
// }
// // filter by version info
// string matchingPattern;
// if (useLatest)
// {
// var min = version;
// var max = new Version(min.Major + 1, 0);
// available = available.Where(s => s.Version >= min && s.Version < max);
// matchingPattern = $"[{min}, {max})";
// }
// else
// {
// available = available.Where(s => s.Version == version);
// matchingPattern = $"[{version}]";
// }
// var matching = available.ToList();
// if (matching.Count > 0)
// {
// logger.LogDebug($"Found matching simulators {matchingPattern}:");
// foreach (var sim in matching)
// {
// logger.LogDebug($" {sim.Name} ({sim.Runtime} {sim.Version}): {sim.Udid}");
// }
// }
// else
// {
// throw new Exception($"Unable to find any simulators that match version {matchingPattern}.");
// }
// return matching;
//}
//private async Task<Version> ParseVersionAsync(string? version, SimulatorRuntime os, CancellationToken cancellationToken = default)
//{
// var osVersion = version?.ToLowerInvariant().Trim();
// if (!Version.TryParse(osVersion, out var numberVersion))
// {
// if (int.TryParse(osVersion, out var v))
// numberVersion = new Version(v, 0);
// else if (string.IsNullOrEmpty(osVersion) || osVersion == "default")
// numberVersion = await simctl.GetDefaultVersionAsync(os, cancellationToken);
// else
// throw new Exception($"Unable to determine the version for {osVersion}.");
// }
// return numberVersion;
//}
//private static SimulatorRuntime ParseSimulatorRuntime(string? runtime)
//{
// var osName = runtime?.ToLowerInvariant()?.Trim();
// var os = osName switch
// {
// null => SimulatorRuntime.iOS,
// "" => SimulatorRuntime.iOS,
// "ios" => SimulatorRuntime.iOS,
// "watchos" => SimulatorRuntime.watchOS,
// "tvos" => SimulatorRuntime.tvOS,
// _ => throw new Exception($"Unable to determine the OS for {runtime}.")
// };
// return os;
//}
//private static SimulatorType ParseSimulatorType(string? deviceType)
//{
// var deviceTypeName = deviceType?.ToLowerInvariant()?.Trim();
// var device = deviceTypeName switch
// {
// // iPhone
// null => SimulatorType.iPhone,
// "" => SimulatorType.iPhone,
// "iphone" => SimulatorType.iPhone,
// "phone" => SimulatorType.iPhone,
// // iPad
// "ipad" => SimulatorType.iPad,
// "tablet" => SimulatorType.iPad,
// // iPod
// "ipod" => SimulatorType.iPod,
// // Apple TV
// "tv" => SimulatorType.AppleTV,
// "appletv" => SimulatorType.AppleTV,
// // Apple Watch
// "watch" => SimulatorType.AppleWatch,
// "applewatch" => SimulatorType.AppleWatch,
// //
// _ => throw new Exception($"Unable to determine the simulator type for {deviceType}.")
// };
// return device;
//}
}
}

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

@ -1,12 +1,12 @@
using System;
using DotNetDevices.Apple;
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;
using DotNetDevices.Apple;
using DotNetDevices.Testing;
using Microsoft.Extensions.Logging;
namespace DotNetDevices.Commands
{
@ -38,6 +38,10 @@ namespace DotNetDevices.Commands
// validate app / bundle
var plist = new PList(Path.Combine(app, "Info.plist"), logger);
var bundleId = await plist.GetBundleIdentifierAsync(cancellationToken);
if (string.IsNullOrEmpty(bundleId))
throw new Exception("Unable to determine the bundle identifer for the app.");
logger.LogInformation($"Running tests on '{bundleId}'...");
// validate requested OS
var simulatorType = ParseSimulatorType(deviceType);

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

@ -1,11 +1,11 @@
using System;
using DotNetDevices.Logging;
using Microsoft.Extensions.Logging;
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using DotNetDevices.Logging;
using Microsoft.Extensions.Logging;
namespace DotNetDevices.Commands
{
@ -60,14 +60,27 @@ namespace DotNetDevices.Commands
try
{
// detect iOS .app files (directories)
if (Path.GetExtension(app).Equals(".app", StringComparison.OrdinalIgnoreCase))
{
// detect iOS .app files (directories)
var cmd = new AppleTestCommand(logger);
await cmd.RunTestsAsync(app, deviceResults, outputResults, runtime, version, latest, deviceType, deviceName, reset, shutdown, cancellationToken);
}
return 0;
return 0;
}
else if (Path.GetExtension(app).Equals(".apk", StringComparison.OrdinalIgnoreCase))
{
// detect Android .apk files
var cmd = new AndroidTestCommand(null, logger);
await cmd.RunTestsAsync(app, deviceResults, outputResults, runtime, version, latest, deviceType, deviceName, reset, shutdown, cancellationToken);
return 0;
}
else
{
logger.LogError($"Unknown app package type: {app}");
return 1;
}
}
catch (Exception ex)
{

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

@ -0,0 +1,8 @@
{
"profiles": {
"dotnet-devices": {
"commandName": "Project",
"commandLineArgs": "test \"C:\\Projects\\dotnet-devices\\DeviceTests\\DeviceTests.Android\\bin\\Debug\\net.dot.devicetests-Signed.apk\""
}
}
}