/* * Author: Andreas Linde * 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 #import #import #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(@"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; // 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(@"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(@"this should not be called from an app store build."); return; } if (_currentHockeyViewController) { BITHockeyLog(@"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(@"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 (!_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(@"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(@"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(@"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 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(@"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(@"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(@"API Server Call: %@, calling iOS with %@", hockeyAPIURL, iOSUpdateURL); BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:iOSUpdateURL]]; BITHockeyLog(@"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 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(@"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]) { [self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain code:BITUpdateAPIServerReturnedEmptyResponse userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Server returned empty response.", NSLocalizedDescriptionKey, nil]]]; 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