2019-09-09 01:05:40 +04:00

1153 lines
49 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 "BITHockeyHelper+Application.h"
#import "BITHockeyBaseManagerPrivate.h"
#import "BITUpdateManagerPrivate.h"
#import "BITUpdateViewControllerPrivate.h"
#import "BITAppVersionMetaInfo.h"
#if HOCKEYSDK_FEATURE_CRASH_REPORTER
#import "BITCrashManagerPrivate.h"
#endif
typedef NS_ENUM(NSInteger, BITUpdateAlertViewTag) {
BITUpdateAlertViewTagDefaultUpdate = 0,
BITUpdateAlertViewTagNeverEndingAlertView = 1,
BITUpdateAlertViewTagMandatoryUpdate = 2,
};
@interface BITUpdateManager ()
@property (nonatomic, copy) NSString *currentAppVersion;
@property (nonatomic) BOOL dataFound;
@property (nonatomic) BOOL showFeedback;
@property (nonatomic) BOOL updateAlertShowing;
@property (nonatomic) BOOL lastCheckFailed;
@property (nonatomic, strong) NSFileManager *fileManager;
@property (nonatomic, copy) NSString *updateDir;
@property (nonatomic, copy) NSString *usageDataFile;
@property (nonatomic, weak) id appDidBecomeActiveObserver;
@property (nonatomic, weak) id appDidEnterBackgroundObserver;
@property (nonatomic, weak) id networkDidBecomeReachableObserver;
@property (nonatomic) BOOL didStartUpdateProcess;
@property (nonatomic) BOOL didEnterBackgroundState;
@property (nonatomic) BOOL firstStartAfterInstall;
@property (nonatomic, strong) NSNumber *versionID;
@property (nonatomic, copy) NSString *versionUUID;
@property (nonatomic, copy) NSString *uuid;
@property (nonatomic, copy) NSString *blockingScreenMessage;
@property (nonatomic, strong) NSDate *lastUpdateCheckFromBlockingScreen;
@end
@implementation BITUpdateManager
#pragma mark - private
- (void)reportError:(NSError *)error {
BITHockeyLogError(@"ERROR: %@", [error localizedDescription]);
self.lastCheckFailed = YES;
// only show error if we enable that
if (self.showFeedback) {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"An Error Occurred")
message:[error localizedDescription]
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"OK")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction __unused *action) {}];
[alertController addAction:okAction];
[self showAlertController:alertController];
self.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 (self.didStartUpdateProcess) {
self.didStartUpdateProcess = NO;
id strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(updateManagerWillExitApp:)]) {
[strongDelegate updateManagerWillExitApp:self];
}
#if HOCKEYSDK_FEATURE_CRASH_REPORTER
[[BITHockeyManager sharedHockeyManager].crashManager leavingAppSafely];
#endif
// 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 (!self.didEnterBackgroundState) return;
self.didEnterBackgroundState = NO;
[self checkExpiryDateReached];
if ([self expiryDateReached]) return;
[self startUsage];
if ([self isCheckForUpdateOnLaunch] && [self shouldCheckForUpdates]) {
[self checkForUpdate];
}
}
- (void)didEnterBackgroundActions {
self.didEnterBackgroundState = NO;
if ([BITHockeyHelper applicationState] == BITApplicationStateBackground) {
self.didEnterBackgroundState = YES;
}
}
#pragma mark - Observers
- (void) registerObservers {
__weak typeof(self) weakSelf = self;
if(nil == self.appDidEnterBackgroundObserver) {
self.appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification __unused *note) {
typeof(self) strongSelf = weakSelf;
[strongSelf didEnterBackgroundActions];
}];
}
if(nil == self.appDidBecomeActiveObserver) {
self.appDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification
object:nil
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification __unused *note) {
typeof(self) strongSelf = weakSelf;
[strongSelf didBecomeActiveActions];
}];
}
if(nil == self.networkDidBecomeReachableObserver) {
self.networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification
object:nil
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification __unused *note) {
typeof(self) strongSelf = weakSelf;
[strongSelf didBecomeActiveActions];
}];
}
}
- (void) unregisterObservers {
id strongDidEnterBackgroundObserver = self.appDidEnterBackgroundObserver;
id strongDidBecomeActiveObserver = self.appDidBecomeActiveObserver;
id strongNetworkDidBecomeReachableObserver = self.networkDidBecomeReachableObserver;
if(strongDidEnterBackgroundObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:strongDidEnterBackgroundObserver];
self.appDidEnterBackgroundObserver = nil;
}
if(strongDidBecomeActiveObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:strongDidBecomeActiveObserver];
self.appDidBecomeActiveObserver = nil;
}
if(strongNetworkDidBecomeReachableObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:strongNetworkDidBecomeReachableObserver];
self.networkDidBecomeReachableObserver = nil;
}
}
#pragma mark - Expiry
- (BOOL)expiryDateReached {
if (self.appEnvironment != BITEnvironmentOther) return NO;
if (self.expiryDate) {
NSDate *currentDate = [NSDate date];
if ([currentDate compare:self.expiryDate] != NSOrderedAscending)
return YES;
}
return NO;
}
- (void)checkExpiryDateReached {
if (![self expiryDateReached]) return;
BOOL shouldShowDefaultAlert = YES;
id strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(shouldDisplayExpiryAlertForUpdateManager:)]) {
shouldShowDefaultAlert = [strongDelegate shouldDisplayExpiryAlertForUpdateManager:self];
}
if (shouldShowDefaultAlert) {
NSString *appName = bit_appName(BITHockeyLocalizedString(@"Telegram"));
if (!self.blockingScreenMessage)
self.blockingScreenMessage = [NSString stringWithFormat:BITHockeyLocalizedString(@"Update expired"), appName];
[self showBlockingScreen:self.blockingScreenMessage image:@"authorize_denied.png"];
if ([strongDelegate respondsToSelector:@selector(didDisplayExpiryAlertForUpdateManager:)]) {
[strongDelegate 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:self.uuid] != NSOrderedSame) {
newVersion = YES;
}
}
if (newVersion) {
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble:[[NSDate date] timeIntervalSinceReferenceDate]] forKey:kBITUpdateDateOfVersionInstallation];
[[NSUserDefaults standardUserDefaults] setObject:self.uuid forKey:kBITUpdateUsageTimeForUUID];
[self storeUsageTimeForCurrentVersion:[NSNumber numberWithDouble:0]];
} else {
if (![self.fileManager fileExistsAtPath:self.usageDataFile])
return;
NSData *codedData = [[NSData alloc] initWithContentsOfFile:self.usageDataFile];
if (codedData == nil) return;
NSKeyedUnarchiver *unarchiver = nil;
@try {
unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData];
}
@catch (NSException __unused *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.appEnvironment != BITEnvironmentOther) return;
if ([self expiryDateReached]) return;
double timeDifference = [[NSDate date] timeIntervalSinceReferenceDate] - [self.usageStartTimestamp timeIntervalSinceReferenceDate];
double previousTimeDifference = [self.currentAppVersionUsageTime doubleValue];
[self storeUsageTimeForCurrentVersion:[NSNumber numberWithDouble:previousTimeDifference + timeDifference]];
}
- (void) storeUsageTimeForCurrentVersion:(NSNumber *)usageTime {
if (self.appEnvironment != BITEnvironmentOther) return;
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:usageTime forKey:kBITUpdateUsageTimeOfCurrentVersion];
[archiver finishEncoding];
[data writeToFile:self.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.0) {
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 comparisonResult = bit_versionCompare(self.newestAppVersion.version, self.currentAppVersion);
if (comparisonResult == NSOrderedDescending) {
self.updateAvailable = YES;
} else if (comparisonResult == NSOrderedSame) {
// compare using the binary UUID and stored version id
self.updateAvailable = NO;
if (self.firstStartAfterInstall) {
if ([self.newestAppVersion hasUUID:self.uuid]) {
self.versionUUID = [self.uuid copy];
self.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:self.uuid];
if (uuidFound) {
self.versionUUID = [self.uuid copy];
self.versionID = [[(BITAppVersionMetaInfo *)obj versionID] copy];
[self saveAppCache];
self.updateAvailable = YES;
}
if (compareVersions != NSOrderedSame || uuidFound) {
*stop = YES;
}
}
}];
}
} else {
if ([self.newestAppVersion.versionID compare:self.versionID] == NSOrderedDescending)
self.updateAvailable = YES;
}
}
}
- (void)loadAppCache {
self.firstStartAfterInstall = NO;
self.versionUUID = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateInstalledUUID];
if (!self.versionUUID) {
self.firstStartAfterInstall = YES;
} else {
if ([self.uuid compare:self.versionUUID] != NSOrderedSame)
self.firstStartAfterInstall = YES;
}
self.versionID = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateInstalledVersionID];
self.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 (self.companyName) {
[[NSUserDefaults standardUserDefaults] setObject:self.companyName forKey:kBITUpdateCurrentCompanyName];
}
if (self.versionUUID) {
[[NSUserDefaults standardUserDefaults] setObject:self.versionUUID forKey:kBITUpdateInstalledUUID];
}
if (self.versionID) {
[[NSUserDefaults standardUserDefaults] setObject:self.versionID forKey:kBITUpdateInstalledVersionID];
}
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.appVersions];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:kBITUpdateArrayOfLastCheck];
}
#pragma mark - Init
- (instancetype)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;
_firstStartAfterInstall = NO;
_companyName = nil;
_currentAppVersionUsageTime = @0;
// set defaults
_showDirectInstallOption = NO;
_alwaysShowUpdateReminder = YES;
_checkForUpdateOnLaunch = YES;
_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]]) {
_lastCheck = tempLastCheck;
}
}
if (!_lastCheck) {
_lastCheck = [NSDate distantPast];
}
if (!BITHockeyBundle()) {
BITHockeyLogWarning(@"[HockeySDK] WARNING: %@ is missing, make sure it is added!", BITHOCKEYSDK_BUNDLE);
}
_fileManager = [[NSFileManager alloc] init];
_usageDataFile = [bit_settingsDir() 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];
}
#pragma mark - BetaUpdateUI
- (BITUpdateViewController *)hockeyViewController:(BOOL)modal {
if (self.appEnvironment != BITEnvironmentOther) {
BITHockeyLogWarning(@"[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.appEnvironment != BITEnvironmentOther) {
BITHockeyLogWarning(@"[HockeySDK] This should not be called from an app store build!");
return;
}
if (self.currentHockeyViewController) {
BITHockeyLogDebug(@"INFO: Update view already visible, aborting");
return;
}
BITUpdateViewController *updateViewController = [self hockeyViewController:YES];
if ([self hasNewerMandatoryVersion] || [self expiryDateReached]) {
[updateViewController setMandatoryUpdate: YES];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self showView:updateViewController];
});
}
- (void)showCheckForUpdateAlert {
if (self.appEnvironment != BITEnvironmentOther) return;
if ([self isUpdateManagerDisabled]) return;
id strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(shouldDisplayUpdateAlertForUpdateManager:forShortVersion:forVersion:)] &&
![strongDelegate shouldDisplayUpdateAlertForUpdateManager:self forShortVersion:[self.newestAppVersion shortVersion] forVersion:[self.newestAppVersion version]]) {
return;
}
if (!self.updateAlertShowing) {
NSString *title = BITHockeyLocalizedString(@"Update Available");
NSString *message = [NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateAlertMandatoryTextWithAppVersion"), [self.newestAppVersion nameAndVersionString]];
if ([self hasNewerMandatoryVersion]) {
__weak typeof(self) weakSelf = self;
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *showAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"Show")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction __unused *action) {
typeof(self) strongSelf = weakSelf;
self.updateAlertShowing = NO;
if (strongSelf.blockingView) {
[strongSelf.blockingView removeFromSuperview];
}
[strongSelf showUpdateView];
}];
[alertController addAction:showAction];
UIAlertAction *installAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"Install")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction __unused *action) {
typeof(self) strongSelf = weakSelf;
self.updateAlertShowing = NO;
(void)[strongSelf initiateAppDownload];
}];
[alertController addAction:installAction];
[self showAlertController:alertController];
self.updateAlertShowing = YES;
} else {
message = [NSString stringWithFormat:BITHockeyLocalizedString(@"An update is available"), [self.newestAppVersion nameAndVersionString]];
__weak typeof(self) weakSelf = self;
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *ignoreAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"Ignore")
style:UIAlertActionStyleCancel
handler:^(UIAlertAction __unused *action) {
typeof(self) strongSelf = weakSelf;
self.updateAlertShowing = NO;
if ([strongSelf expiryDateReached] && !strongSelf.blockingView) {
[strongSelf alertFallback:self.blockingScreenMessage];
}
}];
[alertController addAction:ignoreAction];
UIAlertAction *showAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"Show")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction __unused *action) {
typeof(self) strongSelf = weakSelf;
self.updateAlertShowing = NO;
if (strongSelf.blockingView) {
[strongSelf.blockingView removeFromSuperview];
}
[strongSelf showUpdateView];
}];
[alertController addAction:showAction];
if (self.isShowingDirectInstallOption) {
UIAlertAction *installAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"Install")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction __unused *action) {
typeof(self) strongSelf = weakSelf;
self.updateAlertShowing = NO;
(void)[strongSelf initiateAppDownload];
}];
[alertController addAction:installAction];
}
[self showAlertController:alertController ];
self.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) / (CGFloat)2.0, frame.size.height - 100, 140, 25);
[checkForUpdateButton setTitle:BITHockeyLocalizedString(@"Check") 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 = NSTextAlignmentCenter;
label.numberOfLines = 3;
label.adjustsFontSizeToFitWidth = YES;
label.backgroundColor = [UIColor clearColor];
[self.blockingView addSubview:label];
}
[visibleWindow addSubview:self.blockingView];
}
- (void)checkForUpdateForExpiredVersion {
if (!self.checkInProgress) {
if (!self.lastUpdateCheckFromBlockingScreen ||
fabs([NSDate timeIntervalSinceReferenceDate] - [self.lastUpdateCheckFromBlockingScreen timeIntervalSinceReferenceDate]) > 60) {
self.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 {
__weak typeof(self) weakSelf = self;
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"OK")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction __unused *action) {
typeof(self) strongSelf = weakSelf;
[strongSelf alertFallback:self.blockingScreenMessage];
}];
[alertController addAction:okAction];
if (!self.disableUpdateCheckOptionWhenExpired && [message isEqualToString:self.blockingScreenMessage]) {
UIAlertAction *checkAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"Check")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction __unused *action) {
typeof(self) strongSelf = weakSelf;
[strongSelf checkForUpdateForExpiredVersion];
}];
[alertController addAction:checkAction];
}
[self showAlertController:alertController];
}
#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;
}
return checkForUpdate;
}
- (void)checkForUpdate {
if ((self.appEnvironment == BITEnvironmentOther) && ![self isUpdateManagerDisabled]) {
if ([self expiryDateReached]) return;
if (![self installationIdentified]) return;
if (self.isUpdateAvailable && [self hasNewerMandatoryVersion]) {
[self showCheckForUpdateAlert];
}
[self checkForUpdateShowFeedback:NO];
}
}
- (void)checkForUpdateShowFeedback:(BOOL)feedback {
if (self.appEnvironment != BITEnvironmentOther) return;
if (self.isCheckInProgress) return;
self.showFeedback = feedback;
self.checkInProgress = YES;
// do we need to update?
if (!self.currentHockeyViewController && ![self shouldCheckForUpdates] && self.updateSetting != BITUpdateCheckManually) {
BITHockeyLogDebug(@"INFO: Update not needed right now");
self.checkInProgress = NO;
return;
}
NSURLRequest *request = [self requestForUpdateCheck];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:(id<NSURLSessionDelegate>)self delegateQueue:nil];
NSURLSessionDataTask *sessionTask = [session dataTaskWithRequest:request];
if (!sessionTask) {
self.checkInProgress = NO;
[self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIClientCannotCreateConnection
userInfo:@{NSLocalizedDescriptionKey : @"Url Connection could not be created."}]];
} else {
[sessionTask resume];
}
}
- (NSURLRequest *)requestForUpdateCheck {
NSString *path = [NSString stringWithFormat:@"api/2/apps/%@", self.appIdentifier];
NSString *urlEncodedPath = [path stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]];
NSMutableString *parameters = [NSMutableString stringWithFormat:@"?format=json&extended=true&sdk=%@&sdk_version=%@&uuid=%@",
BITHOCKEY_NAME,
BITHOCKEY_VERSION,
self.uuid];
// add installationIdentificationType and installationIdentifier if available
if (self.installationIdentification && self.installationIdentificationType) {
[parameters appendFormat:@"&%@=%@",
self.installationIdentificationType,
self.installationIdentification
];
}
// add additional statistics if user didn't disable flag
if (self.sendUsageData) {
[parameters appendFormat:@"&app_version=%@&os=iOS&os_version=%@&device=%@&lang=%@&first_start_at=%@&usage_time=%@",
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"],
[[UIDevice currentDevice] systemVersion],
[self getDevicePlatform],
[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0],
[self installationDateString],
[self currentUsageString]
];
}
NSString *urlEncodedParameters = [parameters stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
// build request & send
NSString *url = [NSString stringWithFormat:@"%@%@%@", self.serverURL, urlEncodedPath, urlEncodedParameters];
BITHockeyLogDebug(@"INFO: Sending api request to %@", url);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:(NSURL *)[NSURL URLWithString:url]
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:10.0];
[request setHTTPMethod:@"GET"];
[request setValue:@"Hockey/iOS" forHTTPHeaderField:@"User-Agent"];
[request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
return request;
}
- (BOOL)initiateAppDownload {
if (self.appEnvironment != BITEnvironmentOther) return NO;
if (!self.isUpdateAvailable) {
BITHockeyLogWarning(@"WARNING: No update available. Aborting.");
return NO;
}
#if TARGET_OS_SIMULATOR
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"Warning")
message:BITHockeyLocalizedString(@"UpdateSimulatorMessage")
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"OK")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction __unused *action) {}];
[alertController addAction:okAction];
[self showAlertController:alertController];
return NO;
#else
NSString *extraParameter = [NSString string];
if (self.sendUsageData && self.installationIdentification && self.installationIdentificationType) {
extraParameter = [NSString stringWithFormat:@"&%@=%@",
bit_URLEncodedString(self.installationIdentificationType),
bit_URLEncodedString(self.installationIdentification)
];
}
NSString *hockeyAPIURL = [NSString stringWithFormat:@"%@api/2/apps/%@/app_versions/%@?format=plist%@", self.serverURL, [self encodedAppIdentifier], [self.newestAppVersion.versionID stringValue], extraParameter];
NSString *iOSUpdateURL = [NSString stringWithFormat:@"itms-services://?action=download-manifest&url=%@", bit_URLEncodedString(hockeyAPIURL)];
// Notify delegate of update intent before placing the call
id stronDelegate = self.delegate;
if ([stronDelegate respondsToSelector:@selector(willStartDownloadAndUpdate:)]) {
[stronDelegate willStartDownloadAndUpdate:self];
}
BITHockeyLogDebug(@"INFO: API Server Call: %@, calling iOS with %@", hockeyAPIURL, iOSUpdateURL);
BOOL success = [[UIApplication sharedApplication] openURL:(NSURL*)[NSURL URLWithString:iOSUpdateURL]];
BITHockeyLogDebug(@"INFO: System returned: %d", success);
self.didStartUpdateProcess = success;
return success;
#endif /* TARGET_OS_SIMULATOR */
}
// begin the startup process
- (void)startManager {
if (self.appEnvironment == BITEnvironmentOther) {
if ([self isUpdateManagerDisabled]) return;
BITHockeyLogDebug(@"INFO: Starting UpdateManager");
id strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(updateManagerShouldSendUsageData:)]) {
self.sendUsageData = [strongDelegate updateManagerShouldSendUsageData:self];
}
[self checkExpiryDateReached];
if (![self expiryDateReached]) {
if ([self isCheckForUpdateOnLaunch] && [self shouldCheckForUpdates]) {
if ([BITHockeyHelper applicationState] != BITApplicationStateActive) return;
[self performSelector:@selector(checkForUpdate) withObject:nil afterDelay:1.0];
}
}
}
[self registerObservers];
}
#pragma mark - Handle responses
- (void)handleError:(NSError *)error {
self.receivedData = nil;
self.checkInProgress = NO;
if ([self expiryDateReached]) {
if (!self.blockingView) {
[self alertFallback:self.blockingScreenMessage];
}
} else {
[self reportError:error];
}
}
- (void)finishLoading {
{
self.checkInProgress = NO;
if ([self.receivedData length]) {
NSString *responseString = [[NSString alloc] initWithBytes:[self.receivedData bytes] length:[self.receivedData length] encoding: NSUTF8StringEncoding];
BITHockeyLogDebug(@"INFO: Received API response: %@", responseString);
if (!responseString || ![responseString dataUsingEncoding:NSUTF8StringEncoding]) {
self.receivedData = nil;
return;
}
NSError *error = nil;
NSDictionary *json = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:(NSData *)[responseString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error];
self.companyName = (([[json valueForKey:@"company"] isKindOfClass:[NSString class]]) ? [json valueForKey:@"company"] : nil);
if (self.appEnvironment == BITEnvironmentOther) {
NSArray *feedArray = (NSArray *)[json valueForKey:@"versions"];
// remember that we just checked the server
self.lastCheck = [NSDate date];
// server returned empty response?
if (![feedArray count]) {
BITHockeyLogDebug(@"WARNING: No versions available for download on HockeyApp.");
self.receivedData = nil;
return;
} else {
self.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 comparisonResult = bit_versionCompare(appVersionMetaInfo.minOSVersion, [[UIDevice currentDevice] systemVersion]);
if (comparisonResult == 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 (self.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(@"Version"), shortVersionString, versionString];
NSString *alertMsg = [NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateNoUpdateAvailableMessage"), currentVersionString];
__weak typeof(self) weakSelf = self;
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"UpdateNoUpdateAvailableTitle")
message:alertMsg
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyOK")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction __unused *action) {
typeof(self) strongSelf = weakSelf;
self.updateAlertShowing = NO;
if ([strongSelf expiryDateReached] && !strongSelf.blockingView) {
[strongSelf alertFallback:self.blockingScreenMessage];
}
}];
[alertController addAction:okAction];
[self showAlertController:alertController];
}
if (self.isUpdateAvailable && (self.alwaysShowUpdateReminder || newVersionDiffersFromCachedVersion || [self hasNewerMandatoryVersion])) {
if (self.updateAvailable && !self.currentHockeyViewController) {
[self showCheckForUpdateAlert];
}
}
self.showFeedback = NO;
}
} else if (![self expiryDateReached]) {
[self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain
code:BITUpdateAPIServerReturnedEmptyResponse
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Server returned an empty response.", NSLocalizedDescriptionKey, nil]]];
}
if (!self.updateAlertShowing && [self expiryDateReached] && !self.blockingView) {
[self alertFallback:self.blockingScreenMessage];
}
self.receivedData = nil;
}
}
#pragma mark - NSURLSession
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *) __unused task didCompleteWithError:(NSError *)error {
dispatch_async(dispatch_get_main_queue(), ^{
[session finishTasksAndInvalidate];
if(error){
[self handleError:error];
}else{
[self finishLoading];
}
});
}
- (void)URLSession:(NSURLSession *) __unused session dataTask:(NSURLSessionDataTask *) __unused dataTask didReceiveData:(NSData *)data {
[self.receivedData appendData:data];
}
- (void)URLSession:(NSURLSession *) __unused session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
if ([response respondsToSelector:@selector(statusCode)]) {
NSInteger statusCode = [((NSHTTPURLResponse *)response) statusCode];
if (statusCode == 404) {
[dataTask cancel];
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]]];
if (completionHandler) { completionHandler(NSURLSessionResponseCancel); }
return;
}
if (completionHandler) { completionHandler(NSURLSessionResponseAllow);}
}
self.receivedData = [NSMutableData data];
[self.receivedData setLength:0];
}
- (void)URLSession:(NSURLSession *) __unused session task:(NSURLSessionTask *) __unused task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler {
NSURLRequest *newRequest = request;
if (response) {
newRequest = nil;
}
if (completionHandler) { completionHandler(newRequest); }
}
- (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];
}
}
- (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 = self.currentAppVersion;
defaultApp.shortVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
_appVersions = [NSArray arrayWithObject:defaultApp];
} else {
_appVersions = [anAppVersions copy];
}
[self didChangeValueForKey:@"appVersions"];
}
}
- (BITAppVersionMetaInfo *)newestAppVersion {
BITAppVersionMetaInfo *appVersion = [self.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];
}
}
@end
#endif /* HOCKEYSDK_FEATURE_UPDATES */