Launching activities and handling results in Delphi XE5 Android apps

Brian Long Consultancy & Training Services Ltd.
April 2014

There is an update to this article for Delphi XE6 available here.

Accompanying source files available through this download link.

Contents

Android Delphi

Introduction

Delphi Android apps can use Android APIs, both those already present in the RTL and others we can translate manually, in order to interact with parts of the Android system. This article will look at how we can launch a variety of functionality using standard Android intent objects to instigate execution of available activities. Some of these may be part of the OS, but others will not be. Doing so will enable our own applications to integrate with the Android infrastructure and take advantage of convenient functionality installed on the device.

Through this article you should gain an insight into the techniques involved in communicating with Android activities, both launching them with appropriate parameters, but also receiving responses back from them when they are done.

What sort of activities are we talking about?

As mentioned above we'll use the Android Intent object, which is an object that represents a message of sorts. We'll set the object up and use it to launch a variety of different activities in order to bring functionality present in the Android eco-system 'into' our application. We'll look at displaying battery usage, viewing URLs, looking at maps, sending SMS and email messages, and scanning barcodes.

A toast before we start

Since we're looking at Android-specific things in this article it seems appropriate to make use of Android-specific features. Rather than showing messages with message boxes, we'll use Android toast messages instead. Toast messages are messages that just pop up onto the screen, rather like toast does out of a toaster. The toast API is not wrapped up by Delphi, but it's a simple API so I include my toast import unit below:

unit Androidapi.JNI.Toast;

//Java bridge class imported by hand by Brian Long (http://blong.com)

interface

uses
  Androidapi.JNIBridge,
  Androidapi.JNI.JavaTypes,
  Androidapi.JNI.GraphicsContentViewText;

type
  TToastLength = (LongToast, ShortToast);

  JToast = interface;

  JToastClass = interface(JObjectClass)
  ['{69E2D233-B9D3-4F3E-B882-474C8E1D50E9}']
    {Property methods}
    function _GetLENGTH_LONG: Integer; cdecl;
    function _GetLENGTH_SHORT: Integer; cdecl;
    {Methods}
    function init(context: JContext): JToast; cdecl; overload;
    function makeText(context: JContext; text: JCharSequence; duration: Integer): JToast; cdecl;
    {Properties}
    property LENGTH_LONG: Integer read _GetLENGTH_LONG;
    property LENGTH_SHORT: Integer read _GetLENGTH_SHORT;
  end;

  [JavaSignature('android/widget/Toast')]
  JToast = interface(JObject)
  ['{FD81CC32-BFBC-4838-8893-9DD01DE47B00}']
    {Methods}
    procedure cancel; cdecl;
    function getDuration: Integer; cdecl;
    function getGravity: Integer; cdecl;
    function getHorizontalMargin: Single; cdecl;
    function getVerticalMargin: Single; cdecl;
    function getView: JView; cdecl;
    function getXOffset: Integer; cdecl;
    function getYOffset: Integer; cdecl;
    procedure setDuration(value: Integer); cdecl;
    procedure setGravity(gravity, xOffset, yOffset: Integer); cdecl;
    procedure setMargin(horizontalMargin, verticalMargin: Single); cdecl;
    procedure setText(s: JCharSequence); cdecl;
    procedure setView(view: JView); cdecl;
    procedure show; cdecl;
  end;
  TJToast = class(TJavaGenericImport<JToastClass, JToast>) end;

procedure Toast(const Msg: string; Duration: TToastLength = ShortToast);

implementation

uses
  FMX.Helpers.Android;

procedure Toast(const Msg: string; Duration: TToastLength);
var
  ToastLength: Integer;
begin
  if Duration = ShortToast then
    ToastLength := TJToast.JavaClass.LENGTH_SHORT
  else
    ToastLength := TJToast.JavaClass.LENGTH_LONG;
  CallInUiThread(procedure
  begin
    TJToast.JavaClass.makeText(SharedActivityContext, StrToJCharSequence(msg),
      ToastLength).show
  end);
end;

end.

The ins and outs of using the Java Bridge interface to represent Java classes using Delphi interfaces (one for the class methods and one for the interface methods, bound together by the TJavaGenericImport-based generic bridge class) are a bit beyond the scope of this particular article, but I did go into it in my CodeRage 8 session on using Android APIs from Delphi XE5:

Even if we ignore the details of the listing you'll see that the specifics of the toast message, including invoking the toast in the Java UI thread as opposed to the Delphi FireMonkey thread, have been tucked away and hidden behind a simple Toast procedure, which we'll use in the sample code. The toast import unit has been named similar to how the Delphi RTL's Android import units are named with an Androidapi.JNI. prefix.

Looking at battery usage

Ok, let's take a simple example to start with. Let's look at how your application can launch the standard system battery usage activity. You'll see over in the Android API documentation that ACTION_POWER_USAGE_SUMMARY is a Java string constant defined within the Android Intent class, and is designed specifically for this purpose. This string defines the action that the intent represents, the launching of the battery usage summary activity. So we need to create an Android Intent object, tell it that its action is ACTION_POWER_USAGE_SUMMARY and then start the activity thus represented.

This can be done with this code:

unit ShowBatteryUsageActivity;

interface

procedure ShowBatteryUsage;

implementation

uses
  FMX.Helpers.Android, Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.Toast;

procedure ShowBatteryUsage;
var
  Intent: JIntent;
  ResolveInfo: JResolveInfo;
begin
  Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_POWER_USAGE_SUMMARY);
  ResolveInfo := SharedActivity.getPackageManager.resolveActivity(Intent, 0);
  if ResolveInfo = nil then
    Toast('Cannot display battery usage', ShortToast)
  else
    SharedActivity.startActivity(Intent);
end;

end. 

An Android Intent is represented in Delphi by the JIntent interface, which is returned after constructing an instance of the Android Intent class through the TJIntent bridge class's init method. init is a class method that represents a Java constructor, and this particular overload takes a Java string as an action parameter.

So the bridge class offers up the class methods (including constructors) of the class it represents (Intent in this case) through its JavaClass property. This constructor call returns us a JIntent interface, through which we can access the instance methods of our constructed Intent instance.

If you spend any time browsing the Android SDK documentation then do bear in mind that what we call class methods Java programmers call static methods.

The second statement calls onto an Android helper method to give us an interface reference that represents the FMX activity that sits behind the UI of our Android app. SharedActivity gives us a JActivity reference to represent the app's activity (although in truth the Delphi app uses an activity descendant called NativeActivity as the base class that it inherits from, which is common practice in a native code application). Through the activity reference we gain access to the device's PackageManager, which can help us understand if there is an installed package that will be able to process the intended action.

Assuming it looks like something will understand the request we ask our activity to start the activity represented by the Intent object.

It's quite straightforward stuff really and represents standard Android API programming, just represented in Delphi using the relevant bridge classes and interfaces that are necessary, most of which are provided in various RTL units, but some of which have been added in (for the toast API).

Here's the battery usage display on a HTC One:

Battery usage activity

Launching a URL

It is a common requirement to show a web page from an app. You could embed a browser component to do this or alternatively just ask the system browser to run the URL by requesting the system start an activity that can view the URL in question.

In this case we need to specify an action to view some data and pass the URL (or URI) in as the data to view. So this time we need to initialise the intent with two pieces of information: an action and some data to act upon.

Since the code we need is not very different from what we wrote above, we'll pull that code apart into more reusable routines, along with a variation that solves this new requirement:

unit LaunchActivities;

interface

procedure ShowBatteryUsage;
procedure LaunchURL(const URL: string);

implementation

uses
  FMX.Helpers.Android,
  Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.Toast,
  Androidapi.JNI.JavaTypes, Androidapi.JNI.Net;

function LaunchActivity(const Intent: JIntent): Boolean; overload;
var
  ResolveInfo: JResolveInfo;
begin
  ResolveInfo := SharedActivity.getPackageManager.resolveActivity(Intent, 0);
  Result := ResolveInfo <> nil;
  if Result then
    SharedActivity.startActivity(Intent);
end;

function LaunchActivity(const Action: JString): Boolean; overload;
var
  Intent: JIntent;
begin
  Intent := TJIntent.JavaClass.init(Action);
  Result := LaunchActivity(Intent);
end;

function LaunchActivity(const Action: JString; const URI: Jnet_Uri): Boolean; overload;
var
  Intent: JIntent;
begin
  Intent := TJIntent.JavaClass.init(Action, URI);
  Result := LaunchActivity(Intent);
end;

procedure ShowBatteryUsage;
begin
  if not LaunchActivity(TJIntent.JavaClass.ACTION_POWER_USAGE_SUMMARY) then
    Toast('Cannot display battery usage', ShortToast)
end;

procedure LaunchURL(const URL: string);
begin
  LaunchActivity(TJIntent.JavaClass.ACTION_VIEW, TJnet_Uri.JavaClass.parse(StringToJString(URL)))
end;

end.

So now we have 3 overloaded versions of a new LaunchActivity function. One takes an action string, one takes an action string and a URI, and one takes an intent object (and is called by both the other overloads once they have built up an intent object).

The battery usage routine calls the overload that just takes an action string.

The new LaunchURL routine takes the Delphi URL string and translates it into a Java string before passing this off to the parse class method of the Android URI class, in order to get a URI object returned. An intent object is then created this time with two constructor parameters: a view action and the URI.

The ACTION_VIEW data display action is designed to launch the sensible viewing action for many data types. There's bound to be one suitable or displaying  URL, either the system browser, or maybe an additional browser you have installed, so the code doesn't bother checking the return value from LaunchActivity . Indeed if there is more than one available choice and the user has not previously specified one option to always use then Android will display a chooser dialog showing all the options.

It's useful to note that Android understands a variety of URI formats and will display them in appropriate applications. For example:

Sending an SMS

You can see above how easy it is to prepare a phone call for the user. You need no special permissions to ask the system dialler to show up with a number plugged in. If you wanted to actually make the call, then you'd need to use the telephony APIs and require the appropriate CALL_PHONE permission to use it, which is perhaps not as desirable.

It's a similar story with SMSs. There are APIs that allow you to craft an SMS message and then send it, and optionally identify if it was received and so forth. However use of these APIs requires the SEND_SMS permission, and it is advisable to keep permission requirements to a minimum to avoid arousing suspicion in the user who may bypass/reject your app because of them.

That said, if you are happy to require the permission (set in your Android manifest file as documented by Google here but achieved in Delphi through the project options dialog as documented by Embarcadero here) then that is just fine. Here's a unit that does what is required:

unit AndroidStuff;

interface

function HasPermission(const Permission: string): Boolean;
procedure SendSMS(const Number, Msg: string);

implementation

uses
  System.UITypes,
  FMX.Dialogs,
  FMX.Helpers.Android,
  Androidapi.JNI.JavaTypes,
  Androidapi.JNI.GraphicsContentViewText,
  Androidapi.JNI.Telephony;

function HasPermission(const Permission: string): Boolean;
begin
  //Permissions listed at http://d.android.com/reference/android/Manifest.permission.html
  Result := SharedActivity.checkCallingOrSelfPermission(
    StringToJString(Permission)) =
    TJPackageManager.JavaClass.PERMISSION_GRANTED
end;

procedure SendSMS(const Number, Msg: string);
var
  SmsManager: JSmsManager;
begin
  if not HasPermission('android.permission.SEND_SMS') then
    MessageDlg('App does not have the SEND_SMS permission',
      TMsgDlgType.mtError, [TMsgDlgBtn.mbCancel], 0)
  else
  begin
    SmsManager := TJSmsManager.JavaClass.getDefault;
    SmsManager.sendTextMessage(
      StringToJString(Number),
      nil,
      StringToJString(Msg),
      nil,
      nil);
  end;
end;

end.

Now, back to the point about avoiding permissions. If you want to keep your permission requirements right down then the smart thinking involves launching the system SMS activity and having the user press the Send button. Launching activities requires no extra permissions primarily because the activity already exists on the device. In order to be there it is part of the system or the user has already accepted the permission requirements when installing it.

To launch the SMS app we have another routine, CreateSMS, to add to the LaunchActivities unit:

uses
  System.SysUtils, ...

...

procedure CreateSMS(const Number, Msg: string);
var
  Intent: JIntent;
  URI: Jnet_Uri;
begin
  URI := TJnet_Uri.JavaClass.parse(StringToJString(Format('smsto:%s', [Number])));
  Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_VIEW, URI);
  Intent.putExtra(StringToJString('sms_body'), StringToJString(Msg));
  LaunchActivity(Intent);
end;

A couple of things to note here:

  1. Launching the SMS activity is done through yet another form of URI launch
  2. As well as the view action and the URI data, this code also inserts an extra data item named sms_body into the intent to represent the content of the SMS message

Sending an email

With SMSs above we saw the intent starting to be loaded up with additional parameters required by the target activity. For launching an email we set even more parameters. Here's another routine for the LaunchActivities unit:

uses
  Androidapi.JNIBridge, ...

...

procedure CreateEmail(const Recipient, Subject, Content: string); overload;
var
  Intent: JIntent;
begin
  Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_SEND);
  Intent.putExtra(TJIntent.JavaClass.EXTRA_EMAIL, StringToJString(Recipient));
  Intent.putExtra(TJIntent.JavaClass.EXTRA_SUBJECT, StringToJString(Subject));
  Intent.putExtra(TJIntent.JavaClass.EXTRA_TEXT, StringToJString(Content));
  // Intent.setType(StringToJString('plain/text'));
  Intent.setType(StringToJString('message/rfc822'));
  // LaunchActivity(Intent);
  LaunchActivity(TJIntent.JavaClass.createChooser(Intent, StrToJCharSequence('Which email app?')));
end;

This sends an email to a single recipient with a specified subject and content, adding these data items in as extra string data. You can see that you could choose to send a plain text email but an RFC 822 format email is specified to try to ensure that only email apps will pick up the intent.

As well as EXTRA_TEXT you can also send HTML format content by also specifying EXTRA_HTML_TEXT.

The code doesn't wait to see if the system will pop up a chooser dialog in the case that there are multiple apps that can handle the intent. Instead it directly invokes the chooser dialog for our intent using a custom prompt string.

If you want to support multiple email recipients you have to get a bit clever with setting up Java arrays in Delphi. In this case you need to put the recipients in a Java string array. The following additional overload has some code to show you how this looks:

procedure CreateEmail(const Recipients: array of string; const Subject, Content: string); overload;
var
  Intent: JIntent;
  I: Integer;
  JRecipients: TJavaObjectArray<JString>;
begin
  Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_SEND);
  JRecipients := TJavaObjectArray<JString>.Create(Length(Recipients));
  for I := 0 to Pred(Length(Recipients)) do
    JRecipients.Items[I] := StringToJString(Recipients[I]);
  Intent.putExtra(TJIntent.JavaClass.EXTRA_EMAIL, JRecipients);
  Intent.putExtra(TJIntent.JavaClass.EXTRA_SUBJECT, StringToJString(Subject));
  Intent.putExtra(TJIntent.JavaClass.EXTRA_TEXT, StringToJString(Content));
  Intent.setType(StringToJString('message/rfc822'));
  LaunchActivity(TJIntent.JavaClass.createChooser(Intent, StrToJCharSequence('Which email app?')));
end;

As well as EXTRA_EMAIL for the recipient(s) you can also use EXTRA_CC and EXTRA_BCC.

If you want to take it a step further and send an attachment this is also possible but I'm guessing it's a little less common. There's a post on StackOverflow that shows some code to do this by getting a URI for the file location and adding that URI into an Android Parcelable object and then adding that to the intent as an EXTRA_STREAM item (though other Android code I've seen puts the URI directly into the intent, as per the documentation).

Scanning a bar code

Something that crops up regularly in discussions around mobile app building is how to incorporate barcode scanning behaviour.

Many Android users have installed the open source ZXing (Zebra Crossing) barcode scanner app, which is designed to make a scan activity available. It is quite straightforward to invoke the activity if you've checked the ZXing "documentation" and therefore know the action string and optionally some data strings:

//For more info see https://github.com/zxing/zxing/wiki/Scanning-Via-Intent
procedure LaunchQRScanner;
var
  Intent: JIntent;
begin
  Intent := TJIntent.JavaClass.init(StringToJString('com.google.zxing.client.android.SCAN'));
  Intent.setPackage(StringToJString('com.google.zxing.client.android'));
  // If you want to target QR codes
  //Intent.putExtra(StringToJString('SCAN_MODE'), StringToJString('QR_CODE_MODE'));
  if not LaunchActivity(Intent) then
    Toast('Cannot display QR scanner', ShortToast);
end;

Communication from the launched activity

However we now have something of a problem on our hands. Having launched the activity, how do we get information back from it to identify what barcode, if any, was scanned?

Before looking at the standard Android approach to this problem, let's have a small digression to a quite popular solution that ZXing makes possible. ZXing scanner puts the scanned barcode data on the clipboard when scanned, so you simply need to check the device's clipboard and if a new value appears there then that's likely to be what was scanned. This approach was documented for Delphi XE5 by John Whitham and for documented for Delphi XE6 by Steffen Nyeland.

And now back to the standard Android approach.... Earlier code snippets that launch an activity do so using the current activity's startActivity() method, which launches an activity and forgets all about it. More interesting for our current purpose is startActivityForResult(), which is an activity method that launches an activity with a specified request code and then waits for the activity to exit and return a result code. When the launched activity does exit your activity's onActivityResult() method is called with your request code and the activity's result code.

So, in order to launch an activity and pick up its result we need to implement onActivityResult() for Delphi's underlying activity. The rest of this article looks at how we achieve such a goal but we'll leave this section with the modified activity launching code:

function LaunchActivityForResult(const Intent: JIntent; RequestCode: Integer): Boolean;
var
  ResolveInfo: JResolveInfo;
begin
  ResolveInfo := SharedActivity.getPackageManager.resolveActivity(Intent, 0);
  Result := ResolveInfo <> nil;
  if Result then
    SharedActivity.startActivityForResult(Intent, RequestCode);
end;

//For more info see https://github.com/zxing/zxing/wiki/Scanning-Via-Intent
procedure LaunchQRScanner(RequestCode: Integer);
var
  Intent: JIntent;
begin
  Intent := TJIntent.JavaClass.init(StringToJString('com.google.zxing.client.android.SCAN'));
  Intent.setPackage(StringToJString('com.google.zxing.client.android'));
  // If you want to target QR codes
  //Intent.putExtra(StringToJString('SCAN_MODE'), StringToJString('QR_CODE_MODE'));
  if not LaunchActivityForResult(Intent, RequestCode) then
    Toast('Cannot display QR scanner', ShortToast);
end;

Practical limitations of Delphi XE5's Android support

Delphi apps on Android are native code libraries starting at a few megabytes in size in size, packaged into an .apk file along with some compiled Java, a few resources and an Android manifest. The native ARMv7 code can communicate with the Java world using the Java Bridge (or JNI Bridge as it is sometimes called), but this relies on Delphi representations of the Java classes being present.

This is quite like Delphi for Win32 talking to Windows APIs - many APIs are pre-declared for you and some you need to create the declarations for yourself if they aren't catered for by the Delphi RTL. Similarly Delphi XE5 has many Android classes represented, but also many more that are not, so there is good scope for being required to get on top of Delphi's Java Bridge to call into the Android API.

FireMonkey provides much functionality necessary for business applications needing to access data and display it to the user, but some common aspects of the Android OS are not 'wrapped up' in FireMonkey classes.

At this point you can call upon the various Android APIs that have been provided, or craft new Java Bridge APIs if need be (as we did for the toast API earlier).

In cases where this isn't simply a case of calling into more APIs whose declarations need to declared with Java Bridge interfaces this poses a problem. Some examples of areas of the Android OS that are not wrapped up in XE5 and are difficult to incorporate are:

In Delphi XE5 these things and more require additional Java code to implement. Then some light hacking is needed to make use of this Java code in a Delphi package, which tends to get in the IDE's way, so IDE building/installing of the application becomes interesting, and debugging becomes next to impossible. These consequences tend to force you into managing a pair of synchronised projects:

Handling a launched activity's result

The specifics of the approach to handle a launched activity result will require some of the aforementioned light hacking. This may seem rather onerous and cumbersome but it is what it is and, in Delphi XE5, it is necessary.

The steps required for responding to a launched activity's result are as follows:

That's quite a daunting list but fear not. Fortunately many of the Java-oriented steps can be wrapped up by a suitably crafted batch (.bat) file or command script (.cmd) file. And I'm sure if you really wanted to, you could also create a PowerShell script to do the same, but I've stuck with the old scripting that I know how to work with. 

Setting up the Java code

The standard activity in any Delphi application is a Java class called FMXNativeActivity in the com.embarcadero.firemonkey package (or namespace), which actually inherits from the Android NativeActivity class. We need to inherit from FMXNativeActivity, override onActivityResult() and have it call a native method with an equivalent signature. Here's the code I rustled up for the job, which can be found in the java\src\com\blong\test directory under the project directory in a file called NativeActivitySubclass.java.

package com.blong.test;

import android.os.Bundle;
import android.util.Log;
import android.content.Intent;

public class NativeActivitySubclass extends com.embarcadero.firemonkey.FMXNativeActivity {

    static final String TAG = "NativeActivitySubclass";
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Custom initialization
        Log.d(TAG, "onCreate");
    }
   
    public native boolean onActivityResultNative(int requestCode, int resultCode, Intent data);
   
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data){
        Log.d(TAG, "onActivityResult(" + requestCode + ", " + resultCode + ", " + (data != null ? data : "(null)") + ")");
        if (!onActivityResultNative(requestCode, resultCode, data))
            super.onActivityResult(requestCode, resultCode, data);
    }
}

You might notice the Java code has a bunch of trace statements in (calls to Log.d()). You can also emit the same type of trace calls from your Delphi code by ensuring FMX.Types is in your uses clause and calling Log.d there. These trace statements can be viewed in the Android Device Monitor or its deprecated predecessor Dalvik Debug Monitor Server (DDMS).

Also, tucked away in the code is a dialog display helper message, which we'll come back to look at later on.

Building the Java code

In the java project subdirectory is a batch file called build.bat. Before invoking the batch file you must ensure you have done the required preparatory work.

Prep step #1: The first preparatory job is to add a few directories from the Android SDK and the JDK to your System path.

You can do this either:

Note that the required directories will vary depending on whether you already had an Android SDK installed, or whether you let Delphi install the Android SDK and, if so, where you told it to install it. For example you may have already installed the Android SDK in C:\Android\android-sdk-windows, or Delphi may have installed it into C:\Users\Public\Documents\RAD Studio\12.0\PlatformSDKs\adt-bundle-windows-x86-20130522\sdk\android-4.2.2 or you may have given Delphi a specific target directory, meaning the Android SDK path is, say, C:\PlatformSDKs\adt-bundle-windows-x86-20130522\sdk\android-4.2.2.

Directories to add to the path are:

I'd certainly recommend extending the Windows search path permanently through the environment variables option in the system properties dialog so you can run Android SDK commands and Java commands at any point going forwards, but if you wanted to run SET commands at the command prompt or from within the batch file they might look a little like this:


SET PATH=%PATH%;C:\Android\android-sdk-windows\build-tools\18.0.1
SET PATH=%PATH%;C:\Program Files (x86)\Java\jdk1.6.0_23\bin
 

Once the system path has been updated you will be able to successfully launch the following executables from a command prompt. If you updated the global path then it will be any command prompt launched after you've made the change. If you changed a local environment by running commands at a command prompt then you can run the commands from that command prompt. The commands are:

If you cannot launch any of these commands then go back and review your path settings as they are likely wrong.

Prep step #2: The next preparatory job is to open the batch file in an editor and make appropriate edits to ensure the various environment variables set at the start are correct.

This is what the batch file looks like:

@echo off

setlocal

if x%ANDROID% == x set ANDROID=C:\Users\Public\Documents\Embarcadero\RAD Studio\12.0\PlatformSDKs\adt-bundle-windows-x86-20130522\sdk
set ANDROID_PLATFORM=%ANDROID%\platforms\android-17
set DX_LIB=%ANDROID%\build-tools\android-4.2.2\lib
rem if x%ANDROID% == x set ANDROID=C:\Android\android-sdk-windows
rem set ANDROID_PLATFORM=%ANDROID%\platforms\android-15
rem set DX_LIB=%ANDROID%\build-tools\18.0.1\lib
set EMBO_DEX="C:\Program Files (x86)\Embarcadero\RAD Studio\12.0\lib\android\debug\classes.dex"
set PROJ_DIR=%CD%
set VERBOSE=0

echo.
echo Compiling the Java native activity descendant
echo.
mkdir output\classes 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=-verbose
javac %VERBOSE_FLAG% -Xlint:deprecation -cp %ANDROID_PLATFORM%\android.jar;classes.jar -d output\classes src\com\blong\test\NativeActivitySubclass.java
rem javac -source 1.6 -target 1.6 %VERBOSE_FLAG% -Xlint:deprecation -cp %ANDROID_PLATFORM%\android.jar;classes.jar -d output\classes src\com\blong\test\NativeActivitySubclass.java

echo.
echo Creating jar containing the new class
echo.
mkdir output\jar 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=v
jar c%VERBOSE_FLAG%f output\jar\test_classes.jar -C output\classes com

echo.
echo Converting from jar to dex...
echo.
mkdir output\dex 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=--verbose
call dx --dex %VERBOSE_FLAG% --output=%PROJ_DIR%\output\dex\test_classes.dex --positions=lines %PROJ_DIR%\output\jar\test_classes.jar

echo.
echo Merging dex files
echo.
java -cp %DX_LIB%\dx.jar com.android.dx.merge.DexMerger %PROJ_DIR%\output\dex\classes.dex %PROJ_DIR%\output\dex\test_classes.dex %EMBO_DEX%

echo Tidying up
echo.
del /Q output\classes\com\blong\test\*
rmdir output\classes\com\blong\test
rmdir output\classes\com\blong
rmdir output\classes\com
rmdir output\classes
del output\dex\test_classes.dex
del output\jar\test_classes.jar
rmdir output\jar

echo.
echo Now we have the end result, which is output\dex\classes.dex

:Exit

endlocal

You should modify the set commands for the following environment variables and ensure they refer to appropriate existing directories:

Prep step #3: The final preparatory job before running the batch file is to generate a compiled Java version of Embarcadero's classes.dex file, which will be a file called classes.jar.

The current classes.dex file, found in C:\Program Files (x86)\Embarcadero\RAD Studio\12.0\lib\android\debug and also in C:\Program Files (x86)\Embarcadero\RAD Studio\12.0\lib\android\release was compiled Java code, but has been run through the Android SDK dx tool to generate DEX (Dalvik Executable) code. We need to undo that step to give us a file we can use to help our descendant activity class compile ok.

To turn a .dex file back into a .jar file you can use the open source Dex2Jar tool. Once you've installed Dex2Jar you can use a command like this to process the file:

d2j-dex2jar.bat -o classes.jar "C:\Program Files (x86)\Embarcadero\RAD Studio\12.0\lib\android\debug\classes.dex"

Be sure to put the new classes.jar file in your project's java subdirectory.

Beware that if you move to a different version of Delphi, which includes applying update packs to your current version of Delphi, then you will need to re-do this in case the code in classes.dex was updated and/or changed.

Now, at long last, we can run the Java build script...

To run the build script requires you to first launch a RAD Studio Command Prompt - you can do this in Windows 7 or Windows 8.x by going to the Start menu or Start screen and typing:

  RAD Studio

and then selecting the RAD Studio Command Prompt item that is found. Or you can just hunt through the program groups to find it.

In the RAD Studio Command Prompt you should change directory to the one containing the build.bat batch file and then invoke it by running: build

This should proceed to:

If any of these steps fails then you should review the environment variables and see which one has not been set correctly.

In the case that the dx command fails with the error:

bad class file magic (cafebabe) or version (0033.0000)

then this has a specific cause. You have JDK 1.7 installed (this is what Delphi will install for you) but dx expects Java code as compiled by JDK 1.6.

To resolve this issue, you can either switch back to JDK 1.6 or modify the javac.exe command-line to have some additional command-line switches, which force the JDK 1.7 compiler to emit JDK 1.6 compatible Java byte code.

Edit the build script batch file to include this in the javac command-line:

-source 1.6 -target 1.6

Now try and rebuild and the error should not recur.

After a successful build you will have a new classes.dex file in the project's java\output\dex directory, which contains the native activity descendant class that will respond to launched activity results in addition to all the regular FMX Java code and Android support library included there by default.

Setting up the deployment manager

At last it is time to open the sample project in the Delphi XE5 IDE. The main UI-based functionality in the application is quite trivial - it is a bunch of buttons that call the activity launching functionality looked at thus far in this article, as well as a final button that launches the barcode scanner to scan a barcode.

We'll come back to this last button shortly. First, however, we have a little more setup to do in the IDE, so choose the Project, Deployment menu item, which gives us this:

Delphi Deployment Manager

You should note that the normal classes.dex file from the RAD Studio directory has been de-selected. Instead, the newly created classes.dex in the java\output\dex directory has been selected to be deployed to the same place: the Android package's classes directory. This gets our native activity descendant class on board within the Android package file.

Points to note about Delphi's deployment manager

You should be aware that the Deployment Manager in Delphi XE5 has a problem with substituting alternate versions of classes.dex into Android application packages. The problem is logged in Quality Central as QC 118472.

The issue manifests itself by Delphi automatically re-selecting the default shipped classes.dex when you switch configurations in the Deployment Manager. This overrides the replacement version and so things very much stop going according to plan, so keep an eye open for this issue occurring.

Setting up the native activity result method

The Java code calls down to a native onActivityResultNative method, which is implemented and registered in the code you see below. As you glance up and down the code you might observe that it's a bit hairy, but let's look through it and see if we can get a handle on it.

uses
  System.Classes,
  Androidapi.NativeActivity,
  Androidapi.JNI,
  Androidapi.JNIBridge,
  Androidapi.JNI.GraphicsContentViewText,
  ...

{$REGION 'JNI setup code and callback for onActivityResult() native'}
//This is called from the Java activity's onActivityResult() method
function OnActivityResultNative(PEnv: PJNIEnv; This: JNIObject;
  RequestCode, ResultCode: Integer; dataIntent: JNIObject): Boolean; cdecl;
var
  SyncResult: Boolean;
begin
  Log.d('Queuing native routine to run synchronized');
  TThread.Queue(nil,
    procedure
    var
      Data: JIntent;
    begin
      Log.d('+ThreadSwitcher');
      Log.d('Thread: Main: %.8x, Current: %.8x, Java:%.8d (%2:.8x)', [MainThreadID, TThread.CurrentThread.ThreadID,
        TJThread.JavaClass.CurrentThread.getId]);
      Data := nil;
      if Assigned(DataIntent) then
        Data := TJIntent.Wrap(DataIntent);
      SyncResult := MainForm.OnActivityResult(requestCode, resultCode, Data);
      Log.d('-ThreadSwitcher');
    end);
  Result := SyncResult;
end;

procedure TMainForm.RegisterDelphiNativeMethods;
var
  PEnv: PJNIEnv;
  ActivityClass: JNIClass;
  NativeMethod: JNINativeMethod;
begin
  PEnv := TJNIResolver.GetJNIEnv;
  NativeMethod.Name := 'onActivityResultNative';
  NativeMethod.Signature := '(IILandroid/content/Intent;)Z';
  NativeMethod.FnPtr := @OnActivityResultNative;
  ActivityClass := PEnv^.GetObjectClass(
    PEnv, PANativeActivity(System.DelphiActivity).clazz);
  PEnv^.RegisterNatives(PEnv, ActivityClass, @NativeMethod, 1);
  PEnv^.DeleteLocalRef(PEnv, ActivityClass);
end;
{$ENDREGION}

The actual native Delphi routine is in fact implemented as the OnActivityResultNative function. Like all JNI functions it uses the C calling convention and has the same first couple of arguments. The first argument is the JNI interface pointer, through which we manipulate Java objects. The second argument points to the Java object for an instance method or to the Java class for a class method, rather like Self does in Delphi methods.

After these initial arguments we have JNI representations of the arguments taken by the Java onActivityResult() method, which in this case include two primitive types - integers - and a reference type - an Intent object reference. The intent argument becomes a JNIObject, which is Delphi's version of the JNI specification's jobject type, used to represent reference types.

When this native method gets called we queue up some code to run in the FMX thread (remember that the JNI native routine is invoked in the Java UI thread). The code invokes a method on the main form passing the received parameters, wrapping the JNI intent parameter in a JIntent Java bridge (aka JNI bridge) interface reference to give the form method access to the Java intent object. The called method has been added to look like a regular method, maybe even look a bit like an event handler, to handle the activity result callback.

Ahead of any callbacks actually occurring this JNI routine must first be registered as the implementation of the native method onActivityResult(), which can be called from Java. This task is performed by RegisterDelphiNativeMethods, which gets called in the form's OnCreate event handler.

RegisterDelphiNativeMethods uses an RTL helper to get a JNI interface pointer (the same sort of thing that is given to a JNI method as the first parameter) that will allow the FireMonkey activity object to be manipulated. Then it fills up a JNINativeMethod record with information that JNI can use to map the Java method name and signature to our Delphi routine. Getting the right signature in the relevant format requires a little bit of know-how.

The trick here is to use one of the JDK tools to dump information about NativeActivitySubclass and this will give us the appropriately encoded signature information. The tool in question is javap.exe: the Java class file disassembler.

As it is currently set up the build script deletes all the intermediate files, including the compiled activity descendant. So to work this step through (if you really want to run it - bear in mind I've already done this and used the information gleaned therein within the code) you'd need to re-run the build script after commenting out (or deleting) the tidying up section, particularly removing this line:

del /Q output\classes\com\blong\test\*

Now inside of your project's java\output\classes\com\blong\test directory you will have a few .class files. From your project's java directory you can run this command:

javap -s -classpath output\classes com.blong.test.NativeActivitySubclass

and this will emit information about all the method signatures of the specified class. I've highlighted the key pieces of information that have been used to set up the JNI registration record.

Compiled from "NativeActivitySubclass.java"
public class com.blong.test.NativeActivitySubclass extends com.embarcadero.firemonkey.FMXNativeActivity{
static final java.lang.String TAG;
  Signature: Ljava/lang/String;
public com.blong.test.NativeActivitySubclass();
  Signature: ()V
protected void onCreate(android.os.Bundle);
  Signature: (Landroid/os/Bundle;)V
public native boolean onActivityResultNative(int, int, android.content.Intent);
  Signature: (IILandroid/content/Intent;)Z
protected void onActivityResult(int, int, android.content.Intent);
  Signature: (IILandroid/content/Intent;)V
public void showDialog(java.lang.String, java.lang.String);
  Signature: (Ljava/lang/String;Ljava/lang/String;)V
}

RegisterDelphiNativeMethods then proceeds to use JNI routines to get a local reference to the FireMonkey activity object's class, which it uses to register the native method against (the native method is part of our app's activity class) before cleaning up the local reference.

There is quite a bit to think about when setting this up in a new application but hopefully with the various steps explained it at least becomes understandable.

After that tortuous journey we can now actually write a handler for the activity result!

const
  ScanRequestCode = 0;

procedure TMainForm.BarcodeScannerButtonClick(Sender: TObject);
begin
  LaunchQRScanner(ScanRequestCode);
end;

function TMainForm.OnActivityResult(RequestCode, ResultCode: Integer; Data: JIntent): Boolean;
var
  ScanContent, ScanFormat: string;
begin
  Result := False;
  //For more info see https://github.com/zxing/zxing/wiki/Scanning-Via-Intent
  if RequestCode = ScanRequestCode then
  begin
    if ResultCode = TJActivity.JavaClass.RESULT_OK then
    begin
      if Assigned(Data) then
      begin
        ScanContent := JStringToString(Data.getStringExtra(StringToJString('SCAN_RESULT')));
        ScanFormat := JStringToString(Data.getStringExtra(StringToJString('SCAN_RESULT_FORMAT')));
        Toast(Format('Found %s format barcode:'#10'%s', [ScanFormat, ScanContent]), LongToast);
      end;
    end
    else if ResultCode = TJActivity.JavaClass.RESULT_CANCELED then
    begin
      Toast('You cancelled the scan', ShortToast);
    end;
    Result := True;
  end;
end;

Delphi app receiving scanned barcode info

Building the app

The application can now be compiled to a native Android library and then deployed into an Android application package (.apk file) using the Project, Compile ActivitiesXE5 and Project, Deploy libActivitiesXE5.so menu items respectively.

If you wish you can skip those two steps and have them done implicitly by choosing the Run, Run Without Debugging menu item (or by pressing Ctrl+Shift+F9). This will uninstall the app if already installed, and then install the new version onto the target device.

Note that despite choosing the Run Without Debugging menu item, the application will not be launched on the target device. The same is true if you choose Run, Run (F9). This is due to another shortcoming of the IDE in which it assumes the launch activity will be the stock FireMonkey activity and so is unable to contend with the situation we have here where we are using a descendant of the stock activity, which has a different class name. This issue is open in Quality Central as QC 118450.

Notes on working around issues developing your app

Clearly with the issues with launching the application there is no possibility of debugging your code. You cannot attach to a running Android process as you can with a Windows process, for example.

If you need to actively debug (as opposed to some agricultural equivalent such as using calls to Log.d from the FMX.Types unit) then it is incumbent upon you to set up a pair of almost-mirror projects. One project will have all these hacks described thus far on this page, and will enable launched activity results to be received by your app. The other project will use the same application logic and units, but will ignore all aspects of setting up activity result response. Consequently this second project should support regular Android app debugging.

Over and above debugging your applications there are some issues with the code you put in an application that is invoked from an Android callback, as we have done here. You have seen that we can be successfully notified of an activity finishing and returning a result code, and we've emitted a toast message and all was well.

Or so it seems.

Actually I deliberately chose to use a toast rather than a message box (as in ShowMessage, MessageDlg et al) for the simple reason that I didn't want my demo app to hang.

The thing is that for some odd reason, which has been consistent through the release of both Delphi XE5 and Delphi XE6, if FMX code is called from some Android callback routine and that FMX code invokes code that has a nested message loop (as in the sort of thing that ShowMessage and MessageDlg do) then you won't see the message box. Instead the app will turn into a black screen. Eventually after some delay the OS will step in with an ANR message (Application is Not Responding), offering to close the offending app.

This is not good.

Actually I deliberately chose to use a toast rather than a message box (as in ShowMessage, MessageDlg et al) for the simple reason that doing so adds in something else to remember.

Specifically, what you must remember is that if FMX UI code is called from some Android callback routine (such as this onNewIntent method) you must remember to get the code to run in the FMX UI thread, not the Android/Java UI thread, otherwise the app will disappointingly hang.

It's quite straightforward, you can just run the code through TThread.Queue and it will get synchronised to run in the FMX UI thread, rather like this:

TThread.Queue(nil,
  procedure
  var
    Msg: string;
  begin
    Msg := Format('Found %s format barcode:'#10'%s', [ScanFormat, ScanContent]);
    MessageDlg(Msg, TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOk], 0, TMsgDlgBtn.mbOk, nil);
  end);

Conclusion

You can easily extend the functionality of your application by taking advantage of pre-installed system activities, and you can also make use of custom activities if you check whether they are available. This is a convenient model and goes some way to bring us the component-building model of bolting together large lumps of code into an application that was promised in the 90s.

However when it comes to receiving feedback from launched activities in a Delphi Android application things quickly start to get a bit sticky. The onerous Java-based steps for dealing with the problem are not outlandish, but they are definitely tedious and cumbersome. It is a fact that Delphi XE6 makes the business of hooking into launched activity result codes considerably simpler than this and we'll look at that in an update to this article. However even in Delphi XE5 we can achieve communication from launched activities if it is required.

There are some wrinkles in how this all comes together, but Delphi XE5 is Embarcadero's first stab at Android development support, so hopefully some of these wrinkles will smooth out over time and things will continue to improve in future product updates and releases.


Go back to the top of this page