From f6ea4ec00d8528c665d76585154e0c064213a724 Mon Sep 17 00:00:00 2001 From: Ian Grayson Date: Thu, 25 Aug 2016 12:49:44 -0700 Subject: [PATCH] [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. --- .gitignore | 3 +- README.md | 51 ++++++++ .../microsoft/codepush/react/CodePush.java | 15 ++- .../codepush/react/CodePushNativeModule.java | 110 +++++++++--------- .../codepush/react/ReactInstanceHolder.java | 17 +++ 5 files changed, 141 insertions(+), 55 deletions(-) create mode 100644 android/app/src/main/java/com/microsoft/codepush/react/ReactInstanceHolder.java diff --git a/.gitignore b/.gitignore index 458ca13..51a6235 100755 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +code-push-plugin-testing-framework/node_modules diff --git a/README.md b/README.md index 1949ce3..df1edca 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java index 7aa5094..16bb84b 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java @@ -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 createNativeModules(ReactApplicationContext reactApplicationContext) { CodePushNativeModule codePushModule = new CodePushNativeModule(reactApplicationContext, this, mUpdateManager, mTelemetryManager, mSettingsManager); @@ -297,4 +310,4 @@ public class CodePush implements ReactPackage { public List createViewManagers(ReactApplicationContext reactApplicationContext) { return new ArrayList<>(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java index da9356f..4d39648 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java @@ -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 { } } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/microsoft/codepush/react/ReactInstanceHolder.java b/android/app/src/main/java/com/microsoft/codepush/react/ReactInstanceHolder.java new file mode 100644 index 0000000..dfe871f --- /dev/null +++ b/android/app/src/main/java/com/microsoft/codepush/react/ReactInstanceHolder.java @@ -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(); +}