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.
16 comments
Join the conversationPaul McClernan - June 28, 2017
Absolutely awesome! Cheers to Ali & the LC team!
Andy Piddock - June 28, 2017
Great work …bring it on Android!
Stephen Ezekwem - July 3, 2017
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
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
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
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
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
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
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
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
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:
Paul - August 4, 2017
I agree with Todd. We do need a barcode (QR) reader asap.
Gurgen - September 27, 2017
Is it possible to wrap API Level 23+ classes using Java FFI?
Juan - March 29, 2018
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
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
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