Технология App Tethering в Delphi появилась относительно давно — в Delphi XE6. Эта замечательная технология позволяет нам связывать приложения, расположенные как на одном компьютере, так и на разных и, даже, запущенных в разных операционных системах. Только исходя из этого (связывание приложений, запущенных в разных ОС), технология App Tethering открывает для нас достаточно большие возможности в плане разработки и реализации идей.
Например, можно связать небольшое приложение Android («клиент») и приложение для Windows («сервер») и обрабатывать на компьютере данные с датчиков, которые присылает мобильный телефон. Или организовать передачу файлов на свой планшет или телефон без использования usb-кабелей, например, как это сделано в приложении AirDroid. В общем и целом, возможности использования App Tethering в Delphi ограничиваются лишь фантазией разработчика.
В этой заметке я решил разобраться, что из себя представляет App Tethering теперь — в Delphi 10.1 Berlin.
Транспортные протоколы App Tethering
Работа App Tethering в Delphi не зависит от специфического транспорта или протокола. И мы можем, в случае необходимости, реализовать через App Tethering API свой собственный протокол обмена данными, но уже «из коробки» компоненты для связывания приложений поддерживают IP (Internet Protocol) и Bluetooth. Согласно данным из wiki Embarcadero, в Delphi 10.1 Berlin, поддержка Classic Bluetooth пока не реализована только для iOS:
Платформа | IP-соединения | Bluetooth-соединения |
Windows | ||
OS X | ||
iOS | ||
Android |
Назначение компонентов App Tethering в Delphi
Для того, чтобы начать использовать App Tethering в своих приложениях, Вам достаточно всего два компонента:
- TTetheringManager — компонент для обнаружения других приложений, использующих App Tethering
- TTetheringAppProfile — компонент для выполнения действий и обмена данными с другими приложениями.
В самом общем случае работу App Tethering в Delphi можно представить следующим образом (кликните по картинке, чтобы увеличить):
Рассмотрим схему на рисунке подробнее. Итак, имеется два приложения: «Приложение 1» и «Приложение 2». Обратите внимание, что оба приложения равнозначны. Такую технологию называют «peer-to-peer», то есть оба приложения как бы сочетают в себе и клиент и сервер.
Шаг 1 — обнаружение. Компонент TTetheringManager из «Приложения 1», используя заданный протокол ищет приложения, которые также используют App Tethering. Если в «Приложении 2» TTetheringManager использует тот же самый протокол, то он будет обнаружен и тогда мы переходим к шагу 2.
Шаг 2 — связывание менеджеров. Менеджер первого приложения устанавливает связь с TTetheringManager «Приложения 2». Два спаренных менеджера могут предоставлять друг другу информацию о своих профилях (TTetheringProfile).
Шаг 3 — обнаружение удаленных профилей. TTetheringManager первого приложения предоставляет информацию о своих профилях менеджеру второго приложения и наоборот. Получив информацию об удаленных профилях производится их соединение (Connect).
Шаг 4 — соединение профилей. Профиль TTetheringProfile «Приложения 1» соединяется с профилем TTetheringProfile «Приложения 2», используя данные о типе соединения своего менеджера (TTetheringManager). Если соединение прошло успешно, то профили могут обмениваться данными и выполнять удаленные действия.
Шаг 5 — выполнение действий и обмен данными. Установив соединение с удаленным профилем, TTetheringProfile «Приложения 1» может вызывать действия в «Приложение 2» и наоборот.
Выполнение действий и обмен данными выполняется до тех пор, пока удаленный менеджер активен и доступен. Если один из менеджеров оказывается вне зоны доступа (например, если для спаривания используется Bluetooth, то информация о нем сохраняется, чтобы впоследствии было возможно восстановить утерянную связь).
Исходя из всего вышеизложенного, можно выделить основное назначение компонентов App Tethering в Delphi.
TTetheringManager
Менеджер может обнаружить и произвести сопряжение с удаленными менеджерами, которые используют профили (TTetheringProfile) для обмена данными с зарегистрированными профилями вашего менеджера.
Каждый менеджер (TTetheringManager) может быть связан в приложении с одним или несколькими профилями (TTetheringProfile).
Основное назначение менеджера:
- использует свои разрешенные адаптеры для обнаружения других менеджеров.
- устанавливает соединения с удаленными менеджерами (спаривание).
- два спаренных менеджеры позволяют их профилям знать о профилях другого менеджера. Таким образом профили могут обмениваться данными друг с другом.
- Менеджер предоставляет информацию от своих адаптерах протоколам своих профилей. В результате профили могут обмениваться данными друг с другом, используя свои протоколы.
В приложении может быть более одного TTetheringManager, но для нормальной работы одного TTetheringManager часто бывает вполне достаточно.
TTetheringAppProfile
Профиль в App Tethering определяет, какими данными обмениваться с удаленными профилями и обрабатывает данные, полученные от удаленных профилей. Профиль всегда связан с менеджером.
Основное назначение профиля:
- может подключиться к удаленному профилю ранее обнаруженного с помощью менеджера
- может разрешить или запретить входящие запросы на подключение.
- может отправлять или получать команды от подключенного профиля.
- может обмениваться данными с удаленными профилями.
- может быть временно отключен, чтобы остановить любое общение с другими профилями.
- если удаленный и локальный профили состоят в одной группе, то такое менеджеры могут связываться друг с другом автоматически.
Так же, как и в случае TTetheringManager, в приложении может использоваться один или несколько профилей, в зависимости от того, насколько сложное ваше приложение или от того, какие функции и задачи вы хотите возложить на профиль.
Использование App Tethering в Delphi
Тестовые приложения
Работа с App Tethering начинается с использования свойств и методов компонента TTetheringManager.
Для примера, создадим два приложения у которых на главных формах будет (пока) всего по три компонента:
- TTetheringManager
- TTetheringAppProfile
- TMemo — для вывода разного рода служебной информации по соединению и выполнению удаленных действий.
Для того, чтобы и вам и мне было легче ориентироваться в том, с каким приложением мы работаем в данный момент, я сделал такие приложения:
Приложение VCL (Windows)
Назовем его «VCL Client». Главная форма приложения представлена на рисунке:
Здесь
- VCLManager: TTetheringManager
- VCLProfile: TTetheringAppProfile
Приложение FMX (Android)
Второе приложение я решил сделать для Android и использовать для этого, соответственно, FMX. Конечно, можно было бы обойтись двумя VCL-приложениями или, наоборот — двумя FMX, но я решил также показать вам и то, что сделать связь приложений VCL<->FMX, также просто, как и VCL<->VCL или FMX<->FMX.
Главная форма приложения выглядит (пока) также просто, как и в случае с VCL:
Теперь разберемся со свойствами компонентов App Tethering, расположенных на формах приложений и перейдем к решению практических задач использования App Tethering в Delphi.
Published-свойства TTetheringManager
Свойство | Тип | Описание | Значение | |
VCL Client | Android Client | |||
AllowedAdapters | string | Доступные типы подключений (адаптеры).
Предопределенные значения: Network, Network_V6, Bluetooth, Network_V4. Вы также можете зарегистрировать свой тип подключения (адаптер) и использовать его имя в этом свойстве. Подробнее об этом свойстве см. ниже |
Network | Network |
Enabled | boolean | Указывает будет ли менеджер доступен для обнаружения другими менеджерами и использоваться для сопряжений | True | True |
Name | string | Имя компонента для использования в исходном коде приложения | VCLManager | AndroidManager |
Password | string | Строка, содержащая пароль для подключения. Этот пароль должен быть известен удаленному менеджеру для того, чтобы сопряжение было успешным | 123456 | 123456 |
Text | string | Строка, которая содержит описание вашего менеджера для удаленных менеджеров | VCLManagerDesc | AndroidManagerDesc |
Свойство AllowedAdapters может содержать несколько значений, разделенных знаком |. Например, вы можете задать такое значение свойства «Network | Bluetooth». При этом соединение будет осуществляться по первому указанному значению в менеджере, который инициировал соединение.
Что скрывается за строками в свойстве AllowedAdapters — см. таблицу:
Значение свойства (Adapter Type) | Класс | Технология соединения |
Network | TTetheringNetworkAdapterV4_UDP | IPv4 broadcast addresses (UDP) |
Network_V6 | TTetheringNetworkAdapterMulticast_V6 | IPv6 multicast addresses |
Bluetooth | TTetheringBluetoothAdapter | Bluetooth |
Network_V4 | TTetheringNetworkAdapterMulticast_V6 | IPv4 multicast addresses |
Если вы используете адаптер «Network» (broadcast adresses), то, соответственно, для вашего приложения в firewall должно быть разрешение на получение широковещательных пакетов (UDP).
Published-свойства TTetheringAppProfile
Свойство | Тип | Описание | Значение | |
VCL Client | Android Client | |||
Actions | TActionCollection | Коллекция действий (Actions), доступных для использования совместно с удаленными профилями.
Для использования свойства на форме приложения должен находится компонент TActionList с набором действий (Actions) |
Пустая коллекция | Пустая коллекция |
Enabled | boolean | Указывает доступен ли профиль для обмена данными и выполнения действий удаленными профилями | True | True |
Group | string | Строка, определяющая группу профилей, которые могут соединяться автоматически | AppTesting | AppTesting |
Manager | TTetheringManager | Менеджер профиля | VCLManager | AndriodManager |
Name | string | Имя компонента для использования в исходном коде | VCLProfile | AndroidProfile |
Resources | TResourceCollection | Коллекция ресурсов, которые могут использоваться совместно с другими профилями | Пустая коллекция | Пустая коллекция |
Text | string | Описание профиля для удаленных профилей | VCLProfileDesc | AndroidProfileDesc |
Visible | boolean | Указывает должен ли менеджер профиля передавать сведения об этом профиле удаленным или нет | True | True |
Разобравшись немного с публичными свойствами компонентов, можно приступать к работе с App Tethering. И, для начала, попробуем соединить одно приложение с другим.
Соединение приложений, использующих App Tethering
Технология App Tethering в Delphi использует два подхода к связыванию (соединению) приложений: в автоматическом режиме и в ручном. Автоматический режим отличается своей простотой — достаточно вызвать всего один метод TTetheringManager и ваши приложения соединятся между собой (автоматически выполняются 4 из 5 шагов, показанных на первом рисунке). Ручной режим — более гибкий и универсальный. В ручном режиме вы контролируете все шаги соединения (см. первый рисунок) и можете управлять процессом. Рассмотрим оба варианта.
Автоматическое соединение
У наших тестовых приложений задана одна и та же группа «AppTesting», поэтому мы можем воспользоваться автоматическим соединением. Для этого в обоих приложениях в обработчике OnCreate формы напишем:
VCL Client:
procedure TForm4.FormCreate(Sender: TObject); begin VclManager.AutoConnect(); end;
Android Client:
procedure TForm4.FormCreate(Sender: TObject); begin AndroidManager.AutoConnect(); end;
После выполнения метода AutoConnect у менеджера возникает событие OnEndAutoConnect:
OnEndAutoConnect: TNotifyEvent
Определим обработчик этого события в обоих приложениях (ниже приведен код только для Android, для VCL-приложения достаточно поменять в коде название менеджера):
procedure TForm4.AndroidManagerEndAutoConnect(Sender: TObject); var I: Integer; begin Memo1.Lines.Clear; Memo1.Lines.Add('EndAutoConnect'#13#10'Remote Managers:'); for I := 0 to pred(AndroidManager.RemoteManagers.Count) do begin Memo1.Lines.Add(' MANAGER '+i.ToString); Memo1.Lines.Add(' Name '+AndroidManager.RemoteManagers[i].ManagerName); Memo1.Lines.Add(' Description '+AndroidManager.RemoteManagers[i].ManagerText); Memo1.Lines.Add(' Identifier '+AndroidManager.RemoteManagers[i].ManagerIdentifier); Memo1.Lines.Add(' Connection string '+AndroidManager.RemoteManagers[i].ConnectionString); Memo1.Lines.Add('-------------'); end; Memo1.Lines.Add('Remote Profiles'); for I := 0 to Pred(AndroidManager.RemoteProfiles.Count) do begin Memo1.Lines.Add('Profile '+i.ToString); Memo1.Lines.Add(' Description '+AndroidManager.RemoteProfiles[i].ProfileText); Memo1.Lines.Add(' Manager Identifier '+AndroidManager.RemoteProfiles[i].ManagerIdentifier); Memo1.Lines.Add(' Identifier '+AndroidManager.RemoteProfiles[i].ProfileIdentifier); Memo1.Lines.Add(' Group '+AndroidManager.RemoteProfiles[i].ProfileGroup); Memo1.Lines.Add(' Type '+AndroidManager.RemoteProfiles[i].ProfileType); Memo1.Lines.Add(' Version '+AndroidManager.RemoteProfiles[i].ProfileVersion.ToString); Memo1.Lines.Add('-------------'); end; end;
После выполнения автоматического соединения мы должны получить в нашем приложении список всех спаренных менеджеров и список всех удаленных профилей. Во-первых, запустим «VCL Client». результат запуска представлен на рисунке:
Так как «VCL Client» запущен первым, то, соответственно, менеджер приложения ничего не нашел в сети. Если же мы теперь запустим «Android Client», то это приложение должно соединиться с «VCL Client» и вид формы приложения будет таким:
Как видно по скриншоту, приложение на Android нашло в сети наш «VCL Client», запущенный в Windows и соединилось с ним. Может возникнуть вполне резонный вопрос:
после выполнения AutoConnect будет ли «VCL Client» что-либо знать об удаленном менеджере («Android Client»)? Ведь в Windows-клиенте событие OnEndAutoConnect уже произошло и менеджер вернул пустые списки удаленных менеджеров и профилей.
Ответ: Будет. После соединения (любого: автоматического или ручного) оба менеджера будут знать всю информацию друг о друге.
Проверить это достаточно просто: добавьте в VCL Client на форму кнопку TButton и напишите в OnClick следующую строчку кода:
procedure TForm3.Button1Click(Sender: TObject); begin VCLManagerEndAutoConnect(self) end;
После этого выполните следующие действия:
- Запустите «VCL Client» — менеджер вернет после выполнения AutoConnect пусты списки удаленных профилей и менеджеров
- Запустите «Android Client» — его менеджер обнаружит ранее запущенный VCLManager и его профиль (VCLProfile)
- Теперь вернитесь в «VCL Client» и нажмите кнопку — менеджер покажет вам список, в котором будет присутствовать «AndroidManager» и «AndroidProfile»
Подведем итоги по автоматическому соединению в App Tethering:
- Для соединения в автоматическом режиме профили должны состоять в одной и той же группе (свойство Group содержит одну и ту же строку)
- Автоматическое соединение выполняется после вызова метода AutoConnect() у TTetheringManager
- Вызов AutoConnect() приводит к тому, что TTetheringManager сканирует вашу подсеть и ищет в ней все менеджеры с профилями, состоящими в той же группе, что и локальный. Производится их соединение.
- По окончанию автоматического соединения вызывается событие OnEndAutoConnect.
Что следует предусмотреть перед тем как использовать автоматическое соединение в App Tethering:
- Название группы обязательно должно быть уникальным иначе можно наломать дров, если в подсети окажется ещё одно или несколько приложений, использующих App Tethering.
- Чтобы никто другой не мог присоединиться к вашему менеджеру — используйте пароль. Это же стоит помнить и при ручном режиме соединения.
Соединение вручную
Как отмечалось выше, соединение вручную обладает большей гибкостью. Чтобы выполнить соединение нам необходимо выполнить следующие шаги:
- Вызвать метод DiscoverManagers() у TTetheringManager для обнаружения приложений, использующих App Tethering. Когда вызов завершится сработает событие OnEndManagersDiscovery.
- Прочитать список обнаруженных RemoteManagers у TTetheringManager и вызвать метод TTetheringManager.PairManager для тех удаленных менеджеров, с которыми необходимо установить соединение. Каждый раз, когда происходит сопряжение менеджера с удаленным менеджером вызывается событие OnEndProfilesDiscovery.
- Прочитать список обнаруженных RemoteProfiles у TTetheringManager, и вызвать метод Connect() вашего TTetheringAppProfile для подключения к тому профилю, который необходим.
Удаленные менеджеры и удаленные профили являются экземплярами TTetheringManagerInfo и TTetheringProfileInfo, соответственно (см. пример выше).
Попробуем выполнить эти шаги по очереди. Для этого, я изменил «VCL Client» следующим образом:
- Метод AutoConnect() вынесен в обработчик кнопки «Auto Connect»
- Кнопка «Refresh» обновляет в Memo информацию об удаленных менеджерах и профилях
- Кнопка «Discover Manager» запускает первый шаг подключения — вызывает метод DiscoverManagers() у VCLManager
- Кнопка «Connect» вызывает метод Connect() для профиля, выбранного в ComboBox «Profile».
В начале рассмотрим обработчики событий OnClick кнопок «Discover Managers» и «Connect».
Обработчик OnClick у кнопки «Discover Managers»:
procedure TForm3.Button2Click(Sender: TObject); begin VCLManager.DiscoverManagers(); end;
Обработчик OnClick у кнопки «Connect»:
procedure TForm3.Button4Click(Sender: TObject); var Profile: TTetheringProfileInfo; begin Profile:=FindProfile(cbProfiles.Items[cbProfiles.ItemIndex]); if Profile.ProfileIdentifier<>EmptyStr then VCLProfile.Connect(Profile) else raise Exception.Create('RemoteProfile not found'); end;
Здесь метод FindProfile() вспомогательный — ищет в списке VCLManager.RemoteProfiles профиль по его описанию:
function TForm3.FindProfile(const AProfileText: string): TTetheringProfileInfo; var I: Integer; begin Result.ProfileIdentifier:=EmptyStr; for I := 0 to Pred(VCLManager.RegisteredProfiles.Count) do if SameText(AProfileText, VCLManager.RemoteProfiles[i].ProfileText) then Exit(VCLManager.RemoteProfiles[i]) end;
Теперь рассмотрим обработчики событий менеджера.
Обработчик OnEndManagersDiscovery
var I: Integer; begin cbManagers.Items.Clear;//очистили список менеджеров {выводим информацию по удаленным менеджерам} Memo1.Lines.Add('EndManagersDiscovery'); for I := 0 to Pred(ARemoteManagers.Count) do begin cbManagers.Items.Add(ARemoteManagers[I].ManagerText); Memo1.Lines.Add(' Name ' + ARemoteManagers[I].ManagerName); Memo1.Lines.Add(' Description ' + ARemoteManagers[I].ManagerText); Memo1.Lines.Add(' Identifier ' + ARemoteManagers[I].ManagerIdentifier); Memo1.Lines.Add(' Connection string ' + ARemoteManagers[I].ConnectionString); end; end;
Обработчик события OnEndProfilesDiscovery:
var I: Integer; begin cbProfiles.Items.Clear;//очистили список профилей SetRemoteProfiles(ARemoteProfiles);//заносим в список Profiles профили {выводим информацию по удаленным профилям} Memo1.Lines.Add('EndManagersDiscovery'); for I := 0 to Pred(ARemoteProfiles.Count) do begin Memo1.Lines.Add(' Description ' + ARemoteProfiles[I].ProfileText); Memo1.Lines.Add(' Manager Identifier ' + ARemoteProfiles[I].ManagerIdentifier); Memo1.Lines.Add(' Identifier ' + ARemoteProfiles[I].ProfileIdentifier); Memo1.Lines.Add(' Group ' + ARemoteProfiles[I].ProfileGroup); Memo1.Lines.Add(' Type ' + ARemoteProfiles[I].ProfileType); Memo1.Lines.Add(' Version ' + ARemoteProfiles[I].ProfileVersion.ToString); end; end;
Обработчик события OnRequestManagerPassword:
procedure TForm3.VCLManagerRequestManagerPassword(const Sender: TObject; const ARemoteIdentifier: string; var Password: string); begin Memo1.Lines.Add('RequestManagerPassword. Manager Identifier: ' + ARemoteIdentifier); Password := '123456'; end;
Этого кода будет достаточно, чтобы произвести соединение с удаленным менеджером, подключиться к удаленному профилю и вывести информацию о подключении в Memo.
Чтобы проверить работу нашего «VCL Client» запустите приложения в таком порядке:
- Android Client
- VCL Client
- Нажмите кнопку «Discover Managers»
- Выберите в списке Managers менеджер «AndroidManager»
- Выберите в списке Profiles профиль «AndroidProfileDesc»
- Нажмите кнопку «Connect»
Результат работы приложения показан на рисунке:
Как видите, мы получили информацию о менеджерах, о профилях и о том, что удаленный профиль попросил пароль. Однако нам до сих пор не известно успешно ли прошло соединение или нет. Для того, чтобы отследить весь процесс сопряжения менеджеров мы можем воспользоваться следующими событиями у TTetheingManager:
Событие | Описание |
OnAuthErrorFromLocal | Возникает во время операции спаривания (PairManager), когда удаленный менеджер, который запускает запрос на сопряжение посылает хэш пароля, который не совпадает с ожидаемым хэшем, и в результате ваш менеджер прерывает операцию спаривания. |
OnAuthErrorFromRemote | Возникает во время операции спаривания (PairManager) начатой вашим менеджером, когда удаленный менеджер прерывает операцию спаривания потому, что хэш пароля, который посылает ваш менеджер не совпадает с ожидаемым хэшем. |
OnPairedFromLocal | Возникает при успешном установлении соединения, инициированного удаленным менеджером. |
OnPairedToRemote | Возникает при успешном установлении соединения инициированного вашим менеджером. |
Таким образом, используя эти и рассмотренные ранее события компонента TTetheringManager, мы можем отследить весь процесс соединения приложений вплоть до операции Connect() у TTetheringAppProfile. Если изобразить весь этот процесс в виде упрощенной схемы, то получим примерно следующую картину работы двух приложений:
Цифрами на рисунке обозначена последовательность основных операций.
Теперь осталось разобраться с тем, как отследить соединение двух профилей, когда наш профиль (VCLProfile) выполняет метод Connect() (см. обработчик OnClick у кнопки «Connect»).
Для того, чтобы отследить процесс соединения нам пригодятся два события у TTetheringAppProfile:
Событие | Описание |
OnBeforeConnectProfile | Это событие возникает в момент, когда удаленный профиль пытается соединиться с нашим. Здесь мы можем прервать процесс соединения. |
OnAfterConnectProfile | Возникает, когда удаленный профиль успешно соединился с нашим профилем. |
Проверим как работают эти события. Во-первых, напишем такой обработчик OnBeforeConnectProfile у компонента VCLProfile:
procedure TForm3.VCLProfileBeforeConnectProfile(const Sender: TObject; const AProfileInfo: TTetheringProfileInfo; var AllowConnect: Boolean); begin Memo1.Lines.Add('BeforeConnectProfile '+AProfileInfo.ProfileText); AllowConnect:=True;//разрешаем соединение end;
Здесь мы просто разрешаем удаленному профилю соединиться с нашим. В рабочих проектах в этом обработчике можно, например, проверить информацию об удаленном профиле (в поле AProfileInfo) и, в случае необходимости, запретить соединение.
Обработчик OnAfterConnectProfile у VCLProfile:
procedure TForm3.VCLProfileAfterConnectProfile(const Sender: TObject; const AProfileInfo: TTetheringProfileInfo); var I: Integer; begin Memo1.Lines.Add('AfterConnectProfile '+AProfileInfo.ProfileText); Memo1.Lines.Add('---Connected Profiles---'); for I := 0 to Pred(VCLProfile.ConnectedProfiles.Count) do Memo1.Lines.Add(VCLManager.ProfileInfoToString(VCLProfile.ConnectedProfiles[i])) end;
Здесь мы выводим список подключенных профилей в Memo. При этом, мы воспользовались методом ProfileInfoToString() компонента VCLManager, чтобы представить информацию о профиле в виде строки.
Теперь, чтобы проверить процесс соединения выполните следующую последовательность:
- Запускаем «VCL Client»
- Запускаем «Android Client» — он автоматически соединится с «VCL Client»
Результат работы представлен на рисунке:
Итак, подведем итог по подключению вручную в App Tethering:
- Ручное подключение позволяет контролировать весь процесс соединения и, в случае необходимости, мы можем запретить конкретному удаленному профилю подключиться к нашему профилю.
- Как и в случае с автоматическим подключением, после подключения оба менеджера «знают» друг о друге всё, включая перечень удаленных профилей.
Теперь, когда мы научились соединяться с удаленными профилями, можно перейти к следующей части — выполнению действий.
Выполнение удаленных действий в App Tethering
Технология App Tethering в Delphi позволяет вызывать удаленные действия (Actions), обмениваться строками и данными с другими профилями. Для того, чтобы профиль VCLProfile смог запустить удаленное действие оно должно находиться в коллекции Actions удаленного профиля AndroidProfile (см. описание этого свойства выше).
Простой запуск удаленных действий
Для начала напишем простенький пример, демонстрирующий запуск удаленных действий. Открываем проект приложения «AndroidClient», бросаем на главную форму приложения компонент:
В TActionList создаем новое пользовательское действие (я намеренно сейчас оставляю свойства этого действия со значениями по умолчанию). В итоге, в списке появится действие с именем (Name) «Action1». Обработчик OnExecute у этого действия будет простым:
procedure TForm4.Action1Execute(Sender: TObject); begin Memo1.Lines.Add('Action1 Executed') end;
Теперь открываем редактор свойств TTetheringAppProfile.Actions и добавляем новое действие (TLocalActions) в список. У этого локального действия указываем следующие свойства:
- Action: Action1
- IsPublic: True
- Kind: Shared
- Name: raMemo
- NotifyUpdate: false
Теперь наше приложение для Android, в случае вызова созданного действия должно будет вывести в Memo строку «Action1 Executed«. На этом работа с AndroidClient временно завершена. Переходим к VCLClient.
В «VCL Client» добавляем на главную форму приложения ещё одну кнопку и пишем у неё следующий обработчик OnClick:
var I: Integer; Actions: TList; J: Integer; begin for I := 0 to Pred(VCLProfile.ConnectedProfiles.Count) do begin Actions:=VCLProfile.GetRemoteProfileActions(VCLProfile.ConnectedProfiles[I]); for J := 0 to Pred(Actions.Count) do begin Memo1.Lines.Add(Actions[i].Name+' Caption: '+Actions[i].Caption+' Hint: '+Actions[i].Hint); Actions[i].Execute; end; end; end;
Давайте разберемся, что происходит в этом обработчике.
- Мы проходим по всем подключенным удаленным профилям (цикл for со счётчиком i)
- Используя метод GetRemoteProfileActions(), получаем список действий удаленного профиля, которые были опубликованы (свойство TLocalAction.IsPublic=True)
- Проходим во втором цикле по всему списку действий и выполняем их.
Так как у нас в AndroidClient есть всего одно действие, то и результатом выполнения такого обработчика будет одна строка в Memo. Этот пример показывает как достаточно просто можно получить список всех действий удаленного профиля и демонстрирует их запуск, однако, в реальном приложении такой подход может привести к самым плачевным результатам. Поэтому разберемся с тем как запустить определенное действие в профиле TTetheringAppProfile?
Для запуска удаленных действий мы можем воспользоваться следующими методами TTetheringAppProfile:
Метод | Описание |
function RunRemoteAction(const AnAction: TRemoteAction): Boolean; overload; | Запускает удаленное действие AnAction: TRemoteAction, полученное, например, при вызове метода GetRemoteProfileActions().
Возвращает True в случае успешного запуска. |
function RunRemoteAction(const AProfile: TTetheringProfileInfo; const AnActionName: string): Boolean; | Запускает удаленное действие с именем AnActionName профиля AProfile.
Возвращает True в случае успешного запуска. |
function RunRemoteActionAsync(const AnAction: TRemoteAction): TRemoteActionHandle; overload; | Запускает удаленное действие AnAction: TRemoteAction асинхронно. Чтобы прочитать статус выполнения операции см. методы GetRemoteActionAsyncState.
Возвращает хэндл запущенного действия. |
function RunRemoteActionAsync(const AProfile: TTetheringProfileInfo; const AnActionName: string): TRemoteActionHandle; overload; | Запускает асинхронно удаленное действие с именем AnActionName профиля AProfile.
Возвращает хэндл запущенного действия. |
function GetRemoteActionAsyncState(const AProfile: TTetheringProfileInfo; ARemoteActionHandle: TRemoteActionHandle): TRemoteActionState; overload; | Возвращает статус запущенного асинхронно удаленного события с хэндлом ARemoteActionHandle, находящегося в профиле AProfile. Результат может принимать одно из трех значений:
TRemoteActionState = (Running, NotRunning, NotFound); |
function GetRemoteActionAsyncState(const AnAction: TRemoteAction): TRemoteActionState; overload; | Возвращает статус запущенного асинхронно удаленного события AnAction.
Результат выполнения функции см.выше. |
function GetRemoteActionAsyncState(const AProfile: TTetheringProfileInfo; const AnActionName: string): TRemoteActionState; overload; | Возвращает статус запущенного асинхронно удаленного события с именем AnActionName, находящегося в профиле AProfile.
Результат выполнения функции см.выше. |
В качестве демонстрации, воспользуемся одним из этих методов и перепишем обработчик OnClick у «VCLClient» следующим образом:
procedure TForm3.Button5Click(Sender: TObject); var I: Integer; begin for I := 0 to Pred(VCLProfile.ConnectedProfiles.Count) do begin if VCLProfile.RunRemoteAction(VCLProfile.ConnectedProfiles[I], 'raMemo') then Memo1.Lines.Add('Удаленное действие для профиля ' + VCLProfile.ConnectedProfiles[I].ProfileText + ' успешно выполнено') else Memo1.Lines.Add('Удаленное действие для профиля ' + VCLProfile.ConnectedProfiles[I].ProfileText + ' не выполнено') end; end;
Перейдем к следующему вопросу — «заркальное» выполнение удаленных действий.
Зеркальный запуск удаленных действий
Свойство Kind у TLocalAction может принимать два значения:
type TTetheringRemoteKind = (Shared, Mirror);
С тем, как запускать действия со значением Kind=Shared мы разобрались. Более интересных результатов можно достичь, используя зеркальный запуск. Для чего это может пригодиться? Например, для управления приложением, которое запущено на другом компьютере: управление с телефона плеером, запущенным на компьютере — самый просто пример «зазеркаливания» действий: двигаем ползунок громкости на телефоне и автоматически меняется громкость на компьютере и т.д.
Чтобы действие выполнялось зеркально необходимо:
- Чтобы ваше действие было с Kind=Mirror
- Чтобы удаленное действие было c Kind=Shared.
Попробуем «зазеркалить» действие, которое было создано в «AndroidClient». Для этого модифицируем наш «VCLClient» следующим образом:
- Добавляем на форму приложения компонент TActionList
- В ActionList создаем новое действие «Action1» со следующим обработчиком OnExecute:
procedure TForm3.Action1Execute(Sender: TObject); begin Memo1.Lines.Add('Local Action1 Executed') end;
3. Добавляем в VCLProfile.Actions новое действие со следующими свойствами:
- Action: Action1
- IsPublic: True
- Kind: Mirror
- Name: raMemo
- NotifyUpdate: False
4. Добавляем на главную форму приложения новую кнопку и указываем у этой кнопки в свойстве Action наш Action1 из ActionList. Вид приложения показан на рисунке:
Теперь запустите оба приложения (не важно в каком порядке будет происходить запуск, главное — соединиться с удаленным профилем) и нажмите кнопку «Run Mirror». Вы увидите, что в «VCLClient» появится строка «Local Action1 Executed«, а в «AndroidClient» — «Action1 Executed«.
Подведем итоги по запуску удаленных действий:
- Удаленный действия могу запускаться «зеркально» (связка Shared+Mirror удаленного и локального действия)
- Удаленный действия могут запускаться синхронно или асинхронно. В случае асинхронного запуска необходимо выполнить метод GetRemoteActionAsyncState(), чтобы получить состояние запущенного действия.
- Список действий удаленного профиля можно получить, используя метод GetRemoteProfileActions() у TTetheringAppProfile.
Обмен данными в App Tethering
При обмене данными между приложениями мы можем использовать два типа ресурсов:
- Постоянные ресурсы. В качестве постоянного ресурса может выступать стандартный тип данных (integer, boolean, string, single, double) или TStream; Удаленные приложения могу запрашивать значения постоянных ресурсов и подписываться на обновления таких ресурсов.
- Временные ресурсы. В качестве временного ресурса могут выступать строки или потоки. Такие ресурсы отправляются, как правило, один раз и более не используются. Удаленные приложения не могут подписаться на обновление временного ресурса.
Рассмотрим работы с обоими видами ресурсов.
Работа с постоянными ресурсами
Создадим в AndroidClient три постоянных ресурса. Для этого откроем редактор свойства TTetheringAppProfile.Resources и последовательно добавим три элемента со следующими значениями:
Свойство | Описание | Resource0 | Resource1 | Resource2 |
IsPublic | Виден ли ресурс удаленным профилям | True | True | True |
Kind | Тип ресурса | Shared | Shared | Shared |
Name | Имя ресурса | rsString | rsInteger | rsStream |
ResType | Тип данных ресурса | Data | Data | Stream |
Таким образом, мы создали три постоянных ресурса у первых двух из которых значение будет string и integer, соответственно, а третий будет содержать поток данных.
После того, как мы создали ресурсы в design-time, нам необходимо присвоить им значения. Значения присваиваются в run-time следующим образом (дописываем обработчик OnCreate у главной формы приложения AndroidClient):
procedure TForm4.FormCreate(Sender: TObject); begin ResStream:=TStringStream.Create; AndroidManager.AutoConnect(); AndroidProfile.Resources.FindByName('rsString').Value:='Hello from Android!'; AndroidProfile.Resources.FindByName('rsInteger').Value:=12345; AndroidProfile.Resources.FindByName('rsStream').Value:=ResStream; end;
Здесь ResStream — это:
TForm4 = class(TForm) ... private ResStream: TStringStream; end;
Теперь мы можем прочитать все три ресурса из нашего приложения «VCLClient«. Для начала, прочитаем значения ресурсов, используя какое-либо событие, например, клик по кнопке.
Переходим к проекту «VCLClient» добавляем на главную форму кнопку «Get Resources» и пишем в её обработчике OnClick следующий код:
procedure TForm3.Button7Click(Sender: TObject); const cResDescr = 'Name: %s Value: %s'; var RemoteResources: TList; I, J: Integer; Resource:TRemoteResource; ResVal: string; begin for I := 0 to Pred(VCLProfile.ConnectedProfiles.Count) do begin RemoteResources:=VCLProfile.GetRemoteProfileResources(VCLProfile.ConnectedProfiles[i]); for J := 0 to Pred(RemoteResources.Count) do begin Resource:=VCLProfile.GetRemoteResourceValue(RemoteResources[j]); case Resource.ResType of TRemoteResourceType.Data:begin case Resource.Value.DataType of TResourceType.Integer: ResVal:=Resource.Value.AsInteger.ToString; TResourceType.Single: ResVal:=Resource.Value.AsSingle.ToString; TResourceType.Double: ResVal:=Resource.Value.AsDouble.ToString; TResourceType.Int64: ResVal:=Resource.Value.AsInt64.ToString; TResourceType.Boolean: ResVal:=Resource.Value.AsBoolean.ToString; TResourceType.String: ResVal:=Resource.Value.AsString; end; end; TRemoteResourceType.Stream: ResVal:=Resource.ToJsonString; end; Memo1.Lines.Add(Format(cResDescr,[Resource.Name, ResVal])) end; end; end;
Здесь мы делаем следующее:
- Проходим по списку подключенных удаленных профилей
- У каждого удаленного профиля запрашиваем его ресурсы, используя метод GetRemoteProfileResources()
- Проходим по списку ресурсов удаленного профиля и запрашиваем значение ресурса, используя метод GetRemoteResourceValue().
- Выводим значение в Memo. При этом, если ресурс — это поток, то выводим в Memo его JSON-представление, используя метод ресурса ToJsonString.
Вид приложения «VCLClient» после чтения значений ресурсов у «AndroidClient» представлен на рисунке:
Как говорилось выше, мы можем подписываться на обновление удаленных ресурсов. В случае, если мы подписаны на изменение какого-либо ресурса, то каждый раз, как удаленный ресурс будет изменяться, наш локальный профиль будет об этом знать. Чтобы подписаться на обновление какого-либо ресурса, мы можем воспользоваться двумя способами:
- Как в случае с действиями, «отзеркалить» ресурс. Для этого необходимо создать локальный ресурс с таким же именем, что и удаленный и установить значение Kind = Mirror.
- Воспользоваться парой методов у профиля: SubscribeToRemoteItem() — для подписки на обновление и, противоположным методом UnSubscribeFromRemoteItem() — для того, чтобы отписаться от обновлений.
Я воспользовался первым способом и создал в «VCLClient» ресурс со следующими свойствами:
У этого ресурса в обработчике события я написал следующее:
procedure TForm3.VCLProfileResources0ResourceReceived(const Sender: TObject; const AResource: TRemoteResource); begin Memo1.Lines.Add('Recived Resource0:'); Memo1.Lines.Add('Name: '+AResource.Name); TStringStream(AResource.Value.AsStream).SaveToFile('test.txt'); end;
Теперь, чтобы содержимое ресурса изменялось, в «AndroidClient» я добавил на форму новую кнопку и в её обработчике OnClick написал следующее:
procedure TForm4.Button1Click(Sender: TObject); begin ResStream.Clear; ResStream.WriteString(Memo1.Lines.Text); AndroidProfile.Resources.FindByName('rsStream').Broadcast; Memo1.Lines.Add('Resource Updated') end;
То есть, каждый раз при нажатии на кнопку в поток будет записываться содержимое Memo и сохраняться на компьютере в виде текстового файла.
Вид приложения для Android теперь такой:
Теперь можно запустить оба приложения, соединить их и попробовать нажать кнопку в AndroidClient — вы увидите, что VCLClient сообщит об изменении ресурса и создаст рядом с exe-файлом текстовый файл, содержащий строки из Memo.
Продолжая тему обновлений ресурсов, можно также сказать, что для того, чтобы получать информацию именно об обновлении ресурса, а не просто его получении, как мы это сделали выше, можно воспользоваться событием профиля OnResourceUpdated(). Можете самостоятельно создать его обработчик в VCLClient и перенести в него код из обработчика OnResourceReceived() — результат работы программы конкретно в этом случае не изменится.
Следующий момент — работа с временными ресурсами.
Работа с временными ресурсами
Для отправки временных ресурсов удаленным менеджерам у TTetheringAppProfile есть два метода:
function SendString(const AProfile: TTetheringProfileInfo; const Description, AString: string): Boolean; | Отправляет профилю AProfile строку AString с описанием Description.
В случае успешной отправки возвращает True. |
function SendStream(const AProfile: TTetheringProfileInfo; const Description: string; const AStream: TStream): Boolean; | Отправляет профилю AProfile поток AStream с описанием Description.
В случае успешной отправки возвращает True. |
У профиля, которому посылается временный ресурс срабатывают соответственно два события:
OnAfterReciveData: TTetheringDataEvent;
type TTetheringDataEvent = function(const Sender: TObject; const ADataBuffer: TByteDynArray): TByteDynArray of object; |
Срабатывает после того, как профиль получил данные от удаленного профиля. Можно использовать для предварительной обработки данных |
OnAfterReciveStream: TTetheringStreamEvent
type TTetheringStreamEvent = procedure(const Sender: TObject; const AInputStream, AOutputStream: TStream) of object; |
Срабатывает после того, как профиль получил поток данных от удаленного профиля. |
В профиле который посылает временный ресурс могут обрабатываться следующие события:
OnBeforeSendData: TTetheringDataEvent | Происходит перед тем, как данные будут переданы удаленному профилю |
OnBeforeSendStream: TTetheringStreamEvent | Происходит перед тем, как поток данных будет передан удаленному профилю |
Разобравшись немного с теорией, попробуем ответить на такой вопрос: как при помощи App Tethering в Delphi передать в Android файл?
Для этого добавим в VCLClient новую кнопку «Send File» и диалог TOpenDialog. В обработчике события OnClick кнопки напишем:
procedure TForm3.Button8Click(Sender: TObject); var I: Integer; FS: TFileStream; begin if OpenDialog1.Execute then begin FS := TFileStream.Create(OpenDialog1.FileName, fmOpenRead); try for I := 0 to Pred(VCLProfile.ConnectedProfiles.Count) do if VCLProfile.SendStream(VCLProfile.ConnectedProfiles[i],ExtractFileName(FS.FileName),FS) then Memo1.Lines.Add('Файл '+ExtractFileName(FS.FileName)+' отправлен '+VCLProfile.ConnectedProfiles[i].ProfileText) else Memo1.Lines.Add('Ошибка отправки файла '+ExtractFileName(FS.FileName)+' профилю '+VCLProfile.ConnectedProfiles[i].ProfileText) finally FreeAndNil(FS) end; end; end;
Вид приложения стал таким:
Теперь перейдем в проект «AndroidClient» и напишем обработчик ResourceReceived() у TTetheringAppProfile:
procedure TForm4.AndroidProfileResourceReceived(const Sender: TObject; const AResource: TRemoteResource); var FS: TFileStream; begin if AResource.ResType = TRemoteResourceType.Stream then begin Memo1.Lines.Add('Resource '+AResource.Hint); FS:=TFileStream.Create(TPath.Combine(TPath.GetSharedDocumentsPath, AResource.Hint), fmCreate or fmOpenWrite); try AResource.Value.AsStream.Position:=soFromBeginning; FS.CopyFrom(AResource.Value.AsStream, AResource.Value.AsStream.Size); Memo1.Lines.Add('Resource save to '+FS.FileName); finally FS.Free end; end; end;
Теперь все принимаемые файлы будут сохраняться в папке документов. Проверить это довольно просто:
- Запустите оба приложения и соедините их
- Затем нажмите кнопку «Send File» в VCLClient, выберите любой файл и нажмите «Ok» в окне диалога
- В зависимости от того, на сколько большой файл вы выбрали, подождите пока в Android программа не напишет вам путь к сохраненному файлу
- Можете открыть новый файл в андроид.
Аналогичным образом в App Tethering обрабатываются и строки.
Подведем итоги:
- В App Tethering два типа ресурсов: постоянные и временные. На постоянные ресурсы можно подписываться и получать уведомления об их изменении, «отзеркаливать» их и т.д. Временные ресурсы используются, как правило, единожды, но с помощью них можно легко организовать, например, беспроводную передачу файлов на мобильное устройство Android (см. пример выше).
- При «отзеркаливании» потоков данных необходимо после обновления локального ресурса вызывать его метод Broadcast(), чтобы удаленные профили «поймали» обновление.
Завершение сессий в App Tethering
Помимо того, что нам необходимо соединять устройства в App Tethering нам так же может быть необходимо отслеживать момент, когда профиль отсоединяется или же самим вручную разрывать соединение с удаленным профилем.
Чтобы разорвать соединение с удаленным профилем в TTetheringAppProfile используется метод:
procedure Disconnect(const AProfile: TTetheringProfileInfo); override;
После того, как наш профиль отключается от удаленного срабатывает событие OnDisconnect:
OnDisconnect: TTetheringProfileEvent; type TTetheringProfileEvent = procedure(const Sender: TObject; const AProfileInfo: TTetheringProfileInfo) of object;
Если же удаленный менеджер прекращает свою работу (выключается программа, мобильник отключается от вашей подсети и т.д.), то у TTetheringManager срабатывает событие:
OnRemoteManagerShutdown: TTetheringRemoteManagerShutdownEvent; type TTetheringRemoteManagerShutdownEvent = procedure(const Sender: TObject; const AManagerIdentifier: string) of object;
Продемонстрировать работы метода Disconnect() и событий можно довольно просто. Добавим в VCLClient последнюю кнопку «Disconnect» и напишем у неё в обработчике OnClick следующий код:
procedure TForm3.Button9Click(Sender: TObject); var i: integer; begin for I := 0 to Pred(VCLProfile.ConnectedProfiles.Count) do VCLProfile.Disconnect(VCLProfile.ConnectedProfiles[i]); end;
Теперь добавим обработчик события OnDisconnect() нашего профиля VCLProfile:
procedure TForm3.VCLProfileDisconnect(const Sender: TObject; const AProfileInfo: TTetheringProfileInfo); begin Memo1.Lines.Add('Disconnect '+AProfileInfo.ProfileText); end;
и обработчик OnRemoteManagerShutdown у менеджера TTetheringManager:
procedure TForm3.VCLManagerRemoteManagerShutdown(const Sender: TObject; const AManagerIdentifier: string); begin Memo1.Lines.Add('Manager '+AManagerIdentifier+' Shutdown'); end;
Теперь запустите оба приложения и нажмите кнопку «Disconnect» — вы увидите в логе приложения соответствующие надписи об отключении от удаленного профиля. Если же вы вначале выключите AndroidClient, то в логе появится сообщение о том, что удаленный менеджер выключен.
На этом обзор App Tethering в Delphi можно считать завершенным. Конечно, обзор не охватывает всех возможностей этой технологии. Так «остались за бортом» такие, на мой взгляд, интересные с точки зрения реализации вопросы, как:
- Подключение к удаленному профилю, который находится за пределами вашей подсети (использование параметров Target в методе Connect()).
- Создание своих протоколов обмена данными (адаптеров)
и другие вопросы, которые могут у вас возникнуть в процессе изучения технологии App Tethering. Однако, приведенных выше сведения, я полагаю, будет вполне достаточно, чтобы начать работу. Ну, а, в зависимости от специфики решаемых задач, будут появляться уже конкретные вопросы, которые, я уверен, вы сможете решить :)
Книжная полка
Здравствуйте.
Если протоколом указать Bluetooth, то устройства не видят друг друга.
Тестировал на двух смартфонах. Права на использование bluetooth ставил.
Не видят они друг друга. В чем может быть проблема, подскажите пожалуйста?