Skip to content

Commit

Permalink
plugin: add app channel for pause-resume events
Browse files Browse the repository at this point in the history
Adds an app channel for sending 'pause' and 'resume' events to node.
On iOS, wait for the pause event handler to finish before letting the
iOS application suspend after going to the background, by use of a
pauseLock object passed to the pause event handlers.
  • Loading branch information
jaimecbernardo committed Aug 9, 2018
1 parent 7c922f9 commit 10c1d3e
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 20 deletions.
2 changes: 1 addition & 1 deletion android/src/main/cpp/native-lib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ void rcv_message(const char* channel_name, const char* msg) {
if(!env) return;
jclass cls2 = env->FindClass("com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule"); // try to find the class
if(cls2 != nullptr) {
jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "sendMessageBackToReact", "(Ljava/lang/String;Ljava/lang/String;)V"); // find method
jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "sendMessageToApplication", "(Ljava/lang/String;Ljava/lang/String;)V"); // find method
if(m_sendMessage != nullptr) {
jstring java_channel_name=env->NewStringUTF(channel_name);
jstring java_msg=env->NewStringUTF(msg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
import javax.annotation.Nullable;
import android.util.Log;

Expand All @@ -22,7 +23,7 @@
import java.util.*;
import java.util.concurrent.Semaphore;

public class RNNodeJsMobileModule extends ReactContextBaseJavaModule {
public class RNNodeJsMobileModule extends ReactContextBaseJavaModule implements LifecycleEventListener {

private final ReactApplicationContext reactContext;
private static final String TAG = "NODEJS-RN";
Expand All @@ -32,6 +33,7 @@ public class RNNodeJsMobileModule extends ReactContextBaseJavaModule {
private static final String SHARED_PREFS = "NODEJS_MOBILE_PREFS";
private static final String LAST_UPDATED_TIME = "NODEJS_MOBILE_APK_LastUpdateTime";
private static final String BUILTIN_NATIVE_ASSETS_PREFIX = "nodejs-native-assets-";
private static final String SYSTEM_CHANNEL = "_SYSTEM_";

private static String trashDirPath;
private static String filesDirPath;
Expand All @@ -46,6 +48,9 @@ public class RNNodeJsMobileModule extends ReactContextBaseJavaModule {

private static AssetManager assetManager;

// Flag to indicate if node is ready to receive app events.
private static boolean nodeIsReadyForAppEvents = false;

static {
System.loadLibrary("nodejs-mobile-react-native-native-lib");
System.loadLibrary("node");
Expand All @@ -60,6 +65,7 @@ public class RNNodeJsMobileModule extends ReactContextBaseJavaModule {
public RNNodeJsMobileModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
reactContext.addLifecycleEventListener(this);
filesDirPath = reactContext.getFilesDir().getAbsolutePath();

// The paths where we expect the node project assets to be at runtime.
Expand Down Expand Up @@ -182,6 +188,41 @@ private void sendEvent(String eventName,
.emit(eventName, params);
}

public static void sendMessageToApplication(String channelName, String msg) {
if (channelName.equals(SYSTEM_CHANNEL)) {
// If it's a system channel call, handle it in the plugin native side.
handleAppChannelMessage(msg);
} else {
// Otherwise, send it to React Native.
sendMessageBackToReact(channelName, msg);
}
}

@Override
public void onHostPause() {
if (nodeIsReadyForAppEvents) {
sendMessageToNodeChannel(SYSTEM_CHANNEL, "pause");
}
}

@Override
public void onHostResume() {
if (nodeIsReadyForAppEvents) {
sendMessageToNodeChannel(SYSTEM_CHANNEL, "resume");
}
}

@Override
public void onHostDestroy() {
// Activity `onDestroy`
}

public static void handleAppChannelMessage(String msg) {
if (msg.equals("ready-for-app-events")) {
nodeIsReadyForAppEvents=true;
}
}

// Called from JNI when node sends a message through the bridge.
public static void sendMessageBackToReact(String channelName, String msg) {
if (_instance != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ const NativeBridge = process.binding('rn_bridge');
*/
const EVENT_CHANNEL = '_EVENTS_';

/**
* Built-in, one-way event channel reserved for sending events from
* the react-native plug-in native layer to the Node.js app.
*/
const SYSTEM_CHANNEL = '_SYSTEM_';

/**
* This class is defined in the plugin's root index.js as well.
* Any change made here should be ported to the root index.js too.
Expand Down Expand Up @@ -87,6 +93,34 @@ class EventChannel extends ChannelSuper {
};
};

/**
* System event Lock class
* Helper class to handle lock acquisition and release in system event handlers.
* Will call a callback after every lock has been released.
**/
class SystemEventLock {
constructor(callback, startingLocks) {
this._locksAcquired = startingLocks; // Start with one lock.
this._callback = callback; // Callback to call after all locks are released.
this._hasReleased = false; // To stop doing anything after it's supposed to serve its purpose.
this._checkRelease(); // Initial check. If it's been started with no locks, can be released right away.
}
// Release a lock and call the callback if all locks have been released.
release() {
if (this._hasReleased) return;
this._locksAcquired--;
this._checkRelease();
}
// Check if the lock can be released and release it.
_checkRelease() {
if(this._locksAcquired<=0) {
this._hasReleased=true;
this._callback();
}
}

}

/**
* System channel class.
* Emit pause/resume events when the app goes to background/foreground.
Expand All @@ -96,6 +130,34 @@ class SystemChannel extends ChannelSuper {
super(name);
};

emitWrapper(type) {
// Overload the emitWrapper to handle the pause event locks.
const _this = this;
if (type.startsWith('pause')) {
setImmediate( () => {
let releaseMessage = 'release-pause-event';
let eventArguments = type.split('|');
if (eventArguments.length >= 2) {
// The expected format for the release message is "release-pause-event|{eventId}"
// eventId comes from the pause event, with the format "pause|{eventId}"
releaseMessage = releaseMessage + '|' + eventArguments[1];
}
// Create a lock to signal the native side after the app event has been handled.
let eventLock = new SystemEventLock(
() => {
NativeBridge.sendMessage(_this.name, releaseMessage);
}
, _this.listenerCount("pause") // A lock for each current event listener. All listeners need to call release().
);
_this.emitLocal("pause", eventLock);
});
} else {
setImmediate( () => {
_this.emitLocal(type);
});
}
};

processData(data) {
// The data is the event.
this.emitWrapper(data);
Expand Down Expand Up @@ -130,6 +192,15 @@ function registerChannel(channel) {
NativeBridge.registerChannel(channel.name, bridgeListener);
};

/**
* Module exports.
*/
const systemChannel = new SystemChannel(SYSTEM_CHANNEL);
registerChannel(systemChannel);

// Signal we are ready for app events, so the native code won't lock before node is ready to handle those.
NativeBridge.sendMessage(SYSTEM_CHANNEL, "ready-for-app-events");

const eventChannel = new EventChannel(EVENT_CHANNEL);
registerChannel(eventChannel);

Expand Down
Loading

0 comments on commit 10c1d3e

Please sign in to comment.