Have you ever used React Native? It’s a framework developed by Facebook that allows you to build native apps with Javascript!

React Native lets you build mobile apps using only JavaScript. It uses the same design as React, letting you compose a rich mobile UI from declarative components.

The first step of the call is a notification. We’re using OneSignal to send push notifications to the device, and there is a npm package for React Native (https://github.com/geektimecoil/react-native-onesignal) to manage that. Our goal was to build an app to make VoIP calls.

Users are used to receive the call even if the device is locked and the app process is not running. This is simple on iOS. You can use a very powerful system API called PushKit, which basically does all the work for you. In this case an open source library is saving us once again (https://github.com/ianlin/react-native-callkit)!

That’s great! But what about Android? Well.. Here comes the trouble! 😢

Android doesn’t have a system API to handle VoIP call notification, so how do we implement this? We, unfortunately, need to write some Java from scratch. Why did I say unfortunately? Because our app is written in Javascript: all our business logic, networking, screens and other stuffs are already handled by React-native, so we don’t want to duplicate the code. We just want to popup a screen that will send an event to Native saying “I have accepted/declined the call”.

Receive push notifications when the app is not alive

The first step is writing a service which receives push notifications even if the app is not alive. For the sake of simplicity we’ll use WakefulBroadcastReceiver in this example, but we suggest using a JobScheduler to detect notifications in a better way.

public class PusherReceiver extends WakefulBroadcastReceiver { public void onReceive(final Context context, Intent intent) { if (!isAppOnForeground((context))) { String custom = intent.getStringExtra("custom"); try { JSONObject notificationData = new JSONObject(custom); // This is the Intent to deliver to our service. Intent service = new Intent(context, BackgroundService.class); // Put here your data from the json as extra in in the intent // Start the service, keeping the device awake while it is launching. startWakefulService(context, service); } catch (JSONException e) { e.printStackTrace(); } } } }

The onReceive event will be triggered every time we receive a push from the GCM (remember to add the service to your AndroidManifest.xml). From here we’ll start a BackgroundService which basically just starts your React Native activity with an Intent.

public class BackgroundService extends IntentService { public BackgroundService() { super("BackgroundService"); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override protected void onHandleIntent(@Nullable Intent intent) { Intent i = new Intent(getBaseContext(), MainActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); if (intent != null) { startActivity(i); PusherReceiver.completeWakefulIntent(intent); } } }

Awesome! Now our React Native activity is up and running!

Now we need to show our activity over the lock screen and send the user choice (receive or decline the call) to the app.

The following steps are assuming that you are using some package to handle navigation properly inside your app. We used https://github.com/wix/react-native-navigation which is one of the most supported libs.

Based on this package, we need to extend the NavigationApplication class. This way, we get access to the lifecycle methods of our activities where we can set some flags and start our UnlockScreenActivity.

public class MainApplication extends NavigationApplication { @Override public boolean isDebug() { // Make sure you are using BuildConfig from your own application return BuildConfig.DEBUG; } protected List getPackages() { // Add additional packages you require here // No need to add RnnPackage and MainReactPackage return Arrays.asList( //new MainReactPackage() //your react package here ); } @Override public List createAdditionalReactPackages() { return getPackages(); } @Override public void onCreate() { super.onCreate(); registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { if (activity instanceof NavigationActivity) { activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); } } @Override public void onActivityResumed(Activity activity) { if (activity instanceof NavigationActivity && NavigationApplication.instance.getReactGateway().isInitialized()) { Intent i = new Intent(activity, ScreenUnlockActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); startActivity(i); } } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { } @Override public void onActivityDestroyed(Activity activity) { } }); } }

It’s really important to add the flag on the NavigationActivity, which is the activity generated by react-native-navigation that will be the container of our app. We need to put it over the lock screen, otherwise if you put it only on your “UnlockScreenActivity”, you’ll see the custom screen and after that the phone will turn the screen off again 😓

N.B. you need to set these flags before calling setContentView inside the onCreate method. Keep it in mind!

Last but not least, our UnlockScreenActivity

public UnlockScreenActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); setContentView(R.layout.activity_call_incoming); //onclicklistener final ReactContext reactContext = NavigationApplication.instance.getReactGateway().getReactContext(); Button acceptCallBtn = findViewById(R.id.accept_call_btn); acceptCallBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { WritableMap params = Arguments.createMap(); //put params here sendEvent(reactContext, "accept", params); } }); } private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); } }

Great, now your Java code is ready!

We now need to connect your React Native app with a listener to the event emitted by your screen and we are ready to go:

import React, { Component } from 'react' import { DeviceEventEmitter } from 'react-native' class MyComponent extends Component { componentDidMount() { DeviceEventEmitter.addListener('accept', () => { //Do your stuff! }) } }

That’s it! The magic is working! 🎩

Remarkable notes:

  • Starting from Android SDK 23, we have access to ConnectionService which is similar to what PushKit does on iOS. Probably you can get a better solution using it.
  • The activities lifecycles in this article is quite generic. Pay attention where you add the flags on the window in order to be sure to obtain the desired result.
  • We’ve followed this implementation because we didn’t want to duplicate business login and networking code between JS and Java, but maybe there is better way to handle this directly inside React Native 🙂