Merge pull request #334 from Microsoft/retry-report-status

Retry Report Status if failed
This commit is contained in:
Geoffrey Goh
2016-05-12 15:15:52 -07:00
7 changed files with 220 additions and 77 deletions

View File

@@ -1,7 +1,7 @@
import { AcquisitionManager as Sdk } from "code-push/script/acquisition-sdk";
import { Alert } from "./AlertAdapter";
import requestFetchAdapter from "./request-fetch-adapter";
import { Platform } from "react-native";
import { AppState, Platform } from "react-native";
let NativeCodePush = require("react-native").NativeModules.CodePush;
const PackageMixins = require("./package-mixins")(NativeCodePush);
@@ -174,19 +174,42 @@ const notifyApplicationReady = (() => {
async function notifyApplicationReadyInternal() {
await NativeCodePush.notifyApplicationReady();
tryReportStatus();
}
async function tryReportStatus(resumeListener) {
const statusReport = await NativeCodePush.getNewStatusReport();
if (statusReport) {
const config = await getConfiguration();
const previousLabelOrAppVersion = statusReport.previousLabelOrAppVersion;
const previousDeploymentKey = statusReport.previousDeploymentKey || config.deploymentKey;
if (statusReport.appVersion) {
const sdk = getPromisifiedSdk(requestFetchAdapter, config);
sdk.reportStatusDeploy(/* deployedPackage */ null, /* status */ null, previousLabelOrAppVersion, previousDeploymentKey);
} else {
config.deploymentKey = statusReport.package.deploymentKey;
const sdk = getPromisifiedSdk(requestFetchAdapter, config);
sdk.reportStatusDeploy(statusReport.package, statusReport.status, previousLabelOrAppVersion, previousDeploymentKey);
try {
if (statusReport.appVersion) {
const sdk = getPromisifiedSdk(requestFetchAdapter, config);
await sdk.reportStatusDeploy(/* deployedPackage */ null, /* status */ null, previousLabelOrAppVersion, previousDeploymentKey);
} else {
config.deploymentKey = statusReport.package.deploymentKey;
const sdk = getPromisifiedSdk(requestFetchAdapter, config);
await sdk.reportStatusDeploy(statusReport.package, statusReport.status, previousLabelOrAppVersion, previousDeploymentKey);
}
log(`Reported status: ${JSON.stringify(statusReport)}`);
NativeCodePush.recordStatusReported(statusReport);
resumeListener && AppState.removeEventListener("change", resumeListener);
} catch (e) {
log(`Report status failed: ${JSON.stringify(statusReport)}`);
NativeCodePush.saveStatusReportForRetry(statusReport);
// Try again when the app resumes
if (!resumeListener) {
resumeListener = (newState) => {
newState === "active" && tryReportStatus(resumeListener);
};
AppState.addEventListener("change", resumeListener);
}
}
} else {
resumeListener && AppState.removeEventListener("change", resumeListener);
}
}

View File

@@ -626,6 +626,12 @@ public class CodePush implements ReactPackage {
promise.resolve(newAppVersionStatusReport);
return null;
}
} else {
WritableMap retryStatusReport = codePushTelemetryManager.getRetryStatusReport();
if (retryStatusReport != null) {
promise.resolve(retryStatusReport);
return null;
}
}
promise.resolve("");
@@ -720,6 +726,11 @@ public class CodePush implements ReactPackage {
promise.resolve("");
}
@ReactMethod
public void recordStatusReported(ReadableMap statusReport) {
codePushTelemetryManager.recordStatusReported(statusReport);
}
@ReactMethod
public void restartApp(boolean onlyIfUpdateIsPending) {
// If this is an unconditional restart request, or there
@@ -729,6 +740,11 @@ public class CodePush implements ReactPackage {
}
}
@ReactMethod
public void saveStatusReportForRetry(ReadableMap statusReport) {
codePushTelemetryManager.saveStatusReportForRetry(statusReport);
}
@ReactMethod
// Replaces the current bundle with the one downloaded from removeBundleUrl.
// It is only to be used during tests. No-ops if the test configuration flag is not set.

View File

@@ -3,18 +3,28 @@ package com.microsoft.codepush.react;
import android.content.Context;
import android.content.SharedPreferences;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import org.json.JSONException;
import org.json.JSONObject;
public class CodePushTelemetryManager {
private Context applicationContext;
private final String APP_VERSION_KEY = "appVersion";
private final String CODE_PUSH_PREFERENCES;
private final String DEPLOYMENT_FAILED_STATUS = "DeploymentFailed";
private final String DEPLOYMENT_KEY_KEY = "deploymentKey";
private final String DEPLOYMENT_SUCCEEDED_STATUS = "DeploymentSucceeded";
private final String LABEL_KEY = "label";
private final String LAST_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_LAST_DEPLOYMENT_REPORT";
private final String PACKAGE_KEY = "package";
private final String PREVIOUS_DEPLOYMENT_KEY_KEY = "previousDeploymentKey";
private final String PREVIOUS_LABEL_OR_APP_VERSION_KEY = "previousLabelOrAppVersion";
private final String RETRY_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_RETRY_DEPLOYMENT_REPORT";
private final String STATUS_KEY = "status";
public CodePushTelemetryManager(Context applicationContext, String codePushPreferencesKey) {
this.applicationContext = applicationContext;
@@ -23,26 +33,41 @@ public class CodePushTelemetryManager {
public WritableMap getBinaryUpdateReport(String appVersion) {
String previousStatusReportIdentifier = this.getPreviousStatusReportIdentifier();
WritableNativeMap reportMap = null;
if (previousStatusReportIdentifier == null) {
this.recordDeploymentStatusReported(appVersion);
WritableNativeMap reportMap = new WritableNativeMap();
reportMap.putString("appVersion", appVersion);
return reportMap;
this.clearRetryStatusReport();
reportMap = new WritableNativeMap();
reportMap.putString(APP_VERSION_KEY, appVersion);
} else if (!previousStatusReportIdentifier.equals(appVersion)) {
this.recordDeploymentStatusReported(appVersion);
WritableNativeMap reportMap = new WritableNativeMap();
this.clearRetryStatusReport();
reportMap = new WritableNativeMap();
if (this.isStatusReportIdentifierCodePushLabel(previousStatusReportIdentifier)) {
String previousDeploymentKey = this.getDeploymentKeyFromStatusReportIdentifier(previousStatusReportIdentifier);
String previousLabel = this.getVersionLabelFromStatusReportIdentifier(previousStatusReportIdentifier);
reportMap.putString("appVersion", appVersion);
reportMap.putString("previousDeploymentKey", previousDeploymentKey);
reportMap.putString("previousLabelOrAppVersion", previousLabel);
reportMap.putString(APP_VERSION_KEY, appVersion);
reportMap.putString(PREVIOUS_DEPLOYMENT_KEY_KEY, previousDeploymentKey);
reportMap.putString(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousLabel);
} else {
// Previous status report was with a binary app version.
reportMap.putString("appVersion", appVersion);
reportMap.putString("previousLabelOrAppVersion", previousStatusReportIdentifier);
reportMap.putString(APP_VERSION_KEY, appVersion);
reportMap.putString(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousStatusReportIdentifier);
}
}
return reportMap;
}
public WritableMap getRetryStatusReport() {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
String retryStatusReportString = settings.getString(RETRY_DEPLOYMENT_REPORT_KEY, null);
if (retryStatusReportString != null) {
clearRetryStatusReport();
try {
JSONObject retryStatusReport = new JSONObject(retryStatusReportString);
return CodePushUtils.convertJsonObjectToWritable(retryStatusReport);
} catch (JSONException e) {
e.printStackTrace();
}
return reportMap;
}
return null;
@@ -50,44 +75,61 @@ public class CodePushTelemetryManager {
public WritableMap getRollbackReport(WritableMap lastFailedPackage) {
WritableNativeMap reportMap = new WritableNativeMap();
reportMap.putMap("package", lastFailedPackage);
reportMap.putString("status", DEPLOYMENT_FAILED_STATUS);
reportMap.putMap(PACKAGE_KEY, lastFailedPackage);
reportMap.putString(STATUS_KEY, DEPLOYMENT_FAILED_STATUS);
return reportMap;
}
public WritableMap getUpdateReport(WritableMap currentPackage) {
String currentPackageIdentifier = this.getPackageStatusReportIdentifier(currentPackage);
String previousStatusReportIdentifier = this.getPreviousStatusReportIdentifier();
WritableNativeMap reportMap = null;
if (currentPackageIdentifier != null) {
if (previousStatusReportIdentifier == null) {
this.recordDeploymentStatusReported(currentPackageIdentifier);
WritableNativeMap reportMap = new WritableNativeMap();
reportMap.putMap("package", currentPackage);
reportMap.putString("status", DEPLOYMENT_SUCCEEDED_STATUS);
return reportMap;
this.clearRetryStatusReport();
reportMap = new WritableNativeMap();
reportMap.putMap(PACKAGE_KEY, currentPackage);
reportMap.putString(STATUS_KEY, DEPLOYMENT_SUCCEEDED_STATUS);
} else if (!previousStatusReportIdentifier.equals(currentPackageIdentifier)) {
this.recordDeploymentStatusReported(currentPackageIdentifier);
this.clearRetryStatusReport();
reportMap = new WritableNativeMap();
if (this.isStatusReportIdentifierCodePushLabel(previousStatusReportIdentifier)) {
String previousDeploymentKey = this.getDeploymentKeyFromStatusReportIdentifier(previousStatusReportIdentifier);
String previousLabel = this.getVersionLabelFromStatusReportIdentifier(previousStatusReportIdentifier);
WritableNativeMap reportMap = new WritableNativeMap();
reportMap.putMap("package", currentPackage);
reportMap.putString("status", DEPLOYMENT_SUCCEEDED_STATUS);
reportMap.putString("previousDeploymentKey", previousDeploymentKey);
reportMap.putString("previousLabelOrAppVersion", previousLabel);
return reportMap;
reportMap.putMap(PACKAGE_KEY, currentPackage);
reportMap.putString(STATUS_KEY, DEPLOYMENT_SUCCEEDED_STATUS);
reportMap.putString(PREVIOUS_DEPLOYMENT_KEY_KEY, previousDeploymentKey);
reportMap.putString(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousLabel);
} else {
// Previous status report was with a binary app version.
WritableNativeMap reportMap = new WritableNativeMap();
reportMap.putMap("package", currentPackage);
reportMap.putString("status", DEPLOYMENT_SUCCEEDED_STATUS);
reportMap.putString("previousLabelOrAppVersion", previousStatusReportIdentifier);
return reportMap;
reportMap.putMap(PACKAGE_KEY, currentPackage);
reportMap.putString(STATUS_KEY, DEPLOYMENT_SUCCEEDED_STATUS);
reportMap.putString(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousStatusReportIdentifier);
}
}
}
return null;
return reportMap;
}
public void recordStatusReported(ReadableMap statusReport) {
if (statusReport.hasKey(APP_VERSION_KEY)) {
saveStatusReportedForIdentifier(statusReport.getString(APP_VERSION_KEY));
} else if (statusReport.hasKey(PACKAGE_KEY)) {
String packageIdentifier = getPackageStatusReportIdentifier(statusReport.getMap(PACKAGE_KEY));
saveStatusReportedForIdentifier(packageIdentifier);
}
}
public void saveStatusReportForRetry(ReadableMap statusReport) {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
JSONObject statusReportJSON = CodePushUtils.convertReadableToJsonObject(statusReport);
settings.edit().putString(RETRY_DEPLOYMENT_REPORT_KEY, statusReportJSON.toString()).commit();
}
private void clearRetryStatusReport() {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
settings.edit().remove(RETRY_DEPLOYMENT_REPORT_KEY).commit();
}
private String getDeploymentKeyFromStatusReportIdentifier(String statusReportIdentifier) {
@@ -99,7 +141,7 @@ public class CodePushTelemetryManager {
}
}
private String getPackageStatusReportIdentifier(WritableMap updatePackage) {
private String getPackageStatusReportIdentifier(ReadableMap updatePackage) {
// Because deploymentKeys can be dynamically switched, we use a
// combination of the deploymentKey and label as the packageIdentifier.
String deploymentKey = CodePushUtils.tryGetString(updatePackage, DEPLOYMENT_KEY_KEY);
@@ -129,7 +171,7 @@ public class CodePushTelemetryManager {
return statusReportIdentifier != null && statusReportIdentifier.contains(":");
}
private void recordDeploymentStatusReported(String appVersionOrPackageIdentifier) {
private void saveStatusReportedForIdentifier(String appVersionOrPackageIdentifier) {
SharedPreferences settings = applicationContext.getSharedPreferences(CODE_PUSH_PREFERENCES, 0);
settings.edit().putString(LAST_DEPLOYMENT_REPORT_KEY, appVersionOrPackageIdentifier).commit();
}

View File

@@ -112,8 +112,11 @@ failCallback:(void (^)(NSError *err))failCallback;
@interface CodePushTelemetryManager : NSObject
+ (NSDictionary *)getBinaryUpdateReport:(NSString *)appVersion;
+ (NSDictionary *)getRetryStatusReport;
+ (NSDictionary *)getRollbackReport:(NSDictionary *)lastFailedPackage;
+ (NSDictionary *)getUpdateReport:(NSDictionary *)currentPackage;
+ (void)recordStatusReported:(NSDictionary *)statusReport;
+ (void)saveStatusReportForRetry:(NSDictionary *)statusReport;
@end

View File

@@ -15,7 +15,7 @@
BOOL _isFirstRunAfterUpdate;
int _minimumBackgroundDuration;
NSDate *_lastResignedDate;
// Used to coordinate the dispatching of download progress events to JS.
long long _latestExpectedContentLength;
long long _latestReceivedConentLength;
@@ -507,14 +507,14 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
[mutableUpdatePackage setValue:[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL]
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]
@@ -525,7 +525,7 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
_latestExpectedContentLength = expectedContentLength;
_latestReceivedConentLength = receivedContentLength;
_didUpdateProgress = YES;
// If the download is completed, stop observing frame
// updates and synchronously send the last event.
if (expectedContentLength == receivedContentLength) {
@@ -549,7 +549,7 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
if ([CodePushErrorUtils isCodePushError:err]) {
[self saveFailedUpdate:mutableUpdatePackage];
}
// Stop observing frame updates if the download fails.
_didUpdateProgress = NO;
_paused = YES;
@@ -783,11 +783,27 @@ RCT_EXPORT_METHOD(getNewStatusReport:(RCTPromiseResolveBlock)resolve
NSString *appVersion = [[CodePushConfig current] appVersion];
resolve([CodePushTelemetryManager getBinaryUpdateReport:appVersion]);
return;
} else {
NSDictionary *retryStatusReport = [CodePushTelemetryManager getRetryStatusReport];
if (retryStatusReport) {
resolve(retryStatusReport);
return;
}
}
resolve(nil);
}
RCT_EXPORT_METHOD(recordStatusReported:(NSDictionary *)statusReport)
{
[CodePushTelemetryManager recordStatusReported:statusReport];
}
RCT_EXPORT_METHOD(saveStatusReportForRetry:(NSDictionary *)statusReport)
{
[CodePushTelemetryManager saveStatusReportForRetry:statusReport];
}
#pragma mark - RCTFrameUpdateObserver Methods
- (void)didUpdateFrame:(RCTFrameUpdate *)update
@@ -795,7 +811,7 @@ RCT_EXPORT_METHOD(getNewStatusReport:(RCTPromiseResolveBlock)resolve
if (!_didUpdateProgress) {
return;
}
[self dispatchDownloadProgressEvent];
_didUpdateProgress = NO;
}

View File

@@ -20,7 +20,7 @@ failCallback:(void (^)(NSError *err))failCallback {
return self;
}
-(void)download:(NSString*)url {
- (void)download:(NSString*)url {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
@@ -47,12 +47,12 @@ failCallback:(void (^)(NSError *err))failCallback {
return nil;
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
self.expectedContentLength = response.expectedContentLength;
[self.outputFileStream open];
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
if (self.receivedContentLength < 4) {
for (int i = 0; i < [data length]; i++) {
int headerOffset = (int)self.receivedContentLength + i;
@@ -97,7 +97,7 @@ failCallback:(void (^)(NSError *err))failCallback {
self.failCallback(error);
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// 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.

View File

@@ -1,10 +1,16 @@
#import "CodePush.h"
static NSString *const AppVersionKey = @"appVersion";
static NSString *const DeploymentFailed = @"DeploymentFailed";
static NSString *const DeploymentKeyKey = @"deploymentKey";
static NSString *const DeploymentSucceeded = @"DeploymentSucceeded";
static NSString *const LabelKey = @"label";
static NSString *const LastDeploymentReportKey = @"CODE_PUSH_LAST_DEPLOYMENT_REPORT";
static NSString *const PackageKey = @"package";
static NSString *const PreviousDeploymentKeyKey = @"previousDeploymentKey";
static NSString *const PreviousLabelOrAppVersionKey = @"previousLabelOrAppVersion";
static NSString *const RetryDeploymentReportKey = @"CODE_PUSH_RETRY_DEPLOYMENT_REPORT";
static NSString *const StatusKey = @"status";
@implementation CodePushTelemetryManager
@@ -12,35 +18,48 @@ static NSString *const LastDeploymentReportKey = @"CODE_PUSH_LAST_DEPLOYMENT_REP
{
NSString *previousStatusReportIdentifier = [self getPreviousStatusReportIdentifier];
if (previousStatusReportIdentifier == nil) {
[self recordDeploymentStatusReported:appVersion];
return @{ @"appVersion": appVersion };
[self clearRetryStatusReport];
return @{ AppVersionKey: appVersion };
} else if (![previousStatusReportIdentifier isEqualToString:appVersion]) {
[self recordDeploymentStatusReported:appVersion];
if ([self isStatusReportIdentifierCodePushLabel:previousStatusReportIdentifier]) {
NSString *previousDeploymentKey = [self getDeploymentKeyFromStatusReportIdentifier:previousStatusReportIdentifier];
NSString *previousLabel = [self getVersionLabelFromStatusReportIdentifier:previousStatusReportIdentifier];
[self clearRetryStatusReport];
return @{
@"appVersion": appVersion,
@"previousDeploymentKey": previousDeploymentKey,
@"previousLabelOrAppVersion": previousLabel
AppVersionKey: appVersion,
PreviousDeploymentKeyKey: previousDeploymentKey,
PreviousLabelOrAppVersionKey: previousLabel
};
} else {
[self clearRetryStatusReport];
// Previous status report was with a binary app version.
return @{
@"appVersion": appVersion,
@"previousLabelOrAppVersion": previousStatusReportIdentifier
AppVersionKey: appVersion,
PreviousLabelOrAppVersionKey: previousStatusReportIdentifier
};
}
}
return nil;
}
+ (NSDictionary *)getRetryStatusReport
{
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
NSDictionary *retryStatusReport = [preferences objectForKey:RetryDeploymentReportKey];
if (retryStatusReport) {
[self clearRetryStatusReport];
return retryStatusReport;
} else {
return nil;
}
}
+ (NSDictionary *)getRollbackReport:(NSDictionary *)lastFailedPackage
{
return @{
@"package": lastFailedPackage,
@"status": DeploymentFailed
PackageKey: lastFailedPackage,
StatusKey: DeploymentFailed
};
}
@@ -50,38 +69,62 @@ static NSString *const LastDeploymentReportKey = @"CODE_PUSH_LAST_DEPLOYMENT_REP
NSString *previousStatusReportIdentifier = [self getPreviousStatusReportIdentifier];
if (currentPackageIdentifier) {
if (previousStatusReportIdentifier == nil) {
[self recordDeploymentStatusReported:currentPackageIdentifier];
[self clearRetryStatusReport];
return @{
@"package": currentPackage,
@"status": DeploymentSucceeded
PackageKey: currentPackage,
StatusKey: DeploymentSucceeded
};
} else if (![previousStatusReportIdentifier isEqualToString:currentPackageIdentifier]) {
[self recordDeploymentStatusReported:currentPackageIdentifier];
[self clearRetryStatusReport];
if ([self isStatusReportIdentifierCodePushLabel:previousStatusReportIdentifier]) {
NSString *previousDeploymentKey = [self getDeploymentKeyFromStatusReportIdentifier:previousStatusReportIdentifier];
NSString *previousLabel = [self getVersionLabelFromStatusReportIdentifier:previousStatusReportIdentifier];
return @{
@"package": currentPackage,
@"status": DeploymentSucceeded,
@"previousDeploymentKey": previousDeploymentKey,
@"previousLabelOrAppVersion": previousLabel
PackageKey: currentPackage,
StatusKey: DeploymentSucceeded,
PreviousDeploymentKeyKey: previousDeploymentKey,
PreviousLabelOrAppVersionKey: previousLabel
};
} else {
// Previous status report was with a binary app version.
return @{
@"package": currentPackage,
@"status": DeploymentSucceeded,
@"previousLabelOrAppVersion": previousStatusReportIdentifier
PackageKey: currentPackage,
StatusKey: DeploymentSucceeded,
PreviousLabelOrAppVersionKey: previousStatusReportIdentifier
};
}
}
}
return nil;
}
+ (void)recordStatusReported:(NSDictionary *)statusReport
{
if (statusReport[AppVersionKey]) {
[self saveStatusReportedForIdentifier:statusReport[AppVersionKey]];
} else if (statusReport[PackageKey]) {
NSString *packageIdentifier = [self getPackageStatusReportIdentifier:statusReport[PackageKey]];
[self saveStatusReportedForIdentifier:packageIdentifier];
}
}
+ (void)saveStatusReportForRetry:(NSDictionary *)statusReport
{
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
[preferences setValue:statusReport forKey:RetryDeploymentReportKey];
[preferences synchronize];
}
#pragma mark - private methods
+ (void)clearRetryStatusReport
{
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
[preferences setValue:nil forKey:RetryDeploymentReportKey];
[preferences synchronize];
}
+ (NSString *)getDeploymentKeyFromStatusReportIdentifier:(NSString *)statusReportIdentifier
{
return [[statusReportIdentifier componentsSeparatedByString:@":"] firstObject];
@@ -117,11 +160,11 @@ static NSString *const LastDeploymentReportKey = @"CODE_PUSH_LAST_DEPLOYMENT_REP
return statusReportIdentifier != nil && [statusReportIdentifier rangeOfString:@":"].location != NSNotFound;
}
+ (void)recordDeploymentStatusReported:(NSString *)appVersionOrPackageIdentifier
+ (void)saveStatusReportedForIdentifier:(NSString *)appVersionOrPackageIdentifier
{
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
[preferences setValue:appVersionOrPackageIdentifier forKey:LastDeploymentReportKey];
[preferences synchronize];
}
@end
@end