C#: Автоматизация Android без посредников (adb)
Всем привет
Не давно понадобилось автоматизировать одно приложение.Мне не хотелось использовать какие то посредники по типу Appium, во-первых, ресурсы были ограничены, на одном компьютере нужно было заставить работать 3–4 эмулятора, во-вторых с adb работать не так уж и трудно, в-третьих я наткнулся на библиотеку SharpAdbClient, в которой были уже реализованы базовые функции, но самых важных мне не хватало, поэтому пришлось дописать их самому.Я подумал почему бы не сделать библиотеку с расширенным функционалом, поэтому в этой статье будет обзор на библиотеку AdvancedSharpAdbClient.
Установка
Вы можете установить библиотеку через nuget, либо через package manager:
PM> Install-Package AdvancedSharpAdbClient
Так же проект доступен на github
Инициализация
Для работы библиотеки понадобится adb.exe.Я использовал эмулятор Nox, где adb идёт вместе с приложением и находится по пути Nox\bin\adb.exe.
Первым делом запустим сервер
using System;
using AdvancedSharpAdbClient;
namespace AdbTest
{
class Program
{
static void Main(string[] args)
{
if (!AdbServer.Instance.GetStatus().IsRunning)
{
AdbServer server = new AdbServer();
StartServerResult result = server.StartServer(@"F:\Nox\bin\adb.exe", false);
if (result != StartServerResult.Started)
{
Console.WriteLine("Can't start adb server");
return;
}
}
}
}
}
Далее нам необходимо создать новый клиент и подключиться к устройству
using System;
using System.Linq;
using AdvancedSharpAdbClient;
namespace AdbTest
{
class Program
{
static AdvancedAdbClient client;
static DeviceData device;
static void Main(string[] args)
{
if (!AdbServer.Instance.GetStatus().IsRunning)
{
AdbServer server = new AdbServer();
StartServerResult result = server.StartServer(@"F:\Nox\bin\adb.exe", false);
if (result != StartServerResult.Started)
{
Console.WriteLine("Can't start adb server");
return;
}
}
client = new AdvancedAdbClient();
client.Connect("127.0.0.1:62001"); // Ip Nox'а
device = client.GetDevices().FirstOrDefault(); // Выбираем девайс из подключенных
if (device == null)
{
Console.WriteLine("Can't connect to device");
return;
}
}
}
}
Тут я покажу вам лайфхак, как можно автоматически получать Ip эмуляторов.Это пригодится при работе с несколькими эмуляторами одновременно (многопоточность).
Рассмотрим на примере Nox’a, у которого каждый порт начинается с 620, например:
127.0.0.1:62001
127.0.0.1:62025
127.0.0.1:62040
Для поиска можно воспользоваться утилитой netstat, которая слушает активные порты и подключения
Получение IP для нескольких эмуляторов
static List deviceports = new List();
static string GetIP()
{
while (true)
{
Process[] processes = Process.GetProcessesByName("NoxVMHandle");
foreach (Process process in processes)
{
ProcessStartInfo startInfo = new ProcessStartInfo("cmd", "/c netstat -a -n -o | find \"" + process.Id + "\" | find \"127.0.0.1\" | find \"620\"");
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardOutput = true;
var proc = new Process();
proc.StartInfo = startInfo;
proc.Start();
proc.WaitForExit();
MatchCollection matches = Regex.Matches(proc.StandardOutput.ReadToEnd(), "(?<=127.0.0.1:)62.*?(?= )");
foreach (Match match in matches)
{
if (match.Value != "" && !deviceports.Contains(match.Value))
{
deviceports.Add(match.Value);
return "127.0.0.1:" + match.Value;
}
}
}
}
}
Так же, если вы хотите работать в многопотоке, в каждом потоке необходимо создавать новый AdvancedAdbClient для стабильной работы.
Пример многопоточной работы
static ConcurrentQueue deviceports = new ConcurrentQueue(); // Используем потокобезопасный список
static object locker = new object();
static string GetIP()
{
while (true)
{
Process[] processes = Process.GetProcessesByName("NoxVMHandle");
foreach (Process process in processes)
{
ProcessStartInfo startInfo = new ProcessStartInfo("cmd", "/c netstat -a -n -o | find \"" + process.Id + "\" | find \"127.0.0.1\" | find \"620\"");
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardOutput = true;
var proc = new Process();
proc.StartInfo = startInfo;
proc.Start();
proc.WaitForExit();
MatchCollection matches = Regex.Matches(proc.StandardOutput.ReadToEnd(), "(?<=127.0.0.1:)62.*?(?= )");
foreach (Match match in matches)
{
if (match.Value != "" && !deviceports.Contains(match.Value))
{
deviceports.Enqueue(match.Value);
return "127.0.0.1:" + match.Value;
}
}
}
}
}
static void Main(string[] args)
{
GetIP();
if (!AdbServer.Instance.GetStatus().IsRunning)
{
AdbServer server = new AdbServer();
StartServerResult result = server.StartServer(@"F:\Nox\bin\adb.exe", false);
if (result != StartServerResult.Started)
{
Console.WriteLine("Can't start adb server");
return;
}
}
// Перед запуском необходимо добавить в Multi Drive 3 эмулятора
for (int i = 0; i < 3; i++) // Запускаем три эмулятора
{
new Thread(() =>
{
Process process = new Process();
process.StartInfo.FileName = @"F:\Nox\bin\Nox.exe";
process.StartInfo.Arguments = $"-clone:Nox_{i}"; // Запускаем i-тый эмулятор из Multi-Drive
process.Start();
AdvancedAdbClient client = new AdvancedAdbClient();
lock (locker) // Во избежании ошибок
{
client.Connect(GetIP()); // Ip Nox'а
}
DeviceData device = client.GetDevices().FirstOrDefault();
if (device == null)
{
Console.WriteLine("Can't connect to device");
return;
}
}).Start();
}
}
Автоматизация
Поиск элемента
Для поиска элемента используется метод FindElement и FindElements
static AdvancedAdbClient client;
static DeviceData device;
static void Main(string[] args)
{
client = new AdvancedAdbClient();
client.Connect("127.0.0.1:62001");
device = client.GetDevices().FirstOrDefault();
Element el = client.FindElement(device, "//node[@text='Login']");
}
Можно указать время ожидания элемента (по умолчанию его нету)
Element el = client.FindElement(device, "//node[@text='Login']", TimeSpan.FromSeconds(5));
Element[] els = client.FindElements(device, "//node[@resource-id='Login']", TimeSpan.FromSeconds(5));
Получение атрибута элемента
Каждый элемент содержит свои аттрибуты, для получения необходимо обратиться к полю attributes
static void Main(string[] args)
{
...
Element el = client.FindElement(device, "//node[@resource-id='Login']", TimeSpan.FromSeconds(3));
string eltext = el.attributes["text"];
string bounds = el.attributes["bounds"];
...
}
Клик по элементу
Вы можете кликнуть по координатам на экране
static void Main(string[] args)
{
...
client.Click(device, 600, 600); // Click on the coordinates (600;600)
...
}
Либо же по найденному элементу
static void Main(string[] args)
{
...
Element el = client.FindElement(device, "//node[@text='Login']", TimeSpan.FromSeconds(3));
el.Click();// Click on element by xpath //node[@text='Login']
...
}
Свайп
AdvancedSharpAdbClient позволяет свайпнуть от одного элемента к другому
static void Main(string[] args)
{
...
Element first = client.FindElement(device, "//node[@text='Login']");
Element second = client.FindElement(device, "//node[@text='Password']");
client.Swipe(device, first, second, 100); // Swipe 100 ms
...
}
И так же по координатам
static void Main(string[] args)
{
...
device = client.GetDevices().FirstOrDefault();
client.Swipe(device, 600, 1000, 600, 500, 100); // Swipe from (600;1000) to (600;500) on 100 ms
...
}
Ввод текста
Текст может быть абсолютно любым, но учтите, что adb не поддерживает кириллицу.
static void Main(string[] args)
{
...
client.SendText(device, "text"); // Элемент должен быть в фокусе
...
}
Так же можно автоматически фокусироваться на элементе (кликать, а потом вводить текст)
static void Main(string[] args)
{
...
client.FindElement(device, "//node[@resource-id='Login']").SendText("text"); // Автоматически фокусируется
...
}
Очистка поля ввода
Для очистки поля ввода необходимо указать максимальное количество символов в поле
static void Main(string[] args)
{
...
client.ClearInput(device, 25); // The second argument is to specify the maximum number of characters to be erased
...
}
Автоматически определять длинну текста (может работать неправильно)
static void Main(string[] args)
{
...
client.FindElement(device, "//node[@resource-id='Login']").ClearInput(); // Get element text attribute and remove text length symbols
...
}
Отправка нажатия клавиши (keyevent)
Список эвентов вы можете посмотреть тут
static void Main(string[] args)
{
...
client.SendKeyEvent(device, "KEYCODE_TAB");
...
}
Команды устройства
Все команды устройства вы можете посмотреть на страничке Github, здесь я покажу лишь 2 главных метода.
Установка apk
static void Main(string[] args)
{
...
PackageManager manager = new PackageManager(client, device);
manager.InstallPackage(@"C:\Users\me\Documents\mypackage.apk", reinstall: false);
manager.UninstallPackage("com.android.app");
...
}
Запуск приложений
static void Main(string[] args)
{
...
client.StartApp(device, "com.android.app");
client.StopApp(device, "com.android.app"); // force-stop
...
}
Важные ссылки
AdvancedSharpAdbClient — основная библиотека
SharpAdbClient — была взята за основу, является форком madb
madb — портированная версия ddmlib с Java
ddmlib — оригинальная библиотека для работы с adb
P.S
Я не являюсь противником посредников, типа Appium, просто эти фреймворки больше больше подходят для тестирования приложений, а не автоматизации.
Я являюсь только начинающим разработчиком, поэтому прошу строго не судить правильность. кода
Если у вас появились какие либо вопросы, пишите мне на почту.
Это моя первая статья на Habr, поэтому заранее извиняюсь за косяки.