Skip to content

Commit

Permalink
Merge branch 'persistent_service'
Browse files Browse the repository at this point in the history
  • Loading branch information
amokfa committed Jul 8, 2022
2 parents 23c161b + 0933531 commit f1e9391
Show file tree
Hide file tree
Showing 16 changed files with 382 additions and 246 deletions.
25 changes: 12 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,29 @@ configuration for the uplink bridge, which you have to select using the applicat

The client sdk can be used by app developers to communicate with the uplink service. It handles all the low level
messaging details and provides a simple high level interface for the app developers. The configurator app must be
installed on the device before the client sdk can be used, otherwise an exception will be throws during initialization.
There is also an `boolean Uplink.configuratorAvailable(Context)` method.
installed on the device and the service must be started before the clients can connect to it, otherwise exceptions
will be throws during initialization.

#### API

The `io.bytebeam.uplink.Uplink` class (from the `lib` module) provides the client side api for this sdk. These are the
steps to use this library:

1. Use `Uplink.configuratorAvailable(Context)` to check if the configurator app is installed on the device.
2. Instantiate the `Uplink` class, providing an implementation of `UplinkStateCallback`. This will be used to
notify the app about the state of the uplink bridge. This interface has two methods
1. `onServiceReady()`: invoked when the service is ready to be used
2. `onServiceNotConfigured()`: invoked if the service hasn't been configured yet. The user needs to select the
authorization configuration using the configurator app.
3. Once the service is ready, you can use the methods on the `Uplink` instance to communicate with the backend:
1. Instantiate the `Uplink` class, providing an implementation of `UplinkStateCallback`. This will be used to
notify the app about the state of the uplink bridge. This constructor may throw the following exceptions:
1. `ConfiguratorNotInstalledException`
2. `UplinkServiceNotRunningException`
You may also use the `static boolean Uplink.configuratorAvailable(Context)` and `static boolean Uplink.serviceRunning(Context)`
methods to query the uplink setup.
2. Once the service is ready, you can use the methods on the `Uplink` instance to communicate with the backend:
1. `Uplink.subscribe(ActionSubscriber)` - Subscribe to action targeting this device.
2. `Uplink.sendData(UplinkPayload)` - Send some data to the backend.
3. `Uplink.respondToAction(ActionResponse)` - Respond to an action that the device received from the backend.

Each of these methods can throw an `UplinkTerminatedException` if the uplink service is terminated for some
reason (the user stops it or the device configuration is changed).
If that happens the clients need to wait for some time, go back to step 2, and attempt reconnecting to the
service. The example app shows how to do that.
4. The application must properly dispose the `Uplink` instance when it is no longer needed using the `dispose` method.
If that happens, the clients need to wait for some time, and attempt reconnecting to the service. The example app \shows how to do that.
3. The application must properly dispose the `Uplink` instance when it is no longer needed using the `dispose` method.

#### Generate `.aar`

Expand Down
12 changes: 7 additions & 5 deletions app/src/main/java/io/bytebeam/UplinkDemo/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package io.bytebeam.UplinkDemo

import android.content.Intent
import android.content.res.Resources
import android.os.Bundle
import android.widget.Button
import androidx.annotation.RawRes
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

fun Resources.getRawTextFile(@RawRes id: Int) =
openRawResource(id).bufferedReader().use { it.readText() }
import io.bytebeam.uplink.Uplink

const val TAG = "==APP=="

Expand All @@ -23,4 +20,9 @@ class MainActivity : AppCompatActivity() {
}
}
}

override fun onResume() {
super.onResume()
findViewById<TextView>(R.id.statusView).text = "configuratorAvaialable: ${Uplink.configuratorAvailable(this)}\nserviceRunning: ${Uplink.serviceRunning(this)}";
}
}
19 changes: 7 additions & 12 deletions app/src/main/java/io/bytebeam/UplinkDemo/UplinkActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ package io.bytebeam.UplinkDemo
import android.os.BatteryManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.TextView
import android.widget.Toast
import io.bytebeam.uplink.common.exceptions.ConfiguratorUnavailableException
import io.bytebeam.uplink.common.exceptions.ConfiguratorNotInstalledException
import io.bytebeam.uplink.Uplink
import io.bytebeam.uplink.UplinkStateCallback
import io.bytebeam.uplink.UplinkServiceState
import io.bytebeam.uplink.common.ActionSubscriber
import io.bytebeam.uplink.common.ActionResponse
import io.bytebeam.uplink.common.UplinkAction
import io.bytebeam.uplink.common.UplinkPayload
import io.bytebeam.uplink.common.exceptions.UplinkServiceNotRunningException
import org.json.JSONObject
import java.util.concurrent.Executors

Expand All @@ -39,8 +38,11 @@ class UplinkActivity : AppCompatActivity(), UplinkStateCallback, ActionSubscribe
log("connecting to uplink service")
try {
uplink = Uplink(this, this)
} catch (e: ConfiguratorUnavailableException) {
Toast.makeText(this, "configurator app is not installed on this device", Toast.LENGTH_SHORT).show()
} catch (e: ConfiguratorNotInstalledException) {
Toast.makeText(this, "configurator app is not installed on this device", Toast.LENGTH_LONG).show()
finish()
} catch (e: UplinkServiceNotRunningException) {
Toast.makeText(this, "You need to start the uplink service using the configurator app", Toast.LENGTH_LONG).show()
finish()
}
}
Expand Down Expand Up @@ -89,13 +91,6 @@ class UplinkActivity : AppCompatActivity(), UplinkStateCallback, ActionSubscribe
}
}

override fun onServiceNotConfigured() {
log("uplink service not ready, waiting...")
Handler(Looper.myLooper()!!).postDelayed({
initUplink()
}, 3000)
}

override fun processAction(action: UplinkAction) {
log("Received action: $action")
Executors.newSingleThreadExecutor().execute {
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@
android:text="Start service"
/>

<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/statusView" />

</LinearLayout>
6 changes: 3 additions & 3 deletions configurator/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ dependencies {

def rustBasePath = ".."
def archTriplets = [
'armeabi-v7a': 'armv7-linux-androideabi',
'arm64-v8a' : 'aarch64-linux-android',
// 'armeabi-v7a': 'armv7-linux-androideabi',
// 'arm64-v8a' : 'aarch64-linux-android',
'x86' : 'i686-linux-android',
'x86_64' : 'x86_64-linux-android',
// 'x86_64' : 'x86_64-linux-android',
]

// TODO: only pass --release if buildType is release
Expand Down
16 changes: 12 additions & 4 deletions configurator/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.bytebeam.uplink.configurator">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

<application
android:allowBackup="true"
Expand All @@ -11,11 +12,19 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Uplink">

<provider
android:enabled="true"
android:exported="true"
android:authorities="io.bytebeam.uplink.servicestate"
android:name="io.bytebeam.uplink.configurator.ServiceStateProvider" />

<activity
android:name="io.bytebeam.uplink.configurator.MainActivity"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
Expand All @@ -24,8 +33,7 @@
android:name="io.bytebeam.uplink.service.UplinkService"
android:enabled="true"
android:exported="true"
android:process=":uplink_service"
/>
android:process=":uplink_service"/>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,53 @@ import org.json.JSONObject

const val PICK_AUTH_CONFIG = 1
const val TAG = "MainActivity"
const val PREFS_SERVICE_RUNNING_KEY = "serviceState"

enum class ServiceState {
STOPPING,
STOPPED,
STARTING,
STARTED,
}

class MainActivity : AppCompatActivity(), ServiceConnection {
lateinit var statusView: TextView
lateinit var selectBtn: Button

private lateinit var _serviceState: ServiceState
var serviceState: ServiceState
get() = _serviceState
set(newState) {
_serviceState = newState
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit().let {
it.putBoolean(PREFS_SERVICE_RUNNING_KEY, _serviceState == ServiceState.STARTED)
it.apply()
}
updateUI()
}

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

val prefs = applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putString(PREFS_SERVICE_SUDO_PASS_KEY, genPassKey()).apply()

statusView = findViewById(R.id.status_view)
selectBtn = findViewById(R.id.select_config_btn)

serviceState = if (serviceRunning()) {
ServiceState.STARTED
} else {
ServiceState.STOPPED
}

applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).let {
if (!it.contains(PREFS_SERVICE_SUDO_PASS_KEY)) {
it.edit().putString(PREFS_SERVICE_SUDO_PASS_KEY, genPassKey()).apply()
}
}

selectBtn.setOnClickListener {
if (applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).contains(PREFS_AUTH_CONFIG_NAME_KEY)) {
if (serviceRunning()) {
serviceState = ServiceState.STOPPING
AlertDialog.Builder(this)
.setTitle("Remove device config")
.setMessage("This operation will restart the uplink service, the connected clients will have to reconnect")
Expand All @@ -46,52 +77,69 @@ class MainActivity : AppCompatActivity(), ServiceConnection {
it.apply()
}

if (serviceRunning()) {
Log.e(TAG, "stopping service")
Intent().also {
it.component = ComponentName(CONFIGURATOR_APP_ID, UPLINK_SERVICE_ID)
bindService(
it,
this,
Context.BIND_AUTO_CREATE or Context.BIND_NOT_FOREGROUND
)
Log.d(TAG, "stopping service")
Intent().also {
it.component = ComponentName(CONFIGURATOR_APP_ID, UPLINK_SERVICE_ID)
bindService(
it,
this,
Context.BIND_NOT_FOREGROUND
)
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
serviceState = ServiceState.STARTED
}
.setOnDismissListener {
when (serviceState) {
ServiceState.STARTED -> {}
ServiceState.STOPPED -> {}
ServiceState.STOPPING -> {
serviceState = ServiceState.STARTED
}
} else {
Log.e(TAG, "service not running")
ServiceState.STARTING -> throw IllegalStateException()
}

updateUI()
}
.setNegativeButton(android.R.string.cancel, null)
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
} else {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
}

startActivityForResult(intent, PICK_AUTH_CONFIG)
serviceState = ServiceState.STARTING
startActivityForResult(
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
},
PICK_AUTH_CONFIG
)
}
}

updateUI()
}

private fun updateUI() {
val prefs = applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val configName = prefs.getString(PREFS_AUTH_CONFIG_NAME_KEY, null)
runOnUiThread {
when (configName) {
null -> {
statusView.text = "No device config selected"
when (serviceState) {
ServiceState.STOPPING -> {
statusView.text = "Stopping service"
selectBtn.isEnabled = false
}
ServiceState.STOPPED -> {
statusView.text = "Service stopped"
selectBtn.text = "Select device config"
selectBtn.isEnabled = true
selectBtn.setBackgroundColor(0xFF0022CC.toInt())
}
else -> {
statusView.text = "Service ready for $configName"
selectBtn.text = "Remove device config"
ServiceState.STARTING -> {
statusView.text = "Starting service"
selectBtn.isEnabled = false
}
ServiceState.STARTED -> {
statusView.text = "Service running for $configName"
selectBtn.text = "Stop service"
selectBtn.isEnabled = true
selectBtn.setBackgroundColor(0xFFFF3300.toInt())

}
}
}
Expand Down Expand Up @@ -123,8 +171,13 @@ class MainActivity : AppCompatActivity(), ServiceConnection {
it.putString(PREFS_AUTH_CONFIG_KEY, jsonString)
it.apply()
}
updateUI()
startService(Intent().also {
it.component = ComponentName(CONFIGURATOR_APP_ID, UPLINK_SERVICE_ID)
})
serviceState = ServiceState.STARTED
}
} else {
serviceState = ServiceState.STOPPED
}
}
}
Expand Down Expand Up @@ -152,15 +205,18 @@ class MainActivity : AppCompatActivity(), ServiceConnection {
}
}
)
serviceState = ServiceState.STOPPED
unbindService(this)
}

override fun onServiceDisconnected(name: ComponentName?) {
Log.e(TAG, "onServiceDisconnected")
Log.d(TAG, "onServiceDisconnected")
unbindService(this)
}

override fun onBindingDied(name: ComponentName?) {
Log.e(TAG, "onBindingDied")
Log.d(TAG, "onBindingDied")
unbindService(this)
}

override fun onNullBinding(name: ComponentName?) {
Expand Down
Loading

0 comments on commit f1e9391

Please sign in to comment.