This commit is contained in:
Matthew Leibowitz 2020-10-16 23:43:52 +02:00
Родитель 405864895b
Коммит c0e5600a11
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: ECDB25CC0E22FC46
17 изменённых файлов: 771 добавлений и 206 удалений

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

@ -1,4 +1,5 @@
using DotNetDevices.Android;
using System;
using System.Threading.Tasks;
using Xunit;
@ -21,6 +22,71 @@ namespace DotNetDevices.Tests
Assert.Equal(id, device.Id);
Assert.Equal(name, device.Name);
}
[Fact]
public async Task CanReadTV()
{
var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_TV.txt");
var device = await config.CreateVirtualDeviceAsync();
Assert.Equal(VirtualDeviceType.TV, device.Type);
Assert.Equal(new Version(10, 0), device.Version);
Assert.Equal(29, device.ApiLevel);
Assert.Equal(VirtualDeviceRuntime.AndroidTV, device.Runtime);
}
[Fact]
public async Task CanReadWear()
{
var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Wear.txt");
var device = await config.CreateVirtualDeviceAsync();
Assert.Equal(VirtualDeviceType.Wearable, device.Type);
Assert.Equal(new Version(9, 0), device.Version);
Assert.Equal(28, device.ApiLevel);
Assert.Equal(VirtualDeviceRuntime.AndroidWear, device.Runtime);
}
[Fact]
public async Task CanReadTablet()
{
var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Tablet.txt");
var device = await config.CreateVirtualDeviceAsync();
Assert.Equal(VirtualDeviceType.Tablet, device.Type);
Assert.Equal(new Version(9, 0), device.Version);
Assert.Equal(28, device.ApiLevel);
Assert.Equal(VirtualDeviceRuntime.Android, device.Runtime);
}
[Fact]
public async Task CanReadGeneric()
{
var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Generic.txt");
var device = await config.CreateVirtualDeviceAsync();
Assert.Equal(VirtualDeviceType.Phone, device.Type);
Assert.Equal(new Version(9, 0), device.Version);
Assert.Equal(28, device.ApiLevel);
Assert.Equal(VirtualDeviceRuntime.Android, device.Runtime);
}
[Fact]
public async Task CanReadPhone()
{
var config = new VirtualDeviceConfig("TestData/Android/AvdConfigIni_Phone.txt");
var device = await config.CreateVirtualDeviceAsync();
Assert.Equal(VirtualDeviceType.Phone, device.Type);
Assert.Equal(new Version(10, 0), device.Version);
Assert.Equal(29, device.ApiLevel);
Assert.Equal(VirtualDeviceRuntime.Android, device.Runtime);
}
}
}
}

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

@ -0,0 +1,40 @@
AvdId=phone_xh-dpi_4_7in_pie_9_0_-_api_28
PlayStore.enabled=false
abi.type=x86
avd.ini.displayname=Phone Xh-DPI 4.7in Pie 9.0 - API 28
avd.ini.encoding=UTF-8
disk.dataPartition.size=2G
fastboot.forceColdBoot=no
fastboot.forceFastBoot=yes
hw.accelerometer=yes
hw.arc=no
hw.audioInput=yes
hw.battery=yes
hw.camera.back=virtualscene
hw.cpu.arch=x86
hw.cpu.ncore=4
hw.dPad=no
hw.device.hash2=MD5:ed3db32523e6d6f8210c6377d2c66883
hw.device.manufacturer=Generic
hw.device.name=4.7in WXGA
hw.gps=yes
hw.gpu.enabled=yes
hw.gpu.mode=auto
hw.keyboard=yes
hw.lcd.density=320
hw.lcd.height=720
hw.lcd.width=1280
hw.mainKeys=yes
hw.ramSize=512
hw.sdCard=no
hw.sensors.orientation=yes
hw.sensors.proximity=yes
hw.trackBall=no
image.sysdir.1=system-images\android-28\default\x86\
sdcard.size=512M
showDeviceFrame=no
skin.dynamic=yes
skin.name=1280x720
tag.display=Default
tag.id=default
vm.heapSize=256

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

@ -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,41 @@
AvdId=android_tv_720p_q_10_0_-_api_29
PlayStore.enabled=false
abi.type=x86
avd.ini.displayname=Android TV (720p) Q 10.0 - API 29
avd.ini.encoding=UTF-8
disk.dataPartition.size=2G
fastboot.forceColdBoot=no
fastboot.forceFastBoot=yes
hw.accelerometer=no
hw.arc=no
hw.audioInput=yes
hw.battery=no
hw.cpu.arch=x86
hw.cpu.ncore=4
hw.dPad=yes
hw.device.hash2=MD5:62a27947d2faec95c32cddffb87aa6ec
hw.device.manufacturer=Google
hw.device.name=tv_720p
hw.gps=yes
hw.gpu.enabled=yes
hw.gpu.mode=auto
hw.initialOrientation=landscape
hw.keyboard=yes
hw.keyboard.lid=yes
hw.lcd.density=213
hw.lcd.height=720
hw.lcd.width=1280
hw.mainKeys=yes
hw.ramSize=1536
hw.sdCard=yes
hw.sensors.orientation=no
hw.sensors.proximity=no
hw.trackBall=no
image.sysdir.1=system-images\android-29\android-tv\x86\
sdcard.size=512M
showDeviceFrame=yes
skin.name=tv_720p
skin.path=C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\Extensions\Xamarin\AndroidDeviceManager\SystemSkins\tv_720p
tag.display=Android TV
tag.id=android-tv
vm.heapSize=256

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

@ -0,0 +1,42 @@
AvdId=nexus_10_pie_9_0_-_api_28
PlayStore.enabled=false
abi.type=x86
avd.ini.displayname=Nexus 10 Pie 9.0 - API 28
avd.ini.encoding=UTF-8
disk.dataPartition.size=2G
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:8323741337b2d5dd4418b47c06a242b0
hw.device.manufacturer=Google
hw.device.name=Nexus 10
hw.gps=yes
hw.gpu.enabled=yes
hw.gpu.mode=auto
hw.initialOrientation=landscape
hw.keyboard=yes
hw.lcd.density=320
hw.lcd.height=1600
hw.lcd.width=2560
hw.mainKeys=no
hw.ramSize=1536
hw.sdCard=no
hw.sensors.orientation=yes
hw.sensors.proximity=no
hw.trackBall=no
image.sysdir.1=system-images\android-28\default\x86\
sdcard.size=512M
showDeviceFrame=yes
skin.name=nexus_10
skin.path=C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\Extensions\Xamarin\AndroidDeviceManager\SystemSkins\nexus_10
tag.display=Default
tag.id=default
vm.heapSize=256

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

@ -0,0 +1,41 @@
AvdId=android_wear_round_pie_9_0_-_api_28
PlayStore.enabled=false
abi.type=x86
avd.ini.displayname=Android Wear Round Pie 9.0 - API 28
avd.ini.encoding=UTF-8
disk.dataPartition.size=2G
fastboot.forceColdBoot=no
fastboot.forceFastBoot=yes
hw.accelerometer=yes
hw.arc=no
hw.audioInput=yes
hw.battery=yes
hw.cpu.arch=x86
hw.cpu.ncore=4
hw.dPad=no
hw.device.hash2=MD5:3c452b52cce72363917e3cdd3c1c5954
hw.device.manufacturer=Google
hw.device.name=wear_round
hw.gps=yes
hw.gpu.enabled=yes
hw.gpu.mode=auto
hw.initialOrientation=landscape
hw.keyboard=yes
hw.keyboard.lid=yes
hw.lcd.density=240
hw.lcd.height=320
hw.lcd.width=320
hw.mainKeys=yes
hw.ramSize=512
hw.sdCard=no
hw.sensors.orientation=yes
hw.sensors.proximity=yes
hw.trackBall=no
image.sysdir.1=system-images\android-28\android-wear\x86\
sdcard.size=512M
showDeviceFrame=yes
skin.name=wear_round
skin.path=C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\Extensions\Xamarin\AndroidDeviceManager\SystemSkins\wear_round
tag.display=Android Wear
tag.id=android-wear
vm.heapSize=256

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

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

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

@ -58,6 +58,34 @@ namespace DotNetDevices.Android
return targets;
}
public async Task<IEnumerable<string>> GetVirtualDeviceNamesAsync(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);
var avds = new List<string>();
foreach (var output in GetListResults(result))
{
var pathMatch = virtualDevicePathRegex.Match(output);
if (pathMatch.Success)
{
var path = pathMatch.Groups[1].Value;
if (Directory.Exists(path))
{
var avd = Path.GetFileNameWithoutExtension(path);
avds.Add(avd);
}
}
}
return avds;
}
public async Task<IEnumerable<VirtualDevice>> GetVirtualDevicesAsync(CancellationToken cancellationToken = default)
{
logger?.LogInformation("Retrieving all the virtual devices...");
@ -94,7 +122,27 @@ namespace DotNetDevices.Android
var args = $"delete avd --name \"{name}\"";
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
try
{
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
}
catch (ProcessResultException ex) when (WasExisting(ex.ProcessResult))
{
// no-op
}
bool WasExisting(ProcessResult result)
{
var expected = $"Error: There is no Android Virtual Device named '{name}'.";
foreach (var output in result.GetErrorOutput())
{
if (output.Contains(expected, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
}
public async Task CreateVirtualDeviceAsync(string name, string package, CreateVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
@ -105,7 +153,27 @@ namespace DotNetDevices.Android
if (options?.Overwrite == true)
args += " --force";
await processRunner.RunWithInputAsync("no", avdmanager, args, null, cancellationToken).ConfigureAwait(false);
try
{
await processRunner.RunWithInputAsync("no", avdmanager, args, null, cancellationToken).ConfigureAwait(false);
}
catch (ProcessResultException ex) when (WasExisting(ex.ProcessResult))
{
// no-op
}
bool WasExisting(ProcessResult result)
{
var expected = $"Error: Android Virtual Device '{name}' already exists.";
foreach (var output in result.GetErrorOutput())
{
if (output.Contains(expected, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
}
public async Task RenameVirtualDeviceAsync(string name, string newName, CancellationToken cancellationToken = default)

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

@ -90,7 +90,7 @@ namespace DotNetDevices.Android
}
}
public async Task<IEnumerable<VirtualDevice>> GetVirtualDevicesAsync(CancellationToken cancellationToken = default)
public async Task<IEnumerable<string>> GetVirtualDevicesAsync(CancellationToken cancellationToken = default)
{
logger?.LogInformation("Retrieving all the virtual devices...");
@ -98,10 +98,10 @@ namespace DotNetDevices.Android
var result = await processRunner.RunAsync(emulator, args, null, cancellationToken).ConfigureAwait(false);
var avd = new List<VirtualDevice>(result.OutputCount);
var avd = new List<string>(result.OutputCount);
foreach (var output in result.GetOutput())
{
avd.Add(new VirtualDevice(output, output));
avd.Add(output.Trim());
}
return avd;
}

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

@ -4,21 +4,76 @@ namespace DotNetDevices.Android
{
public class VirtualDevice
{
private readonly string? configPath;
public VirtualDevice(string id, string name, string? configPath = null)
public VirtualDevice(string id, string name, string package, VirtualDeviceType type, int apiLevel, string? configPath)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
Name = name ?? throw new ArgumentNullException(nameof(name));
this.configPath = configPath;
Package = package ?? throw new ArgumentNullException(nameof(package));
Type = type;
ApiLevel = apiLevel;
ConfigPath = configPath;
}
public string Id { get; }
public string Name { get; }
public string Package { get; }
public VirtualDeviceType Type { get; }
public int ApiLevel { get; }
public string? ConfigPath { get; }
public Version Version =>
ApiLevel switch
{
1 => new Version(1, 0),
2 => new Version(1, 1),
3 => new Version(1, 5),
4 => new Version(1, 6),
5 => new Version(2, 0),
6 => new Version(2, 0, 1),
7 => new Version(2, 1),
8 => new Version(2, 2),
9 => new Version(2, 3),
10 => new Version(2, 3, 3),
11 => new Version(3, 0),
12 => new Version(3, 1),
13 => new Version(3, 2),
14 => new Version(4, 0),
15 => new Version(4, 0, 3),
16 => new Version(4, 1),
17 => new Version(4, 2),
18 => new Version(4, 3),
19 => new Version(4, 4),
20 => new Version(4, 4), // 4.4W (wear)
21 => new Version(5, 0),
22 => new Version(5, 1),
23 => new Version(6, 0),
24 => new Version(7, 0),
25 => new Version(7, 1),
26 => new Version(8, 0),
27 => new Version(8, 1),
28 => new Version(9, 0),
29 => new Version(10, 0),
30 => new Version(11, 0),
_ => new Version(),
};
public VirtualDeviceRuntime Runtime =>
Type switch
{
VirtualDeviceType.Unknown => VirtualDeviceRuntime.Android,
VirtualDeviceType.Phone => VirtualDeviceRuntime.Android,
VirtualDeviceType.Tablet => VirtualDeviceRuntime.Android,
VirtualDeviceType.Wearable => VirtualDeviceRuntime.AndroidWear,
VirtualDeviceType.TV => VirtualDeviceRuntime.AndroidTV,
_ => VirtualDeviceRuntime.Android,
};
public override string ToString() =>
$"{Name}";
$"{Name} (API {ApiLevel})";
}
}

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

@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@ -9,6 +10,8 @@ namespace DotNetDevices.Android
{
public class VirtualDeviceConfig
{
private static readonly Regex androidApiRegex = new Regex(@"android-(\d+)");
private readonly string configPath;
private readonly ILogger? logger;
@ -47,12 +50,80 @@ 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);
}
if (string.IsNullOrEmpty(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);
if (!props.TryGetValue("image.sysdir.1", out var package))
package = "";
var packageParts = package.Split(new[] { '\\', '/', ';' }, StringSplitOptions.RemoveEmptyEntries);
package = string.Join(";", packageParts);
var apiLevel = 0;
if (packageParts.Length == 4)
{
var apiMatch = androidApiRegex.Match(packageParts[1]);
if (apiMatch.Success)
apiLevel = int.Parse(apiMatch.Groups[1].Value);
}
if (!TryGetType(props, out var type))
type = VirtualDeviceType.Unknown;
return new VirtualDevice(id, name, package, type, apiLevel, configPath);
}
private static bool TryGetType(IReadOnlyDictionary<string, string> props, out VirtualDeviceType value)
{
if (props.TryGetValue("tag.id", out var type))
{
switch (type.Trim().ToLowerInvariant())
{
case "android-tv":
value = VirtualDeviceType.TV;
return true;
case "android-wear":
value = VirtualDeviceType.Wearable;
return true;
case "default":
case "google_apis":
case "google_apis_playstore":
value =
TryGetDimensions(props, out var width, out var height, out var density)
&& Math.Min(width, height) / (density / 160) >= 600
? VirtualDeviceType.Tablet
: VirtualDeviceType.Phone;
return true;
}
}
value = VirtualDeviceType.Unknown;
return false;
}
private static bool TryGetDimensions(IReadOnlyDictionary<string, string> props, out int width, out int height, out double density)
{
width = 0;
height = 0;
density = 160;
if (!props.TryGetValue("hw.lcd.width", out var widthString) || !int.TryParse(widthString, out width))
return false;
if (!props.TryGetValue("hw.lcd.height", out var heightString) || !int.TryParse(heightString, out height))
return false;
if (!props.TryGetValue("hw.lcd.density", out var densityString) || !double.TryParse(densityString, out density))
density = 160;
return true;
}
private static Dictionary<string, string> ParseConfig(string contents)

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

@ -0,0 +1,9 @@
namespace DotNetDevices.Android
{
public enum VirtualDeviceRuntime
{
Android,
AndroidWear,
AndroidTV
}
}

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

@ -0,0 +1,11 @@
namespace DotNetDevices.Android
{
public enum VirtualDeviceType
{
Unknown,
Phone,
Tablet,
Wearable,
TV
}
}

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

@ -32,6 +32,12 @@ 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("delete", "Delete an existing virtual device.")
{
new Option<string?>(new[] { "--sdk" }, "The path to the Android SDK directory."),
CommandLine.CreateVerbosity(),
new Argument<string?>("NAME", "The name of the new virtual device."),
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleDeleteAsync))!)),
new Command("boot", "Boot a particular virtual device.")
{
new Option<string?>(new[] { "--sdk" }, "The path to the Android SDK directory."),
@ -60,31 +66,6 @@ namespace DotNetDevices.Commands
var filtered = (IEnumerable<VirtualDevice>)devices;
//try
//{
// await avdmanager.DeleteVirtualDeviceAsync("TESTING");
//}
//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", new VirtualDeviceCreateOptions { Overwrite = true });
//await avdmanager.RenameVirtualDeviceAsync("TESTING", "TESTED");
//await avdmanager.MoveVirtualDeviceAsync("TESTED", "/Users/matthew/.android/avd/tested.avd");
//await avdmanager.DeleteVirtualDeviceAsync("TESTING");
//term = term?.ToLowerInvariant()?.Trim();
//var filtered = (IEnumerable<Simulator>)simulators;
@ -128,6 +109,10 @@ namespace DotNetDevices.Commands
var table = new TableView<VirtualDevice>();
table.AddColumn(s => s.Id, "Id");
table.AddColumn(s => s.Name, "Name");
table.AddColumn(s => s.Type, "Type");
table.AddColumn(s => s.Version, "Version");
table.AddColumn(s => s.ApiLevel, "API Level");
//table.AddColumn(s => s.State, "State");
table.Items = all;
console.Append(new StackLayoutView { table });
@ -146,14 +131,45 @@ namespace DotNetDevices.Commands
var avdmanager = new AVDManager(sdk, logger);
if (!replace)
{
var devices = await avdmanager.GetVirtualDeviceNamesAsync(cancellationToken);
if (devices.Any(d => d.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
logger.LogInformation($"Virtual device already exists.");
return;
}
}
var options = new CreateVirtualDeviceOptions
{
Overwrite = replace
Overwrite = replace,
};
await avdmanager.CreateVirtualDeviceAsync(name, package, options, cancellationToken);
}
public static async Task HandleDeleteAsync(
string name,
string? sdk = null,
string? verbosity = null,
IConsole console = null!,
CancellationToken cancellationToken = default)
{
var logger = console.CreateLogger(verbosity);
var avdmanager = new AVDManager(sdk, logger);
var devices = await avdmanager.GetVirtualDeviceNamesAsync(cancellationToken);
if (devices.All(d => !d.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
logger.LogInformation($"Virtual device does not exist.");
return;
}
await avdmanager.DeleteVirtualDeviceAsync(name, cancellationToken);
}
public static async Task<int> HandleBootAsync(
string name,
string? sdk = null,
@ -166,7 +182,7 @@ namespace DotNetDevices.Commands
var emulator = new EmulatorManager(sdk, logger);
var avds = await emulator.GetVirtualDevicesAsync(cancellationToken);
if (avds.All(a => !a.Id.Equals(name, StringComparison.OrdinalIgnoreCase)))
if (avds.All(a => !a.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
logger.LogError($"No virtual device with name {name} was found.");
return 1;

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

@ -1,6 +1,8 @@
using DotNetDevices.Android;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -45,193 +47,198 @@ namespace DotNetDevices.Commands
logger.LogInformation($"Running tests on '{packageName}'...");
//// validate requested OS
//var simulatorType = ParseSimulatorType(deviceType);
//var runtime = ParseSimulatorRuntime(runtimeString);
//var runtimeVersion = await ParseVersionAsync(versionString, runtime, cancellationToken);
// validate requested OS
var avdRuntime = ParseDeviceRuntime(runtimeString);
var avdTypes = ParseDeviceTypes(deviceType, avdRuntime);
var avdApiLevel = ParseApiLevel(versionString);
if (avdApiLevel == 0)
latest = true;
//logger.LogInformation($"Looking for an available {simulatorType} ({runtimeVersion}) simulator...");
//var available = await GetAvailableSimulatorsAsync(simulatorType, runtime, runtimeVersion, latest, cancellationToken);
logger.LogInformation($"Looking for an available {string.Join("|", avdTypes)}{(avdApiLevel == 0 ? "" : $" (API {avdApiLevel})")} virtual device...");
var available = await GetAvailableDevicesAsync(deviceName, avdTypes, avdApiLevel, 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}");
// get the first device
var avd = available.FirstOrDefault();
logger.LogInformation($"Using virtual device {avd.Name} ({avd.Runtime} {avd.Version}): {avd.Id}");
//try
//{
// if (reset)
// await simctl.EraseSimulatorAsync(simulator.Udid, true, cancellationToken);
try
{
// if (reset)
// await simctl.EraseSimulatorAsync(simulator.Udid, true, cancellationToken);
// await simctl.InstallAppAsync(simulator.Udid, app, true, cancellationToken);
// await simctl.InstallAppAsync(simulator.Udid, app, true, cancellationToken);
// try
// {
// var parser = new TestResultsParser();
try
{
// var parser = new TestResultsParser();
// var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// 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);
// 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);
// await simctl.TerminateAppAsync(simulator.Udid, bundleId, cts.Token);
// }
// catch (OperationCanceledException)
// {
// // we expected this
// }
// });
// },
// }, cancellationToken);
// cts.Cancel();
// cts.Cancel();
// 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 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);
//}
// 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);
private async Task<List<VirtualDevice>> GetAvailableDevicesAsync(string? deviceName, VirtualDeviceType[] types, int apiLevel, bool useLatest = true, CancellationToken cancellationToken = default)
{
// load all virtual devices
var avds = await avdmanager.GetVirtualDevicesAsync(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}");
// }
// use the name directly
if (!string.IsNullOrEmpty(deviceName))
return avds.Where(d => d.Id == deviceName || d.Name == deviceName).ToList();
// // 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}]";
// }
// find ones that can be used
var available = avds
.Where(s => types.Contains(s.Type));
logger.LogDebug($"Found some available virtual devices:");
foreach (var avd in available)
{
logger.LogDebug($" {avd.Name} ({avd.Runtime} API {avd.ApiLevel}): {avd.Id}");
}
// 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}.");
// }
// filter by version info
string matchingPattern;
if (useLatest)
{
var max = available.Where(d => d.ApiLevel >= apiLevel).Max(d => d.ApiLevel);
available = available.Where(d => d.ApiLevel == max);
matchingPattern = apiLevel > 0 ? $"[{apiLevel})" : $"[{max}]";
}
else
{
available = available.Where(d => d.ApiLevel == apiLevel);
matchingPattern = $"[{apiLevel}]";
}
// return matching;
//}
var matching = available.ToList();
if (matching.Count == 0)
throw new Exception($"Unable to find any virtual devices that match version {matchingPattern}.");
//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}.");
// }
logger.LogDebug($"Found matching virtual devices {matchingPattern}:");
foreach (var avd in matching)
{
logger.LogDebug($" {avd.Name} ({avd.Runtime} API {avd.ApiLevel}): {avd.Id}");
}
// return numberVersion;
//}
return matching;
}
//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 int ParseApiLevel(string? version)
{
var osVersion = version?.ToLowerInvariant().Trim();
//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;
//}
if (string.IsNullOrEmpty(osVersion))
return 0;
if (!int.TryParse(osVersion, out var numberVersion))
throw new Exception($"Unable to determine the version for {osVersion}.");
return numberVersion;
}
private static VirtualDeviceRuntime ParseDeviceRuntime(string? runtime)
{
var osName = runtime?.ToLowerInvariant()?.Trim();
var os = osName switch
{
null => VirtualDeviceRuntime.Android,
"" => VirtualDeviceRuntime.Android,
"android" => VirtualDeviceRuntime.Android,
"watch" => VirtualDeviceRuntime.AndroidWear,
"wear" => VirtualDeviceRuntime.AndroidWear,
"androidwear" => VirtualDeviceRuntime.AndroidWear,
"wearable" => VirtualDeviceRuntime.AndroidWear,
"tv" => VirtualDeviceRuntime.AndroidTV,
"androidtv" => VirtualDeviceRuntime.AndroidTV,
_ => throw new Exception($"Unable to determine the OS for {runtime}.")
};
return os;
}
private static VirtualDeviceType[] ParseDeviceTypes(string? deviceType, VirtualDeviceRuntime runtime)
{
var fallback = runtime switch
{
VirtualDeviceRuntime.Android => new[] { VirtualDeviceType.Phone, VirtualDeviceType.Tablet },
VirtualDeviceRuntime.AndroidWear => new[] { VirtualDeviceType.Wearable },
VirtualDeviceRuntime.AndroidTV => new[] { VirtualDeviceType.TV },
_ => new[] { VirtualDeviceType.Phone | VirtualDeviceType.Tablet },
};
var deviceTypeName = deviceType?.ToLowerInvariant()?.Trim();
var device = deviceTypeName switch
{
// phone
null => fallback,
"" => fallback,
"phone" => new[] { VirtualDeviceType.Phone },
// tablet
"tab" => new[] { VirtualDeviceType.Tablet },
"tablet" => new[] { VirtualDeviceType.Tablet },
// TV
"tv" => new[] { VirtualDeviceType.TV },
// Wear
"watch" => new[] { VirtualDeviceType.Wearable },
"wear" => new[] { VirtualDeviceType.Wearable },
"wearable" => new[] { VirtualDeviceType.Wearable },
//
_ => throw new Exception($"Unable to determine the virtual device type for {deviceType}.")
};
return device;
}
}
}

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

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using DotNetDevices.Android;
namespace DotNetDevices.Processes
{
@ -43,6 +42,13 @@ namespace DotNetDevices.Processes
yield return item.Data;
}
public IEnumerable<string> GetErrorOutput()
{
foreach (var item in outputItems)
if (item.IsError)
yield return item.Data;
}
public override string ToString() =>
$"Completed with exit code {ExitCode} in {Elapsed}.";