C#: Автоматизация Android без посредников (adb)

eb2dd76d0f446fbbb893d8d85a13e313

Всем привет

Не давно понадобилось автоматизировать одно приложение.Мне не хотелось использовать какие то посредники по типу 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, поэтому заранее извиняюсь за косяки.

© Habrahabr.ru