[Android] Support React instances with no Activity

This change aims to support applications which run React Native's catalyst instance in the background, sometimes without an activity.

We're using this change today in an RN 0.27 application, which overrides `ReactActivity#createReactInstanceManager` to afford us control of the instance's lifecycle, so that we may run and launch without an `Activity` (i.e., in response to push notifications and other triggers).

If we agree with this approach, I'll update this PR with documentation/etc.
This commit is contained in:
Ian Grayson
2016-08-25 12:49:44 -07:00
committed by Ian Grayson
parent 2a182ab35d
commit f6ea4ec00d
5 changed files with 141 additions and 55 deletions

3
.gitignore vendored
View File

@@ -132,6 +132,7 @@ gen/
.gradle/
build/
*/build/
android/app/gradle*
# Local configuration file (sdk path, etc)
local.properties
@@ -149,4 +150,4 @@ proguard/
captures/
# Remove after this framework is published on NPM
code-push-plugin-testing-framework/node_modules
code-push-plugin-testing-framework/node_modules

View File

@@ -343,6 +343,57 @@ public class MainActivity extends ReactActivity {
}
```
#### Background React Instances ####
**This section is only necessary if you're *explicitly* launching a React Native instance without an `Activity` (for example, from within a native push notification receiver). For these situations, CodePush must be told how to find your React Native instance.**
In order to update/restart your React Native instance, CodePush must be configured with a `ReactInstanceHolder` before attempting to restart an instance in the background. This is usually done in your `Application` implementation.
**For React Native >= v0.29**
Update the `MainApplication.java` file to use CodePush via the following changes:
```java
...
// 1. Declare your ReactNativeHost to extend ReactInstanceHolder. ReactInstanceHolder is a subset of ReactNativeHost, so no additional implementation is needed.
import com.microsoft.codepush.react.ReactInstanceHolder;
public class MyReactNativeHost extends ReactNativeHost implements ReactInstanceHolder {
// ... usual overrides
}
// 2. Provide your ReactNativeHost to CodePush.
public class MainApplication extends Application implements ReactApplication {
private final MyReactNativeHost mReactNativeHost = new MyReactNativeHost(this);
@Override
public void onCreate() {
CodePush.setReactInstanceHolder(mReactNativeHost);
super.onCreate();
}
}
```
**For React Native v0.19 - v0.28**
Before v0.29, React Native did not provide a `ReactNativeHost` abstraction. If you're launching a background instance, you'll likely have built your own, which should now implement `ReactInstanceHolder`. Once that's done...
```java
// 1. Provide your ReactInstanceHolder to CodePush.
public class MainApplication extends Application {
@Override
public void onCreate() {
// ... initialize your instance holder
CodePush.setReactInstanceHolder(myInstanceHolder);
super.onCreate();
}
}
```
In order to effectively make use of the `Staging` and `Production` deployments that were created along with your CodePush app, refer to the [multi-deployment testing](#multi-deployment-testing) docs below before actually moving your app's usage of CodePush into production.
## Windows Setup

View File

@@ -1,5 +1,6 @@
package com.microsoft.codepush.react;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
@@ -44,6 +45,7 @@ public class CodePush implements ReactPackage {
private Context mContext;
private final boolean mIsDebugMode;
private static ReactInstanceHolder mReactInstanceHolder;
private static CodePush mCurrentInstance;
public CodePush(String deploymentKey, Context context) {
@@ -277,6 +279,17 @@ public class CodePush implements ReactPackage {
mSettingsManager.removeFailedUpdates();
}
public static void setReactInstanceHolder(ReactInstanceHolder reactInstanceHolder) {
mReactInstanceHolder = reactInstanceHolder;
}
static ReactInstanceManager getReactInstanceManager() {
if (mReactInstanceHolder == null) {
return null;
}
return mReactInstanceHolder.getReactInstanceManager();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactApplicationContext) {
CodePushNativeModule codePushModule = new CodePushNativeModule(reactApplicationContext, this, mUpdateManager, mTelemetryManager, mSettingsManager);
@@ -297,4 +310,4 @@ public class CodePush implements ReactPackage {
public List<ViewManager> createViewManagers(ReactApplicationContext reactApplicationContext) {
return new ArrayList<>();
}
}
}

View File

@@ -1,8 +1,9 @@
package com.microsoft.codepush.react;
import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.view.Choreographer;
@@ -35,7 +36,7 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
private String mClientUniqueId = null;
private LifecycleEventListener mLifecycleEventListener = null;
private int mMinimumBackgroundDuration = 0;
private CodePush mCodePush;
private SettingsManager mSettingsManager;
private CodePushTelemetryManager mTelemetryManager;
@@ -77,16 +78,13 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
return "CodePush";
}
private boolean isReactApplication(Context context) {
Class<?> reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME);
if (reactApplicationClass != null && reactApplicationClass.isInstance(context)) {
return true;
private void loadBundleLegacy() {
final Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
// The currentActivity can be null if it is backgrounded / destroyed, so we simply
// no-op to prevent any null pointer exceptions.
return;
}
return false;
}
private void loadBundleLegacy(final Activity currentActivity) {
mCodePush.invalidateCurrentInstance();
currentActivity.runOnUiThread(new Runnable() {
@@ -99,41 +97,14 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
private void loadBundle() {
mCodePush.clearDebugCacheIfNeeded();
final Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
// The currentActivity can be null if it is backgrounded / destroyed, so we simply
// no-op to prevent any null pointer exceptions.
return;
}
try {
ReactInstanceManager instanceManager;
// #1) Get the ReactInstanceManager instance, which is what includes the
// logic to reload the current React context.
try {
// In RN >=0.29, the "mReactInstanceManager" field yields a null value, so we try
// to get the instance manager via the ReactNativeHost, which only exists in 0.29.
Method getApplicationMethod = ReactActivity.class.getMethod("getApplication");
Object reactApplication = getApplicationMethod.invoke(currentActivity);
Class<?> reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME);
Method getReactNativeHostMethod = reactApplicationClass.getMethod("getReactNativeHost");
Object reactNativeHost = getReactNativeHostMethod.invoke(reactApplication);
Class<?> reactNativeHostClass = tryGetClass(REACT_NATIVE_HOST_CLASS_NAME);
Method getReactInstanceManagerMethod = reactNativeHostClass.getMethod("getReactInstanceManager");
instanceManager = (ReactInstanceManager)getReactInstanceManagerMethod.invoke(reactNativeHost);
} catch (Exception e) {
// The React Native version might be older than 0.29, or the activity does not
// extend ReactActivity, so we try to get the instance manager via the
// "mReactInstanceManager" field.
Class instanceManagerHolderClass = currentActivity instanceof ReactActivity
? ReactActivity.class
: currentActivity.getClass();
Field instanceManagerField = instanceManagerHolderClass.getDeclaredField("mReactInstanceManager");
instanceManagerField.setAccessible(true);
instanceManager = (ReactInstanceManager)instanceManagerField.get(currentActivity);
final ReactInstanceManager instanceManager = resolveInstanceManager();
if (instanceManager == null) {
return;
}
String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());
// #2) Update the locally stored JS bundle file path
@@ -143,28 +114,61 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
final Method recreateMethod = instanceManager.getClass().getMethod("recreateReactContextInBackground");
final ReactInstanceManager finalizedInstanceManager = instanceManager;
currentActivity.runOnUiThread(new Runnable() {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
recreateMethod.invoke(finalizedInstanceManager);
recreateMethod.invoke(instanceManager);
mCodePush.initializeUpdateAfterRestart();
}
catch (Exception e) {
} catch (Exception e) {
// The recreation method threw an unknown exception
// so just simply fallback to restarting the Activity
loadBundleLegacy(currentActivity);
// so just simply fallback to restarting the Activity (if it exists)
loadBundleLegacy();
}
}
});
} catch (Exception e) {
// Our reflection logic failed somewhere
// so fall back to restarting the Activity
loadBundleLegacy(currentActivity);
// so fall back to restarting the Activity (if it exists)
loadBundleLegacy();
}
}
private ReactInstanceManager resolveInstanceManager() throws NoSuchFieldException, IllegalAccessException {
ReactInstanceManager instanceManager = CodePush.getReactInstanceManager();
if (instanceManager != null) {
return instanceManager;
}
final Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
return null;
}
try {
// In RN >=0.29, the "mReactInstanceManager" field yields a null value, so we try
// to get the instance manager via the ReactNativeHost, which only exists in 0.29.
Method getApplicationMethod = ReactActivity.class.getMethod("getApplication");
Object reactApplication = getApplicationMethod.invoke(currentActivity);
Class<?> reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME);
Method getReactNativeHostMethod = reactApplicationClass.getMethod("getReactNativeHost");
Object reactNativeHost = getReactNativeHostMethod.invoke(reactApplication);
Class<?> reactNativeHostClass = tryGetClass(REACT_NATIVE_HOST_CLASS_NAME);
Method getReactInstanceManagerMethod = reactNativeHostClass.getMethod("getReactInstanceManager");
instanceManager = (ReactInstanceManager)getReactInstanceManagerMethod.invoke(reactNativeHost);
} catch (Exception e) {
// The React Native version might be older than 0.29, or the activity does not
// extend ReactActivity, so we try to get the instance manager via the
// "mReactInstanceManager" field.
Class instanceManagerHolderClass = currentActivity instanceof ReactActivity
? ReactActivity.class
: currentActivity.getClass();
Field instanceManagerField = instanceManagerHolderClass.getDeclaredField("mReactInstanceManager");
instanceManagerField.setAccessible(true);
instanceManager = (ReactInstanceManager)instanceManagerField.get(currentActivity);
}
return instanceManager;
}
private Class tryGetClass(String className) {
try {
@@ -493,4 +497,4 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.microsoft.codepush.react;
import com.facebook.react.ReactInstanceManager;
/**
* Provides access to a {@link ReactInstanceManager}.
*
* ReactNativeHost already implements this interface, if you make use of that react-native
* component (just add `implements ReactInstanceHolder`).
*/
public interface ReactInstanceHolder {
/**
* Get the current {@link ReactInstanceManager} instance. May return null.
*/
ReactInstanceManager getReactInstanceManager();
}