0

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()
    }
}
Jim
  • 33
  • 10
  • Does this answer your question? [How to have Android Service communicate with Activity](https://stackoverflow.com/questions/2463175/how-to-have-android-service-communicate-with-activity) – dominicoder Jan 15 '22 at 22:57
  • Thanks. I did see that post and spent a lot of time going through it. It steered me toward using a bound service, but it didn't explain whether or how the server could initiate requests (at least not the I could see :) ) – Jim Jan 15 '22 at 23:50
  • 1
    I would say this totally depends on if your "service" runs in the same process as your activity or in a separate process. If it runs in the same process, you don't really need a service at all, from a technical point of view. Otherwise use aidl to pass a callback interface as a parameter the service later calls. – Emil Jan 16 '22 at 01:38

1 Answers1

1

Yes, but depending on your architecture it may be limited. If you're in the same process, you can pass the service an interface via the Binder that it can call when it needs to send an event.

If you aren't in the same process, you can use AIDL. It's a pain, and its limited in what data types and data sizes you can pass.

You can also use BroadcastReceivers, but that's even more limited than AIDL, although easier to set up. This does have the advantage of being able to send to multiple clients easily.

Gabe Sechan
  • 90,003
  • 9
  • 87
  • 127
  • Thanks Gabe for the suggestion. I've started reading up on AIDL and found a useful example on giving the server a callback to use to initiate communication with the client in Mark Murphy's excellent book "The Busy Coder's Guide to Android Development." Assuming that I will be able to work my way through that, I'm wondering where should the callback reside in the client? I'd like the client side to be able to switch screens to look at data while the service is sending Bluetooth data back to the client. The data collection should continue while the user is looking at either of 2 screens. Ideas? – Jim Jan 17 '22 at 15:58