/* * Author: Andreas Linde * Kent Sutherland * * Copyright (c) 2012 HockeyApp, Bit Stadium GmbH. * 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 "HockeySDK.h" #import "HockeySDKPrivate.h" #import "BITCrashManagerPrivate.h" #import "BITCrashReportTextFormatter.h" #include // flags if the crashreporter should automatically send crashes without asking the user again #define kBITCrashAutomaticallySendReports @"BITCrashAutomaticallySendReports" // stores the set of crashreports that have been approved but aren't sent yet #define kBITCrashApprovedReports @"HockeySDKCrashApprovedReports" // keys for meta information associated to each crash #define kBITCrashMetaUserName @"BITCrashMetaUserName" #define kBITCrashMetaUserEmail @"BITCrashMetaUserEmail" #define kBITCrashMetaApplicationLog @"BITCrashMetaApplicationLog" @interface BITCrashManager () @property (nonatomic, retain) NSFileManager *fileManager; @end @implementation BITCrashManager @synthesize delegate = _delegate; @synthesize crashManagerStatus = _crashManagerStatus; @synthesize showAlwaysButton = _showAlwaysButton; @synthesize didCrashInLastSession = _didCrashInLastSession; @synthesize timeintervalCrashInLastSessionOccured = _timeintervalCrashInLastSessionOccured; @synthesize fileManager = _fileManager; - (id)initWithAppIdentifier:(NSString *)appIdentifier { if ((self = [super init])) { BITHockeyLog(@"Initializing CrashReporter"); _updateURL = BITHOCKEYSDK_URL; _appIdentifier = appIdentifier; _delegate = nil; _showAlwaysButton = NO; _crashIdenticalCurrentVersion = YES; _urlConnection = nil; _responseData = nil; _sendingInProgress = NO; _didCrashInLastSession = NO; _timeintervalCrashInLastSessionOccured = -1; _approvedCrashReports = [[NSMutableDictionary alloc] init]; _fileManager = [[NSFileManager alloc] init]; _crashFiles = [[NSMutableArray alloc] init]; _crashManagerStatus = BITCrashManagerStatusAlwaysAsk; NSString *testValue = [[NSUserDefaults standardUserDefaults] stringForKey:kBITCrashManagerStatus]; if (testValue) { _crashManagerStatus = [[NSUserDefaults standardUserDefaults] integerForKey:kBITCrashManagerStatus]; } else { [[NSUserDefaults standardUserDefaults] setInteger:_crashManagerStatus forKey:kBITCrashManagerStatus]; } // temporary directory for crashes grabbed from PLCrashReporter NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); _crashesDir = [[[paths objectAtIndex:0] stringByAppendingPathComponent:BITHOCKEY_IDENTIFIER] retain]; if (![self.fileManager fileExistsAtPath:_crashesDir]) { NSDictionary *attributes = [NSDictionary dictionaryWithObject: [NSNumber numberWithUnsignedLong: 0755] forKey: NSFilePosixPermissions]; NSError *theError = NULL; [self.fileManager createDirectoryAtPath:_crashesDir withIntermediateDirectories: YES attributes: attributes error: &theError]; } _settingsFile = [[_crashesDir stringByAppendingPathComponent:BITHOCKEY_CRASH_SETTINGS] retain]; _analyzerInProgressFile = [[_crashesDir stringByAppendingPathComponent:BITHOCKEY_CRASH_ANALYZER] retain]; if ([_fileManager fileExistsAtPath:_analyzerInProgressFile]) { NSError *error = nil; [_fileManager removeItemAtPath:_analyzerInProgressFile error:&error]; } // on the very first startup this will always be initialized, since the default value for _crashManagerStatus is BITCrashManagerStatusAlwaysAsk // but we do it anyway, to be able to initialize PLCrashReporter as early as possible if (_crashManagerStatus != BITCrashManagerStatusDisabled) { 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:BITHockeyNetworkDidBecomeReachableNotification object:nil]; } if (!BITHockeyBundle()) { NSLog(@"WARNING: %@ is missing, will send reports automatically!", BITHOCKEYSDK_BUNDLE); } } return self; } - (void) dealloc { _delegate = nil; [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyNetworkDidBecomeReachableNotification object:nil]; [_updateURL release]; _updateURL = nil; [_appIdentifier release]; _appIdentifier = nil; [_urlConnection cancel]; [_urlConnection release]; _urlConnection = nil; [_crashesDir release]; [_crashFiles release]; [_fileManager release]; _fileManager = nil; [_approvedCrashReports release]; _approvedCrashReports = nil; [_analyzerInProgressFile release]; _analyzerInProgressFile = nil; [super dealloc]; } - (void)setCrashManagerStatus:(BITCrashManagerStatus)crashManagerStatus { _crashManagerStatus = crashManagerStatus; [[NSUserDefaults standardUserDefaults] setInteger:crashManagerStatus forKey:kBITCrashManagerStatus]; } #pragma mark - Private - (void)saveSettings { NSString *errorString = nil; NSMutableDictionary *rootObj = [NSMutableDictionary dictionaryWithCapacity:2]; if (_approvedCrashReports && [_approvedCrashReports count] > 0) [rootObj setObject:_approvedCrashReports forKey:kBITCrashApprovedReports]; NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)rootObj format:NSPropertyListBinaryFormat_v1_0 errorDescription:&errorString]; if (plist) { [plist writeToFile:_settingsFile atomically:YES]; } else { BITHockeyLog(@"ERROR: Writing settings. %@", errorString); } } - (void)loadSettings { NSString *errorString = nil; NSPropertyListFormat format; if (![_fileManager fileExistsAtPath:_settingsFile]) return; NSData *plist = [NSData dataWithContentsOfFile:_settingsFile]; if (plist) { NSDictionary *rootObj = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plist mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&errorString]; if ([rootObj objectForKey:kBITCrashApprovedReports]) [_approvedCrashReports setDictionary:[rootObj objectForKey:kBITCrashApprovedReports]]; } else { BITHockeyLog(@"ERROR: Reading settings. %@", errorString); } } - (void)cleanCrashReports { NSError *error = NULL; for (NSUInteger i=0; i < [_crashFiles count]; i++) { [_fileManager removeItemAtPath:[_crashFiles objectAtIndex:i] error:&error]; [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".meta"] error:&error]; } [_crashFiles removeAllObjects]; [_approvedCrashReports removeAllObjects]; [self saveSettings]; } - (NSString *) extractAppUUIDs:(PLCrashReport *)report { NSMutableString *uuidString = [NSMutableString string]; NSArray *uuidArray = [BITCrashReportTextFormatter arrayOfAppUUIDsForCrashReport:report]; for (NSDictionary *element in uuidArray) { if ([element objectForKey:kBITBinaryImageKeyUUID] && [element objectForKey:kBITBinaryImageKeyArch] && [element objectForKey:kBITBinaryImageKeyUUID]) { [uuidString appendFormat:@"%@", [element objectForKey:kBITBinaryImageKeyType], [element objectForKey:kBITBinaryImageKeyArch], [element objectForKey:kBITBinaryImageKeyUUID] ]; } } return uuidString; } - (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; } #pragma mark - PLCrashReporter // Called to handle a pending crash report. - (void) handleCrashReport { PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter]; NSError *error = NULL; [self loadSettings]; // check if the next call ran successfully the last time if (![_fileManager fileExistsAtPath:_analyzerInProgressFile]) { // mark the start of the routine [_fileManager createFileAtPath:_analyzerInProgressFile contents:nil attributes:nil]; [self saveSettings]; // Try loading the crash report NSData *crashData = [[[NSData alloc] initWithData:[crashReporter loadPendingCrashReportDataAndReturnError: &error]] autorelease]; NSString *cacheFilename = [NSString stringWithFormat: @"%.0f", [NSDate timeIntervalSinceReferenceDate]]; if (crashData == nil) { BITHockeyLog(@"ERROR: Could not load crash report: %@", error); } else { [crashData writeToFile:[_crashesDir stringByAppendingPathComponent: cacheFilename] atomically:YES]; // write the meta file NSMutableDictionary *metaDict = [NSMutableDictionary dictionaryWithCapacity:4]; NSString *username = @""; NSString *useremail = @""; NSString *applicationLog = @""; NSString *errorString = nil; if (self.delegate != nil && [self.delegate respondsToSelector:@selector(userNameForCrashManager:)]) { username = [self.delegate userNameForCrashManager:self] ?: @""; } [metaDict setObject:username forKey:kBITCrashMetaUserName]; if (self.delegate != nil && [self.delegate respondsToSelector:@selector(userEmailForCrashManager:)]) { useremail = [self.delegate userEmailForCrashManager:self] ?: @""; } [metaDict setObject:useremail forKey:kBITCrashMetaUserEmail]; if (self.delegate != nil && [self.delegate respondsToSelector:@selector(applicationLogForCrashManager:)]) { applicationLog = [self.delegate applicationLogForCrashManager:self] ?: @""; } [metaDict setObject:applicationLog forKey:kBITCrashMetaApplicationLog]; NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)metaDict format:NSPropertyListBinaryFormat_v1_0 errorDescription:&errorString]; if (plist) { [plist writeToFile:[NSString stringWithFormat:@"%@.meta", [_crashesDir stringByAppendingPathComponent: cacheFilename]] atomically:YES]; } else { BITHockeyLog(@"ERROR: Writing crash meta data failed. %@", error); } // get the startup timestamp from the crash report, and the file timestamp to calculate the timeinterval when the crash happened after startup PLCrashReport *report = [[[PLCrashReport alloc] initWithData:crashData error:&error] autorelease]; if (report.systemInfo.timestamp && report.applicationInfo.applicationStartupTimestamp) { _timeintervalCrashInLastSessionOccured = [report.systemInfo.timestamp timeIntervalSinceDate:report.applicationInfo.applicationStartupTimestamp]; } } } // Purge the report // mark the end of the routine if ([_fileManager fileExistsAtPath:_analyzerInProgressFile]) { [_fileManager removeItemAtPath:_analyzerInProgressFile error:&error]; } [self saveSettings]; [crashReporter purgePendingCrashReport]; } - (BOOL)hasNonApprovedCrashReports { 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 (_crashManagerStatus == BITCrashManagerStatusDisabled) return NO; if ([self.fileManager fileExistsAtPath:_crashesDir]) { NSString *file = nil; NSError *error = NULL; NSDirectoryEnumerator *dirEnum = [self.fileManager enumeratorAtPath: _crashesDir]; while ((file = [dirEnum nextObject])) { NSDictionary *fileAttributes = [self.fileManager attributesOfItemAtPath:[_crashesDir stringByAppendingPathComponent:file] error:&error]; if ([[fileAttributes objectForKey:NSFileSize] intValue] > 0 && ![file hasSuffix:@".analyzer"] && ![file hasSuffix:@".plist"] && ![file hasSuffix:@".meta"]) { [_crashFiles addObject:[_crashesDir stringByAppendingPathComponent: file]]; } } } if ([_crashFiles count] > 0) { BITHockeyLog(@"%i pending crash reports found.", [_crashFiles count]); return YES; } else return NO; } #pragma mark - Crash Report Processing // begin the startup process - (void)startManager { if (_crashManagerStatus == BITCrashManagerStatusDisabled) return; if (!_sendingInProgress && [self hasPendingCrashReport]) { _sendingInProgress = YES; if (!BITHockeyBundle()) { [self sendCrashReports]; } else if (_crashManagerStatus != BITCrashManagerStatusAutoSend && [self hasNonApprovedCrashReports]) { if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillShowSubmitCrashReportAlert:)]) { [self.delegate crashManagerWillShowSubmitCrashReportAlert:self]; } NSString *appName = [[[NSBundle mainBundle] localizedInfoDictionary] objectForKey:@"CFBundleDisplayName"]; if (!appName) appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] ?: BITHockeyLocalizedString(@"HockeyAppNamePlaceholder"); NSString *alertDescription = [NSString stringWithFormat:BITHockeyLocalizedString(@"CrashDataFoundAnonymousDescription"), appName]; // the crash report is not anynomous any more if username or useremail are not nil NSString *username = nil; NSString *useremail = nil; if (self.delegate != nil && [self.delegate respondsToSelector:@selector(userNameForCrashManager:)]) { username = [self.delegate userNameForCrashManager:self]; } if (self.delegate != nil && [self.delegate respondsToSelector:@selector(userEmailForCrashManager:)]) { useremail = [self.delegate userEmailForCrashManager:self]; } if (username || useremail) { alertDescription = [NSString stringWithFormat:BITHockeyLocalizedString(@"CrashDataFoundDescription"), appName]; } UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:BITHockeyLocalizedString(@"CrashDataFoundTitle"), appName] message:alertDescription delegate:self cancelButtonTitle:BITHockeyLocalizedString(@"CrashDontSendReport") otherButtonTitles:BITHockeyLocalizedString(@"CrashSendReport"), nil]; if (self.shouldShowAlwaysButton) { [alertView addButtonWithTitle:BITHockeyLocalizedString(@"CrashSendReportAlways")]; } [alertView show]; [alertView release]; } else { [self sendCrashReports]; } } } - (void)sendCrashReports { // send it to the next runloop [self performSelector:@selector(performSendingCrashReports) withObject:nil afterDelay:1.0f]; } - (void)performSendingCrashReports { NSError *error = NULL; NSMutableString *crashes = nil; _crashIdenticalCurrentVersion = NO; for (NSUInteger i=0; i < [_crashFiles count]; i++) { NSString *filename = [_crashFiles objectAtIndex:i]; NSData *crashData = [NSData dataWithContentsOfFile:filename]; if ([crashData length] > 0) { PLCrashReport *report = [[[PLCrashReport alloc] initWithData:crashData error:&error] autorelease]; if (report == nil) { BITHockeyLog(@"Could not parse crash report"); // we cannot do anything with this report, so delete it [_fileManager removeItemAtPath:filename error:&error]; [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; continue; } NSString *crashUUID = report.reportInfo.reportGUID ?: @""; NSString *crashLogString = [BITCrashReportTextFormatter stringValueForCrashReport:report]; if ([report.applicationInfo.applicationVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { _crashIdenticalCurrentVersion = YES; } if (crashes == nil) { crashes = [NSMutableString string]; } NSString *username = @""; NSString *useremail = @""; NSString *applicationLog = @""; NSString *description = @""; NSString *errorString = nil; NSPropertyListFormat format; NSData *plist = [NSData dataWithContentsOfFile:[filename stringByAppendingString:@".meta"]]; if (plist) { NSDictionary *metaDict = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plist mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&errorString]; username = [metaDict objectForKey:kBITCrashMetaUserName] ?: @""; useremail = [metaDict objectForKey:kBITCrashMetaUserEmail] ?: @""; applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; } else { BITHockeyLog(@"ERROR: Reading crash meta data. %@", error); } if ([applicationLog length] > 0) { description = [NSString stringWithFormat:@"Log:\n%@", applicationLog]; } [crashes appendFormat:@"%s%@%@%@%@%@%@%@%@%@", [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String], [self extractAppUUIDs:report], report.applicationInfo.applicationIdentifier, report.systemInfo.operatingSystemVersion, [self getDevicePlatform], [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], report.applicationInfo.applicationVersion, crashUUID, [crashLogString stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,crashLogString.length)], username, useremail, [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:filename]; } else { // we cannot do anything with this report, so delete it [_fileManager removeItemAtPath:filename error:&error]; [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; } } [self saveSettings]; if (crashes != nil) { BITHockeyLog(@"Sending crash reports:\n%@", crashes); [self postXML:[NSString stringWithFormat:@"%@", crashes]]; } } #pragma mark - UIAlertView Delegate - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 0: if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { [self.delegate crashManagerWillCancelSendingCrashReport:self]; } _sendingInProgress = NO; [self cleanCrashReports]; break; case 1: [self sendCrashReports]; break; case 2: { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITCrashAutomaticallySendReports]; [[NSUserDefaults standardUserDefaults] synchronize]; if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReportsAlways:)]) { [self.delegate crashManagerWillSendCrashReportsAlways:self]; } [self sendCrashReports]; break; } default: _sendingInProgress = NO; [self cleanCrashReports]; break; } } #pragma mark - Networking - (void)postXML:(NSString*)xml { NSMutableURLRequest *request = nil; NSString *boundary = @"----FOO"; request = [NSMutableURLRequest requestWithURL: [NSURL URLWithString:[NSString stringWithFormat:@"%@api/2/apps/%@/crashes?sdk=%@&sdk_version=%@&feedbackEnabled=no", _updateURL, [_appIdentifier stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding], BITHOCKEY_NAME, BITHOCKEY_VERSION ] ]]; [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]]; [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]]; [postBody appendData:[xml dataUsingEncoding:NSUTF8StringEncoding]]; [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; [request setHTTPBody:postBody]; _statusCode = 200; //Release when done in the delegate method _responseData = [[NSMutableData alloc] init]; _urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; if (!_urlConnection) { BITHockeyLog(@"Sending crash reports could not start!"); _sendingInProgress = NO; } else { if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReport:)]) { [self.delegate crashManagerWillSendCrashReport:self]; } BITHockeyLog(@"Sending crash reports started."); } } #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 { if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { [self.delegate crashManager:self didFailWithError:error]; } BITHockeyLog(@"ERROR: %@", [error localizedDescription]); _sendingInProgress = NO; [_responseData release]; _responseData = nil; [_urlConnection release]; _urlConnection = nil; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSError *error = nil; if (_statusCode >= 200 && _statusCode < 400 && _responseData != nil && [_responseData length] > 0) { [self cleanCrashReports]; // HockeyApp uses PList XML format NSMutableDictionary *response = [NSPropertyListSerialization propertyListFromData:_responseData mutabilityOption:NSPropertyListMutableContainersAndLeaves format:nil errorDescription:NULL]; BITHockeyLog(@"Received API response: %@", response); if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerDidFinishSendingCrashReport:)]) { [self.delegate crashManagerDidFinishSendingCrashReport:self]; } } else if (_statusCode == 400) { [self cleanCrashReports]; error = [NSError errorWithDomain:kBITCrashErrorDomain code:BITCrashAPIAppVersionRejected userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"The server rejected receiving crash reports for this app version!", NSLocalizedDescriptionKey, nil]]; if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { [self.delegate crashManager:self didFailWithError:error]; } BITHockeyLog(@"ERROR: %@", [error localizedDescription]); } else { if (_responseData == nil || [_responseData length] == 0) { error = [NSError errorWithDomain:kBITCrashErrorDomain code:BITCrashAPIReceivedEmptyResponse userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Sending failed with an empty response!", NSLocalizedDescriptionKey, nil]]; } else { error = [NSError errorWithDomain:kBITCrashErrorDomain code:BITCrashAPIErrorWithStatusCode userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Sending failed with status code: %i", _statusCode], NSLocalizedDescriptionKey, nil]]; } if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { [self.delegate crashManager:self didFailWithError:error]; } BITHockeyLog(@"ERROR: %@", [error localizedDescription]); } _sendingInProgress = NO; [_responseData release]; _responseData = nil; [_urlConnection release]; _urlConnection = nil; } @end