ClassNotFoundException com.twilio.voice.CallInvite when reading ConnectionService Bundle extras in onCreateIncomingConnection

I'm upgrading a Flutter library to use a Twilio Voice integrated with a ConnectionService for native call management. To accomplish this, one has to extend a ConnectionService (inherits from a Service with associated communication methods, i.e. Intents)


Background & Context

To receive incoming call (via a notification manager like FCM), one needs to notify the ConnectionService of an incoming call using

override fun onCreateIncomingConnection(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?): Connection {
    super.onCreateIncomingConnection(connectionManagerPhoneAccount, request)
    Log.d(TAG, "onCreateIncomingConnection")'
    // ... code goes here
    // return new Connection(...)
}

With this onCreateIncomingConnection, it will ask the native 'Phone Call' app to show an incoming call with parameters you provide, e.g. showing the incoming caller number with Connection.setAddress

This additional information can be passed into the ConnectionService by adding them to an extras Bundle with the Key TelecomManager.EXTRA_INCOMING_CALL_EXTRAS (for incoming calls) which is then added to a new Bundle e.g.

Source

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)
    })
}

Problem

The issue comes in when trying to unparcel() the CallInvite from Bundled extras in the ConnectionService's onCreateIncomingConnection implementation, see 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")
    }

    ...
}

As shown in the snippet above, when calling myBundle.containsKey() with a valid key any time I have a CallInvite object bundled in. If I only pass in e.g. notificationId, contains and accessors work as intended.

When including the CallInvite in the bundle, I get the following exception:

*Note: CallInvite implements Parcelable

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)
    at android.os.Parcel.readParcelable(Parcel.java:3337)
    at android.os.Parcel.readValue(Parcel.java:3239)
    at android.os.Parcel.readArrayMapInternal(Parcel.java:3636)
    at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292)
    at android.os.BaseBundle.unparcel(BaseBundle.java:236)
    at android.os.BaseBundle.containsKey(BaseBundle.java:516)
    at com.twilio.twilio_voice.connectionservice.TwilioVoiceConnectionService.onCreateIncomingConnection(TwilioVoiceConnectionService.kt:43)
    at android.telecom.ConnectionService.createConnection(ConnectionService.java:2061)
    at android.telecom.ConnectionService.access$400(ConnectionService.java:96)
    at android.telecom.ConnectionService$2$1.loggedRun(ConnectionService.java:914)
    at android.telecom.Logging.Runnable$1.run(Runnable.java:37)
    at android.telecom.ConnectionService.onAccountsInitialized(ConnectionService.java:3272)
    at android.telecom.ConnectionService.access$5000(ConnectionService.java:96)
    at android.telecom.ConnectionService$5$1.loggedRun(ConnectionService.java:2577)
    at android.telecom.Logging.Runnable$1.run(Runnable.java:37)
    at android.os.Handler.handleCallback(Handler.java:938)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loopOnce(Looper.java:226)
    at android.os.Looper.loop(Looper.java:313)
    at android.app.ActivityThread.main(ActivityThread.java:8663)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135)
Caused by: java.lang.ClassNotFoundException: com.twilio.voice.CallInvite
    at java.lang.Class.classForName(Native Method)
    at java.lang.BootClassLoader.findClass(ClassLoader.java:1358)
    at java.lang.BootClassLoader.loadClass(ClassLoader.java:1418)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
    at java.lang.Class.classForName(Native Method) 
    at java.lang.Class.forName(Class.java:454) 
    at android.os.Parcel.readParcelableCreator(Parcel.java:3403) 
    at android.os.Parcel.readParcelable(Parcel.java:3337) 
    at android.os.Parcel.readValue(Parcel.java:3239) 
    at android.os.Parcel.readArrayMapInternal(Parcel.java:3636) 
    at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292) 
    at android.os.BaseBundle.unparcel(BaseBundle.java:236) 
    at android.os.BaseBundle.containsKey(BaseBundle.java:516) 
    at com.twilio.twilio_voice.connectionservice.TwilioVoiceConnectionService.onCreateIncomingConnection(TwilioVoiceConnectionService.kt:43) 
    at android.telecom.ConnectionService.createConnection(ConnectionService.java:2061) 
    at android.telecom.ConnectionService.access$400(ConnectionService.java:96) 
    at android.telecom.ConnectionService$2$1.loggedRun(ConnectionService.java:914) 
    at android.telecom.Logging.Runnable$1.run(Runnable.java:37) 
    at android.telecom.ConnectionService.onAccountsInitialized(ConnectionService.java:3272) 
    at android.telecom.ConnectionService.access$5000(ConnectionService.java:96) 
    at android.telecom.ConnectionService$5$1.loggedRun(ConnectionService.java:2577) 
    at android.telecom.Logging.Runnable$1.run(Runnable.java:37) 
    at android.os.Handler.handleCallback(Handler.java:938) 
    at android.os.Handler.dispatchMessage(Handler.java:99) 
    at android.os.Looper.loopOnce(Looper.java:226) 
    at android.os.Looper.loop(Looper.java:313) 
    at android.app.ActivityThread.main(ActivityThread.java:8663) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135) 
Caused by: java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available

Libraries, etc

build.gradle (module)

buildscript {
    ext.kotlin_version = '1.8.0'

    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:7.4.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
...
android {
    compileSdk 33
    ...
}
...
dependencies {
    ...
    implementation("com.twilio:voice-android:6.2.1")
    implementation("com.google.firebase:firebase-messaging-ktx:23.2.1")
    ...
}

UPDATE 1

Using the last working solution (Java), I resolved to parcelling and unparcelling immediately to emulate what's happening in the background, seems there might be a problem here.

public class VoiceFirebaseMessagingService extends FirebaseMessagingService {

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

    private void handleInvite(CallInvite callInvite, int notificationId) {
        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");
        }
    }

}

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)



Comments

Popular posts from this blog

Today Walkin 14th-Sept

Spring Elasticsearch Operations

Hibernate Search - Elasticsearch with JSON manipulation