JPPF, java, parallel computing, distributed computing, grid computing, parallel, distributed, cluster, grid, cloud, open source, android, .net
JPPF, java, parallel computing, distributed computing, grid computing, parallel, distributed, cluster, grid, cloud, open source, android, .net
JPPF

The open source
grid computing
solution

 Home   About   Features   Download   Documentation   Forums 

Android Node

From JPPF 5.2 Documentation

Revision as of 06:57, 23 March 2016 by Lolocohen (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Contents

Main Page > Android Node

1 Introduction

The JPPF Android node is a port of a standard JPPF node to the Android platform. It allows you to execute JPPF jobs and tasks on any Android device running Android Kitkat (version 4.4 - API level 19) or later.

It functions as an offline node (see Offline nodes), which means it will only connect to the server to fetch new work to perform or return the result of tasks execution. It will not be connected while executing tasks.

2 Installation and setup

2.1 Installation

The first step is to install the JPPF Android Node APK to one or more devices. To this effect, follow these steps:

  • ensure that you have enabled "Unkown sources" in your device's security settings, to allow installing apps from internal or external storage
  • download the Android node APK from the JPPF downloads page onto your device.
  • install the application on the device

2.2 Configuration

To configure the Android node such that it can connect to a server, you need to first launch it. Once started, the main screen will look like this:

AndroidMainScreen.gif

To access the settings, tap the tools.png "tools" icon on the top right corner, which transitions to the main settings screen:

SettingsMain.png

2.2.1 Processing threads

The "Threads" setting allows you to configure the number of processing threads in the node, that is, how many tasks can be executed in parallel:

SettingsThreads.png

This is equivalent to setting the configuration property "jppf.processing.threads" in a standard JPPF node.

2.2.2 Server connections

The "Server connection" setting allows you to define one or more JPPF servers the node can connect to:

SettingsServers.png

The node will try all the defined servers in order, until it establishes a connection. When all servers have been tried, it will start back from the first in the list. The format of a connection definition is as follows:

server_host_or_ip_address:server_port:secure

The ":secure" part is optional and when not specified, it will default to "true". It determines whether to use SSL/TLS-secure communication with the server, in which case you will also need to specifiy the security settings. This setting corresponds to defining a node connection strategy (see Node connection strategy).

2.2.3 Security settings

The main security settings screen looks like this:

SettingsSecurityMain.png

Most of these settings are straightforward to handle, with a few exceptions:

a) when "Mutual authentication" is enabled, you also need to specify a key store and associated password, holding a public/private key pair for the node. The server will also need a trust store holding the node's certificate. When this setting is enbaled, the corresponding settings "Node key store location" and "Node key store password" will also be enabled, and when disabled the node ley store settings will be disabled as well.

b) the "Server trust store location" and "Node key store location" settings allow you to browse accessible folders and files on your device to find the required key or trust store:

TrustStoreLocation.png

Notes:

  • the "Browse/Search" button which will lead to a screen allowing to browse accessible files, such as those in "Downloads", "Recent files", or the JPPF node app storage.
  • remember that Android only recognizes key stores in Bouncy Castle format (usually files with a ".bks" extension) and does not accept the Java JKS format, so you may need to convert your existing key or trust stores if you created them with the JDK's "keytool" utility.

2.2.4 Battery state monitoring

These settings allow you to define two thresholds for the percentage of the battery's charge level: warning and critical:

  • when the warning level is reached, the node, will finish the current job, if any, then stop taking any job and wait until either the battery level rises back above warning, in which case it will resume it normal activities, or the critical level is reached.
  • when the critical level is reached, the node will terminate itself

This behavior can be enabled or disabled at anytime, as shown in this screenshot:

BatteryMonitoring.png

3 Creating and submitting jobs

For the Android node to recognize and accept the tasks of a job, the code of these tasks must be provided along with the job. This is done by supplying the code in the job SLA's classpath attribute, as in this code sample:

public static void addToJobClassPath(JPPFJob job, File file) throws Exception {
  // copy the file in memory
  Location fileLoc = new FileLocation(file);
  Location memoryLoc = fileLoc.copyTo(new MemoryLocation(file.length()));
  // add the memory location to the classpath
  ClassPath classpath = job.getSLA().getClassPath();
  // the short file name (without folder) is used on the Android side
  classpath.add(file.getName(), memoryLoc);
}

Important: the only artifacts that Android will load dynamically are either dex-ed jar files or APK files. Nothing else will work. Thus, only these types of files should be added to the job's classpath.

4 Packaging the tasks for Android

In this section, we will discuss ways to package the code of the JPPF tasks so they can be executed by an Android node.

4.1 Packaging as a dex jar

To package a set f classes or jar files in dex format, you need to use (and have available) the Android "dx" tool, which is located in one or more place at: ANDROID_SDK_HOME/build-tools/<build_tools_version>. For instance, if the Android SDK is installed as "android_sdk" in your user's home directory and you use version 22.0.1 of the build tools, you would use this command on Unix/Linux:

~/android_sdk/build-tools/22.0.1/dx [... options ...]

and on Windows

C:\Users\%USERNAME%\android_sdk\build-tools/22.0.1/dx.bat [... options ...]

From now on we will assume that the biuld tools directory is in the system path and refer top the dex tool as "dx".

To convert an existing jar file "demo.jar" into a dex-ed jar named "demo-dex.jar" we would run this command:

dx --dex --output demo-dex.jar demo.jar

As an example of integration with a build system, we could wrap this command in an Ant macrodef definition:

<macrodef name="dex">
  <attribute name="in"/>
  <attribute name="out"/>
  <sequential>
    <!-- compute the name of the dex command from the OS, store it in "cmd" property -->
    <local name="cmd"/>
    <condition property="cmd" value="dx.bat" else="dx">
      <os family="windows"/>
    </condition>
    <!-- execute the dex command -->
    <exec executable="${android.build.tools}/${cmd}" failonerror="true">
      <arg value="--dex"/>
      <arg value="--output=@{out}"/>
      <arg value="@{in}"/>
    </exec>
  </sequential>
</macrodef>

It could then be used in an Ant target as follows:

<property name="android.home"        value="~/android-sdk"/>
<property name="android.build.tools" value="${android.home}/build-tools/22.0.1"/>
<target name="dex-demo.jar" depends="jar">
  <dex in="demo.jar" out="demo-dex.jar"/>
</target>

This is what is done in the related Android demo from the JPPF samples pack.

4.2 Packaging as an APK

While simply using the dex tool may be convenient and sufficient, for packaging tasks classes that do not use any android-specific APIs or libraries, you will need a different way of building when using Android-only features in the code of the tasks.

For this, we recommend creating a separate Android module, either with Android Studio or at least using a Gradle build script with the Gradle Android plugin applied. The JPPF dependencies are the jar files provided in the binary distribution of the Android node JPPF-x.y.z-node-android-redist.zip:

1) unzip the file anywhere

2) in the JPPF-x.y.z-node-android-redist/libs folder, you will find all the required jar dependencies; the libs/JPPF-5.1-alpha-AndroidNodeEvents.aar is not required if you do not need to receive node and task events or provide visual feedback to the node app

3) copy the jar files to a directory under your Android Studio module, for instance 'libs', and add the following to your build.gradle script:

// declare a flat directory structure as repository
repositories {
  flatDir {
    dirs 'libs'
  }
}

dependencies {
  // the JPPF dependencies ae already included in the main node apk,
  // therefore they MUST NOT be included in a dynamically loaded APK,
  // hence we use the 'provided' scope instead of 'compile'
  provided fileTree(dir: 'libs', exclude: ['*.aar'])
  // other dependencies here
  ...
}

5 Getting and providing feedback from the node and tasks.

As seen previously, the JPPF node provides by default a basic view with simple visual feedback such as connection and execution status, current job and number of tasks. You can, instead, specify your own custom feedback handler and view, to process and display data relevant to your jobs.

5.1 Implementing a feedback handler

To implement a custom feedback handler, you need to extend the abstract class AndroidNodeIntegerationAdapter, which is defined as follows:

public abstract class AndroidNodeIntegrationAdapter
  extends NodeIntegrationAdapter<Activity> implements NodeLifeCycleErrorHandler {

   // Get the view to be displayed during a job execution, if any
  public abstract View getContentView();

  // Android-specific extension of the NodeIntegrationAdapter<Activity> class
  @Override
  public final void setUiComponent(Activity uiComponent)

  // Get the Android activity from which the node is launched
  public final Activity getActivity()

  // Android-specific implementation of the NodeLifeCycleErrorHandler interface
  @Override
  public void handleError(
    NodeLifeCycleListener listener, NodeLifeCycleEvent event, Throwable t)
}

Note that the setUIComponent() and getActivity() methods should not be overriden, which is why they are declared as final.

Also note that a default implementation of handleError() is provided, which prints the error to the Android logcat log manager at verbose level.

Finally, the implementation class must have a no-arg constructor, to enable its instantiation via reflection.

We can see that AndroidNodeIntegerationAdapter extends the more general class NodeIntegerationAdapter<Activity>, which provides an empty implementation of the methods in the TaskExecutionListener interface (see the Extending and Customizing JPPF > Receiving notifications from the tasks section of this manual).

It also extends the class NodeLifeCycleListenerAdapter (see Extending and Customizing JPPF > Receiving node life cycle events), which provides an empty implementation of all the methods in the NodeLifeCycleListener interface.

Tip: if you do not wish to implement your own view, but still want to receive and process events, you may either:

- extend the default view class DefaultAndroidNodeIntegration (see also Installation section) and override the methods you need with a call to the same method in the super class. For instance:

public class MyNodeIntegration extends DefaultAndroidNodeIntegration {
  @Override
  public void jobStarting(final NodeLifeCycleEvent event) {
    super.jobStarting(event);
    // additional processing here
    Log.v("SimpleNodeIntegration", "starting job '" + event.getJob().getName() + "'");
  }

  // override other methods as needed ...
}

- or just return an empty view (see code example below)

The DefaultAndroidNodeIntegration looks like this during the execution of a job:

AndroidMainScreenBusy.gif


Here is a simple example of a feedback handler implementation, which returns an empty view and prints some of the events to Logcat :

package test;
import ...;

public class SimpleNodeIntegration extends AndroidNodeIntegrationAdapter {
  private final static String LOG_TAG = SimpleNodeIntegration.class.getSimpleName();
  private long totalTasks = 0L;
  private View view = null;

  @Override
  public View getContentView() {
    if ((view == null) && (getActivity() != null)) {
      // return an empty layout
      view = new LinearLayout(getActivity());
    }
    return view;
  }

  @Override
  public void taskExecuted(final TaskExecutionEvent event) {
    Log.v(LOG_TAG, String.format("received event from task '%s' : %s",
      event.getTask().getId(), event.getUserObject()));
  }

  @Override
  public void nodeStarting(final NodeLifeCycleEvent event) {
    Log.v(LOG_TAG, "the node is connected!");
  }

  @Override
  public void nodeEnding(final NodeLifeCycleEvent event) {
    Log.v(LOG_TAG, "the node is disconnected!");
  }

  @Override
  public void jobStarting(final NodeLifeCycleEvent event) {
    Log.v(LOG_TAG, "starting job '" + event.getJob().getName() + "'");
  }

  @Override
  public void jobEnding(final NodeLifeCycleEvent event) {
    Log.v(LOG_TAG, "ending job '" + event.getJob().getName() + "'");
    totalTasks += event.getTasks().size();
    Log.v(LOG_TAG, "total tasks executed: " + totalTasks);
  }
}

5.2 Integrating and packaging the feedback handler

5.2.1 Packaging the implementation code

Just as for the tasks to execute, the feedback handler implementation is not known initially to the Android node. It must therefore be loaded dynamically and transported with each job for which feedback processing is desired.

For this, the first step is to package the feedback handler as an Android application package (APK). Please note that, at the time being, only Java code and resources will be used from the resulting APK. In pariticular, this means that no Android resource, such as layouts, values or drawables will be accessible. Thus, any such resource, including the feedback handler's view, must be instantiated and accessed programmatically, using the corresponding objects that represent them. Another way to express it is that the "R" class for the loaded APK will not work.

In addition to the dependencies needed for the tasks (see Packaging as an APK section), you will also need a dependency on the JPPF-5.1-alpha-AndroidNodeEvents.aar Android library, which is also found in JPPF-x.y.z-node-android-redist/libs. This means adding the following to your build.gradle file:

// declare a flat directory structure as repository
repositories {
  flatDir {
    dirs 'libs'
  }
}

dependencies {
  // add the feedback handler API as an Android library
  compile(name: 'events-release', ext: 'aar')
  // these ae already included in the main node apk
  provided fileTree(dir: 'libs', exclude: ['*.aar'])
}

5.2.2 Integration within a job

For the feedback handler implementation to be used, it must be transported along with each job for which feedback processing is required. This is done in exactly the same way as described in section Creating and submitting jobs.

However, there is an additional step to configure the feedback handler, such that the node knows which class to instantiate.This is done by setting the job metadata property "jppf.node.integration.class" with its value being the fully qulaified class name of the feedback handler implementation. Here is an example based on the sample code provided in the Implementing a feedback handler section:

public class MyAndroidJobRunner {
  public static void main(String...args) {
    try (JPPFClient client = new JPPFClient()) {
      JPPFJob job = new JPPFJob();
      // add the APK to the job's classpath
      addToJobClassPath(job, new File("libs/MyHandler.apk"));
      // configure the feedback handler
      job.getMetadata().setParameter(
        "jppf.node.integration.class", "test.SimpleNodeIntegration");
      // ... add tasks, submit the job and get the results ...
      List<Task<?>> result = client.submitJob(job);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static void addToJobClassPath(JPPFJob job, File file) throws Exception {
    Location fileLoc = new FileLocation(file);
    Location memoryLoc = fileLoc.copyTo(new MemoryLocation(file.length()));
    ClassPath classpath = job.getSLA().getClassPath();
    classpath.add(file.getName(), memoryLoc);
  }
}

6 Security considerations

6.1 Dynamic loading

As already mentioned in this manual, the JPPF Android node relies on the ability to dynamically load arbitrary code to execute on a device. This is done with the DexClassLoader API and provides JPPF with the ability to execute tasks on Android devices without having to rebuild the node from the source code each time the code of the tasks changes.



Although the dynamic code loading constitutes the most important feature of the JPPF Android node, it may be perceived as a security risk. If this is the case from your or your organization's point of view, or if there is the least doubt about security, then we recommend that you do NOT install or use the JPPF Android node.



For a better understanding of the security implications, we recommend the following readings:

6.2 Android permissions

By default, the JPPF Android node has a limited set of permissions defined in its manifest file:

<!-- enable connectivity with the JPPF server -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- allow exploring the external storage (to find trust and key stores) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- allow exploring the app's private storage (to find trust and key stores) -->
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS"/>

The first permission allows the node to connect to a JPPF server, while the other two provide read access to document sources that are used to find and locate the trust and key stores when SSL communications are enabled.

Since the code of the tasks and optional feedback handler is dynamically loaded, any additional permission they require will not be dynamically granted, since the Android permissions framework only grants static permissions declared in the manifest of an app.

Due to this, you will need to edit the JPPF Android node's manifest and rebuild it from the source code to grant additional permissions. For this, please refer to the dedicated section Building from the source code in this guide.

6.3 Using SSL / TLS

Securing the node-to-server communications with SSL is the safest way to use the JPPF Android node. Enabling SSL is done via the node settings, by specifying secure server connections and configuring the related SSL parameters.

JPPF also allows using non-secure connections, to enable easy testing during the development cycle. We would like to emphasize, however, that this is not recommended for production or security-minded environments. Using unsecured connections may open the door to jobs and tasks of unknown origin being executed on the device, with unpredictable consequences.

You should also consider that there are degrees of seurity that you can achieve, based on the configuration and topology of your JPPF grid:

1) Server configuration: a JPPF server can handle both secure and non-secure connections at the same time. You will probably want to disable unsecure connections (see Configuring SSL/TLS communications > Enabling secure connectivity > In the servers).

2) Choosing server-only or mutual authentication: in some cases, it may be sufficient to to know that the server is who it pretends to be. However, you can add a level of security by requiring that the node authenticates itself with the server as well. This is done by configuring both nodes and server:

3) Additionally, yet another level of security is available by conifguring the server to use separate trust stores for nodes and client when mutual authentication is enabled (see Configuring SSL/TLS communications > SSL configuration properties > Special case: distinct driver trust stores for nodes and clients). This prevents applications with a node certificate from impersonating a JPPF client and vice-versa

7 Building from the source code

7.1 Source distribution structure

The source for the JPPF Android node is available as a separate download: JPPF-x.y.z-node-android-src.zip. To install it, simply unzip the file. You can then either import it in Android Studio (from the menu File > New > Import project) or use it as a Gradle project.

The source package contains 3 Android modules in the following folders:

  • 'app': this is the JPPF Android node app, built as an APK that can be installed on a device. The Android manifest is located at app/src/main/AndroidManifest.xml, this is where you can specify addtional permissions if needed.
  • 'events' : contains the feedback handler API; it is built as an Android library (.aar) and the other projects have a dependency on it
  • 'demo' : this is a an example of a [#Getting and providing feedback from the node and tasks.|feedback handler] implementation, built as an APK and intended for dynamic loading and transport along with a job's data. It depends on the Mandelbrot fractals demo found in the JPPF samples pack, and draws each Mandelbrot image as it is being computed by the node.

7.2 Building the project

If you have imported the project in Android Studio, then the build is generally automated or performed from the user interface. Therefore, we will now focus on building from the comand line with Gradle.

To build from the command line, the first step is to configure the location of the Android SDK. For this, open the file JPPF-x.y.z-node-android-src/local.properties and set the value of the sdk.dir property with the absolute path to the root of the Android SDK installation, for example:

sdk.dir = /home/me/Android/sdk

Once this is done, you are ready to build the project, using the batch script gradlew on Linux/Unix, or gradlew.bat on Windows. For instance, for a full build of the debug and release versions, enter at the command prompt:

on a Linux/Unix system:

./gradlew assemble

on a Windows system:

gradlew.bat assemble
Main Page > Android Node

JPPF Copyright © 2005-2017 JPPF.org Powered by MediaWiki