Раньше описывал, как подружить ZennoPoster, Windows и эмулятор статья здесь ZennoPoster + Android Studio AVD. Но на практике эмулятор при свайпах съедает до ~2 ГБ видеопамяти на один экземпляр; у меня работали два, а локальная ИИ-модель тоже требует VRAM. Поэтому решил отказаться от эмулятора. Не ожидал, что так просто подключить физический телефон и задействовать его ресурсы: в итоге даже старый смартфон с разбитым стеклом за $30 разгружает ПК, освобождая те самые ~2 ГБ видеопамяти.
Мы подключаем физический Android-телефон по USB, запускаем Telegram, снимаем UI-дамп, парсим сообщения и управляем экраном — всё из C#-скрипта в ZennoPoster. Легко повторяемо.
Используем: телефон Samsung Galaxy A12
Что именно мы делаем
Подключаем телефон по USB. В PowerShell выполняем:
Видим:
Серийник (R58R70EYHHW) берём в код.
Базовый каркас: вызовы ADB из C#
Мы запускаем adb.exe напрямую и читаем вывод асинхронно, чтобы не ловить «пустой stdout».
Управление экраном и запуск Telegram
Мы будим устройство, снимаем блокировку и удерживаем экран включённым; далее стартуем Telegram.
Снятие дампа UI и запись в test
Сначала пытаемся exec-out /dev/tty, при пустом результате — фолбэк через файл.
Как мы парсим Telegram
Главная задача: «перейти в конец → svipe свайпов → сбор»
Мы сохраняем исходную логику: если кнопка не найдена — свайпы не делаем; каждый цикл — свайп → дамп → парс.
Настройки и устойчивость
Мы держим консервативные тайминги: SWIPE_DURATION_MS=1000, SWIPE_SLEEP_MS=400, DUMP_SETTLE_MS=320. На быстрых устройствах можем ускоряться (220–350 мс жест и ~200 мс паузы).
Если локализация отличается — расширяем XPath для @Content-desc кнопки «Перейти в конец». Если Telegram открыт не в ленте — запускаем его и входим в нужный чат заранее (интентом tg://resolve?...).
Итог
Мы управляем физическим телефоном напрямую через ADB из ZennoPoster, запускаем Telegram, снимаем XML-дамп интерфейса, парсим сообщения и прокручиваем ленту с контролем шага. Без сторонних приложений на телефоне и компьтере, а главное экономим ресурсы.
Написал полный сценарий — вставляем, настраиваем adbPath и deviceSerial, задаём svipe и запускаем.
Мы подключаем физический Android-телефон по USB, запускаем Telegram, снимаем UI-дамп, парсим сообщения и управляем экраном — всё из C#-скрипта в ZennoPoster. Легко повторяемо.
Используем: телефон Samsung Galaxy A12
Что именно мы делаем
- Поднимаем ADB-соединение с реальным телефоном по USB.
- Будим экран, снимаем блокировку, удерживаем дисплей включённым.
- Запускаем Telegram (оба варианта Activity), при необходимости проверяем фокус.
- Снимаем XML-дамп интерфейса (uiautomator dump) и пишем его в переменную test.
- Находим ленту сообщений (RecyclerView), кликаем «Перейти в конец», выполняем ровно svipe (количество заданное в переменной) свайпов вниз, после каждого — дамп и сбор текста.
- Складываем уникальные сообщения в «Список».
- ADB (platform-tools) по пути, например:
C:\Users\User\AppData\Local\Android\Sdk\platform-tools\adb.exe (путь оставил как у меня, кому нужно поменяет) - USB-драйвер устройства (у Samsung — «Samsung USB Driver»).
- На телефоне включаем Параметры разработчика → USB-отладка, подтверждаем RSA-ключ для ПК («Всегда разрешать»).
Подключаем телефон по USB. В PowerShell выполняем:
C#:
& "C:\Users\User\AppData\Local\Android\Sdk\platform-tools\adb.exe" kill-server
& "C:\Users\User\AppData\Local\Android\Sdk\platform-tools\adb.exe" start-server
& "C:\Users\User\AppData\Local\Android\Sdk\platform-tools\adb.exe" devices
C#:
List of devices attached
R58R70EYHHW device
Базовый каркас: вызовы ADB из C#
Мы запускаем adb.exe напрямую и читаем вывод асинхронно, чтобы не ловить «пустой stdout».
C#:
// Жёсткие настройки
string adbPath = @"C:\Users\User\AppData\Local\Android\Sdk\platform-tools\adb.exe";
string deviceSerial = "R58R70EYHHW";
const int CMD_TIMEOUT_MS = 10000;
string RunAdb(string args, int timeoutMs = CMD_TIMEOUT_MS)
{
var psi = new System.Diagnostics.ProcessStartInfo {
FileName = adbPath, Arguments = args,
UseShellExecute = false, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardError = true,
StandardOutputEncoding = System.Text.Encoding.UTF8,
StandardErrorEncoding = System.Text.Encoding.UTF8
};
using (var p = new System.Diagnostics.Process { StartInfo = psi })
{
var so = new System.Text.StringBuilder();
var se = new System.Text.StringBuilder();
p.OutputDataReceived += (s, e) => { if (e.Data != null) so.AppendLine(e.Data); };
p.ErrorDataReceived += (s, e) => { if (e.Data != null) se.AppendLine(e.Data); };
p.Start(); p.BeginOutputReadLine(); p.BeginErrorReadLine();
if (!p.WaitForExit(timeoutMs)) { try { p.Kill(); } catch {} }
if (!p.HasExited) { try { p.Kill(); } catch {} }
var err = se.ToString();
if (!string.IsNullOrWhiteSpace(err)) project.SendWarningToLog("[adb-err] " + err.Trim());
return so.ToString();
}
}
bool IsDeviceOnline(out bool unauthorized)
{
unauthorized = false;
var s = RunAdb("devices");
foreach (var line in (s ?? "").Replace("\r","").Split('\n"))
{
var l = (line ?? "").Trim();
if (string.IsNullOrEmpty(l) || l.StartsWith("List of devices")) continue;
var parts = l.Split(new[]{'\t',' '}, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2) continue;
if (parts[0] == deviceSerial)
{
var st = parts[1].ToLowerInvariant();
if (st.Contains("unauthorized")) { unauthorized = true; return false; }
return st == "device";
}
}
return false;
}
Мы будим устройство, снимаем блокировку и удерживаем экран включённым; далее стартуем Telegram.
C#:
// Экран
RunAdb($"-s {deviceSerial} shell input keyevent KEYCODE_WAKEUP");
RunAdb($"-s {deviceSerial} shell wm dismiss-keyguard");
RunAdb($"-s {deviceSerial} shell svc power stayon usb");
// Telegram: пробуем оба варианта Activity
var start = RunAdb($"-s {deviceSerial} shell am start -n org.telegram.messenger/.ui.LaunchActivity");
if (start.ToLowerInvariant().Contains("error"))
RunAdb($"-s {deviceSerial} shell am start -n org.telegram.messenger/org.telegram.ui.LaunchActivity");
Сначала пытаемся exec-out /dev/tty, при пустом результате — фолбэк через файл.
C#:
string ExtractXml(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return null;
int s = raw.IndexOf("<hierarchy", StringComparison.OrdinalIgnoreCase);
if (s < 0) return null;
int e = raw.LastIndexOf("</hierarchy>", StringComparison.OrdinalIgnoreCase);
if (e < 0) return null;
e += "</hierarchy>".Length;
if (e > raw.Length) e = raw.Length;
return raw.Substring(s, e - s);
}
string DumpUI()
{
string raw = RunAdb($"-s {deviceSerial} exec-out uiautomator dump /dev/tty");
string xml = ExtractXml(raw);
if (string.IsNullOrWhiteSpace(xml))
{
RunAdb($"-s {deviceSerial} shell uiautomator dump /sdcard/window_dump.xml");
string fileRaw = RunAdb($"-s {deviceSerial} exec-out cat /sdcard/window_dump.xml");
xml = ExtractXml(fileRaw);
}
if (string.IsNullOrWhiteSpace(xml))
throw new Exception("uiautomator не вернул XML");
project.Variables["test"].Value = xml; // всегда держим актуальный дамп
return xml;
}
- Лента — androidx.recyclerview.widget.RecyclerView.
- Сообщения — дочерние android.view.ViewGroup с непустым @text.
- Кнопка «Перейти в конец» — @clickable='true' и @Content-desc с ключевой фразой.
C#:
(int w, int h) GetScreenSize()
{
string s = RunAdb($"-s {deviceSerial} shell wm size");
var m = System.Text.RegularExpressions.Regex.Match(s ?? "", @"Physical\s+size:\s*(\d+)\s*x\s*(\d+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return m.Success ? (int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value)) : (720,1527);
}
int[] ParseBounds(string b)
{
var m = System.Text.RegularExpressions.Regex.Match(b ?? "", @"\[(\d+),(\d+)\]\[(\d+),(\d+)\]");
if (!m.Success) return null;
return new[]{ int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value), int.Parse(m.Groups[3].Value), int.Parse(m.Groups[4].Value) };
}
int[] FindRightChatRecyclerBounds(string xml)
{
var doc = new System.Xml.XmlDocument(); doc.LoadXml(xml);
var nodes = doc.SelectNodes("//node[@class='androidx.recyclerview.widget.RecyclerView']");
if (nodes == null || nodes.Count == 0) return null;
var (W,H) = GetScreenSize();
int[] best = null;
foreach (System.Xml.XmlNode n in nodes)
{
var bb = ParseBounds(n.Attributes?["bounds"]?.Value);
if (bb == null) continue;
if (bb[0] >= W/2) { best = bb; break; }
}
if (best == null)
{
var last = nodes[nodes.Count-1];
best = ParseBounds(last.Attributes?["bounds"]?.Value);
}
return best;
}
System.Collections.Generic.List<(string text,int top,int bottom)> ParseMessages(string xml)
{
var res = new System.Collections.Generic.List<(string text,int top,int bottom)>();
var doc = new System.Xml.XmlDocument(); doc.LoadXml(xml);
var nodes = doc.SelectNodes("//node[@class='androidx.recyclerview.widget.RecyclerView']/node[@class='android.view.ViewGroup' and string-length(@text)>0]");
if (nodes != null)
{
foreach (System.Xml.XmlNode n in nodes)
{
var a = n.Attributes;
string t = a?["text"]?.Value ?? "";
if (string.IsNullOrWhiteSpace(t)) continue;
var bb = ParseBounds(a?["bounds"]?.Value);
if (bb == null) continue;
res.Add((t, bb[1], bb[3]));
}
}
res.Sort((x,y)=> x.bottom.CompareTo(y.bottom)); // снизу — новее
return res;
}
(bool ok,int cx,int cy) FindBottomButton(string xml)
{
var doc = new System.Xml.XmlDocument(); doc.LoadXml(xml);
var node = doc.SelectSingleNode("//node[@clickable='true' and (@content-desc='Перейти в конец' or contains(@content-desc,'в конец'))]");
if (node == null) return (false,0,0);
var bb = ParseBounds(node.Attributes?["bounds"]?.Value);
if (bb == null) return (false,0,0);
int cx = Math.Max(bb[0]+5, Math.Min((bb[0]+bb[2])/2, bb[2]-5));
int cy = Math.Max(bb[1]+5, Math.Min((bb[1]+bb[3])/2, bb[3]-5));
return (true,cx,cy);
}
Мы сохраняем исходную логику: если кнопка не найдена — свайпы не делаем; каждый цикл — свайп → дамп → парс.
C#:
// Выбор количества свайпов из переменной проекта
string svipeStr = (project.Variables["svipe"]?.Value ?? "0").Trim();
int svipeCount = 0; int.TryParse(svipeStr, out svipeCount); if (svipeCount < 0) svipeCount = 0;
// Тайминги
const int SWIPE_DURATION_MS = 1000;
const int SWIPE_SLEEP_MS = 400;
const int DUMP_SETTLE_MS = 320;
const int INITIAL_SETTLE_MS = 900;
const int TAP_RETRY = 2;
const int PAUSE_AFTER_TAP_MS = 350;
// Сбор в «Список 1»
IZennoList outList = project.Lists["Список 1"];
outList.Clear();
try
{
bool una;
if (!IsDeviceOnline(out una))
throw new Exception(una ? "Телефон unauthorized — подтверждаем RSA на экране." : "Телефон не в состоянии device — проверяем кабель/драйвер/отладку.");
// Экран
RunAdb($"-s {deviceSerial} shell input keyevent KEYCODE_WAKEUP");
RunAdb($"-s {deviceSerial} shell wm dismiss-keyguard");
RunAdb($"-s {deviceSerial} shell svc power stayon usb");
if (INITIAL_SETTLE_MS > 0) System.Threading.Thread.Sleep(INITIAL_SETTLE_MS);
var pool = new System.Collections.Generic.List<(string text,int top,int bottom)>();
// Первый дамп и сбор
string xml0 = DumpUI();
pool.AddRange(ParseMessages(xml0));
// Геометрия свайпа
var rv = FindRightChatRecyclerBounds(xml0);
var (W,H) = GetScreenSize();
int SAFE_X, SW_START_Y, SW_END_Y;
if (rv != null)
{
SAFE_X = Math.Max(rv[0]+10, rv[2]-10);
SW_START_Y = rv[1] + 200;
SW_END_Y = rv[3] - 250;
if (SW_END_Y <= SW_START_Y) { SW_START_Y = rv[1]+120; SW_END_Y = rv[3]-120; }
}
else
{
SAFE_X = Math.Max(10, W - 20);
SW_START_Y = (int)(H * 0.35);
SW_END_Y = (int)(H * 0.44);
}
void SwipeDownRight()
{
RunAdb($"-s {deviceSerial} shell input swipe {SAFE_X} {SW_START_Y} {SAFE_X} {SW_END_Y} {SWIPE_DURATION_MS}");
System.Threading.Thread.Sleep(SWIPE_SLEEP_MS);
}
// Переход в конец
var btn = FindBottomButton(xml0);
if (btn.ok)
{
bool atBottom = false;
for (int tr=0; tr<=TAP_RETRY; tr++)
{
RunAdb($"-s {deviceSerial} shell input tap {btn.cx} {btn.cy}");
System.Threading.Thread.Sleep(PAUSE_AFTER_TAP_MS);
string after = DumpUI();
pool.AddRange(ParseMessages(after));
var still = FindBottomButton(after);
if (!still.ok) { atBottom = true; break; }
}
// Свайпы только если реально попали в конец
if (atBottom && svipeCount > 0)
{
for (int i = 0; i < svipeCount; i++)
{
SwipeDownRight();
string xml = DumpUI();
pool.AddRange(ParseMessages(xml));
}
}
}
else
{
project.SendInfoToLog("Кнопка 'Перейти в конец' не найдена — свайпы не выполняем.");
}
// Уникализация (снизу — новее)
pool.Sort((a,b)=> a.bottom.CompareTo(b.bottom));
var seen = new System.Collections.Generic.HashSet<string>();
foreach (var m in pool)
{
var t = (m.text ?? "").Trim();
if (t.Length == 0) continue;
if (seen.Add(t)) outList.Add(t);
}
project.SendInfoToLog($"[DONE] Уникальных сообщений: {outList.Count}. svipe={svipeCount}");
return 0;
}
catch (Exception ex)
{
project.SendErrorToLog("[ERR] " + ex.Message);
return -1;
}
Мы держим консервативные тайминги: SWIPE_DURATION_MS=1000, SWIPE_SLEEP_MS=400, DUMP_SETTLE_MS=320. На быстрых устройствах можем ускоряться (220–350 мс жест и ~200 мс паузы).
Если локализация отличается — расширяем XPath для @Content-desc кнопки «Перейти в конец». Если Telegram открыт не в ленте — запускаем его и входим в нужный чат заранее (интентом tg://resolve?...).
Итог
Мы управляем физическим телефоном напрямую через ADB из ZennoPoster, запускаем Telegram, снимаем XML-дамп интерфейса, парсим сообщения и прокручиваем ленту с контролем шага. Без сторонних приложений на телефоне и компьтере, а главное экономим ресурсы.
Написал полный сценарий — вставляем, настраиваем adbPath и deviceSerial, задаём svipe и запускаем.
Для запуска проектов требуется программа ZennoPoster.
Это основное приложение, предназначенное для выполнения автоматизированных шаблонов действий (ботов).
Подробнее...
Для того чтобы запустить шаблон, откройте программу ZennoPoster. Нажмите кнопку «Добавить», и выберите файл проекта, который хотите запустить.
Подробнее о том, где и как выполняется проект.




