diff --git a/CodePush.js b/CodePush.js index 4c5cb83..a4f5ff1 100644 --- a/CodePush.js +++ b/CodePush.js @@ -239,6 +239,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg ignoreFailedUpdates: true, installMode: CodePush.InstallMode.ON_NEXT_RESTART, mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE, + minimumBackgroundDuration: 0, updateDialog: null, ...options }; @@ -273,7 +274,11 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) { log("Update is installed and will be run on the next app restart."); } else { - log("Update is installed and will be run when the app next resumes."); + if (syncOptions.minimumBackgroundDuration > 0) { + log(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`); + } else { + log("Update is installed and will be run when the app next resumes."); + } } break; case CodePush.SyncStatus.UNKNOWN_ERROR: @@ -302,7 +307,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode; syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE); - await localPackage.install(resolvedInstallMode, () => { + await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => { syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED); }); diff --git a/README.md b/README.md index 6f474c3..30ab25c 100644 --- a/README.md +++ b/README.md @@ -567,6 +567,8 @@ While the `sync` method tries to make it easy to perform silent and active updat * __mandatoryInstallMode__ *(codePush.InstallMode)* - Specifies when you would like to install updates which are marked as mandatory. Defaults to `codePush.InstallMode.IMMEDIATE`. Refer to the [`InstallMode`](#installmode) enum reference for a description of the available options and what they do. +* __minimumBackgroundDuration__ *(Number)* - Specifies the minimum number of seconds that the app needs to have been in the background before restarting the app to apply the udpate. This property only applies to updates which are installed using `InstallMode.ON_NEXT_RESUME`, and can be useful for getting your update in front of end users sooner, without being too obtrusive. Defaults to `0`, which has the effect of applying the update immediately after a resume, regardless how long it was in the background. + * __updateDialog__ *(UpdateDialogOptions)* - An "options" object used to determine whether a confirmation dialog should be displayed to the end user when an update is available, and if so, what strings to use. Defaults to `null`, which has the effect of disabling the dialog completely. Setting this to any truthy value will enable the dialog with the default strings, and passing an object to this parameter allows enabling the dialog as well as overriding one or more of the default strings. Before enabling this option within an App Store-distributed app, please refer to [this note](#user-content-apple-note). The following list represents the available options and their defaults: @@ -595,10 +597,10 @@ Example Usage: // in the Info.plist file codePush.sync({ deploymentKey: "KEY" }); -// Download the update silently -// but install is on the next resume -// instead of waiting until the app is restarted -codePush.sync({ installMode: codePush.InstallMode.ON_NEXT_RESUME }); +// Download the update silently, but install it on +// the next resume, as long as at least 5 minutes +// has passed since the app was put into the background. +codePush.sync({ installMode: codePush.InstallMode.ON_NEXT_RESUME, minimumBackgroundDuration: 60 * 5 }); // Download the update silently, and install optional updates // on the next restart, but install mandatory updates on the next resume. @@ -681,7 +683,7 @@ Contains details about an update that has been downloaded locally or already ins - __packageSize__: The size of the code contained within the update, in bytes. *(Number)* ###### Methods -- __install(installMode: codePush.InstallMode = codePush.InstallMode.ON_NEXT_RESTART): Promise<void>__: Installs the update by saving it to the location on disk where the runtime expects to find the latest version of the app. The `installMode` parameter controls when the changes are actually presented to the end user. The default value is to wait until the next app restart to display the changes, but you can refer to the [`InstallMode`](#installmode) enum reference for a description of the available options and what they do. +- __install(installMode: codePush.InstallMode = codePush.InstallMode.ON_NEXT_RESTART, minimumBackgroundDuration = 0): Promise<void>__: Installs the update by saving it to the location on disk where the runtime expects to find the latest version of the app. The `installMode` parameter controls when the changes are actually presented to the end user. The default value is to wait until the next app restart to display the changes, but you can refer to the [`InstallMode`](#installmode) enum reference for a description of the available options and what they do. If the `installMode` parameter is set to `InstallMode.ON_NEXT_RESUME`, then the `minimumBackgroundDuration` parameter allows you to control how long the app must have been in the background before forcing the install after it is resumed. ##### RemotePackage diff --git a/ios/CodePush/CodePush.m b/ios/CodePush/CodePush.m index b367f67..c4f7448 100644 --- a/ios/CodePush/CodePush.m +++ b/ios/CodePush/CodePush.m @@ -12,6 +12,8 @@ @implementation CodePush { BOOL _hasResumeListener; BOOL _isFirstRunAfterUpdate; + int _minimumBackgroundDuration; + NSDate * _lastResignedDate; } RCT_EXPORT_MODULE() @@ -380,6 +382,27 @@ static NSString *bundleResourceName = @"main"; [preferences synchronize]; } +#pragma mark - Application lifecycle event handlers + +// These two handlers will only be registered when there is +// a resume-based update still pending installation. +- (void)applicationWillEnterForeground +{ + // Determine how long the app was in the background and ensure + // that it meets the minimum amount of time requsted. + int durationInBackground = [[NSDate date] timeIntervalSinceDate:_lastResignedDate]; + if (durationInBackground >= _minimumBackgroundDuration) { + [self loadBundle]; + } +} + +- (void)applicationWillResignActive +{ + // Save the current time so that when the app is later + // resumed, we can detect how long it was in the background. + _lastResignedDate = [NSDate date]; +} + #pragma mark - JavaScript-exported module methods /* @@ -522,16 +545,27 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage [self savePendingUpdate:updatePackage[PackageHashKey] isLoading:NO]; - if (installMode == CodePushInstallModeOnNextResume && !_hasResumeListener) { - // Ensure we do not add the listener twice. - // Register for app resume notifications so that we - // can check for pending updates which support "restart on resume" - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(loadBundle) - name:UIApplicationWillEnterForegroundNotification - object:[UIApplication sharedApplication]]; - _hasResumeListener = YES; + if (installMode == CodePushInstallModeOnNextResume) { + _minimumBackgroundDuration = minimumBackgroundDuration; + + if (!_hasResumeListener) { + // Ensure we do not add the listener twice. + // Register for app resume notifications so that we + // can check for pending updates which support "restart on resume" + [[NSNotificationCenter defaultCenter] addObserver:self + 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); } diff --git a/package-mixins.js b/package-mixins.js index 6078bee..4c68404 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -37,9 +37,9 @@ module.exports = (NativeCodePush) => { }; const local = { - async install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, updateInstalledCallback) { + async install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, minimumBackgroundDuration = 0, updateInstalledCallback) { const localPackage = this; - await NativeCodePush.installUpdate(this, installMode); + await NativeCodePush.installUpdate(this, installMode, minimumBackgroundDuration); updateInstalledCallback && updateInstalledCallback(); if (installMode == NativeCodePush.codePushInstallModeImmediate) { NativeCodePush.restartApp(false);