diff --git a/.gitignore b/.gitignore index 458ca1350..51a623568 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 e72f1fc9a..65944ae0d 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 7aa5094b0..16bb84bf8 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 void clearUpdates() { 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 List> createJSModules() { 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 0f1d83be7..2cd0abf4c 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; @@ -36,7 +37,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; @@ -78,16 +79,13 @@ public String getName() { 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() { @@ -100,41 +98,14 @@ public void run() { 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 @@ -155,27 +126,60 @@ private void loadBundle() { // #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) { @@ -505,4 +509,4 @@ public void downloadAndReplaceCurrentBundle(String remoteBundleUrl) { } } } -} \ 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 000000000..dfe871f61 --- /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(); +}