5 // Created by Caleb Davenport on 10/2/10.
6 // Copyright 2010 GUI Cocoa, LLC. All rights reserved.
10 #import "HTNotifier_iOS.h"
11 #import "HTNotifier_Mac.h"
13 #import "HTFunctions.h"
16 void ht_handle_exception(NSException *);
17 static HTNotifier *sharedNotifier = nil;
18 static NSString *HTNotifierHostName = @"hoptoadapp.com";
19 #define HTNotifierURL [NSURL URLWithString: \
20 [NSString stringWithFormat: \
21 @"%@://%@/notifier_api/v2/notices", \
22 (self.useSSL) ? @"https" : @"http", \
24 #define HTIsMultitaskingSupported \
25 [[UIDevice currentDevice] respondsToSelector:@selector(isMultitaskingSupported)] && \
26 [[UIDevice currentDevice] isMultitaskingSupported]
27 #define HT_IOS_SDK_4 (TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= 4000)
30 NSString *HTNotifierVersion = @"2.1";
31 NSString *HTNotifierBundleName = @"${BUNDLE}";
32 NSString *HTNotifierBundleVersion = @"${VERSION}";
33 NSString *HTNotifierDevelopmentEnvironment = @"Development";
34 NSString *HTNotifierAdHocEnvironment = @"Ad Hoc";
35 NSString *HTNotifierAppStoreEnvironment = @"App Store";
36 NSString *HTNotifierReleaseEnvironment = @"Release";
37 NSString *HTNotifierAlwaysSendKey = @"AlwaysSendCrashReports";
40 #pragma mark private methods
41 @interface HTNotifier (private)
43 // methods to be implemented
44 - (id)initWithAPIKey:(NSString *)key environmentName:(NSString *)name;
45 - (void)checkForNoticesAndReportIfReachable;
46 - (void)postAllNoticesWithAutoreleasePool;
47 - (void)postNoticesWithPaths:(NSArray *)paths;
48 - (void)postNoticeWithPath:(NSString *)path;
49 - (BOOL)isHoptoadReachable;
51 // methods to be overridden
52 - (void)showNoticeAlert;
53 - (void)registerNotifications;
54 - (void)unregisterNotifications;
57 @implementation HTNotifier (private)
59 - (id)initWithAPIKey:(NSString *)key environmentName:(NSString *)name {
64 NSString *directory = HTNoticesDirectory();
65 if (![[NSFileManager defaultManager] fileExistsAtPath:directory]) {
66 [[NSFileManager defaultManager]
67 createDirectoryAtPath:directory
68 withIntermediateDirectories:YES
75 environmentName = [HTStringByReplacingHoptoadVariablesInString(name) retain];
76 environmentInfo = [[NSMutableDictionary alloc] init];
81 [[NSUserDefaults standardUserDefaults] registerDefaults:
82 [NSDictionary dictionaryWithObject:@"NO" forKey:HTNotifierAlwaysSendKey]];
85 reachability = SCNetworkReachabilityCreateWithName(NULL, [HTNotifierHostName UTF8String]);
88 [self registerNotifications];
96 - (void)checkForNoticesAndReportIfReachable {
97 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
99 if ([self isHoptoadReachable]) {
100 [self performSelectorOnMainThread:@selector(unregisterNotifications) withObject:nil waitUntilDone:YES];
102 NSArray *notices = HTNotices();
103 if ([notices count] > 0) {
104 if ([[NSUserDefaults standardUserDefaults] boolForKey:HTNotifierAlwaysSendKey]) {
105 [self postNoticesWithPaths:notices];
108 [self performSelectorOnMainThread:@selector(showNoticeAlert) withObject:nil waitUntilDone:YES];
115 - (void)postAllNoticesWithAutoreleasePool {
116 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
117 NSArray *paths = HTNotices();
118 [self postNoticesWithPaths:paths];
121 - (void)postNoticesWithPaths:(NSArray *)paths {
124 if ([paths count] > 0) {
125 if ([self.delegate respondsToSelector:@selector(notifierWillPostNotices)]) {
126 [self.delegate notifierWillPostNotices];
132 if (HTIsMultitaskingSupported) {
134 // start background task
135 UIApplication *app = [UIApplication sharedApplication];
136 __block BOOL keepPosting = YES;
137 UIBackgroundTaskIdentifier task = [app beginBackgroundTaskWithExpirationHandler:^{
141 // report each notice
142 for (NSString *path in paths) {
143 if (!keepPosting) { break; }
144 [self postNoticeWithPath:path];
147 // end background task
148 if (task != UIBackgroundTaskInvalid) {
149 [app endBackgroundTask:task];
157 // report each notice
158 for (NSString *path in paths) {
159 [self postNoticeWithPath:path];
169 if ([paths count] > 0) {
170 if ([self.delegate respondsToSelector:@selector(notifierDidPostNotices)]) {
171 [self.delegate notifierDidPostNotices];
176 - (void)postNoticeWithPath:(NSString *)path {
178 // get notice payload
179 HTNotice *notice = [HTNotice noticeWithContentsOfFile:path];
181 HTLog(@"%@", notice);
183 NSData *data = [notice hoptoadXMLData];
185 // create url request
186 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:HTNotifierURL];
187 [request setTimeoutInterval:10.0];
188 [request setValue:@"text/xml" forHTTPHeaderField:@"Content-Type"];
189 [request setHTTPMethod:@"POST"];
190 [request setHTTPBody:data];
193 NSHTTPURLResponse *response = nil;
194 NSError *error = nil;
195 NSData *responseBody = [NSURLConnection
196 sendSynchronousRequest:request
197 returningResponse:&response
202 [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
205 HTLog(@"encountered error while posting notice\n%@", error);
208 // status code checking
209 NSInteger statusCode = [response statusCode];
210 if (statusCode == 200) {
211 HTLog(@"crash report posted");
213 else if (responseBody == nil) {
214 HTLog(@"unexpected response\nstatus code:%ld", (long)statusCode);
217 NSString *responseString = [[NSString alloc] initWithData:responseBody
218 encoding:NSUTF8StringEncoding];
219 HTLog(@"unexpected response\nstatus code:%ld\nresponse body:%@",
222 [responseString release];
226 - (BOOL)isHoptoadReachable {
227 SCNetworkReachabilityFlags flags;
228 SCNetworkReachabilityGetFlags(reachability, &flags);
229 return ((flags & kSCNetworkReachabilityFlagsReachable) != 0);
232 - (void)registerNotifications {}
233 - (void)unregisterNotifications {}
234 - (void)showNoticeAlert {}
239 #pragma mark public implementation
240 @implementation HTNotifier
243 @synthesize environmentName;
245 @synthesize environmentInfo;
246 @synthesize delegate;
248 + (void)startNotifierWithAPIKey:(NSString *)key environmentName:(NSString *)name {
249 if (sharedNotifier == nil) {
252 if (key == nil || [key length] == 0) {
253 HTLog(@"The provided API key is not valid");
256 if (name == nil || [name length] == 0) {
257 HTLog(@"The provided environment name is not valid");
263 sharedNotifier = [[HTNotifier_iOS alloc] initWithAPIKey:key environmentName:name];
265 sharedNotifier = [[HTNotifier_Mac alloc] initWithAPIKey:key environmentName:name];
267 #error [Hoptoad] unsupported platform
271 if (sharedNotifier != nil) {
272 HTLog(@"Notifier %@ ready to catch errors", HTNotifierVersion);
273 HTLog(@"Environment \"%@\"", sharedNotifier.environmentName);
277 + (HTNotifier *)sharedNotifier {
278 @synchronized(self) {
279 return sharedNotifier;
282 + (id)allocWithZone:(NSZone *)zone {
283 @synchronized(self) {
284 if (sharedNotifier == nil) {
285 sharedNotifier = [super allocWithZone:zone];
286 return sharedNotifier;
291 - (id)copyWithZone:(NSZone *)zone {
297 - (NSUInteger)retainCount {
298 return NSUIntegerMax;
300 - (oneway void)release {
307 [self unregisterNotifications];
308 if (reachability != NULL) { CFRelease(reachability);reachability = NULL; }
309 [apiKey release];apiKey = nil;
310 [environmentName release];environmentName = nil;
311 [environmentInfo release];environmentInfo = nil;
313 HTReleaseNoticeInfo();
316 - (void)writeTestNotice {
317 NSString *testPath = [HTNoticesDirectory() stringByAppendingPathComponent:@"TEST"];
318 testPath = [testPath stringByAppendingPathExtension:HTNoticePathExtension];
319 if ([[NSFileManager defaultManager] fileExistsAtPath:testPath]) { return; }
320 @try { [NSException raise:@"HTTestException" format:@"This is a test exception"]; }
321 @catch (NSException * e) { ht_handle_exception(e); }
322 NSString *noticePath = [NSString stringWithUTF8String:ht_notice_info.notice_path];
323 [[NSFileManager defaultManager] moveItemAtPath:noticePath toPath:testPath error:nil];