voice-quickstart-android: [Resolved] `Parcel.readParcelable` returns null when reading a parcelled `CallInvite` (from FCM)

From an adapted quickstart example, I added the following snippet (see // SNIPPET tags below).

You will notice the CallInvite read from the Parcel is null, this serves to emulate the functionality provided by TelecomManager.addNewIncomingCall(PhoneAccountHandle, Bundle) by providing the CallInvite as an extra in the bundle.

Code snippet with Parcel

public class VoiceFirebaseMessagingService extends FirebaseMessagingService {

    @Override
    public void onMessageReceived(final RemoteMessage remoteMessage) {
        // ... valid message
        handleInvite(callInvite, notificationId);
        //...
    }

    private void handleInvite(CallInvite callInvite, int notificationId) {
        // SNIPPET START
        Parcel p = Parcel.obtain();
        p.writeParcelable(callInvite, 0);
        ClassLoader classLoader = getApplicationContext().getClassLoader();
        CallInvite ci = p.readParcelable(classLoader); // <----------------- ci is always null
        if(ci != null){
            Log.d(TAG, "handleInvite: " + ci.getCallSid());
        } else {
            Log.d(TAG, "handleInvite: null");
        }
        // SNIPPET END
    }

}

The same occurs on with Kotlin, though I get a ClassNotFoundException for CallInvite.

So far, I’ve traced the issue to Parcel.readParcelableCreator(@Nullable ClassLoader) (for Android 11, API 30)

public final Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader loader) {
        String name = readString();
        if (name == null) {
            return null;
        }
        Parcelable.Creator<?> creator;

name is always null, thus returns null when calling Parcel.readParcelable.

This occurs on both Android 11 & 12 (API 30, 31)

Tested on:

  • Android 11 (Samsung A21s)

  • Android 12 (Samsung Note 10+)

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 15 (2 by maintainers)

Most upvoted comments

On a last note, the original issue well-documented here had me simplifying the issue (in Java) on this Github issue. However, since I resolved setDataPosition issue for reading the Parcelable CallInvite, the overarching issue has not yet been resolved. That is, when passing a CallInvite into a Bundle and attempting to read it in the ConnectionService, I get an exception;

Class not found when unmarshalling: com.twilio.voice.CallInvite
java.lang.ClassNotFoundException: com.twilio.voice.CallInvite
    at java.lang.Class.classForName(Native Method)
    at java.lang.Class.forName(Class.java:454)
    at android.os.Parcel.readParcelableCreator(Parcel.java:3403)
    //...

Some of these functions may look familiar going by my study of Parcel.java in the above issue

Original Problem

My Kotlin code (testing out the ConnectionService) works with:

private fun handleFCMCallInvite(callInvite: CallInvite, notificationId: Int) {
    // Get telecom manager
    val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager

    // Get PhoneAccountHandle
    val caller = callInvite.from!!.toString()
    val componentName = ComponentName(applicationContext.packageName, TwilioVoiceConnectionService::class.java.name)
    val phoneAccountHandle = PhoneAccountHandle(componentName, caller)

    // Create my Bundle containing information e.g. notificationId and callInvite
    val myBundle = Bundle()
    myBundle.putInt(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId)
    myBundle.putParcelable(Constants.INCOMING_CALL_INVITE, callInvite)

    // Add new incoming call to the telecom manager
    telecomManager.addNewIncomingCall(phoneAccountHandle, Bundle().apply {
        putBundle(EXTRA_INCOMING_CALL_EXTRAS, myBundle)
        putParcelable(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
    })
}

Followed by reading the extras from the Bundle in the TwilioVoiceConnectionService.kt as follows:

override fun onCreateIncomingConnection(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?): Connection {
    super.onCreateIncomingConnection(connectionManagerPhoneAccount, request)
    Log.d(TAG, "onCreateIncomingConnection")
    val connection: Connection = VoipConnection(applicationContext)
    connection.extras = request?.extras

    var ci: CallInvite? = null
    val myBundle: Bundle? = connection.extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
    if(myBundle != null) {
        Log.d(TAG, "onCreateIncomingConnection: myBundle is not null")

        /// --------------------------------------------------------
        /// This next line throws the ClassNotFoundException occurs
        /// --------------------------------------------------------
        if (myBundle.containsKey(Constants.INCOMING_CALL_INVITE) ) { 
            Log.d(TAG, "onCreateIncomingConnection: myBundle contains INCOMING_CALL_INVITE")
            ci = myBundle.getParcelable(Constants.INCOMING_CALL_INVITE)
        } else {
            Log.d(TAG, "onCreateIncomingConnection: myBundle does not contain INCOMING_CALL_INVITE")
        }
    } else {
        Log.d(TAG, "onCreateIncomingConnection: myBundle is null")
    }

    ...
}

In a manner of speaking @afalls-twilio, you were correct in saying the ClassLoader of the current thread doesn’t seem to be setup correctly. This was confirmed and discussed in some detail by David Wasser at SO.

TL;DR Solution

A simple line addition when reading the bundle adds the class to be found i.e.

val myBundle: Bundle? = connection.extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
myBundle.classLoader = CallInvite::class.java.classLoader
// read CallInvite from Bundle as required

I suspect, this is a significant issue with Samsung devices by browsing through previous github repo issues including the SO thread mentioned earlier.