CallKit for Android in React Native

Recently we’ve encountered a new challenge: add a custom screen for a push notification in an app written in React Native to receive VoIP calls like Facebook Messenger, Whatsapp and Skype are doing.

CATEGORY

Blog

TECHNOLOGY

react-native android

2017-09-08-fattura-1024x462

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 are 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<ReactPackage> getPackages() {
        // Add additional packages you require here
        // No need to add RnnPackage and MainReactPackage
        return Arrays.<ReactPackage>asList(
                //new MainReactPackage()
                //your react package here
        );
    }

    @Override
    public List<ReactPackage> 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 🙂