DawidPi DawidPi - 24 days ago 10
Java Question

Android BluetoothGattService - exception cannot marshall

I have got following problem.

My application architecture more or less consits of 3 main parts:


  • Devices view (shows available BLE devices)

  • BLE service (handles BLE connections and data)

  • Services view (shows services for particular device)



My issue is with passing BluetoothGATTService to the intent second time (first time it works).

More or less actions are like this:


  • choose device, for which you want to show services

  • send intent with device and action to perform to the BLEService

  • BLEService performs service discovery and sends intent with services back

  • BroadcastReceiver receives intent properly and wats to start new activity using data in received intent.



And on the last step there is a problem as I cannot put ArrayList to the intent/bundle as it causes exception:


E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.projects.dawid.gattclient, PID: 4133
java.lang.RuntimeException: Parcel: unable to marshal value android.bluetooth.BluetoothGattService@ef62956
at android.os.Parcel.writeValue(Parcel.java:1337)
at android.os.Parcel.writeList(Parcel.java:711)
at android.os.Parcel.writeValue(Parcel.java:1284)
at android.os.Parcel.writeArrayMapInternal(Parcel.java:638)
at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1313)
at android.os.Bundle.writeToParcel(Bundle.java:1096)
at android.os.Parcel.writeBundle(Parcel.java:663)
at android.content.Intent.writeToParcel(Intent.java:7838)
at android.app.ActivityManagerProxy.startActivity(ActivityManagerNative.java:2530)


for me it's very very strange, as if the operation is unsupported, then we would get this exception on sending ArrayList of services from BLEService to the BroadcastReceiver, not now.

Source code:

BLEService to Broadcast receiver code



private void notifyServicesDiscovered(BluetoothGatt gatt) {
Intent intent = new Intent();
intent.setAction(BLEService.RESPONSE);
intent.putExtra(BLEService.RESPONSE, BLEService.Responses.SERVICES_DISCOVERED);
intent.putExtra(BLEService.Responses.DEVICE, gatt.getDevice());
intent.putParcelableArrayListExtra(BLEService.Responses.SERVICES_LIST, getArrayListServices(gatt));
LocalBroadcastManager.getInstance(mServiceContext).sendBroadcast(intent);
}

@NonNull
private ArrayList<BluetoothGattService> getArrayListServices(BluetoothGatt gatt) {
ArrayList<BluetoothGattService> services = new ArrayList<>();
services.addAll(gatt.getServices());
return services;
}


This works fine. Now

Broadcast receiver to new activity code



After recognizing the proper intent to handle this method is invoked

private void createViewWithServices(Intent intent) {
Log.i(TAG, "creating new activity!");

BluetoothDevice device = intent.getParcelableExtra(BLEService.Responses.DEVICE);
ArrayList<BluetoothGattService> services = intent.getParcelableArrayListExtra(BLEService.Responses.SERVICES_LIST);

Intent serviceShowIntent = new Intent(mActivityContext, ServiceShowActivity.class);
serviceShowIntent.putExtra(BLEService.Responses.DEVICE, device);
serviceShowIntent.putParcelableArrayListExtra(BLEService.Responses.SERVICES_LIST, services); //this line causes error
mActivityContext.startActivity(serviceShowIntent); // here exception is thrown
}


Can anyone explain me the mistery lying behind this? I just cannot understand why first code is just fine, while second fails with exception.

Already tried doing things in lots of different ways, but all of them failed. I was even exchanging bundles between intents as their contents are same, but this also failed, copying list items makes no difference.

EDIT

refering to the AndroidRuntime error: Parcel: unable to marshal value error. Difference is, that I am using objects of classes provided by android itself, that is BluetoothGATTService, which do implement parcelable interface refering to the https://developer.android.com/reference/android/bluetooth/BluetoothGattService.html

Answer

There are two issues in play here: one causes everything to work fine when local broadcasts are used, another one makes serialization of BluetoothGattService fail.


In order to pass data between two processes, it must be serialized (written to Parcel). Bundle contents are serialized/deserialized lazily — as long as Bundle object does not cross process boundaries or get stored on disk, all contained objects will be stored in memory (within a HashMap) and won't get serialized.

Local broadcasts never cross process boundaries, thus do not serialize/deserialize Intent extras. Inter-process communications (global broadcasts, asking ActivityManager to start an Activity) do.

In order to be passed between processes, Intent extras must be either Parcelable or Serializable, otherwise Android may treat them as opaque objects under some circumstances and will attempt to determine approach to their serialization (as Serializable/Parcelable/String etc.) at runtime (see writeValue) — and fail.


As for BluetoothGattService — it clearly didn't implement Parcelable after initial public release and was retroactively changed to implement Parcelable in API 24. This means, that there are devices in the wild, where that class does not implement Parcelable (even if it does in the source code of the latest Android version). Adding a parent to inheritance chain is not technically a breaking change as far as binary compatibility is concerned — Java Classloaders won't complain. But any code, that relies on parcelability of such class, will either fail during bytecode validation or cause ClassCastException/ArrayStoreException on older versions.

Java generics are not reified — when compiled to binary code, ArrayList<String> and ArrayList<Parcelable> look to ClassLoader the same: as ArrayList<?>. You are not casting BluetoothGattService to Parcelable and neither is ArrayList (internally it stores it's contents in Object[]). In order to serialize ArrayList, HashMap and similar classes, Android have to use reflection, and that fails in your case, because the runtime type of BluetoothGattService does not implement Parcelable on the device. Just add a code, that explicitly makes a cast (such as placing BluetoothGattService in Parcelable[]) and you will see.

Also, even on devices, where it does happen to implement Parcelable prior to API 24, trying to serialize a BluetoothGattService is still bad idea (that wasn't officially public API, wasn't covered by CTS, and thus is completely untested).

I recommend you to refrain from passing BluetoothGattService via parcelization and find other way to approach your task (such as passing it's contents or storing an instance in singlton).