twilio / twilio-voice-react-native

Other
71 stars 26 forks source link

Android Error: class file for com.twilio.voice.MessageListener not found (beta-4) #332

Closed periabyte closed 6 months ago

periabyte commented 6 months ago

Issue

Pre-submission Checklist

Description

My current setup was working with .beta-2. I created a Notification Service to handle expo notifications and Twilio notifications as I did in #160

I disabled the Custom Notification Service.

I made the changes that were stated in the .beta-4 migration guide. Mixed it with the default Expo Kotlin files.

Incoming calls are working properly with this setup.

But other notifications are not coming through

I re-enabled the Custom Notification Service. The app won't compile now.

Here are the files that I modified for Expo and Twilio React Native

// com.rnexpotwilio/app/MainActivity.kt

package com.rnexpotwilio.app

import android.os.Build
import android.os.Bundle
import android.content.Intent
import android.widget.Toast
import android.Manifest

import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate

import com.twiliovoicereactnative.VoiceActivityProxy

import expo.modules.ReactActivityDelegateWrapper

class MainActivity : ReactActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    // Set the theme to AppTheme BEFORE onCreate to support
    // coloring the background, status bar, and navigation bar.
    // This is required for expo-splash-screen.
    setTheme(R.style.AppTheme);
    super.onCreate(null)
    activityProxy.onCreate(savedInstanceState)
  }

  override fun onDestroy() {
    super.onDestroy()
    activityProxy.onDestroy()
  }

  override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    activityProxy.onNewIntent(intent)
  }

  /**
   * Returns the name of the main component registered from JavaScript. This is used to schedule
   * rendering of the component.
   */
  override fun getMainComponentName(): String = "main"

  /**
   * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
   * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
   */
  override fun createReactActivityDelegate(): ReactActivityDelegate {
    return ReactActivityDelegateWrapper(
          this,
          BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
          object : DefaultReactActivityDelegate(
              this,
              mainComponentName,
              fabricEnabled
          ){})
  }

  /**
    * Align the back button behavior with Android S
    * where moving root activities to background instead of finishing activities.
    * @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
    */
  override fun invokeDefaultOnBackPressed() {
      if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
          if (!moveTaskToBack(false)) {
              // For non-root activities, use the default implementation to finish them.
              super.invokeDefaultOnBackPressed()
          }
          return
      }

      // Use the default back button implementation on Android S
      // because it's doing more than [Activity.moveTaskToBack] in fact.
      super.invokeDefaultOnBackPressed()
  }

  private val activityProxy = VoiceActivityProxy(
    this
  ) { permission ->
      when {
          Manifest.permission.RECORD_AUDIO == permission -> {
              Toast.makeText(
                      this@MainActivity,
                      "Microphone permissions needed. Please allow in your application settings.",
                      Toast.LENGTH_LONG
              ).show()
          }

          Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Manifest.permission.BLUETOOTH_CONNECT == permission -> {
              Toast.makeText(
                      this@MainActivity,
                      "Bluetooth permissions needed. Please allow in your application settings.",
                      Toast.LENGTH_LONG
              ).show()
          }

          Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2 && Manifest.permission.POST_NOTIFICATIONS == permission -> {
              Toast.makeText(
                      this@MainActivity,
                      "Notification permissions needed. Please allow in your application settings.",
                      Toast.LENGTH_LONG
              ).show()
          }
      }
  }
}

// com/rnexpotwilio/app/MainApplication.kt
package com.rnexpotwilio.app

import android.app.Application
import android.content.res.Configuration
import androidx.annotation.NonNull

import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost
import com.facebook.react.config.ReactFeatureFlags
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader
import android.content.Context
import com.facebook.react.ReactInstanceManager
import java.lang.NoSuchMethodException
import java.lang.reflect.InvocationTargetException
import com.twiliovoicereactnative.VoiceApplicationProxy
import com.rnexpotwilio.app.newarchitecture.MainApplicationReactNativeHost
import com.rnexpotwilio.app.MainReactNativeHost
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper

public class MainApplication : Application(), ReactApplication {
  private val mNewArchitectureNativeHost = MainApplicationReactNativeHost(this)
  private val mReactNativeHost: MainReactNativeHost = MainReactNativeHost(this)
  private val voiceApplicationProxy: VoiceApplicationProxy

  init {
    voiceApplicationProxy = VoiceApplicationProxy(getReactNativeHost())
  }

  fun getReactNativeHost(): VoiceApplicationProxy.VoiceReactNativeHost {
    return if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
      mNewArchitectureNativeHost
    } else {
      mReactNativeHost
    }
  }

  override fun onTerminate() {
    // Note: this method is not called when running on device, devices just kill the process.
    voiceApplicationProxy.onTerminate()
    super.onTerminate()
  }

  /**
   * Loads Flipper in React Native templates. Call this in the onCreate method with something like
   * initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
   *
   * @param context
   * @param reactInstanceManager
   */
  private fun initializeFlipper(
    context: Context, reactInstanceManager: ReactInstanceManager
  ) {

    if (BuildConfig.DEBUG) {
      try {
        /*
         We use reflection here to pick up the class that initializes Flipper,
        since Flipper library is not available in release mode
        */
        val aClass = Class.forName("com.rnexpotwilio.app.ReactNativeFlipper")
        aClass
                .getMethod("initializeFlipper", Context::class.java, ReactInstanceManager::class.java)
                .invoke(null, context, reactInstanceManager)
      } catch (e: Exception) {
        e.printStackTrace()
      }
    }
  }
  override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
        this,
        object : DefaultReactNativeHost(this) {
          override fun getPackages(): List<ReactPackage> {
            // Packages that cannot be autolinked yet can be added manually here, for example:
            // packages.add(new MyReactNativePackage());
            return PackageList(this).packages
          }

          override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"

          override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

          override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
          override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
      }
  )

  override val reactHost: ReactHost
    get() = getDefaultReactHost(this.applicationContext, reactNativeHost)

  override fun onCreate() {
    super.onCreate()
    voiceApplicationProxy.onCreate()
    SoLoader.init(this, false)
    if (!BuildConfig.REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS) {
      ReactFeatureFlags.unstable_useRuntimeSchedulerAlways = false
    }
    if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
      // If you opted-in for the New Architecture, we load the native entry point for this app.
      load()
    }
    if (BuildConfig.DEBUG) {
      ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
    }
    ApplicationLifecycleDispatcher.onApplicationCreate(this)
  }

  override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
  }
}
// com/rnexpotwilio/app/MainReactNativeHost.kt

package com.rnexpotwilio.app

import android.app.Application
// import build config
import com.rnexpotwilio.app.BuildConfig
import com.facebook.react.PackageList
import com.facebook.react.ReactPackage
import com.twiliovoicereactnative.VoiceApplicationProxy

class MainReactNativeHost(application: Application) : VoiceApplicationProxy.VoiceReactNativeHost(application) {

    override fun getUseDeveloperSupport(): Boolean {
        return BuildConfig.DEBUG
    }

    override fun getPackages(): List<ReactPackage> {
        val packages = PackageList(this).packages
        // Packages that cannot be autolinked yet can be added manually here
        // packages.add(MyReactNativePackage())
        return packages
    }

    override fun getJSMainModuleName(): String {
        return "index"
    }
}
package com.rnexpotwilio.app;

import com.google.firebase.messaging.FirebaseMessagingService;
import expo.modules.notifications.service.ExpoFirebaseMessagingService;
import expo.modules.notifications.service.interfaces.FirebaseMessagingDelegate;
import com.twiliovoicereactnative.VoiceFirebaseMessagingService;
// import com.rnexpotwilio.app.TwilioConstants;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.PowerManager;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.google.firebase.messaging.RemoteMessage;

// import java.util.ArrayList;
// import java.util.List;
// import System
import android.util.Log;
import java.util.*;
import com.google.firebase.messaging.RemoteMessage;

public class RnExpoTwilioNotificationService extends VoiceFirebaseMessagingService {
    private FirebaseMessagingService expoFirebaseService = new ExpoFirebaseMessagingService();
    protected FirebaseMessagingDelegate firebaseMessagingDelegate = new expo.modules.notifications.service.delegates.FirebaseMessagingDelegate(this);

    public static final String TAG = "RETNotificationService";

    public RnExpoTwilioNotificationService() {
        super();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
    }

  @Override
  public void onNewToken(String token) {
    Log.d(TAG, "Refreshed FCM token: " + token);
  }

  /**
   * Called when message is received.
   *
   * @param remoteMessage Object representing the message received from Firebase Cloud Messaging.
   */
  @Override
  public void onMessageReceived(RemoteMessage remoteMessage) {
    if (!remoteMessage.getData().containsKey("twi_message_type")) {
        Log.d(TAG, "Expo Sending Message");
        firebaseMessagingDelegate.onMessageReceived(remoteMessage);
        Log.d(TAG, "Expo Sent Message");
        return;
    }

    Log.d(TAG, "onMessageReceived");
    Log.d(TAG, remoteMessage.getData().toString());
    Log.d(TAG, "contains twilio data:");

    super.onMessageReceived(remoteMessage);
  }
}

Expected Behavior

The app should compile properly

Actual Behavior

NotificationService.java:28: error: cannot access MessageListener
public class RnExpoTwilioNotificationService extends VoiceFirebaseMessagingService {
       ^
  class file for com.twilio.voice.MessageListener not found

Reproduction Frequency

100%

afalls-twilio commented 6 months ago

@periabyte 2 things.. 1) the error you are having is because your build is not associating com.twilio.voice as a dependency to your custom firebase module. com.twilio.voice.MessageListener is part of the Twilio Voice SDK that the RN sdk has as a dependency.

2) We internally ran some tests and discovered that you don't need to extend our VoiceFirebaseMessagingService, infact, you can have two FirebaseMessaging services, the SDK internal one (VoiceFirebaseMessagingService) and your custom one and if they use the same token, they will BOTH receive the same push messages.

periabyte commented 6 months ago

This helped me

the error you are having is because your build is not associating com.twilio.voice as a dependency to your custom firebase module. com.twilio.voice.MessageListener is part of the Twilio Voice SDK that the RN sdk has as a dependency.

I manually added the Twilio Voice SDK in my android/app/build.gradle file

dependencies {
    // The version of react-native is set by the React Native Gradle Plugin
    implementation("com.facebook.react:react-android")
    implementation("com.twilio:voice-android:6.3.3")

and then updated the final

package com.rnexpotwilio.app;

import com.google.firebase.messaging.FirebaseMessagingService;
import expo.modules.notifications.service.ExpoFirebaseMessagingService;
import expo.modules.notifications.service.interfaces.FirebaseMessagingDelegate;
import com.twiliovoicereactnative.VoiceFirebaseMessagingService;
// import com.rnexpotwilio.app.TwilioConstants;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.PowerManager;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.google.firebase.messaging.RemoteMessage;

// import java.util.ArrayList;
// import java.util.List;
// import System
import android.util.Log;
import java.util.*;
import com.google.firebase.messaging.RemoteMessage;

public class RnExpoTwilioNotificationService extends VoiceFirebaseMessagingService {
    private FirebaseMessagingService expoFirebaseService = new ExpoFirebaseMessagingService();
    protected open val firebaseMessagingDelegate: FirebaseMessagingDelegate by lazy {
        expo.modules.notifications.service.delegates.FirebaseMessagingDelegate(this)
    }

    public static final String TAG = "RETNotificationService";

    public RnExpoTwilioNotificationService() {
        super();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
    }

  @Override
  public void onNewToken(String token) {
    Log.d(TAG, "Refreshed FCM token: " + token);
  }

  /**
   * Called when message is received.
   *
   * @param remoteMessage Object representing the message received from Firebase Cloud Messaging.
   */
  @Override
  public void onMessageReceived(RemoteMessage remoteMessage) {
    if (!remoteMessage.getData().containsKey("twi_message_type")) {
        Log.d(TAG, "Expo Sending Message");
        firebaseMessagingDelegate.onMessageReceived(remoteMessage);
        Log.d(TAG, "Expo Sent Message");
        return;
    }

    Log.d(TAG, "onMessageReceived");
    Log.d(TAG, remoteMessage.getData().toString());
    Log.d(TAG, "contains twilio data:");

    super.onMessageReceived(remoteMessage);
  }
}

We internally ran some tests and discovered that you don't need to extend our VoiceFirebaseMessagingService, infact, you can have two FirebaseMessaging services, the SDK internal one (VoiceFirebaseMessagingService) and your custom one and if they use the same token, they will BOTH receive the same push messages.

Yes, they will both receive the same push message internally, but somehow when adding the VoiceFirebaseMessagingService it somehow blocks the other service (in this case the ExpoFirebaseMessagingService) from handling the push message. That's why I needed to create a custom service to send the push message to the proper service to handle that push message. I'm sure there's a better way to implement what I currently have, but for now it works.