Skip to content

Latest commit

 

History

History
338 lines (245 loc) · 12.6 KB

Android.md

File metadata and controls

338 lines (245 loc) · 12.6 KB

joynr Android Developer Guide

Initial Note

This guide is a work in progress as the joynr Android api has not been finalized.

Android Development

In order to see an example implementation you can check android-hello-world-binder provider and consumer examples for a more concrete example of implementing an Android joynr application using MVVM pattern.

See the Java Configuration Reference for a complete list of all available configuration properties available to use in joynr Java applications, which has very similar classes to Android.

If you are planning on taking advantage of multi-user in the chosen Android environment, check out the Multi-User in Android section.

Setting up Android project environment in Android Studio

To develop an Android project with Android Studio first you need to create a new project following the guidelines in "Create an Android Project" .

Setting up Gradle

In order to use joynr java code generator you need to add the generator gradle plugin in the project build.gradle, as it needs to be applied in the app.

	dependencies {
		...
		classpath "io.joynr.tools.generator:joynr-generator-gradle-plugin:$JOYNR_VERSION"
		...
	}

Note: The version tag needs to be replaced with the version you want to use, check here the available versions

Then in the application build.gradle the joynr generator gradle plugin needs to be applied in order to generate the joynr code through the fidl file. This can be done by adding the following line in the beginning of your app/build.gradle

	apply plugin: 'io.joynr.tools.generator.joynr-generator-gradle-plugin'

And now in the dependencies you can add the joynr-android-binder-runtime dependency in order use it in your program. For this in app/build.gradle you should add the following lines:

dependencies {
	...
	implementation "io.joynr.android:joynr-android-binder-runtime:$JOYNR_VERSION"
	...
}

Note: The version tag needs to be replaced with the version you want to use, check here the available versions

Setting up fidl file

To finalize the environment create a fidl folder app/src/main/ and add a file with the following contents

package helloworld

typeCollection {
    const String hello = "Hello World!"
}

interface HelloWorld {
    attribute String hello
}

Then when you sync Gradle or build the project, new files will be generated in app/build/generated/source/fidl/joynr/ from the interfaces defined in the fidl files.

Setting up application structure

With everything setup we will start to create a main application and also a consumer and provider class

The Application class

In an Android project it is recommended to use a class that extends from the Application class and uses that class as a singleton to manage the joynr runtime, just like in the following example.

class HelloWorldApplication: Application() {
	// Must be included for logs to work
    private val logger = LoggerFactory.getLogger(HelloWorldApplication::class.java)

    // The object that will contain the runtime
    private lateinit var runtime: JoynrRuntime

    override fun onCreate() {
        super.onCreate()

        // This is where you can define the debug level
        StaticLoggerBinder.setLogLevel(AndroidLogger.LogLevel.DEBUG)

        // Initializing Android Binder Runtime with Application Context
        // This needs to be in the Application onCreate so the JoynrRuntime is
        // ready to be used immediatly after the application is started
        runtime = AndroidBinderRuntime.init(this)

        ...
    }

}

Then add your application to the Android Manifest file

<manifest
    ...
    >
    <application
        android:name=".HelloWorldApplication"
        ...
        >
    </application>
    ...
</manifest>

Using this approach you can create the runtime on the program initialization and access it anywhere in your application.

The Provider Class

The provider class in this example is just a provider manager and a bridge between the provider and the ViewModel, all the logic of the provider is stored in the provider class.

The logic behind the initialization of this class is very similar to the one behind the Consumer

We create a class based on the HelloWorldAbstractProvider generated by the fidl file with a simple hello message

/**
  This is where you define your Provider
*/
class HelloWorldProvider: HelloWorldAbstractProvider() {

    var hello: String = "Hello"

    /**
      These will be the functions available to the Consumers to call on this Provider
    */

    // This returs the hello String to the Consumer
    override fun getHello(): Promise<Deferred<String>> {
        val deferred = Deferred<String>()
        deferred.resolve(hello)
        return Promise(deferred)
    }

    // This sets the String to be returned in future uses
    override fun setHello(hello: String?): Promise<DeferredVoid> {
        val deferredVoid = DeferredVoid()
        this.hello = hello!!
        deferredVoid.resolve()
        return Promise(deferredVoid)
    }
}

Then we will need to register the provider we created, in the main application

// Register Provider with the Cluster Controller
// "domain" is the identifier that will identify your provider in the Cluster Controller
runtime.getProviderRegistrar("domain", HelloWorldProvider())
	.register()

You should make this call in the onCreate (after the init method) method of the application

And after that the Model just waits for the the data in the provider to be updated and passes that information to the ViewModel.

The Consumer View Model

Setting up the attributes

The consumer view model will have a proxy that links the data with the provider and a data attribute

class ConsumerViewModel : ViewModel() {

    private lateinit var helloWorldProxy: HelloWorldProxy
    private val _providedStr = MutableLiveData<String>()
    val providedStr: LiveData<String> by lazy {
        _providedStr
    }

	...
}

Setting up the consumer proxy

After setting up the Application class we need to initialize the consumer proxy

	...

    fun registerProxy(application: Application) {
        val discoveryQos = DiscoveryQos()
        discoveryQos.discoveryTimeoutMs = 10000
        discoveryQos.discoveryScope = DiscoveryScope.LOCAL_ONLY
        discoveryQos.cacheMaxAgeMs = java.lang.Long.MAX_VALUE
        discoveryQos.arbitrationStrategy = ArbitrationStrategy.HighestPriority

        val app = application as HelloWorldApplication
        val runtime = app.getJoynrRuntime()
        helloWorldProxy = runtime.getProxyBuilder("domain", HelloWorldProxy::class.java)
            .setMessagingQos(MessagingQos()).setDiscoveryQos(discoveryQos).build()
    }

	...

The proxy requests have a similiar api to Java synchronous and asynchronous procedures.

This data is linked to the ViewModel by using LiveData. This means that when the value of the string is updated, an event is triggered notifying the ViewModel of that change.

After that we need to setup how we are going to get the information coming from the providers and we will be using callbacks to ensure that the main thread will not be blocked

	...

    fun getString() {
        helloWorldProxy.getHello(
            object : Callback<String>() {
                override fun onSuccess(s1: String?) {
                    Log.d("Received Message", s1!!)
                    // Do some logic
                    _providedStr.postValue(s1)
                }

                override fun onFailure(e: JoynrRuntimeException) {
                    Log.d("Failed Message", "Failed to receive message with the error: $e")
                    _providedStr.postValue(e.message)
                }
            }
        )
    }

Note: The callback import must be the one from io.joynr.proxy.Callback

Setting up the main activity together with the consumer view model

We then need to register the view model and also setup our listeners and observers

class MainActivity : AppCompatActivity() {

    lateinit var consumerViewModel: ConsumerViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        consumerViewModel = ViewModelProvider(this).get(ConsumerViewModel::class.java)
        consumerViewModel.registerProxy(application)

        talk_button.setOnClickListener {
            consumerViewModel.getString()
        }

        consumerViewModel.providedStr.observe(this, Observer { str ->
            text_box.text = str
        })

    }

}

Multi-User in Android

In the Android world, there is the concept of multi-user where different users may have installations of the same components and applications. Developers that want to consider multi-user during development should be aware of how the joynr Android Binder runtime works.

In joynr, it is understood that the Cluster Controller (CC) runs in user 0, which is the system user. This CC is like the server part in a client-server architecture. Multiple clients, which are joynr components/applications that implement Proxies and Providers, connect to this server. This means that the CC is actually a singleton, and only exists in user 0.

The Android Binder runtime performs logic on how to bind to the joynr BinderService by using the information provided in the BinderAddress. By default, when binding to the Service, the runtime will bind to the CC's Service as user 0 and will bind to any other client Services as their respective user. This information extraction happens under the hood, as the runtime fetches and uses the user ID automatically when creating the BinderAddress.

In order for all of this to work as it should, the implementation of your CC should include the permissions android.permission.INTERACT_ACROSS_USERS and android.permission.INTERACT_ACROSS_USERS_FULL. These permissions allow the CC to successfully bind to the Service in different users. Note that these permissions require the CC to be a system app, as only those can request them.

You should also make sure that joynr's BinderService is declared as singleUser=true in the CC's Android Manifest file. What this does is effectively make the CC's BinderService a singleton within the platform, which will always run in user 0. This can be done by declaring the Service again in the Manifest like so:

<!-- merged from joynr Binder runtime dependency but we want to redeclare it -->
<service
    android:name="io.joynr.android.binder.BinderService"
    android:enabled="true"
    android:exported="true"
    android:singleUser="true">

    <intent-filter>
        <action android:name="io.joynr.android.action.COMMUNICATE" />
    </intent-filter>
</service>

Developers of any client applications or components that need to make use of multi-user should be aware that they need to understand their use cases and see how to best implement these scenarios. You can make the following components single user in Android: Service, Receiver, or Content Provider. Activities can not be declared as single user, which means they will be recreated for each user that is created in the system and uses the app.

Depending on use case, you can choose a strategy where you declare these components as single user, perform their joynr Provider/Proxy execution, and then share results across all users that want to make use of the data. For example, you can register a joynr Provider in a single-user Android Content Provider, store retrieved data there, and then the application retrieves this information from the Content Provider (which in this scenario is the single source of truth), available as a singleton in user 0.

Always remember that any component within the system that is not declared as single user will be run for every user in the system! This means that you need extra care to ensure that neither the CC nor joynr apps, unless required, have any such components, and if they do, you must be aware of this functioning, otherwise things might not work as intended.

You can read more about Android multi-user development in Android's official documentation.