├── .flowconfig ├── .gitignore ├── README.md ├── android └── app │ ├── proguard-rules.pro │ └── src │ └── main │ └── java │ └── com │ └── microsoft │ └── codepush │ └── react │ ├── CodePush.java │ ├── CodePushConstants.java │ ├── CodePushDialog.java │ ├── CodePushInstallMode.java │ ├── CodePushInvalidUpdateException.java │ ├── CodePushMalformedDataException.java │ ├── CodePushNativeModule.java │ ├── CodePushNotInitializedException.java │ ├── CodePushTelemetryManager.java │ ├── CodePushUnknownException.java │ ├── CodePushUpdateManager.java │ ├── CodePushUpdateState.java │ ├── CodePushUpdateUtils.java │ ├── CodePushUtils.java │ ├── DownloadProgress.java │ ├── DownloadProgressCallback.java │ ├── FileUtils.java │ ├── ReactInstanceHolder.java │ └── SettingsManager.java ├── app ├── .gitignore ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── amalbit │ │ └── testandroidapp │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── java │ │ └── com │ │ │ └── amalbit │ │ │ └── testandroidapp │ │ │ ├── ApplicationTest.java │ │ │ ├── BaseReactActivity.java │ │ │ ├── HybridActivity.java │ │ │ ├── Launch2Activity.java │ │ │ ├── ReactCallBackActivity.java │ │ │ ├── ViewModuleActivity.java │ │ │ ├── callback │ │ │ ├── CallbackModule.java │ │ │ └── CallbackPackage.java │ │ │ ├── helpers │ │ │ └── ReactUtils.java │ │ │ ├── module │ │ │ └── package1 │ │ │ │ ├── MessageFromReactToAndroidModule.java │ │ │ │ ├── TestPackage.java │ │ │ │ └── ToastOneModule.java │ │ │ └── viewmodule │ │ │ ├── CustomLayout.java │ │ │ ├── CustomUIPackage.java │ │ │ └── ReactLayoutManager.java │ └── res │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── mipmap-xxxhdpi │ │ └── ic_launcher.png │ └── test │ └── java │ └── com │ └── amalbit │ └── testandroidapp │ └── ExampleUnitTest.java ├── base.apk ├── communication_react_android.gif ├── index.android.js ├── npm-debug.log.1692254132 ├── package.json └── reactapp ├── Callback.js ├── CallbackUI.js ├── CustomUIFromAndroid.js ├── MessageFromReact.js ├── NativeUiModuleUI.js ├── ToastOneAndroid.js └── images ├── header_logo.png └── laptop_phone_howitworks.png /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore templates for 'react-native init' 6 | .*/local-cli/templates/.* 7 | 8 | ; Ignore the website subdir 9 | /website/.* 10 | 11 | ; Ignore "BUCK" generated dirs 12 | /\.buckd/ 13 | 14 | ; Ignore unexpected extra "@providesModule" 15 | .*/node_modules/.*/node_modules/fbjs/.* 16 | 17 | ; Ignore duplicate module providers 18 | ; For RN Apps installed via npm, "Libraries" folder is inside 19 | ; "node_modules/react-native" but in the source repo it is in the root 20 | .*/Libraries/react-native/React.js 21 | .*/Libraries/react-native/ReactNative.js 22 | 23 | [include] 24 | 25 | [libs] 26 | Libraries/react-native/react-native-interface.js 27 | flow/ 28 | 29 | [options] 30 | emoji=true 31 | 32 | module.system=haste 33 | 34 | experimental.strict_type_args=true 35 | 36 | munge_underscores=true 37 | 38 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 39 | 40 | suppress_type=$FlowIssue 41 | suppress_type=$FlowFixMe 42 | suppress_type=$FixMe 43 | 44 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-1]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\) 45 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-1]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)?:? #[0-9]+ 46 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 47 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 48 | 49 | unsafe.enable_getters_and_setters=true 50 | 51 | [version] 52 | ^0.41.0 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | 11 | # Android/IntelliJ 12 | # 13 | build/ 14 | .idea 15 | .gradle 16 | local.properties 17 | *.iml 18 | 19 | # node.js 20 | # 21 | node_modules/ 22 | npm-debug.log 23 | yarn-error.log 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ezgif com-video-to-gif](https://github.com/amalChandran/ReactNative_Android_integration/blob/master/communication_react_android.gif) 2 | 3 | For getting started: 4 | ============ 5 | 6 | https://facebook.github.io/react-native/docs/getting-started.html#content 7 | 8 | 9 | Integrating react native within an existing app required constant communication(synchronous and asynchrounus) between react and native components. This project has examples that do the same. 10 | We use Native modules to talk between android and react native modules. 11 | 12 | 13 | Clone the repo and run nmp init to download all the node dependancies. 14 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Invoked via reflection, when forcing javascript restarts. 20 | -keepclassmembers class com.facebook.react.ReactInstanceManagerImpl { 21 | void recreateReactContextInBackground(); 22 | } 23 | 24 | -keepclassmembers class com.facebook.react.XReactInstanceManagerImpl { 25 | void recreateReactContextInBackground(); 26 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePush.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import com.facebook.react.ReactInstanceManager; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.bridge.JavaScriptModule; 6 | import com.facebook.react.bridge.NativeModule; 7 | import com.facebook.react.bridge.ReactApplicationContext; 8 | import com.facebook.react.uimanager.ViewManager; 9 | 10 | import android.content.Context; 11 | import android.content.pm.ApplicationInfo; 12 | import android.content.pm.PackageInfo; 13 | import android.content.pm.PackageManager; 14 | 15 | import org.json.JSONException; 16 | import org.json.JSONObject; 17 | 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.zip.ZipEntry; 23 | import java.util.zip.ZipFile; 24 | 25 | public class CodePush implements ReactPackage { 26 | 27 | private static boolean sIsRunningBinaryVersion = false; 28 | private static boolean sNeedToReportRollback = false; 29 | private static boolean sTestConfigurationFlag = false; 30 | private static String sAppVersion = null; 31 | 32 | private boolean mDidUpdate = false; 33 | 34 | private String mAssetsBundleFileName; 35 | 36 | // Helper classes. 37 | private CodePushUpdateManager mUpdateManager; 38 | private CodePushTelemetryManager mTelemetryManager; 39 | private SettingsManager mSettingsManager; 40 | 41 | // Config properties. 42 | private String mDeploymentKey; 43 | private String mServerUrl = "https://codepush.azurewebsites.net/"; 44 | 45 | private Context mContext; 46 | private final boolean mIsDebugMode; 47 | 48 | private static ReactInstanceHolder mReactInstanceHolder; 49 | private static CodePush mCurrentInstance; 50 | 51 | public CodePush(String deploymentKey, Context context) { 52 | this(deploymentKey, context, false); 53 | } 54 | 55 | public CodePush(String deploymentKey, Context context, boolean isDebugMode) { 56 | mContext = context.getApplicationContext(); 57 | 58 | mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath()); 59 | mTelemetryManager = new CodePushTelemetryManager(mContext); 60 | mDeploymentKey = deploymentKey; 61 | mIsDebugMode = isDebugMode; 62 | mSettingsManager = new SettingsManager(mContext); 63 | 64 | if (sAppVersion == null) { 65 | try { 66 | PackageInfo pInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0); 67 | sAppVersion = pInfo.versionName; 68 | } catch (PackageManager.NameNotFoundException e) { 69 | throw new CodePushUnknownException("Unable to get package info for " + mContext.getPackageName(), e); 70 | } 71 | } 72 | 73 | mCurrentInstance = this; 74 | 75 | clearDebugCacheIfNeeded(); 76 | initializeUpdateAfterRestart(); 77 | } 78 | 79 | public CodePush(String deploymentKey, Context context, boolean isDebugMode, String serverUrl) { 80 | this(deploymentKey, context, isDebugMode); 81 | mServerUrl = serverUrl; 82 | } 83 | 84 | public void clearDebugCacheIfNeeded() { 85 | if (mIsDebugMode && mSettingsManager.isPendingUpdate(null)) { 86 | // This needs to be kept in sync with https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java#L78 87 | File cachedDevBundle = new File(mContext.getFilesDir(), "ReactNativeDevBundle.js"); 88 | if (cachedDevBundle.exists()) { 89 | cachedDevBundle.delete(); 90 | } 91 | } 92 | } 93 | 94 | public boolean didUpdate() { 95 | return mDidUpdate; 96 | } 97 | 98 | public String getAppVersion() { 99 | return sAppVersion; 100 | } 101 | 102 | public String getAssetsBundleFileName() { 103 | return mAssetsBundleFileName; 104 | } 105 | 106 | long getBinaryResourcesModifiedTime() { 107 | try { 108 | String packageName = this.mContext.getPackageName(); 109 | int codePushApkBuildTimeId = this.mContext.getResources().getIdentifier(CodePushConstants.CODE_PUSH_APK_BUILD_TIME_KEY, "string", packageName); 110 | String codePushApkBuildTime = this.mContext.getResources().getString(codePushApkBuildTimeId); 111 | return Long.parseLong(codePushApkBuildTime); 112 | } catch (Exception e) { 113 | throw new CodePushUnknownException("Error in getting binary resources modified time", e); 114 | } 115 | } 116 | 117 | @Deprecated 118 | public static String getBundleUrl() { 119 | return getJSBundleFile(); 120 | } 121 | 122 | @Deprecated 123 | public static String getBundleUrl(String assetsBundleFileName) { 124 | return getJSBundleFile(assetsBundleFileName); 125 | } 126 | 127 | public Context getContext() { 128 | return mContext; 129 | } 130 | 131 | public String getDeploymentKey() { 132 | return mDeploymentKey; 133 | } 134 | 135 | public static String getJSBundleFile() { 136 | return CodePush.getJSBundleFile(CodePushConstants.DEFAULT_JS_BUNDLE_NAME); 137 | } 138 | 139 | public static String getJSBundleFile(String assetsBundleFileName) { 140 | if (mCurrentInstance == null) { 141 | throw new CodePushNotInitializedException("A CodePush instance has not been created yet. Have you added it to your app's list of ReactPackages?"); 142 | } 143 | 144 | return mCurrentInstance.getJSBundleFileInternal(assetsBundleFileName); 145 | } 146 | 147 | public String getJSBundleFileInternal(String assetsBundleFileName) { 148 | this.mAssetsBundleFileName = assetsBundleFileName; 149 | String binaryJsBundleUrl = CodePushConstants.ASSETS_BUNDLE_PREFIX + assetsBundleFileName; 150 | long binaryResourcesModifiedTime = this.getBinaryResourcesModifiedTime(); 151 | 152 | try { 153 | String packageFilePath = mUpdateManager.getCurrentPackageBundlePath(this.mAssetsBundleFileName); 154 | if (packageFilePath == null) { 155 | // There has not been any downloaded updates. 156 | CodePushUtils.logBundleUrl(binaryJsBundleUrl); 157 | sIsRunningBinaryVersion = true; 158 | return binaryJsBundleUrl; 159 | } 160 | 161 | JSONObject packageMetadata = this.mUpdateManager.getCurrentPackage(); 162 | Long binaryModifiedDateDuringPackageInstall = null; 163 | String binaryModifiedDateDuringPackageInstallString = packageMetadata.optString(CodePushConstants.BINARY_MODIFIED_TIME_KEY, null); 164 | if (binaryModifiedDateDuringPackageInstallString != null) { 165 | binaryModifiedDateDuringPackageInstall = Long.parseLong(binaryModifiedDateDuringPackageInstallString); 166 | } 167 | 168 | String packageAppVersion = packageMetadata.optString("appVersion", null); 169 | if (binaryModifiedDateDuringPackageInstall != null && 170 | binaryModifiedDateDuringPackageInstall == binaryResourcesModifiedTime && 171 | (isUsingTestConfiguration() || sAppVersion.equals(packageAppVersion))) { 172 | CodePushUtils.logBundleUrl(packageFilePath); 173 | sIsRunningBinaryVersion = false; 174 | return packageFilePath; 175 | } else { 176 | // The binary version is newer. 177 | this.mDidUpdate = false; 178 | if (!this.mIsDebugMode || !sAppVersion.equals(packageAppVersion)) { 179 | this.clearUpdates(); 180 | } 181 | 182 | CodePushUtils.logBundleUrl(binaryJsBundleUrl); 183 | sIsRunningBinaryVersion = true; 184 | return binaryJsBundleUrl; 185 | } 186 | } catch (NumberFormatException e) { 187 | throw new CodePushUnknownException("Error in reading binary modified date from package metadata", e); 188 | } 189 | } 190 | 191 | public String getServerUrl() { 192 | return mServerUrl; 193 | } 194 | 195 | void initializeUpdateAfterRestart() { 196 | // Reset the state which indicates that 197 | // the app was just freshly updated. 198 | mDidUpdate = false; 199 | 200 | JSONObject pendingUpdate = mSettingsManager.getPendingUpdate(); 201 | if (pendingUpdate != null) { 202 | try { 203 | boolean updateIsLoading = pendingUpdate.getBoolean(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY); 204 | if (updateIsLoading) { 205 | // Pending update was initialized, but notifyApplicationReady was not called. 206 | // Therefore, deduce that it is a broken update and rollback. 207 | CodePushUtils.log("Update did not finish loading the last time, rolling back to a previous version."); 208 | sNeedToReportRollback = true; 209 | rollbackPackage(); 210 | } else { 211 | // There is in fact a new update running for the first 212 | // time, so update the local state to ensure the client knows. 213 | mDidUpdate = true; 214 | 215 | // Mark that we tried to initialize the new update, so that if it crashes, 216 | // we will know that we need to rollback when the app next starts. 217 | mSettingsManager.savePendingUpdate(pendingUpdate.getString(CodePushConstants.PENDING_UPDATE_HASH_KEY), 218 | /* isLoading */true); 219 | } 220 | } catch (JSONException e) { 221 | // Should not happen. 222 | throw new CodePushUnknownException("Unable to read pending update metadata stored in SharedPreferences", e); 223 | } 224 | } 225 | } 226 | 227 | void invalidateCurrentInstance() { 228 | mCurrentInstance = null; 229 | } 230 | 231 | boolean isDebugMode() { 232 | return mIsDebugMode; 233 | } 234 | 235 | boolean isRunningBinaryVersion() { 236 | return sIsRunningBinaryVersion; 237 | } 238 | 239 | boolean needToReportRollback() { 240 | return sNeedToReportRollback; 241 | } 242 | 243 | public static void overrideAppVersion(String appVersionOverride) { 244 | sAppVersion = appVersionOverride; 245 | } 246 | 247 | private void rollbackPackage() { 248 | JSONObject failedPackage = mUpdateManager.getCurrentPackage(); 249 | mSettingsManager.saveFailedUpdate(failedPackage); 250 | mUpdateManager.rollbackPackage(); 251 | mSettingsManager.removePendingUpdate(); 252 | } 253 | 254 | public void setNeedToReportRollback(boolean needToReportRollback) { 255 | CodePush.sNeedToReportRollback = needToReportRollback; 256 | } 257 | 258 | /* The below 3 methods are used for running tests.*/ 259 | public static boolean isUsingTestConfiguration() { 260 | return sTestConfigurationFlag; 261 | } 262 | 263 | public static void setUsingTestConfiguration(boolean shouldUseTestConfiguration) { 264 | sTestConfigurationFlag = shouldUseTestConfiguration; 265 | } 266 | 267 | public void clearUpdates() { 268 | mUpdateManager.clearUpdates(); 269 | mSettingsManager.removePendingUpdate(); 270 | mSettingsManager.removeFailedUpdates(); 271 | } 272 | 273 | public static void setReactInstanceHolder(ReactInstanceHolder reactInstanceHolder) { 274 | mReactInstanceHolder = reactInstanceHolder; 275 | } 276 | 277 | static ReactInstanceManager getReactInstanceManager() { 278 | if (mReactInstanceHolder == null) { 279 | return null; 280 | } 281 | return mReactInstanceHolder.getReactInstanceManager(); 282 | } 283 | 284 | @Override 285 | public List createNativeModules(ReactApplicationContext reactApplicationContext) { 286 | CodePushNativeModule codePushModule = new CodePushNativeModule(reactApplicationContext, this, mUpdateManager, mTelemetryManager, mSettingsManager); 287 | CodePushDialog dialogModule = new CodePushDialog(reactApplicationContext); 288 | 289 | List nativeModules = new ArrayList<>(); 290 | nativeModules.add(codePushModule); 291 | nativeModules.add(dialogModule); 292 | return nativeModules; 293 | } 294 | 295 | @Override 296 | public List> createJSModules() { 297 | return new ArrayList<>(); 298 | } 299 | 300 | @Override 301 | public List createViewManagers(ReactApplicationContext reactApplicationContext) { 302 | return new ArrayList<>(); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | public class CodePushConstants { 4 | public static final String ASSETS_BUNDLE_PREFIX = "assets://"; 5 | public static final String BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime"; 6 | public static final String CODE_PUSH_FOLDER_PREFIX = "CodePush"; 7 | public static final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json"; 8 | public static final String CODE_PUSH_PREFERENCES = "CodePush"; 9 | public static final String CURRENT_PACKAGE_KEY = "currentPackage"; 10 | public static final String DEFAULT_JS_BUNDLE_NAME = "index.android.bundle"; 11 | public static final String DIFF_MANIFEST_FILE_NAME = "hotcodepush.json"; 12 | public static final int DOWNLOAD_BUFFER_SIZE = 1024 * 256; 13 | public static final String DOWNLOAD_FILE_NAME = "download.zip"; 14 | public static final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress"; 15 | public static final String DOWNLOAD_URL_KEY = "downloadUrl"; 16 | public static final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES"; 17 | public static final String PACKAGE_FILE_NAME = "app.json"; 18 | public static final String PACKAGE_HASH_KEY = "packageHash"; 19 | public static final String PENDING_UPDATE_HASH_KEY = "hash"; 20 | public static final String PENDING_UPDATE_IS_LOADING_KEY = "isLoading"; 21 | public static final String PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE"; 22 | public static final String PREVIOUS_PACKAGE_KEY = "previousPackage"; 23 | public static final String REACT_NATIVE_LOG_TAG = "ReactNative"; 24 | public static final String RELATIVE_BUNDLE_PATH_KEY = "bundlePath"; 25 | public static final String RESOURCES_BUNDLE = "resources.arsc"; 26 | public static final String STATUS_FILE = "codepush.json"; 27 | public static final String UNZIPPED_FOLDER_NAME = "unzipped"; 28 | public static final String CODE_PUSH_APK_BUILD_TIME_KEY = "CODE_PUSH_APK_BUILD_TIME"; 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.content.DialogInterface; 6 | 7 | import com.facebook.react.bridge.Callback; 8 | import com.facebook.react.bridge.LifecycleEventListener; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 11 | import com.facebook.react.bridge.ReactMethod; 12 | 13 | public class CodePushDialog extends ReactContextBaseJavaModule{ 14 | 15 | public CodePushDialog(ReactApplicationContext reactContext) { 16 | super(reactContext); 17 | } 18 | 19 | @ReactMethod 20 | public void showDialog(final String title, final String message, final String button1Text, 21 | final String button2Text, final Callback successCallback, Callback errorCallback) { 22 | Activity currentActivity = getCurrentActivity(); 23 | if (currentActivity == null) { 24 | // If getCurrentActivity is null, it could be because the app is backgrounded, 25 | // so we show the dialog when the app resumes) 26 | getReactApplicationContext().addLifecycleEventListener(new LifecycleEventListener() { 27 | @Override 28 | public void onHostResume() { 29 | Activity currentActivity = getCurrentActivity(); 30 | if (currentActivity != null) { 31 | getReactApplicationContext().removeLifecycleEventListener(this); 32 | showDialogInternal(title, message, button1Text, button2Text, successCallback, currentActivity); 33 | } 34 | } 35 | 36 | @Override 37 | public void onHostPause() { 38 | 39 | } 40 | 41 | @Override 42 | public void onHostDestroy() { 43 | 44 | } 45 | }); 46 | } else { 47 | showDialogInternal(title, message, button1Text, button2Text, successCallback, currentActivity); 48 | } 49 | } 50 | 51 | private void showDialogInternal(String title, String message, String button1Text, 52 | String button2Text, final Callback successCallback, Activity currentActivity) { 53 | AlertDialog.Builder builder = new AlertDialog.Builder(currentActivity); 54 | 55 | builder.setCancelable(false); 56 | 57 | DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { 58 | @Override 59 | public void onClick(DialogInterface dialog, int which) { 60 | dialog.cancel(); 61 | switch (which) { 62 | case DialogInterface.BUTTON_POSITIVE: 63 | successCallback.invoke(0); 64 | break; 65 | case DialogInterface.BUTTON_NEGATIVE: 66 | successCallback.invoke(1); 67 | break; 68 | default: 69 | throw new CodePushUnknownException("Unknown button ID pressed."); 70 | } 71 | } 72 | }; 73 | 74 | if (title != null) { 75 | builder.setTitle(title); 76 | } 77 | 78 | if (message != null) { 79 | builder.setMessage(message); 80 | } 81 | 82 | if (button1Text != null) { 83 | builder.setPositiveButton(button1Text, clickListener); 84 | } 85 | 86 | if (button2Text != null) { 87 | builder.setNegativeButton(button2Text, clickListener); 88 | } 89 | 90 | AlertDialog dialog = builder.create(); 91 | dialog.show(); 92 | } 93 | 94 | @Override 95 | public String getName() { 96 | return "CodePushDialog"; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushInstallMode.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | public enum CodePushInstallMode { 4 | IMMEDIATE(0), 5 | ON_NEXT_RESTART(1), 6 | ON_NEXT_RESUME(2); 7 | 8 | private final int value; 9 | CodePushInstallMode(int value) { 10 | this.value = value; 11 | } 12 | public int getValue() { 13 | return this.value; 14 | } 15 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushInvalidUpdateException.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | public class CodePushInvalidUpdateException extends RuntimeException { 4 | public CodePushInvalidUpdateException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushMalformedDataException.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import java.net.MalformedURLException; 4 | 5 | public class CodePushMalformedDataException extends RuntimeException { 6 | public CodePushMalformedDataException(String path, Throwable cause) { 7 | super("Unable to parse contents of " + path + ", the file may be corrupted.", cause); 8 | } 9 | public CodePushMalformedDataException(String url, MalformedURLException cause) { 10 | super("The package has an invalid downloadUrl: " + url, cause); 11 | } 12 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.os.AsyncTask; 6 | import android.os.Handler; 7 | import android.os.Looper; 8 | import android.provider.Settings; 9 | import android.view.Choreographer; 10 | 11 | import com.facebook.react.ReactActivity; 12 | import com.facebook.react.ReactInstanceManager; 13 | import com.facebook.react.bridge.Arguments; 14 | import com.facebook.react.bridge.LifecycleEventListener; 15 | import com.facebook.react.bridge.Promise; 16 | import com.facebook.react.bridge.ReactApplicationContext; 17 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 18 | import com.facebook.react.bridge.ReactMethod; 19 | import com.facebook.react.bridge.ReadableMap; 20 | import com.facebook.react.bridge.WritableMap; 21 | import com.facebook.react.modules.core.DeviceEventManagerModule; 22 | import com.facebook.react.uimanager.ReactChoreographer; 23 | 24 | import org.json.JSONArray; 25 | import org.json.JSONException; 26 | import org.json.JSONObject; 27 | 28 | import java.io.IOException; 29 | import java.lang.reflect.Field; 30 | import java.lang.reflect.Method; 31 | import java.util.Date; 32 | import java.util.HashMap; 33 | import java.util.Map; 34 | 35 | public class CodePushNativeModule extends ReactContextBaseJavaModule { 36 | private String mBinaryContentsHash = null; 37 | private String mClientUniqueId = null; 38 | private LifecycleEventListener mLifecycleEventListener = null; 39 | private int mMinimumBackgroundDuration = 0; 40 | 41 | private CodePush mCodePush; 42 | private SettingsManager mSettingsManager; 43 | private CodePushTelemetryManager mTelemetryManager; 44 | private CodePushUpdateManager mUpdateManager; 45 | 46 | private static final String REACT_APPLICATION_CLASS_NAME = "com.facebook.react.ReactApplication"; 47 | private static final String REACT_NATIVE_HOST_CLASS_NAME = "com.facebook.react.ReactNativeHost"; 48 | 49 | public CodePushNativeModule(ReactApplicationContext reactContext, CodePush codePush, CodePushUpdateManager codePushUpdateManager, CodePushTelemetryManager codePushTelemetryManager, SettingsManager settingsManager) { 50 | super(reactContext); 51 | 52 | mCodePush = codePush; 53 | mSettingsManager = settingsManager; 54 | mTelemetryManager = codePushTelemetryManager; 55 | mUpdateManager = codePushUpdateManager; 56 | 57 | // Initialize module state while we have a reference to the current context. 58 | mBinaryContentsHash = CodePushUpdateUtils.getHashForBinaryContents(reactContext, mCodePush.isDebugMode()); 59 | mClientUniqueId = Settings.Secure.getString(reactContext.getContentResolver(), Settings.Secure.ANDROID_ID); 60 | } 61 | 62 | @Override 63 | public Map getConstants() { 64 | final Map constants = new HashMap<>(); 65 | 66 | constants.put("codePushInstallModeImmediate", CodePushInstallMode.IMMEDIATE.getValue()); 67 | constants.put("codePushInstallModeOnNextRestart", CodePushInstallMode.ON_NEXT_RESTART.getValue()); 68 | constants.put("codePushInstallModeOnNextResume", CodePushInstallMode.ON_NEXT_RESUME.getValue()); 69 | 70 | constants.put("codePushUpdateStateRunning", CodePushUpdateState.RUNNING.getValue()); 71 | constants.put("codePushUpdateStatePending", CodePushUpdateState.PENDING.getValue()); 72 | constants.put("codePushUpdateStateLatest", CodePushUpdateState.LATEST.getValue()); 73 | 74 | return constants; 75 | } 76 | 77 | @Override 78 | public String getName() { 79 | return "CodePush"; 80 | } 81 | 82 | private void loadBundleLegacy() { 83 | final Activity currentActivity = getCurrentActivity(); 84 | if (currentActivity == null) { 85 | // The currentActivity can be null if it is backgrounded / destroyed, so we simply 86 | // no-op to prevent any null pointer exceptions. 87 | return; 88 | } 89 | mCodePush.invalidateCurrentInstance(); 90 | 91 | currentActivity.runOnUiThread(new Runnable() { 92 | @Override 93 | public void run() { 94 | currentActivity.recreate(); 95 | } 96 | }); 97 | } 98 | 99 | // Use reflection to find and set the appropriate fields on ReactInstanceManager. See #556 for a proposal for a less brittle way 100 | // to approach this. 101 | private void setJSBundle(ReactInstanceManager instanceManager, String latestJSBundleFile) throws NoSuchFieldException, IllegalAccessException { 102 | try { 103 | Field bundleLoaderField = instanceManager.getClass().getDeclaredField("mBundleLoader"); 104 | Class jsBundleLoaderClass = Class.forName("com.facebook.react.cxxbridge.JSBundleLoader"); 105 | Method createFileLoaderMethod = null; 106 | 107 | Method[] methods = jsBundleLoaderClass.getDeclaredMethods(); 108 | for (Method method : methods) { 109 | if (method.getName().equals("createFileLoader")) { 110 | createFileLoaderMethod = method; 111 | break; 112 | } 113 | } 114 | 115 | if (createFileLoaderMethod == null) { 116 | throw new NoSuchMethodException("Could not find a recognized 'createFileLoader' method"); 117 | } 118 | 119 | int numParameters = createFileLoaderMethod.getGenericParameterTypes().length; 120 | Object latestJSBundleLoader; 121 | 122 | if (numParameters == 1) { 123 | // RN >= v0.34 124 | latestJSBundleLoader = createFileLoaderMethod.invoke(jsBundleLoaderClass, latestJSBundleFile); 125 | } else if (numParameters == 2) { 126 | // RN >= v0.31 && RN < v0.34 127 | latestJSBundleLoader = createFileLoaderMethod.invoke(jsBundleLoaderClass, getReactApplicationContext(), latestJSBundleFile); 128 | } else { 129 | throw new NoSuchMethodException("Could not find a recognized 'createFileLoader' method"); 130 | } 131 | 132 | bundleLoaderField.setAccessible(true); 133 | bundleLoaderField.set(instanceManager, latestJSBundleLoader); 134 | } catch (Exception e) { 135 | // RN < v0.31 136 | Field jsBundleField = instanceManager.getClass().getDeclaredField("mJSBundleFile"); 137 | jsBundleField.setAccessible(true); 138 | jsBundleField.set(instanceManager, latestJSBundleFile); 139 | } 140 | } 141 | 142 | private void loadBundle() { 143 | mCodePush.clearDebugCacheIfNeeded(); 144 | try { 145 | // #1) Get the ReactInstanceManager instance, which is what includes the 146 | // logic to reload the current React context. 147 | final ReactInstanceManager instanceManager = resolveInstanceManager(); 148 | if (instanceManager == null) { 149 | return; 150 | } 151 | 152 | String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName()); 153 | 154 | // #2) Update the locally stored JS bundle file path 155 | setJSBundle(instanceManager, latestJSBundleFile); 156 | 157 | // #3) Get the context creation method and fire it on the UI thread (which RN enforces) 158 | final Method recreateMethod = instanceManager.getClass().getMethod("recreateReactContextInBackground"); 159 | new Handler(Looper.getMainLooper()).post(new Runnable() { 160 | @Override 161 | public void run() { 162 | try { 163 | recreateMethod.invoke(instanceManager); 164 | mCodePush.initializeUpdateAfterRestart(); 165 | } catch (Exception e) { 166 | // The recreation method threw an unknown exception 167 | // so just simply fallback to restarting the Activity (if it exists) 168 | loadBundleLegacy(); 169 | } 170 | } 171 | }); 172 | 173 | } catch (Exception e) { 174 | // Our reflection logic failed somewhere 175 | // so fall back to restarting the Activity (if it exists) 176 | loadBundleLegacy(); 177 | } 178 | } 179 | 180 | // Use reflection to find the ReactInstanceManager. See #556 for a proposal for a less brittle way to approach this. 181 | private ReactInstanceManager resolveInstanceManager() throws NoSuchFieldException, IllegalAccessException { 182 | ReactInstanceManager instanceManager = CodePush.getReactInstanceManager(); 183 | if (instanceManager != null) { 184 | return instanceManager; 185 | } 186 | 187 | final Activity currentActivity = getCurrentActivity(); 188 | if (currentActivity == null) { 189 | return null; 190 | } 191 | try { 192 | // In RN >=0.29, the "mReactInstanceManager" field yields a null value, so we try 193 | // to get the instance manager via the ReactNativeHost, which only exists in 0.29. 194 | Method getApplicationMethod = ReactActivity.class.getMethod("getApplication"); 195 | Object reactApplication = getApplicationMethod.invoke(currentActivity); 196 | Class reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME); 197 | Method getReactNativeHostMethod = reactApplicationClass.getMethod("getReactNativeHost"); 198 | Object reactNativeHost = getReactNativeHostMethod.invoke(reactApplication); 199 | Class reactNativeHostClass = tryGetClass(REACT_NATIVE_HOST_CLASS_NAME); 200 | Method getReactInstanceManagerMethod = reactNativeHostClass.getMethod("getReactInstanceManager"); 201 | instanceManager = (ReactInstanceManager)getReactInstanceManagerMethod.invoke(reactNativeHost); 202 | } catch (Exception e) { 203 | // The React Native version might be older than 0.29, or the activity does not 204 | // extend ReactActivity, so we try to get the instance manager via the 205 | // "mReactInstanceManager" field. 206 | Class instanceManagerHolderClass = currentActivity instanceof ReactActivity 207 | ? ReactActivity.class 208 | : currentActivity.getClass(); 209 | Field instanceManagerField = instanceManagerHolderClass.getDeclaredField("mReactInstanceManager"); 210 | instanceManagerField.setAccessible(true); 211 | instanceManager = (ReactInstanceManager)instanceManagerField.get(currentActivity); 212 | } 213 | return instanceManager; 214 | } 215 | 216 | private Class tryGetClass(String className) { 217 | try { 218 | return Class.forName(className); 219 | } catch (ClassNotFoundException e) { 220 | return null; 221 | } 222 | } 223 | 224 | @ReactMethod 225 | public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) { 226 | AsyncTask asyncTask = new AsyncTask() { 227 | @Override 228 | protected Void doInBackground(Void... params) { 229 | try { 230 | JSONObject mutableUpdatePackage = CodePushUtils.convertReadableToJsonObject(updatePackage); 231 | CodePushUtils.setJSONValueForKey(mutableUpdatePackage, CodePushConstants.BINARY_MODIFIED_TIME_KEY, "" + mCodePush.getBinaryResourcesModifiedTime()); 232 | mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() { 233 | private boolean hasScheduledNextFrame = false; 234 | private DownloadProgress latestDownloadProgress = null; 235 | 236 | @Override 237 | public void call(DownloadProgress downloadProgress) { 238 | if (!notifyProgress) { 239 | return; 240 | } 241 | 242 | latestDownloadProgress = downloadProgress; 243 | // If the download is completed, synchronously send the last event. 244 | if (latestDownloadProgress.isCompleted()) { 245 | dispatchDownloadProgressEvent(); 246 | return; 247 | } 248 | 249 | if (hasScheduledNextFrame) { 250 | return; 251 | } 252 | 253 | hasScheduledNextFrame = true; 254 | getReactApplicationContext().runOnUiQueueThread(new Runnable() { 255 | @Override 256 | public void run() { 257 | ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new Choreographer.FrameCallback() { 258 | @Override 259 | public void doFrame(long frameTimeNanos) { 260 | if (!latestDownloadProgress.isCompleted()) { 261 | dispatchDownloadProgressEvent(); 262 | } 263 | 264 | hasScheduledNextFrame = false; 265 | } 266 | }); 267 | } 268 | }); 269 | } 270 | 271 | public void dispatchDownloadProgressEvent() { 272 | getReactApplicationContext() 273 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) 274 | .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap()); 275 | } 276 | }); 277 | 278 | JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY)); 279 | promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage)); 280 | } catch (IOException e) { 281 | e.printStackTrace(); 282 | promise.reject(e); 283 | } catch (CodePushInvalidUpdateException e) { 284 | e.printStackTrace(); 285 | mSettingsManager.saveFailedUpdate(CodePushUtils.convertReadableToJsonObject(updatePackage)); 286 | promise.reject(e); 287 | } 288 | 289 | return null; 290 | } 291 | }; 292 | 293 | asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 294 | } 295 | 296 | @ReactMethod 297 | public void getConfiguration(Promise promise) { 298 | WritableMap configMap = Arguments.createMap(); 299 | configMap.putString("appVersion", mCodePush.getAppVersion()); 300 | configMap.putString("clientUniqueId", mClientUniqueId); 301 | configMap.putString("deploymentKey", mCodePush.getDeploymentKey()); 302 | configMap.putString("serverUrl", mCodePush.getServerUrl()); 303 | 304 | // The binary hash may be null in debug builds 305 | if (mBinaryContentsHash != null) { 306 | configMap.putString(CodePushConstants.PACKAGE_HASH_KEY, mBinaryContentsHash); 307 | } 308 | 309 | promise.resolve(configMap); 310 | } 311 | 312 | @ReactMethod 313 | public void getUpdateMetadata(final int updateState, final Promise promise) { 314 | AsyncTask asyncTask = new AsyncTask() { 315 | @Override 316 | protected Void doInBackground(Void... params) { 317 | JSONObject currentPackage = mUpdateManager.getCurrentPackage(); 318 | 319 | if (currentPackage == null) { 320 | promise.resolve(""); 321 | return null; 322 | } 323 | 324 | Boolean currentUpdateIsPending = false; 325 | 326 | if (currentPackage.has(CodePushConstants.PACKAGE_HASH_KEY)) { 327 | String currentHash = currentPackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null); 328 | currentUpdateIsPending = mSettingsManager.isPendingUpdate(currentHash); 329 | } 330 | 331 | if (updateState == CodePushUpdateState.PENDING.getValue() && !currentUpdateIsPending) { 332 | // The caller wanted a pending update 333 | // but there isn't currently one. 334 | promise.resolve(""); 335 | } else if (updateState == CodePushUpdateState.RUNNING.getValue() && currentUpdateIsPending) { 336 | // The caller wants the running update, but the current 337 | // one is pending, so we need to grab the previous. 338 | JSONObject previousPackage = mUpdateManager.getPreviousPackage(); 339 | 340 | if (previousPackage == null) { 341 | promise.resolve(""); 342 | return null; 343 | } 344 | 345 | promise.resolve(CodePushUtils.convertJsonObjectToWritable(previousPackage)); 346 | } else { 347 | // The current package satisfies the request: 348 | // 1) Caller wanted a pending, and there is a pending update 349 | // 2) Caller wanted the running update, and there isn't a pending 350 | // 3) Caller wants the latest update, regardless if it's pending or not 351 | if (mCodePush.isRunningBinaryVersion()) { 352 | // This only matters in Debug builds. Since we do not clear "outdated" updates, 353 | // we need to indicate to the JS side that somehow we have a current update on 354 | // disk that is not actually running. 355 | CodePushUtils.setJSONValueForKey(currentPackage, "_isDebugOnly", true); 356 | } 357 | 358 | // Enable differentiating pending vs. non-pending updates 359 | CodePushUtils.setJSONValueForKey(currentPackage, "isPending", currentUpdateIsPending); 360 | promise.resolve(CodePushUtils.convertJsonObjectToWritable(currentPackage)); 361 | } 362 | 363 | return null; 364 | } 365 | }; 366 | 367 | asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 368 | } 369 | 370 | @ReactMethod 371 | public void getNewStatusReport(final Promise promise) { 372 | AsyncTask asyncTask = new AsyncTask() { 373 | @Override 374 | protected Void doInBackground(Void... params) { 375 | if (mCodePush.needToReportRollback()) { 376 | mCodePush.setNeedToReportRollback(false); 377 | JSONArray failedUpdates = mSettingsManager.getFailedUpdates(); 378 | if (failedUpdates != null && failedUpdates.length() > 0) { 379 | try { 380 | JSONObject lastFailedPackageJSON = failedUpdates.getJSONObject(failedUpdates.length() - 1); 381 | WritableMap lastFailedPackage = CodePushUtils.convertJsonObjectToWritable(lastFailedPackageJSON); 382 | WritableMap failedStatusReport = mTelemetryManager.getRollbackReport(lastFailedPackage); 383 | if (failedStatusReport != null) { 384 | promise.resolve(failedStatusReport); 385 | return null; 386 | } 387 | } catch (JSONException e) { 388 | throw new CodePushUnknownException("Unable to read failed updates information stored in SharedPreferences.", e); 389 | } 390 | } 391 | } else if (mCodePush.didUpdate()) { 392 | JSONObject currentPackage = mUpdateManager.getCurrentPackage(); 393 | if (currentPackage != null) { 394 | WritableMap newPackageStatusReport = mTelemetryManager.getUpdateReport(CodePushUtils.convertJsonObjectToWritable(currentPackage)); 395 | if (newPackageStatusReport != null) { 396 | promise.resolve(newPackageStatusReport); 397 | return null; 398 | } 399 | } 400 | } else if (mCodePush.isRunningBinaryVersion()) { 401 | WritableMap newAppVersionStatusReport = mTelemetryManager.getBinaryUpdateReport(mCodePush.getAppVersion()); 402 | if (newAppVersionStatusReport != null) { 403 | promise.resolve(newAppVersionStatusReport); 404 | return null; 405 | } 406 | } else { 407 | WritableMap retryStatusReport = mTelemetryManager.getRetryStatusReport(); 408 | if (retryStatusReport != null) { 409 | promise.resolve(retryStatusReport); 410 | return null; 411 | } 412 | } 413 | 414 | promise.resolve(""); 415 | return null; 416 | } 417 | }; 418 | 419 | asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 420 | } 421 | 422 | @ReactMethod 423 | public void installUpdate(final ReadableMap updatePackage, final int installMode, final int minimumBackgroundDuration, final Promise promise) { 424 | AsyncTask asyncTask = new AsyncTask() { 425 | @Override 426 | protected Void doInBackground(Void... params) { 427 | mUpdateManager.installPackage(CodePushUtils.convertReadableToJsonObject(updatePackage), mSettingsManager.isPendingUpdate(null)); 428 | 429 | String pendingHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY); 430 | if (pendingHash == null) { 431 | throw new CodePushUnknownException("Update package to be installed has no hash."); 432 | } else { 433 | mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false); 434 | } 435 | 436 | if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue() || 437 | // We also add the resume listener if the installMode is IMMEDIATE, because 438 | // if the current activity is backgrounded, we want to reload the bundle when 439 | // it comes back into the foreground. 440 | installMode == CodePushInstallMode.IMMEDIATE.getValue()) { 441 | 442 | // Store the minimum duration on the native module as an instance 443 | // variable instead of relying on a closure below, so that any 444 | // subsequent resume-based installs could override it. 445 | CodePushNativeModule.this.mMinimumBackgroundDuration = minimumBackgroundDuration; 446 | 447 | if (mLifecycleEventListener == null) { 448 | // Ensure we do not add the listener twice. 449 | mLifecycleEventListener = new LifecycleEventListener() { 450 | private Date lastPausedDate = null; 451 | 452 | @Override 453 | public void onHostResume() { 454 | // As of RN 36, the resume handler fires immediately if the app is in 455 | // the foreground, so explicitly wait for it to be backgrounded first 456 | if (lastPausedDate != null) { 457 | long durationInBackground = (new Date().getTime() - lastPausedDate.getTime()) / 1000; 458 | if (installMode == CodePushInstallMode.IMMEDIATE.getValue() 459 | || durationInBackground >= CodePushNativeModule.this.mMinimumBackgroundDuration) { 460 | CodePushUtils.log("Loading bundle on resume"); 461 | loadBundle(); 462 | } 463 | } 464 | } 465 | 466 | @Override 467 | public void onHostPause() { 468 | // Save the current time so that when the app is later 469 | // resumed, we can detect how long it was in the background. 470 | lastPausedDate = new Date(); 471 | } 472 | 473 | @Override 474 | public void onHostDestroy() { 475 | } 476 | }; 477 | 478 | getReactApplicationContext().addLifecycleEventListener(mLifecycleEventListener); 479 | } 480 | } 481 | 482 | promise.resolve(""); 483 | 484 | return null; 485 | } 486 | }; 487 | 488 | asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 489 | } 490 | 491 | @ReactMethod 492 | public void isFailedUpdate(String packageHash, Promise promise) { 493 | promise.resolve(mSettingsManager.isFailedHash(packageHash)); 494 | } 495 | 496 | @ReactMethod 497 | public void isFirstRun(String packageHash, Promise promise) { 498 | boolean isFirstRun = mCodePush.didUpdate() 499 | && packageHash != null 500 | && packageHash.length() > 0 501 | && packageHash.equals(mUpdateManager.getCurrentPackageHash()); 502 | promise.resolve(isFirstRun); 503 | } 504 | 505 | @ReactMethod 506 | public void notifyApplicationReady(Promise promise) { 507 | mSettingsManager.removePendingUpdate(); 508 | promise.resolve(""); 509 | } 510 | 511 | @ReactMethod 512 | public void recordStatusReported(ReadableMap statusReport) { 513 | mTelemetryManager.recordStatusReported(statusReport); 514 | } 515 | 516 | @ReactMethod 517 | public void restartApp(boolean onlyIfUpdateIsPending, Promise promise) { 518 | // If this is an unconditional restart request, or there 519 | // is current pending update, then reload the app. 520 | if (!onlyIfUpdateIsPending || mSettingsManager.isPendingUpdate(null)) { 521 | loadBundle(); 522 | promise.resolve(true); 523 | return; 524 | } 525 | 526 | promise.resolve(false); 527 | } 528 | 529 | @ReactMethod 530 | public void saveStatusReportForRetry(ReadableMap statusReport) { 531 | mTelemetryManager.saveStatusReportForRetry(statusReport); 532 | } 533 | 534 | @ReactMethod 535 | // Replaces the current bundle with the one downloaded from removeBundleUrl. 536 | // It is only to be used during tests. No-ops if the test configuration flag is not set. 537 | public void downloadAndReplaceCurrentBundle(String remoteBundleUrl) { 538 | if (mCodePush.isUsingTestConfiguration()) { 539 | try { 540 | mUpdateManager.downloadAndReplaceCurrentBundle(remoteBundleUrl, mCodePush.getAssetsBundleFileName()); 541 | } catch (IOException e) { 542 | throw new CodePushUnknownException("Unable to replace current bundle", e); 543 | } 544 | } 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushNotInitializedException.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | public final class CodePushNotInitializedException extends RuntimeException { 4 | 5 | public CodePushNotInitializedException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | 9 | public CodePushNotInitializedException(String message) { 10 | super(message); 11 | } 12 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushTelemetryManager.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.facebook.react.bridge.Arguments; 7 | import com.facebook.react.bridge.ReadableMap; 8 | import com.facebook.react.bridge.WritableMap; 9 | 10 | import org.json.JSONException; 11 | import org.json.JSONObject; 12 | 13 | public class CodePushTelemetryManager { 14 | private SharedPreferences mSettings; 15 | private final String APP_VERSION_KEY = "appVersion"; 16 | private final String DEPLOYMENT_FAILED_STATUS = "DeploymentFailed"; 17 | private final String DEPLOYMENT_KEY_KEY = "deploymentKey"; 18 | private final String DEPLOYMENT_SUCCEEDED_STATUS = "DeploymentSucceeded"; 19 | private final String LABEL_KEY = "label"; 20 | private final String LAST_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_LAST_DEPLOYMENT_REPORT"; 21 | private final String PACKAGE_KEY = "package"; 22 | private final String PREVIOUS_DEPLOYMENT_KEY_KEY = "previousDeploymentKey"; 23 | private final String PREVIOUS_LABEL_OR_APP_VERSION_KEY = "previousLabelOrAppVersion"; 24 | private final String RETRY_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_RETRY_DEPLOYMENT_REPORT"; 25 | private final String STATUS_KEY = "status"; 26 | 27 | public CodePushTelemetryManager(Context applicationContext) { 28 | mSettings = applicationContext.getSharedPreferences(CodePushConstants.CODE_PUSH_PREFERENCES, 0); 29 | } 30 | 31 | public WritableMap getBinaryUpdateReport(String appVersion) { 32 | String previousStatusReportIdentifier = this.getPreviousStatusReportIdentifier(); 33 | WritableMap reportMap = null; 34 | if (previousStatusReportIdentifier == null) { 35 | this.clearRetryStatusReport(); 36 | reportMap = Arguments.createMap(); 37 | reportMap.putString(APP_VERSION_KEY, appVersion); 38 | } else if (!previousStatusReportIdentifier.equals(appVersion)) { 39 | this.clearRetryStatusReport(); 40 | reportMap = Arguments.createMap(); 41 | if (this.isStatusReportIdentifierCodePushLabel(previousStatusReportIdentifier)) { 42 | String previousDeploymentKey = this.getDeploymentKeyFromStatusReportIdentifier(previousStatusReportIdentifier); 43 | String previousLabel = this.getVersionLabelFromStatusReportIdentifier(previousStatusReportIdentifier); 44 | reportMap.putString(APP_VERSION_KEY, appVersion); 45 | reportMap.putString(PREVIOUS_DEPLOYMENT_KEY_KEY, previousDeploymentKey); 46 | reportMap.putString(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousLabel); 47 | } else { 48 | // Previous status report was with a binary app version. 49 | reportMap.putString(APP_VERSION_KEY, appVersion); 50 | reportMap.putString(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousStatusReportIdentifier); 51 | } 52 | } 53 | 54 | return reportMap; 55 | } 56 | 57 | public WritableMap getRetryStatusReport() { 58 | String retryStatusReportString = mSettings.getString(RETRY_DEPLOYMENT_REPORT_KEY, null); 59 | if (retryStatusReportString != null) { 60 | clearRetryStatusReport(); 61 | try { 62 | JSONObject retryStatusReport = new JSONObject(retryStatusReportString); 63 | return CodePushUtils.convertJsonObjectToWritable(retryStatusReport); 64 | } catch (JSONException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | 69 | return null; 70 | } 71 | 72 | public WritableMap getRollbackReport(WritableMap lastFailedPackage) { 73 | WritableMap reportMap = Arguments.createMap(); 74 | reportMap.putMap(PACKAGE_KEY, lastFailedPackage); 75 | reportMap.putString(STATUS_KEY, DEPLOYMENT_FAILED_STATUS); 76 | return reportMap; 77 | } 78 | 79 | public WritableMap getUpdateReport(WritableMap currentPackage) { 80 | String currentPackageIdentifier = this.getPackageStatusReportIdentifier(currentPackage); 81 | String previousStatusReportIdentifier = this.getPreviousStatusReportIdentifier(); 82 | WritableMap reportMap = null; 83 | if (currentPackageIdentifier != null) { 84 | if (previousStatusReportIdentifier == null) { 85 | this.clearRetryStatusReport(); 86 | reportMap = Arguments.createMap(); 87 | reportMap.putMap(PACKAGE_KEY, currentPackage); 88 | reportMap.putString(STATUS_KEY, DEPLOYMENT_SUCCEEDED_STATUS); 89 | } else if (!previousStatusReportIdentifier.equals(currentPackageIdentifier)) { 90 | this.clearRetryStatusReport(); 91 | reportMap = Arguments.createMap(); 92 | if (this.isStatusReportIdentifierCodePushLabel(previousStatusReportIdentifier)) { 93 | String previousDeploymentKey = this.getDeploymentKeyFromStatusReportIdentifier(previousStatusReportIdentifier); 94 | String previousLabel = this.getVersionLabelFromStatusReportIdentifier(previousStatusReportIdentifier); 95 | reportMap.putMap(PACKAGE_KEY, currentPackage); 96 | reportMap.putString(STATUS_KEY, DEPLOYMENT_SUCCEEDED_STATUS); 97 | reportMap.putString(PREVIOUS_DEPLOYMENT_KEY_KEY, previousDeploymentKey); 98 | reportMap.putString(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousLabel); 99 | } else { 100 | // Previous status report was with a binary app version. 101 | reportMap.putMap(PACKAGE_KEY, currentPackage); 102 | reportMap.putString(STATUS_KEY, DEPLOYMENT_SUCCEEDED_STATUS); 103 | reportMap.putString(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousStatusReportIdentifier); 104 | } 105 | } 106 | } 107 | 108 | return reportMap; 109 | } 110 | 111 | public void recordStatusReported(ReadableMap statusReport) { 112 | // We don't need to record rollback reports, so exit early if that's what was specified. 113 | if (statusReport.hasKey(STATUS_KEY) && DEPLOYMENT_FAILED_STATUS.equals(statusReport.getString(STATUS_KEY))) { 114 | return; 115 | } 116 | 117 | if (statusReport.hasKey(APP_VERSION_KEY)) { 118 | saveStatusReportedForIdentifier(statusReport.getString(APP_VERSION_KEY)); 119 | } else if (statusReport.hasKey(PACKAGE_KEY)) { 120 | String packageIdentifier = getPackageStatusReportIdentifier(statusReport.getMap(PACKAGE_KEY)); 121 | saveStatusReportedForIdentifier(packageIdentifier); 122 | } 123 | } 124 | 125 | public void saveStatusReportForRetry(ReadableMap statusReport) { 126 | JSONObject statusReportJSON = CodePushUtils.convertReadableToJsonObject(statusReport); 127 | mSettings.edit().putString(RETRY_DEPLOYMENT_REPORT_KEY, statusReportJSON.toString()).commit(); 128 | } 129 | 130 | private void clearRetryStatusReport() { 131 | mSettings.edit().remove(RETRY_DEPLOYMENT_REPORT_KEY).commit(); 132 | } 133 | 134 | private String getDeploymentKeyFromStatusReportIdentifier(String statusReportIdentifier) { 135 | String[] parsedIdentifier = statusReportIdentifier.split(":"); 136 | if (parsedIdentifier.length > 0) { 137 | return parsedIdentifier[0]; 138 | } else { 139 | return null; 140 | } 141 | } 142 | 143 | private String getPackageStatusReportIdentifier(ReadableMap updatePackage) { 144 | // Because deploymentKeys can be dynamically switched, we use a 145 | // combination of the deploymentKey and label as the packageIdentifier. 146 | String deploymentKey = CodePushUtils.tryGetString(updatePackage, DEPLOYMENT_KEY_KEY); 147 | String label = CodePushUtils.tryGetString(updatePackage, LABEL_KEY); 148 | if (deploymentKey != null && label != null) { 149 | return deploymentKey + ":" + label; 150 | } else { 151 | return null; 152 | } 153 | } 154 | 155 | private String getPreviousStatusReportIdentifier() { 156 | return mSettings.getString(LAST_DEPLOYMENT_REPORT_KEY, null); 157 | } 158 | 159 | private String getVersionLabelFromStatusReportIdentifier(String statusReportIdentifier) { 160 | String[] parsedIdentifier = statusReportIdentifier.split(":"); 161 | if (parsedIdentifier.length > 1) { 162 | return parsedIdentifier[1]; 163 | } else { 164 | return null; 165 | } 166 | } 167 | 168 | private boolean isStatusReportIdentifierCodePushLabel(String statusReportIdentifier) { 169 | return statusReportIdentifier != null && statusReportIdentifier.contains(":"); 170 | } 171 | 172 | private void saveStatusReportedForIdentifier(String appVersionOrPackageIdentifier) { 173 | mSettings.edit().putString(LAST_DEPLOYMENT_REPORT_KEY, appVersionOrPackageIdentifier).commit(); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushUnknownException.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | class CodePushUnknownException extends RuntimeException { 4 | 5 | public CodePushUnknownException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | 9 | public CodePushUnknownException(String message) { 10 | super(message); 11 | } 12 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import org.json.JSONObject; 4 | 5 | import java.io.BufferedInputStream; 6 | import java.io.BufferedOutputStream; 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import java.net.HttpURLConnection; 11 | import java.net.MalformedURLException; 12 | import java.net.URL; 13 | import java.nio.ByteBuffer; 14 | 15 | public class CodePushUpdateManager { 16 | 17 | private String mDocumentsDirectory; 18 | 19 | public CodePushUpdateManager(String documentsDirectory) { 20 | mDocumentsDirectory = documentsDirectory; 21 | } 22 | 23 | private String getDownloadFilePath() { 24 | return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.DOWNLOAD_FILE_NAME); 25 | } 26 | 27 | private String getUnzippedFolderPath() { 28 | return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.UNZIPPED_FOLDER_NAME); 29 | } 30 | 31 | private String getDocumentsDirectory() { 32 | return mDocumentsDirectory; 33 | } 34 | 35 | private String getCodePushPath() { 36 | String codePushPath = CodePushUtils.appendPathComponent(getDocumentsDirectory(), CodePushConstants.CODE_PUSH_FOLDER_PREFIX); 37 | if (CodePush.isUsingTestConfiguration()) { 38 | codePushPath = CodePushUtils.appendPathComponent(codePushPath, "TestPackages"); 39 | } 40 | 41 | return codePushPath; 42 | } 43 | 44 | private String getStatusFilePath() { 45 | return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.STATUS_FILE); 46 | } 47 | 48 | public JSONObject getCurrentPackageInfo() { 49 | String statusFilePath = getStatusFilePath(); 50 | if (!FileUtils.fileAtPathExists(statusFilePath)) { 51 | return new JSONObject(); 52 | } 53 | 54 | try { 55 | return CodePushUtils.getJsonObjectFromFile(statusFilePath); 56 | } catch (IOException e) { 57 | // Should not happen. 58 | throw new CodePushUnknownException("Error getting current package info" , e); 59 | } 60 | } 61 | 62 | public void updateCurrentPackageInfo(JSONObject packageInfo) { 63 | try { 64 | CodePushUtils.writeJsonToFile(packageInfo, getStatusFilePath()); 65 | } catch (IOException e) { 66 | // Should not happen. 67 | throw new CodePushUnknownException("Error updating current package info" , e); 68 | } 69 | } 70 | 71 | public String getCurrentPackageFolderPath() { 72 | JSONObject info = getCurrentPackageInfo(); 73 | String packageHash = info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null); 74 | if (packageHash == null) { 75 | return null; 76 | } 77 | 78 | return getPackageFolderPath(packageHash); 79 | } 80 | 81 | public String getCurrentPackageBundlePath(String bundleFileName) { 82 | String packageFolder = getCurrentPackageFolderPath(); 83 | if (packageFolder == null) { 84 | return null; 85 | } 86 | 87 | JSONObject currentPackage = getCurrentPackage(); 88 | if (currentPackage == null) { 89 | return null; 90 | } 91 | 92 | String relativeBundlePath = currentPackage.optString(CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, null); 93 | if (relativeBundlePath == null) { 94 | return CodePushUtils.appendPathComponent(packageFolder, bundleFileName); 95 | } else { 96 | return CodePushUtils.appendPathComponent(packageFolder, relativeBundlePath); 97 | } 98 | } 99 | 100 | public String getPackageFolderPath(String packageHash) { 101 | return CodePushUtils.appendPathComponent(getCodePushPath(), packageHash); 102 | } 103 | 104 | public String getCurrentPackageHash() { 105 | JSONObject info = getCurrentPackageInfo(); 106 | return info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null); 107 | } 108 | 109 | public String getPreviousPackageHash() { 110 | JSONObject info = getCurrentPackageInfo(); 111 | return info.optString(CodePushConstants.PREVIOUS_PACKAGE_KEY, null); 112 | } 113 | 114 | public JSONObject getCurrentPackage() { 115 | String packageHash = getCurrentPackageHash(); 116 | if (packageHash == null) { 117 | return null; 118 | } 119 | 120 | return getPackage(packageHash); 121 | } 122 | 123 | public JSONObject getPreviousPackage() { 124 | String packageHash = getPreviousPackageHash(); 125 | if (packageHash == null) { 126 | return null; 127 | } 128 | 129 | return getPackage(packageHash); 130 | } 131 | 132 | public JSONObject getPackage(String packageHash) { 133 | String folderPath = getPackageFolderPath(packageHash); 134 | String packageFilePath = CodePushUtils.appendPathComponent(folderPath, CodePushConstants.PACKAGE_FILE_NAME); 135 | try { 136 | return CodePushUtils.getJsonObjectFromFile(packageFilePath); 137 | } catch (IOException e) { 138 | return null; 139 | } 140 | } 141 | 142 | public void downloadPackage(JSONObject updatePackage, String expectedBundleFileName, 143 | DownloadProgressCallback progressCallback) throws IOException { 144 | String newUpdateHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null); 145 | String newUpdateFolderPath = getPackageFolderPath(newUpdateHash); 146 | String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME); 147 | if (FileUtils.fileAtPathExists(newUpdateFolderPath)) { 148 | // This removes any stale data in newPackageFolderPath that could have been left 149 | // uncleared due to a crash or error during the download or install process. 150 | FileUtils.deleteDirectoryAtPath(newUpdateFolderPath); 151 | } 152 | 153 | String downloadUrlString = updatePackage.optString(CodePushConstants.DOWNLOAD_URL_KEY, null); 154 | HttpURLConnection connection = null; 155 | BufferedInputStream bin = null; 156 | FileOutputStream fos = null; 157 | BufferedOutputStream bout = null; 158 | File downloadFile = null; 159 | boolean isZip = false; 160 | 161 | // Download the file while checking if it is a zip and notifying client of progress. 162 | try { 163 | URL downloadUrl = new URL(downloadUrlString); 164 | connection = (HttpURLConnection) (downloadUrl.openConnection()); 165 | 166 | long totalBytes = connection.getContentLength(); 167 | long receivedBytes = 0; 168 | 169 | bin = new BufferedInputStream(connection.getInputStream()); 170 | File downloadFolder = new File(getCodePushPath()); 171 | downloadFolder.mkdirs(); 172 | downloadFile = new File(downloadFolder, CodePushConstants.DOWNLOAD_FILE_NAME); 173 | fos = new FileOutputStream(downloadFile); 174 | bout = new BufferedOutputStream(fos, CodePushConstants.DOWNLOAD_BUFFER_SIZE); 175 | byte[] data = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE]; 176 | byte[] header = new byte[4]; 177 | 178 | int numBytesRead = 0; 179 | while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) { 180 | if (receivedBytes < 4) { 181 | for (int i = 0; i < numBytesRead; i++) { 182 | int headerOffset = (int)(receivedBytes) + i; 183 | if (headerOffset >= 4) { 184 | break; 185 | } 186 | 187 | header[headerOffset] = data[i]; 188 | } 189 | } 190 | 191 | receivedBytes += numBytesRead; 192 | bout.write(data, 0, numBytesRead); 193 | progressCallback.call(new DownloadProgress(totalBytes, receivedBytes)); 194 | } 195 | 196 | if (totalBytes != receivedBytes) { 197 | throw new CodePushUnknownException("Received " + receivedBytes + " bytes, expected " + totalBytes); 198 | } 199 | 200 | isZip = ByteBuffer.wrap(header).getInt() == 0x504b0304; 201 | } catch (MalformedURLException e) { 202 | throw new CodePushMalformedDataException(downloadUrlString, e); 203 | } finally { 204 | try { 205 | if (bout != null) bout.close(); 206 | if (fos != null) fos.close(); 207 | if (bin != null) bin.close(); 208 | if (connection != null) connection.disconnect(); 209 | } catch (IOException e) { 210 | throw new CodePushUnknownException("Error closing IO resources.", e); 211 | } 212 | } 213 | 214 | if (isZip) { 215 | // Unzip the downloaded file and then delete the zip 216 | String unzippedFolderPath = getUnzippedFolderPath(); 217 | FileUtils.unzipFile(downloadFile, unzippedFolderPath); 218 | FileUtils.deleteFileOrFolderSilently(downloadFile); 219 | 220 | // Merge contents with current update based on the manifest 221 | String diffManifestFilePath = CodePushUtils.appendPathComponent(unzippedFolderPath, 222 | CodePushConstants.DIFF_MANIFEST_FILE_NAME); 223 | boolean isDiffUpdate = FileUtils.fileAtPathExists(diffManifestFilePath); 224 | if (isDiffUpdate) { 225 | String currentPackageFolderPath = getCurrentPackageFolderPath(); 226 | CodePushUpdateUtils.copyNecessaryFilesFromCurrentPackage(diffManifestFilePath, currentPackageFolderPath, newUpdateFolderPath); 227 | File diffManifestFile = new File(diffManifestFilePath); 228 | diffManifestFile.delete(); 229 | } 230 | 231 | FileUtils.copyDirectoryContents(unzippedFolderPath, newUpdateFolderPath); 232 | FileUtils.deleteFileAtPathSilently(unzippedFolderPath); 233 | 234 | // For zip updates, we need to find the relative path to the jsBundle and save it in the 235 | // metadata so that we can find and run it easily the next time. 236 | String relativeBundlePath = CodePushUpdateUtils.findJSBundleInUpdateContents(newUpdateFolderPath, expectedBundleFileName); 237 | 238 | if (relativeBundlePath == null) { 239 | throw new CodePushInvalidUpdateException("Update is invalid - A JS bundle file named \"" + expectedBundleFileName + "\" could not be found within the downloaded contents. Please check that you are releasing your CodePush updates using the exact same JS bundle file name that was shipped with your app's binary."); 240 | } else { 241 | if (FileUtils.fileAtPathExists(newUpdateMetadataPath)) { 242 | File metadataFileFromOldUpdate = new File(newUpdateMetadataPath); 243 | metadataFileFromOldUpdate.delete(); 244 | } 245 | 246 | if (isDiffUpdate) { 247 | CodePushUpdateUtils.verifyHashForDiffUpdate(newUpdateFolderPath, newUpdateHash); 248 | } 249 | 250 | CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath); 251 | } 252 | } else { 253 | // File is a jsbundle, move it to a folder with the packageHash as its name 254 | FileUtils.moveFile(downloadFile, newUpdateFolderPath, expectedBundleFileName); 255 | } 256 | 257 | // Save metadata to the folder. 258 | CodePushUtils.writeJsonToFile(updatePackage, newUpdateMetadataPath); 259 | } 260 | 261 | public void installPackage(JSONObject updatePackage, boolean removePendingUpdate) { 262 | String packageHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null); 263 | JSONObject info = getCurrentPackageInfo(); 264 | 265 | String currentPackageHash = info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null); 266 | if (packageHash != null && packageHash.equals(currentPackageHash)) { 267 | // The current package is already the one being installed, so we should no-op. 268 | return; 269 | } 270 | 271 | if (removePendingUpdate) { 272 | String currentPackageFolderPath = getCurrentPackageFolderPath(); 273 | if (currentPackageFolderPath != null) { 274 | FileUtils.deleteDirectoryAtPath(currentPackageFolderPath); 275 | } 276 | } else { 277 | String previousPackageHash = getPreviousPackageHash(); 278 | if (previousPackageHash != null && !previousPackageHash.equals(packageHash)) { 279 | FileUtils.deleteDirectoryAtPath(getPackageFolderPath(previousPackageHash)); 280 | } 281 | 282 | CodePushUtils.setJSONValueForKey(info, CodePushConstants.PREVIOUS_PACKAGE_KEY, info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null)); 283 | } 284 | 285 | CodePushUtils.setJSONValueForKey(info, CodePushConstants.CURRENT_PACKAGE_KEY, packageHash); 286 | updateCurrentPackageInfo(info); 287 | } 288 | 289 | public void rollbackPackage() { 290 | JSONObject info = getCurrentPackageInfo(); 291 | String currentPackageFolderPath = getCurrentPackageFolderPath(); 292 | FileUtils.deleteDirectoryAtPath(currentPackageFolderPath); 293 | CodePushUtils.setJSONValueForKey(info, CodePushConstants.CURRENT_PACKAGE_KEY, info.optString(CodePushConstants.PREVIOUS_PACKAGE_KEY, null)); 294 | CodePushUtils.setJSONValueForKey(info, CodePushConstants.PREVIOUS_PACKAGE_KEY, null); 295 | updateCurrentPackageInfo(info); 296 | } 297 | 298 | public void downloadAndReplaceCurrentBundle(String remoteBundleUrl, String bundleFileName) throws IOException { 299 | URL downloadUrl; 300 | HttpURLConnection connection = null; 301 | BufferedInputStream bin = null; 302 | FileOutputStream fos = null; 303 | BufferedOutputStream bout = null; 304 | try { 305 | downloadUrl = new URL(remoteBundleUrl); 306 | connection = (HttpURLConnection) (downloadUrl.openConnection()); 307 | bin = new BufferedInputStream(connection.getInputStream()); 308 | File downloadFile = new File(getCurrentPackageBundlePath(bundleFileName)); 309 | downloadFile.delete(); 310 | fos = new FileOutputStream(downloadFile); 311 | bout = new BufferedOutputStream(fos, CodePushConstants.DOWNLOAD_BUFFER_SIZE); 312 | byte[] data = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE]; 313 | int numBytesRead = 0; 314 | while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) { 315 | bout.write(data, 0, numBytesRead); 316 | } 317 | } catch (MalformedURLException e) { 318 | throw new CodePushMalformedDataException(remoteBundleUrl, e); 319 | } finally { 320 | try { 321 | if (bout != null) bout.close(); 322 | if (fos != null) fos.close(); 323 | if (bin != null) bin.close(); 324 | if (connection != null) connection.disconnect(); 325 | } catch (IOException e) { 326 | throw new CodePushUnknownException("Error closing IO resources.", e); 327 | } 328 | } 329 | } 330 | 331 | public void clearUpdates() { 332 | FileUtils.deleteDirectoryAtPath(getCodePushPath()); 333 | } 334 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateState.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | public enum CodePushUpdateState { 4 | RUNNING(0), 5 | PENDING(1), 6 | LATEST(2); 7 | 8 | private final int value; 9 | CodePushUpdateState(int value) { 10 | this.value = value; 11 | } 12 | public int getValue() { 13 | return this.value; 14 | } 15 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import android.content.Context; 4 | 5 | import org.json.JSONArray; 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.io.ByteArrayInputStream; 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.FileNotFoundException; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.security.DigestInputStream; 16 | import java.security.MessageDigest; 17 | import java.security.NoSuchAlgorithmException; 18 | import java.util.ArrayList; 19 | import java.util.Collections; 20 | 21 | public class CodePushUpdateUtils { 22 | 23 | private static void addContentsOfFolderToManifest(String folderPath, String pathPrefix, ArrayList manifest) { 24 | File folder = new File(folderPath); 25 | File[] folderFiles = folder.listFiles(); 26 | for (File file : folderFiles) { 27 | String fileName = file.getName(); 28 | String fullFilePath = file.getAbsolutePath(); 29 | String relativePath = (pathPrefix.isEmpty() ? "" : (pathPrefix + "/")) + fileName; 30 | 31 | if (fileName.equals(".DS_Store") || fileName.equals("__MACOSX")) { 32 | continue; 33 | } else if (file.isDirectory()) { 34 | addContentsOfFolderToManifest(fullFilePath, relativePath, manifest); 35 | } else { 36 | try { 37 | manifest.add(relativePath + ":" + computeHash(new FileInputStream(file))); 38 | } catch (FileNotFoundException e) { 39 | // Should not happen. 40 | throw new CodePushUnknownException("Unable to compute hash of update contents.", e); 41 | } 42 | } 43 | } 44 | } 45 | 46 | private static String computeHash(InputStream dataStream) { 47 | MessageDigest messageDigest = null; 48 | DigestInputStream digestInputStream = null; 49 | try { 50 | messageDigest = MessageDigest.getInstance("SHA-256"); 51 | digestInputStream = new DigestInputStream(dataStream, messageDigest); 52 | byte[] byteBuffer = new byte[1024 * 8]; 53 | while (digestInputStream.read(byteBuffer) != -1); 54 | } catch (NoSuchAlgorithmException | IOException e) { 55 | // Should not happen. 56 | throw new CodePushUnknownException("Unable to compute hash of update contents.", e); 57 | } finally { 58 | try { 59 | if (digestInputStream != null) digestInputStream.close(); 60 | if (dataStream != null) dataStream.close(); 61 | } catch (IOException e) { 62 | e.printStackTrace(); 63 | } 64 | } 65 | 66 | byte[] hash = messageDigest.digest(); 67 | return String.format("%064x", new java.math.BigInteger(1, hash)); 68 | } 69 | 70 | public static void copyNecessaryFilesFromCurrentPackage(String diffManifestFilePath, String currentPackageFolderPath, String newPackageFolderPath) throws IOException{ 71 | FileUtils.copyDirectoryContents(currentPackageFolderPath, newPackageFolderPath); 72 | JSONObject diffManifest = CodePushUtils.getJsonObjectFromFile(diffManifestFilePath); 73 | try { 74 | JSONArray deletedFiles = diffManifest.getJSONArray("deletedFiles"); 75 | for (int i = 0; i < deletedFiles.length(); i++) { 76 | String fileNameToDelete = deletedFiles.getString(i); 77 | File fileToDelete = new File(newPackageFolderPath, fileNameToDelete); 78 | if (fileToDelete.exists()) { 79 | fileToDelete.delete(); 80 | } 81 | } 82 | } catch (JSONException e) { 83 | throw new CodePushUnknownException("Unable to copy files from current package during diff update", e); 84 | } 85 | } 86 | 87 | public static String findJSBundleInUpdateContents(String folderPath, String expectedFileName) { 88 | File folder = new File(folderPath); 89 | File[] folderFiles = folder.listFiles(); 90 | for (File file : folderFiles) { 91 | String fullFilePath = CodePushUtils.appendPathComponent(folderPath, file.getName()); 92 | if (file.isDirectory()) { 93 | String mainBundlePathInSubFolder = findJSBundleInUpdateContents(fullFilePath, expectedFileName); 94 | if (mainBundlePathInSubFolder != null) { 95 | return CodePushUtils.appendPathComponent(file.getName(), mainBundlePathInSubFolder); 96 | } 97 | } else { 98 | String fileName = file.getName(); 99 | if (fileName.equals(expectedFileName)) { 100 | return fileName; 101 | } 102 | } 103 | } 104 | 105 | return null; 106 | } 107 | 108 | public static String getHashForBinaryContents(Context context, boolean isDebugMode) { 109 | try { 110 | return CodePushUtils.getStringFromInputStream(context.getAssets().open(CodePushConstants.CODE_PUSH_HASH_FILE_NAME)); 111 | } catch (IOException e) { 112 | if (!isDebugMode) { 113 | // Only print this message in "Release" mode. In "Debug", we may not have the 114 | // hash if the build skips bundling the files. 115 | CodePushUtils.log("Unable to get the hash of the binary's bundled resources - \"codepush.gradle\" may have not been added to the build definition."); 116 | } 117 | 118 | return null; 119 | } 120 | } 121 | 122 | public static void verifyHashForDiffUpdate(String folderPath, String expectedHash) { 123 | ArrayList updateContentsManifest = new ArrayList<>(); 124 | addContentsOfFolderToManifest(folderPath, "", updateContentsManifest); 125 | Collections.sort(updateContentsManifest); 126 | JSONArray updateContentsJSONArray = new JSONArray(); 127 | for (String manifestEntry : updateContentsManifest) { 128 | updateContentsJSONArray.put(manifestEntry); 129 | } 130 | 131 | // The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png" 132 | String updateContentsManifestString = updateContentsJSONArray.toString().replace("\\/", "/"); 133 | String updateContentsManifestHash = computeHash(new ByteArrayInputStream(updateContentsManifestString.getBytes())); 134 | if (!expectedHash.equals(updateContentsManifestHash)) { 135 | throw new CodePushInvalidUpdateException("The update contents failed the data integrity check."); 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import android.util.Log; 4 | 5 | import com.facebook.react.bridge.Arguments; 6 | import com.facebook.react.bridge.NoSuchKeyException; 7 | import com.facebook.react.bridge.ReadableArray; 8 | import com.facebook.react.bridge.ReadableMap; 9 | import com.facebook.react.bridge.ReadableMapKeySetIterator; 10 | import com.facebook.react.bridge.ReadableType; 11 | import com.facebook.react.bridge.WritableArray; 12 | import com.facebook.react.bridge.WritableMap; 13 | 14 | import org.json.JSONArray; 15 | import org.json.JSONException; 16 | import org.json.JSONObject; 17 | 18 | import java.io.BufferedReader; 19 | import java.io.File; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.InputStreamReader; 23 | import java.util.Iterator; 24 | 25 | public class CodePushUtils { 26 | 27 | public static String appendPathComponent(String basePath, String appendPathComponent) { 28 | return new File(basePath, appendPathComponent).getAbsolutePath(); 29 | } 30 | 31 | public static WritableArray convertJsonArrayToWritable(JSONArray jsonArr) { 32 | WritableArray arr = Arguments.createArray(); 33 | for (int i=0; i it = jsonObj.keys(); 66 | while(it.hasNext()){ 67 | String key = it.next(); 68 | Object obj = null; 69 | try { 70 | obj = jsonObj.get(key); 71 | } catch (JSONException jsonException) { 72 | // Should not happen. 73 | throw new CodePushUnknownException("Key " + key + " should exist in " + jsonObj.toString() + ".", jsonException); 74 | } 75 | 76 | if (obj instanceof JSONObject) 77 | map.putMap(key, convertJsonObjectToWritable((JSONObject) obj)); 78 | else if (obj instanceof JSONArray) 79 | map.putArray(key, convertJsonArrayToWritable((JSONArray) obj)); 80 | else if (obj instanceof String) 81 | map.putString(key, (String) obj); 82 | else if (obj instanceof Double) 83 | map.putDouble(key, (Double) obj); 84 | else if (obj instanceof Integer) 85 | map.putInt(key, (Integer) obj); 86 | else if (obj instanceof Boolean) 87 | map.putBoolean(key, (Boolean) obj); 88 | else if (obj == null) 89 | map.putNull(key); 90 | else 91 | throw new CodePushUnknownException("Unrecognized object: " + obj); 92 | } 93 | 94 | return map; 95 | } 96 | 97 | public static JSONArray convertReadableToJsonArray(ReadableArray arr) { 98 | JSONArray jsonArr = new JSONArray(); 99 | for (int i=0; i 0) { 42 | destStream.write(buffer, 0, bytesRead); 43 | } 44 | } finally { 45 | try { 46 | if (fromFileStream != null) fromFileStream.close(); 47 | if (fromBufferedStream != null) fromBufferedStream.close(); 48 | if (destStream != null) destStream.close(); 49 | } catch (IOException e) { 50 | throw new CodePushUnknownException("Error closing IO resources.", e); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | public static void deleteDirectoryAtPath(String directoryPath) { 58 | File file = new File(directoryPath); 59 | if (file.exists()) { 60 | deleteFileOrFolderSilently(file); 61 | } 62 | } 63 | 64 | public static void deleteFileAtPathSilently(String path) { 65 | deleteFileOrFolderSilently(new File(path)); 66 | } 67 | 68 | public static void deleteFileOrFolderSilently(File file) { 69 | if (file.isDirectory()) { 70 | File[] files = file.listFiles(); 71 | for (File fileEntry : files) { 72 | if (fileEntry.isDirectory()) { 73 | deleteFileOrFolderSilently(fileEntry); 74 | } else { 75 | fileEntry.delete(); 76 | } 77 | } 78 | } 79 | 80 | if (!file.delete()) { 81 | CodePushUtils.log("Error deleting file " + file.getName()); 82 | } 83 | } 84 | 85 | public static boolean fileAtPathExists(String filePath) { 86 | return new File(filePath).exists(); 87 | } 88 | 89 | public static void moveFile(File fileToMove, String newFolderPath, String newFileName) { 90 | File newFolder = new File(newFolderPath); 91 | if (!newFolder.exists()) { 92 | newFolder.mkdirs(); 93 | } 94 | 95 | File newFilePath = new File(newFolderPath, newFileName); 96 | if (!fileToMove.renameTo(newFilePath)) { 97 | throw new CodePushUnknownException("Unable to move file from " + 98 | fileToMove.getAbsolutePath() + " to " + newFilePath.getAbsolutePath() + "."); 99 | } 100 | } 101 | 102 | public static String readFileToString(String filePath) throws IOException { 103 | FileInputStream fin = null; 104 | BufferedReader reader = null; 105 | try { 106 | File fl = new File(filePath); 107 | fin = new FileInputStream(fl); 108 | reader = new BufferedReader(new InputStreamReader(fin)); 109 | StringBuilder sb = new StringBuilder(); 110 | String line = null; 111 | while ((line = reader.readLine()) != null) { 112 | sb.append(line).append("\n"); 113 | } 114 | 115 | return sb.toString(); 116 | } finally { 117 | if (reader != null) reader.close(); 118 | if (fin != null) fin.close(); 119 | } 120 | } 121 | 122 | public static void unzipFile(File zipFile, String destination) throws IOException { 123 | FileInputStream fileStream = null; 124 | BufferedInputStream bufferedStream = null; 125 | ZipInputStream zipStream = null; 126 | try { 127 | fileStream = new FileInputStream(zipFile); 128 | bufferedStream = new BufferedInputStream(fileStream); 129 | zipStream = new ZipInputStream(bufferedStream); 130 | ZipEntry entry; 131 | 132 | File destinationFolder = new File(destination); 133 | if (destinationFolder.exists()) { 134 | deleteFileOrFolderSilently(destinationFolder); 135 | } 136 | 137 | destinationFolder.mkdirs(); 138 | 139 | byte[] buffer = new byte[WRITE_BUFFER_SIZE]; 140 | while ((entry = zipStream.getNextEntry()) != null) { 141 | String fileName = entry.getName(); 142 | File file = new File(destinationFolder, fileName); 143 | if (entry.isDirectory()) { 144 | file.mkdirs(); 145 | } else { 146 | File parent = file.getParentFile(); 147 | if (!parent.exists()) { 148 | parent.mkdirs(); 149 | } 150 | 151 | FileOutputStream fout = new FileOutputStream(file); 152 | try { 153 | int numBytesRead; 154 | while ((numBytesRead = zipStream.read(buffer)) != -1) { 155 | fout.write(buffer, 0, numBytesRead); 156 | } 157 | } finally { 158 | fout.close(); 159 | } 160 | } 161 | long time = entry.getTime(); 162 | if (time > 0) { 163 | file.setLastModified(time); 164 | } 165 | } 166 | } finally { 167 | try { 168 | if (zipStream != null) zipStream.close(); 169 | if (bufferedStream != null) bufferedStream.close(); 170 | if (fileStream != null) fileStream.close(); 171 | } catch (IOException e) { 172 | throw new CodePushUnknownException("Error closing IO resources.", e); 173 | } 174 | } 175 | } 176 | 177 | public static void writeStringToFile(String content, String filePath) throws IOException { 178 | PrintWriter out = null; 179 | try { 180 | out = new PrintWriter(filePath); 181 | out.print(content); 182 | } finally { 183 | if (out != null) out.close(); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/ReactInstanceHolder.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import com.facebook.react.ReactInstanceManager; 4 | 5 | /** 6 | * Provides access to a {@link ReactInstanceManager}. 7 | * 8 | * ReactNativeHost already implements this interface, if you make use of that react-native 9 | * component (just add `implements ReactInstanceHolder`). 10 | */ 11 | public interface ReactInstanceHolder { 12 | 13 | /** 14 | * Get the current {@link ReactInstanceManager} instance. May return null. 15 | */ 16 | ReactInstanceManager getReactInstanceManager(); 17 | } 18 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/microsoft/codepush/react/SettingsManager.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.codepush.react; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | 10 | public class SettingsManager { 11 | 12 | private SharedPreferences mSettings; 13 | 14 | public SettingsManager(Context applicationContext) { 15 | mSettings = applicationContext.getSharedPreferences(CodePushConstants.CODE_PUSH_PREFERENCES, 0); 16 | } 17 | 18 | public JSONArray getFailedUpdates() { 19 | String failedUpdatesString = mSettings.getString(CodePushConstants.FAILED_UPDATES_KEY, null); 20 | if (failedUpdatesString == null) { 21 | return new JSONArray(); 22 | } 23 | 24 | try { 25 | return new JSONArray(failedUpdatesString); 26 | } catch (JSONException e) { 27 | // Unrecognized data format, clear and replace with expected format. 28 | JSONArray emptyArray = new JSONArray(); 29 | mSettings.edit().putString(CodePushConstants.FAILED_UPDATES_KEY, emptyArray.toString()).commit(); 30 | return emptyArray; 31 | } 32 | } 33 | 34 | public JSONObject getPendingUpdate() { 35 | String pendingUpdateString = mSettings.getString(CodePushConstants.PENDING_UPDATE_KEY, null); 36 | if (pendingUpdateString == null) { 37 | return null; 38 | } 39 | 40 | try { 41 | return new JSONObject(pendingUpdateString); 42 | } catch (JSONException e) { 43 | // Should not happen. 44 | CodePushUtils.log("Unable to parse pending update metadata " + pendingUpdateString + 45 | " stored in SharedPreferences"); 46 | return null; 47 | } 48 | } 49 | 50 | 51 | public boolean isFailedHash(String packageHash) { 52 | JSONArray failedUpdates = getFailedUpdates(); 53 | if (packageHash != null) { 54 | for (int i = 0; i < failedUpdates.length(); i++) { 55 | try { 56 | JSONObject failedPackage = failedUpdates.getJSONObject(i); 57 | String failedPackageHash = failedPackage.getString(CodePushConstants.PACKAGE_HASH_KEY); 58 | if (packageHash.equals(failedPackageHash)) { 59 | return true; 60 | } 61 | } catch (JSONException e) { 62 | throw new CodePushUnknownException("Unable to read failedUpdates data stored in SharedPreferences.", e); 63 | } 64 | } 65 | } 66 | 67 | return false; 68 | } 69 | 70 | public boolean isPendingUpdate(String packageHash) { 71 | JSONObject pendingUpdate = getPendingUpdate(); 72 | 73 | try { 74 | return pendingUpdate != null && 75 | !pendingUpdate.getBoolean(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY) && 76 | (packageHash == null || pendingUpdate.getString(CodePushConstants.PENDING_UPDATE_HASH_KEY).equals(packageHash)); 77 | } 78 | catch (JSONException e) { 79 | throw new CodePushUnknownException("Unable to read pending update metadata in isPendingUpdate.", e); 80 | } 81 | } 82 | 83 | public void removeFailedUpdates() { 84 | mSettings.edit().remove(CodePushConstants.FAILED_UPDATES_KEY).commit(); 85 | } 86 | 87 | public void removePendingUpdate() { 88 | mSettings.edit().remove(CodePushConstants.PENDING_UPDATE_KEY).commit(); 89 | } 90 | 91 | public void saveFailedUpdate(JSONObject failedPackage) { 92 | String failedUpdatesString = mSettings.getString(CodePushConstants.FAILED_UPDATES_KEY, null); 93 | JSONArray failedUpdates; 94 | if (failedUpdatesString == null) { 95 | failedUpdates = new JSONArray(); 96 | } else { 97 | try { 98 | failedUpdates = new JSONArray(failedUpdatesString); 99 | } catch (JSONException e) { 100 | // Should not happen. 101 | throw new CodePushMalformedDataException("Unable to parse failed updates information " + 102 | failedUpdatesString + " stored in SharedPreferences", e); 103 | } 104 | } 105 | 106 | failedUpdates.put(failedPackage); 107 | mSettings.edit().putString(CodePushConstants.FAILED_UPDATES_KEY, failedUpdates.toString()).commit(); 108 | } 109 | 110 | public void savePendingUpdate(String packageHash, boolean isLoading) { 111 | JSONObject pendingUpdate = new JSONObject(); 112 | try { 113 | pendingUpdate.put(CodePushConstants.PENDING_UPDATE_HASH_KEY, packageHash); 114 | pendingUpdate.put(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY, isLoading); 115 | mSettings.edit().putString(CodePushConstants.PENDING_UPDATE_KEY, pendingUpdate.toString()).commit(); 116 | } catch (JSONException e) { 117 | // Should not happen. 118 | throw new CodePushUnknownException("Unable to save pending update.", e); 119 | } 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/amal.chandran/Documents/Tools/adt-bundle-mac-x86_64-20140702/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/amalbit/testandroidapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { 18 | @Test public void useAppContext() throws Exception { 19 | // Context of the app under test. 20 | Context appContext = InstrumentationRegistry.getTargetContext(); 21 | 22 | assertEquals("com.amalbit.testandroidapp", appContext.getPackageName()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import com.facebook.react.ReactNativeHost; 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.shell.MainReactPackage; 9 | import com.microsoft.codepush.react.CodePush; 10 | 11 | import java.util.Arrays; 12 | import java.util.List; 13 | 14 | /** 15 | * Created by amal.chandran on 19/03/17. 16 | */ 17 | 18 | public class ApplicationTest extends Application implements ReactApplication { 19 | 20 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 21 | // 2. Override the getJSBundleFile method in order to let 22 | // the CodePush runtime determine where to get the JS 23 | // bundle location from on each app start 24 | @Override 25 | protected String getJSBundleFile() { 26 | return CodePush.getJSBundleFile(); 27 | } 28 | 29 | @Override 30 | protected List getPackages() { 31 | // 3. Instantiate an instance of the CodePush runtime and add it to the list of 32 | // existing packages, specifying the right deployment key. If you don't already 33 | // have it, you can run "code-push deployment ls -k" to retrieve your key. 34 | return Arrays.asList( 35 | new MainReactPackage(), 36 | new CodePush("QFAh1DEPcrYiapxgvQjEuEThvCfr4y542iDoM", ApplicationTest.this, BuildConfig.DEBUG) 37 | ); 38 | } 39 | 40 | @Override 41 | public boolean getUseDeveloperSupport() { 42 | return false; 43 | } 44 | }; 45 | 46 | @Override 47 | public ReactNativeHost getReactNativeHost() { 48 | return mReactNativeHost; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/BaseReactActivity.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Build; 6 | import android.os.Bundle; 7 | import android.provider.Settings; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.view.KeyEvent; 10 | import com.facebook.react.ReactInstanceManager; 11 | import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; 12 | 13 | /** 14 | * Created by amal.chandran on 17/03/17. 15 | */ 16 | 17 | public class BaseReactActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler { 18 | private static int OVERLAY_PERMISSION_REQ_CODE = 2341; 19 | 20 | protected ReactInstanceManager mReactInstanceManager; 21 | 22 | @Override protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | 25 | //This is only for showing error on the screen. 26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 27 | if (!Settings.canDrawOverlays(this)) { 28 | Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, 29 | Uri.parse("package:" + getPackageName())); 30 | startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); 31 | } 32 | } 33 | } 34 | 35 | @Override 36 | public void invokeDefaultOnBackPressed() { 37 | super.onBackPressed(); 38 | } 39 | 40 | @Override 41 | protected void onPause() { 42 | super.onPause(); 43 | 44 | if (mReactInstanceManager != null) { 45 | mReactInstanceManager.onHostPause(); 46 | } 47 | } 48 | 49 | @Override 50 | protected void onResume() { 51 | super.onResume(); 52 | 53 | if (mReactInstanceManager != null) { 54 | mReactInstanceManager.onHostResume(this, this); 55 | } 56 | } 57 | 58 | @Override 59 | protected void onDestroy() { 60 | super.onDestroy(); 61 | 62 | if (mReactInstanceManager != null) { 63 | mReactInstanceManager.onHostDestroy(); 64 | } 65 | } 66 | 67 | @Override 68 | public void onBackPressed() { 69 | if (mReactInstanceManager != null) { 70 | mReactInstanceManager.onBackPressed(); 71 | } else { 72 | super.onBackPressed(); 73 | } 74 | } 75 | 76 | @Override 77 | public boolean onKeyUp(int keyCode, KeyEvent event) { 78 | if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { 79 | mReactInstanceManager.showDevOptionsDialog(); 80 | return true; 81 | } 82 | return super.onKeyUp(keyCode, event); 83 | } 84 | 85 | @Override 86 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 87 | if (requestCode == OVERLAY_PERMISSION_REQ_CODE) { 88 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 89 | if (!Settings.canDrawOverlays(this)) { 90 | // SYSTEM_ALERT_WINDOW permission not granted... 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/HybridActivity.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp; 2 | 3 | import android.os.Bundle; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.text.Editable; 7 | import android.text.TextWatcher; 8 | import android.widget.EditText; 9 | import com.amalbit.testandroidapp.helpers.ReactUtils; 10 | import com.amalbit.testandroidapp.module.package1.MessageFromReactToAndroidModule; 11 | import com.amalbit.testandroidapp.module.package1.TestPackage; 12 | import com.facebook.react.ReactInstanceManager; 13 | import com.facebook.react.ReactRootView; 14 | import com.facebook.react.bridge.Arguments; 15 | import com.facebook.react.bridge.WritableMap; 16 | import com.facebook.react.common.LifecycleState; 17 | import com.facebook.react.shell.MainReactPackage; 18 | 19 | public class HybridActivity extends BaseReactActivity implements MessageFromReactToAndroidModule.ReactEmmiter { 20 | 21 | private ReactRootView mReactRootView; 22 | 23 | private EditText edtMessageToReact; 24 | 25 | @Override protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_main); 28 | 29 | mReactInstanceManager = ReactInstanceManager.builder() 30 | .setApplication(getApplication()) 31 | .setBundleAssetName("index.android.bundle") 32 | .setJSMainModuleName("index.android") 33 | .addPackage(new MainReactPackage()) 34 | .addPackage(new TestPackage(this)) 35 | .setUseDeveloperSupport(BuildConfig.DEBUG) 36 | .setInitialLifecycleState(LifecycleState.RESUMED) 37 | .build(); 38 | 39 | mReactRootView = (ReactRootView) findViewById(R.id.root_react); 40 | mReactRootView.startReactApplication(mReactInstanceManager, "HelloWorld", null); 41 | 42 | edtMessageToReact = (EditText) findViewById(R.id.edt_messenger); 43 | 44 | edtMessageToReact.addTextChangedListener(new TextWatcher() { 45 | 46 | @Override 47 | public void afterTextChanged(Editable s) {} 48 | 49 | @Override 50 | public void beforeTextChanged(CharSequence s, int start, 51 | int count, int after) { 52 | } 53 | 54 | @Override 55 | public void onTextChanged(CharSequence s, int start, int before, int count) { 56 | WritableMap params = Arguments.createMap(); 57 | params.putString("message", s.toString()); 58 | ReactUtils.sendEvent(mReactInstanceManager.getCurrentReactContext(), "from_android", params); 59 | } 60 | }); 61 | } 62 | 63 | public void onReceive(final String msg){ 64 | new Handler(Looper.getMainLooper()).post(new Runnable() { 65 | @Override 66 | public void run() { 67 | edtMessageToReact.setText(msg); 68 | } 69 | }); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/Launch2Activity.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp; 2 | 3 | import android.content.Intent; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.widget.Button; 8 | 9 | public class Launch2Activity extends AppCompatActivity implements View.OnClickListener{ 10 | 11 | @Override protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | setContentView(R.layout.activity_launch2); 14 | 15 | ((Button)findViewById(R.id.btn_partial_screen)).setOnClickListener(this); 16 | ((Button)findViewById(R.id.btn_callbacks)).setOnClickListener(this); 17 | ((Button)findViewById(R.id.btn_module_view)).setOnClickListener(this); 18 | } 19 | 20 | @Override public void onClick(View view) { 21 | switch (view.getId()){ 22 | case R.id.btn_callbacks: 23 | Intent intent1 = new Intent(Launch2Activity.this, ReactCallBackActivity.class); 24 | startActivity(intent1); 25 | break; 26 | case R.id.btn_partial_screen: 27 | Intent intent = new Intent(Launch2Activity.this, HybridActivity.class); 28 | startActivity(intent); 29 | break; 30 | case R.id.btn_module_view: 31 | Intent intent2 = new Intent(Launch2Activity.this, ViewModuleActivity.class); 32 | startActivity(intent2); 33 | break; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/ReactCallBackActivity.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp; 2 | 3 | import android.os.Bundle; 4 | import com.amalbit.testandroidapp.callback.CallbackPackage; 5 | import com.facebook.react.ReactInstanceManager; 6 | import com.facebook.react.ReactRootView; 7 | import com.facebook.react.common.LifecycleState; 8 | import com.facebook.react.shell.MainReactPackage; 9 | 10 | public class ReactCallBackActivity extends BaseReactActivity { 11 | 12 | private ReactRootView mReactRootView; 13 | 14 | @Override protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_react_call_back); 17 | 18 | mReactInstanceManager = ReactInstanceManager.builder() 19 | .setApplication(getApplication()) 20 | .setBundleAssetName("index.android.bundle") 21 | .setJSMainModuleName("index.android") 22 | .addPackage(new MainReactPackage()) 23 | .addPackage(new CallbackPackage()) 24 | .setUseDeveloperSupport(BuildConfig.DEBUG) 25 | .setInitialLifecycleState(LifecycleState.RESUMED) 26 | .build(); 27 | 28 | mReactRootView = (ReactRootView) findViewById(R.id.root_react); 29 | mReactRootView.startReactApplication(mReactInstanceManager, "CallbackUI", null); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/ViewModuleActivity.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | 6 | import com.amalbit.testandroidapp.callback.CallbackPackage; 7 | import com.amalbit.testandroidapp.viewmodule.CustomUIPackage; 8 | import com.facebook.react.ReactInstanceManager; 9 | import com.facebook.react.ReactRootView; 10 | import com.facebook.react.common.LifecycleState; 11 | import com.facebook.react.shell.MainReactPackage; 12 | 13 | public class ViewModuleActivity extends BaseReactActivity { 14 | 15 | private ReactRootView mReactRootView; 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_view_module); 21 | 22 | mReactInstanceManager = ReactInstanceManager.builder() 23 | .setApplication(getApplication()) 24 | .setBundleAssetName("index.android.bundle") 25 | .setJSMainModuleName("index.android") 26 | .addPackage(new MainReactPackage()) 27 | .addPackage(new CustomUIPackage(this)) 28 | .setUseDeveloperSupport(BuildConfig.DEBUG) 29 | .setInitialLifecycleState(LifecycleState.RESUMED) 30 | .build(); 31 | 32 | mReactRootView = (ReactRootView) findViewById(R.id.root_react); 33 | mReactRootView.startReactApplication(mReactInstanceManager, "NativeModuleUI", null); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/callback/CallbackModule.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp.callback; 2 | 3 | import android.os.Handler; 4 | import android.widget.Toast; 5 | import com.facebook.react.bridge.Callback; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 8 | import com.facebook.react.bridge.ReactMethod; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * Created by amal.chandran on 16/03/17. 14 | */ 15 | public class CallbackModule extends ReactContextBaseJavaModule { 16 | 17 | /** 18 | * Wait time before returning the callback. 19 | * **/ 20 | private static final String DURATION_WAIT_TIME = "WAIT_TIME"; 21 | 22 | 23 | public CallbackModule(ReactApplicationContext reactContext) { 24 | super(reactContext); 25 | } 26 | 27 | /** 28 | * The purpose of this method is to return the string name of the NativeModule which 29 | * represents this class in JavaScript. So here we will call this ToastAndroid so that we can 30 | * access it through React.NativeModules.ToastAndroid in JavaScript. 31 | * **/ 32 | @Override public String getName() { 33 | return "CallbackAndroid"; 34 | } 35 | 36 | 37 | 38 | /** 39 | * To expose a method to JavaScript a Java method must be annotated using @ReactMethod. 40 | * The return type of bridge methods is always void. React Native bridge is asynchronous, 41 | * so the only way to pass a result to JavaScript is by using callbacks or emitting events 42 | * 43 | * Supported Argument types: 44 | * Boolean -> Bool 45 | * Integer -> Number 46 | * Double -> Number 47 | * Float -> Number 48 | * String -> String 49 | * Callback -> function 50 | * ReadableMap -> Object 51 | * ReadableArray -> Array 52 | * 53 | * 54 | * 55 | * **/ 56 | @ReactMethod 57 | public void waitOnAndroidAndCallback(final int duration, final Callback callback) { 58 | 59 | if (callback == null) return; 60 | 61 | final Handler handler = new Handler(); 62 | handler.postDelayed(new Runnable() { 63 | @Override 64 | public void run() { 65 | callback.invoke("You just waited on android for " + duration + " seconds."); 66 | } 67 | }, duration); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/callback/CallbackPackage.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp.callback; 2 | 3 | import com.amalbit.testandroidapp.module.package1.MessageFromReactToAndroidModule; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.bridge.JavaScriptModule; 6 | import com.facebook.react.bridge.NativeModule; 7 | import com.facebook.react.bridge.ReactApplicationContext; 8 | import com.facebook.react.uimanager.ViewManager; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | /** 14 | * Created by amal.chandran on 16/03/17. 15 | */ 16 | 17 | public class CallbackPackage implements ReactPackage { 18 | 19 | 20 | public CallbackPackage(){ 21 | } 22 | 23 | @Override 24 | public List> createJSModules() { 25 | return Collections.emptyList(); 26 | } 27 | 28 | @Override 29 | public List createViewManagers(ReactApplicationContext reactContext) { 30 | return Collections.emptyList(); 31 | } 32 | 33 | @Override 34 | public List createNativeModules(ReactApplicationContext reactContext) { 35 | List modules = new ArrayList<>(); 36 | 37 | modules.add(new CallbackModule(reactContext)); 38 | return modules; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/helpers/ReactUtils.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp.helpers; 2 | 3 | import android.support.annotation.Nullable; 4 | import com.facebook.react.bridge.ReactContext; 5 | import com.facebook.react.bridge.WritableMap; 6 | import com.facebook.react.modules.core.DeviceEventManagerModule; 7 | 8 | /** 9 | * Created by amal.chandran on 17/03/17. 10 | */ 11 | 12 | public class ReactUtils { 13 | 14 | public static void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) { 15 | reactContext 16 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) 17 | .emit(eventName, params); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/module/package1/MessageFromReactToAndroidModule.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp.module.package1; 2 | 3 | import android.widget.Toast; 4 | import com.facebook.react.bridge.ReactApplicationContext; 5 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 6 | import com.facebook.react.bridge.ReactMethod; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by amal.chandran on 16/03/17. 12 | * As per react doc. 13 | */ 14 | 15 | public class MessageFromReactToAndroidModule extends ReactContextBaseJavaModule { 16 | 17 | private static final String DURATION_SHORT_KEY = "SHORT"; 18 | private static final String DURATION_LONG_KEY = "LONG"; 19 | 20 | private ReactEmmiter reactEmmiter; 21 | 22 | public MessageFromReactToAndroidModule(ReactApplicationContext reactContext, ReactEmmiter reactEmmiter) { 23 | super(reactContext); 24 | this.reactEmmiter = reactEmmiter; 25 | } 26 | 27 | /** 28 | * The purpose of this method is to return the string name of the NativeModule which 29 | * represents this class in JavaScript. So here we will call this ToastAndroid so that we can 30 | * access it through React.NativeModules.ToastAndroid in JavaScript. 31 | * **/ 32 | @Override public String getName() { 33 | return "MessageFromReact"; 34 | } 35 | 36 | /** 37 | * An optional method called getConstants returns the constant values exposed to JavaScript. 38 | * Its implementation is not required but is very useful to key pre-defined values that need to 39 | * be communicated from JavaScript to Java in sync 40 | * **/ 41 | @Override 42 | public Map getConstants() { 43 | final Map constants = new HashMap<>(); 44 | constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT); 45 | constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG); 46 | return constants; 47 | } 48 | 49 | /** 50 | * To expose a method to JavaScript a Java method must be annotated using @ReactMethod. 51 | * The return type of bridge methods is always void. React Native bridge is asynchronous, 52 | * so the only way to pass a result to JavaScript is by using callbacks or emitting events 53 | * 54 | * Supported Argument types: 55 | * Boolean -> Bool 56 | * Integer -> Number 57 | * Double -> Number 58 | * Float -> Number 59 | * String -> String 60 | * Callback -> function 61 | * ReadableMap -> Object 62 | * ReadableArray -> Array 63 | * **/ 64 | @ReactMethod 65 | public void send(String message) { 66 | reactEmmiter.onReceive(message); 67 | } 68 | 69 | public interface ReactEmmiter { 70 | void onReceive(String msg); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/module/package1/TestPackage.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp.module.package1; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | /** 13 | * Created by amal.chandran on 16/03/17. 14 | */ 15 | 16 | public class TestPackage implements ReactPackage { 17 | 18 | private MessageFromReactToAndroidModule.ReactEmmiter mReactEmmiter; 19 | 20 | public TestPackage(MessageFromReactToAndroidModule.ReactEmmiter reactEmmiter){ 21 | mReactEmmiter = reactEmmiter; 22 | } 23 | 24 | @Override 25 | public List> createJSModules() { 26 | return Collections.emptyList(); 27 | } 28 | 29 | @Override 30 | public List createViewManagers(ReactApplicationContext reactContext) { 31 | return Collections.emptyList(); 32 | } 33 | 34 | @Override 35 | public List createNativeModules(ReactApplicationContext reactContext) { 36 | List modules = new ArrayList<>(); 37 | 38 | modules.add(new ToastOneModule(reactContext)); 39 | modules.add(new MessageFromReactToAndroidModule(reactContext, mReactEmmiter)); 40 | 41 | return modules; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/module/package1/ToastOneModule.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp.module.package1; 2 | 3 | import android.widget.Toast; 4 | import com.facebook.react.bridge.ReactApplicationContext; 5 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 6 | import com.facebook.react.bridge.ReactMethod; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by amal.chandran on 16/03/17. 12 | * As per react doc. 13 | */ 14 | 15 | public class ToastOneModule extends ReactContextBaseJavaModule { 16 | 17 | private static final String DURATION_SHORT_KEY = "SHORT"; 18 | private static final String DURATION_LONG_KEY = "LONG"; 19 | 20 | public ToastOneModule(ReactApplicationContext reactContext) { 21 | super(reactContext); 22 | } 23 | 24 | /** 25 | * The purpose of this method is to return the string name of the NativeModule which 26 | * represents this class in JavaScript. So here we will call this ToastAndroid so that we can 27 | * access it through React.NativeModules.ToastAndroid in JavaScript. 28 | * **/ 29 | @Override public String getName() { 30 | return "ToastOneAndroid"; 31 | } 32 | 33 | /** 34 | * An optional method called getConstants returns the constant values exposed to JavaScript. 35 | * Its implementation is not required but is very useful to key pre-defined values that need to 36 | * be communicated from JavaScript to Java in sync 37 | * **/ 38 | @Override 39 | public Map getConstants() { 40 | final Map constants = new HashMap<>(); 41 | constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT); 42 | constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG); 43 | return constants; 44 | } 45 | 46 | /** 47 | * To expose a method to JavaScript a Java method must be annotated using @ReactMethod. 48 | * The return type of bridge methods is always void. React Native bridge is asynchronous, 49 | * so the only way to pass a result to JavaScript is by using callbacks or emitting events 50 | * 51 | * Supported Argument types: 52 | * Boolean -> Bool 53 | * Integer -> Number 54 | * Double -> Number 55 | * Float -> Number 56 | * String -> String 57 | * Callback -> function 58 | * ReadableMap -> Object 59 | * ReadableArray -> Array 60 | * **/ 61 | @ReactMethod 62 | public void show(String message, int duration) { 63 | Toast.makeText(getReactApplicationContext(), message, duration).show(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/viewmodule/CustomLayout.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp.viewmodule; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.AppCompatButton; 5 | import android.util.AttributeSet; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import com.amalbit.testandroidapp.R; 10 | 11 | /** 12 | * Created by amal.chandran on 21/03/17. 13 | */ 14 | 15 | public class CustomLayout extends ViewGroup { 16 | public CustomLayout(Context context) { 17 | super(context); 18 | initView(); 19 | } 20 | 21 | public CustomLayout(Context context, AttributeSet attrs) { 22 | super(context, attrs); 23 | initView(); 24 | } 25 | 26 | public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) { 27 | super(context, attrs, defStyleAttr); 28 | initView(); 29 | } 30 | 31 | @Override 32 | protected void onLayout(boolean b, int i, int i1, int i2, int i3) { 33 | } 34 | 35 | private void initView() { 36 | View view = inflate(getContext(), R.layout.my_view_layout, null); 37 | addView(view); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/viewmodule/CustomUIPackage.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp.viewmodule; 2 | 3 | import android.content.Context; 4 | 5 | import com.facebook.react.ReactPackage; 6 | import com.facebook.react.bridge.JavaScriptModule; 7 | import com.facebook.react.bridge.NativeModule; 8 | import com.facebook.react.bridge.ReactApplicationContext; 9 | import com.facebook.react.uimanager.ViewManager; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | /** 17 | * Created by amal.chandran on 22/03/17. 18 | */ 19 | 20 | public class CustomUIPackage implements ReactPackage { 21 | 22 | private Context mContext; 23 | 24 | public CustomUIPackage(Context context){ 25 | mContext = context; 26 | } 27 | 28 | @Override 29 | public List createNativeModules(ReactApplicationContext reactContext) { 30 | List modules = new ArrayList<>(); 31 | // modules.add(new ToastModule(reactContext)); 32 | return modules; 33 | } 34 | 35 | @Override 36 | public List> createJSModules() { 37 | return Collections.emptyList(); 38 | } 39 | 40 | @Override 41 | public List createViewManagers(ReactApplicationContext reactContext) { 42 | return Arrays.asList( 43 | new ReactLayoutManager(mContext) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/amalbit/testandroidapp/viewmodule/ReactLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp.viewmodule; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Nullable; 5 | import android.view.ViewGroup; 6 | 7 | import com.facebook.react.uimanager.SimpleViewManager; 8 | import com.facebook.react.uimanager.ThemedReactContext; 9 | 10 | /** 11 | * Created by amal.chandran on 21/03/17. 12 | */ 13 | 14 | public class ReactLayoutManager extends SimpleViewManager { 15 | public static final String REACT_CLASS = "RCTCustomView"; 16 | 17 | private final @Nullable Context mCallerContext; 18 | 19 | public ReactLayoutManager( Context callerContext) { 20 | mCallerContext = callerContext; 21 | } 22 | 23 | @Override 24 | public String getName() { 25 | return REACT_CLASS; 26 | } 27 | 28 | @Override 29 | public ViewGroup createViewInstance(ThemedReactContext context) { 30 | return new CustomLayout(mCallerContext); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/test/java/com/amalbit/testandroidapp/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.amalbit.testandroidapp; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test public void addition_isCorrect() throws Exception { 14 | assertEquals(4, 2 + 2); 15 | } 16 | } -------------------------------------------------------------------------------- /base.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/base.apk -------------------------------------------------------------------------------- /communication_react_android.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/communication_react_android.gif -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import React from 'react'; 3 | import { 4 | AppRegistry, 5 | StyleSheet, 6 | Text, 7 | Image, 8 | ToastAndroid, 9 | TouchableHighlight, 10 | View, 11 | TextInput, 12 | DeviceEventEmitter 13 | } from 'react-native'; 14 | 15 | import MessageFromReact from './reactapp/MessageFromReact'; 16 | import CallbackUI from './reactapp/CallbackUI'; 17 | import NativeModuleUI from './reactapp/NativeUiModuleUI'; 18 | 19 | class HelloWorld extends React.Component { 20 | 21 | constructor(){ 22 | super(); 23 | this.state = { message: "To Android! 1" }; 24 | 25 | DeviceEventEmitter.addListener('from_android', (e) => { 26 | this.setState({ message: e.message }); 27 | }); 28 | 29 | } 30 | 31 | render() { 32 | return ( 33 | 34 | 35 | MessageFromReact.send(text)} 40 | /> 41 | 42 | ) 43 | } 44 | } 45 | var styles = StyleSheet.create({ 46 | container: { 47 | flex: 1, 48 | justifyContent: 'center', 49 | backgroundColor: '#00a4d3', 50 | alignItems: 'center' 51 | }, 52 | hello: { 53 | alignSelf: 'stretch', 54 | fontSize: 16, 55 | textAlign: 'center' 56 | }, 57 | image: { 58 | width : 45, 59 | height : 45 60 | } 61 | }); 62 | 63 | AppRegistry.registerComponent('HelloWorld', () => HelloWorld); 64 | AppRegistry.registerComponent('CallbackUI', () => CallbackUI); 65 | AppRegistry.registerComponent('NativeModuleUI', () => NativeModuleUI); -------------------------------------------------------------------------------- /npm-debug.log.1692254132: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/npm-debug.log.1692254132 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_android_app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.android.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node node_modules/react-native/local-cli/cli.js start" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "react": "^15.4.2", 14 | "react-native": "^0.42.0", 15 | "react-native-code-push": "^1.17.2-beta" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /reactapp/Callback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { NativeModules } from 'react-native'; 3 | module.exports = NativeModules.CallbackAndroid; -------------------------------------------------------------------------------- /reactapp/CallbackUI.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet, Button, ToastAndroid } from 'react-native'; 3 | 4 | import Callback from './Callback'; 5 | 6 | export default class CallbackUI extends React.Component { 7 | render() { 8 | return ( 9 | 10 | CallbackUI 11 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | var styles = StyleSheet.create({ 22 | mainContainer:{ 23 | flex : 1, 24 | backgroundColor:'#81c04d', 25 | flexDirection: 'column', 26 | justifyContent: 'space-around', 27 | alignItems: 'center', 28 | } 29 | }); 30 | 31 | module.exports = CallbackUI; -------------------------------------------------------------------------------- /reactapp/CustomUIFromAndroid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { PropTypes, requireNativeComponent } from 'react-native'; 3 | var iprop = { 4 | name: 'CustomUI', 5 | // propTypes: { 6 | // text: PropTypes.string 7 | // } 8 | } 9 | module.exports = requireNativeComponent('CustomUI', iprop); -------------------------------------------------------------------------------- /reactapp/MessageFromReact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { NativeModules } from 'react-native'; 3 | module.exports = NativeModules.MessageFromReact; -------------------------------------------------------------------------------- /reactapp/NativeUiModuleUI.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet, Button } from 'react-native'; 3 | 4 | import Callback from './Callback'; 5 | 6 | var CustomUI = require('./CustomUIFromAndroid'); 7 | 8 | export default class NativeModuleUI extends React.Component { 9 | render() { 10 | return ( 11 | 12 | 13 | ); 14 | } 15 | } 16 | 17 | var styles = StyleSheet.create({ 18 | mainContainer:{ 19 | flex : 1, 20 | backgroundColor:'#81c04d', 21 | flexDirection: 'column', 22 | justifyContent: 'space-around', 23 | alignItems: 'center', 24 | } 25 | }); 26 | 27 | module.exports = NativeModuleUI; -------------------------------------------------------------------------------- /reactapp/ToastOneAndroid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * This exposes the native ToastAndroid module as a JS module. This has a 4 | * function 'show' which takes the following parameters: 5 | * 6 | * 1. String message: A string with the text to toast 7 | * 2. int duration: The duration of the toast. May be ToastAndroid.SHORT or 8 | * ToastAndroid.LONG 9 | */ 10 | import { NativeModules } from 'react-native'; 11 | module.exports = NativeModules.ToastOneAndroid; -------------------------------------------------------------------------------- /reactapp/images/header_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/reactapp/images/header_logo.png -------------------------------------------------------------------------------- /reactapp/images/laptop_phone_howitworks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalChandran/ReactNative_Android_integration/0c636c16fab31496f2709285fb7df22f2c795b65/reactapp/images/laptop_phone_howitworks.png --------------------------------------------------------------------------------