Swiftgram/Classes/BITUpdateManager.m
Andreas Linde a227ff9f7a Fix disableUpdateManager not working as advertised
This bug did not affect the automatic disabling of the module when run from the App Store! That one was and is working as advertised.
2012-08-21 23:54:13 +02:00

1142 lines
45 KiB
Objective-C

/*
* Author: Andreas Linde <mail@andreaslinde.de>
* Peter Steinberger
*
* Copyright (c) 2012 HockeyApp, Bit Stadium GmbH.
* Copyright (c) 2011 Andreas Linde.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
#import <sys/sysctl.h>
#import <mach-o/ldsyms.h>
#import "HockeySDK.h"
#import "HockeySDKPrivate.h"
#import "BITUpdateManagerPrivate.h"
#import "BITUpdateViewControllerPrivate.h"
#import "BITAppVersionMetaInfo.h"
#import "NSString+BITHockeyAdditions.h"
#import "UIImage+BITHockeyAdditions.h"
// API defines - do not change
#define BETA_DOWNLOAD_TYPE_PROFILE @"profile"
#define BETA_UPDATE_RESULT @"result"
#define BETA_UPDATE_TITLE @"title"
#define BETA_UPDATE_SUBTITLE @"subtitle"
#define BETA_UPDATE_NOTES @"notes"
#define BETA_UPDATE_VERSION @"version"
#define BETA_UPDATE_TIMESTAMP @"timestamp"
#define BETA_UPDATE_APPSIZE @"appsize"
@implementation BITUpdateManager
@synthesize delegate = _delegate;
@synthesize urlConnection = _urlConnection;
@synthesize checkInProgress = _checkInProgress;
@synthesize receivedData = _receivedData;
@synthesize alwaysShowUpdateReminder = _showUpdateReminder;
@synthesize checkForUpdateOnLaunch = _checkForUpdateOnLaunch;
@synthesize compareVersionType = _compareVersionType;
@synthesize lastCheck = _lastCheck;
@synthesize updateSetting = _updateSetting;
@synthesize appVersions = _appVersions;
@synthesize updateAvailable = _updateAvailable;
@synthesize usageStartTimestamp = _usageStartTimestamp;
@synthesize currentHockeyViewController = _currentHockeyViewController;
@synthesize showDirectInstallOption = _showDirectInstallOption;
@synthesize requireAuthorization = _requireAuthorization;
@synthesize authenticationSecret = _authenticationSecret;
@synthesize blockingView = _blockingView;
@synthesize checkForTracker = _checkForTracker;
@synthesize trackerConfig = _trackerConfig;
@synthesize barStyle = _barStyle;
@synthesize modalPresentationStyle = _modalPresentationStyle;
#pragma mark - private
- (void)reportError:(NSError *)error {
BITHockeyLog(@"ERROR: %@", [error localizedDescription]);
_lastCheckFailed = YES;
// only show error if we enable that
if (_showFeedback) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"UpdateError")
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:BITHockeyLocalizedString(@"OK") otherButtonTitles:nil];
[alert show];
[alert release];
_showFeedback = NO;
}
}
- (NSString *)encodedAppIdentifier {
return (_appIdentifier ? [_appIdentifier bit_URLEncodedString] : [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"] bit_URLEncodedString]);
}
- (NSString *)getDevicePlatform {
size_t size;
sysctlbyname("hw.machine", NULL, &size, NULL, 0);
char *answer = (char*)malloc(size);
sysctlbyname("hw.machine", answer, &size, NULL, 0);
NSString *platform = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding];
free(answer);
return platform;
}
- (NSString *)executableUUID {
const uint8_t *command = (const uint8_t *)(&_mh_execute_header + 1);
for (uint32_t idx = 0; idx < _mh_execute_header.ncmds; ++idx) {
const struct load_command *load_command = (const struct load_command *)command;
if (load_command->cmd == LC_UUID) {
const struct uuid_command *uuid_command = (const struct uuid_command *)command;
const uint8_t *uuid = uuid_command->uuid;
return [[NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
uuid[0], uuid[1], uuid[2], uuid[3],
uuid[4], uuid[5], uuid[6], uuid[7],
uuid[8], uuid[9], uuid[10], uuid[11],
uuid[12], uuid[13], uuid[14], uuid[15]]
lowercaseString];
} else {
command += load_command->cmdsize;
}
}
return nil;
}
#pragma mark - Expiry
- (BOOL)expiryDateReached {
if (_isAppStoreEnvironment) return NO;
if (_expiryDate) {
NSDate *currentDate = [NSDate date];
if ([currentDate compare:_expiryDate] != NSOrderedAscending)
return YES;
}
return NO;
}
- (void)checkExpiryDateReached {
if (![self expiryDateReached]) return;
BOOL shouldShowDefaultAlert = YES;
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(shouldDisplayExpiryAlertForUpdateManager:)]) {
shouldShowDefaultAlert = [self.delegate shouldDisplayExpiryAlertForUpdateManager:self];
}
if (shouldShowDefaultAlert) {
NSString *appName = [[[NSBundle mainBundle] localizedInfoDictionary] objectForKey:@"CFBundleDisplayName"];
if (!appName)
appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] ?: BITHockeyLocalizedString(@"HockeyAppNamePlaceholder");
[self showBlockingScreen:[NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateExpired"), appName] image:@"authorize_denied.png"];
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(didDisplayExpiryAlertForUpdateManager:)]) {
[self.delegate didDisplayExpiryAlertForUpdateManager:self];
}
}
}
#pragma mark - Usage
- (void)startUsage {
if ([self expiryDateReached]) return;
self.usageStartTimestamp = [NSDate date];
BOOL newVersion = NO;
if (![[NSUserDefaults standardUserDefaults] valueForKey:kBITUpdateUsageTimeForVersionString]) {
newVersion = YES;
} else {
if ([(NSString *)[[NSUserDefaults standardUserDefaults] valueForKey:kBITUpdateUsageTimeForVersionString] compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] != NSOrderedSame) {
newVersion = YES;
}
}
if (newVersion) {
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble:[[NSDate date] timeIntervalSinceReferenceDate]] forKey:kBITUpdateDateOfVersionInstallation];
[[NSUserDefaults standardUserDefaults] setObject:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] forKey:kBITUpdateUsageTimeForVersionString];
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble:0] forKey:kBITUpdateUsageTimeOfCurrentVersion];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
- (void)stopUsage {
if ([self expiryDateReached]) return;
double timeDifference = [[NSDate date] timeIntervalSinceReferenceDate] - [_usageStartTimestamp timeIntervalSinceReferenceDate];
double previousTimeDifference = [(NSNumber *)[[NSUserDefaults standardUserDefaults] valueForKey:kBITUpdateUsageTimeOfCurrentVersion] doubleValue];
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble:previousTimeDifference + timeDifference] forKey:kBITUpdateUsageTimeOfCurrentVersion];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSString *)currentUsageString {
double currentUsageTime = [[NSUserDefaults standardUserDefaults] doubleForKey:kBITUpdateUsageTimeOfCurrentVersion];
if (currentUsageTime > 0) {
// round (up) to 1 minute
return [NSString stringWithFormat:@"%.0f", ceil(currentUsageTime / 60.0)*60];
} else {
return @"0";
}
}
- (NSString *)installationDateString {
NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease];
[formatter setDateFormat:@"MM/dd/yyyy"];
double installationTimeStamp = [[NSUserDefaults standardUserDefaults] doubleForKey:kBITUpdateDateOfVersionInstallation];
if (installationTimeStamp == 0.0f) {
return [formatter stringFromDate:[NSDate date]];
} else {
return [formatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:installationTimeStamp]];
}
}
#pragma mark - Device identifier
- (NSString *)deviceIdentifier {
if ([_delegate respondsToSelector:@selector(customDeviceIdentifierForUpdateManager:)]) {
NSString *identifier = [_delegate performSelector:@selector(customDeviceIdentifierForUpdateManager:) withObject:self];
if (identifier && [identifier length] > 0) {
return identifier;
}
}
return @"invalid";
}
#pragma mark - Authorization
- (NSString *)authenticationToken {
return [BITHockeyMD5([NSString stringWithFormat:@"%@%@%@%@",
_authenticationSecret,
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"],
[self deviceIdentifier]
]
) lowercaseString];
}
- (BITUpdateAuthorizationState)authorizationState {
NSString *version = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateAuthorizedVersion];
NSString *token = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateAuthorizedToken];
if (version != nil && token != nil) {
if ([version compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) {
// if it is denied, block the screen permanently
if ([token compare:[self authenticationToken]] != NSOrderedSame) {
return BITUpdateAuthorizationDenied;
} else {
return BITUpdateAuthorizationAllowed;
}
}
}
return BITUpdateAuthorizationPending;
}
#pragma mark - Cache
- (void)checkUpdateAvailable {
// check if there is an update available
if (self.compareVersionType == BITUpdateComparisonResultGreater) {
self.updateAvailable = ([self.newestAppVersion.version bit_versionCompare:self.currentAppVersion] == NSOrderedDescending);
} else {
self.updateAvailable = ([self.newestAppVersion.version compare:self.currentAppVersion] != NSOrderedSame);
}
}
- (void)loadAppCache {
NSData *savedHockeyData = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateArrayOfLastCheck];
NSArray *savedHockeyCheck = nil;
if (savedHockeyData) {
savedHockeyCheck = [NSKeyedUnarchiver unarchiveObjectWithData:savedHockeyData];
}
if (savedHockeyCheck) {
self.appVersions = [NSArray arrayWithArray:savedHockeyCheck];
[self checkUpdateAvailable];
} else {
self.appVersions = nil;
}
}
- (void)saveAppCache {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.appVersions];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:kBITUpdateArrayOfLastCheck];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#pragma mark - Window Helper
- (UIWindow *)findVisibleWindow {
UIWindow *visibleWindow = nil;
// if the rootViewController property (available >= iOS 4.0) of the main window is set, we present the modal view controller on top of the rootViewController
NSArray *windows = [[UIApplication sharedApplication] windows];
for (UIWindow *window in windows) {
if (!window.hidden && !visibleWindow) {
visibleWindow = window;
}
if ([UIWindow instancesRespondToSelector:@selector(rootViewController)]) {
if ([window rootViewController]) {
visibleWindow = window;
BITHockeyLog(@"INFO: UIWindow with rootViewController found: %@", visibleWindow);
break;
}
}
}
return visibleWindow;
}
#pragma mark - Init
- (id)initWithAppIdentifier:(NSString *)appIdentifier isAppStoreEnvironemt:(BOOL)isAppStoreEnvironment {
if ((self = [super init])) {
_appIdentifier = appIdentifier;
_isAppStoreEnvironment = isAppStoreEnvironment;
_updateURL = BITHOCKEYSDK_URL;
_delegate = nil;
_expiryDate = nil;
_checkInProgress = NO;
_dataFound = NO;
_updateAvailable = NO;
_lastCheckFailed = NO;
_currentAppVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
_navController = nil;
_blockingView = nil;
_requireAuthorization = NO;
_authenticationSecret = nil;
_lastCheck = nil;
_uuid = [[self executableUUID] retain];
_sendUsageData = YES;
_disableUpdateManager = NO;
// set defaults
self.showDirectInstallOption = NO;
self.requireAuthorization = NO;
self.alwaysShowUpdateReminder = YES;
self.checkForUpdateOnLaunch = YES;
self.compareVersionType = BITUpdateComparisonResultGreater;
self.barStyle = UIBarStyleDefault;
self.modalPresentationStyle = UIModalPresentationFormSheet;
self.updateSetting = BITUpdateCheckStartup;
if ([[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateDateOfLastCheck]) {
// we did write something else in the past, so for compatibility reasons do this
id tempLastCheck = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateDateOfLastCheck];
if ([tempLastCheck isKindOfClass:[NSDate class]]) {
self.lastCheck = tempLastCheck;
}
}
if (!_lastCheck) {
self.lastCheck = [NSDate distantPast];
}
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(updateManagerShouldSendUsageData:)]) {
_sendUsageData = [self.delegate updateManagerShouldSendUsageData:self];
}
if (!BITHockeyBundle()) {
NSLog(@"[HockeySDK] WARNING: %@ is missing, make sure it is added!", BITHOCKEYSDK_BUNDLE);
}
[self loadAppCache];
[self startUsage];
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
[dnc addObserver:self selector:@selector(startManager) name:BITHockeyNetworkDidBecomeReachableNotification object:nil];
[dnc addObserver:self selector:@selector(stopUsage) name:UIApplicationWillTerminateNotification object:nil];
[dnc addObserver:self selector:@selector(checkExpiryDateReached) name:UIApplicationDidBecomeActiveNotification object:nil];
[dnc addObserver:self selector:@selector(startUsage) name:UIApplicationDidBecomeActiveNotification object:nil];
[dnc addObserver:self selector:@selector(stopUsage) name:UIApplicationWillResignActiveNotification object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyNetworkDidBecomeReachableNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillTerminateNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
_delegate = nil;
[_updateURL release];
_updateURL = nil;
[_urlConnection cancel];
self.urlConnection = nil;
[_expiryDate release];
_expiryDate = nil;
[_navController release];
[_blockingView release];
[_currentHockeyViewController release];
[_appVersions release];
[_receivedData release];
[_lastCheck release];
[_usageStartTimestamp release];
[_authenticationSecret release];
[_uuid release];
[super dealloc];
}
#pragma mark - BetaUpdateUI
- (BITUpdateViewController *)hockeyViewController:(BOOL)modal {
return [[[BITUpdateViewController alloc] init:self modal:modal] autorelease];
}
- (void)showUpdateView {
if (_isAppStoreEnvironment) {
NSLog(@"[HockeySDK] This should not be called from an app store build!");
return;
}
if (_currentHockeyViewController) {
BITHockeyLog(@"INFO: Update view already visible, aborting");
return;
}
UIViewController *parentViewController = nil;
if ([[self delegate] respondsToSelector:@selector(viewControllerForUpdateManager:)]) {
parentViewController = [_delegate viewControllerForUpdateManager:self];
}
UIWindow *visibleWindow = [self findVisibleWindow];
if (parentViewController == nil && [UIWindow instancesRespondToSelector:@selector(rootViewController)]) {
parentViewController = [visibleWindow rootViewController];
}
// use topmost modal view
while (parentViewController.modalViewController) {
parentViewController = parentViewController.modalViewController;
}
// special addition to get rootViewController from three20 which has it's own controller handling
if (NSClassFromString(@"TTNavigator")) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
parentViewController = [[NSClassFromString(@"TTNavigator") performSelector:(NSSelectorFromString(@"navigator"))] visibleViewController];
#pragma clang diagnostic pop
}
if (_navController != nil) [_navController release];
BITUpdateViewController *hockeyViewController = [self hockeyViewController:YES];
_navController = [[UINavigationController alloc] initWithRootViewController:hockeyViewController];
_navController.navigationBar.barStyle = _barStyle;
_navController.modalPresentationStyle = _modalPresentationStyle;
if (parentViewController) {
if ([_navController respondsToSelector:@selector(setModalTransitionStyle:)]) {
_navController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
}
// page sheet for the iPad
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad && [_navController respondsToSelector:@selector(setModalPresentationStyle:)]) {
_navController.modalPresentationStyle = UIModalPresentationFormSheet;
}
hockeyViewController.modalAnimated = YES;
[parentViewController presentModalViewController:_navController animated:YES];
} else {
// if not, we add a subview to the window. A bit hacky but should work in most circumstances.
// Also, we don't get a nice animation for free, but hey, this is for beta not production users ;)
NSLog(@"[HockeySDK] Warning: No rootViewController found and no view controller set via delegate, using UIWindow-approach: %@", visibleWindow);
hockeyViewController.modalAnimated = NO;
[visibleWindow addSubview:_navController.view];
}
}
- (void)showCheckForUpdateAlert {
if (_isAppStoreEnvironment) return;
if ([self isUpdateManagerDisabled]) return;
if (!_updateAlertShowing) {
if ([self hasNewerMandatoryVersion]) {
UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"UpdateAvailable")
message:[NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateAlertMandatoryTextWithAppVersion"), [self.newestAppVersion nameAndVersionString]]
delegate:self
cancelButtonTitle:BITHockeyLocalizedString(@"UpdateInstall")
otherButtonTitles:nil
] autorelease];
[alertView setTag:2];
[alertView show];
_updateAlertShowing = YES;
} else {
UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"UpdateAvailable")
message:[NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateAlertTextWithAppVersion"), [self.newestAppVersion nameAndVersionString]]
delegate:self
cancelButtonTitle:BITHockeyLocalizedString(@"UpdateIgnore")
otherButtonTitles:BITHockeyLocalizedString(@"UpdateShow"), nil
] autorelease];
if (self.isShowingDirectInstallOption) {
[alertView addButtonWithTitle:BITHockeyLocalizedString(@"UpdateInstall")];
}
[alertView setTag:0];
[alertView show];
_updateAlertShowing = YES;
}
}
}
// nag the user with neverending alerts if we cannot find out the window for presenting the covering sheet
- (void)alertFallback:(NSString *)message {
UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:nil
message:message
delegate:self
cancelButtonTitle:BITHockeyLocalizedString(@"HockeyOK")
otherButtonTitles:nil
] autorelease];
[alertView setTag:1];
[alertView show];
}
// open an authorization screen
- (void)showBlockingScreen:(NSString *)message image:(NSString *)image {
self.blockingView = nil;
UIWindow *visibleWindow = [self findVisibleWindow];
if (visibleWindow == nil) {
[self alertFallback:message];
return;
}
CGRect frame = [visibleWindow frame];
self.blockingView = [[[UIView alloc] initWithFrame:frame] autorelease];
UIImageView *backgroundView = [[[UIImageView alloc] initWithImage:[UIImage bit_imageNamed:@"bg.png" bundle:BITHOCKEYSDK_BUNDLE]] autorelease];
backgroundView.contentMode = UIViewContentModeScaleAspectFill;
backgroundView.frame = frame;
[self.blockingView addSubview:backgroundView];
if (image != nil) {
UIImageView *imageView = [[[UIImageView alloc] initWithImage:[UIImage bit_imageNamed:image bundle:BITHOCKEYSDK_BUNDLE]] autorelease];
imageView.contentMode = UIViewContentModeCenter;
imageView.frame = frame;
[self.blockingView addSubview:imageView];
}
if (message != nil) {
frame.origin.x = 20;
frame.origin.y = frame.size.height - 140;
frame.size.width -= 40;
frame.size.height = 50;
UILabel *label = [[[UILabel alloc] initWithFrame:frame] autorelease];
label.text = message;
label.textAlignment = UITextAlignmentCenter;
label.numberOfLines = 2;
label.backgroundColor = [UIColor clearColor];
[self.blockingView addSubview:label];
}
[visibleWindow addSubview:self.blockingView];
}
#pragma mark - JSONParsing
- (id)parseJSONResultString:(NSString *)jsonString {
NSError *error = nil;
id feedResult = nil;
if (!jsonString)
return nil;
#if BITHOCKEYSDK_NATIVE_JSON_AVAILABLE
feedResult = [NSJSONSerialization JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error];
#else
id nsjsonClass = NSClassFromString(@"NSJSONSerialization");
SEL nsjsonSelect = NSSelectorFromString(@"JSONObjectWithData:options:error:");
SEL sbJSONSelector = NSSelectorFromString(@"JSONValue");
SEL jsonKitSelector = NSSelectorFromString(@"objectFromJSONStringWithParseOptions:error:");
SEL yajlSelector = NSSelectorFromString(@"yajl_JSONWithOptions:error:");
if (nsjsonClass && [nsjsonClass respondsToSelector:nsjsonSelect]) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[nsjsonClass methodSignatureForSelector:nsjsonSelect]];
invocation.target = nsjsonClass;
invocation.selector = nsjsonSelect;
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
if (!jsonData)
return nil;
[invocation setArgument:&jsonData atIndex:2]; // arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation
NSUInteger readOptions = kNilOptions;
[invocation setArgument:&readOptions atIndex:3];
[invocation setArgument:&error atIndex:4];
[invocation invoke];
[invocation getReturnValue:&feedResult];
} else if (jsonKitSelector && [jsonString respondsToSelector:jsonKitSelector]) {
// first try JSONkit
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[jsonString methodSignatureForSelector:jsonKitSelector]];
invocation.target = jsonString;
invocation.selector = jsonKitSelector;
int parseOptions = 0;
[invocation setArgument:&parseOptions atIndex:2]; // arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation
[invocation setArgument:&error atIndex:3];
[invocation invoke];
[invocation getReturnValue:&feedResult];
} else if (sbJSONSelector && [jsonString respondsToSelector:sbJSONSelector]) {
// now try SBJson
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[jsonString methodSignatureForSelector:sbJSONSelector]];
invocation.target = jsonString;
invocation.selector = sbJSONSelector;
[invocation invoke];
[invocation getReturnValue:&feedResult];
} else if (yajlSelector && [jsonString respondsToSelector:yajlSelector]) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[jsonString methodSignatureForSelector:yajlSelector]];
invocation.target = jsonString;
invocation.selector = yajlSelector;
NSUInteger yajlParserOptions = 0;
[invocation setArgument:&yajlParserOptions atIndex:2]; // arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation
[invocation setArgument:&error atIndex:3];
[invocation invoke];
[invocation getReturnValue:&feedResult];
} else {
error = [NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIServerReturnedEmptyResponse
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"You need a JSON Framework in your runtime for iOS4!", NSLocalizedDescriptionKey, nil]];
}
#endif
if (error) {
[self reportError:error];
return nil;
}
return feedResult;
}
#pragma mark - RequestComments
- (BOOL)shouldCheckForUpdates {
BOOL checkForUpdate = NO;
switch (self.updateSetting) {
case BITUpdateCheckStartup:
checkForUpdate = YES;
break;
case BITUpdateCheckDaily: {
NSTimeInterval dateDiff = fabs([self.lastCheck timeIntervalSinceNow]);
if (dateDiff != 0)
dateDiff = dateDiff / (60*60*24);
checkForUpdate = (dateDiff >= 1);
break;
}
case BITUpdateCheckManually:
checkForUpdate = NO;
break;
default:
break;
}
return checkForUpdate;
}
- (void)checkForAuthorization {
NSMutableString *parameter = [NSMutableString stringWithFormat:@"api/2/apps/%@", [self encodedAppIdentifier]];
[parameter appendFormat:@"?format=json&authorize=yes&app_version=%@&udid=%@&sdk=%@&sdk_version=%@&uuid=%@",
[[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] bit_URLEncodedString],
(_isAppStoreEnvironment ? @"appstore" : [[self deviceIdentifier] bit_URLEncodedString]),
BITHOCKEY_NAME,
BITHOCKEY_VERSION,
_uuid
];
// build request & send
NSString *url = [NSString stringWithFormat:@"%@%@", _updateURL, parameter];
BITHockeyLog(@"INFO: Sending api request to %@", url);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:1 timeoutInterval:10.0];
[request setHTTPMethod:@"GET"];
[request setValue:@"Hockey/iOS" forHTTPHeaderField:@"User-Agent"];
NSURLResponse *response = nil;
NSError *error = NULL;
BOOL failed = YES;
NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if ([responseData length]) {
NSString *responseString = [[[NSString alloc] initWithBytes:[responseData bytes] length:[responseData length] encoding: NSUTF8StringEncoding] autorelease];
NSDictionary *feedDict = (NSDictionary *)[self parseJSONResultString:responseString];
// server returned empty response?
if (![feedDict count]) {
[self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIServerReturnedEmptyResponse
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Server returned empty response.", NSLocalizedDescriptionKey, nil]]];
return;
} else {
BITHockeyLog(@"INFO: Received API response: %@", responseString);
NSString *token = [[feedDict objectForKey:@"authcode"] lowercaseString];
failed = NO;
if ([[self authenticationToken] compare:token] == NSOrderedSame) {
// identical token, activate this version
// store the new data
[[NSUserDefaults standardUserDefaults] setObject:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] forKey:kBITUpdateAuthorizedVersion];
[[NSUserDefaults standardUserDefaults] setObject:token forKey:kBITUpdateAuthorizedVersion];
[[NSUserDefaults standardUserDefaults] synchronize];
self.requireAuthorization = NO;
self.blockingView = nil;
// now continue with an update check right away
if (self.checkForUpdateOnLaunch) {
[self checkForUpdate];
}
} else {
// different token, block this version
BITHockeyLog(@"INFO: AUTH FAILURE: %@", [self authenticationToken]);
// store the new data
[[NSUserDefaults standardUserDefaults] setObject:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] forKey:kBITUpdateAuthorizedVersion];
[[NSUserDefaults standardUserDefaults] setObject:token forKey:kBITUpdateAuthorizedVersion];
[[NSUserDefaults standardUserDefaults] synchronize];
[self showBlockingScreen:BITHockeyLocalizedString(@"UpdateAuthorizationDenied") image:@"authorize_denied.png"];
}
}
}
if (failed) {
[self showBlockingScreen:BITHockeyLocalizedString(@"UpdateAuthorizationOffline") image:@"authorize_request.png"];
}
}
- (void)checkForUpdate {
if (_isAppStoreEnvironment && !_checkForTracker) return;
if ([self isUpdateManagerDisabled]) return;
if ([self expiryDateReached]) return;
if (self.requireAuthorization) return;
if (self.isUpdateAvailable && [self hasNewerMandatoryVersion]) {
[self showCheckForUpdateAlert];
}
[self checkForUpdateShowFeedback:NO];
}
- (void)checkForUpdateShowFeedback:(BOOL)feedback {
if (self.isCheckInProgress) return;
_showFeedback = feedback;
self.checkInProgress = YES;
// do we need to update?
if (![self shouldCheckForUpdates] && !_currentHockeyViewController) {
BITHockeyLog(@"INFO: Update not needed right now");
self.checkInProgress = NO;
return;
}
NSMutableString *parameter = [NSMutableString stringWithFormat:@"api/2/apps/%@?format=json&udid=%@&sdk=%@&sdk_version=%@&uuid=%@",
[[self encodedAppIdentifier] bit_URLEncodedString],
(_isAppStoreEnvironment ? @"appstore" : [[self deviceIdentifier] bit_URLEncodedString]),
BITHOCKEY_NAME,
BITHOCKEY_VERSION,
_uuid];
// add additional statistics if user didn't disable flag
if (_sendUsageData) {
[parameter appendFormat:@"&app_version=%@&os=iOS&os_version=%@&device=%@&lang=%@&first_start_at=%@&usage_time=%@",
[[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] bit_URLEncodedString],
[[[UIDevice currentDevice] systemVersion] bit_URLEncodedString],
[[self getDevicePlatform] bit_URLEncodedString],
[[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0] bit_URLEncodedString],
[[self installationDateString] bit_URLEncodedString],
[[self currentUsageString] bit_URLEncodedString]
];
}
if ([self checkForTracker]) {
[parameter appendFormat:@"&jmc=yes"];
}
// build request & send
NSString *url = [NSString stringWithFormat:@"%@%@", _updateURL, parameter];
BITHockeyLog(@"INFO: Sending api request to %@", url);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:1 timeoutInterval:10.0];
[request setHTTPMethod:@"GET"];
[request setValue:@"Hockey/iOS" forHTTPHeaderField:@"User-Agent"];
[request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
self.urlConnection = [[[NSURLConnection alloc] initWithRequest:request delegate:self] autorelease];
if (!_urlConnection) {
self.checkInProgress = NO;
[self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIClientCannotCreateConnection
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Url Connection could not be created.", NSLocalizedDescriptionKey, nil]]];
}
}
- (BOOL)initiateAppDownload {
if (_isAppStoreEnvironment) return NO;
if (!self.isUpdateAvailable) {
BITHockeyLog(@"WARNING: No update available. Aborting.");
return NO;
}
#if TARGET_IPHONE_SIMULATOR
UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"UpdateWarning") message:BITHockeyLocalizedString(@"UpdateSimulatorMessage") delegate:nil cancelButtonTitle:BITHockeyLocalizedString(@"HockeyOK") otherButtonTitles:nil] autorelease];
[alert show];
return NO;
#endif
NSString *extraParameter = [NSString string];
if (_sendUsageData) {
extraParameter = [NSString stringWithFormat:@"&udid=%@", [self deviceIdentifier]];
}
NSString *hockeyAPIURL = [NSString stringWithFormat:@"%@api/2/apps/%@?format=plist%@", _updateURL, [self encodedAppIdentifier], extraParameter];
NSString *iOSUpdateURL = [NSString stringWithFormat:@"itms-services://?action=download-manifest&url=%@", [hockeyAPIURL bit_URLEncodedString]];
BITHockeyLog(@"INFO: API Server Call: %@, calling iOS with %@", hockeyAPIURL, iOSUpdateURL);
BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:iOSUpdateURL]];
BITHockeyLog(@"INFO: System returned: %d", success);
return success;
}
// checks wether this app version is authorized
- (BOOL)appVersionIsAuthorized {
if (self.requireAuthorization && !_authenticationSecret) {
[self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIClientAuthorizationMissingSecret
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Authentication secret is not set but required.", NSLocalizedDescriptionKey, nil]]];
return NO;
}
if (!self.requireAuthorization) {
self.blockingView = nil;
return YES;
}
BITUpdateAuthorizationState state = [self authorizationState];
if (state == BITUpdateAuthorizationDenied) {
[self showBlockingScreen:BITHockeyLocalizedString(@"UpdateAuthorizationDenied") image:@"authorize_denied.png"];
} else if (state == BITUpdateAuthorizationAllowed) {
self.requireAuthorization = NO;
return YES;
}
return NO;
}
// begin the startup process
- (void)startManager {
if ([self isUpdateManagerDisabled]) return;
BITHockeyLog(@"INFO: Start UpdateManager");
if ([self expiryDateReached]) return;
if (![self appVersionIsAuthorized]) {
if ([self authorizationState] == BITUpdateAuthorizationPending) {
[self showBlockingScreen:BITHockeyLocalizedString(@"UpdateAuthorizationProgress") image:@"authorize_request.png"];
[self performSelector:@selector(checkForAuthorization) withObject:nil afterDelay:0.0f];
}
} else {
if ([self shouldCheckForUpdates]) {
[self performSelector:@selector(checkForUpdate) withObject:nil afterDelay:1.0f];
}
}
}
#pragma mark - NSURLRequest
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse {
NSURLRequest *newRequest = request;
if (redirectResponse) {
newRequest = nil;
}
return newRequest;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
if ([response respondsToSelector:@selector(statusCode)]) {
int statusCode = [((NSHTTPURLResponse *)response) statusCode];
if (statusCode == 404) {
[connection cancel]; // stop connecting; no more delegate messages
NSString *errorStr = [NSString stringWithFormat:@"Hockey API received HTTP Status Code %d", statusCode];
[self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIServerReturnedInvalidStatus
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:errorStr, NSLocalizedDescriptionKey, nil]]];
return;
}
}
self.receivedData = [NSMutableData data];
[_receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_receivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
self.receivedData = nil;
self.urlConnection = nil;
self.checkInProgress = NO;
[self reportError:error];
}
// api call returned, parsing
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
self.checkInProgress = NO;
if ([self.receivedData length]) {
NSString *responseString = [[[NSString alloc] initWithBytes:[_receivedData bytes] length:[_receivedData length] encoding: NSUTF8StringEncoding] autorelease];
BITHockeyLog(@"INFO: Received API response: %@", responseString);
id json = [self parseJSONResultString:responseString];
self.trackerConfig = (([self checkForTracker] && [[json valueForKey:@"tracker"] isKindOfClass:[NSDictionary class]]) ? [json valueForKey:@"tracker"] : nil);
if (!_isAppStoreEnvironment) {
NSArray *feedArray = (NSArray *)([self checkForTracker] ? [json valueForKey:@"versions"] : json);
self.receivedData = nil;
self.urlConnection = nil;
// remember that we just checked the server
self.lastCheck = [NSDate date];
// server returned empty response?
if (![feedArray count]) {
BITHockeyLog(@"WARNING: No versions available for download on HockeyApp.");
return;
} else {
_lastCheckFailed = NO;
}
NSString *currentAppCacheVersion = [[[self newestAppVersion].version copy] autorelease];
// clear cache and reload with new data
NSMutableArray *tmpAppVersions = [NSMutableArray arrayWithCapacity:[feedArray count]];
for (NSDictionary *dict in feedArray) {
BITAppVersionMetaInfo *appVersionMetaInfo = [BITAppVersionMetaInfo appVersionMetaInfoFromDict:dict];
if ([appVersionMetaInfo isValid]) {
[tmpAppVersions addObject:appVersionMetaInfo];
} else {
[self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIServerReturnedInvalidData
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Invalid data received from server.", NSLocalizedDescriptionKey, nil]]];
}
}
// only set if different!
if (![self.appVersions isEqualToArray:tmpAppVersions]) {
self.appVersions = [[tmpAppVersions copy] autorelease];
}
[self saveAppCache];
[self checkUpdateAvailable];
BOOL newVersionDiffersFromCachedVersion = ![self.newestAppVersion.version isEqualToString:currentAppCacheVersion];
// show alert if we are on the latest & greatest
if (_showFeedback && !self.isUpdateAvailable) {
// use currentVersionString, as version still may differ (e.g. server: 1.2, client: 1.3)
NSString *versionString = [self currentAppVersion];
NSString *shortVersionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
shortVersionString = shortVersionString ? [NSString stringWithFormat:@"%@ ", shortVersionString] : @"";
versionString = [shortVersionString length] ? [NSString stringWithFormat:@"(%@)", versionString] : versionString;
NSString *currentVersionString = [NSString stringWithFormat:@"%@ %@ %@%@", self.newestAppVersion.name, BITHockeyLocalizedString(@"UpdateVersion"), shortVersionString, versionString];
NSString *alertMsg = [NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateNoUpdateAvailableMessage"), currentVersionString];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"UpdateNoUpdateAvailableTitle")
message:alertMsg
delegate:nil
cancelButtonTitle:BITHockeyLocalizedString(@"HockeyOK")
otherButtonTitles:nil];
[alert show];
[alert release];
}
if (self.isUpdateAvailable && (self.alwaysShowUpdateReminder || newVersionDiffersFromCachedVersion || [self hasNewerMandatoryVersion])) {
if (_updateAvailable && !_currentHockeyViewController) {
[self showCheckForUpdateAlert];
}
}
_showFeedback = NO;
}
} else {
[self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIServerReturnedEmptyResponse
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Server returned an empty response.", NSLocalizedDescriptionKey, nil]]];
}
}
- (BOOL)hasNewerMandatoryVersion {
BOOL result = NO;
for (BITAppVersionMetaInfo *appVersion in self.appVersions) {
if ([appVersion.version isEqualToString:self.currentAppVersion] || [appVersion.version bit_versionCompare:self.currentAppVersion] == NSOrderedAscending) {
break;
}
if ([appVersion.mandatory boolValue]) {
result = YES;
}
}
return result;
}
#pragma mark - Properties
- (void)setCurrentHockeyViewController:(BITUpdateViewController *)aCurrentHockeyViewController {
if (_currentHockeyViewController != aCurrentHockeyViewController) {
[_currentHockeyViewController release];
_currentHockeyViewController = [aCurrentHockeyViewController retain];
//HockeySDKLog(@"active hockey view controller: %@", aCurrentHockeyViewController);
}
}
- (void)setCheckForUpdateOnLaunch:(BOOL)flag {
if (_checkForUpdateOnLaunch != flag) {
_checkForUpdateOnLaunch = flag;
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
if (flag) {
[dnc addObserver:self selector:@selector(checkForUpdate) name:UIApplicationDidBecomeActiveNotification object:nil];
} else {
[dnc removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
}
}
}
- (NSString *)currentAppVersion {
return _currentAppVersion;
}
- (void)setLastCheck:(NSDate *)aLastCheck {
if (_lastCheck != aLastCheck) {
[_lastCheck release];
_lastCheck = [aLastCheck copy];
[[NSUserDefaults standardUserDefaults] setObject:_lastCheck forKey:kBITUpdateDateOfLastCheck];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
- (void)setAppVersions:(NSArray *)anAppVersions {
if (_appVersions != anAppVersions || !_appVersions) {
[_appVersions release];
[self willChangeValueForKey:@"appVersions"];
// populate with default values (if empty)
if (![anAppVersions count]) {
BITAppVersionMetaInfo *defaultApp = [[[BITAppVersionMetaInfo alloc] init] autorelease];
defaultApp.name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
defaultApp.version = _currentAppVersion;
defaultApp.shortVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
_appVersions = [[NSArray arrayWithObject:defaultApp] retain];
} else {
_appVersions = [anAppVersions copy];
}
[self didChangeValueForKey:@"appVersions"];
}
}
- (BITAppVersionMetaInfo *)newestAppVersion {
BITAppVersionMetaInfo *appVersion = [_appVersions objectAtIndex:0];
return appVersion;
}
- (void)setBlockingView:(UIView *)anBlockingView {
if (_blockingView != anBlockingView) {
[_blockingView removeFromSuperview];
[_blockingView release];
_blockingView = [anBlockingView retain];
}
}
#pragma mark - UIAlertViewDelegate
// invoke the selected action from the actionsheet for a location element
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
if ([alertView tag] == 2) {
(void)[self initiateAppDownload];
_updateAlertShowing = NO;
return;
} else if ([alertView tag] == 1) {
[self alertFallback:[alertView message]];
return;
}
_updateAlertShowing = NO;
if (buttonIndex == [alertView firstOtherButtonIndex]) {
// YES button has been clicked
[self showUpdateView];
} else if (buttonIndex == [alertView firstOtherButtonIndex] + 1) {
// YES button has been clicked
(void)[self initiateAppDownload];
}
}
@end