Why am I getting ConcurrentModificationException when call Wearable.getMessageClient

Viewed 112

This looks like a concurrency bug inside Google Wearable API, but let me know if I'm wrong and suggest a way around. The purpose of this code is to get a Wearable message client and send a message to a WearOS device.

I need to do it asynchronously to avoid locking the main thread. It looks like Google's implementation of getMessageClient is not thread safe. Should I lock on some object to avoid the crashes? What would be the best approach here? Why Google couldn't add a callback in a thread safe manner?

IMO, all they needed to do was to lock the SimpleArrayMap object before "put" is called and release after "put" is completed.

 fun sendToWear(msg:String) {

        lifecycle.coroutineScope.launch {
            withContext(Dispatchers.IO) {
                val nodeListTask = Wearable.getNodeClient(applicationContext).getConnectedNodes()
                try {

                    val nodes = Tasks.await<List<Node>>(nodeListTask)

                    Tasks.await<List<Node>>(nodeListTask, 10, TimeUnit.SECONDS)

                    for (node in nodes) {

                        //Send the message///

                        val sendMessageTask = Wearable.getMessageClient(this@RootActivity).sendMessage(node.getId(),
                                BROADCAST_PATH, msg.toByteArray())

Stack trace:

java.util.ConcurrentModificationException: 
  at androidx.collection.SimpleArrayMap.put (SimpleArrayMap.java:482)
  at com.google.android.gms.common.api.internal.zzc.addCallback (zzc.java:20)
  at com.google.android.gms.common.api.internal.zaae.<init> (zaae.java:14)
  at com.google.android.gms.common.api.internal.zaae.zaa (zaae.java:5)
  at com.google.android.gms.common.api.GoogleApi.<init> (GoogleApi.java:31)
  at com.google.android.gms.wearable.MessageClient.<init> (MessageClient.java:3)
  at com.google.android.gms.wearable.internal.zzez.<init> (zzez.java:4)
  at com.google.android.gms.wearable.Wearable.getMessageClient (Wearable.java:11)
  at info.gryb.gac.mobile.fragments.RootActivity$sendToWear$1$1.invokeSuspend (RootActivity.kt:1048)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
  at kotlinx.coroutines.DispatchedTask.run (Dispatched.kt:241)
  at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely (CoroutineScheduler.kt:594)
  at kotlinx.coroutines.scheduling.CoroutineScheduler.access$createdWorkers (CoroutineScheduler.kt:60)
  at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely (CoroutineScheduler.kt:60)
  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run (CoroutineScheduler.kt:740)

It's crashing while trying to add a callback by calling "put" in Google API's code below:

public final void addCallback(String var1, @NonNull LifecycleCallback var2) {
    if (!this.zzbe.containsKey(var1)) {
        this.zzbe.put(var1, var2); <-- Crashes in this put
        if (this.zzbf > 0) {
            (new zze(Looper.getMainLooper())).post(new zzd(this, var2, var1));
        }

    } else {
        throw new IllegalArgumentException((new StringBuilder(59 + String.valueOf(var1).length())).append("LifecycleCallback with tag ").append(var1).append(" already added to this fragment.").toString());
    }
}
1 Answers

It's definitely worth raising as a bug, either for a concurrency fix, or alternatively clearer documentation.

But the MessageClient apis are all async anyway, they return Task that indicate the result when available, so it doesn't seem like you should need to run these on the IO Dispatcher.

You should also be able to use org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.5.2 to have kotlin friendly await methods.

val nodeList = Wearable.getNodeClient(applicationContext).getConnectedNodes().await()

So

Related