Skip to content

Commit c32369b

Browse files
committed
Refactor CodePushNativeModule to enhance React Native integration and improve bundle loading logic
1 parent f18ff0d commit c32369b

1 file changed

Lines changed: 178 additions & 44 deletions

File tree

android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java

Lines changed: 178 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,48 @@
55
import android.os.AsyncTask;
66
import android.os.Handler;
77
import android.os.Looper;
8-
import android.provider.Settings;
98
import android.view.View;
109

10+
import androidx.annotation.OptIn;
11+
1112
import com.facebook.react.ReactApplication;
13+
import com.facebook.react.ReactHost;
1214
import com.facebook.react.ReactInstanceManager;
1315
import com.facebook.react.ReactRootView;
1416
import com.facebook.react.bridge.Arguments;
17+
import com.facebook.react.bridge.BaseJavaModule;
1518
import com.facebook.react.bridge.JSBundleLoader;
1619
import com.facebook.react.bridge.LifecycleEventListener;
1720
import com.facebook.react.bridge.Promise;
1821
import com.facebook.react.bridge.ReactApplicationContext;
19-
import com.facebook.react.bridge.ReactContextBaseJavaModule;
2022
import com.facebook.react.bridge.ReactMethod;
2123
import com.facebook.react.bridge.ReadableMap;
2224
import com.facebook.react.bridge.WritableMap;
25+
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
26+
import com.facebook.react.devsupport.interfaces.DevSupportManager;
2327
import com.facebook.react.modules.core.ChoreographerCompat;
2428
import com.facebook.react.modules.core.DeviceEventManagerModule;
2529
import com.facebook.react.modules.core.ReactChoreographer;
30+
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
31+
import com.facebook.react.runtime.ReactHostDelegate;
32+
import com.facebook.react.runtime.ReactHostImpl;
2633

2734
import org.json.JSONArray;
2835
import org.json.JSONException;
2936
import org.json.JSONObject;
3037

3138
import java.io.IOException;
3239
import java.lang.reflect.Field;
40+
import java.lang.reflect.Method;
3341
import java.util.ArrayList;
3442
import java.util.Date;
3543
import java.util.HashMap;
3644
import java.util.List;
3745
import java.util.Map;
3846
import java.util.UUID;
3947

40-
public class CodePushNativeModule extends ReactContextBaseJavaModule {
48+
@OptIn(markerClass = UnstableReactNativeAPI.class)
49+
public class CodePushNativeModule extends BaseJavaModule {
4150
private String mBinaryContentsHash = null;
4251
private String mClientUniqueId = null;
4352
private LifecycleEventListener mLifecycleEventListener = null;
@@ -93,7 +102,7 @@ public String getName() {
93102
}
94103

95104
private void loadBundleLegacy() {
96-
final Activity currentActivity = getCurrentActivity();
105+
final Activity currentActivity = getReactApplicationContext().getCurrentActivity();
97106
if (currentActivity == null) {
98107
// The currentActivity can be null if it is backgrounded / destroyed, so we simply
99108
// no-op to prevent any null pointer exceptions.
@@ -124,59 +133,155 @@ private void setJSBundle(ReactInstanceManager instanceManager, String latestJSBu
124133
bundleLoaderField.setAccessible(true);
125134
bundleLoaderField.set(instanceManager, latestJSBundleLoader);
126135
} catch (Exception e) {
127-
CodePushUtils.log("Unable to set JSBundle - CodePush may not support this version of React Native");
136+
CodePushUtils.log("Unable to set JSBundle of ReactInstanceManager - CodePush may not support this version of React Native");
128137
throw new IllegalAccessException("Could not setJSBundle");
129138
}
130139
}
131140

132-
private void loadBundle() {
133-
clearLifecycleEventListener();
141+
// Use reflection to find and set the appropriate fields on ReactHostDelegate. See #556 for a proposal for a less brittle way
142+
// to approach this.
143+
private void setJSBundle(ReactHostDelegate reactHostDelegate, String latestJSBundleFile) throws IllegalAccessException {
134144
try {
135-
mCodePush.clearDebugCacheIfNeeded(resolveInstanceManager());
136-
} catch(Exception e) {
137-
// If we got error in out reflection we should clear debug cache anyway.
138-
mCodePush.clearDebugCacheIfNeeded(null);
145+
JSBundleLoader latestJSBundleLoader;
146+
if (latestJSBundleFile.toLowerCase().startsWith("assets://")) {
147+
latestJSBundleLoader = JSBundleLoader.createAssetLoader(getReactApplicationContext(), latestJSBundleFile, false);
148+
} else {
149+
latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile);
150+
}
151+
152+
Field bundleLoaderField = reactHostDelegate.getClass().getDeclaredField("jsBundleLoader");
153+
bundleLoaderField.setAccessible(true);
154+
bundleLoaderField.set(reactHostDelegate, latestJSBundleLoader);
155+
} catch (Exception e) {
156+
CodePushUtils.log("Unable to set JSBundle of ReactHostDelegate - CodePush may not support this version of React Native");
157+
throw new IllegalAccessException("Could not setJSBundle");
139158
}
159+
}
140160

141-
try {
142-
// #1) Get the ReactInstanceManager instance, which is what includes the
143-
// logic to reload the current React context.
144-
final ReactInstanceManager instanceManager = resolveInstanceManager();
145-
if (instanceManager == null) {
146-
return;
161+
private void loadBundle() {
162+
clearLifecycleEventListener();
163+
164+
// ReactNative core components are changed on new architecture.
165+
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
166+
try {
167+
DevSupportManager devSupportManager = null;
168+
ReactHost reactHost = resolveReactHost();
169+
if (reactHost != null) {
170+
devSupportManager = reactHost.getDevSupportManager();
171+
}
172+
boolean isLiveReloadEnabled = isLiveReloadEnabled(devSupportManager);
173+
174+
mCodePush.clearDebugCacheIfNeeded(isLiveReloadEnabled);
175+
} catch(Exception e) {
176+
// If we got error in out reflection we should clear debug cache anyway.
177+
mCodePush.clearDebugCacheIfNeeded(false);
178+
}
179+
180+
try {
181+
// #1) Get the ReactHost instance, which is what includes the
182+
// logic to reload the current React context.
183+
final ReactHost reactHost = resolveReactHost();
184+
if (reactHost == null) {
185+
return;
186+
}
187+
188+
String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());
189+
190+
// #2) Update the locally stored JS bundle file path
191+
setJSBundle(getReactHostDelegate((ReactHostImpl) reactHost), latestJSBundleFile);
192+
193+
// #3) Get the context creation method
194+
try {
195+
reactHost.reload("CodePush triggers reload");
196+
mCodePush.initializeUpdateAfterRestart();
197+
} catch (Exception e) {
198+
// The recreation method threw an unknown exception
199+
// so just simply fallback to restarting the Activity (if it exists)
200+
loadBundleLegacy();
201+
}
202+
203+
} catch (Exception e) {
204+
// Our reflection logic failed somewhere
205+
// so fall back to restarting the Activity (if it exists)
206+
CodePushUtils.log("Failed to load the bundle, falling back to restarting the Activity (if it exists). " + e.getMessage());
207+
loadBundleLegacy();
208+
}
209+
210+
} else {
211+
212+
try {
213+
DevSupportManager devSupportManager = null;
214+
ReactInstanceManager reactInstanceManager = resolveInstanceManager();
215+
if (reactInstanceManager != null) {
216+
devSupportManager = reactInstanceManager.getDevSupportManager();
217+
}
218+
boolean isLiveReloadEnabled = isLiveReloadEnabled(devSupportManager);
219+
220+
mCodePush.clearDebugCacheIfNeeded(isLiveReloadEnabled);
221+
} catch(Exception e) {
222+
// If we got error in out reflection we should clear debug cache anyway.
223+
mCodePush.clearDebugCacheIfNeeded(false);
147224
}
148225

149-
String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());
226+
try {
227+
// #1) Get the ReactInstanceManager instance, which is what includes the
228+
// logic to reload the current React context.
229+
final ReactInstanceManager instanceManager = resolveInstanceManager();
230+
if (instanceManager == null) {
231+
return;
232+
}
233+
234+
String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());
235+
236+
// #2) Update the locally stored JS bundle file path
237+
setJSBundle(instanceManager, latestJSBundleFile);
238+
239+
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
240+
new Handler(Looper.getMainLooper()).post(new Runnable() {
241+
@Override
242+
public void run() {
243+
try {
244+
// We don't need to resetReactRootViews anymore
245+
// due the issue https://github.com/facebook/react-native/issues/14533
246+
// has been fixed in RN 0.46.0
247+
//resetReactRootViews(instanceManager);
248+
249+
instanceManager.recreateReactContextInBackground();
250+
mCodePush.initializeUpdateAfterRestart();
251+
} catch (Exception e) {
252+
// The recreation method threw an unknown exception
253+
// so just simply fallback to restarting the Activity (if it exists)
254+
loadBundleLegacy();
255+
}
256+
}
257+
});
258+
259+
} catch (Exception e) {
260+
// Our reflection logic failed somewhere
261+
// so fall back to restarting the Activity (if it exists)
262+
CodePushUtils.log("Failed to load the bundle, falling back to restarting the Activity (if it exists). " + e.getMessage());
263+
loadBundleLegacy();
264+
}
150265

151-
// #2) Update the locally stored JS bundle file path
152-
setJSBundle(instanceManager, latestJSBundleFile);
266+
}
267+
}
153268

154-
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
155-
new Handler(Looper.getMainLooper()).post(new Runnable() {
156-
@Override
157-
public void run() {
269+
private boolean isLiveReloadEnabled(DevSupportManager devSupportManager) {
270+
if (devSupportManager != null) {
271+
DeveloperSettings devSettings = devSupportManager.getDevSettings();
272+
Method[] methods = devSettings.getClass().getMethods();
273+
for (Method m : methods) {
274+
if (m.getName().equals("isReloadOnJSChangeEnabled")) {
158275
try {
159-
// We don't need to resetReactRootViews anymore
160-
// due the issue https://github.com/facebook/react-native/issues/14533
161-
// has been fixed in RN 0.46.0
162-
//resetReactRootViews(instanceManager);
163-
164-
instanceManager.recreateReactContextInBackground();
165-
mCodePush.initializeUpdateAfterRestart();
166-
} catch (Exception e) {
167-
// The recreation method threw an unknown exception
168-
// so just simply fallback to restarting the Activity (if it exists)
169-
loadBundleLegacy();
276+
return (boolean) m.invoke(devSettings);
277+
} catch (Exception x) {
278+
return false;
170279
}
171280
}
172-
});
173-
174-
} catch (Exception e) {
175-
// Our reflection logic failed somewhere
176-
// so fall back to restarting the Activity (if it exists)
177-
CodePushUtils.log("Failed to load the bundle, falling back to restarting the Activity (if it exists). " + e.getMessage());
178-
loadBundleLegacy();
281+
}
179282
}
283+
284+
return false;
180285
}
181286

182287
// This workaround has been implemented in order to fix https://github.com/facebook/react-native/issues/14533
@@ -208,7 +313,7 @@ private ReactInstanceManager resolveInstanceManager() throws NoSuchFieldExceptio
208313
return instanceManager;
209314
}
210315

211-
final Activity currentActivity = getCurrentActivity();
316+
final Activity currentActivity = getReactApplicationContext().getCurrentActivity();
212317
if (currentActivity == null) {
213318
return null;
214319
}
@@ -219,6 +324,21 @@ private ReactInstanceManager resolveInstanceManager() throws NoSuchFieldExceptio
219324
return instanceManager;
220325
}
221326

327+
private ReactHost resolveReactHost() throws NoSuchFieldException, IllegalAccessException {
328+
ReactHost reactHost = CodePush.getReactHost();
329+
if (reactHost != null) {
330+
return reactHost;
331+
}
332+
333+
final Activity currentActivity = getReactApplicationContext().getCurrentActivity();
334+
if (currentActivity == null) {
335+
return null;
336+
}
337+
338+
ReactApplication reactApplication = (ReactApplication) currentActivity.getApplication();
339+
return reactApplication.getReactHost();
340+
}
341+
222342
private void restartAppInternal(boolean onlyIfUpdateIsPending) {
223343
if (this._restartInProgress) {
224344
CodePushUtils.log("Restart request queued until the current restart is completed");
@@ -492,7 +612,7 @@ protected Void doInBackground(Void... params) {
492612
return null;
493613
}
494614
}
495-
615+
496616
promise.resolve("");
497617
} catch(CodePushUnknownException e) {
498618
CodePushUtils.log(e);
@@ -711,4 +831,18 @@ public void addListener(String eventName) {
711831
public void removeListeners(Integer count) {
712832
// Remove upstream listeners, stop unnecessary background tasks
713833
}
834+
835+
public ReactHostDelegate getReactHostDelegate(ReactHostImpl reactHostImpl) {
836+
try {
837+
Class<?> clazz = reactHostImpl.getClass();
838+
Field field = clazz.getDeclaredField("mReactHostDelegate");
839+
field.setAccessible(true);
840+
841+
// Get the value of the field for the provided instance
842+
return (ReactHostDelegate) field.get(reactHostImpl);
843+
} catch (NoSuchFieldException | IllegalAccessException e) {
844+
e.printStackTrace();
845+
return null;
846+
}
847+
}
714848
}

0 commit comments

Comments
 (0)