@revopush/react-native-code-push 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.claude/settings.local.json +5 -1
  2. package/README.md +2 -1
  3. package/android/build.gradle +12 -1
  4. package/android/codepush.gradle +21 -44
  5. package/android/gradle.properties +2 -0
  6. package/android/proguard-rules.pro +18 -2
  7. package/android/settings.gradle +8 -9
  8. package/android/src/main/java/com/microsoft/codepush/react/BaseBundleStore.java +78 -0
  9. package/android/src/main/java/com/microsoft/codepush/react/CodePush.java +15 -4
  10. package/android/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +2 -0
  11. package/android/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +74 -18
  12. package/android/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +56 -11
  13. package/android/src/main/java/com/microsoft/codepush/react/FileUtils.java +7 -5
  14. package/docs/setup-android.md +27 -1
  15. package/ios/CodePush/CodePush.h +14 -0
  16. package/ios/CodePush/CodePush.m +17 -44
  17. package/ios/CodePush/CodePushBaseBundleStore.m +104 -0
  18. package/ios/CodePush/CodePushPackage.m +82 -9
  19. package/ios/CodePush.xcodeproj/project.pbxproj +6 -0
  20. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/DiffUpdates +0 -0
  21. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/Headers/DiffUpdates.h +18 -0
  22. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/Headers/hpatch_objc.h +23 -0
  23. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/Info.plist +0 -0
  24. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/DiffUpdates +0 -0
  25. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/Headers/DiffUpdates.h +18 -0
  26. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/Headers/hpatch_objc.h +23 -0
  27. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/Info.plist +0 -0
  28. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/_CodeSignature/CodeDirectory +0 -0
  29. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/_CodeSignature/CodeResources +12 -9
  30. package/package.json +10 -14
  31. package/tsconfig.json +1 -4
  32. package/typings/react-native-code-push.d.ts +18 -0
  33. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/_CodeSignature/CodeRequirements-1 +0 -0
  34. package/scripts/postlink/android/postlink.js +0 -87
  35. package/scripts/postlink/ios/postlink.js +0 -116
  36. package/scripts/postlink/run.js +0 -11
  37. package/scripts/postunlink/android/postunlink.js +0 -74
  38. package/scripts/postunlink/ios/postunlink.js +0 -87
  39. package/scripts/postunlink/run.js +0 -11
  40. package/scripts/tools/linkToolsAndroid.js +0 -57
  41. package/scripts/tools/linkToolsIos.js +0 -130
@@ -3,7 +3,11 @@
3
3
  "allow": [
4
4
  "Bash(git:*)",
5
5
  "Bash(echo \"EXIT:$?\")",
6
- "Bash(find:*)"
6
+ "Bash(find:*)",
7
+ "Bash(cd ios/CodePush)",
8
+ "Bash(grep -rln \"finalizePatchAndZip\\\\|DiffDownloadHandler\\\\|HDIFF\\\\|hpatch\\\\|createZipFileAtPath\\\\|bundleDiffBlob\" .)",
9
+ "Bash(grep -rln \"finalizePatchAndZip\\\\|DiffDownloadHandler\\\\|hpatch\\\\|HDIFF\\\\|createZipFileAtPath\" ios)",
10
+ "Bash(grep -v \".framework/DiffUpdates$\")"
7
11
  ]
8
12
  }
9
13
  }
package/README.md CHANGED
@@ -53,7 +53,8 @@ We try our best to maintain backwards compatibility of our plugin with previous
53
53
  | 0.76, 0.77, 0.78, 0.79 | v1.0+ *(Support both New and Old Architectures)* |
54
54
  | v0.80 | v1.2 |
55
55
  | Expo sdk 52 | v1.3 |
56
- | v0.81 | v1.5 |
56
+ | v0.81, v0.82 | v1.5 |
57
+ | v0.83, expo 55 | v1.6 |
57
58
 
58
59
 
59
60
  We work hard to respond to new RN releases, but they do occasionally break us. We will update this chart with each RN release, so that users can check to see what our "official" support is.
@@ -43,12 +43,20 @@ android {
43
43
  lint {
44
44
  abortOnError = false
45
45
  }
46
+
47
+ testOptions {
48
+ unitTests.returnDefaultValues = true
49
+ unitTests.all {
50
+ testLogging { events "passed", "skipped", "failed" }
51
+ }
52
+ }
46
53
  }
47
54
 
48
55
  dependencies {
49
56
  if (boolProp("REACT_NATIVE_REVOPUSH_DEV_MODE", false)) {
50
57
  // for local dev only
51
58
  implementation("org.revopush:revopush-diff")
59
+ // @react-native/gradle-plugin in package.json must use this same version
52
60
  implementation 'com.facebook.react:react-android:0.79.6'
53
61
  // end of local dev only
54
62
  } else {
@@ -56,9 +64,12 @@ dependencies {
56
64
  // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
57
65
  //noinspection GradleDynamicVersion
58
66
 
59
- implementation "org.revopush:revopush-diff:0.0.7"
67
+ implementation "org.revopush:revopush-diff:0.0.9"
60
68
  implementation "com.facebook.react:react-native:+"
61
69
  }
70
+
71
+ testImplementation 'junit:junit:4.13.2'
72
+ testImplementation 'org.json:json:20231013'
62
73
  }
63
74
 
64
75
  def getExtOrIntegerDefault(name, defaultValue) {
@@ -5,7 +5,6 @@ import java.nio.file.Paths;
5
5
  def config = project.extensions.findByName("react") ?: [:]
6
6
  def bundleAssetName = config.bundleAssetName ? config.bundleAssetName.get() : "index.android.bundle"
7
7
 
8
- // because elvis operator
9
8
  def elvisFile(thing) {
10
9
  return thing ? file(thing) : null;
11
10
  }
@@ -18,30 +17,12 @@ void runBefore(String dependentTaskName, Task task) {
18
17
  }
19
18
 
20
19
  /**
21
- * Finds the path of the installed npm package with the given name using Node's
22
- * module resolution algorithm, which searches "node_modules" directories up to
23
- * the file system root. This handles various cases, including:
24
- *
25
- * - Working in the open-source RN repo:
26
- * Gradle: /path/to/react-native/ReactAndroid
27
- * Node module: /path/to/react-native/node_modules/[package]
28
- *
29
- * - Installing RN as a dependency of an app and searching for hoisted
30
- * dependencies:
31
- * Gradle: /path/to/app/node_modules/react-native/ReactAndroid
32
- * Node module: /path/to/app/node_modules/[package]
33
- *
34
- * - Working in a larger repo (e.g., Facebook) that contains RN:
35
- * Gradle: /path/to/repo/path/to/react-native/ReactAndroid
36
- * Node module: /path/to/repo/node_modules/[package]
37
- *
38
- * The search begins at the given base directory (a File object). The returned
39
- * path is a string.
20
+ * Resolves an installed npm package the way Node does: walk up from baseDir looking for
21
+ * node_modules/<packageName>. Covers app, hoisted and monorepo layouts alike.
40
22
  */
41
23
  static def findNodeModulePath(baseDir, packageName) {
42
24
  def basePath = baseDir.toPath().normalize()
43
- // Node's module resolution algorithm searches up to the root directory,
44
- // after which the base path will be null
25
+ // null once the walk passes the filesystem root
45
26
  while (basePath) {
46
27
  def candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
47
28
  if (candidatePath.toFile().exists()) {
@@ -56,7 +37,6 @@ gradle.projectsEvaluated {
56
37
  def debuggableVariants = config.debuggableVariants ? config.debuggableVariants.get() : ['debug']
57
38
 
58
39
  android.applicationVariants.all { variant ->
59
- // No code push for debuggable variants
60
40
  if (debuggableVariants.contains(variant.name)) {
61
41
  return;
62
42
  }
@@ -75,11 +55,9 @@ gradle.projectsEvaluated {
75
55
  def resourcesDir;
76
56
  def jsBundleFile;
77
57
 
78
- // Additional node commandline arguments
79
58
  def nodeExecutableAndArgs = config.nodeExecutableAndArgs ? config.nodeExecutableAndArgs.get() : ["node"]
80
59
  def extraPackagerArgs = config.extraPackagerArgs ? config.extraPackagerArgs.get() : []
81
60
 
82
- // Make this task run right after the bundle task
83
61
  def generateBundledResourcesHash;
84
62
 
85
63
  def reactBundleTask = tasks.findByName("createBundle${targetName}JsAndAssets")
@@ -99,8 +77,6 @@ gradle.projectsEvaluated {
99
77
  name: "generateBundledResourcesHash${targetName}",
100
78
  type: Exec) {
101
79
  commandLine(*nodeExecutableAndArgs, "${nodeModulesPath}/scripts/generateBundledResourcesHash.js", resourcesDir, jsBundleFile, jsBundleDir)
102
-
103
- enabled !debuggableVariants.contains(variant.name) ?: targetName.toLowerCase().contains("release")
104
80
  }
105
81
 
106
82
  runBefore("merge${targetName}Resources", generateBundledResourcesHash)
@@ -114,9 +90,7 @@ gradle.projectsEvaluated {
114
90
  resourcesDir = elvisFile(config."${resourcesDirConfigName}") ? elvisFile(config."${resourcesDirConfigName}").get() :
115
91
  file("$buildDir/intermediates/res/merged/${targetPath}")
116
92
 
117
- // In case version of 'Android Plugin for Gradle'' is lower than 1.3.0
118
- // '$buildDir' has slightly different structure - 'merged' folder
119
- // does not exists so '${targetPath}' folder contains directly in 'res' folder.
93
+ // AGP below 1.3.0 had no 'merged' folder: res/${targetPath} directly.
120
94
  if (!resourcesDir.exists() && file("$buildDir/intermediates/res/${targetPath}").exists()) {
121
95
  resourcesDir = file("$buildDir/intermediates/res/${targetPath}")
122
96
  }
@@ -131,7 +105,6 @@ gradle.projectsEvaluated {
131
105
  commandLine(*nodeExecutableAndArgs, "${nodeModulesPath}/scripts/generateBundledResourcesHash.js", resourcesDir, jsBundleFile, jsBundleDir, resourcesMapTempFileName)
132
106
  }
133
107
 
134
- // Make this task run right before the bundle task
135
108
  def recordFilesBeforeBundleCommand = tasks.create(
136
109
  name: "recordFilesBeforeBundleCommand${targetName}",
137
110
  type: Exec) {
@@ -142,7 +115,7 @@ gradle.projectsEvaluated {
142
115
  recordFilesBeforeBundleCommand.dependsOn("merge${targetName}Assets")
143
116
  runBefore("bundle${targetName}JsAndAssets", recordFilesBeforeBundleCommand)
144
117
 
145
- // We need to generate and record the resources map, but we use it to generate the bundle hash
118
+ // the map recorded before bundling is the input to the hash
146
119
  generateBundledResourcesHash.dependsOn("recordFilesBeforeBundleCommand${targetName}")
147
120
  }
148
121
 
@@ -155,20 +128,24 @@ gradle.projectsEvaluated {
155
128
  }
156
129
  }
157
130
 
158
- project.extensions.findByName("androidComponents")?.finalizeDsl { extension ->
159
- // This disables density-based APK splitting for the App Bundle
160
- extension.bundle.density.enableSplit = false
131
+ // withPlugin, not findByName: applying this script above the Android plugin would otherwise
132
+ // silently ship an APK with no CODE_PUSH_APK_BUILD_TIME.
133
+ project.pluginManager.withPlugin("com.android.application") {
134
+ project.extensions.getByName("androidComponents").finalizeDsl { extension ->
135
+ // One density bucket per device would make the build-time resources hash unreproducible.
136
+ extension.bundle.density.enableSplit = false
161
137
 
162
- extension.androidResources.noCompress += "png"
138
+ extension.androidResources.noCompress.add("png")
163
139
 
164
- extension.buildTypes.all { buildType ->
165
- // disable PNG crunching for release builds to align APK resources with what Metro builds
166
- if(!buildType.debuggable){
167
- crunchPngs = false
168
- }
140
+ extension.buildTypes.all { buildType ->
141
+ // crunching would change the bytes Metro already hashed
142
+ if (!buildType.debuggable) {
143
+ buildType.crunchPngs = false
144
+ }
169
145
 
170
- // to prevent incorrect long value restoration from strings.xml we need to wrap it with double quotes
171
- // https://github.com/microsoft/cordova-plugin-code-push/issues/264
172
- buildType.resValue('string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis()))
146
+ // quoted, or the long is restored incorrectly from strings.xml
147
+ // https://github.com/microsoft/cordova-plugin-code-push/issues/264
148
+ buildType.resValue('string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis()))
149
+ }
173
150
  }
174
151
  }
@@ -18,3 +18,5 @@
18
18
  # org.gradle.parallel=true
19
19
 
20
20
  #android.useDeprecatedNdk=true
21
+
22
+ android.useAndroidX=true
@@ -18,16 +18,32 @@
18
18
 
19
19
  # Invoked via reflection, when setting js bundle.
20
20
  -keepclassmembers class com.facebook.react.ReactInstanceManager {
21
- private final ** mBundleLoader;
21
+ private ** mBundleLoader;
22
+ private ** mAttachedReactRoots;
23
+ private ** mAttachedRootViews;
22
24
  }
23
25
 
24
26
  -keepclassmembers class com.facebook.react.runtime.ReactHostImpl {
25
- private final ** reactHostDelegate;
27
+ private ** mReactHostDelegate;
28
+ private ** reactHostDelegate;
26
29
  }
27
30
 
28
31
  -keep interface com.facebook.react.runtime.ReactHostDelegate { *; }
29
32
 
30
33
  -keep class * implements com.facebook.react.runtime.ReactHostDelegate { *; }
31
34
 
35
+ -keepclassmembers class * implements com.facebook.react.runtime.ReactHostDelegate {
36
+ private ** jsBundleLoader;
37
+ private ** _jsBundleLoader;
38
+ }
39
+
40
+ -keepclassmembers interface com.facebook.react.ReactRoot {
41
+ public ** getRootViewGroup();
42
+ }
43
+
44
+ -keepclassmembers class * implements com.facebook.react.ReactRoot {
45
+ public ** getRootViewGroup();
46
+ }
47
+
32
48
  # Can't find referenced class org.bouncycastle.**
33
49
  -dontwarn com.nimbusds.jose.**
@@ -1,3 +1,5 @@
1
+ // Only used when this directory is its own Gradle build root (standalone or monorepo).
2
+ // Autolinked apps include it as a subproject, so their settings file is used instead.
1
3
  pluginManagement {
2
4
  repositories {
3
5
  mavenCentral()
@@ -10,22 +12,19 @@ pluginManagement {
10
12
  id("com.android.library") version "8.8.2" apply false
11
13
  }
12
14
 
13
- // to satisfy apply plugin: "com.facebook.react" as a part of monorepo
14
- def gradlePluginPath = getProperty("REACT_NATIVE_GRADLE_PLUGIN_PATH")
15
- if (gradlePluginPath != null) {
15
+ // Inline, not via a helper: method calls inside pluginManagement {} bind to the block's
16
+ // delegate and never reach this script's own methods. Property access on `settings` does.
17
+ def gradlePluginPath = (settings.hasProperty("REACT_NATIVE_GRADLE_PLUGIN_PATH") ? settings["REACT_NATIVE_GRADLE_PLUGIN_PATH"] : null) ?: System.getenv("REACT_NATIVE_GRADLE_PLUGIN_PATH")
18
+ if (gradlePluginPath) {
16
19
  includeBuild(gradlePluginPath)
17
20
  }
18
21
  }
19
22
 
20
- def diffPath = getProperty("REACT_NATIVE_REVOPUSH_DIFF_PATH")
21
- if (diffPath != null) {
23
+ def diffPath = (settings.hasProperty("REACT_NATIVE_REVOPUSH_DIFF_PATH") ? settings["REACT_NATIVE_REVOPUSH_DIFF_PATH"] : null) ?: System.getenv("REACT_NATIVE_REVOPUSH_DIFF_PATH")
24
+ if (diffPath) {
22
25
  includeBuild(diffPath) {
23
26
  dependencySubstitution {
24
27
  substitute(module("org.revopush:revopush-diff")).using(project(":revopush-diff"))
25
28
  }
26
29
  }
27
30
  }
28
-
29
- def getProperty(propName) {
30
- return settings[propName] ?: System.getenv(propName)
31
- }
@@ -0,0 +1,78 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ import static org.revopush.ota.utils.FileUtils.copyFileIfExists;
4
+
5
+ import java.io.File;
6
+ import java.io.IOException;
7
+ import java.util.Arrays;
8
+
9
+ /** Bundles kept as diff bases, named by the package hash of the release they came from. */
10
+ public class BaseBundleStore {
11
+
12
+ public static final int MAX_SAVED_BASES = 3;
13
+
14
+ private final File mBasesDir;
15
+
16
+ public BaseBundleStore(String basesDirPath) {
17
+ mBasesDir = new File(basesDirPath);
18
+ }
19
+
20
+ public File lookup(String packageHash) {
21
+ if (!isValidEntryName(packageHash)) return null;
22
+ File file = new File(mBasesDir, packageHash);
23
+ return file.isFile() ? file : null;
24
+ }
25
+
26
+ /**
27
+ * Copies the bundle into the store, so it outlives its package folder. A hard link would
28
+ * be cheaper, but SELinux denies untrusted_app the link permission on its own files.
29
+ * Never fails an update.
30
+ */
31
+ public boolean save(File sourceBundle, String packageHash) {
32
+ if (sourceBundle == null || !sourceBundle.isFile()) return false;
33
+ if (!isValidEntryName(packageHash)) return false;
34
+
35
+ File dest = new File(mBasesDir, packageHash);
36
+ if (dest.isFile()) {
37
+ // Saving is the moment the base gets used, so this keeps evict() ordered by last use.
38
+ dest.setLastModified(System.currentTimeMillis());
39
+ return true;
40
+ }
41
+
42
+ // Copied aside and renamed: a copy cut short by process death must not leave a truncated
43
+ // file under the hash, which lookup() would hand out forever.
44
+ File temp = new File(mBasesDir, packageHash + ".tmp");
45
+ try {
46
+ if (!copyFileIfExists(sourceBundle, temp) || !temp.renameTo(dest)) {
47
+ FileUtils.deleteFileOrFolderSilently(temp);
48
+ return false;
49
+ }
50
+ } catch (IOException e) {
51
+ CodePushUtils.log("Could not save base bundle: " + e.getMessage());
52
+ FileUtils.deleteFileOrFolderSilently(temp);
53
+ return false;
54
+ }
55
+ evict();
56
+ return true;
57
+ }
58
+
59
+ /** The hash names a file directly inside the store, so it may not steer out of it. */
60
+ private static boolean isValidEntryName(String packageHash) {
61
+ return packageHash != null
62
+ && !packageHash.isEmpty()
63
+ && !packageHash.equals(".")
64
+ && !packageHash.equals("..")
65
+ && packageHash.indexOf('/') < 0
66
+ && packageHash.indexOf('\\') < 0;
67
+ }
68
+
69
+ /** Keeps the MAX_SAVED_BASES most recently used entries. */
70
+ private void evict() {
71
+ File[] files = mBasesDir.listFiles();
72
+ if (files == null || files.length <= MAX_SAVED_BASES) return;
73
+ Arrays.sort(files, (a, b) -> Long.compare(a.lastModified(), b.lastModified()));
74
+ for (int i = 0; i < files.length - MAX_SAVED_BASES; i++) {
75
+ FileUtils.deleteFileOrFolderSilently(files[i]);
76
+ }
77
+ }
78
+ }
@@ -4,6 +4,7 @@ import android.content.Context;
4
4
  import android.content.pm.PackageInfo;
5
5
  import android.content.pm.PackageManager;
6
6
  import android.content.res.Resources;
7
+ import android.os.Build;
7
8
 
8
9
  import com.facebook.react.ReactHost;
9
10
  import com.facebook.react.ReactInstanceManager;
@@ -44,7 +45,7 @@ public class CodePush implements ReactPackage {
44
45
 
45
46
  private boolean mDidUpdate = false;
46
47
 
47
- private String mAssetsBundleFileName;
48
+ private String mAssetsBundleFileName = CodePushConstants.DEFAULT_JS_BUNDLE_NAME;
48
49
 
49
50
  // Helper classes.
50
51
  private CodePushUpdateManager mUpdateManager;
@@ -83,11 +84,13 @@ public class CodePush implements ReactPackage {
83
84
  mSettingsManager = new SettingsManager(mContext);
84
85
  bundleMetadataManager = new NativeBundleManager(mContext);
85
86
 
86
- if (sAppVersion == null) {
87
+ // Guarded per value: overrideAppVersion is documented to run before this constructor, and
88
+ // must not stop the build number from being read (and vice versa).
89
+ if (sAppVersion == null || sBuildNumber == null) {
87
90
  try {
88
91
  PackageInfo pInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
89
- sAppVersion = pInfo.versionName;
90
- sBuildNumber = String.valueOf(pInfo.versionCode);
92
+ if (sAppVersion == null) sAppVersion = pInfo.versionName;
93
+ if (sBuildNumber == null) sBuildNumber = String.valueOf(getVersionCode(pInfo));
91
94
  } catch (PackageManager.NameNotFoundException e) {
92
95
  throw new CodePushUnknownException("Unable to get package info for " + mContext.getPackageName(), e);
93
96
  }
@@ -127,6 +130,14 @@ public class CodePush implements ReactPackage {
127
130
  mServerUrl = serverUrl;
128
131
  }
129
132
 
133
+ // versionCode is only the low half of the version code once versionCodeMajor is set.
134
+ private static long getVersionCode(PackageInfo packageInfo) {
135
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
136
+ return packageInfo.getLongVersionCode();
137
+ }
138
+ return packageInfo.versionCode;
139
+ }
140
+
130
141
  private String getPublicKeyByResourceDescriptor(int publicKeyResourceDescriptor) {
131
142
  String publicKey;
132
143
  try {
@@ -18,6 +18,8 @@ public class CodePushConstants {
18
18
  public static final String ASSET_DOWNLOAD_URL_KEY = "assetDownloadUrl";
19
19
  public static final String BUNDLE_DOWNLOAD_URL_KEY = "bundleBlobUrl";
20
20
  public static final String BUNDLE_DIFF_DOWNLOAD_URL_KEY = "bundleDiffBlobUrl";
21
+ public static final String BASE_PACKAGE_KEY = "basePackage";
22
+ public static final String BASES_FOLDER_NAME = "bases";
21
23
 
22
24
  public static final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES";
23
25
  public static final String PACKAGE_FILE_NAME = BundleManager.PACKAGE_FILE_NAME;
@@ -5,15 +5,15 @@ import android.content.SharedPreferences;
5
5
  import android.os.AsyncTask;
6
6
  import android.os.Handler;
7
7
  import android.os.Looper;
8
- import android.view.View;
9
8
  import android.view.Choreographer;
9
+ import android.view.View;
10
+ import android.view.ViewGroup;
10
11
 
11
12
  import androidx.annotation.OptIn;
12
13
 
13
14
  import com.facebook.react.ReactApplication;
14
15
  import com.facebook.react.ReactHost;
15
16
  import com.facebook.react.ReactInstanceManager;
16
- import com.facebook.react.ReactRootView;
17
17
  import com.facebook.react.bridge.Arguments;
18
18
  import com.facebook.react.bridge.BaseJavaModule;
19
19
  import com.facebook.react.bridge.JSBundleLoader;
@@ -43,7 +43,6 @@ import java.lang.reflect.Method;
43
43
  import java.util.ArrayList;
44
44
  import java.util.Date;
45
45
  import java.util.HashMap;
46
- import java.util.List;
47
46
  import java.util.Map;
48
47
  import java.util.UUID;
49
48
 
@@ -58,6 +57,7 @@ public class CodePushNativeModule extends BaseJavaModule {
58
57
  private SettingsManager mSettingsManager;
59
58
  private CodePushTelemetryManager mTelemetryManager;
60
59
  private CodePushUpdateManager mUpdateManager;
60
+ private boolean mIsExpoApp = false;
61
61
 
62
62
  private boolean _allowed = true;
63
63
  private boolean _restartInProgress = false;
@@ -70,6 +70,8 @@ public class CodePushNativeModule extends BaseJavaModule {
70
70
  mSettingsManager = settingsManager;
71
71
  mTelemetryManager = codePushTelemetryManager;
72
72
  mUpdateManager = codePushUpdateManager;
73
+ mIsExpoApp = detectExpoEnvironment();
74
+
73
75
  // Initialize module state while we have a reference to the current context.
74
76
 
75
77
  mNativeBundleHash = bundleMetadataManager.getNativeBundleHash();
@@ -119,6 +121,31 @@ public class CodePushNativeModule extends BaseJavaModule {
119
121
  });
120
122
  }
121
123
 
124
+ private boolean detectExpoEnvironment() {
125
+ try {
126
+ Class.forName("expo.modules.ReactNativeHostWrapper", false, getClass().getClassLoader());
127
+ return true;
128
+ } catch (Throwable e) {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ private Field findField(Class<?> clazz, String... fieldNames) throws NoSuchFieldException {
134
+ Class<?> currentClass = clazz;
135
+ while (currentClass != null) {
136
+ for (String fieldName : fieldNames) {
137
+ try {
138
+ return currentClass.getDeclaredField(fieldName);
139
+ } catch (NoSuchFieldException ignored) {
140
+ }
141
+ }
142
+
143
+ currentClass = currentClass.getSuperclass();
144
+ }
145
+
146
+ throw new NoSuchFieldException(fieldNames.length > 0 ? fieldNames[0] : "");
147
+ }
148
+
122
149
  // Use reflection to find and set the appropriate fields on ReactInstanceManager. See #556 for a proposal for a less brittle way
123
150
  // to approach this.
124
151
  private void setJSBundle(ReactInstanceManager instanceManager, String latestJSBundleFile) throws IllegalAccessException {
@@ -130,7 +157,7 @@ public class CodePushNativeModule extends BaseJavaModule {
130
157
  latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile);
131
158
  }
132
159
 
133
- Field bundleLoaderField = instanceManager.getClass().getDeclaredField("mBundleLoader");
160
+ Field bundleLoaderField = findField(instanceManager.getClass(), "mBundleLoader");
134
161
  bundleLoaderField.setAccessible(true);
135
162
  bundleLoaderField.set(instanceManager, latestJSBundleLoader);
136
163
  } catch (Exception e) {
@@ -150,12 +177,10 @@ public class CodePushNativeModule extends BaseJavaModule {
150
177
  latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile);
151
178
  }
152
179
 
153
- Field bundleLoaderField = reactHostDelegate.getClass().getDeclaredField("jsBundleLoader");
180
+ Field bundleLoaderField = findField(reactHostDelegate.getClass(), "jsBundleLoader", "_jsBundleLoader");
154
181
  bundleLoaderField.setAccessible(true);
155
182
  bundleLoaderField.set(reactHostDelegate, latestJSBundleLoader);
156
- } catch (NoSuchFieldException noSuchFileFound) {
157
- // Ignore this error for Expo
158
- } catch (Exception e) {
183
+ } catch (Exception e) {
159
184
  CodePushUtils.log("Unable to set JSBundle of ReactHostDelegate - CodePush may not support this version of React Native");
160
185
  throw new IllegalAccessException("Could not setJSBundle");
161
186
  }
@@ -200,7 +225,9 @@ public class CodePushNativeModule extends BaseJavaModule {
200
225
  }
201
226
  }
202
227
  } catch (Exception e) {
203
- CodePushUtils.log("Exception setJSBundle: " + e.getMessage());
228
+ if (!mIsExpoApp) {
229
+ CodePushUtils.log("Exception setJSBundle: " + e.getMessage());
230
+ }
204
231
  }
205
232
 
206
233
  // #3) Get the context creation method
@@ -301,14 +328,38 @@ public class CodePushNativeModule extends BaseJavaModule {
301
328
  // resetReactRootViews allows to call recreateReactContextInBackground without any exceptions
302
329
  // This fix also relates to https://github.com/microsoft/react-native-code-push/issues/878
303
330
  private void resetReactRootViews(ReactInstanceManager instanceManager) throws NoSuchFieldException, IllegalAccessException {
304
- Field mAttachedRootViewsField = instanceManager.getClass().getDeclaredField("mAttachedRootViews");
305
- mAttachedRootViewsField.setAccessible(true);
306
- List<ReactRootView> mAttachedRootViews = (List<ReactRootView>) mAttachedRootViewsField.get(instanceManager);
307
- for (ReactRootView reactRootView : mAttachedRootViews) {
308
- reactRootView.removeAllViews();
309
- reactRootView.setId(View.NO_ID);
331
+ Field attachedRootsField = findField(instanceManager.getClass(), "mAttachedReactRoots", "mAttachedRootViews");
332
+ attachedRootsField.setAccessible(true);
333
+ Object attachedRoots = attachedRootsField.get(instanceManager);
334
+ if (attachedRoots instanceof Iterable) {
335
+ for (Object reactRoot : (Iterable<?>) attachedRoots) {
336
+ resetReactRootView(reactRoot);
337
+ }
338
+ }
339
+ }
340
+
341
+ private void resetReactRootView(Object reactRoot) {
342
+ try {
343
+ View rootView = null;
344
+ if (reactRoot instanceof View) {
345
+ rootView = (View) reactRoot;
346
+ } else {
347
+ Method getRootViewGroupMethod = reactRoot.getClass().getMethod("getRootViewGroup");
348
+ Object rootViewGroup = getRootViewGroupMethod.invoke(reactRoot);
349
+ if (rootViewGroup instanceof View) {
350
+ rootView = (View) rootViewGroup;
351
+ }
352
+ }
353
+
354
+ if (rootView instanceof ViewGroup) {
355
+ ((ViewGroup) rootView).removeAllViews();
356
+ }
357
+ if (rootView != null) {
358
+ rootView.setId(View.NO_ID);
359
+ }
360
+ } catch (Exception e) {
361
+ CodePushUtils.log("Failed to reset root view: " + e.getMessage());
310
362
  }
311
- mAttachedRootViewsField.set(instanceManager, mAttachedRootViews);
312
363
  }
313
364
 
314
365
  private void clearLifecycleEventListener() {
@@ -690,7 +741,12 @@ public class CodePushNativeModule extends BaseJavaModule {
690
741
  if (installMode == CodePushInstallMode.IMMEDIATE.getValue()
691
742
  || durationInBackground >= CodePushNativeModule.this.mMinimumBackgroundDuration) {
692
743
  CodePushUtils.log("Loading bundle on resume");
693
- restartAppInternal(false);
744
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
745
+ @Override
746
+ public void run() {
747
+ restartAppInternal(false);
748
+ }
749
+ });
694
750
  }
695
751
  }
696
752
  }
@@ -856,7 +912,7 @@ public class CodePushNativeModule extends BaseJavaModule {
856
912
  field = clazz.getDeclaredField("mReactHostDelegate");
857
913
  } catch (NoSuchFieldException e) {
858
914
  // RN >= 0.81
859
- field = clazz.getDeclaredField("reactHostDelegate");
915
+ field = findField(reactHostImpl.getClass(), "mReactHostDelegate", "reactHostDelegate");
860
916
  }
861
917
  field.setAccessible(true);
862
918