Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented self managed mode support for Android #395

Merged

Conversation

jonastelzio
Copy link
Contributor

@jonastelzio jonastelzio commented Apr 6, 2021

Implements support for running android in self managed mode. I've performed the implementation in a way that shouldn't change any behavior unless selfManaged is explicitly defined in RNCallKeep.setup():

 RNCallKeep.setup({
  android: {
    alertTitle: 'Permissions required',
    alertDescription: 'This application needs to access your phone accounts',
    cancelButton: 'Cancel',
    okButton: 'OK',
    imageName: 'iconmask',
    selfManaged: true, // <- Enables self managed mode
    additionalPermissions: [],
  },
});

I've purposely not added any changes to the documentation yet - I feel like it would be better to do so after we've decided on if we're going to take this PR.

Reviewing and Testing this PR
It is anything but trivial to provide an example app for this, but I recommend performing these tests:

  1. Verify that all functionality works as expected when selfManaged is NOT enabled.
  2. In selfManaged mode, verify that showIncomingCallUi is fired in response to RNCallKeep.displayIncomingCall

The essence of this feature is just to make RNCallKeep deliver the showIncomingCallUi event to react native.

A summary of changes

  1. Added showIncomingCallUi do the device emitter. This event is fired when the onShowIncomingCall() (link) method is called by android on the Connection implementation.
  2. Adjust which permissions are required in RNCallKeepModule.java when running in selfManaged mode.
  3. Changed index.js so it doesn't open phone accounts if running in selfManaged mode.
  4. Added showIncomingCallUi event to index.d.ts

684d065f-d09e-4ebb-aa92-1e1a7d15d590

Marked in yellow in this diagram, is the only difference in operating principle of this module when running in self managed mode. At that point, responsibility of handling the UI related to the call falls entirely on the implementing app.

The app must still call all the relevant RNCK methods, and listen to the relevant events (such as end call, hold call etc.).

On an incoming call, we eventually execute RNCallKeep.displayIncomingCall(callUuid, callerNumber, callerName); - in turn, if Android deems this appropriate, it'll fire the showIncomingCallUi event, at which point we need code that shows a notification.

Anyway, let's discuss this wonderful feature!

Opinion piece on how this should be implemented:
I can see from the other PR that was made for a feature - implementation is fairly similar to this one, that there's always talk about weather this works in background state, with the app dead, foreground etc, and I'd like to address some of that.

Firstly, there are my personal opinions, so we should of course talk about what we recommend for people implementing this.

But one prevailing fact remains, and that is that every app has individual implementation quirks around the sip stack, how it's implemented, what their infrastructure is capable of and all that - all of which means that this feature shouldn't be opinionated in nature.

Battling with stuff like background state, race conditions with initialization of the React Native bridge and so on, are all symptoms of a poor fit into an existing app and it's quirks.

Now on IOS, because of Apple's requirements to show the ring screen within (2?) seconds, all implementations should converge on doing everything in AppDelegate.m (see my other pull request on that).

However, on Android we're not as restricted (yet), but we should be prepared for that, and as such not impose a ton of opinion on the implementors.

But a requirements that FCM does have is that any high priority notification should result in a high priority notification, or they might deprioritize your messages. As such ANY high priority message should, no matter what, invoke RNCallKeep.displayIncomingCall(callUuid, callerNumber, callerName); asap - before even attempting to establish a SIP connection, and then deal with synchronizing this after the fact. And this is the part that'll be extremely different between apps.

My personal recommandation for implementation, is that people have their own implementation of firebase, that'll immediately launch a headless task in react native, that'll immediately invoke RNCallKeep.displayIncomingCall(callUuid, callerNumber, callerName); - and then start setting up the SIP connection, and converge these based on how the user interacts with the notification that's shown.

One important thing to know, is that the full screen notification can be done fully in react native - all of which will require some interaction with native android through the native bridge.

The headless tasks starts incredibly fast, and ensures that we launch the notification fast.

Libraries like react-native-notifications are unfortunately NOT appropriate for use together with these. They're a recipe for a poor implementation that'll make your apps ability to receive phone calls unreliable. They're too opinionated and hides away too much functionality.

Even on Android, doing something like this WILL require a fair amount of native implementation to achieve any degree of success. It simply won't work to operate only in React Native land for this.

A good example of what we're dealing with in other notification libraries, is that they all rely on an event handler to receive device tokens - and googles documentation is extremely clear on the fact that they can distribute a new device token at any point in time. A headless task is the only appropriate way to transport a device token to react native. Event handlers are NOT.

The immediate example that comes to mind is that Google distributes a new push token while the app is not running - there is no event bridge running, and no new token is stored. A headless task allows native to wake up enough of the react native app to deal with this.

else {
Log.d(TAG, "API Version supports self managed, but it is not enabled in setup");
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just provides us some logging so we can confirm we're running in self managed mode. Not strictly needed, but convenient.

@@ -196,6 +215,7 @@ public void startCall(String uuid, String number, String callerName) {
Log.d(TAG, "startCall called, uuid: " + uuid + ", number: " + number + ", callerName: " + callerName);

if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || number == null) {
Log.d(TAG, "startCall ignored: " + isConnectionServiceAvailable() + ", " + hasPhoneAccount() + ", " + hasPermissions() + ", " + number);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this because I've been screwed over by this silent return more than once. I hope we can add this, but it's not strictly related to self managed.

@@ -274,6 +274,10 @@ class RNCallKeep {
_setupAndroid = async (options) => {
RNCallKeepModule.setup(options);

if (options.selfManaged) {
return false;
}
Copy link
Contributor Author

@jonastelzio jonastelzio Apr 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're running in self managed mode I bail out of this method early.

I realize that this also disables the additionalPermissions being requested, which is a bit opinionated by me. But running CallKeep in self managed mode, would essentially turn it into a permission requesting feature - and there are other better libraries available for that.

But let's discuss! ;)

@@ -5,4 +5,6 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
Copy link
Contributor Author

@jonastelzio jonastelzio Apr 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These permissions are required as per: https://developer.android.com/guide/topics/connectivity/telecom/selfManaged#manifest

In your app manifest, declare that your app uses the MANAGE_OWN_CALLS, READ_CALL_LOG, READ_PHONE_STATE, and READ_PHONE_NUMBERS permissions, as shown in the following example:

@luxiliu
Copy link

luxiliu commented Apr 9, 2021

Is it similar to #310?

@jonastelzio
Copy link
Contributor Author

jonastelzio commented Apr 9, 2021

Is it similar to #310?

Very. Main difference is that I send more data in the showIncomingCallUi event delegate, and that I don't open phone accounts when in self managed mode on setup

@Kelt
Copy link

Kelt commented Apr 13, 2021

@jonastelzio i'm trying to use selfmanaged mode on android, running into issue, where if I call RNCallKeep.startCall it steals audio focus from https://github.com/react-native-webrtc/react-native-incall-manager which i am then unable to use to change audio-out devices ... any idea how to get around it ? Thank you

@DanijelBojcic
Copy link

Can you please share a minimal example implementation, that at least shows some kind of callscreen with answer/hangup buttons?
Thanks!

@sboily
Copy link
Member

sboily commented Apr 15, 2021

@luxiliu we will review this PR soon, looks like this PR will be better from #310. Are you agree? Can we close #310 and merge this PR? Thank you to @luxiliu and @jonastelzio to make better callkeep ❤️

@sboily sboily added the need review Need to review ticket label Apr 15, 2021
@sboily sboily requested a review from manuquentin April 15, 2021 14:10
DanijelBojcic added a commit to DanijelBojcic/react-native-callkeep that referenced this pull request Apr 15, 2021
@jonastelzio
Copy link
Contributor Author

Can you please share a minimal example implementation, that at least shows some kind of callscreen with answer/hangup buttons?
Thanks!

I actually mean to ask - is the example app supposed to be runnable? I can't for the life of me get it to actually start up. Just gives some error (don't remember which), but it looks like the tool chain it's build against is pretty old.

Anyway - like I wrote in the PR, it's a substantial amount of code, and it sits squarely outside call keep to manage any of that. It's part of the self managed concept really - the android documentation for self managed connection services also state that it's completely up to the implementor to deal with every single aspect of the call ui elements.

@Kelt
Copy link

Kelt commented Apr 15, 2021

@jonastelzio i'm trying to use selfmanaged mode on android, running into issue, where if I call RNCallKeep.startCall it steals audio focus from https://github.com/react-native-webrtc/react-native-incall-manager which i am then unable to use to change audio-out devices ... any idea how to get around it ? Thank you

figured it out, using didActivateAudioSession event and starting inCallManager only after i receive this event, inCallManager still wont be able to obtain audioContext, as it has been locked by callKeep (TelecomService) but it is able to change the audioRoutes at least ...

@jonastelzio
Copy link
Contributor Author

figured it out, using didActivateAudioSession event and starting inCallManager only after i receive this event, inCallManager still wont be able to obtain audioContext, as it has been locked by callKeep (TelecomService) but it is able to change the audioRoutes at least ...

Cool! Was about to write that it's not an issue I've encountered, but I also haven't tested specifically for it yet.

@danwhite-ipc
Copy link

danwhite-ipc commented Apr 15, 2021

@jonastelzio I am currently using my own branch very similar to your fork. One issue I have is that I show a full screen intent to answer the call but I can not determine how to communicate the endCall back into CallKeep if dismiss is pressed from the native side.

@jonastelzio
Copy link
Contributor Author

@jonastelzio I am currently using my own branch very similar to your fork. One issue I have is that I show a full screen intent to answer the call but I can not determine how to communicate the endCall back into CallKeep if dismiss is pressed from the native side.

You're showing a full screen intent using full native android? In that case I suppose you'd have to invoke end call on the Connection object in the same way CallKeep does it. I'm pretty sure callkeep will then fire it's callEnd event and you can react to it accordingly. Perhaps there's an intent you can fire for it?

@danwhite-ipc
Copy link

@jonastelzio I am currently using my own branch very similar to your fork. One issue I have is that I show a full screen intent to answer the call but I can not determine how to communicate the endCall back into CallKeep if dismiss is pressed from the native side.

You're showing a full screen intent using full native android? In that case I suppose you'd have to invoke end call on the Connection object in the same way CallKeep does it. I'm pretty sure callkeep will then fire it's callEnd event and you can react to it accordingly. Perhaps there's an intent you can fire for it?

I wasn't able to get a version working where a full screen answer/reject was handled purely in React Native from a dead/quit state. Any pointers on that would be appreciated, I will look into using the connection object. Cheers 👍

@jonastelzio
Copy link
Contributor Author

I wasn't able to get a version working where a full screen answer/reject was handled purely in React Native from a dead/quit state. Any pointers on that would be appreciated, I will look into using the connection object. Cheers 👍

You can just create an activity that extends ReactActivity, register it as an app in the root js file and then invoke that as the full screen pending intent. Then you get to use RN for all of that goodness.

@danwhite-ipc
Copy link

You're showing a full screen intent using full native android? In that case I suppose you'd have to invoke end call on the Connection object in the same way CallKeep does it. I'm pretty sure callkeep will then fire it's callEnd event and you can react to it accordingly. Perhaps there's an intent you can fire for it?

I tried getting the connection via Connection conn = VoiceConnectionService.getConnection(uuid); within my firebaseMessagingService and the connection was not found even though I can see the createConnection logged out

@DanijelBojcic
Copy link

Works for me.
https://medium.com/@dcostalloyd90/show-incoming-voip-call-notification-and-open-activity-for-android-os-10-5aada2d4c1e4

Follow this article. Then this is what I did:
I've reated a native method that starts the service and displays a fullscreen intent notification.
Call that on showIncomingCallUi .
I'm passing down the call_uuid .
Then on notification action or in the callscreen activity buttons simply doing this:


Connection conn = VoiceConnectionService.getConnection(uuid);
if(conn !=null){
   conn.onAbort(); // or conn.onAnswer()
}
... Remove the notification or close the activity 

This will trigger the RNCK listeners on the js side.

Also I'm handling the fcm through the headlessJS, so when the user answers the call via the notification the RNCK is already setup.

It basically behaves like before with the native callscreen, but it doesn't requires those annoying permissions.

@luxiliu
Copy link

luxiliu commented Apr 16, 2021

@luxiliu we will review this PR soon, looks like this PR will be better from #310. Are you agree? Can we close #310 and merge this PR? Thank you to @luxiliu and @jonastelzio to make better callkeep ❤️

OK, I'll close #310 . Thanks 👍

@jonastelzio
Copy link
Contributor Author

Also I'm handling the fcm through the headlessJS, so when the user answers the call via the notification the RNCK is already setup.

Same here, and it seems to work really well!

I'm curious - do you launch the notification with .notify or with startForeground? The android doc examples seems to prefer startForeground, but I ended up implementing it with a more normal .notify approach. I'm just 2nd guessing myself on that right now so I'm wondering if you have any input.

My "show incoming call notification" method that I call from React Native returns a promise that is resolved depending on how the user interacts with the shown notification - which I think works really neat.

@DanijelBojcic
Copy link

@jonastelzio with startForeground, .notify didn't work I think.
I am able to handle every case with my implementation:
-foreground
-background
-quit
-locked-quit
-locked-background

@danwhite-ipc
Copy link

Also I'm handling the fcm through the headlessJS, so when the user answers the call via the notification the RNCK is already setup.

Could you possibly share an example. Hopefully when this PR is merged the example app can be updated

@jonastelzio
Copy link
Contributor Author

jonastelzio commented Apr 23, 2021

@DanijelBojcic

Just by chance have you looked into initializing call keep natively (from MainApplication)? - this would be needed to show the incoming call notification prior to react native being active - which would be optimal. Allow the phone to ring, and then in the background start setting up RN and SIP connectivity.

The library is a bit of a mess for that, at least on the surface - just wondering if you've looked into that at all.

setup does all sorts of things. Not only does it initialize the phone account setup, it also straps up event handlers for the JS Bridge and shit. But it's all very intertwined, so just wondering if I'm missing an obvious way to do it that wouldn't initialize the phone account several times.

@DanijelBojcic
Copy link

@jonastelzio i haven't looked into it.
I would simply just change the emit of the showIncomingCallUi with the notification.
No extra steps needed for the user, and everything works/behaves like with the native screen.

@jonastelzio
Copy link
Contributor Author

jonastelzio commented Apr 24, 2021

@jonastelzio i haven't looked into it.
I would simply just change the emit of the showIncomingCallUi with the notification.
No extra steps needed for the user, and everything works/behaves like with the native screen

The show incoming call ui can be easily gotten native side with a broadcast receiver (praised be!).

I ended up doing a small patch that allows me to create the phone account handle in MainApplication and hand it off to callkeep, but it relies on static fields so yay 🤷‍♂️.

The incoming call notification shown up so delightfully fast now it makes me smile every time!

@GoncaloDuarte98
Copy link

I wasn't able to get a version working where a full screen answer/reject was handled purely in React Native from a dead/quit state. Any pointers on that would be appreciated, I will look into using the connection object. Cheers 👍

You can just create an activity that extends ReactActivity, register it as an app in the root js file and then invoke that as the full screen pending intent. Then you get to use RN for all of that goodn

I wasn't able to get a version working where a full screen answer/reject was handled purely in React Native from a dead/quit state. Any pointers on that would be appreciated, I will look into using the connection object. Cheers 👍

You can just create an activity that extends ReactActivity, register it as an app in the root js file and then invoke that as the full screen pending intent. Then you get to use RN for all of that goodness.

Can you specify better? I don't understand if I am starting activity from native module why I would need RNCK , maybe I am missing something. Also can you tell me how to register other React activity? Thanks for the work btw

@manuquentin
Copy link
Contributor

manuquentin commented May 2, 2021

Hi @jonastelzio!
Sorry for the delay and thanks for this good work :)

I reviewed your PR and it works as expected 👌, the only thing that I would change is the handle attribute in the showIncomingCallUi callback that is like tel:[some number] where other callbacks directly send the number without the _ scheme_.
I suggest to have an extra scheme attribute and let the handle with the same format as other callbacks, what do you think ?
I can merge this PR after discussing/addressing this point.

I've noticed that in self managed mode, we don't have a calling account anymore in the phone settings. The phone account was added by the CAPABILITY_CALL_PROVIDER :

Flag indicating that this PhoneAccount can make phone calls in place of traditional SIM-based telephony calls

This calling account was helpful to place call with our app via the native phone application, but it's not available in self managed mode.

I don't find a way to place call from the native dialer app to our application via phone account, as we can't have 2 phone accounts for an app and we can't mix CAPABILITY_CALL_PROVIDER and CAPABILITY_SELF_MANAGED for an account...

The only way I see is to transform our app into a Dialer app (using the android.intent.action.DIAL action, like in https://github.com/Abror96/CustomPhoneDialer).
Have you found another way to be able to place call via the native dialer in self managed mode ?

Thanks !

@jonastelzio
Copy link
Contributor Author

jonastelzio commented May 2, 2021

@manuquentin

I reviewed your PR and it works as expected 👌, the only thing that I would change is the handle attribute in the showIncomingCallUi callback that is like tel:[some number] where other callbacks directly send the number without the _ scheme_.
I suggest to have an extra scheme attribute and let the handle with the same format as other callbacks, what do you think ?

I think it would be fine to split the scheme out into a separate property. I'm already just ditching that part in my client side code.

If we're in no particular urgency to merge, I'm actually working on testing out some changes now, that allows using callkeep earlier. Optimally, at least in my app, I want to display the incoming call notification as fast as possible. That means performing callkeep setup from MainApplication,java instead of from react native .setup() - and then invoking telecomManager.addNewIncomingCall when receiving the FCM notification before starting up react native.

This shows the incoming call notification ridiculously fast - but the way I've got it implemented right now is a bit hacky, so some work could definitely be done inside call keep to facilitate this better.

The gist of it really is that I perform all registerPhoneAccount stuff in MainApplication, and then just handing a reference to the accountHandle to callkeep, instead of having callkeep performing that initialization.

Ultimate though, this change is in fact unrelated to self managed mode, so it would probably make sense to have a new PR for it. But if you have any feedback for that functionality I'd love to hear it!

Have you found another way to be able to place call via the native dialer in self managed mode?

I don't think it'll really be possible in self managed mode to interact directly with the native dialing app. At least that's my understanding. The best we can do is probably handling the DIAL intent as you mention. I haven't actually implemented that in my app yet, but it's on my list.

@manuquentin
Copy link
Contributor

@jonastelzio yes it would be great to display the incoming call UI natively on Android, like it's done on iOS. That would be very fast.

As you say, it would be nice to merge this PR and make a release of this library. A lot of people are waiting for this feature :)
Meanwhile I've merged another PR and there is a conflict in the README, can you rebase your PR please ?
I'll merge it when the scheme will be in another property.

I look forward to seeing your PR on displaying the incoming call natively !

}
else {
extrasMap.put(EXTRA_CALL_NUMBER, callerNumber);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@manuquentin

This is how I chose to address the schema thing you pointed out. This will at least ensure that it's always applied if schema should make it into the system elsewhere... Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

@jonastelzio
Copy link
Contributor Author

@manuquentin I've address the tel: schema thing - please check the comment I left about it!

}
else {
extrasMap.put(EXTRA_CALL_NUMBER, callerNumber);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

@manuquentin manuquentin merged commit 28b2b47 into react-native-webrtc:master May 3, 2021
@manuquentin
Copy link
Contributor

Thanks for all the good work @jonastelzio .
Do you mind to add some documentation about self managed mode ? I'll make a new 4.2.0 release after that.

@sboily
Copy link
Member

sboily commented May 3, 2021

Can you add you in AUTHORS please.

@agam-colaburate
Copy link

Any update for the release ? Waiting with much eager ? Can we roll this out ?

@manuquentin
Copy link
Contributor

I've published a 4.2.0 containing this PR.

@nero2009
Copy link

Please I have a few questions regarding using Self managed mode on android. I am trying to port to the self managed mode from the managed mode.

  • What does it mean to use an headless task to run react native code? because currently I just run my RNCallkeep.js and attach RNCallKeep eventlistener in the App.js/index.js of my react native app.

  • When the showIncomingCallUi event is called/triggered is it possible to navigate to a specific screen(Calling notification screen where you accept or decline the call) in react native or This screen has to be implmented in Android/native code?

  • Also from my initial test it seems like when the screen is locked the calling UI doesn't show(show on top of the locked screen like whatsapp) until I unlock the phone. How can I fix this?

  • Ideally I want this to work in all modes locked/killed, unlocked/killed, locked/foreground e.t.c

Please any help me be nice @jonastelzio @manuquentin

@jonastelzio
Copy link
Contributor Author

jonastelzio commented Jul 23, 2021

@nero2009

What does it mean to use an headless task to run react native code? because currently I just run my RNCallkeep.js and attach RNCallKeep eventlistener in the App.js/index.js of my react native app.

Headless tasks are documented here: https://reactnative.dev/docs/headless-js-android - it works in conjuction with Android Foreground Services: https://developer.android.com/guide/components/foreground-services

If your app is in the background, or killed, and you want Android to actually give it any sort of priority, you MUST implement a foreground service. This is basically a sticky notification in the notification area, that's associated with the app and a subprocess of it (a headless js task in React Natives case). Don't rely on event handlers on Android - the Android OS will randomly murder your app if you don't use a Foreground Service to execute your JavaScript code.

The incoming call screen can be implement either in native code, or a hybrid approach where you have a native activity that extends React Activity, and then you render a separate react native app inside it. This isn't documented in React Native, but it works. But basically you must create a new Android Activity, and extend it from ReactActivity, overriding the getComponentName (or whatever the method is called), and return a string that match with a component you register such as this:

AppRegistry.registerComponent("ringerScreen, () => someReferenceToAComponent);

I'm not using this approach myself, as I ended up doing a fully native one as it renders much faster. But it worked while I did it.

I'm sorry I can't provide a 10 line code snippet that shows it, but it's literally hundreds of lines of code to do this, and it's not one-size-fits-all. It's an advanced android subject that requires insights into both react native and android itself.

When the showIncomingCallUi event is called/triggered is it possible to navigate to a specific screen(Calling notification screen where you accept or decline the call) in react native or This screen has to be implmented in Android/native code?

You can do whatever you want in the new activity, but be aware that React Native might not be fully initialized, so relying too much on RN will delay displaying the incoming call screen

Also from my initial test it seems like when the screen is locked the calling UI doesn't show(show on top of the locked screen like whatsapp) until I unlock the phone. How can I fix this?

This is because you're not starting a Foreground Service, and associating a high priority notification with it. Be aware that you only have a few seconds to invoke this notification after receiving a high priority notification from FCM. That's another reason why I don't recommend relying on React Native too much in this process.

See: https://developer.android.com/training/notify-user/time-sensitive

Ideally I want this to work in all modes locked/killed, unlocked/killed, locked/foreground e.t.c

It will if you play by Androids rules, and that means running a foreground service for your phone calls. Both inbound and outbound. This is extremely important, and there is no way, no how, you get around that if you want your app to work. It will require moving code that handles phone calls into a headless task.

@nero2009
Copy link

Thanks for the reply @jonastelzio I have spent the past hour trying to understand all you said. But I am mostly a javascript developer and even if I understand everything I dont really know how they fit together.

  • First my notifications are handled by a library react-native-push-notifications in the index.js of react native where it is initialized and event listeners for onRegister and onNotification are added. Does this mean I have to move handling of my notifications to the android/native code? How can I do this?

  • When i receive notification how do I launch/trigger the headless task that runs RNCallKeep.displayIncomingCall(callUuid, callerNumber, callerName)?

  • After triggering RNCallKeep.displayIncomingCall and showIncomingCallUi event is called How do you invoke and render a seperate app that shows the incoming call User interface using this hybrid approach.

  • finally How do I navigate from the incoming call UI to the call screen with is in my main react native app?

Sorry for these questions might seem dumb but I have little to no experience with Native android development

@jonastelzio
Copy link
Contributor Author

First my notifications are handled by a library react-native-push-notifications in the index.js of react native where it is initialized and event listeners for onRegister and onNotification are added. Does this mean I have to move handling of my notifications to the android/native code? How can I do this?

The danger to this approach, is that you're running code directly from react-native-push-notifications's FirebaseService-implementation, without activating a foreground service to grand the app foreground priority. This is the main reason why people talk so much about working from background and killed state. Android will, if the phone is experiencing any sort of memory, cpu or battery pressure, kill that process a few seconds after it's spawned if it is not activating a foreground service.

A foreground service can only be reliably activated from native code, directly called from the firebase implementation class. It WILL require writing Java or Kotlin code, unless react-native-push-notifications offer a feature to invoke a foreground service.

HOWEVER, this brings us to the 2nd issue. In order for that foreground service to be granted maximum execution priority (android might still kill foreground services), it must be directly associated with a high priority ongoing notification (such as an incoming call alert screen).

You CAN write a lot of this in RN, but it risks angering Android OS that you're doing it like that, and especially on devices with low amounts of ram, you'll occasionally see incoming calls being killed by the OS.

First my notifications are handled by a library react-native-push-notifications in the index.js of react native where it is initialized and event listeners for onRegister and onNotification are added. Does this mean I have to move handling of my notifications to the android/native code? How can I do this?

This is the wrong way to do it. Receiving the notification should activate a headless task immediately, and associate this headless task (a headless task is bound to an android service class), which in turn should be associated with an on-going, high priority notification. This requires extending React Native's headless task service, and making changes to how it starts up.

When i receive notification how do I launch/trigger the headless task that runs RNCallKeep.displayIncomingCall(callUuid, callerNumber, callerName)?

https://reactnative.dev/docs/headless-js-android has examples on how to start headless tasks.

After triggering RNCallKeep.displayIncomingCall and showIncomingCallUi event is called How do you invoke and render a seperate app that shows the incoming call User interface using this hybrid approach.

I don't have the code that does it anymore unfortunately, but you need to create an Android Activity (in Java), that extends ReactActivity, then create a pending intent that launches THAT activity and associate it with a notification builder with the right settings. You also need to claim a bunch of permissions to display full screen notifications and what not. Google has documentation on it here:

https://developer.android.com/training/notify-user/time-sensitive

I'm sorry this isn't much help man, but this is an advanced subject and I can reference android terms and documentation, but if you're not well versed in it, you need to dig into the native parts of the app first, and get comfortable with it.

finally How do I navigate from the incoming call UI to the call screen with is in my main react native app?

I just finish the incoming call activity, and then launch the main activity bringing up the main react app.

@nero2009
Copy link

nero2009 commented Jul 23, 2021

Thanks for the replies @jonastelzio .

I really appreciate the detailed explanation. I will try to dig into native code and get it to work

@nero2009
Copy link

nero2009 commented Jul 28, 2021

After triggering RNCallKeep.displayIncomingCall and showIncomingCallUi event is called How do you invoke and render a seperate app that shows the incoming call User interface using this hybrid approach.

I don't have the code that does it anymore unfortunately, but you need to create an Android Activity (in Java), that extends ReactActivity, then create a pending intent that launches THAT activity and associate it with a notification builder with the right settings. You also need to claim a bunch of permissions to display full screen notifications and what not. Google has documentation on it here:

I have been able to implement the firebaseService class natively that triggers the headless task that setup RNCallkeep and triggers CallKeep.displayIncomingCall. Then I created a foreground service that triggers a notification Intent, exposed it via react native modules and callled it in the showIncomingCall UI event listener.

Foreground Service

package com.xxx;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

public class CallNotificationService extends Service {
  
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        startForeground(120, getCallNotification());
        return START_STICKY;
    }

    private Notification getCallNotification(){
        Intent incomingCallAction = new Intent(this, IncomingCallActivity.class);

        PendingIntent incomingCallPendingIntent = PendingIntent.getActivity(this, 1001, incomingCallAction, Intent.FLAG_ACTIVITY_NEW_TASK);
        NotificationCompat.Builder notificationBuilder = null;

        Log.d("Debug", "showIncomingCallNotification");
        notificationBuilder = new NotificationCompat.Builder(this,"voip-calls-logs")
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setCategory(NotificationCompat.CATEGORY_CALL)
                .setContentTitle("Incoming Voice Call")
                .setSmallIcon(R.mipmap.ic_notification)
                .setAutoCancel(false)
                .setOngoing(true)
                .setFullScreenIntent(incomingCallPendingIntent, true);

        Notification incomingCallNotification = null;
        return notificationBuilder.build();
    }
}

exposed via native modules:

package com.xxx;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;

public class CallNotificationModule extends ReactContextBaseJavaModule {
    private ReactApplicationContext reactContext;

    CallNotificationModule(ReactApplicationContext context) {
        super(context);
        this.reactContext = context;
    }

    @Override
    public String getName() {
        return "CallNotificationModule";
    }

    @ReactMethod
    public void startService(){
        Context context = getAppContext();
        Intent incomingCallAction = new Intent(context, CallNotificationService.class);
        context.startService(incomingCallAction);

    }

    @ReactMethod
    public void stopService(){
        Context context = getAppContext();
        Intent incomingCallAction = new Intent(context, CallNotificationService.class);
        context.stopService(incomingCallAction);
    }

    private Context getAppContext() {
        return this.reactContext.getApplicationContext();
    }
}

React native onShowIncomingCallUi listener

showIncomingCallUI = ({handle, callUUID, name}) => {
    const {CallNotificationModule} = NativeModules
    CallNotificationModule.startService()

With this I get the notification right but

  • Locked/killed : I dont get the notification on the locked screen but when I unlock the phone the Incoming call UI(Created by extending react activity. The hybrid approach talked about in the thread) and the notification is in the notification center
  • Locked/alive: same behaviour as above
  • Unlocked/killed: I get the notification but the Incoming call UI is not displayed until I tap the notification
  • Unlocked/Alive: same as Unlocked/killed

Behaviour I am looking for is for the Incoming call UI to show over the Locked screen when the phone is locked. Please how can I fix this? @jonastelzio

@linus-komnick
Copy link
Contributor

@nero2009 were you able solve the locked/killed state notification issue?

@abhinav0031
Copy link

abhinav0031 commented Jun 13, 2022

@jonastelzio hi, wanted to ask after adding the foreground service ourselves do we need to remove the foreground service added by react-native-callkeep inside AndroidManifest.xml.
following service is already there:
<
service
android:name="io.wazo.callkeep.VoiceConnectionService"
android:exported="true"
android:foregroundServiceType="phoneCall"
android:label="Wazo"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
>

so should we just replace it with our service and copy all the properties (like foregroundServiceType) which are used here to the new service which is created by us?

@RamsayRomero
Copy link

@abhinav0031 Did you end up figuring out if you needed to remove the callkeep foreground service?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
need review Need to review ticket
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet