Swiftgram/Classes/BITUpdateManager.m
Andreas Linde 14a8cd332a Add workaround for iOS 8 update process issue
In iOS 8 the app is not moved to background once the users accepts the iOS install alert button. Without this, the install process doesn't start. So for now we exit the app once that alert disappears.

Important: The iOS dialog offers the user to deny installation, we can't find out which button was tapped, so we assume the user agreed.
2014-09-10 12:52:09 +02:00

1092 lines
42 KiB
Objective-C

/*
* Author: Andreas Linde <mail@andreaslinde.de>
* Peter Steinberger
*
* Copyright (c) 2012-2014 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 "HockeySDK.h"
#if HOCKEYSDK_FEATURE_UPDATES
#import <sys/sysctl.h>
#import "HockeySDKPrivate.h"
#import "BITHockeyHelper.h"
#import "BITHockeyBaseManagerPrivate.h"
#import "BITUpdateManagerPrivate.h"
#import "BITUpdateViewControllerPrivate.h"
#import "BITAppVersionMetaInfo.h"
typedef NS_ENUM(NSInteger, BITUpdateAlertViewTag) {
BITUpdateAlertViewTagDefaultUpdate = 0,
BITUpdateAlertViewTagNeverEndingAlertView = 1,
BITUpdateAlertViewTagMandatoryUpdate = 2,
};
@implementation BITUpdateManager {
NSString *_currentAppVersion;
BITUpdateViewController *_currentHockeyViewController;
BOOL _dataFound;
BOOL _showFeedback;
BOOL _updateAlertShowing;
BOOL _lastCheckFailed;
BOOL _sendUsageData;
NSFileManager *_fileManager;
NSString *_updateDir;
NSString *_usageDataFile;
id _appDidBecomeActiveObserver;
id _appDidEnterBackgroundObserver;
id _networkDidBecomeReachableObserver;
BOOL _didStartUpdateProcess;
BOOL _didEnterBackgroundState;
BOOL _firstStartAfterInstall;
NSNumber *_versionID;
NSString *_versionUUID;
NSString *_uuid;
NSString *_blockingScreenMessage;
NSDate *_lastUpdateCheckFromBlockingScreen;
}
#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];
_showFeedback = NO;
}
}
- (void)didBecomeActiveActions {
if ([self isUpdateManagerDisabled]) return;
// this is a special iOS 8 case for handling the case that the app is not moved to background
// once the users accepts the iOS install alert button. Without this, the install process doesn't start.
//
// Important: The iOS dialog offers the user to deny installation, we can't find out which button
// was tapped, so we assume the user agreed
if (_didStartUpdateProcess) {
_didStartUpdateProcess = NO;
// we only care about iOS 8 or later
if (bit_isPreiOS8Environment()) return;
// for now we simply exit the app, later SDK versions might optionally show an alert with localized text
// describing the user to press the home button to start the update process
exit(0);
}
if (!_didEnterBackgroundState) return;
_didEnterBackgroundState = NO;
[self checkExpiryDateReached];
if ([self expiryDateReached]) return;
[self startUsage];
if (_checkForUpdateOnLaunch) {
[self checkForUpdate];
}
}
- (void)didEnterBackgroundActions {
_didEnterBackgroundState = NO;
if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) {
_didEnterBackgroundState = YES;
}
}
#pragma mark - Observers
- (void) registerObservers {
__weak typeof(self) weakSelf = self;
if(nil == _appDidEnterBackgroundObserver) {
_appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification *note) {
typeof(self) strongSelf = weakSelf;
[strongSelf didEnterBackgroundActions];
}];
}
if(nil == _appDidBecomeActiveObserver) {
_appDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification
object:nil
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification *note) {
typeof(self) strongSelf = weakSelf;
[strongSelf didBecomeActiveActions];
}];
}
if(nil == _networkDidBecomeReachableObserver) {
_networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification
object:nil
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification *note) {
typeof(self) strongSelf = weakSelf;
[strongSelf didBecomeActiveActions];
}];
}
}
- (void) unregisterObservers {
if(_appDidEnterBackgroundObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:_appDidEnterBackgroundObserver];
_appDidEnterBackgroundObserver = nil;
}
if(_appDidBecomeActiveObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:_appDidBecomeActiveObserver];
_appDidBecomeActiveObserver = nil;
}
if(_networkDidBecomeReachableObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:_networkDidBecomeReachableObserver];
_networkDidBecomeReachableObserver = nil;
}
}
#pragma mark - Expiry
- (BOOL)expiryDateReached {
if ([self 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 = bit_appName(BITHockeyLocalizedString(@"HockeyAppNamePlaceholder"));
if (!_blockingScreenMessage)
_blockingScreenMessage = [NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateExpired"), appName];
[self showBlockingScreen:_blockingScreenMessage image:@"authorize_denied.png"];
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(didDisplayExpiryAlertForUpdateManager:)]) {
[self.delegate didDisplayExpiryAlertForUpdateManager:self];
}
// the UI is now blocked, make sure we don't add our UI on top of it over and over again
[self unregisterObservers];
}
}
#pragma mark - Usage
- (void)loadAppVersionUsageData {
self.currentAppVersionUsageTime = @0;
if ([self expiryDateReached]) return;
BOOL newVersion = NO;
if (![[NSUserDefaults standardUserDefaults] valueForKey:kBITUpdateUsageTimeForUUID]) {
newVersion = YES;
} else {
if ([(NSString *)[[NSUserDefaults standardUserDefaults] valueForKey:kBITUpdateUsageTimeForUUID] compare:_uuid] != NSOrderedSame) {
newVersion = YES;
}
}
if (newVersion) {
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble:[[NSDate date] timeIntervalSinceReferenceDate]] forKey:kBITUpdateDateOfVersionInstallation];
[[NSUserDefaults standardUserDefaults] setObject:_uuid forKey:kBITUpdateUsageTimeForUUID];
[self storeUsageTimeForCurrentVersion:[NSNumber numberWithDouble:0]];
} else {
if (![_fileManager fileExistsAtPath:_usageDataFile])
return;
NSData *codedData = [[NSData alloc] initWithContentsOfFile:_usageDataFile];
if (codedData == nil) return;
NSKeyedUnarchiver *unarchiver = nil;
@try {
unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData];
}
@catch (NSException *exception) {
return;
}
if ([unarchiver containsValueForKey:kBITUpdateUsageTimeOfCurrentVersion]) {
self.currentAppVersionUsageTime = [unarchiver decodeObjectForKey:kBITUpdateUsageTimeOfCurrentVersion];
}
[unarchiver finishDecoding];
}
}
- (void)startUsage {
if ([self expiryDateReached]) return;
self.usageStartTimestamp = [NSDate date];
}
- (void)stopUsage {
if ([self expiryDateReached]) return;
double timeDifference = [[NSDate date] timeIntervalSinceReferenceDate] - [_usageStartTimestamp timeIntervalSinceReferenceDate];
double previousTimeDifference = [self.currentAppVersionUsageTime doubleValue];
[self storeUsageTimeForCurrentVersion:[NSNumber numberWithDouble:previousTimeDifference + timeDifference]];
}
- (void) storeUsageTimeForCurrentVersion:(NSNumber *)usageTime {
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:usageTime forKey:kBITUpdateUsageTimeOfCurrentVersion];
[archiver finishEncoding];
[data writeToFile:_usageDataFile atomically:YES];
self.currentAppVersionUsageTime = usageTime;
}
- (NSString *)currentUsageString {
double currentUsageTime = [self.currentAppVersionUsageTime doubleValue];
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];
[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 - Cache
- (void)checkUpdateAvailable {
// check if there is an update available
NSComparisonResult comparissonResult = bit_versionCompare(self.newestAppVersion.version, self.currentAppVersion);
if (comparissonResult == NSOrderedDescending) {
self.updateAvailable = YES;
} else if (comparissonResult == NSOrderedSame) {
// compare using the binary UUID and stored version id
self.updateAvailable = NO;
if (_firstStartAfterInstall) {
if ([self.newestAppVersion hasUUID:_uuid]) {
_versionUUID = [_uuid copy];
_versionID = [self.newestAppVersion.versionID copy];
[self saveAppCache];
} else {
[self.appVersions enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (idx > 0 && [obj isKindOfClass:[BITAppVersionMetaInfo class]]) {
NSComparisonResult compareVersions = bit_versionCompare([(BITAppVersionMetaInfo *)obj version], self.currentAppVersion);
BOOL uuidFound = [(BITAppVersionMetaInfo *)obj hasUUID:_uuid];
if (uuidFound) {
_versionUUID = [_uuid copy];
_versionID = [[(BITAppVersionMetaInfo *)obj versionID] copy];
[self saveAppCache];
self.updateAvailable = YES;
}
if (compareVersions != NSOrderedSame || uuidFound) {
*stop = YES;
}
}
}];
}
} else {
if ([self.newestAppVersion.versionID compare:_versionID] == NSOrderedDescending)
self.updateAvailable = YES;
}
}
}
- (void)loadAppCache {
_firstStartAfterInstall = NO;
_versionUUID = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateInstalledUUID];
if (!_versionUUID) {
_firstStartAfterInstall = YES;
} else {
if ([_uuid compare:_versionUUID] != NSOrderedSame)
_firstStartAfterInstall = YES;
}
_versionID = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateInstalledVersionID];
_companyName = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateCurrentCompanyName];
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 {
if (_companyName)
[[NSUserDefaults standardUserDefaults] setObject:_companyName forKey:kBITUpdateCurrentCompanyName];
if (_versionUUID)
[[NSUserDefaults standardUserDefaults] setObject:_versionUUID forKey:kBITUpdateInstalledUUID];
if (_versionID)
[[NSUserDefaults standardUserDefaults] setObject:_versionID forKey:kBITUpdateInstalledVersionID];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.appVersions];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:kBITUpdateArrayOfLastCheck];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#pragma mark - Init
- (id)init {
if ((self = [super init])) {
_delegate = nil;
_expiryDate = nil;
_checkInProgress = NO;
_dataFound = NO;
_updateAvailable = NO;
_lastCheckFailed = NO;
_currentAppVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
_blockingView = nil;
_lastCheck = nil;
_uuid = [[self executableUUID] copy];
_versionUUID = nil;
_versionID = nil;
_sendUsageData = YES;
_disableUpdateManager = NO;
_checkForTracker = NO;
_firstStartAfterInstall = NO;
_companyName = nil;
_currentAppVersionUsageTime = @0;
// set defaults
self.showDirectInstallOption = NO;
self.alwaysShowUpdateReminder = YES;
self.checkForUpdateOnLaunch = YES;
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);
}
_fileManager = [[NSFileManager alloc] init];
// temporary directory for crashes grabbed from PLCrashReporter
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
_updateDir = [[paths objectAtIndex:0] stringByAppendingPathComponent:BITHOCKEY_IDENTIFIER];
if (![_fileManager fileExistsAtPath:_updateDir]) {
NSDictionary *attributes = [NSDictionary dictionaryWithObject: [NSNumber numberWithUnsignedLong: 0755] forKey: NSFilePosixPermissions];
NSError *theError = NULL;
[_fileManager createDirectoryAtPath:_updateDir withIntermediateDirectories: YES attributes: attributes error: &theError];
}
_usageDataFile = [_updateDir stringByAppendingPathComponent:BITHOCKEY_USAGE_DATA];
[self loadAppCache];
_installationIdentification = [self stringValueFromKeychainForKey:kBITUpdateInstallationIdentification];
[self loadAppVersionUsageData];
[self startUsage];
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
[dnc addObserver:self selector:@selector(stopUsage) name:UIApplicationWillTerminateNotification object:nil];
[dnc addObserver:self selector:@selector(stopUsage) name:UIApplicationWillResignActiveNotification object:nil];
}
return self;
}
- (void)dealloc {
[self unregisterObservers];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillTerminateNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
[_urlConnection cancel];
}
#pragma mark - BetaUpdateUI
- (BITUpdateViewController *)hockeyViewController:(BOOL)modal {
if ([self isAppStoreEnvironment]) {
NSLog(@"[HockeySDK] This should not be called from an app store build!");
// return an empty view controller instead
BITHockeyBaseViewController *blankViewController = [[BITHockeyBaseViewController alloc] initWithModalStyle:modal];
return (BITUpdateViewController *)blankViewController;
}
return [[BITUpdateViewController alloc] initWithModalStyle:modal];
}
- (void)showUpdateView {
if ([self isAppStoreEnvironment]) {
NSLog(@"[HockeySDK] This should not be called from an app store build!");
return;
}
if (_currentHockeyViewController) {
BITHockeyLog(@"INFO: Update view already visible, aborting");
return;
}
if ([self isPreiOS7Environment])
self.barStyle = UIBarStyleBlack;
BITUpdateViewController *updateViewController = [self hockeyViewController:YES];
if ([self hasNewerMandatoryVersion] || [self expiryDateReached]) {
[updateViewController setMandatoryUpdate: YES];
}
[self showView:updateViewController];
}
- (void)showCheckForUpdateAlert {
if ([self 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:nil
otherButtonTitles:BITHockeyLocalizedString(@"UpdateShow"), BITHockeyLocalizedString(@"UpdateInstall"), nil
];
[alertView setTag:BITUpdateAlertViewTagMandatoryUpdate];
[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
];
if (self.isShowingDirectInstallOption) {
[alertView addButtonWithTitle:BITHockeyLocalizedString(@"UpdateInstall")];
}
[alertView setTag:BITUpdateAlertViewTagDefaultUpdate];
[alertView show];
_updateAlertShowing = YES;
}
}
}
// 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];
UIImageView *backgroundView = [[UIImageView alloc] initWithImage:bit_imageNamed(@"bg.png", BITHOCKEYSDK_BUNDLE)];
backgroundView.contentMode = UIViewContentModeScaleAspectFill;
backgroundView.frame = frame;
[self.blockingView addSubview:backgroundView];
if (image != nil) {
UIImageView *imageView = [[UIImageView alloc] initWithImage:bit_imageNamed(image, BITHOCKEYSDK_BUNDLE)];
imageView.contentMode = UIViewContentModeCenter;
imageView.frame = frame;
[self.blockingView addSubview:imageView];
}
if (!self.disableUpdateCheckOptionWhenExpired) {
UIButton *checkForUpdateButton = [UIButton buttonWithType:kBITButtonTypeSystem];
checkForUpdateButton.frame = CGRectMake((frame.size.width - 140) / 2.f, frame.size.height - 100, 140, 25);
[checkForUpdateButton setTitle:BITHockeyLocalizedString(@"UpdateButtonCheck") forState:UIControlStateNormal];
[checkForUpdateButton addTarget:self
action:@selector(checkForUpdateForExpiredVersion)
forControlEvents:UIControlEventTouchUpInside];
[self.blockingView addSubview:checkForUpdateButton];
}
if (message != nil) {
frame.origin.x = 20;
frame.origin.y = frame.size.height - 180;
frame.size.width -= 40;
frame.size.height = 70;
UILabel *label = [[UILabel alloc] initWithFrame:frame];
label.text = message;
label.textAlignment = kBITTextLabelAlignmentCenter;
label.numberOfLines = 3;
label.adjustsFontSizeToFitWidth = YES;
label.backgroundColor = [UIColor clearColor];
[self.blockingView addSubview:label];
}
[visibleWindow addSubview:self.blockingView];
}
- (void)checkForUpdateForExpiredVersion {
if (!self.checkInProgress) {
if (!_lastUpdateCheckFromBlockingScreen ||
abs([NSDate timeIntervalSinceReferenceDate] - [_lastUpdateCheckFromBlockingScreen timeIntervalSinceReferenceDate]) > 60) {
_lastUpdateCheckFromBlockingScreen = [NSDate date];
[self checkForUpdateShowFeedback:NO];
}
}
}
// 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
];
if (!self.disableUpdateCheckOptionWhenExpired && [message isEqualToString:_blockingScreenMessage]) {
[alertView addButtonWithTitle:BITHockeyLocalizedString(@"UpdateButtonCheck")];
}
[alertView setTag:BITUpdateAlertViewTagNeverEndingAlertView];
[alertView show];
}
#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)checkForUpdate {
if (![self isAppStoreEnvironment] && ![self isUpdateManagerDisabled]) {
if ([self expiryDateReached]) return;
if (![self installationIdentified]) return;
if (self.isUpdateAvailable && [self hasNewerMandatoryVersion]) {
[self showCheckForUpdateAlert];
}
[self checkForUpdateShowFeedback:NO];
} else if ([self checkForTracker]) {
[self checkForUpdateShowFeedback:NO];
}
}
- (void)checkForUpdateShowFeedback:(BOOL)feedback {
if ([self isAppStoreEnvironment]) return;
if (self.isCheckInProgress) return;
_showFeedback = feedback;
self.checkInProgress = YES;
// do we need to update?
if (![self checkForTracker] && !_currentHockeyViewController && ![self shouldCheckForUpdates] && _updateSetting != BITUpdateCheckManually) {
BITHockeyLog(@"INFO: Update not needed right now");
self.checkInProgress = NO;
return;
}
NSMutableString *parameter = [NSMutableString stringWithFormat:@"api/2/apps/%@?format=json&extended=true%@&sdk=%@&sdk_version=%@&uuid=%@",
bit_URLEncodedString([self encodedAppIdentifier]),
([self isAppStoreEnvironment] ? @"&udid=appstore" : @""),
BITHOCKEY_NAME,
BITHOCKEY_VERSION,
_uuid];
// add installationIdentificationType and installationIdentifier if available
if (self.installationIdentification && self.installationIdentificationType) {
[parameter appendFormat:@"&%@=%@",
bit_URLEncodedString(self.installationIdentificationType),
bit_URLEncodedString(self.installationIdentification)
];
}
// 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=%@",
bit_URLEncodedString([[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])
];
}
if ([self checkForTracker]) {
[parameter appendFormat:@"&jmc=yes"];
}
// build request & send
NSString *url = [NSString stringWithFormat:@"%@%@", self.serverURL, 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];
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 ([self 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];
[alert show];
return NO;
#endif
NSString *extraParameter = [NSString string];
if (_sendUsageData && self.installationIdentification && self.installationIdentificationType) {
extraParameter = [NSString stringWithFormat:@"&%@=%@",
bit_URLEncodedString(self.installationIdentificationType),
bit_URLEncodedString(self.installationIdentification)
];
}
NSString *hockeyAPIURL = [NSString stringWithFormat:@"%@api/2/apps/%@?format=plist%@", self.serverURL, [self encodedAppIdentifier], extraParameter];
NSString *iOSUpdateURL = [NSString stringWithFormat:@"itms-services://?action=download-manifest&url=%@", bit_URLEncodedString(hockeyAPIURL)];
BITHockeyLog(@"INFO: API Server Call: %@, calling iOS with %@", hockeyAPIURL, iOSUpdateURL);
BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:iOSUpdateURL]];
BITHockeyLog(@"INFO: System returned: %d", success);
_didStartUpdateProcess = success;
return success;
}
// begin the startup process
- (void)startManager {
if (![self isAppStoreEnvironment]) {
if ([self isUpdateManagerDisabled]) return;
BITHockeyLog(@"INFO: Starting UpdateManager");
[self checkExpiryDateReached];
if (![self expiryDateReached]) {
if ([self checkForTracker] || ([self isCheckForUpdateOnLaunch] && [self shouldCheckForUpdates])) {
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) return;
[self performSelector:@selector(checkForUpdate) withObject:nil afterDelay:1.0f];
}
}
} else {
if ([self checkForTracker]) {
// if we are in the app store, make sure not to send usage information in any case for now
_sendUsageData = NO;
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) return;
[self performSelector:@selector(checkForUpdate) withObject:nil afterDelay:1.0f];
}
}
[self registerObservers];
}
#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)]) {
NSInteger 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 %ld", (long)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;
if ([self expiryDateReached]) {
if (!self.blockingView) {
[self alertFallback:_blockingScreenMessage];
}
} else {
[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];
BITHockeyLog(@"INFO: Received API response: %@", responseString);
if (!responseString || ![responseString dataUsingEncoding:NSUTF8StringEncoding]) {
self.receivedData = nil;
self.urlConnection = nil;
return;
}
NSError *error = nil;
NSDictionary *json = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error];
self.trackerConfig = (([self checkForTracker] && [[json valueForKey:@"tracker"] isKindOfClass:[NSDictionary class]]) ? [json valueForKey:@"tracker"] : nil);
self.companyName = (([[json valueForKey:@"company"] isKindOfClass:[NSString class]]) ? [json valueForKey:@"company"] : nil);
if (![self isAppStoreEnvironment]) {
NSArray *feedArray = (NSArray *)[json valueForKey:@"versions"];
// 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.");
self.receivedData = nil;
self.urlConnection = nil;
return;
} else {
_lastCheckFailed = NO;
}
NSString *currentAppCacheVersion = [[self newestAppVersion].version copy];
// 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]) {
// check if minOSVersion is set and this device qualifies
BOOL deviceOSVersionQualifies = YES;
if ([appVersionMetaInfo minOSVersion] && ![[appVersionMetaInfo minOSVersion] isKindOfClass:[NSNull class]]) {
NSComparisonResult comparissonResult = bit_versionCompare(appVersionMetaInfo.minOSVersion, [[UIDevice currentDevice] systemVersion]);
if (comparissonResult == NSOrderedDescending) {
deviceOSVersionQualifies = NO;
}
}
if (deviceOSVersionQualifies)
[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];
}
[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];
}
if (self.isUpdateAvailable && (self.alwaysShowUpdateReminder || newVersionDiffersFromCachedVersion || [self hasNewerMandatoryVersion])) {
if (_updateAvailable && !_currentHockeyViewController) {
[self showCheckForUpdateAlert];
}
}
_showFeedback = NO;
}
} else if (![self expiryDateReached]) {
[self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIServerReturnedEmptyResponse
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Server returned an empty response.", NSLocalizedDescriptionKey, nil]]];
}
if (!_updateAlertShowing && [self expiryDateReached] && !self.blockingView) {
[self alertFallback:_blockingScreenMessage];
}
self.receivedData = nil;
self.urlConnection = nil;
}
- (BOOL)hasNewerMandatoryVersion {
BOOL result = NO;
for (BITAppVersionMetaInfo *appVersion in self.appVersions) {
if ([appVersion.version isEqualToString:self.currentAppVersion] || bit_versionCompare(appVersion.version, self.currentAppVersion) == NSOrderedAscending) {
break;
}
if ([appVersion.mandatory boolValue]) {
result = YES;
}
}
return result;
}
#pragma mark - Properties
- (void)setCurrentHockeyViewController:(BITUpdateViewController *)aCurrentHockeyViewController {
if (_currentHockeyViewController != aCurrentHockeyViewController) {
_currentHockeyViewController = aCurrentHockeyViewController;
//HockeySDKLog(@"active hockey view controller: %@", aCurrentHockeyViewController);
}
}
- (NSString *)currentAppVersion {
return _currentAppVersion;
}
- (void)setLastCheck:(NSDate *)aLastCheck {
if (_lastCheck != aLastCheck) {
_lastCheck = [aLastCheck copy];
[[NSUserDefaults standardUserDefaults] setObject:_lastCheck forKey:kBITUpdateDateOfLastCheck];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
- (void)setAppVersions:(NSArray *)anAppVersions {
if (_appVersions != anAppVersions || !_appVersions) {
[self willChangeValueForKey:@"appVersions"];
// populate with default values (if empty)
if (![anAppVersions count]) {
BITAppVersionMetaInfo *defaultApp = [[BITAppVersionMetaInfo alloc] init];
defaultApp.name = bit_appName(BITHockeyLocalizedString(@"HockeyAppNamePlaceholder"));
defaultApp.version = _currentAppVersion;
defaultApp.shortVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
_appVersions = [NSArray arrayWithObject:defaultApp];
} 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 = anBlockingView;
}
}
- (void)setInstallationIdentificationType:(NSString *)installationIdentificationType {
if (![_installationIdentificationType isEqualToString:installationIdentificationType]) {
// we already use "uuid" in our requests for providing the binary UUID to the server
// so we need to stick to "udid" even when BITAuthenticator is providing a plain uuid
if ([installationIdentificationType isEqualToString:@"uuid"]) {
_installationIdentificationType = @"udid";
} else {
_installationIdentificationType = installationIdentificationType;
}
}
}
- (void)setInstallationIdentification:(NSString *)installationIdentification {
if (![_installationIdentification isEqualToString:installationIdentification]) {
if (installationIdentification) {
[self addStringValueToKeychain:installationIdentification forKey:kBITUpdateInstallationIdentification];
} else {
[self removeKeyFromKeychain:kBITUpdateInstallationIdentification];
}
_installationIdentification = installationIdentification;
// we need to reset the usage time, because the user/device may have changed
[self storeUsageTimeForCurrentVersion:[NSNumber numberWithDouble:0]];
self.usageStartTimestamp = [NSDate date];
}
}
#pragma mark - UIAlertViewDelegate
// invoke the selected action from the action sheet for a location element
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
if ([alertView tag] == BITUpdateAlertViewTagNeverEndingAlertView) {
if (buttonIndex == 1) {
[self checkForUpdateForExpiredVersion];
} else {
[self alertFallback:_blockingScreenMessage];
}
return;
}
_updateAlertShowing = NO;
if (buttonIndex == [alertView firstOtherButtonIndex]) {
// YES button has been clicked
if (self.blockingView) {
[self.blockingView removeFromSuperview];
}
[self showUpdateView];
} else if (buttonIndex == [alertView firstOtherButtonIndex] + 1) {
// YES button has been clicked
(void)[self initiateAppDownload];
} else {
if ([self expiryDateReached] && !self.blockingView) {
[self alertFallback:_blockingScreenMessage];
}
}
}
@end
#endif /* HOCKEYSDK_FEATURE_UPDATES */