twilio / twilio-voice-react-native

Other
75 stars 29 forks source link

Unable to start service com.twiliovoicereactnative.VoiceService #430

Open mudassir-iqbal opened 2 months ago

mudassir-iqbal commented 2 months ago

Issue

In Android users occasionally experience crashes or are unable to make calls. The issue does not affect all users and seems to occur intermittently. W've tried to reproduce this locally but not luck. Mostly users are Samsung user who reported this issue.

Pre-submission Checklist

Description

- Platform: android
Fatal Exception: java.lang.RuntimeException: Unable to start service com.twiliovoicereactnative.VoiceService@158663a with null: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Intent.getAction()' on a null object reference
       at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:5286)
       at android.app.ActivityThread.-$$Nest$mhandleServiceArgs()
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2531)
       at android.os.Handler.dispatchMessage(Handler.java:106)
       at android.os.Looper.loopOnce(Looper.java:230)
       at android.os.Looper.loop(Looper.java:319)
       at android.app.ActivityThread.main(ActivityThread.java:8919)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:578)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1103)

Caused by java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Intent.getAction()' on a null object reference
       at com.twiliovoicereactnative.VoiceService.onStartCommand(VoiceService.java)
       at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:5268)
       at android.app.ActivityThread.-$$Nest$mhandleServiceArgs()
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2531)
       at android.os.Handler.dispatchMessage(Handler.java:106)
       at android.os.Looper.loopOnce(Looper.java:230)
       at android.os.Looper.loop(Looper.java:319)
       at android.app.ActivityThread.main(ActivityThread.java:8919)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:578)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1103)

queued-work-looper-data:
       at jdk.internal.misc.Unsafe.park(Unsafe.java)
       at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341)
       at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506)
       at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3466)
       at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3437)
       at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623)
       at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:435)
       at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1071)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1131)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
       at java.lang.Thread.run(Thread.java:1012)

Reproduction Steps

As I mention we are unable to reproduce this locally, this happen only in release builds and reported by users.

Expected Behavior

The application should allow users to initiate calls without crashing.

Actual Behavior

Some users encounter crashes or are unable to make calls. The crash logs typically include a NullPointerException related to a null Intent in the VoiceService.

Reproduction Frequency

This issue has 19 crash events affecting 5 users in last 7 days. All Samsung devices.

Software and Device Information

Additional Context

Here is my callService.ts


class CallService implements ICallService {
  private voice: Voice | null = null;
  private callStatus: ECallStatus = ECallStatus.Idle;
  private statusSubscribers: Array<(status: ECallStatus) => void> = [];
  private activeCall: Call | null = null;
  private token: string | null = null;

  public async initialize(identity: string): Promise<void> {
    try {
      if (!this.token) {
        this.token = await getAccessToken(identity);
      }
      this.voice = new Voice();
      if (Platform.OS === 'ios') {
        await this.voice.initializePushRegistry();
      }
      await this.voice.register(this.token);
      console.log('Voice SDK initialized and registered');
    } catch (error) {
      console.log('Error initializing Voice SDK:', error);
    }
  }

  public async makeCall({to, from, identity}: IMakeCallParams): Promise<void> {
    try {
      if (!this.voice) {
        console.log('Voice SDK is not initialized');
        return;
      }
      this.setStatus(ECallStatus.Connecting);
      if (!this.token) {
        this.token = await getAccessToken(identity);
      }
      this.activeCall = await this.voice.connect(this.token, {
        params: {
          answerOnBridge: 'true',
          recipientType: 'client',
          to,
          from,
        },
      });

      this.activeCall.on(Call.Event.Connected, (res: any) => {
        this.setStatus(ECallStatus.Connected);
        console.log('Call connected', res);
      });
      this.activeCall.on(Call.Event.ConnectFailure, err => {
        this.setStatus(ECallStatus.ConnectFailure);
        console.log('Call ConnectFailure', err);
      });
      this.activeCall.on(Call.Event.Reconnecting, err => {
        this.setStatus(ECallStatus.Reconnecting);
        console.log('Call Reconnecting', err);
      });
      this.activeCall.on(Call.Event.Reconnected, () => {
        this.setStatus(ECallStatus.Reconnected);
        console.log('Call Reconnected');
      });
      this.activeCall.on(Call.Event.Disconnected, err => {
        this.setStatus(ECallStatus.Disconnected);
        console.log('Call Disconnected', err);
      });
      this.activeCall.on(Call.Event.Ringing, () => {
        this.setStatus(ECallStatus.Ringing);
        console.log('Call Ringing');
      });
      this.activeCall.on(Call.Event.QualityWarningsChanged, err => {
        console.log('Call QualityWarningsChanged', err);
      });
    } catch (error) {
      console.log('Error making call:', error);
    }
  }

  public async endCall(): Promise<boolean> {
    try {
      try {
        await this.activeCall?.disconnect();
      } catch (err) {
        // console.log('Call ended error', err);
      }
      this.setStatus(ECallStatus.Idle);
      this.activeCall = null;
      return true;
    } catch (error) {
      return false;
    }
  }

  public async resetCall() {
    try {
      try {
        await this.activeCall?.disconnect();
      } catch (error) {}
      try {
        await this.activeCall?.mute(false);
      } catch (error) {}

      this.activeCall = null;
      this.setStatus(ECallStatus.Idle);
    } catch (error) {
      // console.log('resetCall', error);
    }
  }

  public async toggleMuteMic(mute: boolean): Promise<boolean> {
    try {
      if (!this.activeCall) {
        console.log('No active call to toggleMuteMic');
        return false;
      }
      await this.activeCall.mute(mute);
      console.log('toggleMuteMic');
      return true;
    } catch (error) {
      console.log(`toggleMuteMic=${mute}`, error);
      return false;
    }
  }

  private setStatus(status: ECallStatus) {
    try {
      this.callStatus = status;
      this.notifyStatusSubscribers();
    } catch (error) {
      console.log('setStatus', error);
    }
  }

  private notifyStatusSubscribers() {
    try {
      this.statusSubscribers.forEach(callback => callback(this.callStatus));
    } catch (error) {
      console.log('notifyStatusSubscribers', error);
    }
  }

  public getStatus(): ECallStatus {
    try {
      return this.callStatus;
    } catch (error) {
      console.log('getStatus', error);
      return ECallStatus.ConnectFailure;
    }
  }

  public subscribeToStatusChanges(
    callback: (status: ECallStatus) => void,
  ): void {
    try {
      this.statusSubscribers.push(callback);
    } catch (error) {
      console.log('subscribeToStatusChanges', error);
    }
  }

  public unsubscribeToStatusChanges(
    callback: (status: ECallStatus) => void,
  ): void {
    try {
      this.statusSubscribers = this.statusSubscribers.filter(
        sub => sub !== callback,
      );
    } catch (error) {
      console.log('unsubscribeToStatusChanges', error);
    }
  }
}```
hivelydev commented 2 months ago

I can confirm this happen to our app as well!

afalls-twilio commented 1 month ago

we are struggling to reproduce the issue, do you have any information on what action are occurring leading up to the crash?

We have a fix for it in the next release but without a means of reproducing the issue we can not verify. Please let us know if it resolves your issues.

mudassir-iqbal commented 1 month ago

We can’t reproduce this issue on our end, but it’s affecting our customers, and crashes are being reported in Firebase Crashlytics.

mhuynh5757 commented 1 month ago

Hey @mudassir-iqbal the fix is merged to main and we plan to cut a release this week. Thanks for your patience, we'll let you know when the next version is available. However, @afalls-twilio is correct in that we are unable to reproduce the issue. Please keep informed if you still encounter the issue. Thanks for helping us make the SDK better for everyone!

mudassir-iqbal commented 1 month ago

Thanks for the update! I appreciate the hard work and will keep an eye out for any further issues.