0

I have a class that handles Bluetooth communication and this class needs to forward data that it received from Bluetooth to my activity class. I used to pass this data via Broadcasts but now I am trying to pass the data via an interface. The problem I am having is that upon receiving the data in my activity, I can`t make changes in the UI and I am getting an error that says "Only the original thread that created a view hierarchy can touch its views."

//My Activity class implements an interface and receiveDataFromBluetoothConnection is a method in that interface
//My bluetooth service sends data to my activity via this method.

public void receiveDataFromBluetoothConnection(String data) {
    processIncomingBtMessage(data);
}

private void processIncomingBtMessage(String incomingMessage) {

    //...
    //...
    //...
    if (message == BtMessageIn.BT_MESSAGE_IN_SYSTEM_OFF) {
        Log.d(TAG, "remoteControlBubblePillar: bReceiver: Setting Button to On");
        arduinoPowerStatus = false;
        LightsButtonsBackgroundUnpressed();
        btnOnOff.setBackgroundResource(R.drawable.button_shape_round_corners_gradient_green);
        btnOnOff.setText(R.string.On);
    }
    //...
    //...
    //...
}

Here is the full error message that I get

 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7913)
    at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1373)
    at android.view.ViewGroup.invalidateChild(ViewGroup.java:5449)
    at android.view.View.invalidateInternal(View.java:14825)
    at android.view.View.invalidate(View.java:14761)
    at android.view.View.invalidateDrawable(View.java:19051)
    at android.widget.TextView.invalidateDrawable(TextView.java:6353)
    at android.graphics.drawable.Drawable.invalidateSelf(Drawable.java:436)
    at android.graphics.drawable.Drawable.setVisible(Drawable.java:820)
    at android.view.View.setBackgroundDrawable(View.java:19522)
    at android.support.v7.widget.AppCompatButton.setBackgroundDrawable(AppCompatButton.java:86)
    at android.view.View.setBackground(View.java:19498)
    at android.view.View.setBackgroundResource(View.java:19481)
    at android.support.v7.widget.AppCompatButton.setBackgroundResource(AppCompatButton.java:78)
    at com.bubblewall.saik.bubblewall.remoteControlBubblePillar.processIncomingBtMessage(remoteControlBubblePillar.java:354)
    at com.bubblewall.saik.bubblewall.remoteControlBubblePillar.receiveDataFromBluetoothConnection(remoteControlBubblePillar.java:275)
    at com.bubblewall.saik.bubblewall.BluetoothConnection$1.onCharacteristicChanged(BluetoothConnection.java:97)
    at android.bluetooth.BluetoothGatt$1.onNotify(BluetoothGatt.java:400)
    at android.bluetooth.IBluetoothGattCallback$Stub.onTransact(IBluetoothGattCallback.java:177)
    at android.os.Binder.execTransact(Binder.java:573)

Any ideas how can I fix this issue? Or should I just switch back to Broadcasts?

Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
Saik
  • 993
  • 1
  • 16
  • 40

1 Answers1

2

The problem is that your Bluetooth class doesn't run on the main/UI thread, as the activity does. In order to avoid race conditions, Android crashes the app to let you know that you're doing something wrong.

Fortunately, there is a simple way to fix this by using runOnUiThread(Runnable):

public void receiveDataFromBluetoothConnection(String data) {
    runOnUiThread(() -> processIncomingBtMessage(data));
}

But I have a better suggestion. Instead of letting the activity handle this, it would be better to design your other class such that it makes sure that when it notifies your activity it does so on the proper thread.

new Handler(Looper.getMainLooper()).post(() -> listener.receiveDataFromBluetoothConnection(data))

It's better to do it this way because in case other activities use the Bluetooth class they don't have to worry about making sure they execute code on the proper thread, the Bluetooth class already handles that.

For a more in-depth explanation on exactly what's happening and what the proper way to handle it is you can read Communicating with the UI Thread, on developer.android.com.

Paul Manta
  • 30,618
  • 31
  • 128
  • 208
  • Paul great answer. I had one problem though. I think Android still does not support lambda functions because when I tried to use the 2nd method you suggested, it gave me an error saying lambdas are not supported at this language level. I overcame this problem by doing the following. final Runnable r = new Runnable() { public void run() { bluetoothConnectionInterface.receiveDataFromBluetoothConnection(incomingMessage); } }; new Handler(Looper.getMainLooper()).post(r); – Saik Oct 19 '17 at 19:43