/* * 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 didCrashInLastSession = _didCrashInLastSession; @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; _didCrashInLastSession = NO; 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 (_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]) { _didCrashInLastSession = YES; [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]; } if (!quincyBundle()) { NSLog(@"WARNING: Quincy.bundle is missing in the app bundle!"); } } 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 - (BOOL)autoSendCrashReports { BOOL result = NO; if (!self.autoSubmitCrashReport) { if (self.isShowingAlwaysButton && [[NSUserDefaults standardUserDefaults] boolForKey: kAutomaticallySendCrashReports]) { result = YES; } } else { result = YES; } return result; } // begin the startup process - (void)startManager { if (!_sendingInProgress && [self hasPendingCrashReport]) { _sendingInProgress = YES; if (!quincyBundle()) { NSLog(@"Quincy.bundle is missing, sending report automatically!"); [self _sendCrashReports]; } else 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 && quincyBundle()) { // 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; 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 { if ([alertView tag] == 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; } } } #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]; } else { 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 *)_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 && [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"]) { 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 stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,crashLogString.length)], userid, contact, [description stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,description.length)]]; // 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]; _feedbackRequestID = nil; 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]; if ([response objectForKey:@"id"]) { _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]) { // only proceed if the server did not report any problem if ((self.appIdentifier) && (_serverResult == CrashReportStatusQueued)) { // the report is still in the queue if (_feedbackRequestID) { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_checkForFeedbackStatus) object:nil]; [self performSelector:@selector(_checkForFeedbackStatus) withObject:nil afterDelay:_feedbackDelayInterval]; } } 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