уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Технология 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 yes yes
OS X yes yes
iOS yes remove
Android yes yes

Назначение компонентов App Tethering в Delphi

Для того, чтобы начать использовать App Tethering в своих приложениях, Вам достаточно всего два компонента:

  1. TTetheringManager — компонент для обнаружения других приложений, использующих App Tethering
  2. TTetheringAppProfile — компонент для выполнения действий и обмена данными с другими приложениями.

В самом общем случае работу App Tethering в Delphi можно представить следующим образом (кликните по картинке, чтобы увеличить):

apptethering

Рассмотрим схему на рисунке подробнее. Итак, имеется два приложения: «Приложение 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).

Основное назначение менеджера:

  1. использует свои разрешенные адаптеры для обнаружения других менеджеров.
  2. устанавливает соединения с удаленными менеджерами (спаривание).
  3. два спаренных менеджеры позволяют их профилям знать о профилях другого менеджера. Таким образом профили могут обмениваться данными друг с другом.
  4. Менеджер предоставляет информацию от своих адаптерах протоколам своих профилей. В результате профили могут обмениваться данными друг с другом, используя свои протоколы.

В приложении может быть более одного TTetheringManager, но для нормальной работы одного TTetheringManager часто бывает вполне достаточно.

TTetheringAppProfile

Профиль в App Tethering определяет, какими данными обмениваться с удаленными профилями и обрабатывает данные, полученные от удаленных профилей. Профиль всегда связан с менеджером.

Основное назначение профиля:

  1. может подключиться к удаленному профилю ранее обнаруженного с помощью менеджера
  2. может разрешить или запретить входящие запросы на подключение.
  3. может отправлять или получать команды от подключенного профиля.
  4. может обмениваться данными с удаленными профилями.
  5. может быть временно отключен, чтобы остановить любое общение с другими профилями.
  6. если удаленный и локальный профили состоят в одной группе, то такое менеджеры могут связываться друг с другом автоматически.

Так же, как и в случае TTetheringManager, в приложении может использоваться один или несколько профилей, в зависимости от того, насколько сложное ваше приложение или от того, какие функции и задачи вы хотите возложить на профиль.

В дальнейшем, при рассмотрении конкретных примеров, я буду использовать в каждом приложении по одному компоненту TTetheringManager и одному TTetheringAppProfile.

Использование App Tethering в Delphi

Тестовые приложения

Работа с App Tethering начинается с использования свойств и методов компонента TTetheringManager.

Для примера, создадим два приложения у которых на главных формах будет (пока) всего по три компонента:

  • TTetheringManager
  • TTetheringAppProfile
  • TMemo — для вывода разного рода служебной информации по соединению и выполнению удаленных действий.

Для того, чтобы и вам и мне было легче ориентироваться в том, с каким приложением мы работаем в данный момент, я сделал такие приложения:

Приложение VCL (Windows)

Назовем его «VCL Client». Главная форма приложения представлена на рисунке:

Главная форма приложения "VCL Client"

Главная форма приложения «VCL Client»

Здесь

  1. VCLManager: TTetheringManager
  2. VCLProfile: TTetheringAppProfile
Приложение FMX (Android)

Второе приложение я решил сделать для Android и использовать для этого, соответственно, FMX. Конечно, можно было бы обойтись двумя VCL-приложениями или, наоборот — двумя FMX, но я решил также показать вам и то, что сделать связь приложений VCL<->FMX, также просто, как и VCL<->VCL или FMX<->FMX.

Главная форма приложения выглядит (пока) также просто, как и в случае с VCL:

Главная форма приложения "Android Client"

Главная форма приложения «Android Client»

Теперь разберемся со свойствами компонентов 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 строку «Network | Bluetooth», а второй менеджер «Bluetooth | Network», то соединение будет происходить с использованием адаптера «Network» 

Что скрывается за строками в свойстве 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).

О том, как добавить свое правило в Firewall Windows можно узнать здесь.
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 шагов, показанных на первом рисунке). Ручной режим — более гибкий и универсальный. В ручном режиме вы контролируете все шаги соединения (см. первый рисунок) и можете управлять процессом. Рассмотрим оба варианта.

Автоматическое соединение
Для того, чтобы приложения могли соединяться в автоматическом режиме они должны принадлежать к одной и той же группе (см. published-свойство Group у TTetheringAppProfile).

У наших тестовых приложений задана одна и та же группа «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». результат запуска представлен на рисунке:

Результат выполнения AutoConnect

Результат выполнения AutoConnect

Так как «VCL Client» запущен первым, то, соответственно, менеджер приложения ничего не нашел в сети. Если же мы теперь запустим «Android Client», то это приложение должно соединиться с «VCL Client» и вид формы приложения будет таким:

Результат выполнения AutoConnect

Результат выполнения AutoConnect

Как видно по скриншоту, приложение на Android нашло в сети наш «VCL Client», запущенный в Windows и соединилось с ним. Может возникнуть вполне резонный вопрос:

после выполнения AutoConnect будет ли «VCL Client» что-либо знать об удаленном менеджере («Android Client»)? Ведь в Windows-клиенте событие OnEndAutoConnect уже произошло и менеджер вернул пустые списки удаленных менеджеров и профилей.

Ответ: Будет. После соединения (любого: автоматического или ручного) оба менеджера будут знать всю информацию друг о друге.

Проверить это достаточно просто: добавьте в VCL Client на форму кнопку TButton и напишите в OnClick следующую строчку кода:

procedure TForm3.Button1Click(Sender: TObject);
begin
  VCLManagerEndAutoConnect(self)
end;

После этого выполните следующие действия:

  1. Запустите «VCL Client» — менеджер вернет после выполнения AutoConnect пусты списки удаленных профилей и менеджеров
  2. Запустите «Android Client» — его менеджер обнаружит ранее запущенный VCLManager и его профиль (VCLProfile)
  3. Теперь вернитесь в «VCL Client» и нажмите кнопку — менеджер покажет вам список, в котором будет присутствовать «AndroidManager» и «AndroidProfile»

Подведем итоги по автоматическому соединению в App Tethering:

  1. Для соединения в автоматическом режиме профили должны состоять в одной и той же группе (свойство Group содержит одну и ту же строку)
  2. Автоматическое соединение выполняется после вызова метода AutoConnect() у TTetheringManager
  3. Вызов AutoConnect() приводит к тому, что TTetheringManager сканирует вашу подсеть и ищет в ней все менеджеры с профилями, состоящими в той же группе, что и локальный. Производится их соединение.
  4. По окончанию автоматического соединения вызывается событие OnEndAutoConnect.

Что следует предусмотреть перед тем как использовать автоматическое соединение в App Tethering:

  1. Название группы обязательно должно быть уникальным иначе можно наломать дров, если в подсети окажется ещё одно или несколько приложений, использующих App Tethering.
  2. Чтобы никто другой не мог присоединиться к вашему менеджеру — используйте пароль. Это же стоит помнить и при ручном режиме соединения.
Соединение вручную

Как отмечалось выше, соединение вручную обладает большей гибкостью. Чтобы выполнить соединение нам необходимо выполнить следующие шаги:

  1. Вызвать метод DiscoverManagers() у TTetheringManager для обнаружения приложений, использующих App Tethering. Когда вызов завершится сработает событие OnEndManagersDiscovery.
  2. Прочитать список обнаруженных RemoteManagers у TTetheringManager и вызвать метод TTetheringManager.PairManager для тех удаленных менеджеров, с которыми необходимо установить соединение. Каждый раз, когда происходит сопряжение менеджера с удаленным менеджером вызывается событие OnEndProfilesDiscovery.
  3. Прочитать список обнаруженных RemoteProfiles у TTetheringManager, и вызвать метод Connect() вашего TTetheringAppProfile для подключения к тому профилю, который необходим.
Если удаленный менеджер защищен паролем, то после вызова метода PairManager() сработает событие OnRequestManagerPassword в обработчике которого необходимо указать пароль удаленного менеджера для сопряжения.

Удаленные менеджеры и удаленные профили являются экземплярами TTetheringManagerInfo и TTetheringProfileInfo, соответственно (см. пример выше).

Попробуем выполнить эти шаги по очереди. Для этого, я изменил «VCL Client» следующим образом:

Обновленный "VCL Client"

Обновленный «VCL Client»

  1. Метод AutoConnect() вынесен в обработчик кнопки «Auto Connect»
  2. Кнопка «Refresh» обновляет в Memo информацию об удаленных менеджерах и профилях
  3. Кнопка «Discover Manager» запускает первый шаг подключения — вызывает метод DiscoverManagers() у VCLManager
  4. Кнопка «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&lt;&gt;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.

Обращаю ваше внимание, что второе приложение — «Android Client» я никаким образом не изменял — это приложение также будет работать в автоматическом режиме и подключаться при запуске ко всем доступным менеджерам и профилям.

Чтобы проверить работу нашего «VCL Client» запустите приложения в таком порядке:

  1. Android Client
  2. VCL Client
  3. Нажмите кнопку «Discover Managers»
  4. Выберите в списке Managers менеджер «AndroidManager»
  5. Выберите в списке Profiles профиль «AndroidProfileDesc»
  6. Нажмите кнопку «Connect»

Результат работы приложения показан на рисунке:

Подключение вручную

Подключение вручную

Как видите, мы получили информацию о менеджерах, о профилях и о том, что удаленный профиль попросил пароль. Однако нам до сих пор не известно успешно ли прошло соединение или нет. Для того, чтобы отследить весь процесс сопряжения менеджеров мы можем воспользоваться следующими событиями у TTetheingManager:

Событие Описание
OnAuthErrorFromLocal Возникает во время операции спаривания (PairManager), когда удаленный менеджер, который запускает запрос на сопряжение посылает хэш пароля, который не совпадает с ожидаемым хэшем, и в результате ваш менеджер прерывает операцию спаривания.
OnAuthErrorFromRemote Возникает во время операции спаривания (PairManager) начатой вашим менеджером, когда удаленный менеджер прерывает операцию спаривания потому, что хэш пароля, который посылает ваш менеджер не совпадает с ожидаемым хэшем.
OnPairedFromLocal Возникает при успешном установлении соединения, инициированного удаленным менеджером.
OnPairedToRemote Возникает при успешном установлении соединения инициированного вашим менеджером.

Таким образом, используя эти и рассмотренные ранее события компонента TTetheringManager, мы можем отследить весь процесс соединения приложений вплоть до операции Connect() у TTetheringAppProfile. Если изобразить весь этот процесс в виде упрощенной схемы, то получим примерно следующую картину работы двух приложений:

Процесс сопряжения двух менеджеров App Tethering

Процесс сопряжения двух менеджеров App Tethering

Цифрами на рисунке обозначена последовательность основных операций.

Теперь осталось разобраться с тем, как отследить соединение двух профилей, когда наш профиль (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, чтобы представить информацию о профиле в виде строки.
Теперь, чтобы проверить процесс соединения выполните следующую последовательность:

  1. Запускаем «VCL Client»
  2. Запускаем «Android Client» — он автоматически соединится с «VCL Client»

Результат работы представлен на рисунке:

Результат соединения двух профилей в App Tethering

Результат соединения двух профилей в App Tethering

Обращаю ваше внимание: если соединение инициируется вашим менеджером, а не удаленным, то событие OnAfterConnectProfile не происходит. Проверить это  достаточно просто — запустите сначала «Android Client», а затем вызовите Connect() в «VCL Client» 

Итак, подведем итог по подключению вручную в App Tethering:

  1. Ручное подключение позволяет контролировать весь процесс соединения и, в случае необходимости, мы можем запретить конкретному удаленному профилю подключиться к нашему профилю.
  2. Как и в случае с автоматическим подключением, после подключения оба менеджера «знают» друг о друге всё, включая перечень удаленных профилей.

Теперь, когда мы научились соединяться с удаленными профилями, можно перейти к следующей части — выполнению действий.

Выполнение удаленных действий в App Tethering

Технология App Tethering в Delphi позволяет вызывать удаленные действия (Actions), обмениваться строками и данными с другими профилями. Для того, чтобы профиль VCLProfile смог запустить удаленное действие оно должно находиться в коллекции Actions удаленного профиля AndroidProfile (см. описание этого свойства выше).

Простой запуск удаленных действий

Для начала напишем простенький пример, демонстрирующий запуск удаленных действий. Открываем проект приложения «AndroidClient», бросаем на главную форму приложения компонент:

Главная форма "Android Client"

Главная форма «Android Client»

В 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;

Давайте разберемся, что происходит в этом обработчике.

  1. Мы проходим по всем подключенным удаленным профилям (цикл for со счётчиком i)
  2. Используя метод GetRemoteProfileActions(), получаем список действий удаленного профиля, которые были опубликованы (свойство TLocalAction.IsPublic=True)
  3. Проходим во втором цикле по всему списку действий и выполняем их.

Так как у нас в 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;
Обратите внимание на второй параметр в методе RunRemoteAction() — использовалось имя действия в коллекции TTetheringAppProfile.Actions

Перейдем к следующему вопросу — «заркальное» выполнение удаленных действий.

Зеркальный запуск удаленных действий

Свойство Kind у TLocalAction может принимать два значения:

type
   TTetheringRemoteKind = (Shared, Mirror);

С тем, как запускать действия со значением Kind=Shared мы разобрались. Более интересных результатов можно достичь, используя зеркальный запуск. Для чего это может пригодиться? Например, для управления приложением, которое запущено на другом компьютере: управление с телефона плеером, запущенным на компьютере — самый просто пример «зазеркаливания» действий: двигаем ползунок громкости на телефоне и автоматически меняется громкость на компьютере и т.д.

Чтобы действие выполнялось зеркально необходимо:

  1. Чтобы ваше действие было с Kind=Mirror
  2. Чтобы удаленное действие было c Kind=Shared.

Попробуем «зазеркалить» действие, которое было создано в «AndroidClient». Для этого модифицируем наш «VCLClient» следующим образом:

  1. Добавляем на форму приложения компонент TActionList
  2. В 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«.

Подведем итоги по запуску удаленных действий:

  1. Удаленный действия могу запускаться «зеркально» (связка Shared+Mirror удаленного и локального действия)
  2. Удаленный действия могут запускаться синхронно или асинхронно. В случае асинхронного запуска необходимо выполнить метод GetRemoteActionAsyncState(), чтобы получить состояние запущенного действия.
  3. Список действий удаленного профиля можно получить, используя метод GetRemoteProfileActions() у TTetheringAppProfile.
Обмен данными в App Tethering

При обмене данными между приложениями мы можем использовать два типа ресурсов:

  1. Постоянные ресурсы. В качестве постоянного ресурса может выступать стандартный тип данных (integer, boolean, string, single, double) или TStream; Удаленные приложения могу запрашивать значения постоянных ресурсов и подписываться на обновления таких ресурсов.
  2. Временные ресурсы. В качестве временного ресурса могут выступать строки или потоки. Такие ресурсы отправляются, как правило, один раз и более не используются. Удаленные приложения не могут подписаться на обновление временного ресурса.

Рассмотрим работы с обоими видами ресурсов.

Работа с постоянными ресурсами

Создадим в 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;

Здесь мы делаем следующее:

  1. Проходим по списку подключенных удаленных профилей
  2. У каждого удаленного профиля запрашиваем его ресурсы, используя метод GetRemoteProfileResources()
  3. Проходим по списку ресурсов удаленного профиля и запрашиваем значение ресурса, используя метод GetRemoteResourceValue().
  4. Выводим значение в Memo. При этом, если ресурс — это поток, то выводим в Memo его JSON-представление, используя метод ресурса ToJsonString.

Вид приложения «VCLClient» после чтения значений ресурсов у «AndroidClient» представлен на рисунке:

Чтение значений удаленных ресурсов

Чтение значений удаленных ресурсов

Как говорилось выше, мы можем подписываться на обновление удаленных ресурсов. В случае, если мы подписаны на изменение какого-либо ресурса, то каждый раз, как удаленный ресурс будет изменяться, наш локальный профиль будет об этом знать. Чтобы подписаться на обновление какого-либо ресурса, мы можем воспользоваться двумя способами:

  1. Как в случае с действиями, «отзеркалить» ресурс. Для этого необходимо создать локальный ресурс с таким же именем, что и удаленный и установить значение Kind = Mirror.
  2. Воспользоваться парой методов у профиля: SubscribeToRemoteItem() — для подписки на обновление и, противоположным методом UnSubscribeFromRemoteItem() — для того, чтобы отписаться от обновлений.

Я воспользовался первым способом и создал в «VCLClient» ресурс со следующими свойствами:

"Зеракльный" ресурс у VCLClient

«Зеракльный» ресурс у 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 и сохраняться на компьютере в виде текстового файла.

Третья строка в коде (вызов метода Broadcast()) потребовалась для того, чтобы сообщать профилю VCLProfile об изменении ресурса. Хотя, по идее, этой строчки быть в коде не должно, но без неё обновление локального ресурса со значением Stream не происходит. С типом Data (когда передаются строки, числа) эта строка кода не требуется

Вид приложения для Android теперь такой:

AndroidClient

AndroidClient

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

Вид приложения стал таким:

VCLClient. Отправка файла в Android

VCLClient. Отправка файла в Android

Теперь перейдем в проект «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;

Теперь все принимаемые файлы будут сохраняться в папке документов. Проверить это довольно просто:

  1. Запустите оба приложения и соедините их
  2. Затем нажмите кнопку «Send File» в VCLClient, выберите любой файл и нажмите «Ok» в окне диалога
  3. В зависимости от того, на сколько большой файл вы выбрали, подождите пока в Android программа не напишет вам путь к сохраненному файлу
  4. Можете открыть новый файл в андроид.

Аналогичным образом в App Tethering обрабатываются и строки.

Подведем итоги:

  1. В App Tethering два типа ресурсов: постоянные и временные. На постоянные ресурсы можно подписываться и получать уведомления об их изменении, «отзеркаливать» их и т.д. Временные ресурсы используются, как правило, единожды, но с помощью них можно легко организовать, например, беспроводную передачу файлов на мобильное устройство Android (см. пример выше).
  2. При «отзеркаливании» потоков данных необходимо после обновления локального ресурса вызывать его метод 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 можно считать завершенным. Конечно, обзор не охватывает всех возможностей этой технологии. Так «остались за бортом» такие, на мой взгляд, интересные с точки зрения реализации вопросы, как:

  1. Подключение к удаленному профилю, который находится за пределами вашей подсети (использование параметров Target в методе Connect()).
  2. Создание своих протоколов обмена данными (адаптеров)

и другие вопросы, которые могут у вас возникнуть в процессе изучения технологии App Tethering. Однако, приведенных выше сведения, я полагаю, будет вполне достаточно, чтобы начать работу. Ну, а, в зависимости от специфики решаемых задач, будут появляться уже конкретные вопросы, которые, я уверен, вы сможете решить :)

Скачать исходник: Исходники —> Прочие

Книжная полка

Описание Подробно рассматривается библиотека FM, позволяющая создавать полнофункциональное программное обеспечение для операционных систем Windows и OS X, а также для смартфонов и планшетных компьютеров, работающих под управлением Android и iOS
купить книгу delphi на ЛитРес
Автор: Юрий Магда
Описание: Описаны общие подходы к программированию приложений MS Office. Даны программные методы реализации функций MS Excel, MS Word, MS Access и MS Outlook в среде Delphi.
купить книгу delphi на ЛитРес

 

0 0 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
1 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Лера
Лера
27/10/2016 09:07

Здравствуйте.
Если протоколом указать Bluetooth, то устройства не видят друг друга.
Тестировал на двух смартфонах. Права на использование bluetooth ставил.
Не видят они друг друга. В чем может быть проблема, подскажите пожалуйста?