Skip to content

Commit

Permalink
Merge pull request microsoft#491 from igrayson/patch-1
Browse files Browse the repository at this point in the history
[Android] Support React instances with no Activity
  • Loading branch information
geof90 authored Aug 30, 2016
2 parents c2d2e35 + f6ea4ec commit c376c30
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 55 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ gen/
.gradle/
build/
*/build/
android/app/gradle*

# Local configuration file (sdk path, etc)
local.properties
Expand All @@ -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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<NativeModule> createNativeModules(ReactApplicationContext reactApplicationContext) {
CodePushNativeModule codePushModule = new CodePushNativeModule(reactApplicationContext, this, mUpdateManager, mTelemetryManager, mSettingsManager);
Expand All @@ -297,4 +310,4 @@ public List<Class<? extends JavaScriptModule>> createJSModules() {
public List<ViewManager> createViewManagers(ReactApplicationContext reactApplicationContext) {
return new ArrayList<>();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -505,4 +509,4 @@ public void downloadAndReplaceCurrentBundle(String remoteBundleUrl) {
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}

0 comments on commit c376c30

Please sign in to comment.