Технология App Tethering появилась еще в прошлой версии Delphi. И рассказать о ней я собирался еще после выхода XE6, однако, как это часто бывает “не дошли руки”. Этот механизм предназначен для взаимодействия приложений на разных устройствах. Примечательно, что он поддерживается как в VCL, так и в FireMonkey. В XE7 в App Tethering были добавлены новые возможности, что для меня стало очередным поводом разобраться и сделать пару примеров. Обдумывать идеи для тестового приложения долго не пришлось. Так получилось, что сейчас у меня на рабочем столе стоит два ПК и, время от времени появляется планшет и смартфон (оба под управлением Android). Работаю я с этими устройствами практически одинаково интенсивно и, соответственно, возникает необходимость оперативно обмениваться не только файлами, но и текстовыми фрагментами, ссылками и т. д.. Конечно, существуют сотни программ, решающих это задачу, но коль скоро у нас имеется инструмент, то грех не написать собственное приложение, реализующее данный функционал.
В идеале мне бы хотелось бы создать некоторое приложение, работающее как под Windows, так и под Android, которое позволяло бы:
- осуществлять обмен файлами между устройствами;
- осуществлять обмен содержимым буфера обмена.
При этом хотелось бы исключить сложную процедуру настройки соединения для различных устройств.
Поскольку изобилия русскоязычных материалов по App Tethering пока не наблюдается, я буду перемежать рассказ о процессе разработки приложения выдержками из документации.
Разработку начнем с реализации простейшего функцонала – обмена файлами между ПК. Для того, что бы упростить понимание принципов работы App Tethering создадим два приложения – приложение, передающее файлы (“передатчик”) и приложение, принимающее файлы (“приемник”). Во избежание путанницы я сознательно не стану использовать термины “клиент”, “сервер” и т.д. “Передатчик” реализуем на VCL, а “приемник” – на FireMonkey. Опять же все это из соображений наглядности. В идеале это должно быть одно мультиплатформенное приложение, совмещающее в себе функции приема и передачи файлов. К этому вопросу, надеюсь, мы вернемся чуть позже.
Итак создадим два приложения,VCL и FireMonkey(Multi-device application), назовем их PrjSender и PrjReceiver и объеденим их в одной группе проектов. Механизм App Tethering в Delphi реализуют всего два компонента TTetheringManager и TTetheringAppProfile (менеджер и профиль). Размещаем их на главной (и пока единственной) форме обоих приложений. В обоих случаях для TetheringAppProfile устанавливаем значение свойства TetheringManager – TetheringManager1, таким образом связываем менеджер с профилем.
Для чего нужны эти компоненты? Документация гласит следующее:
Менеджер может обнаруживать и попарно связываться с другими менеджерами, представляющие удаленные профили, которые могут “расшаривать” данные для зарегистрированных профилей вашего менеджера. Менеджер может быть связан с одним или несколькими профилями. Основные функции менеджера:
- Менеджер использует адаптеры, для поиска других менеджеров. Например, менеджер использует сетевой адаптер для подключения к другим менеджерам по сети;
- Менеджер устанавливает связь с удаленными менеджерами (сопряжение);
- Два парных менеджера позволяют своим профилям узнать о профилях других менеджеров, таким образом профили могут обмениваться данными друг с другом;
- Менеджер предоставляет информацию от своих адаптеров протоколам своих профилей таким образом, что бы эти профили могли обмениваться данными друг с другом, используя собственные протоколы.
Все это звучит немного сумбурно, но попробуем разобраться во всем на практике. Прежде всего, нам нужно обеспечить соединение между двумя приложениями. В верхней части главной формы приложения-приемника разместим компонент ListBox (lbSenders) и кнопку (btnGetAvailableSenders). По нажатию на кнопку отобразим список доступных удаленных менеджеров. Для этого определим обработчик события OnEndManagersDiscovery следующим образом:
procedure TfRecMain.TetheringManager1EndManagersDiscovery(const Sender: TObject; const ARemoteManagers: TTetheringManagerInfoList); var I: Integer; begin lbSenders.Clear; for I := 0 to aRemoteManagers.Count - 1 do begin lbSenders.Items.Add(ARemoteManagers[i].ManagerText); end; end;
А по нажатию на кнопку вызовем метод DiscoverManagers компонента TetheringManager.
procedure TfRecMain.btnGetavailableSendersClick(Sender: TObject); var i: integer; begin for I := TetheringManager1.PairedManagers.Count - 1 downto 0 do TetheringManager1.UnPairManager(TetheringManager1.PairedManagers[I]); lbSenders.Clear; TetheringManager1.DiscoverManagers; end;
Предварительно здесь мы очищаем список и разрываем все связи (метод UnPairManager).
Теперь для того, что бы увидеть что-либо в списке доступных менеджеров запустите сначала приложение-передатчик. Я рекомендую предварительно скомпилировать его и запустить не из IDE. После этого можете запустить приложение-приемник и нажать на кнопку btnGetavailableSenders. Как вы понимаете, в списке появятся значения свойства Text всех доступных менеджеров. Если у вас есть машина в локальной сети – попробуйте запустить передатчик на ней. Результат должен быть тем же (конечно, не забывайте про Firewall).
Для наглядности можем определить событие OnCreate приложения-передатчика.
procedure TfSndrMain.FormCreate(Sender: TObject); var buffer: array[0..255] of char; size: dword; begin size := 256; if GetComputerName(buffer, size) then TetheringManager1.Text:= buffer+' '+ TetheringManager1.Text; end;
К значению свойства Text добавим название машины.
Следующим шагом, который мы попытаемся реализовать будет установление связи с удаленным менеджером. Согласно документации это можно сделать двумя способами – автоматически, указав одну и ту же группу, или вручную. Мы воспользуемся вторым вариантом. Список удаленных менеджеров мы уже получили. Теперь нам потребуется вызвать метод PairManager и в качестве параметра передать удаленный менеджер. А затем считать список обнаруженных профилей и связаться с ними при помощи метода Connect.
Поместим на форму приемника еще одну кнопку – btnConnect. И напишем следующий обработчик ее нажатия.
procedure TfRecMain.btnConnectClick(Sender: TObject); var I: Integer; begin // !!! Не правильно !!! Wrong if lbSenders.ItemIndex<0 then Exit; TetheringManager1.PairManager(TetheringManager1.RemoteManagers[lbSenders.ItemIndex]); CodeSite.Send(IntToStr(TetheringManager1.RemoteProfiles.Count)); if TetheringManager1.RemoteProfiles.Count> 0 then begin TetheringAppProfile1.Connect(TetheringManager1.RemoteProfiles[0]); end; end;
Здесь, как мне кажется, очень важный для понимания момент. После вызова метода PairManager в логе CodeSite мы увидим значение 0. Т. е. количество удаленных профилей будет нулевым. Однако при повторном нажатии на кнопку в лог будет записана единица (1), что правильно, поскольку в приложении-передатчике к менеджеру подключен один профиль. Насколько я понимаю, происходит это потому, что метод PairManager не успевает установить соединение с удаленным менеджером. Поэтому такой код использовать нельзя. Я не случайно задействовал систему логирования CodeSite Logging (ее бесплатная версия доступна для зарегистрированных пользователей Delphi), поскольку с ее помощью удобно отслеживать последовательность вызова событий компонентов.
Очевидно, что вызывать TetheringAppProfile1.Connect следует после того, как менеджер обнаружит все удаленные профили. В этот момент сработает событие OnEndProfilesDiscovery. Таким образом код будет выглядеть следующим образом:
procedure TfRecMain.btnConnectClick(Sender: TObject); var I: Integer; begin if lbSenders.ItemIndex < 0 then Exit; TetheringManager1.PairManager(TetheringManager1.RemoteManagers [lbSenders.ItemIndex]); CodeSite.Send(IntToStr(TetheringManager1.RemoteProfiles.Count)); end; procedure TfRecMain.TetheringManager1EndProfilesDiscovery(const Sender: TObject; const ARemoteProfiles: TTetheringProfileInfoList); begin CodeSite.Send(IntToStr(TetheringManager1.RemoteProfiles.Count)); if TetheringManager1.RemoteProfiles.Count > 0 then begin // Предполагается, что удаленное приложение содержит только один удаленный профиль. // На практике это может быть не так. TetheringAppProfile1.Connect(TetheringManager1.RemoteProfiles[0]); end; end;
Конечно, это весьма упрощенный пример и в коде не лишне делать проверки на предмет обновления списков удаленных профилей. Кроме того, не лишне проверить идентификатор профиля и/или использовать пароли. Как все это осуществить показано в демонстрационных примерах “из коробки”. Я же пока ограничусь наиболее простым вариантом. Замечу только, что App Tethering позволяет связывать приложения и на устройствах, находящихся и не в одной подсети. А в XE7 можно связаться и через Bluetooth. Но это тема для отдельного разговора.
Теперь немного модифицируем приложение-передатчик. Добавим на главную форму ListBox (lbFileList), OpenDialog и кнопку (btnOpen). При нажатии на кнопку открывается диалог выбора файла и выбранный файл попадает в список. Все просто:
procedure TfSndrMain.btnOpenClick(Sender: TObject); begin if OpenDialog1.Execute then begin lbFileList.Items.Add(OpenDialog1.FileName); end; end;
Следующей нашей задачей будет передать этот список в приложение-приемник. Для этого в передатчике напишем следующую функцию.
function TfSndrMain.SendFileList:Boolean; var LStream: TMemoryStream; i: integer; begin if lbFileList.Items.Count = 0 then Exit; LStream := TMemoryStream.Create; try lbFileList.Items.SaveToStream(LStream); for i := 0 to TetheringManager1.RemoteProfiles.Count-1 do Result:= TetheringAppProfile1.SendStream(TetheringManager1.RemoteProfiles[i], 'File List', LStream); finally LStream.Free; end; end;
Вызов этой функции поставим сразу после добавления имени файла в список. В приемнике так же добавим ListBox (lbFiles), в котором будем отображать список файлов, передаваемых передатчиком. Событие OnResourceReceived компонента TetheringAppProfile1 обработаем следующим образом:
procedure TfRecMain.TetheringAppProfile1ResourceReceived(const Sender: TObject; const AResource: TRemoteResource); begin if AResource.Hint = 'File List' then begin lbFiles.Items.Clear; lbFiles.Items.LoadFromStream(AResource.Value.AsStream); end; end;
Таким образом, как только в список файлов в программе-передатчике попадает новый файл, обновленный список сохраняется в поток, который передается всем подключенным удаленным профилям. Профиль программы-приемника получает ресурс и если в его описании значится ‘File List’, загружает его в собственный ListBox. Опять же, данный код далёк от совершенства (проверку лучше привязать к событию OnAcceptResource), но оставим его в таком виде исключительно для наглядности.
И последним действием станет передача файла. Здесь, в принципе, все по аналогии, за исключением того, что передачу файла должен инициировать приемник.
Размещаем на форме приложения-приемника кнопку (btnFileDownload). Обрабатываем нажатие:
procedure TfRecMain.btnFileDownloadClick(Sender: TObject); begin // if (TetheringManager1.RemoteProfiles.Count = 0) or (lbFiles.ItemIndex < 0) then Exit; TetheringAppProfile1.SendString(TetheringManager1.RemoteProfiles[0], 'FileName', lbFiles.Items[lbFiles.ItemIndex]); end;
Код отсылает удаленному профилю строку, содержащую полное имя запрашиваемого файла, и которую должен обработать передатчик. Соответственно, в передатчике обрабатываем событие OnResourceReceived для компонента TetheringAppProfile1:
procedure TfSndrMain.TetheringAppProfile1ResourceReceived(const Sender: TObject; const AResource: TRemoteResource); begin if AResource.Hint='FileName' then Begin SendFile(AResource.Value.AsString); End; end;
Код метода SendFile может выглядеть так:
function TfSndrMain.SendFile(aFn: string): boolean; var LStream: TMemoryStream; i: integer; begin LStream := TMemoryStream.Create; try LStream.LoadFromFile(aFn); if TetheringManager1.RemoteProfiles.Count>0 then begin Result:= TetheringAppProfile1.SendStream(TetheringManager1.RemoteProfiles[0], 'File', LStream); end; finally LStream.Free; end; end;
Все просто. Файл считывается в поток и поток пересылается приложению приемнику.Когда приемник принимает ресурс, он инициирует открытие диалога сохранения файла, после чего сохраняет принятый поток в новый файл. Код события OnResourceReceived для компонента TetheringAppProfile1 в приемнике меняется следующим образом:
procedure TfRecMain.TetheringAppProfile1ResourceReceived(const Sender: TObject; const AResource: TRemoteResource); var ms: TMemoryStream; begin if AResource.Hint = 'File List' then begin lbFiles.Items.Clear; lbFiles.Items.LoadFromStream(AResource.Value.AsStream); end; if AResource.Hint = 'File' then begin SaveDialog1.FileName:= ExtractFileName(lbFiles.Items[lbFiles.ItemIndex]); if SaveDialog1.Execute then begin ms:= TMemoryStream.Create; try ms.LoadFromStream(AResource.Value.AsStream); ms.SaveToFile(SaveDialog1.FileName); finally FreeAndNil(ms); end; end; end; end;
Теперь приложения можно протестировать:
- запускаем оба приложения;
- в приемнике нажимаем кнопку btnGetAvailableSenders;
- выбираем в списке появившийся элемент (если вы запустили более одного экземпляра приложения-передатчика, то элементов в списке будет несколько);
- жмем кнопку btnConnect;
- в приложении передатчике открываем файл(ы);
- в приемнике, в обновившемся списке файлов, выбираем нужный файл;
- сохраняем переданный файл на диск.
Естественно, такая сложная последовательность действий нужна только для наглядности. В реальном приложении все должно быть несколько проще.
В мои планы входят дальнейшие эксперименты с App Tethering, о чем я постараюсь рассказать. В частности интересно посмотреть, как это работает на мобильных платформах.
У меня на Android 4.4.3 никак не работал метод

TetheringManager1.DiscoverManagers;
В Win – пашет, в Android – тупо виснет. В итоге не стал публиковать аналогичный пост
Будем разбираться
А вот мне почему-то резко захотелось написать сетевой менеджер буфера обмена
Тем более заготовки классов для самого менеджера БО остались от DB2Clipboard
У меня по поводу AppTethering немного другая идея была, но косяк с мобильными платформами пока не позволяет реализовать идею в полном объеме. Если разберешься и напишешь как заставить работать DiscoverManagers на Android – буду премного благодарен.
А у меня заработало с пол-пинка.
Система передачи данных уже есть вот сайт http://www.netboard.com.ua
Спасибо. В принципе, я и не сомневался в том, что что-то подобное есть. Но у меня немного другое видение подобной системы. Да и основной интерес – самому реализовать это все.