Fun with Delphi XE6 App tethering and barcodes
This article not my own, i'm copyed from http://fixedbycode.blogspot.com/2014/04/fun-with-delphi-xe6-app-tethering-and.html
One of the new key features in XE6 is what Embarcadero has chosen to call App Tethering components - the only problem I initially had with the name was that it implied that mobile had to be involved in some way - which is not the case. You can make a windows service that is a "receiver" from a desktop application running on the same sub-net.
An example could be POS systems, running on various devices talking to the same receiver - without bothering about implementing multiple communications protocols - these components all wraps this up nicely.
I wanted to give these new component a try, by making a very simple barcode scanner using my phone as the barcode scanner and then have the receiver send the barcode to whatever application was active.
A word of warning: this is a prof of concept - not ready for production or deployment - you need to do some work yourself to make it fit your needs. I have tried to use as little code as possible - leaving out all exception handling :-).
Start with a new Desktop application either VCL or FireMonkey will do, add a TTetheringManager, a TTetheringAppProfile, a TLabel and a TCheckBox - which all should end up looking a bit like this:
Change the (Manager)Text property on the TTetheringManager to 'BarcodeReceiverManager', on the TTetheringAppProfile change the (Profile)Text property to 'BarcodeReceiver' and set the Manager property to point to the TTetheringManager.
Since we want to send the received barcode to the active application, we just use a send key function - in this case I have just used the WinAPI keybd_event() method - it would be more correct to use SendInput() - but the barcodes consists of non-unicode characters. For the letters we also need to send along the shift key.
procedure SendKeys(const S: String);
var
I: Integer;
begin
for I := 1 to Length(S) do
begin
if CharInSet(S[I], ['A'..'Z']) then
keybd_event(VK_SHIFT, 0, 0, 0);
keybd_event(Ord(S[I]), MapVirtualKey(Ord(S[I]), 0),0, 0);
keybd_event(Ord(S[I]), MapVirtualKey(Ord(S[I]), 0), KEYEVENTF_KEYUP, 0);
if CharInSet(S[I], ['A'..'Z']) then
keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);
end;
end;
On the TTetheringAppProfile.OnResourceReceived event add the following code to send the string received from the mobile app - and maybe a carriage return - if our TCheckBox is checked.
SendKeys(AResource.Value.AsString);
if cbAddReturn.IsChecked then
SendKeys(char(13));
This design will never get accepted on any App Store :-D
On the TTetheringAppProfile set the Manager property to point to the TTetheringManager.
Now on the FormShow event we want to unpair any Managers already paired with this TTetheringManager, and after that trigger the Discover event.
for I := TetheringManager1.PairedManagers.Count - 1 downto 0 do
TetheringManager1.UnPairManager(TetheringManager1.PairedManagers[I]);
TetheringManager1.DiscoverManagers;
At the end of the discovery, the TTetheringManager.OnEndManagersDiscovery event is called:
for I := 0 to RemoteManagers.Count-1 do
if (RemoteManagers[I].ManagerText = 'BarcodeReceiverManager') then
begin
TetheringManager1.PairManager(RemoteManagers[I]);
Break; // Break since we only want the first...
end;
followed by the TTetheringManager.OnEndProfilesDiscovery event:
Label1.Text := 'No receiver found';
for i := 0 to TetheringManager1.RemoteProfiles.Count-1 do
if (TetheringManager1.RemoteProfiles[i].ProfileText = 'BarcodeReceiver') then
begin
if TetheringAppProfile1.Connect(TetheringManager1.RemoteProfiles[i]) then
Label1.Text := 'Receiver ready.'
end;
Notice the only two references to the "Text" properties we set in our "receiver" applications Manager and Profile components - the rest is done by magic (or a lot of Indy code I didn't need to write :-D)
And the last thing we need to add is on the OnClick event of the "Send barcode" button:
TetheringAppProfile1.SendString(TetheringManager1.RemoteProfiles[0],
'Barcode from mobile', Edit1.Text);
Now we can start our "receiver" application, and run our "sender" mobile app on iOS or Android, and in theory it should work.
I did put the tethering example to a minimum, for a more correct examples see the samples include with XE6 - it seems that there is a lot of extra stuff hidden in these 2 components - maybe for another post.
I previously been using the ZBar library on iOS/ObjectiveC and xzing library Android/Java - because ZBar wasn't that good on Android years back.
For this example I am only going to do Android using xzing as what is called an intent. I guess that using intents on Android is best practice, but I guess most Delphi developers are not that keen on being depending on any framework or other thing we do not control - like Windows :-D
When doing Android Apps in Java, I did not always follow the intent way, but included the xzing core.jar file - that would also be possible in Delphi - but getting the classes wrapped into Delphi interfaces would take some work, or someone should sponsor me by buying either WinSoft JavaImportor CHUA Chee Wee's Android2DelphiImport - even better if Embarcadero bundles one of them in XE6 update 1 :-)
Update: Software Union also provides Java2Pas which might do the trick.
I did have a peek at the code from Thomas K's TKRBarcodeScanner component (beware terrible link :-) ) which for iOS is depending on the TMS component.
Since we are going to use the clipboard to get the barcode from the xzing intent, and still behave as good citizens preserving what is already there - we declare some variables:
FPreservedClipboardValue: TValue;
FMonitorClipboard: Boolean;
ClipService: IFMXClipboardService;
Notice the TValue from the System.Rtti unit - it is kind of a Variant and then not.
On the FormCreate event we start testing if the platform supports what we think of as a clipboard, and we also wants it to support application events - so that we can check when the intent is done and our app becomes active again.
var
aFMXApplicationEventService: IFMXApplicationEventService;
begin
FMonitorClipboard := False;
if not TPlatformServices.Current.SupportsPlatformService(IFMXClipboardService, IInterface(ClipService)) then
ClipService := nil;
if TPlatformServices.Current.SupportsPlatformService(IFMXApplicationEventService, IInterface(aFMXApplicationEventService)) then
begin
aFMXApplicationEventService.SetApplicationEventHandler(HandleAppEvent);
end
else
begin
Log.d('Application Event Service is not supported.');
end;
end;
Now we want to call the xzing intent to scan a barcode, put the result on the clipboard and return when done. So on the OnClick event of the "Scan barcode" we put the following code:
if Assigned(ClipService) then
begin
FPreservedClipboardValue := ClipService.GetClipboard;
FMonitorClipboard := True;
ClipService.SetClipboard('nil');
intent := TJIntent.Create;
intent.setAction(StringToJString('com.google.zxing.client.android.SCAN'));
// intent.putExtras(TJIntent.JavaClass.EXTRA_TEXT, StringToJString('"SCAN_MODE", "CODE_39"'));
SharedActivity.startActivityForResult(intent, 0);
end;
We create an instance of TJIntent, set its action to the zxing barcode scanner intent, which you might need to install on your mobile before running this app, if you do not already have the intent via some other app. You need to convert the name of the intent to a Java string. You can among other things also specify which barcode types that can be read - doing that increases the speed and precision of the scanning - so if you know you only need Code 39 or EAN13 you should specify that.
When the application event fires, we check if it was because our app became active and we were monitoring the clipboard.
Result := False;
if FMonitorClipboard and (AAppEvent = TApplicationEvent.BecameActive) then
begin
Result := GetBarcodeValue;
end;
The GetBarcodeValue function gets the barcode from the clipboard, and restores the old value from before we initiated the scan:
Result := False;
FMonitorClipboard := False;
if (ClipService.GetClipboard.ToString <> 'nil') then
begin
Edit1.Text := ClipService.GetClipboard.ToString;
ClipService.SetClipboard(FPreservedClipboardValue);
Result := True;
end;
Done - you should now have a working app - which could need a lot of extra error checking and care. But like baking your own bread I hope this has been fun. I have added some extra links below to people or code that might also be helpful - including the full code for download. If you just have interest in the barcode part there are better examples in the links below - also be aware that in XE6 the StringToJString has move to the Androidapi.Helpers unit.
FMX Express is very active collecting relevnat stuff: http://www.fmxexpress.com/qr-code-scanner-source-code-for-delphi-xe5-firemonkey-on-android-and-ios/ - there is also a better link to Thomas K. code there.
John Whitham should also be mentioned: http://john.whitham.me.uk/xe5/
An example could be POS systems, running on various devices talking to the same receiver - without bothering about implementing multiple communications protocols - these components all wraps this up nicely.
I wanted to give these new component a try, by making a very simple barcode scanner using my phone as the barcode scanner and then have the receiver send the barcode to whatever application was active.
A word of warning: this is a prof of concept - not ready for production or deployment - you need to do some work yourself to make it fit your needs. I have tried to use as little code as possible - leaving out all exception handling :-).
Part 1 - The "receiver" desktop app.
Start with a new Desktop application either VCL or FireMonkey will do, add a TTetheringManager, a TTetheringAppProfile, a TLabel and a TCheckBox - which all should end up looking a bit like this:
Change the (Manager)Text property on the TTetheringManager to 'BarcodeReceiverManager', on the TTetheringAppProfile change the (Profile)Text property to 'BarcodeReceiver' and set the Manager property to point to the TTetheringManager.
Since we want to send the received barcode to the active application, we just use a send key function - in this case I have just used the WinAPI keybd_event() method - it would be more correct to use SendInput() - but the barcodes consists of non-unicode characters. For the letters we also need to send along the shift key.
procedure SendKeys(const S: String);
var
I: Integer;
begin
for I := 1 to Length(S) do
begin
if CharInSet(S[I], ['A'..'Z']) then
keybd_event(VK_SHIFT, 0, 0, 0);
keybd_event(Ord(S[I]), MapVirtualKey(Ord(S[I]), 0),0, 0);
keybd_event(Ord(S[I]), MapVirtualKey(Ord(S[I]), 0), KEYEVENTF_KEYUP, 0);
if CharInSet(S[I], ['A'..'Z']) then
keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);
end;
end;
SendKeys(AResource.Value.AsString);
if cbAddReturn.IsChecked then
SendKeys(char(13));
Part 2 - The "sender" mobile app.
Create a new FireMonkey Mobile Application, add a TTetheringManager, a TTetheringAppProfile, a TEdit, a TLabel and two TSpeedButtons - which all should end up looking a bit like this:This design will never get accepted on any App Store :-D
On the TTetheringAppProfile set the Manager property to point to the TTetheringManager.
Now on the FormShow event we want to unpair any Managers already paired with this TTetheringManager, and after that trigger the Discover event.
for I := TetheringManager1.PairedManagers.Count - 1 downto 0 do
TetheringManager1.UnPairManager(TetheringManager1.PairedManagers[I]);
TetheringManager1.DiscoverManagers;
At the end of the discovery, the TTetheringManager.OnEndManagersDiscovery event is called:
for I := 0 to RemoteManagers.Count-1 do
if (RemoteManagers[I].ManagerText = 'BarcodeReceiverManager') then
begin
TetheringManager1.PairManager(RemoteManagers[I]);
Break; // Break since we only want the first...
end;
followed by the TTetheringManager.OnEndProfilesDiscovery event:
Label1.Text := 'No receiver found';
for i := 0 to TetheringManager1.RemoteProfiles.Count-1 do
if (TetheringManager1.RemoteProfiles[i].ProfileText = 'BarcodeReceiver') then
begin
if TetheringAppProfile1.Connect(TetheringManager1.RemoteProfiles[i]) then
Label1.Text := 'Receiver ready.'
end;
Notice the only two references to the "Text" properties we set in our "receiver" applications Manager and Profile components - the rest is done by magic (or a lot of Indy code I didn't need to write :-D)
And the last thing we need to add is on the OnClick event of the "Send barcode" button:
TetheringAppProfile1.SendString(TetheringManager1.RemoteProfiles[0],
'Barcode from mobile', Edit1.Text);
Now we can start our "receiver" application, and run our "sender" mobile app on iOS or Android, and in theory it should work.
I did put the tethering example to a minimum, for a more correct examples see the samples include with XE6 - it seems that there is a lot of extra stuff hidden in these 2 components - maybe for another post.
Part 3 - Barcode scanning.
There are some examples out there for doing this both on Android and iOS - I guess you could also invest in some commercial components like those from TMS or WinSoft.I previously been using the ZBar library on iOS/ObjectiveC and xzing library Android/Java - because ZBar wasn't that good on Android years back.
For this example I am only going to do Android using xzing as what is called an intent. I guess that using intents on Android is best practice, but I guess most Delphi developers are not that keen on being depending on any framework or other thing we do not control - like Windows :-D
When doing Android Apps in Java, I did not always follow the intent way, but included the xzing core.jar file - that would also be possible in Delphi - but getting the classes wrapped into Delphi interfaces would take some work, or someone should sponsor me by buying either WinSoft JavaImportor CHUA Chee Wee's Android2DelphiImport - even better if Embarcadero bundles one of them in XE6 update 1 :-)
Update: Software Union also provides Java2Pas which might do the trick.
I did have a peek at the code from Thomas K's TKRBarcodeScanner component (beware terrible link :-) ) which for iOS is depending on the TMS component.
Since we are going to use the clipboard to get the barcode from the xzing intent, and still behave as good citizens preserving what is already there - we declare some variables:
FPreservedClipboardValue: TValue;
FMonitorClipboard: Boolean;
ClipService: IFMXClipboardService;
Notice the TValue from the System.Rtti unit - it is kind of a Variant and then not.
On the FormCreate event we start testing if the platform supports what we think of as a clipboard, and we also wants it to support application events - so that we can check when the intent is done and our app becomes active again.
var
aFMXApplicationEventService: IFMXApplicationEventService;
begin
FMonitorClipboard := False;
if not TPlatformServices.Current.SupportsPlatformService(IFMXClipboardService, IInterface(ClipService)) then
ClipService := nil;
if TPlatformServices.Current.SupportsPlatformService(IFMXApplicationEventService, IInterface(aFMXApplicationEventService)) then
begin
aFMXApplicationEventService.SetApplicationEventHandler(HandleAppEvent);
end
else
begin
Log.d('Application Event Service is not supported.');
end;
end;
Now we want to call the xzing intent to scan a barcode, put the result on the clipboard and return when done. So on the OnClick event of the "Scan barcode" we put the following code:
if Assigned(ClipService) then
begin
FPreservedClipboardValue := ClipService.GetClipboard;
FMonitorClipboard := True;
ClipService.SetClipboard('nil');
intent := TJIntent.Create;
intent.setAction(StringToJString('com.google.zxing.client.android.SCAN'));
// intent.putExtras(TJIntent.JavaClass.EXTRA_TEXT, StringToJString('"SCAN_MODE", "CODE_39"'));
SharedActivity.startActivityForResult(intent, 0);
end;
We create an instance of TJIntent, set its action to the zxing barcode scanner intent, which you might need to install on your mobile before running this app, if you do not already have the intent via some other app. You need to convert the name of the intent to a Java string. You can among other things also specify which barcode types that can be read - doing that increases the speed and precision of the scanning - so if you know you only need Code 39 or EAN13 you should specify that.
When the application event fires, we check if it was because our app became active and we were monitoring the clipboard.
Result := False;
if FMonitorClipboard and (AAppEvent = TApplicationEvent.BecameActive) then
begin
Result := GetBarcodeValue;
end;
The GetBarcodeValue function gets the barcode from the clipboard, and restores the old value from before we initiated the scan:
Result := False;
FMonitorClipboard := False;
if (ClipService.GetClipboard.ToString <> 'nil') then
begin
Edit1.Text := ClipService.GetClipboard.ToString;
ClipService.SetClipboard(FPreservedClipboardValue);
Result := True;
end;
Done - you should now have a working app - which could need a lot of extra error checking and care. But like baking your own bread I hope this has been fun. I have added some extra links below to people or code that might also be helpful - including the full code for download. If you just have interest in the barcode part there are better examples in the links below - also be aware that in XE6 the StringToJString has move to the Androidapi.Helpers unit.
Links:
The source code: Receiver application and Scanner app.
Jim McKeeth is always helpfull: http://delphi.org/2014/02/scanning-barcodes-with-rad-studio-xe5/FMX Express is very active collecting relevnat stuff: http://www.fmxexpress.com/qr-code-scanner-source-code-for-delphi-xe5-firemonkey-on-android-and-ios/ - there is also a better link to Thomas K. code there.
John Whitham should also be mentioned: http://john.whitham.me.uk/xe5/
0 comments: