Практика
Автоматизация установки приложений на Android

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: 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 : Направляет команду 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.
Скомпилированный пример можно скачать в разделе "Проекты".

.: Пример к данной статье :.


При использовании материала - ссылка на сайт обязательна