mirror of
https://github.com/zhigang1992/react-native-code-push.git
synced 2026-06-17 03:59:19 +08:00
merge with master
This commit is contained in:
@@ -1,137 +1,115 @@
|
||||
package com.microsoft.codepush.react;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.ReactChoreographer;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.Settings;
|
||||
import android.view.Choreographer;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class CodePush implements ReactPackage {
|
||||
private static boolean needToReportRollback = false;
|
||||
private static boolean isRunningBinaryVersion = false;
|
||||
private static boolean testConfigurationFlag = false;
|
||||
|
||||
private boolean didUpdate = false;
|
||||
private static boolean sIsRunningBinaryVersion = false;
|
||||
private static boolean sNeedToReportRollback = false;
|
||||
private static boolean sTestConfigurationFlag = false;
|
||||
|
||||
private String assetsBundleFileName;
|
||||
private boolean mDidUpdate = false;
|
||||
|
||||
private static final String ASSETS_BUNDLE_PREFIX = "assets://";
|
||||
private static final String BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime";
|
||||
private final String CODE_PUSH_PREFERENCES = "CodePush";
|
||||
private static final String DEFAULT_JS_BUNDLE_NAME = "index.android.bundle";
|
||||
private final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress";
|
||||
private final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES";
|
||||
private final String PACKAGE_HASH_KEY = "packageHash";
|
||||
private final String PENDING_UPDATE_HASH_KEY = "hash";
|
||||
private final String PENDING_UPDATE_IS_LOADING_KEY = "isLoading";
|
||||
private final String PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE";
|
||||
private final String RESOURCES_BUNDLE = "resources.arsc";
|
||||
private String mAssetsBundleFileName;
|
||||
|
||||
// Helper classes.
|
||||
private CodePushNativeModule codePushNativeModule;
|
||||
private CodePushPackage codePushPackage;
|
||||
private CodePushTelemetryManager codePushTelemetryManager;
|
||||
private CodePushNativeModule mNativeModule;
|
||||
private CodePushUpdateManager mUpdateManager;
|
||||
private CodePushTelemetryManager mTelemetryManager;
|
||||
private SettingsManager mSettingsManager;
|
||||
|
||||
// Config properties.
|
||||
private String appVersion;
|
||||
private int buildVersion;
|
||||
private String deploymentKey;
|
||||
private String serverUrl = "https://codepush.azurewebsites.net/";
|
||||
private String mAppVersion;
|
||||
private String mDeploymentKey;
|
||||
private String mServerUrl = "https://codepush.azurewebsites.net/";
|
||||
|
||||
private Activity mainActivity;
|
||||
private Context applicationContext;
|
||||
private final boolean isDebugMode;
|
||||
private Context mContext;
|
||||
private final boolean mIsDebugMode;
|
||||
|
||||
private static CodePush currentInstance;
|
||||
private static CodePush mCurrentInstance;
|
||||
|
||||
public CodePush(String deploymentKey, Activity mainActivity) {
|
||||
this(deploymentKey, mainActivity, false);
|
||||
public CodePush(String deploymentKey, Context context) {
|
||||
this(deploymentKey, context, false);
|
||||
}
|
||||
|
||||
public CodePush(String deploymentKey, Activity mainActivity, boolean isDebugMode) {
|
||||
SoLoader.init(mainActivity, false);
|
||||
this.applicationContext = mainActivity.getApplicationContext();
|
||||
this.codePushPackage = new CodePushPackage(mainActivity.getFilesDir().getAbsolutePath());
|
||||
this.codePushTelemetryManager = new CodePushTelemetryManager(this.applicationContext, CODE_PUSH_PREFERENCES);
|
||||
this.deploymentKey = deploymentKey;
|
||||
this.isDebugMode = isDebugMode;
|
||||
this.mainActivity = mainActivity;
|
||||
public CodePush(String deploymentKey, Context context, boolean isDebugMode) {
|
||||
SoLoader.init(context, false);
|
||||
mContext = context.getApplicationContext();
|
||||
|
||||
mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath());
|
||||
mTelemetryManager = new CodePushTelemetryManager(mContext);
|
||||
mDeploymentKey = deploymentKey;
|
||||
mIsDebugMode = isDebugMode;
|
||||
mSettingsManager = new SettingsManager(mContext);
|
||||
|
||||
try {
|
||||
PackageInfo pInfo = applicationContext.getPackageManager().getPackageInfo(applicationContext.getPackageName(), 0);
|
||||
appVersion = pInfo.versionName;
|
||||
buildVersion = pInfo.versionCode;
|
||||
PackageInfo pInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
||||
mAppVersion = pInfo.versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new CodePushUnknownException("Unable to get package info for " + applicationContext.getPackageName(), e);
|
||||
throw new CodePushUnknownException("Unable to get package info for " + mContext.getPackageName(), e);
|
||||
}
|
||||
|
||||
currentInstance = this;
|
||||
mCurrentInstance = this;
|
||||
|
||||
clearDebugCacheIfNeeded();
|
||||
initializeUpdateAfterRestart();
|
||||
}
|
||||
|
||||
// USED FOR TESTING SO THAT IT CAN CONNECT TO DEBUG SERVER
|
||||
public CodePush(String deploymentKey, Activity mainActivity, boolean isDebugMode, String serverUrl) {
|
||||
this(deploymentKey, mainActivity, isDebugMode);
|
||||
this.serverUrl = serverUrl;
|
||||
public CodePush(String deploymentKey, Context context, boolean isDebugMode, String serverUrl) {
|
||||
this(deploymentKey, context, isDebugMode);
|
||||
mServerUrl = serverUrl;
|
||||
}
|
||||
|
||||
private void clearDebugCacheIfNeeded() {
|
||||
if (isDebugMode && isPendingUpdate(null)) {
|
||||
public void clearDebugCacheIfNeeded() {
|
||||
if (mIsDebugMode && mSettingsManager.isPendingUpdate(null)) {
|
||||
// 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
|
||||
File cachedDevBundle = new File(applicationContext.getFilesDir(), "ReactNativeDevBundle.js");
|
||||
File cachedDevBundle = new File(mContext.getFilesDir(), "ReactNativeDevBundle.js");
|
||||
if (cachedDevBundle.exists()) {
|
||||
cachedDevBundle.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long getBinaryResourcesModifiedTime() {
|
||||
public boolean didUpdate() {
|
||||
return mDidUpdate;
|
||||
}
|
||||
|
||||
public String getAppVersion() {
|
||||
return mAppVersion;
|
||||
}
|
||||
|
||||
public String getAssetsBundleFileName() {
|
||||
return mAssetsBundleFileName;
|
||||
}
|
||||
|
||||
long getBinaryResourcesModifiedTime() {
|
||||
ZipFile applicationFile = null;
|
||||
try {
|
||||
ApplicationInfo ai = applicationContext.getPackageManager().getApplicationInfo(applicationContext.getPackageName(), 0);
|
||||
ApplicationInfo ai = this.mContext.getPackageManager().getApplicationInfo(this.mContext.getPackageName(), 0);
|
||||
applicationFile = new ZipFile(ai.sourceDir);
|
||||
ZipEntry classesDexEntry = applicationFile.getEntry(RESOURCES_BUNDLE);
|
||||
ZipEntry classesDexEntry = applicationFile.getEntry(CodePushConstants.RESOURCES_BUNDLE);
|
||||
return classesDexEntry.getTime();
|
||||
} catch (PackageManager.NameNotFoundException | IOException e) {
|
||||
throw new CodePushUnknownException("Error in getting file information about compiled resources", e);
|
||||
@@ -146,35 +124,53 @@ public class CodePush implements ReactPackage {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static String getBundleUrl() {
|
||||
return getBundleUrl(DEFAULT_JS_BUNDLE_NAME);
|
||||
return getJSBundleFile();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static String getBundleUrl(String assetsBundleFileName) {
|
||||
if (currentInstance == null) {
|
||||
return getJSBundleFile(assetsBundleFileName);
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
public String getDeploymentKey() {
|
||||
return mDeploymentKey;
|
||||
}
|
||||
|
||||
public static String getJSBundleFile() {
|
||||
return CodePush.getJSBundleFile(CodePushConstants.DEFAULT_JS_BUNDLE_NAME);
|
||||
}
|
||||
|
||||
public static String getJSBundleFile(String assetsBundleFileName) {
|
||||
if (mCurrentInstance == null) {
|
||||
throw new CodePushNotInitializedException("A CodePush instance has not been created yet. Have you added it to your app's list of ReactPackages?");
|
||||
}
|
||||
|
||||
return currentInstance.getBundleUrlInternal(assetsBundleFileName);
|
||||
return mCurrentInstance.getJSBundleFileInternal(assetsBundleFileName);
|
||||
}
|
||||
|
||||
public String getBundleUrlInternal(String assetsBundleFileName) {
|
||||
this.assetsBundleFileName = assetsBundleFileName;
|
||||
String binaryJsBundleUrl = ASSETS_BUNDLE_PREFIX + assetsBundleFileName;
|
||||
public String getJSBundleFileInternal(String assetsBundleFileName) {
|
||||
this.mAssetsBundleFileName = assetsBundleFileName;
|
||||
String binaryJsBundleUrl = CodePushConstants.ASSETS_BUNDLE_PREFIX + assetsBundleFileName;
|
||||
long binaryResourcesModifiedTime = this.getBinaryResourcesModifiedTime();
|
||||
|
||||
try {
|
||||
String packageFilePath = codePushPackage.getCurrentPackageBundlePath(this.assetsBundleFileName);
|
||||
String packageFilePath = mUpdateManager.getCurrentPackageBundlePath(this.mAssetsBundleFileName);
|
||||
if (packageFilePath == null) {
|
||||
// There has not been any downloaded updates.
|
||||
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
|
||||
isRunningBinaryVersion = true;
|
||||
sIsRunningBinaryVersion = true;
|
||||
return binaryJsBundleUrl;
|
||||
}
|
||||
|
||||
ReadableMap packageMetadata = this.codePushPackage.getCurrentPackage();
|
||||
ReadableMap packageMetadata = this.mUpdateManager.getCurrentPackage();
|
||||
Long binaryModifiedDateDuringPackageInstall = null;
|
||||
String binaryModifiedDateDuringPackageInstallString = CodePushUtils.tryGetString(packageMetadata, BINARY_MODIFIED_TIME_KEY);
|
||||
String binaryModifiedDateDuringPackageInstallString = CodePushUtils.tryGetString(packageMetadata, CodePushConstants.BINARY_MODIFIED_TIME_KEY);
|
||||
if (binaryModifiedDateDuringPackageInstallString != null) {
|
||||
binaryModifiedDateDuringPackageInstall = Long.parseLong(binaryModifiedDateDuringPackageInstallString);
|
||||
}
|
||||
@@ -182,19 +178,19 @@ public class CodePush implements ReactPackage {
|
||||
String packageAppVersion = CodePushUtils.tryGetString(packageMetadata, "appVersion");
|
||||
if (binaryModifiedDateDuringPackageInstall != null &&
|
||||
binaryModifiedDateDuringPackageInstall == binaryResourcesModifiedTime &&
|
||||
(isUsingTestConfiguration() || this.appVersion.equals(packageAppVersion))) {
|
||||
(isUsingTestConfiguration() || this.mAppVersion.equals(packageAppVersion))) {
|
||||
CodePushUtils.logBundleUrl(packageFilePath);
|
||||
isRunningBinaryVersion = false;
|
||||
sIsRunningBinaryVersion = false;
|
||||
return packageFilePath;
|
||||
} else {
|
||||
// The binary version is newer.
|
||||
this.didUpdate = false;
|
||||
if (!this.isDebugMode || !this.appVersion.equals(packageAppVersion)) {
|
||||
this.mDidUpdate = false;
|
||||
if (!this.mIsDebugMode || !this.mAppVersion.equals(packageAppVersion)) {
|
||||
this.clearUpdates();
|
||||
}
|
||||
|
||||
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
|
||||
isRunningBinaryVersion = true;
|
||||
sIsRunningBinaryVersion = true;
|
||||
return binaryJsBundleUrl;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
@@ -202,63 +198,33 @@ public class CodePush implements ReactPackage {
|
||||
}
|
||||
}
|
||||
|
||||
private JSONArray getFailedUpdates() {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null);
|
||||
if (failedUpdatesString == null) {
|
||||
return new JSONArray();
|
||||
}
|
||||
|
||||
try {
|
||||
return new JSONArray(failedUpdatesString);
|
||||
} catch (JSONException e) {
|
||||
// Unrecognized data format, clear and replace with expected format.
|
||||
JSONArray emptyArray = new JSONArray();
|
||||
settings.edit().putString(FAILED_UPDATES_KEY, emptyArray.toString()).commit();
|
||||
return emptyArray;
|
||||
}
|
||||
public String getServerUrl() {
|
||||
return mServerUrl;
|
||||
}
|
||||
|
||||
private JSONObject getPendingUpdate() {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
String pendingUpdateString = settings.getString(PENDING_UPDATE_KEY, null);
|
||||
if (pendingUpdateString == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new JSONObject(pendingUpdateString);
|
||||
} catch (JSONException e) {
|
||||
// Should not happen.
|
||||
CodePushUtils.log("Unable to parse pending update metadata " + pendingUpdateString +
|
||||
" stored in SharedPreferences");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeUpdateAfterRestart() {
|
||||
void initializeUpdateAfterRestart() {
|
||||
// Reset the state which indicates that
|
||||
// the app was just freshly updated.
|
||||
didUpdate = false;
|
||||
mDidUpdate = false;
|
||||
|
||||
JSONObject pendingUpdate = getPendingUpdate();
|
||||
JSONObject pendingUpdate = mSettingsManager.getPendingUpdate();
|
||||
if (pendingUpdate != null) {
|
||||
try {
|
||||
boolean updateIsLoading = pendingUpdate.getBoolean(PENDING_UPDATE_IS_LOADING_KEY);
|
||||
boolean updateIsLoading = pendingUpdate.getBoolean(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY);
|
||||
if (updateIsLoading) {
|
||||
// Pending update was initialized, but notifyApplicationReady was not called.
|
||||
// Therefore, deduce that it is a broken update and rollback.
|
||||
CodePushUtils.log("Update did not finish loading the last time, rolling back to a previous version.");
|
||||
needToReportRollback = true;
|
||||
sNeedToReportRollback = true;
|
||||
rollbackPackage();
|
||||
} else {
|
||||
// There is in fact a new update running for the first
|
||||
// time, so update the local state to ensure the client knows.
|
||||
didUpdate = true;
|
||||
mDidUpdate = true;
|
||||
|
||||
// Mark that we tried to initialize the new update, so that if it crashes,
|
||||
// we will know that we need to rollback when the app next starts.
|
||||
savePendingUpdate(pendingUpdate.getString(PENDING_UPDATE_HASH_KEY),
|
||||
mSettingsManager.savePendingUpdate(pendingUpdate.getString(CodePushConstants.PENDING_UPDATE_HASH_KEY),
|
||||
/* isLoading */true);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
@@ -268,512 +234,55 @@ public class CodePush implements ReactPackage {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFailedHash(String packageHash) {
|
||||
JSONArray failedUpdates = getFailedUpdates();
|
||||
if (packageHash != null) {
|
||||
for (int i = 0; i < failedUpdates.length(); i++) {
|
||||
try {
|
||||
JSONObject failedPackage = failedUpdates.getJSONObject(i);
|
||||
String failedPackageHash = failedPackage.getString(PACKAGE_HASH_KEY);
|
||||
if (packageHash.equals(failedPackageHash)) {
|
||||
return true;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to read failedUpdates data stored in SharedPreferences.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
void invalidateCurrentInstance() {
|
||||
mCurrentInstance = null;
|
||||
}
|
||||
|
||||
private boolean isPendingUpdate(String packageHash) {
|
||||
JSONObject pendingUpdate = getPendingUpdate();
|
||||
|
||||
try {
|
||||
return pendingUpdate != null &&
|
||||
!pendingUpdate.getBoolean(PENDING_UPDATE_IS_LOADING_KEY) &&
|
||||
(packageHash == null || pendingUpdate.getString(PENDING_UPDATE_HASH_KEY).equals(packageHash));
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to read pending update metadata in isPendingUpdate.", e);
|
||||
}
|
||||
boolean isDebugMode() {
|
||||
return mIsDebugMode;
|
||||
}
|
||||
|
||||
private void removeFailedUpdates() {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
settings.edit().remove(FAILED_UPDATES_KEY).commit();
|
||||
boolean isRunningBinaryVersion() {
|
||||
return sIsRunningBinaryVersion;
|
||||
}
|
||||
|
||||
private void removePendingUpdate() {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
settings.edit().remove(PENDING_UPDATE_KEY).commit();
|
||||
boolean needToReportRollback() {
|
||||
return sNeedToReportRollback;
|
||||
}
|
||||
|
||||
private void rollbackPackage() {
|
||||
WritableMap failedPackage = codePushPackage.getCurrentPackage();
|
||||
saveFailedUpdate(failedPackage);
|
||||
codePushPackage.rollbackPackage();
|
||||
removePendingUpdate();
|
||||
WritableMap failedPackage = mUpdateManager.getCurrentPackage();
|
||||
mSettingsManager.saveFailedUpdate(failedPackage);
|
||||
mUpdateManager.rollbackPackage();
|
||||
mSettingsManager.removePendingUpdate();
|
||||
}
|
||||
|
||||
private void saveFailedUpdate(ReadableMap failedPackage) {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
String failedUpdatesString = settings.getString(FAILED_UPDATES_KEY, null);
|
||||
JSONArray failedUpdates;
|
||||
if (failedUpdatesString == null) {
|
||||
failedUpdates = new JSONArray();
|
||||
} else {
|
||||
try {
|
||||
failedUpdates = new JSONArray(failedUpdatesString);
|
||||
} catch (JSONException e) {
|
||||
// Should not happen.
|
||||
throw new CodePushMalformedDataException("Unable to parse failed updates information " +
|
||||
failedUpdatesString + " stored in SharedPreferences", e);
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject failedPackageJSON = CodePushUtils.convertReadableToJsonObject(failedPackage);
|
||||
failedUpdates.put(failedPackageJSON);
|
||||
settings.edit().putString(FAILED_UPDATES_KEY, failedUpdates.toString()).commit();
|
||||
}
|
||||
|
||||
private void savePendingUpdate(String packageHash, boolean isLoading) {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
JSONObject pendingUpdate = new JSONObject();
|
||||
try {
|
||||
pendingUpdate.put(PENDING_UPDATE_HASH_KEY, packageHash);
|
||||
pendingUpdate.put(PENDING_UPDATE_IS_LOADING_KEY, isLoading);
|
||||
settings.edit().putString(PENDING_UPDATE_KEY, pendingUpdate.toString()).commit();
|
||||
} catch (JSONException e) {
|
||||
// Should not happen.
|
||||
throw new CodePushUnknownException("Unable to save pending update.", e);
|
||||
}
|
||||
public void setNeedToReportRollback(boolean needToReportRollback) {
|
||||
CodePush.sNeedToReportRollback = needToReportRollback;
|
||||
}
|
||||
|
||||
/* The below 3 methods are used for running tests.*/
|
||||
public static boolean isUsingTestConfiguration() {
|
||||
return testConfigurationFlag;
|
||||
return sTestConfigurationFlag;
|
||||
}
|
||||
|
||||
public static void setUsingTestConfiguration(boolean shouldUseTestConfiguration) {
|
||||
testConfigurationFlag = shouldUseTestConfiguration;
|
||||
sTestConfigurationFlag = shouldUseTestConfiguration;
|
||||
}
|
||||
|
||||
public void clearUpdates() {
|
||||
codePushPackage.clearUpdates();
|
||||
removePendingUpdate();
|
||||
removeFailedUpdates();
|
||||
}
|
||||
|
||||
private class CodePushNativeModule extends ReactContextBaseJavaModule {
|
||||
private LifecycleEventListener lifecycleEventListener = null;
|
||||
private int minimumBackgroundDuration = 0;
|
||||
|
||||
public CodePushNativeModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
final Map<String, Object> constants = new HashMap<>();
|
||||
|
||||
constants.put("codePushInstallModeImmediate", CodePushInstallMode.IMMEDIATE.getValue());
|
||||
constants.put("codePushInstallModeOnNextRestart", CodePushInstallMode.ON_NEXT_RESTART.getValue());
|
||||
constants.put("codePushInstallModeOnNextResume", CodePushInstallMode.ON_NEXT_RESUME.getValue());
|
||||
|
||||
constants.put("codePushUpdateStateRunning", CodePushUpdateState.RUNNING.getValue());
|
||||
constants.put("codePushUpdateStatePending", CodePushUpdateState.PENDING.getValue());
|
||||
constants.put("codePushUpdateStateLatest", CodePushUpdateState.LATEST.getValue());
|
||||
|
||||
return constants;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CodePush";
|
||||
}
|
||||
|
||||
private void loadBundleLegacy() {
|
||||
Intent intent = mainActivity.getIntent();
|
||||
mainActivity.finish();
|
||||
mainActivity.startActivity(intent);
|
||||
|
||||
currentInstance = null;
|
||||
}
|
||||
|
||||
private void loadBundle() {
|
||||
CodePush.this.clearDebugCacheIfNeeded();
|
||||
|
||||
// Our preferred reload logic relies on the user's Activity inheriting from the
|
||||
// core ReactActivity class, so if it doesn't, we fallback early to our legacy behavior.
|
||||
if (!ReactActivity.class.isInstance(mainActivity)) {
|
||||
loadBundleLegacy();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// #1) Get the private ReactInstanceManager, which is what includes
|
||||
// the logic to reload the current React context.
|
||||
Field instanceManagerField = ReactActivity.class.getDeclaredField("mReactInstanceManager");
|
||||
instanceManagerField.setAccessible(true); // Make a private field accessible
|
||||
final Object instanceManager = instanceManagerField.get(mainActivity);
|
||||
|
||||
// #2) Update the locally stored JS bundle file path
|
||||
String latestJSBundleFile = CodePush.this.getBundleUrlInternal(CodePush.this.assetsBundleFileName);
|
||||
Field jsBundleField = instanceManager.getClass().getDeclaredField("mJSBundleFile");
|
||||
jsBundleField.setAccessible(true);
|
||||
jsBundleField.set(instanceManager, latestJSBundleFile);
|
||||
|
||||
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
|
||||
final Method recreateMethod = instanceManager.getClass().getMethod("recreateReactContextInBackground");
|
||||
mainActivity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
recreateMethod.invoke(instanceManager);
|
||||
initializeUpdateAfterRestart();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// The recreation method threw an unknown exception
|
||||
// so just simply fallback to restarting the Activity
|
||||
loadBundleLegacy();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Our reflection logic failed somewhere
|
||||
// so fall back to restarting the Activity
|
||||
loadBundleLegacy();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) {
|
||||
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
WritableMap mutableUpdatePackage = CodePushUtils.convertReadableMapToWritableMap(updatePackage);
|
||||
mutableUpdatePackage.putString(BINARY_MODIFIED_TIME_KEY, "" + getBinaryResourcesModifiedTime());
|
||||
codePushPackage.downloadPackage(mutableUpdatePackage, CodePush.this.assetsBundleFileName, new DownloadProgressCallback() {
|
||||
private boolean hasScheduledNextFrame = false;
|
||||
private DownloadProgress latestDownloadProgress = null;
|
||||
|
||||
@Override
|
||||
public void call(DownloadProgress downloadProgress) {
|
||||
if (!notifyProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
latestDownloadProgress = downloadProgress;
|
||||
// If the download is completed, synchronously send the last event.
|
||||
if (latestDownloadProgress.isCompleted()) {
|
||||
dispatchDownloadProgressEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasScheduledNextFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasScheduledNextFrame = true;
|
||||
getReactApplicationContext().runOnUiQueueThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new Choreographer.FrameCallback() {
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
if (!latestDownloadProgress.isCompleted()) {
|
||||
dispatchDownloadProgressEvent();
|
||||
}
|
||||
|
||||
hasScheduledNextFrame = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchDownloadProgressEvent() {
|
||||
getReactApplicationContext()
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
|
||||
}
|
||||
});
|
||||
|
||||
WritableMap newPackage = codePushPackage.getPackage(CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY));
|
||||
promise.resolve(newPackage);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
promise.reject(e);
|
||||
} catch (CodePushInvalidUpdateException e) {
|
||||
e.printStackTrace();
|
||||
saveFailedUpdate(updatePackage);
|
||||
promise.reject(e);
|
||||
} catch (CodePushMalformedDataException e) {
|
||||
e.printStackTrace();
|
||||
saveFailedUpdate(updatePackage);
|
||||
promise.reject(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
asyncTask.execute();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getConfiguration(Promise promise) {
|
||||
WritableNativeMap configMap = new WritableNativeMap();
|
||||
configMap.putString("appVersion", appVersion);
|
||||
configMap.putInt("buildVersion", buildVersion);
|
||||
configMap.putString("deploymentKey", deploymentKey);
|
||||
configMap.putString("serverUrl", serverUrl);
|
||||
configMap.putString("clientUniqueId",
|
||||
Settings.Secure.getString(mainActivity.getContentResolver(),
|
||||
android.provider.Settings.Secure.ANDROID_ID));
|
||||
String binaryHash = CodePushUpdateUtils.getHashForBinaryContents(mainActivity, isDebugMode);
|
||||
if (binaryHash != null) {
|
||||
// binaryHash will be null if the React Native assets were not bundled into the APK
|
||||
// (e.g. in Debug builds)
|
||||
configMap.putString(PACKAGE_HASH_KEY, binaryHash);
|
||||
}
|
||||
|
||||
promise.resolve(configMap);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getUpdateMetadata(final int updateState, final Promise promise) {
|
||||
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
WritableMap currentPackage = codePushPackage.getCurrentPackage();
|
||||
|
||||
if (currentPackage == null) {
|
||||
promise.resolve("");
|
||||
return null;
|
||||
}
|
||||
|
||||
Boolean currentUpdateIsPending = false;
|
||||
|
||||
if (currentPackage.hasKey(PACKAGE_HASH_KEY)) {
|
||||
String currentHash = currentPackage.getString(PACKAGE_HASH_KEY);
|
||||
currentUpdateIsPending = CodePush.this.isPendingUpdate(currentHash);
|
||||
}
|
||||
|
||||
if (updateState == CodePushUpdateState.PENDING.getValue() && !currentUpdateIsPending) {
|
||||
// The caller wanted a pending update
|
||||
// but there isn't currently one.
|
||||
promise.resolve("");
|
||||
} else if (updateState == CodePushUpdateState.RUNNING.getValue() && currentUpdateIsPending) {
|
||||
// The caller wants the running update, but the current
|
||||
// one is pending, so we need to grab the previous.
|
||||
promise.resolve(codePushPackage.getPreviousPackage());
|
||||
} else {
|
||||
// The current package satisfies the request:
|
||||
// 1) Caller wanted a pending, and there is a pending update
|
||||
// 2) Caller wanted the running update, and there isn't a pending
|
||||
// 3) Caller wants the latest update, regardless if it's pending or not
|
||||
if (isRunningBinaryVersion) {
|
||||
// This only matters in Debug builds. Since we do not clear "outdated" updates,
|
||||
// we need to indicate to the JS side that somehow we have a current update on
|
||||
// disk that is not actually running.
|
||||
currentPackage.putBoolean("_isDebugOnly", true);
|
||||
}
|
||||
|
||||
// Enable differentiating pending vs. non-pending updates
|
||||
currentPackage.putBoolean("isPending", currentUpdateIsPending);
|
||||
promise.resolve(currentPackage);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
asyncTask.execute();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getNewStatusReport(final Promise promise) {
|
||||
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (needToReportRollback) {
|
||||
needToReportRollback = false;
|
||||
JSONArray failedUpdates = getFailedUpdates();
|
||||
if (failedUpdates != null && failedUpdates.length() > 0) {
|
||||
try {
|
||||
JSONObject lastFailedPackageJSON = failedUpdates.getJSONObject(failedUpdates.length() - 1);
|
||||
WritableMap lastFailedPackage = CodePushUtils.convertJsonObjectToWritable(lastFailedPackageJSON);
|
||||
WritableMap failedStatusReport = codePushTelemetryManager.getRollbackReport(lastFailedPackage);
|
||||
if (failedStatusReport != null) {
|
||||
promise.resolve(failedStatusReport);
|
||||
return null;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to read failed updates information stored in SharedPreferences.", e);
|
||||
}
|
||||
}
|
||||
} else if (didUpdate) {
|
||||
WritableMap currentPackage = codePushPackage.getCurrentPackage();
|
||||
if (currentPackage != null) {
|
||||
WritableMap newPackageStatusReport = codePushTelemetryManager.getUpdateReport(currentPackage);
|
||||
if (newPackageStatusReport != null) {
|
||||
promise.resolve(newPackageStatusReport);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if (isRunningBinaryVersion) {
|
||||
WritableMap newAppVersionStatusReport = codePushTelemetryManager.getBinaryUpdateReport(appVersion);
|
||||
if (newAppVersionStatusReport != null) {
|
||||
promise.resolve(newAppVersionStatusReport);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
WritableMap retryStatusReport = codePushTelemetryManager.getRetryStatusReport();
|
||||
if (retryStatusReport != null) {
|
||||
promise.resolve(retryStatusReport);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
promise.resolve("");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
asyncTask.execute();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void installUpdate(final ReadableMap updatePackage, final int installMode, final int minimumBackgroundDuration, final Promise promise) {
|
||||
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
codePushPackage.installPackage(updatePackage, isPendingUpdate(null));
|
||||
|
||||
String pendingHash = CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY);
|
||||
if (pendingHash == null) {
|
||||
throw new CodePushUnknownException("Update package to be installed has no hash.");
|
||||
} else {
|
||||
savePendingUpdate(pendingHash, /* isLoading */false);
|
||||
}
|
||||
|
||||
if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue()) {
|
||||
// Store the minimum duration on the native module as an instance
|
||||
// variable instead of relying on a closure below, so that any
|
||||
// subsequent resume-based installs could override it.
|
||||
CodePushNativeModule.this.minimumBackgroundDuration = minimumBackgroundDuration;
|
||||
|
||||
if (lifecycleEventListener == null) {
|
||||
// Ensure we do not add the listener twice.
|
||||
lifecycleEventListener = new LifecycleEventListener() {
|
||||
private Date lastPausedDate = null;
|
||||
|
||||
@Override
|
||||
public void onHostResume() {
|
||||
// Determine how long the app was in the background and ensure
|
||||
// that it meets the minimum duration amount of time.
|
||||
long durationInBackground = 0;
|
||||
if (lastPausedDate != null) {
|
||||
durationInBackground = (new Date().getTime() - lastPausedDate.getTime()) / 1000;
|
||||
}
|
||||
|
||||
if (durationInBackground >= CodePushNativeModule.this.minimumBackgroundDuration) {
|
||||
loadBundle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
// Save the current time so that when the app is later
|
||||
// resumed, we can detect how long it was in the background.
|
||||
lastPausedDate = new Date();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostDestroy() {
|
||||
}
|
||||
};
|
||||
|
||||
getReactApplicationContext().addLifecycleEventListener(lifecycleEventListener);
|
||||
}
|
||||
}
|
||||
|
||||
promise.resolve("");
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
asyncTask.execute();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isFailedUpdate(String packageHash, Promise promise) {
|
||||
promise.resolve(isFailedHash(packageHash));
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isFirstRun(String packageHash, Promise promise) {
|
||||
boolean isFirstRun = didUpdate
|
||||
&& packageHash != null
|
||||
&& packageHash.length() > 0
|
||||
&& packageHash.equals(codePushPackage.getCurrentPackageHash());
|
||||
promise.resolve(isFirstRun);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void notifyApplicationReady(Promise promise) {
|
||||
removePendingUpdate();
|
||||
promise.resolve("");
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void recordStatusReported(ReadableMap statusReport) {
|
||||
codePushTelemetryManager.recordStatusReported(statusReport);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void restartApp(boolean onlyIfUpdateIsPending, Promise promise) {
|
||||
// If this is an unconditional restart request, or there
|
||||
// is current pending update, then reload the app.
|
||||
if (!onlyIfUpdateIsPending || CodePush.this.isPendingUpdate(null)) {
|
||||
loadBundle();
|
||||
promise.resolve(true);
|
||||
}
|
||||
promise.resolve(false);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void saveStatusReportForRetry(ReadableMap statusReport) {
|
||||
codePushTelemetryManager.saveStatusReportForRetry(statusReport);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
// Replaces the current bundle with the one downloaded from removeBundleUrl.
|
||||
// It is only to be used during tests. No-ops if the test configuration flag is not set.
|
||||
public void downloadAndReplaceCurrentBundle(String remoteBundleUrl) {
|
||||
if (isUsingTestConfiguration()) {
|
||||
try {
|
||||
codePushPackage.downloadAndReplaceCurrentBundle(remoteBundleUrl, assetsBundleFileName);
|
||||
} catch (IOException e) {
|
||||
throw new CodePushUnknownException("Unable to replace current bundle", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
mUpdateManager.clearUpdates();
|
||||
mSettingsManager.removePendingUpdate();
|
||||
mSettingsManager.removeFailedUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> nativeModules = new ArrayList<>();
|
||||
this.codePushNativeModule = new CodePushNativeModule(reactApplicationContext);
|
||||
CodePushDialog dialogModule = new CodePushDialog(reactApplicationContext, mainActivity);
|
||||
mNativeModule = new CodePushNativeModule(reactApplicationContext, this, mUpdateManager, mTelemetryManager, mSettingsManager);
|
||||
CodePushDialog dialogModule = new CodePushDialog(reactApplicationContext);
|
||||
|
||||
nativeModules.add(this.codePushNativeModule);
|
||||
nativeModules.add(mNativeModule);
|
||||
nativeModules.add(dialogModule);
|
||||
|
||||
return nativeModules;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.microsoft.codepush.react;
|
||||
|
||||
public class CodePushConstants {
|
||||
public static final String ASSETS_BUNDLE_PREFIX = "assets://";
|
||||
public static final String BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime";
|
||||
public static final String CODE_PUSH_FOLDER_PREFIX = "CodePush";
|
||||
public static final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json";
|
||||
public static final String CODE_PUSH_PREFERENCES = "CodePush";
|
||||
public static final String CURRENT_PACKAGE_KEY = "currentPackage";
|
||||
public static final String DEFAULT_JS_BUNDLE_NAME = "index.android.bundle";
|
||||
public static final String DIFF_MANIFEST_FILE_NAME = "hotcodepush.json";
|
||||
public static final int DOWNLOAD_BUFFER_SIZE = 1024 * 256;
|
||||
public static final String DOWNLOAD_FILE_NAME = "download.zip";
|
||||
public static final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress";
|
||||
public static final String DOWNLOAD_URL_KEY = "downloadUrl";
|
||||
public static final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES";
|
||||
public static final String PACKAGE_FILE_NAME = "app.json";
|
||||
public static final String PACKAGE_HASH_KEY = "packageHash";
|
||||
public static final String PENDING_UPDATE_HASH_KEY = "hash";
|
||||
public static final String PENDING_UPDATE_IS_LOADING_KEY = "isLoading";
|
||||
public static final String PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE";
|
||||
public static final String PREVIOUS_PACKAGE_KEY = "previousPackage";
|
||||
public static final String REACT_NATIVE_LOG_TAG = "ReactNative";
|
||||
public static final String RELATIVE_BUNDLE_PATH_KEY = "bundlePath";
|
||||
public static final String RESOURCES_BUNDLE = "resources.arsc";
|
||||
public static final String STATUS_FILE = "codepush.json";
|
||||
public static final String UNZIPPED_FOLDER_NAME = "unzipped";
|
||||
}
|
||||
@@ -11,30 +11,20 @@ import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
public class CodePushDialog extends ReactContextBaseJavaModule{
|
||||
|
||||
private Activity mainActivity;
|
||||
|
||||
public CodePushDialog(ReactApplicationContext reactContext, Activity mainActivity) {
|
||||
public CodePushDialog(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.mainActivity = mainActivity;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void showDialog(String title, String message, String button1Text, String button2Text,
|
||||
final Callback successCallback, Callback errorCallback) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getCurrentActivity());
|
||||
|
||||
builder.setCancelable(false);
|
||||
|
||||
DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
|
||||
private boolean callbackConsumed = false;
|
||||
|
||||
@Override
|
||||
public synchronized void onClick(DialogInterface dialog, int which) {
|
||||
if (callbackConsumed) {
|
||||
return;
|
||||
}
|
||||
|
||||
callbackConsumed = true;
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.cancel();
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
package com.microsoft.codepush.react;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.Settings;
|
||||
import android.view.Choreographer;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.ReactChoreographer;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class CodePushNativeModule extends ReactContextBaseJavaModule {
|
||||
private LifecycleEventListener mLifecycleEventListener = null;
|
||||
private int mMinimumBackgroundDuration = 0;
|
||||
private CodePush mCodePush;
|
||||
private CodePushUpdateManager mUpdateManager;
|
||||
private CodePushTelemetryManager mTelemetryManager;
|
||||
private SettingsManager mSettingsManager;
|
||||
|
||||
private static final String REACT_APPLICATION_CLASS_NAME = "com.facebook.react.ReactApplication";
|
||||
private static final String REACT_NATIVE_HOST_CLASS_NAME = "com.facebook.react.ReactNativeHost";
|
||||
|
||||
public CodePushNativeModule(ReactApplicationContext reactContext, CodePush codePush, CodePushUpdateManager codePushUpdateManager, CodePushTelemetryManager codePushTelemetryManager, SettingsManager settingsManager) {
|
||||
super(reactContext);
|
||||
mCodePush = codePush;
|
||||
mUpdateManager = codePushUpdateManager;
|
||||
mTelemetryManager = codePushTelemetryManager;
|
||||
mSettingsManager = settingsManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
final Map<String, Object> constants = new HashMap<>();
|
||||
|
||||
constants.put("codePushInstallModeImmediate", CodePushInstallMode.IMMEDIATE.getValue());
|
||||
constants.put("codePushInstallModeOnNextRestart", CodePushInstallMode.ON_NEXT_RESTART.getValue());
|
||||
constants.put("codePushInstallModeOnNextResume", CodePushInstallMode.ON_NEXT_RESUME.getValue());
|
||||
|
||||
constants.put("codePushUpdateStateRunning", CodePushUpdateState.RUNNING.getValue());
|
||||
constants.put("codePushUpdateStatePending", CodePushUpdateState.PENDING.getValue());
|
||||
constants.put("codePushUpdateStateLatest", CodePushUpdateState.LATEST.getValue());
|
||||
|
||||
return constants;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CodePush";
|
||||
}
|
||||
|
||||
private boolean isReactApplication(Context context) {
|
||||
Class reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME);
|
||||
if (reactApplicationClass != null && reactApplicationClass.isInstance(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void loadBundleLegacy() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
Intent intent = currentActivity.getIntent();
|
||||
currentActivity.finish();
|
||||
currentActivity.startActivity(intent);
|
||||
|
||||
mCodePush.invalidateCurrentInstance();
|
||||
}
|
||||
|
||||
private void loadBundle() {
|
||||
mCodePush.clearDebugCacheIfNeeded();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (!ReactActivity.class.isInstance(currentActivity)) {
|
||||
// Our preferred reload logic relies on the user's Activity inheriting
|
||||
// from the core ReactActivity class, so if it doesn't, we fallback
|
||||
// early to our legacy behavior.
|
||||
loadBundleLegacy();
|
||||
} else {
|
||||
try {
|
||||
ReactActivity reactActivity = (ReactActivity)currentActivity;
|
||||
ReactInstanceManager instanceManager;
|
||||
|
||||
// #1) Get the ReactInstanceManager instance, which is what includes the
|
||||
// logic to reload the current React context.
|
||||
try {
|
||||
// In RN 0.29, the "mReactInstanceManager" field yields a null value, so we try
|
||||
// to get the instance manager via the ReactNativeHost, which only exists in 0.29.
|
||||
Method getApplicationMethod = ReactActivity.class.getMethod("getApplication");
|
||||
Object reactApplication = getApplicationMethod.invoke(reactActivity);
|
||||
Class reactApplicationClass = tryGetClass(REACT_APPLICATION_CLASS_NAME);
|
||||
Method getReactNativeHostMethod = reactApplicationClass.getMethod("getReactNativeHost");
|
||||
Object reactNativeHost = getReactNativeHostMethod.invoke(reactApplication);
|
||||
Class reactNativeHostClass = tryGetClass(REACT_NATIVE_HOST_CLASS_NAME);
|
||||
Method getReactInstanceManagerMethod = reactNativeHostClass.getMethod("getReactInstanceManager");
|
||||
instanceManager = (ReactInstanceManager)getReactInstanceManagerMethod.invoke(reactNativeHost);
|
||||
} catch (Exception e) {
|
||||
// The React Native version might be older than 0.29, so we try to get the
|
||||
// instance manager via the "mReactInstanceManager" field.
|
||||
Field instanceManagerField = ReactActivity.class.getDeclaredField("mReactInstanceManager");
|
||||
instanceManagerField.setAccessible(true);
|
||||
instanceManager = (ReactInstanceManager)instanceManagerField.get(reactActivity);
|
||||
}
|
||||
|
||||
String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());
|
||||
|
||||
// #2) Update the locally stored JS bundle file path
|
||||
Field jsBundleField = instanceManager.getClass().getDeclaredField("mJSBundleFile");
|
||||
jsBundleField.setAccessible(true);
|
||||
jsBundleField.set(instanceManager, latestJSBundleFile);
|
||||
|
||||
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
|
||||
final Method recreateMethod = instanceManager.getClass().getMethod("recreateReactContextInBackground");
|
||||
|
||||
final ReactInstanceManager finalizedInstanceManager = instanceManager;
|
||||
reactActivity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
recreateMethod.invoke(finalizedInstanceManager);
|
||||
mCodePush.initializeUpdateAfterRestart();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// The recreation method threw an unknown exception
|
||||
// so just simply fallback to restarting the Activity
|
||||
loadBundleLegacy();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// Our reflection logic failed somewhere
|
||||
// so fall back to restarting the Activity
|
||||
loadBundleLegacy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Class tryGetClass(String className) {
|
||||
try {
|
||||
return Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) {
|
||||
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
WritableMap mutableUpdatePackage = CodePushUtils.convertReadableMapToWritableMap(updatePackage);
|
||||
mutableUpdatePackage.putString(CodePushConstants.BINARY_MODIFIED_TIME_KEY, "" + mCodePush.getBinaryResourcesModifiedTime());
|
||||
mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
|
||||
private boolean hasScheduledNextFrame = false;
|
||||
private DownloadProgress latestDownloadProgress = null;
|
||||
|
||||
@Override
|
||||
public void call(DownloadProgress downloadProgress) {
|
||||
if (!notifyProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
latestDownloadProgress = downloadProgress;
|
||||
// If the download is completed, synchronously send the last event.
|
||||
if (latestDownloadProgress.isCompleted()) {
|
||||
dispatchDownloadProgressEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasScheduledNextFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasScheduledNextFrame = true;
|
||||
getReactApplicationContext().runOnUiQueueThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new Choreographer.FrameCallback() {
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
if (!latestDownloadProgress.isCompleted()) {
|
||||
dispatchDownloadProgressEvent();
|
||||
}
|
||||
|
||||
hasScheduledNextFrame = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchDownloadProgressEvent() {
|
||||
getReactApplicationContext()
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
|
||||
}
|
||||
});
|
||||
|
||||
WritableMap newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
|
||||
promise.resolve(newPackage);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
promise.reject(e);
|
||||
} catch (CodePushInvalidUpdateException e) {
|
||||
e.printStackTrace();
|
||||
mSettingsManager.saveFailedUpdate(updatePackage);
|
||||
promise.reject(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
asyncTask.execute();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getConfiguration(Promise promise) {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
WritableNativeMap configMap = new WritableNativeMap();
|
||||
configMap.putString("appVersion", mCodePush.getAppVersion());
|
||||
configMap.putString("deploymentKey", mCodePush.getDeploymentKey());
|
||||
configMap.putString("serverUrl", mCodePush.getServerUrl());
|
||||
configMap.putString("clientUniqueId",
|
||||
Settings.Secure.getString(currentActivity.getContentResolver(),
|
||||
android.provider.Settings.Secure.ANDROID_ID));
|
||||
String binaryHash = CodePushUpdateUtils.getHashForBinaryContents(currentActivity, mCodePush.isDebugMode());
|
||||
if (binaryHash != null) {
|
||||
// binaryHash will be null if the React Native assets were not bundled into the APK
|
||||
// (e.g. in Debug builds)
|
||||
configMap.putString(CodePushConstants.PACKAGE_HASH_KEY, binaryHash);
|
||||
}
|
||||
|
||||
promise.resolve(configMap);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getUpdateMetadata(final int updateState, final Promise promise) {
|
||||
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
WritableMap currentPackage = mUpdateManager.getCurrentPackage();
|
||||
|
||||
if (currentPackage == null) {
|
||||
promise.resolve("");
|
||||
return null;
|
||||
}
|
||||
|
||||
Boolean currentUpdateIsPending = false;
|
||||
|
||||
if (currentPackage.hasKey(CodePushConstants.PACKAGE_HASH_KEY)) {
|
||||
String currentHash = currentPackage.getString(CodePushConstants.PACKAGE_HASH_KEY);
|
||||
currentUpdateIsPending = mSettingsManager.isPendingUpdate(currentHash);
|
||||
}
|
||||
|
||||
if (updateState == CodePushUpdateState.PENDING.getValue() && !currentUpdateIsPending) {
|
||||
// The caller wanted a pending update
|
||||
// but there isn't currently one.
|
||||
promise.resolve("");
|
||||
} else if (updateState == CodePushUpdateState.RUNNING.getValue() && currentUpdateIsPending) {
|
||||
// The caller wants the running update, but the current
|
||||
// one is pending, so we need to grab the previous.
|
||||
promise.resolve(mUpdateManager.getPreviousPackage());
|
||||
} else {
|
||||
// The current package satisfies the request:
|
||||
// 1) Caller wanted a pending, and there is a pending update
|
||||
// 2) Caller wanted the running update, and there isn't a pending
|
||||
// 3) Caller wants the latest update, regardless if it's pending or not
|
||||
if (mCodePush.isRunningBinaryVersion()) {
|
||||
// This only matters in Debug builds. Since we do not clear "outdated" updates,
|
||||
// we need to indicate to the JS side that somehow we have a current update on
|
||||
// disk that is not actually running.
|
||||
currentPackage.putBoolean("_isDebugOnly", true);
|
||||
}
|
||||
|
||||
// Enable differentiating pending vs. non-pending updates
|
||||
currentPackage.putBoolean("isPending", currentUpdateIsPending);
|
||||
promise.resolve(currentPackage);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
asyncTask.execute();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getNewStatusReport(final Promise promise) {
|
||||
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (mCodePush.needToReportRollback()) {
|
||||
mCodePush.setNeedToReportRollback(false);
|
||||
JSONArray failedUpdates = mSettingsManager.getFailedUpdates();
|
||||
if (failedUpdates != null && failedUpdates.length() > 0) {
|
||||
try {
|
||||
JSONObject lastFailedPackageJSON = failedUpdates.getJSONObject(failedUpdates.length() - 1);
|
||||
WritableMap lastFailedPackage = CodePushUtils.convertJsonObjectToWritable(lastFailedPackageJSON);
|
||||
WritableMap failedStatusReport = mTelemetryManager.getRollbackReport(lastFailedPackage);
|
||||
if (failedStatusReport != null) {
|
||||
promise.resolve(failedStatusReport);
|
||||
return null;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to read failed updates information stored in SharedPreferences.", e);
|
||||
}
|
||||
}
|
||||
} else if (mCodePush.didUpdate()) {
|
||||
WritableMap currentPackage = mUpdateManager.getCurrentPackage();
|
||||
if (currentPackage != null) {
|
||||
WritableMap newPackageStatusReport = mTelemetryManager.getUpdateReport(currentPackage);
|
||||
if (newPackageStatusReport != null) {
|
||||
promise.resolve(newPackageStatusReport);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if (mCodePush.isRunningBinaryVersion()) {
|
||||
WritableMap newAppVersionStatusReport = mTelemetryManager.getBinaryUpdateReport(mCodePush.getAppVersion());
|
||||
if (newAppVersionStatusReport != null) {
|
||||
promise.resolve(newAppVersionStatusReport);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
WritableMap retryStatusReport = mTelemetryManager.getRetryStatusReport();
|
||||
if (retryStatusReport != null) {
|
||||
promise.resolve(retryStatusReport);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
promise.resolve("");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
asyncTask.execute();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void installUpdate(final ReadableMap updatePackage, final int installMode, final int minimumBackgroundDuration, final Promise promise) {
|
||||
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
mUpdateManager.installPackage(updatePackage, mSettingsManager.isPendingUpdate(null));
|
||||
|
||||
String pendingHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY);
|
||||
if (pendingHash == null) {
|
||||
throw new CodePushUnknownException("Update package to be installed has no hash.");
|
||||
} else {
|
||||
mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false);
|
||||
}
|
||||
|
||||
if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue()) {
|
||||
// Store the minimum duration on the native module as an instance
|
||||
// variable instead of relying on a closure below, so that any
|
||||
// subsequent resume-based installs could override it.
|
||||
CodePushNativeModule.this.mMinimumBackgroundDuration = minimumBackgroundDuration;
|
||||
|
||||
if (mLifecycleEventListener == null) {
|
||||
// Ensure we do not add the listener twice.
|
||||
mLifecycleEventListener = new LifecycleEventListener() {
|
||||
private Date lastPausedDate = null;
|
||||
|
||||
@Override
|
||||
public void onHostResume() {
|
||||
// Determine how long the app was in the background and ensure
|
||||
// that it meets the minimum duration amount of time.
|
||||
long durationInBackground = 0;
|
||||
if (lastPausedDate != null) {
|
||||
durationInBackground = (new Date().getTime() - lastPausedDate.getTime()) / 1000;
|
||||
}
|
||||
|
||||
if (durationInBackground >= CodePushNativeModule.this.mMinimumBackgroundDuration) {
|
||||
loadBundle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
// Save the current time so that when the app is later
|
||||
// resumed, we can detect how long it was in the background.
|
||||
lastPausedDate = new Date();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostDestroy() {
|
||||
}
|
||||
};
|
||||
|
||||
getReactApplicationContext().addLifecycleEventListener(mLifecycleEventListener);
|
||||
}
|
||||
}
|
||||
|
||||
promise.resolve("");
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
asyncTask.execute();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isFailedUpdate(String packageHash, Promise promise) {
|
||||
promise.resolve(mSettingsManager.isFailedHash(packageHash));
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isFirstRun(String packageHash, Promise promise) {
|
||||
boolean isFirstRun = mCodePush.didUpdate()
|
||||
&& packageHash != null
|
||||
&& packageHash.length() > 0
|
||||
&& packageHash.equals(mUpdateManager.getCurrentPackageHash());
|
||||
promise.resolve(isFirstRun);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void notifyApplicationReady(Promise promise) {
|
||||
mSettingsManager.removePendingUpdate();
|
||||
promise.resolve("");
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void recordStatusReported(ReadableMap statusReport) {
|
||||
mTelemetryManager.recordStatusReported(statusReport);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void restartApp(boolean onlyIfUpdateIsPending, Promise promise) {
|
||||
// If this is an unconditional restart request, or there
|
||||
// is current pending update, then reload the app.
|
||||
if (!onlyIfUpdateIsPending || CodePush.this.isPendingUpdate(null)) {
|
||||
loadBundle();
|
||||
promise.resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
promise.resolve(false);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void restartApp(boolean onlyIfUpdateIsPending) {
|
||||
// If this is an unconditional restart request, or there
|
||||
// is current pending update, then reload the app.
|
||||
if (!onlyIfUpdateIsPending || mSettingsManager.isPendingUpdate(null)) {
|
||||
loadBundle();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void saveStatusReportForRetry(ReadableMap statusReport) {
|
||||
mTelemetryManager.saveStatusReportForRetry(statusReport);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
// Replaces the current bundle with the one downloaded from removeBundleUrl.
|
||||
// It is only to be used during tests. No-ops if the test configuration flag is not set.
|
||||
public void downloadAndReplaceCurrentBundle(String remoteBundleUrl) {
|
||||
if (mCodePush.isUsingTestConfiguration()) {
|
||||
try {
|
||||
mUpdateManager.downloadAndReplaceCurrentBundle(remoteBundleUrl, mCodePush.getAssetsBundleFileName());
|
||||
} catch (IOException e) {
|
||||
throw new CodePushUnknownException("Unable to replace current bundle", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,8 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class CodePushTelemetryManager {
|
||||
|
||||
private Context applicationContext;
|
||||
private SharedPreferences mSettings;
|
||||
private final String APP_VERSION_KEY = "appVersion";
|
||||
private final String CODE_PUSH_PREFERENCES;
|
||||
private final String DEPLOYMENT_FAILED_STATUS = "DeploymentFailed";
|
||||
private final String DEPLOYMENT_KEY_KEY = "deploymentKey";
|
||||
private final String DEPLOYMENT_SUCCEEDED_STATUS = "DeploymentSucceeded";
|
||||
@@ -26,9 +24,8 @@ public class CodePushTelemetryManager {
|
||||
private final String RETRY_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_RETRY_DEPLOYMENT_REPORT";
|
||||
private final String STATUS_KEY = "status";
|
||||
|
||||
public CodePushTelemetryManager(Context applicationContext, String codePushPreferencesKey) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.CODE_PUSH_PREFERENCES = codePushPreferencesKey;
|
||||
public CodePushTelemetryManager(Context applicationContext) {
|
||||
mSettings = applicationContext.getSharedPreferences(CodePushConstants.CODE_PUSH_PREFERENCES, 0);
|
||||
}
|
||||
|
||||
public WritableMap getBinaryUpdateReport(String appVersion) {
|
||||
@@ -58,8 +55,7 @@ public class CodePushTelemetryManager {
|
||||
}
|
||||
|
||||
public WritableMap getRetryStatusReport() {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
String retryStatusReportString = settings.getString(RETRY_DEPLOYMENT_REPORT_KEY, null);
|
||||
String retryStatusReportString = mSettings.getString(RETRY_DEPLOYMENT_REPORT_KEY, null);
|
||||
if (retryStatusReportString != null) {
|
||||
clearRetryStatusReport();
|
||||
try {
|
||||
@@ -127,14 +123,12 @@ public class CodePushTelemetryManager {
|
||||
}
|
||||
|
||||
public void saveStatusReportForRetry(ReadableMap statusReport) {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
JSONObject statusReportJSON = CodePushUtils.convertReadableToJsonObject(statusReport);
|
||||
settings.edit().putString(RETRY_DEPLOYMENT_REPORT_KEY, statusReportJSON.toString()).commit();
|
||||
mSettings.edit().putString(RETRY_DEPLOYMENT_REPORT_KEY, statusReportJSON.toString()).commit();
|
||||
}
|
||||
|
||||
private void clearRetryStatusReport() {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
settings.edit().remove(RETRY_DEPLOYMENT_REPORT_KEY).commit();
|
||||
mSettings.edit().remove(RETRY_DEPLOYMENT_REPORT_KEY).commit();
|
||||
}
|
||||
|
||||
private String getDeploymentKeyFromStatusReportIdentifier(String statusReportIdentifier) {
|
||||
@@ -159,8 +153,7 @@ public class CodePushTelemetryManager {
|
||||
}
|
||||
|
||||
private String getPreviousStatusReportIdentifier() {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
return settings.getString(LAST_DEPLOYMENT_REPORT_KEY, null);
|
||||
return mSettings.getString(LAST_DEPLOYMENT_REPORT_KEY, null);
|
||||
}
|
||||
|
||||
private String getVersionLabelFromStatusReportIdentifier(String statusReportIdentifier) {
|
||||
@@ -177,7 +170,6 @@ public class CodePushTelemetryManager {
|
||||
}
|
||||
|
||||
private void saveStatusReportedForIdentifier(String appVersionOrPackageIdentifier) {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
settings.edit().putString(LAST_DEPLOYMENT_REPORT_KEY, appVersionOrPackageIdentifier).commit();
|
||||
mSettings.edit().putString(LAST_DEPLOYMENT_REPORT_KEY, appVersionOrPackageIdentifier).commit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,40 +17,28 @@ import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class CodePushPackage {
|
||||
private final String CODE_PUSH_FOLDER_PREFIX = "CodePush";
|
||||
private final String CURRENT_PACKAGE_KEY = "currentPackage";
|
||||
private final String DIFF_MANIFEST_FILE_NAME = "hotcodepush.json";
|
||||
private final int DOWNLOAD_BUFFER_SIZE = 1024 * 256;
|
||||
private final String DOWNLOAD_FILE_NAME = "download.zip";
|
||||
private final String DOWNLOAD_URL_KEY = "downloadUrl";
|
||||
private final String PACKAGE_FILE_NAME = "app.json";
|
||||
private final String PACKAGE_HASH_KEY = "packageHash";
|
||||
private final String PREVIOUS_PACKAGE_KEY = "previousPackage";
|
||||
private final String RELATIVE_BUNDLE_PATH_KEY = "bundlePath";
|
||||
private final String STATUS_FILE = "codepush.json";
|
||||
private final String UNZIPPED_FOLDER_NAME = "unzipped";
|
||||
public class CodePushUpdateManager {
|
||||
|
||||
private String documentsDirectory;
|
||||
private String mDocumentsDirectory;
|
||||
|
||||
public CodePushPackage(String documentsDirectory) {
|
||||
this.documentsDirectory = documentsDirectory;
|
||||
public CodePushUpdateManager(String documentsDirectory) {
|
||||
mDocumentsDirectory = documentsDirectory;
|
||||
}
|
||||
|
||||
private String getDownloadFilePath() {
|
||||
return CodePushUtils.appendPathComponent(getCodePushPath(), DOWNLOAD_FILE_NAME);
|
||||
return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.DOWNLOAD_FILE_NAME);
|
||||
}
|
||||
|
||||
private String getUnzippedFolderPath() {
|
||||
return CodePushUtils.appendPathComponent(getCodePushPath(), UNZIPPED_FOLDER_NAME);
|
||||
return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.UNZIPPED_FOLDER_NAME);
|
||||
}
|
||||
|
||||
private String getDocumentsDirectory() {
|
||||
return documentsDirectory;
|
||||
return mDocumentsDirectory;
|
||||
}
|
||||
|
||||
private String getCodePushPath() {
|
||||
String codePushPath = CodePushUtils.appendPathComponent(getDocumentsDirectory(), CODE_PUSH_FOLDER_PREFIX);
|
||||
String codePushPath = CodePushUtils.appendPathComponent(getDocumentsDirectory(), CodePushConstants.CODE_PUSH_FOLDER_PREFIX);
|
||||
if (CodePush.isUsingTestConfiguration()) {
|
||||
codePushPath = CodePushUtils.appendPathComponent(codePushPath, "TestPackages");
|
||||
}
|
||||
@@ -59,7 +47,7 @@ public class CodePushPackage {
|
||||
}
|
||||
|
||||
private String getStatusFilePath() {
|
||||
return CodePushUtils.appendPathComponent(getCodePushPath(), STATUS_FILE);
|
||||
return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.STATUS_FILE);
|
||||
}
|
||||
|
||||
public WritableMap getCurrentPackageInfo() {
|
||||
@@ -87,7 +75,7 @@ public class CodePushPackage {
|
||||
|
||||
public String getCurrentPackageFolderPath() {
|
||||
WritableMap info = getCurrentPackageInfo();
|
||||
String packageHash = CodePushUtils.tryGetString(info, CURRENT_PACKAGE_KEY);
|
||||
String packageHash = CodePushUtils.tryGetString(info, CodePushConstants.CURRENT_PACKAGE_KEY);
|
||||
if (packageHash == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -106,7 +94,7 @@ public class CodePushPackage {
|
||||
return null;
|
||||
}
|
||||
|
||||
String relativeBundlePath = CodePushUtils.tryGetString(currentPackage, RELATIVE_BUNDLE_PATH_KEY);
|
||||
String relativeBundlePath = CodePushUtils.tryGetString(currentPackage, CodePushConstants.RELATIVE_BUNDLE_PATH_KEY);
|
||||
if (relativeBundlePath == null) {
|
||||
return CodePushUtils.appendPathComponent(packageFolder, bundleFileName);
|
||||
} else {
|
||||
@@ -120,12 +108,12 @@ public class CodePushPackage {
|
||||
|
||||
public String getCurrentPackageHash() {
|
||||
WritableMap info = getCurrentPackageInfo();
|
||||
return CodePushUtils.tryGetString(info, CURRENT_PACKAGE_KEY);
|
||||
return CodePushUtils.tryGetString(info, CodePushConstants.CURRENT_PACKAGE_KEY);
|
||||
}
|
||||
|
||||
public String getPreviousPackageHash() {
|
||||
WritableMap info = getCurrentPackageInfo();
|
||||
return CodePushUtils.tryGetString(info, PREVIOUS_PACKAGE_KEY);
|
||||
return CodePushUtils.tryGetString(info, CodePushConstants.PREVIOUS_PACKAGE_KEY);
|
||||
}
|
||||
|
||||
public WritableMap getCurrentPackage() {
|
||||
@@ -148,7 +136,7 @@ public class CodePushPackage {
|
||||
|
||||
public WritableMap getPackage(String packageHash) {
|
||||
String folderPath = getPackageFolderPath(packageHash);
|
||||
String packageFilePath = CodePushUtils.appendPathComponent(folderPath, PACKAGE_FILE_NAME);
|
||||
String packageFilePath = CodePushUtils.appendPathComponent(folderPath, CodePushConstants.PACKAGE_FILE_NAME);
|
||||
try {
|
||||
return CodePushUtils.getWritableMapFromFile(packageFilePath);
|
||||
} catch (IOException e) {
|
||||
@@ -158,16 +146,16 @@ public class CodePushPackage {
|
||||
|
||||
public void downloadPackage(ReadableMap updatePackage, String expectedBundleFileName,
|
||||
DownloadProgressCallback progressCallback) throws IOException {
|
||||
String newUpdateHash = CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY);
|
||||
String newUpdateHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY);
|
||||
String newUpdateFolderPath = getPackageFolderPath(newUpdateHash);
|
||||
String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, PACKAGE_FILE_NAME);
|
||||
String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME);
|
||||
if (FileUtils.fileAtPathExists(newUpdateFolderPath)) {
|
||||
// This removes any stale data in newPackageFolderPath that could have been left
|
||||
// uncleared due to a crash or error during the download or install process.
|
||||
FileUtils.deleteDirectoryAtPath(newUpdateFolderPath);
|
||||
}
|
||||
|
||||
String downloadUrlString = CodePushUtils.tryGetString(updatePackage, DOWNLOAD_URL_KEY);
|
||||
String downloadUrlString = CodePushUtils.tryGetString(updatePackage, CodePushConstants.DOWNLOAD_URL_KEY);
|
||||
HttpURLConnection connection = null;
|
||||
BufferedInputStream bin = null;
|
||||
FileOutputStream fos = null;
|
||||
@@ -186,14 +174,14 @@ public class CodePushPackage {
|
||||
bin = new BufferedInputStream(connection.getInputStream());
|
||||
File downloadFolder = new File(getCodePushPath());
|
||||
downloadFolder.mkdirs();
|
||||
downloadFile = new File(downloadFolder, DOWNLOAD_FILE_NAME);
|
||||
downloadFile = new File(downloadFolder, CodePushConstants.DOWNLOAD_FILE_NAME);
|
||||
fos = new FileOutputStream(downloadFile);
|
||||
bout = new BufferedOutputStream(fos, DOWNLOAD_BUFFER_SIZE);
|
||||
byte[] data = new byte[DOWNLOAD_BUFFER_SIZE];
|
||||
bout = new BufferedOutputStream(fos, CodePushConstants.DOWNLOAD_BUFFER_SIZE);
|
||||
byte[] data = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE];
|
||||
byte[] header = new byte[4];
|
||||
|
||||
int numBytesRead = 0;
|
||||
while ((numBytesRead = bin.read(data, 0, DOWNLOAD_BUFFER_SIZE)) >= 0) {
|
||||
while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) {
|
||||
if (receivedBytes < 4) {
|
||||
for (int i = 0; i < numBytesRead; i++) {
|
||||
int headerOffset = (int)(receivedBytes) + i;
|
||||
@@ -236,7 +224,7 @@ public class CodePushPackage {
|
||||
|
||||
// Merge contents with current update based on the manifest
|
||||
String diffManifestFilePath = CodePushUtils.appendPathComponent(unzippedFolderPath,
|
||||
DIFF_MANIFEST_FILE_NAME);
|
||||
CodePushConstants.DIFF_MANIFEST_FILE_NAME);
|
||||
boolean isDiffUpdate = FileUtils.fileAtPathExists(diffManifestFilePath);
|
||||
if (isDiffUpdate) {
|
||||
String currentPackageFolderPath = getCurrentPackageFolderPath();
|
||||
@@ -253,7 +241,7 @@ public class CodePushPackage {
|
||||
String relativeBundlePath = CodePushUpdateUtils.findJSBundleInUpdateContents(newUpdateFolderPath, expectedBundleFileName);
|
||||
|
||||
if (relativeBundlePath == null) {
|
||||
throw new CodePushInvalidUpdateException("Update is invalid - A JS bundle file named \"" + expectedBundleFileName + "\" could not be found within the downloaded contents. Please ensure that your app is syncing with the correct deployment and that you are releasing your CodePush updates using the exact same JS bundle file name that was shipped with your app's binary.");
|
||||
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.");
|
||||
} else {
|
||||
if (FileUtils.fileAtPathExists(newUpdateMetadataPath)) {
|
||||
File metadataFileFromOldUpdate = new File(newUpdateMetadataPath);
|
||||
@@ -266,10 +254,10 @@ public class CodePushPackage {
|
||||
|
||||
JSONObject updatePackageJSON = CodePushUtils.convertReadableToJsonObject(updatePackage);
|
||||
try {
|
||||
updatePackageJSON.put(RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath);
|
||||
updatePackageJSON.put(CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath);
|
||||
} catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to set key " +
|
||||
RELATIVE_BUNDLE_PATH_KEY + " to value " + relativeBundlePath +
|
||||
CodePushConstants.RELATIVE_BUNDLE_PATH_KEY + " to value " + relativeBundlePath +
|
||||
" in update package.", e);
|
||||
}
|
||||
|
||||
@@ -285,7 +273,7 @@ public class CodePushPackage {
|
||||
}
|
||||
|
||||
public void installPackage(ReadableMap updatePackage, boolean removePendingUpdate) {
|
||||
String packageHash = CodePushUtils.tryGetString(updatePackage, PACKAGE_HASH_KEY);
|
||||
String packageHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY);
|
||||
WritableMap info = getCurrentPackageInfo();
|
||||
if (removePendingUpdate) {
|
||||
String currentPackageFolderPath = getCurrentPackageFolderPath();
|
||||
@@ -298,10 +286,10 @@ public class CodePushPackage {
|
||||
FileUtils.deleteDirectoryAtPath(getPackageFolderPath(previousPackageHash));
|
||||
}
|
||||
|
||||
info.putString(PREVIOUS_PACKAGE_KEY, CodePushUtils.tryGetString(info, CURRENT_PACKAGE_KEY));
|
||||
info.putString(CodePushConstants.PREVIOUS_PACKAGE_KEY, CodePushUtils.tryGetString(info, CodePushConstants.CURRENT_PACKAGE_KEY));
|
||||
}
|
||||
|
||||
info.putString(CURRENT_PACKAGE_KEY, packageHash);
|
||||
info.putString(CodePushConstants.CURRENT_PACKAGE_KEY, packageHash);
|
||||
updateCurrentPackageInfo(info);
|
||||
}
|
||||
|
||||
@@ -309,8 +297,8 @@ public class CodePushPackage {
|
||||
WritableMap info = getCurrentPackageInfo();
|
||||
String currentPackageFolderPath = getCurrentPackageFolderPath();
|
||||
FileUtils.deleteDirectoryAtPath(currentPackageFolderPath);
|
||||
info.putString(CURRENT_PACKAGE_KEY, CodePushUtils.tryGetString(info, PREVIOUS_PACKAGE_KEY));
|
||||
info.putNull(PREVIOUS_PACKAGE_KEY);
|
||||
info.putString(CodePushConstants.CURRENT_PACKAGE_KEY, CodePushUtils.tryGetString(info, CodePushConstants.PREVIOUS_PACKAGE_KEY));
|
||||
info.putNull(CodePushConstants.PREVIOUS_PACKAGE_KEY);
|
||||
updateCurrentPackageInfo(info);
|
||||
}
|
||||
|
||||
@@ -327,10 +315,10 @@ public class CodePushPackage {
|
||||
File downloadFile = new File(getCurrentPackageBundlePath(bundleFileName));
|
||||
downloadFile.delete();
|
||||
fos = new FileOutputStream(downloadFile);
|
||||
bout = new BufferedOutputStream(fos, DOWNLOAD_BUFFER_SIZE);
|
||||
byte[] data = new byte[DOWNLOAD_BUFFER_SIZE];
|
||||
bout = new BufferedOutputStream(fos, CodePushConstants.DOWNLOAD_BUFFER_SIZE);
|
||||
byte[] data = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE];
|
||||
int numBytesRead = 0;
|
||||
while ((numBytesRead = bin.read(data, 0, DOWNLOAD_BUFFER_SIZE)) >= 0) {
|
||||
while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) {
|
||||
bout.write(data, 0, numBytesRead);
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
@@ -21,8 +21,6 @@ import java.util.Collections;
|
||||
|
||||
public class CodePushUpdateUtils {
|
||||
|
||||
private static final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json";
|
||||
|
||||
private static void addContentsOfFolderToManifest(String folderPath, String pathPrefix, ArrayList<String> manifest) {
|
||||
File folder = new File(folderPath);
|
||||
File[] folderFiles = folder.listFiles();
|
||||
@@ -102,7 +100,7 @@ public class CodePushUpdateUtils {
|
||||
|
||||
public static String getHashForBinaryContents(Activity mainActivity, boolean isDebugMode) {
|
||||
try {
|
||||
return CodePushUtils.getStringFromInputStream(mainActivity.getAssets().open(CODE_PUSH_HASH_FILE_NAME));
|
||||
return CodePushUtils.getStringFromInputStream(mainActivity.getAssets().open(CodePushConstants.CODE_PUSH_HASH_FILE_NAME));
|
||||
} catch (IOException e) {
|
||||
if (!isDebugMode) {
|
||||
// Only print this message in "Release" mode. In "Debug", we may not have the
|
||||
|
||||
@@ -24,8 +24,6 @@ import java.util.Iterator;
|
||||
|
||||
public class CodePushUtils {
|
||||
|
||||
public static final String REACT_NATIVE_LOG_TAG = "ReactNative";
|
||||
|
||||
public static String appendPathComponent(String basePath, String appendPathComponent) {
|
||||
return new File(basePath, appendPathComponent).getAbsolutePath();
|
||||
}
|
||||
@@ -208,7 +206,7 @@ public class CodePushUtils {
|
||||
}
|
||||
|
||||
public static void log(String message) {
|
||||
Log.d(REACT_NATIVE_LOG_TAG, "[CodePush] " + message);
|
||||
Log.d(CodePushConstants.REACT_NATIVE_LOG_TAG, "[CodePush] " + message);
|
||||
}
|
||||
|
||||
public static void logBundleUrl(String path) {
|
||||
|
||||
@@ -4,27 +4,27 @@ import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
|
||||
class DownloadProgress {
|
||||
private long totalBytes;
|
||||
private long receivedBytes;
|
||||
private long mTotalBytes;
|
||||
private long mReceivedBytes;
|
||||
|
||||
public DownloadProgress (long totalBytes, long receivedBytes){
|
||||
this.totalBytes = totalBytes;
|
||||
this.receivedBytes = receivedBytes;
|
||||
mTotalBytes = totalBytes;
|
||||
mReceivedBytes = receivedBytes;
|
||||
}
|
||||
|
||||
public WritableMap createWritableMap() {
|
||||
WritableMap map = new WritableNativeMap();
|
||||
if (totalBytes < Integer.MAX_VALUE) {
|
||||
map.putInt("totalBytes", (int) totalBytes);
|
||||
map.putInt("receivedBytes", (int) receivedBytes);
|
||||
if (mTotalBytes < Integer.MAX_VALUE) {
|
||||
map.putInt("totalBytes", (int) mTotalBytes);
|
||||
map.putInt("receivedBytes", (int) mReceivedBytes);
|
||||
} else {
|
||||
map.putDouble("totalBytes", totalBytes);
|
||||
map.putDouble("receivedBytes", receivedBytes);
|
||||
map.putDouble("totalBytes", mTotalBytes);
|
||||
map.putDouble("receivedBytes", mReceivedBytes);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return this.totalBytes == this.receivedBytes;
|
||||
return mTotalBytes == mReceivedBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.microsoft.codepush.react;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class SettingsManager {
|
||||
|
||||
private SharedPreferences mSettings;
|
||||
|
||||
public SettingsManager(Context applicationContext) {
|
||||
mSettings = applicationContext.getSharedPreferences(CodePushConstants.CODE_PUSH_PREFERENCES, 0);
|
||||
}
|
||||
|
||||
public JSONArray getFailedUpdates() {
|
||||
String failedUpdatesString = mSettings.getString(CodePushConstants.FAILED_UPDATES_KEY, null);
|
||||
if (failedUpdatesString == null) {
|
||||
return new JSONArray();
|
||||
}
|
||||
|
||||
try {
|
||||
return new JSONArray(failedUpdatesString);
|
||||
} catch (JSONException e) {
|
||||
// Unrecognized data format, clear and replace with expected format.
|
||||
JSONArray emptyArray = new JSONArray();
|
||||
mSettings.edit().putString(CodePushConstants.FAILED_UPDATES_KEY, emptyArray.toString()).commit();
|
||||
return emptyArray;
|
||||
}
|
||||
}
|
||||
|
||||
public JSONObject getPendingUpdate() {
|
||||
String pendingUpdateString = mSettings.getString(CodePushConstants.PENDING_UPDATE_KEY, null);
|
||||
if (pendingUpdateString == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new JSONObject(pendingUpdateString);
|
||||
} catch (JSONException e) {
|
||||
// Should not happen.
|
||||
CodePushUtils.log("Unable to parse pending update metadata " + pendingUpdateString +
|
||||
" stored in SharedPreferences");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean isFailedHash(String packageHash) {
|
||||
JSONArray failedUpdates = getFailedUpdates();
|
||||
if (packageHash != null) {
|
||||
for (int i = 0; i < failedUpdates.length(); i++) {
|
||||
try {
|
||||
JSONObject failedPackage = failedUpdates.getJSONObject(i);
|
||||
String failedPackageHash = failedPackage.getString(CodePushConstants.PACKAGE_HASH_KEY);
|
||||
if (packageHash.equals(failedPackageHash)) {
|
||||
return true;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to read failedUpdates data stored in SharedPreferences.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isPendingUpdate(String packageHash) {
|
||||
JSONObject pendingUpdate = getPendingUpdate();
|
||||
|
||||
try {
|
||||
return pendingUpdate != null &&
|
||||
!pendingUpdate.getBoolean(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY) &&
|
||||
(packageHash == null || pendingUpdate.getString(CodePushConstants.PENDING_UPDATE_HASH_KEY).equals(packageHash));
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new CodePushUnknownException("Unable to read pending update metadata in isPendingUpdate.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFailedUpdates() {
|
||||
mSettings.edit().remove(CodePushConstants.FAILED_UPDATES_KEY).commit();
|
||||
}
|
||||
|
||||
public void removePendingUpdate() {
|
||||
mSettings.edit().remove(CodePushConstants.PENDING_UPDATE_KEY).commit();
|
||||
}
|
||||
|
||||
public void saveFailedUpdate(ReadableMap failedPackage) {
|
||||
String failedUpdatesString = mSettings.getString(CodePushConstants.FAILED_UPDATES_KEY, null);
|
||||
JSONArray failedUpdates;
|
||||
if (failedUpdatesString == null) {
|
||||
failedUpdates = new JSONArray();
|
||||
} else {
|
||||
try {
|
||||
failedUpdates = new JSONArray(failedUpdatesString);
|
||||
} catch (JSONException e) {
|
||||
// Should not happen.
|
||||
throw new CodePushMalformedDataException("Unable to parse failed updates information " +
|
||||
failedUpdatesString + " stored in SharedPreferences", e);
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject failedPackageJSON = CodePushUtils.convertReadableToJsonObject(failedPackage);
|
||||
failedUpdates.put(failedPackageJSON);
|
||||
mSettings.edit().putString(CodePushConstants.FAILED_UPDATES_KEY, failedUpdates.toString()).commit();
|
||||
}
|
||||
|
||||
public void savePendingUpdate(String packageHash, boolean isLoading) {
|
||||
JSONObject pendingUpdate = new JSONObject();
|
||||
try {
|
||||
pendingUpdate.put(CodePushConstants.PENDING_UPDATE_HASH_KEY, packageHash);
|
||||
pendingUpdate.put(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY, isLoading);
|
||||
mSettings.edit().putString(CodePushConstants.PENDING_UPDATE_KEY, pendingUpdate.toString()).commit();
|
||||
} catch (JSONException e) {
|
||||
// Should not happen.
|
||||
throw new CodePushUnknownException("Unable to save pending update.", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user