Creating a splash screen for 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 XE4 and Delphi XE5 both support building iOS applications. One of the neat things about a Delphi iOS app is that it automatically gets a splash screen by default. You simply customise which image is to be used and away you go.

This all works straightforwardly because iOS is set up ready to pop up the splash screen as specified in the application's configuration data during the loading/invocation process.

Unfortunately Android has no equivalent in-built splash screen behaviour, and as a consequence Delphi XE5 Android apps do not get a similar splash screen created for them. This is a little disappointing as, given the average large size of a Delphi XE5 Android app, its load time (certainly the first load time of the first invocation, at least) is often rather longer than we might wish on our users.

The Android package (.apk file) is an archive containing the compiled Delphi code in an Android native library, as well a bunch of compiled Java startup and support code in a classes.dex file and an optimised/crunched version of the various necessary Android resources and the application manifest. On first invocation the (sizeable) native library is pulled out of the archive by the Android package loader and stored outside for future immediate availability.

This operation can be quite tardy on devices that don't have cutting edge hardware and it's only when the library has been extracted that execution of your Delphi code can actually start. When an Android app starts, the FMX startup kicks off and does various setup jobs, such as copying any suitably located asset files from the package out onto the device storage and running the initialization sections of all units compiled into the library.

So the question arises as to how we can rectify this Delphi omission and set up a splash screen for an Android app built with XE5, the first version of Delphi that supports building Android apps.

It turns out there are several choices available to us. They vary in difficulty (to implement) and effectiveness, so let's briefly run through the available options.

Options

A Delphi splash screen

Back in January 2014, Marco Cantù blogged about how to set up a splash screen for Android apps and this was re-posted on FMX Express here, by simply using a Delphi form in your Android app to do the job. If this form is the main form then the first thing the user sees is the splash screen, just as with a Delphi Win32 application.

The main argument against this approach is that the splash screen still won't appear until the native library has been extracted from the app package (on first invocation), the compiled Java startup startup code has been loaded and requested to call into the native Delphi library, the previously extracted Delphi library has then been loaded for execution purposes and all the FMX startup has run. On any moderate hardware the user is still likely to have a few seconds looking at a black screen waiting for the Delphi native code to start running, and quite possibly rather more than a few.

This is certainly the most straightforward approach to the problem but doesn't really help with the need for a splash screen caused by the very nature of Delphi XE5 Android apps - the time it takes to start executing Delphi code.

An Android resource splash screen

A splendid OS native solution to the problem is to modify your application's personal Android app theme. This involves adding a styles file in the Android resource directory tree, which defines an Android app theme, along with a referenced splash image. The Android manifest is then tweaked to reference the theme defined in this styles file.

This is quite straightforward, although it involves several steps and a few things set up in the Deployment Manager.

You can see the details of this approach in this Chinese blog post from November 2013 (if you read it in Chrome it will offer to translate it for you). Another run-through appears on this Korean blog post from February 2014, which has an accompanying sample application available. These are both referenced in this FMX Express post.

One down side to this approach is that after the splash screen has been displayed, if the app is running on hardware unsuitable for Delphi applications (i.e. an ARM v6 processor, an ARM v7 processor that has no NEON instruction support, or an Intel processor) then the app can still just gracelessly terminate with a system error message.

Note that as of Delphi XE7 and RAD Studio XE7 this is the approach used to implement the default splash screen support.

A Java splash screen

The third option was actually the first one to be publicised as an option for Delphi XE5 Android apps and I showed the technique in one of my two CodeRage 8 sessions back in October 2013, with the code made available in this blog post and reposted on FMX Express here.

This is the most complicated approach to the problem but has the advantage that it implements a splash screen in much the same way as Java Android programmers commonly implement splash screens. As soon as Android has loaded the application package and started running the main activity's code then the splash screen will appear, as the main activity either is the splash screen or directly invokes the splash screen.

The splash screen is actually written as Java code to accomplish this, and it is designed with a standard Android activity layout. One opportunity afforded by this approach is to run some sanity checking code to ensure the CPU hardware and Android OS version are actually compatible with Delphi applications.

This third option was the approach I took when considering how to solve this problem and so the rest of this article looks at how we can accomplish the goal.

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 ARM 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 many more 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 access to data and display it to the user, but some common aspects of the Android OS are not 'wrapped up' in FireMonkey classes. 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:

Implementing a splash screen

The specifics of the approach will require some of the aforementioned light hacking. This may seem rather onerous and cumbersome for such a simple result as creating a splash screen, but becoming familiar with circumventing the normal Delphi build cycle becomes useful when you decide you need some of the other Android aspects that also require some of the same jiggery-pokery.

One of the benefits of this splash screen implementation approach is that it does take the opportunity to do some hardware checking and fail gracefully if the hardware or OS is not suitable for running a Delphi XE5 app. In Delphi XE6 this particular benefit goes away as the FMX Java startup code does this for us.

The steps required for the splash screen setup 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 Android resources

Android resource files usually reside in a res folder under the project directory, so we'll stick with that norm.

There are three resource files required to get the splash screen running as required here:

The layout resource is a file called res\layout\splash-activity.xml and contains this layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
  <ImageView android:src="@drawable/splash"
            android:scaleType="centerInside"
            android:layout_centerInParent="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
</RelativeLayout>

This defines an Android layout that fills its parent (essentially the screen) and has an image view control centred inside it containing a drawable resource (an image) called splash. The image will be made as large as the image.

To set up the image we can either create a suitable file as res\drawable\splash.png or, as is the case in this example, we can have the Deployment Manager do that later.

Finally, the string values file is called res\values\strings.xml and looks like this:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="unsupported_os">Sorry, this app only supports the following
    Android vesions:\n\n
      - GingerBread MR1\n
      - Ice Cream Sandwich MR1\n
      - Jellybean (or later)\n\n
    :o(</string>
  <string name="cpu_not_arm7">This CPU is not an ARMv7 CPU, which is required
    by the FM Application Platform as used in this app, so we cannot proceed
    :o(</string>
  <string name="no_neon_support">This CPU is ARMv7, but alas doesn\'t support
    NEON instructions as required by the FM Application Platform as used in
    this app, so we cannot proceed :o(</string>
</resources>

If your Delphi app is published on the Google Play store then the fact that it contains an ARM 7 native library means it won't be available to any Android devices with other CPU hardware and so some of these messages won't be relevant.

However if you distribute the app in an ad hoc manner, say just as a download from your site, then it may get installed on a device with an Intel processor, and so having all these messages available is more important.

These string values will get referenced from the Java code. The point of having string constants isolated into value files allows easy translation. You can simply add another file called res\values-fr\strings.xml and redefine any or all of the strings in the original file in French and, if the app is run on a device set to French, the French string will automatically be used by the app.

Setting up the Java code

The Java splash screen activity class is in a file in the project directory called java\src\com\blong\test\SplashActivity.java and looks like this:

package com.blong.test;

public class SplashActivity extends Activity
{
  private boolean active = true;

  private int elapsed = 0;

  private static int SPLASH_TIME_OUT = 3000;

  private static int SPLASH_INTERVAL = 100;

  int get_resource_id(String resourceName, String resourceType)
  {
    return this.getResources().getIdentifier(resourceName, resourceType,
      this.getPackageName());
  }

  int get_layout_id(String resourceName)
  {
    return get_resource_id(resourceName, "layout");
  }

  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);

    requestWindowFeature(Window.FEATURE_NO_TITLE);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

    setContentView(get_layout_id("splash_activity"));

    new Thread(new Runnable()
    {
      @Override
      public void run() {
        try
        {
          int elapsed = 0;
          while (SplashActivity.this.active && (elapsed < SPLASH_TIME_OUT))
          {
            Thread.sleep(SPLASH_INTERVAL);
            if (SplashActivity.this.active)
              elapsed += SPLASH_INTERVAL;
          }
        }
        catch (InterruptedException e)
        {
          // Nothing
        }
        finally
        {
          finish();
          if (SplashActivity.this.active)
          {
            Intent launchIntent = new Intent();
            Log.d("Splash", "Launching the main activity now");
            launchIntent.setClassName(SplashActivity.this, "com.embarcadero.firemonkey.FMXNativeActivity");
            startActivity(launchIntent);
          }
        }
      }
    }).start();
  }

  protected void onDestroy()
  {
    Log.d("Splash", "onDestroy");
    this.active = false;
    super.onDestroy();
  }

  public boolean onTouchEvent(MotionEvent anEvent)
  {
    if (anEvent.getAction() == MotionEvent.ACTION_DOWN)
    {
      elapsed = SPLASH_TIME_OUT;
      return true;
    }
    return false;
  }

}

On creation it hides the Android title bar and goes full screen, then loads up the activity layout resource we saw earlier. Then it starts a thread to run for a few seconds in the background, hopefully giving the native library chance to be copied from the Android package (if necessary) and brought into memory. When the background thread has run for 3 seconds (or the user has touched the splash screen) it then launches the FireMonkey activity to take over and execute the application as usual.

While we're here, let's check the app will run

Seeing as this custom Java code will execute before any of the Delphi code can run it seems an ideal place to add in some code that checks out the OS version and the CPU capabilities to ensure they match Delphi's requirements.

Here's my code that does this when the splash activity starts up:

  int get_string_id(String resourceName)
  {
    return get_resource_id(resourceName, "string");
  }

  protected void onResume()
  {
    super.onResume();

    //Check OS is an appropriate version
    if ((Build.VERSION.SDK_INT == 10) ||  //GINGERBREAD_MR1
        (Build.VERSION.SDK_INT == 15) ||  //ICE_CREAM_SANDWICH_MR1
        (Build.VERSION.SDK_INT == 16) ||  //JELLY_BEAN
        (Build.VERSION.SDK_INT == 17) ||  //JELLY_BEAN_MR1
        (Build.VERSION.SDK_INT >= 18))    //JELLY_BEAN_MR2 or later
    {
      //Delphi FireMonkey supports you
      Log.d("Splash", "Supported OS version");
    }
    else
    {
      //Unsupported OS version
      Log.e("Splash", "Unsupported OS version :o(");
      active = false;
      Toast.makeText(this, get_string_id("unsupported_os"),
        Toast.LENGTH_LONG).show();
      return;
    }
    try
    {
      //Check for NEON
      String cpuFilename = "/proc/cpuinfo";
      File cpuFile = new File(cpuFilename);
      if (cpuFile.exists())
      {
        FileInputStream fis = new FileInputStream(cpuFilename);
        //Scan the whole file in as a delimited token, where the delimiter isn't
        //actually there, so we end up with just one token: the entire file content
        java.util.Scanner s = new java.util.Scanner(fis).useDelimiter("\\A");
        String cpuinfo = s.hasNext() ? s.next() : "";
        if (cpuinfo.contains("ARMv7"))
        {
          Log.d("Splash", "ARMv7 CPU detected");
        }
        else
        {
          Log.e("Splash", "ARMv7 CPU not detected!!! :o(");
          active = false;
          Toast.makeText(this, get_string_id("cpu_not_arm7"),
            Toast.LENGTH_LONG).show();
          return;
        }
        if (cpuinfo.contains("neon"))
        {
          Log.d("Splash", "NEON instructions supported");
        }
        else
        {
          Log.e("Splash", "NEON instructions not supported :o(");
          Toast.makeText(this, get_string_id("no_neon_support"),
            Toast.LENGTH_LONG).show();
          active = false;
        }
      }
    }
    catch (java.io.IOException e)
    {
      Log.d("Splash", e.toString());
    }
  }

There's nothing much to say about the code in there - just code that looks in the Android /proc/cpuinfo file for the relevant indicators and potentially produces toast pop-up messages using the string value resource IDs. However it neatly finishes off a Delphi XE5 application, which without any such checks may potentially fail unpleasantly.

Note that Delphi XE6 will check the processor is ARM 7 and supports NEON instructions in its own Java startup code. However it doesn't do any checks against the running Android version.

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 added 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 in, 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.

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.

Before changing to the directory that contains the batch file using the CD /D <path_to_batch_file_dir> command you must 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\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 splash screen activity source file
echo.
mkdir output\classes 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=-verbose
javac %VERBOSE_FLAG% -Xlint:deprecation -cp %ANDROID_PLATFORM%\android.jar -d output\classes src\com\blong\test\SplashActivity.java

echo.
echo Creating jar containing the new classes
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 output\classes\com\blong\test\SplashActivity.class
del output\classes\com\blong\test\SplashActivity$1.class
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 exising directories:

In the RAD Studio Command Prompt you should change directory to the one containing the build 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 new splash screen code 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 functionality in the application is irrelevant and in this case it is intensely trivial. 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

There are a couple of things to note about what is set up here:

  1. 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 all our splash screen startup Java code on board within the Android package file.
  2. The activity layout file, splash_activity.xml, is set to be deployed to the res\layout directory.
  3. The string values file, strings.xml, is set to be deployed to the res\values directory.
  4. The splash screen image file is set to be deployed to the res\drawable directory. You can choose whatever splash image you like, but the sample project uses the launch screen that Delphi XE5 offers for iPad apps: FM_LaunchImagePortrait_768x1004.png.

Delphi sample splash screen

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 splash screen

In order for the splash screen activity to be launched when Android loads the application package the Android manifest must be updated to reflect this requirement.

When you first compile a Delphi project that is set to target Android, an Android manifest template is written into the project directory by the name of AndroidManifest.template.xml. This is a templatised file that is expanded during compilation cycle into the real AndroidManifest.xml file in the project's Android\Debug or Android\Release directory.

The Android manifest for a Delphi Android app normally specifies that the FireMonkey activity, com.embarcadero.firemonkey.FMXNativeActivity, is the default activity by virtue of a nested intent filter. The activity in the manifest template file looks like this by default:

<activity android:name="com.embarcadero.firemonkey.FMXNativeActivity"
        android:label="%activityLabel%"
        android:configChanges="orientation|keyboardHidden">
    <!-- Tell NativeActivity the name of our .so -->
    <meta-data android:name="android.app.lib_name"
        android:value="%libNameValue%" />
    <intent-filter>  
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

To make the splash screen be the main startup activity, which will launch the FireMonkey activity, this needs to be updated to look as follows. Essentially we have another activity defined, and that new activity takes the intent filter from the FireMonkey activity.

<activity android:name="com.blong.test.SplashActivity"
          android:screenOrientation="portrait">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<!-- Our activity is a subclass of the built-in NativeActivity framework class.
     This will take care of integrating with our NDK code. -->
<activity android:name="com.embarcadero.firemonkey.FMXNativeActivity"
          android:label="%activityLabel%"
          android:configChanges="orientation|keyboardHidden"
          android:screenOrientation="portrait">
    <!-- Tell NativeActivity the name of our .so -->
    <meta-data android:name="android.app.lib_name"
        android:value="%libNameValue%" />
</activity>

You can see the actvity is defined by its package-qualified class name and is specified to always display in portrait mode.

Building the splash screen app

The application can now be compiled to a native Android library and then deployed onto an Android application package (.apk file) using the Project, Compile SplashScreenTestXE5 and Project, Deploy libSplashScreenTestXE5.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 FireMonkey activity and so is unable to contend with the situation we have here where the launch activity has been switched. This issue is open in Quality Central as QC 118450.

Notes about 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 a splash screen in your application. The other project will use the same application logic and units, but will ignore all aspects of setting up a splash screen. Consequently this second project should support regular Android app debugging.

Conclusion

Setting up an Android splash screen is feasible. There are three ways to do it and they have various pros and cons. The approach detailed at length on this page is the most cumbersome to set up but offers the most benefits - a splash screen and suitability checking of the OS and CPU as well as familiarity with using custom Java code to achoieve your ends, which can be useful in various areas of Android development with Delphi.

Some aspects of this approach have been slightly simplified in Delphi XE6. An XE6 equivalent of this page is available here.


Go back to the top of this page