Kotlin callback not called within async function - android

I am attempting to subscribe to multiple characteristics of a BLE peripheral within Android API 28.
Due to the asynchronous nature of the BLE API I need to make the function that subscribes to each characteristic (gatt.writeDescriptor()) block; otherwise the BLE API will attempt to subscribe to multiple characteristics at once, despite the fact that only one descriptor can be written at a time: meaning that only one characteristic is ever subscribed to.
The blocking is achieved by overriding the onServicesDiscovered callback and calling an asynchronous function to loop through and subscribe to characteristics. This is blocked with a simple Boolean value (canContinue). Unfortunately, the callback function onDescriptorWrite is never called.
See the code below:
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
canContinue = true
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
runBlocking {
loopAsync(gatt)
}
}
private suspend fun loopAsync(gatt: BluetoothGatt) {
coroutineScope {
async {
gatt.services.forEach { gattService ->
gattService.characteristics.forEach { gattChar ->
CHAR_LIST.forEach {
if (gattChar.uuid.toString().contains(it)) {
canContinue = false
gatt.setCharacteristicNotification(gattChar, true)
val descriptor = gattChar.getDescriptor(UUID.fromString(BleNamesResolver.CLIENT_CHARACTERISTIC_CONFIG))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
val write = Runnable {
gatt.writeDescriptor(descriptor)
}
//private val mainHandler = Handler(Looper.getMainLooper())
//mainHandler.post(write)
//runOnUiThread(write)
gatt.writeDescriptor(descriptor)
}
while (!canContinue)
}
}
}
}
}
}
It was suggested in a related post that I run the gatt.writeDescriptor() function in the main thread. As you can see in the code above I have tried this to no avail using both runOnUiThread() and creating a Handler object following suggestions from this question.
The callback gets called if I call gatt.writeDescriptor() from a synchronous function, I have no idea why it doesn't get called from an asynchronous function.
EDIT: It appears that the while(!canContinue); loop is actually blocking the callback. If I comment this line out, the callback triggers but then I face the same issue as before. How can I block this function?
Any suggestions are most welcome! Forgive my ignorance, but I am very much used to working on embedded systems, Android is very much a new world to me!
Thanks,
Adam

I posted some notes in the comments but I figured it would be better to format it as an answer.
Even though you already fixed your issue I'd suggest running the actual coroutine asynchronously and inside of it wait for the write notification using channels
private var channel: Channel<Boolean> = Channel()
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
GlobalScope.async {
channel.send(true)
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
GlobalScope.async {
loopAsync(gatt)
}
}
private suspend fun loopAsync(gatt: BluetoothGatt) {
gatt.services.forEach { gattService ->
gattService.characteristics.forEach { gattChar ->
CHAR_LIST.forEach {
if (gattChar.uuid.toString().contains(it)) {
gatt.setCharacteristicNotification(gattChar, true)
val descriptor = gattChar.getDescriptor(UUID.fromString(BleNamesResolver.CLIENT_CHARACTERISTIC_CONFIG))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
channel.receive()
}
}
}
}
}

So I actually figured out the answer myself.
The while(!canContinue); loop was actually blocking the callback as it was running in the main thread and took priority over the callback required to set the canContinue variable.
This was solved simply by calling both the gatt.writeDescriptor() function and the while loop from within the main thread:
val subscribe = Runnable {
gatt.writeDescriptor(descriptor)
while (!canContinue);
}
runOnUiThread(subscribe)

Related

Android Kotlin Coroutine Channel message not sended in websocket callback

I am developing an chat android app using WebSocket (OkHttp)
To do this, I implemented the okhttp3.WebSocketListener interface.
And I am receiving the chat messages from the onMessage callback method.
I already developed it using the Rx-PublishSubject, and it works fine.
But I want to change it to Coroutine-Channel.
To do this, I added the Channel in my WebSocketListener class.
#Singleton
class MyWebSocketService #Inject constructor(
private val ioDispatcher: CoroutineDispatcher
): WebSocketListener() {
// previous
val messageSubject: PublishSubject<WsMsg> = PublishSubject.create()
// new
val messageChannel: Channel<WsMsg> by lazy { Channel() }
override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
// previous
messageSubject.onNext(text)
// new
runBlocking(ioDispatcher) {
Log.d(TAG, "message: $text")
messageChannel.send(text)
}
}
}
But... the coroutine channel doesn't work...
It receives and prints the Log only once.
But it doesn't print the log after the second message.
But when I change the code like below, it works!
override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
GlobalScope.launch(ioDispatcher) {
Log.d(TAG, "message: $text")
messageChannel.send(text)
}
}
The difference is runBlocking vs GlobalScope.
I head that the GlobakScope may not ensure the message's ordering.
So It is not suitable for the Chat app.
How can I solve this issue?
The default Channel() has no buffer, which causes send(message) to suspend until a consumer of the channel calls channel.receive() (which is implicitely done in a for(element in channel){} loop)
Since you are using runBlocking, suspending effectively means blocking the current thread. It appears that okhttp will always deliver messages on the same thread, but it can not do that because you are still blocking that thread.
The correct solution to this would be to add a buffer to your channel. If it is unlikely that messages will pour in faster than you can process them, you can simply replace Channel() with Channel(Channel.UNLIMITED)

Android Mockito Kotlin coroutine test cancel

I have a retrofit service
interface Service {
#PUT("path")
suspend fun dostuff(#Body body: String)
}
It is used in android view model.
class VM : ViewModel(private val service: Service){
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
val state = MutableLiveData<String()
init {
uiScope.launch {
service.doStuff()
state.value = "lol"
}
}
override fun onCleared(){
viewModelJob.cancel()
}
}
I would like to write a test for the cancelling of the view model. This will be done mocking service and delaying so that the co routine does not complete. Whilst blocking, we invoke onCleared to cancel the co routine. This should prevent state getting set...
#Test
fun `on cleared - cancels request`() = runBlocking {
//given
`when`(service.doStuff()).thenAnswer { launch { delay(1000) } }
val vm = ViewModel(service)
// when
vm.cleared()
//then
assertThat(vm.state, nullValue())
}
However it seems that vm.state always gets set??? What is the best way to test when clearing a scope that a co routine gets cancelled?
The problem here is in thenAnswer { launch { delay(1000) } }, which effectively makes your doStuff method look like that:
suspend fun doStuff() {
launch { delay(1000) }
}
As you can see, this function does not actually suspend, it launches a coroutine and returns immediately. What would actually work here is thenAnswer { delay(1000) }, which does not work, because there is no suspend version of thenAnswer in Mockito (as far as I know at least).
I would recommend to switch to Mokk mocking library, which supports kotlin natively. Then you can write coEvery { doStuff() } coAnswers { delay(1000) } and it will make your test pass (after fixing all the syntax errors ofc).

subscribing on scheduler.io() is not working

My viewmodel calls the repository methods to fetch some data from room database and also from network.
class Repository #Inject constructor(
private val remoteDatasource: IRemoteSource,
private val localDatasource: ILocalSource,
private val subscriberScheduler: Scheduler,
private val observerScheduler: Scheduler
) : IRepository {
//this method fetches data from room
override fun getData(): Flowable<Boolean> {
return localDatasource.shouldFetchRemote().subscribeOn(subscriberScheduler)
.observeOn(observerScheduler)
}
// makes api call
override fun getRemoteData(): Flowable<Data> {
return remoteDatasource.getData().subscribeOn(subscriberScheduler)
.observeOn(observerScheduler)
}
subscriberScheduler is Schedulers.io() and observer scheduler is AndroidSchedulers.mainThread().
I get exception when I do query from room, saying that the opertion is in main thread.
Also when I get data from remote source, I check the thread, it is main thread, but this no exception like network call on main thread.
Here is my localsource class which uses room:
class Localsource constructor(private val dataDao: DataDao):ILocalSource {
override fun shouldFetchRemote(): Flowable<Boolean> {
if (Looper.getMainLooper().thread == Thread.currentThread()) {
Log.v("thread","main thread")
//this log prints
}
//exception thrown here
return Flowable.just(dataDao.isDataPresent() != 0)
}
Here is class for RemoteSource
#OpenForTesting
class Remotesource #Inject constructor():IRemoteSource{
override fun getData(): Flowable<Data> {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
Log.v("thread","main thread")
//this log prints but no exception is thrown like network call on main thread.
}
return service.getData().flatMap { Flowable.just(it.data) }
}
}
Your assumptions about what happens where are wrong. That is an issue.
Lets look at shouldFetchRemote() method.
//This part will always be on the main thread because it is run on it.
//Schedulers applied only for the created reactive
//stream(Flowable, Observable, Single etc.) but not for the rest of the code in the method.
if (Looper.getMainLooper().thread == Thread.currentThread()) {
Log.v("thread","main thread")
//this log prints
}
//exception thrown here
//Yes it is correct that exception is thrown in this line
//because you do reach for the database on the main thread here.
//It is because Flowable.just() creates stream out of the independent data
//that does not know anything about scheduler here.
// dataDao.isDataPresent() - is run on the main thread
//because it is not yet part of the reactive stream - only its result is!!!!
//That is crucial
return Flowable.just(dataDao.isDataPresent() != 0)
In order to include the function into a stream you need to take another approach. Room has an ability to return Flowables directly and store booleans. This way you can use it like this
In DAO
#Query(...)
Boolean isDataPresent(): Flowable<Boolean>
In your local source
override fun shouldFetchRemote(): Flowable<Boolean> = dataDao.isDataPresent()
This way it will work as expected because now the whole function is the part of reactive stream and will react to schedulers.
The same with remote sourse. Retrofit can return Observables or Flowables out of the box
interface Service{
#GET("data")
fun getData(): Flowable<Data>
}
// and the repo will be
val service = retrofit.create(Service::class.java)
override fun getData(): Flowable<Data> = service.getData()
This way everything will work as expected because now it is the part of a stream.
If you want to use plan data from Room or Retrofit - you can do it either. The only thing is Flowable.just() won't work.
For example for your local source you will need to do something like
//DAO
#Query(...)
Boolean isDataPresent(): Boolean
override fun shouldFetchRemote(): Flowable<Boolean> = Flowable.create<Boolean>(
{ emitter ->
emitter.onNext(dataDao.isDataPresent())
emitter.onComplete() //This is crucial because without onComplete the emitter won't emit anything
//There is also emitter.onError(throwable: Throwable) to handle errors
}, BackpressureStrategy.LATEST).toObservable() // there are different Backpressure Strategies
There are similar factories for Obserwable and other reactive stream.
And generally I would recommend you to read the documentation.

How to dispose Rx in WorkManager?

I implemented an AlarmManager to send notifications when user adds a due date to a Task. However, when the user turns off the device, all the alarms are lost. Now I'm updating the BroadcastReceiver to receive an android.intent.action.BOOT_COMPLETED and reschedule all the alarms set to each task.
My first attempt was to get an Rx Single with all the tasks where the due date is higher than the current time inside the BroadcastReceiver, then reschedule all the alarms. The issue is I'm not able to dispose the Observable once the BroadcastReceiver has no lifecycle. Also, it seems that this is not a good approach.
During my researches, the IntentService was a good solution for this case, but I'm getting into the new WorkManager library and the OneTimeWorkRequest looks like a good and simple solution.
The Worker is being called and executing correctly, but I'm not able to dispose the Observable because the onStopped method is never called.
Here is the implementation, based on this snippet:
class TaskAlarmWorker(context: Context, params: WorkerParameters) :
Worker(context, params), KoinComponent {
private val daoRepository: DaoRepository by inject()
private val compositeDisposable = CompositeDisposable()
override fun doWork(): Result {
Timber.d("doWork")
val result = LinkedBlockingQueue<Result>()
val disposable =
daoRepository.getTaskDao().getAllTasks().applySchedulers().subscribe(
{ result.put(Result.SUCCESS) },
{ result.put(Result.FAILURE) }
)
compositeDisposable.add(disposable)
return try {
result.take()
} catch (e: InterruptedException) {
Result.RETRY
}
}
override fun onStopped(cancelled: Boolean) {
Timber.d("onStopped")
compositeDisposable.clear()
}
}
Is WorkManager a good solution for this case?
Is it possible to dispose the Observable correctly?
Yes WorkManager is a good solution(even could be the best one)
You should use RxWorker instead of Worker. here is an example:
To implement it. add androidx.work:work-rxjava2:$work_version to your build.gradle file as dependency.
Extend your class from RxWorker class, then override createWork() function.
class TaskAlarmWorker(context: Context, params: WorkerParameters) :
RxWorker(context, params), KoinComponent {
private val daoRepository: DaoRepository by inject()
override fun createWork(): Single<Result> {
Timber.d("doRxWork")
return daoRepository.getTaskDao().getAllTasks()
.doOnSuccess { /* process result somehow */ }
.map { Result.success() }
.onErrorReturn { Result.failure() }
}
}
Important notes about RxWorker:
The createWork() method is called on the main thread but returned single is subscribed on the background thread.
You don’t need to worry about disposing the Observer since RxWorker will dispose it automatically when the work stops.
Both returning Single with the value Result.failure() and single with an error will cause the worker to enter the failed state.
You can override onStopped function to do more.
Read more :
How to use WorkManager with RxJava
Stackoverflow answer
You can clear it in onStoped() method then compositeDisposable.dispose();
Then call super.onStoped()

Kotlin Coroutine to escape callback hell

I'm trying to use Kotlin's coroutines to avoid callback hell, but it doesnt look like I can in this specific situation, I would like some thougths about it.
I have this SyncService class which calls series of different methods to send data to the server like the following:
SyncService calls Sync Student, which calls Student Repository, which calls DataSource that makes a server request sending the data through Apollo's Graphql Client.
The same pattern follows in each of my features:
SyncService -> Sync Feature -> Feature Repository -> DataSource
So every one of the method that I call has this signature:
fun save(onSuccess: ()-> Unit, onError:()->Unit) {
//To Stuff here
}
The problem is:
When I sync and successfully save the Student on server, I need to sync his enrollment, and if I successfully save the enrollment, I need to sync another object and so on.
It all depends on each other and I need to do it sequentially, that's why I was using callbacks.
But as you can imagine, the code result is not very friendly, and me and my team starting searching for alternatives to keep it better. And we ended up with this extension function:
suspend fun <T> ApolloCall<T>.execute() = suspendCoroutine<Response<T>> { cont ->
enqueue(object: ApolloCall.Callback<T>() {
override fun onResponse(response: Response<T>) {
cont.resume(response)
}
override fun onFailure(e: ApolloException) {
cont.resumeWithException(e)
}
})
}
But the function in DataSource still has a onSuccess() and onError() as callbacks that needs to be passed to whoever call it.
fun saveStudents(
students: List<StudentInput>,
onSuccess: () -> Unit,
onError: (errorMessage: String) -> Unit) {
runBlocking {
try {
val response = GraphQLClient.apolloInstance
.mutate(CreateStudentsMutation
.builder()
.students(students)
.build())
.execute()
if (!response.hasErrors())
onSuccess()
else
onError("Response has errors!")
} catch (e: ApolloException) {
e.printStackTrace()
onError("Server error occurred!")
}
}
}
The SyncService class code changed to be like:
private fun runSync(onComplete: () -> Unit) = async(CommonPool) {
val syncStudentProcess = async(coroutineContext, start = CoroutineStart.LAZY) {
syncStudents()
}
val syncEnrollmentProcess = async(coroutineContext, start = CoroutineStart.LAZY) {
syncEnrollments()
}
syncStudentProcess.await()
syncEnrollmentProcess.await()
onComplete()
}
It does execute it sequentially, but I need a way to stop every other coroutine if any got any errors. Error that might come only from Apollo's
So I've been trying a lot to find a way to simplify this code, but didn't get any good result. I don't even know if this chaining of callbacks can be simplify at all. That's why I came here to see some thoughts on it.
TLDR: I want a way to execute all of my functions sequentially, and still be able to stop all coroutines if any got an exception without a lot o chaining callbacks.

Categories

Resources