Saturday 10 July 2021

Killed React Native Android app with locked device Headless JS Logcat: Calling JS function after bridge has been destroyed

I am using react-native-callkeep to display the native Android calling UI. When swiping up to answer the call I want to call a Native Module of my own to unlock the device. This works and unlocks the device when the app is in the background and foreground, but not when the app has been killed by the user before locking the device.

When the device receives a data only Firebase Cloud Message, the calling UI is displayed correctly, but when swiping up to answer the call, I see this log in Logcat:

ReactNative: Calling JS function after bridge has been destroyed: RCTDeviceEventEmitter.emit(["RNCallKeepPerformAnswerCallAction",{"callUUID":"d2a035dc-3bcc-4ece-a509-e93a8bc62ab3"}])

Prior to this I also see this error when calling RNCallKeep.registerPhoneAccount(...) as described below:

2021-07-03 16:13:32.874 29401-29470/com.xyz E/unknown:ReactNative: CatalystInstanceImpl caught native exception
java.lang.NullPointerException: Attempt to invoke interface method 'boolean com.facebook.react.bridge.ReadableMap.hasKey(java.lang.String)' on a null object reference
at io.wazo.callkeep.RNCallKeepModule.isSelfManaged(RNCallKeepModule.java:120)
at io.wazo.callkeep.RNCallKeepModule.registerPhoneAccount(RNCallKeepModule.java:630)
at io.wazo.callkeep.RNCallKeepModule.registerPhoneAccount(RNCallKeepModule.java:166)
at java.lang.reflect.Method.invoke(Native Method)
at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:151)
at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:27)
at android.os.Looper.loop(Looper.java:223)
at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:226)
at java.lang.Thread.run(Thread.java:923)

Here is my Javascript code that is run by Headless JS when a message is received:

    RNCallKeep.registerPhoneAccount()
    RNCallKeep.displayIncomingCall()
    RNCallKeep.setForegroundServiceSettings()
    RNCallKeep.registerAndroidEvents()
    RNCallKeep.addEventListener('answerCall', () => {
        NativeModules.DeviceLock.unlock(); // call to my native module here on swiping up
    })

Here is the Native Module I am calling in the callback:

    public class DeviceLock extends ReactContextBaseJavaModule {
    
        @Override
        public String getName() {
            return "DeviceLock";
        }
    
        private ReactContext mReactContext;
        private PowerManager.WakeLock sCpuWakeLock;
        private Activity activity;
    
        public DeviceLock(ReactApplicationContext reactContext) {
            super(reactContext);
            mReactContext = reactContext;
        }
    
        /* React Methods */
        @ReactMethod
        public void unlock() {
            activity = mReactContext.getCurrentActivity();
    
            PowerManager pm = (PowerManager) 
            mReactContext.getSystemService(Context.POWER_SERVICE);
            int flags = PowerManager.ACQUIRE_CAUSES_WAKEUP | 
            PowerManager.ON_AFTER_RELEASE;
            sCpuWakeLock = pm.newWakeLock(flags, activity.getClass().getName());
            sCpuWakeLock.acquire();
    
            activity.runOnUiThread(() -> {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
                    activity.setShowWhenLocked(true);
                    activity.setTurnScreenOn(true);
                }
    
                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);
    
            });
        }
    }

I have also tried calling the native code above as soon as the device receives the FCM message without calling the RNCallkeep methods but it has no effect.

I believe the problem to be related to React Context available to the phone / application. What do the error messages mean, how can I call my Native Module successfully and why does this work when the app is in the background but not when killed?

Update

I understand that Android places restrictions on us as developers to encourage best practices relating to the implementation of feature that could potentially be insecure or dangerous. My main problem here is understanding why my code will work when the app is killed without the device being locked, and not when the app is killed and the device is locked.

If what I need is an alternative solution whereby I employ a notification with a full screen Intent or similar I am willing to use it as long as it is functional and allows my users to interact with the app in a practical way. In summary, I need a solution so that I can display and interface on a locked device when the app is killed that allows a user to answer or decline a call and launching the app over the lock screen accordingly. I know this is possible because it works like this in many calling apps, WhatApp for example.

###Update 2

I have tried this approach using the DeviceEventEmitter to notify the JS background task when React context becomes available and to only attempt to unlock the device then, but I now understand that the problem is launching an Activityin the first place from a background task when the screen is locked:

public class MainActivity extends ReactActivity implements ReactInstanceManager.ReactInstanceEventListener {

    private DeviceEventManagerModule.RCTDeviceEventEmitter mEmitter = null;
    private static final String TAG = "MainActivity";

    @Override
    protected String getMainComponentName() {
        return "fleeting";
    }

    public void onReactContextInitialized(ReactContext context) {
        Log.d(TAG, "Here's your valid ReactContext ················································");
        if (mEmitter == null) {
            mEmitter = context.getJSModule((DeviceEventManagerModule.RCTDeviceEventEmitter.class));
        }
        if (mEmitter != null){
            WritableMap eventData = Arguments.createMap();
            eventData.putString("data", "data");
            mEmitter.emit("reactContext", eventData);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        RNBootSplash.init(R.drawable.bootsplash, MainActivity.this);
                         
        getReactInstanceManager().addReactInstanceEventListener(this);
    }
}

Then in JS:

DeviceEventEmitter.addListener('reactContext', () => {
        console.log('React Application Context Available ========== 
 LAUNCH APP HERE');
    });


from Killed React Native Android app with locked device Headless JS Logcat: Calling JS function after bridge has been destroyed

No comments:

Post a Comment