Skip to content

Commit

Permalink
Added Android service comms demo
Browse files Browse the repository at this point in the history
  • Loading branch information
DelphiWorlds committed Jan 24, 2024
1 parent 912ea92 commit 63535c8
Show file tree
Hide file tree
Showing 11 changed files with 3,379 additions and 0 deletions.
115 changes: 115 additions & 0 deletions Demos/AndroidServiceComms/ASC.Common.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
unit ASC.Common;

interface

uses
AndroidApi.JNI.GraphicsContentViewText,
DW.MultiReceiver.Android;

const
cActionAppMessage = 'ACTION_APP_MESSAGE';
cActionServiceMessage = 'ACTION_SERVICE_MESSAGE';
cExtraMessageData = 'EXTRA_MESSAGE_DATA';

type
TReceiveProc = reference to procedure(const Intent: JIntent);

TLocalBroadcastReceiver = class(TMultiReceiver)
private
FActions: TArray<string>;
FReceiveProc: TReceiveProc;
protected
procedure ConfigureActions; override;
procedure Receive(context: JContext; intent: JIntent); override;
public
constructor Create(const AActions: TArray<string>; const AReceiveProc: TReceiveProc);
end;

TDataMessage = record
DataType: Integer;
Data: string;
class function CreateCommandMessage(const ACommand: string): TDataMessage; static;
class function CreateImageMessage(const ABase64: string): TDataMessage; static;
class function CreateStringMessage(const AData: string): TDataMessage; static;
procedure FromJSON(const AValue: string);
function ToJSON: string;
end;

implementation

uses
System.JSON,
Androidapi.Helpers, Androidapi.JNI.JavaTypes;

{ TLocalBroadcastReceiver }

constructor TLocalBroadcastReceiver.Create(const AActions: TArray<string>; const AReceiveProc: TReceiveProc);
begin
FActions := AActions;
inherited Create(True);
FReceiveProc := AReceiveProc;
end;

procedure TLocalBroadcastReceiver.ConfigureActions;
var
LAction: string;
begin
for LAction in FActions do
IntentFilter.addAction(StringToJString(LAction));
end;

procedure TLocalBroadcastReceiver.Receive(context: JContext; intent: JIntent);
begin
if Assigned(FReceiveProc) then
FReceiveProc(intent);
end;

{ TDataMessage }

class function TDataMessage.CreateCommandMessage(const ACommand: string): TDataMessage;
begin
Result.DataType := 2;
Result.Data := ACommand;
end;

class function TDataMessage.CreateImageMessage(const ABase64: string): TDataMessage;
begin
Result.DataType := 3;
Result.Data := ABase64;
end;

class function TDataMessage.CreateStringMessage(const AData: string): TDataMessage;
begin
Result.DataType := 1;
Result.Data := AData;
end;

procedure TDataMessage.FromJSON(const AValue: string);
var
LJSON: TJSONValue;
begin
LJSON := TJSONObject.ParseJSONValue(AValue);
if LJSON <> nil then
try
LJSON.TryGetValue('DataType', DataType);
LJSON.TryGetValue('Data', Data);
finally
LJSON.Free;
end;
end;

function TDataMessage.ToJSON: string;
var
LJSON: TJSONObject;
begin
LJSON := TJSONObject.Create;
try
LJSON.AddPair('DataType', DataType);
LJSON.AddPair('Data', Data);
Result := LJSON.ToJSON;
finally
LJSON.Free;
end;
end;

end.
15 changes: 15 additions & 0 deletions Demos/AndroidServiceComms/AndroidServiceCommsDemo.dpr
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
program AndroidServiceCommsDemo;

uses
System.StartUpCopy,
FMX.Forms,
Unit1 in 'Unit1.pas' {Form1},
ASC.ServiceModule in 'Service\ASC.ServiceModule.pas' {ServiceModule: TAndroidService};

{$R *.res}

begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
1,586 changes: 1,586 additions & 0 deletions Demos/AndroidServiceComms/AndroidServiceCommsDemo.dproj

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions Demos/AndroidServiceComms/AndroidServiceCommsDemoGroup.groupproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{CD2F57EA-E050-4D85-A845-F4F206FA07DB}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<Projects Include="AndroidServiceCommsDemo.dproj">
<Dependencies>Service\AndroidServiceCommsDemoService.dproj</Dependencies>
</Projects>
<Projects Include="Service\AndroidServiceCommsDemoService.dproj">
<Dependencies/>
</Projects>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Default.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Default.Personality/>
</BorlandProject>
</ProjectExtensions>
<Target Name="AndroidServiceCommsDemo" DependsOnTargets="AndroidServiceCommsDemoService">
<MSBuild Projects="AndroidServiceCommsDemo.dproj"/>
</Target>
<Target Name="AndroidServiceCommsDemo:Clean" DependsOnTargets="AndroidServiceCommsDemoService:Clean">
<MSBuild Projects="AndroidServiceCommsDemo.dproj" Targets="Clean"/>
</Target>
<Target Name="AndroidServiceCommsDemo:Make" DependsOnTargets="AndroidServiceCommsDemoService:Make">
<MSBuild Projects="AndroidServiceCommsDemo.dproj" Targets="Make"/>
</Target>
<Target Name="AndroidServiceCommsDemoService">
<MSBuild Projects="Service\AndroidServiceCommsDemoService.dproj"/>
</Target>
<Target Name="AndroidServiceCommsDemoService:Clean">
<MSBuild Projects="Service\AndroidServiceCommsDemoService.dproj" Targets="Clean"/>
</Target>
<Target Name="AndroidServiceCommsDemoService:Make">
<MSBuild Projects="Service\AndroidServiceCommsDemoService.dproj" Targets="Make"/>
</Target>
<Target Name="Build">
<CallTarget Targets="AndroidServiceCommsDemo;AndroidServiceCommsDemoService"/>
</Target>
<Target Name="Clean">
<CallTarget Targets="AndroidServiceCommsDemo:Clean;AndroidServiceCommsDemoService:Clean"/>
</Target>
<Target Name="Make">
<CallTarget Targets="AndroidServiceCommsDemo:Make;AndroidServiceCommsDemoService:Make"/>
</Target>
<Import Project="$(BDS)\Bin\CodeGear.Group.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Group.Targets')"/>
</Project>
50 changes: 50 additions & 0 deletions Demos/AndroidServiceComms/ReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Android Service Comms Demo

## Description

Demonstrates implementations of communication between an Android app, and a service used by the app.

The demo uses two forms:

1. Via service "binding", which is supported "out of the box" in Delphi, and appears in RAD Studio demos provided by Embarcadero
2. Via local broadcasts

Checking/unchecking the checkbox at the top of the demo determines which is used.

There may be other means that can be used for communication, however the two above are simple, and work.

There is a [ChatGPT conversation here](https://github.com/DelphiWorlds/HowTo/blob/main/ChatGPTConversations/AndroidBindServiceVsStartService.md) about the merits of using `bindService` (used in method 1) vs using `startService` (used in method 2)

## Supported Delphi versions

Delphi 12, Delphi 11.x.

## Project Configuration

If you are starting your own Android project that contains a service, [this is a recommended video](https://www.youtube.com/watch?v=0mD3WLK8FYc) to watch. It's from 2015, however the process is pretty much the same. The important things are that the application and service should be part of a project group, that you build the service at least once before using the `Add Service` menu item in Project Manager to add the service to the application.

Communication method 2 (local broadcasts) requires the addition of the Kastri base library: `dw-kastri-base-2.0.0.jar` for Delphi 11.x and `dw-kastri-base-3.0.0.jar` for Delphi 12. These libraries are in the `Lib` folder of Kastri. Add it to the `Libraries` node under the Android platform in Project Manager.

**Note**:

Due to a bug in Delphi 11.3 **ONLY**, if you need to compile for Android 64-bit, you will need to either apply [this workaround](https://docs.code-kungfu.com/books/hotfix-113-alexandria/page/fix-jar-libraries-added-to-android-64-bit-platform-target-are-not-compiled) (which will apply to **all** projects), **OR** copy the jar file(s) to _another folder_, and add them to the Libraries node of the Android 64-bit target. (Adding the same `.jar` file(s) to Android 64-bit does _not_ work)

## Service "binding"

Service binding is achieved in the demo by creating an instance of `TLocalServiceConnection`, setting the `OnConnected` and `OnDisconnected` event handlers, and calling `BindService` (in the `ConnectButtonClick` handler). Once the `OnConnected` event occurs (handled by `ServiceConnectedHandler`) a reference to the service instance can be obtained, and its `OnSendData` event handler can be assigned. If this event is assigned, the service uses it to send data to the application.

## Local Broadcasts

If local broadcasts are used, the service needs to be started (via `ConnectButtonClick`). When the service starts (see `AndroidServiceStartCommand` in the service), a message is sent via local broadcast to notify the application that the service has started. Both the service and application use a local broadcast receiver to receive messages from each other.

## Messages in the demo

Messages are constructed using the `TDataMessage` record type (in the `ASC.Common` unit), which are serialized to/deserialized from JSON, so that the local broadcast can use a plain string in the extras of the intent being sent.

Messages that are "commands" have a value of `2` for `DataType` and the `Data` is a plain string. Messages that are just a response have a value of `1`. Messages that contain image data (as a base64 string) have a value of `3` for `DataType`

## Expanding beyond simple data

As described above, the communication here contains very rudimentary information, but could be expanded to handle more complex data.


7 changes: 7 additions & 0 deletions Demos/AndroidServiceComms/Service/ASC.ServiceModule.dfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
object ServiceModule: TServiceModule
OnCreate = AndroidServiceCreate
OnStartCommand = AndroidServiceStartCommand
Height = 357
Width = 486
PixelsPerInch = 144
end
117 changes: 117 additions & 0 deletions Demos/AndroidServiceComms/Service/ASC.ServiceModule.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
unit ASC.ServiceModule;

interface

uses
System.SysUtils,
System.Classes,
System.Android.Service,
AndroidApi.JNI.GraphicsContentViewText,
Androidapi.JNI.Os,
ASC.Common;

type
TSendDataEvent = procedure(Sender: TObject; const Data: string) of object;

TServiceModule = class(TAndroidService)
procedure AndroidServiceCreate(Sender: TObject);
function AndroidServiceStartCommand(const Sender: TObject; const Intent: JIntent; Flags, StartId: Integer): Integer;
private
// FReceiver, ReceiverReceiveHandler and SendData are for when *not* using service "binding"
FReceiver: TLocalBroadcastReceiver;
FOnSendData: TSendDataEvent;
function GetImageBase64: string;
procedure ReceiverReceiveHandler(const AIntent: JIntent);
procedure SendData(const AData: string);
public
// This method is made public only so that the application can call it direct when using service "binding"
procedure ReceiveData(const AData: string);
// OnSendData is for when using service "binding"
property OnSendData: TSendDataEvent read FOnSendData write FOnSendData;
end;

var
ServiceModule: TServiceModule;

implementation

{%CLASSGROUP 'FMX.Controls.TControl'}

{$R *.dfm}

uses
Androidapi.Helpers, Androidapi.JNI.JavaTypes, Androidapi.JNI.App,
System.Net.HttpClient,
DW.Base64.Helpers,
DW.Androidapi.JNI.AndroidX.LocalBroadcastManager;

{ TServiceModule }

procedure TServiceModule.AndroidServiceCreate(Sender: TObject);
begin
FReceiver := TLocalBroadcastReceiver.Create([cActionServiceMessage], ReceiverReceiveHandler);
end;

procedure TServiceModule.ReceiverReceiveHandler(const AIntent: JIntent);
var
LExtraName: JString;
begin
LExtraName := StringToJString(cExtraMessageData);
if AIntent.hasExtra(LExtraName) then
ReceiveData(JStringToString(AIntent.getStringExtra(LExtraName)));
end;

function TServiceModule.AndroidServiceStartCommand(const Sender: TObject; const Intent: JIntent; Flags, StartId: Integer): Integer;
begin
SendData(TDataMessage.CreateStringMessage('Service started').ToJSON);
Result := TJService.JavaClass.START_STICKY;
end;

function TServiceModule.GetImageBase64: string;
var
LHTTP: THTTPClient;
LResponse: IHTTPResponse;
begin
Result := '';
LHTTP := THTTPClient.Create;
try
LResponse := LHTTP.Get('https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Cat_August_2010-4.jpg/2880px-Cat_August_2010-4.jpg');
if LResponse.StatusCode = 200 then
Result := TBase64Helper.Encode(LResponse.ContentStream);
finally
LHTTP.Free;
end;
end;

procedure TServiceModule.ReceiveData(const AData: string);
var
LMessage: TDataMessage;
begin
LMessage.FromJSON(AData);
case LMessage.DataType of
2:
begin
if LMessage.Data.Equals('ping') then
SendData(TDataMessage.CreateStringMessage('pong').ToJSON)
else if LMessage.Data.Equals('image') then
SendData(TDataMessage.CreateImageMessage(GetImageBase64).ToJSON);
end;
end;
end;

procedure TServiceModule.SendData(const AData: string);
var
LIntent: JIntent;
begin
// If FOnSendData is not assigned, then service "binding" is not being used
if not Assigned(FOnSendData) then
begin
LIntent := TJIntent.JavaClass.init(StringToJString(cActionAppMessage));
LIntent.putExtra(StringToJString(cExtraMessageData), StringToJString(AData));
TJLocalBroadcastManager.JavaClass.getInstance(TAndroidHelper.Context).sendBroadcast(LIntent);
end
else
FOnSendData(Self, AData);
end;

end.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
program AndroidServiceCommsDemoService;

uses
System.Android.ServiceApplication,
ASC.ServiceModule in 'ASC.ServiceModule.pas' {ServiceModule: TAndroidService};

{$R *.res}

begin
Application.Initialize;
Application.CreateForm(TServiceModule, ServiceModule);
Application.Run;
end.
Loading

0 comments on commit 63535c8

Please sign in to comment.