Using Infinite LiveCode for Android to Create Native Controls and Wrap OS APIs

by Ali Lloyd on June 28, 2017 16 comments

With the release of LiveCode 9.0 DP 7, the amount you can do with the android API has been significantly increased. Firstly, it is possible to use LiveCode Builder to respond to user events, which essentially allows the Android native control syntax (mobileControlCreate, mobileControlSet etc) to be replaced with draggable widget objects and standard LiveCode syntax. Secondly, it is possible to run services in the background, allowing, for example, background audio on Android.

Android Native Widget Template

After looking at the code for the native android button, I realised that it could be written in such a way that only one small tweak was needed to switch between a large variety of different native widgets. Here is the slightly modified button widget:

widget com.livecode.widget.native.android.widget

use com.livecode.foreign
use com.livecode.java
use com.livecode.widget
use com.livecode.canvas
use com.livecode.engine
use com.livecode.library.widgetutils

metadata version is "1.0.0"
metadata author is "LiveCode"
metadata title is "Android Native Widget"

__safe foreign handler _JNI_GetAndroidEngine() returns JObject \
   binds to "java:com.runrev.android.Engine>getEngine()Lcom/runrev/android/Engine;!static"
__safe foreign handler _JNI_GetEngineContext(in pEngine as JObject) returns JObject \
   binds to "java:android.view.View>getContext()Landroid/content/Context;"

// Handlers for creating and attaching view
__safe foreign handler _JNI_CreateView(in pContext as JObject) returns JObject \
   binds to "javaui:android.widget.Button>new(Landroid/content/Context;)"
__safe foreign handler _JNI_AddView(in pParentView as JObject, in pChildView as JObject) returns nothing \
   binds to "javaui:android.view.ViewGroup>addView(Landroid/view/View;)V"

private variable mNativeObj as optional JObject
private variable mOpen as Boolean

private handler IsAndroid() returns Boolean
    return the operating system is "android"
end handler

public handler OnCreate()
    put false into mOpen
end handler

private handler InitView()
	// Create an android button using the Engine Context
    variable tEngine as JObject
    put _JNI_GetAndroidEngine() into tEngine
    
    variable tContext as JObject
    put _JNI_GetEngineContext(tEngine) into tContext
    put _JNI_CreateView(tContext) into mNativeObj
    
	// put my native window into tParent
    variable tParent as Pointer
    MCWidgetGetMyStackNativeView(tParent)
    
    // wrap the parent pointer
    variable tParentObj as JObject
    put PointerToJObject(tParent) into tParentObj
    
    // add the view
    _JNI_AddView(tParentObj, mNativeObj)
    
    // get the pointer from the view and set the native layer
    variable tPointer as Pointer
    put PointerFromJObject(mNativeObj) into tPointer
    set my native layer to tPointer
end handler

private handler FinalizeView()
    set my native layer to nothing
    put nothing into mNativeObj
end handler

public handler OnOpen()
    put true into mOpen
    if IsAndroid() then
         InitView()
    end if
end handler

public handler OnClose()
    if IsAndroid() then
         FinalizeView()
    end if
    put false into mOpen
end handler

public handler OnPaint()
    if IsAndroid() then
        return
    end if
    
    fill text "Native widget" at center of my bounds on this canvas
end handler

end widget

All you have to do to get, for example, a ToggleButton is to change

__safe foreign handler _JNI_CreateView(in pContext as JObject) returns JObject \
   binds to "javaui:android.widget.Button>new(Landroid/content/Context;)"

to

__safe foreign handler _JNI_CreateView(in pContext as JObject) returns JObject \
   binds to "javaui:android.widget.ToggleButton>new(Landroid/content/Context;)"

Here is a sample of some of these widgets running in a LiveCode app on an Android device:

Obviously at this point these widgets don’t actually do anything. However, all classes that inherit from android.widget.Button can simply use the rest of the code from the button widget in order to have the basic properties that the Button does. For properties specific to a particular widget, we need to delve into the API a little more.

Android Native Field Widget

The Android native field is implemented as the class android.widget.EditText. So, to display a native field, we just use the relevant class in our CreateView handler:

__safe foreign handler _JNI_CreateView(in pContext as JObject) returns JObject \
   binds to "javaui:android.widget.EditText>new(Landroid/content/Context;)"

Adding a simple property

We will add the lockText property to the native field widget. The property maps to the methods android.view.View.isFocusable() and android.view.View.setFocusable(), so first of all we create foreign handler bindings to these methods:

__safe foreign handler _JNI_View_isFocusable(in pObj as JObject) returns JBoolean \
   binds to "javaui:android.view.View>isFocusable()Z"
__safe foreign handler _JNI_View_setFocusable(in pObj as JObject, in pParam_focusable as JBoolean) returns nothing \
   binds to "javaui:android.view.View>setFocusable(Z)V"

These are methods which must be called on an instance of the View class, so the first argument to the foreign handler is always a JObject containing the View to call the methods on. isFocusable takes no arguments and returns a (Java) bool, so the foreign handler returns JBoolean and the signature string is ()Z. setFocusable takes a bool argument and returns void i.e. nothing in LCB, and the signature string is Z(V). Again, these signature strings can be worked out using the mappings here or using the javap command line tool, or alternatively you can use the lc-compile-ffi-java tool to autogenerate a wrapper from a spec file.

Next we wrap the foreign handler calls:

public handler SetLockText(in pValue as Boolean) returns nothing
   _JNI_View_setFocusable(mNativeObj, pValue)
end handler

public handler GetLockText() returns Boolean
   return _JNI_View_isFocusable(mNativeObj)
end handler

and declare a property:

property lockText get GetLockText set SetLockText
metadata lockText.label is "Lock text"

Ensuring the property is settable in the IDE

Currently trying to set such a property from the IDE will cause an LCB runtime error, as the android API is not available on desktop. You would see something like the following, an “unable to bind foreign handler” error:

So, we need to gate the use of the actual foreign handler to when we are running on android. But we still want properties of the widget to be configurable in the IDE. The solution is to add a private variable for the property, and store the value in the setter and return the value in the getter when not on Android:

public handler SetLockText(in pValue as Boolean) returns nothing
   put pValue into mLockText
   redraw all
   if not (IsAndroid() and mOpen) then
      return
   end if
   View_setFocusable(mNativeObj, pValue)
end handler

public handler GetLockText() returns Boolean
   if not (IsAndroid() and mOpen) then
      return mLockText
   end if
   return View_isFocusable(mNativeObj)
end handler

We add a ‘redraw all’ in the setter just in case the property should affect the display of the widget in the IDE, by adjusting the OnPaint handler accordingly. However this has no effect on a widget with a native layer set.

Adding an event listener

Another feature of the field we want to add to our native field is the sending of messages on particular events. Here we will add a textChanged message when the text of the field is changed. The first task here is to take a look at the addTextChangedListener method in the TextView API. From this we can derive the foreign handler declaration to use:

__safe foreign handler _JNI_TextView_addTextChangedListener(in pObj as JObject, in pParam_watcher as JObject) returns nothing \
	binds to "javaui:android.widget.TextView>addTextChangedListener(Landroid/text/TextWatcher;)V"

In Java, to create such a Listener, the standard process would be to write a concrete class that implements the listener interface, and overrides its callback method to do what you want. For example:


public class MyTextWatcher implements TextWatcher {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
       // Do things when the text changes
    }
}

In LiveCode 9 DP 7 we added the ability to create InvocationHandler classes for arbitrary Java interfaces in order to allow LCB handlers to be attached to things like listener callbacks. In short, this means you can get an LCB handler to trigger when a listener event happens. There is a special form of the foreign handler declaration to do this:

__safe foreign handler _JNI_TextWatcher_TextChangedListener(in pCallbacks as Array) returns JObject \
   binds to "java:android.text.TextWatcher>interface()"

>interface() tells the engine that we want to create an InvocationHandler for the given interface. The foreign handler takes a single parameter, which is either an Array or a Handler. A handler can be used if there is a single callback method in the specified interface – in this case that handler is called when the callback triggers. In this case, the TextWatcher interface has three callback methods, beforeTextChanged, onTextChanged, and afterTextChanged so we need to specify which callback is mapped to which LCB handler using an array.

We are only interested in the onTextChanged callback at the moment, so we need to add the following code to the widget:

public handler OnTextChanged(in pText as JObject, in pStart as JObject, in pLengthBefore as JObject, in pLengthAfter as JObject)
    post "textChanged"
    MCEngineRunloopBreakWait()
end handler

handler SetListener()
    variable tTextChangedListener as JObject
    put _JNI_TextWatcher_TextChangedListener({"onTextChanged": OnTextChanged}) \
        into tTextChangedListener
    _JNI_TextView_addTextChangedListener(mNativeObj, tTextChangedListener)
end handler

This causes the OnTextChanged handler to be triggered whenever the field text changes, which in turn causes a textChanged message to be posted to the widget’s script object. At the moment, the MCEngineRunloopBreakWait() call is required to wake the engine thread after the event has come in via the TextWatcher.

Testing it out

I created a simple stack with the field on it, and a script to just pop up an answer dialog on textChanged:

Here is the textChanged message being handled, having been posted from LCB:

Background audio library

Another addition to 9.0 DP 7 is the ability to run services. These can run in the background so that they continue even when the user switches to a different application. Again we use the `interface()` binding string to create a ServiceListener, and foreign handlers to start and stop the service:


__safe foreign handler _JNI_EngineServiceListener(in pHandlers as Array) returns JObject \
    binds to "java:com.runrev.android.Engine$ServiceListener>interface()"

__safe foreign handler _JNI_EngineStartService(in pEngine as JObject, in pListener as JObject) returns nothing \
    binds to "java:com.runrev.android.Engine>startService(Lcom/runrev/android/Engine$ServiceListener;)V"
__safe foreign handler _JNI_EngineStopService(in pEngine as JObject, in pListener as JObject) returns nothing \
    binds to "java:com.runrev.android.Engine>stopService(Lcom/runrev/android/Engine$ServiceListener;)V"

Then we add handlers bgaudioOnStart and bgAudioOnFinish for the service callbacks onStart and onFinish:


private handler bgaudioStartup(in pAudio as String)
	-- If there is no context, then onStart hasn't been received, so cache
	-- the requested Audio URI.
	if mContext is nothing then
		put pAudio into mPendingAudio
	end if

	-- If there is no listener, then we've not started the service yet, so
	-- do so.
	if mListener is nothing then
		variable tCallbacks as Array
		put bgaudioOnStart into tCallbacks["onStart"]
		put bgaudioOnFinish into tCallbacks["onFinish"]
		put _JNI_EngineServiceListener(tCallbacks) into mListener
		_JNI_EngineStartService(_JNI_EngineGet(), mListener)
	end if
end handler

private handler bgaudioTeardown()
	if mListener is not nothing then
		_JNI_EngineStopService(_JNI_EngineGet(), mListener)
		put nothing into mListener
	end if
end handler

private handler bgaudioOnStart(in pContext as JObject) returns nothing
	-- If there is no listener, then the service must have been stopped
	-- before it started.
	if mListener is nothing then
		return
	end if

	-- Otherwise cache the context
	put pContext into mContext

	-- Create the media player and start playing
	mediaplayerStart(mPendingAudio)
	put nothing into mPendingAudio
end handler

private handler bgaudioOnFinish(in pContext as JObject) returns nothing
	mediaplayerStop()
	put nothing into mContext
	put nothing into mPendingAudio
end handler

The full example, including a sample stack and android media player implementation, is available on GitHub


As you can see there has been incredible progress here and this shows an all but complete implementation of Infinite LiveCode for Android with iOS coming right up.

Ali will be holding a dedicated Widgets seminar on each of the LiveCode Global conference days, and you can send in queries for him to answer in advance. See schedule here.

Ali LloydUsing Infinite LiveCode for Android to Create Native Controls and Wrap OS APIs

Related Posts

Take a look at these posts

16 comments

Join the conversation
  • Paul McClernan - June 28, 2017 reply

    Absolutely awesome! Cheers to Ali & the LC team!

  • Andy Piddock - June 28, 2017 reply

    Great work …bring it on Android!

  • Stephen Ezekwem - July 3, 2017 reply

    Am supposing this would be for us to compile and run this widget on our own rather than LC making it a part of the widget that comes with LC out of the box? On feature exchange, Kevin had indicated this goal for infinite livecode has been fulfilled, but my understanding of “fulfilled” (and how I understood the funding campaign) was we get it out of the box without worrying about compiling it on our?

    To clarify, I am on about Android Native Widget

    Ali Lloyd - July 3, 2017 reply

    Hi Stephen, the point here was to explain how native widgets could be written by you already in 9.0 DP 7 (if for example you really need that native date picker), but the native field widget for Android will be included in 9.0 DP 8.

  • Paul - July 5, 2017 reply

    Great that the android environment is unlocked with Infinite LiveCode. I am really happy with this big step!
    However because most of us LiveCode developers started to work with LiveCode because of it’s simplicity to develop applications with, we do need all of those functionality to be unlocked the LiveCode way. E.g. still waiting for a simple way to do BarCode (QR) scanning not only for Apple environments (via MergExt) but also for Android And Windows. How will LiveCode go about this translation of functionalities that do not exist now?

    Ali Lloyd - July 5, 2017 reply

    Hi Paul – the ultimate goal for the Java FFI project will be a complete (automated) wrapping of the Android API. Once that is done, you won’t need to worry about writing any foreign handler bindings yourself. However that is probably the best we can do in terms of guarantee for any individual feature – we may or may not provide our own QR code scanner for example, depending on how various priorities play out – but at the point the Android APIs are wrapped it should be a lot more obvious how to proceed with such a project, and we will continue to provide plenty of examples.

    Finally, I think you can be fairly confident that _someone_ will make an Android QR code reader, even if it turns out not to be us.

    Paul - July 5, 2017 reply

    Sounds good… thank for the reply.. when the wrapping of the Android API is done (let’s hope in the near future) I probably will try to have a go at it myself…

    Todd M Fabacher - July 14, 2017 reply

    Ali…Thanks for all the FANTASTIC work. We need a native Android barcode reader VERY much!!!! Please let me know how we can go about it. Is there native functionality we can wrap? The ZXings is not bad, but it locks up quite a bit.

    Again…congrats and well done.

    Ali Lloyd - July 19, 2017 reply

    Hi Todd, I will have a look – it doesn’t seem to be entirely straight forward but I’m certain it’s doable!

  • Dwayne Short - July 10, 2017 reply

    I haven’t been able to print from Android: “print this card into 100,100,400,400”. I want to port my existing Mac/Windows application to Chromebook via an Android app. Will Infinite LiveCode and Java FFI allow me to print directly from an Android app?

    Ali Lloyd - July 19, 2017 reply

    Hi Dwayne,
    Yes it will. I haven’t tested this code, but I had a quick look at the PrintHelper class and something like this would allow you to print an image for example:

    
    library com.livecode.library.android.print
    
    use com.livecode.java
    
    // Boiler-plate to get engine context
    __safe foreign handler _JNI_GetAndroidEngine() returns JObject 
        binds to "java:com.runrev.android.Engine>getEngine()Lcom/runrev/android/Engine;!static"
    __safe foreign handler _JNI_GetEngineContext(in pEngine as JObject) returns JObject 
        binds to "java:android.view.View>getContext()Landroid/content/Context;"
    
    private handler GetEngineContext() returns JObject
        variable tEngine as JObject
        put _JNI_GetAndroidEngine() into tEngine
        
        variable tContext as JObject
        put _JNI_GetEngineContext(tEngine) into tContext
        
        return tContext
    end handler
    
    __safe foreign handler _JNI_BitmapFactory_decodeFile(in pParam_pathName as JString) returns JObject 
    binds to "java:android.graphics.BitmapFactory>decodeFile(Ljava/lang/String;)Landroid/graphics/Bitmap;!static"
    
    public handler BitmapFactory_decodeFile(in pParam_pathName as String) returns JObject
    	variable tParam_pathName as JString
    	put StringToJString(pParam_pathName) into tParam_pathName
    
    	return _JNI_BitmapFactory_decodeFile(tParam_pathName)
    end handler
    
    __safe foreign handler _JNI_PrintHelper_PrintHelper(in pParam_ctxt as JObject) returns JObject 
    binds to "java:android.support.v4.print.PrintHelper>new(Landroid/support/v4/print/Context;)V"
    __safe foreign handler _JNI_PrintHelper_setScaleMode(in pObj as JObject, in pParam_scalemode as JInt) returns nothing 
    binds to "java:android.support.v4.print.PrintHelper>setScaleMode(I)V"
    __safe foreign handler _JNI_PrintHelper_printBitmap(in pObj as JObject, in pParam_jobname as JString, in pParam_bitmap as JObject) returns nothing 
    binds to "java:android.support.v4.print.PrintHelper>printBitmap(Ljava/lang/String;Landroid/graphics/Bitmap;)V"
    
    handler PrintHelper_Constructor(in pParam_ctxt as JObject) returns JObject
    	return _JNI_PrintHelper_PrintHelper(pParam_ctxt)
    end handler
    
    handler PrintHelper_setScaleMode(in pObj as JObject, in pParam_scalemode as Number) returns nothing
    	_JNI_PrintHelper_setScaleMode(pObj, pParam_scalemode)
    end handler
    
    handler PrintHelper_printBitmap(in pObj as JObject, in pParam_jobname as String, in pParam_bitmap as JObject) returns nothing
    	variable tParam_jobname as JString
    	put StringToJString(pParam_jobname) into tParam_jobname
    
    	_JNI_PrintHelper_printBitmap(pObj, tParam_jobname, pParam_bitmap)
    end handler
    
    public constant SCALE_MODE_FIT is 1
    
    // Handler to call from android app
    public handler doPhotoPrint(in pFile as String)
       // Construct a PrintHelper
        variable tPhotoPrinter as JObject
        put PrintHelper_Constructor(GetEngineContext()) into tPhotoPrinter
        
        // Set the scale mode
        PrintHelper_setScaleMode(tPhotoPrinter, SCALE_MODE_FIT)
        
       // Create a Bitmap from the passed-in file
        variable tBitmap as JObject
        put BitmapFactory_decodeFile(pFile) into tBitmap
        
        // Print the Bitmap
        PrintHelper_printBitmap(tPhotoPrinter, "Printing" && pFile, tBitmap)
    end handler
    
    end library 
    

    Paul - August 4, 2017 reply

    I agree with Todd. We do need a barcode (QR) reader asap.

  • Gurgen - September 27, 2017 reply

    Is it possible to wrap API Level 23+ classes using Java FFI?

  • Juan - March 29, 2018 reply

    Hey the controls look great. I really want to use the “SeekBar” control. Where is the stack containing the controls? I would like to use that one to implement SeekBars in my app. (I don’t understand the code written in this post, it is not LiveCode sintaxis…)

    Ali Lloyd - April 3, 2018 reply

    Hi Juan, checkout the mobile essentials widget pack here: https://livecode.com/products/thirdparty/livecode-factory/mobile-native-essentials-widget-pack-1-0-0/

    This contains (among other things) a SeekBar widget for Android / Slider for iOS!

  • JC - April 23, 2020 reply

    Hello,
    It is what I’m looking for.
    Is it possible to play a internet audio stream in background (mp3) ?
    It seems not 🙁

    Thank you

    JC

Join the conversation

*