Creating an NPAPI Plugin for RhoElements

Anonymous (not verified) -
16 MIN READ

Overview

Speaking with many of our customers one common question I get asked all the time is "how can we extend the functionality of our RhoElements applications with native code" and one way to do this is by writing an NPAPI plugin.

NPAPI (Netscape Plugin API) is an API designed to allow browsers to call native code; It's been around for quite a while (hence the 'Netscape' in the name) but the simplicity of the interface has allowed for its longevity.  RhoElements implements NPRuntime to allow you to write scriptable plugins and this blog post will guide you through an example to get you started.

Note this blog is an example of how to write an NPAPI, it should not be considered absolute best practise as for reasons of clarity I have omitted full error handling and there may be more efficient ways to achieve the same result.

Prerequisites

  • Although NPAPI worked with the first release of RhoElements there have been several bug fixes since then and you are advised to use RhoElements version 1.0.3 as a minimum.
  • Currently NPAPI is only supported in RhoElements on Windows Mobile and Windows CE devices however we will be extending this functionality to Android in the future.
  • Your NPAPI will only be found by RhoElements if it has been copied to the NPAPI Directory, as defined in your RhoElements configuration file.

The Goal

We are going to create a "Sensor" plugin which will interface with a series of hardware sensors, in reality we will stub out the hardware interface but it shows how the example could work in a real world scenario.

final.png

The above image shows our RhoElements application running, using NPAPI to interface with both a temperature and pressure sensor, it works as follows:

  • User presses 'Create' to instantiate a sensor of the desired type (temperature or pressure).  This will create a new Javascript object.
  • User selects the checkbox to start the desired sensor(s).  This will call a method on the javascript object representing that sensor.
  • Every 3 seconds (default) the sensor will report its reading in the message window at the bottom of the display
  • The user can optionally change the poll interval to 1 second or retrieve the current value instantly

What is the Javascript doing?

First we will take a look at the Javascript behind the page above before moving onto the C++ code of the NPAPI.

Referencing the NPAPI

Out of the box RhoElements does not know about the Sensor NPAPI we are about to create (how could it?!) so we need to let the current page know that we are going to refer to an external plugin.

The above line of HTML code will be parsed by the RhoElements browser which will then look in its plugin directory to locate the appropriate NPAPI.  The NPAPI Directory is defined in your RhoElements configuration so ensure you have copied the DLL to the appropriate location.  By default this location is \Program Files\RhoElements\NPAPI on Windows Mobile and CE.

Object construction

Pressing the button to create either a temperature or a pressure sensor will call the following Javascript:

function fnCreateObject(whichObj)

{

          if (whichObj == 'temperature')

                    if (tempObj == null)

                    {

                              tempObj = new MySensor();

                              tempObj.sensorType = 'temperature';

                    }

                    else

                              alert('Temperature sensor already created');

          else if (whichObj == 'pressure')

                    if (pressureObj == null)

                    {

                              pressureObj = new MySensor();

                              pressureObj.sensorType = 'pressure';

                    }

                    else

                              alert('Pressure sensor already created');

}

MySensor is defined by our NPAPI as a constructable object, so we are able to create objects of the type exported by the plugin.  Once we have created the object we set the 'sensorType' property to give it some state.

Starting the sensors

'Starting' the sensors will instruct the sensors to begin polling their hardware interfaces and reporting the current value (be it temperature or pressure).  The following javascript starts the sensors monitoring with some additional error handing:

function fnMonitorSensor(whichSensor, isChecked)

{

          if (whichSensor == 'temperature')

          {

                    if (tempObj == null)

                    {

                              alert('You have not Created the Sensor');

                              theForm.cb_tempSensor.checked = false;

                    }

                    else

                    {

                              tempObj.monitor(isChecked);

                    }

          }

          else if (whichSensor == 'pressure')

          {

                    if (pressureObj == null)

                    {

                              alert('You have not Created the Sensor');

                              theForm.cb_pressureSensor.checked = false;

                    }

                    else

                    {

                              pressureObj.monitor(isChecked);

                    }

          }

}

The key code is on lines 12 and 24 which calls a method on the javascript object to start or stop sensor monitoring.  The objects are defined as global in the javascript file for simplicity.

Other Javascript Functions

The code extracts above show how we are creating a new Javascript object, setting a property and invoking a method on it.  The other functionality exposed by MySensor is accessed in a very similar way, there is a property representing the poll interval which can be set using the radio buttons for each sensor and another property for the current sensor value, which is retrieved in the same way you would retrieve any Javascript variable:

addSensorOutput('Current Temp: ' + tempObj.currentValue + ' Kelvin');

Interaction with the page

In our example we require the NPAPI to be able to interact with the web page, so it can write a message to the information box at the bottom of the page whenever the poll interval expires or we ask for the current value.  This is achieved by having the NPAPI invoke a javascript function on the page which I have called 'addSensorOutput'

function addSensorOutput(newOutput)

{

          //alert(newOutput);

          document.getElementById("sensorOutput").value = newOutput + "\n" + document.getElementById("sensorOutput").value;

          //sensorDiv.innerHTML = newOutput + "
" + sensorDiv.innerHTML;

}

The javascript can do whatever you like but in the case above I populate the text area.  The NPAPI will invoke this Javascript function directly by locating it in the DOM so it is important not to change the name of the function.

Please see the source code for the sample page 'MySensor.htm' to better understand how it works.

The C++ code of the NPAPI

A visual studio project for building this sample NPAPI is attached to this blog and can be used as the starting point for your own NPAPI if desired.  You will need Visual Studio 2008 and the Windows Mobile 5.0 platform SDK in order to compile it.  I would recommend studying the code to best understand exactly what is going on in NPAPI (along with reading the NPAPI Plugin development document attached to this blog) but to get you going I will pick out the important bits of code here.

Declaring our plugin type

You may recall we used an embed tag on our HTML page (MySensor.htm) to embed a plugin of type 'application/x-motorolasolutions-mysensor' on the page.  In common with all supporting browsers RhoElements knows which types are implemented by plugins by querying the MIME Types exposed in the resource file (.rc).  Our 'MySensor' resource exposes the MIME type as follows:

VALUE "MIMEType", "application/x-motorolasolutions-mysensor"

If you have multiple MIME types supported by a plugin you can separate them with a ';'.

When the browser comes across an embed tag the NPP_New function is called (which is part of the NPAPI framework).  The MySensor NPP_New creates a new 'CMySensorPlugin' which is a class I have defined myself.

NPError NPP_New(NPMIMEType pluginType,

                NPP instance,

                uint16_t mode,

                int16_t argc,

                char* argn[],

                char* argv[],

                NPSavedData* saved)

{  

  if(instance == NULL)

    return NPERR_INVALID_INSTANCE_ERROR;

  NPError rv = NPERR_NO_ERROR;

  if (strcmp(pluginType, "application/x-motorolasolutions-mysensor") == 0)

  {

            CMySensorPlugin * pMySensorPlugin = new CMySensorPlugin(instance);

            if(pMySensorPlugin == NULL)

                    return NPERR_OUT_OF_MEMORY_ERROR;

            CPluginInfo* pluginInfoObject = new CPluginInfo();

                    pluginInfoObject->iPluginType = 0;

                    pluginInfoObject->pPlugin = (void *)pMySensorPlugin;

            instance->pdata = (void *)pluginInfoObject;

  }

  return rv;

}

The constructor for CMySensorPlugin is as follows:

CMySensorPlugin::CMySensorPlugin(NPP pNPInstance) :

  m_pNPInstance(pNPInstance),

  m_pNPStream(NULL),

  m_bInitialized(FALSE),

  m_pScriptableObject(NULL)

{

          // Must initialise this before getting NPNVPluginElementNPObject, as it'll

          // call back into our GetValue method and require a valid plugin.

          pNPInstance->pdata = this;

    // Say that we're a windowless plugin.

    NPN_SetValue(m_pNPInstance, NPPVpluginWindowBool, false);

          //  Instantiate the values of the methods / properties we possess

          sMonitor_id = NPN_GetStringIdentifier("monitor");

          sPollInterval_id = NPN_GetStringIdentifier("pollInterval");

          sCurrentValue_id = NPN_GetStringIdentifier("currentValue");

          sSensorType_id = NPN_GetStringIdentifier("sensorType");

          //  Export onto the webpage the JS object 'MySensor'.  This enables us

          //  to say var myObj = new MySensor();

          NPObject *sWindowObj;

          NPN_GetValue(m_pNPInstance, NPNVWindowNPObject, &sWindowObj);

          NPObject *mySensorObject =NPN_CreateObject(m_pNPInstance,GET_NPOBJECT_CLASS(MySensorPluginObject));

          NPVariant v;

          OBJECT_TO_NPVARIANT(mySensorObject, v);

          NPIdentifier n = NPN_GetStringIdentifier("MySensor");

          NPN_SetProperty(m_pNPInstance, sWindowObj, n, &v);

          NPN_ReleaseObject(mySensorObject);

          NPN_ReleaseObject(sWindowObj);

}

Pay particular attention to line 24 which is creating an instance of 'CMySensorPluginObject' and adding it to the page at line 28;  this is a 'Scriptable' object, meaning you can call it from Javascript.  The scriptable object is added to the page as 'MySensor' allowing you to create new MySensor()'s in your Javascript.  The 'MySensorPluginObject' is also a class I have defined myself, inheriting from the 'ScriptablePluginObjectBase' which is part of the NPAPI framework.

The constructor also instantiates the names for our parameters and methods in lines 17-20.  MySensor has a single method, 'monitor' and 3 parameters.

Object Construction

Let us now look at what happens in the NPAPI when we call the javascript line:

tempObj = new MySensor();

What's going on here?  We already exported a 'MySensor' (MySensorPluginObject) to the page as part of the CMySensorPlugin constructor (above)... what we now want to happen is for tempObj to be another instance of MySensor (another MySensorPluginObject object).

Before creating MySensorPluginObjects it is first necessary to invoke the NPAPI Macro to declare it, as below:

DECLARE_NPOBJECT_CLASS_WITH_BASE(MySensorPluginObject,

                                 AllocateMySensorPluginObject);

Notice the Macro takes function which will be called whenever a new sensor object is created, AllocateMySensorPluginObject, this is where you can perform any creation and initialisation of the object.

Creating a new MySensorPlugin object via Javascript will cause its 'Construct' method to be called, shown below.

bool MySensorPluginObject::Construct(const NPVariant *args, uint32_t argCount,

                                     NPVariant *result)

{

          //  Where the JS Object is created, called when we say:

          //  var myObj = new MySensor();

          bool bRetVal = false;

          //  Create Object, expect no arguments

          if (argCount == 0)

          {

                    NPObject* genericObj = NPN_CreateObject(mNpp, GET_NPOBJECT_CLASS(MySensorPluginObject));

                    if (!genericObj)

                              return false;

                    MySensorPluginObject* obj = (MySensorPluginObject*)genericObj;

                    OBJECT_TO_NPVARIANT(genericObj, *result);

                    //  We have a function in our plugin to output text to a text box

                    MessageToUser("Creating Sensor");

                    bRetVal = true;

          }

  return bRetVal;

}

All we do in the MySensor example above is to allocate another MySensorPluginObject, just as we did when we first embedded the plugin, and notify the user.  We have now allocated two MySensorPluginObjects, one resident on the page enabling us to say new MySensor() and one which has been assigned to the javascript variable 'tempObj'.

Whenever we call NPN_CreateObj the function 'AllocateMySensorPluginObject' will be called, this is because of how we called the DECLARE_NPOBJECT_CLASS_WITH_BASE macro above.

The function to allocate a new MySensor looks as follows:

static NPObject * AllocateMySensorPluginObject(NPP npp, NPClass *aClass)

{

          //  Called in response to NPN_CreateObject

          MySensorPluginObject* obj = new MySensorPluginObject(npp);

          //  Setting the default properties of the created object.

          obj->m_iPollInterval = 3000;

          obj->m_iCurrentValue = 0.0;

          obj->m_hStopSensorMonitor = CreateEvent(NULL, FALSE, FALSE, NULL);

          obj->m_iSensorType = -1;

          //  Create a Hidden Window so our sensor thread can re-synchronize back

          //  to the main thread

          obj->hWindow = CreateWindow(L"SensorWindow", NULL, 0, 0, 0, 0, 0, NULL, (HMENU) 0, NULL, NULL);

          if (obj->hWindow == NULL)

          {

                    WNDCLASS wndclass;

                    memset (&wndclass, 0, sizeof wndclass);

                    wndclass.lpfnWndProc = obj->NpapiProc;

                    wndclass.hInstance = NULL;

                    wndclass.lpszClassName = L"SensorWindow";

                    RegisterClass (&wndclass);

                    obj->hWindow = CreateWindow(L"SensorWindow", NULL, 0, 0, 0, 0, 0, NULL, (HMENU) 0, NULL, NULL);

          }

          SetWindowLong(obj->hWindow, GWL_WNDPROC, (DWORD)obj->NpapiProc);

          return obj;

}

Notice this function creates a new instance of the scriptable object and returns a pointer to that instance.  To reiterate what was said earlier, MySensorPluginObject inherits from ScriptablePluginObjectBase which in turn inherits from NPObject, this method's return type.

AllocateMySensorPluginObject initialises the variables associated with the sensor (in this case the poll interval e.t.c.) and creates a (hidden) window which will be associated with the object.  NPAPI is not thread safe, our sensor will be receiving values on a different thread so this hidden window is required to resynchronize back to the calling thread.

Setting a property

Now let us look at what happens when you set a javascript property on your scriptable object, as below:

tempObj = new MySensor();

tempObj.sensorType = 'temperature';

To backtrack slightly, any scriptable NPAPI object (in our case MySensorPluginObject) should inherit from the ScriptablePluginObjectBase class, doing so provides the necessary functions to make it scriptable.  Of particular note in this article are:

  • Construct - when new objects are created in Javascript, as illustrated above.
  • HasProperty - To determine whether an object has a specific property
  • HasMethod - To determine whether an object has a specific method
  • GetProperty - To return the value of a property
  • SetProperty - To set the value of a property
  • Invoke - To invoke an object's method.

We have overridden all these methods in our implementation of MySensorPluginObject to customise the behaviour for our MySensor JavaScript object.

When we call tempObj.sensorType = 'temperature'; the first thing the framework does is to determine whether this property exists, by calling the HasProperty function:

bool MySensorPluginObject::HasProperty(NPIdentifier name)

{

          //  Called by the plugin framework to query whether a JS object

          //  has a specified property, we have three properties.

          return (name == sPollInterval_id ||

                              name == sCurrentValue_id ||

                              name == sSensorType_id);

}

Since sSensorType_id holds 'temperature' this function will return true.  A couple of notes here:

  • This comparison is case sensitive (as is Javascript).  To make the comparisons case insensitive you could do it yourself here (e.g. by converting both parameters to lower case, see 'Invoke' later)
  • HasMethod will also be called, even when trying to set / get a property, for this reason you can not have methods and properties of the same name in NPAPI.

The framework is clever enough at this stage to realise you are setting a property and therefore calls 'SetProperty' with the value to set.  GetProperty would be called at this stage if you had called var myVal = tempObj.sensorType.

bool MySensorPluginObject::SetProperty(NPIdentifier name, const NPVariant *value)

{

          //  Sets the specified property to the specified value.

          bool bRetVal = false;

          if (name == sSensorType_id)

          {

                    //  mySensor.sensorType = 'temperature';

                    char szSensor[1024];

                    memset(szSensor, 0, 1024);

                    sprintf(szSensor, NPVARIANT_TO_STRING(*value).UTF8Characters);

                    if (strcmp(szSensor, "temperature") == 0)

                              this->m_iSensorType = 0;

                    else if (strcmp(szSensor, "pressure") == 0)

                              this->m_iSensorType = 1;

                    bRetVal = true;

          }

          else if (name == sPollInterval_id)

          {

                    //  mySensor.pollInterval = [value];

                    this->m_iPollInterval = (int)NPVARIANT_TO_DOUBLE(*value);

                    bRetVal = true;

          }

          return bRetVal;

}

The sensor type (received as an NPVariant and processed as a string) just sets a member variable in the sensor object (m_iSensorType) which is used later to determine whether we're reading Kelvin or Pascals.  SetProperty is common for all properties, setting the poll interval will again set a member variable but setting the 'currentValue' property will have no effect.  Remember our MySensor object has a 'currentValue' property but by design we do not allow users to set this.

Retrieving a Property

For completeness the code for retrieving a property is also included here, it is similar to setting a property in that first 'HasProperty' is called but the context of the call will cause the framework to return the property's value:

bool MySensorPluginObject::GetProperty(NPIdentifier name, NPVariant *result)

{

          //  Retrieve the value of a property.  *result is an out parameter

          //  into which we should store the value

          bool bReturnVal = false;

          VOID_TO_NPVARIANT(*result);

          if (name == sCurrentValue_id)

          {

                    //  Called by: var sensorVal = mySensor.currentValue;

                    //  Return the current value to the web page.

                    DOUBLE_TO_NPVARIANT(this->m_iCurrentValue, *result);

                    bReturnVal = true;

          }

          if (!bReturnVal)

                    VOID_TO_NPVARIANT(*result);

          return bReturnVal;

}

Only the sensor's current value is retrievable, making the other properties write only.  The value is returned in the *result pointer which is an NPVariant, the NPAPI framework provides a number of macros for converting to NPVariants, in this case we are returning a double value.

Starting the Sensor

Now lets look what happens when we call:

tempObj.monitor(isChecked);

Our only method provided by the sensors is 'monitor' which takes a boolean, true if the sensors should be started and false if they should be stopped.  Similar to properties when the framework encounters an object method call it will call the 'HasMethod' function to determine if the Javascript object supports that method, our HasMethod is as follows:

bool MySensorPluginObject::HasMethod(NPIdentifier name)

{

          //  Called by the plugin framework to query whether an object

          //  has a specified method, we only have one method, 'monitor()'

          return (name == sMonitor_id);

}

After HasMethod returns true the MySensorPluginObject 'Invoke' method is called, as below:

bool MySensorPluginObject::Invoke(NPIdentifier name, const NPVariant *args,

                               uint32_t argCount, NPVariant *result)

{

          //  Called when a method is called on an object

          bool bReturnVal = false;

          VOID_TO_NPVARIANT(*result);

          //  Convert to lower case to make our methods case insensitive

          char* szNameCmp = _strlwr(NPN_UTF8FromIdentifier(name));

          NPIdentifier methodName =  NPN_GetStringIdentifier(szNameCmp);

          NPN_MemFree(szNameCmp);

          //  mySensor.monitor(bool)

          if (methodName == sMonitor_id)

          {

                    //  Expect one argument which is a boolean (start / stop)

                    if (argCount == 1 && NPVARIANT_IS_BOOLEAN(args[0]))

                    {

                              if (NPVARIANT_TO_BOOLEAN(args[0]))

                              {

                                        //  mySensor.monitor(true);

                                        //  Create a thread to monitor the sensor

                                        CloseHandle(CreateThread(NULL, 0,

                                        (LPTHREAD_START_ROUTINE)SensorMonitorThread, this, 0, NULL));

                              }

                              else

                              {

                                        //  mySensor.monitor(false);

                                        //  Stop monitoring the sensor

                                        SetEvent(m_hStopSensorMonitor);

                              }

                              //  Monitor has no return value

                              VOID_TO_NPVARIANT(*result);

                              bReturnVal = true;

                    }

          }

          if (!bReturnVal)

                    VOID_TO_NPVARIANT(*result);

          return bReturnVal;

}

Note that this function demonstrates how to make method names case insensitive.  The functionality is fairly self explanatory: provided the method name is 'monitor' and there is a single boolean parameter then we either start or stop a separate thread (SensorMonitorThread) to read from the sensor.

For completeness the code for the monitoring thread is below (though this code is not specific to NPAPI)

DWORD MySensorPluginObject::SensorMonitorThread(LPVOID lpParameter)

{

          MySensorPluginObject* pSensor = (MySensorPluginObject*)lpParameter;

          bool exitThread = false;

          DWORD dwEvent;

          HANDLE hWaitHandles[1];

          hWaitHandles[0] = pSensor->m_hStopSensorMonitor;

          DEBUGMSG(TRUE, (L"Sensor Monitor Thread Starting\n"));

          while (true)

          {

                    //  Wait for an exit event (indicating stop the thread) or timeout

                    //  Note if we change the timeout value we have to wait until the next cycle

                    //  before the new timeout value is read... this is just an example.

                    dwEvent = WaitForMultipleObjects(

                              1,

                              hWaitHandles,

                              FALSE,

                              pSensor->m_iPollInterval);

                    switch (dwEvent)

                    {

                    case WAIT_OBJECT_0:

                              {

                                        goto _exitThread;

                              }

                    case WAIT_TIMEOUT:

                              {

                                        //  Create a fake sensor reading to send to the page

                                        char szSensorReading[512];

                                        if (pSensor->m_iSensorType == 0)

                                        {

                                                  float currentValue = (float)((rand() % 100 + 10000) / 100.0);

                                                  //  truncate the output

                                                  pSensor->m_iCurrentValue = (int)(currentValue * 100);

                                                  sprintf(szSensorReading, "Sensor Reading: %.02f Kelvin", currentValue);

                                        }

                                        else

                                        {

                                                  float currentValue = (float)((rand() % 100 + 1000) / 100.0);

                                                  //  truncate the output

                                                  pSensor->m_iCurrentValue = (int)(currentValue * 100);

                                                  sprintf(szSensorReading, "Sensor Reading: %.02f Pascals", currentValue);

                                        }

                                        //  Contention here if timeout is too small but this is just a demo

                                        //  Resynchronise with the main thread.

                                        SendMessage(pSensor->hWindow, WM_USER + 1, (WPARAM)pSensor, (LPARAM)szSensorReading);

                              }

                    }  //  End Switch

          }          //  End While !exitThread

_exitThread:

          DEBUGMSG(TRUE, (L"Sensor Monitor Thread Exiting\n"));

          return 0;

}

Notes about the monitoring thread:

  • Changing the poll interval will only take effect after the current poll interval has expired (this was to keep the example simple)
  • rand() is used in place of real sensors and we store the value in m_iCurrentValue, this is the value retrieved if we say myVal = tempObj.currentValue;  I have cheated a bit and cast m_iCurrentValue to an integer to make it easier to truncate to 2 decimal places.
  • NPAPI is not thread safe, in order to synchronize with the calling thread we send a message to the hidden window associated with this JavaScript object, which was created when the JS object was created.

Interacting with the DOM from NPAPI

Finally, we want the NPAPI to be able to communicate with the page's DOM, to allow it to call a javascript function to notify the user when the sensor changes.  This is achieved with the method below:

void MySensorPluginObject::MessageToUser(char* szMessage)

{

          NPVariant functionval;

          NPVariant rval;

          NPObject *sWindowObj;

          NPN_GetValue(mNpp, NPNVWindowNPObject, &sWindowObj);

          //  Populate 'functionval' with the name of our function

          NPN_GetProperty(mNpp, sWindowObj, NPN_GetStringIdentifier("addSensorOutput"), &functionval);

          NPVariant arg;

          if (NPVARIANT_TO_OBJECT(functionval) == 0)

                    return;

          //  Create the argument to call 'addSensorOutput' with

          char szSourceMessage[1024];

          if (m_iSensorType == 0)

                    sprintf(szSourceMessage, "Temperature: %s", szMessage);

          else if (m_iSensorType == 1)

                    sprintf(szSourceMessage, "Pressure: %s", szMessage);

          else

                    sprintf(szSourceMessage, "%s", szMessage);

          //  Add the string argument to our javascript function to an argument, 'arg'

          STRINGZ_TO_NPVARIANT(szSourceMessage, arg);

          //  Invoke the Javascript function on the page

          NPN_InvokeDefault(mNpp, NPVARIANT_TO_OBJECT(functionval), &arg, 1,

                                                            &rval);

          //  Clean up allocated memory

          NPN_ReleaseVariantValue(&functionval);

          NPN_ReleaseVariantValue(&rval);

          NPN_ReleaseObject(sWindowObj);

}

addSensorOutput is a Javascript function on the current page.  From a handle to the window (sWindowObj) we retrieve the Javascript function (functionval) and then call that function (NPN_InvokeDefault) with the appropriate argument which will be a string.

This blog has now shown two ways to interact with the page:

  • The CMySensorPlugin constructor used the m_pNPInstance variable to interact with the page
  • The 'MessageToUser method of 'MySensorPluginObject' used the mNpp variable, provided automatically by the framework since we are a child of ScriptablePluginObjectBase.

In both these examples m_pNPInstance and mNpp point to the same thing and provide an interface to the page.

Next Steps

What has been covered here is only a very simple NPAPI plugin, obviously the capabilities of NPAPI extend far beyond writing a simple, scriptable object. 

Because the online resources for NPAPI are far from extensive (and often implementation specific) our team have created a document to explain the NPAPI interface in more detail, though I would recommend understanding the sample above before delving deeper into the detail - you can get the document as an attachment to this blog.

profile

Anonymous (not verified)

Dfghjkjjn

Please register or login to post a reply

2 Replies

I Igor Andriychuk

Thank you Darryn for good explanation and source code. It works well.

H Hector Meza

Darryn, you mentioned that this functionality will be added to RE to support Android in the future, do we have an ETA and do you know whom I should speak to get a more information on an Adroid like feature?