mirror of
https://github.com/zhigang1992/react-native-code-push.git
synced 2026-05-19 19:39:54 +08:00
update to master
This commit is contained in:
189
CodePush.js
189
CodePush.js
@@ -9,7 +9,7 @@ const PackageMixins = require("./package-mixins")(NativeCodePush);
|
||||
async function checkForUpdate(deploymentKey = null) {
|
||||
/*
|
||||
* Before we ask the server if an update exists, we
|
||||
* need to retrieve three pieces of information from the
|
||||
* need to retrieve three pieces of information from the
|
||||
* native side: deployment key, app version (e.g. 1.0.1)
|
||||
* and the hash of the currently running update (if there is one).
|
||||
* This allows the client to only receive updates which are targetted
|
||||
@@ -17,7 +17,7 @@ async function checkForUpdate(deploymentKey = null) {
|
||||
* different from the CodePush update they have already installed.
|
||||
*/
|
||||
const nativeConfig = await getConfiguration();
|
||||
|
||||
|
||||
/*
|
||||
* If a deployment key was explicitly provided,
|
||||
* then let's override the one we retrieved
|
||||
@@ -30,7 +30,7 @@ async function checkForUpdate(deploymentKey = null) {
|
||||
|
||||
// Use dynamically overridden getCurrentPackage() during tests.
|
||||
const localPackage = await module.exports.getCurrentPackage();
|
||||
|
||||
|
||||
/*
|
||||
* If the app has a previously installed update, and that update
|
||||
* was targetted at the same app version that is currently running,
|
||||
@@ -48,9 +48,9 @@ async function checkForUpdate(deploymentKey = null) {
|
||||
queryPackage.packageHash = config.packageHash;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);
|
||||
|
||||
|
||||
/*
|
||||
* There are four cases where checkForUpdate will resolve to null:
|
||||
* ----------------------------------------------------------------
|
||||
@@ -69,13 +69,13 @@ async function checkForUpdate(deploymentKey = null) {
|
||||
* because we want to avoid having to install diff updates against the binary's
|
||||
* version, which we can't do yet on Android.
|
||||
*/
|
||||
if (!update || update.updateAppVersion ||
|
||||
localPackage && (update.packageHash === localPackage.packageHash) ||
|
||||
if (!update || update.updateAppVersion ||
|
||||
localPackage && (update.packageHash === localPackage.packageHash) ||
|
||||
(!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {
|
||||
if (update && update.updateAppVersion) {
|
||||
log("An update is available but it is targeting a newer binary version than you are currently running.");
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
} else {
|
||||
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
|
||||
@@ -93,19 +93,23 @@ const getConfiguration = (() => {
|
||||
} else if (testConfig) {
|
||||
return testConfig;
|
||||
} else {
|
||||
config = await NativeCodePush.getConfiguration();
|
||||
config = await NativeCodePush.getConfiguration();
|
||||
return config;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
async function getCurrentPackage() {
|
||||
const localPackage = await NativeCodePush.getCurrentPackage();
|
||||
if (localPackage) {
|
||||
localPackage.failedInstall = await NativeCodePush.isFailedUpdate(localPackage.packageHash);
|
||||
localPackage.isFirstRun = await NativeCodePush.isFirstRun(localPackage.packageHash);
|
||||
return await getUpdateMetadata(CodePush.UpdateState.LATEST);
|
||||
}
|
||||
|
||||
async function getUpdateMetadata(updateState) {
|
||||
const updateMetadata = await NativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING);
|
||||
if (updateMetadata) {
|
||||
updateMetadata.failedInstall = await NativeCodePush.isFailedUpdate(updateMetadata.packageHash);
|
||||
updateMetadata.isFirstRun = await NativeCodePush.isFirstRun(updateMetadata.packageHash);
|
||||
}
|
||||
return localPackage;
|
||||
return updateMetadata;
|
||||
}
|
||||
|
||||
function getPromisifiedSdk(requestFetchAdapter, config) {
|
||||
@@ -119,7 +123,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
|
||||
} else {
|
||||
resolve(update);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -131,7 +135,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -143,7 +147,7 @@ function getPromisifiedSdk(requestFetchAdapter, config) {
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -155,7 +159,7 @@ function log(message) {
|
||||
console.log(`[CodePush] ${message}`)
|
||||
}
|
||||
|
||||
// This ensures that notifyApplicationReadyInternal is only called once
|
||||
// This ensures that notifyApplicationReadyInternal is only called once
|
||||
// in the lifetime of this module instance.
|
||||
const notifyApplicationReady = (() => {
|
||||
let notifyApplicationReadyPromise;
|
||||
@@ -163,13 +167,13 @@ const notifyApplicationReady = (() => {
|
||||
if (!notifyApplicationReadyPromise) {
|
||||
notifyApplicationReadyPromise = notifyApplicationReadyInternal();
|
||||
}
|
||||
|
||||
|
||||
return notifyApplicationReadyPromise;
|
||||
};
|
||||
})();
|
||||
|
||||
async function notifyApplicationReadyInternal() {
|
||||
await NativeCodePush.notifyApplicationReady();
|
||||
await NativeCodePush.notifyApplicationReady();
|
||||
const statusReport = await NativeCodePush.getNewStatusReport();
|
||||
if (statusReport) {
|
||||
const config = await getConfiguration();
|
||||
@@ -204,15 +208,15 @@ function setUpTestDependencies(testSdk, providedTestConfig, testNativeBridge) {
|
||||
const sync = (() => {
|
||||
let syncInProgress = false;
|
||||
const setSyncCompleted = () => { syncInProgress = false; };
|
||||
|
||||
|
||||
return (options = {}, syncStatusChangeCallback, downloadProgressCallback) => {
|
||||
if (syncInProgress) {
|
||||
typeof syncStatusChangeCallback === "function"
|
||||
? syncStatusChangeCallback(CodePush.SyncStatus.SYNC_IN_PROGRESS)
|
||||
: log("Sync already in progress.");
|
||||
return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
syncInProgress = true;
|
||||
const syncPromise = syncInternal(options, syncStatusChangeCallback, downloadProgressCallback);
|
||||
syncPromise
|
||||
@@ -226,7 +230,7 @@ const sync = (() => {
|
||||
/*
|
||||
* The syncInternal method provides a simple, one-line experience for
|
||||
* incorporating the check, download and installation of an update.
|
||||
*
|
||||
*
|
||||
* It simply composes the existing API methods together and adds additional
|
||||
* support for respecting mandatory updates, ignoring previously failed
|
||||
* releases, and displaying a standard confirmation UI to the end-user
|
||||
@@ -241,9 +245,9 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
||||
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
|
||||
minimumBackgroundDuration: 0,
|
||||
updateDialog: null,
|
||||
...options
|
||||
...options
|
||||
};
|
||||
|
||||
|
||||
syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
|
||||
? syncStatusChangeCallback
|
||||
: (syncStatus) => {
|
||||
@@ -267,7 +271,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
||||
log("User cancelled the update.");
|
||||
break;
|
||||
case CodePush.SyncStatus.UPDATE_INSTALLED:
|
||||
/*
|
||||
/*
|
||||
* If the install mode is IMMEDIATE, this will not get returned as the
|
||||
* app will be restarted to a new Javascript context.
|
||||
*/
|
||||
@@ -286,40 +290,34 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
downloadProgressCallback = typeof downloadProgressCallback === "function"
|
||||
? downloadProgressCallback
|
||||
: (downloadProgress) => {
|
||||
log(`Expecting ${downloadProgress.totalBytes} bytes, received ${downloadProgress.receivedBytes} bytes.`);
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
await CodePush.notifyApplicationReady();
|
||||
|
||||
|
||||
syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
|
||||
const remotePackage = await checkForUpdate(syncOptions.deploymentKey);
|
||||
|
||||
|
||||
const doDownloadAndInstall = async () => {
|
||||
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
|
||||
const localPackage = await remotePackage.download(downloadProgressCallback);
|
||||
|
||||
|
||||
// Determine the correct install mode based on whether the update is mandatory or not.
|
||||
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
|
||||
|
||||
|
||||
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
|
||||
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
|
||||
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
|
||||
});
|
||||
|
||||
|
||||
return CodePush.SyncStatus.UPDATE_INSTALLED;
|
||||
};
|
||||
|
||||
|
||||
const updateShouldBeIgnored = remotePackage && (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates);
|
||||
if (!remotePackage || updateShouldBeIgnored) {
|
||||
if (updateShouldBeIgnored) {
|
||||
log("An update is available, but it is being ignored due to having been previously rolled back.");
|
||||
}
|
||||
|
||||
|
||||
syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
|
||||
return CodePush.SyncStatus.UP_TO_DATE;
|
||||
} else if (syncOptions.updateDialog) {
|
||||
@@ -330,24 +328,24 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
||||
} else {
|
||||
syncOptions.updateDialog = { ...CodePush.DEFAULT_UPDATE_DIALOG, ...syncOptions.updateDialog };
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
let message = null;
|
||||
const dialogButtons = [{
|
||||
text: null,
|
||||
onPress: async () => {
|
||||
onPress: async () => {
|
||||
resolve(await doDownloadAndInstall());
|
||||
}
|
||||
}];
|
||||
|
||||
|
||||
if (remotePackage.isMandatory) {
|
||||
message = syncOptions.updateDialog.mandatoryUpdateMessage;
|
||||
dialogButtons[0].text = syncOptions.updateDialog.mandatoryContinueButtonLabel;
|
||||
} else {
|
||||
message = syncOptions.updateDialog.optionalUpdateMessage;
|
||||
dialogButtons[0].text = syncOptions.updateDialog.optionalInstallButtonLabel;
|
||||
dialogButtons[0].text = syncOptions.updateDialog.optionalInstallButtonLabel;
|
||||
// Since this is an optional update, add another button
|
||||
// to allow the end-user to ignore it
|
||||
// to allow the end-user to ignore it
|
||||
dialogButtons.push({
|
||||
text: syncOptions.updateDialog.optionalIgnoreButtonLabel,
|
||||
onPress: () => {
|
||||
@@ -356,13 +354,13 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// If the update has a description, and the developer
|
||||
// explicitly chose to display it, then set that as the message
|
||||
if (syncOptions.updateDialog.appendReleaseDescription && remotePackage.description) {
|
||||
message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
|
||||
message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
|
||||
}
|
||||
|
||||
|
||||
syncStatusChangeCallback(CodePush.SyncStatus.AWAITING_USER_ACTION);
|
||||
Alert.alert(syncOptions.updateDialog.title, message, dialogButtons);
|
||||
});
|
||||
@@ -371,57 +369,64 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
||||
}
|
||||
} catch (error) {
|
||||
syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
|
||||
log(error.message);
|
||||
log(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let CodePush;
|
||||
|
||||
// If the "NativeCodePush" variable isn't defined, then
|
||||
// If the "NativeCodePush" variable isn't defined, then
|
||||
// the app didn't properly install the native module,
|
||||
// and therefore, it doesn't make sense initializing
|
||||
// and therefore, it doesn't make sense initializing
|
||||
// the JS interface when it wouldn't work anyways.
|
||||
if (NativeCodePush) {
|
||||
CodePush = {
|
||||
AcquisitionSdk: Sdk,
|
||||
checkForUpdate,
|
||||
getConfiguration,
|
||||
getCurrentPackage,
|
||||
log,
|
||||
notifyApplicationReady,
|
||||
restartApp,
|
||||
setUpTestDependencies,
|
||||
sync,
|
||||
InstallMode: {
|
||||
IMMEDIATE: NativeCodePush.codePushInstallModeImmediate, // Restart the app immediately
|
||||
ON_NEXT_RESTART: NativeCodePush.codePushInstallModeOnNextRestart, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart
|
||||
ON_NEXT_RESUME: NativeCodePush.codePushInstallModeOnNextResume // Restart the app the next time it is resumed from the background
|
||||
},
|
||||
SyncStatus: {
|
||||
CHECKING_FOR_UPDATE: 0,
|
||||
AWAITING_USER_ACTION: 1,
|
||||
DOWNLOADING_PACKAGE: 2,
|
||||
INSTALLING_UPDATE: 3,
|
||||
UP_TO_DATE: 4, // The running app is up-to-date
|
||||
UPDATE_IGNORED: 5, // The app had an optional update and the end-user chose to ignore it
|
||||
UPDATE_INSTALLED: 6, // The app had an optional/mandatory update that was successfully downloaded and is about to be installed.
|
||||
SYNC_IN_PROGRESS: 7, // There is an ongoing "sync" operation in progress.
|
||||
UNKNOWN_ERROR: -1
|
||||
},
|
||||
DEFAULT_UPDATE_DIALOG: {
|
||||
appendReleaseDescription: false,
|
||||
descriptionPrefix: " Description: ",
|
||||
mandatoryContinueButtonLabel: "Continue",
|
||||
mandatoryUpdateMessage: "An update is available that must be installed.",
|
||||
optionalIgnoreButtonLabel: "Ignore",
|
||||
optionalInstallButtonLabel: "Install",
|
||||
optionalUpdateMessage: "An update is available. Would you like to install it?",
|
||||
title: "Update available"
|
||||
}
|
||||
CodePush = {
|
||||
AcquisitionSdk: Sdk,
|
||||
checkForUpdate,
|
||||
getConfiguration,
|
||||
getCurrentPackage,
|
||||
getUpdateMetadata,
|
||||
log,
|
||||
notifyAppReady: notifyApplicationReady,
|
||||
notifyApplicationReady,
|
||||
restartApp,
|
||||
setUpTestDependencies,
|
||||
sync,
|
||||
InstallMode: {
|
||||
IMMEDIATE: NativeCodePush.codePushInstallModeImmediate, // Restart the app immediately
|
||||
ON_NEXT_RESTART: NativeCodePush.codePushInstallModeOnNextRestart, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart
|
||||
ON_NEXT_RESUME: NativeCodePush.codePushInstallModeOnNextResume // Restart the app the next time it is resumed from the background
|
||||
},
|
||||
SyncStatus: {
|
||||
CHECKING_FOR_UPDATE: 0,
|
||||
AWAITING_USER_ACTION: 1,
|
||||
DOWNLOADING_PACKAGE: 2,
|
||||
INSTALLING_UPDATE: 3,
|
||||
UP_TO_DATE: 4, // The running app is up-to-date
|
||||
UPDATE_IGNORED: 5, // The app had an optional update and the end-user chose to ignore it
|
||||
UPDATE_INSTALLED: 6, // The app had an optional/mandatory update that was successfully downloaded and is about to be installed.
|
||||
SYNC_IN_PROGRESS: 7, // There is an ongoing "sync" operation in progress.
|
||||
UNKNOWN_ERROR: -1
|
||||
},
|
||||
UpdateState: {
|
||||
RUNNING: NativeCodePush.codePushUpdateStateRunning,
|
||||
PENDING: NativeCodePush.codePushUpdateStatePending,
|
||||
LATEST: NativeCodePush.codePushUpdateStateLatest
|
||||
},
|
||||
DEFAULT_UPDATE_DIALOG: {
|
||||
appendReleaseDescription: false,
|
||||
descriptionPrefix: " Description: ",
|
||||
mandatoryContinueButtonLabel: "Continue",
|
||||
mandatoryUpdateMessage: "An update is available that must be installed.",
|
||||
optionalIgnoreButtonLabel: "Ignore",
|
||||
optionalInstallButtonLabel: "Install",
|
||||
optionalUpdateMessage: "An update is available. Would you like to install it?",
|
||||
title: "Update available"
|
||||
}
|
||||
};
|
||||
} else {
|
||||
log("The CodePush module doesn't appear to be properly installed. Please double-check that everything is setup correctly.");
|
||||
log("The CodePush module doesn't appear to be properly installed. Please double-check that everything is setup correctly.");
|
||||
}
|
||||
|
||||
module.exports = CodePush;
|
||||
module.exports = CodePush;
|
||||
@@ -1,12 +1,16 @@
|
||||
require 'json'
|
||||
|
||||
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = 'CodePush'
|
||||
s.version = '1.10.1-beta'
|
||||
s.summary = 'React Native plugin for the CodePush service'
|
||||
s.version = package['version'].sub('-beta', '')
|
||||
s.summary = 'React Native module for the CodePush service'
|
||||
s.author = 'Microsoft Corporation'
|
||||
s.license = 'MIT'
|
||||
s.homepage = 'http://microsoft.github.io/code-push/'
|
||||
s.source = { :git => 'https://github.com/Microsoft/react-native-code-push.git', :tag => "v#{s.version}" }
|
||||
s.source = { :git => 'https://github.com/Microsoft/react-native-code-push.git', :tag => "v#{s.version}-beta" }
|
||||
s.platform = :ios, '7.0'
|
||||
s.source_files = 'ios/CodePush/*.{h,m}', 'ios/CodePush/SSZipArchive/*.{h,m}', 'ios/CodePush/SSZipArchive/aes/*.{h,c}', 'ios/CodePush/SSZipArchive/minizip/*.{h,c}'
|
||||
s.public_header_files = 'ios/CodePush/CodePush.h'
|
||||
@@ -14,4 +18,4 @@ Pod::Spec.new do |s|
|
||||
s.library = 'z'
|
||||
s.dependency 'React'
|
||||
|
||||
end
|
||||
end
|
||||
@@ -18,13 +18,13 @@ let CodePushDemoApp = React.createClass({
|
||||
let self = this;
|
||||
try {
|
||||
return await CodePush.sync(
|
||||
{
|
||||
{
|
||||
updateDialog: true,
|
||||
installMode: CodePush.InstallMode.ON_NEXT_RESUME
|
||||
},
|
||||
},
|
||||
(syncStatus) => {
|
||||
switch(syncStatus) {
|
||||
case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
|
||||
case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
|
||||
self.setState({
|
||||
syncMessage: "Checking for update."
|
||||
});
|
||||
@@ -80,36 +80,36 @@ let CodePushDemoApp = React.createClass({
|
||||
CodePush.log(error);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
CodePush.notifyApplicationReady();
|
||||
},
|
||||
|
||||
|
||||
getInitialState() {
|
||||
return { };
|
||||
},
|
||||
|
||||
|
||||
render() {
|
||||
let syncView, syncButton, progressView;
|
||||
|
||||
|
||||
if (this.state.syncMessage) {
|
||||
syncView = (
|
||||
<Text style={styles.messages}>{this.state.syncMessage}</Text>
|
||||
);
|
||||
} else {
|
||||
syncButton = (
|
||||
syncButton = (
|
||||
<Button style={{color: 'green'}} onPress={this.sync}>
|
||||
Start Sync!
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (this.state.progress) {
|
||||
progressView = (
|
||||
<Text style={styles.messages}>{this.state.progress.receivedBytes} of {this.state.progress.totalBytes} bytes received</Text>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.welcome}>
|
||||
|
||||
159
README.md
159
README.md
@@ -24,15 +24,15 @@ A React Native app is composed of JavaScript files and any accompanying [images]
|
||||
|
||||
The CodePush plugin helps get product improvements in front of your end users instantly, by keeping your JavaScript and images synchronized with updates you release to the CodePush server. This way, your app gets the benefits of an offline mobile experience, as well as the "web-like" agility of side-loading updates as soon as they are available. It's a win-win!
|
||||
|
||||
In order to ensure that your end users always have a functioning version of your app, the CodePush plugin maintains a copy of the previous update, so that in the event that you accidentally push an update which includes a crash, it can automatically roll back. This way, you can rest assured that your newfound release agility won't result in users becoming blocked before you have a chance to [roll back](http://microsoft.github.io/code-push/docs/cli.html#link-8) on the server. It's a win-win-win!
|
||||
In order to ensure that your end users always have a functioning version of your app, the CodePush plugin maintains a copy of the previous update, so that in the event that you accidentally push an update which includes a crash, it can automatically roll back. This way, you can rest assured that your newfound release agility won't result in users becoming blocked before you have a chance to [roll back](http://microsoft.github.io/code-push/docs/cli.html#link-10) on the server. It's a win-win-win!
|
||||
|
||||
*Note: Any product changes which touch native code (e.g. modifying your `AppDelegate.m`/`MainActivity.java` file, adding a new plugin) cannot be distributed via CodePush, and therefore, must be updated via the appropriate store(s).*
|
||||
|
||||
## Supported React Native platforms
|
||||
|
||||
- iOS
|
||||
- Android
|
||||
- Windows
|
||||
- iOS (7+)
|
||||
- Android (4.1+)
|
||||
- Windows (UWP)
|
||||
|
||||
We try our best to maintain backwards compatability of our plugin with previous versions of React Native, but due to the nature of the platform, and the existence of breaking changes between releases, it is possible that you need to use a specific version of the CodePush plugin in order to support the exact version of React Native you are using. The following table outlines which CodePush plugin versions officially support the respective React Native versions:
|
||||
|
||||
@@ -41,16 +41,17 @@ We try our best to maintain backwards compatability of our plugin with previous
|
||||
| <0.14.0 | **Unsupported** |
|
||||
| v0.14.0 | v1.3.0 *(introduced Android support)* |
|
||||
| v0.15.0-v0.18.0 | v1.4.0-v1.6.0 *(introduced iOS asset support)* |
|
||||
| v0.19.0-v0.24.0 | v1.7.0+ *(introduced Android asset support)* |
|
||||
| v0.25.0+ | TBD :) We work hard to respond to new RN releases, but they do occasionally break us. We will update this chart with each RN release, so that users can check to see what our "official" support is.
|
||||
| v0.19.0-v0.25.0 | v1.7.0+ *(introduced Android asset support)* |
|
||||
| v0.26.0+ | TBD :) We work hard to respond to new RN releases, but they do occasionally break us. We will update this chart with each RN release, so that users can check to see what our "official" support is.
|
||||
|
||||
## Supported Components
|
||||
|
||||
When using the React Native assets sytem (i.e. using the `require("./foo.png")` syntax), the following list represents the set of components (and props) that support having their referenced images updated via CodePush:
|
||||
When using the React Native assets sytem (i.e. using the `require("./foo.png")` syntax), the following list represents the set of core components (and props) that support having their referenced images updated via CodePush:
|
||||
|
||||
| Component | Prop(s) |
|
||||
|-------------------------------------------------|------------------------------------------|
|
||||
| `Image` | `source` |
|
||||
| `Image` | `source` |
|
||||
| `MapView.Marker` <br />*(Requires [react-native-maps](https://github.com/lelandrichardson/react-native-maps) `>=O.3.2`)* | `image` |
|
||||
| `ProgressViewIOS` | `progressImage`, `trackImage` |
|
||||
| `TabBarIOS.Item` | `icon`, `selectedIcon` |
|
||||
| `ToolbarAndroid` <br />*(React Native 0.21.0+)* | `actions[].icon`, `logo`, `overflowIcon` |
|
||||
@@ -60,6 +61,7 @@ The following list represents the set of components (and props) that don't curre
|
||||
| Component | Prop(s) |
|
||||
|-------------|----------------------------------------------------------------------|
|
||||
| `SliderIOS` | `maximumTrackImage`, `minimumTrackImage`, `thumbImage`, `trackImage` |
|
||||
| `Video` | `source` |
|
||||
|
||||
As new core components are released, which support referencing assets, we'll update this list to ensure users know what exactly they can expect to update using CodePush.
|
||||
|
||||
@@ -291,7 +293,7 @@ Once you've acquired the CodePush plugin, you need to integrate it into the Visu
|
||||
|
||||

|
||||
|
||||
3. Browse to the `node_modules\react-native-code-push\windows` directory, select the `CodePush.sln` file and click `OK`
|
||||
3. Browse to the `node_modules\react-native-code-push\windows` directory, select the `CodePush.csproj` file and click `OK`
|
||||
|
||||
4. Back in the `Solution Explorer`, right-click the project node that is named after your app, and select the `Add -> Reference...` menu item
|
||||
|
||||
@@ -312,19 +314,23 @@ using CodePush.ReactNative;
|
||||
...
|
||||
class AppReactPage : ReactPage
|
||||
{
|
||||
// 2. Update the JavaScriptBundleFile property to return the
|
||||
// bundle URL from CodePush instead of statically from the binary
|
||||
// 2. Declare a private instance variable for the CodePushModule instance.
|
||||
private CodePushModule codePushModule;
|
||||
|
||||
// 3. Update the JavaScriptBundleFile property to initalize the CodePush runtime,
|
||||
// specifying the right deployment key, then use it to return the bundle URL from
|
||||
// CodePush instead of statically from the binary. If you don't already have your
|
||||
// deployment key, you can run "code-push deployment ls <appName> -k" to retrieve it.
|
||||
public override string JavaScriptBundleFile
|
||||
{
|
||||
get
|
||||
{
|
||||
return CodePush.GetJavaScriptBundleFile();
|
||||
codePushModule = new CodePushModule("deployment-key-here", this);
|
||||
return codePushModule.GetJavaScriptBundleFile();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Instantiate an instance of the CodePush runtime and add it to the list of
|
||||
// existing packages, specifying the right deployment key. If you don't already
|
||||
// have it, you can run "code-push deployment ls <appName> -k" to retrieve your key.
|
||||
// 4. Add the codePushModule instance to the list of existing packages.
|
||||
public override List<IReactPackage> Packages
|
||||
{
|
||||
get
|
||||
@@ -333,7 +339,7 @@ class AppReactPage : ReactPage
|
||||
{
|
||||
new MainReactPackage(),
|
||||
...
|
||||
new CodePush("deployment-key-here", this)
|
||||
codePushModule
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -363,13 +369,23 @@ The simplest way to do this is to perform the following in your app's root compo
|
||||
codePush.sync();
|
||||
```
|
||||
|
||||
If an update is available, it will be silently downloaded, and installed the next time the app is restarted (either explicitly by the end user or by the OS), which ensures the least invasive experience for your end users. If an available update is mandatory, then it will be installed immediately, ensuring that the end user gets it as soon as possible. Additionally, if you would like to display a confirmation dialog (an "active install"), or customize the update experience in any way, refer to the `sync` method's [API reference](#codepushsync) for information on how to tweak this default behavior.
|
||||
If an update is available, it will be silently downloaded, and installed the next time the app is restarted (either explicitly by the end user or by the OS), which ensures the least invasive experience for your end users. If an available update is mandatory, then it will be installed immediately, ensuring that the end user gets it as soon as possible.
|
||||
|
||||
If you would like your app to discover updates more quickly, you can also choose to call `sync` every time the app resumes from the background, by adding the following code (or something equivalent) as part of your app's startup behavior (e.g. your root component's `componentDidMount` method). You can call `sync` as frequently as you would like, so when and where you call it just depends on your personal preference.
|
||||
|
||||
```javascript
|
||||
AppState.addEventListener("change", (newState) => {
|
||||
newState === "active" && codePush.sync();
|
||||
});
|
||||
```
|
||||
|
||||
Additionally, if you would like to display an update confirmation dialog (an "active install"), configure when an available update is installed (e.g. force an immediate restart) or customize the update experience in any way, refer to the `sync` method's [API reference](#codepushsync) for information on how to tweak this default behavior.
|
||||
|
||||
<a id="apple-note">*NOTE: While [Apple's developer agreement](https://developer.apple.com/programs/ios/information/iOS_Program_Information_4_3_15.pdf) fully allows performing over-the-air updates of JavaScript and assets (which is what enables CodePush!), it is against their policy for an app to display an update prompt. Because of this, we recommend that App Store-distributed apps don't enable the `updateDialog` option when calling `sync`, whereas Google Play and internally distributed apps (e.g. Enterprise, Fabric, HockeyApp) can choose to enable/customize it.*</a>
|
||||
|
||||
## Releasing Updates
|
||||
|
||||
Once your app has been configured and distributed to your users, and you've made some JS and/or asset changes, it's time to instantly release them! The simplest (and recommended) way to do this is to use the `release-react` comand in the CodePush CLI, which will handle bundling your JavaScript and asset files and releasing the update to the CodePush server.
|
||||
Once your app has been configured and distributed to your users, and you've made some JS and/or asset changes, it's time to instantly release them! The simplest (and recommended) way to do this is to use the `release-react` command in the CodePush CLI, which will handle bundling your JavaScript and asset files and releasing the update to the CodePush server.
|
||||
|
||||
In it's most basic form, this command only requires two parameters: your app name and the platform you are bundling the update for (either `ios` or `android`).
|
||||
|
||||
@@ -423,9 +439,11 @@ When you require `react-native-code-push`, the module object provides the follow
|
||||
|
||||
* [checkForUpdate](#codepushcheckforupdate): Asks the CodePush service whether the configured app deployment has an update available.
|
||||
|
||||
* [getCurrentPackage](#codepushgetcurrentpackage): Retrieves the metadata about the currently installed update (e.g. description, installation time, size).
|
||||
* [getCurrentPackage](#codepushgetcurrentpackage): Retrieves the metadata about the currently installed update (e.g. description, installation time, size). *NOTE: As of `v1.10.3-beta` of the CodePush module, this method is deprecated in favor of [`getUpdateMetadata`](#codepushgetupdatemetadata)*.
|
||||
|
||||
* [notifyApplicationReady](#codepushnotifyapplicationready): Notifies the CodePush runtime that an installed update is considered successful. If you are manually checking for and installing updates (i.e. not using the [sync](#codepushsync) method to handle it all for you), then this method **MUST** be called; otherwise CodePush will treat the update as failed and rollback to the previous version when the app next restarts.
|
||||
* [getUpdateMetadata](#codepushgetupdatemetadata): Retrieves the metadata for an installed update (e.g. description, mandatory).
|
||||
|
||||
* [notifyAppReady](#codepushnotifyappready): Notifies the CodePush runtime that an installed update is considered successful. If you are manually checking for and installing updates (i.e. not using the [sync](#codepushsync) method to handle it all for you), then this method **MUST** be called; otherwise CodePush will treat the update as failed and rollback to the previous version when the app next restarts.
|
||||
|
||||
* [restartApp](#codepushrestartapp): Immediately restarts the app. If there is an update pending, it will be immediately displayed to the end user. Otherwise, calling this method simply has the same behavior as the end user killing and restarting the process.
|
||||
|
||||
@@ -464,6 +482,8 @@ codePush.checkForUpdate()
|
||||
|
||||
#### codePush.getCurrentPackage
|
||||
|
||||
*NOTE: This method is considered deprecated as of `v1.10.3-beta` of the CodePush module. If you're running this version (or newer), we would recommend using the [`codePush.getUpdateMetadata`](#codepushgetupdatemetadata) instead, since it has more predictable behavior.*
|
||||
|
||||
```javascript
|
||||
codePush.getCurrentPackage(): Promise<LocalPackage>;
|
||||
```
|
||||
@@ -493,15 +513,59 @@ codePush.getCurrentPackage()
|
||||
});
|
||||
```
|
||||
|
||||
#### codePush.notifyApplicationReady
|
||||
#### codePush.getUpdateMetadata
|
||||
|
||||
```javascript
|
||||
codePush.notifyApplicationReady(): Promise<void>;
|
||||
codePush.getUpdateMetadata(updateState: UpdateState = UpdateState.RUNNING): Promise<LocalPackage>;
|
||||
```
|
||||
|
||||
Retrieves the metadata for an installed update (e.g. description, mandatory) whose state matches the specified `updateState` parameter. This can be useful for scenarios such as displaying a "what's new?" dialog after an update has been applied or checking whether there is a pending update that is waiting to be applied via a resume or restart. For more details about the possible update states, and what they represent, refer to the [UpdateState reference](#updatestate).
|
||||
|
||||
This method returns a `Promise` which resolves to one of two possible values:
|
||||
|
||||
1. `null` if an update with the specified state doesn't currently exist. This occurs in the following scenarios:
|
||||
|
||||
1. The end-user hasn't installed any CodePush updates yet, and therefore, no metadata is available for any updates, regardless what you specify as the `updateState` parameter.
|
||||
|
||||
2. The end-user installed an update of the binary (e.g. from the store), which cleared away the old CodePush updates, and gave precedence back to the JS binary in the binary. Therefore, it would exhibit the same behavior as #1
|
||||
|
||||
3. The `updateState` parameter is set to `UpdateState.RUNNING`, but the app isn't currently running a CodePush update. There may be a pending update, but the app hasn't been restarted yet in order to make it active.
|
||||
|
||||
4. The `updateState` parameter is set to `UpdateState.PENDING`, but the app doesn't have any currently pending updates.
|
||||
|
||||
2. A [`LocalPackage`](#localpackage) instance which represents the metadata for the currently requested CodePush update (either the running or pending).
|
||||
|
||||
Example Usage:
|
||||
|
||||
```javascript
|
||||
// Check if there is currently a CodePush update running, and if
|
||||
// so, register it with the HockeyApp SDK (https://github.com/slowpath/react-native-hockeyapp)
|
||||
// so that crash reports will correctly display the JS bundle version the user was running.
|
||||
codePush.getUpdateMetadata().then((update) => {
|
||||
if (update) {
|
||||
hockeyApp.addMetadata({ CodePushRelease: update.label });
|
||||
}
|
||||
});
|
||||
|
||||
// Check to see if there is still an update pending.
|
||||
codePush.getUpdateMetadata(UpdateState.PENDING).then((update) => {
|
||||
if (update) {
|
||||
// There's a pending update, do we want to force a restart?
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### codePush.notifyAppReady
|
||||
|
||||
```javascript
|
||||
codePush.notifyAppReady(): Promise<void>;
|
||||
```
|
||||
|
||||
Notifies the CodePush runtime that a freshly installed update should be considered successful, and therefore, an automatic client-side rollback isn't necessary. It is mandatory to call this function somewhere in the code of the updated bundle. Otherwise, when the app next restarts, the CodePush runtime will assume that the installed update has failed and roll back to the previous version. This behavior exists to help ensure that your end users aren't blocked by a broken update.
|
||||
|
||||
If you are using the `sync` function, and doing your update check on app start, then you don't need to manually call `notifyApplicationReady` since `sync` will call it for you. This behavior exists due to the assumption that the point at which `sync` is called in your app represents a good approximation of a successful startup.
|
||||
If you are using the `sync` function, and doing your update check on app start, then you don't need to manually call `notifyAppReady` since `sync` will call it for you. This behavior exists due to the assumption that the point at which `sync` is called in your app represents a good approximation of a successful startup.
|
||||
|
||||
*NOTE: This method is also aliased as `notifyApplicationReady` (for backwards compatibility).*
|
||||
|
||||
#### codePush.restartApp
|
||||
|
||||
@@ -531,7 +595,6 @@ This method provides support for two different (but customizable) "modes" to eas
|
||||
|
||||
2. **Active mode**, which when an update is available, prompts the end user for permission before downloading it, and then immediately applies the update. If an update was released using the `mandatory` flag, the end user would still be notified about the update, but they wouldn't have the choice to ignore it.
|
||||
|
||||
|
||||
Example Usage:
|
||||
|
||||
```javascript
|
||||
@@ -615,24 +678,31 @@ In addition to the options, the `sync` method also accepts two optional function
|
||||
* __syncStatusChangedCallback__ *((syncStatus: Number) => void)* - Called when the sync process moves from one stage to another in the overall update process. The method is called with a status code which represents the current state, and can be any of the [`SyncStatus`](#syncstatus) values.
|
||||
|
||||
* __downloadProgressCallback__ *((progress: DownloadProgress) => void)* - Called periodically when an available update is being downloaded from the CodePush server. The method is called with a `DownloadProgress` object, which contains the following two properties:
|
||||
* __totalBytes__ *(Number)* - The total number of bytes expected to be received for this update package
|
||||
* __receivedBytes__ *(Number)* - The number of bytes downloaded thus far.
|
||||
|
||||
* __totalBytes__ *(Number)* - The total number of bytes expected to be received for this update (i.e. the size of the set of files which changed from the previous release).
|
||||
|
||||
* __receivedBytes__ *(Number)* - The number of bytes downloaded thus far, which can be used to track download progress.
|
||||
|
||||
Example Usage:
|
||||
|
||||
```javascript
|
||||
// Prompt the user when an update is available
|
||||
// and then display a "downloading" modal
|
||||
codePush.sync({ updateDialog: true }, (status) => {
|
||||
switch (status) {
|
||||
case codePush.SyncStatus.DOWNLOADING_PACKAGE:
|
||||
// Show "downloading" modal
|
||||
break;
|
||||
case codePush.SyncStatus.INSTALLING_UPDATE:
|
||||
// Hide "downloading" modal
|
||||
break;
|
||||
}
|
||||
});
|
||||
codePush.sync({ updateDialog: true },
|
||||
(status) => {
|
||||
switch (status) {
|
||||
case codePush.SyncStatus.DOWNLOADING_PACKAGE:
|
||||
// Show "downloading" modal
|
||||
break;
|
||||
case codePush.SyncStatus.INSTALLING_UPDATE:
|
||||
// Hide "downloading" modal
|
||||
break;
|
||||
}
|
||||
},
|
||||
({ receivedBytes, totalBytes, }) => {
|
||||
/* Update download modal progress */
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
This method returns a `Promise` which is resolved to a `SyncStatus` code that indicates why the `sync` call succeeded. This code can be one of the following `SyncStatus` values:
|
||||
@@ -649,7 +719,7 @@ The `sync` method can be called anywhere you'd like to check for an update. That
|
||||
|
||||
#### Package objects
|
||||
|
||||
The `checkForUpdate` and `getCurrentPackage` methods return promises, that when resolved, provide acces to "package" objects. The package represents your code update as well as any extra metadata (e.g. description, mandatory?). The CodePush API has the distinction between the following types of packages:
|
||||
The `checkForUpdate` and `getUpdateMetadata` methods return `Promise` objects, that when resolved, provide acces to "package" objects. The package represents your code update as well as any extra metadata (e.g. description, mandatory?). The CodePush API has the distinction between the following types of packages:
|
||||
|
||||
* [LocalPackage](#localpackage): Represents a downloaded update that is either already running, or has been installed and is pending an app restart.
|
||||
|
||||
@@ -657,7 +727,7 @@ The `checkForUpdate` and `getCurrentPackage` methods return promises, that when
|
||||
|
||||
##### LocalPackage
|
||||
|
||||
Contains details about an update that has been downloaded locally or already installed. You can get a reference to an instance of this object either by calling the module-level `getCurrentPackage` method, or as the value of the promise returned by the `RemotePackage.download` method.
|
||||
Contains details about an update that has been downloaded locally or already installed. You can get a reference to an instance of this object either by calling the module-level `getUpdateMetadata` method, or as the value of the promise returned by the `RemotePackage.download` method.
|
||||
|
||||
###### Properties
|
||||
- __appVersion__: The app binary version that this update is dependent on. This is the value that was specified via the `appStoreVersion` parameter when calling the CLI's `release` command. *(String)*
|
||||
@@ -695,7 +765,7 @@ The CodePush API includes the following enums which can be used to customize the
|
||||
|
||||
##### InstallMode
|
||||
|
||||
This enum specified when you would like an installed update to actually be applied, and can be passed to either the `sync` or `LocalPackage.install` methods. It includes the following values:
|
||||
This enum specifies when you would like an installed update to actually be applied, and can be passed to either the `sync` or `LocalPackage.install` methods. It includes the following values:
|
||||
|
||||
* __codePush.InstallMode.IMMEDIATE__ *(0)* - Indicates that you want to install the update and restart the app immediately. This value is appropriate for debugging scenarios as well as when displaying an update prompt to the user, since they would expect to see the changes immediately after accepting the installation. Additionally, this mode can be used to enforce mandatory updates, since it removes the potentially undesired latency between the update installation and the next time the end user restarts or resumes the app.
|
||||
|
||||
@@ -717,6 +787,16 @@ This enum is provided to the `syncStatusChangedCallback` function that can be pa
|
||||
* __codePush.SyncStatus.SYNC_IN_PROGRESS__ *(7)* - There is an ongoing `sync` operation running which prevents the current call from being executed.
|
||||
* __codePush.SyncStatus.UNKNOWN_ERROR__ *(-1)* - The sync operation encountered an unknown error.
|
||||
|
||||
##### UpdateState
|
||||
|
||||
This enum specifies the state that an update is currently in, and can be specified when calling the `getUpdateMetadata` method. It includes the following values:
|
||||
|
||||
* __codePush.UpdateState.RUNNING__ *(0)* - Indicates that an update represents the version of the app that is currently running. This can be useful for identifying attributes about the app, for scenarios such as displaying the release description in a "what's new?" dialog or reporting the latest version to an analytics and/or crash reporting service.
|
||||
|
||||
* __codePush.UpdateState.PENDING__ *(1)* - Indicates than an update has been installed, but the app hasn't been restarted yet in order to apply it. This can be useful for determining whether there is a pending update, which you may want to force a programmatic restart (via `restartApp`) in order to apply.
|
||||
|
||||
* __codePush.UpdateState.LATEST__ *(2)* - Indicates than an update represents the latest available release, and can be either currently running or pending.
|
||||
|
||||
### Objective-C API Reference (iOS)
|
||||
|
||||
The Objective-C API is made available by importing the `CodePush.h` header into your `AppDelegate.m` file, and consists of a single public class named `CodePush`.
|
||||
@@ -775,6 +855,7 @@ Constructs the CodePush client runtime and represents the `ReactPackage` instanc
|
||||
|
||||
The React Native community has graciously created some awesome open source apps that can serve as examples for developers that are getting started. The following is a list of OSS React Native apps that are also using CodePush, and can therefore be used to see how others are using the service:
|
||||
|
||||
* [F8 App](https://github.com/fbsamples/f8app) - The official conference app for [F8 2016](https://www.fbf8.com/).
|
||||
* [Feline for Product Hunt](https://github.com/arjunkomath/Feline-for-Product-Hunt) - An Android client for Product Hunt.
|
||||
* [GeoEncoding](https://github.com/LynxITDigital/GeoEncoding) - An app by [Lynx IT Digital](https://digital.lynxit.com.au) which demonstrates how to use numerous React Native components and modules.
|
||||
* [Math Facts](https://github.com/Khan/math-facts) - An app by Khan Academy to help memorize math facts more easily.
|
||||
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
|
||||
@@ -25,12 +26,12 @@ 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.ReflectiveOperationException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@@ -108,8 +109,21 @@ public class CodePush implements ReactPackage {
|
||||
}
|
||||
|
||||
currentInstance = this;
|
||||
|
||||
clearDebugCacheIfNeeded();
|
||||
initializeUpdateAfterRestart();
|
||||
}
|
||||
|
||||
|
||||
private void clearDebugCacheIfNeeded() {
|
||||
if (isDebugMode && 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");
|
||||
if (cachedDevBundle.exists()) {
|
||||
cachedDevBundle.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long getBinaryResourcesModifiedTime() {
|
||||
ZipFile applicationFile = null;
|
||||
try {
|
||||
@@ -133,7 +147,7 @@ public class CodePush implements ReactPackage {
|
||||
public static String getBundleUrl() {
|
||||
return getBundleUrl(DEFAULT_JS_BUNDLE_NAME);
|
||||
}
|
||||
|
||||
|
||||
public static String getBundleUrl(String assetsBundleFileName) {
|
||||
if (currentInstance == null) {
|
||||
throw new CodePushNotInitializedException("A CodePush instance has not been created yet. Have you added it to your app's list of ReactPackages?");
|
||||
@@ -141,7 +155,7 @@ public class CodePush implements ReactPackage {
|
||||
|
||||
return currentInstance.getBundleUrlInternal(assetsBundleFileName);
|
||||
}
|
||||
|
||||
|
||||
public String getBundleUrlInternal(String assetsBundleFileName) {
|
||||
this.assetsBundleFileName = assetsBundleFileName;
|
||||
String binaryJsBundleUrl = ASSETS_BUNDLE_PREFIX + assetsBundleFileName;
|
||||
@@ -219,11 +233,14 @@ public class CodePush implements ReactPackage {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void initializeUpdateAfterRestart() {
|
||||
// Reset the state which indicates that
|
||||
// the app was just freshly updated.
|
||||
didUpdate = false;
|
||||
|
||||
JSONObject pendingUpdate = getPendingUpdate();
|
||||
if (pendingUpdate != null) {
|
||||
didUpdate = true;
|
||||
try {
|
||||
boolean updateIsLoading = pendingUpdate.getBoolean(PENDING_UPDATE_IS_LOADING_KEY);
|
||||
if (updateIsLoading) {
|
||||
@@ -233,6 +250,10 @@ public class CodePush implements ReactPackage {
|
||||
needToReportRollback = 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;
|
||||
|
||||
// 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),
|
||||
@@ -266,7 +287,7 @@ public class CodePush implements ReactPackage {
|
||||
|
||||
private boolean isPendingUpdate(String packageHash) {
|
||||
JSONObject pendingUpdate = getPendingUpdate();
|
||||
|
||||
|
||||
try {
|
||||
return pendingUpdate != null &&
|
||||
!pendingUpdate.getBoolean(PENDING_UPDATE_IS_LOADING_KEY) &&
|
||||
@@ -286,7 +307,7 @@ public class CodePush implements ReactPackage {
|
||||
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
|
||||
settings.edit().remove(PENDING_UPDATE_KEY).commit();
|
||||
}
|
||||
|
||||
|
||||
private void rollbackPackage() {
|
||||
WritableMap failedPackage = codePushPackage.getCurrentPackage();
|
||||
saveFailedUpdate(failedPackage);
|
||||
@@ -346,17 +367,23 @@ public class CodePush implements ReactPackage {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -364,34 +391,18 @@ public class CodePush implements ReactPackage {
|
||||
public String getName() {
|
||||
return "CodePush";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
CodePush.this.initializeUpdateAfterRestart();
|
||||
}
|
||||
|
||||
private void clearReactDevBundleCache() {
|
||||
// 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(CodePush.this.applicationContext.getFilesDir(), "ReactNativeDevBundle.js");
|
||||
if (cachedDevBundle.exists()) {
|
||||
cachedDevBundle.delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void loadBundleLegacy() {
|
||||
Intent intent = mainActivity.getIntent();
|
||||
mainActivity.finish();
|
||||
mainActivity.startActivity(intent);
|
||||
|
||||
|
||||
currentInstance = null;
|
||||
}
|
||||
|
||||
|
||||
private void loadBundle() {
|
||||
// Clear the React dev bundle cache so that new updates can be loaded.
|
||||
if (CodePush.this.isDebugMode) {
|
||||
clearReactDevBundleCache();
|
||||
}
|
||||
|
||||
CodePush.this.clearDebugCacheIfNeeded();
|
||||
|
||||
try {
|
||||
// #1) Get the private ReactInstanceManager, which is what includes
|
||||
// the logic to reload the current React context.
|
||||
@@ -404,7 +415,7 @@ public class CodePush implements ReactPackage {
|
||||
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() {
|
||||
@@ -412,8 +423,9 @@ public class CodePush implements ReactPackage {
|
||||
public void run() {
|
||||
try {
|
||||
recreateMethod.invoke(instanceManager);
|
||||
initializeUpdateAfterRestart();
|
||||
}
|
||||
catch (ReflectiveOperationException e) {
|
||||
catch (Exception e) {
|
||||
// The recreation method threw an unknown exception
|
||||
// so just simply fallback to restarting the Activity
|
||||
loadBundleLegacy();
|
||||
@@ -421,7 +433,7 @@ public class CodePush implements ReactPackage {
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (ReflectiveOperationException e) {
|
||||
catch (Exception e) {
|
||||
// Our reflection logic failed somewhere
|
||||
// so fall back to restarting the Activity
|
||||
loadBundleLegacy();
|
||||
@@ -429,7 +441,7 @@ public class CodePush implements ReactPackage {
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void downloadUpdate(final ReadableMap updatePackage, final Promise promise) {
|
||||
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) {
|
||||
@@ -437,11 +449,48 @@ public class CodePush implements ReactPackage {
|
||||
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, downloadProgress.createWritableMap());
|
||||
.emit(DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -484,29 +533,49 @@ public class CodePush implements ReactPackage {
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getCurrentPackage(final Promise promise) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (isRunningBinaryVersion) {
|
||||
currentPackage.putBoolean("_isDebugOnly", true);
|
||||
}
|
||||
|
||||
Boolean isPendingUpdate = false;
|
||||
Boolean currentUpdateIsPending = false;
|
||||
|
||||
if (currentPackage.hasKey(PACKAGE_HASH_KEY)) {
|
||||
String currentHash = currentPackage.getString(PACKAGE_HASH_KEY);
|
||||
isPendingUpdate = CodePush.this.isPendingUpdate(currentHash);
|
||||
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);
|
||||
}
|
||||
|
||||
currentPackage.putBoolean("isPending", isPendingUpdate);
|
||||
promise.resolve(currentPackage);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -589,7 +658,11 @@ public class CodePush implements ReactPackage {
|
||||
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 = (new Date().getTime() - lastPausedDate.getTime()) / 1000;
|
||||
long durationInBackground = 0;
|
||||
if (lastPausedDate != null) {
|
||||
durationInBackground = (new Date().getTime() - lastPausedDate.getTime()) / 1000;
|
||||
}
|
||||
|
||||
if (durationInBackground >= CodePushNativeModule.this.minimumBackgroundDuration) {
|
||||
loadBundle();
|
||||
}
|
||||
@@ -619,7 +692,7 @@ public class CodePush implements ReactPackage {
|
||||
|
||||
asyncTask.execute();
|
||||
}
|
||||
|
||||
|
||||
@ReactMethod
|
||||
public void isFailedUpdate(String packageHash, Promise promise) {
|
||||
promise.resolve(isFailedHash(packageHash));
|
||||
@@ -639,7 +712,7 @@ public class CodePush implements ReactPackage {
|
||||
removePendingUpdate();
|
||||
promise.resolve("");
|
||||
}
|
||||
|
||||
|
||||
@ReactMethod
|
||||
public void restartApp(boolean onlyIfUpdateIsPending) {
|
||||
// If this is an unconditional restart request, or there
|
||||
|
||||
@@ -9,4 +9,4 @@ public class CodePushMalformedDataException extends RuntimeException {
|
||||
public CodePushMalformedDataException(String url, MalformedURLException cause) {
|
||||
super("The package has an invalid downloadUrl: " + url, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ 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";
|
||||
@@ -72,6 +71,7 @@ public class CodePushPackage {
|
||||
try {
|
||||
return CodePushUtils.getWritableMapFromFile(statusFilePath);
|
||||
} catch (IOException e) {
|
||||
// Should not happen.
|
||||
throw new CodePushUnknownException("Error getting current package info" , e);
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ public class CodePushPackage {
|
||||
try {
|
||||
CodePushUtils.writeReadableMapToFile(packageInfo, getStatusFilePath());
|
||||
} catch (IOException e) {
|
||||
// Should not happen.
|
||||
throw new CodePushUnknownException("Error updating current package info" , e);
|
||||
}
|
||||
}
|
||||
@@ -101,6 +102,10 @@ public class CodePushPackage {
|
||||
}
|
||||
|
||||
WritableMap currentPackage = getCurrentPackage();
|
||||
if (currentPackage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String relativeBundlePath = CodePushUtils.tryGetString(currentPackage, RELATIVE_BUNDLE_PATH_KEY);
|
||||
if (relativeBundlePath == null) {
|
||||
return CodePushUtils.appendPathComponent(packageFolder, bundleFileName);
|
||||
@@ -124,18 +129,21 @@ public class CodePushPackage {
|
||||
}
|
||||
|
||||
public WritableMap getCurrentPackage() {
|
||||
String folderPath = getCurrentPackageFolderPath();
|
||||
if (folderPath == null) {
|
||||
String packageHash = getCurrentPackageHash();
|
||||
if (packageHash == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String packagePath = CodePushUtils.appendPathComponent(folderPath, PACKAGE_FILE_NAME);
|
||||
try {
|
||||
return CodePushUtils.getWritableMapFromFile(packagePath);
|
||||
} catch (IOException e) {
|
||||
// Should not happen unless the update metadata was somehow deleted.
|
||||
|
||||
return getPackage(packageHash);
|
||||
}
|
||||
|
||||
public WritableMap getPreviousPackage() {
|
||||
String packageHash = getPreviousPackageHash();
|
||||
if (packageHash == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getPackage(packageHash);
|
||||
}
|
||||
|
||||
public WritableMap getPackage(String packageHash) {
|
||||
@@ -340,8 +348,6 @@ public class CodePushPackage {
|
||||
}
|
||||
|
||||
public void clearUpdates() {
|
||||
File statusFile = new File(getStatusFilePath());
|
||||
statusFile.delete();
|
||||
FileUtils.deleteDirectoryAtPath(getCodePushPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.microsoft.codepush.react;
|
||||
|
||||
public enum CodePushUpdateState {
|
||||
RUNNING(0),
|
||||
PENDING(1),
|
||||
LATEST(2);
|
||||
|
||||
private final int value;
|
||||
CodePushUpdateState(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
public int getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,7 @@ public class CodePushUtils {
|
||||
JSONObject json = new JSONObject(content);
|
||||
return convertJsonObjectToWritable(json);
|
||||
} catch (JSONException jsonException) {
|
||||
// Should not happen
|
||||
throw new CodePushMalformedDataException(filePath, jsonException);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,8 @@ class DownloadProgress {
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return this.totalBytes == this.receivedBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
13BE3DEE1AC21097009241FE /* CodePush.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* CodePush.m */; };
|
||||
1B23B9141BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */; };
|
||||
1B762E901C9A5E9A006EF800 /* CodePushErrorUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B762E8F1C9A5E9A006EF800 /* CodePushErrorUtils.m */; };
|
||||
1BCC09A71CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCC09A61CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m */; };
|
||||
540D20121C7684FE00D6EF41 /* CodePushUpdateUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */; };
|
||||
5421FE311C58AD5A00986A55 /* CodePushTelemetryManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */; };
|
||||
54A0026C1C0E2880004C3CEC /* aescrypt.c in Sources */ = {isa = PBXBuildFile; fileRef = 54A0024C1C0E2880004C3CEC /* aescrypt.c */; };
|
||||
@@ -49,6 +50,7 @@
|
||||
13BE3DED1AC21097009241FE /* CodePush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePush.m; path = CodePush/CodePush.m; sourceTree = "<group>"; };
|
||||
1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RCTConvert+CodePushInstallMode.m"; path = "CodePush/RCTConvert+CodePushInstallMode.m"; sourceTree = "<group>"; };
|
||||
1B762E8F1C9A5E9A006EF800 /* CodePushErrorUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushErrorUtils.m; path = CodePush/CodePushErrorUtils.m; sourceTree = "<group>"; };
|
||||
1BCC09A61CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RCTConvert+CodePushUpdateState.m"; path = "CodePush/RCTConvert+CodePushUpdateState.m"; sourceTree = "<group>"; };
|
||||
540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushUpdateUtils.m; path = CodePush/CodePushUpdateUtils.m; sourceTree = "<group>"; };
|
||||
5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushTelemetryManager.m; path = CodePush/CodePushTelemetryManager.m; sourceTree = "<group>"; };
|
||||
54A0024A1C0E2880004C3CEC /* aes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = aes.h; sourceTree = "<group>"; };
|
||||
@@ -168,16 +170,17 @@
|
||||
58B511D21A9E6C8500147676 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54A002481C0E2880004C3CEC /* SSZipArchive */,
|
||||
1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */,
|
||||
13BE3DEC1AC21097009241FE /* CodePush.h */,
|
||||
13BE3DED1AC21097009241FE /* CodePush.m */,
|
||||
81D51F391B6181C2000DA084 /* CodePushConfig.m */,
|
||||
54FFEDDF1BF550630061DD23 /* CodePushDownloadHandler.m */,
|
||||
1B762E8F1C9A5E9A006EF800 /* CodePushErrorUtils.m */,
|
||||
810D4E6C1B96935000B397E9 /* CodePushPackage.m */,
|
||||
5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */,
|
||||
540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */,
|
||||
13BE3DEC1AC21097009241FE /* CodePush.h */,
|
||||
13BE3DED1AC21097009241FE /* CodePush.m */,
|
||||
1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */,
|
||||
1BCC09A61CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m */,
|
||||
54A002481C0E2880004C3CEC /* SSZipArchive */,
|
||||
134814211AA4EA7D00B7C361 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -242,6 +245,7 @@
|
||||
540D20121C7684FE00D6EF41 /* CodePushUpdateUtils.m in Sources */,
|
||||
54A0026E1C0E2880004C3CEC /* aestab.c in Sources */,
|
||||
54A002761C0E2880004C3CEC /* mztools.c in Sources */,
|
||||
1BCC09A71CC19EB700DDC0DD /* RCTConvert+CodePushUpdateState.m in Sources */,
|
||||
54A002781C0E2880004C3CEC /* zip.c in Sources */,
|
||||
54A002791C0E2880004C3CEC /* SSZipArchive.m in Sources */,
|
||||
1B23B9141BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m in Sources */,
|
||||
|
||||
@@ -54,11 +54,13 @@
|
||||
@property (strong) NSOutputStream *outputFileStream;
|
||||
@property long long expectedContentLength;
|
||||
@property long long receivedContentLength;
|
||||
@property dispatch_queue_t operationQueue;
|
||||
@property (copy) void (^progressCallback)(long long, long long);
|
||||
@property (copy) void (^doneCallback)(BOOL);
|
||||
@property (copy) void (^failCallback)(NSError *err);
|
||||
|
||||
- (id)init:(NSString *)downloadFilePath
|
||||
operationQueue:(dispatch_queue_t)operationQueue
|
||||
progressCallback:(void (^)(long long, long long))progressCallback
|
||||
doneCallback:(void (^)(BOOL))doneCallback
|
||||
failCallback:(void (^)(NSError *err))failCallback;
|
||||
@@ -78,12 +80,14 @@ failCallback:(void (^)(NSError *err))failCallback;
|
||||
|
||||
+ (void)downloadPackage:(NSDictionary *)updatePackage
|
||||
expectedBundleFileName:(NSString *)expectedBundleFileName
|
||||
operationQueue:(dispatch_queue_t)operationQueue
|
||||
progressCallback:(void (^)(long long, long long))progressCallback
|
||||
doneCallback:(void (^)())doneCallback
|
||||
failCallback:(void (^)(NSError *err))failCallback;
|
||||
|
||||
+ (NSString *)getBinaryAssetsPath;
|
||||
+ (NSDictionary *)getCurrentPackage:(NSError **)error;
|
||||
+ (NSDictionary *)getPreviousPackage:(NSError **)error;
|
||||
+ (NSString *)getCurrentPackageFolderPath:(NSError **)error;
|
||||
+ (NSString *)getCurrentPackageBundlePath:(NSError **)error;
|
||||
+ (NSString *)getCurrentPackageHash:(NSError **)error;
|
||||
@@ -140,4 +144,10 @@ typedef NS_ENUM(NSInteger, CodePushInstallMode) {
|
||||
CodePushInstallModeImmediate,
|
||||
CodePushInstallModeOnNextRestart,
|
||||
CodePushInstallModeOnNextResume
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CodePushUpdateState) {
|
||||
CodePushUpdateStateRunning,
|
||||
CodePushUpdateStatePending,
|
||||
CodePushUpdateStateLatest
|
||||
};
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#import "CodePush.h"
|
||||
|
||||
@interface CodePush () <RCTBridgeModule>
|
||||
@interface CodePush () <RCTBridgeModule, RCTFrameUpdateObserver>
|
||||
@end
|
||||
|
||||
@implementation CodePush {
|
||||
@@ -15,6 +15,11 @@
|
||||
BOOL _isFirstRunAfterUpdate;
|
||||
int _minimumBackgroundDuration;
|
||||
NSDate *_lastResignedDate;
|
||||
|
||||
// Used to coordinate the dispatching of download progress events to JS.
|
||||
long long _latestExpectedContentLength;
|
||||
long long _latestReceivedConentLength;
|
||||
BOOL _didUpdateProgress;
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
@@ -76,21 +81,21 @@ static NSString *bundleResourceName = @"main";
|
||||
{
|
||||
bundleResourceName = resourceName;
|
||||
bundleResourceExtension = resourceExtension;
|
||||
|
||||
|
||||
[self ensureBinaryBundleExists];
|
||||
|
||||
|
||||
NSString *logMessageFormat = @"Loading JS bundle from %@";
|
||||
|
||||
|
||||
NSError *error;
|
||||
NSString *packageFile = [CodePushPackage getCurrentPackageBundlePath:&error];
|
||||
NSURL *binaryBundleURL = [self binaryBundleURL];
|
||||
|
||||
|
||||
if (error || !packageFile) {
|
||||
NSLog(logMessageFormat, binaryBundleURL);
|
||||
isRunningBinaryVersion = YES;
|
||||
return binaryBundleURL;
|
||||
}
|
||||
|
||||
|
||||
NSString *binaryAppVersion = [[CodePushConfig current] appVersion];
|
||||
NSDictionary *currentPackageMetadata = [CodePushPackage getCurrentPackage:&error];
|
||||
if (error || !currentPackageMetadata) {
|
||||
@@ -98,10 +103,10 @@ static NSString *bundleResourceName = @"main";
|
||||
isRunningBinaryVersion = YES;
|
||||
return binaryBundleURL;
|
||||
}
|
||||
|
||||
|
||||
NSString *packageDate = [currentPackageMetadata objectForKey:BinaryBundleDateKey];
|
||||
NSString *packageAppVersion = [currentPackageMetadata objectForKey:AppVersionKey];
|
||||
|
||||
|
||||
if ([[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL] isEqualToString:packageDate] && ([CodePush isUsingTestConfiguration] ||[binaryAppVersion isEqualToString:packageAppVersion])) {
|
||||
// Return package file because it is newer than the app store binary's JS bundle
|
||||
NSURL *packageUrl = [[NSURL alloc] initFileURLWithPath:packageFile];
|
||||
@@ -113,11 +118,11 @@ static NSString *bundleResourceName = @"main";
|
||||
#ifndef DEBUG
|
||||
isRelease = YES;
|
||||
#endif
|
||||
|
||||
|
||||
if (isRelease || ![binaryAppVersion isEqualToString:packageAppVersion]) {
|
||||
[CodePush clearUpdates];
|
||||
}
|
||||
|
||||
|
||||
NSLog(logMessageFormat, binaryBundleURL);
|
||||
isRunningBinaryVersion = YES;
|
||||
return binaryBundleURL;
|
||||
@@ -171,6 +176,31 @@ static NSString *bundleResourceName = @"main";
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
@synthesize methodQueue = _methodQueue;
|
||||
@synthesize pauseCallback = _pauseCallback;
|
||||
@synthesize paused = _paused;
|
||||
|
||||
/*
|
||||
* This method is used to clear updates that are installed
|
||||
* under a different app version and hence don't apply anymore,
|
||||
* during a debug run configuration and when the bridge is
|
||||
* running the JS bundle from the dev server.
|
||||
*/
|
||||
- (void)clearDebugUpdates
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([_bridge.bundleURL.scheme hasPrefix:@"http"]) {
|
||||
NSError *error;
|
||||
NSString *binaryAppVersion = [[CodePushConfig current] appVersion];
|
||||
NSDictionary *currentPackageMetadata = [CodePushPackage getCurrentPackage:&error];
|
||||
if (currentPackageMetadata) {
|
||||
NSString *packageAppVersion = [currentPackageMetadata objectForKey:AppVersionKey];
|
||||
if (![binaryAppVersion isEqualToString:packageAppVersion]) {
|
||||
[CodePush clearUpdates];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is used by the React Native bridge to allow
|
||||
@@ -180,12 +210,16 @@ static NSString *bundleResourceName = @"main";
|
||||
*/
|
||||
- (NSDictionary *)constantsToExport
|
||||
{
|
||||
// Export the values of the CodePushInstallMode enum
|
||||
// so that the script-side can easily stay in sync
|
||||
// Export the values of the CodePushInstallMode and CodePushUpdateState
|
||||
// enums so that the script-side can easily stay in sync
|
||||
return @{
|
||||
@"codePushInstallModeOnNextRestart":@(CodePushInstallModeOnNextRestart),
|
||||
@"codePushInstallModeImmediate": @(CodePushInstallModeImmediate),
|
||||
@"codePushInstallModeOnNextResume": @(CodePushInstallModeOnNextResume)
|
||||
@"codePushInstallModeOnNextResume": @(CodePushInstallModeOnNextResume),
|
||||
|
||||
@"codePushUpdateStateRunning": @(CodePushUpdateStateRunning),
|
||||
@"codePushUpdateStatePending": @(CodePushUpdateStatePending),
|
||||
@"codePushUpdateStateLatest": @(CodePushUpdateStateLatest)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -196,6 +230,17 @@ static NSString *bundleResourceName = @"main";
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)dispatchDownloadProgressEvent
|
||||
{
|
||||
// Notify the script-side about the progress
|
||||
[self.bridge.eventDispatcher
|
||||
sendDeviceEventWithName:@"CodePushDownloadProgress"
|
||||
body:@{
|
||||
@"totalBytes":[NSNumber numberWithLongLong:_latestExpectedContentLength],
|
||||
@"receivedBytes":[NSNumber numberWithLongLong:_latestReceivedConentLength]
|
||||
}];
|
||||
}
|
||||
|
||||
/*
|
||||
* This method ensures that the app was packaged with a JS bundle
|
||||
* file, and if not, it throws the appropriate exception.
|
||||
@@ -204,23 +249,23 @@ static NSString *bundleResourceName = @"main";
|
||||
{
|
||||
if (![self binaryBundleURL]) {
|
||||
NSString *errorMessage;
|
||||
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
errorMessage = @"React Native doesn't generate your app's JS bundle by default when deploying to the simulator. "
|
||||
"If you'd like to test CodePush using the simulator, you can do one of three things depending on your React "
|
||||
"Native version and/or preferred workflow:\n\n"
|
||||
|
||||
|
||||
"1. Update your AppDelegate.m file to load the JS bundle from the packager instead of from CodePush. "
|
||||
"You can still test your CodePush update experience using this workflow (debug builds only).\n\n"
|
||||
|
||||
|
||||
"2. Force the JS bundle to be generated in simulator builds by removing the if block that echoes "
|
||||
"\"Skipping bundling for Simulator platform\" in the \"node_modules/react-native/packager/react-native-xcode.sh\" file.\n\n"
|
||||
|
||||
|
||||
"3. Deploy a release build to the simulator, which unlike debug builds, will generate the JS bundle (React Native >=0.22.0 only).";
|
||||
#else
|
||||
errorMessage = [NSString stringWithFormat:@"The specified JS bundle file wasn't found within the app's binary. Is \"%@\" the correct file name?", [bundleResourceName stringByAppendingPathExtension:bundleResourceExtension]];
|
||||
#endif
|
||||
|
||||
|
||||
RCTFatal([CodePushErrorUtils errorWithMessage:errorMessage]);
|
||||
}
|
||||
}
|
||||
@@ -228,11 +273,11 @@ static NSString *bundleResourceName = @"main";
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
|
||||
if (self) {
|
||||
[self initializeUpdateAfterRestart];
|
||||
}
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -243,6 +288,10 @@ static NSString *bundleResourceName = @"main";
|
||||
*/
|
||||
- (void)initializeUpdateAfterRestart
|
||||
{
|
||||
#ifdef DEBUG
|
||||
[self clearDebugUpdates];
|
||||
#endif
|
||||
_paused = YES;
|
||||
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
|
||||
NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
|
||||
if (pendingUpdate) {
|
||||
@@ -287,7 +336,7 @@ static NSString *bundleResourceName = @"main";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
@@ -301,13 +350,13 @@ static NSString *bundleResourceName = @"main";
|
||||
{
|
||||
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
|
||||
NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
|
||||
|
||||
|
||||
// If there is a pending update whose "state" isn't loading, then we consider it "pending".
|
||||
// Additionally, if a specific hash was provided, we ensure it matches that of the pending update.
|
||||
BOOL updateIsPending = pendingUpdate &&
|
||||
[pendingUpdate[PendingUpdateIsLoadingKey] boolValue] == NO &&
|
||||
(!packageHash || [pendingUpdate[PendingUpdateHashKey] isEqualToString:packageHash]);
|
||||
|
||||
|
||||
return updateIsPending;
|
||||
}
|
||||
|
||||
@@ -328,7 +377,7 @@ static NSString *bundleResourceName = @"main";
|
||||
if ([CodePush isUsingTestConfiguration] || ![_bridge.bundleURL.scheme hasPrefix:@"http"]) {
|
||||
[_bridge setValue:[CodePush bundleURL] forKey:@"bundleURL"];
|
||||
}
|
||||
|
||||
|
||||
[_bridge reload];
|
||||
});
|
||||
}
|
||||
@@ -344,10 +393,10 @@ static NSString *bundleResourceName = @"main";
|
||||
{
|
||||
NSError *error;
|
||||
NSDictionary *failedPackage = [CodePushPackage getCurrentPackage:&error];
|
||||
|
||||
|
||||
// Write the current package's metadata to the "failed list"
|
||||
[self saveFailedUpdate:failedPackage];
|
||||
|
||||
|
||||
// Rollback to the previous version and de-register the new update
|
||||
[CodePushPackage rollbackPackage];
|
||||
[CodePush removePendingUpdate];
|
||||
@@ -370,7 +419,7 @@ static NSString *bundleResourceName = @"main";
|
||||
// objects, regardless if you stored something mutable.
|
||||
failedUpdates = [failedUpdates mutableCopy];
|
||||
}
|
||||
|
||||
|
||||
[failedUpdates addObject:failedPackage];
|
||||
[preferences setObject:failedUpdates forKey:FailedUpdatesKey];
|
||||
[preferences synchronize];
|
||||
@@ -412,7 +461,7 @@ static NSString *bundleResourceName = @"main";
|
||||
NSDictionary *pendingUpdate = [[NSDictionary alloc] initWithObjectsAndKeys:
|
||||
packageHash,PendingUpdateHashKey,
|
||||
[NSNumber numberWithBool:isLoading],PendingUpdateIsLoadingKey, nil];
|
||||
|
||||
|
||||
[preferences setObject:pendingUpdate forKey:PendingUpdateKey];
|
||||
[preferences synchronize];
|
||||
}
|
||||
@@ -425,7 +474,11 @@ static NSString *bundleResourceName = @"main";
|
||||
{
|
||||
// Determine how long the app was in the background and ensure
|
||||
// that it meets the minimum duration amount of time.
|
||||
int durationInBackground = [[NSDate date] timeIntervalSinceDate:_lastResignedDate];
|
||||
int durationInBackground = 0;
|
||||
if (_lastResignedDate) {
|
||||
durationInBackground = [[NSDate date] timeIntervalSinceDate:_lastResignedDate];
|
||||
}
|
||||
|
||||
if (durationInBackground >= _minimumBackgroundDuration) {
|
||||
[self loadBundle];
|
||||
}
|
||||
@@ -444,6 +497,7 @@ static NSString *bundleResourceName = @"main";
|
||||
* This is native-side of the RemotePackage.download method
|
||||
*/
|
||||
RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
|
||||
notifyProgress:(BOOL)notifyProgress
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
@@ -454,43 +508,52 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
|
||||
forKey:BinaryBundleDateKey];
|
||||
}
|
||||
|
||||
if (notifyProgress) {
|
||||
// Set up and unpause the frame observer so that it can emit
|
||||
// progress events every frame if the progress is updated.
|
||||
_didUpdateProgress = NO;
|
||||
_paused = NO;
|
||||
}
|
||||
|
||||
[CodePushPackage
|
||||
downloadPackage:mutableUpdatePackage
|
||||
expectedBundleFileName:[bundleResourceName stringByAppendingPathExtension:bundleResourceExtension]
|
||||
operationQueue:_methodQueue
|
||||
// The download is progressing forward
|
||||
progressCallback:^(long long expectedContentLength, long long receivedContentLength) {
|
||||
dispatch_async(_methodQueue, ^{
|
||||
// Notify the script-side about the progress
|
||||
[self.bridge.eventDispatcher
|
||||
sendDeviceEventWithName:@"CodePushDownloadProgress"
|
||||
body:@{
|
||||
@"totalBytes":[NSNumber numberWithLongLong:expectedContentLength],
|
||||
@"receivedBytes":[NSNumber numberWithLongLong:receivedContentLength]
|
||||
}];
|
||||
});
|
||||
// Update the download progress so that the frame observer can notify the JS side
|
||||
_latestExpectedContentLength = expectedContentLength;
|
||||
_latestReceivedConentLength = receivedContentLength;
|
||||
_didUpdateProgress = YES;
|
||||
|
||||
// If the download is completed, stop observing frame
|
||||
// updates and synchronously send the last event.
|
||||
if (expectedContentLength == receivedContentLength) {
|
||||
_didUpdateProgress = NO;
|
||||
_paused = YES;
|
||||
[self dispatchDownloadProgressEvent];
|
||||
}
|
||||
}
|
||||
// The download completed
|
||||
doneCallback:^{
|
||||
dispatch_async(_methodQueue, ^{
|
||||
NSError *err;
|
||||
NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];
|
||||
|
||||
if (err) {
|
||||
return reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
|
||||
}
|
||||
|
||||
resolve(newPackage);
|
||||
});
|
||||
NSError *err;
|
||||
NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];
|
||||
|
||||
if (err) {
|
||||
return reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
|
||||
}
|
||||
resolve(newPackage);
|
||||
}
|
||||
// The download failed
|
||||
failCallback:^(NSError *err) {
|
||||
dispatch_async(_methodQueue, ^{
|
||||
if ([CodePushErrorUtils isCodePushError:err]) {
|
||||
[self saveFailedUpdate:mutableUpdatePackage];
|
||||
}
|
||||
|
||||
reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
|
||||
});
|
||||
if ([CodePushErrorUtils isCodePushError:err]) {
|
||||
[self saveFailedUpdate:mutableUpdatePackage];
|
||||
}
|
||||
|
||||
// Stop observing frame updates if the download fails.
|
||||
_didUpdateProgress = NO;
|
||||
_paused = YES;
|
||||
reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -513,7 +576,7 @@ RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve
|
||||
resolve(configuration);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (binaryHash == nil) {
|
||||
// The hash was not generated either due to a previous unknown error or the fact that
|
||||
// the React Native assets were not bundled in the binary (e.g. during dev/simulator)
|
||||
@@ -521,46 +584,62 @@ RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve
|
||||
resolve(configuration);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
NSMutableDictionary *mutableConfiguration = [configuration mutableCopy];
|
||||
[mutableConfiguration setObject:binaryHash forKey:PackageHashKey];
|
||||
resolve(mutableConfiguration);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
resolve(configuration);
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is the native side of the CodePush.getCurrentPackage method.
|
||||
* This method is the native side of the CodePush.getUpdateMetadata method.
|
||||
*/
|
||||
RCT_EXPORT_METHOD(getCurrentPackage:(RCTPromiseResolveBlock)resolve
|
||||
RCT_EXPORT_METHOD(getUpdateMetadata:(CodePushUpdateState)updateState
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
NSError *error;
|
||||
NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy];
|
||||
|
||||
|
||||
if (error) {
|
||||
reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
|
||||
return;
|
||||
return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
|
||||
} else if (package == nil) {
|
||||
// The app hasn't downloaded any CodePush updates yet,
|
||||
// so we simply return nil regardless if the user
|
||||
// wanted to retrieve the pending or running update.
|
||||
return resolve(nil);
|
||||
}
|
||||
|
||||
// We have a CodePush update, so let's see if it's currently in a pending state.
|
||||
BOOL currentUpdateIsPending = [self isPendingUpdate:[package objectForKey:PackageHashKey]];
|
||||
|
||||
if (updateState == CodePushUpdateStatePending && !currentUpdateIsPending) {
|
||||
// The caller wanted a pending update
|
||||
// but there isn't currently one.
|
||||
resolve(nil);
|
||||
return;
|
||||
} else if (updateState == CodePushUpdateStateRunning && currentUpdateIsPending) {
|
||||
// The caller wants the running update, but the current
|
||||
// one is pending, so we need to grab the previous.
|
||||
resolve([CodePushPackage getPreviousPackage:&error]);
|
||||
} 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.
|
||||
[package setObject:@(YES) forKey:@"_isDebugOnly"];
|
||||
}
|
||||
|
||||
// Enable differentiating pending vs. non-pending updates
|
||||
[package setObject:@(currentUpdateIsPending) forKey:PackageIsPendingKey];
|
||||
resolve(package);
|
||||
}
|
||||
|
||||
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.
|
||||
[package setObject:@(YES) forKey:@"_isDebugOnly"];
|
||||
}
|
||||
|
||||
// Add the "isPending" virtual property to the package at this point, so that
|
||||
// the script-side doesn't need to immediately call back into native to populate it.
|
||||
BOOL isPendingUpdate = [self isPendingUpdate:[package objectForKey:PackageHashKey]];
|
||||
[package setObject:@(isPendingUpdate) forKey:PackageIsPendingKey];
|
||||
|
||||
resolve(package);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -576,16 +655,16 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage
|
||||
[CodePushPackage installPackage:updatePackage
|
||||
removePendingUpdate:[self isPendingUpdate:nil]
|
||||
error:&error];
|
||||
|
||||
|
||||
if (error) {
|
||||
reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
|
||||
} else {
|
||||
[self savePendingUpdate:updatePackage[PackageHashKey]
|
||||
isLoading:NO];
|
||||
|
||||
|
||||
if (installMode == CodePushInstallModeOnNextResume) {
|
||||
_minimumBackgroundDuration = minimumBackgroundDuration;
|
||||
|
||||
|
||||
if (!_hasResumeListener) {
|
||||
// Ensure we do not add the listener twice.
|
||||
// Register for app resume notifications so that we
|
||||
@@ -594,16 +673,16 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage
|
||||
selector:@selector(applicationWillEnterForeground)
|
||||
name:UIApplicationWillEnterForegroundNotification
|
||||
object:[UIApplication sharedApplication]];
|
||||
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationWillResignActive)
|
||||
name:UIApplicationWillResignActiveNotification
|
||||
object:[UIApplication sharedApplication]];
|
||||
|
||||
|
||||
_hasResumeListener = YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Signal to JS that the update has been applied.
|
||||
resolve(nil);
|
||||
}
|
||||
@@ -634,7 +713,7 @@ RCT_EXPORT_METHOD(isFirstRun:(NSString *)packageHash
|
||||
&& nil != packageHash
|
||||
&& [packageHash length] > 0
|
||||
&& [packageHash isEqualToString:[CodePushPackage getCurrentPackageHash:&error]];
|
||||
|
||||
|
||||
resolve(@(isFirstRun));
|
||||
}
|
||||
|
||||
@@ -681,7 +760,7 @@ RCT_EXPORT_METHOD(downloadAndReplaceCurrentBundle:(NSString *)remoteBundleUrl)
|
||||
*/
|
||||
RCT_EXPORT_METHOD(getNewStatusReport:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
{
|
||||
if (needToReportRollback) {
|
||||
needToReportRollback = NO;
|
||||
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
|
||||
@@ -705,8 +784,20 @@ RCT_EXPORT_METHOD(getNewStatusReport:(RCTPromiseResolveBlock)resolve
|
||||
resolve([CodePushTelemetryManager getBinaryUpdateReport:appVersion]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
resolve(nil);
|
||||
}
|
||||
|
||||
#pragma mark - RCTFrameUpdateObserver Methods
|
||||
|
||||
- (void)didUpdateFrame:(RCTFrameUpdate *)update
|
||||
{
|
||||
if (!_didUpdateProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self dispatchDownloadProgressEvent];
|
||||
_didUpdateProgress = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -27,12 +27,12 @@ static NSString * const ServerURLConfigKey = @"serverUrl";
|
||||
{
|
||||
self = [super init];
|
||||
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
|
||||
|
||||
|
||||
NSString *appVersion = [infoDictionary objectForKey:@"CFBundleShortVersionString"];
|
||||
NSString *buildVersion = [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey];
|
||||
NSString *deploymentKey = [infoDictionary objectForKey:@"CodePushDeploymentKey"];
|
||||
NSString *serverURL = [infoDictionary objectForKey:@"CodePushServerURL"];
|
||||
|
||||
|
||||
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
|
||||
NSString *clientUniqueId = [userDefaults stringForKey:ClientUniqueIDConfigKey];
|
||||
if (clientUniqueId == nil) {
|
||||
@@ -40,11 +40,11 @@ static NSString * const ServerURLConfigKey = @"serverUrl";
|
||||
[userDefaults setObject:clientUniqueId forKey:ClientUniqueIDConfigKey];
|
||||
[userDefaults synchronize];
|
||||
}
|
||||
|
||||
|
||||
if (!serverURL) {
|
||||
serverURL = @"https://codepush.azurewebsites.net/";
|
||||
}
|
||||
|
||||
|
||||
_configDictionary = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
|
||||
appVersion,AppVersionConfigKey,
|
||||
buildVersion,BuildVdersionConfigKey,
|
||||
@@ -52,7 +52,7 @@ static NSString * const ServerURLConfigKey = @"serverUrl";
|
||||
clientUniqueId,ClientUniqueIDConfigKey,
|
||||
deploymentKey,DeploymentKeyConfigKey,
|
||||
nil];
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
}
|
||||
|
||||
- (id)init:(NSString *)downloadFilePath
|
||||
operationQueue:(dispatch_queue_t)operationQueue
|
||||
progressCallback:(void (^)(long long, long long))progressCallback
|
||||
doneCallback:(void (^)(BOOL))doneCallback
|
||||
failCallback:(void (^)(NSError *err))failCallback {
|
||||
self.outputFileStream = [NSOutputStream outputStreamToFileAtPath:downloadFilePath
|
||||
append:NO];
|
||||
self.receivedContentLength = 0;
|
||||
self.operationQueue = operationQueue;
|
||||
self.progressCallback = progressCallback;
|
||||
self.doneCallback = doneCallback;
|
||||
self.failCallback = failCallback;
|
||||
@@ -22,12 +24,12 @@ failCallback:(void (^)(NSError *err))failCallback {
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]
|
||||
cachePolicy:NSURLRequestUseProtocolCachePolicy
|
||||
timeoutInterval:60.0];
|
||||
|
||||
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
|
||||
delegate:self
|
||||
startImmediately:NO];
|
||||
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop]
|
||||
forMode:NSDefaultRunLoopMode];
|
||||
NSOperationQueue *delegateQueue = [NSOperationQueue new];
|
||||
delegateQueue.underlyingQueue = self.operationQueue;
|
||||
[connection setDelegateQueue:delegateQueue];
|
||||
[connection start];
|
||||
}
|
||||
|
||||
@@ -51,31 +53,31 @@ failCallback:(void (^)(NSError *err))failCallback {
|
||||
if (headerOffset >= 4) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
const char *bytes = [data bytes];
|
||||
_header[headerOffset] = bytes[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.receivedContentLength = self.receivedContentLength + [data length];
|
||||
|
||||
|
||||
NSInteger bytesLeft = [data length];
|
||||
|
||||
|
||||
do {
|
||||
NSInteger bytesWritten = [self.outputFileStream write:[data bytes]
|
||||
maxLength:bytesLeft];
|
||||
if (bytesWritten == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
bytesLeft -= bytesWritten;
|
||||
} while (bytesLeft > 0);
|
||||
|
||||
|
||||
self.progressCallback(self.expectedContentLength, self.receivedContentLength);
|
||||
|
||||
|
||||
// bytesLeft should not be negative.
|
||||
assert(bytesLeft >= 0);
|
||||
|
||||
|
||||
if (bytesLeft) {
|
||||
[self.outputFileStream close];
|
||||
[connection cancel];
|
||||
@@ -90,12 +92,15 @@ failCallback:(void (^)(NSError *err))failCallback {
|
||||
}
|
||||
|
||||
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
|
||||
// We should have received all of the bytes if this is called.
|
||||
assert(self.receivedContentLength == self.expectedContentLength);
|
||||
|
||||
// expectedContentLength might be -1 when NSURLConnection don't know the length(e.g. response encode with gzip)
|
||||
if (self.expectedContentLength > 0) {
|
||||
// We should have received all of the bytes if this is called.
|
||||
assert(self.receivedContentLength == self.expectedContentLength);
|
||||
}
|
||||
|
||||
[self.outputFileStream close];
|
||||
BOOL isZip = _header[0] == 'P' && _header[1] == 'K' && _header[2] == 3 && _header[3] == 4;
|
||||
self.doneCallback(isZip);
|
||||
}
|
||||
|
||||
@end
|
||||
@end
|
||||
|
||||
@@ -10,6 +10,7 @@ static NSString *const DownloadFileName = @"download.zip";
|
||||
static NSString *const RelativeBundlePathKey = @"bundlePath";
|
||||
static NSString *const StatusFile = @"codepush.json";
|
||||
static NSString *const UpdateBundleFileName = @"app.jsbundle";
|
||||
static NSString *const UpdateMetadataFileName = @"app.json";
|
||||
static NSString *const UnzippedFolderName = @"unzipped";
|
||||
|
||||
#pragma mark - Public methods
|
||||
@@ -17,7 +18,6 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
+ (void)clearUpdates
|
||||
{
|
||||
[[NSFileManager defaultManager] removeItemAtPath:[self getCodePushPath] error:nil];
|
||||
[[NSFileManager defaultManager] removeItemAtPath:[self getStatusFilePath] error:nil];
|
||||
}
|
||||
|
||||
+ (void)downloadAndReplaceCurrentBundle:(NSString *)remoteBundleUrl
|
||||
@@ -41,13 +41,14 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
|
||||
+ (void)downloadPackage:(NSDictionary *)updatePackage
|
||||
expectedBundleFileName:(NSString *)expectedBundleFileName
|
||||
operationQueue:(dispatch_queue_t)operationQueue
|
||||
progressCallback:(void (^)(long long, long long))progressCallback
|
||||
doneCallback:(void (^)())doneCallback
|
||||
failCallback:(void (^)(NSError *err))failCallback
|
||||
{
|
||||
NSString *newUpdateHash = updatePackage[@"packageHash"];
|
||||
NSString *newUpdateFolderPath = [self getPackageFolderPath:newUpdateHash];
|
||||
NSString *newUpdateMetadataPath = [newUpdateFolderPath stringByAppendingPathComponent:@"app.json"];
|
||||
NSString *newUpdateMetadataPath = [newUpdateFolderPath stringByAppendingPathComponent:UpdateMetadataFileName];
|
||||
NSError *error;
|
||||
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:newUpdateFolderPath]) {
|
||||
@@ -60,6 +61,11 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:&error];
|
||||
|
||||
// Ensure that none of the CodePush updates we store on disk are
|
||||
// ever included in the end users iTunes and/or iCloud backups
|
||||
NSURL *codePushURL = [NSURL fileURLWithPath:[self getCodePushPath]];
|
||||
[codePushURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@@ -71,6 +77,7 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
|
||||
CodePushDownloadHandler *downloadHandler = [[CodePushDownloadHandler alloc]
|
||||
init:downloadFilePath
|
||||
operationQueue:operationQueue
|
||||
progressCallback:progressCallback
|
||||
doneCallback:^(BOOL isZip) {
|
||||
NSError *error = nil;
|
||||
@@ -290,27 +297,12 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
|
||||
+ (NSDictionary *)getCurrentPackage:(NSError **)error
|
||||
{
|
||||
NSString *folderPath = [CodePushPackage getCurrentPackageFolderPath:error];
|
||||
if (!*error) {
|
||||
if (!folderPath) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *packagePath = [folderPath stringByAppendingPathComponent:@"app.json"];
|
||||
NSString *content = [NSString stringWithContentsOfFile:packagePath
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:error];
|
||||
if (!*error) {
|
||||
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSDictionary* jsonDict = [NSJSONSerialization JSONObjectWithData:data
|
||||
options:kNilOptions
|
||||
error:error];
|
||||
|
||||
return jsonDict;
|
||||
}
|
||||
NSString *packageHash = [CodePushPackage getCurrentPackageHash:error];
|
||||
if (*error || !packageHash) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return nil;
|
||||
return [CodePushPackage getPackage:packageHash error:error];
|
||||
}
|
||||
|
||||
+ (NSString *)getCurrentPackageBundlePath:(NSError **)error
|
||||
@@ -338,8 +330,8 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
+ (NSString *)getCurrentPackageHash:(NSError **)error
|
||||
{
|
||||
NSDictionary *info = [self getCurrentPackageInfo:error];
|
||||
if (*error) {
|
||||
return NULL;
|
||||
if (*error || !info) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return info[@"currentPackage"];
|
||||
@@ -373,7 +365,7 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:error];
|
||||
if (*error) {
|
||||
return NULL;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
|
||||
@@ -381,7 +373,7 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
options:kNilOptions
|
||||
error:error];
|
||||
if (*error) {
|
||||
return NULL;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [json mutableCopy];
|
||||
@@ -395,27 +387,25 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
+ (NSDictionary *)getPackage:(NSString *)packageHash
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSString *folderPath = [self getPackageFolderPath:packageHash];
|
||||
NSString *updateDirectoryPath = [self getPackageFolderPath:packageHash];
|
||||
NSString *updateMetadataFilePath = [updateDirectoryPath stringByAppendingPathComponent:UpdateMetadataFileName];
|
||||
|
||||
if (!folderPath) {
|
||||
return [NSDictionary dictionary];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:updateMetadataFilePath]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *packageFilePath = [folderPath stringByAppendingPathComponent:@"app.json"];
|
||||
NSString *updateMetadataString = [NSString stringWithContentsOfFile:updateMetadataFilePath
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:error];
|
||||
|
||||
NSString *content = [NSString stringWithContentsOfFile:packageFilePath
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:error];
|
||||
if (!*error) {
|
||||
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSDictionary* jsonDict = [NSJSONSerialization JSONObjectWithData:data
|
||||
options:kNilOptions
|
||||
error:error];
|
||||
|
||||
return jsonDict;
|
||||
if (*error) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
NSData *updateMetadata = [updateMetadataString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [NSJSONSerialization JSONObjectWithData:updateMetadata
|
||||
options:kNilOptions
|
||||
error:error];
|
||||
}
|
||||
|
||||
+ (NSString *)getPackageFolderPath:(NSString *)packageHash
|
||||
@@ -423,11 +413,21 @@ static NSString *const UnzippedFolderName = @"unzipped";
|
||||
return [[self getCodePushPath] stringByAppendingPathComponent:packageHash];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)getPreviousPackage:(NSError **)error
|
||||
{
|
||||
NSString *packageHash = [self getPreviousPackageHash:error];
|
||||
if (*error || !packageHash) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [CodePushPackage getPackage:packageHash error:error];
|
||||
}
|
||||
|
||||
+ (NSString *)getPreviousPackageHash:(NSError **)error
|
||||
{
|
||||
NSDictionary *info = [self getCurrentPackageInfo:error];
|
||||
if (*error) {
|
||||
return NULL;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return info[@"previousPackage"];
|
||||
|
||||
15
ios/CodePush/RCTConvert+CodePushUpdateState.m
Normal file
15
ios/CodePush/RCTConvert+CodePushUpdateState.m
Normal file
@@ -0,0 +1,15 @@
|
||||
#import "CodePush.h"
|
||||
#import "RCTConvert.h"
|
||||
|
||||
// Extending the RCTConvert class allows the React Native
|
||||
// bridge to handle args of type "CodePushUpdateState"
|
||||
@implementation RCTConvert (CodePushUpdateState)
|
||||
|
||||
RCT_ENUM_CONVERTER(CodePushUpdateState, (@{ @"codePushUpdateStateRunning": @(CodePushUpdateStateRunning),
|
||||
@"codePushUpdateStatePending": @(CodePushUpdateStatePending),
|
||||
@"codePushUpdateStateLatest": @(CodePushUpdateStateLatest)
|
||||
}),
|
||||
CodePushUpdateStateRunning, // Default enum value
|
||||
integerValue)
|
||||
|
||||
@end
|
||||
@@ -14,24 +14,24 @@ module.exports = (NativeCodePush) => {
|
||||
|
||||
let downloadProgressSubscription;
|
||||
if (downloadProgressCallback) {
|
||||
// Use event subscription to obtain download progress.
|
||||
// Use event subscription to obtain download progress.
|
||||
downloadProgressSubscription = DeviceEventEmitter.addListener(
|
||||
"CodePushDownloadProgress",
|
||||
downloadProgressCallback
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Use the downloaded package info. Native code will save the package info
|
||||
// so that the client knows what the current package version is.
|
||||
try {
|
||||
const downloadedPackage = await NativeCodePush.downloadUpdate(this);
|
||||
try {
|
||||
const downloadedPackage = await NativeCodePush.downloadUpdate(this, !!downloadProgressCallback);
|
||||
reportStatusDownload && reportStatusDownload(this);
|
||||
return { ...downloadedPackage, ...local };
|
||||
} finally {
|
||||
downloadProgressSubscription && downloadProgressSubscription.remove();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
isPending: false // A remote package could never be in a pending state
|
||||
};
|
||||
};
|
||||
@@ -47,7 +47,7 @@ module.exports = (NativeCodePush) => {
|
||||
localPackage.isPending = true; // Mark the package as pending since it hasn't been applied yet
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
isPending: false // A local package wouldn't be pending until it was installed
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native-code-push",
|
||||
"version": "1.10.1-beta",
|
||||
"version": "1.10.6-beta",
|
||||
"description": "React Native plugin for the CodePush service",
|
||||
"main": "CodePush.js",
|
||||
"homepage": "https://microsoft.github.io/code-push",
|
||||
@@ -23,12 +23,12 @@
|
||||
"packageInstance": "new CodePush(${androidDeploymentKey}, this, BuildConfig.DEBUG)"
|
||||
},
|
||||
"ios": {
|
||||
"sharedLibraries": ["libz"]
|
||||
"sharedLibraries": ["libz"]
|
||||
},
|
||||
"params": [{
|
||||
"type": "input",
|
||||
"name": "androidDeploymentKey",
|
||||
"message": "What is your CodePush deployment key for Android (hit <ENTER> to ignore)"
|
||||
"message": "What is your CodePush deployment key for Android (hit <ENTER> to ignore)"
|
||||
}]
|
||||
}
|
||||
}
|
||||
38
react-native-code-push.d.ts
vendored
38
react-native-code-push.d.ts
vendored
@@ -193,16 +193,18 @@ declare namespace CodePush {
|
||||
* @param deploymentKey The deployment key to use to query the CodePush server for an update.
|
||||
*/
|
||||
function checkForUpdate(deploymentKey?: string): ReactNativePromise<RemotePackage>;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the metadata about the currently installed update (e.g. description, installation time, size).
|
||||
* Retrieves the metadata for an installed update (e.g. description, mandatory).
|
||||
*
|
||||
* @param updateState The state of the update you want to retrieve the metadata for. Defaults to UpdateState.RUNNING.
|
||||
*/
|
||||
function getCurrentPackage(): ReactNativePromise<LocalPackage>;
|
||||
function getUpdateMetadata(updateState?: UpdateState) : ReactNativePromise<LocalPackage>;
|
||||
|
||||
/**
|
||||
* Notifies the CodePush runtime that an installed update is considered successful.
|
||||
*/
|
||||
function notifyApplicationReady(): ReactNativePromise<void>;
|
||||
function notifyAppReady(): ReactNativePromise<void>;
|
||||
|
||||
/**
|
||||
* Immediately restarts the app.
|
||||
@@ -218,7 +220,7 @@ declare namespace CodePush {
|
||||
* @param syncStatusChangedCallback An optional callback that allows tracking the status of the sync operation, as opposed to simply checking the resolved state via the returned Promise.
|
||||
* @param downloadProgressCallback An optional callback that allows tracking the progress of an update while it is being downloaded.
|
||||
*/
|
||||
function sync(options?: SyncOptions, syncStatusChangedCallback?: SyncStatusChangedCallback, downloadProgressCallback?: DowloadProgressCallback): __React.Promise<SyncStatus>;
|
||||
function sync(options?: SyncOptions, syncStatusChangedCallback?: SyncStatusChangedCallback, downloadProgressCallback?: DowloadProgressCallback): ReactNativePromise<SyncStatus>;
|
||||
|
||||
/**
|
||||
* Indicates when you would like an installed update to actually be applied.
|
||||
@@ -241,6 +243,9 @@ declare namespace CodePush {
|
||||
ON_NEXT_RESUME
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the current status of a sync operation.
|
||||
*/
|
||||
enum SyncStatus {
|
||||
/**
|
||||
* The CodePush server is being queried for an update.
|
||||
@@ -291,6 +296,29 @@ declare namespace CodePush {
|
||||
*/
|
||||
UNKNOWN_ERROR
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the state that an update is currently in.
|
||||
*/
|
||||
enum UpdateState {
|
||||
/**
|
||||
* Indicates that an update represents the
|
||||
* version of the app that is currently running.
|
||||
*/
|
||||
RUNNING,
|
||||
|
||||
/**
|
||||
* Indicates than an update has been installed, but the
|
||||
* app hasn't been restarted yet in order to apply it.
|
||||
*/
|
||||
PENDING,
|
||||
|
||||
/**
|
||||
* Indicates than an update represents the latest available
|
||||
* release, and can be either currently running or pending.
|
||||
*/
|
||||
LATEST
|
||||
}
|
||||
}
|
||||
|
||||
declare module "react-native-code-push" {
|
||||
|
||||
@@ -21,7 +21,7 @@ module.exports = {
|
||||
headers: headers,
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
|
||||
const statusCode = response.status;
|
||||
const body = await response.text();
|
||||
callback(null, { statusCode, body });
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace CodePush.ReactNative
|
||||
private readonly string DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress";
|
||||
private readonly string FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES";
|
||||
private static readonly string FILE_BUNDLE_PREFIX = "ms-appdata:///local";
|
||||
private readonly string PACKAGE_HASH_KEY = "packageHash";
|
||||
private static readonly string PACKAGE_HASH_KEY = "packageHash";
|
||||
private readonly string PENDING_UPDATE_HASH_KEY = "hash";
|
||||
private readonly string PENDING_UPDATE_IS_LOADING_KEY = "isLoading";
|
||||
private readonly string PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE";
|
||||
@@ -397,6 +397,9 @@ namespace CodePush.ReactNative
|
||||
constants["codePushInstallModeImmediate"] = CodePushInstallMode.IMMEDIATE;
|
||||
constants["codePushInstallModeOnNextRestart"] = CodePushInstallMode.ON_NEXT_RESTART;
|
||||
constants["codePushInstallModeOnNextResume"] = CodePushInstallMode.ON_NEXT_RESUME;
|
||||
constants["codePushUpdateStateRunning"] = CodePushUpdateState.RUNNING;
|
||||
constants["codePushUpdateStatePending"] = CodePushUpdateState.PENDING;
|
||||
constants["codePushUpdateStateLatest"] = CodePushUpdateState.LATEST;
|
||||
return constants;
|
||||
}
|
||||
}
|
||||
@@ -433,7 +436,7 @@ namespace CodePush.ReactNative
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void downloadUpdate(JObject updatePackage, IPromise promise)
|
||||
public void downloadUpdate(JObject updatePackage, bool notifyProgress, IPromise promise)
|
||||
{
|
||||
Action downloadAction = async () =>
|
||||
{
|
||||
@@ -446,6 +449,11 @@ namespace CodePush.ReactNative
|
||||
new Progress<HttpProgress>(
|
||||
(HttpProgress progress) =>
|
||||
{
|
||||
if (!notifyProgress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
JObject downloadProgress = new JObject();
|
||||
downloadProgress["totalBytes"] = progress.TotalBytesToReceive;
|
||||
downloadProgress["receivedBytes"] = progress.BytesReceived;
|
||||
@@ -456,7 +464,7 @@ namespace CodePush.ReactNative
|
||||
)
|
||||
);
|
||||
|
||||
JObject newPackage = await codePush.codePushPackage.GetPackage((string)updatePackage[codePush.PACKAGE_HASH_KEY]);
|
||||
JObject newPackage = await codePush.codePushPackage.GetPackage((string)updatePackage[PACKAGE_HASH_KEY]);
|
||||
promise.Resolve(newPackage);
|
||||
}
|
||||
catch (CodePushInvalidUpdateException e)
|
||||
@@ -496,10 +504,10 @@ namespace CodePush.ReactNative
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void getCurrentPackage(IPromise promise)
|
||||
public void getUpdateMetadata(int updateState, IPromise promise)
|
||||
{
|
||||
Action getCurrentPackageAction = async () =>
|
||||
{
|
||||
{
|
||||
JObject currentPackage = await codePush.codePushPackage.GetCurrentPackage();
|
||||
if (currentPackage == null)
|
||||
{
|
||||
@@ -507,20 +515,44 @@ namespace CodePush.ReactNative
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRunningBinaryVersion)
|
||||
bool currentUpdateIsPending = false;
|
||||
|
||||
if (currentPackage[PACKAGE_HASH_KEY] != null)
|
||||
{
|
||||
currentPackage["_isDebugOnly"] = true;
|
||||
string currentHash = (string)currentPackage[PACKAGE_HASH_KEY];
|
||||
currentUpdateIsPending = codePush.IsPendingUpdate(currentHash);
|
||||
}
|
||||
|
||||
bool isPendingUpdate = false;
|
||||
string currentHash = (string)currentPackage[codePush.PACKAGE_HASH_KEY];
|
||||
if (currentHash != null)
|
||||
if (updateState == (int)CodePushUpdateState.PENDING && !currentUpdateIsPending)
|
||||
{
|
||||
isPendingUpdate = codePush.IsPendingUpdate(currentHash);
|
||||
// The caller wanted a pending update
|
||||
// but there isn't currently one.
|
||||
promise.Resolve("");
|
||||
}
|
||||
else if (updateState == (int)CodePushUpdateState.RUNNING && currentUpdateIsPending)
|
||||
{
|
||||
// The caller wants the running update, but the current
|
||||
// one is pending, so we need to grab the previous.
|
||||
promise.Resolve(await codePush.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["_isDebugOnly"] = true;
|
||||
}
|
||||
|
||||
currentPackage["isPending"] = isPendingUpdate;
|
||||
promise.Resolve(currentPackage);
|
||||
// Enable differentiating pending vs. non-pending updates
|
||||
currentPackage["isPending"] = currentUpdateIsPending;
|
||||
promise.Resolve(currentPackage);
|
||||
}
|
||||
};
|
||||
|
||||
Context.RunOnNativeModulesQueueThread(getCurrentPackageAction);
|
||||
@@ -540,7 +572,7 @@ namespace CodePush.ReactNative
|
||||
Action installUpdateAction = async () =>
|
||||
{
|
||||
await codePush.codePushPackage.InstallPackage(updatePackage, codePush.IsPendingUpdate(null));
|
||||
string pendingHash = (string)updatePackage[codePush.PACKAGE_HASH_KEY];
|
||||
string pendingHash = (string)updatePackage[PACKAGE_HASH_KEY];
|
||||
codePush.SavePendingUpdate(pendingHash, /* isLoading */false);
|
||||
if (installMode == (int)CodePushInstallMode.ON_NEXT_RESUME)
|
||||
{
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
<Compile Include="CodePushNotInitializedException.cs" />
|
||||
<Compile Include="CodePushPackage.cs" />
|
||||
<Compile Include="CodePushUnknownException.cs" />
|
||||
<Compile Include="CodePushUpdateState.cs" />
|
||||
<Compile Include="CodePushUpdateUtils.cs" />
|
||||
<Compile Include="CodePushUtils.cs" />
|
||||
<Compile Include="FileUtils.cs" />
|
||||
|
||||
@@ -133,8 +133,22 @@ namespace CodePush.ReactNative
|
||||
|
||||
public async Task<string> GetCurrentPackageHash()
|
||||
{
|
||||
JObject metadata = await GetCurrentPackage();
|
||||
return (string)metadata[PACKAGE_HASH_KEY];
|
||||
JObject info = await GetCurrentPackageInfo();
|
||||
string currentPackageShortHash = (string)info[CURRENT_PACKAGE_KEY];
|
||||
if (currentPackageShortHash == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
JObject currentPackageMetadata = await GetPackage(currentPackageShortHash);
|
||||
if (currentPackageMetadata == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (string)currentPackageMetadata[PACKAGE_HASH_KEY];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetPreviousPackageHash()
|
||||
@@ -159,22 +173,24 @@ namespace CodePush.ReactNative
|
||||
|
||||
public async Task<JObject> GetCurrentPackage()
|
||||
{
|
||||
StorageFolder currentPackageFolder = await GetCurrentPackageFolder();
|
||||
if (currentPackageFolder == null)
|
||||
string packageHash = await GetCurrentPackageHash();
|
||||
if (packageHash == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
return await GetPackage(packageHash);
|
||||
}
|
||||
|
||||
public async Task<JObject> GetPreviousPackage()
|
||||
{
|
||||
string packageHash = await GetPreviousPackageHash();
|
||||
if (packageHash == null)
|
||||
{
|
||||
StorageFile packageFile = await currentPackageFolder.GetFileAsync(PACKAGE_FILE_NAME);
|
||||
return await CodePushUtils.GetJObjectFromFile(packageFile);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Should not happen unless the update metadata was somehow deleted.
|
||||
return null;
|
||||
}
|
||||
|
||||
return await GetPackage(packageHash);
|
||||
}
|
||||
|
||||
public async Task<JObject> GetPackage(string packageHash)
|
||||
|
||||
6
windows/CodePushUpdateState.cs
Normal file
6
windows/CodePushUpdateState.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
enum CodePushUpdateState
|
||||
{
|
||||
RUNNING = 0,
|
||||
PENDING = 1,
|
||||
LATEST = 2
|
||||
}
|
||||
Reference in New Issue
Block a user