:: MVP ::
|
|
:: RSS ::
|
|
|
Иногда, общаясь с заказчиками, возникает необходимость установить на их Android устройство
демонстрационную версию приложения. Вариант с установкой приложения из исходников не удобен,
так как он требует наличия под рукой как самих исходников, так и среды разработки. Можно закачать
пакет на устройство, но и в этом случае можно столкнуться с отсутствием на телефоне файлового
менеджера или специализированной утилиты по установке пакетов. И вот на этом фоне возникла идея
написать небольшое приложение, призванное максимально упростить процесс установки программных
пакетов на Android устройства заказчиков.
В качестве основы для реализации задуманного я решил использовать консольную утилиту adb.exe,
которая является составной частью Android SDK, основным назначением которой является установление
связи между устройством и компьютером через USB или Wi-Fi соединение (для установки связи нужно в
настройках разработчика на телефоне разрешить отладку по USB).
Общий вид команды adb следующий:
adb [-d|-e|-s <serialNumber>] <command>
|
Если на хосте отладки работает только один эмулятор, или подключено только одно устройство, то команда adb
подключится к нему по умолчанию. Если работает несколько эмуляторов или подключено несколько устройств, то
нужно через опции -d, -e или -s указать целевое устройство для подключения – кому предназначена команда command.
- -d: Направляет команду adb на любое подключенное (не эмулятор) устройство. Возвращает ошибку,
если подключено больше одного устройства;
- -e: Направляет команду adb на любой запущенный экземпляр эмулятора. Возвращает ошибку, если запущено
больше одного экземпляра эмулятора;
- -s <serialNumber>: Направляет команду adb на специально указанный экземпляр эмулятора/устройства по
его серийному номеру (о нем чуть ниже).
Некоторые из команд, которые пригодятся нам для решения данной задачи.
- devices: Выводит список всех подключенных экземпляров эмуляторов/устройств. Результатом команды будет
список подключенных устройств и их состояние;
- Serial number – строка, созданная adb для уникальной идентификации экземпляра эмулятора или устройства.
Именно этот номер нужно указывать после ключа -s;
- State – состояние соединения. Оно может быть одним из нескольких вариантов: offline, device, no device,
unauthorized.
- install [-lrtsdg] – Проталкивает приложение (нужно указать полный путь к файлу .apk на хосте
отладки) на эмулятор или устройство. Из всех имеющихся у команды ключей мы используем 2:
- -r – заменять установленное приложение;
- -s – установить приложение на съёмный носитель (sdcard).
Концепция приложения будет следующей. Так как утилита adb – консольная, будем читать её вывод через пайпы. А так
как выполнение команды может занять некоторое время, целесообразно делать это в отдельном потоке.
type
PThreadData = ^TThreadData;
TThreadData = record
AppName, CmdLine: string;
Output: TStrings;
end;
threadvar
ThreadData: TThreadData;
function ExecCommand(Parameter: Pointer): Integer;
procedure ReadFromConsole(AppName, CmdLine: string; var Output: TStrings);
const
ReadBuffer = 2400;
var
Security: TSecurityAttributes;
ReadPipe, WritePipe: THandle;
Start: TStartupInfo;
ProcessInfo: TProcessInformation;
Buffer: PAnsiChar;
BytesRead: DWORD;
Apprunning: DWORD;
begin
with Security do
begin
nLength := SizeOf(TSecurityAttributes);
bInheritHandle := True;
lpSecurityDescriptor := nil;
end;
if Createpipe(ReadPipe, WritePipe, @Security, 0) then
begin
Buffer := AllocMem(ReadBuffer+1);
FillChar(Start, Sizeof(Start), #0);
Start.cb := SizeOf(Start);
Start.hStdOutput := WritePipe;
Start.hStdInput := ReadPipe;
Start.dwFlags := STARTF_USESTDHANDLES + STARTF_USESHOWWINDOW;
Start.wShowWindow := SW_HIDE;
if CreateProcess(nil, PChar(AppName + ' ' + CmdLine), @Security,
@Security, True, NORMAL_PRIORITY_CLASS, nil, nil, Start, ProcessInfo) then
begin
repeat
Apprunning := WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
ReadFile(ReadPipe, Buffer[0], ReadBuffer, BytesRead, nil);
Buffer[BytesRead] := #0;
OemToAnsi(Buffer, Buffer);
Output.Text := Output.text + String(Buffer);
until Apprunning = WAIT_OBJECT_0;
end;
FreeMem(Buffer);
CloseHandle(ProcessInfo.hProcess);
CloseHandle(ProcessInfo.hThread);
CloseHandle(ReadPipe);
CloseHandle(WritePipe);
end;
end;
var
lpThreadData: PThreadData;
begin
lpThreadData := Parameter;
ReadFromConsole(lpThreadData^.AppName, lpThreadData^.CmdLine, lpThreadData^.Output);
EndThread(0);
end;
|
Так как в отдельном потоке нужно запускать всего 1 процедуру, нет смысла создавать для этого экземпляр объекта
TThread, вместо этого воспользуемся функцией BeginThread. В нее мы передадим указатель на процедуру ExecCommand,
которая и будет работать в отдельном потоке, и указатель на данные для нее (в нашем случае на структуру типа
TThreadData). Поля AppName и CmdLine этой структуры представляют собой входные данные для функции, а через поле
Output процедура будет возвращать результат своей работы.
Первым делом нужно получить список подключенных устройств.
procedure TMainForm.Btn_DevicesRefreshClick(Sender: TObject);
var
ThreadHandle: THandle;
ThreadId, WaitResult: DWORD;
Devices: TStrings;
IsDevice: Boolean;
i: Integer;
begin
Devices := TStringList.Create;
LB_Devices.Clear;
Btn_ApkInstall.Enabled := False;
PB_InstallProgress.Style := pbstMarquee;
Btn_DevicesRefresh.Enabled := False;
ThreadData.AppName := 'adb.exe';
ThreadData.CmdLine := 'devices';
ThreadData.Output := Devices;
ThreadHandle := BeginThread(nil, 0, @ExecCommand, @ThreadData, 0, ThreadId);
repeat
WaitResult := WaitForSingleObject(ThreadHandle, 10);
Application.ProcessMessages;
until WaitResult = WAIT_OBJECT_0;
CloseHandle(ThreadHandle);
Btn_DevicesRefresh.Enabled := True;
if Devices.Count > 1 then
begin
for i := Devices.Count-1 downto 0 do
begin
IsDevice := TRegEx.IsMatch(Devices[i], '(offline|device|no device|unauthorized)$');
if (not IsDevice) or (Trim(Devices[i]) = '') then
Devices.Delete(i)
else
Devices[i] := Trim(TRegEx.Match(Devices[i], '^.+\t').Value) + ' (' +
Trim(TRegEx.Match(Devices[i], '\t.+$').Value) + ')';
end;
end;
PB_InstallProgress.Style := pbstNormal;
LB_Devices.Items := Devices;
Devices.Free;
end;
|
Если список успешно получен, его нужно немного обработать, а именно удалить все строки, не содержащие идентификаторов
устройств, так как в зависимости от условий запуска (например, не запущен adb сервер), вывод команды может различаться.
List of devices attached
adb server is out of date. killing...
* daemon started successfully *
4adb249 unauthorized
List of devices attached
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
4adb249 device
List of devices attached
4adb249 device
|
Как определить эти строки? Я решил определять их по отсутствию в них состояние соединения (см. описание вывода команды
devices выше).
Теперь осталось установить приложение на устройство. Выбрав из списка устройство нужно проверить его статус, и только
если он равен "device", активировать кнопку установки.
procedure TMainForm.LB_DevicesClick(Sender: TObject);
begin
if (LB_Devices.Items.Count = 0) or (LB_Devices.ItemIndex < 0) then
begin
Btn_ApkInstall.Enabled := False;
Exit;
end;
Btn_ApkInstall.Enabled :=
not TRegEx.IsMatch(LB_Devices.Items[LB_Devices.ItemIndex],
'\((offline|no device|unauthorized)\)$');
end;
procedure TMainForm.Btn_ApkInstallClick(Sender: TObject);
var
ThreadHandle: THandle;
ThreadId, WaitResult: DWORD;
Result: TStrings;
i: Integer;
Device: string;
begin
if LB_Devices.ItemIndex < 0 then
begin
ShowMessage('Не выбрано устройство, на которое нужно произвести установку');
Exit;
end;
if not FileExists(Ed_ApkFile.Text) then
begin
ShowMessage('Не выбран файл для установки');
Exit;
end;
Result := TStringList.Create;
PB_InstallProgress.Style := pbstMarquee;
Btn_DevicesRefresh.Enabled := False;
Btn_ApkSelect.Enabled := False;
Btn_ApkInstall.Enabled := False;
Device := Trim(TRegEx.Match(LB_Devices.Items[LB_Devices.ItemIndex], '^.+\ ').Value);
ThreadData.AppName := 'adb.exe';
ThreadData.CmdLine := Format('-s %s install%s%s "%s"',
[Device, IfThen(CB_ReplacePackage.Checked, ' -r', ''),
IfThen(CB_InstallOnSDCard.Checked, ' -s', ''), Ed_ApkFile.Text]);
ThreadData.Output := Result;
ThreadHandle := BeginThread(nil, 0, @ExecCommand, @ThreadData, 0, ThreadId);
repeat
WaitResult := WaitForSingleObject(ThreadHandle, 10);
Application.ProcessMessages;
until WaitResult = WAIT_OBJECT_0;
CloseHandle(ThreadHandle);
Btn_ApkInstall.Enabled := True;
Btn_ApkSelect.Enabled := True;
Btn_DevicesRefresh.Enabled := True;
if Result.Count > 0 then
begin
for i := Result.Count-1 downto 0 do
if Trim(Result[i]) = '' then
Result.Delete(i)
else
Break;
for i := 0 to Result.Count-1 do
Result[i] := Trim(Result[i]);
end;
PB_InstallProgress.Style := pbstNormal;
ShowMessage(Result.Text);
Result.Free;
end;
|
После ряда проверок формируется командная строка, в которую (при необходимости) добавляется ключ для переустановки приложения (-r)
или для установки на съемный носитель (-s). И вновь вывод команды подвергается небольшой косметической обработке.
Чтобы пользователь не думал, что приложение зависло, в данном примере я использовал компонент TProgressBar. На время выполнения
команд я меняю его стиль с pbstNormal на pbstMarquee. Конечно, красивее (с моей субъективной точки зрения) использовать для этой
цели компонент TActivityIndicator, но так как он появился сравнительно недавно (в RAD Studio 10 Seattle) я решил на включать его
в пример, с целью улучшения обратной совместимости с более ранними версиями среды разработки. Тем не менее на иллюстрации к статье
я показал, как это может выглядеть.
На сегодня все, удобных всем инсталляций!
P.S.
Скомпилированный пример можно скачать в разделе "Проекты".
.: Пример к данной статье :.
|
При использовании материала - ссылка на сайт обязательна
|
|