Via examples on the web, I've put together the following test app with a single Activity (MainActivity) and a remote service (MyRemoteService). From MainActivity I'm able to establish a connection and send a message with an incremented message count that the service displays to the user via a toast statement.
At some point I hope to put my Bluetooth code in MyRemoteService and send commands from an activity in my app to the service to scan, connect, discover services, and set up notifications. Once the BLE device begins sending notifications to MyRemoteService, is there a way for MyRemoteService to notify my activity that there's new data available?
I have only seen the paradigm where the activity (client) sends a message and the service (server) responds. That is, I haven't seen an example with a server initiated request using Android bound services with messages. Is that possible?
I rather not use polling to see what's going on. Or should I be approaching this problem using a different method or service type? The ultimate goal is that I could receive a notification (or message or broadcast) from the service that could be handled from any activity that the app is currently in (which seems like I'd have to set up listeners of some type in each activity).
I made the bound service use a different process by adding android:process=":myPrivateProcess" to the service tag in AndroidManifest.xml (all it takes is a process name that starts with a colon). I did this so that the BLE code (eventually) won't cause blocking of the UI code in the activity since from what I've read bound services run on the main (or UI) thread by default.
Below are the four files of interest that make up my remote service example code. If server initiated requests are possible, I could use some pointers as to how to implement it.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.remote_service_example" >
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.REMOTE_SERVICE_EXAMPLE" >
<activity
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>
<service
android:name=".MyRemoteService"
android:enabled="true"
android:exported="false"
android:process=":myPrivateProcess" />
</application>
</manifest>
activity_mail.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView_A"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Remote Service Experiment"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@+id/button_send"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send Message"
app:layout_constraintBottom_toTopOf="@+id/button_stop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_A" />
<Button
android:id="@+id/button_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop Service"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_send" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
package com.example.remote_service_example
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.*
import androidx.appcompat.app.AppCompatActivity
import android.util.Log
import com.example.remote_service_example.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
var myService: Messenger? = null
var isBound: Boolean = false
var msgCount: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val intent = Intent(applicationContext, MyRemoteService::class.java)
binding.buttonSend.setOnClickListener{
sendMessage()
}
binding.buttonStop.setOnClickListener{
if (isBound) {
Log.d("DBG","Sending unbindService command")
unbindService(myConnection)
isBound = false
} else {
Log.d("DBG","Service already unbound - command not sent")
}
}
bindService(intent, myConnection, Context.BIND_AUTO_CREATE)
}
private val myConnection = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
myService = Messenger(service)
isBound = true
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d("DBG","Entered onServiceDisconnected")
myService = null
isBound = false
}
}
private fun sendMessage() {
Log.d("DBG","Entered sendMessage - isBound = $isBound")
if (!isBound) return
++msgCount
val msg = Message.obtain()
val bundle = Bundle()
bundle.putString("MY_MSG", "Message $msgCount Received")
msg.data = bundle
try {
myService?.send(msg)
} catch (e: RemoteException) {
Log.d("DBG","Error sending message")
e.printStackTrace()
}
}
}
MyRemoteService.kt
package com.example.remote_service_example
import android.app.Service
import android.content.Intent
import android.os.*
import android.util.Log
import android.widget.Toast
class MyRemoteService : Service() {
inner class IncomingHandler: Handler(Looper.getMainLooper() ){
override fun handleMessage(msg: Message) {
Log.d("DBG","Entered remote handleMessage")
val data = msg.data
val dataString = data.getString("MY_MSG")
Toast.makeText(applicationContext, dataString, Toast.LENGTH_SHORT).show()
}
}
private val myMessenger = Messenger(IncomingHandler())
override fun onBind(intent: Intent): IBinder {
Log.d("DBG","Entered onBind")
return myMessenger.binder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
Toast.makeText(applicationContext, "Service destroyed", Toast.LENGTH_SHORT).show()
}
}