diff --git a/README.md b/README.md index d6736ff..cfa4917 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/app/src/main/java/io/bytebeam/UplinkDemo/MainActivity.kt b/app/src/main/java/io/bytebeam/UplinkDemo/MainActivity.kt index 0355411..cc669f0 100644 --- a/app/src/main/java/io/bytebeam/UplinkDemo/MainActivity.kt +++ b/app/src/main/java/io/bytebeam/UplinkDemo/MainActivity.kt @@ -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==" @@ -23,4 +20,9 @@ class MainActivity : AppCompatActivity() { } } } + + override fun onResume() { + super.onResume() + findViewById(R.id.statusView).text = "configuratorAvaialable: ${Uplink.configuratorAvailable(this)}\nserviceRunning: ${Uplink.serviceRunning(this)}"; + } } \ No newline at end of file diff --git a/app/src/main/java/io/bytebeam/UplinkDemo/UplinkActivity.kt b/app/src/main/java/io/bytebeam/UplinkDemo/UplinkActivity.kt index 1dc8820..cb7e416 100644 --- a/app/src/main/java/io/bytebeam/UplinkDemo/UplinkActivity.kt +++ b/app/src/main/java/io/bytebeam/UplinkDemo/UplinkActivity.kt @@ -3,12 +3,10 @@ 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 @@ -16,6 +14,7 @@ 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 @@ -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() } } @@ -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 { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b670246..27b2e18 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -14,4 +14,6 @@ android:text="Start service" /> + + \ No newline at end of file diff --git a/configurator/build.gradle b/configurator/build.gradle index b91f2a3..82724f6 100644 --- a/configurator/build.gradle +++ b/configurator/build.gradle @@ -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 diff --git a/configurator/src/main/AndroidManifest.xml b/configurator/src/main/AndroidManifest.xml index b4157bb..c278d71 100644 --- a/configurator/src/main/AndroidManifest.xml +++ b/configurator/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ - + + + + + + @@ -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"/> \ No newline at end of file diff --git a/configurator/src/main/java/io/bytebeam/uplink/configurator/MainActivity.kt b/configurator/src/main/java/io/bytebeam/uplink/configurator/MainActivity.kt index 0ee65c5..58edd5b 100644 --- a/configurator/src/main/java/io/bytebeam/uplink/configurator/MainActivity.kt +++ b/configurator/src/main/java/io/bytebeam/uplink/configurator/MainActivity.kt @@ -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") @@ -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()) + } } } @@ -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 } } } @@ -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?) { diff --git a/configurator/src/main/java/io/bytebeam/uplink/configurator/ServiceStateProvider.kt b/configurator/src/main/java/io/bytebeam/uplink/configurator/ServiceStateProvider.kt new file mode 100644 index 0000000..a98ada1 --- /dev/null +++ b/configurator/src/main/java/io/bytebeam/uplink/configurator/ServiceStateProvider.kt @@ -0,0 +1,51 @@ +package io.bytebeam.uplink.configurator + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import io.bytebeam.uplink.common.Constants.PREFS_NAME + +class ServiceStateProvider : ContentProvider() { + override fun onCreate(): Boolean { + return true + } + + /** + * Service state is a boolean flag, we use null as false value here + */ + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + val running = context?.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).let { + it?.getBoolean(PREFS_SERVICE_RUNNING_KEY, false) + } ?: false + if (running) { + return MatrixCursor(arrayOf(), 0) + } else { + return null + } + } + + override fun getType(uri: Uri): String? { + return null + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + return null + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + return 0 + } + + override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int { + return 0 + } +} \ No newline at end of file diff --git a/configurator/src/main/java/io/bytebeam/uplink/configurator/types.kt b/configurator/src/main/java/io/bytebeam/uplink/configurator/types.kt deleted file mode 100644 index d9acbe9..0000000 --- a/configurator/src/main/java/io/bytebeam/uplink/configurator/types.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.bytebeam.uplink.configurator - -data class AuthConfig( - val project_id: String, - -) \ No newline at end of file diff --git a/configurator/src/main/java/io/bytebeam/uplink/service/UplinkService.java b/configurator/src/main/java/io/bytebeam/uplink/service/UplinkService.java deleted file mode 100644 index 0920c0a..0000000 --- a/configurator/src/main/java/io/bytebeam/uplink/service/UplinkService.java +++ /dev/null @@ -1,124 +0,0 @@ -package io.bytebeam.uplink.service; - -import android.app.Service; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.*; -import android.util.Log; -import androidx.annotation.Nullable; -import io.bytebeam.uplink.common.UplinkAction; -import io.bytebeam.uplink.common.UplinkPayload; - -import java.util.ArrayList; -import java.util.List; - -import static io.bytebeam.uplink.common.Constants.*; - -public class UplinkService extends Service { - public static final String TAG = "UplinkService"; - List subscribers = new ArrayList<>(); - long uplink = 0; - - @Nullable - @Override - public IBinder onBind(Intent intent) { - Log.d(TAG, "creating binder"); - - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - String authConfig = prefs.getString(PREFS_AUTH_CONFIG_KEY, null); - if (authConfig == null) { - Log.d(TAG, "auth config not found"); - new Handler(Looper.myLooper()).postDelayed(() -> {onUnbind(null);}, 200); - return null; - } - - uplink = NativeApi.createUplink( - authConfig, - String.format( - "[persistence]\n" + - "path = \"%s/uplink\"\n" + - "\n" + - "[streams.battery_stream]\n" + - "topic = \"/tenants/{tenant_id}/devices/{device_id}/events/battery_level\"\n" + - "buf_size = 1\n", - getApplicationContext().getFilesDir().getAbsolutePath() - ), - true, - this::uplinkSubscription - ); - Log.d(TAG, "uplink native context initialized"); - - Log.d(TAG, "returning messenger"); - IBinder result = new Messenger(new Handler(Looper.myLooper(), this::handleMessage)).getBinder(); - return result; - } - - @Override - public boolean onUnbind(Intent intent) { - Log.d(TAG, "shutting down uplink service and process"); - subscribers.clear(); - if (uplink != 0) { - NativeApi.destroyUplink(uplink); - uplink = 0; - } - // forcefully kill the service process to allow the cleanup of the native resources - System.exit(0); - return false; - } - - private boolean handleMessage(Message message) { - if (uplink == 0) { - Log.e(TAG, "messenger of an unbound service is being used, ignoring"); - return true; - } - switch (message.what) { - case SEND_DATA: - Bundle b = message.getData(); - b.setClassLoader(UplinkPayload.class.getClassLoader()); - UplinkPayload payload = b.getParcelable(DATA_KEY); - Log.d(TAG, String.format("Submitting payload: %s", payload.toString())); - NativeApi.sendData(uplink, payload); - break; - case SUBSCRIBE: - Log.d(TAG, "adding a subscriber"); - subscribers.add(message.replyTo); - break; - case STOP_SERVICE: - String sudoPass = getApplicationContext().getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(PREFS_SERVICE_SUDO_PASS_KEY, null); - if (sudoPass == null) { - Log.e(TAG, "Illegal service state error, privileged operation key not found"); - } else { - String pass = message.getData().getString(DATA_KEY, ""); - if (!pass.equals(sudoPass)) { - Log.e(TAG, String.format("privileged operation key mismatch: %s != %s", pass, sudoPass)); - } else { - Log.d(TAG, "stopping service"); - new Handler(Looper.myLooper()).postDelayed(() -> {onUnbind(null);}, 200); - } - } - break; - default: - throw new IllegalArgumentException(); - } - return true; - } - - private void uplinkSubscription(UplinkAction uplinkAction) { - if (uplink == 0) { - Log.e(TAG, "Action delivered to an unbound service, ignoring"); - return; - } - Log.d(TAG, String.format(TAG, "Received action: %s", uplinkAction.toString())); - for (Messenger subscriber : subscribers) { - Message m = new Message(); - Bundle b = new Bundle(); - b.putParcelable(DATA_KEY, uplinkAction); - m.setData(b); - try { - subscriber.send(m); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - } -} \ No newline at end of file diff --git a/configurator/src/main/java/io/bytebeam/uplink/service/UplinkService.kt b/configurator/src/main/java/io/bytebeam/uplink/service/UplinkService.kt new file mode 100644 index 0000000..99b9210 --- /dev/null +++ b/configurator/src/main/java/io/bytebeam/uplink/service/UplinkService.kt @@ -0,0 +1,163 @@ +package io.bytebeam.uplink.service + +import android.app.* +import android.content.Context +import android.content.Intent +import android.os.* +import android.util.Log +import android.widget.Toast +import androidx.core.app.NotificationCompat +import io.bytebeam.uplink.common.Constants +import io.bytebeam.uplink.common.UplinkAction +import io.bytebeam.uplink.common.UplinkPayload +import io.bytebeam.uplink.configurator.MainActivity +import io.bytebeam.uplink.configurator.R + +class UplinkService : Service() { + var subscribers: MutableList = ArrayList() + var uplink: Long = 0 + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + val prefs = applicationContext.getSharedPreferences(Constants.PREFS_NAME, MODE_PRIVATE) + val authConfig = prefs.getString(Constants.PREFS_AUTH_CONFIG_KEY, null) + if (authConfig == null) { + Log.d(TAG, "device config not found") + Toast.makeText(this, "device config not found", Toast.LENGTH_LONG).show() + stopSelf() + return START_NOT_STICKY + } + uplink = NativeApi.createUplink( + authConfig, String.format( + """ + [persistence] + path = "%s/uplink" + + [streams.battery_stream] + topic = "/tenants/{tenant_id}/devices/{device_id}/events/battery_level" + buf_size = 1 + + """.trimIndent(), + applicationContext.filesDir.absolutePath + ), + true + ) { uplinkAction: UplinkAction -> uplinkSubscription(uplinkAction) } + Log.d(TAG, "uplink native context initialized") + makeForeground() + return START_REDELIVER_INTENT + } + + private fun makeForeground() { + val NOTIFICATION_CHANNEL_ID = "io.bytebeam.uplink.sevice" + val channelName = "Uplink service status" + + val nm = (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) + nm.createNotificationChannel( + NotificationChannel( + NOTIFICATION_CHANNEL_ID, + channelName, + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + + val startAppIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java), + Intent.FLAG_ACTIVITY_NEW_TASK + ) + val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setOngoing(true) + .setChannelId(NOTIFICATION_CHANNEL_ID) + .setContentTitle("Uplink service") + .setContentText("Service is running and ready to be used") + .setPriority(NotificationManager.IMPORTANCE_DEFAULT) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentIntent(startAppIntent) + .build() + + startForeground(1, notification) + nm.notify(1, notification) + } + + override fun onBind(intent: Intent): IBinder? { + return if (uplink == 0L) { + Log.d(TAG, "uplink service not running") + null + } else { + Log.d(TAG, "returning messenger") + Messenger(Handler(Looper.myLooper()!!) { message: Message -> handleMessage(message) }).binder + } + } + + override fun onDestroy() { + Handler(Looper.myLooper()!!).postDelayed({ end() }, 200) + } + + private fun end() { + Log.d(TAG, "shutting down uplink service and process") + stopForeground(true) + subscribers.clear() + if (uplink != 0L) { + NativeApi.destroyUplink(uplink) + uplink = 0 + } + // forcefully kill the service process to allow the cleanup of the native resources + System.exit(0) + } + + private fun handleMessage(message: Message): Boolean { + if (uplink == 0L) { + Log.e(TAG, "messenger of an unbound service is being used, ignoring") + return true + } + when (message.what) { + Constants.SEND_DATA -> { + val b = message.data + b.classLoader = UplinkPayload::class.java.classLoader + val payload = b.getParcelable(Constants.DATA_KEY) + Log.d(TAG, String.format("Submitting payload: %s", payload.toString())) + NativeApi.sendData(uplink, payload) + } + Constants.SUBSCRIBE -> { + Log.d(TAG, "adding a subscriber") + subscribers.add(message.replyTo) + } + Constants.STOP_SERVICE -> { + val sudoPass = applicationContext.getSharedPreferences(Constants.PREFS_NAME, MODE_PRIVATE).getString( + Constants.PREFS_SERVICE_SUDO_PASS_KEY, null + ) + val pass = message.data.getString(Constants.DATA_KEY, null) + if (pass != null && pass == sudoPass) { + Log.d(TAG, "stopping service") + end() + } else { + Log.e(TAG, String.format("privileged operation key mismatch: %s is not valid", pass)) + } + } + else -> throw IllegalArgumentException() + } + return true + } + + private fun uplinkSubscription(uplinkAction: UplinkAction) { + if (uplink == 0L) { + Log.e(TAG, "Action delivered to an unbound service, ignoring") + return + } + Log.d(TAG, String.format("Broadcasting action: %s", uplinkAction.toString())) + for (subscriber in subscribers) { + val m = Message() + val b = Bundle() + b.putParcelable(Constants.DATA_KEY, uplinkAction) + m.data = b + try { + subscriber.send(m) + } catch (e: RemoteException) { + throw RuntimeException(e) + } + } + } + + companion object { + const val TAG = "UplinkService" + } +} \ No newline at end of file diff --git a/lib/src/main/java/io/bytebeam/uplink/Uplink.java b/lib/src/main/java/io/bytebeam/uplink/Uplink.java index 54d0c39..0fdfb80 100644 --- a/lib/src/main/java/io/bytebeam/uplink/Uplink.java +++ b/lib/src/main/java/io/bytebeam/uplink/Uplink.java @@ -5,11 +5,13 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.net.Uri; import android.os.*; import android.util.Log; import io.bytebeam.uplink.common.*; -import io.bytebeam.uplink.common.exceptions.ConfiguratorUnavailableException; -import io.bytebeam.uplink.common.exceptions.UplinkNotConfiguredException; +import io.bytebeam.uplink.common.exceptions.ConfiguratorNotInstalledException; +import io.bytebeam.uplink.common.exceptions.UplinkServiceNotRunningException; import io.bytebeam.uplink.common.exceptions.UplinkTerminatedException; import java.util.List; @@ -36,9 +38,12 @@ public UplinkServiceState getState() { public Uplink( Context context, UplinkStateCallback uplinkReadyCallback - ) throws ConfiguratorUnavailableException { + ) throws ConfiguratorNotInstalledException, UplinkServiceNotRunningException { if (!configuratorAvailable(context)) { - throw new ConfiguratorUnavailableException(); + throw new ConfiguratorNotInstalledException(); + } + if (!serviceRunning(context)) { + throw new UplinkServiceNotRunningException(); } this.context = context; this.serviceStateCallback = uplinkReadyCallback; @@ -118,22 +123,8 @@ public void respondToAction(ActionResponse response) throws UplinkTerminatedExce * The instance must not be used after this method is called */ public void dispose() { - switch (state) { - case CONNECTED: - context.unbindService(this); - state = UplinkServiceState.FINISHED; - break; - case SERVICE_NOT_CONFIGURED: - case SERVICE_STOPPED: - // do nothing since the connection has already been unbound - break; - case UNINITIALIZED: - context.unbindService(this); - Log.e(TAG, "Attempting to dispose an uninitialized instance"); - case FINISHED: - Log.e(TAG, "Attempted to dispose an Uplink instance twice"); - break; - } + state = UplinkServiceState.DISPOSED; + context.unbindService(this); } @Override @@ -145,34 +136,32 @@ public void onServiceConnected(ComponentName name, IBinder service) { @Override public void onServiceDisconnected(ComponentName name) { - context.unbindService(this); - if (state != UplinkServiceState.FINISHED) { + if (state != UplinkServiceState.DISPOSED) { state = UplinkServiceState.SERVICE_STOPPED; } + context.unbindService(this); } @Override public void onBindingDied(ComponentName name) { Log.e(TAG, "uplink binding died"); + context.unbindService(this); } @Override public void onNullBinding(ComponentName name) { Log.i(TAG, "uplink service not ready"); - state = UplinkServiceState.SERVICE_NOT_CONFIGURED; - serviceStateCallback.onServiceNotConfigured(); + state = UplinkServiceState.SERVICE_STOPPED; context.unbindService(this); } private void stateAssertion() throws UplinkTerminatedException { switch (state) { - case SERVICE_NOT_CONFIGURED: - throw new UplinkNotConfiguredException(); case SERVICE_STOPPED: throw new UplinkTerminatedException(); case UNINITIALIZED: throw new IllegalStateException("attempt to use service before initialization is complete"); - case FINISHED: + case DISPOSED: throw new IllegalStateException("attempt to use service after it was disposed"); } } @@ -181,8 +170,19 @@ public static boolean configuratorAvailable(Context context) { Intent intent = new Intent(); intent.setComponent(new ComponentName(CONFIGURATOR_APP_ID, UPLINK_SERVICE_ID)); List services = context.getPackageManager().queryIntentServices(intent, 0); - Log.e(TAG, String.format("Available services: %s", services.toString())); + Log.d(TAG, String.format("Available services: %s", services.toString())); return services.size() != 0; } + + public static boolean serviceRunning(Context context) { + Cursor cursor = context.getContentResolver().query( + Uri.parse("content://io.bytebeam.uplink.servicestate"), + new String[] {}, + null, + new String[] {}, + null + ); + return cursor != null; + } } diff --git a/lib/src/main/java/io/bytebeam/uplink/UplinkServiceState.java b/lib/src/main/java/io/bytebeam/uplink/UplinkServiceState.java index 174bde4..1cc3d59 100644 --- a/lib/src/main/java/io/bytebeam/uplink/UplinkServiceState.java +++ b/lib/src/main/java/io/bytebeam/uplink/UplinkServiceState.java @@ -9,16 +9,12 @@ public enum UplinkServiceState { * The client is ready to be used */ CONNECTED, - /** - * No device configuration selected in configurator app - */ - SERVICE_NOT_CONFIGURED, /** * The service has stopped */ SERVICE_STOPPED, /** - * The client has disconnected from the service + * Dispose method has been called */ - FINISHED + DISPOSED } diff --git a/lib/src/main/java/io/bytebeam/uplink/UplinkStateCallback.java b/lib/src/main/java/io/bytebeam/uplink/UplinkStateCallback.java index 9efdd8c..e637658 100644 --- a/lib/src/main/java/io/bytebeam/uplink/UplinkStateCallback.java +++ b/lib/src/main/java/io/bytebeam/uplink/UplinkStateCallback.java @@ -6,10 +6,4 @@ public interface UplinkStateCallback { * ready to be used */ void onUplinkReady(); - - /** - * Called if the user hasn't initialized the uplink service using - * the configurator app - */ - void onServiceNotConfigured(); } diff --git a/lib/src/main/java/io/bytebeam/uplink/common/exceptions/ConfiguratorUnavailableException.java b/lib/src/main/java/io/bytebeam/uplink/common/exceptions/ConfiguratorNotInstalledException.java similarity index 62% rename from lib/src/main/java/io/bytebeam/uplink/common/exceptions/ConfiguratorUnavailableException.java rename to lib/src/main/java/io/bytebeam/uplink/common/exceptions/ConfiguratorNotInstalledException.java index 252dbfd..6c70cb3 100644 --- a/lib/src/main/java/io/bytebeam/uplink/common/exceptions/ConfiguratorUnavailableException.java +++ b/lib/src/main/java/io/bytebeam/uplink/common/exceptions/ConfiguratorNotInstalledException.java @@ -3,8 +3,8 @@ /** * Thrown if the configurator app is not installed on this device */ -public class ConfiguratorUnavailableException extends Exception { - public ConfiguratorUnavailableException() { +public class ConfiguratorNotInstalledException extends Exception { + public ConfiguratorNotInstalledException() { super("Configurator app is not installed on this device"); } } diff --git a/lib/src/main/java/io/bytebeam/uplink/common/exceptions/UplinkNotConfiguredException.java b/lib/src/main/java/io/bytebeam/uplink/common/exceptions/UplinkServiceNotRunningException.java similarity index 67% rename from lib/src/main/java/io/bytebeam/uplink/common/exceptions/UplinkNotConfiguredException.java rename to lib/src/main/java/io/bytebeam/uplink/common/exceptions/UplinkServiceNotRunningException.java index eaf16c3..d77226e 100644 --- a/lib/src/main/java/io/bytebeam/uplink/common/exceptions/UplinkNotConfiguredException.java +++ b/lib/src/main/java/io/bytebeam/uplink/common/exceptions/UplinkServiceNotRunningException.java @@ -3,8 +3,8 @@ /** * Thrown if the service is not configured properly using the configurator app */ -public class UplinkNotConfiguredException extends RuntimeException { - public UplinkNotConfiguredException() { +public class UplinkServiceNotRunningException extends Exception { + public UplinkServiceNotRunningException() { super("attempt to use an uplink instance when the service is not configured properly"); } }