-
Notifications
You must be signed in to change notification settings - Fork 432
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
Implemented self managed mode support for Android #395
Conversation
else { | ||
Log.d(TAG, "API Version supports self managed, but it is not enabled in setup"); | ||
} | ||
} |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; | |||
} |
There was a problem hiding this comment.
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" /> |
There was a problem hiding this comment.
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:
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 |
@jonastelzio i'm trying to use selfmanaged mode on android, running into issue, where if I call |
Can you please share a minimal example implementation, that at least shows some kind of callscreen with answer/hangup buttons? |
@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 ❤️ |
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. |
figured it out, using |
Cool! Was about to write that it's not an issue I've encountered, but I also haven't tested specifically for it yet. |
@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 👍 |
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. |
I tried getting the connection via |
Works for me. Follow this article. Then this is what I did:
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. |
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. |
4652b1d
to
62214b9
Compare
@jonastelzio with startForeground, .notify didn't work I think. |
Could you possibly share an example. Hopefully when this PR is merged the example app can be updated |
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. |
@jonastelzio i haven't looked into it. |
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! |
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 |
Hi @jonastelzio! I reviewed your PR and it works as expected 👌, the only thing that I would change is the 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 :
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 The only way I see is to transform our app into a Dialer app (using the Thanks ! |
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 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!
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. |
@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 :) I look forward to seeing your PR on displaying the incoming call natively ! |
} | ||
else { | ||
extrasMap.put(EXTRA_CALL_NUMBER, callerNumber); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
@manuquentin I've address the tel: schema thing - please check the comment I left about it! |
} | ||
else { | ||
extrasMap.put(EXTRA_CALL_NUMBER, callerNumber); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
Thanks for all the good work @jonastelzio . |
Can you add you in AUTHORS please. |
Any update for the release ? Waiting with much eager ? Can we roll this out ? |
I've published a |
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.
Please any help me be nice @jonastelzio @manuquentin |
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:
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.
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
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
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. |
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.
Sorry for these questions might seem dumb but I have little to no experience with Native android development |
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.
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.
https://reactnative.dev/docs/headless-js-android has examples on how to start headless tasks.
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.
I just finish the incoming call activity, and then launch the main activity bringing up the main react app. |
Thanks for the replies @jonastelzio . I really appreciate the detailed explanation. I will try to dig into native code and get it to work |
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
exposed via native modules:
React native onShowIncomingCallUi listener
With this I get the notification right but
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 |
@nero2009 were you able solve the locked/killed state notification issue? |
@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. 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? |
@abhinav0031 Did you end up figuring out if you needed to remove the callkeep foreground service? |
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 inRNCallKeep.setup()
: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:
selfManaged
is NOT enabled.selfManaged
mode, verify thatshowIncomingCallUi
is fired in response toRNCallKeep.displayIncomingCall
The essence of this feature is just to make RNCallKeep deliver the
showIncomingCallUi
event to react native.A summary of changes
showIncomingCallUi
do the device emitter. This event is fired when theonShowIncomingCall()
(link) method is called by android on the Connection implementation.RNCallKeepModule.java
when running inselfManaged
mode.index.js
so it doesn't open phone accounts if running inselfManaged
mode.showIncomingCallUi
event toindex.d.ts
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 theshowIncomingCallUi
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.