diff --git a/Classes/.DS_Store b/Classes/.DS_Store new file mode 100644 index 0000000000..5008ddfcf5 Binary files /dev/null and b/Classes/.DS_Store differ diff --git a/Classes/BWApp.h b/Classes/BWApp.h new file mode 100644 index 0000000000..f8cfd4bc9a --- /dev/null +++ b/Classes/BWApp.h @@ -0,0 +1,56 @@ +// +// BWApp.h +// HockeyDemo +// +// Created by Peter Steinberger on 04.02.11. +// Copyright 2011 Buzzworks. 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 + +@interface BWApp : NSObject { + NSString *name_; + NSString *version_; + NSString *shortVersion_; + NSString *notes_; + NSDate *date_; + NSNumber *size_; + NSNumber *mandatory_; +} +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSString *version; +@property (nonatomic, copy) NSString *shortVersion; +@property (nonatomic, copy) NSString *notes; +@property (nonatomic, copy) NSDate *date; +@property (nonatomic, copy) NSNumber *size; +@property (nonatomic, copy) NSNumber *mandatory; + +- (NSString *)nameAndVersionString; +- (NSString *)versionString; +- (NSString *)dateString; +- (NSString *)sizeInMB; +- (NSString *)notesOrEmptyString; +- (void)setDateWithTimestamp:(NSTimeInterval)timestamp; +- (BOOL)isValid; +- (BOOL)isEqualToBWApp:(BWApp *)anApp; + ++ (BWApp *)appFromDict:(NSDictionary *)dict; + +@end diff --git a/Classes/BWApp.m b/Classes/BWApp.m new file mode 100644 index 0000000000..598a370d7c --- /dev/null +++ b/Classes/BWApp.m @@ -0,0 +1,186 @@ +// +// BWApp.m +// HockeyDemo +// +// Created by Peter Steinberger on 04.02.11. +// Copyright 2011 Buzzworks. 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 "BWApp.h" +#import "BWGlobal.h" + +@implementation BWApp + +@synthesize name = name_; +@synthesize version = version_; +@synthesize shortVersion = shortVersion_; +@synthesize notes = notes_; +@synthesize date = date_; +@synthesize size = size_; +@synthesize mandatory = mandatory_; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark static + ++ (BWApp *)appFromDict:(NSDictionary *)dict { + BWApp *app = [[[[self class] alloc] init] autorelease]; + + // NSParameterAssert([dict isKindOfClass:[NSDictionary class]]); + if ([dict isKindOfClass:[NSDictionary class]]) { + app.name = [dict objectForKey:@"title"]; + app.version = [dict objectForKey:@"version"]; + app.shortVersion = [dict objectForKey:@"shortversion"]; + [app setDateWithTimestamp:[[dict objectForKey:@"timestamp"] doubleValue]]; + app.size = [dict objectForKey:@"appsize"]; + app.notes = [dict objectForKey:@"notes"]; + app.mandatory = [dict objectForKey:@"mandatory"]; + } + + return app; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSObject + +- (void)dealloc { + [name_ release]; + [version_ release]; + [shortVersion_ release]; + [notes_ release]; + [date_ release]; + [size_ release]; + [mandatory_ release]; + + [super dealloc]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) + return YES; + if (!other || ![other isKindOfClass:[self class]]) + return NO; + return [self isEqualToBWApp:other]; +} + +- (BOOL)isEqualToBWApp:(BWApp *)anApp { + if (self == anApp) + return YES; + if (self.name != anApp.name && ![self.name isEqualToString:anApp.name]) + return NO; + if (self.version != anApp.version && ![self.version isEqualToString:anApp.version]) + return NO; + if (self.shortVersion != anApp.shortVersion && ![self.shortVersion isEqualToString:anApp.shortVersion]) + return NO; + if (self.notes != anApp.notes && ![self.notes isEqualToString:anApp.notes]) + return NO; + if (self.date != anApp.date && ![self.date isEqualToDate:anApp.date]) + return NO; + if (self.size != anApp.size && ![self.size isEqualToNumber:anApp.size]) + return NO; + if (self.mandatory != anApp.mandatory && ![self.mandatory isEqualToNumber:anApp.mandatory]) + return NO; + return YES; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSCoder + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeObject:self.name forKey:@"name"]; + [encoder encodeObject:self.version forKey:@"version"]; + [encoder encodeObject:self.shortVersion forKey:@"shortVersion"]; + [encoder encodeObject:self.notes forKey:@"notes"]; + [encoder encodeObject:self.date forKey:@"date"]; + [encoder encodeObject:self.size forKey:@"size"]; + [encoder encodeObject:self.mandatory forKey:@"mandatory"]; +} + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super init])) { + self.name = [decoder decodeObjectForKey:@"name"]; + self.version = [decoder decodeObjectForKey:@"version"]; + self.shortVersion = [decoder decodeObjectForKey:@"shortVersion"]; + self.notes = [decoder decodeObjectForKey:@"notes"]; + self.date = [decoder decodeObjectForKey:@"date"]; + self.size = [decoder decodeObjectForKey:@"size"]; + self.mandatory = [decoder decodeObjectForKey:@"mandatory"]; + } + return self; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Properties + +- (NSString *)nameAndVersionString { + NSString *appNameAndVersion = [NSString stringWithFormat:@"%@ %@", self.name, [self versionString]]; + return appNameAndVersion; +} + +- (NSString *)versionString { + NSString *shortString = ([self.shortVersion respondsToSelector:@selector(length)] && [self.shortVersion length]) ? [NSString stringWithFormat:@"%@", self.shortVersion] : @""; + NSString *versionString = [shortString length] ? [NSString stringWithFormat:@" (%@)", self.version] : self.version; + return [NSString stringWithFormat:@"%@ %@%@", BWHockeyLocalize(@"HockeyVersion"), shortString, versionString]; +} + +- (NSString *)dateString { + NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; + [formatter setDateStyle:NSDateFormatterMediumStyle]; + + return [formatter stringFromDate:self.date]; +} + +- (NSString *)sizeInMB { + if ([[size_ class] isKindOfClass: [NSNumber class]] && [size_ doubleValue] > 0) { + double appSizeInMB = [size_ doubleValue]/(1024*1024); + NSString *appSizeString = [NSString stringWithFormat:@"%.1f MB", appSizeInMB]; + return appSizeString; + } + + return @"0 MB"; +} + +- (void)setDateWithTimestamp:(NSTimeInterval)timestamp { + if (timestamp) { + NSDate *appDate = [NSDate dateWithTimeIntervalSince1970:timestamp]; + self.date = appDate; + } else { + self.date = nil; + } +} + +- (NSString *)notesOrEmptyString { + if (self.notes) { + return self.notes; + }else { + return [NSString string]; + } +} + +// a valid app needs at least following properties: name, version, date +- (BOOL)isValid { + BOOL valid = [self.name length] && [self.version length] && self.date; + return valid; +} + +@end diff --git a/Classes/BWGlobal.h b/Classes/BWGlobal.h new file mode 100644 index 0000000000..5316ff352c --- /dev/null +++ b/Classes/BWGlobal.h @@ -0,0 +1,116 @@ +// +// BWGlobal.h +// +// Created by Andreas Linde on 08/17/10. +// Copyright 2010-2011 Andreas Linde, Peter Steinberger. 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 "BWHockeyManager.h" +#import "BWApp.h" + +#define HOCKEYKIT_VERSION_MAJOR 2 +#define HOCKEYKIT_VERSION_MINOR 0 + +// uncomment this line to enable NSLog-debugging output +//#define kHockeyDebugEnabled + +#define kArrayOfLastHockeyCheck @"ArrayOfLastHockeyCheck" +#define kDateOfLastHockeyCheck @"DateOfLastHockeyCheck" +#define kDateOfVersionInstallation @"DateOfVersionInstallation" +#define kUsageTimeOfCurrentVersion @"UsageTimeOfCurrentVersion" +#define kUsageTimeForVersionString @"kUsageTimeForVersionString" +#define kHockeyAutoUpdateSetting @"HockeyAutoUpdateSetting" +#define kHockeyAllowUserSetting @"HockeyAllowUserSetting" +#define kHockeyAllowUsageSetting @"HockeyAllowUsageSetting" +#define kHockeyAutoUpdateSetting @"HockeyAutoUpdateSetting" +#define kHockeyAuthorizedVersion @"HockeyAuthorizedVersion" +#define kHockeyAuthorizedToken @"HockeyAuthorizedToken" + +#define kHockeyBundleName @"Hockey.bundle" + +// Notification message which HockeyManager is listening to, to retry requesting updated from the server +#define BWHockeyNetworkBecomeReachable @"NetworkDidBecomeReachable" + +#define BWHockeyLog(fmt, ...) do { if([BWHockeyManager sharedHockeyManager].isLoggingEnabled) { NSLog((@"[HockeyLib] %s/%d " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__); }} while(0) + +NSBundle *hockeyBundle(void); +NSString *BWmd5(NSString *str); + +#define BWHockeyLocalize(StringToken) NSLocalizedStringFromTableInBundle(StringToken, @"Hockey", hockeyBundle(), @"") + + +// compatibility helper +#ifndef kCFCoreFoundationVersionNumber_iPhoneOS_3_2 +#define kCFCoreFoundationVersionNumber_iPhoneOS_3_2 478.61 +#endif +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 32000 +#define BW_IF_3_2_OR_GREATER(...) \ +if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iPhoneOS_3_2) \ +{ \ +__VA_ARGS__ \ +} +#else +#define BW_IF_3_2_OR_GREATER(...) +#endif +#define BW_IF_PRE_3_2(...) \ +if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iPhoneOS_3_2) \ +{ \ +__VA_ARGS__ \ +} + +#ifndef kCFCoreFoundationVersionNumber_iPhoneOS_4_0 +#define kCFCoreFoundationVersionNumber_iPhoneOS_4_0 550.32 +#endif +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 +#define BW_IF_IOS4_OR_GREATER(...) \ +if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iPhoneOS_4_0) \ +{ \ +__VA_ARGS__ \ +} +#else +#define BW_IF_IOS4_OR_GREATER(...) +#endif + +#define BW_IF_PRE_IOS4(...) \ +if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iPhoneOS_4_0) \ +{ \ +__VA_ARGS__ \ +} + + + +#ifndef kCFCoreFoundationVersionNumber_iPhoneOS_5_0 +#define kCFCoreFoundationVersionNumber_iPhoneOS_5_0 674.0 +#endif +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000 +#define BW_IF_IOS5_OR_GREATER(...) \ +if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iPhoneOS_5_0) \ +{ \ +__VA_ARGS__ \ +} +#else +#define BW_IF_IOS5_OR_GREATER(...) +#endif + +#define BW_IF_PRE_IOS5(...) \ +if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iPhoneOS_5_0) \ +{ \ +__VA_ARGS__ \ +} diff --git a/Classes/BWGlobal.m b/Classes/BWGlobal.m new file mode 100644 index 0000000000..566bc1a4ec --- /dev/null +++ b/Classes/BWGlobal.m @@ -0,0 +1,53 @@ +// +// BWGlobal.h +// +// Created by Andreas Linde on 08/17/10. +// Copyright 2010-2011 Andreas Linde, Peter Steinberger. 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 "BWGlobal.h" +#include + +NSBundle *hockeyBundle(void) { + static NSBundle* bundle = nil; + if (!bundle) { + NSString* path = [[[NSBundle mainBundle] resourcePath] + stringByAppendingPathComponent:kHockeyBundleName]; + bundle = [[NSBundle bundleWithPath:path] retain]; + } + return bundle; +} + +NSString *BWmd5(NSString *str) { + const char *cStr = [str UTF8String]; + unsigned char result[CC_MD5_DIGEST_LENGTH]; + CC_MD5( cStr, strlen(cStr), result ); + return [NSString + stringWithFormat: @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + result[0], result[1], + result[2], result[3], + result[4], result[5], + result[6], result[7], + result[8], result[9], + result[10], result[11], + result[12], result[13], + result[14], result[15] + ]; +} diff --git a/Classes/BWHockeyManager.h b/Classes/BWHockeyManager.h new file mode 100644 index 0000000000..ce76e4b31c --- /dev/null +++ b/Classes/BWHockeyManager.h @@ -0,0 +1,227 @@ +// +// BWHockeyManager.h +// +// Created by Andreas Linde on 8/17/10. +// Copyright 2010-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 "BWHockeyViewController.h" + +typedef enum { + HockeyComparisonResultDifferent, + HockeyComparisonResultGreater +} HockeyComparisonResult; + +typedef enum { + HockeyAuthorizationDenied, + HockeyAuthorizationAllowed, + HockeyAuthorizationPending +} HockeyAuthorizationState; + +typedef enum { + HockeyUpdateCheckStartup = 0, + HockeyUpdateCheckDaily = 1, + HockeyUpdateCheckManually = 2 +} HockeyUpdateSetting; + +@protocol BWHockeyManagerDelegate; + +@class BWApp; + +@interface BWHockeyManager : NSObject { + id delegate_; + NSArray *apps_; + + NSString *updateURL_; + NSString *appIdentifier_; + NSString *currentAppVersion_; + + UINavigationController *navController_; + BWHockeyViewController *currentHockeyViewController_; + UIView *authorizeView_; + + NSMutableData *receivedData_; + + BOOL loggingEnabled_; + BOOL checkInProgress_; + BOOL dataFound; + BOOL updateAvailable_; + BOOL showFeedback_; + BOOL updateURLOffline_; + BOOL updateAlertShowing_; + BOOL lastCheckFailed_; + + NSURLConnection *urlConnection_; + NSDate *lastCheck_; + NSDate *usageStartTimestamp_; + + BOOL sendUserData_; + BOOL sendUsageTime_; + BOOL allowUserToDisableSendData_; + BOOL userAllowsSendUserData_; + BOOL userAllowsSendUsageTime_; + BOOL showUpdateReminder_; + BOOL checkForUpdateOnLaunch_; + HockeyComparisonResult compareVersionType_; + HockeyUpdateSetting updateSetting_; + BOOL showUserSettings_; + BOOL showDirectInstallOption_; + + BOOL requireAuthorization_; + NSString *authenticationSecret_; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// this is a singleton ++ (BWHockeyManager *)sharedHockeyManager; + +// update url needs to be set +@property (nonatomic, retain) NSString *updateURL; + +// private app identifier (optional) +@property (nonatomic, retain) NSString *appIdentifier; + +// delegate is optional +@property (nonatomic, assign) id delegate; + +// hockey secret is required if authentication is used +@property (nonatomic, retain) NSString *authenticationSecret; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// settings + +// if YES, the API will return an existing JMC config +// if NO, the API will return only version information +@property (nonatomic, assign) BOOL checkForTracker; + +@property (nonatomic, retain, readonly) NSDictionary *trackerConfig; + +// if YES, states will be logged using NSLog. Only enable this for debugging! +// if NO, nothing will be logged. (default) +@property (nonatomic, assign, getter=isLoggingEnabled) BOOL loggingEnabled; + +// if YES, the current user data is send: device type, iOS version, app version, UDID (default) +// if NO, no such data is send to the server +@property (nonatomic, assign, getter=shouldSendUserData) BOOL sendUserData; + +// if YES, the the users usage time of the app to the service, only in 1 minute granularity! (default) +// if NO, no such data is send to the server +@property (nonatomic, assign, getter=shouldSendUsageTime) BOOL sendUsageTime; + +// if YES, the user agrees to send the usage data, user can change it if the developer shows the settings (default) +// if NO, the user overwrites the developer setting and no such data is sent +@property (nonatomic, assign, getter=isAllowUserToDisableSendData) BOOL allowUserToDisableSendData; + +// if YES, the user allowed to send user data (default) +// if NO, the user denied to send user data +@property (nonatomic, assign, getter=doesUserAllowsSendUserData) BOOL userAllowsSendUserData; + +// if YES, the user allowed to send usage data (default) +// if NO, the user denied to send usage data +@property (nonatomic, assign, getter=doesUserAllowsSendUsageTime) BOOL userAllowsSendUsageTime; + +// if YES, the new version alert will be displayed always if the current version is outdated (default) +// if NO, the alert will be displayed only once for each new update +@property (nonatomic, assign) BOOL alwaysShowUpdateReminder; + +// if YES, the user can change the HockeyUpdateSetting value (default) +// if NO, the user can not change it, and the default or developer defined value will be used +@property (nonatomic, assign, getter=shouldShowUserSettings) BOOL showUserSettings; + +// if YES, then an update check will be performed after the application becomes active (default) +// if NO, then the update check will not happen unless invoked explicitly +@property (nonatomic, assign, getter=isCheckForUpdateOnLaunch) BOOL checkForUpdateOnLaunch; + +// if YES, the alert notifying about an new update also shows a button to install the update directly +// if NO, the alert notifying about an new update only shows ignore and show update button +@property (nonatomic, assign, getter=ishowingDirectInstallOption) BOOL showDirectInstallOption; + +// if YES, each app version needs to be authorized by the server to run on this device +// if NO, each app version does not need to be authorized (default) +@property (nonatomic, assign, getter=isRequireAuthorization) BOOL requireAuthorization; + +// HockeyComparisonResultDifferent: alerts if the version on the server is different (default) +// HockeyComparisonResultGreater: alerts if the version on the server is greater +@property (nonatomic, assign) HockeyComparisonResult compareVersionType; + +// see HockeyUpdateSetting-enum. Will be saved in user defaults. +// default value: HockeyUpdateCheckStartup +@property (nonatomic, assign) HockeyUpdateSetting updateSetting; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// is an update available? +- (BOOL)isUpdateAvailable; + +// are we currently checking for updates? +- (BOOL)isCheckInProgress; + +// open update info view +- (void)showUpdateView; + +// manually start an update check +- (void)checkForUpdate; + +// checks for update, informs the user (error, no update found, etc) +- (void)checkForUpdateShowFeedback:(BOOL)feedback; + +// initiates app-download call. displays an system UIAlertView +- (BOOL)initiateAppDownload; + +// checks wether this app version is authorized +- (BOOL)appVersionIsAuthorized; + +// start checking for an authorization key +- (void)checkForAuthorization; + +// convenience methode to create hockey view controller +- (BWHockeyViewController *)hockeyViewController:(BOOL)modal; + +// get/set current active hockey view controller +@property (nonatomic, retain) BWHockeyViewController *currentHockeyViewController; + +// convenience method to get current running version string +- (NSString *)currentAppVersion; + +// get newest app or array of all available versions +- (BWApp *)app; + +- (NSArray *)apps; + +@end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +@protocol BWHockeyManagerDelegate +@optional + +// Invoked when the internet connection is started, to let the app enable the activity indicator +- (void)connectionOpened; + +// Invoked when the internet connection is closed, to let the app disable the activity indicator +- (void)connectionClosed; + +// optional parent view controller for the update screen when invoked via the alert view, default is the root UIWindow instance +- (UIViewController *)viewControllerForHockeyController:(BWHockeyManager *)hockeyController; + +@end diff --git a/Classes/BWHockeyManager.m b/Classes/BWHockeyManager.m new file mode 100644 index 0000000000..8c791454ec --- /dev/null +++ b/Classes/BWHockeyManager.m @@ -0,0 +1,1152 @@ +// +// BWHockeyManager.m +// +// Created by Andreas Linde on 8/17/10. +// Copyright 2010-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 "BWHockeyManager.h" +#import "BWGlobal.h" +#import "BWApp.h" +#import "NSString+HockeyAdditions.h" +#import "UIImage+HockeyAdditions.h" +#import +#import + +// 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" + +@interface BWHockeyManager () +- (NSString *)getDevicePlatform_; +- (id)parseJSONResultString:(NSString *)jsonString; +- (void)connectionOpened_; +- (void)connectionClosed_; +- (BOOL)shouldCheckForUpdates; +- (void)startUsage; +- (void)stopUsage; +- (void)startManager; +- (void)wentOnline; +- (void)showAuthorizationScreen:(NSString *)message image:(NSString *)image; +- (BOOL)canSendUserData; +- (BOOL)canSendUsageTime; +- (NSString *)currentUsageString; +- (NSString *)installationDateString; +- (NSString *)authenticationToken; +- (HockeyAuthorizationState)authorizationState; + +@property (nonatomic, assign, getter=isUpdateAvailable) BOOL updateAvailable; +@property (nonatomic, assign, getter=isCheckInProgress) BOOL checkInProgress; +@property (nonatomic, retain) NSMutableData *receivedData; +@property (nonatomic, copy) NSDate *lastCheck; +@property (nonatomic, copy) NSArray *apps; +@property (nonatomic, retain) NSURLConnection *urlConnection; +@property (nonatomic, copy) NSDate *usageStartTimestamp; +@property (nonatomic, retain) UIView *authorizeView; +@property (nonatomic, retain) NSDictionary *trackerConfig; +@end + +// hockey api error domain +typedef enum { + HockeyErrorUnknown, + HockeyAPIServerReturnedInvalidStatus, + HockeyAPIServerReturnedInvalidData, + HockeyAPIServerReturnedEmptyResponse, + HockeyAPIClientAuthorizationMissingSecret, + HockeyAPIClientCannotCreateConnection +} HockeyErrorReason; +static NSString *kHockeyErrorDomain = @"HockeyErrorDomain"; + +@implementation BWHockeyManager + +@synthesize checkForTracker; +@synthesize trackerConfig; +@synthesize delegate = delegate_; +@synthesize updateURL = updateURL_; +@synthesize appIdentifier = appIdentifier_; +@synthesize urlConnection = urlConnection_; +@synthesize loggingEnabled = loggingEnabled_; +@synthesize checkInProgress = checkInProgress_; +@synthesize receivedData = receivedData_; +@synthesize sendUserData = sendUserData_; +@synthesize sendUsageTime = sendUsageTime_; +@synthesize allowUserToDisableSendData = allowUserToDisableSendData_; +@synthesize userAllowsSendUserData = userAllowsSendUserData_; +@synthesize userAllowsSendUsageTime = userAllowsSendUsageTime_; +@synthesize alwaysShowUpdateReminder = showUpdateReminder_; +@synthesize checkForUpdateOnLaunch = checkForUpdateOnLaunch_; +@synthesize compareVersionType = compareVersionType_; +@synthesize lastCheck = lastCheck_; +@synthesize showUserSettings = showUserSettings_; +@synthesize updateSetting = updateSetting_; +@synthesize apps = apps_; +@synthesize updateAvailable = updateAvailable_; +@synthesize usageStartTimestamp = usageStartTimestamp_; +@synthesize currentHockeyViewController = currentHockeyViewController_; +@synthesize showDirectInstallOption = showDirectInstallOption_; +@synthesize requireAuthorization = requireAuthorization_; +@synthesize authenticationSecret = authenticationSecret_; +@synthesize authorizeView = authorizeView_; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark static + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 40000 ++(BWHockeyManager *)sharedHockeyManager +{ + static BWHockeyManager *sharedInstance = nil; + static dispatch_once_t pred; + + if (sharedInstance) return sharedInstance; + + dispatch_once(&pred, ^{ + sharedInstance = [BWHockeyManager alloc]; + sharedInstance = [sharedInstance init]; + }); + + return sharedInstance; +} +#else ++ (BWHockeyManager *)sharedHockeyManager { + static BWHockeyManager *hockeyManager = nil; + + if (hockeyManager == nil) { + hockeyManager = [[BWHockeyManager alloc] init]; + } + + return hockeyManager; +} +#endif + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark private + +- (void)reportError_:(NSError *)error { + BWHockeyLog(@"Error: %@", [error localizedDescription]); + lastCheckFailed_ = YES; + + // only show error if we enable that + if (showFeedback_) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:BWHockeyLocalize(@"HockeyError") + message:[error localizedDescription] + delegate:nil + cancelButtonTitle:BWHockeyLocalize(@"OK") otherButtonTitles:nil]; + [alert show]; + [alert release]; + showFeedback_ = NO; + } +} + +- (NSString *)encodedAppIdentifier_ { + return (self.appIdentifier ? [self.appIdentifier stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] : [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]); +} + +- (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; +} + +- (void)connectionOpened_ { + if ([self.delegate respondsToSelector:@selector(connectionOpened)]) + [(id)self.delegate connectionOpened]; +} + +- (void)connectionClosed_ { + if ([self.delegate respondsToSelector:@selector(connectionClosed)]) + [(id)self.delegate connectionClosed]; +} + +- (void)startUsage { + self.usageStartTimestamp = [NSDate date]; + BOOL newVersion = NO; + + if (![[NSUserDefaults standardUserDefaults] valueForKey:kUsageTimeForVersionString]) { + newVersion = YES; + } else { + if ([(NSString *)[[NSUserDefaults standardUserDefaults] valueForKey:kUsageTimeForVersionString] compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] != NSOrderedSame) { + newVersion = YES; + } + } + + if (newVersion) { + [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble:[[NSDate date] timeIntervalSinceReferenceDate]] forKey:kDateOfVersionInstallation]; + [[NSUserDefaults standardUserDefaults] setObject:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] forKey:kUsageTimeForVersionString]; + [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble:0] forKey:kUsageTimeOfCurrentVersion]; + [[NSUserDefaults standardUserDefaults] synchronize]; + } +} + +- (void)stopUsage { + double timeDifference = [[NSDate date] timeIntervalSinceReferenceDate] - [usageStartTimestamp_ timeIntervalSinceReferenceDate]; + double previousTimeDifference = [(NSNumber *)[[NSUserDefaults standardUserDefaults] valueForKey:kUsageTimeOfCurrentVersion] doubleValue]; + + [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble:previousTimeDifference + timeDifference] forKey:kUsageTimeOfCurrentVersion]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (NSString *)currentUsageString { + double currentUsageTime = [(NSNumber *)[[NSUserDefaults standardUserDefaults] valueForKey:kUsageTimeOfCurrentVersion] 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] autorelease]; + [formatter setDateFormat:@"MM/dd/yyyy"]; + return [formatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:[(NSNumber *)[[NSUserDefaults standardUserDefaults] valueForKey:kDateOfVersionInstallation] doubleValue]]]; +} + +- (NSString *)deviceIdentifier { + if ([[UIDevice currentDevice] respondsToSelector:@selector(uniqueIdentifier)]) { + return [[UIDevice currentDevice] performSelector:@selector(uniqueIdentifier)]; + } + else { + return @"invalid"; + } +} + +- (NSString *)authenticationToken { + return [BWmd5([NSString stringWithFormat:@"%@%@%@%@", + authenticationSecret_, + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"], + [self deviceIdentifier] + ] + ) lowercaseString]; +} + +- (HockeyAuthorizationState)authorizationState { + NSString *version = [[NSUserDefaults standardUserDefaults] objectForKey:kHockeyAuthorizedVersion]; + NSString *token = [[NSUserDefaults standardUserDefaults] objectForKey:kHockeyAuthorizedToken]; + + 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 HockeyAuthorizationDenied; + } else { + return HockeyAuthorizationAllowed; + } + } + } + return HockeyAuthorizationPending; +} + +- (void)checkUpdateAvailable_ { + // check if there is an update available + if (self.compareVersionType == HockeyComparisonResultGreater) { + self.updateAvailable = ([self.app.version compare:self.currentAppVersion options:NSNumericSearch] == NSOrderedDescending); + } else { + self.updateAvailable = ([self.app.version compare:self.currentAppVersion] != NSOrderedSame); + } +} + +- (void)loadAppCache_ { + NSData *savedHockeyData = [[NSUserDefaults standardUserDefaults] objectForKey:kArrayOfLastHockeyCheck]; + NSArray *savedHockeyCheck = nil; + if (savedHockeyData) { + savedHockeyCheck = [NSKeyedUnarchiver unarchiveObjectWithData:savedHockeyData]; + } + if (savedHockeyCheck) { + self.apps = [NSArray arrayWithArray:savedHockeyCheck]; + [self checkUpdateAvailable_]; + } else { + self.apps = nil; + } +} + +- (void)saveAppCache_ { + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.apps]; + [[NSUserDefaults standardUserDefaults] setObject:data forKey:kArrayOfLastHockeyCheck]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (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; + BWHockeyLog(@"UIWindow with rootViewController found: %@", visibleWindow); + break; + } + } + } + + return visibleWindow; +} + +- (BOOL)canSendUserData { + if (self.shouldSendUserData) { + if (self.allowUserToDisableSendData) { + return self.userAllowsSendUserData; + } + + return YES; + } + + return NO; +} + +- (BOOL)canSendUsageTime { + if (self.shouldSendUsageTime) { + if (self.allowUserToDisableSendData) { + return self.userAllowsSendUsageTime; + } + + return YES; + } + + return NO; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSObject + +- (id)init { + if ((self = [super init])) { + updateURL_ = nil; + appIdentifier_ = nil; + checkInProgress_ = NO; + dataFound = NO; + updateAvailable_ = NO; + lastCheckFailed_ = NO; + currentAppVersion_ = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + navController_ = nil; + authorizeView_ = nil; + requireAuthorization_ = NO; + authenticationSecret_= nil; + loggingEnabled_ = NO; + + // set defaults + self.showDirectInstallOption = NO; + self.requireAuthorization = NO; + self.sendUserData = YES; + self.sendUsageTime = YES; + self.allowUserToDisableSendData = YES; + self.alwaysShowUpdateReminder = YES; + self.checkForUpdateOnLaunch = YES; + self.showUserSettings = YES; + self.compareVersionType = HockeyComparisonResultDifferent; + + // load update setting from user defaults and check value + if ([[NSUserDefaults standardUserDefaults] objectForKey:kHockeyAutoUpdateSetting]) { + self.updateSetting = (HockeyUpdateSetting)[[NSUserDefaults standardUserDefaults] integerForKey:kHockeyAutoUpdateSetting]; + } else { + self.updateSetting = HockeyUpdateCheckStartup; + } + + if ([[NSUserDefaults standardUserDefaults] objectForKey:kHockeyAllowUserSetting]) { + self.userAllowsSendUserData = [[NSUserDefaults standardUserDefaults] boolForKey:kHockeyAllowUserSetting]; + } else { + self.userAllowsSendUserData = YES; + } + + if ([[NSUserDefaults standardUserDefaults] objectForKey:kHockeyAllowUsageSetting]) { + self.userAllowsSendUsageTime = [[NSUserDefaults standardUserDefaults] boolForKey:kHockeyAllowUsageSetting]; + } else { + self.userAllowsSendUsageTime = YES; + } + + [self loadAppCache_]; + + [self startUsage]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(startManager) + name:BWHockeyNetworkBecomeReachable + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(stopUsage) + name:UIApplicationWillTerminateNotification + object:nil]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self name:BWHockeyNetworkBecomeReachable object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillTerminateNotification object:nil]; + + BW_IF_IOS4_OR_GREATER( + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; + ) + self.delegate = nil; + + [urlConnection_ cancel]; + self.urlConnection = nil; + + [navController_ release]; + [authorizeView_ release]; + [currentHockeyViewController_ release]; + [updateURL_ release]; + [apps_ release]; + [receivedData_ release]; + [lastCheck_ release]; + [usageStartTimestamp_ release]; + [authenticationSecret_ release]; + + [super dealloc]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark BetaUpdateUI + +- (BWHockeyViewController *)hockeyViewController:(BOOL)modal { + return [[[BWHockeyViewController alloc] init:self modal:modal] autorelease]; +} + +- (void)showUpdateView { + if (currentHockeyViewController_) { + BWHockeyLog(@"update view already visible, aborting"); + return; + } + + UIViewController *parentViewController = nil; + + if ([[self delegate] respondsToSelector:@selector(viewControllerForHockeyController:)]) { + parentViewController = [[self delegate] viewControllerForHockeyController: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")) { + parentViewController = [[NSClassFromString(@"TTNavigator") performSelector:(NSSelectorFromString(@"navigator"))] visibleViewController]; + } + + if (navController_ != nil) [navController_ release]; + + BWHockeyViewController *hockeyViewController = [self hockeyViewController:YES]; + navController_ = [[UINavigationController alloc] initWithRootViewController:hockeyViewController]; + + 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; + } + + [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 ;) + BWHockeyLog(@"No rootViewController found, using UIWindow-approach: %@", visibleWindow); + [visibleWindow addSubview:navController_.view]; + } +} + + +- (void)showCheckForUpdateAlert_ { + if (!updateAlertShowing_) { + if ([self.app.mandatory boolValue] ) { + UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:BWHockeyLocalize(@"HockeyUpdateAvailable") + message:[NSString stringWithFormat:BWHockeyLocalize(@"HockeyUpdateAlertMandatoryTextWithAppVersion"), [self.app nameAndVersionString]] + delegate:self + cancelButtonTitle:BWHockeyLocalize(@"HockeyInstallUpdate") + otherButtonTitles:nil + ] autorelease]; + [alertView setTag:2]; + [alertView show]; + updateAlertShowing_ = YES; + } else { + UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:BWHockeyLocalize(@"HockeyUpdateAvailable") + message:[NSString stringWithFormat:BWHockeyLocalize(@"HockeyUpdateAlertTextWithAppVersion"), [self.app nameAndVersionString]] + delegate:self + cancelButtonTitle:BWHockeyLocalize(@"HockeyIgnore") + otherButtonTitles:BWHockeyLocalize(@"HockeyShowUpdate"), nil + ] autorelease]; + BW_IF_IOS4_OR_GREATER( + if (self.ishowingDirectInstallOption) { + [alertView addButtonWithTitle:BWHockeyLocalize(@"HockeyInstallUpdate")]; + } + ) + [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:@"Ok" + otherButtonTitles:nil + ] autorelease]; + [alertView setTag:1]; + [alertView show]; +} + + +// open an authorization screen +- (void)showAuthorizationScreen:(NSString *)message image:(NSString *)image { + self.authorizeView = nil; + + UIWindow *visibleWindow = [self findVisibleWindow]; + if (visibleWindow == nil) { + [self alertFallback:message]; + return; + } + + CGRect frame = [visibleWindow frame]; + + self.authorizeView = [[[UIView alloc] initWithFrame:frame] autorelease]; + UIImageView *backgroundView = [[[UIImageView alloc] initWithImage:[UIImage bw_imageNamed:@"bg.png" bundle:kHockeyBundleName]] autorelease]; + backgroundView.contentMode = UIViewContentModeScaleAspectFill; + backgroundView.frame = frame; + [self.authorizeView addSubview:backgroundView]; + + if (image != nil) { + UIImageView *imageView = [[[UIImageView alloc] initWithImage:[UIImage bw_imageNamed:image bundle:kHockeyBundleName]] autorelease]; + imageView.contentMode = UIViewContentModeCenter; + imageView.frame = frame; + [self.authorizeView addSubview:imageView]; + } + + if (message != nil) { + frame.origin.x = 20; + frame.origin.y = frame.size.height - 140; + frame.size.width -= 40; + frame.size.height = 40; + + UILabel *label = [[[UILabel alloc] initWithFrame:frame] autorelease]; + label.text = message; + label.textAlignment = UITextAlignmentCenter; + label.numberOfLines = 2; + label.backgroundColor = [UIColor clearColor]; + + [self.authorizeView addSubview:label]; + } + + [visibleWindow addSubview:self.authorizeView]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark JSONParsing + +- (id)parseJSONResultString:(NSString *)jsonString { + NSError *error = nil; + id feedResult = nil; + + SEL sbJSONSelector = NSSelectorFromString(@"JSONValue"); + SEL jsonKitSelector = NSSelectorFromString(@"objectFromJSONStringWithParseOptions:error:"); + + 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 { + BWHockeyLog(@"Error: You need a JSON Framework in your runtime!"); + [self doesNotRecognizeSelector:_cmd]; + } + if (error) { + BWHockeyLog(@"Error while parsing response feed: %@", [error localizedDescription]); + [self reportError_:error]; + return nil; + } + + return feedResult; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark RequestComments + +- (BOOL)shouldCheckForUpdates { + BOOL checkForUpdate = NO; + switch (self.updateSetting) { + case HockeyUpdateCheckStartup: + checkForUpdate = YES; + break; + case HockeyUpdateCheckDaily: + checkForUpdate = [[[self.lastCheck description] substringToIndex:10] compare:[[[NSDate date] description] substringToIndex:10]] != NSOrderedSame; + break; + case HockeyUpdateCheckManually: + checkForUpdate = NO; + break; + default: + break; + } + return checkForUpdate; +} + +- (void)checkForAuthorization { + NSMutableString *parameter = [NSMutableString stringWithFormat:@"api/2/apps/%@", [[self encodedAppIdentifier_] bw_URLEncodedString]]; + + [parameter appendFormat:@"?format=json&authorize=yes&app_version=%@&udid=%@", + [[[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] bw_URLEncodedString], + [[self deviceIdentifier] bw_URLEncodedString] + ]; + + // build request & send + NSString *url = [NSString stringWithFormat:@"%@%@", self.updateURL, parameter]; + BWHockeyLog(@"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:kHockeyErrorDomain code:HockeyAPIServerReturnedEmptyResponse userInfo: + [NSDictionary dictionaryWithObjectsAndKeys:@"Server returned empty response.", NSLocalizedDescriptionKey, nil]]]; + return; + } else { + BWHockeyLog(@"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:kHockeyAuthorizedVersion]; + [[NSUserDefaults standardUserDefaults] setObject:token forKey:kHockeyAuthorizedToken]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + self.requireAuthorization = NO; + self.authorizeView = nil; + + // now continue with an update check right away + if (self.checkForUpdateOnLaunch) { + [self checkForUpdate]; + } + } else { + // different token, block this version + BWHockeyLog(@"AUTH FAILURE: %@", [self authenticationToken]); + + // store the new data + [[NSUserDefaults standardUserDefaults] setObject:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] forKey:kHockeyAuthorizedVersion]; + [[NSUserDefaults standardUserDefaults] setObject:token forKey:kHockeyAuthorizedToken]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + [self showAuthorizationScreen:BWHockeyLocalize(@"HockeyAuthorizationDenied") image:@"authorize_denied.png"]; + } + } + + } + + if (failed) { + [self showAuthorizationScreen:BWHockeyLocalize(@"HockeyAuthorizationOffline") image:@"authorize_request.png"]; + } +} + +- (void)checkForUpdate { + if (self.requireAuthorization) return; + if (self.isUpdateAvailable && [self.app.mandatory boolValue]) { + [self showCheckForUpdateAlert_]; + return; + } + [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_) { + BWHockeyLog(@"update not needed right now"); + self.checkInProgress = NO; + return; + } + + NSMutableString *parameter = [NSMutableString stringWithFormat:@"api/2/apps/%@?format=json&udid=%@", + [[self encodedAppIdentifier_] bw_URLEncodedString], + [[self deviceIdentifier] bw_URLEncodedString]]; + + // add additional statistics if user didn't disable flag + if ([self canSendUserData]) { + [parameter appendFormat:@"&app_version=%@&os=iOS&os_version=%@&device=%@&lang=%@&first_start_at=%@", + [[[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] bw_URLEncodedString], + [[[UIDevice currentDevice] systemVersion] bw_URLEncodedString], + [[self getDevicePlatform_] bw_URLEncodedString], + [[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0] bw_URLEncodedString], + [[[self installationDateString] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] bw_URLEncodedString] + ]; + if ([self canSendUsageTime]) { + [parameter appendFormat:@"&usage_time=%@", + [[[self currentUsageString] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] bw_URLEncodedString] + ]; + } + } + + if ([self checkForTracker]) { + [parameter appendFormat:@"&jmc=yes"]; + } + + // build request & send + NSString *url = [NSString stringWithFormat:@"%@%@", self.updateURL, parameter]; + BWHockeyLog(@"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:kHockeyErrorDomain code:HockeyAPIClientCannotCreateConnection userInfo: + [NSDictionary dictionaryWithObjectsAndKeys:@"Url Connection could not be created.", NSLocalizedDescriptionKey, nil]]]; + } +} + +- (BOOL)initiateAppDownload { + if (!self.isUpdateAvailable) { + BWHockeyLog(@"Warning: No update available. Aborting."); + return NO; + } + + BW_IF_PRE_IOS4 + ( + NSString *message = [NSString stringWithFormat:BWHockeyLocalize(@"HockeyiOS3Message"), self.updateURL]; + UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:BWHockeyLocalize(@"HockeyWarning") message:message delegate:nil cancelButtonTitle:BWHockeyLocalize(@"HockeyOK") otherButtonTitles:nil] autorelease]; + [alert show]; + return NO; + ) + +#if TARGET_IPHONE_SIMULATOR + UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:BWHockeyLocalize(@"HockeyWarning") message:BWHockeyLocalize(@"HockeySimulatorMessage") delegate:nil cancelButtonTitle:BWHockeyLocalize(@"HockeyOK") otherButtonTitles:nil] autorelease]; + [alert show]; + return NO; +#endif + + NSString *extraParameter = [NSString string]; + if ([self canSendUserData]) { + extraParameter = [NSString stringWithFormat:@"&udid=%@", [self deviceIdentifier]]; + } + + NSString *hockeyAPIURL = [NSString stringWithFormat:@"%@api/2/apps/%@?format=plist%@", self.updateURL, [self encodedAppIdentifier_], extraParameter]; + NSString *iOSUpdateURL = [NSString stringWithFormat:@"itms-services://?action=download-manifest&url=%@", [hockeyAPIURL bw_URLEncodedString]]; + + BWHockeyLog(@"API Server Call: %@, calling iOS with %@", hockeyAPIURL, iOSUpdateURL); + BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:iOSUpdateURL]]; + BWHockeyLog(@"System returned: %d", success); + return success; +} + + +// checks wether this app version is authorized +- (BOOL)appVersionIsAuthorized { + if (self.requireAuthorization && !authenticationSecret_) { + [self reportError_:[NSError errorWithDomain:kHockeyErrorDomain code:HockeyAPIClientAuthorizationMissingSecret userInfo: + [NSDictionary dictionaryWithObjectsAndKeys:@"Authentication secret is not set but required.", NSLocalizedDescriptionKey, nil]]]; + + return NO; + } + + if (!self.requireAuthorization) { + self.authorizeView = nil; + return YES; + } + + HockeyAuthorizationState state = [self authorizationState]; + if (state == HockeyAuthorizationDenied) { + [self showAuthorizationScreen:BWHockeyLocalize(@"HockeyAuthorizationDenied") image:@"authorize_denied.png"]; + } else if (state == HockeyAuthorizationAllowed) { + self.requireAuthorization = NO; + return YES; + } + + return NO; +} + + +// begin the startup process +- (void)startManager { + if (![self appVersionIsAuthorized]) { + if ([self authorizationState] == HockeyAuthorizationPending) { + [self showAuthorizationScreen:BWHockeyLocalize(@"HockeyAuthorizationProgress") image:@"authorize_request.png"]; + + [self performSelector:@selector(checkForAuthorization) withObject:nil afterDelay:0.0f]; + } + } else { + if ([self shouldCheckForUpdates]) { + [self performSelector:@selector(checkForUpdate) withObject:nil afterDelay:0.0f]; + } + } +} + + +- (void)wentOnline { + if (![self appVersionIsAuthorized]) { + if ([self authorizationState] == HockeyAuthorizationPending) { + [self checkForAuthorization]; + } + } else { + if (lastCheckFailed_) { + [self checkForUpdate]; + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSURLRequest + +- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse { + [self connectionOpened_]; + 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 connectionClosed_]; + [self reportError_:[NSError errorWithDomain:kHockeyErrorDomain code:HockeyAPIServerReturnedInvalidStatus 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 connectionClosed_]; + self.receivedData = nil; + self.urlConnection = nil; + self.checkInProgress = NO; + [self reportError_:error]; +} + +// api call returned, parsing +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + [self connectionClosed_]; + self.checkInProgress = NO; + + if ([self.receivedData length]) { + NSString *responseString = [[[NSString alloc] initWithBytes:[receivedData_ bytes] length:[receivedData_ length] encoding: NSUTF8StringEncoding] autorelease]; + BWHockeyLog(@"Received API response: %@", responseString); + + id json = [self parseJSONResultString:responseString]; + NSArray *feedArray = (NSArray *)([self checkForTracker] ? [json valueForKey:@"versions"] : json); + self.trackerConfig = ([self checkForTracker] ? [json valueForKey:@"tracker"] : nil); + + 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:kHockeyErrorDomain code:HockeyAPIServerReturnedEmptyResponse userInfo: + [NSDictionary dictionaryWithObjectsAndKeys:@"Server returned empty response.", NSLocalizedDescriptionKey, nil]]]; + return; + } else { + lastCheckFailed_ = NO; + } + + + NSString *currentAppCacheVersion = [[[self app].version copy] autorelease]; + + // clear cache and reload with new data + NSMutableArray *tmpApps = [NSMutableArray arrayWithCapacity:[feedArray count]]; + for (NSDictionary *dict in feedArray) { + BWApp *app = [BWApp appFromDict:dict]; + if ([app isValid]) { + [tmpApps addObject:app]; + } else { + [self reportError_:[NSError errorWithDomain:kHockeyErrorDomain code:HockeyAPIServerReturnedInvalidData userInfo: + [NSDictionary dictionaryWithObjectsAndKeys:@"Invalid data received from server.", NSLocalizedDescriptionKey, nil]]]; + } + } + // only set if different! + if (![self.apps isEqualToArray:tmpApps]) { + self.apps = [[tmpApps copy] autorelease]; + } + [self saveAppCache_]; + + [self checkUpdateAvailable_]; + BOOL newVersionDiffersFromCachedVersion = ![self.app.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.app.name, BWHockeyLocalize(@"HockeyVersion"), shortVersionString, versionString]; + NSString *alertMsg = [NSString stringWithFormat:BWHockeyLocalize(@"HockeyNoUpdateNeededMessage"), currentVersionString]; + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:BWHockeyLocalize(@"HockeyNoUpdateNeededTitle") message:alertMsg delegate:nil cancelButtonTitle:BWHockeyLocalize(@"HockeyOK") otherButtonTitles:nil]; + [alert show]; + [alert release]; + } + + if (self.isUpdateAvailable && (self.alwaysShowUpdateReminder || newVersionDiffersFromCachedVersion || [self.app.mandatory boolValue])) { + if (updateAvailable_ && !currentHockeyViewController_) { + [self showCheckForUpdateAlert_]; + } + } + showFeedback_ = NO; + }else { + [self reportError_:[NSError errorWithDomain:kHockeyErrorDomain code:HockeyAPIServerReturnedEmptyResponse userInfo: + [NSDictionary dictionaryWithObjectsAndKeys:@"Server returned an empty response.", NSLocalizedDescriptionKey, nil]]]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Properties + +- (void)setCurrentHockeyViewController:(BWHockeyViewController *)aCurrentHockeyViewController { + if (currentHockeyViewController_ != aCurrentHockeyViewController) { + [currentHockeyViewController_ release]; + currentHockeyViewController_ = [aCurrentHockeyViewController retain]; + //BWHockeyLog(@"active hockey view controller: %@", aCurrentHockeyViewController); + } +} + +- (void)setUpdateURL:(NSString *)anUpdateURL { + // ensure url ends with a trailing slash + if (![anUpdateURL hasSuffix:@"/"]) { + anUpdateURL = [NSString stringWithFormat:@"%@/", anUpdateURL]; + } + + BW_IF_IOS4_OR_GREATER( + // register/deregister logic + NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter]; + if (!updateURL_ && anUpdateURL) { + [dnc addObserver:self selector:@selector(startUsage) name:UIApplicationDidBecomeActiveNotification object:nil]; + [dnc addObserver:self selector:@selector(stopUsage) name:UIApplicationWillResignActiveNotification object:nil]; + } else if (updateURL_ && !anUpdateURL) { + [dnc removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; + [dnc removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; + } + ) + + if (updateURL_ != anUpdateURL) { + [updateURL_ release]; + updateURL_ = [anUpdateURL copy]; + } + + [self performSelector:@selector(startManager) withObject:nil afterDelay:0.0f]; +} + +- (void)setAppIdentifier:(NSString *)anAppIdentifier { + if (appIdentifier_ != anAppIdentifier) { + [appIdentifier_ release]; + appIdentifier_ = [anAppIdentifier copy]; + } + + [self setUpdateURL:@"https://rink.hockeyapp.net/"]; +} + +- (void)setCheckForUpdateOnLaunch:(BOOL)flag { + if (checkForUpdateOnLaunch_ != flag) { + checkForUpdateOnLaunch_ = flag; + BW_IF_IOS4_OR_GREATER( + NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter]; + if (flag) { + [dnc addObserver:self selector:@selector(checkForUpdate) name:UIApplicationDidBecomeActiveNotification object:nil]; + } else { + [dnc removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; + } + ) + } +} + +- (void)setUserAllowsSendUserData:(BOOL)flag { + userAllowsSendUserData_ = flag; + + [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInteger:userAllowsSendUserData_] forKey:kHockeyAllowUserSetting]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void)setUserAllowsSendUsageTime:(BOOL)flag { + userAllowsSendUsageTime_ = flag; + + [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInteger:userAllowsSendUsageTime_] forKey:kHockeyAllowUsageSetting]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (NSString *)currentAppVersion { + return currentAppVersion_; +} + + +- (void)setUpdateSetting:(HockeyUpdateSetting)anUpdateSetting { + if (anUpdateSetting > HockeyUpdateCheckManually) { + updateSetting_ = HockeyUpdateCheckStartup; + } + + updateSetting_ = anUpdateSetting; + [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInteger:updateSetting_] forKey:kHockeyAutoUpdateSetting]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void)setLastCheck:(NSDate *)aLastCheck { + if (lastCheck_ != aLastCheck) { + [lastCheck_ release]; + lastCheck_ = [aLastCheck copy]; + + [[NSUserDefaults standardUserDefaults] setObject:[[lastCheck_ description] substringToIndex:10] forKey:kDateOfLastHockeyCheck]; + [[NSUserDefaults standardUserDefaults] synchronize]; + } +} + +- (void)setApps:(NSArray *)anApps { + if (apps_ != anApps || !apps_) { + [apps_ release]; + [self willChangeValueForKey:@"apps"]; + + // populate with default values (if empty) + if (![anApps count]) { + BWApp *defaultApp = [[[BWApp alloc] init] autorelease]; + defaultApp.name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; + defaultApp.version = currentAppVersion_; + defaultApp.shortVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + apps_ = [[NSArray arrayWithObject:defaultApp] retain]; + }else { + apps_ = [anApps copy]; + } + [self didChangeValueForKey:@"apps"]; + } +} + +- (BWApp *)app { + BWApp *app = [apps_ objectAtIndex:0]; + return app; +} + +- (void)setAuthorizeView:(UIView *)anAuthorizeView { + if (authorizeView_ != anAuthorizeView) { + [authorizeView_ removeFromSuperview]; + [authorizeView_ release]; + authorizeView_ = [anAuthorizeView retain]; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#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) { + [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 + [self initiateAppDownload]; + } +} + +@end diff --git a/Classes/BWHockeySettingsViewController.h b/Classes/BWHockeySettingsViewController.h new file mode 100644 index 0000000000..7e318f417a --- /dev/null +++ b/Classes/BWHockeySettingsViewController.h @@ -0,0 +1,22 @@ +// +// BWHockeySettingsViewController.h +// HockeyDemo +// +// Created by Andreas Linde on 3/8/11. +// Copyright 2011 Andreas Linde. All rights reserved. +// + +#import +@class BWHockeyManager; + + +@interface BWHockeySettingsViewController : UIViewController { + BWHockeyManager *hockeyManager_; +} + +@property (nonatomic, retain) BWHockeyManager *hockeyManager; + +- (id)init:(BWHockeyManager *)newHockeyManager; +- (id)init; + +@end diff --git a/Classes/BWHockeySettingsViewController.m b/Classes/BWHockeySettingsViewController.m new file mode 100644 index 0000000000..4615b240e0 --- /dev/null +++ b/Classes/BWHockeySettingsViewController.m @@ -0,0 +1,294 @@ +// +// BWHockeySettingsViewController.m +// HockeyDemo +// +// Created by Andreas Linde on 3/8/11. +// Copyright 2011 Andreas Linde. All rights reserved. +// + +#import "BWHockeySettingsViewController.h" +#import "BWHockeyManager.h" +#import "BWGlobal.h" + +#define BW_RGBCOLOR(r,g,b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1] + +@implementation BWHockeySettingsViewController + +@synthesize hockeyManager = hockeyManager_; + +- (void)dismissSettings { + [self.navigationController dismissModalViewControllerAnimated:YES]; +} + +#pragma mark - +#pragma mark Initialization + +- (id)init:(BWHockeyManager *)newHockeyManager { + if ((self = [super init])) { + self.hockeyManager = newHockeyManager; + self.title = BWHockeyLocalize(@"HockeySettingsTitle"); + + CGRect frame = self.view.frame; + frame.origin = CGPointZero; + + UITableView *tableView_ = [[[UITableView alloc] initWithFrame:CGRectMake(0, self.view.frame.size.height - 260, self.view.frame.size.width, 260) style:UITableViewStyleGrouped] autorelease]; + tableView_.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth; + + BW_IF_3_2_OR_GREATER( + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { + self.view.backgroundColor = BW_RGBCOLOR(200, 202, 204); + tableView_.backgroundColor = BW_RGBCOLOR(200, 202, 204); + } else { + tableView_.frame = frame; + tableView_.autoresizingMask = tableView_.autoresizingMask | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin; + } + ) + BW_IF_PRE_3_2( + self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(dismissSettings)] autorelease]; + tableView_.frame = frame; + tableView_.autoresizingMask = tableView_.autoresizingMask | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin; + ) + + tableView_.delegate = self; + tableView_.dataSource = self; + tableView_.clipsToBounds = NO; + + [self.view addSubview:tableView_]; + + } + return self; +} + +- (id)init { + return [self init:[BWHockeyManager sharedHockeyManager]]; +} + +#pragma mark - +#pragma mark Table view data source + +- (int)numberOfSections { + int numberOfSections = 1; + + if ([self.hockeyManager isAllowUserToDisableSendData]) { + if ([self.hockeyManager shouldSendUserData]) numberOfSections++; + if ([self.hockeyManager shouldSendUsageTime]) numberOfSections++; + } + + return numberOfSections; +} + + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + if (section == [self numberOfSections] - 1) { + return BWHockeyLocalize(@"HockeySectionCheckTitle"); + } else { + return nil; + } +} + + +- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { + if (section < [self numberOfSections] - 1) { + return 66; + } else return 0; +} + + +- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { + if ([self numberOfSections] > 1 && section < [self numberOfSections] - 1) { + UILabel *footer = [[[UILabel alloc] initWithFrame:CGRectMake(0, 0, 285, 66)] autorelease]; + footer.backgroundColor = [UIColor clearColor]; + footer.numberOfLines = 3; + footer.textAlignment = UITextAlignmentCenter; + footer.adjustsFontSizeToFitWidth = YES; + footer.textColor = [UIColor grayColor]; + footer.font = [UIFont systemFontOfSize:13]; + + if (section == 0 && [self.hockeyManager isAllowUserToDisableSendData] && [self.hockeyManager shouldSendUserData]) { + footer.text = BWHockeyLocalize(@"HockeySettingsUserDataDescription"); + } else if ([self.hockeyManager isAllowUserToDisableSendData] && section < [self numberOfSections]) { + footer.text = BWHockeyLocalize(@"HockeySettingsUsageDataDescription"); + } + + UIView* view = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 285, footer.frame.size.height + 6 + 11)] autorelease]; + [view setBackgroundColor:[UIColor clearColor]]; + + CGRect frame = footer.frame; + frame.origin.y = 8; + frame.origin.x = 16; + frame.size.width = 285; + footer.frame = frame; + + [view addSubview:footer]; + [view sizeToFit]; + + return view; + } + + return nil; +} + + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + // Return the number of sections. + return [self numberOfSections]; +} + + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + // Return the number of rows in the section. + if (section == [self numberOfSections] - 1) + return 3; + else + return 1; +} + + +- (void)sendUserData:(UISwitch *)switcher { + [self.hockeyManager setUserAllowsSendUserData:switcher.on]; +} + +- (void)sendUsageData:(UISwitch *)switcher { + [self.hockeyManager setUserAllowsSendUsageTime:switcher.on]; +} + + +// Customize the appearance of table view cells. +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + + static NSString *CheckmarkCellIdentifier = @"CheckmarkCell"; + static NSString *SwitchCellIdentifier = @"SwitchCell"; + + NSString *requiredIdentifier = nil; + UITableViewCellStyle cellStyle = UITableViewCellStyleSubtitle; + + if (indexPath.section == (NSUInteger)[self numberOfSections] - 1) { + cellStyle = UITableViewCellStyleDefault; + requiredIdentifier = CheckmarkCellIdentifier; + } else { + cellStyle = UITableViewCellStyleValue1; + requiredIdentifier = SwitchCellIdentifier; + } + + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:requiredIdentifier]; + if (cell == nil) { + cell = [[[UITableViewCell alloc] initWithStyle:cellStyle reuseIdentifier:requiredIdentifier] autorelease]; + } + + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + + // Configure the cell... + if (indexPath.section == (NSUInteger)[self numberOfSections] - 1) { + cell.selectionStyle = UITableViewCellSelectionStyleBlue; + + // update check selection + HockeyUpdateSetting hockeyAutoUpdateSetting = [[BWHockeyManager sharedHockeyManager] updateSetting]; + if (indexPath.row == 0) { + // on startup + cell.textLabel.text = BWHockeyLocalize(@"HockeySectionCheckStartup"); + if (hockeyAutoUpdateSetting == HockeyUpdateCheckStartup) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } + } else if (indexPath.row == 1) { + // daily + cell.textLabel.text = BWHockeyLocalize(@"HockeySectionCheckDaily"); + if (hockeyAutoUpdateSetting == HockeyUpdateCheckDaily) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } + } else { + // manually + cell.textLabel.text = BWHockeyLocalize(@"HockeySectionCheckManually"); + if (hockeyAutoUpdateSetting == HockeyUpdateCheckManually) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } + } + } else { + UISwitch *toggleSwitch = [[[UISwitch alloc] initWithFrame:CGRectZero] autorelease]; + + if (indexPath.section == 0 && [self.hockeyManager shouldSendUserData] && [self.hockeyManager isAllowUserToDisableSendData]) { + // send user data + cell.textLabel.text = BWHockeyLocalize(@"HockeySettingsUserData"); + [toggleSwitch addTarget:self action:@selector(sendUserData:) + forControlEvents:UIControlEventValueChanged]; + [toggleSwitch setOn:[self.hockeyManager doesUserAllowsSendUserData]]; + + } else if ([self.hockeyManager shouldSendUsageTime] && [self.hockeyManager isAllowUserToDisableSendData]) { + // send usage time + cell.textLabel.text = BWHockeyLocalize(@"HockeySettingsUsageData"); + [toggleSwitch addTarget:self action:@selector(sendUsageData:) + forControlEvents:UIControlEventValueChanged]; + [toggleSwitch setOn:[self.hockeyManager doesUserAllowsSendUsageTime]]; + } + + cell.accessoryView = toggleSwitch; + + } + + return cell; +} + + +#pragma mark - +#pragma mark Table view delegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; + + // update check interval selection + if (indexPath.row == 0) { + // on startup + [BWHockeyManager sharedHockeyManager].updateSetting = HockeyUpdateCheckStartup; + } else if (indexPath.row == 1) { + // daily + [BWHockeyManager sharedHockeyManager].updateSetting = HockeyUpdateCheckDaily; + } else { + // manually + [BWHockeyManager sharedHockeyManager].updateSetting = HockeyUpdateCheckManually; + } + + [tableView reloadData]; +} + + +#pragma mark - +#pragma mark Memory management + +- (void)didReceiveMemoryWarning { + // Releases the view if it doesn't have a superview. + [super didReceiveMemoryWarning]; + + // Relinquish ownership any cached data, images, etc. that aren't in use. +} + +- (void)viewDidUnload { + // Relinquish ownership of anything that can be recreated in viewDidLoad or on demand. + // For example: self.myOutlet = nil; +} + + +- (void)dealloc { + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Rotation + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + BOOL shouldAutorotate; + + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { + shouldAutorotate = (interfaceOrientation == UIInterfaceOrientationPortrait); + } else { + shouldAutorotate = YES; + } + + return shouldAutorotate; +} + +@end + diff --git a/Classes/BWHockeyViewController.h b/Classes/BWHockeyViewController.h new file mode 100644 index 0000000000..7b50d9162c --- /dev/null +++ b/Classes/BWHockeyViewController.h @@ -0,0 +1,65 @@ +// +// BWHockeyViewController.h +// +// Created by Andreas Linde on 8/17/10. +// Copyright 2010-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 "PSStoreButton.h" +#import "PSAppStoreHeader.h" + +typedef enum { + AppStoreButtonStateOffline, + AppStoreButtonStateCheck, + AppStoreButtonStateSearching, + AppStoreButtonStateUpdate, + AppStoreButtonStateInstalling +} AppStoreButtonState; + + +@class BWHockeyManager; + +@interface BWHockeyViewController : UITableViewController { + BWHockeyManager *hockeyManager_; + + NSDictionary *cellLayout; + + BOOL modal_; + BOOL kvoRegistered_; + BOOL showAllVersions_; + UIStatusBarStyle statusBarStyle_; + PSAppStoreHeader *appStoreHeader_; + PSStoreButton *appStoreButton_; + + id popOverController_; + + AppStoreButtonState appStoreButtonState_; + + NSMutableArray *cells_; +} + +@property (nonatomic, retain) BWHockeyManager *hockeyManager; +@property (nonatomic, readwrite) BOOL modal; + +- (id)init:(BWHockeyManager *)newHockeyManager modal:(BOOL)newModal; +- (id)init; + +@end diff --git a/Classes/BWHockeyViewController.m b/Classes/BWHockeyViewController.m new file mode 100644 index 0000000000..42e5ed6e60 --- /dev/null +++ b/Classes/BWHockeyViewController.m @@ -0,0 +1,626 @@ +// +// BWHockeyViewController.m +// +// Created by Andreas Linde on 8/17/10. +// Copyright 2010-2011 Andreas Linde, Peter Steinberger. 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 "NSString+HockeyAdditions.h" +#import "BWHockeyViewController.h" +#import "BWHockeyManager.h" +#import "BWGlobal.h" +#import "UIImage+HockeyAdditions.h" +#import "PSWebTableViewCell.h" +#import "BWHockeySettingsViewController.h" + +#define BW_RGBCOLOR(r,g,b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1] +#define kWebCellIdentifier @"PSWebTableViewCell" +#define kAppStoreViewHeight 90 + +@interface BWHockeyViewController () +// updates the whole view +- (void)showPreviousVersionAction; +- (void)redrawTableView; +@property (nonatomic, assign) AppStoreButtonState appStoreButtonState; +- (void)setAppStoreButtonState:(AppStoreButtonState)anAppStoreButtonState animated:(BOOL)animated; +@end + + +@implementation BWHockeyViewController + +@synthesize appStoreButtonState = appStoreButtonState_; +@synthesize hockeyManager = hockeyManager_; +@synthesize modal = modal_; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark private + +- (void)restoreStoreButtonStateAnimated_:(BOOL)animated { + if ([self.hockeyManager isUpdateAvailable]) { + [self setAppStoreButtonState:AppStoreButtonStateUpdate animated:animated]; + } else { + [self setAppStoreButtonState:AppStoreButtonStateCheck animated:animated]; + } +} + +- (void)updateAppStoreHeader_ { + BWApp *app = self.hockeyManager.app; + appStoreHeader_.headerLabel = app.name; + appStoreHeader_.middleHeaderLabel = [app versionString]; + NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; + [formatter setDateStyle:NSDateFormatterMediumStyle]; + NSMutableString *subHeaderString = [NSMutableString string]; + if (app.date) { + [subHeaderString appendString:[formatter stringFromDate:app.date]]; + } + if (app.size) { + if ([subHeaderString length]) { + [subHeaderString appendString:@" - "]; + } + [subHeaderString appendString:app.sizeInMB]; + } + appStoreHeader_.subHeaderLabel = subHeaderString; +} + +- (void)appDidBecomeActive_ { + if (self.appStoreButtonState == AppStoreButtonStateInstalling) { + [self setAppStoreButtonState:AppStoreButtonStateUpdate animated:YES]; + } else if (![self.hockeyManager isCheckInProgress]) { + [self restoreStoreButtonStateAnimated_:YES]; + } +} + +- (void)openSettings:(id)sender { + BWHockeySettingsViewController *settings = [[[BWHockeySettingsViewController alloc] init] autorelease]; + + Class popoverControllerClass = NSClassFromString(@"UIPopoverController"); + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad && popoverControllerClass) { + if (popOverController_ == nil) { + popOverController_ = [[popoverControllerClass alloc] initWithContentViewController:settings]; + } + if ([popOverController_ contentViewController].view.window) { + [popOverController_ dismissPopoverAnimated:YES]; + }else { + [popOverController_ setPopoverContentSize: CGSizeMake(320, 440)]; + [popOverController_ presentPopoverFromBarButtonItem:self.navigationItem.rightBarButtonItem + permittedArrowDirections:UIPopoverArrowDirectionUp animated:YES]; + } + } else { + + BW_IF_3_2_OR_GREATER( + settings.modalTransitionStyle = UIModalTransitionStylePartialCurl; + [self presentModalViewController:settings animated:YES]; + ) + BW_IF_PRE_3_2( + UINavigationController *navController = [[[UINavigationController alloc] initWithRootViewController:settings] autorelease]; + navController.modalTransitionStyle = UIModalTransitionStyleCoverVertical; + [self presentModalViewController:navController animated:YES]; + ) + } +} + +- (UIImage *)addGlossToImage_:(UIImage *)image { + BW_IF_IOS4_OR_GREATER(UIGraphicsBeginImageContextWithOptions(image.size, NO, 0.0);) + BW_IF_PRE_IOS4(UIGraphicsBeginImageContext(image.size);) + + [image drawAtPoint:CGPointZero]; + UIImage *iconGradient = [UIImage bw_imageNamed:@"IconGradient.png" bundle:kHockeyBundleName]; + [iconGradient drawInRect:CGRectMake(0, 0, image.size.width, image.size.height) blendMode:kCGBlendModeNormal alpha:0.5]; + + UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return result; +} + +#define kMinPreviousVersionButtonHeight 50 +- (void)realignPreviousVersionButton { + + // manually collect actual table height size + NSUInteger tableViewContentHeight = 0; + for (int i=0; i < [self tableView:self.tableView numberOfRowsInSection:0]; i++) { + tableViewContentHeight += [self tableView:self.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]]; + } + tableViewContentHeight += self.tableView.tableHeaderView.frame.size.height; + + NSUInteger footerViewSize = kMinPreviousVersionButtonHeight; + NSUInteger frameHeight = self.view.frame.size.height; + if(tableViewContentHeight < frameHeight && (frameHeight - tableViewContentHeight > 100)) { + footerViewSize = frameHeight - tableViewContentHeight; + } + + // update footer view + if(self.tableView.tableFooterView) { + CGRect frame = self.tableView.tableFooterView.frame; + frame.size.height = footerViewSize; + self.tableView.tableFooterView.frame = frame; + } +} + +- (void)changePreviousVersionButtonBackground:(id)sender { + [(UIButton *)sender setBackgroundColor:BW_RGBCOLOR(183,183,183)]; +} + +- (void)changePreviousVersionButtonBackgroundHighlighted:(id)sender { + [(UIButton *)sender setBackgroundColor:BW_RGBCOLOR(183,183,183)]; +} + +- (void)showHidePreviousVersionsButton { + BOOL multipleVersionButtonNeeded = [self.hockeyManager.apps count] > 1 && !showAllVersions_; + + if(multipleVersionButtonNeeded) { + // align at the bottom if tableview is small + UIView *footerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, kMinPreviousVersionButtonHeight)]; + footerView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + footerView.backgroundColor = BW_RGBCOLOR(200, 202, 204); + UIButton *footerButton = [UIButton buttonWithType:UIButtonTypeCustom]; + BW_IF_IOS4_OR_GREATER( + //footerButton.layer.shadowOffset = CGSizeMake(-2, 2); + footerButton.layer.shadowColor = [[UIColor blackColor] CGColor]; + footerButton.layer.shadowRadius = 2.0f; + ) + footerButton.titleLabel.font = [UIFont boldSystemFontOfSize:14]; + [footerButton setTitle:BWHockeyLocalize(@"HockeyShowPreviousVersions") forState:UIControlStateNormal]; + [footerButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + [footerButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; + [footerButton setBackgroundImage:[UIImage bw_imageNamed:@"buttonHighlight.png" bundle:kHockeyBundleName] forState:UIControlStateHighlighted]; + footerButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + [footerButton addTarget:self action:@selector(showPreviousVersionAction) forControlEvents:UIControlEventTouchUpInside]; + footerButton.frame = CGRectMake(0, kMinPreviousVersionButtonHeight-44, self.view.frame.size.width, 44); + footerButton.backgroundColor = BW_RGBCOLOR(183,183,183); + [footerView addSubview:footerButton]; + self.tableView.tableFooterView = footerView; + [self realignPreviousVersionButton]; + [footerView release]; + } else { + self.tableView.tableFooterView = nil; + self.tableView.backgroundColor = BW_RGBCOLOR(200, 202, 204); + } +} + +- (void)configureWebCell:(PSWebTableViewCell *)cell forApp_:(BWApp *)app { + // create web view for a version + NSString *installed = @""; + if ([app.version isEqualToString:[self.hockeyManager currentAppVersion]]) { + installed = [NSString stringWithFormat:@"%@", [app isEqual:self.hockeyManager.app] ? @"left" : @"right", BWHockeyLocalize(@"HockeyInstalled")]; + } + + if ([app isEqual:self.hockeyManager.app]) { + if ([app.notes length] > 0) { + installed = [NSString stringWithFormat:@"

 %@

", installed]; + cell.webViewContent = [NSString stringWithFormat:@"%@%@", installed, app.notes]; + } else { + cell.webViewContent = [NSString stringWithFormat:@"
%@
", BWHockeyLocalize(@"HockeyNoReleaseNotesAvailable")]; + } + } else { + cell.webViewContent = [NSString stringWithFormat:@"

%@%@
%@

%@

", [app versionString], installed, [app dateString], [app notesOrEmptyString]]; + } + cell.cellBackgroundColor = BW_RGBCOLOR(200, 202, 204); + + [cell addWebView]; + // hack + cell.textLabel.text = @""; + + [cell addObserver:self forKeyPath:@"webViewSize" options:0 context:nil]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSObject + +- (id)init:(BWHockeyManager *)newHockeyManager modal:(BOOL)newModal { + if ((self = [super initWithStyle:UITableViewStylePlain])) { + self.hockeyManager = newHockeyManager; + self.modal = newModal; + self.title = BWHockeyLocalize(@"HockeyUpdateScreenTitle"); + + if ([self.hockeyManager shouldShowUserSettings]) { + self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithImage:[UIImage bw_imageNamed:@"gear.png" bundle:kHockeyBundleName] + style:UIBarButtonItemStyleBordered + target:self + action:@selector(openSettings:)] autorelease]; + } + + cells_ = [[NSMutableArray alloc] initWithCapacity:5]; + popOverController_ = nil; + } + return self; +} + +- (id)init { + return [self init:[BWHockeyManager sharedHockeyManager] modal:NO]; +} + +- (void)dealloc { + [self viewDidUnload]; + for (UITableViewCell *cell in cells_) { + [cell removeObserver:self forKeyPath:@"webViewSize"]; + } + [cells_ release]; + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark View lifecycle + +- (void)onAction:(id)sender { + if (self.modal) { + + // Note that as of 5.0, parentViewController will no longer return the presenting view controller + UIViewController *presentingViewController = nil; + + BW_IF_IOS5_OR_GREATER(presentingViewController = self.navigationController.presentingViewController;); + BW_IF_PRE_IOS5(presentingViewController = self.navigationController.parentViewController;) + + if (presentingViewController) { + [self.navigationController dismissModalViewControllerAnimated:YES]; + } else { + [self.navigationController.view removeFromSuperview]; + } + } + else + [self.navigationController popViewControllerAnimated:YES]; + + [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle_]; +} + +- (CAGradientLayer *)backgroundLayer { + UIColor *colorOne = [UIColor colorWithWhite:0.9 alpha:1.0]; + UIColor *colorTwo = [UIColor colorWithHue:0.625 saturation:0.0 brightness:0.85 alpha:1.0]; + UIColor *colorThree = [UIColor colorWithHue:0.625 saturation:0.0 brightness:0.7 alpha:1.0]; + UIColor *colorFour = [UIColor colorWithHue:0.625 saturation:0.0 brightness:0.4 alpha:1.0]; + + NSArray *colors = [NSArray arrayWithObjects:(id)colorOne.CGColor, colorTwo.CGColor, colorThree.CGColor, colorFour.CGColor, nil]; + + NSNumber *stopOne = [NSNumber numberWithFloat:0.0]; + NSNumber *stopTwo = [NSNumber numberWithFloat:0.02]; + NSNumber *stopThree = [NSNumber numberWithFloat:0.99]; + NSNumber *stopFour = [NSNumber numberWithFloat:1.0]; + + NSArray *locations = [NSArray arrayWithObjects:stopOne, stopTwo, stopThree, stopFour, nil]; + + CAGradientLayer *headerLayer = [CAGradientLayer layer]; + //headerLayer.frame = CGRectMake(0.0, 0.0, 320.0, 77.0); + headerLayer.colors = colors; + headerLayer.locations = locations; + + return headerLayer; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // add notifications only to loaded view + NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter]; + [dnc addObserver:self selector:@selector(appDidBecomeActive_) name:UIApplicationDidBecomeActiveNotification object:nil]; + + // hook into manager with kvo! + [self.hockeyManager addObserver:self forKeyPath:@"checkInProgress" options:0 context:nil]; + [self.hockeyManager addObserver:self forKeyPath:@"isUpdateURLOffline" options:0 context:nil]; + [self.hockeyManager addObserver:self forKeyPath:@"updateAvailable" options:0 context:nil]; + [self.hockeyManager addObserver:self forKeyPath:@"apps" options:0 context:nil]; + kvoRegistered_ = YES; + + self.tableView.backgroundColor = BW_RGBCOLOR(200, 202, 204); + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + + UIView *topView = [[[UIView alloc] initWithFrame:CGRectMake(0, -(600-kAppStoreViewHeight), self.view.frame.size.width, 600)] autorelease]; + topView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + topView.backgroundColor = BW_RGBCOLOR(140, 141, 142); + [self.tableView addSubview:topView]; + + appStoreHeader_ = [[PSAppStoreHeader alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, kAppStoreViewHeight)]; + [self updateAppStoreHeader_]; + + NSString *iconString = nil; + NSArray *icons = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFiles"]; + if (!icons) { + iconString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFile"]; + if (!iconString) { + iconString = @"Icon.png"; + } + } else { + BOOL useHighResIcon = NO; + BW_IF_IOS4_OR_GREATER(if ([UIScreen mainScreen].scale == 2.0f) useHighResIcon = YES;) + + for(NSString *icon in icons) { + iconString = icon; + UIImage *iconImage = [UIImage imageNamed:icon]; + + if (iconImage.size.height == 57 && !useHighResIcon) { + // found! + break; + } + if (iconImage.size.height == 114 && useHighResIcon) { + // found! + break; + } + } + } + + BOOL addGloss = YES; + NSNumber *prerendered = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIPrerenderedIcon"]; + if (prerendered) { + addGloss = ![prerendered boolValue]; + } + + if (addGloss) { + appStoreHeader_.iconImage = [self addGlossToImage_:[UIImage imageNamed:iconString]]; + } else { + appStoreHeader_.iconImage = [UIImage imageNamed:iconString]; + } + + self.tableView.tableHeaderView = appStoreHeader_; + + if (self.modal) { + self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(onAction:)] autorelease]; + } + + PSStoreButton *storeButton = [[[PSStoreButton alloc] initWithPadding:CGPointMake(5, 40)] autorelease]; + storeButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + storeButton.buttonDelegate = self; + [self.tableView.tableHeaderView addSubview:storeButton]; + storeButton.buttonData = [PSStoreButtonData dataWithLabel:@"" colors:[PSStoreButton appStoreGrayColor] enabled:NO]; + self.appStoreButtonState = AppStoreButtonStateCheck; + [storeButton alignToSuperview]; + appStoreButton_ = [storeButton retain]; +} + +- (void)viewWillAppear:(BOOL)animated { + self.hockeyManager.currentHockeyViewController = self; + [super viewWillAppear:animated]; + statusBarStyle_ = [[UIApplication sharedApplication] statusBarStyle]; + [[UIApplication sharedApplication] setStatusBarStyle:(self.navigationController.navigationBar.barStyle == UIBarStyleDefault) ? UIStatusBarStyleDefault : UIStatusBarStyleBlackOpaque]; + [self redrawTableView]; +} + +- (void)viewWillDisappear:(BOOL)animated { + self.hockeyManager.currentHockeyViewController = nil; + //if the popover is still visible, dismiss it + [popOverController_ dismissPopoverAnimated:YES]; + [super viewWillDisappear:animated]; + [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle_]; +} + +- (void)redrawTableView { + [self restoreStoreButtonStateAnimated_:NO]; + [self updateAppStoreHeader_]; + + // clean up and remove any pending overservers + for (UITableViewCell *cell in cells_) { + [cell removeObserver:self forKeyPath:@"webViewSize"]; + } + [cells_ removeAllObjects]; + + int i = 0; + BOOL breakAfterThisApp = NO; + for (BWApp *app in self.hockeyManager.apps) { + i++; + + // only show the newer version of the app by default, if we don't show all versions + if (!showAllVersions_) { + if ([app.version isEqualToString:[self.hockeyManager currentAppVersion]]) { + if (i == 1) { + breakAfterThisApp = YES; + } else { + break; + } + } + } + + PSWebTableViewCell *cell = [[[PSWebTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kWebCellIdentifier] autorelease]; + [self configureWebCell:cell forApp_:app]; + [cells_ addObject:cell]; + + if (breakAfterThisApp) break; + } + + [self.tableView reloadData]; + [self showHidePreviousVersionsButton]; +} + +- (void)showPreviousVersionAction { + showAllVersions_ = YES; + BOOL showAllPending = NO; + + for (BWApp *app in self.hockeyManager.apps) { + if (!showAllPending) { + if ([app.version isEqualToString:[self.hockeyManager currentAppVersion]]) { + showAllPending = YES; + if (app == self.hockeyManager.app) { + continue; // skip this version already if it the latest version is the installed one + } + } else { + continue; // skip already shown + } + } + + PSWebTableViewCell *cell = [[[PSWebTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kWebCellIdentifier] autorelease]; + [self configureWebCell:cell forApp_:app]; + [cells_ addObject:cell]; + } + [self.tableView reloadData]; + [self showHidePreviousVersionsButton]; +} + +- (void)viewDidUnload { + [appStoreHeader_ release]; appStoreHeader_ = nil; + [popOverController_ release], popOverController_ = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + // test if KVO's are registered. if class is destroyed before it was shown(viewDidLoad) no KVOs are registered. + if (kvoRegistered_) { + [self.hockeyManager removeObserver:self forKeyPath:@"checkInProgress"]; + [self.hockeyManager removeObserver:self forKeyPath:@"isUpdateURLOffline"]; + [self.hockeyManager removeObserver:self forKeyPath:@"updateAvailable"]; + [self.hockeyManager removeObserver:self forKeyPath:@"apps"]; + kvoRegistered_ = NO; + } + + [super viewDidUnload]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + CGFloat rowHeight = 0; + + if ([cells_ count] > indexPath.row) { + PSWebTableViewCell *cell = [cells_ objectAtIndex:indexPath.row]; + rowHeight = cell.webViewSize.height; + } + + if ([self.hockeyManager.apps count] > 1 && !showAllVersions_) { + self.tableView.backgroundColor = BW_RGBCOLOR(183, 183, 183); + } + + if (rowHeight == 0) { + rowHeight = indexPath.row == 0 ? 250 : 44; // fill screen on startup + self.tableView.backgroundColor = BW_RGBCOLOR(200, 202, 204); + } + + return rowHeight; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSInteger cellCount = [cells_ count]; + return cellCount; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + // only make changes if we are visible + if(self.view.window) { + if ([keyPath isEqualToString:@"webViewSize"]) { + [self.tableView reloadData]; + [self realignPreviousVersionButton]; + } else if ([keyPath isEqualToString:@"checkInProgress"]) { + if (self.hockeyManager.isCheckInProgress) { + [self setAppStoreButtonState:AppStoreButtonStateSearching animated:YES]; + }else { + [self restoreStoreButtonStateAnimated_:YES]; + } + } else if ([keyPath isEqualToString:@"isUpdateURLOffline"]) { + [self restoreStoreButtonStateAnimated_:YES]; + } else if ([keyPath isEqualToString:@"updateAvailable"]) { + [self restoreStoreButtonStateAnimated_:YES]; + } else if ([keyPath isEqualToString:@"apps"]) { + [self redrawTableView]; + } + } +} + +// Customize the appearance of table view cells. +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + if ([cells_ count] > indexPath.row) { + return [cells_ objectAtIndex:indexPath.row]; + } else { + BWHockeyLog(@"Warning: cells_ and indexPath do not match? forgot calling redrawTableView?"); + } + return nil; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Rotation + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + BOOL shouldAutorotate; + + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { + shouldAutorotate = (interfaceOrientation == UIInterfaceOrientationLandscapeLeft || + interfaceOrientation == UIInterfaceOrientationLandscapeRight || + interfaceOrientation == UIInterfaceOrientationPortrait); + } else { + shouldAutorotate = YES; + } + + return shouldAutorotate; +} + +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration { + // update all cells + [cells_ makeObjectsPerformSelector:@selector(addWebView)]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark PSAppStoreHeaderDelegate + +- (void)setAppStoreButtonState:(AppStoreButtonState)anAppStoreButtonState { + [self setAppStoreButtonState:anAppStoreButtonState animated:NO]; +} + +- (void)setAppStoreButtonState:(AppStoreButtonState)anAppStoreButtonState animated:(BOOL)animated { + appStoreButtonState_ = anAppStoreButtonState; + + switch (anAppStoreButtonState) { + case AppStoreButtonStateOffline: + [appStoreButton_ setButtonData:[PSStoreButtonData dataWithLabel:BWHockeyLocalize(@"HockeyButtonOffline") colors:[PSStoreButton appStoreGrayColor] enabled:NO] animated:animated]; + break; + case AppStoreButtonStateCheck: + [appStoreButton_ setButtonData:[PSStoreButtonData dataWithLabel:BWHockeyLocalize(@"HockeyButtonCheck") colors:[PSStoreButton appStoreGreenColor] enabled:YES] animated:animated]; + break; + case AppStoreButtonStateSearching: + [appStoreButton_ setButtonData:[PSStoreButtonData dataWithLabel:BWHockeyLocalize(@"HockeyButtonSearching") colors:[PSStoreButton appStoreGrayColor] enabled:NO] animated:animated]; + break; + case AppStoreButtonStateUpdate: + [appStoreButton_ setButtonData:[PSStoreButtonData dataWithLabel:BWHockeyLocalize(@"HockeyButtonUpdate") colors:[PSStoreButton appStoreBlueColor] enabled:YES] animated:animated]; + break; + case AppStoreButtonStateInstalling: + [appStoreButton_ setButtonData:[PSStoreButtonData dataWithLabel:BWHockeyLocalize(@"HockeyButtonInstalling") colors:[PSStoreButton appStoreGrayColor] enabled:NO] animated:animated]; + break; + default: + break; + } +} + +- (void)storeButtonFired:(PSStoreButton *)button { + switch (appStoreButtonState_) { + case AppStoreButtonStateCheck: + [self.hockeyManager checkForUpdateShowFeedback:YES]; + break; + case AppStoreButtonStateUpdate: + if ([self.hockeyManager initiateAppDownload]) { + [self setAppStoreButtonState:AppStoreButtonStateInstalling animated:YES]; + }; + break; + default: + break; + } +} + +@end diff --git a/Classes/BWQuincyManager.h b/Classes/BWQuincyManager.h new file mode 100755 index 0000000000..3efa41d36c --- /dev/null +++ b/Classes/BWQuincyManager.h @@ -0,0 +1,221 @@ +/* + * Author: Andreas Linde + * Kent Sutherland + * + * Copyright (c) 2011 Andreas Linde & Kent Sutherland. + * 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 + +#define kQuincyBundleName @"Quincy.bundle" + +NSBundle *quincyBundle(void); +NSString *BWQuincyLocalize(NSString *stringToken); + +//#define BWQuincyLocalize(StringToken) NSLocalizedStringFromTableInBundle(StringToken, @"Quincy", quincyBundle(), @"") + +// flags if the crashlog analyzer is started. since this may theoretically crash we need to track it +#define kQuincyKitAnalyzerStarted @"QuincyKitAnalyzerStarted" + +// flags if the QuincyKit is activated at all +#define kQuincyKitActivated @"QuincyKitActivated" + +// flags if the crashreporter should automatically send crashes without asking the user again +#define kAutomaticallySendCrashReports @"AutomaticallySendCrashReports" + +// stores the set of crashreports that have been approved but aren't sent yet +#define kApprovedCrashReports @"ApprovedCrashReports" + +// Notification message which QuincyManager is listening to, to retry sending pending crash reports to the server +#define BWQuincyNetworkBecomeReachable @"NetworkDidBecomeReachable" + +typedef enum QuincyKitAlertType { + QuincyKitAlertTypeSend = 0, + QuincyKitAlertTypeFeedback = 1, +} CrashAlertType; + +typedef enum CrashReportStatus { + // The status of the crash is queued, need to check later (HockeyApp) + CrashReportStatusQueued = -80, + + // This app version is set to discontinued, no new crash reports accepted by the server + CrashReportStatusFailureVersionDiscontinued = -30, + + // XML: Sender version string contains not allowed characters, only alphanumberical including space and . are allowed + CrashReportStatusFailureXMLSenderVersionNotAllowed = -21, + + // XML: Version string contains not allowed characters, only alphanumberical including space and . are allowed + CrashReportStatusFailureXMLVersionNotAllowed = -20, + + // SQL for adding a symoblicate todo entry in the database failed + CrashReportStatusFailureSQLAddSymbolicateTodo = -18, + + // SQL for adding crash log in the database failed + CrashReportStatusFailureSQLAddCrashlog = -17, + + // SQL for adding a new version in the database failed + CrashReportStatusFailureSQLAddVersion = -16, + + // SQL for checking if the version is already added in the database failed + CrashReportStatusFailureSQLCheckVersionExists = -15, + + // SQL for creating a new pattern for this bug and set amount of occurrances to 1 in the database failed + CrashReportStatusFailureSQLAddPattern = -14, + + // SQL for checking the status of the bugfix version in the database failed + CrashReportStatusFailureSQLCheckBugfixStatus = -13, + + // SQL for updating the occurances of this pattern in the database failed + CrashReportStatusFailureSQLUpdatePatternOccurances = -12, + + // SQL for getting all the known bug patterns for the current app version in the database failed + CrashReportStatusFailureSQLFindKnownPatterns = -11, + + // SQL for finding the bundle identifier in the database failed + CrashReportStatusFailureSQLSearchAppName = -10, + + // the post request didn't contain valid data + CrashReportStatusFailureInvalidPostData = -3, + + // incoming data may not be added, because e.g. bundle identifier wasn't found + CrashReportStatusFailureInvalidIncomingData = -2, + + // database cannot be accessed, check hostname, username, password and database name settings in config.php + CrashReportStatusFailureDatabaseNotAvailable = -1, + + CrashReportStatusUnknown = 0, + + CrashReportStatusAssigned = 1, + + CrashReportStatusSubmitted = 2, + + CrashReportStatusAvailable = 3, + + CrashReportStatusDiscontinued = 4, + + CrashReportStatusMoreInfo = 5, + +} CrashReportStatus; + +// This protocol is used to send the image updates +@protocol BWQuincyManagerDelegate + +@optional + +// Return the userid the crashreport should contain, empty by default +-(NSString *) crashReportUserID; + +// Return the contact value (e.g. email) the crashreport should contain, empty by default +-(NSString *) crashReportContact; + +// Return the description the crashreport should contain, empty by default. The string will automatically be wrapped into <[DATA[ ]]>, so make sure you don't do that in your string. +-(NSString *) crashReportDescription; + +// Invoked when the internet connection is started, to let the app enable the activity indicator +-(void) connectionOpened; + +// Invoked when the internet connection is closed, to let the app disable the activity indicator +-(void) connectionClosed; + +-(void) askForCrashInfo:(NSString*)messageBody; + +// Invoked before the user is asked to send a crash report, so you can do additional actions. E.g. to make sure not to ask the user for an app rating :) +-(void) willShowSubmitCrashReportAlert; + +@end + +@interface BWQuincyManager : NSObject { + NSString *_submissionURL; + + id _delegate; + + BOOL _showAlwaysButton; + BOOL _feedbackActivated; + BOOL _autoSubmitCrashReport; + BOOL _autoSubmitDeviceUDID; + + NSString *_appIdentifier; + + NSString *_feedbackRequestID; + float _feedbackDelayInterval; + + NSMutableString *_contentOfProperty; + CrashReportStatus _serverResult; + + int _analyzerStarted; + NSString *_crashesDir; + + BOOL _crashIdenticalCurrentVersion; + BOOL _crashReportActivated; + + NSMutableArray *_crashFiles; + + NSMutableData *_responseData; + NSInteger _statusCode; + + NSURLConnection *_urlConnection; + + NSData *_crashData; + + NSString *_languageStyle; + BOOL _sendingInProgress; +} + ++ (BWQuincyManager *)sharedQuincyManager; + +// submission URL defines where to send the crash reports to (required) +@property (nonatomic, retain) NSString *submissionURL; + +// delegate is optional +@property (nonatomic, assign) id delegate; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// settings + +// nil, using the default localization files (Default) +// set to another string which will be appended to the Quincy localization file name, "Alternate" is another provided text set +@property (nonatomic, retain) NSString *languageStyle; + +// if YES, the user will get the option to choose "Always" for sending crash reports. This will cause the dialog not to show the alert description text landscape mode! (default) +// if NO, the dialog will not show a "Always" button +@property (nonatomic, assign, getter=isShowingAlwaysButton) BOOL showAlwaysButton; + +// if YES, the user will be presented with a status of the crash, if known +// if NO, the user will not see any feedback information (default) +@property (nonatomic, assign, getter=isFeedbackActivated) BOOL feedbackActivated; + +// if YES, the crash report will be submitted without asking the user +// if NO, the user will be asked if the crash report can be submitted (default) +@property (nonatomic, assign, getter=isAutoSubmitCrashReport) BOOL autoSubmitCrashReport; + +// if YES, the device UDID will be submitted as the user id, without the need to define it in the crashReportUserID delegate (meant for beta versions!) +// if NO, the crashReportUserID delegate defines what to be sent as user id (default) +@property (nonatomic, assign, getter=isAutoSubmitDeviceUDID) BOOL autoSubmitDeviceUDID; + +// If you want to use HockeyApp instead of your own server, this is required +@property (nonatomic, retain) NSString *appIdentifier; + +@end diff --git a/Classes/BWQuincyManager.m b/Classes/BWQuincyManager.m new file mode 100755 index 0000000000..6fa8a1d20e --- /dev/null +++ b/Classes/BWQuincyManager.m @@ -0,0 +1,772 @@ +/* + * Author: Andreas Linde + * Kent Sutherland + * + * Copyright (c) 2011 Andreas Linde & Kent Sutherland. + * 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 "BWQuincyManager.h" + +#include +#include //needed for PRIx64 macro + +NSBundle *quincyBundle(void) { + static NSBundle* bundle = nil; + if (!bundle) { + NSString* path = [[[NSBundle mainBundle] resourcePath] + stringByAppendingPathComponent:kQuincyBundleName]; + bundle = [[NSBundle bundleWithPath:path] retain]; + } + return bundle; +} + +NSString *BWQuincyLocalize(NSString *stringToken) { + if ([BWQuincyManager sharedQuincyManager].languageStyle == nil) + return NSLocalizedStringFromTableInBundle(stringToken, @"Quincy", quincyBundle(), @""); + else { + NSString *alternate = [NSString stringWithFormat:@"Quincy%@", [BWQuincyManager sharedQuincyManager].languageStyle]; + return NSLocalizedStringFromTableInBundle(stringToken, alternate, quincyBundle(), @""); + } +} + + +@interface BWQuincyManager () + +- (void)startManager; + +- (void)showCrashStatusMessage; + +- (void)handleCrashReport; +- (void)_cleanCrashReports; + +- (void)_checkForFeedbackStatus; + +- (void)_performSendingCrashReports; +- (void)_sendCrashReports; + +- (void)_postXML:(NSString*)xml toURL:(NSURL*)url; +- (NSString *)_getDevicePlatform; + +- (BOOL)hasNonApprovedCrashReports; +- (BOOL)hasPendingCrashReport; + +@end + +@implementation BWQuincyManager + +@synthesize delegate = _delegate; +@synthesize submissionURL = _submissionURL; +@synthesize showAlwaysButton = _showAlwaysButton; +@synthesize feedbackActivated = _feedbackActivated; +@synthesize autoSubmitCrashReport = _autoSubmitCrashReport; +@synthesize autoSubmitDeviceUDID = _autoSubmitDeviceUDID; +@synthesize languageStyle = _languageStyle; + +@synthesize appIdentifier = _appIdentifier; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 40000 ++(BWQuincyManager *)sharedQuincyManager { + static BWQuincyManager *sharedInstance = nil; + static dispatch_once_t pred; + + dispatch_once(&pred, ^{ + sharedInstance = [BWQuincyManager alloc]; + sharedInstance = [sharedInstance init]; + }); + + return sharedInstance; +} +#else ++ (BWQuincyManager *)sharedQuincyManager { + static BWQuincyManager *quincyManager = nil; + + if (quincyManager == nil) { + quincyManager = [[BWQuincyManager alloc] init]; + } + + return quincyManager; +} +#endif + +- (id) init { + if ((self = [super init])) { + _serverResult = CrashReportStatusUnknown; + _crashIdenticalCurrentVersion = YES; + _crashData = nil; + _urlConnection = nil; + _submissionURL = nil; + _responseData = nil; + _appIdentifier = nil; + _sendingInProgress = NO; + _languageStyle = nil; + + self.delegate = nil; + self.feedbackActivated = NO; + self.showAlwaysButton = NO; + self.autoSubmitCrashReport = NO; + self.autoSubmitDeviceUDID = NO; + + NSString *testValue = [[NSUserDefaults standardUserDefaults] stringForKey:kQuincyKitAnalyzerStarted]; + if (testValue) { + _analyzerStarted = [[NSUserDefaults standardUserDefaults] integerForKey:kQuincyKitAnalyzerStarted]; + } else { + _analyzerStarted = 0; + } + + testValue = nil; + testValue = [[NSUserDefaults standardUserDefaults] stringForKey:kQuincyKitActivated]; + if (testValue) { + _crashReportActivated = [[NSUserDefaults standardUserDefaults] boolForKey:kQuincyKitActivated]; + } else { + _crashReportActivated = YES; + [[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:YES] forKey:kQuincyKitActivated]; + } + + if ([[NSUserDefaults standardUserDefaults] stringForKey:kAutomaticallySendCrashReports]) { + self.autoSubmitCrashReport = [[NSUserDefaults standardUserDefaults] boolForKey: kAutomaticallySendCrashReports]; + } + + if (_crashReportActivated) { + _crashFiles = [[NSMutableArray alloc] init]; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + _crashesDir = [[NSString stringWithFormat:@"%@", [[paths objectAtIndex:0] stringByAppendingPathComponent:@"/crashes/"]] retain]; + + NSFileManager *fm = [NSFileManager defaultManager]; + + if (![fm fileExistsAtPath:_crashesDir]) { + NSDictionary *attributes = [NSDictionary dictionaryWithObject: [NSNumber numberWithUnsignedLong: 0755] forKey: NSFilePosixPermissions]; + NSError *theError = NULL; + + [fm createDirectoryAtPath:_crashesDir withIntermediateDirectories: YES attributes: attributes error: &theError]; + } + + PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter]; + NSError *error = NULL; + + // Check if we previously crashed + if ([crashReporter hasPendingCrashReport]) { + [self handleCrashReport]; + } + + // Enable the Crash Reporter + if (![crashReporter enableCrashReporterAndReturnError: &error]) + NSLog(@"Warning: Could not enable crash reporter: %@", error); + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startManager) name:BWQuincyNetworkBecomeReachable object:nil]; + } + } + return self; +} + + +- (void) dealloc { + self.delegate = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self name:BWQuincyNetworkBecomeReachable object:nil]; + + [_languageStyle release]; + + [_submissionURL release]; + _submissionURL = nil; + + [_appIdentifier release]; + _appIdentifier = nil; + + [_urlConnection cancel]; + [_urlConnection release]; + _urlConnection = nil; + + [_crashData release]; + + [_crashesDir release]; + [_crashFiles release]; + + [super dealloc]; +} + + +#pragma mark - +#pragma mark setter +- (void)setSubmissionURL:(NSString *)anSubmissionURL { + if (_submissionURL != anSubmissionURL) { + [_submissionURL release]; + _submissionURL = [anSubmissionURL copy]; + } + + [self performSelector:@selector(startManager) withObject:nil afterDelay:1.0f]; +} + +- (void)setAppIdentifier:(NSString *)anAppIdentifier { + if (_appIdentifier != anAppIdentifier) { + [_appIdentifier release]; + _appIdentifier = [anAppIdentifier copy]; + } + + [self setSubmissionURL:@"https://rink.hockeyapp.net/"]; +} + +#pragma mark - +#pragma mark private methods + +// begin the startup process +- (void)startManager { + if (!_sendingInProgress && [self hasPendingCrashReport]) { + _sendingInProgress = YES; + if (!self.autoSubmitCrashReport && [self hasNonApprovedCrashReports]) { + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(willShowSubmitCrashReportAlert)]) { + [self.delegate willShowSubmitCrashReportAlert]; + } + + NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; + + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:BWQuincyLocalize(@"CrashDataFoundTitle"), appName] + message:[NSString stringWithFormat:BWQuincyLocalize(@"CrashDataFoundDescription"), appName] + delegate:self + cancelButtonTitle:BWQuincyLocalize(@"CrashDontSendReport") + otherButtonTitles:BWQuincyLocalize(@"CrashSendReport"), nil]; + + if ([self isShowingAlwaysButton]) { + [alertView addButtonWithTitle:BWQuincyLocalize(@"CrashSendReportAlways")]; + } + + [alertView setTag: QuincyKitAlertTypeSend]; + [alertView show]; + [alertView release]; + } else { + [self _sendCrashReports]; + } + } +} + +- (BOOL)hasNonApprovedCrashReports { + NSDictionary *approvedCrashReports = [[NSUserDefaults standardUserDefaults] dictionaryForKey: kApprovedCrashReports]; + + if (!approvedCrashReports || [approvedCrashReports count] == 0) return YES; + + for (NSUInteger i=0; i < [_crashFiles count]; i++) { + NSString *filename = [_crashFiles objectAtIndex:i]; + + if (![approvedCrashReports objectForKey:filename]) return YES; + } + + return NO; +} + +- (BOOL)hasPendingCrashReport { + if (_crashReportActivated) { + NSFileManager *fm = [NSFileManager defaultManager]; + + if ([_crashFiles count] == 0 && [fm fileExistsAtPath:_crashesDir]) { + NSString *file = nil; + NSError *error = NULL; + + NSDirectoryEnumerator *dirEnum = [fm enumeratorAtPath: _crashesDir]; + + while ((file = [dirEnum nextObject])) { + NSDictionary *fileAttributes = [fm attributesOfItemAtPath:[_crashesDir stringByAppendingPathComponent:file] error:&error]; + if ([[fileAttributes objectForKey:NSFileSize] intValue] > 0) { + [_crashFiles addObject:file]; + } + } + } + + if ([_crashFiles count] > 0) { + return YES; + } else + return NO; + } else + return NO; +} + + +- (void) showCrashStatusMessage { + UIAlertView *alertView = nil; + + if (_serverResult >= CrashReportStatusAssigned && + _crashIdenticalCurrentVersion) { + // show some feedback to the user about the crash status + NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; + switch (_serverResult) { + case CrashReportStatusAssigned: + alertView = [[UIAlertView alloc] initWithTitle: [NSString stringWithFormat:BWQuincyLocalize(@"CrashResponseTitle"), appName ] + message: [NSString stringWithFormat:BWQuincyLocalize(@"CrashResponseNextRelease"), appName] + delegate: self + cancelButtonTitle: BWQuincyLocalize(@"CrashResponseTitleOK") + otherButtonTitles: nil]; + break; + case CrashReportStatusSubmitted: + alertView = [[UIAlertView alloc] initWithTitle: [NSString stringWithFormat:BWQuincyLocalize(@"CrashResponseTitle"), appName ] + message: [NSString stringWithFormat:BWQuincyLocalize(@"CrashResponseWaitingApple"), appName] + delegate: self + cancelButtonTitle: BWQuincyLocalize(@"CrashResponseTitleOK") + otherButtonTitles: nil]; + break; + case CrashReportStatusAvailable: + alertView = [[UIAlertView alloc] initWithTitle: [NSString stringWithFormat:BWQuincyLocalize(@"CrashResponseTitle"), appName ] + message: [NSString stringWithFormat:BWQuincyLocalize(@"CrashResponseAvailable"), appName] + delegate: self + cancelButtonTitle: BWQuincyLocalize(@"CrashResponseTitleOK") + otherButtonTitles: nil]; + break; + case CrashReportStatusMoreInfo: + if ([MFMailComposeViewController canSendMail]) { + alertView = [[UIAlertView alloc] initWithTitle: [NSString stringWithFormat:BWQuincyLocalize(@"CrashResponseTitle"), appName ] + message: [NSString stringWithFormat:BWQuincyLocalize(@"CrashResponseMoreInfo"), appName] + delegate: self + cancelButtonTitle: BWQuincyLocalize(@"Skip") + otherButtonTitles: BWQuincyLocalize(@"Send Email"), nil]; + } + break; + default: + alertView = nil; + break; + } + + if (alertView) { + [alertView setTag: QuincyKitAlertTypeFeedback]; + [alertView show]; + [alertView release]; + } + } +} + + +#pragma mark - +#pragma mark UIAlertView Delegate + +- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { + + switch ([alertView tag]) { + case QuincyKitAlertTypeSend: + switch (buttonIndex) { + case 0: + _sendingInProgress = NO; + [self _cleanCrashReports]; + break; + case 1: + [self _sendCrashReports]; + break; + case 2: + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kAutomaticallySendCrashReports]; + + [self _sendCrashReports]; + break; + } + break; + case QuincyKitAlertTypeFeedback: + switch (buttonIndex) { + case 0: + break; + case 1: + [self.delegate askForCrashInfo:@"Please describe what you were doing when the crash occured:\n\n"]; + } + break; + + default: + break; + } +} + + +#pragma mark - +#pragma mark NSXMLParser Delegate + +#pragma mark NSXMLParser + +- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { + if (qName) { + elementName = qName; + } + + if ([elementName isEqualToString:@"result"]) { + _contentOfProperty = [NSMutableString string]; + } +} + +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { + if (qName) { + elementName = qName; + } + + // open source implementation + if ([elementName isEqualToString: @"result"]) { + if ([_contentOfProperty intValue] > _serverResult) { + _serverResult = (CrashReportStatus)[_contentOfProperty intValue]; + } + CrashReportStatus errorcode = (CrashReportStatus)[_contentOfProperty intValue]; + NSLog(@"CrashReporter ended in error code: %i", errorcode); + } +} + + +- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { + if (_contentOfProperty) { + // If the current element is one whose content we care about, append 'string' + // to the property that holds the content of the current element. + if (string != nil) { + [_contentOfProperty appendString:string]; + } + } +} + +#pragma mark - +#pragma mark Private + + +- (NSString *)_getOSVersionBuild { + size_t size = 0; + NSString *osBuildVersion = nil; + + sysctlbyname("kern.osversion", NULL, &size, NULL, 0); + char *answer = (char*)malloc(size); + int result = sysctlbyname("kern.osversion", answer, &size, NULL, 0); + if (result >= 0) { + osBuildVersion = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding]; + } + + return osBuildVersion; +} + +- (NSString *)_getDevicePlatform { + size_t size = 0; + 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 *)deviceIdentifier { + if ([[UIDevice currentDevice] respondsToSelector:@selector(uniqueIdentifier)]) { + return [[UIDevice currentDevice] performSelector:@selector(uniqueIdentifier)]; + } + else { + return @"invalid"; + } +} + +- (void)_performSendingCrashReports { + NSMutableDictionary *approvedCrashReports = [NSMutableDictionary dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults] dictionaryForKey: kApprovedCrashReports]]; + + NSFileManager *fm = [NSFileManager defaultManager]; + NSError *error = NULL; + + NSString *userid = @""; + NSString *contact = @""; + NSString *description = @""; + + if (self.autoSubmitDeviceUDID) { + userid = [self deviceIdentifier]; + } else if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashReportUserID)]) { + userid = [self.delegate crashReportUserID] ?: @""; + } + + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashReportContact)]) { + contact = [self.delegate crashReportContact] ?: @""; + } + + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashReportDescription)]) { + description = [self.delegate crashReportDescription] ?: @""; + } + + NSMutableString *crashes = nil; + _crashIdenticalCurrentVersion = NO; + + for (NSUInteger i=0; i < [_crashFiles count]; i++) { + NSString *filename = [_crashesDir stringByAppendingPathComponent:[_crashFiles objectAtIndex:i]]; + NSData *crashData = [NSData dataWithContentsOfFile:filename]; + + if ([crashData length] > 0) { + PLCrashReport *report = [[[PLCrashReport alloc] initWithData:crashData error:&error] autorelease]; + + if (report == nil) { + NSLog(@"Could not parse crash report"); + continue; + } + + NSString *crashLogString = [PLCrashReportTextFormatter stringValueForCrashReport:report withTextFormat:PLCrashReportTextFormatiOS]; + + if ([report.applicationInfo.applicationVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + _crashIdenticalCurrentVersion = YES; + } + + if (crashes == nil) { + crashes = [NSMutableString string]; + } + + [crashes appendFormat:@"%s%@%@%@%@%@%@%@", + [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String], + report.applicationInfo.applicationIdentifier, + report.systemInfo.operatingSystemVersion, + [self _getDevicePlatform], + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], + report.applicationInfo.applicationVersion, + crashLogString, + userid, + contact, + description]; + + // store this crash report as user approved, so if it fails it will retry automatically + [approvedCrashReports setObject:[NSNumber numberWithBool:YES] forKey:[_crashFiles objectAtIndex:i]]; + } else { + // we cannot do anything with this report, so delete it + [fm removeItemAtPath:filename error:&error]; + } + } + + [[NSUserDefaults standardUserDefaults] setObject:approvedCrashReports forKey:kApprovedCrashReports]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + if (crashes != nil) { + [self _postXML:[NSString stringWithFormat:@"%@", crashes] + toURL:[NSURL URLWithString:self.submissionURL]]; + + } +} + +- (void)_cleanCrashReports { + NSError *error = NULL; + + NSFileManager *fm = [NSFileManager defaultManager]; + + for (NSUInteger i=0; i < [_crashFiles count]; i++) { + [fm removeItemAtPath:[_crashesDir stringByAppendingPathComponent:[_crashFiles objectAtIndex:i]] error:&error]; + } + [_crashFiles removeAllObjects]; + + [[NSUserDefaults standardUserDefaults] setObject:nil forKey:kApprovedCrashReports]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void)_sendCrashReports { + // send it to the next runloop + [self performSelector:@selector(_performSendingCrashReports) withObject:nil afterDelay:0.0f]; +} + +- (void)_checkForFeedbackStatus { + NSMutableURLRequest *request = nil; + + request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@api/2/apps/%@/crashes/%@", + self.submissionURL, + [self.appIdentifier stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding], + _feedbackRequestID + ] + ]]; + + [request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData]; + [request setValue:@"Quincy/iOS" forHTTPHeaderField:@"User-Agent"]; + [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; + [request setTimeoutInterval: 15]; + [request setHTTPMethod:@"GET"]; + + _serverResult = CrashReportStatusUnknown; + _statusCode = 200; + + // Release when done in the delegate method + _responseData = [[NSMutableData alloc] init]; + + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(connectionOpened)]) { + [self.delegate connectionOpened]; + } + + _urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; +} + +- (void)_postXML:(NSString*)xml toURL:(NSURL*)url { + NSMutableURLRequest *request = nil; + NSString *boundary = @"----FOO"; + + if (self.appIdentifier) { + request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@api/2/apps/%@/crashes", + self.submissionURL, + [self.appIdentifier stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] + ] + ]]; + } else { + request = [NSMutableURLRequest requestWithURL:url]; + } + + [request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData]; + [request setValue:@"Quincy/iOS" forHTTPHeaderField:@"User-Agent"]; + [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; + [request setTimeoutInterval: 15]; + [request setHTTPMethod:@"POST"]; + NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; + [request setValue:contentType forHTTPHeaderField:@"Content-type"]; + + NSMutableData *postBody = [NSMutableData data]; + [postBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + if (self.appIdentifier) { + [postBody appendData:[@"Content-Disposition: form-data; name=\"xml\"; filename=\"crash.xml\"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:[[NSString stringWithFormat:@"Content-Type: text/xml\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + } else { + [postBody appendData:[@"Content-Disposition: form-data; name=\"xmlstring\"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + } + [postBody appendData:[xml dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + [request setHTTPBody:postBody]; + + _serverResult = CrashReportStatusUnknown; + _statusCode = 200; + + //Release when done in the delegate method + _responseData = [[NSMutableData alloc] init]; + + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(connectionOpened)]) { + [self.delegate connectionOpened]; + } + + _urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; + + if (!_urlConnection) { + _sendingInProgress = NO; + } +} + +#pragma mark NSURLConnection Delegate + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + _statusCode = [(NSHTTPURLResponse *)response statusCode]; + } +} + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + [_responseData appendData:data]; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { + [_responseData release]; + _responseData = nil; + _urlConnection = nil; + + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(connectionClosed)]) { + [self.delegate connectionClosed]; + } + + _sendingInProgress = NO; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + if (_statusCode >= 200 && _statusCode < 400) { + [self _cleanCrashReports]; + + if (self.appIdentifier) { + // HockeyApp uses PList XML format + NSMutableDictionary *response = [NSPropertyListSerialization propertyListFromData:_responseData + mutabilityOption:NSPropertyListMutableContainersAndLeaves + format:nil + errorDescription:NULL]; + _serverResult = (CrashReportStatus)[[response objectForKey:@"status"] intValue]; + _feedbackRequestID = [[NSString alloc] initWithString:[response objectForKey:@"id"]]; + _feedbackDelayInterval = [[response objectForKey:@"delay"] floatValue]; + if (_feedbackDelayInterval > 0) + _feedbackDelayInterval *= 0.01; + } else { + NSXMLParser *parser = [[NSXMLParser alloc] initWithData:_responseData]; + // Set self as the delegate of the parser so that it will receive the parser delegate methods callbacks. + [parser setDelegate:self]; + // Depending on the XML document you're parsing, you may want to enable these features of NSXMLParser. + [parser setShouldProcessNamespaces:NO]; + [parser setShouldReportNamespacePrefixes:NO]; + [parser setShouldResolveExternalEntities:NO]; + + [parser parse]; + + [parser release]; + } + + if ([self isFeedbackActivated]) { + if (self.appIdentifier) { + // only proceed if the server did not report any problem + if (_serverResult == CrashReportStatusQueued) { + // the report is still in the queue + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_checkForFeedbackStatus) object:nil]; + [self performSelector:@selector(_checkForFeedbackStatus) withObject:nil afterDelay:_feedbackDelayInterval]; + } else { + // we do have a status, show it if needed + [self showCrashStatusMessage]; + } + } else { + [self showCrashStatusMessage]; + } + } + } + + [_responseData release]; + _responseData = nil; + _urlConnection = nil; + + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(connectionClosed)]) { + [self.delegate connectionClosed]; + } + + _sendingInProgress = NO; +} + +#pragma mark PLCrashReporter + +// +// Called to handle a pending crash report. +// +- (void) handleCrashReport { + PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter]; + NSError *error = NULL; + + // check if the next call ran successfully the last time + if (_analyzerStarted == 0) { + // mark the start of the routine + _analyzerStarted = 1; + [[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInt:_analyzerStarted] forKey:kQuincyKitAnalyzerStarted]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + // Try loading the crash report + _crashData = [[NSData alloc] initWithData:[crashReporter loadPendingCrashReportDataAndReturnError: &error]]; + + NSString *cacheFilename = [NSString stringWithFormat: @"%.0f", [NSDate timeIntervalSinceReferenceDate]]; + + if (_crashData == nil) { + NSLog(@"Could not load crash report: %@", error); + } else { + [_crashData writeToFile:[_crashesDir stringByAppendingPathComponent: cacheFilename] atomically:YES]; + } + } + + // Purge the report + // mark the end of the routine + _analyzerStarted = 0; + [[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInt:_analyzerStarted] forKey:kQuincyKitAnalyzerStarted]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + [crashReporter purgePendingCrashReport]; + return; +} + + +@end diff --git a/Classes/CNSHockeyManager.h b/Classes/CNSHockeyManager.h new file mode 100644 index 0000000000..c99bdb7dc3 --- /dev/null +++ b/Classes/CNSHockeyManager.h @@ -0,0 +1,29 @@ +// Copyright 2011 Codenauts UG (haftungsbeschränkt). All rights reserved. +// See LICENSE.txt for author information. +// +// 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. + + +@interface CNSHockeyManager : NSObject + ++ (CNSHockeyManager *)sharedHockeyManager; + +- (void)configureWithIdentifier:(NSString *)appIdentifier delegate:(id)delegate; + +@end diff --git a/Classes/CNSHockeyManager.m b/Classes/CNSHockeyManager.m new file mode 100644 index 0000000000..e9c601c16c --- /dev/null +++ b/Classes/CNSHockeyManager.m @@ -0,0 +1,111 @@ +// Copyright 2011 Codenauts UG (haftungsbeschränkt). All rights reserved. +// See LICENSE.txt for author information. +// +// 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 "CNSHockeyManager.h" +#import "BWQuincyManager.h" +#import "BWHockeyManager.h" + +#ifdef JMC_PRESENT +#import "JMC.h" +#endif + +@interface CNSHockeyManager () + +#ifdef JMC_PRESENT +- (void)configureJMC; +#endif + +@end + +@implementation CNSHockeyManager + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 40000 ++ (CNSHockeyManager *)sharedHockeyManager { + static CNSHockeyManager *sharedInstance = nil; + static dispatch_once_t pred; + + if (sharedInstance) { + return sharedInstance; + } + + dispatch_once(&pred, ^{ + sharedInstance = [CNSHockeyManager alloc]; + sharedInstance = [sharedInstance init]; + }); + + return sharedInstance; +} +#else ++ (CNSHockeyManager *)sharedHockeyManager { + static CNSHockeyManager *hockeyManager = nil; + + if (hockeyManager == nil) { + hockeyManager = [[CNSHockeyManager alloc] init]; + } + + return hockeyManager; +} +#endif + +- (void)configureWithIdentifier:(NSString *)appIdentifier delegate:(id)delegate { + // Crash Reporting + [[BWQuincyManager sharedQuincyManager] setAppIdentifier:appIdentifier]; + + // Distribution + [[BWHockeyManager sharedHockeyManager] setAppIdentifier:appIdentifier]; + [[BWHockeyManager sharedHockeyManager] setUpdateURL:@"http://192.168.178.53:3000"]; + [[BWHockeyManager sharedHockeyManager] setCheckForTracker:YES]; + +#ifdef JMC_PRESENT + // JMC + [[[JMC instance] options] setCrashReportingEnabled:NO]; + [[BWHockeyManager sharedHockeyManager] addObserver:self forKeyPath:@"trackerConfig" options:0 context:nil]; + [self performSelector:@selector(configureJMC) withObject:nil afterDelay:0]; +#endif +} + +#ifdef JMC_PRESENT +- (void)configureJMC { + // Return if JMC is already configured + if ([[JMC instance] url]) { + return; + } + + // Configure JMC from user defaults + NSDictionary *config = [[NSUserDefaults standardUserDefaults] valueForKey:@"CNSTrackerConfig"]; + if (([[config valueForKey:@"enabled"] boolValue]) && + ([[config valueForKey:@"url"] length] > 0) && + ([[config valueForKey:@"key"] length] > 0) && + ([[config valueForKey:@"project"] length] > 0)) { + [[JMC instance] configureJiraConnect:[config valueForKey:@"url"] projectKey:[config valueForKey:@"project"] apiKey:[config valueForKey:@"key"]]; + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([object trackerConfig]) { + [[NSUserDefaults standardUserDefaults] setValue:[object trackerConfig] forKey:@"CNSTrackerConfig"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + [self configureJMC]; + } +} +#endif + +@end diff --git a/Classes/NSString+HockeyAdditions.h b/Classes/NSString+HockeyAdditions.h new file mode 100644 index 0000000000..d32e13d4a6 --- /dev/null +++ b/Classes/NSString+HockeyAdditions.h @@ -0,0 +1,33 @@ +// +// NSString+HockeyAdditions.h +// +// Created by Jon Crosby on 10/19/07. +// Copyright 2007 Kaboomerang LLC. 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 + +@interface NSString (HockeyAdditions) + +- (NSString *)bw_URLEncodedString; +- (NSString *)bw_URLDecodedString; + +@end diff --git a/Classes/NSString+HockeyAdditions.m b/Classes/NSString+HockeyAdditions.m new file mode 100644 index 0000000000..5db3528dfe --- /dev/null +++ b/Classes/NSString+HockeyAdditions.m @@ -0,0 +1,50 @@ +// +// NSString+HockeyAdditions.m +// +// Created by Jon Crosby on 10/19/07. +// Copyright 2007 Kaboomerang LLC. 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 "NSString+HockeyAdditions.h" + + +@implementation NSString (HockeyAdditions) + +- (NSString *)bw_URLEncodedString { + NSString *result = (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, + (CFStringRef)self, + NULL, + CFSTR("!*'();:@&=+$,/?%#[]"), + kCFStringEncodingUTF8); + [result autorelease]; + return result; +} + +- (NSString*)bw_URLDecodedString { + NSString *result = (NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, + (CFStringRef)self, + CFSTR(""), + kCFStringEncodingUTF8); + [result autorelease]; + return result; +} + +@end diff --git a/Classes/PSAppStoreHeader.h b/Classes/PSAppStoreHeader.h new file mode 100644 index 0000000000..1d9a8f7a3e --- /dev/null +++ b/Classes/PSAppStoreHeader.h @@ -0,0 +1,42 @@ +// +// PSAppStoreHeader.h +// HockeyDemo +// +// Created by Peter Steinberger on 09.01.11. +// Copyright 2011 Peter Steinberger. 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 + +@interface PSAppStoreHeader : UIView { + NSString *headerLabel_; + NSString *middleHeaderLabel_; + NSString *subHeaderLabel; + UIImage *iconImage_; + + UIImage *reflectedImage_; +} + +@property (nonatomic, copy) NSString *headerLabel; +@property (nonatomic, copy) NSString *middleHeaderLabel; +@property (nonatomic, copy) NSString *subHeaderLabel; +@property (nonatomic, retain) UIImage *iconImage; + +@end diff --git a/Classes/PSAppStoreHeader.m b/Classes/PSAppStoreHeader.m new file mode 100644 index 0000000000..c7898aeca0 --- /dev/null +++ b/Classes/PSAppStoreHeader.m @@ -0,0 +1,173 @@ +// +// PSAppStoreHeader.m +// HockeyDemo +// +// Created by Peter Steinberger on 09.01.11. +// Copyright 2011 Peter Steinberger. 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 "PSAppStoreHeader.h" +#import "UIImage+HockeyAdditions.h" +#import "BWGlobal.h" + +#define BW_RGBCOLOR(r,g,b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1] + +#define kLightGrayColor BW_RGBCOLOR(200, 202, 204) +#define kDarkGrayColor BW_RGBCOLOR(140, 141, 142) + +#define kImageHeight 57 +#define kReflectionHeight 20 +#define kImageBorderRadius 10 +#define kImageMargin 8 +#define kTextRow kImageMargin*2 + kImageHeight + +@implementation PSAppStoreHeader + +@synthesize headerLabel = headerLabel_; +@synthesize middleHeaderLabel = middleHeaderLabel_; +@synthesize subHeaderLabel; +@synthesize iconImage = iconImage_; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSObject + +- (id)initWithFrame:(CGRect)frame { + if ((self = [super initWithFrame:frame])) { + self.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.backgroundColor = kLightGrayColor; + } + return self; +} + +- (void)dealloc { + [headerLabel_ release]; + [middleHeaderLabel_ release]; + [subHeaderLabel release]; + [iconImage_ release]; + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UIView + +- (void)drawRect:(CGRect)rect { + CGRect bounds = self.bounds; + CGFloat globalWidth = self.frame.size.width; + CGContextRef context = UIGraphicsGetCurrentContext(); + + // draw the gradient + NSArray *colors = [NSArray arrayWithObjects:(id)kDarkGrayColor.CGColor, (id)kLightGrayColor.CGColor, nil]; + CGGradientRef gradient = CGGradientCreateWithColors(CGColorGetColorSpace((CGColorRef)[colors objectAtIndex:0]), (CFArrayRef)colors, (CGFloat[2]){0, 1}); + CGPoint top = CGPointMake(CGRectGetMidX(bounds), bounds.origin.y); + CGPoint bottom = CGPointMake(CGRectGetMidX(bounds), CGRectGetMaxY(bounds)-kReflectionHeight); + CGContextDrawLinearGradient(context, gradient, top, bottom, 0); + CGGradientRelease(gradient); + + // draw header name + UIColor *mainTextColor = BW_RGBCOLOR(0,0,0); + UIColor *secondaryTextColor = BW_RGBCOLOR(48,48,48); + UIFont *mainFont = [UIFont boldSystemFontOfSize:20]; + UIFont *secondaryFont = [UIFont boldSystemFontOfSize:12]; + UIFont *smallFont = [UIFont systemFontOfSize:12]; + + float myColorValues[] = {255, 255, 255, .6}; + CGColorSpaceRef myColorSpace = CGColorSpaceCreateDeviceRGB(); + CGColorRef myColor = CGColorCreate(myColorSpace, myColorValues); + + // icon + [iconImage_ drawAtPoint:CGPointMake(kImageMargin, kImageMargin)]; + [reflectedImage_ drawAtPoint:CGPointMake(kImageMargin, kImageMargin+kImageHeight)]; + + // shadows are a beast + NSInteger shadowOffset = 2; + BW_IF_IOS4_OR_GREATER(if([[UIScreen mainScreen] scale] == 2) shadowOffset = 1;) + BW_IF_IOS5_OR_GREATER(shadowOffset = 1;) // iOS5 changes this - again! + + BW_IF_3_2_OR_GREATER(CGContextSetShadowWithColor(context, CGSizeMake(shadowOffset, shadowOffset), 0, myColor);) + BW_IF_PRE_3_2(shadowOffset=1;CGContextSetShadowWithColor(context, CGSizeMake(shadowOffset, -shadowOffset), 0, myColor);) + + + [mainTextColor set]; + [headerLabel_ drawInRect:CGRectMake(kTextRow, kImageMargin, globalWidth-kTextRow, 20) withFont:mainFont lineBreakMode:UILineBreakModeTailTruncation]; + + // middle + [secondaryTextColor set]; + [middleHeaderLabel_ drawInRect:CGRectMake(kTextRow, kImageMargin + 25, globalWidth-kTextRow, 20) withFont:secondaryFont lineBreakMode:UILineBreakModeTailTruncation]; + CGContextSetShadowWithColor(context, CGSizeZero, 0, nil); + + // sub + [secondaryTextColor set]; + [subHeaderLabel drawAtPoint:CGPointMake(kTextRow, kImageMargin+kImageHeight-12) forWidth:globalWidth-kTextRow withFont:smallFont lineBreakMode:UILineBreakModeTailTruncation]; + + CGColorRelease(myColor); + CGColorSpaceRelease(myColorSpace); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Properties + +- (void)setHeaderLabel:(NSString *)anHeaderLabel { + if (headerLabel_ != anHeaderLabel) { + [headerLabel_ release]; + headerLabel_ = [anHeaderLabel copy]; + [self setNeedsDisplay]; + } +} + +- (void)setMiddleHeaderLabel:(NSString *)aMiddleHeaderLabel { + if (middleHeaderLabel_ != aMiddleHeaderLabel) { + [middleHeaderLabel_ release]; + middleHeaderLabel_ = [aMiddleHeaderLabel copy]; + [self setNeedsDisplay]; + } +} + +- (void)setSubHeaderLabel:(NSString *)aSubHeaderLabel { + if (subHeaderLabel != aSubHeaderLabel) { + [subHeaderLabel release]; + subHeaderLabel = [aSubHeaderLabel copy]; + [self setNeedsDisplay]; + } +} + +- (void)setIconImage:(UIImage *)anIconImage { + if (iconImage_ != anIconImage) { + [iconImage_ release]; + + // scale, make borders and reflection + iconImage_ = [anIconImage bw_imageToFitSize:CGSizeMake(kImageHeight, kImageHeight) honorScaleFactor:YES]; + iconImage_ = [[iconImage_ bw_roundedCornerImage:kImageBorderRadius borderSize:0.0] retain]; + + // create reflected image + [reflectedImage_ release]; + reflectedImage_ = nil; + if (anIconImage) { + reflectedImage_ = [[iconImage_ bw_reflectedImageWithHeight:kReflectionHeight fromAlpha:0.5 toAlpha:0.0] retain]; + } + [self setNeedsDisplay]; + } +} + +@end diff --git a/Classes/PSStoreButton.h b/Classes/PSStoreButton.h new file mode 100644 index 0000000000..10356f0333 --- /dev/null +++ b/Classes/PSStoreButton.h @@ -0,0 +1,81 @@ +// +// PSStoreButton.h +// HockeyDemo +// +// Created by Peter Steinberger on 09.01.11. +// Copyright 2011 Peter Steinberger. 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 + +// defines a button action set (data container) +@interface PSStoreButtonData : NSObject { + CGPoint customPadding_; + NSString *label_; + NSArray *colors_; + BOOL enabled_; +} + ++ (id)dataWithLabel:(NSString*)aLabel colors:(NSArray*)aColors enabled:(BOOL)flag; + +@property (nonatomic, copy) NSString *label; +@property (nonatomic, retain) NSArray *colors; +@property (nonatomic, assign, getter=isEnabled) BOOL enabled; + +@end + + +@class PSStoreButton; +@protocol PSStoreButtonDelegate +- (void)storeButtonFired:(PSStoreButton *)button; +@end + + +// Simulate the Paymeny-Button from the AppStore +// The interface is flexible, so there is now fixed order +@interface PSStoreButton : UIButton { + PSStoreButtonData *buttonData_; + id buttonDelegate_; + + CAGradientLayer *gradient_; + CGPoint customPadding_; +} + +- (id)initWithFrame:(CGRect)frame; +- (id)initWithPadding:(CGPoint)padding; + +// action delegate +@property (nonatomic, assign) id buttonDelegate; + +// change the button layer +@property (nonatomic, retain) PSStoreButtonData *buttonData; +- (void)setButtonData:(PSStoreButtonData *)aButtonData animated:(BOOL)animated; + +// align helper +@property (nonatomic, assign) CGPoint customPadding; +- (void)alignToSuperview; + +// helpers to mimic an AppStore button ++ (NSArray *)appStoreGreenColor; ++ (NSArray *)appStoreBlueColor; ++ (NSArray *)appStoreGrayColor; + +@end diff --git a/Classes/PSStoreButton.m b/Classes/PSStoreButton.m new file mode 100644 index 0000000000..4eb23997cb --- /dev/null +++ b/Classes/PSStoreButton.m @@ -0,0 +1,295 @@ +// +// PSStoreButton.m +// HockeyDemo +// +// Created by Peter Steinberger on 09.01.11. +// Copyright 2011 Peter Steinberger. All rights reserved. +// +// This code was inspired by https://github.com/dhmspector/ZIStoreButton +// +// 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 "PSStoreButton.h" + +#ifdef DEBUG +#define PSLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__); +#else +#define PSLog(...) +#endif + +#define PS_RGBCOLOR(r,g,b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1] +#define PS_MIN_HEIGHT 25.0f +#define PS_MAX_WIDTH 120.0f +#define PS_PADDING 12.0f +#define kDefaultButtonAnimationTime 0.25f + +@implementation PSStoreButtonData + +@synthesize label = label_; +@synthesize colors = colors_; +@synthesize enabled = enabled_; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSObject + +- (id)initWithLabel:(NSString*)aLabel colors:(NSArray*)aColors enabled:(BOOL)flag { + if ((self = [super init])) { + self.label = aLabel; + self.colors = aColors; + self.enabled = flag; + } + return self; +} + ++ (id)dataWithLabel:(NSString*)aLabel colors:(NSArray*)aColors enabled:(BOOL)flag { + return [[[[self class] alloc] initWithLabel:aLabel colors:aColors enabled:flag] autorelease]; +} + +- (void)dealloc { + [label_ release]; + [colors_ release]; + + [super dealloc]; +} +@end + + +@interface PSStoreButton () +// call when buttonData was updated +- (void)updateButtonAnimated:(BOOL)animated; +@end + + +@implementation PSStoreButton + +@synthesize buttonData = buttonData_; +@synthesize buttonDelegate = buttonDelegate_; +@synthesize customPadding = customPadding_; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark private + +- (void)touchedUpOutside:(id)sender { + PSLog(@"touched outside..."); +} + +- (void)buttonPressed:(id)sender { + PSLog(@"calling delegate:storeButtonFired for %@", sender); + [buttonDelegate_ storeButtonFired:self]; +} + +- (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context { + // show text again, but only if animation did finish (or else another animation is on the way) + if ([finished boolValue]) { + [self setTitle:self.buttonData.label forState:UIControlStateNormal]; + } +} + +- (void)updateButtonAnimated:(BOOL)animated { + if (animated) { + // hide text, then start animation + [self setTitle:@"" forState:UIControlStateNormal]; + [UIView beginAnimations:@"storeButtonUpdate" context:nil]; + [UIView setAnimationBeginsFromCurrentState:YES]; + [UIView setAnimationDuration:kDefaultButtonAnimationTime]; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)]; + }else { + [self setTitle:self.buttonData.label forState:UIControlStateNormal]; + } + + self.enabled = self.buttonData.isEnabled; + gradient_.colors = self.buttonData.colors; + + // show white or gray text, depending on the state + if (self.buttonData.isEnabled) { + [self setTitleShadowColor:[UIColor colorWithWhite:0.200 alpha:1.000] forState:UIControlStateNormal]; + [self.titleLabel setShadowOffset:CGSizeMake(0.0, -0.6)]; + [self setTitleColor:[UIColor colorWithWhite:1.0 alpha:1.000] forState:UIControlStateNormal]; + }else { + [self.titleLabel setShadowOffset:CGSizeMake(0.0, 0.0)]; + [self setTitleColor:PS_RGBCOLOR(148,150,151) forState:UIControlStateNormal]; + } + + // calculate optimal new size + CGSize sizeThatFits = [self sizeThatFits:CGSizeZero]; + + // move sublayer (can't be animated explcitely) + for (CALayer *aLayer in self.layer.sublayers) { + [CATransaction begin]; + + if (animated) { + [CATransaction setAnimationDuration:kDefaultButtonAnimationTime]; + [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]]; + }else { + // frame is calculated and explicitely animated. so we absolutely need kCATransactionDisableActions + [CATransaction setValue:[NSNumber numberWithBool:YES] forKey:kCATransactionDisableActions]; + } + + CGRect newFrame = aLayer.frame; + newFrame.size.width = sizeThatFits.width; + aLayer.frame = newFrame; + + [CATransaction commit]; + } + + // set outer frame changes + self.titleEdgeInsets = UIEdgeInsetsMake(2.0, self.titleEdgeInsets.left, 0.0, 0.0); + [self alignToSuperview]; + + if (animated) { + [UIView commitAnimations]; + } +} + +- (void)alignToSuperview { + [self sizeToFit]; + if (self.superview) { + CGRect cr = self.frame; + cr.origin.y = customPadding_.y; + cr.origin.x = self.superview.frame.size.width - cr.size.width - customPadding_.x * 2; + self.frame = cr; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSObject + +- (id)initWithFrame:(CGRect)frame { + if ((self = [super initWithFrame:frame])) { + self.layer.needsDisplayOnBoundsChange = YES; + + // setup title label + [self.titleLabel setFont:[UIFont boldSystemFontOfSize:13.0]]; + + // register for touch events + [self addTarget:self action:@selector(touchedUpOutside:) forControlEvents:UIControlEventTouchUpOutside]; + [self addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside]; + + // border layers for more sex! + CAGradientLayer *bevelLayer = [CAGradientLayer layer]; + bevelLayer.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:0.4 alpha:1.0] CGColor], [[UIColor whiteColor] CGColor], nil]; + bevelLayer.frame = CGRectMake(0.0, 0.0, CGRectGetWidth(frame), CGRectGetHeight(frame)); + bevelLayer.cornerRadius = 2.5; + bevelLayer.needsDisplayOnBoundsChange = YES; + [self.layer addSublayer:bevelLayer]; + + CAGradientLayer *topBorderLayer = [CAGradientLayer layer]; + topBorderLayer.colors = [NSArray arrayWithObjects:(id)[[UIColor darkGrayColor] CGColor], [[UIColor lightGrayColor] CGColor], nil]; + topBorderLayer.frame = CGRectMake(0.5, 0.5, CGRectGetWidth(frame) - 1.0, CGRectGetHeight(frame) - 1.0); + topBorderLayer.cornerRadius = 2.6; + topBorderLayer.needsDisplayOnBoundsChange = YES; + [self.layer addSublayer:topBorderLayer]; + + // main gradient layer + gradient_ = [[CAGradientLayer layer] retain]; + gradient_.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0], [NSNumber numberWithFloat:1.0], nil];//[NSNumber numberWithFloat:0.500], [NSNumber numberWithFloat:0.5001], + gradient_.frame = CGRectMake(0.75, 0.75, CGRectGetWidth(frame) - 1.5, CGRectGetHeight(frame) - 1.5); + gradient_.cornerRadius = 2.5; + gradient_.needsDisplayOnBoundsChange = YES; + [self.layer addSublayer:gradient_]; + [self bringSubviewToFront:self.titleLabel]; + } + return self; +} + +- (id)initWithPadding:(CGPoint)padding { + if ((self = [self initWithFrame:CGRectMake(0, 0, 40, PS_MIN_HEIGHT)])) { + customPadding_ = padding; + } + return self; +} + +- (void)dealloc { + [buttonData_ release]; + [gradient_ release]; + + [super dealloc]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UIView + +- (CGSize)sizeThatFits:(CGSize)size { + CGSize constr = (CGSize){.height = self.frame.size.height, .width = PS_MAX_WIDTH}; + CGSize newSize = [self.buttonData.label sizeWithFont:self.titleLabel.font constrainedToSize:constr lineBreakMode:UILineBreakModeMiddleTruncation]; + CGFloat newWidth = newSize.width + (PS_PADDING * 2); + CGFloat newHeight = PS_MIN_HEIGHT > newSize.height ? PS_MIN_HEIGHT : newSize.height; + + CGSize sizeThatFits = CGSizeMake(newWidth, newHeight); + return sizeThatFits; +} + +- (void)setFrame:(CGRect)aRect { + [super setFrame:aRect]; + + // copy frame changes to sublayers (but watch out for NaN's) + for (CALayer *aLayer in self.layer.sublayers) { + CGRect rect = aLayer.frame; + rect.size.width = self.frame.size.width; + rect.size.height = self.frame.size.height; + aLayer.frame = rect; + [aLayer layoutIfNeeded]; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Properties + +- (void)setButtonData:(PSStoreButtonData *)aButtonData { + [self setButtonData:aButtonData animated:NO]; +} + +- (void)setButtonData:(PSStoreButtonData *)aButtonData animated:(BOOL)animated { + if (buttonData_ != aButtonData) { + [buttonData_ release]; + buttonData_ = [aButtonData retain]; + } + + [self updateButtonAnimated:animated]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Static + ++ (NSArray *)appStoreGreenColor { + return [NSArray arrayWithObjects:(id) + [UIColor colorWithRed:0.482 green:0.674 blue:0.406 alpha:1.000].CGColor, + [UIColor colorWithRed:0.299 green:0.606 blue:0.163 alpha:1.000].CGColor, nil]; +} + ++ (NSArray *)appStoreBlueColor { + return [NSArray arrayWithObjects:(id) + [UIColor colorWithRed:0.306 green:0.380 blue:0.547 alpha:1.000].CGColor, + [UIColor colorWithRed:0.129 green:0.220 blue:0.452 alpha:1.000].CGColor, nil]; +} + ++ (NSArray *)appStoreGrayColor { + return [NSArray arrayWithObjects:(id) + PS_RGBCOLOR(187,189,191).CGColor, + PS_RGBCOLOR(210,210,210).CGColor, nil]; +} + +@end diff --git a/Classes/PSWebTableViewCell.h b/Classes/PSWebTableViewCell.h new file mode 100644 index 0000000000..b858e324e7 --- /dev/null +++ b/Classes/PSWebTableViewCell.h @@ -0,0 +1,44 @@ +// +// PSWebTableViewCell.h +// HockeyDemo +// +// Created by Peter Steinberger on 04.02.11. +// Copyright 2011 Peter Steinberger. 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 + +@interface PSWebTableViewCell : UITableViewCell { + UIWebView *webView_; + NSString *webViewContent_; + CGSize webViewSize_; + + UIColor *cellBackgroundColor_; +} + +@property (nonatomic, retain) UIWebView *webView; +@property (nonatomic, copy) NSString *webViewContent; +@property (nonatomic, assign) CGSize webViewSize; +@property (nonatomic, retain) UIColor *cellBackgroundColor; + +- (void)addWebView; + +@end diff --git a/Classes/PSWebTableViewCell.m b/Classes/PSWebTableViewCell.m new file mode 100644 index 0000000000..005d2738f2 --- /dev/null +++ b/Classes/PSWebTableViewCell.m @@ -0,0 +1,189 @@ +// +// PSWebTableViewCell.m +// HockeyDemo +// +// Created by Peter Steinberger on 04.02.11. +// Copyright 2011 Peter Steinberger. 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 "PSWebTableViewCell.h" +#import "BWGlobal.h" + +@implementation PSWebTableViewCell + +static NSString* PSWebTableViewCellHtmlTemplate = @"\ +\ +\ +\ +\ +\ +\ +%@\ +\ +\ +"; + +@synthesize webView = webView_; +@synthesize webViewContent = webViewContent_; +@synthesize webViewSize = webViewSize_; +@synthesize cellBackgroundColor = cellBackgroundColor_; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark private + +- (void)addWebView { + if(webViewContent_) { + CGRect webViewRect = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height); + if(!webView_) { + webView_ = [[[UIWebView alloc] initWithFrame:webViewRect] retain]; + [self addSubview:webView_]; + webView_.hidden = YES; + webView_.backgroundColor = self.cellBackgroundColor; + webView_.opaque = NO; + webView_.delegate = self; + webView_.autoresizingMask = UIViewAutoresizingFlexibleWidth; + + for(UIView* subView in webView_.subviews){ + if([subView isKindOfClass:[UIScrollView class]]){ + // disable scrolling + UIScrollView *sv = (UIScrollView *)subView; + sv.scrollEnabled = NO; + sv.bounces = NO; + + // hide shadow + for (UIView* shadowView in [subView subviews]) { + if ([shadowView isKindOfClass:[UIImageView class]]) { + shadowView.hidden = YES; + } + } + } + } + } + else + webView_.frame = webViewRect; + + NSString *deviceWidth = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? [NSString stringWithFormat:@"%d", CGRectGetWidth(self.bounds)] : @"device-width"; + //BWHockeyLog(@"%@\n%@\%@", PSWebTableViewCellHtmlTemplate, deviceWidth, self.webViewContent); + NSString *contentHtml = [NSString stringWithFormat:PSWebTableViewCellHtmlTemplate, deviceWidth, self.webViewContent]; + [webView_ loadHTMLString:contentHtml baseURL:nil]; + } +} + +- (void)showWebView { + webView_.hidden = NO; + self.textLabel.text = @""; + [self setNeedsDisplay]; +} + + +- (void)removeWebView { + if(webView_) { + webView_.delegate = nil; + [webView_ resignFirstResponder]; + [webView_ removeFromSuperview]; + [webView_ release]; + } + webView_ = nil; + [self setNeedsDisplay]; +} + + +- (void)setWebViewContent:(NSString *)aWebViewContent { + if (webViewContent_ != aWebViewContent) { + [webViewContent_ release]; + webViewContent_ = [aWebViewContent retain]; + + // add basic accessiblity (prevents "snarfed from ivar layout") logs + self.accessibilityLabel = aWebViewContent; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSObject + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + if((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { + self.cellBackgroundColor = [UIColor clearColor]; + } + return self; +} + +- (void)dealloc { + [self removeWebView]; + [webViewContent_ release]; + [super dealloc]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UIView + +- (void)setFrame:(CGRect)aFrame { + BOOL needChange = !CGRectEqualToRect(aFrame, self.frame); + [super setFrame:aFrame]; + + if (needChange) { + [self addWebView]; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UITableViewCell + +- (void)prepareForReuse { + [self removeWebView]; + self.webViewContent = nil; + [super prepareForReuse]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UIWebView + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { + if(navigationType == UIWebViewNavigationTypeOther) + return YES; + + return NO; +} + + +- (void)webViewDidFinishLoad:(UIWebView *)webView { + if(webViewContent_) + [self showWebView]; + + CGRect frame = webView_.frame; + frame.size.height = 1; + webView_.frame = frame; + CGSize fittingSize = [webView_ sizeThatFits:CGSizeZero]; + frame.size = fittingSize; + webView_.frame = frame; + + // sizeThatFits is not reliable - use javascript for optimal height + NSString *output = [webView_ stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"]; + self.webViewSize = CGSizeMake(fittingSize.width, [output integerValue]); +} + +@end diff --git a/Classes/UIImage+HockeyAdditions.h b/Classes/UIImage+HockeyAdditions.h new file mode 100644 index 0000000000..dccc74d1ba --- /dev/null +++ b/Classes/UIImage+HockeyAdditions.h @@ -0,0 +1,38 @@ +// +// UIImage+HockeyAdditions.h +// HockeyDemo +// +// Created by Peter Steinberger on 10.01.11. +// Copyright 2011 Peter Steinberger. 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 + +@interface UIImage (HockeyAdditions) + +- (UIImage *)bw_roundedCornerImage:(NSInteger)cornerSize borderSize:(NSInteger)borderSize; +- (UIImage *)bw_imageToFitSize:(CGSize)fitSize honorScaleFactor:(BOOL)honorScaleFactor; +- (UIImage *)bw_reflectedImageWithHeight:(NSUInteger)height fromAlpha:(float)fromAlpha toAlpha:(float)toAlpha; + +- (id)bw_initWithContentsOfResolutionIndependentFile:(NSString *)path NS_RETURNS_RETAINED; ++ (UIImage*)bw_imageWithContentsOfResolutionIndependentFile:(NSString *)path; ++ (UIImage *)bw_imageNamed:(NSString *)imageName bundle:(NSString *)bundleName; + +@end diff --git a/Classes/UIImage+HockeyAdditions.m b/Classes/UIImage+HockeyAdditions.m new file mode 100644 index 0000000000..79cd70042f --- /dev/null +++ b/Classes/UIImage+HockeyAdditions.m @@ -0,0 +1,346 @@ +// +// UIImage+HockeyAdditions.m +// HockeyDemo +// +// Created by Peter Steinberger on 10.01.11. +// Copyright 2011 Peter Steinberger. 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 "UIImage+HockeyAdditions.h" +#import "BWGlobal.h" + +// Private helper methods +@interface UIImage (HockeyAdditionsPrivate) +- (void)addRoundedRectToPath:(CGRect)rect context:(CGContextRef)context ovalWidth:(CGFloat)ovalWidth ovalHeight:(CGFloat)ovalHeight; + +CGContextRef MyOpenBitmapContext(int pixelsWide, int pixelsHigh); +CGImageRef CreateGradientImage(int pixelsWide, int pixelsHigh, float fromAlpha, float toAlpha); +@end + +@implementation UIImage (HockeyAdditions) + +// Returns true if the image has an alpha layer +- (BOOL)hasAlpha { + CGImageAlphaInfo alpha = CGImageGetAlphaInfo(self.CGImage); + return (alpha == kCGImageAlphaFirst || + alpha == kCGImageAlphaLast || + alpha == kCGImageAlphaPremultipliedFirst || + alpha == kCGImageAlphaPremultipliedLast); +} + +// Returns a copy of the given image, adding an alpha channel if it doesn't already have one +- (UIImage *)imageWithAlpha { + if ([self hasAlpha]) { + return self; + } + + CGImageRef imageRef = self.CGImage; + size_t width = CGImageGetWidth(imageRef) * self.scale; + size_t height = CGImageGetHeight(imageRef) * self.scale; + + // The bitsPerComponent and bitmapInfo values are hard-coded to prevent an "unsupported parameter combination" error + CGContextRef offscreenContext = CGBitmapContextCreate(NULL, + width, + height, + 8, + 0, + CGImageGetColorSpace(imageRef), + kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); + + // Draw the image into the context and retrieve the new image, which will now have an alpha layer + CGContextDrawImage(offscreenContext, CGRectMake(0, 0, width, height), imageRef); + CGImageRef imageRefWithAlpha = CGBitmapContextCreateImage(offscreenContext); + UIImage *imageWithAlpha = [UIImage imageWithCGImage:imageRefWithAlpha]; + + // Clean up + CGContextRelease(offscreenContext); + CGImageRelease(imageRefWithAlpha); + + return imageWithAlpha; +} + +// Creates a copy of this image with rounded corners +// If borderSize is non-zero, a transparent border of the given size will also be added +// Original author: Björn Sållarp. Used with permission. See: http://blog.sallarp.com/iphone-uiimage-round-corners/ +- (UIImage *)bw_roundedCornerImage:(NSInteger)cornerSize borderSize:(NSInteger)borderSize { + // If the image does not have an alpha layer, add one + + UIImage *roundedImage = nil; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 + BW_IF_IOS4_OR_GREATER( + UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0); // 0.0 for scale means "correct scale for device's main screen". + CGImageRef sourceImg = CGImageCreateWithImageInRect([self CGImage], CGRectMake(0, 0, self.size.width * self.scale, self.size.height * self.scale)); // cropping happens here. + + // Create a clipping path with rounded corners + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextBeginPath(context); + [self addRoundedRectToPath:CGRectMake(borderSize, borderSize, self.size.width - borderSize * 2, self.size.height - borderSize * 2) + context:context + ovalWidth:cornerSize + ovalHeight:cornerSize]; + CGContextClosePath(context); + CGContextClip(context); + + roundedImage = [UIImage imageWithCGImage:sourceImg scale:0.0 orientation:self.imageOrientation]; // create cropped UIImage. + [roundedImage drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)]; // the actual scaling happens here, and orientation is taken care of automatically. + CGImageRelease(sourceImg); + roundedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + ) +#endif + if (!roundedImage) { + // Try older method. + UIImage *image = [self imageWithAlpha]; + + // Build a context that's the same dimensions as the new size + CGContextRef context = CGBitmapContextCreate(NULL, + image.size.width, + image.size.height, + CGImageGetBitsPerComponent(image.CGImage), + 0, + CGImageGetColorSpace(image.CGImage), + CGImageGetBitmapInfo(image.CGImage)); + + // Create a clipping path with rounded corners + CGContextBeginPath(context); + [self addRoundedRectToPath:CGRectMake(borderSize, borderSize, image.size.width - borderSize * 2, image.size.height - borderSize * 2) + context:context + ovalWidth:cornerSize + ovalHeight:cornerSize]; + CGContextClosePath(context); + CGContextClip(context); + + // Draw the image to the context; the clipping path will make anything outside the rounded rect transparent + CGContextDrawImage(context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage); + + // Create a CGImage from the context + CGImageRef clippedImage = CGBitmapContextCreateImage(context); + CGContextRelease(context); + + // Create a UIImage from the CGImage + roundedImage = [UIImage imageWithCGImage:clippedImage]; + CGImageRelease(clippedImage); + } + return roundedImage; +} + +#pragma mark - +#pragma mark Private helper methods + +// Adds a rectangular path to the given context and rounds its corners by the given extents +// Original author: Björn Sållarp. Used with permission. See: http://blog.sallarp.com/iphone-uiimage-round-corners/ +- (void)addRoundedRectToPath:(CGRect)rect context:(CGContextRef)context ovalWidth:(CGFloat)ovalWidth ovalHeight:(CGFloat)ovalHeight { + if (ovalWidth == 0 || ovalHeight == 0) { + CGContextAddRect(context, rect); + return; + } + CGContextSaveGState(context); + CGContextTranslateCTM(context, CGRectGetMinX(rect), CGRectGetMinY(rect)); + CGContextScaleCTM(context, ovalWidth, ovalHeight); + CGFloat fw = CGRectGetWidth(rect) / ovalWidth; + CGFloat fh = CGRectGetHeight(rect) / ovalHeight; + CGContextMoveToPoint(context, fw, fh/2); + CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1); + CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1); + CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1); + CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1); + CGContextClosePath(context); + CGContextRestoreGState(context); +} + +- (UIImage *)bw_imageToFitSize:(CGSize)fitSize honorScaleFactor:(BOOL)honorScaleFactor +{ + float imageScaleFactor = 1.0; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 + if (honorScaleFactor) { + if ([self respondsToSelector:@selector(scale)]) { + imageScaleFactor = [self scale]; + } + } +#endif + + float sourceWidth = [self size].width * imageScaleFactor; + float sourceHeight = [self size].height * imageScaleFactor; + float targetWidth = fitSize.width; + float targetHeight = fitSize.height; + + // Calculate aspect ratios + float sourceRatio = sourceWidth / sourceHeight; + float targetRatio = targetWidth / targetHeight; + + // Determine what side of the source image to use for proportional scaling + BOOL scaleWidth = (sourceRatio <= targetRatio); + // Deal with the case of just scaling proportionally to fit, without cropping + scaleWidth = !scaleWidth; + + // Proportionally scale source image + float scalingFactor, scaledWidth, scaledHeight; + if (scaleWidth) { + scalingFactor = 1.0 / sourceRatio; + scaledWidth = targetWidth; + scaledHeight = round(targetWidth * scalingFactor); + } else { + scalingFactor = sourceRatio; + scaledWidth = round(targetHeight * scalingFactor); + scaledHeight = targetHeight; + } + + // Calculate compositing rectangles + CGRect sourceRect, destRect; + sourceRect = CGRectMake(0, 0, sourceWidth, sourceHeight); + destRect = CGRectMake(0, 0, scaledWidth, scaledHeight); + + // Create appropriately modified image. + UIImage *image = nil; + BW_IF_IOS4_OR_GREATER + ( + UIGraphicsBeginImageContextWithOptions(destRect.size, NO, honorScaleFactor ? 0.0 : 1.0); // 0.0 for scale means "correct scale for device's main screen". + CGImageRef sourceImg = CGImageCreateWithImageInRect([self CGImage], sourceRect); // cropping happens here. + image = [UIImage imageWithCGImage:sourceImg scale:0.0 orientation:self.imageOrientation]; // create cropped UIImage. + [image drawInRect:destRect]; // the actual scaling happens here, and orientation is taken care of automatically. + CGImageRelease(sourceImg); + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + ) + if (!image) { + // Try older method. + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate(NULL, scaledWidth, scaledHeight, 8, (fitSize.width * 4), + colorSpace, kCGImageAlphaPremultipliedLast); + CGImageRef sourceImg = CGImageCreateWithImageInRect([self CGImage], sourceRect); + CGContextDrawImage(context, destRect, sourceImg); + CGImageRelease(sourceImg); + CGImageRef finalImage = CGBitmapContextCreateImage(context); + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + image = [UIImage imageWithCGImage:finalImage]; + CGImageRelease(finalImage); + } + + return image; +} + + + +CGImageRef CreateGradientImage(int pixelsWide, int pixelsHigh, float fromAlpha, float toAlpha) { + CGImageRef theCGImage = NULL; + + // gradient is always black-white and the mask must be in the gray colorspace + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); + + // create the bitmap context + CGContextRef gradientBitmapContext = CGBitmapContextCreate(NULL, pixelsWide, pixelsHigh, + 8, 0, colorSpace, kCGImageAlphaNone); + + // define the start and end grayscale values (with the alpha, even though + // our bitmap context doesn't support alpha the gradient requires it) + CGFloat colors[] = {toAlpha, 1.0, fromAlpha, 1.0}; + + // create the CGGradient and then release the gray color space + CGGradientRef grayScaleGradient = CGGradientCreateWithColorComponents(colorSpace, colors, NULL, 2); + CGColorSpaceRelease(colorSpace); + + // create the start and end points for the gradient vector (straight down) + CGPoint gradientEndPoint = CGPointZero; + CGPoint gradientStartPoint = CGPointMake(0, pixelsHigh); + + // draw the gradient into the gray bitmap context + CGContextDrawLinearGradient(gradientBitmapContext, grayScaleGradient, gradientStartPoint, + gradientEndPoint, kCGGradientDrawsAfterEndLocation); + CGGradientRelease(grayScaleGradient); + + // convert the context into a CGImageRef and release the context + theCGImage = CGBitmapContextCreateImage(gradientBitmapContext); + CGContextRelease(gradientBitmapContext); + + // return the imageref containing the gradient + return theCGImage; +} + +CGContextRef MyOpenBitmapContext(int pixelsWide, int pixelsHigh) { + CGSize size = CGSizeMake(pixelsWide, pixelsHigh); + if (UIGraphicsBeginImageContextWithOptions != NULL) { + UIGraphicsBeginImageContextWithOptions(size, NO, 0.0); + } + else { + UIGraphicsBeginImageContext(size); + } + + return UIGraphicsGetCurrentContext(); +} + +- (UIImage *)bw_reflectedImageWithHeight:(NSUInteger)height fromAlpha:(float)fromAlpha toAlpha:(float)toAlpha { + if(height == 0) + return nil; + + // create a bitmap graphics context the size of the image + CGContextRef mainViewContentContext = MyOpenBitmapContext(self.size.width, height); + + // create a 2 bit CGImage containing a gradient that will be used for masking the + // main view content to create the 'fade' of the reflection. The CGImageCreateWithMask + // function will stretch the bitmap image as required, so we can create a 1 pixel wide gradient + CGImageRef gradientMaskImage = CreateGradientImage(1, height, fromAlpha, toAlpha); + + // create an image by masking the bitmap of the mainView content with the gradient view + // then release the pre-masked content bitmap and the gradient bitmap + CGContextClipToMask(mainViewContentContext, CGRectMake(0.0, 0.0, self.size.width, height), gradientMaskImage); + CGImageRelease(gradientMaskImage); + + // draw the image into the bitmap context + CGContextDrawImage(mainViewContentContext, CGRectMake(0, 0, self.size.width, self.size.height), self.CGImage); + + // convert the finished reflection image to a UIImage + UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); // returns autoreleased + UIGraphicsEndImageContext(); + + return theImage; +} + +- (id)bw_initWithContentsOfResolutionIndependentFile:(NSString *)path { + if ([UIScreen instancesRespondToSelector:@selector(scale)] && (int)[[UIScreen mainScreen] scale] == 2.0) { + NSString *path2x = [[path stringByDeletingLastPathComponent] + stringByAppendingPathComponent:[NSString stringWithFormat:@"%@@2x.%@", + [[path lastPathComponent] stringByDeletingPathExtension], + [path pathExtension]]]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:path2x]) { + return [self initWithContentsOfFile:path2x]; + } + } + + return [self initWithContentsOfFile:path]; +} + ++ (UIImage*)bw_imageWithContentsOfResolutionIndependentFile:(NSString *)path { +#ifndef __clang_analyzer__ + // clang alayzer in 4.2b3 thinks here's a leak, which is not the case. + return [[[UIImage alloc] bw_initWithContentsOfResolutionIndependentFile:path] autorelease]; +#endif +} + + ++ (UIImage *)bw_imageNamed:(NSString *)imageName bundle:(NSString *)bundleName { + NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; + NSString *bundlePath = [resourcePath stringByAppendingPathComponent:bundleName]; + NSString *imagePath = [bundlePath stringByAppendingPathComponent:imageName]; + return [UIImage bw_imageWithContentsOfResolutionIndependentFile:imagePath]; +} + +@end diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100755 index 0000000000..88a6782d6a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,81 @@ +## Authors + +Andreas Linde +Stanley Rost +Fabian Kreiser +Tobias Höhmann +FutureTap +Kent Sutherland +Peter Steinberger +Thomas Dohmke + +## Licenses + +The Hockey SDK is provided under the following license: + + The MIT License + Copyright (c) 2011 Codenauts UG (haftungsbeschränkt). 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. + +Except as noted below, PLCrashReporter +is provided under the following license: + + Copyright (c) 2008 - 2009 Plausible Labs Cooperative, Inc. + 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. + +The protobuf-c library, as well as the PLCrashLogWriterEncoding.c +file are licensed as follows: + + Copyright 2008, Dave Benson. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with + the License. You may obtain a copy of the License + at http://www.apache.org/licenses/LICENSE-2.0 Unless + required by applicable law or agreed to in writing, + software distributed under the License is distributed on + an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. diff --git a/Resources/.DS_Store b/Resources/.DS_Store new file mode 100644 index 0000000000..c7424949bd Binary files /dev/null and b/Resources/.DS_Store differ diff --git a/Resources/Hockey.bundle/IconGradient.png b/Resources/Hockey.bundle/IconGradient.png new file mode 100644 index 0000000000..5473e5e8de Binary files /dev/null and b/Resources/Hockey.bundle/IconGradient.png differ diff --git a/Resources/Hockey.bundle/IconGradient@2x.png b/Resources/Hockey.bundle/IconGradient@2x.png new file mode 100644 index 0000000000..d79b85f577 Binary files /dev/null and b/Resources/Hockey.bundle/IconGradient@2x.png differ diff --git a/Resources/Hockey.bundle/authorize_denied.png b/Resources/Hockey.bundle/authorize_denied.png new file mode 100644 index 0000000000..6a97107d4f Binary files /dev/null and b/Resources/Hockey.bundle/authorize_denied.png differ diff --git a/Resources/Hockey.bundle/authorize_denied@2x.png b/Resources/Hockey.bundle/authorize_denied@2x.png new file mode 100644 index 0000000000..be5a42c34b Binary files /dev/null and b/Resources/Hockey.bundle/authorize_denied@2x.png differ diff --git a/Resources/Hockey.bundle/authorize_request.png b/Resources/Hockey.bundle/authorize_request.png new file mode 100644 index 0000000000..7b863cfdd9 Binary files /dev/null and b/Resources/Hockey.bundle/authorize_request.png differ diff --git a/Resources/Hockey.bundle/authorize_request@2x.png b/Resources/Hockey.bundle/authorize_request@2x.png new file mode 100644 index 0000000000..36ab230766 Binary files /dev/null and b/Resources/Hockey.bundle/authorize_request@2x.png differ diff --git a/Resources/Hockey.bundle/bg.png b/Resources/Hockey.bundle/bg.png new file mode 100644 index 0000000000..abd2975a2a Binary files /dev/null and b/Resources/Hockey.bundle/bg.png differ diff --git a/Resources/Hockey.bundle/buttonHighlight.png b/Resources/Hockey.bundle/buttonHighlight.png new file mode 100644 index 0000000000..e2de297538 Binary files /dev/null and b/Resources/Hockey.bundle/buttonHighlight.png differ diff --git a/Resources/Hockey.bundle/buttonHighlight@2x.png b/Resources/Hockey.bundle/buttonHighlight@2x.png new file mode 100644 index 0000000000..1ddefa33c6 Binary files /dev/null and b/Resources/Hockey.bundle/buttonHighlight@2x.png differ diff --git a/Resources/Hockey.bundle/de.lproj/Hockey.strings b/Resources/Hockey.bundle/de.lproj/Hockey.strings new file mode 100644 index 0000000000..4c156d6cf9 --- /dev/null +++ b/Resources/Hockey.bundle/de.lproj/Hockey.strings @@ -0,0 +1,86 @@ +/* + Hockey.strings + Hockey + + Created by Andreas Linde on 11/15/10. + Copyright 2010 buzzworks.de. All rights reserved. + */ + + +/* Alert view */ + +/* For dialogs yes buttons */ +"HockeyYes" = "Ja"; + +/* For dialogs no buttons */ +"HockeyNo" = "Nein"; + +/* Update available */ +"HockeyUpdateAvailable" = "Aktualisierung verfügbar"; + +/* Would you like to check out the new update? You can do this later on at any time in the In-App settings. */ +"HockeyUpdateAlertText" = "Möchten Sie sich weitere Informationen zu der Aktualisierung ansehen?"; + + +/* Update details screen */ + +/* Update Details */ +"HockeyUpdateScreenTitle" = "Aktualisierung"; + + +/* Settings */ + +/* Screen title for settings view */ +"HockeySettingsTitle" = "Einstellungen"; + +/* Text asking the user to send user data (on/off switch) */ +"HockeySettingsUserData" = "Daten senden"; + +/* Description text for turning on/off sending user data */ +"HockeySettingsUserDataDescription" = "Daten senden liefert dem Entwickler die folgenden Daten: Programmversion, Sprache, Gerätetyp und iOS Version."; + +/* Text asking the user to send usage data (on/off switch) */ +"HockeySettingsUsageData" = "Testzeit senden"; + +/* Description text for turning on/off sending usage data */ +"HockeySettingsUsageDataDescription" = "Testzeit senden informiert den Entwickler über die Summe der Testzeit, in einer Genauigkeit von 1 Minute."; + +/* Title for defining when update checks may be made */ +"HockeySectionCheckTitle" = "Nach Aktualisierung Prüfen"; + +/* On Startup */ +"HockeySectionCheckStartup" = "Beim Start"; + +/* Daily */ +"HockeySectionCheckDaily" = "Täglich"; + +/* Manually */ +"HockeySectionCheckManually" = "Manuell"; + + +"HockeyVersion" = "Version"; +"HockeyShowPreviousVersions" = "Zeige frühere Versionen..."; +"HockeyNoUpdateNeededTitle" = "Kein Update Verfügbar"; +"HockeyNoUpdateNeededMessage" = "%@ ist bereits die aktuellste Version."; +"HockeyUpdateAlertTextWithAppVersion" = "%@ ist verfügbar."; +"HockeyIgnore" = "Ignorieren"; +"HockeyShowUpdate" = "Anzeigen"; +"HockeyInstallUpdate" = "Installieren"; +"HockeyError" = "Fehler"; +"HockeyOK" = "OK"; +"HockeyWarning" = "Warnung"; +"HockeyNoReleaseNotesAvailable" = "Keine Release Notes verfügbar."; + +"HockeyAuthorizationProgress" = "Autorisierung..."; +"HockeyAuthorizationOffline" = "Internet Verbindung wird benötigt!"; +"HockeyAuthorizationDenied" = "Autorisierung verweigert. Bitte kontaktieren Sie den Entwickler."; + +"HockeyiOS3Message" = "In-App download benötigt iOS 4 oder höher. Sie können die Applikation manuell updaten, indem sie das IPA von %@ laden und es via iTunes syncen."; +"HockeySimulatorMessage" = "Hockey funktioniert nicht im Simulator."; + +"HockeyButtonCheck" = "PRÜFE"; +"HockeyButtonSearching" = "PRÜFEN"; +"HockeyButtonUpdate" = "UPDATE"; +"HockeyButtonInstalling" = "INSTALLIEREN"; +"HockeyButtonOffline" = "OFFLINE"; +"HockeyInstalled" = "INSTALLIERT"; diff --git a/Resources/Hockey.bundle/en.lproj/Hockey.strings b/Resources/Hockey.bundle/en.lproj/Hockey.strings new file mode 100644 index 0000000000..fd2790d3cb --- /dev/null +++ b/Resources/Hockey.bundle/en.lproj/Hockey.strings @@ -0,0 +1,86 @@ +/* + Hockey.strings + Hockey + + Created by Andreas Linde on 11/15/10. + Copyright 2010 buzzworks.de. All rights reserved. + */ + + +/* Alert view */ + +/* For dialogs yes buttons */ +"HockeyYes" = "Yes"; + +/* For dialogs no buttons */ +"HockeyNo" = "No"; + +/* Update available */ +"HockeyUpdateAvailable" = "Update available"; + +/* Would you like to check out the new update? You can do this later on at any time in the In-App settings. */ +"HockeyUpdateAlertText" = "Would you like to check out the new update?"; + + +/* Update details screen */ + +/* Update Details */ +"HockeyUpdateScreenTitle" = "Update"; + + +/* Settings */ + +/* Screen title for settings view */ +"HockeySettingsTitle" = "Settings"; + +/* Text asking the user to send user data (on/off switch) */ +"HockeySettingsUserData" = "Send User Data"; + +/* Description text for turning on/off sending user data */ +"HockeySettingsUserDataDescription" = "Send User Data will send the following data to the developer: app version, language, device type and iOS version."; + +/* Text asking the user to send usage data (on/off switch) */ +"HockeySettingsUsageData" = "Send Usage Data"; + +/* Description text for turning on/off sending usage data */ +"HockeySettingsUsageDataDescription" = "Send Usage Data will send the amount of test time to the developer, in a granularity of 1 minute."; + +/* Title for defining when update checks may be made */ +"HockeySectionCheckTitle" = "Check For Update"; + +/* On Startup */ +"HockeySectionCheckStartup" = "On Startup"; + +/* Daily */ +"HockeySectionCheckDaily" = "Daily"; + +/* Manually */ +"HockeySectionCheckManually" = "Manually"; + + +"HockeyVersion" = "Version"; +"HockeyShowPreviousVersions" = "Show previous versions..."; +"HockeyNoUpdateNeededTitle" = "No Update Available"; +"HockeyNoUpdateNeededMessage" = "%@ is already the latest version."; +"HockeyUpdateAlertTextWithAppVersion" = "%@ is available."; +"HockeyIgnore" = "Ignore"; +"HockeyShowUpdate" = "Show"; +"HockeyInstallUpdate" = "Install"; +"HockeyError" = "Error"; +"HockeyOK" = "OK"; +"HockeyWarning" = "Warning"; +"HockeyNoReleaseNotesAvailable" = "No release notes available."; + +"HockeyAuthorizationProgress" = "Authorizing..."; +"HockeyAuthorizationOffline" = "Internet connection required!"; +"HockeyAuthorizationDenied" = "Authorizing denied. Please contact the developer."; + +"HockeyiOS3Message" = "In-App download requires iOS 4 or higher. You can update this application via downloading the IPA from %@ and syncing it with iTunes."; +"HockeySimulatorMessage" = "Hockey Update does not work in the Simulator.\nThe itms-services:// url scheme is implemented but nonfunctional."; + +"HockeyButtonCheck" = "CHECK"; +"HockeyButtonSearching" = "CHECKING"; +"HockeyButtonUpdate" = "UPDATE"; +"HockeyButtonInstalling" = "INSTALLING"; +"HockeyButtonOffline" = "OFFLINE"; +"HockeyInstalled" = "INSTALLED"; diff --git a/Resources/Hockey.bundle/gear.png b/Resources/Hockey.bundle/gear.png new file mode 100644 index 0000000000..55629bbf80 Binary files /dev/null and b/Resources/Hockey.bundle/gear.png differ diff --git a/Resources/Hockey.bundle/gear@2x.png b/Resources/Hockey.bundle/gear@2x.png new file mode 100644 index 0000000000..af9de9af51 Binary files /dev/null and b/Resources/Hockey.bundle/gear@2x.png differ diff --git a/Resources/Hockey.bundle/it.lproj/Hockey.strings b/Resources/Hockey.bundle/it.lproj/Hockey.strings new file mode 100755 index 0000000000..dc2bc73c22 --- /dev/null +++ b/Resources/Hockey.bundle/it.lproj/Hockey.strings @@ -0,0 +1,86 @@ +/* + Hockey.strings + Hockey + + Created by Andreas Linde on 11/15/10. + Copyright 2010 buzzworks.de. All rights reserved. + */ + + +/* Alert view */ + +/* For dialogs yes buttons */ +"HockeyYes" = "Si"; + +/* For dialogs no buttons */ +"HockeyNo" = "No"; + +/* Update available */ +"HockeyUpdateAvailable" = "Aggiornamento disponibile"; + +/* Would you like to check out the new update? You can do this later on at any time in the In-App settings. */ +"HockeyUpdateAlertText" = "Vuoi scaricare il nuovo aggiornamento ?"; + + +/* Update details screen */ + +/* Update Details */ +"HockeyUpdateScreenTitle" = "Aggiorna"; + + +/* Settings */ + +/* Screen title for settings view */ +"HockeySettingsTitle" = "Impostazioni"; + +/* Text asking the user to send user data (on/off switch) */ +"HockeySettingsUserData" = "Invia dati di sistema"; + +/* Description text for turning on/off sending user data */ +"HockeySettingsUserDataDescription" = "Invia le seguenti informazioni allo sviluppatore: versione app, lingua, tipo dispositivo e versione iOS."; + +/* Text asking the user to send usage data (on/off switch) */ +"HockeySettingsUsageData" = "Invia dati di utilizzo"; + +/* Description text for turning on/off sending usage data */ +"HockeySettingsUsageDataDescription" = "Invia il tempo di utilizzo allo svilupatore, con un dettaglio di 1 minuto."; + +/* Title for defining when update checks may be made */ +"HockeySectionCheckTitle" = "Controlla gli aggiornamenti"; + +/* On Startup */ +"HockeySectionCheckStartup" = "All'avvio"; + +/* Daily */ +"HockeySectionCheckDaily" = "Ogni giorno"; + +/* Manually */ +"HockeySectionCheckManually" = "Manualmente"; + + +"HockeyVersion" = "Versione"; +"HockeyShowPreviousVersions" = "Mostra le versioni precedenti..."; +"HockeyNoUpdateNeededTitle" = "Nessun aggiornamento disponibile"; +"HockeyNoUpdateNeededMessage" = "%@ aggiornata all'ultima versione."; +"HockeyUpdateAlertTextWithAppVersion" = "%@ disponibile."; +"HockeyIgnore" = "Ignora"; +"HockeyShowUpdate" = "Mostra"; +"HockeyInstallUpdate" = "Installa"; +"HockeyError" = "Errore"; +"HockeyOK" = "OK"; +"HockeyWarning" = "Avviso"; +"HockeyNoReleaseNotesAvailable" = "Non sono disponibili note di rilascio."; + +"HockeyAuthorizationProgress" = "Attendo autorizzazione..."; +"HockeyAuthorizationOffline" = "E' richiesta la connessione internet!"; +"HockeyAuthorizationDenied" = "Autorizzazione negata, contattare lo sviluppatore."; + +"HockeyiOS3Message" = "L'aggioranento dall'applicazione possibile con iOS 4 o superiore. Puoi aggiornare questa applicazione scaricandola da %@."; +"HockeySimulatorMessage" = "Hockey Update does not work in the Simulator.\nThe itms-services:// url scheme is implemented but nonfunctional."; + +"HockeyButtonCheck" = "CONTROLLA"; +"HockeyButtonSearching" = "CONTROLLO"; +"HockeyButtonUpdate" = "AGGIORNA"; +"HockeyButtonInstalling" = "INSTALLO"; +"HockeyButtonOffline" = "OFFLINE"; +"HockeyInstalled" = "INSTALLATA"; diff --git a/Resources/Hockey.bundle/sv.lproj/Hockey.strings b/Resources/Hockey.bundle/sv.lproj/Hockey.strings new file mode 100644 index 0000000000..37815e564f --- /dev/null +++ b/Resources/Hockey.bundle/sv.lproj/Hockey.strings @@ -0,0 +1,87 @@ +/* + Hockey.strings + Hockey + + Created by Andreas Linde on 11/15/10. + Copyright 2010 buzzworks.de. All rights reserved. + Swedish translation by Joakim Ramer. + */ + + +/* Alert view */ + +/* For dialogs yes buttons */ +"HockeyYes" = "Ja"; + +/* For dialogs no buttons */ +"HockeyNo" = "Nej"; + +/* Update available */ +"HockeyUpdateAvailable" = "Uppdatering tillgänglig"; + +/* Would you like to check out the new update? You can do this later on at any time in the In-App settings. */ +"HockeyUpdateAlertText" = "Vill du hämta den nya uppdateringen?"; + + +/* Update details screen */ + +/* Update Details */ +"HockeyUpdateScreenTitle" = "Uppdatera"; + + +/* Settings */ + +/* Screen title for settings view */ +"HockeySettingsTitle" = "Inställningar"; + +/* Text asking the user to send user data (on/off switch) */ +"HockeySettingsUserData" = "Skicka användardata"; + +/* Description text for turning on/off sending user data */ +"HockeySettingsUserDataDescription" = "Skicka användardata skickar följande till utvecklaren: app version, språk, enhetstyp och iOS version."; + +/* Text asking the user to send usage data (on/off switch) */ +"HockeySettingsUsageData" = "Skicka användn.data"; + +/* Description text for turning on/off sending usage data */ +"HockeySettingsUsageDataDescription" = "Skicka användningsdata skickar information om hur länge appen testats (i antal minuter), till utvecklaren."; + +/* Title for defining when update checks may be made */ +"HockeySectionCheckTitle" = "Leta efter uppdateringar"; + +/* On Startup */ +"HockeySectionCheckStartup" = "Vid uppstart"; + +/* Daily */ +"HockeySectionCheckDaily" = "Dagligen"; + +/* Manually */ +"HockeySectionCheckManually" = "Manuellt"; + + +"HockeyVersion" = "Version"; +"HockeyShowPreviousVersions" = "Se tidigare versioner..."; +"HockeyNoUpdateNeededTitle" = "Ingen uppdatering tillgänglig"; +"HockeyNoUpdateNeededMessage" = "%@ är den senaste versionen."; +"HockeyUpdateAlertTextWithAppVersion" = "%@ finns."; +"HockeyIgnore" = "Ignorera"; +"HockeyShowUpdate" = "Visa"; +"HockeyInstallUpdate" = "Installera"; +"HockeyError" = "Error"; +"HockeyOK" = "OK"; +"HockeyWarning" = "Varning"; +"HockeyNoReleaseNotesAvailable" = "Inga releasenoteringar tillgängliga."; + +"HockeyAuthorizationProgress" = "Auktoriserar..."; +"HockeyAuthorizationOffline" = "Internetuppkoppling krävs!"; +"HockeyAuthorizationDenied" = "Auktoriseringen nekades. Kontakta utvecklaren."; + +"HockeyiOS3Message" = "In-App nedladdning kräver iOS 4 eller högre. Du kan uppdatera applikationen genoma att ladda ner IPA-filen från %@ och synka in den i iTunes."; +"HockeySimulatorMessage" = "Hockey Update fungerar inte i Simulatorn.\nitms-services:// url schemat är implementerat men fungerar ej."; + +"HockeyButtonCheck" = "FRÅGA"; +"HockeyButtonSearching" = "FRÅGAR"; +"HockeyButtonUpdate" = "UPPDATERA"; +"HockeyButtonInstalling" = "INSTALLERAR"; +"HockeyButtonOffline" = "OFFLINE"; +"HockeyInstalled" = "INSTALLERAD"; \ No newline at end of file diff --git a/Resources/Quincy.bundle/de.lproj/Quincy.strings b/Resources/Quincy.bundle/de.lproj/Quincy.strings new file mode 100755 index 0000000000..4159496752 Binary files /dev/null and b/Resources/Quincy.bundle/de.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/de.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/de.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..eafcfb06b6 Binary files /dev/null and b/Resources/Quincy.bundle/de.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/en.lproj/Quincy.strings b/Resources/Quincy.bundle/en.lproj/Quincy.strings new file mode 100755 index 0000000000..a6dc5214f0 Binary files /dev/null and b/Resources/Quincy.bundle/en.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/en.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/en.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..e7ec07a725 Binary files /dev/null and b/Resources/Quincy.bundle/en.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/es.lproj/Quincy.strings b/Resources/Quincy.bundle/es.lproj/Quincy.strings new file mode 100755 index 0000000000..eacdb6eaba Binary files /dev/null and b/Resources/Quincy.bundle/es.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/es.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/es.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..ce9a8297d3 Binary files /dev/null and b/Resources/Quincy.bundle/es.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/fr.lproj/Quincy.strings b/Resources/Quincy.bundle/fr.lproj/Quincy.strings new file mode 100755 index 0000000000..7000849e36 Binary files /dev/null and b/Resources/Quincy.bundle/fr.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/fr.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/fr.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..7dd9d219a2 Binary files /dev/null and b/Resources/Quincy.bundle/fr.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/it.lproj/Quincy.strings b/Resources/Quincy.bundle/it.lproj/Quincy.strings new file mode 100755 index 0000000000..40c34494da Binary files /dev/null and b/Resources/Quincy.bundle/it.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/it.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/it.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..b9b2910177 Binary files /dev/null and b/Resources/Quincy.bundle/it.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/ja.lproj/Quincy.strings b/Resources/Quincy.bundle/ja.lproj/Quincy.strings new file mode 100755 index 0000000000..163427f0a6 Binary files /dev/null and b/Resources/Quincy.bundle/ja.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/ja.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/ja.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..75125cba35 Binary files /dev/null and b/Resources/Quincy.bundle/ja.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/nl.lproj/Quincy.strings b/Resources/Quincy.bundle/nl.lproj/Quincy.strings new file mode 100755 index 0000000000..d84491fbbd Binary files /dev/null and b/Resources/Quincy.bundle/nl.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/nl.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/nl.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..3def446d3e Binary files /dev/null and b/Resources/Quincy.bundle/nl.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/pt-PT.lproj/Quincy.strings b/Resources/Quincy.bundle/pt-PT.lproj/Quincy.strings new file mode 100755 index 0000000000..d7affaf645 Binary files /dev/null and b/Resources/Quincy.bundle/pt-PT.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/pt-PT.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/pt-PT.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..52978f95ff Binary files /dev/null and b/Resources/Quincy.bundle/pt-PT.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/pt.lproj/Quincy.strings b/Resources/Quincy.bundle/pt.lproj/Quincy.strings new file mode 100755 index 0000000000..5d082845fc Binary files /dev/null and b/Resources/Quincy.bundle/pt.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/pt.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/pt.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..90b375ec23 Binary files /dev/null and b/Resources/Quincy.bundle/pt.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/ru.lproj/Quincy.strings b/Resources/Quincy.bundle/ru.lproj/Quincy.strings new file mode 100755 index 0000000000..65ebcb320c Binary files /dev/null and b/Resources/Quincy.bundle/ru.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/ru.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/ru.lproj/QuincyAlternate.strings new file mode 100755 index 0000000000..59dfd4ee7f Binary files /dev/null and b/Resources/Quincy.bundle/ru.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/tr.lproj/Quincy.strings b/Resources/Quincy.bundle/tr.lproj/Quincy.strings new file mode 100644 index 0000000000..9f1ef4d8ec Binary files /dev/null and b/Resources/Quincy.bundle/tr.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/tr.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/tr.lproj/QuincyAlternate.strings new file mode 100644 index 0000000000..d5fc249f15 Binary files /dev/null and b/Resources/Quincy.bundle/tr.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/zh_CN.lproj/Quincy.strings b/Resources/Quincy.bundle/zh_CN.lproj/Quincy.strings new file mode 100644 index 0000000000..56568d7de9 Binary files /dev/null and b/Resources/Quincy.bundle/zh_CN.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/zh_CN.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/zh_CN.lproj/QuincyAlternate.strings new file mode 100644 index 0000000000..5f95868c91 Binary files /dev/null and b/Resources/Quincy.bundle/zh_CN.lproj/QuincyAlternate.strings differ diff --git a/Resources/Quincy.bundle/zh_TW.lproj/Quincy.strings b/Resources/Quincy.bundle/zh_TW.lproj/Quincy.strings new file mode 100644 index 0000000000..904f5b7d3e Binary files /dev/null and b/Resources/Quincy.bundle/zh_TW.lproj/Quincy.strings differ diff --git a/Resources/Quincy.bundle/zh_TW.lproj/QuincyAlternate.strings b/Resources/Quincy.bundle/zh_TW.lproj/QuincyAlternate.strings new file mode 100644 index 0000000000..da8908afdd Binary files /dev/null and b/Resources/Quincy.bundle/zh_TW.lproj/QuincyAlternate.strings differ diff --git a/Vendor/CrashReporter.framework/CrashReporter b/Vendor/CrashReporter.framework/CrashReporter new file mode 120000 index 0000000000..92b400e68b --- /dev/null +++ b/Vendor/CrashReporter.framework/CrashReporter @@ -0,0 +1 @@ +Versions/Current/CrashReporter \ No newline at end of file diff --git a/Vendor/CrashReporter.framework/Headers b/Vendor/CrashReporter.framework/Headers new file mode 120000 index 0000000000..a177d2a6b9 --- /dev/null +++ b/Vendor/CrashReporter.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers \ No newline at end of file diff --git a/Vendor/CrashReporter.framework/Resources b/Vendor/CrashReporter.framework/Resources new file mode 120000 index 0000000000..953ee36f3b --- /dev/null +++ b/Vendor/CrashReporter.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/Vendor/CrashReporter.framework/Versions/A/CrashReporter b/Vendor/CrashReporter.framework/Versions/A/CrashReporter new file mode 100644 index 0000000000..addf3e307f Binary files /dev/null and b/Vendor/CrashReporter.framework/Versions/A/CrashReporter differ diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/CrashReporter.h b/Vendor/CrashReporter.framework/Versions/A/Headers/CrashReporter.h new file mode 100644 index 0000000000..7239d6d9ef --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/CrashReporter.h @@ -0,0 +1,229 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2009 Plausible Labs Cooperative, Inc. + * 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 + +#ifdef __APPLE__ +#import +#endif + +#import "PLCrashReporter.h" +#import "PLCrashReport.h" +#import "PLCrashReportTextFormatter.h" + +/** + * @defgroup functions Crash Reporter Functions Reference + */ + +/** + * @defgroup types Crash Reporter Data Types Reference + */ + +/** + * @defgroup constants Crash Reporter Constants Reference + */ + +/** + * @internal + * @defgroup plcrash_internal Crash Reporter Internal Documentation + */ + +/** + * @defgroup enums Enumerations + * @ingroup constants + */ + +/** + * @defgroup globals Global Variables + * @ingroup constants + */ + +/** + * @defgroup exceptions Exceptions + * @ingroup constants + */ + +/* Exceptions */ +extern NSString *PLCrashReporterException; + +/* Error Domain and Codes */ +extern NSString *PLCrashReporterErrorDomain; + +/** + * NSError codes in the Plausible Crash Reporter error domain. + * @ingroup enums + */ +typedef enum { + /** An unknown error has occured. If this + * code is received, it is a bug, and should be reported. */ + PLCrashReporterErrorUnknown = 0, + + /** An Mach or POSIX operating system error has occured. The underlying NSError cause may be fetched from the userInfo + * dictionary using the NSUnderlyingErrorKey key. */ + PLCrashReporterErrorOperatingSystem = 1, + + /** The crash report log file is corrupt or invalid */ + PLCrashReporterErrorCrashReportInvalid = 2, +} PLCrashReporterError; + + +/* Library Imports */ +#import "PLCrashReporter.h" +#import "PLCrashReport.h" +#import "PLCrashReportTextFormatter.h" + +/** + * @mainpage Plausible Crash Reporter + * + * @section intro_sec Introduction + * + * Plausile CrashReporter implements in-process crash reporting on the iPhone and Mac OS X. + * + * The following features are supported: + * + * - Implemented as an in-process signal handler. + * - Does not interfer with debugging in gdb.. + * - Handles both uncaught Objective-C exceptions and fatal signals (SIGSEGV, SIGBUS, etc). + * - Full thread state for all active threads (backtraces, register dumps) is provided. + * + * If your application crashes, a crash report will be written. When the application is next run, you may check for a + * pending crash report, and submit the report to your own HTTP server, send an e-mail, or even introspect the + * report locally. + * + * @section intro_encoding Crash Report Format + * + * Crash logs are encoded using google protobuf, and may be decoded + * using the provided PLCrashReport API. Additionally, the include plcrashutil handles conversion of binary crash reports to the + * symbolicate-compatible iPhone text format. + * + * @section doc_sections Documentation Sections + * - @subpage example_usage_iphone + * - @subpage error_handling + * - @subpage async_safety + */ + +/** + * @page example_usage_iphone Example iPhone Usage + * + * @code + * // + * // Called to handle a pending crash report. + * // + * - (void) handleCrashReport { + * PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter]; + * NSData *crashData; + * NSError *error; + * + * // Try loading the crash report + * crashData = [crashReporter loadPendingCrashReportDataAndReturnError: &error]; + * if (crashData == nil) { + * NSLog(@"Could not load crash report: %@", error); + * goto finish; + * } + * + * // We could send the report from here, but we'll just print out + * // some debugging info instead + * PLCrashReport *report = [[[PLCrashReport alloc] initWithData: crashData error: &error] autorelease]; + * if (report == nil) { + * NSLog(@"Could not parse crash report"); + * goto finish; + * } + * + * NSLog(@"Crashed on %@", report.systemInfo.timestamp); + * NSLog(@"Crashed with signal %@ (code %@, address=0x%" PRIx64 ")", report.signalInfo.name, + * report.signalInfo.code, report.signalInfo.address); + * + * // Purge the report + * finish: + * [crashReporter purgePendingCrashReport]; + * return; + * } + * + * // from UIApplicationDelegate protocol + * - (void) applicationDidFinishLaunching: (UIApplication *) application { + * PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter]; + * NSError *error; + * + * // Check if we previously crashed + * if ([crashReporter hasPendingCrashReport]) + * [self handleCrashReport]; + + * // Enable the Crash Reporter + * if (![crashReporter enableCrashReporterAndReturnError: &error]) + * NSLog(@"Warning: Could not enable crash reporter: %@", error); + * + * } + * @endcode + * + */ + +/** + * @page error_handling Error Handling Programming Guide + * + * Where a method may return an error, Plausible Crash Reporter provides access to the underlying + * cause via an optional NSError argument. + * + * All returned errors will be a member of one of the below defined domains, however, new domains and + * error codes may be added at any time. If you do not wish to report on the error cause, many methods + * support a simple form that requires no NSError argument. + * + * @section error_domains Error Domains, Codes, and User Info + * + * @subsection crashreporter_errors Crash Reporter Errors + * + * Any errors in Plausible Crash Reporter use the #PLCrashReporterErrorDomain error domain, and and one + * of the error codes defined in #PLCrashReporterError. + */ + +/** + * @page async_safety Async-Safe Programming Guide + * + * Plausible CrashReporter provides support for executing an application specified function in the context of the + * crash reporter's signal handler, after the crash report has been written to disk. This was a regularly requested + * feature, and provides the ability to implement application finalization in the event of a crash. However, writing + * code intended for execution inside of a signal handler is exceptionally difficult, and is not recommended. + * + * @section program_flow Program Flow and Signal Handlers + * + * When the signal handler is called the normal flow of the program is interrupted, and your program is an unknown + * state. Locks may be held, the heap may be corrupt (or in the process of being updated), and your signal + * handler may invoke a function that was being executed at the time of the signal. This may result in deadlocks, + * data corruption, and program termination. + * + * @section functions Async-Safe Functions + * + * A subset of functions are defined to be async-safe by the OS, and are safely callable from within a signal handler. If + * you do implement a custom post-crash handler, it must be async-safe. A table of POSIX-defined async-safe functions + * and additional information is available from the + * CERT programming guide - SIG30-C + * + * Most notably, the Objective-C runtime itself is not async-safe, and Objective-C may not be used within a signal + * handler. + * + * @sa PLCrashReporter::setCrashCallbacks: + */ \ No newline at end of file diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReport.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReport.h new file mode 100644 index 0000000000..f229fd03f7 --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReport.h @@ -0,0 +1,169 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2010 Plausible Labs Cooperative, Inc. + * 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 "PLCrashReportSystemInfo.h" +#import "PLCrashReportMachineInfo.h" +#import "PLCrashReportApplicationInfo.h" +#import "PLCrashReportProcessInfo.h" +#import "PLCrashReportSignalInfo.h" +#import "PLCrashReportThreadInfo.h" +#import "PLCrashReportBinaryImageInfo.h" +#import "PLCrashReportExceptionInfo.h" + +/** + * @ingroup constants + * Crash file magic identifier */ +#define PLCRASH_REPORT_FILE_MAGIC "plcrash" + +/** + * @ingroup constants + * Crash format version byte identifier. Will not change outside of the introduction of + * an entirely new crash log format. */ +#define PLCRASH_REPORT_FILE_VERSION 1 + +/** + * @ingroup types + * Crash log file header format. + * + * Crash log files start with 7 byte magic identifier (#PLCRASH_REPORT_FILE_MAGIC), + * followed by a single unsigned byte version number (#PLCRASH_REPORT_FILE_VERSION). + * The crash log message format itself is extensible, so this version number will only + * be incremented in the event of an incompatible encoding or format change. + */ +struct PLCrashReportFileHeader { + /** Crash log magic identifier, not NULL terminated */ + const char magic[7]; + + /** Crash log encoding/format version */ + const uint8_t version; + + /** File data */ + const uint8_t data[]; +} __attribute__((packed)); + + +/** + * @internal + * Private decoder instance variables (used to hide the underlying protobuf parser). + */ +typedef struct _PLCrashReportDecoder _PLCrashReportDecoder; + +@interface PLCrashReport : NSObject { +@private + /** Private implementation variables (used to hide the underlying protobuf parser) */ + _PLCrashReportDecoder *_decoder; + + /** System info */ + PLCrashReportSystemInfo *_systemInfo; + + /** Machine info */ + PLCrashReportMachineInfo *_machineInfo; + + /** Application info */ + PLCrashReportApplicationInfo *_applicationInfo; + + /** Process info */ + PLCrashReportProcessInfo *_processInfo; + + /** Signal info */ + PLCrashReportSignalInfo *_signalInfo; + + /** Thread info (PLCrashReportThreadInfo instances) */ + NSArray *_threads; + + /** Binary images (PLCrashReportBinaryImageInfo instances */ + NSArray *_images; + + /** Exception information (may be nil) */ + PLCrashReportExceptionInfo *_exceptionInfo; +} + +- (id) initWithData: (NSData *) encodedData error: (NSError **) outError; + +- (PLCrashReportBinaryImageInfo *) imageForAddress: (uint64_t) address; + +/** + * System information. + */ +@property(nonatomic, readonly) PLCrashReportSystemInfo *systemInfo; + +/** + * YES if machine information is available. + */ +@property(nonatomic, readonly) BOOL hasMachineInfo; + +/** + * Machine information. Only available in later (v1.1+) crash report format versions. If not available, + * will be nil. + */ +@property(nonatomic, readonly) PLCrashReportMachineInfo *machineInfo; + +/** + * Application information. + */ +@property(nonatomic, readonly) PLCrashReportApplicationInfo *applicationInfo; + +/** + * YES if process information is available. + */ +@property(nonatomic, readonly) BOOL hasProcessInfo; + +/** + * Process information. Only available in later (v1.1+) crash report format versions. If not available, + * will be nil. + */ +@property(nonatomic, readonly) PLCrashReportProcessInfo *processInfo; + +/** + * Signal information. This provides the signal and signal code of the fatal signal. + */ +@property(nonatomic, readonly) PLCrashReportSignalInfo *signalInfo; + +/** + * Thread information. Returns a list of PLCrashReportThreadInfo instances. + */ +@property(nonatomic, readonly) NSArray *threads; + +/** + * Binary image information. Returns a list of PLCrashReportBinaryImageInfo instances. + */ +@property(nonatomic, readonly) NSArray *images; + +/** + * YES if exception information is available. + */ +@property(nonatomic, readonly) BOOL hasExceptionInfo; + +/** + * Exception information. Only available if a crash was caused by an uncaught exception, + * otherwise nil. + */ +@property(nonatomic, readonly) PLCrashReportExceptionInfo *exceptionInfo; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportApplicationInfo.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportApplicationInfo.h new file mode 100644 index 0000000000..2c2ab97086 --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportApplicationInfo.h @@ -0,0 +1,53 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2009 Plausible Labs Cooperative, Inc. + * 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 + +@interface PLCrashReportApplicationInfo : NSObject { +@private + /** Application identifier */ + NSString *_applicationIdentifier; + + /** Application version */ + NSString *_applicationVersion; +} + +- (id) initWithApplicationIdentifier: (NSString *) applicationIdentifier + applicationVersion: (NSString *) applicationVersion; + +/** + * The application identifier. This is usually the application's CFBundleIdentifier value. + */ +@property(nonatomic, readonly) NSString *applicationIdentifier; + +/** + * The application version. This is usually the application's CFBundleVersion value. + */ +@property(nonatomic, readonly) NSString *applicationVersion; + +@end \ No newline at end of file diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportBinaryImageInfo.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportBinaryImageInfo.h new file mode 100644 index 0000000000..339fdbb08f --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportBinaryImageInfo.h @@ -0,0 +1,90 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2009 Plausible Labs Cooperative, Inc. + * 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 "PLCrashReportProcessorInfo.h" + +@interface PLCrashReportBinaryImageInfo : NSObject { +@private + /** Code type */ + PLCrashReportProcessorInfo *_processorInfo; + + /** Base image address */ + uint64_t _baseAddress; + + /** Image segment size */ + uint64_t _imageSize; + + /** Name of binary image */ + NSString *_imageName; + + /** If the UUID is available */ + BOOL _hasImageUUID; + + /** 128-bit object UUID. May be nil. */ + NSString *_imageUUID; +} + +- (id) initWithCodeType: (PLCrashReportProcessorInfo *) processorInfo + baseAddress: (uint64_t) baseAddress + size: (uint64_t) imageSize + name: (NSString *) imageName + uuid: (NSData *) uuid; + +/** + * Image code type, or nil if unavailable. + */ +@property(nonatomic, readonly) PLCrashReportProcessorInfo *codeType; + +/** + * Image base address. + */ +@property(nonatomic, readonly) uint64_t imageBaseAddress; + +/** + * Segment size. + */ +@property(nonatomic, readonly) uint64_t imageSize; + +/** + * Image name (absolute path) + */ +@property(nonatomic, readonly) NSString *imageName; + + +/** + * YES if this image has an associated UUID. + */ +@property(nonatomic, readonly) BOOL hasImageUUID; + +/** + * 128-bit object UUID (matches Mach-O DWARF dSYM files). May be nil if unavailable. + */ +@property(nonatomic, readonly) NSString *imageUUID; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportExceptionInfo.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportExceptionInfo.h new file mode 100644 index 0000000000..623b1d4e15 --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportExceptionInfo.h @@ -0,0 +1,65 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2009 Plausible Labs Cooperative, Inc. + * 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 "PLCrashReportThreadInfo.h" + + +@interface PLCrashReportExceptionInfo : NSObject { +@private + /** Name */ + NSString *_name; + + /** Reason */ + NSString *_reason; + + /** Ordered list of PLCrashReportStackFrame instances, or nil if unavailable. */ + NSArray *_stackFrames; +} + +- (id) initWithExceptionName: (NSString *) name reason: (NSString *) reason; + +- (id) initWithExceptionName: (NSString *) name + reason: (NSString *) reason + stackFrames: (NSArray *) stackFrames; + +/** + * The exception name. + */ +@property(nonatomic, readonly) NSString *exceptionName; + +/** + * The exception reason. + */ +@property(nonatomic, readonly) NSString *exceptionReason; + +/* The exception's original call stack, as an array of PLCrashReportStackFrameInfo instances, or nil if unavailable. + * This may be preserved across rethrow of an exception, and can be used to determine the original call stack. */ +@property(nonatomic, readonly) NSArray *stackFrames; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportFormatter.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportFormatter.h new file mode 100644 index 0000000000..a32a243f63 --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportFormatter.h @@ -0,0 +1,51 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2010 Plausible Labs Cooperative, Inc. + * 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 "PLCrashReport.h" + +/** + * A crash report formatter accepts a PLCrashReport instance, formats it according to implementation-specified rules, + * (such as implementing text output support), and returns the result. + */ +@protocol PLCrashReportFormatter + +/** + * Format the provided @a report. + * + * @param report Report to be formatted. + * @param outError A pointer to an NSError object variable. If an error occurs, this pointer will contain an error + * object indicating why the pending crash report could not be formatted. If no error occurs, this parameter will + * be left unmodified. You may specify nil for this parameter, and no error information will be provided. + * + * @return Returns the formatted report data on success, or nil on failure. + */ +- (NSData *) formatReport: (PLCrashReport *) report error: (NSError **) outError; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportMachineInfo.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportMachineInfo.h new file mode 100644 index 0000000000..58c4baa5f5 --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportMachineInfo.h @@ -0,0 +1,73 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2011 Plausible Labs Cooperative, Inc. + * 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 "PLCrashReportProcessorInfo.h" + +@interface PLCrashReportMachineInfo : NSObject { +@private + /** The hardware model name (eg, MacBookPro6,1). This may be unavailable, and this property will be nil. */ + NSString *_modelName; + + /** The processor type. */ + PLCrashReportProcessorInfo *_processorInfo; + + /* The number of actual physical processor cores. */ + NSUInteger _processorCount; + + /* The number of logical processors. */ + NSUInteger _logicalProcessorCount; +} + +- (id) initWithModelName: (NSString *) modelName + processorInfo: (PLCrashReportProcessorInfo *) processorInfo + processorCount: (NSUInteger) processorCount + logicalProcessorCount: (NSUInteger) logicalProcessorCount; + +/** The hardware model name (eg, MacBookPro6,1). This may be unavailable, and this property will be nil. */ +@property(nonatomic, readonly) NSString *modelName; + +/** The processor type. */ +@property(nonatomic, readonly) PLCrashReportProcessorInfo *processorInfo; + +/* + * The number of actual physical processor cores. Note that the number of active processors may be managed by the + * operating system's power management system, and this value may not reflect the number of active + * processors at the time of the crash. + */ +@property(nonatomic, readonly) NSUInteger processorCount; + +/* + * The number of logical processors. Note that the number of active processors may be managed by the + * operating system's power management system, and this value may not reflect the number of active + * processors at the time of the crash. + */ +@property(nonatomic, readonly) NSUInteger logicalProcessorCount; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportProcessInfo.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportProcessInfo.h new file mode 100644 index 0000000000..ffc81ed9cf --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportProcessInfo.h @@ -0,0 +1,92 @@ +/* + * Author: Damian Morris + * + * Copyright (c) 2010 MOSO Corporation, Pty Ltd. + * Copyright (c) 2010 Plausible Labs Cooperative, Inc. + * + * 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 + +@interface PLCrashReportProcessInfo : NSObject { +@private + /** Process name */ + NSString *_processName; + + /** Process ID */ + NSUInteger _processID; + + /** Process path */ + NSString* _processPath; + + /** Parent process name */ + NSString *_parentProcessName; + + /** Parent process ID */ + NSUInteger _parentProcessID; + + /** If false, the process is being run via process-level CPU emulation (such as Rosetta). */ + BOOL _native; +} + +- (id) initWithProcessName: (NSString *) processName + processID: (NSUInteger) processID + processPath: (NSString *) processPath + parentProcessName: (NSString *) parentProcessName + parentProcessID: (NSUInteger) parentProcessID + native: (BOOL) native; + +/** + * The process name. This value may not be included in the crash report, in which case this property + * will be nil. + */ +@property(nonatomic, readonly) NSString *processName; + +/** + * The process ID. + */ +@property(nonatomic, readonly) NSUInteger processID; + +/** + * The path to the process executable. This value may not be included in the crash report, in which case this property + * will be nil. + */ +@property(nonatomic, readonly) NSString *processPath; + +/** + * The parent process name. This value may not be included in the crash report, in which case this property + * will be nil. + */ +@property(nonatomic, readonly) NSString *parentProcessName; + +/** + * The parent process ID. + */ +@property(nonatomic, readonly) NSUInteger parentProcessID; + +/** The process' native execution status. If false, the process is being run via process-level CPU emulation (such as Rosetta). */ +@property(nonatomic, readonly) BOOL native; + +@end \ No newline at end of file diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportProcessorInfo.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportProcessorInfo.h new file mode 100644 index 0000000000..af027bea5b --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportProcessorInfo.h @@ -0,0 +1,74 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2011 Plausible Labs Cooperative, Inc. + * 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 + +/** + * @ingroup constants + * + * The type encodings supported for CPU types and subtypes. Currently only Apple + * Mach-O defined encodings are supported. + * + * @internal + * These enum values match the protobuf values. Keep them synchronized. + */ +typedef enum { + /** Unknown cpu type encoding. */ + PLCrashReportProcessorTypeEncodingUnknown = 0, + + /** Apple Mach-defined processor types. */ + PLCrashReportProcessorTypeEncodingMach = 1 +} PLCrashReportProcessorTypeEncoding; + +@interface PLCrashReportProcessorInfo : NSObject { +@private + /** Type encoding */ + PLCrashReportProcessorTypeEncoding _typeEncoding; + + /** CPU type */ + uint64_t _type; + + /** CPU subtype */ + uint64_t _subtype; +} + +- (id) initWithTypeEncoding: (PLCrashReportProcessorTypeEncoding) typeEncoding + type: (uint64_t) type + subtype: (uint64_t) subtype; + +/** The CPU type encoding. */ +@property(nonatomic, readonly) PLCrashReportProcessorTypeEncoding typeEncoding; + +/** The CPU type. */ +@property(nonatomic, readonly) uint64_t type; + +/** The CPU subtype. */ +@property(nonatomic, readonly) uint64_t subtype; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportSignalInfo.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportSignalInfo.h new file mode 100644 index 0000000000..2c5c5fe23d --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportSignalInfo.h @@ -0,0 +1,60 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2009 Plausible Labs Cooperative, Inc. + * 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 + +@interface PLCrashReportSignalInfo : NSObject { +@private + /** Signal name */ + NSString *_name; + + /** Signal code */ + NSString *_code; + + /** Fauling instruction or address */ + uint64_t _address; +} + +- (id) initWithSignalName: (NSString *) name code: (NSString *) code address: (uint64_t) address; + +/** + * The signal name. + */ +@property(nonatomic, readonly) NSString *name; + +/** + * The signal code. + */ +@property(nonatomic, readonly) NSString *code; + +/** + * The faulting instruction or address. + */ +@property(nonatomic, readonly) uint64_t address; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportSystemInfo.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportSystemInfo.h new file mode 100644 index 0000000000..70167dfcfe --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportSystemInfo.h @@ -0,0 +1,142 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2009 Plausible Labs Cooperative, Inc. + * 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 + +/** + * @ingroup constants + * + * Indicates the Operating System under which a Crash Log was generated. + * + * @internal + * These enum values match the protobuf values. Keep them synchronized. + */ +typedef enum { + /** Mac OS X. */ + PLCrashReportOperatingSystemMacOSX = 0, + + /** iPhone OS */ + PLCrashReportOperatingSystemiPhoneOS = 1, + + /** iPhone Simulator (Mac OS X with additional simulator-specific runtime libraries) */ + PLCrashReportOperatingSystemiPhoneSimulator = 2, + + /** Unknown operating system */ + PLCrashReportOperatingSystemUnknown = 3, +} PLCrashReportOperatingSystem; + +/** + * @ingroup constants + * + * Indicates the architecture under which a Crash Log was generated. + * + * @deprecated The architecture value has been deprecated in v1.1 and later crash reports. All new reports + * will make use of the new PLCrashReportProcessorInfo CPU type encodings. + * + * @internal + * These enum values match the protobuf values. Keep them synchronized. + */ +typedef enum { + /** x86-32. */ + PLCrashReportArchitectureX86_32 = 0, + + /** x86-64 */ + PLCrashReportArchitectureX86_64 = 1, + + /** ARMv6 */ + PLCrashReportArchitectureARMv6 = 2, + + /** + * ARMv6 + * @deprecated + * @sa PLCrashReportArchitectureARMv6 + */ + PLCrashReportArchitectureARM = PLCrashReportArchitectureARMv6, + + /** PPC */ + PLCrashReportArchitecturePPC = 3, + + /** PPC64 */ + PLCrashReportArchitecturePPC64 = 4, + + /** ARMv7 */ + PLCrashReportArchitectureARMv7 = 5, + + /** Unknown */ + PLCrashReportArchitectureUnknown = 6 +} PLCrashReportArchitecture; + + +extern PLCrashReportOperatingSystem PLCrashReportHostOperatingSystem; +extern PLCrashReportArchitecture PLCrashReportHostArchitecture; + +@interface PLCrashReportSystemInfo : NSObject { +@private + /** Operating system */ + PLCrashReportOperatingSystem _operatingSystem; + + /** Operating system version */ + NSString *_osVersion; + + /** OS build. May be nil. */ + NSString *_osBuild; + + /** Architecture */ + PLCrashReportArchitecture _architecture; + + /** Date crash report was generated. May be nil if the date is unknown. */ + NSDate *_timestamp; +} + +- (id) initWithOperatingSystem: (PLCrashReportOperatingSystem) operatingSystem + operatingSystemVersion: (NSString *) operatingSystemVersion + architecture: (PLCrashReportArchitecture) architecture + timestamp: (NSDate *) timestamp; + +- (id) initWithOperatingSystem: (PLCrashReportOperatingSystem) operatingSystem + operatingSystemVersion: (NSString *) operatingSystemVersion + operatingSystemBuild: (NSString *) operatingSystemBuild + architecture: (PLCrashReportArchitecture) architecture + timestamp: (NSDate *) timestamp; + +/** The operating system. */ +@property(nonatomic, readonly) PLCrashReportOperatingSystem operatingSystem; + +/** The operating system's release version. */ +@property(nonatomic, readonly) NSString *operatingSystemVersion; + +/** The operating system's build identifier (eg, 10J869). This may be unavailable, and this property will be nil. */ +@property(nonatomic, readonly) NSString *operatingSystemBuild; + +/** Architecture. */ +@property(nonatomic, readonly) PLCrashReportArchitecture architecture; + +/** Date and time that the crash report was generated. This may be unavailable, and this property will be nil. */ +@property(nonatomic, readonly) NSDate *timestamp; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportTextFormatter.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportTextFormatter.h new file mode 100644 index 0000000000..61e6689f74 --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportTextFormatter.h @@ -0,0 +1,62 @@ +/* + * Authors: + * Landon Fuller + * Damian Morris + * + * Copyright (c) 2008-2010 Plausible Labs Cooperative, Inc. + * Copyright (c) 2010 MOSO Corporation, Pty Ltd. + * 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 "PLCrashReportFormatter.h" + +/** + * Supported text output formats. + * + * @ingroup enums + */ +typedef enum { + /** An iOS-compatible crash log text format. Compatible with the crash logs generated by the device and available + * through iTunes Connect. */ + PLCrashReportTextFormatiOS = 0 +} PLCrashReportTextFormat; + + +@interface PLCrashReportTextFormatter : NSObject { +@private + /** Text output format. */ + PLCrashReportTextFormat _textFormat; + + /** Encoding to use for string output. */ + NSStringEncoding _stringEncoding; +} + ++ (NSString *) stringValueForCrashReport: (PLCrashReport *) report withTextFormat: (PLCrashReportTextFormat) textFormat; + +- (id) initWithTextFormat: (PLCrashReportTextFormat) textFormat stringEncoding: (NSStringEncoding) stringEncoding; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportThreadInfo.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportThreadInfo.h new file mode 100644 index 0000000000..7ea39b2593 --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReportThreadInfo.h @@ -0,0 +1,114 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2009 Plausible Labs Cooperative, Inc. + * 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 + +@interface PLCrashReportStackFrameInfo : NSObject { +@private + /** Frame instruction pointer. */ + uint64_t _instructionPointer; +} + +- (id) initWithInstructionPointer: (uint64_t) instructionPointer; + +/** + * Frame's instruction pointer. + */ +@property(nonatomic, readonly) uint64_t instructionPointer; + +@end + + +@interface PLCrashReportRegisterInfo : NSObject { +@private + /** Register name */ + NSString *_registerName; + + /** Register value */ + uint64_t _registerValue; +} + +- (id) initWithRegisterName: (NSString *) registerName registerValue: (uint64_t) registerValue; + +/** + * Register name. + */ +@property(nonatomic, readonly) NSString *registerName; + +/** + * Register value. + */ +@property(nonatomic, readonly) uint64_t registerValue; + +@end + + +@interface PLCrashReportThreadInfo : NSObject { +@private + /** The thread number. Should be unique within a given crash log. */ + NSInteger _threadNumber; + + /** Ordered list of PLCrashReportStackFrame instances */ + NSArray *_stackFrames; + + /** YES if this thread crashed. */ + BOOL _crashed; + + /** List of PLCrashReportRegister instances. Will be empty if _crashed is NO. */ + NSArray *_registers; +} + +- (id) initWithThreadNumber: (NSInteger) threadNumber + stackFrames: (NSArray *) stackFrames + crashed: (BOOL) crashed + registers: (NSArray *) registers; + +/** + * Application thread number. + */ +@property(nonatomic, readonly) NSInteger threadNumber; + +/** + * Thread backtrace. Provides an array of PLCrashReportStackFrameInfo instances. + * The array is ordered, last callee to first. + */ +@property(nonatomic, readonly) NSArray *stackFrames; + +/** + * If this thread crashed, set to YES. + */ +@property(nonatomic, readonly) BOOL crashed; + +/** + * State of the general purpose and related registers, as a list of + * PLCrashReportRegister instances. If this thead did not crash (crashed returns NO), + * this list will be empty. + */ +@property(nonatomic, readonly) NSArray *registers; + +@end diff --git a/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReporter.h b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReporter.h new file mode 100644 index 0000000000..8ea5e46e6d --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Headers/PLCrashReporter.h @@ -0,0 +1,97 @@ +/* + * Author: Landon Fuller + * + * Copyright (c) 2008-2009 Plausible Labs Cooperative, Inc. + * 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 + +/** + * @ingroup functions + * + * Prototype of a callback function used to execute additional user code with signal information as provided + * by PLCrashReporter. Called upon completion of crash handling, after the crash report has been written to disk. + * + * @param info The signal info. + * @param uap The crash's threads context. + * @param context The API client's supplied context value. + * + * @sa @ref async_safety + * @sa PLCrashReporter::setPostCrashCallbacks: + */ +typedef void (*PLCrashReporterPostCrashSignalCallback)(siginfo_t *info, ucontext_t *uap, void *context); + +/** + * @ingroup types + * + * This structure contains callbacks supported by PLCrashReporter to allow the host application to perform + * additional tasks prior to program termination after a crash has occured. + * + * @sa @ref async_safety + */ +typedef struct PLCrashReporterCallbacks { + /** The version number of this structure. If not one of the defined version numbers for this type, the behavior + * is undefined. The current version of this structure is 0. */ + uint16_t version; + + /** An arbitrary user-supplied context value. This value may be NULL. */ + void *context; + + /** The callback used to report caught signal information. In version 0 of this structure, all crashes will be + * reported via this function. */ + PLCrashReporterPostCrashSignalCallback handleSignal; +} PLCrashReporterCallbacks; + +@interface PLCrashReporter : NSObject { +@private + /** YES if the crash reporter has been enabled */ + BOOL _enabled; + + /** Application identifier */ + NSString *_applicationIdentifier; + + /** Application version */ + NSString *_applicationVersion; + + /** Path to the crash reporter internal data directory */ + NSString *_crashReportDirectory; +} + ++ (PLCrashReporter *) sharedReporter; + +- (BOOL) hasPendingCrashReport; + +- (NSData *) loadPendingCrashReportData; +- (NSData *) loadPendingCrashReportDataAndReturnError: (NSError **) outError; + +- (BOOL) purgePendingCrashReport; +- (BOOL) purgePendingCrashReportAndReturnError: (NSError **) outError; + +- (BOOL) enableCrashReporter; +- (BOOL) enableCrashReporterAndReturnError: (NSError **) outError; + +- (void) setCrashCallbacks: (PLCrashReporterCallbacks *) callbacks; + +@end \ No newline at end of file diff --git a/Vendor/CrashReporter.framework/Versions/A/Resources/Info.plist b/Vendor/CrashReporter.framework/Versions/A/Resources/Info.plist new file mode 100644 index 0000000000..04814d9d9f --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,38 @@ + + + + + BuildMachineOSBuild + 11C74 + CFBundleDevelopmentRegion + English + CFBundleExecutable + CrashReporter + CFBundleIdentifier + com.yourcompany.CrashReporter + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + CrashReporter + CFBundlePackageType + FMWK + CFBundleSignature + ???? + CFBundleVersion + 1.0 + DTCompiler + + DTPlatformBuild + 4D199 + DTPlatformVersion + GM + DTSDKBuild + 11C63 + DTSDKName + macosx10.7 + DTXcode + 0420 + DTXcodeBuild + 4D199 + + diff --git a/Vendor/CrashReporter.framework/Versions/Current b/Vendor/CrashReporter.framework/Versions/Current new file mode 120000 index 0000000000..8c7e5a667f --- /dev/null +++ b/Vendor/CrashReporter.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file