From 0cfa00924c11aad16172512e9326d6c13e58acbe Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Mon, 10 Feb 2014 01:06:59 +0100 Subject: [PATCH 001/206] Add `enableDectionAppKillWhileInForeground` option to BITCrashManager This option implements basic heuristics to detect if the app got killed by the iOS watchdog while running in foreground, which can only happen if: - The app tried to allocate too much memory - Main thread doesn't respond for some time It is not possible to detect all cases where such kills can occur. --- Classes/BITCrashManager.h | 74 ++++++ Classes/BITCrashManager.m | 355 ++++++++++++++++++++++---- Classes/BITCrashReportTextFormatter.h | 1 + Classes/BITCrashReportTextFormatter.m | 117 +++++---- 4 files changed, 443 insertions(+), 104 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 64f695d6..37ac2071 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -166,6 +166,38 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { @property (nonatomic, assign, getter=isMachExceptionHandlerEnabled) BOOL enableMachExceptionHandler; +/** + * Enables heuristics to detect the app getting killed while being in the foreground + * + * This allows it to get a crash report if the app got killed while being in the foreground + * because of now of the following reasons: + * - The main thread was blocked for too long + * - The app took too long to start up + * - The app tried to allocate too much memory. If iOS did send a memory warning before killing the app because of this reason, `didReceiveMemoryWarningInLastSession` returns `YES`. + * - Permitted background duration if main thread is running in an endless loop + * - App failed to resume in time if main thread is running in an endless loop + * + * The following kills can _NOT_ be detected: + * - Terminating the app takes too long + * - Permitted background duration too long for all other cases + * - App failed to resume in time for all other cases + * - possibly more cases + * + * Crash reports triggered by this mechanisms do _NOT_ contain any stack traces since the time of the kill + * cannot be intercepted and hence no stack trace of the time of the kill event can't be gathered. + * + * Default: _NO_ + * + * @warning This is a heuristic and it _MAY_ report false positives! + * + * @see wasKilledInLastSession + * @see didReceiveMemoryWarningInLastSession + * @see [Apple Technical Note TN2151](https://developer.apple.com/library/ios/technotes/tn2151/_index.html) + * @see [Apple Technical Q&A QA1693](https://developer.apple.com/library/ios/qa/qa1693/_index.html) + */ +@property (nonatomic, assign, getter = isAppKillDetectionWhileInForegroundEnabled) BOOL enableAppKillDetectionWhileInForeground; + + /** * Set the callbacks that will be executed prior to program termination after a crash has occurred * @@ -224,6 +256,48 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { @property (nonatomic, readonly) BOOL didCrashInLastSession; +/** + Indicates if the app was killed while being in foreground from the iOS + + If `enableDectionAppKillWhileInForeground` is enabled, use this on startup to check if the + app starts the first time after it was killed by iOS in the previous session. + + This can happen if it consumed too much memory or the watchdog killed the app because it + took too long to startup or blocks the main thread for too long, or other reasons. See Apple + documentation: https://developer.apple.com/library/ios/qa/qa1693/_index.html + + See `enableDectionAppKillWhileInForeground` for more details about which kind of kills can be detected. + + @warning This property only has a correct value, once `[BITHockeyManager startManager]` was + invoked! In addition, it is automatically disabled while a debugger session is active! + + @see enableAppKillDetectionWhileInForeground + @see didReceiveMemoryWarningInLastSession + */ +@property (nonatomic, readonly) BOOL wasKilledInLastSession; + + +/** + Indicates if the app did receive a low memory warning in the last session + + It may happen that low memory warning where send but couldn't be logged, since iOS + killed the app before updating the flag in the filesystem did complete. + + This property may be true in case of low memory kills, but it doesn't have to be! Apps + can also be killed without the app ever receiving a low memory warning. + + Also the app could have received a low memory warning, but the reason for being killed was + actually different. + + @warning This property only has a correct value, once `[BITHockeyManager startManager]` was + invoked! + + @see enableAppKillDetectionWhileInForeground + @see wasKilledInLastSession + */ +@property (nonatomic, readonly) BOOL didReceiveMemoryWarningInLastSession; + + /** Provides the time between startup and crash in seconds diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index fa1c8bca..76f2a4c8 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -55,6 +55,19 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; +NSString *const kBITAppWentIntoBackgroundSafely = @"BITAppWentIntoBackgroundSafely"; +NSString *const kBITAppDidReceiveLowMemoryNotification = @"BITAppDidReceiveLowMemoryNotification"; +NSString *const kBITAppVersion = @"BITAppVersion"; +NSString *const kBITAppOSVersion = @"BITAppOSVersion"; +NSString *const kBITAppUUIDs = @"BITAppUUIDs"; + +NSString *const kBITFakeCrashUUID = @"BITFakeCrashUUID"; +NSString *const kBITFakeCrashAppVersion = @"BITFakeCrashAppVersion"; +NSString *const kBITFakeCrashAppBundleIdentifier = @"BITFakeCrashAppBundleIdentifier"; +NSString *const kBITFakeCrashOSVersion = @"BITFakeCrashOSVersion"; +NSString *const kBITFakeCrashDeviceModel = @"BITFakeCrashDeviceModel"; +NSString *const kBITFakeCrashAppBinaryUUID = @"BITFakeCrashAppBinaryUUID"; +NSString *const kBITFakeCrashReport = @"BITFakeCrashAppString"; @interface BITCrashManager () @@ -83,7 +96,13 @@ @implementation BITCrashManager { BOOL _sendingInProgress; BOOL _isSetup; + BOOL _didLogLowMemoryWarning; + id _appDidBecomeActiveObserver; + id _appWillTerminateObserver; + id _appDidEnterBackgroundObserver; + id _appWillEnterForegroundObserver; + id _appDidReceiveLowMemoryWarningObserver; id _networkDidBecomeReachableObserver; } @@ -105,6 +124,7 @@ - (id)init { _didCrashInLastSession = NO; _timeintervalCrashInLastSessionOccured = -1; + _didLogLowMemoryWarning = NO; _approvedCrashReports = [[NSMutableDictionary alloc] init]; @@ -287,20 +307,116 @@ - (void) registerObservers { [strongSelf triggerDelayedProcessing]; }]; } + + if (nil == _appWillTerminateObserver) { + _appWillTerminateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf leavingAppSafely]; + }]; + } + + if (nil == _appDidEnterBackgroundObserver) { + _appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf leavingAppSafely]; + }]; + } + + if (nil == _appWillEnterForegroundObserver) { + _appWillEnterForegroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf appEnteredForeground]; + }]; + } + + if (nil == _appDidReceiveLowMemoryWarningObserver) { + _appDidReceiveLowMemoryWarningObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + // we only need to log this once + if (!_didLogLowMemoryWarning) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppDidReceiveLowMemoryNotification]; + _didLogLowMemoryWarning = YES; + } + }]; + } } - (void) unregisterObservers { - if(_appDidBecomeActiveObserver) { - [[NSNotificationCenter defaultCenter] removeObserver:_appDidBecomeActiveObserver]; - _appDidBecomeActiveObserver = nil; - } + [self unregisterObserver:_appDidBecomeActiveObserver]; + [self unregisterObserver:_appWillTerminateObserver]; + [self unregisterObserver:_appDidEnterBackgroundObserver]; + [self unregisterObserver:_appWillEnterForegroundObserver]; + [self unregisterObserver:_appDidReceiveLowMemoryWarningObserver]; - if(_networkDidBecomeReachableObserver) { - [[NSNotificationCenter defaultCenter] removeObserver:_networkDidBecomeReachableObserver]; - _networkDidBecomeReachableObserver = nil; + [self unregisterObserver:_networkDidBecomeReachableObserver]; +} + +- (void) unregisterObserver:(id)observer { + if (observer) { + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + observer = nil; } } +- (void)leavingAppSafely { + if (self.isAppKillDetectionWhileInForegroundEnabled) + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppWentIntoBackgroundSafely]; +} + +- (void)appEnteredForeground { + // we disable kill detection while the debugger is running, since we'd get only false positives if the app is terminated by the user using the debugger + if (self.isDebuggerAttached) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppWentIntoBackgroundSafely]; + } else if (self.isAppKillDetectionWhileInForegroundEnabled) { + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kBITAppWentIntoBackgroundSafely]; + + static dispatch_once_t predAppData; + + dispatch_once(&predAppData, ^{ + id bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + if (bundleVersion && [bundleVersion isKindOfClass:[NSString class]]) + [[NSUserDefaults standardUserDefaults] setObject:bundleVersion forKey:kBITAppVersion]; + [[NSUserDefaults standardUserDefaults] setObject:[[UIDevice currentDevice] systemVersion] forKey:kBITAppOSVersion]; + + NSString *uuidString =[NSString stringWithFormat:@"%@", + [self deviceArchitecture], + [self executableUUID] + ]; + + [[NSUserDefaults standardUserDefaults] setObject:uuidString forKey:kBITAppUUIDs]; + }); + } +} + +- (NSString *)deviceArchitecture { + NSString *archName = @"???"; + + size_t size; + cpu_type_t type; + cpu_subtype_t subtype; + size = sizeof(type); + if (sysctlbyname("hw.cputype", &type, &size, NULL, 0)) + return archName; + + size = sizeof(subtype); + if (sysctlbyname("hw.cpusubtype", &subtype, &size, NULL, 0)) + return archName; + + archName = [BITCrashReportTextFormatter bit_archNameFromCPUType:type subType:subtype] ?: @"???"; + + return archName; +} /** * Get the userID from the delegate which should be stored with the crash report @@ -437,6 +553,35 @@ - (void)generateTestCrash { } } +/** + * Write a meta file for a new crash report + * + * @param filename the crash reports temp filename + */ +- (void)storeMetaDataForCrashReportFilename:(NSString *)filename { + NSError *error = NULL; + NSMutableDictionary *metaDict = [NSMutableDictionary dictionaryWithCapacity:4]; + NSString *applicationLog = @""; + NSString *errorString = nil; + + [self addStringValueToKeychain:[self userNameForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", filename, kBITCrashMetaUserName]]; + [self addStringValueToKeychain:[self userEmailForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", filename, kBITCrashMetaUserEmail]]; + [self addStringValueToKeychain:[self userIDForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", filename, kBITCrashMetaUserID]]; + + 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:[_crashesDir stringByAppendingPathComponent: [filename stringByAppendingPathExtension:@"meta"]] atomically:YES]; + } else { + BITHockeyLog(@"ERROR: Writing crash meta data failed. %@", error); + } +} #pragma mark - PLCrashReporter @@ -481,28 +626,7 @@ - (void) handleCrashReport { [crashData writeToFile:[_crashesDir stringByAppendingPathComponent: cacheFilename] atomically:YES]; - // write the meta file - NSMutableDictionary *metaDict = [NSMutableDictionary dictionaryWithCapacity:4]; - NSString *applicationLog = @""; - NSString *errorString = nil; - - [self addStringValueToKeychain:[self userNameForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self addStringValueToKeychain:[self userEmailForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self addStringValueToKeychain:[self userIDForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; - - 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); - } + [self storeMetaDataForCrashReportFilename:cacheFilename]; } } } @@ -734,10 +858,103 @@ - (void)startManager { _isSetup = YES; }); } + + if ([[NSUserDefaults standardUserDefaults] valueForKey:kBITAppDidReceiveLowMemoryNotification]) + _didReceiveMemoryWarningInLastSession = [[NSUserDefaults standardUserDefaults] boolForKey:kBITAppDidReceiveLowMemoryNotification]; + + if (!_didCrashInLastSession && self.isAppKillDetectionWhileInForegroundEnabled) { + BOOL didAppSwitchToBackgroundSafely = YES; + + if ([[NSUserDefaults standardUserDefaults] valueForKey:kBITAppWentIntoBackgroundSafely]) + didAppSwitchToBackgroundSafely = [[NSUserDefaults standardUserDefaults] boolForKey:kBITAppWentIntoBackgroundSafely]; + + if (!didAppSwitchToBackgroundSafely) { + NSLog(@"AppHasBeenKilled!"); + + [self createCrashReportForAppKill]; + + _wasKilledInLastSession = YES; + _didCrashInLastSession = YES; + } + } + [self appEnteredForeground]; + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kBITAppDidReceiveLowMemoryNotification]; + [[NSUserDefaults standardUserDefaults] synchronize]; [self triggerDelayedProcessing]; } +/** + * Creates a fake crash report because the app was killed while being in foreground + */ +- (void)createCrashReportForAppKill { + NSString *fakeReportUUID = bit_UUID(); + + NSString *fakeReportAppVersion = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppVersion]; + if (!fakeReportAppVersion) + return; + + NSString *fakeReportOSVersion = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppOSVersion] ?: [[UIDevice currentDevice] systemVersion]; + NSString *fakeReportAppBundleIdentifier = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; + NSString *fakeReportDeviceModel = [self getDevicePlatform] ?: @"Unknown"; + NSString *fakeReportAppUUIDs = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppUUIDs] ?: @""; + + NSMutableString *fakeReportString = [NSMutableString string]; + + [fakeReportString appendFormat:@"Incident Identifier: %@\n", fakeReportUUID]; + [fakeReportString appendFormat:@"CrashReporter Key: %@\n", bit_appAnonID() ?: @"???"]; + [fakeReportString appendFormat:@"Hardware Model: %@\n", fakeReportDeviceModel]; + [fakeReportString appendFormat:@"Identifier: %@\n", fakeReportAppBundleIdentifier]; + [fakeReportString appendFormat:@"Version: %@\n", fakeReportAppVersion]; + [fakeReportString appendString:@"Code Type: ARM\n"]; + [fakeReportString appendString:@"\n"]; + + NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + NSDateFormatter *rfc3339Formatter = [[NSDateFormatter alloc] init]; + [rfc3339Formatter setLocale:enUSPOSIXLocale]; + [rfc3339Formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; + [rfc3339Formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + + // we use the current date, since we don't know when the kill actually happened + [fakeReportString appendFormat:@"Date/Time: %@\n", [rfc3339Formatter stringFromDate:[NSDate date]]]; + [fakeReportString appendFormat:@"OS Version: %@\n", fakeReportOSVersion]; + [fakeReportString appendString:@"Report Version: 104\n"]; + [fakeReportString appendString:@"\n"]; + [fakeReportString appendString:@"Exception Type: SIGKILL\n"]; + [fakeReportString appendString:@"Exception Codes: 00000020 at 0x8badf00d\n"]; + [fakeReportString appendString:@"\n"]; + [fakeReportString appendString:@"Application Specific Information:\n"]; + [fakeReportString appendString:@"The application was killed by the iOS watchdog."]; + if (self.didReceiveMemoryWarningInLastSession) { + [fakeReportString appendString:@" The app received at least one Low Memory Warning."]; + } + [fakeReportString appendString:@"\n"]; + + NSString *fakeReportFilename = [NSString stringWithFormat: @"%.0f", [NSDate timeIntervalSinceReferenceDate]]; + + NSString *errorString = nil; + + NSMutableDictionary *rootObj = [NSMutableDictionary dictionaryWithCapacity:2]; + [rootObj setObject:fakeReportUUID forKey:kBITFakeCrashUUID]; + [rootObj setObject:fakeReportAppVersion forKey:kBITFakeCrashAppVersion]; + [rootObj setObject:fakeReportAppBundleIdentifier forKey:kBITFakeCrashAppBundleIdentifier]; + [rootObj setObject:fakeReportOSVersion forKey:kBITFakeCrashOSVersion]; + [rootObj setObject:fakeReportDeviceModel forKey:kBITFakeCrashDeviceModel]; + [rootObj setObject:fakeReportAppUUIDs forKey:kBITFakeCrashAppBinaryUUID]; + [rootObj setObject:fakeReportString forKey:kBITFakeCrashReport]; + + NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)rootObj + format:NSPropertyListBinaryFormat_v1_0 + errorDescription:&errorString]; + if (plist) { + if ([plist writeToFile:[_crashesDir stringByAppendingPathComponent:[fakeReportFilename stringByAppendingPathExtension:@"fake"]] atomically:YES]) { + [self storeMetaDataForCrashReportFilename:fakeReportFilename]; + } + } else { + BITHockeyLog(@"ERROR: Writing fake crash report. %@", errorString); + } +} + /** * Send all approved crash reports * @@ -755,13 +972,49 @@ - (void)sendCrashReports { NSData *crashData = [NSData dataWithContentsOfFile:filename]; if ([crashData length] > 0) { - BITPLCrashReport *report = [[BITPLCrashReport alloc] initWithData:crashData error:&error]; + BITPLCrashReport *report = nil; + NSString *crashUUID = @""; + NSString *installString = nil; + NSString *crashLogString = nil; + NSString *appBundleIdentifier = nil; + NSString *appBundleVersion = nil; + NSString *osVersion = nil; + NSString *deviceModel = nil; + NSString *appBinaryUUIDs = nil; + NSString *metaFilename = nil; + + NSString *errorString = nil; + NSPropertyListFormat format; + + if ([[cacheFilename pathExtension] isEqualToString:@"fake"]) { + NSDictionary *fakeReportDict = (NSDictionary *)[NSPropertyListSerialization + propertyListFromData:crashData + mutabilityOption:NSPropertyListMutableContainersAndLeaves + format:&format + errorDescription:&errorString]; + + crashLogString = [fakeReportDict objectForKey:kBITFakeCrashReport]; + crashUUID = [fakeReportDict objectForKey:kBITFakeCrashUUID]; + appBundleIdentifier = [fakeReportDict objectForKey:kBITFakeCrashAppBundleIdentifier]; + appBundleVersion = [fakeReportDict objectForKey:kBITFakeCrashAppVersion]; + appBinaryUUIDs = [fakeReportDict objectForKey:kBITFakeCrashAppBinaryUUID]; + deviceModel = [fakeReportDict objectForKey:kBITFakeCrashDeviceModel]; + osVersion = [fakeReportDict objectForKey:kBITFakeCrashOSVersion]; + + metaFilename = [cacheFilename stringByReplacingOccurrencesOfString:@".fake" withString:@".meta"]; + if ([appBundleVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + _crashIdenticalCurrentVersion = YES; + } + + } else { + report = [[BITPLCrashReport alloc] initWithData:crashData error:&error]; + } - if (report == nil) { + if (report == nil && crashLogString == nil) { BITHockeyLog(@"WARNING: 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]; + [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", metaFilename] error:&error]; [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; @@ -769,16 +1022,23 @@ - (void)sendCrashReports { continue; } - NSString *crashUUID = @""; - if (report.uuidRef != NULL) { - crashUUID = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); + if (report) { + if (report.uuidRef != NULL) { + crashUUID = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); + } + metaFilename = [filename stringByAppendingPathExtension:@"meta"]; + crashLogString = [BITCrashReportTextFormatter stringValueForCrashReport:report crashReporterKey:installString]; + appBundleIdentifier = report.applicationInfo.applicationIdentifier; + appBundleVersion = report.applicationInfo.applicationVersion; + osVersion = report.systemInfo.operatingSystemVersion; + deviceModel = [self getDevicePlatform]; + appBinaryUUIDs = [self extractAppUUIDs:report]; + if ([report.applicationInfo.applicationVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + _crashIdenticalCurrentVersion = YES; + } } - NSString *installString = bit_appAnonID() ?: @""; - NSString *crashLogString = [BITCrashReportTextFormatter stringValueForCrashReport:report crashReporterKey:installString]; - if ([report.applicationInfo.applicationVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { - _crashIdenticalCurrentVersion = YES; - } + installString = bit_appAnonID() ?: @""; if (crashes == nil) { crashes = [NSMutableString string]; @@ -790,10 +1050,7 @@ - (void)sendCrashReports { NSString *applicationLog = @""; NSString *description = @""; - NSString *errorString = nil; - NSPropertyListFormat format; - - NSData *plist = [NSData dataWithContentsOfFile:[filename stringByAppendingString:@".meta"]]; + NSData *plist = [NSData dataWithContentsOfFile:[_crashesDir stringByAppendingPathComponent:metaFilename]]; if (plist) { NSDictionary *metaDict = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plist @@ -815,12 +1072,12 @@ - (void)sendCrashReports { [crashes appendFormat:@"%s%@%@%@%@%@%@%@%@%@%@%@", [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String], - [self extractAppUUIDs:report], - report.applicationInfo.applicationIdentifier, - report.systemInfo.operatingSystemVersion, - [self getDevicePlatform], + appBinaryUUIDs, + appBundleIdentifier, + osVersion, + deviceModel, [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], - report.applicationInfo.applicationVersion, + appBundleVersion, crashUUID, [crashLogString stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,crashLogString.length)], userid, diff --git a/Classes/BITCrashReportTextFormatter.h b/Classes/BITCrashReportTextFormatter.h index 13ab19f9..5ddef4a2 100644 --- a/Classes/BITCrashReportTextFormatter.h +++ b/Classes/BITCrashReportTextFormatter.h @@ -49,5 +49,6 @@ + (NSString *)stringValueForCrashReport:(PLCrashReport *)report crashReporterKey:(NSString *)crashReporterKey; + (NSArray *)arrayOfAppUUIDsForCrashReport:(PLCrashReport *)report; ++ (NSString *)bit_archNameFromCPUType:(uint64_t)cpuType subType:(uint64_t)subType; @end diff --git a/Classes/BITCrashReportTextFormatter.m b/Classes/BITCrashReportTextFormatter.m index 6094affc..c8b060d3 100644 --- a/Classes/BITCrashReportTextFormatter.m +++ b/Classes/BITCrashReportTextFormatter.m @@ -464,66 +464,73 @@ + (NSString *)bit_archNameFromImageInfo:(BITPLCrashReportBinaryImageInfo *)image { NSString *archName = @"???"; if (imageInfo.codeType != nil && imageInfo.codeType.typeEncoding == PLCrashReportProcessorTypeEncodingMach) { - switch (imageInfo.codeType.type) { - case CPU_TYPE_ARM: - /* Apple includes subtype for ARM binaries. */ - switch (imageInfo.codeType.subtype) { - case CPU_SUBTYPE_ARM_V6: - archName = @"armv6"; - break; - - case CPU_SUBTYPE_ARM_V7: - archName = @"armv7"; - break; - - case CPU_SUBTYPE_ARM_V7S: - archName = @"armv7s"; - break; - - default: - archName = @"arm-unknown"; - break; - } - break; - - case CPU_TYPE_ARM64: - /* Apple includes subtype for ARM64 binaries. */ - switch (imageInfo.codeType.subtype) { - case CPU_SUBTYPE_ARM_ALL: - archName = @"arm64"; - break; - - case CPU_SUBTYPE_ARM_V8: - archName = @"arm64"; - break; - - default: - archName = @"arm64-unknown"; - break; - } - break; - - case CPU_TYPE_X86: - archName = @"i386"; - break; - - case CPU_TYPE_X86_64: - archName = @"x86_64"; - break; - - case CPU_TYPE_POWERPC: - archName = @"powerpc"; - break; - - default: - // Use the default archName value (initialized above). - break; - } + archName = [BITCrashReportTextFormatter bit_archNameFromCPUType:imageInfo.codeType.type subType:imageInfo.codeType.subtype]; } return archName; } ++ (NSString *)bit_archNameFromCPUType:(uint64_t)cpuType subType:(uint64_t)subType { + NSString *archName = @"???"; + switch (cpuType) { + case CPU_TYPE_ARM: + /* Apple includes subtype for ARM binaries. */ + switch (subType) { + case CPU_SUBTYPE_ARM_V6: + archName = @"armv6"; + break; + + case CPU_SUBTYPE_ARM_V7: + archName = @"armv7"; + break; + + case CPU_SUBTYPE_ARM_V7S: + archName = @"armv7s"; + break; + + default: + archName = @"arm-unknown"; + break; + } + break; + + case CPU_TYPE_ARM64: + /* Apple includes subtype for ARM64 binaries. */ + switch (subType) { + case CPU_SUBTYPE_ARM_ALL: + archName = @"arm64"; + break; + + case CPU_SUBTYPE_ARM_V8: + archName = @"arm64"; + break; + + default: + archName = @"arm64-unknown"; + break; + } + break; + + case CPU_TYPE_X86: + archName = @"i386"; + break; + + case CPU_TYPE_X86_64: + archName = @"x86_64"; + break; + + case CPU_TYPE_POWERPC: + archName = @"powerpc"; + break; + + default: + // Use the default archName value (initialized above). + break; + } + + return archName; +} + /** * Format a stack frame for display in a thread backtrace. From cc480299eb9d96a702821fc944a7550568374ca6 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Mon, 10 Feb 2014 01:52:15 +0100 Subject: [PATCH 002/206] Multiple improvements for heuristic based kill detection handling - Marked the feature as `EXPERIMENTAL` - renamed property to `enableAppNotTerminatingCleanlyDetection` - Added details about the heuristic algorithm - Added optional delegate to let the developer influence if a report should be considered as a crash or not - Adjusted the description string in the generated report to make it more clear what actually happened. --- Classes/BITCrashManager.h | 18 +++++++++++++----- Classes/BITCrashManager.m | 23 +++++++++++++++-------- Classes/BITCrashManagerDelegate.h | 21 +++++++++++++++++++++ 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 37ac2071..c56d1091 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -167,7 +167,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { /** - * Enables heuristics to detect the app getting killed while being in the foreground + * EXPERIMENTAL: Enable heuristics to detect the app not terminating cleanly * * This allows it to get a crash report if the app got killed while being in the foreground * because of now of the following reasons: @@ -176,6 +176,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { * - The app tried to allocate too much memory. If iOS did send a memory warning before killing the app because of this reason, `didReceiveMemoryWarningInLastSession` returns `YES`. * - Permitted background duration if main thread is running in an endless loop * - App failed to resume in time if main thread is running in an endless loop + * - If `enableMachExceptionHandler` is not activated, crashed due to stackoverflow will also be reported * * The following kills can _NOT_ be detected: * - Terminating the app takes too long @@ -186,16 +187,23 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { * Crash reports triggered by this mechanisms do _NOT_ contain any stack traces since the time of the kill * cannot be intercepted and hence no stack trace of the time of the kill event can't be gathered. * + * The heuristic is implemented as follows: + * If the app never gets a `UIApplicationDidEnterBackgroundNotification` or `UIApplicationWillTerminateNotification` + * notification, PLCrashReporter doesn't detect a crash itself, and the app starts up again, it is assumed that + * the app got either killed by iOS while being in foreground or a crash occured that couldn't be detected. + * * Default: _NO_ * - * @warning This is a heuristic and it _MAY_ report false positives! + * @warning This is a heuristic and it _MAY_ report false positives! It has been tested with iOS 6.1 and iOS 7. + * Depending on Apple changing notification events, new iOS version may cause more false positives! * * @see wasKilledInLastSession * @see didReceiveMemoryWarningInLastSession + * @see `BITCrashManagerDelegate considerAppNotTerminatedCleanlyReportForCrashManager:` * @see [Apple Technical Note TN2151](https://developer.apple.com/library/ios/technotes/tn2151/_index.html) * @see [Apple Technical Q&A QA1693](https://developer.apple.com/library/ios/qa/qa1693/_index.html) */ -@property (nonatomic, assign, getter = isAppKillDetectionWhileInForegroundEnabled) BOOL enableAppKillDetectionWhileInForeground; +@property (nonatomic, assign, getter = isAppNotTerminatingCleanlyDetectionEnabled) BOOL enableAppNotTerminatingCleanlyDetection; /** @@ -271,7 +279,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { @warning This property only has a correct value, once `[BITHockeyManager startManager]` was invoked! In addition, it is automatically disabled while a debugger session is active! - @see enableAppKillDetectionWhileInForeground + @see enableAppNotTerminatingCleanlyDetection @see didReceiveMemoryWarningInLastSession */ @property (nonatomic, readonly) BOOL wasKilledInLastSession; @@ -292,7 +300,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { @warning This property only has a correct value, once `[BITHockeyManager startManager]` was invoked! - @see enableAppKillDetectionWhileInForeground + @see enableAppNotTerminatingCleanlyDetection @see wasKilledInLastSession */ @property (nonatomic, readonly) BOOL didReceiveMemoryWarningInLastSession; diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 76f2a4c8..e7280a92 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -370,7 +370,7 @@ - (void) unregisterObserver:(id)observer { } - (void)leavingAppSafely { - if (self.isAppKillDetectionWhileInForegroundEnabled) + if (self.isAppNotTerminatingCleanlyDetectionEnabled) [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppWentIntoBackgroundSafely]; } @@ -378,7 +378,7 @@ - (void)appEnteredForeground { // we disable kill detection while the debugger is running, since we'd get only false positives if the app is terminated by the user using the debugger if (self.isDebuggerAttached) { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppWentIntoBackgroundSafely]; - } else if (self.isAppKillDetectionWhileInForegroundEnabled) { + } else if (self.isAppNotTerminatingCleanlyDetectionEnabled) { [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kBITAppWentIntoBackgroundSafely]; static dispatch_once_t predAppData; @@ -862,19 +862,26 @@ - (void)startManager { if ([[NSUserDefaults standardUserDefaults] valueForKey:kBITAppDidReceiveLowMemoryNotification]) _didReceiveMemoryWarningInLastSession = [[NSUserDefaults standardUserDefaults] boolForKey:kBITAppDidReceiveLowMemoryNotification]; - if (!_didCrashInLastSession && self.isAppKillDetectionWhileInForegroundEnabled) { + if (!_didCrashInLastSession && self.isAppNotTerminatingCleanlyDetectionEnabled) { BOOL didAppSwitchToBackgroundSafely = YES; if ([[NSUserDefaults standardUserDefaults] valueForKey:kBITAppWentIntoBackgroundSafely]) didAppSwitchToBackgroundSafely = [[NSUserDefaults standardUserDefaults] boolForKey:kBITAppWentIntoBackgroundSafely]; if (!didAppSwitchToBackgroundSafely) { - NSLog(@"AppHasBeenKilled!"); + BOOL considerReport = YES; - [self createCrashReportForAppKill]; + if (self.delegate && + [self.delegate respondsToSelector:@selector(considerAppNotTerminatedCleanlyReportForCrashManager:)]) { + considerReport = [self.delegate considerAppNotTerminatedCleanlyReportForCrashManager:self]; + } + + if (considerReport) { + [self createCrashReportForAppKill]; - _wasKilledInLastSession = YES; - _didCrashInLastSession = YES; + _wasKilledInLastSession = YES; + _didCrashInLastSession = YES; + } } } [self appEnteredForeground]; @@ -924,7 +931,7 @@ - (void)createCrashReportForAppKill { [fakeReportString appendString:@"Exception Codes: 00000020 at 0x8badf00d\n"]; [fakeReportString appendString:@"\n"]; [fakeReportString appendString:@"Application Specific Information:\n"]; - [fakeReportString appendString:@"The application was killed by the iOS watchdog."]; + [fakeReportString appendString:@"The application did not terminate cleanly but no crash occured."]; if (self.didReceiveMemoryWarningInLastSession) { [fakeReportString appendString:@" The app received at least one Low Memory Warning."]; } diff --git a/Classes/BITCrashManagerDelegate.h b/Classes/BITCrashManagerDelegate.h index e2792b5c..0c3174c5 100644 --- a/Classes/BITCrashManagerDelegate.h +++ b/Classes/BITCrashManagerDelegate.h @@ -130,4 +130,25 @@ */ - (void)crashManagerDidFinishSendingCrashReport:(BITCrashManager *)crashManager; +///----------------------------------------------------------------------------- +/// @name Experimental +///----------------------------------------------------------------------------- + +/** Define if a report should be considered as a crash report + + Due to the risk, that these reports may be false positives, this delegates allows the + developer to influence which reports detected by the heuristic should actually be reported. + + The developer can use the following property to get more information about the crash scenario: + - `[BITCrashManager didReceiveMemoryWarningInLastSession]`: Did the app receive a low memory warning + + This allows only reports to be considered where at least one low memory warning notification was + received by the app to reduce to possibility of having false positives. + + @param crashManager The `BITCrashManager` instance invoking this delegate + @return `YES` if the heuristic based detected report should be reported, otherwise `NO` + @see `[BITCrashManager didReceiveMemoryWarningInLastSession]` + */ +-(BOOL)considerAppNotTerminatedCleanlyReportForCrashManager:(BITCrashManager *)crashManager; + @end From 4d414b78f4cd233de35357fe0149eb5a4f55c84b Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Mon, 10 Feb 2014 13:10:10 +0100 Subject: [PATCH 003/206] Make sure the flag that a memory warning was received is persisted right away NSUserDefaults may not synchronize changes immediately, so if the kill happens before it persists changes, the information is lost. --- Classes/BITCrashManager.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index e7280a92..27b686c4 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -346,6 +346,7 @@ - (void) registerObservers { // we only need to log this once if (!_didLogLowMemoryWarning) { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppDidReceiveLowMemoryNotification]; + [[NSUserDefaults standardUserDefaults] synchronize]; _didLogLowMemoryWarning = YES; } }]; From ddf16f70623078ac4540a1d55a5279f235d073e9 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Mon, 10 Feb 2014 13:30:46 +0100 Subject: [PATCH 004/206] Add a new line at the end of the `application specific information` string to be considered by the servers grouping --- Classes/BITCrashManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 27b686c4..7f045737 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -936,7 +936,7 @@ - (void)createCrashReportForAppKill { if (self.didReceiveMemoryWarningInLastSession) { [fakeReportString appendString:@" The app received at least one Low Memory Warning."]; } - [fakeReportString appendString:@"\n"]; + [fakeReportString appendString:@"\n\n"]; NSString *fakeReportFilename = [NSString stringWithFormat: @"%.0f", [NSDate timeIntervalSinceReferenceDate]]; From 9b57875a47dae4b9836fea9ac5b7c3bef28a79db Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 12 Feb 2014 18:15:24 +0100 Subject: [PATCH 005/206] Added Photo Picker functionality to Feedback Composer (#3) --- Classes/BITFeedbackComposeViewController.m | 143 ++++++++++++++++++-- Resources/iconCamera.png | Bin 0 -> 346 bytes Resources/iconCamera@2x.png | Bin 0 -> 642 bytes Support/HockeySDK.xcodeproj/project.pbxproj | 8 ++ 4 files changed, 143 insertions(+), 8 deletions(-) create mode 100755 Resources/iconCamera.png create mode 100755 Resources/iconCamera@2x.png diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index c4eda023..72004106 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -42,15 +42,22 @@ #import "BITHockeyHelper.h" -@interface BITFeedbackComposeViewController () { +@interface BITFeedbackComposeViewController () { UIStatusBarStyle _statusBarStyle; } @property (nonatomic, weak) BITFeedbackManager *manager; @property (nonatomic, strong) UITextView *textView; +@property (nonatomic, strong) UIView *contentViewContainer; +@property (nonatomic, strong) UIScrollView *photoScrollView; +@property (nonatomic, strong) NSMutableArray *photoScrollViewImageViews; @property (nonatomic, strong) NSString *text; +@property (nonatomic, strong) NSMutableArray *photos; + +@property (nonatomic, strong) UIView *textAccessoryView; + @end @@ -68,6 +75,8 @@ - (id)init { _blockUserDataScreen = NO; _delegate = nil; _manager = [BITHockeyManager sharedHockeyManager].feedbackManager; + _photos = [NSMutableArray new]; + _photoScrollViewImageViews = [NSMutableArray new]; _text = nil; } @@ -122,12 +131,12 @@ - (void)keyboardWasShown:(NSNotification*)aNotification { frame.size.height = windowSize.width - navBarHeight - modalGap - kbSize.width; } } - [self.textView setFrame:frame]; + [self.contentViewContainer setFrame:frame]; } - (void)keyboardWillBeHidden:(NSNotification*)aNotification { CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); - [self.textView setFrame:frame]; + [self.contentViewContainer setFrame:frame]; } @@ -142,20 +151,49 @@ - (void)viewDidLoad { self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(dismissAction:)]; - self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeSend") style:UIBarButtonItemStyleDone target:self action:@selector(sendAction:)]; + + // Container that contains both the textfield and eventually the photo scroll view on the right side + self.contentViewContainer = [[UIView alloc] initWithFrame:self.view.bounds]; + [self.view addSubview:self.contentViewContainer]; + + // message input textfield - self.textView = [[UITextView alloc] initWithFrame:self.view.frame]; + self.textView = [[UITextView alloc] initWithFrame:self.view.bounds]; self.textView.font = [UIFont systemFontOfSize:17]; self.textView.delegate = self; self.textView.backgroundColor = [UIColor whiteColor]; self.textView.returnKeyType = UIReturnKeyDefault; - self.textView.autoresizingMask = UIViewAutoresizingFlexibleWidth; - [self.view addSubview:self.textView]; + self.textView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; + + [self.contentViewContainer addSubview:self.textView]; + + // Add Photo Button + Container that's displayed above the keyboard. + self.textAccessoryView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44)]; + self.textAccessoryView.backgroundColor = [UIColor colorWithRed:0.9f green:0.9f blue:0.9f alpha:1.0f]; + UIButton *addPhotoButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [addPhotoButton setTitle:@"+ Add Photo" forState:UIControlStateNormal]; + addPhotoButton.frame = CGRectMake(0, 0, 100, 44); + + [addPhotoButton addTarget:self action:@selector(addPhotoAction:) forControlEvents:UIControlEventTouchUpInside]; + + [self.textAccessoryView addSubview:addPhotoButton]; + + self.textView.inputAccessoryView = self.textAccessoryView; + + self.photoScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero]; + self.photoScrollView.scrollEnabled = YES; + self.photoScrollView.bounces = YES; + self.photoScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; + + + [self.contentViewContainer addSubview:self.photoScrollView]; + + } - (void)viewWillAppear:(BOOL)animated { @@ -178,7 +216,7 @@ - (void)viewWillAppear:(BOOL)animated { [[UIApplication sharedApplication] setStatusBarStyle:(self.navigationController.navigationBar.barStyle == UIBarStyleDefault) ? UIStatusBarStyleDefault : UIStatusBarStyleBlackOpaque]; #endif - [self.textView setFrame:self.view.frame]; + // [self.textView setFrame:self.view.frame]; if (_text) { self.textView.text = _text; @@ -218,6 +256,69 @@ - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; } +-(void)refreshPhotoScrollview { + CGFloat scrollViewWidth = 0; + + if (self.photos.count){ + scrollViewWidth = 100; + } + + CGRect textViewFrame = self.textView.frame; + + CGRect scrollViewFrame = self.photoScrollView.frame; + + BOOL alreadySetup = CGRectGetWidth(scrollViewFrame) == scrollViewWidth; + + if (!alreadySetup){ + textViewFrame.size.width -= scrollViewWidth; + + // status bar? + + scrollViewFrame = CGRectMake(CGRectGetMaxX(textViewFrame), self.view.frame.origin.y, scrollViewWidth, CGRectGetHeight(textViewFrame)); + self.textView.frame = textViewFrame; + self.photoScrollView.frame = scrollViewFrame; + self.photoScrollView.contentInset = self.textView.contentInset; + } + + for (UIView *subview in self.photoScrollView.subviews){ + [subview removeFromSuperview]; + } + + if (self.photos.count > self.photoScrollViewImageViews.count){ + NSInteger numberOfViewsToCreate = self.photos.count - self.photoScrollViewImageViews.count; + for (int i = 0;iINyI0iW-~k|Lo>1XIWv&31>&1P{1u3$7^n+)n2Is#^nX$;2!R^XfJLqWDi?w)qM3+@sH0Fi6pLIani{P6fTn;L3#_5^YAkZA(bQl7 z79uS02jVqAJPR#YRwIe|<1>Jo$rJ;l`#=UqAqO%<1)7PDA_WV`<Fbobk21Yt?*O8Gi z90Mct2|_0Z7I=g_0p$sJgtTKP;0Z!Ure^HcjNR(78TiI2N)1>;c1+_X!{X9GwcqwU{GGA2NtL`0jvn8%2J_(6?$5`95I4&^M@qyo~8j zz?I-Tht6vSc$bdtBJqRMF7W`NTXm8ZC=1t6MH($KY<8OQIbaItHJN-`wK)S`06$Il ze42a#E`V81-7(;Vju1-emAmR=(wXD~IXD;zBU|#f9hl`H9pKtDolj`QKEnt}{FSsI zOVt3k;GKL1j5CN+BgVK!Cl}4w8@D~+5Z~ljx5+SoO0Nv^v4*%T!6BZ~>WLj{%JT+w zs4~@jXIcjNKyw1hBYcfXs`g52eCHW-j4;NBh$S~+ZM}Xb9*+oVUBee)%qL0?#Kfo( zC0w-UPvm)PAg&s)HR$?xKW|et=&j4TKQZVkojmnspE+MmUQsuBu86$rizc5iy(hTd zE}HzDJahga91XfkC-0NzouxO0rXBaIChrlC$3`GQ_n`o#bx@M3z-!t~76jUjx~t zO9`QfTmz|2KTnu&$ls^*)Epx`P4iXy1-uo%KDuRkri%YINt?+BE#-c6u{nBd*$(_& c_ Date: Thu, 13 Feb 2014 11:04:32 +0100 Subject: [PATCH 006/206] + Finalized preliminary feedback support. Now comes the polish. --- Classes/BITFeedbackComposeViewController.m | 2 +- Classes/BITFeedbackManager.m | 14 ++++++++++++-- Classes/BITFeedbackManagerPrivate.h | 2 +- Classes/BITFeedbackMessage.h | 1 + Classes/BITFeedbackMessage.m | 3 +++ Classes/BITHockeyAppClient.h | 16 ++++++++++++++-- Classes/BITHockeyAppClient.m | 8 ++++++-- 7 files changed, 38 insertions(+), 8 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 72004106..a33060af 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -351,7 +351,7 @@ - (void)sendAction:(id)sender { NSString *text = self.textView.text; - [self.manager submitMessageWithText:text]; + [self.manager submitMessageWithText:text andPhotos:self.photos]; [self dismissWithResult:BITFeedbackComposeResultSubmitted]; } diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 90318326..4344fab4 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -837,6 +837,15 @@ - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BIT [postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + NSInteger photoIndex = 0; + + for (UIImage *image in message.photos){ + NSString *contentType = @"image/png"; + NSData* imageData = UIImagePNGRepresentation(image); + [postBody appendData:[BITHockeyAppClient dataWithPostValue:imageData forKey:[NSString stringWithFormat:@"attachment%ld", (long)photoIndex] contentType:contentType boundary:boundary]]; + photoIndex++; + } + [request setHTTPBody:postBody]; } @@ -971,11 +980,12 @@ - (void)submitPendingMessages { } } -- (void)submitMessageWithText:(NSString *)text { +- (void)submitMessageWithText:(NSString *)text andPhotos:(NSArray *)photos { BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init]; message.text = text; [message setStatus:BITFeedbackMessageStatusSendPending]; - [message setToken:[self uuidAsLowerCaseAndShortened]]; + [message setToken:[self uuidAsLowerCaseAndShortened]]; + [message setPhotos:photos]; [message setUserMessage:YES]; [_feedbackList addObject:message]; diff --git a/Classes/BITFeedbackManagerPrivate.h b/Classes/BITFeedbackManagerPrivate.h index dd5f03a1..9748b0dc 100644 --- a/Classes/BITFeedbackManagerPrivate.h +++ b/Classes/BITFeedbackManagerPrivate.h @@ -65,7 +65,7 @@ - (NSUInteger)numberOfMessages; - (BITFeedbackMessage *)messageAtIndex:(NSUInteger)index; -- (void)submitMessageWithText:(NSString *)text; +- (void)submitMessageWithText:(NSString *)text andPhotos:(NSArray *)photos; - (void)submitPendingMessages; // Returns YES if manual user data can be entered, required or optional diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index c938e217..78bc124f 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -69,6 +69,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { @property (nonatomic, copy) NSDate *date; @property (nonatomic, copy) NSNumber *id; @property (nonatomic, copy) NSString *token; +@property (nonatomic, strong) NSArray *photos; @property (nonatomic) BITFeedbackMessageStatus status; @property (nonatomic) BOOL userMessage; diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index a5b0e0d8..b0860b6b 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -42,6 +42,7 @@ - (id) init { _email = nil; _date = [[NSDate alloc] init]; _token = nil; + _photos = nil; _id = [[NSNumber alloc] initWithInteger:0]; _status = BITFeedbackMessageStatusSendPending; _userMessage = NO; @@ -59,6 +60,7 @@ - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:self.email forKey:@"email"]; [encoder encodeObject:self.date forKey:@"date"]; [encoder encodeObject:self.id forKey:@"id"]; + [encoder encodeObject:self.photos forKey:@"photos"]; [encoder encodeInteger:self.status forKey:@"status"]; [encoder encodeBool:self.userMessage forKey:@"userMessage"]; [encoder encodeObject:self.token forKey:@"token"]; @@ -72,6 +74,7 @@ - (id)initWithCoder:(NSCoder *)decoder { self.email = [decoder decodeObjectForKey:@"email"]; self.date = [decoder decodeObjectForKey:@"date"]; self.id = [decoder decodeObjectForKey:@"id"]; + self.photos = [decoder decodeObjectForKey:@"photos"]; self.status = (BITFeedbackMessageStatus)[decoder decodeIntegerForKey:@"status"]; self.userMessage = [decoder decodeBoolForKey:@"userMessage"]; self.token = [decoder decodeObjectForKey:@"token"]; diff --git a/Classes/BITHockeyAppClient.h b/Classes/BITHockeyAppClient.h index a8d5fdbd..617b9b39 100644 --- a/Classes/BITHockeyAppClient.h +++ b/Classes/BITHockeyAppClient.h @@ -118,8 +118,8 @@ #pragma mark - Helpers /** - * create a post body from the given value, key and boundary - * c/p from HockeyBaseManager + * create a post body from the given value, key and boundary. This is a convenience call to + * dataWithPostValue:forKey:contentType:boundary and aimed at NSString-content. * * @param value - * @param key - @@ -128,4 +128,16 @@ * @return NSData instance configured to be attached on a (post) URLRequest */ + (NSData *)dataWithPostValue:(NSString *)value forKey:(NSString *)key boundary:(NSString *) boundary; + +/** + * create a post body from the given value, key and boundary and content type. + * + * @param value - + * @param key - + * @param boundary - + * + * @return NSData instance configured to be attached on a (post) URLRequest + */ ++ (NSData *)dataWithPostValue:(NSData *)value forKey:(NSString *)key contentType:(NSString *)contentType boundary:(NSString *) boundary; + @end diff --git a/Classes/BITHockeyAppClient.m b/Classes/BITHockeyAppClient.m index 168162b4..442dbaf3 100644 --- a/Classes/BITHockeyAppClient.m +++ b/Classes/BITHockeyAppClient.m @@ -85,12 +85,16 @@ - (NSMutableURLRequest *) requestWithMethod:(NSString*) method } + (NSData *)dataWithPostValue:(NSString *)value forKey:(NSString *)key boundary:(NSString *) boundary { + return [self dataWithPostValue:[value dataUsingEncoding:NSUTF8StringEncoding] forKey:key contentType:@"text" boundary:boundary]; +} + ++ (NSData *)dataWithPostValue:(NSData *)value forKey:(NSString *)key contentType:(NSString *)contentType boundary:(NSString *) boundary { NSMutableData *postBody = [NSMutableData data]; [postBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\";\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; - [postBody appendData:[[NSString stringWithFormat:@"Content-Type: text\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; - [postBody appendData:[value dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:value]; [postBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; return postBody; From 4b359e7771c512df98601234e3363c198fb9f4ba Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Thu, 13 Feb 2014 11:32:54 +0100 Subject: [PATCH 007/206] + Fixes a warning --- Classes/BITFeedbackComposeViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index a33060af..c8b4e154 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -42,7 +42,7 @@ #import "BITHockeyHelper.h" -@interface BITFeedbackComposeViewController () { +@interface BITFeedbackComposeViewController () { UIStatusBarStyle _statusBarStyle; } From 38af43bdb598444a4b0773d66b17e26a31aedef2 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 17 Feb 2014 21:04:03 +0100 Subject: [PATCH 008/206] + Changing machines.. --- Classes/BITFeedbackComposeViewController.m | 25 +++--- Classes/BITFeedbackManager.m | 24 +++-- Classes/BITFeedbackManagerPrivate.h | 2 +- Classes/BITFeedbackMessage.h | 2 +- Classes/BITFeedbackMessage.m | 6 +- Classes/BITFeedbackMessageAttachment.h | 47 ++++++++++ Classes/BITFeedbackMessageAttachment.m | 97 +++++++++++++++++++++ Classes/BITImageAnnotationViewController.h | 15 ++++ Classes/BITImageAnnotationViewController.m | 57 ++++++++++++ Support/HockeySDK.xcodeproj/project.pbxproj | 16 +++- 10 files changed, 266 insertions(+), 25 deletions(-) create mode 100644 Classes/BITFeedbackMessageAttachment.h create mode 100644 Classes/BITFeedbackMessageAttachment.m create mode 100644 Classes/BITImageAnnotationViewController.h create mode 100644 Classes/BITImageAnnotationViewController.m diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index c8b4e154..f22b0237 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -34,6 +34,7 @@ #import "HockeySDKPrivate.h" #import "BITFeedbackManagerPrivate.h" +#import "BITFeedbackMessageAttachment.h" #import "BITFeedbackComposeViewController.h" #import "BITFeedbackUserDataViewController.h" @@ -160,8 +161,6 @@ - (void)viewDidLoad { self.contentViewContainer = [[UIView alloc] initWithFrame:self.view.bounds]; [self.view addSubview:self.contentViewContainer]; - - // message input textfield self.textView = [[UITextView alloc] initWithFrame:self.view.bounds]; self.textView.font = [UIFont systemFontOfSize:17]; @@ -185,15 +184,13 @@ - (void)viewDidLoad { self.textView.inputAccessoryView = self.textAccessoryView; + // This could be a subclass, yet self.photoScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero]; self.photoScrollView.scrollEnabled = YES; self.photoScrollView.bounces = YES; self.photoScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; - [self.contentViewContainer addSubview:self.photoScrollView]; - - } - (void)viewWillAppear:(BOOL)animated { @@ -271,9 +268,6 @@ -(void)refreshPhotoScrollview { if (!alreadySetup){ textViewFrame.size.width -= scrollViewWidth; - - // status bar? - scrollViewFrame = CGRectMake(CGRectGetMaxX(textViewFrame), self.view.frame.origin.y, scrollViewWidth, CGRectGetHeight(textViewFrame)); self.textView.frame = textViewFrame; self.photoScrollView.frame = scrollViewFrame; @@ -315,8 +309,6 @@ -(void)refreshPhotoScrollview { } [self.photoScrollView setContentSize:CGSizeMake(CGRectGetWidth(self.photoScrollView.frame), currentYOffset)]; - - } @@ -351,7 +343,16 @@ - (void)sendAction:(id)sender { NSString *text = self.textView.text; - [self.manager submitMessageWithText:text andPhotos:self.photos]; + // Create attachments from the photos. + + NSMutableArray *attachments = [NSMutableArray new]; + + for (UIImage *photo in self.photos){ + BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:UIImageJPEGRepresentation(photo, 0.7f) contentType:@"image/jpeg"]; + [attachments addObject:attachment]; + } + + [self.manager submitMessageWithText:text andAttachments:attachments]; [self dismissWithResult:BITFeedbackComposeResultSubmitted]; } @@ -392,7 +393,7 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { - + [picker dismissModalViewControllerAnimated:YES]; } #pragma mark - BITFeedbackUserDataDelegate diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 4344fab4..527e2d1b 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -34,6 +34,7 @@ #import "HockeySDKPrivate.h" #import "BITFeedbackManager.h" +#import "BITFeedbackMessageAttachment.h" #import "BITFeedbackManagerPrivate.h" #import "BITHockeyBaseManagerPrivate.h" @@ -697,6 +698,14 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { matchingSendInProgressOrInConflictMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]]; matchingSendInProgressOrInConflictMessage.id = messageID; matchingSendInProgressOrInConflictMessage.status = BITFeedbackMessageStatusRead; + NSArray *feedbackAttachments =[(NSDictionary *)objMessage objectForKey:@"attachments"]; + if (matchingSendInProgressOrInConflictMessage.attachments.count == feedbackAttachments.count) { + int attachmentIndex = 0; + for (BITFeedbackMessageAttachment* attachment in matchingSendInProgressOrInConflictMessage.attachments){ + attachment.id =feedbackAttachments[attachmentIndex][@"id"]; + attachmentIndex++; + } + } } else { if ([(NSDictionary *)objMessage objectForKey:@"clean_text"] || [(NSDictionary *)objMessage objectForKey:@"text"]) { BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init]; @@ -835,17 +844,18 @@ - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BIT [postBody appendData:[BITHockeyAppClient dataWithPostValue:self.userEmail forKey:@"email" boundary:boundary]]; } - [postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; NSInteger photoIndex = 0; - for (UIImage *image in message.photos){ - NSString *contentType = @"image/png"; - NSData* imageData = UIImagePNGRepresentation(image); - [postBody appendData:[BITHockeyAppClient dataWithPostValue:imageData forKey:[NSString stringWithFormat:@"attachment%ld", (long)photoIndex] contentType:contentType boundary:boundary]]; + for (BITFeedbackMessageAttachment *attachment in message.attachments){ + NSString *key = [NSString stringWithFormat:@"attachment%ld", (long)photoIndex]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.data forKey:key contentType:attachment.contentType boundary:boundary]]; photoIndex++; } + [postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + [request setHTTPBody:postBody]; } @@ -980,12 +990,12 @@ - (void)submitPendingMessages { } } -- (void)submitMessageWithText:(NSString *)text andPhotos:(NSArray *)photos { +- (void)submitMessageWithText:(NSString *)text andAttachments:(NSArray *)attachments { BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init]; message.text = text; [message setStatus:BITFeedbackMessageStatusSendPending]; [message setToken:[self uuidAsLowerCaseAndShortened]]; - [message setPhotos:photos]; + [message setAttachments:attachments]; [message setUserMessage:YES]; [_feedbackList addObject:message]; diff --git a/Classes/BITFeedbackManagerPrivate.h b/Classes/BITFeedbackManagerPrivate.h index 9748b0dc..4deddcbd 100644 --- a/Classes/BITFeedbackManagerPrivate.h +++ b/Classes/BITFeedbackManagerPrivate.h @@ -65,7 +65,7 @@ - (NSUInteger)numberOfMessages; - (BITFeedbackMessage *)messageAtIndex:(NSUInteger)index; -- (void)submitMessageWithText:(NSString *)text andPhotos:(NSArray *)photos; +- (void)submitMessageWithText:(NSString *)text andAttachments:(NSArray *)photos; - (void)submitPendingMessages; // Returns YES if manual user data can be entered, required or optional diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index 78bc124f..fe736d00 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -69,7 +69,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { @property (nonatomic, copy) NSDate *date; @property (nonatomic, copy) NSNumber *id; @property (nonatomic, copy) NSString *token; -@property (nonatomic, strong) NSArray *photos; +@property (nonatomic, strong) NSArray *attachments; @property (nonatomic) BITFeedbackMessageStatus status; @property (nonatomic) BOOL userMessage; diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index b0860b6b..7d9fed37 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -42,7 +42,7 @@ - (id) init { _email = nil; _date = [[NSDate alloc] init]; _token = nil; - _photos = nil; + _attachments = nil; _id = [[NSNumber alloc] initWithInteger:0]; _status = BITFeedbackMessageStatusSendPending; _userMessage = NO; @@ -60,7 +60,7 @@ - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:self.email forKey:@"email"]; [encoder encodeObject:self.date forKey:@"date"]; [encoder encodeObject:self.id forKey:@"id"]; - [encoder encodeObject:self.photos forKey:@"photos"]; + [encoder encodeObject:self.attachments forKey:@"attachments"]; [encoder encodeInteger:self.status forKey:@"status"]; [encoder encodeBool:self.userMessage forKey:@"userMessage"]; [encoder encodeObject:self.token forKey:@"token"]; @@ -74,7 +74,7 @@ - (id)initWithCoder:(NSCoder *)decoder { self.email = [decoder decodeObjectForKey:@"email"]; self.date = [decoder decodeObjectForKey:@"date"]; self.id = [decoder decodeObjectForKey:@"id"]; - self.photos = [decoder decodeObjectForKey:@"photos"]; + self.attachments = [decoder decodeObjectForKey:@"attachments"]; self.status = (BITFeedbackMessageStatus)[decoder decodeIntegerForKey:@"status"]; self.userMessage = [decoder decodeBoolForKey:@"userMessage"]; self.token = [decoder decodeObjectForKey:@"token"]; diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h new file mode 100644 index 00000000..082d9754 --- /dev/null +++ b/Classes/BITFeedbackMessageAttachment.h @@ -0,0 +1,47 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 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 + +@interface BITFeedbackMessageAttachment : NSObject + +@property (nonatomic, copy) NSString *filename; +@property (nonatomic, copy) NSNumber *id; +@property (nonatomic, copy) NSString *contentType; + +@property (readonly) UIImage *imageRepresentation; +@property (readonly) NSData *data; + + ++ (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType; + +- (UIImage *)thumbnailWithSize:(CGSize)size; + +@end diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m new file mode 100644 index 00000000..f7d8ab90 --- /dev/null +++ b/Classes/BITFeedbackMessageAttachment.m @@ -0,0 +1,97 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITFeedbackMessageAttachment.h" +#import "BITHockeyHelper.h" +#import "HockeySDKPrivate.h" + +@interface BITFeedbackMessageAttachment() + +@property (nonatomic, strong) NSData *data; +@property (nonatomic, strong) NSMutableDictionary *thumbnailRepresentations; + +@end + +@implementation BITFeedbackMessageAttachment + ++ (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType { + BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; + newAttachment.contentType = contentType; + newAttachment.data = data; + return newAttachment; +} + +-(id)init { + self = [super init]; + if (self){ + self.thumbnailRepresentations = [NSMutableDictionary new]; + } + return self; +} + +#pragma mark NSCoding + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.contentType forKey:@"contentType"]; + [aCoder encodeObject:self.filename forKey:@"filename"]; + +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + + if (self){ + self.contentType = [aDecoder decodeObjectForKey:@"contentType"]; + self.filename = [aDecoder decodeObjectForKey:@"filename"]; + self.thumbnailRepresentations = [NSMutableDictionary new]; + } + + return self; +} + +- (UIImage *)imageRepresentation { + if ([self.contentType rangeOfString:@"image"].location != NSNotFound){ + return [UIImage imageWithData:self.data]; + } else { + // return bit_imageNamed(@"feedbackActiviy.png", BITHOCKEYSDK_BUNDLE); + } +} + +- (UIImage *)thumbnailWithSize:(CGSize)size { + id cacheKey = [NSValue valueWithCGSize:size]; + +// if (!self.thumbnailRepresentations[cacheKey]){ +// UIImage *thumbnail =bit_imageToFitSize(self.imageRepresentation, size, YES); +// [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; +// } + + return self.thumbnailRepresentations[cacheKey]; +} + +@end diff --git a/Classes/BITImageAnnotationViewController.h b/Classes/BITImageAnnotationViewController.h new file mode 100644 index 00000000..6321c9bd --- /dev/null +++ b/Classes/BITImageAnnotationViewController.h @@ -0,0 +1,15 @@ +// +// BITImageAnnotationViewController.h +// HockeySDK +// +// Created by Moritz Haarmann on 14.02.14. +// +// + +#import + +@interface BITImageAnnotationViewController : UIViewController + +@property (nonatomic, strong) UIImage *image; + +@end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m new file mode 100644 index 00000000..e6101ab5 --- /dev/null +++ b/Classes/BITImageAnnotationViewController.m @@ -0,0 +1,57 @@ +// +// BITImageAnnotationViewController.m +// HockeySDK +// +// Created by Moritz Haarmann on 14.02.14. +// +// + +#import "BITImageAnnotationViewController.h" + +@interface BITImageAnnotationViewController () + +@property (nonatomic, strong) UIImageView *imageView; +@property (nonatomic, strong) UISegmentedControl *editingControls; +@property (nonatomic, strong) NSMutableArray *layers; + +@end + +@implementation BITImageAnnotationViewController + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Custom initialization + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Arrow", @"Rect", @"Blur"]]; + + self.navigationItem.titleView = self.editingControls; + + [self.editingControls addTarget:self action:@selector(editingAction:) forControlEvents:UIControlEventTouchUpInside]; + + self.imageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; + + self.imageView.image = self.image; + self.imageView.contentMode = UIViewContentModeScaleAspectFit; + // Do any additional setup after loading the view. +} + +-(void)editingAction:(id)sender { + +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +@end diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index c4c886eb..50cca02c 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -133,6 +133,9 @@ 1EFF03E517F2485500A5F13C /* BITCrashManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */; }; 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; }; 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; }; + 97F0FA0118AE375E00EF50AA /* BITImageAnnotationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */; }; + 97F0FA0418AE5AED00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; + 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E40E0B0917DA19DC005E38C1 /* BITHockeyAppClientTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E40E0B0817DA19DC005E38C1 /* BITHockeyAppClientTests.m */; }; E40E0B0C17DA1AFF005E38C1 /* BITHockeyAppClient.h in Headers */ = {isa = PBXBuildFile; fileRef = E40E0B0A17DA1AFF005E38C1 /* BITHockeyAppClient.h */; }; @@ -293,6 +296,10 @@ 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashManagerTests.m; sourceTree = ""; }; 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = ""; }; 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconCamera@2x.png"; sourceTree = ""; }; + 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotationViewController.h; sourceTree = ""; }; + 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotationViewController.m; sourceTree = ""; }; + 97F0FA0218AE5AED00EF50AA /* BITFeedbackMessageAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackMessageAttachment.h; sourceTree = ""; }; + 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITFeedbackMessageAttachment.m; sourceTree = ""; }; BEE0207C16C5107E004426EA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/HockeySDK.strings; sourceTree = ""; }; E400561D148D79B500EB22B9 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackManagerDelegate.h; sourceTree = ""; }; @@ -444,6 +451,8 @@ children = ( 1E49A4361612223B00463151 /* BITFeedbackMessage.h */, 1E49A4371612223B00463151 /* BITFeedbackMessage.m */, + 97F0FA0218AE5AED00EF50AA /* BITFeedbackMessageAttachment.h */, + 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */, 1E49A42D1612223B00463151 /* BITFeedbackComposeViewController.h */, 1E49A42E1612223B00463151 /* BITFeedbackComposeViewController.m */, 1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */, @@ -459,6 +468,8 @@ 1E49A4341612223B00463151 /* BITFeedbackManager.m */, E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */, 1E49A4351612223B00463151 /* BITFeedbackManagerPrivate.h */, + 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */, + 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */, ); name = Feedback; sourceTree = ""; @@ -887,11 +898,12 @@ 1E49A4601612223B00463151 /* BITFeedbackUserDataViewController.m in Sources */, 1E49A4701612226D00463151 /* BITAppVersionMetaInfo.m in Sources */, 1E49A4761612226D00463151 /* BITUpdateManager.m in Sources */, + 1E49A4C1161222B900463151 /* BITHockeyHelper.m in Sources */, 1E49A4821612226D00463151 /* BITUpdateViewController.m in Sources */, E4B4DB7E17B435550099C67F /* BITAuthenticationViewController.m in Sources */, 1E49A4B2161222B900463151 /* BITHockeyBaseManager.m in Sources */, 1E49A4BB161222B900463151 /* BITHockeyBaseViewController.m in Sources */, - 1E49A4C1161222B900463151 /* BITHockeyHelper.m in Sources */, + 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */, 1E49A4C7161222B900463151 /* BITAppStoreHeader.m in Sources */, 1E49A4CD161222B900463151 /* BITStoreButton.m in Sources */, 1E49A4D3161222B900463151 /* BITWebTableViewCell.m in Sources */, @@ -910,6 +922,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 97F0FA0118AE375E00EF50AA /* BITImageAnnotationViewController.m in Sources */, + 97F0FA0418AE5AED00EF50AA /* BITFeedbackMessageAttachment.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 02dc42390ac68f30bcd5495d54ecc47b5dd39960 Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Mon, 17 Feb 2014 22:10:39 +0100 Subject: [PATCH 009/206] + Fixes File upload. --- Classes/BITHockeyAppClient.m | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Classes/BITHockeyAppClient.m b/Classes/BITHockeyAppClient.m index 442dbaf3..c3db96ae 100644 --- a/Classes/BITHockeyAppClient.m +++ b/Classes/BITHockeyAppClient.m @@ -92,8 +92,16 @@ + (NSData *)dataWithPostValue:(NSData *)value forKey:(NSString *)key contentType NSMutableData *postBody = [NSMutableData data]; [postBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\";\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; - [postBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]]; + + // There's certainly a better way to check if we are supposed to send binary data here. + if ([contentType rangeOfString:@"text"].location == NSNotFound){ + [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"image.jpg\"\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:[[NSString stringWithFormat:@"Content-Transfer-Encoding: binary\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + } else { + [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]]; + } [postBody appendData:value]; [postBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; From da2c8268d992aa3b5eea02e530d85eee6ea3d6cf Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Mon, 17 Feb 2014 23:35:04 +0100 Subject: [PATCH 010/206] + File persistence for Attachments --- Classes/BITFeedbackMessageAttachment.h | 2 +- Classes/BITFeedbackMessageAttachment.m | 66 +++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index 082d9754..053b1192 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -35,9 +35,9 @@ @property (nonatomic, copy) NSString *filename; @property (nonatomic, copy) NSNumber *id; @property (nonatomic, copy) NSString *contentType; +@property (nonatomic, readonly) NSData *data; @property (readonly) UIImage *imageRepresentation; -@property (readonly) NSData *data; + (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType; diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index f7d8ab90..95a6e312 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -31,11 +31,12 @@ #import "BITHockeyHelper.h" #import "HockeySDKPrivate.h" +#define kCacheFolderName @"hockey_attachments" + @interface BITFeedbackMessageAttachment() -@property (nonatomic, strong) NSData *data; @property (nonatomic, strong) NSMutableDictionary *thumbnailRepresentations; - +@property (nonatomic, strong) NSData *internalData; @end @implementation BITFeedbackMessageAttachment @@ -55,6 +56,24 @@ -(id)init { return self; } +-(void)setData:(NSData *)data { + self->_internalData = data; + self.filename = [self createFilename]; + [self->_internalData writeToFile:self.filename atomically:NO]; +} + +-(NSData *)data { + if (!self->_internalData && self.filename){ + self.internalData = [NSData dataWithContentsOfFile:self.filename]; + } + + if (self.internalData){ + return self.internalData; + } + + return nil; +} + #pragma mark NSCoding - (void)encodeWithCoder:(NSCoder *)aCoder { @@ -75,23 +94,56 @@ - (id)initWithCoder:(NSCoder *)aDecoder { return self; } +#pragma mark - Thubmnails / Image Representation + - (UIImage *)imageRepresentation { if ([self.contentType rangeOfString:@"image"].location != NSNotFound){ + NSData *imageData = self.data; return [UIImage imageWithData:self.data]; } else { - // return bit_imageNamed(@"feedbackActiviy.png", BITHOCKEYSDK_BUNDLE); + return bit_imageNamed(@"feedbackActiviy.png", BITHOCKEYSDK_BUNDLE); // TODO add another placeholder. } } - (UIImage *)thumbnailWithSize:(CGSize)size { id cacheKey = [NSValue valueWithCGSize:size]; -// if (!self.thumbnailRepresentations[cacheKey]){ -// UIImage *thumbnail =bit_imageToFitSize(self.imageRepresentation, size, YES); -// [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; -// } + if (!self.thumbnailRepresentations[cacheKey]){ + UIImage *image = self.imageRepresentation; + UIImage *thumbnail = bit_imageToFitSize(image, size, NO); + if (thumbnail){ + [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; + } + } return self.thumbnailRepresentations[cacheKey]; } +#pragma mark - Persistence Helpers + +- (NSString *)createFilename { + NSArray* cachePathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString* cachePath = [cachePathArray lastObject]; + cachePath = [cachePath stringByAppendingPathComponent:kCacheFolderName]; + + BOOL isDirectory; + + if (![[NSFileManager defaultManager] fileExistsAtPath:cachePath isDirectory:&isDirectory]){ + [[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:nil]; + } + + NSString *uniqueString = [BITFeedbackMessageAttachment GetUUID]; + cachePath = [cachePath stringByAppendingPathComponent:uniqueString]; + + return cachePath; +} + ++ (NSString *)GetUUID +{ + CFUUIDRef theUUID = CFUUIDCreate(NULL); + CFStringRef string = CFUUIDCreateString(NULL, theUUID); + CFRelease(theUUID); + return (__bridge NSString *)string; +} + @end From 6b09e75cd6e7ef520b6321247a9a23ee9db344cc Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Mon, 17 Feb 2014 23:49:49 +0100 Subject: [PATCH 011/206] + Original Filename of picked image is used as attachment name. --- Classes/BITFeedbackComposeViewController.m | 72 +++++++++++----------- Classes/BITFeedbackManager.m | 2 +- Classes/BITFeedbackMessageAttachment.h | 2 +- Classes/BITFeedbackMessageAttachment.m | 4 ++ Classes/BITHockeyAppClient.h | 3 +- Classes/BITHockeyAppClient.m | 9 +-- 6 files changed, 47 insertions(+), 45 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index f22b0237..d492f393 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -50,12 +50,12 @@ @interface BITFeedbackComposeViewController () self.photoScrollViewImageViews.count){ - NSInteger numberOfViewsToCreate = self.photos.count - self.photoScrollViewImageViews.count; + if (self.attachments.count > self.attachmentScrollViewImageViews.count){ + NSInteger numberOfViewsToCreate = self.attachments.count - self.attachmentScrollViewImageViews.count; for (int i = 0;i -@property (nonatomic, copy) NSString *filename; @property (nonatomic, copy) NSNumber *id; +@property (nonatomic, copy) NSString *originalFilename; @property (nonatomic, copy) NSString *contentType; @property (nonatomic, readonly) NSData *data; diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 95a6e312..99865897 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -37,6 +37,8 @@ @interface BITFeedbackMessageAttachment() @property (nonatomic, strong) NSMutableDictionary *thumbnailRepresentations; @property (nonatomic, strong) NSData *internalData; +@property (nonatomic, copy) NSString *filename; + @end @implementation BITFeedbackMessageAttachment @@ -79,6 +81,7 @@ -(NSData *)data { - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.contentType forKey:@"contentType"]; [aCoder encodeObject:self.filename forKey:@"filename"]; + [aCoder encodeObject:self.originalFilename forKey:@"originalFilename"]; } @@ -89,6 +92,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder { self.contentType = [aDecoder decodeObjectForKey:@"contentType"]; self.filename = [aDecoder decodeObjectForKey:@"filename"]; self.thumbnailRepresentations = [NSMutableDictionary new]; + self.originalFilename = [aDecoder decodeObjectForKey:@"originalFilename"]; } return self; diff --git a/Classes/BITHockeyAppClient.h b/Classes/BITHockeyAppClient.h index 617b9b39..8d2fce15 100644 --- a/Classes/BITHockeyAppClient.h +++ b/Classes/BITHockeyAppClient.h @@ -138,6 +138,5 @@ * * @return NSData instance configured to be attached on a (post) URLRequest */ -+ (NSData *)dataWithPostValue:(NSData *)value forKey:(NSString *)key contentType:(NSString *)contentType boundary:(NSString *) boundary; - ++ (NSData *)dataWithPostValue:(NSData *)value forKey:(NSString *)key contentType:(NSString *)contentType boundary:(NSString *) boundary filename:(NSString *)filename; @end diff --git a/Classes/BITHockeyAppClient.m b/Classes/BITHockeyAppClient.m index c3db96ae..39944284 100644 --- a/Classes/BITHockeyAppClient.m +++ b/Classes/BITHockeyAppClient.m @@ -85,23 +85,24 @@ - (NSMutableURLRequest *) requestWithMethod:(NSString*) method } + (NSData *)dataWithPostValue:(NSString *)value forKey:(NSString *)key boundary:(NSString *) boundary { - return [self dataWithPostValue:[value dataUsingEncoding:NSUTF8StringEncoding] forKey:key contentType:@"text" boundary:boundary]; + return [self dataWithPostValue:[value dataUsingEncoding:NSUTF8StringEncoding] forKey:key contentType:@"text" boundary:boundary filename:nil]; } -+ (NSData *)dataWithPostValue:(NSData *)value forKey:(NSString *)key contentType:(NSString *)contentType boundary:(NSString *) boundary { ++ (NSData *)dataWithPostValue:(NSData *)value forKey:(NSString *)key contentType:(NSString *)contentType boundary:(NSString *) boundary filename:(NSString *)filename { NSMutableData *postBody = [NSMutableData data]; [postBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; // There's certainly a better way to check if we are supposed to send binary data here. - if ([contentType rangeOfString:@"text"].location == NSNotFound){ - [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"image.jpg\"\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; + if (filename){ + [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, filename] dataUsingEncoding:NSUTF8StringEncoding]]; [postBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]]; [postBody appendData:[[NSString stringWithFormat:@"Content-Transfer-Encoding: binary\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; } else { [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; [postBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]]; } + [postBody appendData:value]; [postBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; From 2146788f7b03ad69098958e99896c92fba7fcf7d Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 18 Feb 2014 10:56:40 +0100 Subject: [PATCH 012/206] Added Delete/Edit ActionSheet Fixed a Bug where the Feedback cannot be sent if no text is entered. --- Classes/BITFeedbackComposeViewController.m | 70 ++++++++++++++++------ 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index d492f393..1ab8c557 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -43,7 +43,7 @@ #import "BITHockeyHelper.h" -@interface BITFeedbackComposeViewController () { +@interface BITFeedbackComposeViewController () { UIStatusBarStyle _statusBarStyle; } @@ -58,6 +58,7 @@ @interface BITFeedbackComposeViewController () self.attachmentScrollViewImageViews.count){ NSInteger numberOfViewsToCreate = self.attachments.count - self.attachmentScrollViewImageViews.count; for (int i = 0;i Date: Tue, 18 Feb 2014 11:56:07 +0100 Subject: [PATCH 013/206] + Improved Persistence handling. --- Classes/BITFeedbackComposeViewController.m | 1 + Classes/BITFeedbackManager.m | 2 ++ Classes/BITFeedbackMessage.h | 7 +++++++ Classes/BITFeedbackMessage.m | 9 +++++++++ Classes/BITFeedbackMessageAttachment.h | 2 ++ Classes/BITFeedbackMessageAttachment.m | 8 ++++++++ Support/HockeySDK.xcodeproj/project.pbxproj | 2 -- 7 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 1ab8c557..3d1bf6a1 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -458,6 +458,7 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn if (self.selectedAttachmentIndex != NSNotFound){ BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; + [attachment deleteContents]; // mandatory call to delete the files associatd. [self.attachments removeObject:attachment]; } diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 75cf6a48..58fb3851 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -572,6 +572,8 @@ - (void)updateLastMessageID { - (BOOL)deleteMessageAtIndex:(NSUInteger)index { if (_feedbackList && [_feedbackList count] > index && [_feedbackList objectAtIndex:index]) { + BITFeedbackMessage *message = _feedbackList[index]; + [message deleteContents]; [_feedbackList removeObjectAtIndex:index]; [self saveMessages]; diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index fe736d00..baa6d6fd 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -73,4 +73,11 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { @property (nonatomic) BITFeedbackMessageStatus status; @property (nonatomic) BOOL userMessage; +/** + * This method must be called before a feedback message is deleted. It handles the + * deletion of any data stored on the device in association with the feedback message. + */ +-(void)deleteContents; + + @end diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index 7d9fed37..eb442896 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -28,6 +28,7 @@ #import "BITFeedbackMessage.h" +#import "BITFeedbackMessageAttachment.h" @implementation BITFeedbackMessage @@ -82,4 +83,12 @@ - (id)initWithCoder:(NSCoder *)decoder { return self; } +#pragma mark - Deletion + +-(void)deleteContents { + for (BITFeedbackMessageAttachment *attachment in self.attachments){ + [attachment deleteContents]; + } +} + @end diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index 595bf51c..77a4b92b 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -44,4 +44,6 @@ - (UIImage *)thumbnailWithSize:(CGSize)size; +- (void)deleteContents; + @end diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 99865897..ab8f8d8f 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -150,4 +150,12 @@ + (NSString *)GetUUID return (__bridge NSString *)string; } +- (void)deleteContents { + if (self.filename){ + [[NSFileManager defaultManager] removeItemAtPath:self.filename error:nil]; + self.filename = nil; + } +} + + @end diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 50cca02c..d2e9b086 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -134,7 +134,6 @@ 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; }; 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; }; 97F0FA0118AE375E00EF50AA /* BITImageAnnotationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */; }; - 97F0FA0418AE5AED00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E40E0B0917DA19DC005E38C1 /* BITHockeyAppClientTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E40E0B0817DA19DC005E38C1 /* BITHockeyAppClientTests.m */; }; @@ -923,7 +922,6 @@ buildActionMask = 2147483647; files = ( 97F0FA0118AE375E00EF50AA /* BITImageAnnotationViewController.m in Sources */, - 97F0FA0418AE5AED00EF50AA /* BITFeedbackMessageAttachment.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 5a88cab36eff7ae130bd4fe96b79a00455ce0636 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 19 Feb 2014 23:38:54 +0100 Subject: [PATCH 014/206] Fix referencing wrong property name --- Classes/BITCrashManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 5bad86eb..b958cc89 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -281,7 +281,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { /** Indicates if the app was killed while being in foreground from the iOS - If `enableDectionAppKillWhileInForeground` is enabled, use this on startup to check if the + If `enableAppNotTerminatingCleanlyDetection` is enabled, use this on startup to check if the app starts the first time after it was killed by iOS in the previous session. This can happen if it consumed too much memory or the watchdog killed the app because it From 9c957b168803263612f4ea64163d2672c3798973 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Feb 2014 09:54:21 +0100 Subject: [PATCH 015/206] + Fixes Potentail crash in helper for resizing images. --- Classes/BITHockeyHelper.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 11b29ad8..3a9cdca7 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -346,6 +346,11 @@ BOOL bit_hasAlpha(UIImage *inputImage) { #pragma mark UIImage helpers UIImage *bit_imageToFitSize(UIImage *inputImage, CGSize fitSize, BOOL honorScaleFactor) { + + if (!inputImage){ + return nil; + } + float imageScaleFactor = 1.0; if (honorScaleFactor) { if ([inputImage respondsToSelector:@selector(scale)]) { From 77b68f4508c20ee9ae9f59d97bd8477d23740b61 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Feb 2014 10:34:51 +0100 Subject: [PATCH 016/206] + List View Display of attachments. --- Classes/BITFeedbackListViewCell.h | 2 + Classes/BITFeedbackListViewCell.m | 69 +++++++++++++++++++++++-- Classes/BITFeedbackListViewController.m | 1 + 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/Classes/BITFeedbackListViewCell.h b/Classes/BITFeedbackListViewCell.h index 1a71800a..4842cb1a 100644 --- a/Classes/BITFeedbackListViewCell.h +++ b/Classes/BITFeedbackListViewCell.h @@ -71,4 +71,6 @@ typedef NS_ENUM(NSUInteger, BITFeedbackListViewCellBackgroundStyle) { + (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width; +- (void)setAttachments:(NSArray *)attachments; + @end diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 8c8e90bd..6bf7ac62 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -29,6 +29,7 @@ #import "BITFeedbackListViewCell.h" #import "HockeySDKPrivate.h" +#import "BITFeedbackMessageAttachment.h" #define BACKGROUNDCOLOR_DEFAULT BIT_RGBCOLOR(245, 245, 245) #define BACKGROUNDCOLOR_ALTERNATE BIT_RGBCOLOR(235, 235, 235) @@ -54,6 +55,9 @@ #define LABEL_TEXT_Y 25 +#define ATTACHMENT_SIZE 45 + + @interface BITFeedbackListViewCell () @property (nonatomic, strong) NSDateFormatter *dateFormatter; @@ -61,6 +65,8 @@ @interface BITFeedbackListViewCell () @property (nonatomic, strong) UILabel *labelTitle; +@property (nonatomic, strong) NSMutableArray *attachmentViews; + @end @@ -96,6 +102,8 @@ - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reus self.labelText.numberOfLines = 0; self.labelText.textAlignment = kBITTextLabelAlignmentLeft; self.labelText.dataDetectorTypes = UIDataDetectorTypeAll; + + self.attachmentViews = [NSMutableArray new]; } return self; } @@ -135,6 +143,19 @@ - (BOOL)isSameDayWithDate1:(NSDate*)date1 date2:(NSDate*)date2 { #pragma mark - Layout + (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width { + + CGFloat baseHeight = [self heightForTextInRowWithMessage:message tableViewWidth:width]; + + CGFloat attachmentsPerRow = floorf(width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); + + CGFloat calculatedHeight = baseHeight + (FRAME_TOP_BORDER + ATTACHMENT_SIZE) * ceil(message.attachments.count/attachmentsPerRow); + + return ceil(calculatedHeight); +} + + + ++ (CGFloat) heightForTextInRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width { CGFloat calculatedHeight; #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 @@ -143,7 +164,11 @@ + (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:TEXT_FONTSIZE]} context:nil]; - calculatedHeight = calculatedRect.size.height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER; + calculatedHeight = calculatedRect.size.height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER; + + // added to make space for the images. + + } else { #endif #pragma clang diagnostic push @@ -151,6 +176,7 @@ + (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth calculatedHeight = [message.text sizeWithFont:[UIFont systemFontOfSize:TEXT_FONTSIZE] constrainedToSize:CGSizeMake(width - (2 * FRAME_SIDE_BORDER), CGFLOAT_MAX) ].height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER; + #pragma clang diagnostic pop #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 } @@ -159,6 +185,22 @@ + (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth return ceil(calculatedHeight); } +- (void)setAttachments:(NSArray *)attachments { + for (UIView *view in self.attachmentViews){ + [view removeFromSuperview]; + } + + [self.attachmentViews removeAllObjects]; + + for (BITFeedbackMessageAttachment *attachment in attachments){ + UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectZero]; + imageView.image = [attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)]; + [self.attachmentViews addObject:imageView]; + [self addSubview:imageView]; + } +} + + - (void)layoutSubviews { UIView *accessoryViewBackground = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)]; accessoryViewBackground.autoresizingMask = UIViewAutoresizingFlexibleHeight; @@ -208,13 +250,32 @@ - (void)layoutSubviews { // text [self.labelText setText:_message.text]; - CGSize size = CGSizeMake(self.frame.size.width - (2 * FRAME_SIDE_BORDER), - [[self class] heightForRowWithMessage:_message tableViewWidth:self.frame.size.width] - LABEL_TEXT_Y - FRAME_BOTTOM_BORDER); + CGSize sizeForTextLabel = CGSizeMake(self.frame.size.width - (2 * FRAME_SIDE_BORDER), + [[self class] heightForTextInRowWithMessage:_message tableViewWidth:self.frame.size.width] - LABEL_TEXT_Y - FRAME_BOTTOM_BORDER); - [self.labelText setFrame:CGRectMake(FRAME_SIDE_BORDER, LABEL_TEXT_Y, size.width, size.height)]; + [self.labelText setFrame:CGRectMake(FRAME_SIDE_BORDER, LABEL_TEXT_Y, sizeForTextLabel.width, sizeForTextLabel.height)]; [self addSubview:self.labelText]; + CGFloat baseOffsetOfText = CGRectGetMaxY(self.labelText.frame); + + + int i = 0; + + CGFloat attachmentsPerRow = ceilf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); + + for ( UIImageView *imageView in self.attachmentViews){ + if ( !_message.userMessage){ + imageView.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER * ATTACHMENT_SIZE) * i , floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + } else { + imageView.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER * ATTACHMENT_SIZE) * (i) ), floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + + + }i++; + + } + + [super layoutSubviews]; } diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index 812634ca..ff59a222 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -628,6 +628,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell.message = message; cell.labelText.delegate = self; cell.labelText.userInteractionEnabled = YES; + [cell setAttachments:message.attachments]; if ( [self.manager isPreiOS7Environment] || From 1c120b95886d09c935c367f5dd7a910cec5e069f Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Feb 2014 10:36:23 +0100 Subject: [PATCH 017/206] + Removes a warning. --- Classes/BITFeedbackMessageAttachment.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index ab8f8d8f..9184a723 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -102,7 +102,6 @@ - (id)initWithCoder:(NSCoder *)aDecoder { - (UIImage *)imageRepresentation { if ([self.contentType rangeOfString:@"image"].location != NSNotFound){ - NSData *imageData = self.data; return [UIImage imageWithData:self.data]; } else { return bit_imageNamed(@"feedbackActiviy.png", BITHOCKEYSDK_BUNDLE); // TODO add another placeholder. @@ -116,7 +115,7 @@ - (UIImage *)thumbnailWithSize:(CGSize)size { UIImage *image = self.imageRepresentation; UIImage *thumbnail = bit_imageToFitSize(image, size, NO); if (thumbnail){ - [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; + [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; } } From 3f7e6904a64bcef6dd3ef0a8c9ab6c6a47c66543 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Feb 2014 10:36:49 +0100 Subject: [PATCH 018/206] + Attachment handling --- Classes/BITFeedbackManager.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 58fb3851..8eac1761 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -709,7 +709,7 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { } } } else { - if ([(NSDictionary *)objMessage objectForKey:@"clean_text"] || [(NSDictionary *)objMessage objectForKey:@"text"]) { + if ([(NSDictionary *)objMessage objectForKey:@"clean_text"] || [(NSDictionary *)objMessage objectForKey:@"text"] || [(NSDictionary *)objMessage objectForKey:@"attachments"]) { BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init]; message.text = [(NSDictionary *)objMessage objectForKey:@"clean_text"] ?: [(NSDictionary *)objMessage objectForKey:@"text"] ?: @""; message.name = [(NSDictionary *)objMessage objectForKey:@"name"] ?: @""; @@ -1018,6 +1018,9 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) } } + @end + + #endif /* HOCKEYSDK_FEATURE_FEEDBACK */ From 6cfc0ac2d2c1d40c9d30e48a238d8900183157b3 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Feb 2014 11:36:23 +0100 Subject: [PATCH 019/206] + Calculation problem. --- Classes/BITFeedbackListViewCell.m | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 6bf7ac62..7be19e9a 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -263,15 +263,16 @@ - (void)layoutSubviews { int i = 0; CGFloat attachmentsPerRow = ceilf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); - for ( UIImageView *imageView in self.attachmentViews){ if ( !_message.userMessage){ - imageView.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER * ATTACHMENT_SIZE) * i , floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + imageView.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * i , floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } else { - imageView.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER * ATTACHMENT_SIZE) * (i) ), floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); - - }i++; + imageView.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i) ), floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + + NSLog(@"ImageView %@", imageView); + } + i++; } From 21e5ca3b9db68411fc8ed1fdec3db9546d00ec8a Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Feb 2014 11:37:18 +0100 Subject: [PATCH 020/206] + prepareWithItems accepts UIImages now. --- Classes/BITFeedbackComposeViewController.h | 1 + Classes/BITFeedbackComposeViewController.m | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Classes/BITFeedbackComposeViewController.h b/Classes/BITFeedbackComposeViewController.h index 488e3004..5df188c1 100644 --- a/Classes/BITFeedbackComposeViewController.h +++ b/Classes/BITFeedbackComposeViewController.h @@ -70,6 +70,7 @@ The follwoing data object classes are currently supported: - NSString - NSURL + - UIImage These are automatically concatenated to one text string. diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 3d1bf6a1..b9ab0f15 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -95,6 +95,9 @@ - (void)prepareWithItems:(NSArray *)items { self.text = [(self.text ? self.text : @"") stringByAppendingFormat:@"%@%@", (self.text ? @" " : @""), item]; } else if ([item isKindOfClass:[NSURL class]]) { self.text = [(self.text ? self.text : @"") stringByAppendingFormat:@"%@%@", (self.text ? @" " : @""), [(NSURL *)item absoluteString]]; + } else if ([item isKindOfClass:[UIImage class]]) { + UIImage *image = item; + [self.attachments addObject:[BITFeedbackMessageAttachment attachmentWithData:UIImageJPEGRepresentation(image, 0.7f) contentType:@"image/jpeg"]]; } else { BITHockeyLog(@"Unknown item type %@", item); } @@ -221,6 +224,8 @@ - (void)viewWillAppear:(BOOL)animated { } [self updateBarButtonState]; + + } - (void)viewDidAppear:(BOOL)animated { @@ -236,6 +241,9 @@ - (void)viewDidAppear:(BOOL)animated { // Invoke delayed to fix iOS 7 iPad landscape bug, where this view will be moved if not called delayed [self.textView performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.0]; } + + [self refreshAttachmentScrollview]; + } - (void)viewWillDisappear:(BOOL)animated { @@ -264,7 +272,7 @@ -(void)refreshAttachmentScrollview { CGRect scrollViewFrame = self.attachmentScrollView.frame; - BOOL alreadySetup = CGRectGetWidth(scrollViewFrame) == scrollViewWidth; + BOOL alreadySetup = CGRectGetWidth(scrollViewFrame) > 0; if (!alreadySetup){ textViewFrame.size.width -= scrollViewWidth; @@ -468,6 +476,7 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn self.selectedAttachmentIndex = NSNotFound; } + @end #endif /* HOCKEYSDK_FEATURE_FEEDBACK */ From f1bb46e1b374bccea21c354855c4065772a83313 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Feb 2014 11:37:23 +0100 Subject: [PATCH 021/206] + More Cleanups --- Classes/BITFeedbackListViewCell.m | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 7be19e9a..c428d377 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -264,14 +264,13 @@ - (void)layoutSubviews { CGFloat attachmentsPerRow = ceilf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); for ( UIImageView *imageView in self.attachmentViews){ + if ( !_message.userMessage){ imageView.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * i , floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } else { - imageView.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i) ), floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); - - NSLog(@"ImageView %@", imageView); } + i++; } From 1ddf9d5eb81745cf6912a575e7cce9d705aba826 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Feb 2014 12:12:51 +0100 Subject: [PATCH 022/206] + Added Feedback collection methods. --- Classes/BITFeedbackManager.h | 34 +++++++++++++++++++++++++ Classes/BITFeedbackManager.m | 49 ++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 1f1543df..e4059d1d 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -59,6 +59,25 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { BITFeedbackUserDataElementRequired = 2 }; +/** + * Available modes for collecting automated feedback. + */ +typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { + /** + * No automatic feedback gathering. + */ + BITFeedbackObservationNone = 0, + /** + * Feedback compose form will open once a screenshot is taken. + */ + BITFeedbackObservationModeOnScreenshot = 1, + /** + * Feedback compose will open with a generated screenshot if the screen is tapped + * three fingers for three seconds. + */ + BITFeedbackObservationModeThreeFingersThreeSeconds = 2 +}; + @class BITFeedbackMessage; @protocol BITFeedbackManagerDelegate; @@ -241,6 +260,19 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { */ - (void)showFeedbackComposeView; +/** + Present the modal feedback compose message user interface with the items given. + All NSString-Content in the array will be concatenated and result in the message, + while all UIImage and NSData-instances will be turned into attachments. + */ +- (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items; + +/** + Present the modal feedback compose message user interface with a screenshot that is taken + at the time of calling this method. + */ +- (void)showFeedbackComposeViewWithGeneratedScreenshot; + /** Create an feedback compose view @@ -262,5 +294,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { */ - (BITFeedbackComposeViewController *)feedbackComposeViewController; +- (void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode; + @end diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 8eac1761..c79dc986 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -29,6 +29,8 @@ #import "HockeySDK.h" +#import + #if HOCKEYSDK_FEATURE_FEEDBACK #import "HockeySDKPrivate.h" @@ -63,6 +65,8 @@ @implementation BITFeedbackManager { BOOL _incomingMessagesAlertShowing; BOOL _didEnterBackgroundState; BOOL _networkRequestInProgress; + + BITFeedbackObservationMode _observationMode; } #pragma mark - Initialization @@ -214,14 +218,23 @@ - (BITFeedbackComposeViewController *)feedbackComposeViewController { } - (void)showFeedbackComposeView { + [self showFeedbackComposeViewWithPreparedItems:nil]; +} + +- (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items{ if (_currentFeedbackComposeViewController) { BITHockeyLog(@"INFO: update view already visible, aborting"); return; } - - [self showView:[self feedbackComposeViewController]]; + BITFeedbackComposeViewController *composeView = [self feedbackComposeViewController]; + [composeView prepareWithItems:items]; + [self showView:composeView]; } +- (void)showFeedbackComposeViewWithGeneratedScreenshot { + UIImage *screenshot = bit_screenshot(); + [self showFeedbackComposeViewWithPreparedItems:@[screenshot]]; +} #pragma mark - Manager Control @@ -1018,6 +1031,38 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) } } +#pragma mark - Observation Handling + +-(void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode { + if (mode == BITFeedbackObservationModeOnScreenshot){ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + } +} + +-(void)screenshotNotificationReceived:(NSNotification *)notification { + +} + +-(void)extractLastPictureFromLibraryAndLaunchFeedback { + ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; + + [library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) { + + [group setAssetsFilter:[ALAssetsFilter allPhotos]]; + + [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *alAsset, NSUInteger index, BOOL *innerStop) { + + if (alAsset) { + ALAssetRepresentation *representation = [alAsset defaultRepresentation]; + UIImage *latestPhoto = [UIImage imageWithCGImage:[representation fullScreenImage]]; + + *stop = YES; *innerStop = YES; + + // [self sendTweet:latestPhoto]; + } + }]; + } failureBlock: nil]; +} @end From 70e2b6635c9777a3f6b911b1fff7da5e67ecb2b7 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Feb 2014 12:17:34 +0100 Subject: [PATCH 023/206] + Screenshot Feedback Observation Mode is working on iOS7.. --- Classes/BITFeedbackManager.m | 5 ++--- Support/HockeySDK.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index c79dc986..e194a8ff 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -1040,7 +1040,7 @@ -(void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode { } -(void)screenshotNotificationReceived:(NSNotification *)notification { - + [self extractLastPictureFromLibraryAndLaunchFeedback]; } -(void)extractLastPictureFromLibraryAndLaunchFeedback { @@ -1057,8 +1057,7 @@ -(void)extractLastPictureFromLibraryAndLaunchFeedback { UIImage *latestPhoto = [UIImage imageWithCGImage:[representation fullScreenImage]]; *stop = YES; *innerStop = YES; - - // [self sendTweet:latestPhoto]; + [self showFeedbackComposeViewWithPreparedItems:@[latestPhoto]]; } }]; } failureBlock: nil]; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index d2e9b086..e9a92d28 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ 1EF95CA7162CB037000AE3AD /* BITFeedbackActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EF95CA5162CB036000AE3AD /* BITFeedbackActivity.m */; }; 1EF95CAA162CB314000AE3AD /* BITFeedbackComposeViewControllerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1EFF03E517F2485500A5F13C /* BITCrashManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */; }; + 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */; }; 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; }; 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; }; 97F0FA0118AE375E00EF50AA /* BITImageAnnotationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */; }; @@ -293,6 +294,7 @@ 1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackComposeViewControllerDelegate.h; sourceTree = ""; }; 1EFF03D717F20F8300A5F13C /* BITCrashManagerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BITCrashManagerPrivate.h; sourceTree = ""; }; 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashManagerTests.m; sourceTree = ""; }; + 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = ""; }; 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconCamera@2x.png"; sourceTree = ""; }; 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotationViewController.h; sourceTree = ""; }; @@ -324,6 +326,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */, 1E5954DC15B6F24A00A03429 /* Foundation.framework in Frameworks */, 1E5954DD15B6F24A00A03429 /* CrashReporter.framework in Frameworks */, ); @@ -573,6 +576,7 @@ E400561C148D79B500EB22B9 /* Frameworks */ = { isa = PBXGroup; children = ( + 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */, E41EB48B148D7C4E0015DEDC /* CrashReporter.framework */, E400561D148D79B500EB22B9 /* Foundation.framework */, 1E5A459116F0DFC200B55C04 /* SenTestingKit.framework */, From 28eee0eb3d573ed2dd45cc5ae41b95c10220fccc Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 25 Feb 2014 15:07:59 +0100 Subject: [PATCH 024/206] + Some basic image editing functionality. --- Classes/BITFeedbackComposeViewController.m | 34 ++++++++- Classes/BITFeedbackManager.m | 2 +- Classes/BITFeedbackMessageAttachment.h | 2 + Classes/BITFeedbackMessageAttachment.m | 5 ++ Classes/BITImageAnnotation.h | 13 ++++ Classes/BITImageAnnotation.m | 34 +++++++++ Classes/BITImageAnnotationViewController.h | 10 +++ Classes/BITImageAnnotationViewController.m | 76 ++++++++++++++++++++- Classes/BITRectangleImageAnnotation.h | 13 ++++ Classes/BITRectangleImageAnnotation.m | 49 +++++++++++++ Support/HockeySDK.xcodeproj/project.pbxproj | 32 +++++++-- 11 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 Classes/BITImageAnnotation.h create mode 100644 Classes/BITImageAnnotation.m create mode 100644 Classes/BITRectangleImageAnnotation.h create mode 100644 Classes/BITRectangleImageAnnotation.m diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index b9ab0f15..ac687dc2 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -42,8 +42,10 @@ #import "BITHockeyHelper.h" +#import "BITImageAnnotationViewController.h" -@interface BITFeedbackComposeViewController () { + +@interface BITFeedbackComposeViewController () { UIStatusBarStyle _statusBarStyle; } @@ -469,14 +471,40 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn [attachment deleteContents]; // mandatory call to delete the files associatd. [self.attachments removeObject:attachment]; } - + self.selectedAttachmentIndex = NSNotFound; + [self refreshAttachmentScrollview]; + } else { + if (self.selectedAttachmentIndex != NSNotFound){ + BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; + BITImageAnnotationViewController *annotationEditor = [[BITImageAnnotationViewController alloc ] init]; + annotationEditor.delegate = self; + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:annotationEditor]; + annotationEditor.image = attachment.imageRepresentation; + [self presentViewController:navController animated:YES completion:nil]; + } + } - self.selectedAttachmentIndex = NSNotFound; } +#pragma mark - Image Annotation Delegate + +- (void)annotationController:(BITImageAnnotationViewController *)annotationController didFinishWithImage:(UIImage *)image { + if (self.selectedAttachmentIndex != NSNotFound){ + BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; + [attachment replaceData:UIImageJPEGRepresentation(image, 0.7f)]; + [self refreshAttachmentScrollview]; + } + + self.selectedAttachmentIndex = NSNotFound; +} + +- (void)annotationControllerDidCancel:(BITImageAnnotationViewController *)annotationController { + self.selectedAttachmentIndex = NSNotFound; +} + @end #endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index e194a8ff..e727ea52 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -1035,7 +1035,7 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) -(void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode { if (mode == BITFeedbackObservationModeOnScreenshot){ - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; } } diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index 77a4b92b..4a9a8ea6 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -44,6 +44,8 @@ - (UIImage *)thumbnailWithSize:(CGSize)size; +- (void)replaceData:(NSData *)data; + - (void)deleteContents; @end diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 9184a723..eebc4693 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -76,6 +76,11 @@ -(NSData *)data { return nil; } +- (void)replaceData:(NSData *)data { + self.data = data; + self.thumbnailRepresentations = [NSMutableDictionary new]; +} + #pragma mark NSCoding - (void)encodeWithCoder:(NSCoder *)aCoder { diff --git a/Classes/BITImageAnnotation.h b/Classes/BITImageAnnotation.h new file mode 100644 index 00000000..51e3a8c6 --- /dev/null +++ b/Classes/BITImageAnnotation.h @@ -0,0 +1,13 @@ +// +// BITImageAnnotation.h +// HockeySDK +// +// Created by Moritz Haarmann on 24.02.14. +// +// + +#import + +@interface BITImageAnnotation : UIView + +@end diff --git a/Classes/BITImageAnnotation.m b/Classes/BITImageAnnotation.m new file mode 100644 index 00000000..644a27b3 --- /dev/null +++ b/Classes/BITImageAnnotation.m @@ -0,0 +1,34 @@ +// +// BITImageAnnotation.m +// HockeySDK +// +// Created by Moritz Haarmann on 24.02.14. +// +// + +#import "BITImageAnnotation.h" + +@implementation BITImageAnnotation + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + // Initialization code + //self.backgroundColor = [UIColor redColor]; + } + return self; +} + + + +/* +// Only override drawRect: if you perform custom drawing. +// An empty implementation adversely affects performance during animation. +- (void)drawRect:(CGRect)rect +{ + // Drawing code +} +*/ + +@end diff --git a/Classes/BITImageAnnotationViewController.h b/Classes/BITImageAnnotationViewController.h index 6321c9bd..d333d612 100644 --- a/Classes/BITImageAnnotationViewController.h +++ b/Classes/BITImageAnnotationViewController.h @@ -8,8 +8,18 @@ #import +@class BITImageAnnotationViewController; + +@protocol BITImageAnnotationDelegate + +- (void)annotationControllerDidCancel:(BITImageAnnotationViewController *)annotationController; +- (void)annotationController:(BITImageAnnotationViewController *)annotationController didFinishWithImage:(UIImage *)image; + +@end + @interface BITImageAnnotationViewController : UIViewController @property (nonatomic, strong) UIImage *image; +@property (nonatomic, weak) id delegate; @end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index e6101ab5..a49800da 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -7,12 +7,19 @@ // #import "BITImageAnnotationViewController.h" +#import "BITImageAnnotation.h" +#import "BITRectangleImageAnnotation.h" @interface BITImageAnnotationViewController () @property (nonatomic, strong) UIImageView *imageView; @property (nonatomic, strong) UISegmentedControl *editingControls; -@property (nonatomic, strong) NSMutableArray *layers; +@property (nonatomic, strong) NSMutableArray *objects; +@property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer; +@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; + +@property (nonatomic) CGPoint panStart; +@property (nonatomic,strong) BITImageAnnotation *currentAnnotation; @end @@ -41,11 +48,78 @@ - (void)viewDidLoad self.imageView.image = self.image; self.imageView.contentMode = UIViewContentModeScaleAspectFit; + + + [self.view addSubview:self.imageView]; + self.imageView.frame = self.view.bounds; + + + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; + self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panned:)]; + + [self.tapRecognizer requireGestureRecognizerToFail:self.panRecognizer]; + + [self.view addGestureRecognizer:self.tapRecognizer]; + [self.view addGestureRecognizer:self.panRecognizer]; + + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithTitle:@"Discard" style:UIBarButtonItemStyleBordered target:self action:@selector(discard:)]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithTitle:@"Save" style:UIBarButtonItemStyleBordered target:self action:@selector(save:)]; + // Do any additional setup after loading the view. } -(void)editingAction:(id)sender { +} + +- (BITImageAnnotation *)annotationForCurrentMode { + if (self.editingControls.selectedSegmentIndex == 0){ + return [[BITRectangleImageAnnotation alloc] initWithFrame:CGRectZero]; + } else { + return [[BITImageAnnotation alloc] initWithFrame:CGRectZero]; + } +} + +#pragma mark - Actions + +- (void)discard:(id)sender { + [self.delegate annotationControllerDidCancel:self]; + [self dismissModalViewControllerAnimated:YES]; +} + +- (void)save:(id)sender { + UIImage *image = [self extractImage]; + [self.delegate annotationController:self didFinishWithImage:image]; + [self dismissModalViewControllerAnimated:YES]; +} + +- (UIImage *)extractImage { + UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0); + CGContextRef ctx = UIGraphicsGetCurrentContext(); + [self.view.layer renderInContext:ctx]; + UIImage *renderedImageOfMyself = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return renderedImageOfMyself; +} + +#pragma mark - Gesture Handling + +- (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + self.currentAnnotation = [self annotationForCurrentMode]; + + [self.view insertSubview:self.currentAnnotation aboveSubview:self.imageView]; + self.panStart = [gestureRecognizer locationInView:self.imageView]; + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ + CGPoint bla = [gestureRecognizer translationInView:self.imageView]; + self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x, bla.y); + } + +} + +-(void)tapped:(UITapGestureRecognizer *)gestureRecognizer { + + } - (void)didReceiveMemoryWarning diff --git a/Classes/BITRectangleImageAnnotation.h b/Classes/BITRectangleImageAnnotation.h new file mode 100644 index 00000000..0ba16c0c --- /dev/null +++ b/Classes/BITRectangleImageAnnotation.h @@ -0,0 +1,13 @@ +// +// BITRectangleImageAnnotation.h +// HockeySDK +// +// Created by Moritz Haarmann on 25.02.14. +// +// + +#import "BITImageAnnotation.h" + +@interface BITRectangleImageAnnotation : BITImageAnnotation + +@end diff --git a/Classes/BITRectangleImageAnnotation.m b/Classes/BITRectangleImageAnnotation.m new file mode 100644 index 00000000..465008fe --- /dev/null +++ b/Classes/BITRectangleImageAnnotation.m @@ -0,0 +1,49 @@ +// +// BITRectangleImageAnnotation.m +// HockeySDK +// +// Created by Moritz Haarmann on 25.02.14. +// +// + +#import "BITRectangleImageAnnotation.h" + +@interface BITRectangleImageAnnotation() + +@property (nonatomic, strong) CAShapeLayer *shapeLayer; + +@end + +@implementation BITRectangleImageAnnotation + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.shapeLayer = [CAShapeLayer layer]; + self.shapeLayer.strokeColor = [UIColor redColor].CGColor; + self.shapeLayer.lineWidth = 5; + self.shapeLayer.fillColor = [UIColor clearColor].CGColor; + [self.layer addSublayer:self.shapeLayer]; + + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + self.shapeLayer.frame = self.bounds; + self.shapeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath; +} + +/* +// Only override drawRect: if you perform custom drawing. +// An empty implementation adversely affects performance during animation. +- (void)drawRect:(CGRect)rect +{ + // Drawing code +} +*/ + +@end diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index e9a92d28..002105aa 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -131,10 +131,14 @@ 1EF95CA7162CB037000AE3AD /* BITFeedbackActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EF95CA5162CB036000AE3AD /* BITFeedbackActivity.m */; }; 1EF95CAA162CB314000AE3AD /* BITFeedbackComposeViewControllerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1EFF03E517F2485500A5F13C /* BITCrashManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */; }; + 973EC8B418BCA7BC00DBFFBB /* BITImageAnnotationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */; }; + 973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */; }; + 973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */; }; 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */; }; + 9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */; }; + 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */; }; 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; }; 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; }; - 97F0FA0118AE375E00EF50AA /* BITImageAnnotationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */; }; 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E40E0B0917DA19DC005E38C1 /* BITHockeyAppClientTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E40E0B0817DA19DC005E38C1 /* BITHockeyAppClientTests.m */; }; @@ -294,7 +298,11 @@ 1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackComposeViewControllerDelegate.h; sourceTree = ""; }; 1EFF03D717F20F8300A5F13C /* BITCrashManagerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BITCrashManagerPrivate.h; sourceTree = ""; }; 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashManagerTests.m; sourceTree = ""; }; + 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITRectangleImageAnnotation.h; sourceTree = ""; }; + 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITRectangleImageAnnotation.m; sourceTree = ""; }; 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; + 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotation.h; sourceTree = ""; }; + 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotation.m; sourceTree = ""; }; 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = ""; }; 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconCamera@2x.png"; sourceTree = ""; }; 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotationViewController.h; sourceTree = ""; }; @@ -451,6 +459,7 @@ 1E754E461621FA9A0070AB92 /* Feedback */ = { isa = PBXGroup; children = ( + 9760F6CC18BB684200959B93 /* Image Editor */, 1E49A4361612223B00463151 /* BITFeedbackMessage.h */, 1E49A4371612223B00463151 /* BITFeedbackMessage.m */, 97F0FA0218AE5AED00EF50AA /* BITFeedbackMessageAttachment.h */, @@ -470,8 +479,6 @@ 1E49A4341612223B00463151 /* BITFeedbackManager.m */, E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */, 1E49A4351612223B00463151 /* BITFeedbackManagerPrivate.h */, - 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */, - 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */, ); name = Feedback; sourceTree = ""; @@ -548,6 +555,19 @@ name = Private; sourceTree = ""; }; + 9760F6CC18BB684200959B93 /* Image Editor */ = { + isa = PBXGroup; + children = ( + 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */, + 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */, + 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */, + 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */, + 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */, + 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */, + ); + name = "Image Editor"; + sourceTree = ""; + }; E400560F148D79B500EB22B9 = { isa = PBXGroup; children = ( @@ -681,6 +701,7 @@ 1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */, 1E49A4C4161222B900463151 /* BITAppStoreHeader.h in Headers */, 1E49A4CA161222B900463151 /* BITStoreButton.h in Headers */, + 973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */, E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */, 1E49A4D0161222B900463151 /* BITWebTableViewCell.h in Headers */, 1E49A4D8161222D400463151 /* HockeySDKPrivate.h in Headers */, @@ -690,6 +711,7 @@ 1EACC97B162F041E007578C5 /* BITAttributedLabel.h in Headers */, 1E0FEE28173BDB260061331F /* BITKeychainUtils.h in Headers */, 1E94F9E416E9136B006570AD /* BITStoreUpdateManagerPrivate.h in Headers */, + 9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -894,6 +916,7 @@ 1E49A43F1612223B00463151 /* BITFeedbackComposeViewController.m in Sources */, E40E0B0D17DA1AFF005E38C1 /* BITHockeyAppClient.m in Sources */, 1E49A4451612223B00463151 /* BITFeedbackListViewCell.m in Sources */, + 973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */, 1E49A44B1612223B00463151 /* BITFeedbackListViewController.m in Sources */, 1E49A4511612223B00463151 /* BITFeedbackManager.m in Sources */, E4933E8117B66CDA00B11ACC /* BITHTTPOperation.m in Sources */, @@ -905,6 +928,7 @@ 1E49A4821612226D00463151 /* BITUpdateViewController.m in Sources */, E4B4DB7E17B435550099C67F /* BITAuthenticationViewController.m in Sources */, 1E49A4B2161222B900463151 /* BITHockeyBaseManager.m in Sources */, + 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */, 1E49A4BB161222B900463151 /* BITHockeyBaseViewController.m in Sources */, 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */, 1E49A4C7161222B900463151 /* BITAppStoreHeader.m in Sources */, @@ -916,6 +940,7 @@ 1E754E611621FBB70070AB92 /* BITCrashReportTextFormatter.m in Sources */, 1EF95CA7162CB037000AE3AD /* BITFeedbackActivity.m in Sources */, 1EACC97C162F041E007578C5 /* BITAttributedLabel.m in Sources */, + 973EC8B418BCA7BC00DBFFBB /* BITImageAnnotationViewController.m in Sources */, 1E0FEE29173BDB260061331F /* BITKeychainUtils.m in Sources */, 1E94F9E216E91330006570AD /* BITStoreUpdateManager.m in Sources */, ); @@ -925,7 +950,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 97F0FA0118AE375E00EF50AA /* BITImageAnnotationViewController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 35dc6c58d523144d016319b259d24c083371a357 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 26 Feb 2014 10:10:56 +0100 Subject: [PATCH 025/206] + Fixes ActionSheet madness --- Classes/BITArrowImageAnnotation.h | 13 +++++++++ Classes/BITArrowImageAnnotation.m | 31 ++++++++++++++++++++++ Classes/BITFeedbackComposeViewController.m | 8 +++--- 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 Classes/BITArrowImageAnnotation.h create mode 100644 Classes/BITArrowImageAnnotation.m diff --git a/Classes/BITArrowImageAnnotation.h b/Classes/BITArrowImageAnnotation.h new file mode 100644 index 00000000..ff243af6 --- /dev/null +++ b/Classes/BITArrowImageAnnotation.h @@ -0,0 +1,13 @@ +// +// BITArrowImageAnnotation.h +// HockeySDK +// +// Created by Moritz Haarmann on 26.02.14. +// +// + +#import "BITImageAnnotation.h" + +@interface BITArrowImageAnnotation : BITImageAnnotation + +@end diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m new file mode 100644 index 00000000..d215bdb7 --- /dev/null +++ b/Classes/BITArrowImageAnnotation.m @@ -0,0 +1,31 @@ +// +// BITArrowImageAnnotation.m +// HockeySDK +// +// Created by Moritz Haarmann on 26.02.14. +// +// + +#import "BITArrowImageAnnotation.h" + +@implementation BITArrowImageAnnotation + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + // Initialization code + } + return self; +} + +/* +// Only override drawRect: if you perform custom drawing. +// An empty implementation adversely affects performance during animation. +- (void)drawRect:(CGRect)rect +{ + // Drawing code +} +*/ + +@end diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index ac687dc2..c2806e0d 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -421,8 +421,8 @@ - (void)imageButtonAction:(UIButton *)sender { self.selectedAttachmentIndex = index; UIActionSheet * actionSheet = [[UIActionSheet alloc] initWithTitle: nil delegate: self - cancelButtonTitle: @"Delete Attachment" - destructiveButtonTitle: nil + cancelButtonTitle:@"Cancel" + destructiveButtonTitle: @"Delete Attachment" otherButtonTitles: @"Edit Attachment", nil]; [actionSheet showFromRect: sender.frame inView: self.attachmentScrollView animated: YES]; @@ -464,7 +464,7 @@ - (void)textViewDidChange:(UITextView *)textView { #pragma mark - UIActionSheet Delegate - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex { - if (buttonIndex == [actionSheet cancelButtonIndex]){ + if (buttonIndex == [actionSheet destructiveButtonIndex]){ if (self.selectedAttachmentIndex != NSNotFound){ BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; @@ -474,7 +474,7 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn self.selectedAttachmentIndex = NSNotFound; [self refreshAttachmentScrollview]; - } else { + } else if(buttonIndex != [actionSheet cancelButtonIndex]){ if (self.selectedAttachmentIndex != NSNotFound){ BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; BITImageAnnotationViewController *annotationEditor = [[BITImageAnnotationViewController alloc ] init]; From 77bab0d73df7a9d87f8f1c9c071cd8f355623b54 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 26 Feb 2014 12:40:53 +0100 Subject: [PATCH 026/206] + Image Extraction from Annotation Controller is working --- Classes/BITArrowImageAnnotation.m | 100 +++++++++++++++++++- Classes/BITImageAnnotationViewController.m | 51 ++++++++-- Support/HockeySDK.xcodeproj/project.pbxproj | 8 ++ 3 files changed, 146 insertions(+), 13 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index d215bdb7..472b5a01 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -8,15 +8,47 @@ #import "BITArrowImageAnnotation.h" +#define kArrowPointCount 7 + + +@interface BITArrowImageAnnotation() + +@property (nonatomic, strong) CAShapeLayer *shapeLayer; + +@end + @implementation BITArrowImageAnnotation - (id)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - // Initialization code - } - return self; + self = [super initWithFrame:frame]; + if (self) { + self.shapeLayer = [CAShapeLayer layer]; + self.shapeLayer.strokeColor = [UIColor redColor].CGColor; + self.shapeLayer.lineWidth = 5; + self.shapeLayer.fillColor = [UIColor clearColor].CGColor; + [self.layer addSublayer:self.shapeLayer]; + + } + return self; +} + +- (void)buildShape { + CGFloat topHeight = MAX(self.frame.size.width / 3.0f,20); + + CGFloat lineWidth = MAX(self.frame.size.width / 5.0f,20); + + UIBezierPath *path = [self bezierPathWithArrowFromPoint:CGPointMake(CGRectGetMinX(self.frame), CGRectGetMinY(self.frame)) toPoint:CGPointMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame)) tailWidth:lineWidth headWidth:self.frame.size.height headLength:topHeight]; + + self.shapeLayer.path = path.CGPath; +} + +-(void)layoutSubviews{ + [super layoutSubviews]; + + [self buildShape]; + + self.shapeLayer.frame = self.bounds; } /* @@ -28,4 +60,62 @@ - (void)drawRect:(CGRect)rect } */ +- (UIBezierPath *)bezierPathWithArrowFromPoint:(CGPoint)startPoint + toPoint:(CGPoint)endPoint + tailWidth:(CGFloat)tailWidth + headWidth:(CGFloat)headWidth + headLength:(CGFloat)headLength { + CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y); + + CGPoint points[kArrowPointCount]; + [self getAxisAlignedArrowPoints:points + forLength:length + tailWidth:tailWidth + headWidth:headWidth + headLength:headLength]; + + CGAffineTransform transform = [self transformForStartPoint:startPoint + endPoint:endPoint + length:length]; + + CGMutablePathRef cgPath = CGPathCreateMutable(); + CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points); + CGPathCloseSubpath(cgPath); + + UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath]; + CGPathRelease(cgPath); + return uiPath; +} + +- (void)getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points + forLength:(CGFloat)length + tailWidth:(CGFloat)tailWidth + headWidth:(CGFloat)headWidth + headLength:(CGFloat)headLength { + CGFloat tailLength = length - headLength; + points[0] = CGPointMake(0, tailWidth / 2); + points[1] = CGPointMake(tailLength, tailWidth / 2); + points[2] = CGPointMake(tailLength, headWidth / 2); + points[3] = CGPointMake(length, 0); + points[4] = CGPointMake(tailLength, -headWidth / 2); + points[5] = CGPointMake(tailLength, -tailWidth / 2); + points[6] = CGPointMake(0, -tailWidth / 2); +} + ++ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint + endPoint:(CGPoint)endPoint + length:(CGFloat)length { + CGFloat cosine = (endPoint.x - startPoint.x) / length; + CGFloat sine = (endPoint.y - startPoint.y) / length; + return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y }; +} + +- (CGAffineTransform)transformForStartPoint:(CGPoint)startPoint + endPoint:(CGPoint)endPoint + length:(CGFloat)length { + CGFloat cosine = (endPoint.x - startPoint.x) / length; + CGFloat sine = (endPoint.y - startPoint.y) / length; + return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y }; +} + @end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index a49800da..2f37b351 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -9,6 +9,7 @@ #import "BITImageAnnotationViewController.h" #import "BITImageAnnotation.h" #import "BITRectangleImageAnnotation.h" +#import "BITArrowImageAnnotation.h" @interface BITImageAnnotationViewController () @@ -17,6 +18,7 @@ @interface BITImageAnnotationViewController () @property (nonatomic, strong) NSMutableArray *objects; @property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer; @property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; +@property (nonatomic) CGFloat scaleFactor; @property (nonatomic) CGPoint panStart; @property (nonatomic,strong) BITImageAnnotation *currentAnnotation; @@ -38,32 +40,54 @@ - (void)viewDidLoad { [super viewDidLoad]; + self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; + self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Arrow", @"Rect", @"Blur"]]; self.navigationItem.titleView = self.editingControls; + + + self.objects = [NSMutableArray new]; [self.editingControls addTarget:self action:@selector(editingAction:) forControlEvents:UIControlEventTouchUpInside]; self.imageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; + + + self.imageView.clipsToBounds = YES; + + self.imageView.layer.masksToBounds = YES; + self.imageView.image = self.image; - self.imageView.contentMode = UIViewContentModeScaleAspectFit; + self.imageView.contentMode = UIViewContentModeScaleToFill; [self.view addSubview:self.imageView]; self.imageView.frame = self.view.bounds; - self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panned:)]; [self.tapRecognizer requireGestureRecognizerToFail:self.panRecognizer]; - [self.view addGestureRecognizer:self.tapRecognizer]; - [self.view addGestureRecognizer:self.panRecognizer]; + [self.imageView addGestureRecognizer:self.tapRecognizer]; + [self.imageView addGestureRecognizer:self.panRecognizer]; + + self.imageView.userInteractionEnabled = YES; self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithTitle:@"Discard" style:UIBarButtonItemStyleBordered target:self action:@selector(discard:)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithTitle:@"Save" style:UIBarButtonItemStyleBordered target:self action:@selector(save:)]; + + CGFloat heightScaleFactor = self.view.frame.size.height / self.image.size.height; + CGFloat widthScaleFactor = self.view.frame.size.width / self.image.size.width; + + CGFloat factor = MIN(heightScaleFactor, widthScaleFactor); + self.scaleFactor = factor; + CGSize scaledImageSize = CGSizeMake(self.image.size.width * factor, self.image.size.height * factor); + + self.imageView.frame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height/2 - scaledImageSize.height/2, scaledImageSize.width, scaledImageSize.height); + // Do any additional setup after loading the view. } @@ -75,6 +99,8 @@ -(void)editingAction:(id)sender { - (BITImageAnnotation *)annotationForCurrentMode { if (self.editingControls.selectedSegmentIndex == 0){ return [[BITRectangleImageAnnotation alloc] initWithFrame:CGRectZero]; + } else if(self.editingControls.selectedSegmentIndex==1){ + return [[BITArrowImageAnnotation alloc] initWithFrame:CGRectZero]; } else { return [[BITImageAnnotation alloc] initWithFrame:CGRectZero]; } @@ -94,9 +120,18 @@ - (void)save:(id)sender { } - (UIImage *)extractImage { - UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0); + UIGraphicsBeginImageContextWithOptions(self.image.size, YES, 0.0); CGContextRef ctx = UIGraphicsGetCurrentContext(); - [self.view.layer renderInContext:ctx]; + [self.image drawInRect:CGRectMake(0, 0, self.image.size.width, self.image.size.height)]; + CGContextScaleCTM(ctx,1.0/self.scaleFactor,1.0f/self.scaleFactor); + + // Drawing all the annotations onto the final image. + for (BITImageAnnotation *annotation in self.objects){ + CGContextTranslateCTM(ctx, annotation.frame.origin.x, annotation.frame.origin.y); + [annotation.layer renderInContext:ctx]; + CGContextTranslateCTM(ctx,-1 * annotation.frame.origin.x,-1 * annotation.frame.origin.y); + } + UIImage *renderedImageOfMyself = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return renderedImageOfMyself; @@ -107,8 +142,8 @@ - (UIImage *)extractImage { - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ self.currentAnnotation = [self annotationForCurrentMode]; - - [self.view insertSubview:self.currentAnnotation aboveSubview:self.imageView]; + [self.objects addObject:self.currentAnnotation]; + [self.imageView insertSubview:self.currentAnnotation aboveSubview:self.imageView]; self.panStart = [gestureRecognizer locationInView:self.imageView]; } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ CGPoint bla = [gestureRecognizer translationInView:self.imageView]; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 002105aa..a2bce997 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -134,6 +134,8 @@ 973EC8B418BCA7BC00DBFFBB /* BITImageAnnotationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */; }; 973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */; }; 973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */; }; + 973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */; }; + 973EC8BC18BDE29800DBFFBB /* BITArrowImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */; }; 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */; }; 9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */; }; 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */; }; @@ -300,6 +302,8 @@ 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashManagerTests.m; sourceTree = ""; }; 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITRectangleImageAnnotation.h; sourceTree = ""; }; 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITRectangleImageAnnotation.m; sourceTree = ""; }; + 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITArrowImageAnnotation.h; sourceTree = ""; }; + 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITArrowImageAnnotation.m; sourceTree = ""; }; 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotation.h; sourceTree = ""; }; 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotation.m; sourceTree = ""; }; @@ -564,6 +568,8 @@ 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */, 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */, 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */, + 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */, + 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */, ); name = "Image Editor"; sourceTree = ""; @@ -699,6 +705,7 @@ 1E49A4B5161222B900463151 /* BITHockeyBaseManagerPrivate.h in Headers */, E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */, 1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */, + 973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */, 1E49A4C4161222B900463151 /* BITAppStoreHeader.h in Headers */, 1E49A4CA161222B900463151 /* BITStoreButton.h in Headers */, 973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */, @@ -940,6 +947,7 @@ 1E754E611621FBB70070AB92 /* BITCrashReportTextFormatter.m in Sources */, 1EF95CA7162CB037000AE3AD /* BITFeedbackActivity.m in Sources */, 1EACC97C162F041E007578C5 /* BITAttributedLabel.m in Sources */, + 973EC8BC18BDE29800DBFFBB /* BITArrowImageAnnotation.m in Sources */, 973EC8B418BCA7BC00DBFFBB /* BITImageAnnotationViewController.m in Sources */, 1E0FEE29173BDB260061331F /* BITKeychainUtils.m in Sources */, 1E94F9E216E91330006570AD /* BITStoreUpdateManager.m in Sources */, From ad086273d7349b766047e2329c7618ecf2e0357e Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 26 Feb 2014 15:22:44 +0100 Subject: [PATCH 027/206] + Replicated Bugshot. --- Classes/BITArrowImageAnnotation.m | 39 ++++++++++++-- Classes/BITBlurImageAnnotation.h | 13 +++++ Classes/BITBlurImageAnnotation.m | 57 +++++++++++++++++++++ Classes/BITImageAnnotation.h | 4 +- Classes/BITImageAnnotationViewController.m | 17 +++--- Classes/BITRectangleImageAnnotation.m | 13 +++++ Support/HockeySDK.xcodeproj/project.pbxproj | 8 +++ 7 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 Classes/BITBlurImageAnnotation.h create mode 100644 Classes/BITBlurImageAnnotation.m diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index 472b5a01..b53bb502 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -14,6 +14,8 @@ @interface BITArrowImageAnnotation() @property (nonatomic, strong) CAShapeLayer *shapeLayer; +@property (nonatomic, strong) CAShapeLayer *strokeLayer; + @end @@ -27,7 +29,15 @@ - (id)initWithFrame:(CGRect)frame self.shapeLayer.strokeColor = [UIColor redColor].CGColor; self.shapeLayer.lineWidth = 5; self.shapeLayer.fillColor = [UIColor clearColor].CGColor; + + self.strokeLayer = [CAShapeLayer layer]; + self.strokeLayer.strokeColor = [UIColor whiteColor].CGColor; + self.strokeLayer.lineWidth = 10; + self.strokeLayer.fillColor = [UIColor clearColor].CGColor; + [self.layer addSublayer:self.strokeLayer]; + [self.layer addSublayer:self.shapeLayer]; + } return self; @@ -35,20 +45,41 @@ - (id)initWithFrame:(CGRect)frame - (void)buildShape { CGFloat topHeight = MAX(self.frame.size.width / 3.0f,20); + - CGFloat lineWidth = MAX(self.frame.size.width / 5.0f,20); + CGFloat lineWidth = MAX(self.frame.size.width / 10.0f,10); + CGFloat startX, startY, endX, endY; + if ( self.movedDelta.width > 0){ + startX = CGRectGetMinX(self.bounds); + endX = CGRectGetMaxX(self.bounds); + } else { + startX = CGRectGetMaxX(self.bounds); + endX = CGRectGetMinX(self.bounds); + + } - UIBezierPath *path = [self bezierPathWithArrowFromPoint:CGPointMake(CGRectGetMinX(self.frame), CGRectGetMinY(self.frame)) toPoint:CGPointMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame)) tailWidth:lineWidth headWidth:self.frame.size.height headLength:topHeight]; + if ( self.movedDelta.height > 0){ + startY = CGRectGetMinY(self.bounds); + endY = CGRectGetMaxY(self.bounds); + } else { + startY = CGRectGetMaxY(self.bounds); + endY = CGRectGetMinY(self.bounds); + + } + + NSLog(@"Start X: %f, Y: %f, END: %f %f %@", startX, startY, endX,endY, self); + + UIBezierPath *path = [self bezierPathWithArrowFromPoint:CGPointMake(endX,endY) toPoint:CGPointMake(startX,startY) tailWidth:lineWidth headWidth:topHeight headLength:topHeight]; self.shapeLayer.path = path.CGPath; + self.strokeLayer.path = path.CGPath; } -(void)layoutSubviews{ [super layoutSubviews]; - + [self buildShape]; - self.shapeLayer.frame = self.bounds; } /* diff --git a/Classes/BITBlurImageAnnotation.h b/Classes/BITBlurImageAnnotation.h new file mode 100644 index 00000000..b23d7f2d --- /dev/null +++ b/Classes/BITBlurImageAnnotation.h @@ -0,0 +1,13 @@ +// +// BITBlurImageAnnotation.h +// HockeySDK +// +// Created by Moritz Haarmann on 26.02.14. +// +// + +#import "BITImageAnnotation.h" + +@interface BITBlurImageAnnotation : BITImageAnnotation + +@end diff --git a/Classes/BITBlurImageAnnotation.m b/Classes/BITBlurImageAnnotation.m new file mode 100644 index 00000000..898ce2a3 --- /dev/null +++ b/Classes/BITBlurImageAnnotation.m @@ -0,0 +1,57 @@ +// +// BITBlurImageAnnotation.m +// HockeySDK +// +// Created by Moritz Haarmann on 26.02.14. +// +// + +#import "BITBlurImageAnnotation.h" + +@interface BITBlurImageAnnotation() + +@property (nonatomic, strong) CALayer* imageLayer; +@property (nonatomic, strong) UIImage* scaledImage; + + +@end + +@implementation BITBlurImageAnnotation + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.clipsToBounds = YES; + self.imageLayer = [CALayer layer]; + [self.layer addSublayer:self.imageLayer]; + } + return self; +} + +-(void)setSourceImage:(UIImage *)sourceImage { + CGSize size = CGSizeMake(sourceImage.size.width/30, sourceImage.size.height/30); + + UIGraphicsBeginImageContext(size); + [sourceImage drawInRect:CGRectMake(0, 0, size.width, size.height)]; + self.scaledImage = UIGraphicsGetImageFromCurrentImageContext(); + self.imageLayer.contents = (id)self.scaledImage.CGImage; + UIGraphicsEndImageContext(); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.imageLayer.frame = self.imageFrame; + self.imageLayer.masksToBounds = YES; +} + +/* +// Only override drawRect: if you perform custom drawing. +// An empty implementation adversely affects performance during animation. +- (void)drawRect:(CGRect)rect +{ + // Drawing code +} +*/ + +@end diff --git a/Classes/BITImageAnnotation.h b/Classes/BITImageAnnotation.h index 51e3a8c6..64493012 100644 --- a/Classes/BITImageAnnotation.h +++ b/Classes/BITImageAnnotation.h @@ -9,5 +9,7 @@ #import @interface BITImageAnnotation : UIView - +@property (nonatomic) CGSize movedDelta; +@property (nonatomic, weak) UIImage *sourceImage; +@property (nonatomic) CGRect imageFrame; @end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 2f37b351..f9977060 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -10,6 +10,7 @@ #import "BITImageAnnotation.h" #import "BITRectangleImageAnnotation.h" #import "BITArrowImageAnnotation.h" +#import "BITBlurImageAnnotation.h" @interface BITImageAnnotationViewController () @@ -50,15 +51,12 @@ - (void)viewDidLoad self.objects = [NSMutableArray new]; [self.editingControls addTarget:self action:@selector(editingAction:) forControlEvents:UIControlEventTouchUpInside]; + [self.editingControls setSelectedSegmentIndex:0]; self.imageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; - - self.imageView.clipsToBounds = YES; - - self.imageView.layer.masksToBounds = YES; - + self.imageView.image = self.image; self.imageView.contentMode = UIViewContentModeScaleToFill; @@ -102,7 +100,7 @@ - (BITImageAnnotation *)annotationForCurrentMode { } else if(self.editingControls.selectedSegmentIndex==1){ return [[BITArrowImageAnnotation alloc] initWithFrame:CGRectZero]; } else { - return [[BITImageAnnotation alloc] initWithFrame:CGRectZero]; + return [[BITBlurImageAnnotation alloc] initWithFrame:CGRectZero]; } } @@ -143,11 +141,14 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ self.currentAnnotation = [self annotationForCurrentMode]; [self.objects addObject:self.currentAnnotation]; + self.currentAnnotation.sourceImage = self.image; [self.imageView insertSubview:self.currentAnnotation aboveSubview:self.imageView]; self.panStart = [gestureRecognizer locationInView:self.imageView]; } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ - CGPoint bla = [gestureRecognizer translationInView:self.imageView]; - self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x, bla.y); + CGPoint bla = [gestureRecognizer locationInView:self.imageView]; + self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x - self.panStart.x, bla.y - self.panStart.y); + self.currentAnnotation.movedDelta = CGSizeMake(bla.x - self.panStart.x, bla.y - self.panStart.y); + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; } } diff --git a/Classes/BITRectangleImageAnnotation.m b/Classes/BITRectangleImageAnnotation.m index 465008fe..5fd44c75 100644 --- a/Classes/BITRectangleImageAnnotation.m +++ b/Classes/BITRectangleImageAnnotation.m @@ -11,6 +11,8 @@ @interface BITRectangleImageAnnotation() @property (nonatomic, strong) CAShapeLayer *shapeLayer; +@property (nonatomic, strong) CAShapeLayer *strokeLayer; + @end @@ -24,6 +26,13 @@ - (id)initWithFrame:(CGRect)frame self.shapeLayer.strokeColor = [UIColor redColor].CGColor; self.shapeLayer.lineWidth = 5; self.shapeLayer.fillColor = [UIColor clearColor].CGColor; + + self.strokeLayer = [CAShapeLayer layer]; + self.strokeLayer.strokeColor = [UIColor whiteColor].CGColor; + self.strokeLayer.lineWidth = 10; + self.strokeLayer.fillColor = [UIColor clearColor].CGColor; + [self.layer addSublayer:self.strokeLayer]; + [self.layer addSublayer:self.shapeLayer]; } @@ -35,6 +44,10 @@ - (void)layoutSubviews { self.shapeLayer.frame = self.bounds; self.shapeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath; + + + self.strokeLayer.frame = self.bounds; + self.strokeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath; } /* diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index a2bce997..bf311915 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -136,6 +136,8 @@ 973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */; }; 973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */; }; 973EC8BC18BDE29800DBFFBB /* BITArrowImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */; }; + 973EC8BF18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */; }; + 973EC8C018BE2B5B00DBFFBB /* BITBlurImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */; }; 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */; }; 9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */; }; 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */; }; @@ -304,6 +306,8 @@ 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITRectangleImageAnnotation.m; sourceTree = ""; }; 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITArrowImageAnnotation.h; sourceTree = ""; }; 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITArrowImageAnnotation.m; sourceTree = ""; }; + 973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITBlurImageAnnotation.h; sourceTree = ""; }; + 973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITBlurImageAnnotation.m; sourceTree = ""; }; 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotation.h; sourceTree = ""; }; 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotation.m; sourceTree = ""; }; @@ -570,6 +574,8 @@ 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */, 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */, 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */, + 973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */, + 973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */, ); name = "Image Editor"; sourceTree = ""; @@ -684,6 +690,7 @@ 1E49A4731612226D00463151 /* BITUpdateManager.h in Headers */, 1E49A4791612226D00463151 /* BITUpdateManagerDelegate.h in Headers */, 1E49A44E1612223B00463151 /* BITFeedbackManager.h in Headers */, + 973EC8BF18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h in Headers */, E4B4DB7D17B435550099C67F /* BITAuthenticationViewController.h in Headers */, 1E49A4481612223B00463151 /* BITFeedbackListViewController.h in Headers */, 1E49A47F1612226D00463151 /* BITUpdateViewController.h in Headers */, @@ -935,6 +942,7 @@ 1E49A4821612226D00463151 /* BITUpdateViewController.m in Sources */, E4B4DB7E17B435550099C67F /* BITAuthenticationViewController.m in Sources */, 1E49A4B2161222B900463151 /* BITHockeyBaseManager.m in Sources */, + 973EC8C018BE2B5B00DBFFBB /* BITBlurImageAnnotation.m in Sources */, 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */, 1E49A4BB161222B900463151 /* BITHockeyBaseViewController.m in Sources */, 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */, From b269c8ee4aa7464fd4b46394e75d0ab1edced0cd Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 26 Feb 2014 15:23:03 +0100 Subject: [PATCH 028/206] + Wording --- Classes/BITImageAnnotationViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index f9977060..b5f3aae6 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -43,7 +43,7 @@ - (void)viewDidLoad self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; - self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Arrow", @"Rect", @"Blur"]]; + self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Rectangle", @"Arrow", @"Blur"]]; self.navigationItem.titleView = self.editingControls; From 3890e9410123ad4b96557f160c61dbbde35e3839 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 26 Feb 2014 15:32:00 +0100 Subject: [PATCH 029/206] + Removes Leak warning. --- Classes/BITFeedbackMessageAttachment.m | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index eebc4693..b0d68b82 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -140,20 +140,12 @@ - (NSString *)createFilename { [[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:nil]; } - NSString *uniqueString = [BITFeedbackMessageAttachment GetUUID]; + NSString *uniqueString = bit_UUID(); cachePath = [cachePath stringByAppendingPathComponent:uniqueString]; return cachePath; } -+ (NSString *)GetUUID -{ - CFUUIDRef theUUID = CFUUIDCreate(NULL); - CFStringRef string = CFUUIDCreateString(NULL, theUUID); - CFRelease(theUUID); - return (__bridge NSString *)string; -} - - (void)deleteContents { if (self.filename){ [[NSFileManager defaultManager] removeItemAtPath:self.filename error:nil]; From cb48105663646fa57603e3e8c87a43afd694b8b0 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 26 Feb 2014 15:33:03 +0100 Subject: [PATCH 030/206] Add required AssetsLibrary to the frameworks --- Support/HockeySDK.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Support/HockeySDK.xcconfig b/Support/HockeySDK.xcconfig index 45a57e9a..012479d8 100644 --- a/Support/HockeySDK.xcconfig +++ b/Support/HockeySDK.xcconfig @@ -1,3 +1,3 @@ -OTHER_LDFLAGS=$(inherited) -framework CoreText -framework CoreGraphics -framework Foundation -framework QuartzCore -framework SystemConfiguration -framework UIKit -framework Security +OTHER_LDFLAGS=$(inherited) -framework CoreText -framework CoreGraphics -framework Foundation -framework QuartzCore -framework SystemConfiguration -framework UIKit -framework Security -framework AssetsLibrary HOCKEYSDK_DOCSET_NAME=HockeySDK-iOS GCC_PREPROCESSOR_DEFINITIONS=$(inherited) CONFIGURATION_$(CONFIGURATION) From 74f9e5d5ca4e91391768f5e7e41a5684228a016c Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Thu, 27 Feb 2014 11:16:22 +0100 Subject: [PATCH 031/206] + Prevent Messages without any text from ever being sent. --- Classes/BITFeedbackComposeViewController.m | 2 +- Classes/BITFeedbackListViewCell.m | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index c2806e0d..d4ab2379 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -327,7 +327,7 @@ -(void)refreshAttachmentScrollview { } - (void)updateBarButtonState { - if (self.textView.text.length == 0 && self.attachments.count == 0 ) { + if (self.textView.text.length > 0 ) { self.navigationItem.rightBarButtonItem.enabled = NO; } else { self.navigationItem.rightBarButtonItem.enabled = YES; diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index c428d377..d0091e3b 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -264,6 +264,7 @@ - (void)layoutSubviews { CGFloat attachmentsPerRow = ceilf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); for ( UIImageView *imageView in self.attachmentViews){ + imageView.contentMode = UIViewContentModeScaleAspectFit; if ( !_message.userMessage){ imageView.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * i , floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); From 672e38a7e296bb906106c920b6b9353edc80231b Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Thu, 6 Mar 2014 10:57:32 +0100 Subject: [PATCH 032/206] + Fixes a Bug where attachments in the compose view are hidden on an iPad. --- Classes/BITFeedbackComposeViewController.m | 7 +++++-- Classes/BITFeedbackManager.m | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index d4ab2379..689e7083 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -104,6 +104,7 @@ - (void)prepareWithItems:(NSArray *)items { BITHockeyLog(@"Unknown item type %@", item); } } + } @@ -165,6 +166,8 @@ - (void)viewDidLoad { // Container that contains both the textfield and eventually the photo scroll view on the right side self.contentViewContainer = [[UIView alloc] initWithFrame:self.view.bounds]; + self.contentViewContainer.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; + [self.view addSubview:self.contentViewContainer]; // message input textfield @@ -194,7 +197,7 @@ - (void)viewDidLoad { self.attachmentScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero]; self.attachmentScrollView.scrollEnabled = YES; self.attachmentScrollView.bounces = YES; - self.attachmentScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; + self.attachmentScrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleRightMargin; [self.contentViewContainer addSubview:self.attachmentScrollView]; } @@ -278,7 +281,7 @@ -(void)refreshAttachmentScrollview { if (!alreadySetup){ textViewFrame.size.width -= scrollViewWidth; - scrollViewFrame = CGRectMake(CGRectGetMaxX(textViewFrame), self.view.frame.origin.y, scrollViewWidth, CGRectGetHeight(textViewFrame)); + scrollViewFrame = CGRectMake(CGRectGetMaxX(textViewFrame), self.view.frame.origin.y, scrollViewWidth, CGRectGetHeight(self.view.bounds)); self.textView.frame = textViewFrame; self.attachmentScrollView.frame = scrollViewFrame; self.attachmentScrollView.contentInset = self.textView.contentInset; diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index e727ea52..efabd38d 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -228,7 +228,9 @@ - (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items{ } BITFeedbackComposeViewController *composeView = [self feedbackComposeViewController]; [composeView prepareWithItems:items]; + [self showView:composeView]; + } - (void)showFeedbackComposeViewWithGeneratedScreenshot { From c08a308ce42ca31500801c09e4be6d09db284a02 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Thu, 6 Mar 2014 15:49:25 +0100 Subject: [PATCH 033/206] + Gallery View Controller for Viewing Attachments --- Classes/BITAttachmentGalleryViewController.h | 15 ++ Classes/BITAttachmentGalleryViewController.m | 161 +++++++++++++++++++ Classes/BITFeedbackListViewCell.h | 10 ++ Classes/BITFeedbackListViewCell.m | 25 ++- Classes/BITFeedbackListViewController.m | 17 +- Support/HockeySDK.xcodeproj/project.pbxproj | 16 ++ 6 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 Classes/BITAttachmentGalleryViewController.h create mode 100644 Classes/BITAttachmentGalleryViewController.m diff --git a/Classes/BITAttachmentGalleryViewController.h b/Classes/BITAttachmentGalleryViewController.h new file mode 100644 index 00000000..c246390f --- /dev/null +++ b/Classes/BITAttachmentGalleryViewController.h @@ -0,0 +1,15 @@ +// +// BITAttachmentGalleryViewController.h +// HockeySDK +// +// Created by Moritz Haarmann on 06.03.14. +// +// + +#import + +@interface BITAttachmentGalleryViewController : UIViewController + +@property (nonatomic, strong) NSArray *messages; + +@end diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m new file mode 100644 index 00000000..13ccfee5 --- /dev/null +++ b/Classes/BITAttachmentGalleryViewController.m @@ -0,0 +1,161 @@ +// +// BITAttachmentGalleryViewController.m +// HockeySDK +// +// Created by Moritz Haarmann on 06.03.14. +// +// + +#import "BITAttachmentGalleryViewController.h" + +#import "BITFeedbackMessage.h" +#import "BITFeedbackMessageAttachment.h" + +@interface BITAttachmentGalleryViewController () + +@property (nonatomic, strong) UIScrollView *scrollView; +@property (nonatomic, strong) NSArray *imageViews; +@property (nonatomic, strong) NSArray *extractedAttachments; +@property (nonatomic) NSInteger currentIndex; +@property (nonatomic, strong) UITapGestureRecognizer *tapognizer; + +@end + +@implementation BITAttachmentGalleryViewController + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Custom initialization + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Close" style:UIBarButtonItemStylePlain target:self action:@selector(close:)]; + + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(share:)]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + [self extractUsableAttachments]; + + [self setupScrollView]; + + self.tapognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; + [self.view addGestureRecognizer:self.tapognizer]; + +} +- (void)setupScrollView { + self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; + [self.view addSubview:self.scrollView]; + self.scrollView.delegate = self; + self.scrollView.pagingEnabled = YES; + self.scrollView.backgroundColor = [UIColor groupTableViewBackgroundColor]; + self.scrollView.bounces = NO; + + + NSMutableArray *imageviews = [NSMutableArray new]; + + for (int i = 0; i<3; i++){ + UIImageView *newImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; + [imageviews addObject:newImageView]; + newImageView.contentMode = UIViewContentModeScaleAspectFit; + [self.scrollView addSubview:newImageView]; + } + + self.imageViews = imageviews; + +} + +- (void)extractUsableAttachments { + NSMutableArray *extractedOnes = [NSMutableArray new]; + + for (BITFeedbackMessage *message in self.messages){ + for (BITFeedbackMessageAttachment *attachment in message.attachments){ + if ([attachment imageRepresentation]){ + [extractedOnes addObject:attachment]; + } + } + } + + self.extractedAttachments = extractedOnes; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + NSInteger newIndex = self.scrollView.contentOffset.x / self.scrollView.frame.size.width; + if (newIndex!=self.currentIndex){ + self.currentIndex = newIndex; + // requeue elements. + NSInteger baseIndex = MAX(0,self.currentIndex-1); + [self layoutViews]; + NSInteger z = baseIndex; + + + + for ( NSInteger i = baseIndex; i < MIN(baseIndex+2, self.extractedAttachments.count);i++ ){ + UIImageView *imageView = self.imageViews[z]; + BITFeedbackMessageAttachment *attachment = self.extractedAttachments[i]; + imageView.image =[attachment imageRepresentation]; + z++; + } + + + } +} + +- (void)layoutViews { + + self.scrollView.frame = self.view.bounds; + self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds) * self.extractedAttachments.count, CGRectGetHeight(self.view.bounds)); + + NSInteger baseIndex = MAX(0,self.currentIndex-1); + NSInteger z = baseIndex; + for ( NSInteger i = baseIndex; i < MIN(baseIndex+2, self.extractedAttachments.count);i++ ){ + UIImageView *imageView = self.imageViews[z]; + imageView.frame = [self frameForItemAtIndex:i]; + z++; + } +} + +- (BOOL)prefersStatusBarHidden { + return YES; +} + +- (void)close:(id)sender { + [self dismissModalViewControllerAnimated:YES]; +} + +- (void)share:(id)sender { + BITFeedbackMessageAttachment *attachment = self.extractedAttachments[self.currentIndex]; + + UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:[NSArray arrayWithObjects:attachment.originalFilename, attachment.imageRepresentation , nil] applicationActivities:nil]; + [self presentViewController:activityVC animated:YES completion:nil]; +} + +- (void)tapped:(UITapGestureRecognizer *)tapRecognizer { + if (self.navigationController.navigationBarHidden){ + [[UIApplication sharedApplication] setStatusBarHidden:NO]; + self.navigationController.navigationBarHidden = NO; + } else { + [[UIApplication sharedApplication] setStatusBarHidden:YES]; + self.navigationController.navigationBarHidden = YES; + } + [self layoutViews]; +} + +- (CGRect)frameForItemAtIndex:(NSInteger)index { + return CGRectMake(index * CGRectGetWidth(self.scrollView.frame), 0, CGRectGetWidth(self.scrollView.frame), CGRectGetHeight(self.scrollView.frame)); +} + +@end diff --git a/Classes/BITFeedbackListViewCell.h b/Classes/BITFeedbackListViewCell.h index 4842cb1a..8ce9c3fc 100644 --- a/Classes/BITFeedbackListViewCell.h +++ b/Classes/BITFeedbackListViewCell.h @@ -31,6 +31,14 @@ #import "BITFeedbackMessage.h" #import "BITAttributedLabel.h" +@class BITFeedbackMessageAttachment; + +@protocol BITFeedbackListViewCellDelegate + +- (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment; + +@end + /** * Cell style depending on the iOS version */ @@ -69,6 +77,8 @@ typedef NS_ENUM(NSUInteger, BITFeedbackListViewCellBackgroundStyle) { @property (nonatomic, strong) BITAttributedLabel *labelText; +@property (nonatomic, weak) id delegate; + + (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width; - (void)setAttachments:(NSArray *)attachments; diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index d0091e3b..d6221876 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -193,8 +193,11 @@ - (void)setAttachments:(NSArray *)attachments { [self.attachmentViews removeAllObjects]; for (BITFeedbackMessageAttachment *attachment in attachments){ - UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectZero]; - imageView.image = [attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)]; + UIButton *imageView = [UIButton buttonWithType:UIButtonTypeCustom]; + [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; + + [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + [self.attachmentViews addObject:imageView]; [self addSubview:imageView]; } @@ -263,13 +266,13 @@ - (void)layoutSubviews { int i = 0; CGFloat attachmentsPerRow = ceilf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); - for ( UIImageView *imageView in self.attachmentViews){ - imageView.contentMode = UIViewContentModeScaleAspectFit; + for ( UIButton *imageButton in self.attachmentViews){ + imageButton.contentMode = UIViewContentModeScaleAspectFit; if ( !_message.userMessage){ - imageView.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * i , floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + imageButton.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * i , floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } else { - imageView.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i) ), floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + imageButton.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i) ), floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } i++; @@ -280,5 +283,15 @@ - (void)layoutSubviews { [super layoutSubviews]; } +- (void)imageButtonPressed:(id)sender { + if ([self.delegate respondsToSelector:@selector(listCell:didSelectAttachment:)]){ + NSInteger index = [self.attachmentViews indexOfObject:sender]; + if (index != NSNotFound){ + BITFeedbackMessageAttachment *attachment = self.message.attachments[index]; + [self.delegate listCell:self didSelectAttachment:attachment]; + } + } +} + @end diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index ff59a222..fe45d9e5 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -40,6 +40,7 @@ #import "BITFeedbackUserDataViewController.h" #import "BITFeedbackMessage.h" #import "BITAttributedLabel.h" +#import "BITAttachmentGalleryViewController.h" #import "BITHockeyBaseManagerPrivate.h" @@ -64,7 +65,7 @@ #define BORDER_COLOR BIT_RGBCOLOR(215, 215, 215) -@interface BITFeedbackListViewController () +@interface BITFeedbackListViewController () @property (nonatomic, weak) BITFeedbackManager *manager; @property (nonatomic, strong) NSDateFormatter *lastUpdateDateFormatter; @@ -628,6 +629,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell.message = message; cell.labelText.delegate = self; cell.labelText.userInteractionEnabled = YES; + cell.delegate = self; [cell setAttachments:message.attachments]; if ( @@ -769,6 +771,19 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn } } +- (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment { + BITAttachmentGalleryViewController *galleryController = [BITAttachmentGalleryViewController new]; + + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:galleryController]; + NSMutableArray *collectedMessages = [NSMutableArray new]; + for (int i = 0; i Date: Tue, 18 Mar 2014 23:24:30 +0100 Subject: [PATCH 034/206] + Fixes #60: Drawing direction of arrow. --- Classes/BITArrowImageAnnotation.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index b53bb502..c45e5e2f 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -49,7 +49,7 @@ - (void)buildShape { CGFloat lineWidth = MAX(self.frame.size.width / 10.0f,10); CGFloat startX, startY, endX, endY; - if ( self.movedDelta.width > 0){ + if ( self.movedDelta.width < 0){ startX = CGRectGetMinX(self.bounds); endX = CGRectGetMaxX(self.bounds); } else { @@ -58,7 +58,7 @@ - (void)buildShape { } - if ( self.movedDelta.height > 0){ + if ( self.movedDelta.height < 0){ startY = CGRectGetMinY(self.bounds); endY = CGRectGetMaxY(self.bounds); } else { From b33ee9a61fd6784c96c2e65f62b5deab5e75511a Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Tue, 18 Mar 2014 23:46:49 +0100 Subject: [PATCH 035/206] + Added #63 (Moving drawn elements.) --- Classes/BITImageAnnotationViewController.m | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index b5f3aae6..8e780f06 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -19,11 +19,15 @@ @interface BITImageAnnotationViewController () @property (nonatomic, strong) NSMutableArray *objects; @property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer; @property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; +@property (nonatomic, strong) UIPinchGestureRecognizer *pinchRecognizer; + @property (nonatomic) CGFloat scaleFactor; @property (nonatomic) CGPoint panStart; @property (nonatomic,strong) BITImageAnnotation *currentAnnotation; +@property (nonatomic) BOOL isDrawing; + @end @implementation BITImageAnnotationViewController @@ -138,17 +142,47 @@ - (UIImage *)extractImage { #pragma mark - Gesture Handling - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { + if ([self.editingControls selectedSegmentIndex] != UISegmentedControlNoSegment || self.isDrawing ){ if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ self.currentAnnotation = [self annotationForCurrentMode]; [self.objects addObject:self.currentAnnotation]; self.currentAnnotation.sourceImage = self.image; [self.imageView insertSubview:self.currentAnnotation aboveSubview:self.imageView]; self.panStart = [gestureRecognizer locationInView:self.imageView]; + + [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; + self.isDrawing = YES; + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ CGPoint bla = [gestureRecognizer locationInView:self.imageView]; self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x - self.panStart.x, bla.y - self.panStart.y); self.currentAnnotation.movedDelta = CGSizeMake(bla.x - self.panStart.x, bla.y - self.panStart.y); self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + } else { + self.currentAnnotation = nil; + self.isDrawing = NO; + } + } else { + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + // find and possibly move an existing annotation. + BITImageAnnotation *selectedAnnotation = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationInView:self.view] withEvent:nil]; + + if ([self.objects indexOfObject:selectedAnnotation] != NSNotFound){ + self.currentAnnotation = selectedAnnotation; + } + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation){ + CGPoint delta = [gestureRecognizer translationInView:self.view]; + + CGRect annotationFrame = self.currentAnnotation.frame; + annotationFrame.origin.x += delta.x; + annotationFrame.origin.y += delta.y; + self.currentAnnotation.frame = annotationFrame; + + [gestureRecognizer setTranslation:CGPointZero inView:self.view]; + + } else { + self.currentAnnotation = nil; + } } } From 8c3351fdc530ff4d8a25cb9e03415664fa34960d Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Wed, 19 Mar 2014 11:05:33 +0100 Subject: [PATCH 036/206] + I'm in the middle of something here. --- Classes/BITImageAnnotationViewController.m | 112 +++++++++++++-------- 1 file changed, 68 insertions(+), 44 deletions(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 8e780f06..3dc2f27b 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -18,7 +18,6 @@ @interface BITImageAnnotationViewController () @property (nonatomic, strong) UISegmentedControl *editingControls; @property (nonatomic, strong) NSMutableArray *objects; @property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer; -@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; @property (nonatomic, strong) UIPinchGestureRecognizer *pinchRecognizer; @property (nonatomic) CGFloat scaleFactor; @@ -28,17 +27,19 @@ @interface BITImageAnnotationViewController () @property (nonatomic) BOOL isDrawing; +@property (nonatomic) CGRect pinchStartingFrame; + @end @implementation BITImageAnnotationViewController - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; - if (self) { - // Custom initialization - } - return self; + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Custom initialization + } + return self; } - (void)viewDidLoad @@ -50,7 +51,7 @@ - (void)viewDidLoad self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Rectangle", @"Arrow", @"Blur"]]; self.navigationItem.titleView = self.editingControls; - + self.objects = [NSMutableArray new]; @@ -60,7 +61,7 @@ - (void)viewDidLoad self.imageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; self.imageView.clipsToBounds = YES; - + self.imageView.image = self.image; self.imageView.contentMode = UIViewContentModeScaleToFill; @@ -68,12 +69,10 @@ - (void)viewDidLoad [self.view addSubview:self.imageView]; self.imageView.frame = self.view.bounds; - self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panned:)]; + self.pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinched:)]; - [self.tapRecognizer requireGestureRecognizerToFail:self.panRecognizer]; - - [self.imageView addGestureRecognizer:self.tapRecognizer]; + [self.imageView addGestureRecognizer:self.pinchRecognizer]; [self.imageView addGestureRecognizer:self.panRecognizer]; self.imageView.userInteractionEnabled = YES; @@ -89,8 +88,8 @@ - (void)viewDidLoad CGSize scaledImageSize = CGSizeMake(self.image.size.width * factor, self.image.size.height * factor); self.imageView.frame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height/2 - scaledImageSize.height/2, scaledImageSize.width, scaledImageSize.height); - - + + // Do any additional setup after loading the view. } @@ -133,7 +132,7 @@ - (UIImage *)extractImage { [annotation.layer renderInContext:ctx]; CGContextTranslateCTM(ctx,-1 * annotation.frame.origin.x,-1 * annotation.frame.origin.y); } - + UIImage *renderedImageOfMyself = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return renderedImageOfMyself; @@ -143,33 +142,33 @@ - (UIImage *)extractImage { - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { if ([self.editingControls selectedSegmentIndex] != UISegmentedControlNoSegment || self.isDrawing ){ - if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ - self.currentAnnotation = [self annotationForCurrentMode]; - [self.objects addObject:self.currentAnnotation]; - self.currentAnnotation.sourceImage = self.image; - [self.imageView insertSubview:self.currentAnnotation aboveSubview:self.imageView]; - self.panStart = [gestureRecognizer locationInView:self.imageView]; - - [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; - self.isDrawing = YES; - - } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ - CGPoint bla = [gestureRecognizer locationInView:self.imageView]; - self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x - self.panStart.x, bla.y - self.panStart.y); - self.currentAnnotation.movedDelta = CGSizeMake(bla.x - self.panStart.x, bla.y - self.panStart.y); - self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; - } else { - self.currentAnnotation = nil; - self.isDrawing = NO; - } + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + self.currentAnnotation = [self annotationForCurrentMode]; + [self.objects addObject:self.currentAnnotation]; + self.currentAnnotation.sourceImage = self.image; + [self.imageView insertSubview:self.currentAnnotation aboveSubview:self.imageView]; + self.panStart = [gestureRecognizer locationInView:self.imageView]; + + [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; + self.isDrawing = YES; + + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ + CGPoint bla = [gestureRecognizer locationInView:self.imageView]; + self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x - self.panStart.x, bla.y - self.panStart.y); + self.currentAnnotation.movedDelta = CGSizeMake(bla.x - self.panStart.x, bla.y - self.panStart.y); + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + } else { + self.currentAnnotation = nil; + self.isDrawing = NO; + } } else { if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ - // find and possibly move an existing annotation. - BITImageAnnotation *selectedAnnotation = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationInView:self.view] withEvent:nil]; - - if ([self.objects indexOfObject:selectedAnnotation] != NSNotFound){ + // find and possibly move an existing annotation. + BITImageAnnotation *selectedAnnotation = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationInView:self.view] withEvent:nil]; + + if ([self.objects indexOfObject:selectedAnnotation] != NSNotFound){ self.currentAnnotation = selectedAnnotation; - } + } } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation){ CGPoint delta = [gestureRecognizer translationInView:self.view]; @@ -187,15 +186,40 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { } --(void)tapped:(UITapGestureRecognizer *)gestureRecognizer { - - +-(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + // try to figure out which view we are talking about. + BITImageAnnotation *candidate = nil; + BOOL validView = YES; + + for ( int i = 0; i Date: Wed, 19 Mar 2014 11:55:44 +0100 Subject: [PATCH 037/206] + Pinch to Zoom --- Classes/BITImageAnnotationViewController.m | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 3dc2f27b..d5b944e1 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -207,8 +207,27 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { self.pinchStartingFrame = self.currentAnnotation.frame; } - } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation){ + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation && gestureRecognizer.numberOfTouches>1){ CGRect newFrame= (self.pinchStartingFrame); + NSLog(@"%f", [gestureRecognizer scale]); + + // upper point? + CGPoint point1 = [gestureRecognizer locationOfTouch:0 inView:self.view]; + CGPoint point2 = [gestureRecognizer locationOfTouch:1 inView:self.view]; + + + newFrame.origin.x = point1.x; + newFrame.origin.y = point1.y; + + newFrame.origin.x = (point1.x > point2.x) ? point2.x : point1.x; + newFrame.origin.y = (point1.y > point2.y) ? point2.y : point1.y; + + newFrame.size.width = (point1.x > point2.x) ? point1.x - point2.x : point2.x - point1.x; + newFrame.size.height = (point1.y > point2.y) ? point1.y - point2.y : point2.y - point1.y; + + + self.currentAnnotation.frame = newFrame; + // we } else { From eba31ba2790f536f321e229d125de27dbbfafc6d Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 19 Mar 2014 13:10:44 +0100 Subject: [PATCH 038/206] + Fixed Blur z-index. --- Classes/BITArrowImageAnnotation.m | 4 +--- Classes/BITImageAnnotation.h | 3 +++ Classes/BITImageAnnotation.m | 10 ++------- Classes/BITImageAnnotationViewController.m | 24 +++++++++++++++++++--- Classes/BITRectangleImageAnnotation.m | 10 +++------ 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index c45e5e2f..0edc1af0 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -66,9 +66,7 @@ - (void)buildShape { endY = CGRectGetMinY(self.bounds); } - - NSLog(@"Start X: %f, Y: %f, END: %f %f %@", startX, startY, endX,endY, self); - + UIBezierPath *path = [self bezierPathWithArrowFromPoint:CGPointMake(endX,endY) toPoint:CGPointMake(startX,startY) tailWidth:lineWidth headWidth:topHeight headLength:topHeight]; self.shapeLayer.path = path.CGPath; diff --git a/Classes/BITImageAnnotation.h b/Classes/BITImageAnnotation.h index 64493012..f848ee24 100644 --- a/Classes/BITImageAnnotation.h +++ b/Classes/BITImageAnnotation.h @@ -12,4 +12,7 @@ @property (nonatomic) CGSize movedDelta; @property (nonatomic, weak) UIImage *sourceImage; @property (nonatomic) CGRect imageFrame; + +-(BOOL)resizable; + @end diff --git a/Classes/BITImageAnnotation.m b/Classes/BITImageAnnotation.m index 644a27b3..d0e42614 100644 --- a/Classes/BITImageAnnotation.m +++ b/Classes/BITImageAnnotation.m @@ -21,14 +21,8 @@ - (id)initWithFrame:(CGRect)frame } - -/* -// Only override drawRect: if you perform custom drawing. -// An empty implementation adversely affects performance during animation. -- (void)drawRect:(CGRect)rect -{ - // Drawing code +-(BOOL)resizable { + return NO; } -*/ @end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index d5b944e1..d555fa88 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -52,7 +52,6 @@ - (void)viewDidLoad self.navigationItem.titleView = self.editingControls; - self.objects = [NSMutableArray new]; [self.editingControls addTarget:self action:@selector(editingAction:) forControlEvents:UIControlEventTouchUpInside]; @@ -146,7 +145,13 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { self.currentAnnotation = [self annotationForCurrentMode]; [self.objects addObject:self.currentAnnotation]; self.currentAnnotation.sourceImage = self.image; - [self.imageView insertSubview:self.currentAnnotation aboveSubview:self.imageView]; + + if (self.imageView.subviews.count > 0 && [self.currentAnnotation isKindOfClass:[BITBlurImageAnnotation class]]){ + [self.imageView insertSubview:self.currentAnnotation belowSubview:[self firstAnnotationThatIsNotBlur]]; + } else { + [self.imageView addSubview:self.currentAnnotation]; + } + self.panStart = [gestureRecognizer locationInView:self.imageView]; [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; @@ -176,6 +181,8 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { annotationFrame.origin.x += delta.x; annotationFrame.origin.y += delta.y; self.currentAnnotation.frame = annotationFrame; + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + [gestureRecognizer setTranslation:CGPointZero inView:self.view]; @@ -183,7 +190,16 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { self.currentAnnotation = nil; } } +} + +-(BITImageAnnotation *)firstAnnotationThatIsNotBlur { + for (BITImageAnnotation *annotation in self.imageView.subviews){ + if (![annotation isKindOfClass:[BITBlurImageAnnotation class]]){ + return annotation; + } + } + return self.imageView; } -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { @@ -202,7 +218,7 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { } } - if (validView){ + if (validView && [candidate resizable]){ self.currentAnnotation = candidate; self.pinchStartingFrame = self.currentAnnotation.frame; } @@ -227,6 +243,8 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { self.currentAnnotation.frame = newFrame; + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + // we diff --git a/Classes/BITRectangleImageAnnotation.m b/Classes/BITRectangleImageAnnotation.m index 5fd44c75..4709d4f4 100644 --- a/Classes/BITRectangleImageAnnotation.m +++ b/Classes/BITRectangleImageAnnotation.m @@ -50,13 +50,9 @@ - (void)layoutSubviews { self.strokeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath; } -/* -// Only override drawRect: if you perform custom drawing. -// An empty implementation adversely affects performance during animation. -- (void)drawRect:(CGRect)rect -{ - // Drawing code +-(BOOL)resizable { + return YES; } -*/ + @end From e0057f650e60a5e142ea9c978438d9a10c94e7ec Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 19 Mar 2014 13:14:38 +0100 Subject: [PATCH 039/206] + Removed some Deprecation warnings. --- Classes/BITAttachmentGalleryViewController.m | 2 +- Classes/BITFeedbackComposeViewController.m | 6 +++--- Classes/BITImageAnnotationViewController.m | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index 13ccfee5..0352b4d2 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -133,7 +133,7 @@ - (BOOL)prefersStatusBarHidden { } - (void)close:(id)sender { - [self dismissModalViewControllerAnimated:YES]; + [self dismissViewControllerAnimated:YES completion:nil]; } - (void)share:(id)sender { diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 689e7083..aaf2d245 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -393,7 +393,7 @@ -(void)addPhotoAction:(id)sender { pickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; pickerController.delegate = self; pickerController.editing = NO; - [self presentModalViewController:pickerController animated:YES]; + [self presentViewController:pickerController animated:YES completion:nil]; } #pragma mark - UIImagePickerControllerDelegate @@ -410,12 +410,12 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking [self.attachments addObject:newAttachment]; } - [picker dismissModalViewControllerAnimated:YES]; + [picker dismissViewControllerAnimated:YES completion:nil]; [self refreshAttachmentScrollview]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { - [picker dismissModalViewControllerAnimated:YES]; + [picker dismissViewControllerAnimated:YES completion:nil]; } - (void)imageButtonAction:(UIButton *)sender { diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index d555fa88..2c412b9e 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -110,13 +110,13 @@ - (BITImageAnnotation *)annotationForCurrentMode { - (void)discard:(id)sender { [self.delegate annotationControllerDidCancel:self]; - [self dismissModalViewControllerAnimated:YES]; + [self dismissViewControllerAnimated:YES completion:nil]; } - (void)save:(id)sender { UIImage *image = [self extractImage]; [self.delegate annotationController:self didFinishWithImage:image]; - [self dismissModalViewControllerAnimated:YES]; + [self dismissViewControllerAnimated:YES completion:nil]; } - (UIImage *)extractImage { @@ -192,7 +192,7 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { } } --(BITImageAnnotation *)firstAnnotationThatIsNotBlur { +-(UIView *)firstAnnotationThatIsNotBlur { for (BITImageAnnotation *annotation in self.imageView.subviews){ if (![annotation isKindOfClass:[BITBlurImageAnnotation class]]){ return annotation; From 7b19d21c49cf031af3b9c5051533ab3bf63225be Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 19 Mar 2014 13:27:55 +0100 Subject: [PATCH 040/206] + Vastly improved blur Annotation. --- Classes/BITBlurImageAnnotation.m | 21 ++++++++++++++------- Classes/BITImageAnnotationViewController.m | 4 ++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Classes/BITBlurImageAnnotation.m b/Classes/BITBlurImageAnnotation.m index 898ce2a3..9d8f762c 100644 --- a/Classes/BITBlurImageAnnotation.m +++ b/Classes/BITBlurImageAnnotation.m @@ -35,23 +35,30 @@ -(void)setSourceImage:(UIImage *)sourceImage { UIGraphicsBeginImageContext(size); [sourceImage drawInRect:CGRectMake(0, 0, size.width, size.height)]; self.scaledImage = UIGraphicsGetImageFromCurrentImageContext(); + self.imageLayer.shouldRasterize = YES; + self.imageLayer.rasterizationScale = 1; + self.imageLayer.magnificationFilter = kCAFilterNearest; self.imageLayer.contents = (id)self.scaledImage.CGImage; + + + UIGraphicsEndImageContext(); } - (void)layoutSubviews { [super layoutSubviews]; + + [CATransaction begin]; + [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; + self.imageLayer.frame = self.imageFrame; self.imageLayer.masksToBounds = YES; + + [CATransaction commit]; } -/* -// Only override drawRect: if you perform custom drawing. -// An empty implementation adversely affects performance during animation. -- (void)drawRect:(CGRect)rect -{ - // Drawing code +-(BOOL)resizable { + return YES; } -*/ @end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 2c412b9e..b8579cf6 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -162,6 +162,8 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x - self.panStart.x, bla.y - self.panStart.y); self.currentAnnotation.movedDelta = CGSizeMake(bla.x - self.panStart.x, bla.y - self.panStart.y); self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + [self.currentAnnotation setNeedsLayout]; + [self.currentAnnotation layoutIfNeeded]; } else { self.currentAnnotation = nil; self.isDrawing = NO; @@ -183,6 +185,8 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { self.currentAnnotation.frame = annotationFrame; self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + [self.currentAnnotation setNeedsLayout]; + [self.currentAnnotation layoutIfNeeded]; [gestureRecognizer setTranslation:CGPointZero inView:self.view]; From 14947a577c04207317a6f46d9a5df4ffa3e0f0b9 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 19 Mar 2014 13:39:56 +0100 Subject: [PATCH 041/206] + Re-Enabled the ability to well, submit, reviews. --- Classes/BITFeedbackComposeViewController.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index aaf2d245..6f147dcf 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -331,9 +331,9 @@ -(void)refreshAttachmentScrollview { - (void)updateBarButtonState { if (self.textView.text.length > 0 ) { - self.navigationItem.rightBarButtonItem.enabled = NO; - } else { self.navigationItem.rightBarButtonItem.enabled = YES; + } else { + self.navigationItem.rightBarButtonItem.enabled = NO; } } From 1c2b743b3b723370d7ea3fbba87adf208734be75 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 19 Mar 2014 14:37:06 +0100 Subject: [PATCH 042/206] + Improved gallery display. --- Classes/BITAttachmentGalleryViewController.m | 43 ++++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index 0352b4d2..60fa6ff9 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -17,6 +17,7 @@ @interface BITAttachmentGalleryViewController () @property (nonatomic, strong) NSArray *imageViews; @property (nonatomic, strong) NSArray *extractedAttachments; @property (nonatomic) NSInteger currentIndex; +@property (nonatomic) NSInteger loadedImageIndex; @property (nonatomic, strong) UITapGestureRecognizer *tapognizer; @end @@ -35,17 +36,28 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)viewDidLoad { [super viewDidLoad]; + self.automaticallyAdjustsScrollViewInsets = NO; + self.navigationController.navigationBar.translucent = YES; + self.edgesForExtendedLayout = YES; + self.extendedLayoutIncludesOpaqueBars = YES; self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Close" style:UIBarButtonItemStylePlain target:self action:@selector(close:)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(share:)]; + + + self.currentIndex = 0; + + [self extractUsableAttachments]; + [self setupScrollView]; + + [self layoutViews]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - [self extractUsableAttachments]; - [self setupScrollView]; + self.tapognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; [self.view addGestureRecognizer:self.tapognizer]; @@ -53,6 +65,7 @@ - (void)viewDidAppear:(BOOL)animated { } - (void)setupScrollView { self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; + self.view.autoresizesSubviews = NO; [self.view addSubview:self.scrollView]; self.scrollView.delegate = self; self.scrollView.pagingEnabled = YES; @@ -85,6 +98,8 @@ - (void)extractUsableAttachments { } self.extractedAttachments = extractedOnes; + + [self layoutViews]; } - (void)didReceiveMemoryWarning @@ -99,17 +114,7 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // requeue elements. NSInteger baseIndex = MAX(0,self.currentIndex-1); [self layoutViews]; - NSInteger z = baseIndex; - - - - for ( NSInteger i = baseIndex; i < MIN(baseIndex+2, self.extractedAttachments.count);i++ ){ - UIImageView *imageView = self.imageViews[z]; - BITFeedbackMessageAttachment *attachment = self.extractedAttachments[i]; - imageView.image =[attachment imageRepresentation]; - z++; - } - + } } @@ -123,13 +128,17 @@ - (void)layoutViews { NSInteger z = baseIndex; for ( NSInteger i = baseIndex; i < MIN(baseIndex+2, self.extractedAttachments.count);i++ ){ UIImageView *imageView = self.imageViews[z]; + BITFeedbackMessageAttachment *attachment = self.extractedAttachments[i]; + imageView.image =[attachment imageRepresentation]; imageView.frame = [self frameForItemAtIndex:i]; z++; } + + } - (BOOL)prefersStatusBarHidden { - return YES; + return self.navigationController.navigationBarHidden; } - (void)close:(id)sender { @@ -146,12 +155,12 @@ - (void)share:(id)sender { - (void)tapped:(UITapGestureRecognizer *)tapRecognizer { if (self.navigationController.navigationBarHidden){ [[UIApplication sharedApplication] setStatusBarHidden:NO]; - self.navigationController.navigationBarHidden = NO; + [self.navigationController setNavigationBarHidden:NO animated:YES]; } else { + [self.navigationController setNavigationBarHidden:YES animated:YES]; [[UIApplication sharedApplication] setStatusBarHidden:YES]; - self.navigationController.navigationBarHidden = YES; + } - [self layoutViews]; } - (CGRect)frameForItemAtIndex:(NSInteger)index { From 252137463c228242d016c65a9424e3b3d0b552f4 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 24 Mar 2014 12:07:03 +0100 Subject: [PATCH 043/206] + Refined the documentation --- Classes/BITFeedbackComposeViewController.h | 3 ++- Classes/BITFeedbackManager.h | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Classes/BITFeedbackComposeViewController.h b/Classes/BITFeedbackComposeViewController.h index 5df188c1..6bc132e6 100644 --- a/Classes/BITFeedbackComposeViewController.h +++ b/Classes/BITFeedbackComposeViewController.h @@ -72,7 +72,8 @@ - NSURL - UIImage - These are automatically concatenated to one text string. + These are automatically concatenated to one text string, while any image attachments are + added as attachments to the feedback. @param items Array of data objects to prefill the feedback text message. */ diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index e4059d1d..cfc744f5 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -294,6 +294,13 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { */ - (BITFeedbackComposeViewController *)feedbackComposeViewController; +/** + Set the so-called feedback observation mode. Depending on the chosen mode, + the feedback manager will automatically launch once the event has been detected. + + You can choose from the modes in BITFeedbackObservationMode. The default mode is + BITFeedbackObservationNone. + */ - (void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode; From c62b444272645f428abfef314e607e24e0deea2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Fri, 28 Mar 2014 12:30:45 +0100 Subject: [PATCH 044/206] start refactoring AlertView delegate --- Classes/BITCrashManager.h | 14 ++++++++++ Classes/BITCrashManager.m | 55 ++++++++++++++++++++------------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index bf651da5..a524c2b6 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -58,6 +58,16 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { BITCrashManagerStatusAutoSend = 2 }; +typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { + + BITCrashManagerUserInputDontSend, + + BITCrashManagerUserInputSend, + + BITCrashManagerUserInputAlwaysSend + +}; + @protocol BITCrashManagerDelegate; @@ -163,6 +173,10 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { * the debugger during runtime, this may cause issues the Mach exception handler is enabled! * @see isDebuggerAttached */ + +@property (nonatomic, assign) BITCrashManagerUserInput crashManagerUserInput; + + @property (nonatomic, assign, getter=isMachExceptionHandlerEnabled) BOOL enableMachExceptionHandler; diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 18b3aaf2..be655b91 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -930,34 +930,35 @@ - (void)sendCrashReports { #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: { - _crashManagerStatus = BITCrashManagerStatusAutoSend; - [[NSUserDefaults standardUserDefaults] setInteger:_crashManagerStatus forKey:kBITCrashManagerStatus]; - [[NSUserDefaults standardUserDefaults] synchronize]; - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReportsAlways:)]) { - [self.delegate crashManagerWillSendCrashReportsAlways:self]; - } - - [self sendCrashReports]; - break; + _crashManagerUserInput = buttonIndex; + [self handleUserInput]; +} + +- (void)handleUserInput { + switch (_crashManagerUserInput) { + case BITCrashManagerUserInputDontSend: + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { + [self.delegate crashManagerWillCancelSendingCrashReport:self]; + } + + _sendingInProgress = NO; + [self cleanCrashReports]; + break; + case BITCrashManagerUserInputSend: + [self sendCrashReports]; + break; + case BITCrashManagerUserInputAlwaysSend: + _crashManagerStatus = BITCrashManagerStatusAutoSend; + [[NSUserDefaults standardUserDefaults] setInteger:_crashManagerStatus forKey:kBITCrashManagerStatus]; + [[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; - } + } From 50b0eb9bd1591d625a2c6c48c9a10823a50d122e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Fri, 28 Mar 2014 12:39:06 +0100 Subject: [PATCH 045/206] remove unnecessary property --- Classes/BITCrashManager.h | 4 ---- Classes/BITCrashManager.m | 7 +++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index a524c2b6..dfff2f67 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -173,10 +173,6 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { * the debugger during runtime, this may cause issues the Mach exception handler is enabled! * @see isDebuggerAttached */ - -@property (nonatomic, assign) BITCrashManagerUserInput crashManagerUserInput; - - @property (nonatomic, assign, getter=isMachExceptionHandlerEnabled) BOOL enableMachExceptionHandler; diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index be655b91..8dfff286 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -930,12 +930,11 @@ - (void)sendCrashReports { #pragma mark - UIAlertView Delegate - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { - _crashManagerUserInput = buttonIndex; - [self handleUserInput]; + [self handleUserInput:buttonIndex]; } -- (void)handleUserInput { - switch (_crashManagerUserInput) { +- (void)handleUserInput:(BITCrashManagerUserInput)userInput { + switch (userInput) { case BITCrashManagerUserInputDontSend: if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { [self.delegate crashManagerWillCancelSendingCrashReport:self]; From 1f96aaab5ee49a0b46490ab0e9065f596de34187 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 3 Apr 2014 19:43:53 +0200 Subject: [PATCH 046/206] Fix installString not being added to the crash report --- Classes/BITCrashManager.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 4164ec22..bbdef084 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -1029,6 +1029,8 @@ - (void)sendCrashReports { continue; } + installString = bit_appAnonID() ?: @""; + if (report) { if (report.uuidRef != NULL) { crashUUID = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); @@ -1045,8 +1047,6 @@ - (void)sendCrashReports { } } - installString = bit_appAnonID() ?: @""; - if (crashes == nil) { crashes = [NSMutableString string]; } From e66714afcaeb72f3bb753564ce556e62902fc08b Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 3 Apr 2014 17:34:15 +0200 Subject: [PATCH 047/206] Define our own crash callback struct, so we don't need the PLCrashReporter headers to be public any longer --- Classes/BITCrashManager.h | 44 ++++++++++++++++----- Classes/BITCrashManager.m | 39 +++++++++++++++++- Classes/BITCrashManagerPrivate.h | 2 + Support/HockeySDK.xcodeproj/project.pbxproj | 2 +- 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index b958cc89..13b605ec 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -32,14 +32,6 @@ #import "BITHockeyBaseManager.h" -// We need this check depending on integrating as a subproject or using the binary distribution -#if __has_include("CrashReporter.h") -#import "CrashReporter.h" -#else -#import -#endif - - /** * Crash Manager status */ @@ -59,6 +51,35 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { }; +/** + * Prototype of a callback function used to execute additional user code. Called upon completion of crash + * handling, after the crash report has been written to disk. + * + * @param context The API client's supplied context value. + * + * @see `BITCrashManagerCallbacks` + * @see `[BITCrashManager setCrashCallbacks:]` + */ +typedef void (*BITCrashManagerPostCrashSignalCallback)(void *context); + +/** + * This structure contains callbacks supported by `BITCrashManager` to allow the host application to perform + * additional tasks prior to program termination after a crash has occured. + * + * @see `BITCrashManagerPostCrashSignalCallback` + * @see `[BITCrashManager setCrashCallbacks:]` + */ +typedef struct BITCrashManagerCallbacks { + /** An arbitrary user-supplied context value. This value may be NULL. */ + void *context; + + /** + * The callback used to report caught signal information. + */ + BITCrashManagerPostCrashSignalCallback handleSignal; +} BITCrashManagerCallbacks; + + @protocol BITCrashManagerDelegate; /** @@ -234,15 +255,18 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { * * _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, see https://www.securecoding.cert.org/confluence/display/seccode/SIG30-C.+Call+only+asynchronous-safe+functions+within+signal+handlers + * 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](https://www.securecoding.cert.org/confluence/display/seccode/SIG30-C.+Call+only+asynchronous-safe+functions+within+signal+handlers). * * Most notably, the Objective-C runtime itself is not async-safe, and Objective-C may not be used within a signal handler. * * Documentation taken from PLCrashReporter: https://www.plcrashreporter.org/documentation/api/v1.2-rc2/async_safety.html * + * @see `BITCrashManagerPostCrashSignalCallback` + * @see `BITCrashManagerCallbacks` + * * @param callbacks A pointer to an initialized PLCrashReporterCallback structure, see https://www.plcrashreporter.org/documentation/api/v1.2-rc2/struct_p_l_crash_reporter_callbacks.html */ -- (void)setCrashCallbacks: (PLCrashReporterCallbacks *) callbacks; +- (void)setCrashCallbacks: (BITCrashManagerCallbacks *) callbacks; /** diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index bbdef084..867ffc93 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -32,6 +32,8 @@ #if HOCKEYSDK_FEATURE_CRASH_REPORTER +#import + #import #import @@ -69,6 +71,25 @@ NSString *const kBITFakeCrashAppBinaryUUID = @"BITFakeCrashAppBinaryUUID"; NSString *const kBITFakeCrashReport = @"BITFakeCrashAppString"; + +static BITCrashManagerCallbacks bitCrashCallbacks = { + .context = NULL, + .handleSignal = NULL +}; + +// proxy implementation for PLCrashReporter to keep our interface stable while this can change +static void plcr_post_crash_callback (siginfo_t *info, ucontext_t *uap, void *context) { + if (bitCrashCallbacks.handleSignal != NULL) + bitCrashCallbacks.handleSignal(context); +} + +static PLCrashReporterCallbacks plCrashCallbacks = { + .version = 0, + .context = NULL, + .handleSignal = plcr_post_crash_callback +}; + + @interface BITCrashManager () @property (nonatomic, strong) NSFileManager *fileManager; @@ -498,8 +519,22 @@ - (NSString *)userEmailForCrashReport { #pragma mark - Public -- (void)setCrashCallbacks: (PLCrashReporterCallbacks *) callbacks { - _crashCallBacks = callbacks; +/** + * Set the callback for PLCrashReporter + * + * @param callbacks BITCrashManagerCallbacks instance + */ +- (void)setCrashCallbacks: (BITCrashManagerCallbacks *) callbacks { + if (!callbacks) return; + + // set our proxy callback struct + bitCrashCallbacks.context = callbacks->context; + bitCrashCallbacks.handleSignal = callbacks->handleSignal; + + // set the PLCrashReporterCallbacks struct + plCrashCallbacks.context = callbacks->context; + + _crashCallBacks = &plCrashCallbacks; } /** diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index f2231342..7951cc7a 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -31,6 +31,8 @@ #if HOCKEYSDK_FEATURE_CRASH_REPORTER +#import + @interface BITCrashManager () { } diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 910428df..a661402c 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -833,7 +833,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Sets the target folders and the final framework product.\nFMK_NAME=HockeySDK\nFMK_VERSION=A\nFMK_RESOURCE_BUNDLE=HockeySDKResources\n\n# Documentation\nHOCKEYSDK_DOCSET_VERSION_NAME=\"de.bitstadium.${HOCKEYSDK_DOCSET_NAME}-${VERSION_STRING}\"\n\n# Install dir will be the final output to the framework.\n# The following line create it in the root folder of the current project.\nPRODUCTS_DIR=${SRCROOT}/../Products\nPLCR_DIR=${SRCROOT}/../Vendor/CrashReporter.framework\nZIP_FOLDER=HockeySDK-iOS\nTEMP_DIR=${PRODUCTS_DIR}/${ZIP_FOLDER}\nINSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.framework\n\n# Working dir will be deleted after the framework creation.\nWRK_DIR=build\nDEVICE_DIR=${WRK_DIR}/Release-iphoneos\nSIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator\nHEADERS_DIR=${WRK_DIR}/Release-iphoneos/usr/local/include\n\n# Building both architectures.\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphoneos\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphonesimulator\n\n# Cleaning the oldest.\nif [ -d \"${TEMP_DIR}\" ]\nthen\nrm -rf \"${TEMP_DIR}\"\nfi\n\n# Creates and renews the final product folder.\nmkdir -p \"${INSTALL_DIR}\"\nmkdir -p \"${INSTALL_DIR}/Versions\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers\"\n\n# Creates the internal links.\n# It MUST uses relative path, otherwise will not work when the folder is copied/moved.\nln -s \"${FMK_VERSION}\" \"${INSTALL_DIR}/Versions/Current\"\nln -s \"Versions/Current/Headers\" \"${INSTALL_DIR}/Headers\"\nln -s \"Versions/Current/Resources\" \"${INSTALL_DIR}/Resources\"\nln -s \"Versions/Current/${FMK_NAME}\" \"${INSTALL_DIR}/${FMK_NAME}\"\n\n# Copies the headers and resources files to the final product folder.\ncp -R \"${SRCROOT}/build/Release-iphoneos/include/HockeySDK/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${PLCR_DIR}/Versions/A/Headers/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${DEVICE_DIR}/${FMK_RESOURCE_BUNDLE}.bundle\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\ncp -f \"${SRCROOT}/${FMK_NAME}.xcconfig\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\n\n# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.\nlipo -create \"${DEVICE_DIR}/lib${FMK_NAME}.a\" \"${SIMULATOR_DIR}/lib${FMK_NAME}.a\" -output \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\"\n\n# Combine the CrashReporter static library into a new Hockey static library file if they are not already present and copy the public headers too\nif [ -z $(otool -L \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" | grep 'libCrashReporter') ]\nthen\nlibtool -static -o \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${SRCROOT}/../Vendor/CrashReporter.framework/Versions/A/CrashReporter\"\nfi\n\nrm -r \"${WRK_DIR}\"\n\n# build embeddedframework folder and move framework into it\nmkdir \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework\"\nmv \"${INSTALL_DIR}\" \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework/${FMK_NAME}.framework\"\n\n# link Resources\nNEW_INSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.embeddedframework\nmkdir \"${NEW_INSTALL_DIR}/Resources\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_RESOURCE_BUNDLE}.bundle\" \"${NEW_INSTALL_DIR}/Resources/${FMK_RESOURCE_BUNDLE}.bundle\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_NAME}.xcconfig\" \"${NEW_INSTALL_DIR}/Resources/${FMK_NAME}.xcconfig\"\n\n# copy license, changelog, documentation, integration json\ncp -f \"${SRCROOT}/../Docs/Changelog-template.md\" \"${TEMP_DIR}/CHANGELOG\"\ncp -f \"${SRCROOT}/../Docs/Guide-Installation-Setup-template.md\" \"${TEMP_DIR}/README.md\"\ncp -f \"${SRCROOT}/../LICENSE\" \"${TEMP_DIR}\"\nmkdir \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\ncp -R \"${SRCROOT}/../documentation/docset/Contents\" \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\n\n# build zip\ncd \"${PRODUCTS_DIR}\"\nrm -f \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nzip -yr \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${ZIP_FOLDER}\" -x \\*/.*\n\n#copy to output dir on cisimple\nif [ $CISIMPLE ]; then\n if [ ! -d \"${CONFIGURATION_BUILD_DIR}\" ]; then\n mkdir \"${CONFIGURATION_BUILD_DIR}\"\n fi\n cd \"${PRODUCTS_DIR}\"\n cp \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${CONFIGURATION_BUILD_DIR}/${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nfi"; + shellScript = "# Sets the target folders and the final framework product.\nFMK_NAME=HockeySDK\nFMK_VERSION=A\nFMK_RESOURCE_BUNDLE=HockeySDKResources\n\n# Documentation\nHOCKEYSDK_DOCSET_VERSION_NAME=\"de.bitstadium.${HOCKEYSDK_DOCSET_NAME}-${VERSION_STRING}\"\n\n# Install dir will be the final output to the framework.\n# The following line create it in the root folder of the current project.\nPRODUCTS_DIR=${SRCROOT}/../Products\nZIP_FOLDER=HockeySDK-iOS\nTEMP_DIR=${PRODUCTS_DIR}/${ZIP_FOLDER}\nINSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.framework\n\n# Working dir will be deleted after the framework creation.\nWRK_DIR=build\nDEVICE_DIR=${WRK_DIR}/Release-iphoneos\nSIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator\nHEADERS_DIR=${WRK_DIR}/Release-iphoneos/usr/local/include\n\n# Building both architectures.\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphoneos\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphonesimulator\n\n# Cleaning the oldest.\nif [ -d \"${TEMP_DIR}\" ]\nthen\nrm -rf \"${TEMP_DIR}\"\nfi\n\n# Creates and renews the final product folder.\nmkdir -p \"${INSTALL_DIR}\"\nmkdir -p \"${INSTALL_DIR}/Versions\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers\"\n\n# Creates the internal links.\n# It MUST uses relative path, otherwise will not work when the folder is copied/moved.\nln -s \"${FMK_VERSION}\" \"${INSTALL_DIR}/Versions/Current\"\nln -s \"Versions/Current/Headers\" \"${INSTALL_DIR}/Headers\"\nln -s \"Versions/Current/Resources\" \"${INSTALL_DIR}/Resources\"\nln -s \"Versions/Current/${FMK_NAME}\" \"${INSTALL_DIR}/${FMK_NAME}\"\n\n# Copies the headers and resources files to the final product folder.\ncp -R \"${SRCROOT}/build/Release-iphoneos/include/HockeySDK/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${DEVICE_DIR}/${FMK_RESOURCE_BUNDLE}.bundle\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\ncp -f \"${SRCROOT}/${FMK_NAME}.xcconfig\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\n\n# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.\nlipo -create \"${DEVICE_DIR}/lib${FMK_NAME}.a\" \"${SIMULATOR_DIR}/lib${FMK_NAME}.a\" -output \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\"\n\n# Combine the CrashReporter static library into a new Hockey static library file if they are not already present and copy the public headers too\nif [ -z $(otool -L \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" | grep 'libCrashReporter') ]\nthen\nlibtool -static -o \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${SRCROOT}/../Vendor/CrashReporter.framework/Versions/A/CrashReporter\"\nfi\n\nrm -r \"${WRK_DIR}\"\n\n# build embeddedframework folder and move framework into it\nmkdir \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework\"\nmv \"${INSTALL_DIR}\" \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework/${FMK_NAME}.framework\"\n\n# link Resources\nNEW_INSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.embeddedframework\nmkdir \"${NEW_INSTALL_DIR}/Resources\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_RESOURCE_BUNDLE}.bundle\" \"${NEW_INSTALL_DIR}/Resources/${FMK_RESOURCE_BUNDLE}.bundle\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_NAME}.xcconfig\" \"${NEW_INSTALL_DIR}/Resources/${FMK_NAME}.xcconfig\"\n\n# copy license, changelog, documentation, integration json\ncp -f \"${SRCROOT}/../Docs/Changelog-template.md\" \"${TEMP_DIR}/CHANGELOG\"\ncp -f \"${SRCROOT}/../Docs/Guide-Installation-Setup-template.md\" \"${TEMP_DIR}/README.md\"\ncp -f \"${SRCROOT}/../LICENSE\" \"${TEMP_DIR}\"\nmkdir \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\ncp -R \"${SRCROOT}/../documentation/docset/Contents\" \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\n\n# build zip\ncd \"${PRODUCTS_DIR}\"\nrm -f \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nzip -yr \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${ZIP_FOLDER}\" -x \\*/.*\n\n#copy to output dir on cisimple\nif [ $CISIMPLE ]; then\nif [ ! -d \"${CONFIGURATION_BUILD_DIR}\" ]; then\nmkdir \"${CONFIGURATION_BUILD_DIR}\"\nfi\ncd \"${PRODUCTS_DIR}\"\ncp \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${CONFIGURATION_BUILD_DIR}/${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nfi"; }; 1E8E66B215BC3D8200632A2E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; From 06adc38535ee44d79d1479bf81fccafff9c78c75 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 3 Apr 2014 19:57:35 +0200 Subject: [PATCH 048/206] Add a generic object which contains some basic information about the crash in the last session --- Classes/BITCrashDetails.h | 35 ++++++++++++++++++ Classes/BITCrashDetails.m | 33 +++++++++++++++++ Classes/BITCrashManager.h | 13 +++---- Classes/BITCrashManager.m | 39 +++++++++++++++++++-- Classes/HockeySDK.h | 1 + Support/HockeySDK.xcodeproj/project.pbxproj | 8 +++++ 6 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 Classes/BITCrashDetails.h create mode 100644 Classes/BITCrashDetails.m diff --git a/Classes/BITCrashDetails.h b/Classes/BITCrashDetails.h new file mode 100644 index 00000000..1c5988f7 --- /dev/null +++ b/Classes/BITCrashDetails.h @@ -0,0 +1,35 @@ +// +// BITCrashDetails.h +// HockeySDK +// +// Created by Andreas Linde on 03.04.14. +// +// + +#import + +@interface BITCrashDetails : NSObject + +@property (nonatomic, readonly, strong) NSString *incidentIdentifier; + +@property (nonatomic, readonly, strong) NSString *reporterKey; + +@property (nonatomic, readonly, strong) NSString *signal; + +@property (nonatomic, readonly, strong) NSString *exceptionName; + +@property (nonatomic, readonly, strong) NSString *exceptionReason; + +@property (nonatomic, readonly, strong) NSDate *appStartTime; + +@property (nonatomic, readonly, strong) NSDate *crashTime; + +- (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier + reporterKey:(NSString *)reporterKey + signal:(NSString *)signal + exceptionName:(NSString *)exceptionName + exceptionReason:(NSString *)exceptionReason + appStartTime:(NSDate *)appStartTime + crashTime:(NSDate *)crashTime; + +@end diff --git a/Classes/BITCrashDetails.m b/Classes/BITCrashDetails.m new file mode 100644 index 00000000..d201fac9 --- /dev/null +++ b/Classes/BITCrashDetails.m @@ -0,0 +1,33 @@ +// +// BITCrashDetails.m +// HockeySDK +// +// Created by Andreas Linde on 03.04.14. +// +// + +#import "BITCrashDetails.h" + +@implementation BITCrashDetails + +- (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier + reporterKey:(NSString *)reporterKey + signal:(NSString *)signal + exceptionName:(NSString *)exceptionName + exceptionReason:(NSString *)exceptionReason + appStartTime:(NSDate *)appStartTime + crashTime:(NSDate *)crashTime; +{ + if ((self = [super init])) { + _incidentIdentifier = incidentIdentifier; + _reporterKey = reporterKey; + _signal = signal; + _exceptionName = exceptionName; + _exceptionReason = exceptionReason; + _appStartTime = appStartTime; + _crashTime = crashTime; + } + return self; +} + +@end diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index b958cc89..6025d95b 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -32,12 +32,7 @@ #import "BITHockeyBaseManager.h" -// We need this check depending on integrating as a subproject or using the binary distribution -#if __has_include("CrashReporter.h") -#import "CrashReporter.h" -#else -#import -#endif +@class BITCrashDetails; /** @@ -299,6 +294,12 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { @property (nonatomic, readonly) BOOL wasKilledInLastSession; +/** + * Provides details about the crash that occured in the last app session + */ +@property (nonatomic, readonly) BITCrashDetails *lastSessionCrashDetails; + + /** Indicates if the app did receive a low memory warning in the last session diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index bbdef084..6ce74427 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -612,8 +612,12 @@ - (void) handleCrashReport { if (report == nil) { BITHockeyLog(@"WARNING: Could not parse crash report"); } else { + NSDate *appStartTime = nil; + NSDate *appCrashTime = nil; if ([report.processInfo respondsToSelector:@selector(processStartTime)]) { if (report.systemInfo.timestamp && report.processInfo.processStartTime) { + appStartTime = report.processInfo.processStartTime; + appCrashTime =report.systemInfo.timestamp; _timeintervalCrashInLastSessionOccured = [report.systemInfo.timestamp timeIntervalSinceDate:report.processInfo.processStartTime]; } } @@ -621,6 +625,22 @@ - (void) handleCrashReport { [crashData writeToFile:[_crashesDir stringByAppendingPathComponent: cacheFilename] atomically:YES]; [self storeMetaDataForCrashReportFilename:cacheFilename]; + + NSString *incidentIdentifier = @"???"; + if (report.uuidRef != NULL) { + incidentIdentifier = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); + } + + NSString *reporterKey = bit_appAnonID() ?: @""; + + _lastSessionCrashDetails = [[BITCrashDetails alloc] initWithIncidentIdentifier:incidentIdentifier + reporterKey:reporterKey + signal:report.signalInfo.name + exceptionName:report.exceptionInfo.exceptionName + exceptionReason:report.exceptionInfo.exceptionReason + appStartTime:appStartTime + crashTime:appCrashTime + ]; } } } @@ -896,6 +916,7 @@ - (void)startManager { */ - (void)createCrashReportForAppKill { NSString *fakeReportUUID = bit_UUID(); + NSString *fakeReporterKey = bit_appAnonID() ?: @"???"; NSString *fakeReportAppVersion = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppVersion]; if (!fakeReportAppVersion) @@ -906,10 +927,12 @@ - (void)createCrashReportForAppKill { NSString *fakeReportDeviceModel = [self getDevicePlatform] ?: @"Unknown"; NSString *fakeReportAppUUIDs = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppUUIDs] ?: @""; + NSString *fakeSignalName = @"SIGKILL"; + NSMutableString *fakeReportString = [NSMutableString string]; [fakeReportString appendFormat:@"Incident Identifier: %@\n", fakeReportUUID]; - [fakeReportString appendFormat:@"CrashReporter Key: %@\n", bit_appAnonID() ?: @"???"]; + [fakeReportString appendFormat:@"CrashReporter Key: %@\n", fakeReporterKey]; [fakeReportString appendFormat:@"Hardware Model: %@\n", fakeReportDeviceModel]; [fakeReportString appendFormat:@"Identifier: %@\n", fakeReportAppBundleIdentifier]; [fakeReportString appendFormat:@"Version: %@\n", fakeReportAppVersion]; @@ -921,13 +944,14 @@ - (void)createCrashReportForAppKill { [rfc3339Formatter setLocale:enUSPOSIXLocale]; [rfc3339Formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; [rfc3339Formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + NSString *fakeCrashTimestamp = [rfc3339Formatter stringFromDate:[NSDate date]]; // we use the current date, since we don't know when the kill actually happened - [fakeReportString appendFormat:@"Date/Time: %@\n", [rfc3339Formatter stringFromDate:[NSDate date]]]; + [fakeReportString appendFormat:@"Date/Time: %@\n", fakeCrashTimestamp]; [fakeReportString appendFormat:@"OS Version: %@\n", fakeReportOSVersion]; [fakeReportString appendString:@"Report Version: 104\n"]; [fakeReportString appendString:@"\n"]; - [fakeReportString appendString:@"Exception Type: SIGKILL\n"]; + [fakeReportString appendFormat:@"Exception Type: %@\n", fakeSignalName]; [fakeReportString appendString:@"Exception Codes: 00000020 at 0x8badf00d\n"]; [fakeReportString appendString:@"\n"]; [fakeReportString appendString:@"Application Specific Information:\n"]; @@ -950,6 +974,15 @@ - (void)createCrashReportForAppKill { [rootObj setObject:fakeReportAppUUIDs forKey:kBITFakeCrashAppBinaryUUID]; [rootObj setObject:fakeReportString forKey:kBITFakeCrashReport]; + _lastSessionCrashDetails = [[BITCrashDetails alloc] initWithIncidentIdentifier:fakeReportUUID + reporterKey:fakeReporterKey + signal:fakeSignalName + exceptionName:nil + exceptionReason:nil + appStartTime:nil + crashTime:nil + ]; + NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)rootObj format:NSPropertyListBinaryFormat_v1_0 errorDescription:&errorString]; diff --git a/Classes/HockeySDK.h b/Classes/HockeySDK.h index aa1c4da3..fe9d7b98 100644 --- a/Classes/HockeySDK.h +++ b/Classes/HockeySDK.h @@ -38,6 +38,7 @@ #if HOCKEYSDK_FEATURE_CRASH_REPORTER #import "BITCrashManager.h" #import "BITCrashManagerDelegate.h" +#import "BITCrashDetails.h" #endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ #if HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 910428df..8627e93a 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -111,6 +111,8 @@ 1E7A45FC16F54FB5005B08F1 /* OCHamcrestIOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E7A45FA16F54FB5005B08F1 /* OCHamcrestIOS.framework */; }; 1E7A45FD16F54FB5005B08F1 /* OCMockitoIOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E7A45FB16F54FB5005B08F1 /* OCMockitoIOS.framework */; }; 1E84DB3417E099BA00AC83FD /* HockeySDKFeatureConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E84DB3317E0977C00AC83FD /* HockeySDKFeatureConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1E90FD7318EDB86400CF0417 /* BITCrashDetails.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1E90FD7418EDB86400CF0417 /* BITCrashDetails.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */; }; 1E94F9E116E91330006570AD /* BITStoreUpdateManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E94F9DF16E91330006570AD /* BITStoreUpdateManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1E94F9E216E91330006570AD /* BITStoreUpdateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E94F9E016E91330006570AD /* BITStoreUpdateManager.m */; }; 1E94F9E416E9136B006570AD /* BITStoreUpdateManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E94F9E316E9136B006570AD /* BITStoreUpdateManagerPrivate.h */; }; @@ -268,6 +270,8 @@ 1E7A45FA16F54FB5005B08F1 /* OCHamcrestIOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCHamcrestIOS.framework; sourceTree = ""; }; 1E7A45FB16F54FB5005B08F1 /* OCMockitoIOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCMockitoIOS.framework; sourceTree = ""; }; 1E84DB3317E0977C00AC83FD /* HockeySDKFeatureConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HockeySDKFeatureConfig.h; sourceTree = ""; }; + 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashDetails.h; sourceTree = ""; }; + 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashDetails.m; sourceTree = ""; }; 1E94F9DF16E91330006570AD /* BITStoreUpdateManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITStoreUpdateManager.h; sourceTree = ""; }; 1E94F9E016E91330006570AD /* BITStoreUpdateManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITStoreUpdateManager.m; sourceTree = ""; }; 1E94F9E316E9136B006570AD /* BITStoreUpdateManagerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITStoreUpdateManagerPrivate.h; sourceTree = ""; }; @@ -478,6 +482,8 @@ children = ( 1E754E561621FBB70070AB92 /* BITCrashManager.h */, 1E754E571621FBB70070AB92 /* BITCrashManager.m */, + 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */, + 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */, 1EFF03D717F20F8300A5F13C /* BITCrashManagerPrivate.h */, 1E754E581621FBB70070AB92 /* BITCrashManagerDelegate.h */, 1E754E5A1621FBB70070AB92 /* BITCrashReportTextFormatter.h */, @@ -635,6 +641,7 @@ 1E5955FD15B7877B00A03429 /* BITHockeyManagerDelegate.h in Headers */, 1E754E5C1621FBB70070AB92 /* BITCrashManager.h in Headers */, 1E754E5E1621FBB70070AB92 /* BITCrashManagerDelegate.h in Headers */, + 1E90FD7318EDB86400CF0417 /* BITCrashDetails.h in Headers */, 1E49A4731612226D00463151 /* BITUpdateManager.h in Headers */, 1E49A4791612226D00463151 /* BITUpdateManagerDelegate.h in Headers */, 1E49A44E1612223B00463151 /* BITFeedbackManager.h in Headers */, @@ -886,6 +893,7 @@ 1E49A4C1161222B900463151 /* BITHockeyHelper.m in Sources */, 1E49A4C7161222B900463151 /* BITAppStoreHeader.m in Sources */, 1E49A4CD161222B900463151 /* BITStoreButton.m in Sources */, + 1E90FD7418EDB86400CF0417 /* BITCrashDetails.m in Sources */, 1E49A4D3161222B900463151 /* BITWebTableViewCell.m in Sources */, E48A3DED17B3ED1C00924C3D /* BITAuthenticator.m in Sources */, 1E49A4DB161222D400463151 /* HockeySDKPrivate.m in Sources */, From 96c60ec3947bf859af1908e287c96c93a7b1819b Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Fri, 11 Apr 2014 14:57:23 +0200 Subject: [PATCH 049/206] + Added Icons to the Editing Screens. --- Classes/BITImageAnnotationViewController.m | 79 +++++++++++++++----- Resources/Arrow.png | Bin 0 -> 394 bytes Resources/Arrow@2x.png | Bin 0 -> 896 bytes Resources/Blur.png | Bin 0 -> 169 bytes Resources/Blur@2x.png | Bin 0 -> 250 bytes Resources/Cancel.png | Bin 0 -> 179 bytes Resources/Cancel@2x.png | Bin 0 -> 303 bytes Resources/Ok.png | Bin 0 -> 245 bytes Resources/Ok@2x.png | Bin 0 -> 363 bytes Resources/Rectangle.png | Bin 0 -> 176 bytes Resources/Rectangle@2x.png | Bin 0 -> 244 bytes Support/HockeySDK.xcodeproj/project.pbxproj | 40 ++++++++++ 12 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 Resources/Arrow.png create mode 100644 Resources/Arrow@2x.png create mode 100644 Resources/Blur.png create mode 100644 Resources/Blur@2x.png create mode 100644 Resources/Cancel.png create mode 100644 Resources/Cancel@2x.png create mode 100644 Resources/Ok.png create mode 100644 Resources/Ok@2x.png create mode 100644 Resources/Rectangle.png create mode 100644 Resources/Rectangle@2x.png diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index b8579cf6..910b81ea 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -11,12 +11,16 @@ #import "BITRectangleImageAnnotation.h" #import "BITArrowImageAnnotation.h" #import "BITBlurImageAnnotation.h" +#import "BITHockeyHelper.h" +#import "HockeySDKPrivate.h" @interface BITImageAnnotationViewController () @property (nonatomic, strong) UIImageView *imageView; @property (nonatomic, strong) UISegmentedControl *editingControls; @property (nonatomic, strong) NSMutableArray *objects; + +@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; @property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer; @property (nonatomic, strong) UIPinchGestureRecognizer *pinchRecognizer; @@ -48,7 +52,15 @@ - (void)viewDidLoad self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; + NSArray *icons = @[@"Rectangle.png", @"Arrow.png", @"Blur.png"]; + self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Rectangle", @"Arrow", @"Blur"]]; + int i=0; + for (NSString *imageName in icons){ + [self.editingControls setImage:bit_imageNamed(imageName, BITHOCKEYSDK_BUNDLE) forSegmentAtIndex:i++]; + } + + [self.editingControls setSegmentedControlStyle:UISegmentedControlStyleBar]; self.navigationItem.titleView = self.editingControls; @@ -66,18 +78,32 @@ - (void)viewDidLoad [self.view addSubview:self.imageView]; - self.imageView.frame = self.view.bounds; + // Erm. + self.imageView.frame = [UIScreen mainScreen].bounds; self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panned:)]; self.pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinched:)]; + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; + + [self.panRecognizer requireGestureRecognizerToFail:self.tapRecognizer]; [self.imageView addGestureRecognizer:self.pinchRecognizer]; [self.imageView addGestureRecognizer:self.panRecognizer]; + [self.view addGestureRecognizer:self.tapRecognizer]; self.imageView.userInteractionEnabled = YES; self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithTitle:@"Discard" style:UIBarButtonItemStyleBordered target:self action:@selector(discard:)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithTitle:@"Save" style:UIBarButtonItemStyleBordered target:self action:@selector(save:)]; + + [self fitImageViewFrame]; + + + // Do any additional setup after loading the view. +} + +- (void)fitImageViewFrame { + CGFloat heightScaleFactor = self.view.frame.size.height / self.image.size.height; CGFloat widthScaleFactor = self.view.frame.size.width / self.image.size.width; @@ -87,9 +113,6 @@ - (void)viewDidLoad CGSize scaledImageSize = CGSizeMake(self.image.size.width * factor, self.image.size.height * factor); self.imageView.frame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height/2 - scaledImageSize.height/2, scaledImageSize.width, scaledImageSize.height); - - - // Do any additional setup after loading the view. } -(void)editingAction:(id)sender { @@ -196,16 +219,6 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { } } --(UIView *)firstAnnotationThatIsNotBlur { - for (BITImageAnnotation *annotation in self.imageView.subviews){ - if (![annotation isKindOfClass:[BITBlurImageAnnotation class]]){ - return annotation; - } - } - - return self.imageView; -} - -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ // try to figure out which view we are talking about. @@ -257,10 +270,40 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { } } -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. +-(void)tapped:(UIGestureRecognizer *)tapRecognizer { + if (self.navigationController.navigationBarHidden){ + // [[UIApplication sharedApplication] setStatusBarHidden:NO]; + [UIView animateWithDuration:0.35f animations:^{ + self.navigationController.navigationBar.alpha = 1; + } completion:^(BOOL finished) { + [self fitImageViewFrame]; + [self.navigationController setNavigationBarHidden:NO animated:NO]; + + + }]; + } else { + [UIView animateWithDuration:0.35f animations:^{ + self.navigationController.navigationBar.alpha = 0; + + } completion:^(BOOL finished) { + [self.navigationController setNavigationBarHidden:YES animated:NO]; + + [self fitImageViewFrame]; + + }]; + } + +} + +#pragma mark - Helpers +-(UIView *)firstAnnotationThatIsNotBlur { + for (BITImageAnnotation *annotation in self.imageView.subviews){ + if (![annotation isKindOfClass:[BITBlurImageAnnotation class]]){ + return annotation; + } + } + + return self.imageView; } @end diff --git a/Resources/Arrow.png b/Resources/Arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..6d8fac10390585862e4384ef2e86820ba5da9356 GIT binary patch literal 394 zcmV;50d@X~P)VCQ8QMKOVG}7t5@%Go!kASp$#?^g*u~$2&~1xlGWoWwl~`bWi5HYH1T*3c zN@(E-S(q`*;1EL6Sv0;2IuWu}HFk3#>p7RmCalKAO%k!je_IqJ{~$Vv^(z zbVJ`U3%!lrs^bx|=0Eg%Z9^OC7U}_9;gQtaXuU$4bajM3tBsE|{R7-1XXJyrJsqK4 zXr4q9C&<{7);QD=nl*G`3eXL!m=lhE=p!_1jWSq+{&s?*uOn39Eu07K;Lr7Sgu2!r oSn6UUEeU;u$9!4;aC=q%1~jHedh8Y&asU7T07*qoM6N<$g8UGz%>V!Z literal 0 HcmV?d00001 diff --git a/Resources/Arrow@2x.png b/Resources/Arrow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..116a47ee03672a17c2b662403bdcfa9158fc5a6c GIT binary patch literal 896 zcmV-`1AqL9P)0009=NklMY)7gdg>70Ezn)&KCd!Mzx@Be*k?X}ifXNns9ZGHK827bV8_zLyf8dCi^ z_?e-a>w4${_10!nxg9zQWl#xz{sQ}92-GVprrH-!13fd9H|9DOExY1wRwC<7^l7lTZ?F~GLO$l&nh0mXc)1KSz%?1wjJXT* zq6TL3pT&b`U}0WoLKG{-c^|l2s!;_?!A&S%n~$ThAD~yhLAm_e;jkTiV~YC*eUnkF zGtTSa6$AyUnMURu+ zm=$4;N!na53&9=EWcOhLL^T)vcsKzbztp-4bD$|`9l$(pIDNnkt2TqvI(6%W;sLN2 zIRf1vj`b&KH+%xMp2KR`nfbhnYhcT{(Z{!z>#HV+iF_SH#2qk^3wa5|+2IIl&@mHV z$yGhypr;wvV_>eQ3*ht3L1Q7V%@B3&2P1uh`;iF}IVkS3+hA3QPEXn#Ks%jnErx`} zYckn!2u67?_l|xcY%Zease<-}Nbs-b6dE3bNne8nxWS}t?uh2tpSEx|JrA{;z@5v) zmtYX2X>L|8LH%iI3g^-qh=Uz6u^rq3`5w1$&1sYHP`eub+)#1`CUtP?IR%}H7Vs3N zLt3^6A$48Pi4VO|i*nPb0Cz2mI0aoHsrlv~f|?7Gip7l|Mc_@ah!0Jp(4S0SNdDq85fF6^k41QOJ?4h$|2hSC-Lq2DA)$UeG3wRsJHbf$k}0@+;&l zI9Lwaf3<^TGjVwVvX-Ff&iVt!7C=8#vNV&>P8FIb%!oqwF~N2$}<=fCu_x|UA)P`KiKxv^n& U(aWb>fp#-^y85}Sb4q9e0Ejw8Qvd(} literal 0 HcmV?d00001 diff --git a/Resources/Blur@2x.png b/Resources/Blur@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8cf6ffe5bc51406989170ce3e695f8e9bab7a385 GIT binary patch literal 250 zcmVtK?~9Qgpi#8)j^P+kk`K~aU~9=kg^o<6wF{E;s7IMDdH)Z!Q|tD81;G4BfA@H5(qP6+gJPp%e$iHljKYx`$K6RcZ_2lv|OI(SQqhA^l@uhgl z<|0m@EDeeHQoLky?3|&Q!S?jd)24&F%fD=?T35Dn4Z3kP`{3^-T=u5|C- zIl*s%cV)4n`UQVpwxz7I6e22oFZXq%v8;dV`7r!h=Fu0e73MOhjBQRYTf<|f(LDRa zVZ~JsO@2K-@9Z(Tczu0Hty$LAT<_YwXR@aYn@m!6j&WA}79gBH;UIIF+L6YHC9j{z c?w$CUt+LNxR!nOrBhVELp00i_>zopr0D&w*m;e9( literal 0 HcmV?d00001 diff --git a/Resources/Cancel@2x.png b/Resources/Cancel@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3ddef8cb237955e0fc3133b1ef8ef9be76d43f92 GIT binary patch literal 303 zcmV+~0nq-5P)A3f$@P#^eJOuqfBk)bTyPmiX#Elq)8SxhT+s#H0zSXvG zCP9c{+J4I>MoZAjyOCZYZ=DK`3PkHWINKQR3pey5wuQUjT_^xDG5QN zK?(tJ6o*Y>#o9tvTtIOKo4J7;!BV+_vUCJjuyF>;1z1|lPGLim?<>VfGyH$`)x4Sc z-n@DLs1Tk$YNHtFH}Mv52=PluE7(Cbq`?&F3f&O7hjfQgh+IQTU2M@*@&sZ00000NkvXXu0mjfuOMVM literal 0 HcmV?d00001 diff --git a/Resources/Ok@2x.png b/Resources/Ok@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f1915b84f885ce463de072db6185ffbee702968e GIT binary patch literal 363 zcmV-x0hIoUP)`$q!%F{{Kzr)=6uI<578(JD~J6nj3AiqaQ=i}h(lG;#dwnxp{qUzkvgDGs3Yo3 z5II~`gistr?OfQlw{wwpqn(Si6S|)7T#W74s3G>6iwBJLmRrDe4MlXx;q^N*;lX!M zF>f%0DylRWH#o0A#r(iC_{i~)atOs<>hRPQ(nK%BS0`lhnv0_R_bh&Z3fjQ{%H%y4 zWr=Uuum%``rT=^Llo=fdR94DC^U zvEvaMn~Tf}%+2GY+OyW!To~Wn&>q!+T`qU$KP>2|me8xq>ks$^WSTBB^J@SA002ov JPDHLkV1ka>sf_>t literal 0 HcmV?d00001 diff --git a/Resources/Rectangle.png b/Resources/Rectangle.png new file mode 100644 index 0000000000000000000000000000000000000000..49d81ea6b4e7033c300ccdbfd8b7ac056ac7683d GIT binary patch literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^5bV9sh$l zo6j)Kk=Z6);G#7zrFnGH9xvXsQ4tT(EVnTT=OdE_|{FKaoucY5{YwNhv=rw*%V`*R4!0cpuQvpUfU~RH7jD=*{lJ<*}yL z$EJ#=>mQq2cJ+-!c-Wl166Lo-Hz&tL7Jf^*lNo-tuMnnC`tkX{3lCR4+Oyg3nI5;| of_gEh-0*@M?Ek_tH-D*NwiMW_C2FeQ5A+6ur>mdKI;Vst0J6wu1poj5 literal 0 HcmV?d00001 diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 0ab1521d..993fffc7 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -145,6 +145,16 @@ 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */; }; 9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */; }; 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */; }; + 9782023218F81BFC00A98D8B /* Arrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022818F81BFC00A98D8B /* Arrow.png */; }; + 9782023318F81BFC00A98D8B /* Arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022918F81BFC00A98D8B /* Arrow@2x.png */; }; + 9782023418F81BFC00A98D8B /* Blur.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022A18F81BFC00A98D8B /* Blur.png */; }; + 9782023518F81BFC00A98D8B /* Blur@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022B18F81BFC00A98D8B /* Blur@2x.png */; }; + 9782023618F81BFC00A98D8B /* Cancel.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022C18F81BFC00A98D8B /* Cancel.png */; }; + 9782023718F81BFC00A98D8B /* Cancel@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022D18F81BFC00A98D8B /* Cancel@2x.png */; }; + 9782023818F81BFC00A98D8B /* Ok.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022E18F81BFC00A98D8B /* Ok.png */; }; + 9782023918F81BFC00A98D8B /* Ok@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022F18F81BFC00A98D8B /* Ok@2x.png */; }; + 9782023A18F81BFC00A98D8B /* Rectangle.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023018F81BFC00A98D8B /* Rectangle.png */; }; + 9782023B18F81BFC00A98D8B /* Rectangle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023118F81BFC00A98D8B /* Rectangle@2x.png */; }; 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; }; 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; }; 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; @@ -319,6 +329,16 @@ 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotation.h; sourceTree = ""; }; 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotation.m; sourceTree = ""; }; + 9782022818F81BFC00A98D8B /* Arrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Arrow.png; sourceTree = ""; }; + 9782022918F81BFC00A98D8B /* Arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Arrow@2x.png"; sourceTree = ""; }; + 9782022A18F81BFC00A98D8B /* Blur.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Blur.png; sourceTree = ""; }; + 9782022B18F81BFC00A98D8B /* Blur@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Blur@2x.png"; sourceTree = ""; }; + 9782022C18F81BFC00A98D8B /* Cancel.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Cancel.png; sourceTree = ""; }; + 9782022D18F81BFC00A98D8B /* Cancel@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Cancel@2x.png"; sourceTree = ""; }; + 9782022E18F81BFC00A98D8B /* Ok.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Ok.png; sourceTree = ""; }; + 9782022F18F81BFC00A98D8B /* Ok@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Ok@2x.png"; sourceTree = ""; }; + 9782023018F81BFC00A98D8B /* Rectangle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Rectangle.png; sourceTree = ""; }; + 9782023118F81BFC00A98D8B /* Rectangle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Rectangle@2x.png"; sourceTree = ""; }; 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = ""; }; 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconCamera@2x.png"; sourceTree = ""; }; 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotationViewController.h; sourceTree = ""; }; @@ -382,6 +402,16 @@ 1E5955A415B71BDC00A03429 /* Images */ = { isa = PBXGroup; children = ( + 9782022818F81BFC00A98D8B /* Arrow.png */, + 9782022918F81BFC00A98D8B /* Arrow@2x.png */, + 9782022A18F81BFC00A98D8B /* Blur.png */, + 9782022B18F81BFC00A98D8B /* Blur@2x.png */, + 9782022C18F81BFC00A98D8B /* Cancel.png */, + 9782022D18F81BFC00A98D8B /* Cancel@2x.png */, + 9782022E18F81BFC00A98D8B /* Ok.png */, + 9782022F18F81BFC00A98D8B /* Ok@2x.png */, + 9782023018F81BFC00A98D8B /* Rectangle.png */, + 9782023118F81BFC00A98D8B /* Rectangle@2x.png */, 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */, 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */, 1E5955BB15B71C8600A03429 /* authorize_denied.png */, @@ -860,19 +890,29 @@ files = ( 1E5955C615B71C8600A03429 /* authorize_denied.png in Resources */, 1E5955C715B71C8600A03429 /* authorize_denied@2x.png in Resources */, + 9782023B18F81BFC00A98D8B /* Rectangle@2x.png in Resources */, 1E5955CA15B71C8600A03429 /* bg.png in Resources */, 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */, + 9782023618F81BFC00A98D8B /* Cancel.png in Resources */, + 9782023318F81BFC00A98D8B /* Arrow@2x.png in Resources */, + 9782023818F81BFC00A98D8B /* Ok.png in Resources */, 1E5955CB15B71C8600A03429 /* buttonHighlight.png in Resources */, 1E5955CC15B71C8600A03429 /* buttonHighlight@2x.png in Resources */, 1E5955CF15B71C8600A03429 /* IconGradient.png in Resources */, 1E5955D015B71C8600A03429 /* IconGradient@2x.png in Resources */, + 9782023218F81BFC00A98D8B /* Arrow.png in Resources */, + 9782023A18F81BFC00A98D8B /* Rectangle.png in Resources */, 1EAF20A8162DC0F600957B1D /* feedbackActivity@2x~ipad.png in Resources */, 1EAF20A9162DC0F600957B1D /* feedbackActivity~ipad.png in Resources */, + 9782023918F81BFC00A98D8B /* Ok@2x.png in Resources */, + 9782023418F81BFC00A98D8B /* Blur.png in Resources */, 1EAF20AA162DC0F600957B1D /* feedbackActiviy.png in Resources */, 1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */, + 9782023518F81BFC00A98D8B /* Blur@2x.png in Resources */, 1E1127C416580C87007067A2 /* buttonRoundedDelete.png in Resources */, 1E1127C516580C87007067A2 /* buttonRoundedDelete@2x.png in Resources */, 1E1127C616580C87007067A2 /* buttonRoundedDeleteHighlighted.png in Resources */, + 9782023718F81BFC00A98D8B /* Cancel@2x.png in Resources */, 1E1127C716580C87007067A2 /* buttonRoundedDeleteHighlighted@2x.png in Resources */, 1E1127C816580C87007067A2 /* buttonRoundedRegular.png in Resources */, 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */, From 6151ed6ac79eedf00182099c33598cd28f4a199c Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Fri, 11 Apr 2014 15:01:24 +0200 Subject: [PATCH 050/206] + Adapted Add Photo Layout to iOS6 style. --- Classes/BITFeedbackComposeViewController.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 6f147dcf..4ff3b4aa 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -183,10 +183,10 @@ - (void)viewDidLoad { // Add Photo Button + Container that's displayed above the keyboard. self.textAccessoryView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44)]; self.textAccessoryView.backgroundColor = [UIColor colorWithRed:0.9f green:0.9f blue:0.9f alpha:1.0f]; - UIButton *addPhotoButton = [UIButton buttonWithType:UIButtonTypeSystem]; + UIButton *addPhotoButton = [UIButton buttonWithType:UIButtonTypeCustom]; [addPhotoButton setTitle:@"+ Add Photo" forState:UIControlStateNormal]; - addPhotoButton.frame = CGRectMake(0, 0, 100, 44); - + [addPhotoButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; + addPhotoButton.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44); [addPhotoButton addTarget:self action:@selector(addPhotoAction:) forControlEvents:UIControlEventTouchUpInside]; [self.textAccessoryView addSubview:addPhotoButton]; From 5e7ff0043ff318c35222c070f8f4209125991fa1 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Fri, 11 Apr 2014 16:52:24 +0200 Subject: [PATCH 051/206] + Fixes List Display of Attachments for iOS6 --- Classes/BITFeedbackListViewCell.m | 33 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index d6221876..9998a076 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -67,6 +67,8 @@ @interface BITFeedbackListViewCell () @property (nonatomic, strong) NSMutableArray *attachmentViews; +@property (nonatomic, strong) UIView *accessoryBackgroundView; + @end @@ -112,6 +114,7 @@ - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reus #pragma mark - Private - (UIColor *)backgroundColor { + if (self.backgroundStyle == BITFeedbackListViewCellBackgroundStyleNormal) { if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) { return BACKGROUNDCOLOR_DEFAULT; @@ -187,7 +190,7 @@ + (CGFloat) heightForTextInRowWithMessage:(BITFeedbackMessage *)message tableVie - (void)setAttachments:(NSArray *)attachments { for (UIView *view in self.attachmentViews){ - [view removeFromSuperview]; + [view removeFromSuperview]; } [self.attachmentViews removeAllObjects]; @@ -199,18 +202,25 @@ - (void)setAttachments:(NSArray *)attachments { [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; [self.attachmentViews addObject:imageView]; - [self addSubview:imageView]; } } - (void)layoutSubviews { - UIView *accessoryViewBackground = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)]; - accessoryViewBackground.autoresizingMask = UIViewAutoresizingFlexibleHeight; - accessoryViewBackground.clipsToBounds = YES; + if (!self.accessoryBackgroundView){ + self.accessoryBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)]; + self.accessoryBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleHeight; + self.accessoryBackgroundView.clipsToBounds = YES; // colors - accessoryViewBackground.backgroundColor = [self backgroundColor]; + self.accessoryBackgroundView.backgroundColor = [self backgroundColor]; + } + + if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) { + [self addSubview:self.accessoryBackgroundView]; + } else if (self.accessoryBackgroundView.superview){ + [self.accessoryBackgroundView removeFromSuperview]; + } self.contentView.backgroundColor = [self backgroundColor]; self.labelTitle.backgroundColor = [self backgroundColor]; self.labelText.backgroundColor = [self backgroundColor]; @@ -223,9 +233,7 @@ - (void)layoutSubviews { } // background for deletion accessory view - if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) { - [self addSubview:accessoryViewBackground]; - } + // header NSString *dateString = @""; @@ -266,6 +274,7 @@ - (void)layoutSubviews { int i = 0; CGFloat attachmentsPerRow = ceilf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); + for ( UIButton *imageButton in self.attachmentViews){ imageButton.contentMode = UIViewContentModeScaleAspectFit; @@ -275,11 +284,13 @@ - (void)layoutSubviews { imageButton.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i) ), floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } + if (!imageButton.superview){ + [self addSubview:imageButton]; + } + i++; - } - [super layoutSubviews]; } From cf17b298a7785b86d85b6e99d3af29fb1ff0173b Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Fri, 11 Apr 2014 16:52:41 +0200 Subject: [PATCH 052/206] + Fixes an iOS6 Crash in the gallery-component. --- Classes/BITAttachmentGalleryViewController.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index 60fa6ff9..7837fec2 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -36,10 +36,12 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)viewDidLoad { [super viewDidLoad]; - self.automaticallyAdjustsScrollViewInsets = NO; self.navigationController.navigationBar.translucent = YES; +#if __IPHONE_OS_VERSION_MIN_REQUIRED > __IPHONE_6_1 + self.automaticallyAdjustsScrollViewInsets = NO; self.edgesForExtendedLayout = YES; self.extendedLayoutIncludesOpaqueBars = YES; +#endif self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Close" style:UIBarButtonItemStylePlain target:self action:@selector(close:)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(share:)]; From b230c964a6f1b70cc8c31f40a5e57a5f62f0aab5 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Fri, 11 Apr 2014 17:43:34 +0200 Subject: [PATCH 053/206] + WIP --- Classes/BITAttachmentGalleryViewController.m | 16 ++++++++++------ Classes/BITFeedbackComposeViewController.m | 3 +-- Classes/BITFeedbackMessageAttachment.m | 13 ++++++++++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index 7837fec2..48b2d594 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -52,6 +52,9 @@ - (void)viewDidLoad [self extractUsableAttachments]; [self setupScrollView]; + self.view.frame = UIScreen.mainScreen.applicationFrame; + // self.view.frame.origin = CGPointZero; + [self layoutViews]; } @@ -80,7 +83,7 @@ - (void)setupScrollView { for (int i = 0; i<3; i++){ UIImageView *newImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; [imageviews addObject:newImageView]; - newImageView.contentMode = UIViewContentModeScaleAspectFit; + newImageView.contentMode = UIViewContentModeScaleAspectFit|UIViewContentModeBottom; [self.scrollView addSubview:newImageView]; } @@ -113,11 +116,7 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { NSInteger newIndex = self.scrollView.contentOffset.x / self.scrollView.frame.size.width; if (newIndex!=self.currentIndex){ self.currentIndex = newIndex; - // requeue elements. - NSInteger baseIndex = MAX(0,self.currentIndex-1); [self layoutViews]; - - } } @@ -125,6 +124,10 @@ - (void)layoutViews { self.scrollView.frame = self.view.bounds; self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds) * self.extractedAttachments.count, CGRectGetHeight(self.view.bounds)); + + self.scrollView.contentInset = UIEdgeInsetsZero; + + self.scrollView.contentOffset = CGPointMake(self.scrollView.contentOffset.x, 0); NSInteger baseIndex = MAX(0,self.currentIndex-1); NSInteger z = baseIndex; @@ -163,10 +166,11 @@ - (void)tapped:(UITapGestureRecognizer *)tapRecognizer { [[UIApplication sharedApplication] setStatusBarHidden:YES]; } + [self layoutViews]; } - (CGRect)frameForItemAtIndex:(NSInteger)index { - return CGRectMake(index * CGRectGetWidth(self.scrollView.frame), 0, CGRectGetWidth(self.scrollView.frame), CGRectGetHeight(self.scrollView.frame)); + return CGRectMake(index * CGRectGetWidth(self.scrollView.frame), 0, CGRectGetWidth(self.scrollView.frame), CGRectGetHeight(self.view.bounds)); } @end diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 4ff3b4aa..bb353832 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -184,8 +184,7 @@ - (void)viewDidLoad { self.textAccessoryView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44)]; self.textAccessoryView.backgroundColor = [UIColor colorWithRed:0.9f green:0.9f blue:0.9f alpha:1.0f]; UIButton *addPhotoButton = [UIButton buttonWithType:UIButtonTypeCustom]; - [addPhotoButton setTitle:@"+ Add Photo" forState:UIControlStateNormal]; - [addPhotoButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; + [addPhotoButton setTitle:@"+ Add Photo" forState:UIControlStateNormal]; [addPhotoButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; addPhotoButton.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44); [addPhotoButton addTarget:self action:@selector(addPhotoAction:) forControlEvents:UIControlEventTouchUpInside]; diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index b0d68b82..e3b6ef30 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -107,7 +107,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder { - (UIImage *)imageRepresentation { if ([self.contentType rangeOfString:@"image"].location != NSNotFound){ - return [UIImage imageWithData:self.data]; + return [UIImage imageWithData:self.data scale:[UIScreen mainScreen].scale]; } else { return bit_imageNamed(@"feedbackActiviy.png", BITHOCKEYSDK_BUNDLE); // TODO add another placeholder. } @@ -118,9 +118,16 @@ - (UIImage *)thumbnailWithSize:(CGSize)size { if (!self.thumbnailRepresentations[cacheKey]){ UIImage *image = self.imageRepresentation; - UIImage *thumbnail = bit_imageToFitSize(image, size, NO); + // consider the scale. + + CGFloat scale = [UIScreen mainScreen].scale; + + CGSize scaledSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale)); + UIImage *thumbnail = bit_imageToFitSize(image, scaledSize, NO) ; + + UIImage *scaledTumbnail = [UIImage imageWithCGImage:thumbnail.CGImage scale:scale orientation:thumbnail.imageOrientation]; if (thumbnail){ - [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; + [self.thumbnailRepresentations setObject:scaledTumbnail forKey:cacheKey]; } } From d891af1929bb57048c722e68ca1021282857ba37 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 15 Apr 2014 16:13:53 +0200 Subject: [PATCH 054/206] + Fixes a layout problem in the attachment gallery. --- Classes/BITAttachmentGalleryViewController.m | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index 48b2d594..24c73168 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -37,6 +37,7 @@ - (void)viewDidLoad { [super viewDidLoad]; self.navigationController.navigationBar.translucent = YES; + self.navigationController.navigationBar.opaque = NO; #if __IPHONE_OS_VERSION_MIN_REQUIRED > __IPHONE_6_1 self.automaticallyAdjustsScrollViewInsets = NO; self.edgesForExtendedLayout = YES; @@ -55,15 +56,22 @@ - (void)viewDidLoad self.view.frame = UIScreen.mainScreen.applicationFrame; // self.view.frame.origin = CGPointZero; +} +-(void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + // Hide the navigation bar and stuff initially. + [self.navigationController setNavigationBarHidden:YES animated:NO]; + [[UIApplication sharedApplication] setStatusBarHidden:YES]; + [self layoutViews]; + + } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - - - self.tapognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; [self.view addGestureRecognizer:self.tapognizer]; @@ -83,7 +91,7 @@ - (void)setupScrollView { for (int i = 0; i<3; i++){ UIImageView *newImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; [imageviews addObject:newImageView]; - newImageView.contentMode = UIViewContentModeScaleAspectFit|UIViewContentModeBottom; + newImageView.contentMode = UIViewContentModeScaleAspectFit; [self.scrollView addSubview:newImageView]; } @@ -126,6 +134,7 @@ - (void)layoutViews { self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds) * self.extractedAttachments.count, CGRectGetHeight(self.view.bounds)); self.scrollView.contentInset = UIEdgeInsetsZero; + self.scrollView.autoresizesSubviews = NO; self.scrollView.contentOffset = CGPointMake(self.scrollView.contentOffset.x, 0); From 1df69d131a47f18e4d9269bb88188cd7231f08a4 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 15 Apr 2014 16:29:51 +0200 Subject: [PATCH 055/206] + Preselecting an attachment works. --- Classes/BITAttachmentGalleryViewController.h | 4 ++++ Classes/BITAttachmentGalleryViewController.m | 21 +++++++++++++++----- Classes/BITFeedbackListViewController.m | 6 ++++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Classes/BITAttachmentGalleryViewController.h b/Classes/BITAttachmentGalleryViewController.h index c246390f..a745b6dd 100644 --- a/Classes/BITAttachmentGalleryViewController.h +++ b/Classes/BITAttachmentGalleryViewController.h @@ -8,8 +8,12 @@ #import +@class BITFeedbackMessageAttachment; + @interface BITAttachmentGalleryViewController : UIViewController @property (nonatomic, strong) NSArray *messages; +@property (nonatomic, strong) BITFeedbackMessageAttachment *preselectedAttachment; + @end diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index 24c73168..f3e5b79c 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -54,8 +54,9 @@ - (void)viewDidLoad [self setupScrollView]; self.view.frame = UIScreen.mainScreen.applicationFrame; - // self.view.frame.origin = CGPointZero; + self.tapognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; + [self.view addGestureRecognizer:self.tapognizer]; } -(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; @@ -64,6 +65,15 @@ -(void)viewWillAppear:(BOOL)animated { [self.navigationController setNavigationBarHidden:YES animated:NO]; [[UIApplication sharedApplication] setStatusBarHidden:YES]; + if (self.preselectedAttachment){ + NSInteger indexOfSelectedAttachment = [self.extractedAttachments indexOfObject:self.preselectedAttachment]; + if (indexOfSelectedAttachment != NSNotFound){ + self.currentIndex = indexOfSelectedAttachment; + self.scrollView.contentOffset = CGPointMake(self.scrollView.frame.size.width * self.currentIndex, 0); + + } + } + [self layoutViews]; @@ -72,8 +82,7 @@ -(void)viewWillAppear:(BOOL)animated { - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - self.tapognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; - [self.view addGestureRecognizer:self.tapognizer]; + } - (void)setupScrollView { @@ -129,14 +138,16 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { } - (void)layoutViews { + CGPoint savedOffset = self.scrollView.contentOffset; + self.scrollView.delegate = nil; self.scrollView.frame = self.view.bounds; self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds) * self.extractedAttachments.count, CGRectGetHeight(self.view.bounds)); - + self.scrollView.delegate = self; self.scrollView.contentInset = UIEdgeInsetsZero; self.scrollView.autoresizesSubviews = NO; + self.scrollView.contentOffset = savedOffset; - self.scrollView.contentOffset = CGPointMake(self.scrollView.contentOffset.x, 0); NSInteger baseIndex = MAX(0,self.currentIndex-1); NSInteger z = baseIndex; diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index fe45d9e5..70f58a1f 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -773,14 +773,16 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn - (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment { BITAttachmentGalleryViewController *galleryController = [BITAttachmentGalleryViewController new]; - UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:galleryController]; + NSMutableArray *collectedMessages = [NSMutableArray new]; + for (int i = 0; i Date: Tue, 15 Apr 2014 17:06:06 +0200 Subject: [PATCH 056/206] + Fixes a problem with more than 3 attachments. --- Classes/BITAttachmentGalleryViewController.m | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index f3e5b79c..c1224ac6 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -120,8 +120,6 @@ - (void)extractUsableAttachments { } self.extractedAttachments = extractedOnes; - - [self layoutViews]; } - (void)didReceiveMemoryWarning @@ -152,7 +150,7 @@ - (void)layoutViews { NSInteger baseIndex = MAX(0,self.currentIndex-1); NSInteger z = baseIndex; for ( NSInteger i = baseIndex; i < MIN(baseIndex+2, self.extractedAttachments.count);i++ ){ - UIImageView *imageView = self.imageViews[z]; + UIImageView *imageView = self.imageViews[z%self.imageViews.count]; BITFeedbackMessageAttachment *attachment = self.extractedAttachments[i]; imageView.image =[attachment imageRepresentation]; imageView.frame = [self frameForItemAtIndex:i]; From f9b76db0d700bd3eef72a730a086809438a3f389 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 15 Apr 2014 17:06:12 +0200 Subject: [PATCH 057/206] + Resolves a scaling issue. --- Classes/BITFeedbackMessageAttachment.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index e3b6ef30..0c8d3a87 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -107,7 +107,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder { - (UIImage *)imageRepresentation { if ([self.contentType rangeOfString:@"image"].location != NSNotFound){ - return [UIImage imageWithData:self.data scale:[UIScreen mainScreen].scale]; + return [UIImage imageWithData:self.data]; } else { return bit_imageNamed(@"feedbackActiviy.png", BITHOCKEYSDK_BUNDLE); // TODO add another placeholder. } From 5038c3e4e9d233c350183aeb1bb088257292dd42 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 15 Apr 2014 17:25:06 +0200 Subject: [PATCH 058/206] + Fixed a problem where missing file names would prevent the submittal of attachments. --- Classes/BITFeedbackManager.m | 10 +++++++++- Classes/BITFeedbackMessageAttachment.m | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index efabd38d..7d18f91c 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -866,7 +866,15 @@ - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BIT for (BITFeedbackMessageAttachment *attachment in message.attachments){ NSString *key = [NSString stringWithFormat:@"attachment%ld", (long)photoIndex]; - [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.data forKey:key contentType:attachment.contentType boundary:boundary filename:attachment.originalFilename]]; + + NSString *filename = attachment.originalFilename; + + if (!filename) { + filename = [NSString stringWithFormat:@"Attachment %ld", (long)photoIndex]; + } + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.data forKey:key contentType:attachment.contentType boundary:boundary filename:filename]]; + photoIndex++; } diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 0c8d3a87..ba63a7f2 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -44,9 +44,20 @@ @interface BITFeedbackMessageAttachment() @implementation BITFeedbackMessageAttachment + (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType { + + static NSDateFormatter *formatter; + + if(!formatter){ + formatter = [NSDateFormatter new]; + formatter.dateStyle = NSDateFormatterShortStyle; + formatter.timeStyle = NSDateFormatterShortStyle; + + } + BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; newAttachment.contentType = contentType; newAttachment.data = data; + newAttachment.originalFilename = [NSString stringWithFormat:@"Attachment: %@", [formatter stringFromDate:[NSDate date]]]; return newAttachment; } From 3aa45bbb9f909a2c717f3e64a2f8c4554d1ff6e1 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 16 Apr 2014 11:24:39 +0200 Subject: [PATCH 059/206] + Added preliminary load mechanics for Feedback Images. --- Classes/BITFeedbackManager.m | 137 ++++++++++++++++--------- Classes/BITFeedbackMessage.h | 4 + Classes/BITFeedbackMessage.m | 7 +- Classes/BITFeedbackMessageAttachment.h | 3 + Classes/BITFeedbackMessageAttachment.m | 11 +- 5 files changed, 113 insertions(+), 49 deletions(-) diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 7d18f91c..5cd2091d 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -81,7 +81,7 @@ - (id)init { _requireUserEmail = BITFeedbackUserDataElementOptional; _showAlertOnIncomingMessages = YES; _showFirstRequiredPresentationModal = YES; - + _disableFeedbackManager = NO; _networkRequestInProgress = NO; _incomingMessagesAlertShowing = NO; @@ -90,9 +90,9 @@ - (id)init { _lastMessageID = nil; self.feedbackList = [NSMutableArray array]; - + _fileManager = [[NSFileManager alloc] init]; - + _settingsFile = [bit_settingsDir() stringByAppendingPathComponent:BITHOCKEY_FEEDBACK_SETTINGS]; _userID = nil; @@ -152,12 +152,12 @@ - (void) registerObservers { } if(nil == _networkDidBecomeReachableObserver) { _networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification - object:nil - queue:NSOperationQueue.mainQueue - usingBlock:^(NSNotification *note) { - typeof(self) strongSelf = weakSelf; - [strongSelf didBecomeActiveActions]; - }]; + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; + }]; } } @@ -228,9 +228,9 @@ - (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items{ } BITFeedbackComposeViewController *composeView = [self feedbackComposeViewController]; [composeView prepareWithItems:items]; - + [self showView:composeView]; - + } - (void)showFeedbackComposeViewWithGeneratedScreenshot { @@ -242,7 +242,7 @@ - (void)showFeedbackComposeViewWithGeneratedScreenshot { - (void)startManager { if ([self isFeedbackManagerDisabled]) return; - + [self registerObservers]; // we are already delayed, so the notification already came in and this won't invoked twice @@ -289,12 +289,12 @@ - (BOOL)updateUserIDUsingKeychainAndDelegate { userIDForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self]; } - + if (userID) { availableViaDelegate = YES; self.userID = userID; } - + return availableViaDelegate; } @@ -306,16 +306,16 @@ - (BOOL)updateUserNameUsingKeychainAndDelegate { if ([BITHockeyManager sharedHockeyManager].delegate && [[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userNameForHockeyManager:componentManager:)]) { userName = [[BITHockeyManager sharedHockeyManager].delegate - userNameForHockeyManager:[BITHockeyManager sharedHockeyManager] - componentManager:self]; + userNameForHockeyManager:[BITHockeyManager sharedHockeyManager] + componentManager:self]; } - + if (userName) { availableViaDelegate = YES; self.userName = userName; self.requireUserName = BITFeedbackUserDataElementDontShow; } - + return availableViaDelegate; } @@ -323,20 +323,20 @@ - (BOOL)updateUserEmailUsingKeychainAndDelegate { BOOL availableViaDelegate = NO; NSString *userEmail = [self stringValueFromKeychainForKey:kBITHockeyMetaUserEmail]; - + if ([BITHockeyManager sharedHockeyManager].delegate && [[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userEmailForHockeyManager:componentManager:)]) { userEmail = [[BITHockeyManager sharedHockeyManager].delegate userEmailForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self]; } - + if (userEmail) { availableViaDelegate = YES; self.userEmail = userEmail; self.requireUserEmail = BITFeedbackUserDataElementDontShow; } - + return availableViaDelegate; } @@ -344,7 +344,7 @@ - (void)updateAppDefinedUserData { [self updateUserIDUsingKeychainAndDelegate]; [self updateUserNameUsingKeychainAndDelegate]; [self updateUserEmailUsingKeychainAndDelegate]; - + // if both values are shown via the delegates, we never ever did ask and will never ever ask for user data if (self.requireUserName == BITFeedbackUserDataElementDontShow && self.requireUserEmail == BITFeedbackUserDataElementDontShow) { @@ -361,7 +361,7 @@ - (void)loadMessages { if (![_fileManager fileExistsAtPath:_settingsFile]) return; - + NSData *codedData = [[NSData alloc] initWithContentsOfFile:_settingsFile]; if (codedData == nil) return; @@ -373,7 +373,7 @@ - (void)loadMessages { @catch (NSException *exception) { return; } - + if (!userIDViaDelegate) { if ([unarchiver containsValueForKey:kBITFeedbackUserID]) { self.userID = [unarchiver decodeObjectForKey:kBITFeedbackUserID]; @@ -389,7 +389,7 @@ - (void)loadMessages { } self.userName = [self stringValueFromKeychainForKey:kBITFeedbackName]; } - + if (!userEmailViaDelegate) { if ([unarchiver containsValueForKey:kBITFeedbackEmail]) { self.userEmail = [unarchiver decodeObjectForKey:kBITFeedbackEmail]; @@ -409,7 +409,7 @@ - (void)loadMessages { if ([unarchiver containsValueForKey:kBITFeedbackAppID]) { NSString *appID = [unarchiver decodeObjectForKey:kBITFeedbackAppID]; - + // the stored thread is from another application identifier, so clear the token // which will cause the new posts to create a new thread on the server for the // current app identifier @@ -432,9 +432,9 @@ - (void)loadMessages { // inform the UI to update its data in case the list is already showing [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; } - + [unarchiver finishDecoding]; - + if (!self.lastCheck) { self.lastCheck = [NSDate distantPast]; } @@ -446,7 +446,7 @@ - (void)saveMessages { NSMutableData *data = [[NSMutableData alloc] init]; NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; - + if (_didAskUserData) [archiver encodeObject:[NSNumber numberWithBool:YES] forKey:kBITFeedbackUserDataAsked]; @@ -631,7 +631,7 @@ - (BOOL)requireManualUserDataMissing { - (BOOL)isManualUserDataAvailable { [self updateAppDefinedUserData]; - + if ((self.requireUserName != BITFeedbackUserDataElementDontShow && self.userName) || (self.requireUserEmail != BITFeedbackUserDataElementDontShow && self.userEmail)) return YES; @@ -645,23 +645,23 @@ - (BOOL)isManualUserDataAvailable { - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { if (!jsonDictionary) { // nil is used when the server returns 404, so we need to mark all existing threads as archives and delete the discussion token - + NSArray *messagesSendInProgress = [self messagesWithStatus:BITFeedbackMessageStatusSendInProgress]; NSInteger pendingMessagesCount = [messagesSendInProgress count] + [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; - + [self markSendInProgressMessagesAsPending]; [_feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { if ([(BITFeedbackMessage *)objMessage status] != BITFeedbackMessageStatusSendPending) [(BITFeedbackMessage *)objMessage setStatus:BITFeedbackMessageStatusArchived]; }]; - + if ([self token]) { self.token = nil; } NSInteger pendingMessagesCountAfterProcessing = [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; - + [self saveMessages]; // check if this request was successful and we have more messages pending and continue if positive @@ -710,7 +710,7 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { *stop2 = YES; } }]; - + if (matchingSendInProgressOrInConflictMessage) { matchingSendInProgressOrInConflictMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]]; matchingSendInProgressOrInConflictMessage.id = messageID; @@ -734,6 +734,15 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { message.id = [(NSDictionary *)objMessage objectForKey:@"id"]; message.status = BITFeedbackMessageStatusUnread; + for (NSDictionary *attachmentData in objMessage[@"attachments"]){ + BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; + newAttachment.originalFilename = attachmentData[@"file_name"]; + newAttachment.id = attachmentData[@"id"]; + newAttachment.sourceURL = attachmentData[@"url"]; + newAttachment.contentType = @"image/jpg"; + [message addAttachmentsObject:newAttachment]; + } + [_feedbackList addObject:message]; newMessage = YES; @@ -750,7 +759,7 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { [self sortFeedbackList]; [self updateLastMessageID]; - + // we got a new incoming message, trigger user notification system if (newMessage) { // check if the latest message is from the users own email address, then don't show an alert since he answered using his own email @@ -759,12 +768,12 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { BITFeedbackMessage *latestMessage = [self lastMessageHavingID]; if (self.userEmail && latestMessage.email && [self.userEmail compare:latestMessage.email] == NSOrderedSame) latestMessageFromUser = YES; - + if (!latestMessageFromUser) { if([self.delegate respondsToSelector:@selector(feedbackManagerDidReceiveNewFeedback:)]) { [self.delegate feedbackManagerDidReceiveNewFeedback:self]; } - + if(self.showAlertOnIncomingMessages && !self.currentFeedbackListViewController && !self.currentFeedbackComposeViewController) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageTitle") message:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageText") @@ -780,7 +789,7 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { } NSInteger pendingMessagesCountAfterProcessing = [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; - + // check if this request was successful and we have more messages pending and continue if positive if (pendingMessagesCount > pendingMessagesCountAfterProcessing && pendingMessagesCountAfterProcessing > 0) { [self performSelector:@selector(submitPendingMessages) withObject:nil afterDelay:0.1]; @@ -790,18 +799,52 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { [self markSendInProgressMessagesAsPending]; } + [self synchronizeMissingAttachments]; + [self saveMessages]; - + return; } +/** + Load all attachments without any local data to have them available. + */ +-(BOOL)synchronizeMissingAttachments { + // Extract all Attachments. + NSMutableArray *allAttachments = [NSMutableArray new]; + for (int i = 0; i < [self numberOfMessages]; i++){ + BITFeedbackMessage *message = [self messageAtIndex:i]; + for (BITFeedbackMessageAttachment *attachment in message.attachments){ + if (attachment.needsLoadingFromURL){ + [allAttachments addObject:attachment]; + } + } + } + + for (BITFeedbackMessageAttachment *attachment in allAttachments){ + // we will just update the objects here and perform a save after each successful load operation. + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]]; + __weak BITFeedbackManager *weakSelf = self; + [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) { + if (responseData.length){ + [attachment replaceData:responseData]; + [weakSelf saveMessages]; + + } + }]; + + } + return NO; +} + - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BITFeedbackMessage *)message completionHandler:(void (^)(NSError *err))completionHandler { NSString *boundary = @"----FOO"; _networkRequestInProgress = YES; // inform the UI to update its data in case the list is already showing [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingStarted object:nil]; - + NSString *tokenParameter = @""; if ([self token]) { tokenParameter = [NSString stringWithFormat:@"/%@", [self token]]; @@ -879,7 +922,7 @@ - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BIT } [postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - + [request setHTTPBody:postBody]; } @@ -902,14 +945,14 @@ - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BIT if (!self.token) { // set the token to the first message token, since this is identical __block NSString *token = nil; - + [_feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { if ([(BITFeedbackMessage *)objMessage status] == BITFeedbackMessageStatusSendInProgress) { token = [(BITFeedbackMessage *)objMessage token]; *stop = YES; } }]; - + if (token) { self.token = token; } @@ -982,7 +1025,7 @@ - (void)submitPendingMessages { [self saveMessages]; NSArray *pendingMessages = [self messagesWithStatus:BITFeedbackMessageStatusSendPending]; - + if ([pendingMessages count] > 0) { // we send one message at a time BITFeedbackMessage *messageToSend = [pendingMessages objectAtIndex:0]; @@ -1041,11 +1084,11 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) } } -#pragma mark - Observation Handling +#pragma mark - Observation Handling -(void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode { if (mode == BITFeedbackObservationModeOnScreenshot){ - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; } } diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index baa6d6fd..44ff0e0d 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -29,6 +29,8 @@ #import +@class BITFeedbackMessageAttachment; + /** * Status for each feedback message */ @@ -79,5 +81,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { */ -(void)deleteContents; +-(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object; + @end diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index eb442896..446fb0da 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -90,5 +90,10 @@ -(void)deleteContents { [attachment deleteContents]; } } - +-(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object{ + if (!self.attachments){ + self.attachments = [NSArray array]; + } + self.attachments = [self.attachments arrayByAddingObject:object]; +} @end diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index 4a9a8ea6..1d056419 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -35,6 +35,7 @@ @property (nonatomic, copy) NSNumber *id; @property (nonatomic, copy) NSString *originalFilename; @property (nonatomic, copy) NSString *contentType; +@property (nonatomic, copy) NSString *sourceURL; @property (nonatomic, readonly) NSData *data; @property (readonly) UIImage *imageRepresentation; @@ -48,4 +49,6 @@ - (void)deleteContents; +-(BOOL)needsLoadingFromURL; + @end diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index ba63a7f2..3350b96d 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -39,6 +39,7 @@ @interface BITFeedbackMessageAttachment() @property (nonatomic, strong) NSData *internalData; @property (nonatomic, copy) NSString *filename; + @end @implementation BITFeedbackMessageAttachment @@ -92,12 +93,18 @@ - (void)replaceData:(NSData *)data { self.thumbnailRepresentations = [NSMutableDictionary new]; } +-(BOOL)needsLoadingFromURL { + return (self.sourceURL); +} + #pragma mark NSCoding - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.contentType forKey:@"contentType"]; [aCoder encodeObject:self.filename forKey:@"filename"]; [aCoder encodeObject:self.originalFilename forKey:@"originalFilename"]; + [aCoder encodeObject:self.sourceURL forKey:@"url"]; + } @@ -109,6 +116,8 @@ - (id)initWithCoder:(NSCoder *)aDecoder { self.filename = [aDecoder decodeObjectForKey:@"filename"]; self.thumbnailRepresentations = [NSMutableDictionary new]; self.originalFilename = [aDecoder decodeObjectForKey:@"originalFilename"]; + self.sourceURL = [aDecoder decodeObjectForKey:@"sourceURL"]; + } return self; @@ -117,7 +126,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder { #pragma mark - Thubmnails / Image Representation - (UIImage *)imageRepresentation { - if ([self.contentType rangeOfString:@"image"].location != NSNotFound){ + if ([self.contentType rangeOfString:@"image"].location != NSNotFound || [self.sourceURL rangeOfString:@"jpeg"].location != NSNotFound){ return [UIImage imageWithData:self.data]; } else { return bit_imageNamed(@"feedbackActiviy.png", BITHOCKEYSDK_BUNDLE); // TODO add another placeholder. From 9f9f44916a1b6bb706da292c672b37ce1bee3a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Wed, 16 Apr 2014 12:50:45 +0200 Subject: [PATCH 060/206] adds public method to handle user input from an alert view --- Classes/BITCrashManager.h | 5 +++ Classes/BITCrashManager.m | 69 ++++++++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index dfff2f67..97d39c21 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -247,6 +247,11 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { */ @property (nonatomic, readonly) BOOL didCrashInLastSession; +/** +Provides an interface to handle user input from a custom alert + @return BOOl if the input is a valid option + */ +- (BOOL)handleUserInput: (BITCrashManagerUserInput) userInput; /** Provides the time between startup and crash in seconds diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 8dfff286..1bc77ea7 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -487,6 +487,36 @@ - (void)generateTestCrash { } } +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput { + switch (userInput) { + case BITCrashManagerUserInputDontSend: + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { + [self.delegate crashManagerWillCancelSendingCrashReport:self]; + } + + [self cleanCrashReports]; + YES; + + case BITCrashManagerUserInputSend: + [self sendCrashReports]; + YES; + + case BITCrashManagerUserInputAlwaysSend: + _crashManagerStatus = BITCrashManagerStatusAutoSend; + [[NSUserDefaults standardUserDefaults] setInteger:_crashManagerStatus forKey:kBITCrashManagerStatus]; + [[NSUserDefaults standardUserDefaults] synchronize]; + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReportsAlways:)]) { + [self.delegate crashManagerWillSendCrashReportsAlways:self]; + } + + [self sendCrashReports]; + return YES; + + default: + return NO; + } + +} #pragma mark - PLCrashReporter @@ -930,35 +960,20 @@ - (void)sendCrashReports { #pragma mark - UIAlertView Delegate - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { - [self handleUserInput:buttonIndex]; + switch (buttonIndex) { + case 0: + [self handleUserInput:BITCrashManagerUserInputDontSend]; + break; + case 1: + [self handleUserInput:BITCrashManagerUserInputSend]; + break; + case 2: + [self handleUserInput:BITCrashManagerUserInputAlwaysSend]; + break; + } } -- (void)handleUserInput:(BITCrashManagerUserInput)userInput { - switch (userInput) { - case BITCrashManagerUserInputDontSend: - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { - [self.delegate crashManagerWillCancelSendingCrashReport:self]; - } - - _sendingInProgress = NO; - [self cleanCrashReports]; - break; - case BITCrashManagerUserInputSend: - [self sendCrashReports]; - break; - case BITCrashManagerUserInputAlwaysSend: - _crashManagerStatus = BITCrashManagerStatusAutoSend; - [[NSUserDefaults standardUserDefaults] setInteger:_crashManagerStatus forKey:kBITCrashManagerStatus]; - [[NSUserDefaults standardUserDefaults] synchronize]; - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReportsAlways:)]) { - [self.delegate crashManagerWillSendCrashReportsAlways:self]; - } - - [self sendCrashReports]; - break; - } - -} + #pragma mark - Networking From 68c7fd95b4845e5aa3eb28857143d1ae4d791ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Wed, 16 Apr 2014 12:51:51 +0200 Subject: [PATCH 061/206] makes enum more explicit --- Classes/BITCrashManager.h | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 97d39c21..c20cce68 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -57,14 +57,22 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { */ BITCrashManagerStatusAutoSend = 2 }; - +/** + * Crash Manager alert user input + */ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { - - BITCrashManagerUserInputDontSend, - - BITCrashManagerUserInputSend, - - BITCrashManagerUserInputAlwaysSend + /** + * User chose not to send the crash report + */ + BITCrashManagerUserInputDontSend = 0, + /** + * User wants the crash report to be sent + */ + BITCrashManagerUserInputSend = 1, + /** + * User chose to always send crash reports + */ + BITCrashManagerUserInputAlwaysSend = 2 }; From 954e5c9ac2d54a35238c90f0e4f1281463b90009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Wed, 16 Apr 2014 15:29:54 +0200 Subject: [PATCH 062/206] add tests for handleUserInput method --- Support/HockeySDKTests/BITCrashManagerTests.m | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 099b608e..34510ba3 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -123,6 +123,40 @@ - (void)testUserEmailForCrashReport { [verifyCount(delegateMock, times(1)) userEmailForHockeyManager:hm componentManager:_sut]; } +#pragma mark - Handle User Input + +- (void)testHandleUserInputDontSend { + id delegateMock = mockProtocol(@protocol(BITCrashManagerDelegate)); + _sut.delegate = delegateMock; + + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputDontSend], equalToBool(YES)); + + [verify(delegateMock) crashManagerWillCancelSendingCrashReport:_sut]; + +} + +- (void)testHandleUserInputSend { + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputSend], equalToBool(YES)); +} + +- (void)testHandleUserInputAlwaysSend { + id delegateMock = mockProtocol(@protocol(BITCrashManagerDelegate)); + _sut.delegate = delegateMock; + NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]); + + //Test if CrashManagerStatus is unset + [given([mockUserDefaults integerForKey:@"BITCrashManagerStatus"]) willReturn:nil]; + + //Test if method runs through + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputAlwaysSend], equalToBool(YES)); + + //Test if correct CrashManagerStatus is now set + [given([mockUserDefaults integerForKey:@"BITCrashManagerStauts"]) willReturnInt:BITCrashManagerStatusAutoSend]; + + //Verify that delegate method has been called + [verify(delegateMock) crashManagerWillSendCrashReportsAlways:_sut]; + +} #pragma mark - Debugger From 91698b85a3e4a7d2dc9edcc655c805e9d06653ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Wed, 16 Apr 2014 17:12:17 +0200 Subject: [PATCH 063/206] updates formatting and documentation --- Classes/BITCrashManager.h | 12 +++++++++--- Classes/BITCrashManager.m | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index c20cce68..f52d3734 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -57,6 +57,8 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { */ BITCrashManagerStatusAutoSend = 2 }; + + /** * Crash Manager alert user input */ @@ -220,7 +222,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { * * @param callbacks A pointer to an initialized PLCrashReporterCallback structure, see https://www.plcrashreporter.org/documentation/api/v1.2-rc2/struct_p_l_crash_reporter_callbacks.html */ -- (void)setCrashCallbacks: (PLCrashReporterCallbacks *) callbacks; +- (void)setCrashCallbacks:(PLCrashReporterCallbacks *)callbacks; /** @@ -256,10 +258,14 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { @property (nonatomic, readonly) BOOL didCrashInLastSession; /** -Provides an interface to handle user input from a custom alert + Provides an interface to handle user input from a custom alert + + On this input depends, whether crash reports are sent, always sent or not sent and deleted. + @return BOOl if the input is a valid option + @see BITCrashManagerUserInput */ -- (BOOL)handleUserInput: (BITCrashManagerUserInput) userInput; +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput; /** Provides the time between startup and crash in seconds diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 1bc77ea7..0dbca1cc 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -174,9 +174,9 @@ - (void)saveSettings { NSString *errorString = nil; NSMutableDictionary *rootObj = [NSMutableDictionary dictionaryWithCapacity:2]; - if (_approvedCrashReports && [_approvedCrashReports count] > 0) + if (_approvedCrashReports && [_approvedCrashReports count] > 0) { [rootObj setObject:_approvedCrashReports forKey:kBITCrashApprovedReports]; - + } NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)rootObj format:NSPropertyListBinaryFormat_v1_0 errorDescription:&errorString]; @@ -438,7 +438,7 @@ - (NSString *)userEmailForCrashReport { #pragma mark - Public -- (void)setCrashCallbacks: (PLCrashReporterCallbacks *) callbacks { +- (void)setCrashCallbacks:(PLCrashReporterCallbacks *)callbacks { _crashCallBacks = callbacks; } From 973e130325aa27f63f4c6c1ce87ce75b9d293bf0 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 17 Apr 2014 22:50:22 +0200 Subject: [PATCH 064/206] Migrate BITCrashManager to use BITHockeyAppClient --- Classes/BITCrashManager.m | 217 ++++++++++++++----------------- Classes/BITCrashManagerPrivate.h | 7 + Classes/BITHockeyManager.m | 1 + 3 files changed, 105 insertions(+), 120 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 18b3aaf2..4d1bff75 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -82,11 +82,6 @@ @implementation BITCrashManager { BOOL _crashIdenticalCurrentVersion; - NSMutableData *_responseData; - NSInteger _statusCode; - - NSURLConnection *_urlConnection; - BOOL _sendingInProgress; BOOL _isSetup; @@ -106,9 +101,6 @@ - (id)init { _crashCallBacks = nil; _crashIdenticalCurrentVersion = YES; - _urlConnection = nil; - _responseData = nil; - _sendingInProgress = NO; _didCrashInLastSession = NO; _timeintervalCrashInLastSessionOccured = -1; @@ -151,8 +143,6 @@ - (id)init { - (void) dealloc { [self unregisterObservers]; - - [_urlConnection cancel]; } @@ -963,36 +953,35 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) #pragma mark - Networking -/** - * Send the XML data to the server - * - * Wraps the XML structure into a POST body and starts sending the data asynchronously - * - * @param xml The XML data that needs to be send to the server - */ -- (void)postXML:(NSString*)xml attachments:(NSArray *)attachments { - NSMutableURLRequest *request = nil; - NSString *boundary = @"----FOO"; +- (NSURLRequest *)requestWithXML:(NSString*)xml attachments:(NSArray *)attachments { + NSString *postCrashPath = [NSString stringWithFormat:@"api/2/apps/%@/crashes", self.encodedAppIdentifier]; - request = [NSMutableURLRequest requestWithURL: - [NSURL URLWithString:[NSString stringWithFormat:@"%@api/2/apps/%@/crashes?sdk=%@&sdk_version=%@&feedbackEnabled=no", - self.serverURL, - [self encodedAppIdentifier], - BITHOCKEY_NAME, - BITHOCKEY_VERSION - ] - ]]; + NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST" + path:postCrashPath + parameters:nil]; [request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData]; [request setValue:@"HockeySDK/iOS" forHTTPHeaderField:@"User-Agent"]; [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; - [request setTimeoutInterval: 15]; - [request setHTTPMethod:@"POST"]; + + NSString *boundary = @"----FOO"; NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; [request setValue:contentType forHTTPHeaderField:@"Content-type"]; NSMutableData *postBody = [NSMutableData data]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:BITHOCKEY_NAME + forKey:@"sdk" + boundary:boundary]]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:BITHOCKEY_VERSION + forKey:@"sdk_version" + boundary:boundary]]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:@"no" + forKey:@"feedbackEnabled" + boundary:boundary]]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:[xml dataUsingEncoding:NSUTF8StringEncoding] forKey:@"xml" contentType:@"text/xml" @@ -1011,107 +1000,95 @@ - (void)postXML:(NSString*)xml attachments:(NSArray *)attachments { boundary:boundary filename:attachment.filename]]; } - + [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(@"INFO: Sending crash reports could not start!"); - _sendingInProgress = NO; - } else { - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReport:)]) { - [self.delegate crashManagerWillSendCrashReport:self]; - } - - BITHockeyLog(@"INFO: 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]; + return request; } -- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { - [self.delegate crashManager:self didFailWithError:error]; - } +/** + * Send the XML data to the server + * + * Wraps the XML structure into a POST body and starts sending the data asynchronously + * + * @param xml The XML data that needs to be send to the server + */ +- (void)postXML:(NSString*)xml attachments:(NSArray *)attachments { - BITHockeyLog(@"ERROR: %@", [error localizedDescription]); + NSURLRequest* request = [self requestWithXML:xml attachments:attachments]; - _sendingInProgress = NO; - - _responseData = nil; - _urlConnection = nil; -} - -- (void)connectionDidFinishLoading:(NSURLConnection *)connection { - NSError *error = nil; + __weak typeof (self) weakSelf = self; + BITHTTPOperation *operation = [self.hockeyAppClient + operationWithURLRequest:request + completion:^(BITHTTPOperation *operation, NSData* responseData, NSError *error) { + typeof (self) strongSelf = weakSelf; + + _sendingInProgress = NO; + + NSInteger statusCode = [operation.response statusCode]; + + if (nil == error) { + if (nil == responseData || [responseData length] == 0) { + error = [NSError errorWithDomain:kBITCrashErrorDomain + code:BITCrashAPIReceivedEmptyResponse + userInfo:@{ + NSLocalizedDescriptionKey: @"Sending failed with an empty response!" + } + ]; + } else if (statusCode >= 200 && statusCode < 400) { + [strongSelf cleanCrashReports]; + + // HockeyApp uses PList XML format + NSMutableDictionary *response = [NSPropertyListSerialization propertyListFromData:responseData + mutabilityOption:NSPropertyListMutableContainersAndLeaves + format:nil + errorDescription:NULL]; + BITHockeyLog(@"INFO: Received API response: %@", response); + + if (strongSelf.delegate != nil && + [strongSelf.delegate respondsToSelector:@selector(crashManagerDidFinishSendingCrashReport:)]) { + [strongSelf.delegate crashManagerDidFinishSendingCrashReport:self]; + } + } else if (statusCode == 400) { + [strongSelf cleanCrashReports]; + + error = [NSError errorWithDomain:kBITCrashErrorDomain + code:BITCrashAPIAppVersionRejected + userInfo:@{ + NSLocalizedDescriptionKey: @"The server rejected receiving crash reports for this app version!" + } + ]; + } else { + error = [NSError errorWithDomain:kBITCrashErrorDomain + code:BITCrashAPIErrorWithStatusCode + userInfo:@{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Sending failed with status code: %li", (long)statusCode] + } + ]; + } + } + + if (error) { + if (strongSelf.delegate != nil && + [strongSelf.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { + [strongSelf.delegate crashManager:self didFailWithError:error]; + } + + BITHockeyLog(@"ERROR: %@", [error localizedDescription]); + } + + }]; - 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(@"INFO: 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: %li", (long)_statusCode], NSLocalizedDescriptionKey, nil]]; - } - - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { - [self.delegate crashManager:self didFailWithError:error]; - } - - BITHockeyLog(@"ERROR: %@", [error localizedDescription]); + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReport:)]) { + [self.delegate crashManagerWillSendCrashReport:self]; } - _sendingInProgress = NO; - - _responseData = nil; - _urlConnection = nil; -} + BITHockeyLog(@"INFO: Sending crash reports started."); + [self.hockeyAppClient enqeueHTTPOperation:operation]; +} @end diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index f2231342..90902a9d 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -31,9 +31,16 @@ #if HOCKEYSDK_FEATURE_CRASH_REPORTER +@class BITHockeyAppClient; + @interface BITCrashManager () { } +/** + * must be set + */ +@property (nonatomic, strong) BITHockeyAppClient *hockeyAppClient; + @property (nonatomic) NSUncaughtExceptionHandler *exceptionHandler; @property (nonatomic, strong) BITPLCrashReporter *plCrashReporter; diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index baa1a7e8..4fe3e873 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -604,6 +604,7 @@ - (void)initializeModules { #if HOCKEYSDK_FEATURE_CRASH_REPORTER BITHockeyLog(@"INFO: Setup CrashManager"); _crashManager = [[BITCrashManager alloc] initWithAppIdentifier:_appIdentifier isAppStoreEnvironment:_appStoreEnvironment]; + _crashManager.hockeyAppClient = [self hockeyAppClient]; _crashManager.delegate = _delegate; #endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ From 20c73dcdc3018a3cdade669b4c90b3ad640da137 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 17 Apr 2014 23:47:13 +0200 Subject: [PATCH 065/206] Send each crash report using an individual network request --- Classes/BITCrashManager.m | 107 ++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 61 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 4d1bff75..c348826e 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -204,28 +204,41 @@ - (void)loadSettings { } } + /** - * Remove all crash reports and stored meta data for each from the file system and keychain + * Remove a cached crash report + * + * @param filename The base filename of the crash report */ -- (void)cleanCrashReports { +- (void)cleanCrashReportWithFilename:(NSString *)filename { + if (!filename) return; + NSError *error = NULL; - for (NSUInteger i=0; i < [_crashFiles count]; i++) { - [_fileManager removeItemAtPath:[_crashFiles objectAtIndex:i] error:&error]; - [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".data"] error:&error]; - [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".meta"] error:&error]; - - NSString *cacheFilename = [[_crashFiles objectAtIndex:i] lastPathComponent]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; - } - [_crashFiles removeAllObjects]; - [_approvedCrashReports removeAllObjects]; + [_fileManager removeItemAtPath:filename error:&error]; + [_fileManager removeItemAtPath:[filename stringByAppendingString:@".data"] error:&error]; + [_fileManager removeItemAtPath:[filename stringByAppendingString:@".meta"] error:&error]; + + NSString *cacheFilename = [filename lastPathComponent]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; + + [_crashFiles removeObject:filename]; + [_approvedCrashReports removeObjectForKey:filename]; [self saveSettings]; } +/** + * Remove all crash reports and stored meta data for each from the file system and keychain + */ +- (void)cleanCrashReports { + for (NSUInteger i=0; i < [_crashFiles count]; i++) { + [self cleanCrashReportWithFilename:[_crashFiles objectAtIndex:i]]; + } +} + - (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename { NSString *attachmentFilename = [filename stringByAppendingString:@".data"]; @@ -242,7 +255,7 @@ - (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSStrin /** * Read the attachment data from the stored file * - * @param filename The crash report id + * @param filename The crash report file path * * @return an BITCrashAttachment instance or nil */ @@ -800,12 +813,13 @@ - (void)startManager { */ - (void)sendCrashReports { NSError *error = NULL; - - NSMutableString *crashes = nil; - NSMutableArray *attachments = [NSMutableArray array]; + _crashIdenticalCurrentVersion = NO; for (NSUInteger i=0; i < [_crashFiles count]; i++) { + NSString *crashXML = nil; + BITCrashAttachment *attachment = nil; + NSString *filename = [_crashFiles objectAtIndex:i]; NSString *cacheFilename = [filename lastPathComponent]; NSData *crashData = [NSData dataWithContentsOfFile:filename]; @@ -816,13 +830,7 @@ - (void)sendCrashReports { if (report == nil) { BITHockeyLog(@"WARNING: Could not parse crash report"); // we cannot do anything with this report, so delete it - [_fileManager removeItemAtPath:filename error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.data", filename] error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; - - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; + [self cleanCrashReportWithFilename:filename]; continue; } @@ -837,10 +845,6 @@ - (void)sendCrashReports { _crashIdenticalCurrentVersion = YES; } - if (crashes == nil) { - crashes = [NSMutableString string]; - } - NSString *username = @""; NSString *useremail = @""; NSString *userid = @""; @@ -863,12 +867,7 @@ - (void)sendCrashReports { userid = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]] ?: @""; applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; - BITCrashAttachment *attachment = [self attachmentForCrashReport:filename]; - if (attachment) { - NSDictionary *attachmentDict = @{KBITAttachmentDictIndex: @(i), - KBITAttachmentDictAttachment: attachment}; - [attachments addObject:attachmentDict]; - } + attachment = [self attachmentForCrashReport:filename]; } else { BITHockeyLog(@"ERROR: Reading crash meta data. %@", error); } @@ -877,7 +876,7 @@ - (void)sendCrashReports { description = [NSString stringWithFormat:@"%@", applicationLog]; } - [crashes appendFormat:@"%s%@%@%@%@%@%@%@%@%@%@%@", + crashXML = [NSString stringWithFormat:@"%s%@%@%@%@%@%@%@%@%@%@%@", [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String], [self extractAppUUIDs:report], report.applicationInfo.applicationIdentifier, @@ -893,27 +892,18 @@ - (void)sendCrashReports { installString, [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]; + + BITHockeyLog(@"INFO: Sending crash reports:\n%@", crashXML); + [self sendCrashReportWithFilename:filename xml:crashXML attachment:attachment]; } else { // we cannot do anything with this report, so delete it - [_fileManager removeItemAtPath:filename error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.data", filename] error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; - - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; + [self cleanCrashReportWithFilename:filename]; } } [self saveSettings]; - - if (crashes != nil) { - BITHockeyLog(@"INFO: Sending crash reports:\n%@", crashes); - [self postXML:[NSString stringWithFormat:@"%@", crashes] attachments:attachments]; - } } @@ -953,7 +943,7 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) #pragma mark - Networking -- (NSURLRequest *)requestWithXML:(NSString*)xml attachments:(NSArray *)attachments { +- (NSURLRequest *)requestWithXML:(NSString*)xml attachment:(BITCrashAttachment *)attachment { NSString *postCrashPath = [NSString stringWithFormat:@"api/2/apps/%@/crashes", self.encodedAppIdentifier]; NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST" @@ -988,14 +978,9 @@ - (NSURLRequest *)requestWithXML:(NSString*)xml attachments:(NSArray *)attachmen boundary:boundary filename:@"crash.xml"]]; - for (NSDictionary *dict in attachments) { - NSInteger index = [(NSNumber *)dict[KBITAttachmentDictIndex] integerValue]; - NSString *key = [NSString stringWithFormat:@"attachment%ld", (long)index]; - - BITCrashAttachment *attachment = (BITCrashAttachment *)dict[KBITAttachmentDictAttachment]; - + if (attachment) { [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.attachmentData - forKey:key + forKey:@"attachment0" contentType:attachment.contentType boundary:boundary filename:attachment.filename]]; @@ -1015,9 +1000,9 @@ - (NSURLRequest *)requestWithXML:(NSString*)xml attachments:(NSArray *)attachmen * * @param xml The XML data that needs to be send to the server */ -- (void)postXML:(NSString*)xml attachments:(NSArray *)attachments { +- (void)sendCrashReportWithFilename:(NSString *)filename xml:(NSString*)xml attachment:(BITCrashAttachment *)attachment { - NSURLRequest* request = [self requestWithXML:xml attachments:attachments]; + NSURLRequest* request = [self requestWithXML:xml attachment:attachment]; __weak typeof (self) weakSelf = self; BITHTTPOperation *operation = [self.hockeyAppClient @@ -1038,7 +1023,7 @@ - (void)postXML:(NSString*)xml attachments:(NSArray *)attachments { } ]; } else if (statusCode >= 200 && statusCode < 400) { - [strongSelf cleanCrashReports]; + [strongSelf cleanCrashReportWithFilename:filename]; // HockeyApp uses PList XML format NSMutableDictionary *response = [NSPropertyListSerialization propertyListFromData:responseData @@ -1052,7 +1037,7 @@ - (void)postXML:(NSString*)xml attachments:(NSArray *)attachments { [strongSelf.delegate crashManagerDidFinishSendingCrashReport:self]; } } else if (statusCode == 400) { - [strongSelf cleanCrashReports]; + [strongSelf cleanCrashReportWithFilename:filename]; error = [NSError errorWithDomain:kBITCrashErrorDomain code:BITCrashAPIAppVersionRejected From fd35eee132dcc719ac049679151c38fe0873589c Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Mon, 21 Apr 2014 16:50:25 +0200 Subject: [PATCH 066/206] Update CocoaSpec with a fix for some rare integration issues --- HockeySDK.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HockeySDK.podspec b/HockeySDK.podspec index ebfd9eef..75107724 100644 --- a/HockeySDK.podspec +++ b/HockeySDK.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/bitstadium/HockeySDK-iOS.git', :tag => s.version.to_s } s.platform = :ios, '5.0' - s.source_files = 'Classes' + s.source_files = 'Classes', "Vendor/CrashReporter.framework/Versions/A/Headers/*.h" s.requires_arc = true s.frameworks = 'CoreText', 'QuartzCore', 'SystemConfiguration', 'CoreGraphics', 'UIKit', 'Security' From d22fb02ec20a3ee7530e9a7cb7b0a57aa3dc9177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Tue, 22 Apr 2014 17:52:14 +0200 Subject: [PATCH 067/206] Add ability to add a description to a crash report --- Classes/BITCrashManager.h | 2 +- Classes/BITCrashManager.m | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index f52d3734..0682dffd 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -265,7 +265,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { @return BOOl if the input is a valid option @see BITCrashManagerUserInput */ -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput; +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescription:metaDescription; /** Provides the time between startup and crash in seconds diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 0dbca1cc..c1110b56 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -55,6 +55,7 @@ #define kBITCrashMetaUserID @"BITCrashMetaUserID" #define kBITCrashMetaApplicationLog @"BITCrashMetaApplicationLog" #define kBITCrashMetaAttachment @"BITCrashMetaAttachment" +#define kBITCrashMetaDescription @"BITCrashMetaDescription" // internal keys NSString *const KBITAttachmentDictIndex = @"index"; @@ -74,6 +75,7 @@ @implementation BITCrashManager { NSMutableArray *_crashFiles; NSString *_crashesDir; + NSString *_lastCrashFilename; NSString *_settingsFile; NSString *_analyzerInProgressFile; NSFileManager *_fileManager; @@ -487,7 +489,7 @@ - (void)generateTestCrash { } } -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput { +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescription:(NSString *)metaDescription{ switch (userInput) { case BITCrashManagerUserInputDontSend: if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { @@ -498,6 +500,11 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput { YES; case BITCrashManagerUserInputSend: + if (!metaDescription && [metaDescription length] > 0) { + NSError *error; + [metaDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; + BITHockeyLog(@"ERROR: Writing crash meta description failed. %@", error); + } [self sendCrashReports]; YES; @@ -591,6 +598,7 @@ - (void) handleCrashReport { } else { BITHockeyLog(@"ERROR: Writing crash meta data failed. %@", error); } + _lastCrashFilename = cacheFilename; } } } @@ -626,7 +634,7 @@ - (BOOL)hasNonApprovedCrashReports { /** * Check if there are any new crash reports that are not yet processed * - * @return `YES` if ther eis at least one new crash report found, `NO` otherwise + * @return `YES` if there is at least one new crash report found, `NO` otherwise */ - (BOOL)hasPendingCrashReport { if (_crashManagerStatus == BITCrashManagerStatusDisabled) return NO; @@ -902,7 +910,8 @@ - (void)sendCrashReports { useremail = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]] ?: @""; userid = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]] ?: @""; applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; - + description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] encoding:NSUTF8StringEncoding error:&error]; + BITCrashAttachment *attachment = [self attachmentForCrashReport:filename]; if (attachment) { NSDictionary *attachmentDict = @{KBITAttachmentDictIndex: @(i), @@ -914,7 +923,11 @@ - (void)sendCrashReports { } if ([applicationLog length] > 0) { - description = [NSString stringWithFormat:@"%@", applicationLog]; + if ([description length] > 0) { + description = [NSString stringWithFormat:@"%@\n\nLog:\n%@", description, applicationLog]; + } else { + description = [NSString stringWithFormat:@"Log:\n%@", applicationLog]; + } } [crashes appendFormat:@"%s%@%@%@%@%@%@%@%@%@%@%@", @@ -962,13 +975,13 @@ - (void)sendCrashReports { - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 0: - [self handleUserInput:BITCrashManagerUserInputDontSend]; + [self handleUserInput:BITCrashManagerUserInputDontSend withCrashMetaDescription:nil]; break; case 1: - [self handleUserInput:BITCrashManagerUserInputSend]; + [self handleUserInput:BITCrashManagerUserInputSend withCrashMetaDescription:nil]; break; case 2: - [self handleUserInput:BITCrashManagerUserInputAlwaysSend]; + [self handleUserInput:BITCrashManagerUserInputAlwaysSend withCrashMetaDescription:nil]; break; } } From 0e9e0e87363ccb1ce0b2d0bb04b86fa267486f67 Mon Sep 17 00:00:00 2001 From: Steven Fisher Date: Tue, 22 Apr 2014 12:45:06 -0700 Subject: [PATCH 068/206] Remove x flag from BITAttributedLabel. --- Classes/BITAttributedLabel.h | 0 Classes/BITAttributedLabel.m | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 Classes/BITAttributedLabel.h mode change 100755 => 100644 Classes/BITAttributedLabel.m diff --git a/Classes/BITAttributedLabel.h b/Classes/BITAttributedLabel.h old mode 100755 new mode 100644 diff --git a/Classes/BITAttributedLabel.m b/Classes/BITAttributedLabel.m old mode 100755 new mode 100644 From f30129bb345f562848dbc5100e6711eb152f31b8 Mon Sep 17 00:00:00 2001 From: Steven Fisher Date: Tue, 22 Apr 2014 12:46:31 -0700 Subject: [PATCH 069/206] Remove +x from LICENSE. --- LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 LICENSE diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 From 7a5077e7c15d587751f574c4ba99dd81cce26198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Wed, 23 Apr 2014 16:09:23 +0200 Subject: [PATCH 070/206] semi-working prototype of custom Alert --- Classes/BITCrashManager.h | 6 ++++++ Classes/BITCrashManager.m | 31 ++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 0682dffd..0b7c2a75 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -267,6 +267,12 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { */ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescription:metaDescription; +/** + Property that lets you set a custom block which handles showing a custom UI and asking the user + whether he wants to send the crash report. Needs to call the `handleUserInput` method. + */ +@property (nonatomic, copy, setter = setAlertViewHandler:) void (^alertViewHandler) (); + /** Provides the time between startup and crash in seconds diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index c1110b56..1e89ce51 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -116,6 +116,7 @@ - (id)init { _timeintervalCrashInLastSessionOccured = -1; _approvedCrashReports = [[NSMutableDictionary alloc] init]; + _alertViewHandler = nil; _fileManager = [[NSFileManager alloc] init]; _crashFiles = [[NSMutableArray alloc] init]; @@ -489,6 +490,10 @@ - (void)generateTestCrash { } } +- (void)setAlertViewHandler:(void (^)())alertViewHandler{ + _alertViewHandler = alertViewHandler; +} + - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescription:(NSString *)metaDescription{ switch (userInput) { case BITCrashManagerUserInputDontSend: @@ -500,7 +505,7 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescrip YES; case BITCrashManagerUserInputSend: - if (!metaDescription && [metaDescription length] > 0) { + if (metaDescription && [metaDescription length] > 0) { NSError *error; [metaDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; BITHockeyLog(@"ERROR: Writing crash meta description failed. %@", error); @@ -730,17 +735,21 @@ - (void)invokeDelayedProcessing { 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")]; + if (_alertViewHandler) { + _alertViewHandler(); + } else { + 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 show]; } else { [self sendCrashReports]; } From 15582b773481791ded907290736f1fe06296ba12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 12:32:01 +0200 Subject: [PATCH 071/206] Cleanup and small logic error fix --- Classes/BITCrashManager.m | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 1e89ce51..d39c5977 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -445,6 +445,11 @@ - (void)setCrashCallbacks:(PLCrashReporterCallbacks *)callbacks { _crashCallBacks = callbacks; } + +- (void)setAlertViewHandler:(void (^)())alertViewHandler{ + _alertViewHandler = alertViewHandler; +} + /** * Check if the debugger is attached * @@ -490,9 +495,6 @@ - (void)generateTestCrash { } } -- (void)setAlertViewHandler:(void (^)())alertViewHandler{ - _alertViewHandler = alertViewHandler; -} - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescription:(NSString *)metaDescription{ switch (userInput) { @@ -508,7 +510,6 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescrip if (metaDescription && [metaDescription length] > 0) { NSError *error; [metaDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; - BITHockeyLog(@"ERROR: Writing crash meta description failed. %@", error); } [self sendCrashReports]; YES; @@ -555,6 +556,7 @@ - (void) handleCrashReport { NSData *crashData = [[NSData alloc] initWithData:[self.plCrashReporter loadPendingCrashReportDataAndReturnError: &error]]; NSString *cacheFilename = [NSString stringWithFormat: @"%.0f", [NSDate timeIntervalSinceReferenceDate]]; + _lastCrashFilename = cacheFilename; if (crashData == nil) { BITHockeyLog(@"ERROR: Could not load crash report: %@", error); @@ -603,7 +605,6 @@ - (void) handleCrashReport { } else { BITHockeyLog(@"ERROR: Writing crash meta data failed. %@", error); } - _lastCrashFilename = cacheFilename; } } } @@ -919,7 +920,7 @@ - (void)sendCrashReports { useremail = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]] ?: @""; userid = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]] ?: @""; applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; - description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] encoding:NSUTF8StringEncoding error:&error]; + description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: cacheFilename]] encoding:NSUTF8StringEncoding error:&error]; BITCrashAttachment *attachment = [self attachmentForCrashReport:filename]; if (attachment) { From a05e15463b15da16bd594c666c55b5bb4b052bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 15:37:34 +0200 Subject: [PATCH 072/206] Update tests for new method syntax --- Support/HockeySDKTests/BITCrashManagerTests.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 34510ba3..4e1c09e7 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -129,14 +129,14 @@ - (void)testHandleUserInputDontSend { id delegateMock = mockProtocol(@protocol(BITCrashManagerDelegate)); _sut.delegate = delegateMock; - assertThatBool([_sut handleUserInput:BITCrashManagerUserInputDontSend], equalToBool(YES)); + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputDontSend crashMetaDescription:nil], equalToBool(YES)); [verify(delegateMock) crashManagerWillCancelSendingCrashReport:_sut]; } - (void)testHandleUserInputSend { - assertThatBool([_sut handleUserInput:BITCrashManagerUserInputSend], equalToBool(YES)); + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputSend crashMetaDescription:nil], equalToBool(YES)); } - (void)testHandleUserInputAlwaysSend { @@ -148,7 +148,7 @@ - (void)testHandleUserInputAlwaysSend { [given([mockUserDefaults integerForKey:@"BITCrashManagerStatus"]) willReturn:nil]; //Test if method runs through - assertThatBool([_sut handleUserInput:BITCrashManagerUserInputAlwaysSend], equalToBool(YES)); + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputAlwaysSend crashMetaDescription:nil], equalToBool(YES)); //Test if correct CrashManagerStatus is now set [given([mockUserDefaults integerForKey:@"BITCrashManagerStauts"]) willReturnInt:BITCrashManagerStatusAutoSend]; From 27cc84a84c8b96e40d432f5581192f7295f42951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 15:38:10 +0200 Subject: [PATCH 073/206] Maybe a switch case should return a value... --- Classes/BITCrashManager.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index d39c5977..e4561fac 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -504,7 +504,7 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescrip } [self cleanCrashReports]; - YES; + return YES; case BITCrashManagerUserInputSend: if (metaDescription && [metaDescription length] > 0) { @@ -512,7 +512,7 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescrip [metaDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; } [self sendCrashReports]; - YES; + return YES; case BITCrashManagerUserInputAlwaysSend: _crashManagerStatus = BITCrashManagerStatusAutoSend; From 4aee9f81b547046164bad62ad5a96de35d2e1270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 15:38:31 +0200 Subject: [PATCH 074/206] Small optimizations --- Classes/BITCrashManager.h | 2 +- Classes/BITCrashManager.m | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 0b7c2a75..76dd9866 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -265,7 +265,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { @return BOOl if the input is a valid option @see BITCrashManagerUserInput */ -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescription:metaDescription; +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput crashMetaDescription:metaDescription; /** Property that lets you set a custom block which handles showing a custom UI and asking the user diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index e4561fac..8fa17d22 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -496,7 +496,7 @@ - (void)generateTestCrash { } -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescription:(NSString *)metaDescription{ +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput crashMetaDescription:(NSString *)metaDescription{ switch (userInput) { case BITCrashManagerUserInputDontSend: if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { @@ -522,6 +522,11 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withCrashMetaDescrip [self.delegate crashManagerWillSendCrashReportsAlways:self]; } + if (metaDescription && [metaDescription length] > 0) { + NSError *error; + [metaDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; + } + [self sendCrashReports]; return YES; @@ -985,13 +990,13 @@ - (void)sendCrashReports { - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 0: - [self handleUserInput:BITCrashManagerUserInputDontSend withCrashMetaDescription:nil]; + [self handleUserInput:BITCrashManagerUserInputDontSend crashMetaDescription:nil]; break; case 1: - [self handleUserInput:BITCrashManagerUserInputSend withCrashMetaDescription:nil]; + [self handleUserInput:BITCrashManagerUserInputSend crashMetaDescription:nil]; break; case 2: - [self handleUserInput:BITCrashManagerUserInputAlwaysSend withCrashMetaDescription:nil]; + [self handleUserInput:BITCrashManagerUserInputAlwaysSend crashMetaDescription:nil]; break; } } From 13118462413ff8e1583ce81d7626737e62fd029b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 16:10:12 +0200 Subject: [PATCH 075/206] update method signature --- Classes/BITCrashManager.h | 4 ++-- Classes/BITCrashManager.m | 8 ++++---- Support/HockeySDKTests/BITCrashManagerTests.m | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 76dd9866..47614596 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -265,13 +265,13 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { @return BOOl if the input is a valid option @see BITCrashManagerUserInput */ -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput crashMetaDescription:metaDescription; +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString*)metaDescription; /** Property that lets you set a custom block which handles showing a custom UI and asking the user whether he wants to send the crash report. Needs to call the `handleUserInput` method. */ -@property (nonatomic, copy, setter = setAlertViewHandler:) void (^alertViewHandler) (); +@property (nonatomic, copy, setter = setAlertViewHandler:) customAlertViewHandler alertViewHandler; /** Provides the time between startup and crash in seconds diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 8fa17d22..303c0d09 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -496,7 +496,7 @@ - (void)generateTestCrash { } -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput crashMetaDescription:(NSString *)metaDescription{ +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString *)metaDescription{ switch (userInput) { case BITCrashManagerUserInputDontSend: if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { @@ -990,13 +990,13 @@ - (void)sendCrashReports { - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 0: - [self handleUserInput:BITCrashManagerUserInputDontSend crashMetaDescription:nil]; + [self handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedCrashDescription:nil]; break; case 1: - [self handleUserInput:BITCrashManagerUserInputSend crashMetaDescription:nil]; + [self handleUserInput:BITCrashManagerUserInputSend withUserProvidedCrashDescription:nil]; break; case 2: - [self handleUserInput:BITCrashManagerUserInputAlwaysSend crashMetaDescription:nil]; + [self handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedCrashDescription:nil]; break; } } diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 4e1c09e7..45f6c044 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -129,14 +129,14 @@ - (void)testHandleUserInputDontSend { id delegateMock = mockProtocol(@protocol(BITCrashManagerDelegate)); _sut.delegate = delegateMock; - assertThatBool([_sut handleUserInput:BITCrashManagerUserInputDontSend crashMetaDescription:nil], equalToBool(YES)); + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedCrashDescription:nil], equalToBool(YES)); [verify(delegateMock) crashManagerWillCancelSendingCrashReport:_sut]; } - (void)testHandleUserInputSend { - assertThatBool([_sut handleUserInput:BITCrashManagerUserInputSend crashMetaDescription:nil], equalToBool(YES)); + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputSend withUserProvidedCrashDescription:nil], equalToBool(YES)); } - (void)testHandleUserInputAlwaysSend { @@ -148,7 +148,7 @@ - (void)testHandleUserInputAlwaysSend { [given([mockUserDefaults integerForKey:@"BITCrashManagerStatus"]) willReturn:nil]; //Test if method runs through - assertThatBool([_sut handleUserInput:BITCrashManagerUserInputAlwaysSend crashMetaDescription:nil], equalToBool(YES)); + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedCrashDescription:nil], equalToBool(YES)); //Test if correct CrashManagerStatus is now set [given([mockUserDefaults integerForKey:@"BITCrashManagerStauts"]) willReturnInt:BITCrashManagerStatusAutoSend]; From f8561afacc4e47a00ceccee2f9d29908674386e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 16:11:05 +0200 Subject: [PATCH 076/206] Updates documentation --- Classes/BITCrashManager.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 47614596..3afe4709 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -260,16 +260,19 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { /** Provides an interface to handle user input from a custom alert - On this input depends, whether crash reports are sent, always sent or not sent and deleted. + @param userInput On this input depends, whether crash reports are sent, always sent or not sent and deleted. + @param metaDescription The content of this string will be attached to the crash report as the description and allows to ask the user for e.g. additional comments or info - @return BOOl if the input is a valid option + @return Returns YES if the input is a valid option and successfully triggered further processing of the crash report @see BITCrashManagerUserInput */ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString*)metaDescription; /** Property that lets you set a custom block which handles showing a custom UI and asking the user - whether he wants to send the crash report. Needs to call the `handleUserInput` method. + whether he wants to send the crash report. + + @warning Needs to call the `handleUserInput` method! */ @property (nonatomic, copy, setter = setAlertViewHandler:) customAlertViewHandler alertViewHandler; From 3fd8cdc7e0b4d4b3227302575b29dff2498e088c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 16:11:34 +0200 Subject: [PATCH 077/206] replace ugly block notation with clean typedef --- Classes/BITCrashManager.h | 5 +++++ Classes/BITCrashManager.m | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 3afe4709..9a6ba8a0 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -39,6 +39,11 @@ #import #endif +/** + * Custom block that handles the alert that prompts the user whether he wants to send crash reports + */ +typedef void(^customAlertViewHandler)(); + /** * Crash Manager status diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 303c0d09..f4cb6e15 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -446,7 +446,7 @@ - (void)setCrashCallbacks:(PLCrashReporterCallbacks *)callbacks { } -- (void)setAlertViewHandler:(void (^)())alertViewHandler{ +- (void)setAlertViewHandler:(customAlertViewHandler)alertViewHandler{ _alertViewHandler = alertViewHandler; } From 10c5475a02b5387b006e119384e8f0ba69091d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 16:32:34 +0200 Subject: [PATCH 078/206] make method signature consistent and reflect those changes in documentation --- Classes/BITCrashManager.h | 4 ++-- Classes/BITCrashManager.m | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 9a6ba8a0..825ec489 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -266,12 +266,12 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { Provides an interface to handle user input from a custom alert @param userInput On this input depends, whether crash reports are sent, always sent or not sent and deleted. - @param metaDescription The content of this string will be attached to the crash report as the description and allows to ask the user for e.g. additional comments or info + @param userProvidedCrashDescription The content of this string will be attached to the crash report as the description and allows to ask the user for e.g. additional comments or info @return Returns YES if the input is a valid option and successfully triggered further processing of the crash report @see BITCrashManagerUserInput */ -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString*)metaDescription; +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString*)userProvidedCrashDescription; /** Property that lets you set a custom block which handles showing a custom UI and asking the user diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index f4cb6e15..8f932764 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -496,7 +496,7 @@ - (void)generateTestCrash { } -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString *)metaDescription{ +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString *)userProvidedCrashDescription{ switch (userInput) { case BITCrashManagerUserInputDontSend: if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { @@ -507,9 +507,9 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCras return YES; case BITCrashManagerUserInputSend: - if (metaDescription && [metaDescription length] > 0) { + if (userProvidedCrashDescription && [userProvidedCrashDescription length] > 0) { NSError *error; - [metaDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; + [userProvidedCrashDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; } [self sendCrashReports]; return YES; @@ -522,9 +522,9 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCras [self.delegate crashManagerWillSendCrashReportsAlways:self]; } - if (metaDescription && [metaDescription length] > 0) { + if (userProvidedCrashDescription && [userProvidedCrashDescription length] > 0) { NSError *error; - [metaDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; + [userProvidedCrashDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; } [self sendCrashReports]; From b5348f7dcbd33392164ae5e66fd085dbcab7f5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 16:38:23 +0200 Subject: [PATCH 079/206] Types should start with a capital letter --- Classes/BITCrashManager.h | 2 +- Classes/BITCrashManager.m | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 825ec489..8312702a 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -42,7 +42,7 @@ /** * Custom block that handles the alert that prompts the user whether he wants to send crash reports */ -typedef void(^customAlertViewHandler)(); +typedef void(^CustomAlertViewHandler)(); /** diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 8f932764..3c546c47 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -55,7 +55,6 @@ #define kBITCrashMetaUserID @"BITCrashMetaUserID" #define kBITCrashMetaApplicationLog @"BITCrashMetaApplicationLog" #define kBITCrashMetaAttachment @"BITCrashMetaAttachment" -#define kBITCrashMetaDescription @"BITCrashMetaDescription" // internal keys NSString *const KBITAttachmentDictIndex = @"index"; @@ -67,6 +66,7 @@ @interface BITCrashManager () @property (nonatomic, strong) NSFileManager *fileManager; +@property (nonatomic, copy, setter = setAlertViewHandler:) CustomAlertViewHandler alertViewHandler; @end @@ -79,7 +79,7 @@ @implementation BITCrashManager { NSString *_settingsFile; NSString *_analyzerInProgressFile; NSFileManager *_fileManager; - + PLCrashReporterCallbacks *_crashCallBacks; BOOL _crashIdenticalCurrentVersion; @@ -446,7 +446,7 @@ - (void)setCrashCallbacks:(PLCrashReporterCallbacks *)callbacks { } -- (void)setAlertViewHandler:(customAlertViewHandler)alertViewHandler{ +- (void)setAlertViewHandler:(CustomAlertViewHandler)alertViewHandler{ _alertViewHandler = alertViewHandler; } From e026e42664c009aed605e5eca22fafee395a0999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 16:41:07 +0200 Subject: [PATCH 080/206] expose setter rather than the whole property --- Classes/BITCrashManager.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 8312702a..0e0c6996 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -274,12 +274,12 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString*)userProvidedCrashDescription; /** - Property that lets you set a custom block which handles showing a custom UI and asking the user + Lets you set a custom block which handles showing a custom UI and asking the user whether he wants to send the crash report. - @warning Needs to call the `handleUserInput` method! + @warning Block needs to call the `handleUserInput:withUserProvidedCrashDescription` method! */ -@property (nonatomic, copy, setter = setAlertViewHandler:) customAlertViewHandler alertViewHandler; +- (void) setAlertViewHandler:(CustomAlertViewHandler)alertViewHandler; /** Provides the time between startup and crash in seconds From 3b2b76b34ec6bbfc46279598a8af1bce87db8eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 16:46:11 +0200 Subject: [PATCH 081/206] move persisting of userProvidedCrashDescription to own method --- Classes/BITCrashManager.m | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 3c546c47..f6b414ae 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -252,6 +252,13 @@ - (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSStrin [data writeToFile:attachmentFilename atomically:YES]; } +- (void)persistUserProvidedCrashDescription:(NSString *)userProvidedCrashDescription { + if (userProvidedCrashDescription && [userProvidedCrashDescription length] > 0) { + NSError *error; + [userProvidedCrashDescription writeToFile:[NSString stringWithFormat:@"%@.desc", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; + } +} + /** * Read the attachment data from the stored file * @@ -507,10 +514,8 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCras return YES; case BITCrashManagerUserInputSend: - if (userProvidedCrashDescription && [userProvidedCrashDescription length] > 0) { - NSError *error; - [userProvidedCrashDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; - } + [self persistUserProvidedCrashDescription:userProvidedCrashDescription]; + [self sendCrashReports]; return YES; @@ -522,10 +527,7 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCras [self.delegate crashManagerWillSendCrashReportsAlways:self]; } - if (userProvidedCrashDescription && [userProvidedCrashDescription length] > 0) { - NSError *error; - [userProvidedCrashDescription writeToFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; - } + [self persistUserProvidedCrashDescription:userProvidedCrashDescription]; [self sendCrashReports]; return YES; From 5442b217eb2e355cfb75cbe0a9fe3229b53e3e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 16:47:47 +0200 Subject: [PATCH 082/206] Use .desc file extension for userProvidedCrashDescription --- Classes/BITCrashManager.m | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index f6b414ae..e0e5e3b1 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -227,6 +227,8 @@ - (void)cleanCrashReports { [_fileManager removeItemAtPath:[_crashFiles objectAtIndex:i] error:&error]; [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".data"] error:&error]; [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".meta"] error:&error]; + [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".desc"] error:&error]; + NSString *cacheFilename = [[_crashFiles objectAtIndex:i] lastPathComponent]; [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; @@ -665,7 +667,8 @@ - (BOOL)hasPendingCrashReport { ![file hasSuffix:@".analyzer"] && ![file hasSuffix:@".plist"] && ![file hasSuffix:@".data"] && - ![file hasSuffix:@".meta"]) { + ![file hasSuffix:@".meta"] && + ![file hasSuffix:@".desc"]) { [_crashFiles addObject:[_crashesDir stringByAppendingPathComponent: file]]; } } @@ -884,6 +887,7 @@ - (void)sendCrashReports { [_fileManager removeItemAtPath:filename error:&error]; [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.data", filename] error:&error]; [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; + [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.desc", filename] error:&error]; [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; @@ -927,7 +931,7 @@ - (void)sendCrashReports { useremail = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]] ?: @""; userid = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]] ?: @""; applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; - description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@_description.meta", [_crashesDir stringByAppendingPathComponent: cacheFilename]] encoding:NSUTF8StringEncoding error:&error]; + description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [_crashesDir stringByAppendingPathComponent: cacheFilename]] encoding:NSUTF8StringEncoding error:&error]; BITCrashAttachment *attachment = [self attachmentForCrashReport:filename]; if (attachment) { From 080eb3c657aa5352d3f1e08d6e4fc96e0f1d3222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 16:58:31 +0200 Subject: [PATCH 083/206] More detailed documentation --- Classes/BITCrashManager.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 0e0c6996..fbcbcd95 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -266,7 +266,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { Provides an interface to handle user input from a custom alert @param userInput On this input depends, whether crash reports are sent, always sent or not sent and deleted. - @param userProvidedCrashDescription The content of this string will be attached to the crash report as the description and allows to ask the user for e.g. additional comments or info + @param userProvidedCrashDescription The content of this optional string will be attached to the crash report as the description and allows to ask the user for e.g. additional comments or info @return Returns YES if the input is a valid option and successfully triggered further processing of the crash report @see BITCrashManagerUserInput @@ -275,7 +275,9 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { /** Lets you set a custom block which handles showing a custom UI and asking the user - whether he wants to send the crash report. + whether he wants to send the crash report. + + @param alertViewHandler A block that is responsible for loading, presenting and and dismissing your custom user interface which prompts the user if he wants to send crash reports. The block is also responsible for triggering further processing of the crash reports. @warning Block needs to call the `handleUserInput:withUserProvidedCrashDescription` method! */ From d232fe58f6cc09b9dc4814c7f1c7960537518a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 18:17:46 +0200 Subject: [PATCH 084/206] move cleanup to separate method --- Classes/BITCrashManager.m | 40 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index e0e5e3b1..fd7bda8e 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -224,16 +224,11 @@ - (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:@".data"] error:&error]; - [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".meta"] error:&error]; - [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".desc"] error:&error]; - - + NSString *filename = [_crashFiles objectAtIndex:i]; NSString *cacheFilename = [[_crashFiles objectAtIndex:i] lastPathComponent]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; + + [self cleanFilesAndKeychainWithFileName:filename CacheFilename:cacheFilename Error:error]; + } [_crashFiles removeAllObjects]; [_approvedCrashReports removeAllObjects]; @@ -241,6 +236,16 @@ - (void)cleanCrashReports { [self saveSettings]; } +- (void)cleanFilesAndKeychainWithFileName:(NSString *)filename CacheFilename:(NSString *)cacheFilename Error:(NSError *)error{ + [_fileManager removeItemAtPath:filename error:&error]; + [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.data", filename] error:&error]; + [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; + [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.desc", filename] error:&error]; + + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; +} - (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename { NSString *attachmentFilename = [filename stringByAppendingString:@".data"]; @@ -884,14 +889,7 @@ - (void)sendCrashReports { if (report == nil) { BITHockeyLog(@"WARNING: Could not parse crash report"); // we cannot do anything with this report, so delete it - [_fileManager removeItemAtPath:filename error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.data", filename] error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.desc", filename] error:&error]; - - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; + [self cleanFilesAndKeychainWithFileName:filename CacheFilename:cacheFilename Error:error]; continue; } @@ -972,13 +970,7 @@ - (void)sendCrashReports { [_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:@"%@.data", filename] error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; - - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; + [self cleanFilesAndKeychainWithFileName:filename CacheFilename:cacheFilename Error:error]; } } From 654620ee3f61cd672d836b738317470f45d8736f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Thu, 24 Apr 2014 18:18:09 +0200 Subject: [PATCH 085/206] add test for persistUserProvidedCrashDescription --- Classes/BITCrashManager.m | 7 +++++++ Classes/BITCrashManagerPrivate.h | 5 +++++ Support/HockeySDKTests/BITCrashManagerTests.m | 7 +++++++ 3 files changed, 19 insertions(+) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index fd7bda8e..5714696e 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -451,6 +451,13 @@ - (NSString *)userEmailForCrashReport { return useremail; } +- (NSString *)getCrashesDir { + return _crashesDir; +} + +- (void)setLastCrashFilename:(NSString *)lastCrashFilename { + _lastCrashFilename = lastCrashFilename; +} #pragma mark - Public diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index f2231342..cf429643 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -61,9 +61,14 @@ - (BOOL)hasPendingCrashReport; - (BOOL)hasNonApprovedCrashReports; +- (void)persistUserProvidedCrashDescription:(NSString *)userProvidedCrashDescription; + - (void)invokeDelayedProcessing; - (void)sendCrashReports; +- (NSString *)getCrashesDir; +- (void)setLastCrashFilename:(NSString *)lastCrashFilename; + @end diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 45f6c044..8cecb1fb 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -71,6 +71,13 @@ - (void)startManagerAutoSend { [self startManager]; } +- (void)testPersistUserProvidedCrashDescription { + [_sut setLastCrashFilename:@"temp"]; + [_sut persistUserProvidedCrashDescription:@"Test string"]; + NSError *error; + NSString *description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [[_sut getCrashesDir] stringByAppendingPathComponent: @"temp"]] encoding:NSUTF8StringEncoding error:&error]; + assertThat(description, equalTo(@"Test string")); +} #pragma mark - Setup Tests From 535537aa739a5e5ee43cd7c75ff42a8a4275033a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Fri, 25 Apr 2014 14:23:18 +0200 Subject: [PATCH 086/206] add additional tests --- Classes/BITCrashManager.m | 4 --- Classes/BITCrashManagerPrivate.h | 3 ++ Support/HockeySDKTests/BITCrashManagerTests.m | 30 ++++++++++++++----- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 5714696e..99ee0614 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -455,10 +455,6 @@ - (NSString *)getCrashesDir { return _crashesDir; } -- (void)setLastCrashFilename:(NSString *)lastCrashFilename { - _lastCrashFilename = lastCrashFilename; -} - #pragma mark - Public diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index cf429643..c12749e7 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -38,6 +38,8 @@ @property (nonatomic, strong) BITPLCrashReporter *plCrashReporter; +@property (nonatomic) NSString *lastCrashFilename; + #if HOCKEYSDK_FEATURE_AUTHENTICATOR // Only set via BITAuthenticator @@ -62,6 +64,7 @@ - (BOOL)hasNonApprovedCrashReports; - (void)persistUserProvidedCrashDescription:(NSString *)userProvidedCrashDescription; +- (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename; - (void)invokeDelayedProcessing; - (void)sendCrashReports; diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 8cecb1fb..efae8c1c 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -71,14 +71,6 @@ - (void)startManagerAutoSend { [self startManager]; } -- (void)testPersistUserProvidedCrashDescription { - [_sut setLastCrashFilename:@"temp"]; - [_sut persistUserProvidedCrashDescription:@"Test string"]; - NSError *error; - NSString *description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [[_sut getCrashesDir] stringByAppendingPathComponent: @"temp"]] encoding:NSUTF8StringEncoding error:&error]; - assertThat(description, equalTo(@"Test string")); -} - #pragma mark - Setup Tests - (void)testThatItInstantiates { @@ -88,6 +80,24 @@ - (void)testThatItInstantiates { #pragma mark - Persistence tests +- (void)testPersistUserProvidedCrashDescription { + NSString *tempCrashName = @"tempCrash"; + [_sut setLastCrashFilename:tempCrashName]; + [_sut persistUserProvidedCrashDescription:@"Test string"]; + + NSError *error; + NSString *description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [[_sut getCrashesDir] stringByAppendingPathComponent: tempCrashName]] encoding:NSUTF8StringEncoding error:&error]; + assertThat(description, equalTo(@"Test string")); +} + +- (void)testPersistAttachment { + BITCrashAttachment *attachment = mock([BITCrashAttachment class]); + NSString *filename = @"testAttachment"; + + [_sut persistAttachment:attachment withFilename:filename]; + + [given([NSData dataWithContentsOfFile:[filename stringByAppendingString:@".data"]]) willReturn:[NSMutableDictionary class]]; +} #pragma mark - Helper @@ -165,6 +175,10 @@ - (void)testHandleUserInputAlwaysSend { } +- (void)testHandleUserInputWithInvalidInput { + assertThatBool([_sut handleUserInput:3 withUserProvidedCrashDescription:nil], equalToBool(NO)); +} + #pragma mark - Debugger /** From 82b217f19d774e188d6161fe3b947bee167fa9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Fri, 25 Apr 2014 16:23:39 +0200 Subject: [PATCH 087/206] Much better test for attachment persisting --- Classes/BITCrashManager.m | 1 - Classes/BITCrashManagerPrivate.h | 4 ++++ Support/HockeySDKTests/BITCrashManagerTests.m | 17 +++++++++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 99ee0614..de9fb5db 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -65,7 +65,6 @@ @interface BITCrashManager () -@property (nonatomic, strong) NSFileManager *fileManager; @property (nonatomic, copy, setter = setAlertViewHandler:) CustomAlertViewHandler alertViewHandler; @end diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index c12749e7..36e8ce96 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -36,6 +36,8 @@ @property (nonatomic) NSUncaughtExceptionHandler *exceptionHandler; +@property (nonatomic, strong) NSFileManager *fileManager; + @property (nonatomic, strong) BITPLCrashReporter *plCrashReporter; @property (nonatomic) NSString *lastCrashFilename; @@ -66,6 +68,8 @@ - (void)persistUserProvidedCrashDescription:(NSString *)userProvidedCrashDescription; - (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename; +- (BITCrashAttachment *)attachmentForCrashReport:(NSString *)filename; + - (void)invokeDelayedProcessing; - (void)sendCrashReports; diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index efae8c1c..b6d02409 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -23,6 +23,7 @@ #import "BITTestHelper.h" +#define kBITCrashMetaAttachment @"BITCrashMetaAttachment" @interface BITCrashManagerTests : SenTestCase @@ -91,12 +92,20 @@ - (void)testPersistUserProvidedCrashDescription { } - (void)testPersistAttachment { - BITCrashAttachment *attachment = mock([BITCrashAttachment class]); - NSString *filename = @"testAttachment"; + NSString *filename = @"TestAttachment"; + NSData *data = [[NSData alloc] initWithBase64Encoding:@"TestData"]; + NSString* type = @"text/plain"; - [_sut persistAttachment:attachment withFilename:filename]; + BITCrashAttachment *originalAttachment = [[BITCrashAttachment alloc] initWithFilename:filename attachmentData:data contentType:type]; + NSString *attachmentFilename = [_sut.getCrashesDir stringByAppendingPathComponent:@"testAttachment"]; - [given([NSData dataWithContentsOfFile:[filename stringByAppendingString:@".data"]]) willReturn:[NSMutableDictionary class]]; + [_sut persistAttachment:originalAttachment withFilename:attachmentFilename]; + + BITCrashAttachment *decodedAttachment = [_sut attachmentForCrashReport:attachmentFilename]; + + assertThat(decodedAttachment.filename, equalTo(filename)); + assertThat(decodedAttachment.attachmentData, equalTo(data)); + assertThat(decodedAttachment.contentType, equalTo(type)); } #pragma mark - Helper From 01eef668d156d7a65ec0512a6138276545ec154d Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 27 Apr 2014 19:42:42 +0200 Subject: [PATCH 088/206] Remove +x from some more files --- .../Versions/A/Headers/HCInvocationMatcher.h | 0 Vendor/XcodeCoverage/.gitignore | 0 Vendor/XcodeCoverage/LICENSE.txt | 0 Vendor/XcodeCoverage/README.md | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 Support/HockeySDKTests/OCHamcrestIOS.framework/Versions/A/Headers/HCInvocationMatcher.h mode change 100755 => 100644 Vendor/XcodeCoverage/.gitignore mode change 100755 => 100644 Vendor/XcodeCoverage/LICENSE.txt mode change 100755 => 100644 Vendor/XcodeCoverage/README.md diff --git a/Support/HockeySDKTests/OCHamcrestIOS.framework/Versions/A/Headers/HCInvocationMatcher.h b/Support/HockeySDKTests/OCHamcrestIOS.framework/Versions/A/Headers/HCInvocationMatcher.h old mode 100755 new mode 100644 diff --git a/Vendor/XcodeCoverage/.gitignore b/Vendor/XcodeCoverage/.gitignore old mode 100755 new mode 100644 diff --git a/Vendor/XcodeCoverage/LICENSE.txt b/Vendor/XcodeCoverage/LICENSE.txt old mode 100755 new mode 100644 diff --git a/Vendor/XcodeCoverage/README.md b/Vendor/XcodeCoverage/README.md old mode 100755 new mode 100644 From 38a59fc73f89049ddad986fa74c32a7923512fda Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 27 Apr 2014 20:20:10 +0200 Subject: [PATCH 089/206] Naming convention cleanup --- Classes/BITCrashManager.h | 4 ++-- Classes/BITCrashManager.m | 8 +------- Classes/BITCrashManagerPrivate.h | 2 ++ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 2bb4f00f..49e32601 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -37,7 +37,7 @@ /** * Custom block that handles the alert that prompts the user whether he wants to send crash reports */ -typedef void(^CustomAlertViewHandler)(); +typedef void(^BITCustomAlertViewHandler)(); /** @@ -347,7 +347,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { @warning Block needs to call the `handleUserInput:withUserProvidedCrashDescription` method! */ -- (void) setAlertViewHandler:(CustomAlertViewHandler)alertViewHandler; +- (void) setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler; /** Indicates if the app was killed while being in foreground from the iOS diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 35306955..093792fe 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -95,12 +95,6 @@ static void plcr_post_crash_callback (siginfo_t *info, ucontext_t *uap, void *co }; -@interface BITCrashManager () - -@property (nonatomic, copy, setter = setAlertViewHandler:) CustomAlertViewHandler alertViewHandler; - -@end - @implementation BITCrashManager { NSMutableDictionary *_approvedCrashReports; @@ -608,7 +602,7 @@ - (void)setCrashCallbacks: (BITCrashManagerCallbacks *) callbacks { } -- (void)setAlertViewHandler:(CustomAlertViewHandler)alertViewHandler{ +- (void)setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler{ _alertViewHandler = alertViewHandler; } diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index eff1209f..b280b1ab 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -51,6 +51,8 @@ @property (nonatomic) NSString *lastCrashFilename; +@property (nonatomic, copy, setter = setAlertViewHandler:) BITCustomAlertViewHandler alertViewHandler; + #if HOCKEYSDK_FEATURE_AUTHENTICATOR // Only set via BITAuthenticator From db45c6654a9acd9875199b81aac29f8f0858ef79 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 27 Apr 2014 23:33:07 +0200 Subject: [PATCH 090/206] Minor code format update --- Classes/BITCrashManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 093792fe..cca18876 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -253,7 +253,7 @@ - (void)cleanCrashReportWithFilename:(NSString *)filename { [_fileManager removeItemAtPath:filename error:&error]; [_fileManager removeItemAtPath:[filename stringByAppendingString:@".data"] error:&error]; [_fileManager removeItemAtPath:[filename stringByAppendingString:@".meta"] error:&error]; - [_fileManager removeItemAtPath:[filename stringByAppendingString:@".desc"]error:&error]; + [_fileManager removeItemAtPath:[filename stringByAppendingString:@".desc"] error:&error]; NSString *cacheFilename = [filename lastPathComponent]; [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; From bc0245978ad18e31032812ac81913533ff416246 Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Mon, 28 Apr 2014 00:45:41 +0200 Subject: [PATCH 091/206] + Hides Statusbar --- Classes/BITImageAnnotationViewController.m | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 910b81ea..59622803 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -85,7 +85,7 @@ - (void)viewDidLoad self.pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinched:)]; self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; - [self.panRecognizer requireGestureRecognizerToFail:self.tapRecognizer]; + [self.tapRecognizer requireGestureRecognizerToFail:self.panRecognizer]; [self.imageView addGestureRecognizer:self.pinchRecognizer]; [self.imageView addGestureRecognizer:self.panRecognizer]; @@ -97,11 +97,13 @@ - (void)viewDidLoad self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithTitle:@"Save" style:UIBarButtonItemStyleBordered target:self action:@selector(save:)]; [self fitImageViewFrame]; - - - // Do any additional setup after loading the view. } +- (BOOL)prefersStatusBarHidden { + return self.navigationController.navigationBarHidden; +} + + - (void)fitImageViewFrame { @@ -278,6 +280,7 @@ -(void)tapped:(UIGestureRecognizer *)tapRecognizer { } completion:^(BOOL finished) { [self fitImageViewFrame]; [self.navigationController setNavigationBarHidden:NO animated:NO]; + [[UIApplication sharedApplication] setStatusBarHidden:NO]; }]; @@ -287,6 +290,8 @@ -(void)tapped:(UIGestureRecognizer *)tapRecognizer { } completion:^(BOOL finished) { [self.navigationController setNavigationBarHidden:YES animated:NO]; + [[UIApplication sharedApplication] setStatusBarHidden:YES]; + [self fitImageViewFrame]; @@ -296,6 +301,7 @@ -(void)tapped:(UIGestureRecognizer *)tapRecognizer { } #pragma mark - Helpers + -(UIView *)firstAnnotationThatIsNotBlur { for (BITImageAnnotation *annotation in self.imageView.subviews){ if (![annotation isKindOfClass:[BITBlurImageAnnotation class]]){ From 35a7e50bb2f5c3a85c7076ee89a54234579d1bad Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Mon, 28 Apr 2014 00:53:58 +0200 Subject: [PATCH 092/206] + Added Edit icons. --- Classes/BITImageAnnotationViewController.m | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 59622803..714080d2 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -85,16 +85,14 @@ - (void)viewDidLoad self.pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinched:)]; self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; - [self.tapRecognizer requireGestureRecognizerToFail:self.panRecognizer]; - [self.imageView addGestureRecognizer:self.pinchRecognizer]; [self.imageView addGestureRecognizer:self.panRecognizer]; [self.view addGestureRecognizer:self.tapRecognizer]; self.imageView.userInteractionEnabled = YES; - self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithTitle:@"Discard" style:UIBarButtonItemStyleBordered target:self action:@selector(discard:)]; - self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithTitle:@"Save" style:UIBarButtonItemStyleBordered target:self action:@selector(save:)]; + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(discard:)]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(save:)]; [self fitImageViewFrame]; } @@ -105,8 +103,6 @@ - (BOOL)prefersStatusBarHidden { - (void)fitImageViewFrame { - - CGFloat heightScaleFactor = self.view.frame.size.height / self.image.size.height; CGFloat widthScaleFactor = self.view.frame.size.width / self.image.size.width; @@ -179,7 +175,7 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { self.panStart = [gestureRecognizer locationInView:self.imageView]; - [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; + // [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; self.isDrawing = YES; } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ From fb271e8acb64490e1be905a6d324d62f849550ab Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Mon, 28 Apr 2014 01:06:19 +0200 Subject: [PATCH 093/206] + Dynamic widths of the outlines, depending on the size of annotations. --- Classes/BITArrowImageAnnotation.m | 11 +++++++++-- Classes/BITRectangleImageAnnotation.m | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index 0edc1af0..ae9d6b34 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -44,10 +44,10 @@ - (id)initWithFrame:(CGRect)frame } - (void)buildShape { - CGFloat topHeight = MAX(self.frame.size.width / 3.0f,20); + CGFloat topHeight = MAX(self.frame.size.width / 3.0f,10); - CGFloat lineWidth = MAX(self.frame.size.width / 10.0f,10); + CGFloat lineWidth = MAX(self.frame.size.width / 10.0f,3); CGFloat startX, startY, endX, endY; if ( self.movedDelta.width < 0){ startX = CGRectGetMinX(self.bounds); @@ -71,6 +71,13 @@ - (void)buildShape { self.shapeLayer.path = path.CGPath; self.strokeLayer.path = path.CGPath; + [CATransaction begin]; + [CATransaction setAnimationDuration:0]; + self.strokeLayer.lineWidth = lineWidth/1.5f; + self.shapeLayer.lineWidth = lineWidth / 3.0f; + + [CATransaction commit]; + } -(void)layoutSubviews{ diff --git a/Classes/BITRectangleImageAnnotation.m b/Classes/BITRectangleImageAnnotation.m index 4709d4f4..9b677cb3 100644 --- a/Classes/BITRectangleImageAnnotation.m +++ b/Classes/BITRectangleImageAnnotation.m @@ -48,6 +48,15 @@ - (void)layoutSubviews { self.strokeLayer.frame = self.bounds; self.strokeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath; + + CGFloat lineWidth = MAX(self.frame.size.width / 10.0f,10); + + [CATransaction begin]; + [CATransaction setAnimationDuration:0]; + self.strokeLayer.lineWidth = lineWidth/1.5f; + self.shapeLayer.lineWidth = lineWidth / 3.0f; + + [CATransaction commit]; } -(BOOL)resizable { From ab2ec512049a95d523d79e72c825f153eb25767a Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Mon, 28 Apr 2014 01:31:39 +0200 Subject: [PATCH 094/206] + Selection is highlighted when moved. --- Classes/BITBlurImageAnnotation.m | 22 ++++++++++++++++++++-- Classes/BITImageAnnotation.h | 8 +++++++- Classes/BITImageAnnotation.m | 8 ++++++++ Classes/BITImageAnnotationViewController.m | 19 +++++++++++++++---- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/Classes/BITBlurImageAnnotation.m b/Classes/BITBlurImageAnnotation.m index 9d8f762c..983c49b8 100644 --- a/Classes/BITBlurImageAnnotation.m +++ b/Classes/BITBlurImageAnnotation.m @@ -12,6 +12,7 @@ @interface BITBlurImageAnnotation() @property (nonatomic, strong) CALayer* imageLayer; @property (nonatomic, strong) UIImage* scaledImage; +@property (nonatomic, strong) CALayer* selectedLayer; @end @@ -25,6 +26,13 @@ - (id)initWithFrame:(CGRect)frame self.clipsToBounds = YES; self.imageLayer = [CALayer layer]; [self.layer addSublayer:self.imageLayer]; + + self.selectedLayer = [CALayer layer]; + [self.layer insertSublayer:self.selectedLayer above:self.imageLayer]; + + self.selectedLayer.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.5f].CGColor; + self.selectedLayer.opacity = 0.6f; + self.clipsToBounds = YES; } return self; } @@ -33,6 +41,7 @@ -(void)setSourceImage:(UIImage *)sourceImage { CGSize size = CGSizeMake(sourceImage.size.width/30, sourceImage.size.height/30); UIGraphicsBeginImageContext(size); + [sourceImage drawInRect:CGRectMake(0, 0, size.width, size.height)]; self.scaledImage = UIGraphicsGetImageFromCurrentImageContext(); self.imageLayer.shouldRasterize = YES; @@ -40,11 +49,19 @@ -(void)setSourceImage:(UIImage *)sourceImage { self.imageLayer.magnificationFilter = kCAFilterNearest; self.imageLayer.contents = (id)self.scaledImage.CGImage; - - UIGraphicsEndImageContext(); } +- (void)setSelected:(BOOL)selected { + self->_selected = selected; + + if (selected){ + self.selectedLayer.opacity = 0.6f; + } else { + self.selectedLayer.opacity = 0.0f; + } +} + - (void)layoutSubviews { [super layoutSubviews]; @@ -54,6 +71,7 @@ - (void)layoutSubviews { self.imageLayer.frame = self.imageFrame; self.imageLayer.masksToBounds = YES; + self.selectedLayer.frame= self.bounds; [CATransaction commit]; } diff --git a/Classes/BITImageAnnotation.h b/Classes/BITImageAnnotation.h index f848ee24..24d312e5 100644 --- a/Classes/BITImageAnnotation.h +++ b/Classes/BITImageAnnotation.h @@ -8,11 +8,17 @@ #import -@interface BITImageAnnotation : UIView +@interface BITImageAnnotation : UIView { + BOOL _selected; +} + @property (nonatomic) CGSize movedDelta; @property (nonatomic, weak) UIImage *sourceImage; @property (nonatomic) CGRect imageFrame; -(BOOL)resizable; +- (void)setSelected:(BOOL)selected; +- (BOOL)isSelected; + @end diff --git a/Classes/BITImageAnnotation.m b/Classes/BITImageAnnotation.m index d0e42614..473889a3 100644 --- a/Classes/BITImageAnnotation.m +++ b/Classes/BITImageAnnotation.m @@ -25,4 +25,12 @@ -(BOOL)resizable { return NO; } +- (void)setSelected:(BOOL)selected { + self->_selected = selected; +} + +- (BOOL)isSelected { + return self->_selected; +} + @end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 714080d2..9ff33cb2 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -161,7 +161,13 @@ - (UIImage *)extractImage { #pragma mark - Gesture Handling - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { - if ([self.editingControls selectedSegmentIndex] != UISegmentedControlNoSegment || self.isDrawing ){ + BITImageAnnotation *annotationAtLocation = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationInView:self.view] withEvent:nil]; + + if (![annotationAtLocation isKindOfClass:[BITImageAnnotation class]]){ + annotationAtLocation = nil; + } + + if (([self.editingControls selectedSegmentIndex] != UISegmentedControlNoSegment || self.isDrawing) && !annotationAtLocation ){ if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ self.currentAnnotation = [self annotationForCurrentMode]; [self.objects addObject:self.currentAnnotation]; @@ -192,11 +198,14 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { } else { if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ // find and possibly move an existing annotation. - BITImageAnnotation *selectedAnnotation = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationInView:self.view] withEvent:nil]; + - if ([self.objects indexOfObject:selectedAnnotation] != NSNotFound){ - self.currentAnnotation = selectedAnnotation; + if ([self.objects indexOfObject:annotationAtLocation] != NSNotFound){ + self.currentAnnotation = annotationAtLocation; + [annotationAtLocation setSelected:YES]; } + + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation){ CGPoint delta = [gestureRecognizer translationInView:self.view]; @@ -213,6 +222,8 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { } else { self.currentAnnotation = nil; + [annotationAtLocation setSelected:NO]; + } } } From ce835a0cee032a4ab64f6f585180ce85203e717d Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 28 Apr 2014 11:07:48 +0200 Subject: [PATCH 095/206] + Improved Selection Handling in the annotation view controller --- Classes/BITArrowImageAnnotation.m | 41 ++++++++++++++++++++-- Classes/BITImageAnnotationViewController.m | 40 ++++++++++++++++----- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index ae9d6b34..fee2b8a2 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -26,12 +26,12 @@ - (id)initWithFrame:(CGRect)frame self = [super initWithFrame:frame]; if (self) { self.shapeLayer = [CAShapeLayer layer]; - self.shapeLayer.strokeColor = [UIColor redColor].CGColor; + self.shapeLayer.strokeColor = [UIColor whiteColor].CGColor; self.shapeLayer.lineWidth = 5; - self.shapeLayer.fillColor = [UIColor clearColor].CGColor; + self.shapeLayer.fillColor = [UIColor redColor].CGColor; self.strokeLayer = [CAShapeLayer layer]; - self.strokeLayer.strokeColor = [UIColor whiteColor].CGColor; + self.strokeLayer.strokeColor = [UIColor redColor].CGColor; self.strokeLayer.lineWidth = 10; self.strokeLayer.fillColor = [UIColor clearColor].CGColor; [self.layer addSublayer:self.strokeLayer]; @@ -154,4 +154,39 @@ - (CGAffineTransform)transformForStartPoint:(CGPoint)startPoint return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y }; } +#pragma mark - UIView + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + UIColor *color = [self colorAtPoint:point]; + CGFloat alpha, white; + [color getWhite:&white alpha:&alpha]; + if (white || alpha){ + return self; + } else { + return nil; + } + +} + +#pragma mark - Helpers + +// This is taken from http://stackoverflow.com/questions/12770181/how-to-get-the-pixel-color-on-touch +- (UIColor *)colorAtPoint:(CGPoint)point { + unsigned char pixel[4] = {0}; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate(pixel, + 1, 1, 8, 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); + + CGContextTranslateCTM(context, -point.x, -point.y); + + [self.layer renderInContext:context]; + + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + UIColor *color = [UIColor colorWithRed:pixel[0]/255.0 + green:pixel[1]/255.0 blue:pixel[2]/255.0 + alpha:pixel[3]/255.0]; + return color; +} + @end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 9ff33cb2..09744129 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -14,6 +14,12 @@ #import "BITHockeyHelper.h" #import "HockeySDKPrivate.h" +typedef NS_ENUM(NSInteger, BITImageAnnotationViewControllerInteractionMode) { + BITImageAnnotationViewControllerInteractionModeNone, + BITImageAnnotationViewControllerInteractionModeDraw, + BITImageAnnotationViewControllerInteractionModeMove +}; + @interface BITImageAnnotationViewController () @property (nonatomic, strong) UIImageView *imageView; @@ -29,7 +35,7 @@ @interface BITImageAnnotationViewController () @property (nonatomic) CGPoint panStart; @property (nonatomic,strong) BITImageAnnotation *currentAnnotation; -@property (nonatomic) BOOL isDrawing; +@property (nonatomic) BITImageAnnotationViewControllerInteractionMode currentInteraction; @property (nonatomic) CGRect pinchStartingFrame; @@ -158,7 +164,7 @@ - (UIImage *)extractImage { return renderedImageOfMyself; } -#pragma mark - Gesture Handling +#pragma mark - UIGestureRecognizers - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { BITImageAnnotation *annotationAtLocation = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationInView:self.view] withEvent:nil]; @@ -167,7 +173,22 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { annotationAtLocation = nil; } - if (([self.editingControls selectedSegmentIndex] != UISegmentedControlNoSegment || self.isDrawing) && !annotationAtLocation ){ + // determine the interaction mode if none is set so far. + + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeNone){ + if (annotationAtLocation){ + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeMove; + } else if ([self canDrawNewAnnotation]){ + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeDraw; + } + } + + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeNone){ + return; + } + + + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeDraw){ if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ self.currentAnnotation = [self annotationForCurrentMode]; [self.objects addObject:self.currentAnnotation]; @@ -182,7 +203,6 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { self.panStart = [gestureRecognizer locationInView:self.imageView]; // [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; - self.isDrawing = YES; } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ CGPoint bla = [gestureRecognizer locationInView:self.imageView]; @@ -193,9 +213,9 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { [self.currentAnnotation layoutIfNeeded]; } else { self.currentAnnotation = nil; - self.isDrawing = NO; + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone; } - } else { + } else if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeMove){ if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ // find and possibly move an existing annotation. @@ -223,6 +243,8 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { } else { self.currentAnnotation = nil; [annotationAtLocation setSelected:NO]; + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone; + } } @@ -281,15 +303,12 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { -(void)tapped:(UIGestureRecognizer *)tapRecognizer { if (self.navigationController.navigationBarHidden){ - // [[UIApplication sharedApplication] setStatusBarHidden:NO]; [UIView animateWithDuration:0.35f animations:^{ self.navigationController.navigationBar.alpha = 1; } completion:^(BOOL finished) { [self fitImageViewFrame]; [self.navigationController setNavigationBarHidden:NO animated:NO]; [[UIApplication sharedApplication] setStatusBarHidden:NO]; - - }]; } else { [UIView animateWithDuration:0.35f animations:^{ @@ -319,4 +338,7 @@ -(UIView *)firstAnnotationThatIsNotBlur { return self.imageView; } +- (BOOL)canDrawNewAnnotation { + return [self.editingControls selectedSegmentIndex] != UISegmentedControlNoSegment; +} @end From 2207f107e48d80ce3160354aee707fe6dfd23c15 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 28 Apr 2014 11:08:57 +0200 Subject: [PATCH 096/206] + Fixed initial highlighting bug in blur annotation. --- Classes/BITImageAnnotationViewController.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 09744129..a154af21 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -212,6 +212,7 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { [self.currentAnnotation setNeedsLayout]; [self.currentAnnotation layoutIfNeeded]; } else { + [self.currentAnnotation setSelected:NO]; self.currentAnnotation = nil; self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone; } From a2d850b5c346e779d7ed495ec7c88acdeef7c49c Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 28 Apr 2014 11:52:11 +0200 Subject: [PATCH 097/206] + Minimum Arrow Length. --- Classes/BITArrowImageAnnotation.m | 42 +++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index fee2b8a2..99b4d0d0 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -44,30 +44,50 @@ - (id)initWithFrame:(CGRect)frame } - (void)buildShape { - CGFloat topHeight = MAX(self.frame.size.width / 3.0f,10); + CGFloat baseWidth = MAX(self.frame.size.width, self.frame.size.height); + CGFloat topHeight = MAX(baseWidth / 3.0f,10); - CGFloat lineWidth = MAX(self.frame.size.width / 10.0f,3); + CGFloat lineWidth = MAX(baseWidth / 10.0f,3); CGFloat startX, startY, endX, endY; + + CGRect boundRect = CGRectInset(self.bounds, 0, 0); + CGFloat arrowLength= sqrt(pow(CGRectGetWidth(boundRect), 2) + pow(CGRectGetHeight(boundRect), 2)); + if (arrowLength < 30){ + + CGFloat factor = 30.f/arrowLength; + + boundRect = CGRectApplyAffineTransform(boundRect, CGAffineTransformMakeScale(factor,factor)); + } + if ( self.movedDelta.width < 0){ - startX = CGRectGetMinX(self.bounds); - endX = CGRectGetMaxX(self.bounds); + startX = CGRectGetMinX(boundRect); + endX = CGRectGetMaxX(boundRect); } else { - startX = CGRectGetMaxX(self.bounds); - endX = CGRectGetMinX(self.bounds); + startX = CGRectGetMaxX(boundRect); + endX = CGRectGetMinX(boundRect); } if ( self.movedDelta.height < 0){ - startY = CGRectGetMinY(self.bounds); - endY = CGRectGetMaxY(self.bounds); + startY = CGRectGetMinY(boundRect); + endY = CGRectGetMaxY(boundRect); } else { - startY = CGRectGetMaxY(self.bounds); - endY = CGRectGetMinY(self.bounds); + startY = CGRectGetMaxY(boundRect); + endY = CGRectGetMinY(boundRect); } + + + if (abs(CGRectGetWidth(boundRect)) < 30 || abs(CGRectGetHeight(boundRect)) < 30){ + CGFloat smallerOne = MIN(abs(CGRectGetHeight(boundRect)), abs(CGRectGetWidth(boundRect))); + + CGFloat factor = smallerOne/30.f; - UIBezierPath *path = [self bezierPathWithArrowFromPoint:CGPointMake(endX,endY) toPoint:CGPointMake(startX,startY) tailWidth:lineWidth headWidth:topHeight headLength:topHeight]; + CGRectApplyAffineTransform(boundRect, CGAffineTransformMakeScale(factor,factor)); + } + + UIBezierPath *path = [self bezierPathWithArrowFromPoint:CGPointMake(endX, endY) toPoint:CGPointMake(startX, startY) tailWidth:lineWidth headWidth:topHeight headLength:topHeight]; self.shapeLayer.path = path.CGPath; self.strokeLayer.path = path.CGPath; From aaccc5cc1d64d83ac60492012fbbdf247cf34191 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 28 Apr 2014 11:57:03 +0200 Subject: [PATCH 098/206] + Fixes a crash relating to resizing annotations. --- Classes/BITImageAnnotationViewController.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index a154af21..3d40cde2 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -259,6 +259,11 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { for ( int i = 0; i Date: Mon, 28 Apr 2014 12:12:02 +0200 Subject: [PATCH 099/206] + Removed a deprecation warning. --- Classes/BITImageAnnotationViewController.m | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 3d40cde2..3a5d5912 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -66,7 +66,10 @@ - (void)viewDidLoad [self.editingControls setImage:bit_imageNamed(imageName, BITHOCKEYSDK_BUNDLE) forSegmentAtIndex:i++]; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self.editingControls setSegmentedControlStyle:UISegmentedControlStyleBar]; +#pragma clang diagnostic pop self.navigationItem.titleView = self.editingControls; @@ -279,7 +282,6 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation && gestureRecognizer.numberOfTouches>1){ CGRect newFrame= (self.pinchStartingFrame); - NSLog(@"%f", [gestureRecognizer scale]); // upper point? CGPoint point1 = [gestureRecognizer locationOfTouch:0 inView:self.view]; @@ -298,10 +300,6 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { self.currentAnnotation.frame = newFrame; self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; - - // we - - } else { self.currentAnnotation = nil; } From 59216cde17be3a769a612082a584c18b4284027d Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 28 Apr 2014 13:59:36 +0200 Subject: [PATCH 100/206] + Some iOS6 Fixes. --- Classes/BITArrowImageAnnotation.m | 4 +++- Classes/BITAttachmentGalleryViewController.m | 14 +++++++++----- Classes/BITImageAnnotationViewController.m | 14 ++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index 99b4d0d0..f3abe3a8 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -180,7 +180,9 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIColor *color = [self colorAtPoint:point]; CGFloat alpha, white; [color getWhite:&white alpha:&alpha]; - if (white || alpha){ + NSLog(@"%f %f", alpha,white); + + if ((int)white > 0 || (int)alpha > 0){ return self; } else { return nil; diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index c1224ac6..bcee1602 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -177,13 +177,17 @@ - (void)share:(id)sender { - (void)tapped:(UITapGestureRecognizer *)tapRecognizer { if (self.navigationController.navigationBarHidden){ - [[UIApplication sharedApplication] setStatusBarHidden:NO]; - [self.navigationController setNavigationBarHidden:NO animated:YES]; + [UIView animateWithDuration:0.35f animations:^{ + [[UIApplication sharedApplication] setStatusBarHidden:NO]; + [self.navigationController setNavigationBarHidden:NO animated:NO]; + } completion:nil]; } else { - [self.navigationController setNavigationBarHidden:YES animated:YES]; - [[UIApplication sharedApplication] setStatusBarHidden:YES]; - + [UIView animateWithDuration:0.35f animations:^{ + [[UIApplication sharedApplication] setStatusBarHidden:YES]; + [self.navigationController setNavigationBarHidden:YES animated:NO]; + } completion:nil]; } + [self layoutViews]; } diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 3a5d5912..0513544e 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -308,23 +308,17 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { -(void)tapped:(UIGestureRecognizer *)tapRecognizer { if (self.navigationController.navigationBarHidden){ [UIView animateWithDuration:0.35f animations:^{ - self.navigationController.navigationBar.alpha = 1; + [[UIApplication sharedApplication] setStatusBarHidden:NO]; + [self.navigationController setNavigationBarHidden:NO animated:NO]; } completion:^(BOOL finished) { [self fitImageViewFrame]; - [self.navigationController setNavigationBarHidden:NO animated:NO]; - [[UIApplication sharedApplication] setStatusBarHidden:NO]; }]; } else { [UIView animateWithDuration:0.35f animations:^{ - self.navigationController.navigationBar.alpha = 0; - - } completion:^(BOOL finished) { - [self.navigationController setNavigationBarHidden:YES animated:NO]; [[UIApplication sharedApplication] setStatusBarHidden:YES]; - - + [self.navigationController setNavigationBarHidden:YES animated:NO]; + } completion:^(BOOL finished) { [self fitImageViewFrame]; - }]; } From 9fd4ead1550c60c2392b1541abcca5b9d588d876 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 28 Apr 2014 14:50:24 +0200 Subject: [PATCH 101/206] + Improved Performance of Attachment Viewer. --- Classes/BITAttachmentGalleryViewController.m | 77 ++++++++++++-------- Classes/BITImageAnnotationViewController.m | 3 +- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index bcee1602..0a10dbeb 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -19,19 +19,13 @@ @interface BITAttachmentGalleryViewController () @property (nonatomic) NSInteger currentIndex; @property (nonatomic) NSInteger loadedImageIndex; @property (nonatomic, strong) UITapGestureRecognizer *tapognizer; +@property (nonatomic, strong) NSMutableDictionary *images; @end @implementation BITAttachmentGalleryViewController -- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; - if (self) { - // Custom initialization - } - return self; -} +#pragma mark - UIViewController - (void)viewDidLoad { @@ -58,6 +52,7 @@ - (void)viewDidLoad self.tapognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; [self.view addGestureRecognizer:self.tapognizer]; } + -(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; @@ -70,21 +65,34 @@ -(void)viewWillAppear:(BOOL)animated { if (indexOfSelectedAttachment != NSNotFound){ self.currentIndex = indexOfSelectedAttachment; self.scrollView.contentOffset = CGPointMake(self.scrollView.frame.size.width * self.currentIndex, 0); - } } + self.images = [NSMutableDictionary new]; [self layoutViews]; - - } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - +} +-(void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + [self.images removeAllObjects]; +} +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + [self.images removeAllObjects]; } + +- (BOOL)prefersStatusBarHidden { + return self.navigationController.navigationBarHidden; +} + +#pragma mark - Scroll View Content/Layout + - (void)setupScrollView { self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; self.view.autoresizesSubviews = NO; @@ -122,18 +130,6 @@ - (void)extractUsableAttachments { self.extractedAttachments = extractedOnes; } -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; -} - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView { - NSInteger newIndex = self.scrollView.contentOffset.x / self.scrollView.frame.size.width; - if (newIndex!=self.currentIndex){ - self.currentIndex = newIndex; - [self layoutViews]; - } -} - (void)layoutViews { CGPoint savedOffset = self.scrollView.contentOffset; @@ -146,24 +142,32 @@ - (void)layoutViews { self.scrollView.autoresizesSubviews = NO; self.scrollView.contentOffset = savedOffset; - + NSInteger baseIndex = MAX(0,self.currentIndex-1); NSInteger z = baseIndex; for ( NSInteger i = baseIndex; i < MIN(baseIndex+2, self.extractedAttachments.count);i++ ){ UIImageView *imageView = self.imageViews[z%self.imageViews.count]; BITFeedbackMessageAttachment *attachment = self.extractedAttachments[i]; - imageView.image =[attachment imageRepresentation]; + imageView.image = [self imageForAttachment:attachment]; imageView.frame = [self frameForItemAtIndex:i]; z++; } - + } -- (BOOL)prefersStatusBarHidden { - return self.navigationController.navigationBarHidden; +#pragma mark - UIScrollViewDelegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + NSInteger newIndex = self.scrollView.contentOffset.x / self.scrollView.frame.size.width; + if (newIndex!=self.currentIndex){ + self.currentIndex = newIndex; + [self layoutViews]; + } } +#pragma mark - IBActions + - (void)close:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; } @@ -175,6 +179,8 @@ - (void)share:(id)sender { [self presentViewController:activityVC animated:YES completion:nil]; } +#pragma mark - UIGestureRecognizer + - (void)tapped:(UITapGestureRecognizer *)tapRecognizer { if (self.navigationController.navigationBarHidden){ [UIView animateWithDuration:0.35f animations:^{ @@ -191,8 +197,21 @@ - (void)tapped:(UITapGestureRecognizer *)tapRecognizer { [self layoutViews]; } +#pragma mark - Layout Helpers + - (CGRect)frameForItemAtIndex:(NSInteger)index { return CGRectMake(index * CGRectGetWidth(self.scrollView.frame), 0, CGRectGetWidth(self.scrollView.frame), CGRectGetHeight(self.view.bounds)); } +- (UIImage *)imageForAttachment:(BITFeedbackMessageAttachment *)attachment { + UIImage *cachedObject = self.images[@([self.extractedAttachments indexOfObject:attachment])]; + + if (!cachedObject){ + cachedObject = [attachment imageRepresentation]; + self.images[@([self.extractedAttachments indexOfObject:attachment])] = cachedObject; + } + + return cachedObject; +} + @end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 0513544e..69e23590 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -85,7 +85,8 @@ - (void)viewDidLoad self.imageView.image = self.image; self.imageView.contentMode = UIViewContentModeScaleToFill; - + self.view.frame = UIScreen.mainScreen.applicationFrame; + [self.view addSubview:self.imageView]; // Erm. self.imageView.frame = [UIScreen mainScreen].bounds; From c7a3d6761a2117e9ac3cb4a4c9be1b2ca3427fad Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 28 Apr 2014 15:34:09 +0200 Subject: [PATCH 102/206] + Fixed a blur/highlighting selection bug. --- Classes/BITImageAnnotationViewController.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 69e23590..270180d3 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -246,11 +246,9 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { [gestureRecognizer setTranslation:CGPointZero inView:self.view]; } else { + [self.currentAnnotation setSelected:NO]; self.currentAnnotation = nil; - [annotationAtLocation setSelected:NO]; self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone; - - } } } @@ -279,6 +277,7 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { if (validView && [candidate resizable]){ self.currentAnnotation = candidate; self.pinchStartingFrame = self.currentAnnotation.frame; + [self.currentAnnotation setSelected:YES]; } } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation && gestureRecognizer.numberOfTouches>1){ @@ -302,6 +301,7 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { self.currentAnnotation.frame = newFrame; self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; } else { + [self.currentAnnotation setSelected:NO]; self.currentAnnotation = nil; } } From 75263f64fcd50719b3113951698797d9efdfeedc Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 29 Apr 2014 13:46:01 +0200 Subject: [PATCH 103/206] Change attachmentData property of BITCrashAttachment to crashAttachmentData --- Classes/BITCrashAttachment.h | 4 ++-- Classes/BITCrashAttachment.m | 8 ++++---- Classes/BITCrashManager.m | 2 +- Classes/BITCrashManagerDelegate.h | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Classes/BITCrashAttachment.h b/Classes/BITCrashAttachment.h index 482e0c00..73217a2a 100644 --- a/Classes/BITCrashAttachment.h +++ b/Classes/BITCrashAttachment.h @@ -43,7 +43,7 @@ /** * The attachment data as NSData object */ -@property (nonatomic, readonly, strong) NSData *attachmentData; +@property (nonatomic, readonly, strong) NSData *crashAttachmentData; /** * The content type of your data as MIME type @@ -60,7 +60,7 @@ * @return An instsance of BITCrashAttachment */ - (instancetype)initWithFilename:(NSString *)filename - attachmentData:(NSData *)attachmentData + crashAttachmentData:(NSData *)crashAttachmentData contentType:(NSString *)contentType; @end diff --git a/Classes/BITCrashAttachment.m b/Classes/BITCrashAttachment.m index def32d25..a4f2b17b 100644 --- a/Classes/BITCrashAttachment.m +++ b/Classes/BITCrashAttachment.m @@ -31,12 +31,12 @@ @implementation BITCrashAttachment - (instancetype)initWithFilename:(NSString *)filename - attachmentData:(NSData *)attachmentData + crashAttachmentData:(NSData *)crashAttachmentData contentType:(NSString *)contentType { if (self = [super init]) { _filename = filename; - _attachmentData = attachmentData; + _crashAttachmentData = crashAttachmentData; _contentType = contentType; } @@ -48,14 +48,14 @@ - (instancetype)initWithFilename:(NSString *)filename - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:self.filename forKey:@"filename"]; - [encoder encodeObject:self.attachmentData forKey:@"data"]; + [encoder encodeObject:self.crashAttachmentData forKey:@"data"]; [encoder encodeObject:self.contentType forKey:@"contentType"]; } - (id)initWithCoder:(NSCoder *)decoder { if ((self = [super init])) { _filename = [decoder decodeObjectForKey:@"filename"]; - _attachmentData = [decoder decodeObjectForKey:@"data"]; + _crashAttachmentData = [decoder decodeObjectForKey:@"data"]; _contentType = [decoder decodeObjectForKey:@"contentType"]; } return self; diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 18b3aaf2..c9dc2ec7 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -1005,7 +1005,7 @@ - (void)postXML:(NSString*)xml attachments:(NSArray *)attachments { BITCrashAttachment *attachment = (BITCrashAttachment *)dict[KBITAttachmentDictAttachment]; - [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.attachmentData + [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.crashAttachmentData forKey:key contentType:attachment.contentType boundary:boundary diff --git a/Classes/BITCrashManagerDelegate.h b/Classes/BITCrashManagerDelegate.h index c88aef2c..2697106a 100644 --- a/Classes/BITCrashManagerDelegate.h +++ b/Classes/BITCrashManagerDelegate.h @@ -66,7 +66,7 @@ NSData *data = [NSData dataWithContentsOfURL:@"mydatafile"]; BITCrashAttachment *attachment = [[BITCrashAttachment alloc] initWithFilename:@"myfile.data" - attachmentData:data + crashAttachmentData:data contentType:@"'application/octet-stream"]; return attachment; } From 622d18dd685929dbe960da26eb895f1f862d12c6 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 29 Apr 2014 15:14:31 +0200 Subject: [PATCH 104/206] + Some layout adjustments. --- Classes/BITImageAnnotationViewController.m | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 270180d3..1de321dd 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -43,14 +43,7 @@ @interface BITImageAnnotationViewController () @implementation BITImageAnnotationViewController -- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; - if (self) { - // Custom initialization - } - return self; -} +#pragma mark - UIViewController - (void)viewDidLoad { @@ -103,8 +96,13 @@ - (void)viewDidLoad self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(discard:)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(save:)]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; [self fitImageViewFrame]; + } - (BOOL)prefersStatusBarHidden { From 25b315f3b20d9d5ba882b9adcdc63dcad1de3087 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 29 Apr 2014 16:53:22 +0200 Subject: [PATCH 105/206] + Fixed Navigationbar/Statusbar quirks in Annotation View Controller --- Classes/BITImageAnnotationViewController.m | 55 ++++++++++++++++------ 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 1de321dd..42713a00 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -79,7 +79,7 @@ - (void)viewDidLoad self.imageView.contentMode = UIViewContentModeScaleToFill; self.view.frame = UIScreen.mainScreen.applicationFrame; - + [self.view addSubview:self.imageView]; // Erm. self.imageView.frame = [UIScreen mainScreen].bounds; @@ -96,29 +96,33 @@ - (void)viewDidLoad self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(discard:)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(save:)]; + + self.view.autoresizesSubviews = NO; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self fitImageViewFrame]; - + } - (BOOL)prefersStatusBarHidden { - return self.navigationController.navigationBarHidden; + return self.navigationController.navigationBarHidden || self.navigationController.navigationBar.alpha == 0.0f; } - (void)fitImageViewFrame { - CGFloat heightScaleFactor = self.view.frame.size.height / self.image.size.height; - CGFloat widthScaleFactor = self.view.frame.size.width / self.image.size.width; + CGFloat heightScaleFactor = [[UIScreen mainScreen] bounds].size.height / self.image.size.height; + CGFloat widthScaleFactor = [[UIScreen mainScreen] bounds].size.width / self.image.size.width; CGFloat factor = MIN(heightScaleFactor, widthScaleFactor); self.scaleFactor = factor; CGSize scaledImageSize = CGSizeMake(self.image.size.width * factor, self.image.size.height * factor); - self.imageView.frame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height/2 - scaledImageSize.height/2, scaledImageSize.width, scaledImageSize.height); + CGRect baseFrame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height - [[UIScreen mainScreen] bounds].size.height, scaledImageSize.width, scaledImageSize.height); + + self.imageView.frame = baseFrame; } -(void)editingAction:(id)sender { @@ -189,7 +193,7 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { return; } - + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeDraw){ if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ self.currentAnnotation = [self annotationForCurrentMode]; @@ -204,7 +208,7 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { self.panStart = [gestureRecognizer locationInView:self.imageView]; - // [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; + // [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ CGPoint bla = [gestureRecognizer locationInView:self.imageView]; @@ -221,7 +225,7 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { } else if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeMove){ if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ // find and possibly move an existing annotation. - + if ([self.objects indexOfObject:annotationAtLocation] != NSNotFound){ self.currentAnnotation = annotationAtLocation; @@ -237,7 +241,7 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { annotationFrame.origin.y += delta.y; self.currentAnnotation.frame = annotationFrame; self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; - + [self.currentAnnotation setNeedsLayout]; [self.currentAnnotation layoutIfNeeded]; @@ -294,7 +298,7 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { newFrame.size.width = (point1.x > point2.x) ? point1.x - point2.x : point2.x - point1.x; newFrame.size.height = (point1.y > point2.y) ? point1.y - point2.y : point2.y - point1.y; - + self.currentAnnotation.frame = newFrame; self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; @@ -305,20 +309,41 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { } -(void)tapped:(UIGestureRecognizer *)tapRecognizer { - if (self.navigationController.navigationBarHidden){ + // This toggles the nav and status bar. Since iOS7 and pre-iOS7 behave weirdly different, + // this might look rather hacky, but hiding the navbar under iOS6 leads to some ugly + // animation effect which is avoided by simply hiding the navbar setting it's alpha to 0. // moritzh + + if (self.navigationController.navigationBar.alpha == 0 || self.navigationController.navigationBarHidden ){ + [UIView animateWithDuration:0.35f animations:^{ + + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) { + [self.navigationController setNavigationBarHidden:NO animated:NO]; + } else { + self.navigationController.navigationBar.alpha = 1.0; + } + [[UIApplication sharedApplication] setStatusBarHidden:NO]; - [self.navigationController setNavigationBarHidden:NO animated:NO]; + } completion:^(BOOL finished) { [self fitImageViewFrame]; + }]; } else { [UIView animateWithDuration:0.35f animations:^{ + + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) { + [self.navigationController setNavigationBarHidden:YES animated:NO]; + } else { + self.navigationController.navigationBar.alpha = 0.0; + } + [[UIApplication sharedApplication] setStatusBarHidden:YES]; - [self.navigationController setNavigationBarHidden:YES animated:NO]; + } completion:^(BOOL finished) { [self fitImageViewFrame]; - }]; + + }]; } } From 475d3e624c444fa1c5eb0036fc422132ed71365e Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 29 Apr 2014 17:58:51 +0200 Subject: [PATCH 106/206] + Massively improved arrow hit testing. --- Classes/BITArrowImageAnnotation.m | 49 +++++++------------------------ 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index f3abe3a8..40c58e95 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -16,7 +16,6 @@ @interface BITArrowImageAnnotation() @property (nonatomic, strong) CAShapeLayer *shapeLayer; @property (nonatomic, strong) CAShapeLayer *strokeLayer; - @end @implementation BITArrowImageAnnotation @@ -100,21 +99,6 @@ - (void)buildShape { } --(void)layoutSubviews{ - [super layoutSubviews]; - - [self buildShape]; - -} - -/* -// Only override drawRect: if you perform custom drawing. -// An empty implementation adversely affects performance during animation. -- (void)drawRect:(CGRect)rect -{ - // Drawing code -} -*/ - (UIBezierPath *)bezierPathWithArrowFromPoint:(CGPoint)startPoint toPoint:(CGPoint)endPoint @@ -177,12 +161,14 @@ - (CGAffineTransform)transformForStartPoint:(CGPoint)startPoint #pragma mark - UIView - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - UIColor *color = [self colorAtPoint:point]; - CGFloat alpha, white; - [color getWhite:&white alpha:&alpha]; - NSLog(@"%f %f", alpha,white); - if ((int)white > 0 || (int)alpha > 0){ + CGPathRef strokePath = CGPathCreateCopyByStrokingPath(self.shapeLayer.path, NULL, fmaxf(90.0f, self.shapeLayer.lineWidth), kCGLineCapRound,kCGLineJoinMiter,0); + + BOOL containsPoint = CGPathContainsPoint(strokePath, NULL, point, NO); + + CGPathRelease(strokePath); + + if (containsPoint){ return self; } else { return nil; @@ -190,25 +176,10 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { } -#pragma mark - Helpers - -// This is taken from http://stackoverflow.com/questions/12770181/how-to-get-the-pixel-color-on-touch -- (UIColor *)colorAtPoint:(CGPoint)point { - unsigned char pixel[4] = {0}; - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(pixel, - 1, 1, 8, 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); - - CGContextTranslateCTM(context, -point.x, -point.y); - - [self.layer renderInContext:context]; +-(void)layoutSubviews{ + [super layoutSubviews]; - CGContextRelease(context); - CGColorSpaceRelease(colorSpace); - UIColor *color = [UIColor colorWithRed:pixel[0]/255.0 - green:pixel[1]/255.0 blue:pixel[2]/255.0 - alpha:pixel[3]/255.0]; - return color; + [self buildShape]; } @end From ae5c87c99bb74637d094db653f37545f526a8cb5 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 29 Apr 2014 18:02:30 +0200 Subject: [PATCH 107/206] Fixes #62: Arrow should be default. --- Classes/BITImageAnnotationViewController.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 42713a00..5bf851a0 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -51,7 +51,7 @@ - (void)viewDidLoad self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; - NSArray *icons = @[@"Rectangle.png", @"Arrow.png", @"Blur.png"]; + NSArray *icons = @[@"Arrow.png",@"Rectangle.png", @"Blur.png"]; self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Rectangle", @"Arrow", @"Blur"]]; int i=0; @@ -131,9 +131,9 @@ -(void)editingAction:(id)sender { - (BITImageAnnotation *)annotationForCurrentMode { if (self.editingControls.selectedSegmentIndex == 0){ - return [[BITRectangleImageAnnotation alloc] initWithFrame:CGRectZero]; - } else if(self.editingControls.selectedSegmentIndex==1){ return [[BITArrowImageAnnotation alloc] initWithFrame:CGRectZero]; + } else if(self.editingControls.selectedSegmentIndex==1){ + return [[BITRectangleImageAnnotation alloc] initWithFrame:CGRectZero]; } else { return [[BITBlurImageAnnotation alloc] initWithFrame:CGRectZero]; } From afb46f66a06cbcf4e9a0ba3757135aaef92a2f94 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Tue, 29 Apr 2014 18:21:16 +0200 Subject: [PATCH 108/206] + Ported a long due fix to the Attachment Gallery View Controller. --- Classes/BITAttachmentGalleryViewController.m | 50 ++++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m index 0a10dbeb..f3d1fc94 100644 --- a/Classes/BITAttachmentGalleryViewController.m +++ b/Classes/BITAttachmentGalleryViewController.m @@ -49,6 +49,8 @@ - (void)viewDidLoad self.view.frame = UIScreen.mainScreen.applicationFrame; + + self.tapognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; [self.view addGestureRecognizer:self.tapognizer]; } @@ -57,8 +59,7 @@ -(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // Hide the navigation bar and stuff initially. - [self.navigationController setNavigationBarHidden:YES animated:NO]; - [[UIApplication sharedApplication] setStatusBarHidden:YES]; + [self tapped:nil]; if (self.preselectedAttachment){ NSInteger indexOfSelectedAttachment = [self.extractedAttachments indexOfObject:self.preselectedAttachment]; @@ -95,6 +96,11 @@ - (BOOL)prefersStatusBarHidden { - (void)setupScrollView { self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; + CGRect frame = self.scrollView.frame; + + frame.origin.y = self.scrollView.frame.size.height - [[UIScreen mainScreen] bounds].size.height; + + self.scrollView.frame = frame; self.view.autoresizesSubviews = NO; [self.view addSubview:self.scrollView]; self.scrollView.delegate = self; @@ -136,7 +142,7 @@ - (void)layoutViews { self.scrollView.delegate = nil; self.scrollView.frame = self.view.bounds; - self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds) * self.extractedAttachments.count, CGRectGetHeight(self.view.bounds)); + self.scrollView.contentSize = CGSizeMake( [[UIScreen mainScreen] bounds].size.width * self.extractedAttachments.count, [[UIScreen mainScreen] bounds].size.height); self.scrollView.delegate = self; self.scrollView.contentInset = UIEdgeInsetsZero; self.scrollView.autoresizesSubviews = NO; @@ -153,6 +159,11 @@ - (void)layoutViews { z++; } + CGRect frame = self.scrollView.frame; + + frame.origin.y = self.scrollView.frame.size.height - [[UIScreen mainScreen] bounds].size.height; + + self.scrollView.frame = frame; } @@ -182,25 +193,44 @@ - (void)share:(id)sender { #pragma mark - UIGestureRecognizer - (void)tapped:(UITapGestureRecognizer *)tapRecognizer { - if (self.navigationController.navigationBarHidden){ + if (self.navigationController.navigationBar.alpha == 0 || self.navigationController.navigationBarHidden ){ + [UIView animateWithDuration:0.35f animations:^{ + + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) { + [self.navigationController setNavigationBarHidden:NO animated:NO]; + } else { + self.navigationController.navigationBar.alpha = 1.0; + } + [[UIApplication sharedApplication] setStatusBarHidden:NO]; - [self.navigationController setNavigationBarHidden:NO animated:NO]; - } completion:nil]; + + } completion:^(BOOL finished){ + [self layoutViews]; + }]; } else { [UIView animateWithDuration:0.35f animations:^{ + + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) { + [self.navigationController setNavigationBarHidden:YES animated:NO]; + } else { + self.navigationController.navigationBar.alpha = 0.0; + } + [[UIApplication sharedApplication] setStatusBarHidden:YES]; - [self.navigationController setNavigationBarHidden:YES animated:NO]; - } completion:nil]; + + } completion:^(BOOL finished){ + [self layoutViews]; + }]; } - [self layoutViews]; + } #pragma mark - Layout Helpers - (CGRect)frameForItemAtIndex:(NSInteger)index { - return CGRectMake(index * CGRectGetWidth(self.scrollView.frame), 0, CGRectGetWidth(self.scrollView.frame), CGRectGetHeight(self.view.bounds)); + return CGRectMake(index * [[UIScreen mainScreen] bounds].size.width, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height); } - (UIImage *)imageForAttachment:(BITFeedbackMessageAttachment *)attachment { From e4700dab9b73265604cc6c775841aa020e8205d5 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 30 Apr 2014 16:53:20 +0200 Subject: [PATCH 109/206] + Refactored Preview of Attachments. --- Classes/BITAttachmentGalleryViewController.h | 19 -- Classes/BITAttachmentGalleryViewController.m | 247 ------------------ Classes/BITFeedbackListViewCell.m | 41 +-- Classes/BITFeedbackListViewController.h | 3 +- Classes/BITFeedbackListViewController.m | 47 +++- Classes/BITFeedbackManager.m | 3 +- Classes/BITFeedbackMessage.m | 2 + ...TFeedbackMessageAttachment+QLPreviewItem.h | 16 ++ ...TFeedbackMessageAttachment+QLPreviewItem.m | 21 ++ Classes/BITFeedbackMessageAttachment.h | 6 +- Classes/BITFeedbackMessageAttachment.m | 113 +++++--- Classes/View.xib | 31 +++ Support/HockeySDK.xcodeproj/project.pbxproj | 30 +-- 13 files changed, 230 insertions(+), 349 deletions(-) delete mode 100644 Classes/BITAttachmentGalleryViewController.h delete mode 100644 Classes/BITAttachmentGalleryViewController.m create mode 100644 Classes/BITFeedbackMessageAttachment+QLPreviewItem.h create mode 100644 Classes/BITFeedbackMessageAttachment+QLPreviewItem.m create mode 100644 Classes/View.xib diff --git a/Classes/BITAttachmentGalleryViewController.h b/Classes/BITAttachmentGalleryViewController.h deleted file mode 100644 index a745b6dd..00000000 --- a/Classes/BITAttachmentGalleryViewController.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// BITAttachmentGalleryViewController.h -// HockeySDK -// -// Created by Moritz Haarmann on 06.03.14. -// -// - -#import - -@class BITFeedbackMessageAttachment; - -@interface BITAttachmentGalleryViewController : UIViewController - -@property (nonatomic, strong) NSArray *messages; - -@property (nonatomic, strong) BITFeedbackMessageAttachment *preselectedAttachment; - -@end diff --git a/Classes/BITAttachmentGalleryViewController.m b/Classes/BITAttachmentGalleryViewController.m deleted file mode 100644 index f3d1fc94..00000000 --- a/Classes/BITAttachmentGalleryViewController.m +++ /dev/null @@ -1,247 +0,0 @@ -// -// BITAttachmentGalleryViewController.m -// HockeySDK -// -// Created by Moritz Haarmann on 06.03.14. -// -// - -#import "BITAttachmentGalleryViewController.h" - -#import "BITFeedbackMessage.h" -#import "BITFeedbackMessageAttachment.h" - -@interface BITAttachmentGalleryViewController () - -@property (nonatomic, strong) UIScrollView *scrollView; -@property (nonatomic, strong) NSArray *imageViews; -@property (nonatomic, strong) NSArray *extractedAttachments; -@property (nonatomic) NSInteger currentIndex; -@property (nonatomic) NSInteger loadedImageIndex; -@property (nonatomic, strong) UITapGestureRecognizer *tapognizer; -@property (nonatomic, strong) NSMutableDictionary *images; - -@end - -@implementation BITAttachmentGalleryViewController - -#pragma mark - UIViewController - -- (void)viewDidLoad -{ - [super viewDidLoad]; - self.navigationController.navigationBar.translucent = YES; - self.navigationController.navigationBar.opaque = NO; -#if __IPHONE_OS_VERSION_MIN_REQUIRED > __IPHONE_6_1 - self.automaticallyAdjustsScrollViewInsets = NO; - self.edgesForExtendedLayout = YES; - self.extendedLayoutIncludesOpaqueBars = YES; -#endif - self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Close" style:UIBarButtonItemStylePlain target:self action:@selector(close:)]; - - self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(share:)]; - - - self.currentIndex = 0; - - [self extractUsableAttachments]; - [self setupScrollView]; - - self.view.frame = UIScreen.mainScreen.applicationFrame; - - - - self.tapognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; - [self.view addGestureRecognizer:self.tapognizer]; -} - --(void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - - // Hide the navigation bar and stuff initially. - [self tapped:nil]; - - if (self.preselectedAttachment){ - NSInteger indexOfSelectedAttachment = [self.extractedAttachments indexOfObject:self.preselectedAttachment]; - if (indexOfSelectedAttachment != NSNotFound){ - self.currentIndex = indexOfSelectedAttachment; - self.scrollView.contentOffset = CGPointMake(self.scrollView.frame.size.width * self.currentIndex, 0); - } - } - - self.images = [NSMutableDictionary new]; - [self layoutViews]; -} - -- (void)viewDidAppear:(BOOL)animated { - [super viewDidAppear:animated]; -} - --(void)viewWillDisappear:(BOOL)animated { - [super viewWillDisappear:animated]; - [self.images removeAllObjects]; -} - -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - [self.images removeAllObjects]; -} - -- (BOOL)prefersStatusBarHidden { - return self.navigationController.navigationBarHidden; -} - -#pragma mark - Scroll View Content/Layout - -- (void)setupScrollView { - self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; - CGRect frame = self.scrollView.frame; - - frame.origin.y = self.scrollView.frame.size.height - [[UIScreen mainScreen] bounds].size.height; - - self.scrollView.frame = frame; - self.view.autoresizesSubviews = NO; - [self.view addSubview:self.scrollView]; - self.scrollView.delegate = self; - self.scrollView.pagingEnabled = YES; - self.scrollView.backgroundColor = [UIColor groupTableViewBackgroundColor]; - self.scrollView.bounces = NO; - - - NSMutableArray *imageviews = [NSMutableArray new]; - - for (int i = 0; i<3; i++){ - UIImageView *newImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; - [imageviews addObject:newImageView]; - newImageView.contentMode = UIViewContentModeScaleAspectFit; - [self.scrollView addSubview:newImageView]; - } - - self.imageViews = imageviews; - -} - -- (void)extractUsableAttachments { - NSMutableArray *extractedOnes = [NSMutableArray new]; - - for (BITFeedbackMessage *message in self.messages){ - for (BITFeedbackMessageAttachment *attachment in message.attachments){ - if ([attachment imageRepresentation]){ - [extractedOnes addObject:attachment]; - } - } - } - - self.extractedAttachments = extractedOnes; -} - - -- (void)layoutViews { - CGPoint savedOffset = self.scrollView.contentOffset; - - self.scrollView.delegate = nil; - self.scrollView.frame = self.view.bounds; - self.scrollView.contentSize = CGSizeMake( [[UIScreen mainScreen] bounds].size.width * self.extractedAttachments.count, [[UIScreen mainScreen] bounds].size.height); - self.scrollView.delegate = self; - self.scrollView.contentInset = UIEdgeInsetsZero; - self.scrollView.autoresizesSubviews = NO; - self.scrollView.contentOffset = savedOffset; - - - NSInteger baseIndex = MAX(0,self.currentIndex-1); - NSInteger z = baseIndex; - for ( NSInteger i = baseIndex; i < MIN(baseIndex+2, self.extractedAttachments.count);i++ ){ - UIImageView *imageView = self.imageViews[z%self.imageViews.count]; - BITFeedbackMessageAttachment *attachment = self.extractedAttachments[i]; - imageView.image = [self imageForAttachment:attachment]; - imageView.frame = [self frameForItemAtIndex:i]; - z++; - } - - CGRect frame = self.scrollView.frame; - - frame.origin.y = self.scrollView.frame.size.height - [[UIScreen mainScreen] bounds].size.height; - - self.scrollView.frame = frame; - -} - -#pragma mark - UIScrollViewDelegate - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView { - NSInteger newIndex = self.scrollView.contentOffset.x / self.scrollView.frame.size.width; - if (newIndex!=self.currentIndex){ - self.currentIndex = newIndex; - [self layoutViews]; - } -} - -#pragma mark - IBActions - -- (void)close:(id)sender { - [self dismissViewControllerAnimated:YES completion:nil]; -} - -- (void)share:(id)sender { - BITFeedbackMessageAttachment *attachment = self.extractedAttachments[self.currentIndex]; - - UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:[NSArray arrayWithObjects:attachment.originalFilename, attachment.imageRepresentation , nil] applicationActivities:nil]; - [self presentViewController:activityVC animated:YES completion:nil]; -} - -#pragma mark - UIGestureRecognizer - -- (void)tapped:(UITapGestureRecognizer *)tapRecognizer { - if (self.navigationController.navigationBar.alpha == 0 || self.navigationController.navigationBarHidden ){ - - [UIView animateWithDuration:0.35f animations:^{ - - if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) { - [self.navigationController setNavigationBarHidden:NO animated:NO]; - } else { - self.navigationController.navigationBar.alpha = 1.0; - } - - [[UIApplication sharedApplication] setStatusBarHidden:NO]; - - } completion:^(BOOL finished){ - [self layoutViews]; - }]; - } else { - [UIView animateWithDuration:0.35f animations:^{ - - if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) { - [self.navigationController setNavigationBarHidden:YES animated:NO]; - } else { - self.navigationController.navigationBar.alpha = 0.0; - } - - [[UIApplication sharedApplication] setStatusBarHidden:YES]; - - } completion:^(BOOL finished){ - [self layoutViews]; - }]; - } - - -} - -#pragma mark - Layout Helpers - -- (CGRect)frameForItemAtIndex:(NSInteger)index { - return CGRectMake(index * [[UIScreen mainScreen] bounds].size.width, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height); -} - -- (UIImage *)imageForAttachment:(BITFeedbackMessageAttachment *)attachment { - UIImage *cachedObject = self.images[@([self.extractedAttachments indexOfObject:attachment])]; - - if (!cachedObject){ - cachedObject = [attachment imageRepresentation]; - self.images[@([self.extractedAttachments indexOfObject:attachment])] = cachedObject; - } - - return cachedObject; -} - -@end diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 9998a076..e10afbb8 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -87,15 +87,15 @@ - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reus self.dateFormatter = [[NSDateFormatter alloc] init]; [self.dateFormatter setTimeStyle:NSDateFormatterNoStyle]; [self.dateFormatter setDateStyle:NSDateFormatterMediumStyle]; - [self.dateFormatter setLocale:[NSLocale currentLocale]]; + [self.dateFormatter setLocale:[NSLocale currentLocale]]; [self.dateFormatter setDoesRelativeDateFormatting:YES]; - + self.timeFormatter = [[NSDateFormatter alloc] init]; [self.timeFormatter setTimeStyle:NSDateFormatterShortStyle]; [self.timeFormatter setDateStyle:NSDateFormatterNoStyle]; [self.timeFormatter setLocale:[NSLocale currentLocale]]; [self.timeFormatter setDoesRelativeDateFormatting:YES]; - + self.labelTitle = [[UILabel alloc] init]; self.labelTitle.font = [UIFont systemFontOfSize:TITLE_FONTSIZE]; @@ -171,7 +171,7 @@ + (CGFloat) heightForTextInRowWithMessage:(BITFeedbackMessage *)message tableVie // added to make space for the images. - + } else { #endif #pragma clang diagnostic push @@ -179,7 +179,7 @@ + (CGFloat) heightForTextInRowWithMessage:(BITFeedbackMessage *)message tableVie calculatedHeight = [message.text sizeWithFont:[UIFont systemFontOfSize:TEXT_FONTSIZE] constrainedToSize:CGSizeMake(width - (2 * FRAME_SIDE_BORDER), CGFLOAT_MAX) ].height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER; - + #pragma clang diagnostic pop #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 } @@ -190,7 +190,7 @@ + (CGFloat) heightForTextInRowWithMessage:(BITFeedbackMessage *)message tableVie - (void)setAttachments:(NSArray *)attachments { for (UIView *view in self.attachmentViews){ - [view removeFromSuperview]; + [view removeFromSuperview]; } [self.attachmentViews removeAllObjects]; @@ -198,9 +198,14 @@ - (void)setAttachments:(NSArray *)attachments { for (BITFeedbackMessageAttachment *attachment in attachments){ UIButton *imageView = [UIButton buttonWithType:UIButtonTypeCustom]; [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; + [imageView setTitle:attachment.originalFilename forState:UIControlStateNormal]; + [imageView setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter]; + [imageView setContentHorizontalAlignment:UIControlContentHorizontalAlignmentCenter]; [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + imageView.contentMode = UIViewContentModeScaleAspectFill; + [self.attachmentViews addObject:imageView]; } } @@ -208,12 +213,12 @@ - (void)setAttachments:(NSArray *)attachments { - (void)layoutSubviews { if (!self.accessoryBackgroundView){ - self.accessoryBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)]; - self.accessoryBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleHeight; - self.accessoryBackgroundView.clipsToBounds = YES; - - // colors - self.accessoryBackgroundView.backgroundColor = [self backgroundColor]; + self.accessoryBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)]; + self.accessoryBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleHeight; + self.accessoryBackgroundView.clipsToBounds = YES; + + // colors + self.accessoryBackgroundView.backgroundColor = [self backgroundColor]; } if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) { @@ -231,9 +236,9 @@ - (void)layoutSubviews { } else { [self.labelText setTextColor:TEXTCOLOR_DEFAULT]; } - + // background for deletion accessory view - + // header NSString *dateString = @""; @@ -248,7 +253,7 @@ - (void)layoutSubviews { } [self.labelTitle setText:dateString]; [self.labelTitle setFrame:CGRectMake(FRAME_SIDE_BORDER, FRAME_TOP_BORDER + LABEL_TITLE_Y, self.frame.size.width - (2 * FRAME_SIDE_BORDER), LABEL_TITLE_HEIGHT)]; - + if (_message.userMessage) { self.labelTitle.textAlignment = kBITTextLabelAlignmentRight; self.labelText.textAlignment = kBITTextLabelAlignmentRight; @@ -256,13 +261,13 @@ - (void)layoutSubviews { self.labelTitle.textAlignment = kBITTextLabelAlignmentLeft; self.labelText.textAlignment = kBITTextLabelAlignmentLeft; } - + [self addSubview:self.labelTitle]; - + // text [self.labelText setText:_message.text]; CGSize sizeForTextLabel = CGSizeMake(self.frame.size.width - (2 * FRAME_SIDE_BORDER), - [[self class] heightForTextInRowWithMessage:_message tableViewWidth:self.frame.size.width] - LABEL_TEXT_Y - FRAME_BOTTOM_BORDER); + [[self class] heightForTextInRowWithMessage:_message tableViewWidth:self.frame.size.width] - LABEL_TEXT_Y - FRAME_BOTTOM_BORDER); [self.labelText setFrame:CGRectMake(FRAME_SIDE_BORDER, LABEL_TEXT_Y, sizeForTextLabel.width, sizeForTextLabel.height)]; diff --git a/Classes/BITFeedbackListViewController.h b/Classes/BITFeedbackListViewController.h index 0a513904..8ee2b064 100644 --- a/Classes/BITFeedbackListViewController.h +++ b/Classes/BITFeedbackListViewController.h @@ -28,6 +28,7 @@ #import +#import #import "BITHockeyBaseViewController.h" @@ -54,7 +55,7 @@ This ensures that the presentation on iOS 6 and iOS 7 will use the corret design on each OS Version. */ -@interface BITFeedbackListViewController : BITHockeyBaseViewController { +@interface BITFeedbackListViewController : BITHockeyBaseViewController { } @end diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index 70f58a1f..88b92d2e 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -39,13 +39,15 @@ #import "BITFeedbackComposeViewController.h" #import "BITFeedbackUserDataViewController.h" #import "BITFeedbackMessage.h" +#import "BITFeedbackMessageAttachment.h" +#import "BITFeedbackMessageAttachment+QLPreviewItem.h" #import "BITAttributedLabel.h" -#import "BITAttachmentGalleryViewController.h" #import "BITHockeyBaseManagerPrivate.h" #import "BITHockeyHelper.h" #import +#import #define DEFAULT_BACKGROUNDCOLOR BIT_RGBCOLOR(245, 245, 245) @@ -70,6 +72,7 @@ @interface BITFeedbackListViewController () ) previewController: (QLPreviewController *) controller previewItemAtIndex: (NSInteger) index { + if (index>=0){ + return self.cachedPreviewItems[index];} + return nil; } @end diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 5cd2091d..c2cf0fac 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -739,7 +739,7 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { newAttachment.originalFilename = attachmentData[@"file_name"]; newAttachment.id = attachmentData[@"id"]; newAttachment.sourceURL = attachmentData[@"url"]; - newAttachment.contentType = @"image/jpg"; + newAttachment.contentType = attachmentData[@"content_type"]; [message addAttachmentsObject:newAttachment]; } @@ -1048,7 +1048,6 @@ - (void)submitPendingMessages { completionHandler:^(NSError *err){ if (err) { [self markSendInProgressMessagesAsPending]; - [self saveMessages]; } diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index 446fb0da..6d2ab49c 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -96,4 +96,6 @@ -(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object{ } self.attachments = [self.attachments arrayByAddingObject:object]; } + + @end diff --git a/Classes/BITFeedbackMessageAttachment+QLPreviewItem.h b/Classes/BITFeedbackMessageAttachment+QLPreviewItem.h new file mode 100644 index 00000000..5c05718a --- /dev/null +++ b/Classes/BITFeedbackMessageAttachment+QLPreviewItem.h @@ -0,0 +1,16 @@ +// +// BITFeedbackMessageAttachment+QLPreviewItem.h +// HockeySDK +// +// Created by Moritz Haarmann on 30.04.14. +// +// + +#import "BITFeedbackMessageAttachment.h" +#import + +@interface BITFeedbackMessageAttachment (QLPreviewItem) + + + +@end diff --git a/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m b/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m new file mode 100644 index 00000000..da5424eb --- /dev/null +++ b/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m @@ -0,0 +1,21 @@ +// +// BITFeedbackMessageAttachment+QLPreviewItem.m +// HockeySDK +// +// Created by Moritz Haarmann on 30.04.14. +// +// + +#import "BITFeedbackMessageAttachment+QLPreviewItem.h" + +@implementation BITFeedbackMessageAttachment (QLPreviewItem) + +- (NSString *)previewItemTitle { + return self.originalFilename; +} + +- (NSURL *)previewItemURL { + return self.localURL; +} + +@end diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index 1d056419..a76b995e 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -49,6 +49,10 @@ - (void)deleteContents; --(BOOL)needsLoadingFromURL; +- (BOOL)needsLoadingFromURL; + +- (BOOL)isImage; + +- (NSURL *)localURL; @end diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 3350b96d..541d25fd 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -30,6 +30,7 @@ #import "BITFeedbackMessageAttachment.h" #import "BITHockeyHelper.h" #import "HockeySDKPrivate.h" +#import #define kCacheFolderName @"hockey_attachments" @@ -52,7 +53,7 @@ + (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType: formatter = [NSDateFormatter new]; formatter.dateStyle = NSDateFormatterShortStyle; formatter.timeStyle = NSDateFormatterShortStyle; - + } BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; @@ -97,6 +98,17 @@ -(BOOL)needsLoadingFromURL { return (self.sourceURL); } +- (BOOL)isImage { + return ([self.contentType rangeOfString:@"image"].location != NSNotFound); +} + +- (NSURL *)localURL { + if (self.filename){ + return [NSURL fileURLWithPath:self.filename]; + } else + { return nil;} +} + #pragma mark NSCoding - (void)encodeWithCoder:(NSCoder *)aCoder { @@ -104,8 +116,8 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.filename forKey:@"filename"]; [aCoder encodeObject:self.originalFilename forKey:@"originalFilename"]; [aCoder encodeObject:self.sourceURL forKey:@"url"]; - - + + } - (id)initWithCoder:(NSCoder *)aDecoder { @@ -117,7 +129,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder { self.thumbnailRepresentations = [NSMutableDictionary new]; self.originalFilename = [aDecoder decodeObjectForKey:@"originalFilename"]; self.sourceURL = [aDecoder decodeObjectForKey:@"sourceURL"]; - + } return self; @@ -126,10 +138,14 @@ - (id)initWithCoder:(NSCoder *)aDecoder { #pragma mark - Thubmnails / Image Representation - (UIImage *)imageRepresentation { - if ([self.contentType rangeOfString:@"image"].location != NSNotFound || [self.sourceURL rangeOfString:@"jpeg"].location != NSNotFound){ + if ([self.contentType rangeOfString:@"image"].location != NSNotFound ){ return [UIImage imageWithData:self.data]; } else { - return bit_imageNamed(@"feedbackActiviy.png", BITHOCKEYSDK_BUNDLE); // TODO add another placeholder. + // Create a Icon .. + UIDocumentInteractionController* docController = [[UIDocumentInteractionController alloc] init]; + docController.name = self.originalFilename; + NSArray* icons = docController.icons; + return icons[0]; } } @@ -139,46 +155,69 @@ - (UIImage *)thumbnailWithSize:(CGSize)size { if (!self.thumbnailRepresentations[cacheKey]){ UIImage *image = self.imageRepresentation; // consider the scale. + if (!image) + return nil; CGFloat scale = [UIScreen mainScreen].scale; - CGSize scaledSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale)); - UIImage *thumbnail = bit_imageToFitSize(image, scaledSize, NO) ; + if (scale != image.scale){ + + CGSize scaledSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale)); + UIImage *thumbnail = bit_imageToFitSize(image, scaledSize, YES) ; + + UIImage *scaledTumbnail = [UIImage imageWithCGImage:thumbnail.CGImage scale:scale orientation:thumbnail.imageOrientation]; + if (thumbnail){ + [self.thumbnailRepresentations setObject:scaledTumbnail forKey:cacheKey]; + } + + } else { + UIImage *thumbnail = bit_imageToFitSize(image, size, YES) ; + + [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; + + } - UIImage *scaledTumbnail = [UIImage imageWithCGImage:thumbnail.CGImage scale:scale orientation:thumbnail.imageOrientation]; - if (thumbnail){ - [self.thumbnailRepresentations setObject:scaledTumbnail forKey:cacheKey]; - } } - return self.thumbnailRepresentations[cacheKey]; -} - + return self.thumbnailRepresentations[cacheKey]; + } + #pragma mark - Persistence Helpers - -- (NSString *)createFilename { - NSArray* cachePathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - NSString* cachePath = [cachePathArray lastObject]; - cachePath = [cachePath stringByAppendingPathComponent:kCacheFolderName]; - BOOL isDirectory; + - (NSString *)createFilename { + NSArray* cachePathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString* cachePath = [cachePathArray lastObject]; + cachePath = [cachePath stringByAppendingPathComponent:kCacheFolderName]; + + BOOL isDirectory; + + if (![[NSFileManager defaultManager] fileExistsAtPath:cachePath isDirectory:&isDirectory]){ + [[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:nil]; + } + + NSString *uniqueString = bit_UUID(); + cachePath = [cachePath stringByAppendingPathComponent:uniqueString]; + + // File extension that suits the Content type. + + CFStringRef mimeType = (__bridge CFStringRef)self.contentType; + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); + CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); + + cachePath = [cachePath stringByAppendingPathExtension:(__bridge NSString *)(extension)]; + + CFRelease(uti); + CFRelease(extension); + + return cachePath; + } - if (![[NSFileManager defaultManager] fileExistsAtPath:cachePath isDirectory:&isDirectory]){ - [[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:nil]; + - (void)deleteContents { + if (self.filename){ + [[NSFileManager defaultManager] removeItemAtPath:self.filename error:nil]; + self.filename = nil; + } } - NSString *uniqueString = bit_UUID(); - cachePath = [cachePath stringByAppendingPathComponent:uniqueString]; - return cachePath; -} - -- (void)deleteContents { - if (self.filename){ - [[NSFileManager defaultManager] removeItemAtPath:self.filename error:nil]; - self.filename = nil; - } -} - - -@end + @end diff --git a/Classes/View.xib b/Classes/View.xib new file mode 100644 index 00000000..8cd84946 --- /dev/null +++ b/Classes/View.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 993fffc7..3d5d956f 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -140,8 +140,6 @@ 973EC8BC18BDE29800DBFFBB /* BITArrowImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */; }; 973EC8BF18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */; }; 973EC8C018BE2B5B00DBFFBB /* BITBlurImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */; }; - 973EC91818C87E7300DBFFBB /* BITAttachmentGalleryViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC91618C87E7300DBFFBB /* BITAttachmentGalleryViewController.h */; }; - 973EC91918C87E7300DBFFBB /* BITAttachmentGalleryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC91718C87E7300DBFFBB /* BITAttachmentGalleryViewController.m */; }; 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */; }; 9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */; }; 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */; }; @@ -155,6 +153,9 @@ 9782023918F81BFC00A98D8B /* Ok@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022F18F81BFC00A98D8B /* Ok@2x.png */; }; 9782023A18F81BFC00A98D8B /* Rectangle.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023018F81BFC00A98D8B /* Rectangle.png */; }; 9782023B18F81BFC00A98D8B /* Rectangle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023118F81BFC00A98D8B /* Rectangle@2x.png */; }; + 97BD9BD21911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 97BD9BD01911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h */; }; + 97BD9BD31911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */; }; + 97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; }; 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; }; 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; }; 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; @@ -324,8 +325,6 @@ 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITArrowImageAnnotation.m; sourceTree = ""; }; 973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITBlurImageAnnotation.h; sourceTree = ""; }; 973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITBlurImageAnnotation.m; sourceTree = ""; }; - 973EC91618C87E7300DBFFBB /* BITAttachmentGalleryViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITAttachmentGalleryViewController.h; sourceTree = ""; }; - 973EC91718C87E7300DBFFBB /* BITAttachmentGalleryViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITAttachmentGalleryViewController.m; sourceTree = ""; }; 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotation.h; sourceTree = ""; }; 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotation.m; sourceTree = ""; }; @@ -339,6 +338,10 @@ 9782022F18F81BFC00A98D8B /* Ok@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Ok@2x.png"; sourceTree = ""; }; 9782023018F81BFC00A98D8B /* Rectangle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Rectangle.png; sourceTree = ""; }; 9782023118F81BFC00A98D8B /* Rectangle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Rectangle@2x.png"; sourceTree = ""; }; + 97BD9BD01911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "BITFeedbackMessageAttachment+QLPreviewItem.h"; sourceTree = ""; }; + 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "BITFeedbackMessageAttachment+QLPreviewItem.m"; sourceTree = ""; }; + 97BD9BD4191109730043FD59 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; + 97BD9BDF19113F9B0043FD59 /* View.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = View.xib; sourceTree = ""; }; 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = ""; }; 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconCamera@2x.png"; sourceTree = ""; }; 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotationViewController.h; sourceTree = ""; }; @@ -370,6 +373,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */, 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */, 1E5954DC15B6F24A00A03429 /* Foundation.framework in Frameworks */, 1E5954DD15B6F24A00A03429 /* CrashReporter.framework in Frameworks */, @@ -505,7 +509,6 @@ 1E754E461621FA9A0070AB92 /* Feedback */ = { isa = PBXGroup; children = ( - 973EC91518C87E5A00DBFFBB /* Image Viewer */, 9760F6CC18BB684200959B93 /* Image Editor */, 1E49A4361612223B00463151 /* BITFeedbackMessage.h */, 1E49A4371612223B00463151 /* BITFeedbackMessage.m */, @@ -518,6 +521,7 @@ 1E49A4391612223B00463151 /* BITFeedbackUserDataViewController.m */, 1E49A42F1612223B00463151 /* BITFeedbackListViewCell.h */, 1E49A4301612223B00463151 /* BITFeedbackListViewCell.m */, + 97BD9BDF19113F9B0043FD59 /* View.xib */, 1E49A4311612223B00463151 /* BITFeedbackListViewController.h */, 1E49A4321612223B00463151 /* BITFeedbackListViewController.m */, 1EF95CA4162CB036000AE3AD /* BITFeedbackActivity.h */, @@ -526,6 +530,8 @@ 1E49A4341612223B00463151 /* BITFeedbackManager.m */, E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */, 1E49A4351612223B00463151 /* BITFeedbackManagerPrivate.h */, + 97BD9BD01911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h */, + 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */, ); name = Feedback; sourceTree = ""; @@ -604,15 +610,6 @@ name = Private; sourceTree = ""; }; - 973EC91518C87E5A00DBFFBB /* Image Viewer */ = { - isa = PBXGroup; - children = ( - 973EC91618C87E7300DBFFBB /* BITAttachmentGalleryViewController.h */, - 973EC91718C87E7300DBFFBB /* BITAttachmentGalleryViewController.m */, - ); - name = "Image Viewer"; - sourceTree = ""; - }; 9760F6CC18BB684200959B93 /* Image Editor */ = { isa = PBXGroup; children = ( @@ -658,6 +655,7 @@ E400561C148D79B500EB22B9 /* Frameworks */ = { isa = PBXGroup; children = ( + 97BD9BD4191109730043FD59 /* QuickLook.framework */, 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */, E41EB48B148D7C4E0015DEDC /* CrashReporter.framework */, E400561D148D79B500EB22B9 /* Foundation.framework */, @@ -735,6 +733,7 @@ 1E59559A15B6FDA500A03429 /* BITHockeyManager.h in Headers */, 1E5955FD15B7877B00A03429 /* BITHockeyManagerDelegate.h in Headers */, 1E754E5C1621FBB70070AB92 /* BITCrashManager.h in Headers */, + 97BD9BD21911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h in Headers */, 1E754E5E1621FBB70070AB92 /* BITCrashManagerDelegate.h in Headers */, 1E49A4731612226D00463151 /* BITUpdateManager.h in Headers */, 1E49A4791612226D00463151 /* BITUpdateManagerDelegate.h in Headers */, @@ -760,7 +759,6 @@ 1E49A47C1612226D00463151 /* BITUpdateManagerPrivate.h in Headers */, 1E49A4851612226D00463151 /* BITUpdateViewControllerPrivate.h in Headers */, 1E49A4B5161222B900463151 /* BITHockeyBaseManagerPrivate.h in Headers */, - 973EC91818C87E7300DBFFBB /* BITAttachmentGalleryViewController.h in Headers */, E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */, 1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */, 973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */, @@ -989,7 +987,7 @@ files = ( 1E5954D315B6F24A00A03429 /* BITHockeyManager.m in Sources */, 1E49A43F1612223B00463151 /* BITFeedbackComposeViewController.m in Sources */, - 973EC91918C87E7300DBFFBB /* BITAttachmentGalleryViewController.m in Sources */, + 97BD9BD31911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m in Sources */, E40E0B0D17DA1AFF005E38C1 /* BITHockeyAppClient.m in Sources */, 1E49A4451612223B00463151 /* BITFeedbackListViewCell.m in Sources */, 973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */, From 2155f5f93b8dc5744f069da53aa0492f2f4fa49b Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 1 May 2014 16:57:28 +0200 Subject: [PATCH 110/206] Add app build property to BITCrashDetails --- Classes/BITCrashDetails.h | 29 ++++++++++++++++++++++++++++- Classes/BITCrashDetails.m | 4 +++- Classes/BITCrashManager.m | 2 ++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Classes/BITCrashDetails.h b/Classes/BITCrashDetails.h index 1c5988f7..b9322f35 100644 --- a/Classes/BITCrashDetails.h +++ b/Classes/BITCrashDetails.h @@ -10,26 +10,53 @@ @interface BITCrashDetails : NSObject +/** + * UUID for the crash report + */ @property (nonatomic, readonly, strong) NSString *incidentIdentifier; +/** + * UUID for the app installation on the device + */ @property (nonatomic, readonly, strong) NSString *reporterKey; +/** + * Signal that caused the crash + */ @property (nonatomic, readonly, strong) NSString *signal; +/** + * Exception name that triggered the crash, nil if the crash was not caused by an exception + */ @property (nonatomic, readonly, strong) NSString *exceptionName; +/** + * Exception reason, nil if the crash was not caused by an exception + */ @property (nonatomic, readonly, strong) NSString *exceptionReason; +/** + * Date and time the app started, nil if unknown + */ @property (nonatomic, readonly, strong) NSDate *appStartTime; +/** + * Date and time the crash occured, nil if unknown + */ @property (nonatomic, readonly, strong) NSDate *crashTime; +/** + * CFBundleVersion value of the app that crashed + */ +@property (nonatomic, readonly, strong) NSString *appBuild; + - (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier reporterKey:(NSString *)reporterKey signal:(NSString *)signal exceptionName:(NSString *)exceptionName exceptionReason:(NSString *)exceptionReason appStartTime:(NSDate *)appStartTime - crashTime:(NSDate *)crashTime; + crashTime:(NSDate *)crashTime + appBuild:(NSString *)appBuild; @end diff --git a/Classes/BITCrashDetails.m b/Classes/BITCrashDetails.m index d201fac9..187b7ba0 100644 --- a/Classes/BITCrashDetails.m +++ b/Classes/BITCrashDetails.m @@ -16,7 +16,8 @@ - (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier exceptionName:(NSString *)exceptionName exceptionReason:(NSString *)exceptionReason appStartTime:(NSDate *)appStartTime - crashTime:(NSDate *)crashTime; + crashTime:(NSDate *)crashTime + appBuild:(NSString *)appBuild { if ((self = [super init])) { _incidentIdentifier = incidentIdentifier; @@ -26,6 +27,7 @@ - (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier _exceptionReason = exceptionReason; _appStartTime = appStartTime; _crashTime = crashTime; + _appBuild = appBuild; } return self; } diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index cca18876..226ae9bf 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -788,6 +788,7 @@ - (void) handleCrashReport { exceptionReason:report.exceptionInfo.exceptionReason appStartTime:appStartTime crashTime:appCrashTime + appBuild:report.applicationInfo.applicationVersion ]; } } @@ -1135,6 +1136,7 @@ - (void)createCrashReportForAppKill { exceptionReason:nil appStartTime:nil crashTime:nil + appBuild:fakeReportAppVersion ]; NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)rootObj From cf926593f54978fafd14e4cb4456fece54fb4db2 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 4 May 2014 17:55:00 +0200 Subject: [PATCH 111/206] Don't delete all crash reports when the user denies to send the latest one --- Classes/BITCrashManager.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 226ae9bf..83d52da7 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -268,6 +268,8 @@ - (void)cleanCrashReportWithFilename:(NSString *)filename { /** * Remove all crash reports and stored meta data for each from the file system and keychain + * + * This is currently only used as a helper method for tests */ - (void)cleanCrashReports { for (NSUInteger i=0; i < [_crashFiles count]; i++) { @@ -696,7 +698,7 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCras [self.delegate crashManagerWillCancelSendingCrashReport:self]; } - [self cleanCrashReports]; + [self cleanCrashReportWithFilename:[_crashesDir stringByAppendingPathComponent: _lastCrashFilename]]; return YES; case BITCrashManagerUserInputSend: From 0f117b27c557cd931dd170fc8864489ae2465a3f Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 4 May 2014 17:56:45 +0200 Subject: [PATCH 112/206] Send each crash report individually one after another and only if the previous succeeds This reduces unnecessary network traffic and performance issues if there are lots of crashes pending for sending on the device --- Classes/BITCrashManager.m | 260 +++++++++--------- Classes/BITCrashManagerPrivate.h | 2 +- Support/HockeySDKTests/BITCrashManagerTests.m | 2 +- 3 files changed, 135 insertions(+), 129 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 83d52da7..7ac9094c 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -704,7 +704,7 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCras case BITCrashManagerUserInputSend: [self persistUserProvidedCrashDescription:userProvidedCrashDescription]; - [self sendCrashReports]; + [self sendNextCrashReport]; return YES; case BITCrashManagerUserInputAlwaysSend: @@ -717,7 +717,7 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCras [self persistUserProvidedCrashDescription:userProvidedCrashDescription]; - [self sendCrashReports]; + [self sendNextCrashReport]; return YES; default: @@ -903,7 +903,7 @@ - (void)invokeDelayedProcessing { if (!_sendingInProgress && [self hasPendingCrashReport]) { _sendingInProgress = YES; if (!BITHockeyBundle()) { - [self sendCrashReports]; + [self sendNextCrashReport]; } else if (_crashManagerStatus != BITCrashManagerStatusAutoSend && [self hasNonApprovedCrashReports]) { if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillShowSubmitCrashReportAlert:)]) { @@ -940,7 +940,7 @@ - (void)invokeDelayedProcessing { [alertView show]; } } else { - [self sendCrashReports]; + [self sendNextCrashReport]; } } } @@ -1158,146 +1158,149 @@ - (void)createCrashReportForAppKill { * * Gathers all collected data and constructs the XML structure and starts the sending process */ -- (void)sendCrashReports { +- (void)sendNextCrashReport { NSError *error = NULL; _crashIdenticalCurrentVersion = NO; - for (NSUInteger i=0; i < [_crashFiles count]; i++) { - NSString *crashXML = nil; - BITCrashAttachment *attachment = nil; + if ([_crashFiles count] == 0) + return; + + NSString *crashXML = nil; + BITCrashAttachment *attachment = nil; + + NSString *filename = [_crashFiles objectAtIndex:0]; + NSString *cacheFilename = [filename lastPathComponent]; + NSData *crashData = [NSData dataWithContentsOfFile:filename]; + + if ([crashData length] > 0) { + BITPLCrashReport *report = nil; + NSString *crashUUID = @""; + NSString *installString = nil; + NSString *crashLogString = nil; + NSString *appBundleIdentifier = nil; + NSString *appBundleVersion = nil; + NSString *osVersion = nil; + NSString *deviceModel = nil; + NSString *appBinaryUUIDs = nil; + NSString *metaFilename = nil; - NSString *filename = [_crashFiles objectAtIndex:i]; - NSString *cacheFilename = [filename lastPathComponent]; - NSData *crashData = [NSData dataWithContentsOfFile:filename]; - - if ([crashData length] > 0) { - BITPLCrashReport *report = nil; - NSString *crashUUID = @""; - NSString *installString = nil; - NSString *crashLogString = nil; - NSString *appBundleIdentifier = nil; - NSString *appBundleVersion = nil; - NSString *osVersion = nil; - NSString *deviceModel = nil; - NSString *appBinaryUUIDs = nil; - NSString *metaFilename = nil; + NSString *errorString = nil; + NSPropertyListFormat format; + + if ([[cacheFilename pathExtension] isEqualToString:@"fake"]) { + NSDictionary *fakeReportDict = (NSDictionary *)[NSPropertyListSerialization + propertyListFromData:crashData + mutabilityOption:NSPropertyListMutableContainersAndLeaves + format:&format + errorDescription:&errorString]; - NSString *errorString = nil; - NSPropertyListFormat format; - - if ([[cacheFilename pathExtension] isEqualToString:@"fake"]) { - NSDictionary *fakeReportDict = (NSDictionary *)[NSPropertyListSerialization - propertyListFromData:crashData - mutabilityOption:NSPropertyListMutableContainersAndLeaves - format:&format - errorDescription:&errorString]; - - crashLogString = [fakeReportDict objectForKey:kBITFakeCrashReport]; - crashUUID = [fakeReportDict objectForKey:kBITFakeCrashUUID]; - appBundleIdentifier = [fakeReportDict objectForKey:kBITFakeCrashAppBundleIdentifier]; - appBundleVersion = [fakeReportDict objectForKey:kBITFakeCrashAppVersion]; - appBinaryUUIDs = [fakeReportDict objectForKey:kBITFakeCrashAppBinaryUUID]; - deviceModel = [fakeReportDict objectForKey:kBITFakeCrashDeviceModel]; - osVersion = [fakeReportDict objectForKey:kBITFakeCrashOSVersion]; - - metaFilename = [cacheFilename stringByReplacingOccurrencesOfString:@".fake" withString:@".meta"]; - if ([appBundleVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { - _crashIdenticalCurrentVersion = YES; - } - - } else { - report = [[BITPLCrashReport alloc] initWithData:crashData error:&error]; - } - - if (report == nil && crashLogString == nil) { - BITHockeyLog(@"WARNING: Could not parse crash report"); - // we cannot do anything with this report, so delete it - [self cleanCrashReportWithFilename:filename]; - continue; - } + crashLogString = [fakeReportDict objectForKey:kBITFakeCrashReport]; + crashUUID = [fakeReportDict objectForKey:kBITFakeCrashUUID]; + appBundleIdentifier = [fakeReportDict objectForKey:kBITFakeCrashAppBundleIdentifier]; + appBundleVersion = [fakeReportDict objectForKey:kBITFakeCrashAppVersion]; + appBinaryUUIDs = [fakeReportDict objectForKey:kBITFakeCrashAppBinaryUUID]; + deviceModel = [fakeReportDict objectForKey:kBITFakeCrashDeviceModel]; + osVersion = [fakeReportDict objectForKey:kBITFakeCrashOSVersion]; - installString = bit_appAnonID() ?: @""; - - if (report) { - if (report.uuidRef != NULL) { - crashUUID = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); - } - metaFilename = [filename stringByAppendingPathExtension:@"meta"]; - crashLogString = [BITCrashReportTextFormatter stringValueForCrashReport:report crashReporterKey:installString]; - appBundleIdentifier = report.applicationInfo.applicationIdentifier; - appBundleVersion = report.applicationInfo.applicationVersion; - osVersion = report.systemInfo.operatingSystemVersion; - deviceModel = [self getDevicePlatform]; - appBinaryUUIDs = [self extractAppUUIDs:report]; - if ([report.applicationInfo.applicationVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { - _crashIdenticalCurrentVersion = YES; - } + metaFilename = [cacheFilename stringByReplacingOccurrencesOfString:@".fake" withString:@".meta"]; + if ([appBundleVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + _crashIdenticalCurrentVersion = YES; } + } else { + report = [[BITPLCrashReport alloc] initWithData:crashData error:&error]; + } + + if (report == nil && crashLogString == nil) { + BITHockeyLog(@"WARNING: Could not parse crash report"); + // we cannot do anything with this report, so delete it + [self cleanCrashReportWithFilename:filename]; + // we don't continue with the next report here, even if there are to prevent calling sendCrashReports from itself again + // the next crash will be automatically send on the next app start/becoming active event + return; + } + + installString = bit_appAnonID() ?: @""; + + if (report) { + if (report.uuidRef != NULL) { + crashUUID = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); + } + metaFilename = [filename stringByAppendingPathExtension:@"meta"]; + crashLogString = [BITCrashReportTextFormatter stringValueForCrashReport:report crashReporterKey:installString]; + appBundleIdentifier = report.applicationInfo.applicationIdentifier; + appBundleVersion = report.applicationInfo.applicationVersion; + osVersion = report.systemInfo.operatingSystemVersion; + deviceModel = [self getDevicePlatform]; + appBinaryUUIDs = [self extractAppUUIDs:report]; if ([report.applicationInfo.applicationVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { _crashIdenticalCurrentVersion = YES; } - - NSString *username = @""; - NSString *useremail = @""; - NSString *userid = @""; - NSString *applicationLog = @""; - NSString *description = @""; + } + + if ([report.applicationInfo.applicationVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + _crashIdenticalCurrentVersion = YES; + } + + NSString *username = @""; + NSString *useremail = @""; + NSString *userid = @""; + NSString *applicationLog = @""; + NSString *description = @""; + + NSData *plist = [NSData dataWithContentsOfFile:[_crashesDir stringByAppendingPathComponent:metaFilename]]; + if (plist) { + NSDictionary *metaDict = (NSDictionary *)[NSPropertyListSerialization + propertyListFromData:plist + mutabilityOption:NSPropertyListMutableContainersAndLeaves + format:&format + errorDescription:&errorString]; - NSData *plist = [NSData dataWithContentsOfFile:[_crashesDir stringByAppendingPathComponent:metaFilename]]; - if (plist) { - NSDictionary *metaDict = (NSDictionary *)[NSPropertyListSerialization - propertyListFromData:plist - mutabilityOption:NSPropertyListMutableContainersAndLeaves - format:&format - errorDescription:&errorString]; - - username = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]] ?: @""; - useremail = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]] ?: @""; - userid = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]] ?: @""; - applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; - description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [_crashesDir stringByAppendingPathComponent: cacheFilename]] encoding:NSUTF8StringEncoding error:&error]; - attachment = [self attachmentForCrashReport:filename]; + username = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]] ?: @""; + useremail = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]] ?: @""; + userid = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]] ?: @""; + applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; + description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [_crashesDir stringByAppendingPathComponent: cacheFilename]] encoding:NSUTF8StringEncoding error:&error]; + attachment = [self attachmentForCrashReport:filename]; + } else { + BITHockeyLog(@"ERROR: Reading crash meta data. %@", error); + } + + if ([applicationLog length] > 0) { + if ([description length] > 0) { + description = [NSString stringWithFormat:@"%@\n\nLog:\n%@", description, applicationLog]; } else { - BITHockeyLog(@"ERROR: Reading crash meta data. %@", error); - } - - if ([applicationLog length] > 0) { - if ([description length] > 0) { - description = [NSString stringWithFormat:@"%@\n\nLog:\n%@", description, applicationLog]; - } else { - description = [NSString stringWithFormat:@"Log:\n%@", applicationLog]; - } + description = [NSString stringWithFormat:@"Log:\n%@", applicationLog]; } - - crashXML = [NSString stringWithFormat:@"%s%@%@%@%@%@%@%@%@%@%@%@", - [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String], - appBinaryUUIDs, - appBundleIdentifier, - osVersion, - deviceModel, - [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], - appBundleVersion, - crashUUID, - [crashLogString stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,crashLogString.length)], - userid, - username, - useremail, - installString, - [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]; - - BITHockeyLog(@"INFO: Sending crash reports:\n%@", crashXML); - [self sendCrashReportWithFilename:filename xml:crashXML attachment:attachment]; - } else { - // we cannot do anything with this report, so delete it - [self cleanCrashReportWithFilename:filename]; } + + crashXML = [NSString stringWithFormat:@"%s%@%@%@%@%@%@%@%@%@%@%@", + [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String], + appBinaryUUIDs, + appBundleIdentifier, + osVersion, + deviceModel, + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], + appBundleVersion, + crashUUID, + [crashLogString stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,crashLogString.length)], + userid, + username, + useremail, + installString, + [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]; + + BITHockeyLog(@"INFO: Sending crash reports:\n%@", crashXML); + [self sendCrashReportWithFilename:filename xml:crashXML attachment:attachment]; + } else { + // we cannot do anything with this report, so delete it + [self cleanCrashReportWithFilename:filename]; } - + [self saveSettings]; } @@ -1416,6 +1419,9 @@ - (void)sendCrashReportWithFilename:(NSString *)filename xml:(NSString*)xml atta [strongSelf.delegate respondsToSelector:@selector(crashManagerDidFinishSendingCrashReport:)]) { [strongSelf.delegate crashManagerDidFinishSendingCrashReport:self]; } + + // only if sending the crash report went successfully, continue with the next one (if there are more) + [strongSelf sendNextCrashReport]; } else if (statusCode == 400) { [strongSelf cleanCrashReportWithFilename:filename]; diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index b280b1ab..50158963 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -82,7 +82,7 @@ - (BITCrashAttachment *)attachmentForCrashReport:(NSString *)filename; - (void)invokeDelayedProcessing; -- (void)sendCrashReports; +- (void)sendNextCrashReport; - (NSString *)getCrashesDir; - (void)setLastCrashFilename:(NSString *)lastCrashFilename; diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index b6d02409..5bb154e4 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -272,7 +272,7 @@ - (void)testStartManagerWithAutoSend { assertThatBool([_sut hasNonApprovedCrashReports], equalToBool(YES)); // this is currently sending blindly, needs refactoring to test properly - [_sut sendCrashReports]; + [_sut sendNextCrashReport]; [verifyCount(delegateMock, times(1)) crashManagerWillSendCrashReport:_sut]; [_sut cleanCrashReports]; From e7635a9f8550788eb69db1183aa914bf7bd27dd4 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 5 May 2014 10:34:56 +0200 Subject: [PATCH 113/206] + Removed XIB :) --- Classes/View.xib | 31 --------------------- Support/HockeySDK.xcodeproj/project.pbxproj | 2 -- 2 files changed, 33 deletions(-) delete mode 100644 Classes/View.xib diff --git a/Classes/View.xib b/Classes/View.xib deleted file mode 100644 index 8cd84946..00000000 --- a/Classes/View.xib +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 3d5d956f..ad75c59f 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -341,7 +341,6 @@ 97BD9BD01911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "BITFeedbackMessageAttachment+QLPreviewItem.h"; sourceTree = ""; }; 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "BITFeedbackMessageAttachment+QLPreviewItem.m"; sourceTree = ""; }; 97BD9BD4191109730043FD59 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; - 97BD9BDF19113F9B0043FD59 /* View.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = View.xib; sourceTree = ""; }; 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = ""; }; 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconCamera@2x.png"; sourceTree = ""; }; 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotationViewController.h; sourceTree = ""; }; @@ -521,7 +520,6 @@ 1E49A4391612223B00463151 /* BITFeedbackUserDataViewController.m */, 1E49A42F1612223B00463151 /* BITFeedbackListViewCell.h */, 1E49A4301612223B00463151 /* BITFeedbackListViewCell.m */, - 97BD9BDF19113F9B0043FD59 /* View.xib */, 1E49A4311612223B00463151 /* BITFeedbackListViewController.h */, 1E49A4321612223B00463151 /* BITFeedbackListViewController.m */, 1EF95CA4162CB036000AE3AD /* BITFeedbackActivity.h */, From 9fd79bc79fc5f77bfa04441b3e3c9b2f85f7fbe5 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 5 May 2014 11:01:45 +0200 Subject: [PATCH 114/206] + Enabled Feedback Observation mode Three-Finger-Tap --- Classes/BITFeedbackManager.h | 2 +- Classes/BITFeedbackManager.m | 46 +++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index cfc744f5..02441beb 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -75,7 +75,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { * Feedback compose will open with a generated screenshot if the screen is tapped * three fingers for three seconds. */ - BITFeedbackObservationModeThreeFingersThreeSeconds = 2 + BITFeedbackObservationModeThreeFingerTap = 2 }; diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index c2cf0fac..35a5e46f 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -53,6 +53,12 @@ #define kBITFeedbackLastMessageID @"HockeyFeedbackLastMessageID" #define kBITFeedbackAppID @"HockeyFeedbackAppID" +@interface BITFeedbackManager() + +@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; +@property (nonatomic) BOOL screenshotNotificationEnabled; + +@end @implementation BITFeedbackManager { NSFileManager *_fileManager; @@ -1087,12 +1093,34 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) -(void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode { if (mode == BITFeedbackObservationModeOnScreenshot){ - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + self.screenshotNotificationEnabled = YES; + if (self.tapRecognizer){ + [[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer]; + self.tapRecognizer = nil; + } + } else if (mode == BITFeedbackObservationModeThreeFingerTap){ + if (!self.tapRecognizer){ + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(screenshotTripleTap:)]; + self.tapRecognizer.numberOfTouchesRequired = 3; + self.tapRecognizer.delegate = self; + + dispatch_async(dispatch_get_main_queue(), ^{ + [[UIApplication sharedApplication].keyWindow addGestureRecognizer:self.tapRecognizer]; + }); + } + + if (self.screenshotNotificationEnabled){ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + self.screenshotNotificationEnabled = NO; + } } } -(void)screenshotNotificationReceived:(NSNotification *)notification { - [self extractLastPictureFromLibraryAndLaunchFeedback]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self extractLastPictureFromLibraryAndLaunchFeedback]; + }); } -(void)extractLastPictureFromLibraryAndLaunchFeedback { @@ -1108,13 +1136,25 @@ -(void)extractLastPictureFromLibraryAndLaunchFeedback { ALAssetRepresentation *representation = [alAsset defaultRepresentation]; UIImage *latestPhoto = [UIImage imageWithCGImage:[representation fullScreenImage]]; - *stop = YES; *innerStop = YES; + *stop = YES; + *innerStop = YES; + [self showFeedbackComposeViewWithPreparedItems:@[latestPhoto]]; } }]; } failureBlock: nil]; } +- (void)screenshotTripleTap:(UITapGestureRecognizer *)tapRecognizer { + if (tapRecognizer.state == UIGestureRecognizerStateRecognized){ + [self showFeedbackComposeViewWithGeneratedScreenshot]; + } +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + return YES; +} + @end From 4c88a10a8f153c355134d4f07b02a418c6626248 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 5 May 2014 11:20:20 +0200 Subject: [PATCH 115/206] + Feedback Observation Mode: Not supported message for screenshot observation in iOS6 --- Classes/BITFeedbackListViewController.m | 3 ++- Classes/BITFeedbackManager.h | 1 + Classes/BITFeedbackManager.m | 16 +++++++++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index 88b92d2e..27ed0d80 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -782,8 +782,9 @@ - (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)at previewController.dataSource = self; [self presentViewController:previewController animated:YES completion:nil]; - + if (self.cachedPreviewItems.count > [self.cachedPreviewItems indexOfObject:attachment]){ [previewController setCurrentPreviewItemIndex:[self.cachedPreviewItems indexOfObject:attachment]]; + } } - (void)refreshPreviewItems { diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 02441beb..01ad9a46 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -32,6 +32,7 @@ #import "BITHockeyBaseManager.h" #import "BITFeedbackListViewController.h" #import "BITFeedbackComposeViewController.h" +#import "HockeySDKPrivate.h" // Notification message which tells that loading messages finished diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 35a5e46f..9f905a3b 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -1093,13 +1093,21 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) -(void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode { if (mode == BITFeedbackObservationModeOnScreenshot){ + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + } else { + BITHockeyLog("Not enabling Screenshot notifications: iOS6.1 and lower is not supported."); + } + self.screenshotNotificationEnabled = YES; + if (self.tapRecognizer){ [[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer]; self.tapRecognizer = nil; } - } else if (mode == BITFeedbackObservationModeThreeFingerTap){ + } + + if (mode == BITFeedbackObservationModeThreeFingerTap){ if (!self.tapRecognizer){ self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(screenshotTripleTap:)]; self.tapRecognizer.numberOfTouchesRequired = 3; @@ -1111,8 +1119,10 @@ -(void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode { } if (self.screenshotNotificationEnabled){ - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; - self.screenshotNotificationEnabled = NO; + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + self.screenshotNotificationEnabled = NO; + } } } } From 1b4ea4492e9d1e0b26deb9c85cd5cd9ceaa85c08 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 5 May 2014 12:12:22 +0200 Subject: [PATCH 116/206] + Landscape support in Attachment editor --- Classes/BITFeedbackListViewCell.m | 6 +----- Classes/BITImageAnnotationViewController.m | 24 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index e10afbb8..d5c343eb 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -198,15 +198,11 @@ - (void)setAttachments:(NSArray *)attachments { for (BITFeedbackMessageAttachment *attachment in attachments){ UIButton *imageView = [UIButton buttonWithType:UIButtonTypeCustom]; [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; - [imageView setTitle:attachment.originalFilename forState:UIControlStateNormal]; - [imageView setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter]; - [imageView setContentHorizontalAlignment:UIControlContentHorizontalAlignmentCenter]; [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; - imageView.contentMode = UIViewContentModeScaleAspectFill; - [self.attachmentViews addObject:imageView]; + [self addSubview:imageView]; } } diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 5bf851a0..9f8c74f6 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -103,24 +103,42 @@ - (void)viewDidLoad - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; + [self fitImageViewFrame]; } +- (void)viewWillDisappear:(BOOL)animated { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; + +} + - (BOOL)prefersStatusBarHidden { return self.navigationController.navigationBarHidden || self.navigationController.navigationBar.alpha == 0.0f; } +- (void)orientationDidChange:(NSNotification *)notification { + [self fitImageViewFrame]; +} + - (void)fitImageViewFrame { - CGFloat heightScaleFactor = [[UIScreen mainScreen] bounds].size.height / self.image.size.height; - CGFloat widthScaleFactor = [[UIScreen mainScreen] bounds].size.width / self.image.size.width; + + CGSize size = [UIScreen mainScreen].bounds.size; + if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)){ + size = CGSizeMake(size.height, size.width); + } + + CGFloat heightScaleFactor = size.height / self.image.size.height; + CGFloat widthScaleFactor = size.width / self.image.size.width; CGFloat factor = MIN(heightScaleFactor, widthScaleFactor); self.scaleFactor = factor; CGSize scaledImageSize = CGSizeMake(self.image.size.width * factor, self.image.size.height * factor); - CGRect baseFrame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height - [[UIScreen mainScreen] bounds].size.height, scaledImageSize.width, scaledImageSize.height); + CGRect baseFrame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height - size.height, scaledImageSize.width, scaledImageSize.height); self.imageView.frame = baseFrame; } From 926dc48aa6c3c30771ab50a75f8394bf139eedfc Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 5 May 2014 14:35:32 +0200 Subject: [PATCH 117/206] + Async Attachment loading. --- Classes/BITFeedbackListViewController.m | 16 +++++++++++++++- Classes/BITFeedbackManager.m | 3 ++- .../BITFeedbackMessageAttachment+QLPreviewItem.m | 6 +++++- Classes/BITFeedbackMessageAttachment.h | 7 +++++++ Classes/BITFeedbackMessageAttachment.m | 13 +++++++++---- Support/HockeySDK.xcodeproj/project.pbxproj | 2 ++ 6 files changed, 40 insertions(+), 7 deletions(-) diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index 27ed0d80..2b774217 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -814,7 +814,21 @@ - (NSInteger) numberOfPreviewItemsInPreviewController: (QLPreviewController *) c - (id ) previewController: (QLPreviewController *) controller previewItemAtIndex: (NSInteger) index { if (index>=0){ - return self.cachedPreviewItems[index];} + BITFeedbackMessageAttachment *attachment = self.cachedPreviewItems[index]; + if (attachment.needsLoadingFromURL && !attachment.isLoading){ + attachment.isLoading = YES; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]]; + [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue new] completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) { + if (responseData.length){ + [attachment replaceData:responseData]; + [controller reloadData]; + } + }]; + return nil; + } else { + return self.cachedPreviewItems[index]; + } + } return nil; } diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 9f905a3b..5c89cf1c 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -805,7 +805,8 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { [self markSendInProgressMessagesAsPending]; } - [self synchronizeMissingAttachments]; + // we'll load the images on demand. + //[self synchronizeMissingAttachments]; [self saveMessages]; diff --git a/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m b/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m index da5424eb..96b8a792 100644 --- a/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m +++ b/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m @@ -15,7 +15,11 @@ - (NSString *)previewItemTitle { } - (NSURL *)previewItemURL { - return self.localURL; + if (self.localURL){ + return self.localURL; + } else { + return [NSURL fileURLWithPath:self.possibleFilename]; + } } @end diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index a76b995e..5162b24e 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -36,8 +36,10 @@ @property (nonatomic, copy) NSString *originalFilename; @property (nonatomic, copy) NSString *contentType; @property (nonatomic, copy) NSString *sourceURL; +@property (nonatomic) BOOL isLoading; @property (nonatomic, readonly) NSData *data; + @property (readonly) UIImage *imageRepresentation; @@ -55,4 +57,9 @@ - (NSURL *)localURL; +/** + Used to determine whether QuickLook can preview this file or not. If not, we don't download it. + */ +- (NSString*)possibleFilename; + @end diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 541d25fd..9fb38bdd 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -73,7 +73,7 @@ -(id)init { -(void)setData:(NSData *)data { self->_internalData = data; - self.filename = [self createFilename]; + self.filename = [self possibleFilename]; [self->_internalData writeToFile:self.filename atomically:NO]; } @@ -138,14 +138,18 @@ - (id)initWithCoder:(NSCoder *)aDecoder { #pragma mark - Thubmnails / Image Representation - (UIImage *)imageRepresentation { - if ([self.contentType rangeOfString:@"image"].location != NSNotFound ){ + if ([self.contentType rangeOfString:@"image"].location != NSNotFound && self.filename ){ return [UIImage imageWithData:self.data]; } else { // Create a Icon .. UIDocumentInteractionController* docController = [[UIDocumentInteractionController alloc] init]; docController.name = self.originalFilename; NSArray* icons = docController.icons; - return icons[0]; + if (icons.count){ + return icons[0]; + } else { + return nil; + } } } @@ -181,10 +185,11 @@ - (UIImage *)thumbnailWithSize:(CGSize)size { return self.thumbnailRepresentations[cacheKey]; } + #pragma mark - Persistence Helpers - - (NSString *)createFilename { + - (NSString *)possibleFilename { NSArray* cachePathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); NSString* cachePath = [cachePathArray lastObject]; cachePath = [cachePath stringByAppendingPathComponent:kCacheFolderName]; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index ad75c59f..bfef238a 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -156,6 +156,7 @@ 97BD9BD21911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 97BD9BD01911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h */; }; 97BD9BD31911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */; }; 97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; }; + 97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; }; 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; }; 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; }; 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; @@ -390,6 +391,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */, 1EA1170016F4D32C001C015C /* libHockeySDK.a in Frameworks */, 1E5A459216F0DFC200B55C04 /* SenTestingKit.framework in Frameworks */, 1EA1170116F4D354001C015C /* CrashReporter.framework in Frameworks */, From 64989bce0da5b98d6eabaaec32a9e30b8d1acb06 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 5 May 2014 14:49:52 +0200 Subject: [PATCH 118/206] + Added MobileCoreServices framework. --- Support/HockeySDK.xcodeproj/project.pbxproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index bfef238a..365d97bf 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -157,6 +157,8 @@ 97BD9BD31911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */; }; 97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; }; 97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; }; + 97CC11F91917C0310028768F /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97CC11F81917C0310028768F /* MobileCoreServices.framework */; }; + 97CC11FA1917C0390028768F /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97CC11F81917C0310028768F /* MobileCoreServices.framework */; }; 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; }; 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; }; 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; @@ -342,6 +344,7 @@ 97BD9BD01911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "BITFeedbackMessageAttachment+QLPreviewItem.h"; sourceTree = ""; }; 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "BITFeedbackMessageAttachment+QLPreviewItem.m"; sourceTree = ""; }; 97BD9BD4191109730043FD59 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; + 97CC11F81917C0310028768F /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = ""; }; 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconCamera@2x.png"; sourceTree = ""; }; 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotationViewController.h; sourceTree = ""; }; @@ -373,6 +376,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 97CC11F91917C0310028768F /* MobileCoreServices.framework in Frameworks */, 97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */, 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */, 1E5954DC15B6F24A00A03429 /* Foundation.framework in Frameworks */, @@ -391,6 +395,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 97CC11FA1917C0390028768F /* MobileCoreServices.framework in Frameworks */, 97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */, 1EA1170016F4D32C001C015C /* libHockeySDK.a in Frameworks */, 1E5A459216F0DFC200B55C04 /* SenTestingKit.framework in Frameworks */, @@ -655,6 +660,7 @@ E400561C148D79B500EB22B9 /* Frameworks */ = { isa = PBXGroup; children = ( + 97CC11F81917C0310028768F /* MobileCoreServices.framework */, 97BD9BD4191109730043FD59 /* QuickLook.framework */, 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */, E41EB48B148D7C4E0015DEDC /* CrashReporter.framework */, From 68791f592fb86ad0b9d038285fbd3db06cbdde1e Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 5 May 2014 14:57:38 +0200 Subject: [PATCH 119/206] + Fixed some async loading problems. --- Classes/BITFeedbackListViewCell.m | 2 +- Classes/BITFeedbackListViewController.m | 4 +++- Classes/BITFeedbackMessageAttachment.m | 10 ++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index d5c343eb..cc41817a 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -198,7 +198,7 @@ - (void)setAttachments:(NSArray *)attachments { for (BITFeedbackMessageAttachment *attachment in attachments){ UIButton *imageView = [UIButton buttonWithType:UIButtonTypeCustom]; [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; - + [imageView setContentMode:UIViewContentModeScaleAspectFit]; [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; [self.attachmentViews addObject:imageView]; diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index 2b774217..535373af 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -34,6 +34,7 @@ #import "HockeySDKPrivate.h" #import "BITFeedbackManagerPrivate.h" +#import "BITFeedbackManager.h" #import "BITFeedbackListViewController.h" #import "BITFeedbackListViewCell.h" #import "BITFeedbackComposeViewController.h" @@ -794,7 +795,7 @@ - (void)refreshPreviewItems { for (int i = 0; i Date: Mon, 5 May 2014 15:02:52 +0200 Subject: [PATCH 120/206] + Fixed Row display for more than 5 attachments. --- Classes/BITFeedbackListViewCell.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index cc41817a..3afd4db3 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -274,15 +274,15 @@ - (void)layoutSubviews { int i = 0; - CGFloat attachmentsPerRow = ceilf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); + CGFloat attachmentsPerRow = floorf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); for ( UIButton *imageButton in self.attachmentViews){ imageButton.contentMode = UIViewContentModeScaleAspectFit; if ( !_message.userMessage){ - imageButton.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * i , floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + imageButton.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) , floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } else { - imageButton.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i) ), floor(i/attachmentsPerRow) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + imageButton.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) ), floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } if (!imageButton.superview){ From 782a6f2e7960e18a2085fd271f5e6c30729741d9 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 5 May 2014 15:04:14 +0200 Subject: [PATCH 121/206] + Scaling issues resolved. --- Classes/BITFeedbackListViewCell.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 3afd4db3..6e49f2f1 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -278,6 +278,7 @@ - (void)layoutSubviews { for ( UIButton *imageButton in self.attachmentViews){ imageButton.contentMode = UIViewContentModeScaleAspectFit; + imageButton.imageView.contentMode = UIViewContentModeScaleAspectFill; if ( !_message.userMessage){ imageButton.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) , floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); From 53713537e808d09685f22ed7dee0264e7c29dc7e Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 7 May 2014 14:54:31 +0200 Subject: [PATCH 122/206] + Fixed a bug where attachments would not load. --- Classes/BITFeedbackMessageAttachment.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 9e14ba0c..45e4da3b 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -66,6 +66,7 @@ + (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType: -(id)init { self = [super init]; if (self){ + self.isLoading = NO; self.thumbnailRepresentations = [NSMutableDictionary new]; } return self; From 5f9de5a4ebdb1d520ee52f3f6243bebff7f35153 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 9 May 2014 14:41:28 +0200 Subject: [PATCH 123/206] Fix `updateManagerShouldSendUsageData` from `BITUpdateManagerDelegate` being called at the wrong time --- Classes/BITUpdateManager.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Classes/BITUpdateManager.m b/Classes/BITUpdateManager.m index 597e35a4..ccf0b561 100644 --- a/Classes/BITUpdateManager.m +++ b/Classes/BITUpdateManager.m @@ -424,10 +424,6 @@ - (id)init { self.lastCheck = [NSDate distantPast]; } - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(updateManagerShouldSendUsageData:)]) { - _sendUsageData = [self.delegate updateManagerShouldSendUsageData:self]; - } - if (!BITHockeyBundle()) { NSLog(@"[HockeySDK] WARNING: %@ is missing, make sure it is added!", BITHOCKEYSDK_BUNDLE); } @@ -763,6 +759,10 @@ - (void)startManager { BITHockeyLog(@"INFO: Starting UpdateManager"); + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(updateManagerShouldSendUsageData:)]) { + _sendUsageData = [self.delegate updateManagerShouldSendUsageData:self]; + } + [self checkExpiryDateReached]; if (![self expiryDateReached]) { if ([self checkForTracker] || ([self isCheckForUpdateOnLaunch] && [self shouldCheckForUpdates])) { From 7f0a04044ccc86d06056ce619bc681325947ce0c Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Mon, 12 May 2014 01:25:30 +0200 Subject: [PATCH 124/206] Some fixes, but too much movement. ATM. --- Classes/BITFeedbackComposeViewController.m | 41 +++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index bb353832..b987112a 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -62,6 +62,10 @@ @interface BITFeedbackComposeViewController () self.attachmentScrollViewImageViews.count){ @@ -296,6 +309,7 @@ -(void)refreshAttachmentScrollview { UIButton *newImageButton = [UIButton buttonWithType:UIButtonTypeCustom]; [newImageButton addTarget:self action:@selector(imageButtonAction:) forControlEvents:UIControlEventTouchUpInside]; [self.attachmentScrollViewImageViews addObject:newImageButton]; + [self.attachmentScrollView addSubview:newImageButton]; } } @@ -303,6 +317,8 @@ -(void)refreshAttachmentScrollview { CGFloat currentYOffset = 0.0f; + CGFloat heightOfLastAttachment = 0.0f; + for (BITFeedbackMessageAttachment* attachment in self.attachments){ UIButton *imageButton = self.attachmentScrollViewImageViews[index]; @@ -317,13 +333,23 @@ -(void)refreshAttachmentScrollview { currentYOffset += height; - [self.attachmentScrollView addSubview:imageButton]; + // [self.attachmentScrollView addSubview:imageButton]; + + heightOfLastAttachment = height + 20; [imageButton setImage:image forState:UIControlStateNormal]; index++; } [self.attachmentScrollView setContentSize:CGSizeMake(CGRectGetWidth(self.attachmentScrollView.frame), currentYOffset)]; + NSLog(@"%f %f", self.attachmentScrollView.contentOffset.x, self.attachmentScrollView.contentOffset.y); + if (self.scrollToNewestAttachment && self.reallyVisible && currentYOffset - heightOfLastAttachment > 0){ + self.scrollToNewestAttachment = NO; + + [self.attachmentScrollView setContentOffset:CGPointMake(0,currentYOffset-heightOfLastAttachment)]; + } else { + self.attachmentScrollView.contentOffset = preservedOffset; + } [self updateBarButtonState]; } @@ -334,6 +360,13 @@ - (void)updateBarButtonState { } else { self.navigationItem.rightBarButtonItem.enabled = NO; } + + if (self.attachments.count > 2){ + self.textView.inputAccessoryView = nil; + } else { + self.textView.inputAccessoryView = self.textAccessoryView; + + } } @@ -407,10 +440,10 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking NSString *imageName = [imagePath lastPathComponent]; newAttachment.originalFilename = imageName; [self.attachments addObject:newAttachment]; + self.scrollToNewestAttachment = YES; } [picker dismissViewControllerAnimated:YES completion:nil]; - [self refreshAttachmentScrollview]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { @@ -476,6 +509,7 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn self.selectedAttachmentIndex = NSNotFound; [self refreshAttachmentScrollview]; + } else if(buttonIndex != [actionSheet cancelButtonIndex]){ if (self.selectedAttachmentIndex != NSNotFound){ BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; @@ -497,7 +531,6 @@ - (void)annotationController:(BITImageAnnotationViewController *)annotationContr if (self.selectedAttachmentIndex != NSNotFound){ BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; [attachment replaceData:UIImageJPEGRepresentation(image, 0.7f)]; - [self refreshAttachmentScrollview]; } self.selectedAttachmentIndex = NSNotFound; From 901a2e1cdd953558f7589dd0a398fea2fcf0e7e4 Mon Sep 17 00:00:00 2001 From: Moritz Haarmann Date: Mon, 12 May 2014 02:05:45 +0200 Subject: [PATCH 125/206] + Async loading of attachments once shown. --- Classes/BITFeedbackListViewController.m | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index 535373af..f9d20946 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -74,6 +74,7 @@ @interface BITFeedbackListViewController () Date: Mon, 12 May 2014 12:01:29 +0200 Subject: [PATCH 126/206] + Now comes with vertical AND horizontal centering. --- Classes/BITImageAnnotationViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 9f8c74f6..987c23a3 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -138,7 +138,7 @@ - (void)fitImageViewFrame { self.scaleFactor = factor; CGSize scaledImageSize = CGSizeMake(self.image.size.width * factor, self.image.size.height * factor); - CGRect baseFrame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height - size.height, scaledImageSize.width, scaledImageSize.height); + CGRect baseFrame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height/2 - scaledImageSize.height/2, scaledImageSize.width, scaledImageSize.height); self.imageView.frame = baseFrame; } From d736b35f04157e179fa8288ea08402c22780f26b Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 12 May 2014 12:28:00 +0200 Subject: [PATCH 127/206] + Append new Attachments on top. Solves some problems. --- Classes/BITFeedbackComposeViewController.m | 51 +++++++++------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index b987112a..f1da11ad 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -61,10 +61,7 @@ @interface BITFeedbackComposeViewController () self.attachmentScrollViewImageViews.count){ @@ -317,9 +309,9 @@ -(void)refreshAttachmentScrollview { CGFloat currentYOffset = 0.0f; - CGFloat heightOfLastAttachment = 0.0f; + NSEnumerator *reverseAttachments = self.attachments.reverseObjectEnumerator; - for (BITFeedbackMessageAttachment* attachment in self.attachments){ + for (BITFeedbackMessageAttachment* attachment in reverseAttachments.allObjects){ UIButton *imageButton = self.attachmentScrollViewImageViews[index]; UIImage *image = [attachment thumbnailWithSize:CGSizeMake(100, 100)]; @@ -333,23 +325,11 @@ -(void)refreshAttachmentScrollview { currentYOffset += height; - // [self.attachmentScrollView addSubview:imageButton]; - - heightOfLastAttachment = height + 20; - [imageButton setImage:image forState:UIControlStateNormal]; index++; } [self.attachmentScrollView setContentSize:CGSizeMake(CGRectGetWidth(self.attachmentScrollView.frame), currentYOffset)]; - NSLog(@"%f %f", self.attachmentScrollView.contentOffset.x, self.attachmentScrollView.contentOffset.y); - if (self.scrollToNewestAttachment && self.reallyVisible && currentYOffset - heightOfLastAttachment > 0){ - self.scrollToNewestAttachment = NO; - - [self.attachmentScrollView setContentOffset:CGPointMake(0,currentYOffset-heightOfLastAttachment)]; - } else { - self.attachmentScrollView.contentOffset = preservedOffset; - } [self updateBarButtonState]; } @@ -428,6 +408,16 @@ -(void)addPhotoAction:(id)sender { [self presentViewController:pickerController animated:YES completion:nil]; } +- (void)scrollViewTapped:(id)unused { + UIMenuController *menuController = [UIMenuController sharedMenuController]; + [menuController setTargetRect:CGRectMake([self.tapRecognizer locationInView:self.view].x, [self.tapRecognizer locationInView:self.view].x, 1, 1) inView:self.view]; + [menuController setMenuVisible:YES animated:YES]; +} + +- (void)paste:(id)sender { + +} + #pragma mark - UIImagePickerControllerDelegate - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { @@ -440,7 +430,6 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking NSString *imageName = [imagePath lastPathComponent]; newAttachment.originalFilename = imageName; [self.attachments addObject:newAttachment]; - self.scrollToNewestAttachment = YES; } [picker dismissViewControllerAnimated:YES completion:nil]; From 6082885d6592794a4f7af62b2d86b54f3fd01817 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 12 May 2014 12:34:18 +0200 Subject: [PATCH 128/206] + Removed some Logging stuff. --- Classes/BITFeedbackListViewController.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index f9d20946..ede1c4f8 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -814,7 +814,6 @@ - (void)refreshPreviewItems { for (int i = 0; i Date: Mon, 12 May 2014 12:36:03 +0200 Subject: [PATCH 129/206] + Added License Headers --- Classes/BITArrowImageAnnotation.h | 34 +++++++++++++++++----- Classes/BITArrowImageAnnotation.m | 34 +++++++++++++++++----- Classes/BITBlurImageAnnotation.h | 34 +++++++++++++++++----- Classes/BITBlurImageAnnotation.m | 34 +++++++++++++++++----- Classes/BITImageAnnotation.h | 34 +++++++++++++++++----- Classes/BITImageAnnotation.m | 34 +++++++++++++++++----- Classes/BITImageAnnotationViewController.h | 34 +++++++++++++++++----- Classes/BITImageAnnotationViewController.m | 34 +++++++++++++++++----- Classes/BITRectangleImageAnnotation.h | 34 +++++++++++++++++----- Classes/BITRectangleImageAnnotation.m | 34 +++++++++++++++++----- 10 files changed, 270 insertions(+), 70 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.h b/Classes/BITArrowImageAnnotation.h index ff243af6..feeca761 100644 --- a/Classes/BITArrowImageAnnotation.h +++ b/Classes/BITArrowImageAnnotation.h @@ -1,10 +1,30 @@ -// -// BITArrowImageAnnotation.h -// HockeySDK -// -// Created by Moritz Haarmann on 26.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITImageAnnotation.h" diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index 40c58e95..2d739b4f 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -1,10 +1,30 @@ -// -// BITArrowImageAnnotation.m -// HockeySDK -// -// Created by Moritz Haarmann on 26.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITArrowImageAnnotation.h" diff --git a/Classes/BITBlurImageAnnotation.h b/Classes/BITBlurImageAnnotation.h index b23d7f2d..cb5a1f3e 100644 --- a/Classes/BITBlurImageAnnotation.h +++ b/Classes/BITBlurImageAnnotation.h @@ -1,10 +1,30 @@ -// -// BITBlurImageAnnotation.h -// HockeySDK -// -// Created by Moritz Haarmann on 26.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITImageAnnotation.h" diff --git a/Classes/BITBlurImageAnnotation.m b/Classes/BITBlurImageAnnotation.m index 983c49b8..4760229d 100644 --- a/Classes/BITBlurImageAnnotation.m +++ b/Classes/BITBlurImageAnnotation.m @@ -1,10 +1,30 @@ -// -// BITBlurImageAnnotation.m -// HockeySDK -// -// Created by Moritz Haarmann on 26.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITBlurImageAnnotation.h" diff --git a/Classes/BITImageAnnotation.h b/Classes/BITImageAnnotation.h index 24d312e5..8f1354a6 100644 --- a/Classes/BITImageAnnotation.h +++ b/Classes/BITImageAnnotation.h @@ -1,10 +1,30 @@ -// -// BITImageAnnotation.h -// HockeySDK -// -// Created by Moritz Haarmann on 24.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 diff --git a/Classes/BITImageAnnotation.m b/Classes/BITImageAnnotation.m index 473889a3..428e8b11 100644 --- a/Classes/BITImageAnnotation.m +++ b/Classes/BITImageAnnotation.m @@ -1,10 +1,30 @@ -// -// BITImageAnnotation.m -// HockeySDK -// -// Created by Moritz Haarmann on 24.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITImageAnnotation.h" diff --git a/Classes/BITImageAnnotationViewController.h b/Classes/BITImageAnnotationViewController.h index d333d612..026968ce 100644 --- a/Classes/BITImageAnnotationViewController.h +++ b/Classes/BITImageAnnotationViewController.h @@ -1,10 +1,30 @@ -// -// BITImageAnnotationViewController.h -// HockeySDK -// -// Created by Moritz Haarmann on 14.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 987c23a3..e9420da0 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -1,10 +1,30 @@ -// -// BITImageAnnotationViewController.m -// HockeySDK -// -// Created by Moritz Haarmann on 14.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITImageAnnotationViewController.h" #import "BITImageAnnotation.h" diff --git a/Classes/BITRectangleImageAnnotation.h b/Classes/BITRectangleImageAnnotation.h index 0ba16c0c..02495799 100644 --- a/Classes/BITRectangleImageAnnotation.h +++ b/Classes/BITRectangleImageAnnotation.h @@ -1,10 +1,30 @@ -// -// BITRectangleImageAnnotation.h -// HockeySDK -// -// Created by Moritz Haarmann on 25.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITImageAnnotation.h" diff --git a/Classes/BITRectangleImageAnnotation.m b/Classes/BITRectangleImageAnnotation.m index 9b677cb3..b08f5d30 100644 --- a/Classes/BITRectangleImageAnnotation.m +++ b/Classes/BITRectangleImageAnnotation.m @@ -1,10 +1,30 @@ -// -// BITRectangleImageAnnotation.m -// HockeySDK -// -// Created by Moritz Haarmann on 25.02.14. -// -// +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITRectangleImageAnnotation.h" From 3338486c539126c72aca4b4271a2b5a136f3df0a Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Mon, 12 May 2014 12:36:52 +0200 Subject: [PATCH 130/206] + Reintegrated categorized methods into BITFeedbackMessageAttachment. --- Classes/BITFeedbackComposeViewController.m | 4 +- Classes/BITFeedbackListViewController.m | 1 - ...TFeedbackMessageAttachment+QLPreviewItem.h | 16 --- ...TFeedbackMessageAttachment+QLPreviewItem.m | 25 ----- Classes/BITFeedbackMessageAttachment.h | 3 +- Classes/BITFeedbackMessageAttachment.m | 99 +++++++++++-------- Support/HockeySDK.xcodeproj/project.pbxproj | 8 -- 7 files changed, 59 insertions(+), 97 deletions(-) delete mode 100644 Classes/BITFeedbackMessageAttachment+QLPreviewItem.h delete mode 100644 Classes/BITFeedbackMessageAttachment+QLPreviewItem.m diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index f1da11ad..7a5dff7e 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -273,9 +273,7 @@ - (void)viewDidDisappear:(BOOL)animated { -(void)refreshAttachmentScrollview { CGFloat scrollViewWidth = 0; - - CGPoint preservedOffset = self.attachmentScrollView.contentOffset; - + if (self.attachments.count){ scrollViewWidth = 100; } diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index ede1c4f8..a384bc0b 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -41,7 +41,6 @@ #import "BITFeedbackUserDataViewController.h" #import "BITFeedbackMessage.h" #import "BITFeedbackMessageAttachment.h" -#import "BITFeedbackMessageAttachment+QLPreviewItem.h" #import "BITAttributedLabel.h" #import "BITHockeyBaseManagerPrivate.h" diff --git a/Classes/BITFeedbackMessageAttachment+QLPreviewItem.h b/Classes/BITFeedbackMessageAttachment+QLPreviewItem.h deleted file mode 100644 index 5c05718a..00000000 --- a/Classes/BITFeedbackMessageAttachment+QLPreviewItem.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// BITFeedbackMessageAttachment+QLPreviewItem.h -// HockeySDK -// -// Created by Moritz Haarmann on 30.04.14. -// -// - -#import "BITFeedbackMessageAttachment.h" -#import - -@interface BITFeedbackMessageAttachment (QLPreviewItem) - - - -@end diff --git a/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m b/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m deleted file mode 100644 index 96b8a792..00000000 --- a/Classes/BITFeedbackMessageAttachment+QLPreviewItem.m +++ /dev/null @@ -1,25 +0,0 @@ -// -// BITFeedbackMessageAttachment+QLPreviewItem.m -// HockeySDK -// -// Created by Moritz Haarmann on 30.04.14. -// -// - -#import "BITFeedbackMessageAttachment+QLPreviewItem.h" - -@implementation BITFeedbackMessageAttachment (QLPreviewItem) - -- (NSString *)previewItemTitle { - return self.originalFilename; -} - -- (NSURL *)previewItemURL { - if (self.localURL){ - return self.localURL; - } else { - return [NSURL fileURLWithPath:self.possibleFilename]; - } -} - -@end diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index 5162b24e..96adde85 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -29,8 +29,9 @@ #import #import +#import -@interface BITFeedbackMessageAttachment : NSObject +@interface BITFeedbackMessageAttachment : NSObject @property (nonatomic, copy) NSNumber *id; @property (nonatomic, copy) NSString *originalFilename; diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 45e4da3b..ac855101 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -175,57 +175,70 @@ - (UIImage *)thumbnailWithSize:(CGSize)size { [self.thumbnailRepresentations setObject:scaledTumbnail forKey:cacheKey]; } - } else { - UIImage *thumbnail = bit_imageToFitSize(image, size, YES) ; - - [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; - - } + } else { + UIImage *thumbnail = bit_imageToFitSize(image, size, YES) ; + + [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; + + } } - return self.thumbnailRepresentations[cacheKey]; - } + return self.thumbnailRepresentations[cacheKey]; +} + - #pragma mark - Persistence Helpers + +- (NSString *)possibleFilename { + NSArray* cachePathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString* cachePath = [cachePathArray lastObject]; + cachePath = [cachePath stringByAppendingPathComponent:kCacheFolderName]; - - (NSString *)possibleFilename { - NSArray* cachePathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - NSString* cachePath = [cachePathArray lastObject]; - cachePath = [cachePath stringByAppendingPathComponent:kCacheFolderName]; - - BOOL isDirectory; - - if (![[NSFileManager defaultManager] fileExistsAtPath:cachePath isDirectory:&isDirectory]){ - [[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:nil]; - } - - NSString *uniqueString = bit_UUID(); - cachePath = [cachePath stringByAppendingPathComponent:uniqueString]; - - // File extension that suits the Content type. - - CFStringRef mimeType = (__bridge CFStringRef)self.contentType; - CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); - CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); - if (extension){ - cachePath = [cachePath stringByAppendingPathExtension:(__bridge NSString *)(extension)]; - CFRelease(extension); - - } - - CFRelease(uti); - - return cachePath; + BOOL isDirectory; + + if (![[NSFileManager defaultManager] fileExistsAtPath:cachePath isDirectory:&isDirectory]){ + [[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:nil]; } - - (void)deleteContents { - if (self.filename){ - [[NSFileManager defaultManager] removeItemAtPath:self.filename error:nil]; - self.filename = nil; - } + NSString *uniqueString = bit_UUID(); + cachePath = [cachePath stringByAppendingPathComponent:uniqueString]; + + // File extension that suits the Content type. + + CFStringRef mimeType = (__bridge CFStringRef)self.contentType; + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); + CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); + if (extension){ + cachePath = [cachePath stringByAppendingPathExtension:(__bridge NSString *)(extension)]; + CFRelease(extension); + } + CFRelease(uti); - @end + return cachePath; +} + +- (void)deleteContents { + if (self.filename){ + [[NSFileManager defaultManager] removeItemAtPath:self.filename error:nil]; + self.filename = nil; + } +} + +#pragma mark - QLPreviewItem + +- (NSString *)previewItemTitle { + return self.originalFilename; +} + +- (NSURL *)previewItemURL { + if (self.localURL){ + return self.localURL; + } else { + return [NSURL fileURLWithPath:self.possibleFilename]; + } +} + +@end diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 365d97bf..7094783b 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -153,8 +153,6 @@ 9782023918F81BFC00A98D8B /* Ok@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022F18F81BFC00A98D8B /* Ok@2x.png */; }; 9782023A18F81BFC00A98D8B /* Rectangle.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023018F81BFC00A98D8B /* Rectangle.png */; }; 9782023B18F81BFC00A98D8B /* Rectangle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023118F81BFC00A98D8B /* Rectangle@2x.png */; }; - 97BD9BD21911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 97BD9BD01911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h */; }; - 97BD9BD31911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */; }; 97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; }; 97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; }; 97CC11F91917C0310028768F /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97CC11F81917C0310028768F /* MobileCoreServices.framework */; }; @@ -341,8 +339,6 @@ 9782022F18F81BFC00A98D8B /* Ok@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Ok@2x.png"; sourceTree = ""; }; 9782023018F81BFC00A98D8B /* Rectangle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Rectangle.png; sourceTree = ""; }; 9782023118F81BFC00A98D8B /* Rectangle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Rectangle@2x.png"; sourceTree = ""; }; - 97BD9BD01911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "BITFeedbackMessageAttachment+QLPreviewItem.h"; sourceTree = ""; }; - 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "BITFeedbackMessageAttachment+QLPreviewItem.m"; sourceTree = ""; }; 97BD9BD4191109730043FD59 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; 97CC11F81917C0310028768F /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = ""; }; @@ -535,8 +531,6 @@ 1E49A4341612223B00463151 /* BITFeedbackManager.m */, E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */, 1E49A4351612223B00463151 /* BITFeedbackManagerPrivate.h */, - 97BD9BD01911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h */, - 97BD9BD11911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m */, ); name = Feedback; sourceTree = ""; @@ -739,7 +733,6 @@ 1E59559A15B6FDA500A03429 /* BITHockeyManager.h in Headers */, 1E5955FD15B7877B00A03429 /* BITHockeyManagerDelegate.h in Headers */, 1E754E5C1621FBB70070AB92 /* BITCrashManager.h in Headers */, - 97BD9BD21911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.h in Headers */, 1E754E5E1621FBB70070AB92 /* BITCrashManagerDelegate.h in Headers */, 1E49A4731612226D00463151 /* BITUpdateManager.h in Headers */, 1E49A4791612226D00463151 /* BITUpdateManagerDelegate.h in Headers */, @@ -993,7 +986,6 @@ files = ( 1E5954D315B6F24A00A03429 /* BITHockeyManager.m in Sources */, 1E49A43F1612223B00463151 /* BITFeedbackComposeViewController.m in Sources */, - 97BD9BD31911085A0043FD59 /* BITFeedbackMessageAttachment+QLPreviewItem.m in Sources */, E40E0B0D17DA1AFF005E38C1 /* BITHockeyAppClient.m in Sources */, 1E49A4451612223B00463151 /* BITFeedbackListViewCell.m in Sources */, 973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */, From 6cd4c089f0ac1a15e525c65080845250bd5343cd Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 13 May 2014 11:41:15 +0200 Subject: [PATCH 131/206] Fix BITUpdateManager BITUpdateCheckManually being ignored when the app comes back from background --- Classes/BITUpdateManager.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/BITUpdateManager.m b/Classes/BITUpdateManager.m index ccf0b561..61d1b6b0 100644 --- a/Classes/BITUpdateManager.m +++ b/Classes/BITUpdateManager.m @@ -108,7 +108,8 @@ - (void)didBecomeActiveActions { if ([self expiryDateReached]) return; [self startUsage]; - if (_checkForUpdateOnLaunch) { + + if ([self isCheckForUpdateOnLaunch] && [self shouldCheckForUpdates]) { [self checkForUpdate]; } } From 58f6854a5e8120d16fcdacfc93a548f933a7a11a Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 20 May 2014 12:03:22 +0200 Subject: [PATCH 132/206] Update BITCrashDetails and remove non needed properties in BITCrashManager cleanup :) --- Classes/BITCrashDetails.h | 63 ++++++++++++++++----- Classes/BITCrashDetails.m | 46 ++++++++++++--- Classes/BITCrashDetailsPrivate.h | 46 +++++++++++++++ Classes/BITCrashManager.h | 27 ++------- Classes/BITCrashManager.m | 4 +- Support/HockeySDK.xcodeproj/project.pbxproj | 4 ++ 6 files changed, 143 insertions(+), 47 deletions(-) create mode 100644 Classes/BITCrashDetailsPrivate.h diff --git a/Classes/BITCrashDetails.h b/Classes/BITCrashDetails.h index b9322f35..4cd8fb73 100644 --- a/Classes/BITCrashDetails.h +++ b/Classes/BITCrashDetails.h @@ -1,10 +1,30 @@ -// -// BITCrashDetails.h -// HockeySDK -// -// Created by Andreas Linde on 03.04.14. -// -// +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 @@ -50,13 +70,26 @@ */ @property (nonatomic, readonly, strong) NSString *appBuild; -- (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier - reporterKey:(NSString *)reporterKey - signal:(NSString *)signal - exceptionName:(NSString *)exceptionName - exceptionReason:(NSString *)exceptionReason - appStartTime:(NSDate *)appStartTime - crashTime:(NSDate *)crashTime - appBuild:(NSString *)appBuild; +/** + Indicates if the app was killed while being in foreground from the iOS + + If `[BITCrashManager enableAppNotTerminatingCleanlyDetection]` is enabled, use this on startup + to check if the app starts the first time after it was killed by iOS in the previous session. + + This can happen if it consumed too much memory or the watchdog killed the app because it + took too long to startup or blocks the main thread for too long, or other reasons. See Apple + documentation: https://developer.apple.com/library/ios/qa/qa1693/_index.html + + See `[BITCrashManager enableDectionAppKillWhileInForeground]` for more details about which kind of kills can be detected. + + @warning This property only has a correct value, once `[BITHockeyManager startManager]` was + invoked! In addition, it is automatically disabled while a debugger session is active! + + @see `[BITCrashManager enableAppNotTerminatingCleanlyDetection]` + @see `[BITCrashManager didReceiveMemoryWarningInLastSession]` + + @return YES if the details represent an app kill instead of a crash + */ +- (BOOL)isAppKill; @end diff --git a/Classes/BITCrashDetails.m b/Classes/BITCrashDetails.m index 187b7ba0..b7f5b52c 100644 --- a/Classes/BITCrashDetails.m +++ b/Classes/BITCrashDetails.m @@ -1,12 +1,35 @@ -// -// BITCrashDetails.m -// HockeySDK -// -// Created by Andreas Linde on 03.04.14. -// -// +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITCrashDetails.h" +#import "BITCrashDetailsPrivate.h" + +NSString *const kBITCrashKillSignal = @"SIGKILL"; @implementation BITCrashDetails @@ -32,4 +55,13 @@ - (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier return self; } +- (BOOL)isAppKill { + BOOL result = NO; + + if (_signal && [[_signal uppercaseString] isEqualToString:kBITCrashKillSignal]) + result = YES; + + return result; +} + @end diff --git a/Classes/BITCrashDetailsPrivate.h b/Classes/BITCrashDetailsPrivate.h new file mode 100644 index 00000000..6c6fa241 --- /dev/null +++ b/Classes/BITCrashDetailsPrivate.h @@ -0,0 +1,46 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 + +extern NSString *const __attribute__((unused)) kBITCrashKillSignal; + +@interface BITCrashDetails () { + +} + +- (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier + reporterKey:(NSString *)reporterKey + signal:(NSString *)signal + exceptionName:(NSString *)exceptionName + exceptionReason:(NSString *)exceptionReason + appStartTime:(NSDate *)appStartTime + crashTime:(NSDate *)crashTime + appBuild:(NSString *)appBuild; + +@end diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 49e32601..6cb8033a 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -259,7 +259,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { * @warning This is a heuristic and it _MAY_ report false positives! It has been tested with iOS 6.1 and iOS 7. * Depending on Apple changing notification events, new iOS version may cause more false positives! * - * @see wasKilledInLastSession + * @see lastSessionCrashDetails * @see didReceiveMemoryWarningInLastSession * @see `BITCrashManagerDelegate considerAppNotTerminatedCleanlyReportForCrashManager:` * @see [Apple Technical Note TN2151](https://developer.apple.com/library/ios/technotes/tn2151/_index.html) @@ -325,6 +325,8 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { @warning This property only has a correct value, once `[BITHockeyManager startManager]` was invoked! + + @see lastSessionCrashDetails */ @property (nonatomic, readonly) BOOL didCrashInLastSession; @@ -349,27 +351,6 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { */ - (void) setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler; -/** - Indicates if the app was killed while being in foreground from the iOS - - If `enableAppNotTerminatingCleanlyDetection` is enabled, use this on startup to check if the - app starts the first time after it was killed by iOS in the previous session. - - This can happen if it consumed too much memory or the watchdog killed the app because it - took too long to startup or blocks the main thread for too long, or other reasons. See Apple - documentation: https://developer.apple.com/library/ios/qa/qa1693/_index.html - - See `enableDectionAppKillWhileInForeground` for more details about which kind of kills can be detected. - - @warning This property only has a correct value, once `[BITHockeyManager startManager]` was - invoked! In addition, it is automatically disabled while a debugger session is active! - - @see enableAppNotTerminatingCleanlyDetection - @see didReceiveMemoryWarningInLastSession - */ -@property (nonatomic, readonly) BOOL wasKilledInLastSession; - - /** * Provides details about the crash that occured in the last app session */ @@ -392,7 +373,7 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { invoked! @see enableAppNotTerminatingCleanlyDetection - @see wasKilledInLastSession + @see lastSessionCrashDetails */ @property (nonatomic, readonly) BOOL didReceiveMemoryWarningInLastSession; diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 7ac9094c..33a8fff6 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -43,6 +43,7 @@ #import "BITHockeyBaseManagerPrivate.h" #import "BITCrashManagerPrivate.h" #import "BITCrashReportTextFormatter.h" +#import "BITCrashDetailsPrivate.h" #include @@ -1056,7 +1057,6 @@ - (void)startManager { if (considerReport) { [self createCrashReportForAppKill]; - _wasKilledInLastSession = YES; _didCrashInLastSession = YES; } } @@ -1084,7 +1084,7 @@ - (void)createCrashReportForAppKill { NSString *fakeReportDeviceModel = [self getDevicePlatform] ?: @"Unknown"; NSString *fakeReportAppUUIDs = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppUUIDs] ?: @""; - NSString *fakeSignalName = @"SIGKILL"; + NSString *fakeSignalName = kBITCrashKillSignal; NSMutableString *fakeReportString = [NSMutableString string]; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 13d4da67..e2f862b2 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -129,6 +129,7 @@ 1EAF20AA162DC0F600957B1D /* feedbackActiviy.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A6162DC0F600957B1D /* feedbackActiviy.png */; }; 1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */; }; 1EB52FD5167B766100C801D5 /* HockeySDK.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E59555F15B6F80E00A03429 /* HockeySDK.strings */; }; + 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */; }; 1ED570C718BF878C00AB3350 /* BITCrashAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1ED570C818BF878C00AB3350 /* BITCrashAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ED570C618BF878C00AB3350 /* BITCrashAttachment.m */; }; 1EF95CA6162CB037000AE3AD /* BITFeedbackActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EF95CA4162CB036000AE3AD /* BITFeedbackActivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -289,6 +290,7 @@ 1EAF20A6162DC0F600957B1D /* feedbackActiviy.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = feedbackActiviy.png; sourceTree = ""; }; 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "feedbackActiviy@2x.png"; sourceTree = ""; }; 1EB52FC3167B73D400C801D5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/HockeySDK.strings; sourceTree = ""; }; + 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashDetailsPrivate.h; sourceTree = ""; }; 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashAttachment.h; sourceTree = ""; }; 1ED570C618BF878C00AB3350 /* BITCrashAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashAttachment.m; sourceTree = ""; }; 1EDA60CF15C2C1450032D10B /* HockeySDK-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "HockeySDK-Info.plist"; sourceTree = ""; }; @@ -488,6 +490,7 @@ 1E754E571621FBB70070AB92 /* BITCrashManager.m */, 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */, 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */, + 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */, 1EFF03D717F20F8300A5F13C /* BITCrashManagerPrivate.h */, 1E754E581621FBB70070AB92 /* BITCrashManagerDelegate.h */, 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */, @@ -652,6 +655,7 @@ 1E49A44E1612223B00463151 /* BITFeedbackManager.h in Headers */, E4B4DB7D17B435550099C67F /* BITAuthenticationViewController.h in Headers */, 1E49A4481612223B00463151 /* BITFeedbackListViewController.h in Headers */, + 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */, 1E49A47F1612226D00463151 /* BITUpdateViewController.h in Headers */, 1E49A43C1612223B00463151 /* BITFeedbackComposeViewController.h in Headers */, E40E0B0C17DA1AFF005E38C1 /* BITHockeyAppClient.h in Headers */, From 89a73989713e0d90fe1b929bc2677ce0b21c2b52 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 20 May 2014 13:28:05 +0200 Subject: [PATCH 133/206] Allow the developer to set more than just the description when handling user alert for crashes - Also fixes a bug where crash caches dir could get deleted - Some refactoring --- Classes/BITCrashManager.h | 12 +++-- Classes/BITCrashManager.m | 44 ++++++++++----- Classes/BITCrashManagerPrivate.h | 5 +- Classes/BITCrashMetaData.h | 54 +++++++++++++++++++ Classes/BITCrashMetaData.m | 34 ++++++++++++ Classes/HockeySDK.h | 1 + Support/HockeySDK.xcodeproj/project.pbxproj | 12 ++++- Support/HockeySDKTests/BITCrashManagerTests.m | 19 ++++--- 8 files changed, 151 insertions(+), 30 deletions(-) create mode 100644 Classes/BITCrashMetaData.h create mode 100644 Classes/BITCrashMetaData.m diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 6cb8033a..3a72a5fe 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -33,6 +33,8 @@ #import "BITHockeyBaseManager.h" @class BITCrashDetails; +@class BITCrashMetaData; + /** * Custom block that handles the alert that prompts the user whether he wants to send crash reports @@ -331,15 +333,17 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { @property (nonatomic, readonly) BOOL didCrashInLastSession; /** - Provides an interface to handle user input from a custom alert + Provides an interface to pass user input from a custom alert to a crash report - @param userInput On this input depends, whether crash reports are sent, always sent or not sent and deleted. - @param userProvidedCrashDescription The content of this optional string will be attached to the crash report as the description and allows to ask the user for e.g. additional comments or info + @param userInput Defines the users action wether to send, always send, or not to send the crash report. + @param userProvidedMetaData The content of this optional BITCrashMetaData instance will be attached to the crash report and allows to ask the user for e.g. additional comments or info. @return Returns YES if the input is a valid option and successfully triggered further processing of the crash report + @see BITCrashManagerUserInput + @see BITCrashMetaData */ -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString*)userProvidedCrashDescription; +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData; /** Lets you set a custom block which handles showing a custom UI and asking the user diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 33a8fff6..80f31e5c 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -100,7 +100,6 @@ @implementation BITCrashManager { NSMutableDictionary *_approvedCrashReports; NSMutableArray *_crashFiles; - NSString *_crashesDir; NSString *_lastCrashFilename; NSString *_settingsFile; NSString *_analyzerInProgressFile; @@ -290,10 +289,26 @@ - (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSStrin [data writeToFile:attachmentFilename atomically:YES]; } -- (void)persistUserProvidedCrashDescription:(NSString *)userProvidedCrashDescription { - if (userProvidedCrashDescription && [userProvidedCrashDescription length] > 0) { +- (void)persistUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData { + if (!userProvidedMetaData) return; + + if (userProvidedMetaData.userDescription && [userProvidedMetaData.userDescription length] > 0) { NSError *error; - [userProvidedCrashDescription writeToFile:[NSString stringWithFormat:@"%@.desc", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; + [userProvidedMetaData.userDescription writeToFile:[NSString stringWithFormat:@"%@.desc", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; + } + + if (userProvidedMetaData.userName && [userProvidedMetaData.userName length] > 0) { + [self addStringValueToKeychain:userProvidedMetaData.userName forKey:[NSString stringWithFormat:@"%@.%@", _lastCrashFilename, kBITCrashMetaUserName]]; + + } + + if (userProvidedMetaData.userEmail && [userProvidedMetaData.userEmail length] > 0) { + [self addStringValueToKeychain:userProvidedMetaData.userEmail forKey:[NSString stringWithFormat:@"%@.%@", _lastCrashFilename, kBITCrashMetaUserEmail]]; + } + + if (userProvidedMetaData.userID && [userProvidedMetaData.userID length] > 0) { + [self addStringValueToKeychain:userProvidedMetaData.userID forKey:[NSString stringWithFormat:@"%@.%@", _lastCrashFilename, kBITCrashMetaUserID]]; + } } @@ -579,9 +594,6 @@ - (NSString *)userEmailForCrashReport { return useremail; } -- (NSString *)getCrashesDir { - return _crashesDir; -} #pragma mark - Public @@ -692,18 +704,21 @@ - (void)storeMetaDataForCrashReportFilename:(NSString *)filename { } } -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCrashDescription:(NSString *)userProvidedCrashDescription{ +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData { switch (userInput) { case BITCrashManagerUserInputDontSend: if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { [self.delegate crashManagerWillCancelSendingCrashReport:self]; } - [self cleanCrashReportWithFilename:[_crashesDir stringByAppendingPathComponent: _lastCrashFilename]]; + if (_lastCrashFilename) + [self cleanCrashReportWithFilename:[_crashesDir stringByAppendingPathComponent: _lastCrashFilename]]; + return YES; case BITCrashManagerUserInputSend: - [self persistUserProvidedCrashDescription:userProvidedCrashDescription]; + if (userProvidedMetaData) + [self persistUserProvidedMetaData:userProvidedMetaData]; [self sendNextCrashReport]; return YES; @@ -716,7 +731,8 @@ - (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedCras [self.delegate crashManagerWillSendCrashReportsAlways:self]; } - [self persistUserProvidedCrashDescription:userProvidedCrashDescription]; + if (userProvidedMetaData) + [self persistUserProvidedMetaData:userProvidedMetaData]; [self sendNextCrashReport]; return YES; @@ -1310,13 +1326,13 @@ - (void)sendNextCrashReport { - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 0: - [self handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedCrashDescription:nil]; + [self handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedMetaData:nil]; break; case 1: - [self handleUserInput:BITCrashManagerUserInputSend withUserProvidedCrashDescription:nil]; + [self handleUserInput:BITCrashManagerUserInputSend withUserProvidedMetaData:nil]; break; case 2: - [self handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedCrashDescription:nil]; + [self handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedMetaData:nil]; break; } } diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index 50158963..f42062d0 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -53,6 +53,8 @@ @property (nonatomic, copy, setter = setAlertViewHandler:) BITCustomAlertViewHandler alertViewHandler; +@property (nonatomic, strong) NSString *crashesDir; + #if HOCKEYSDK_FEATURE_AUTHENTICATOR // Only set via BITAuthenticator @@ -76,7 +78,7 @@ - (BOOL)hasPendingCrashReport; - (BOOL)hasNonApprovedCrashReports; -- (void)persistUserProvidedCrashDescription:(NSString *)userProvidedCrashDescription; +- (void)persistUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData; - (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename; - (BITCrashAttachment *)attachmentForCrashReport:(NSString *)filename; @@ -84,7 +86,6 @@ - (void)invokeDelayedProcessing; - (void)sendNextCrashReport; -- (NSString *)getCrashesDir; - (void)setLastCrashFilename:(NSString *)lastCrashFilename; @end diff --git a/Classes/BITCrashMetaData.h b/Classes/BITCrashMetaData.h new file mode 100644 index 00000000..ac6bb2b6 --- /dev/null +++ b/Classes/BITCrashMetaData.h @@ -0,0 +1,54 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 BITCrashMetaData : NSObject + +/** + * User provided description that should be attached to the crash report as plain text + */ +@property (nonatomic, copy) NSString *userDescription; + +/** + * User name that should be attached to the crash report + */ +@property (nonatomic, copy) NSString *userName; + +/** + * User email that should be attached to the crash report + */ +@property (nonatomic, copy) NSString *userEmail; + +/** + * User ID that should be attached to the crash report + */ +@property (nonatomic, copy) NSString *userID; + +@end diff --git a/Classes/BITCrashMetaData.m b/Classes/BITCrashMetaData.m new file mode 100644 index 00000000..824d149b --- /dev/null +++ b/Classes/BITCrashMetaData.m @@ -0,0 +1,34 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITCrashMetaData.h" + + +@implementation BITCrashMetaData + +@end diff --git a/Classes/HockeySDK.h b/Classes/HockeySDK.h index db47dec9..709333dc 100644 --- a/Classes/HockeySDK.h +++ b/Classes/HockeySDK.h @@ -40,6 +40,7 @@ #import "BITCrashAttachment.h" #import "BITCrashManagerDelegate.h" #import "BITCrashDetails.h" +#import "BITCrashMetaData.h" #endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ #if HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index e2f862b2..73f560e0 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -130,6 +130,8 @@ 1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */; }; 1EB52FD5167B766100C801D5 /* HockeySDK.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E59555F15B6F80E00A03429 /* HockeySDK.strings */; }; 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */; }; + 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */; }; + 1ECA8F52192B6954006B9416 /* BITCrashMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ECA8F50192B6954006B9416 /* BITCrashMetaData.m */; }; 1ED570C718BF878C00AB3350 /* BITCrashAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1ED570C818BF878C00AB3350 /* BITCrashAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ED570C618BF878C00AB3350 /* BITCrashAttachment.m */; }; 1EF95CA6162CB037000AE3AD /* BITFeedbackActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EF95CA4162CB036000AE3AD /* BITFeedbackActivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -291,6 +293,8 @@ 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "feedbackActiviy@2x.png"; sourceTree = ""; }; 1EB52FC3167B73D400C801D5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/HockeySDK.strings; sourceTree = ""; }; 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashDetailsPrivate.h; sourceTree = ""; }; + 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashMetaData.h; sourceTree = ""; }; + 1ECA8F50192B6954006B9416 /* BITCrashMetaData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashMetaData.m; sourceTree = ""; }; 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashAttachment.h; sourceTree = ""; }; 1ED570C618BF878C00AB3350 /* BITCrashAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashAttachment.m; sourceTree = ""; }; 1EDA60CF15C2C1450032D10B /* HockeySDK-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "HockeySDK-Info.plist"; sourceTree = ""; }; @@ -488,11 +492,13 @@ children = ( 1E754E561621FBB70070AB92 /* BITCrashManager.h */, 1E754E571621FBB70070AB92 /* BITCrashManager.m */, + 1EFF03D717F20F8300A5F13C /* BITCrashManagerPrivate.h */, + 1E754E581621FBB70070AB92 /* BITCrashManagerDelegate.h */, 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */, 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */, 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */, - 1EFF03D717F20F8300A5F13C /* BITCrashManagerPrivate.h */, - 1E754E581621FBB70070AB92 /* BITCrashManagerDelegate.h */, + 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */, + 1ECA8F50192B6954006B9416 /* BITCrashMetaData.m */, 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */, 1ED570C618BF878C00AB3350 /* BITCrashAttachment.m */, 1E754E5A1621FBB70070AB92 /* BITCrashReportTextFormatter.h */, @@ -673,6 +679,7 @@ 1E49A46D1612226D00463151 /* BITAppVersionMetaInfo.h in Headers */, 1E49A47C1612226D00463151 /* BITUpdateManagerPrivate.h in Headers */, 1E49A4851612226D00463151 /* BITUpdateViewControllerPrivate.h in Headers */, + 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */, 1E49A4B5161222B900463151 /* BITHockeyBaseManagerPrivate.h in Headers */, E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */, 1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */, @@ -891,6 +898,7 @@ 1E49A4451612223B00463151 /* BITFeedbackListViewCell.m in Sources */, 1E49A44B1612223B00463151 /* BITFeedbackListViewController.m in Sources */, 1E49A4511612223B00463151 /* BITFeedbackManager.m in Sources */, + 1ECA8F52192B6954006B9416 /* BITCrashMetaData.m in Sources */, E4933E8117B66CDA00B11ACC /* BITHTTPOperation.m in Sources */, 1E49A45A1612223B00463151 /* BITFeedbackMessage.m in Sources */, 1ED570C818BF878C00AB3350 /* BITCrashAttachment.m in Sources */, diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 5bb154e4..1f0f2587 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -81,13 +81,16 @@ - (void)testThatItInstantiates { #pragma mark - Persistence tests -- (void)testPersistUserProvidedCrashDescription { +- (void)testPersistUserProvidedMetaData { NSString *tempCrashName = @"tempCrash"; [_sut setLastCrashFilename:tempCrashName]; - [_sut persistUserProvidedCrashDescription:@"Test string"]; + + BITCrashMetaData *metaData = [BITCrashMetaData new]; + [metaData setUserDescription:@"Test string"]; + [_sut persistUserProvidedMetaData:metaData]; NSError *error; - NSString *description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [[_sut getCrashesDir] stringByAppendingPathComponent: tempCrashName]] encoding:NSUTF8StringEncoding error:&error]; + NSString *description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [[_sut crashesDir] stringByAppendingPathComponent: tempCrashName]] encoding:NSUTF8StringEncoding error:&error]; assertThat(description, equalTo(@"Test string")); } @@ -97,7 +100,7 @@ - (void)testPersistAttachment { NSString* type = @"text/plain"; BITCrashAttachment *originalAttachment = [[BITCrashAttachment alloc] initWithFilename:filename attachmentData:data contentType:type]; - NSString *attachmentFilename = [_sut.getCrashesDir stringByAppendingPathComponent:@"testAttachment"]; + NSString *attachmentFilename = [[_sut crashesDir] stringByAppendingPathComponent:@"testAttachment"]; [_sut persistAttachment:originalAttachment withFilename:attachmentFilename]; @@ -155,14 +158,14 @@ - (void)testHandleUserInputDontSend { id delegateMock = mockProtocol(@protocol(BITCrashManagerDelegate)); _sut.delegate = delegateMock; - assertThatBool([_sut handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedCrashDescription:nil], equalToBool(YES)); + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedMetaData:nil], equalToBool(YES)); [verify(delegateMock) crashManagerWillCancelSendingCrashReport:_sut]; } - (void)testHandleUserInputSend { - assertThatBool([_sut handleUserInput:BITCrashManagerUserInputSend withUserProvidedCrashDescription:nil], equalToBool(YES)); + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputSend withUserProvidedMetaData:nil], equalToBool(YES)); } - (void)testHandleUserInputAlwaysSend { @@ -174,7 +177,7 @@ - (void)testHandleUserInputAlwaysSend { [given([mockUserDefaults integerForKey:@"BITCrashManagerStatus"]) willReturn:nil]; //Test if method runs through - assertThatBool([_sut handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedCrashDescription:nil], equalToBool(YES)); + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedMetaData:nil], equalToBool(YES)); //Test if correct CrashManagerStatus is now set [given([mockUserDefaults integerForKey:@"BITCrashManagerStauts"]) willReturnInt:BITCrashManagerStatusAutoSend]; @@ -185,7 +188,7 @@ - (void)testHandleUserInputAlwaysSend { } - (void)testHandleUserInputWithInvalidInput { - assertThatBool([_sut handleUserInput:3 withUserProvidedCrashDescription:nil], equalToBool(NO)); + assertThatBool([_sut handleUserInput:3 withUserProvidedMetaData:nil], equalToBool(NO)); } #pragma mark - Debugger From 7c0495c401787a824e8b27cf75da1caef164fceb Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 20 May 2014 13:32:38 +0200 Subject: [PATCH 134/206] Fix unit tests that broke in last commit --- Support/HockeySDKTests/BITCrashManagerTests.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 1f0f2587..e2768130 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -99,7 +99,7 @@ - (void)testPersistAttachment { NSData *data = [[NSData alloc] initWithBase64Encoding:@"TestData"]; NSString* type = @"text/plain"; - BITCrashAttachment *originalAttachment = [[BITCrashAttachment alloc] initWithFilename:filename attachmentData:data contentType:type]; + BITCrashAttachment *originalAttachment = [[BITCrashAttachment alloc] initWithFilename:filename crashAttachmentData:data contentType:type]; NSString *attachmentFilename = [[_sut crashesDir] stringByAppendingPathComponent:@"testAttachment"]; [_sut persistAttachment:originalAttachment withFilename:attachmentFilename]; @@ -107,7 +107,7 @@ - (void)testPersistAttachment { BITCrashAttachment *decodedAttachment = [_sut attachmentForCrashReport:attachmentFilename]; assertThat(decodedAttachment.filename, equalTo(filename)); - assertThat(decodedAttachment.attachmentData, equalTo(data)); + assertThat(decodedAttachment.crashAttachmentData, equalTo(data)); assertThat(decodedAttachment.contentType, equalTo(type)); } From 0df0347eb946e8370f0463ce36bc6d5463379573 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 21 May 2014 12:54:32 +0200 Subject: [PATCH 135/206] + Improved Loading display in ListView. --- Classes/BITActivityIndicatorButton.h | 15 ++++++ Classes/BITActivityIndicatorButton.m | 58 +++++++++++++++++++++ Classes/BITFeedbackListViewCell.m | 18 +++++-- Support/HockeySDK.xcodeproj/project.pbxproj | 8 +++ 4 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 Classes/BITActivityIndicatorButton.h create mode 100644 Classes/BITActivityIndicatorButton.m diff --git a/Classes/BITActivityIndicatorButton.h b/Classes/BITActivityIndicatorButton.h new file mode 100644 index 00000000..52f0206b --- /dev/null +++ b/Classes/BITActivityIndicatorButton.h @@ -0,0 +1,15 @@ +// +// BITActivityIndicatorButton.h +// HockeySDK +// +// Created by Moritz Haarmann on 21.05.14. +// +// + +#import + +@interface BITActivityIndicatorButton : UIButton + +- (void)setShowsActivityIndicator:(BOOL)showsIndicator; + +@end diff --git a/Classes/BITActivityIndicatorButton.m b/Classes/BITActivityIndicatorButton.m new file mode 100644 index 00000000..4e30bb0f --- /dev/null +++ b/Classes/BITActivityIndicatorButton.m @@ -0,0 +1,58 @@ +// +// BITActivityIndicatorButton.m +// HockeySDK +// +// Created by Moritz Haarmann on 21.05.14. +// +// + +#import "BITActivityIndicatorButton.h" + +@interface BITActivityIndicatorButton() + +@property (nonatomic, strong) UIActivityIndicatorView *indicator; +@property (nonatomic) BOOL indicatorVisible; + +@end + +@implementation BITActivityIndicatorButton + +- (void)setShowsActivityIndicator:(BOOL)showsIndicator { + if (self.indicatorVisible == showsIndicator){ + return; + } + + if (!self.indicator){ + self.indicator = [[UIActivityIndicatorView alloc] initWithFrame:self.bounds]; + [self addSubview:self.indicator]; + [self.indicator setColor:[UIColor blackColor]]; + } + + self.indicatorVisible = showsIndicator; + + if (showsIndicator){ + [self.indicator startAnimating]; + self.indicator.alpha = 1; + self.layer.borderWidth = 1; + self.layer.borderColor = [UIColor lightGrayColor].CGColor; + self.layer.cornerRadius = 5; + self.imageView.image = nil; + } else { + [self.indicator stopAnimating]; + self.layer.cornerRadius = 0; + self.indicator.alpha = 0; + self.layer.borderWidth = 0; + + } + +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + [self.indicator setFrame:self.bounds]; + +} + + +@end diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 6e49f2f1..912a3770 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -26,10 +26,11 @@ * OTHER DEALINGS IN THE SOFTWARE. */ +#import "HockeySDKPrivate.h" #import "BITFeedbackListViewCell.h" -#import "HockeySDKPrivate.h" #import "BITFeedbackMessageAttachment.h" +#import "BITActivityIndicatorButton.h" #define BACKGROUNDCOLOR_DEFAULT BIT_RGBCOLOR(245, 245, 245) #define BACKGROUNDCOLOR_ALTERNATE BIT_RGBCOLOR(235, 235, 235) @@ -196,13 +197,22 @@ - (void)setAttachments:(NSArray *)attachments { [self.attachmentViews removeAllObjects]; for (BITFeedbackMessageAttachment *attachment in attachments){ - UIButton *imageView = [UIButton buttonWithType:UIButtonTypeCustom]; - [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; + BITActivityIndicatorButton *imageView = [BITActivityIndicatorButton buttonWithType:UIButtonTypeCustom]; + + if (attachment.localURL){ + [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; + [imageView setShowsActivityIndicator:NO]; + } else { + [imageView setImage:nil forState:UIControlStateNormal]; + [imageView setShowsActivityIndicator:YES]; + } [imageView setContentMode:UIViewContentModeScaleAspectFit]; [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; [self.attachmentViews addObject:imageView]; [self addSubview:imageView]; + + } } @@ -276,7 +286,7 @@ - (void)layoutSubviews { CGFloat attachmentsPerRow = floorf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); - for ( UIButton *imageButton in self.attachmentViews){ + for ( BITActivityIndicatorButton *imageButton in self.attachmentViews){ imageButton.contentMode = UIViewContentModeScaleAspectFit; imageButton.imageView.contentMode = UIViewContentModeScaleAspectFill; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 7094783b..802fe2d4 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -143,6 +143,8 @@ 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */; }; 9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */; }; 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */; }; + 9774BCFF192CB20A00085EB5 /* BITActivityIndicatorButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 9774BCFD192CB20A00085EB5 /* BITActivityIndicatorButton.h */; }; + 9774BD00192CB20A00085EB5 /* BITActivityIndicatorButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 9774BCFE192CB20A00085EB5 /* BITActivityIndicatorButton.m */; }; 9782023218F81BFC00A98D8B /* Arrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022818F81BFC00A98D8B /* Arrow.png */; }; 9782023318F81BFC00A98D8B /* Arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022918F81BFC00A98D8B /* Arrow@2x.png */; }; 9782023418F81BFC00A98D8B /* Blur.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022A18F81BFC00A98D8B /* Blur.png */; }; @@ -329,6 +331,8 @@ 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotation.h; sourceTree = ""; }; 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotation.m; sourceTree = ""; }; + 9774BCFD192CB20A00085EB5 /* BITActivityIndicatorButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITActivityIndicatorButton.h; sourceTree = ""; }; + 9774BCFE192CB20A00085EB5 /* BITActivityIndicatorButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITActivityIndicatorButton.m; sourceTree = ""; }; 9782022818F81BFC00A98D8B /* Arrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Arrow.png; sourceTree = ""; }; 9782022918F81BFC00A98D8B /* Arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Arrow@2x.png"; sourceTree = ""; }; 9782022A18F81BFC00A98D8B /* Blur.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Blur.png; sourceTree = ""; }; @@ -531,6 +535,8 @@ 1E49A4341612223B00463151 /* BITFeedbackManager.m */, E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */, 1E49A4351612223B00463151 /* BITFeedbackManagerPrivate.h */, + 9774BCFD192CB20A00085EB5 /* BITActivityIndicatorButton.h */, + 9774BCFE192CB20A00085EB5 /* BITActivityIndicatorButton.m */, ); name = Feedback; sourceTree = ""; @@ -758,6 +764,7 @@ 1E49A47C1612226D00463151 /* BITUpdateManagerPrivate.h in Headers */, 1E49A4851612226D00463151 /* BITUpdateViewControllerPrivate.h in Headers */, 1E49A4B5161222B900463151 /* BITHockeyBaseManagerPrivate.h in Headers */, + 9774BCFF192CB20A00085EB5 /* BITActivityIndicatorButton.h in Headers */, E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */, 1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */, 973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */, @@ -998,6 +1005,7 @@ 1E49A4701612226D00463151 /* BITAppVersionMetaInfo.m in Sources */, 1E49A4761612226D00463151 /* BITUpdateManager.m in Sources */, 1E49A4C1161222B900463151 /* BITHockeyHelper.m in Sources */, + 9774BD00192CB20A00085EB5 /* BITActivityIndicatorButton.m in Sources */, 1E49A4821612226D00463151 /* BITUpdateViewController.m in Sources */, E4B4DB7E17B435550099C67F /* BITAuthenticationViewController.m in Sources */, 1E49A4B2161222B900463151 /* BITHockeyBaseManager.m in Sources */, From ef6d5224da5b7f942f2be17a13c1fd63eb46fdd2 Mon Sep 17 00:00:00 2001 From: moritz haarmann Date: Wed, 21 May 2014 15:10:21 +0200 Subject: [PATCH 136/206] + Some corrections regarding the display and loading of attachments. - Attachments are no longer displayed in a cell if the system indicates that they cannot be displayed. - Fixed a bug that prevented devices running iOS6 from displaying attachments in the Overview List. --- Classes/BITFeedbackListViewCell.m | 10 +++++++--- Classes/BITFeedbackListViewController.m | 18 +++++++++--------- Classes/BITFeedbackMessage.h | 6 ++++++ Classes/BITFeedbackMessage.m | 13 +++++++++++++ 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 912a3770..c0526376 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -210,7 +210,7 @@ - (void)setAttachments:(NSArray *)attachments { [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; [self.attachmentViews addObject:imageView]; - [self addSubview:imageView]; + //[self addSubview:imageView]; } @@ -289,7 +289,7 @@ - (void)layoutSubviews { for ( BITActivityIndicatorButton *imageButton in self.attachmentViews){ imageButton.contentMode = UIViewContentModeScaleAspectFit; imageButton.imageView.contentMode = UIViewContentModeScaleAspectFill; - + if ( !_message.userMessage){ imageButton.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) , floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } else { @@ -297,7 +297,11 @@ - (void)layoutSubviews { } if (!imageButton.superview){ - [self addSubview:imageButton]; + if (self.accessoryBackgroundView.superview){ + [self insertSubview:imageButton aboveSubview:self.accessoryBackgroundView]; + } else { + [self addSubview:imageButton]; + } } i++; diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index a384bc0b..c7595fa7 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -637,7 +637,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell.labelText.delegate = self; cell.labelText.userInteractionEnabled = YES; cell.delegate = self; - [cell setAttachments:message.attachments]; + [cell setAttachments:message.previewableAttachments]; for (BITFeedbackMessageAttachment *attachment in message.attachments){ if (attachment.needsLoadingFromURL && !attachment.isLoading){ @@ -812,12 +812,7 @@ - (void)refreshPreviewItems { for (int i = 0; i) previewController: (QLPreviewController *) controller previewItemAtIndex: (NSInteger) index { if (index>=0){ + __weak QLPreviewController* blockController = controller; BITFeedbackMessageAttachment *attachment = self.cachedPreviewItems[index]; if (attachment.needsLoadingFromURL && !attachment.isLoading){ attachment.isLoading = YES; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]]; [NSURLConnection sendAsynchronousRequest:request queue:self.thumbnailQueue completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) { + attachment.isLoading = NO; if (responseData.length){ [attachment replaceData:responseData]; - [controller reloadData]; - [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; + [blockController reloadData]; + + [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; + } else { + [blockController reloadData]; } }]; return attachment; diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index 44ff0e0d..4587449d 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -83,5 +83,11 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { -(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object; +/** + * Returns an array of attachment objects that may be previewed on this device. + */ +- (NSArray *)previewableAttachments; + + @end diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index 6d2ab49c..42da4e60 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -90,6 +90,19 @@ -(void)deleteContents { [attachment deleteContents]; } } + +- (NSArray *)previewableAttachments { + NSMutableArray *returnArray = [NSMutableArray new]; + + for (BITFeedbackMessageAttachment *attachment in self.attachments){ + if ([QLPreviewController canPreviewItem:attachment ]){ + [returnArray addObject:attachment]; + } + } + + return returnArray; +} + -(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object{ if (!self.attachments){ self.attachments = [NSArray array]; From eb43a12bb5b7c52484a9b5e1cdb934c9a9b7a5de Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 22 May 2014 16:00:42 +0200 Subject: [PATCH 137/206] Show warning in the console if `delegate` is set after calling `startManager` and improve documentation for this --- Classes/BITHockeyManager.h | 60 ++++++++++++++++++++++++++++---------- Classes/BITHockeyManager.m | 6 ++++ 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/Classes/BITHockeyManager.h b/Classes/BITHockeyManager.h index 585a8f2a..3bf72fcd 100644 --- a/Classes/BITHockeyManager.h +++ b/Classes/BITHockeyManager.h @@ -66,7 +66,7 @@ @warning The SDK is **NOT** thread safe and has to be set up on the main thread! - @warning You should **NOT** change any module configuration after calling `startManager`! + @warning Most properties of all components require to be set **BEFORE** calling`startManager`! */ @@ -182,14 +182,19 @@ /** - * Set the delegate - * - * Defines the class that implements the optional protocol `BITHockeyManagerDelegate`. - * - * @see BITHockeyManagerDelegate - * @see BITCrashManagerDelegate - * @see BITUpdateManagerDelegate - * @see BITFeedbackManagerDelegate + Set the delegate + + Defines the class that implements the optional protocol `BITHockeyManagerDelegate`. + + The delegate will automatically be propagated to all components. There is no need to set the delegate + for each component individually. + + @warning This property needs to be set before calling `startManager` + + @see BITHockeyManagerDelegate + @see BITCrashManagerDelegate + @see BITUpdateManagerDelegate + @see BITFeedbackManagerDelegate */ @property (nonatomic, weak) id delegate; @@ -199,6 +204,8 @@ By default this is set to the HockeyApp servers and there rarely should be a need to modify that. + + @warning This property needs to be set before calling `startManager` */ @property (nonatomic, strong) NSString *serverURL; @@ -222,7 +229,10 @@ If this flag is enabled, then crash reporting is disabled and no crashes will be send. - Please note that the Crash Manager will be initialized anyway! + Please note that the Crash Manager instance will be initialized anyway, but crash report + handling (signal and uncaught exception handlers) will **not** be registered. + + @warning This property needs to be set before calling `startManager` *Default*: _NO_ @see crashManager @@ -249,8 +259,10 @@ If this flag is enabled, then checking for updates and submitting beta usage analytics will be turned off! - Please note that the Update Manager will be initialized anyway! + Please note that the Update Manager instance will be initialized anyway! + @warning This property needs to be set before calling `startManager` + *Default*: _NO_ @see updateManager */ @@ -276,8 +288,10 @@ If this flag is enabled, then checking for updates when the app runs from the app store will be turned on! - Please note that the Store Update Manager will be initialized anyway! - + Please note that the Store Update Manager instance will be initialized anyway! + + @warning This property needs to be set before calling `startManager` + *Default*: _NO_ @see storeUpdateManager */ @@ -302,8 +316,10 @@ If this flag is enabled, then letting the user give feedback and get responses will be turned off! - Please note that the Feedback Manager will be initialized anyway! - + Please note that the Feedback Manager instance will be initialized anyway! + + @warning This property needs to be set before calling `startManager` + *Default*: _NO_ @see feedbackManager */ @@ -359,6 +375,8 @@ This is ignored if the app is running in the App Store and reverts to the default value in that case. + @warning This property needs to be set before calling `startManager` + *Default*: _NO_ */ @property (nonatomic, assign, getter=isDebugLogEnabled) BOOL debugLogEnabled; @@ -399,6 +417,12 @@ want to define specific data for each component, use the delegate instead which does overwrite the values set by this property. + @warning When returning a non nil value, crash reports are not anonymous any more + and the crash alerts will not show the word "anonymous"! + + @warning This property needs to be set before calling `startManager` to be considered + for being added to crash reports as meta data. + @see userName @see userEmail @see `[BITHockeyManagerDelegate userIDForHockeyManager:componentManager:]` @@ -421,6 +445,9 @@ @warning When returning a non nil value, crash reports are not anonymous any more and the crash alerts will not show the word "anonymous"! + @warning This property needs to be set before calling `startManager` to be considered + for being added to crash reports as meta data. + @see userID @see userEmail @see `[BITHockeyManagerDelegate userNameForHockeyManager:componentManager:]` @@ -442,6 +469,9 @@ @warning When returning a non nil value, crash reports are not anonymous any more and the crash alerts will not show the word "anonymous"! + + @warning This property needs to be set before calling `startManager` to be considered + for being added to crash reports as meta data. @see userID @see userName diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index baa1a7e8..34c8292c 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -336,6 +336,12 @@ - (void)setServerURL:(NSString *)aServerURL { - (void)setDelegate:(id)delegate { + if (![self isAppStoreEnvironment]) { + if (!_startManagerIsInvoked) { + NSLog(@"[HockeySDK] ERROR: The `delegate` property has to be set before calling [[BITHockeyManager sharedHockeyManager] startManager] !"); + } + } + if (_delegate != delegate) { _delegate = delegate; From b48bb8fb12a920f62479cb3521f5a89590dde374 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 25 May 2014 18:55:24 +0200 Subject: [PATCH 138/206] Cleanup and bugfixes - Remove not used code - Fixed some code style - use a subdirectory in the SDK caches folder, instead of creating another folder directly under caches - Height of feedback cells only considers attachments that actually can be previewed. - If an attachment has no source URL and no local URL, don't show it, not even a loading placeholder, don't consider to reserve space in the cell for it - refresh attachment cache when new data is loaded or one or all message(s) with attachments have been deleted locally - `feedbackObservationMode` is a property instead of a method --- Classes/BITActivityIndicatorButton.h | 32 ++++++-- Classes/BITActivityIndicatorButton.m | 32 ++++++-- Classes/BITCrashManager.m | 14 ++-- Classes/BITFeedbackListViewCell.h | 2 + Classes/BITFeedbackListViewCell.m | 47 ++++++----- Classes/BITFeedbackListViewController.m | 80 +++++++++++------- Classes/BITFeedbackManager.h | 47 +++++++---- Classes/BITFeedbackManager.m | 94 ++++++++------------- Classes/BITFeedbackMessage.h | 20 +++-- Classes/BITFeedbackMessage.m | 6 +- Classes/BITFeedbackMessageAttachment.h | 1 - Classes/BITFeedbackMessageAttachment.m | 104 ++++++++++++++---------- 12 files changed, 270 insertions(+), 209 deletions(-) diff --git a/Classes/BITActivityIndicatorButton.h b/Classes/BITActivityIndicatorButton.h index 52f0206b..2c633222 100644 --- a/Classes/BITActivityIndicatorButton.h +++ b/Classes/BITActivityIndicatorButton.h @@ -1,10 +1,28 @@ -// -// BITActivityIndicatorButton.h -// HockeySDK -// -// Created by Moritz Haarmann on 21.05.14. -// -// +/* + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 diff --git a/Classes/BITActivityIndicatorButton.m b/Classes/BITActivityIndicatorButton.m index 4e30bb0f..00966186 100644 --- a/Classes/BITActivityIndicatorButton.m +++ b/Classes/BITActivityIndicatorButton.m @@ -1,10 +1,28 @@ -// -// BITActivityIndicatorButton.m -// HockeySDK -// -// Created by Moritz Haarmann on 21.05.14. -// -// +/* + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITActivityIndicatorButton.h" diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 18b3aaf2..ba8b2009 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -602,20 +602,22 @@ - (BOOL)hasPendingCrashReport { if (_crashManagerStatus == BITCrashManagerStatusDisabled) return NO; if ([self.fileManager fileExistsAtPath:_crashesDir]) { - NSString *file = nil; NSError *error = NULL; - NSDirectoryEnumerator *dirEnum = [self.fileManager enumeratorAtPath: _crashesDir]; + NSArray *dirArray = [self.fileManager contentsOfDirectoryAtPath:_crashesDir error:&error]; - while ((file = [dirEnum nextObject])) { - NSDictionary *fileAttributes = [self.fileManager attributesOfItemAtPath:[_crashesDir stringByAppendingPathComponent:file] error:&error]; - if ([[fileAttributes objectForKey:NSFileSize] intValue] > 0 && + for (NSString *file in dirArray) { + NSString *filePath = [_crashesDir stringByAppendingPathComponent:file]; + + NSDictionary *fileAttributes = [self.fileManager attributesOfItemAtPath:filePath error:&error]; + if ([[fileAttributes objectForKey:NSFileType] isEqualToString:NSFileTypeRegular] && + [[fileAttributes objectForKey:NSFileSize] intValue] > 0 && ![file hasSuffix:@".DS_Store"] && ![file hasSuffix:@".analyzer"] && ![file hasSuffix:@".plist"] && ![file hasSuffix:@".data"] && ![file hasSuffix:@".meta"]) { - [_crashFiles addObject:[_crashesDir stringByAppendingPathComponent: file]]; + [_crashFiles addObject:filePath]; } } } diff --git a/Classes/BITFeedbackListViewCell.h b/Classes/BITFeedbackListViewCell.h index 8ce9c3fc..f146b8ac 100644 --- a/Classes/BITFeedbackListViewCell.h +++ b/Classes/BITFeedbackListViewCell.h @@ -39,6 +39,7 @@ @end + /** * Cell style depending on the iOS version */ @@ -67,6 +68,7 @@ typedef NS_ENUM(NSUInteger, BITFeedbackListViewCellBackgroundStyle) { BITFeedbackListViewCellBackgroundStyleAlternate = 1 }; + @interface BITFeedbackListViewCell : UITableViewCell @property (nonatomic, strong) BITFeedbackMessage *message; diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index c0526376..367721ac 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -152,7 +152,7 @@ + (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth CGFloat attachmentsPerRow = floorf(width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); - CGFloat calculatedHeight = baseHeight + (FRAME_TOP_BORDER + ATTACHMENT_SIZE) * ceil(message.attachments.count/attachmentsPerRow); + CGFloat calculatedHeight = baseHeight + (FRAME_TOP_BORDER + ATTACHMENT_SIZE) * ceil([message previewableAttachments].count / attachmentsPerRow); return ceil(calculatedHeight); } @@ -197,22 +197,21 @@ - (void)setAttachments:(NSArray *)attachments { [self.attachmentViews removeAllObjects]; for (BITFeedbackMessageAttachment *attachment in attachments){ - BITActivityIndicatorButton *imageView = [BITActivityIndicatorButton buttonWithType:UIButtonTypeCustom]; - - if (attachment.localURL){ - [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; - [imageView setShowsActivityIndicator:NO]; - } else { - [imageView setImage:nil forState:UIControlStateNormal]; - [imageView setShowsActivityIndicator:YES]; - } - [imageView setContentMode:UIViewContentModeScaleAspectFit]; - [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; - - [self.attachmentViews addObject:imageView]; - //[self addSubview:imageView]; - + if (attachment.localURL || attachment.sourceURL) { + BITActivityIndicatorButton *imageView = [BITActivityIndicatorButton buttonWithType:UIButtonTypeCustom]; + if (attachment.localURL){ + [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; + [imageView setShowsActivityIndicator:NO]; + } else { + [imageView setImage:nil forState:UIControlStateNormal]; + [imageView setShowsActivityIndicator:YES]; + } + [imageView setContentMode:UIViewContentModeScaleAspectFit]; + [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + + [self.attachmentViews addObject:imageView]; + } } } @@ -286,18 +285,18 @@ - (void)layoutSubviews { CGFloat attachmentsPerRow = floorf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); - for ( BITActivityIndicatorButton *imageButton in self.attachmentViews){ + for (BITActivityIndicatorButton *imageButton in self.attachmentViews) { imageButton.contentMode = UIViewContentModeScaleAspectFit; imageButton.imageView.contentMode = UIViewContentModeScaleAspectFill; - - if ( !_message.userMessage){ + + if (!_message.userMessage) { imageButton.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) , floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } else { imageButton.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) ), floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); } - if (!imageButton.superview){ - if (self.accessoryBackgroundView.superview){ + if (!imageButton.superview) { + if (self.accessoryBackgroundView.superview) { [self insertSubview:imageButton aboveSubview:self.accessoryBackgroundView]; } else { [self addSubview:imageButton]; @@ -311,10 +310,10 @@ - (void)layoutSubviews { } - (void)imageButtonPressed:(id)sender { - if ([self.delegate respondsToSelector:@selector(listCell:didSelectAttachment:)]){ + if ([self.delegate respondsToSelector:@selector(listCell:didSelectAttachment:)]) { NSInteger index = [self.attachmentViews indexOfObject:sender]; - if (index != NSNotFound){ - BITFeedbackMessageAttachment *attachment = self.message.attachments[index]; + if (index != NSNotFound && [self.message previewableAttachments].count > index) { + BITFeedbackMessageAttachment *attachment = [self.message previewableAttachments][index]; [self.delegate listCell:self didSelectAttachment:attachment]; } } diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index c7595fa7..9397bf85 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -105,7 +105,7 @@ - (instancetype)initWithStyle:(UITableViewStyle)style { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesLoadingStarted object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesLoadingFinished object:nil]; - + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showDelayedUserDataViewController) object:nil]; } @@ -114,12 +114,12 @@ - (void)dealloc { - (void)viewDidLoad { [super viewDidLoad]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startLoadingIndicator) name:BITHockeyFeedbackMessagesLoadingStarted object:nil]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateList) name:BITHockeyFeedbackMessagesLoadingFinished @@ -135,13 +135,13 @@ - (void)viewDidLoad { [self.tableView setBackgroundColor:[UIColor colorWithRed:0.82 green:0.84 blue:0.84 alpha:1]]; [self.tableView setSeparatorColor:[UIColor colorWithRed:0.79 green:0.79 blue:0.79 alpha:1]]; } else { -// [self.tableView setBackgroundColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1]]; + // [self.tableView setBackgroundColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1]]; } if ([self.manager isPreiOS7Environment]) { self.view.backgroundColor = DEFAULT_BACKGROUNDCOLOR; } else { -// self.view.backgroundColor = DEFAULT_BACKGROUNDCOLOR_OS7; + // self.view.backgroundColor = DEFAULT_BACKGROUNDCOLOR_OS7; } id refreshClass = NSClassFromString(@"UIRefreshControl"); @@ -150,9 +150,9 @@ - (void)viewDidLoad { [self.refreshControl addTarget:self action:@selector(reloadList) forControlEvents:UIControlEventValueChanged]; } else { self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh - target:self - action:@selector(reloadList)]; - } + target:self + action:@selector(reloadList)]; + } } - (void)startLoadingIndicator { @@ -191,7 +191,9 @@ - (void)updateList { CGSize contentSize = self.tableView.contentSize; CGPoint contentOffset = self.tableView.contentOffset; + [self refreshPreviewItems]; [self.tableView reloadData]; + if (contentSize.height > 0 && self.tableView.contentSize.height > self.tableView.frame.size.height && self.tableView.contentSize.height > contentSize.height && @@ -199,7 +201,7 @@ - (void)updateList { [self.tableView setContentOffset:CGPointMake(contentOffset.x, self.tableView.contentSize.height - contentSize.height + contentOffset.y) animated:NO]; [self stopLoadingIndicator]; - + [self.tableView flashScrollIndicators]; } @@ -231,7 +233,7 @@ - (void)viewDidAppear:(BOOL)animated { } else { [self.tableView reloadData]; } - + [super viewDidAppear:animated]; } @@ -272,6 +274,8 @@ - (void)newFeedbackAction:(id)sender { - (void)deleteAllMessages { [_manager deleteAllMessages]; + [self refreshPreviewItems]; + [self.tableView reloadData]; } @@ -289,9 +293,9 @@ - (void)deleteAllMessagesAction:(id)sender { } else { UIAlertView *deleteAction = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackListButonDeleteAllMessages") message:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllTitle") - delegate:self - cancelButtonTitle:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllCancel") - otherButtonTitles:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllDelete"), nil]; + delegate:self + cancelButtonTitle:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllCancel") + otherButtonTitles:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllDelete"), nil]; [deleteAction setTag:0]; [deleteAction show]; @@ -495,7 +499,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; - + cell.textLabel.font = [UIFont systemFontOfSize:14]; cell.textLabel.numberOfLines = 0; cell.accessoryType = UITableViewCellAccessoryNone; @@ -644,17 +648,18 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N attachment.isLoading = YES; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]]; [NSURLConnection sendAsynchronousRequest:request queue:self.thumbnailQueue completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) { - if (responseData.length){ + if (responseData.length) { [attachment replaceData:responseData]; dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; }); + [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; } }]; } } - + if ( [self.manager isPreiOS7Environment] || (![self.manager isPreiOS7Environment] && indexPath.row != 0) @@ -679,12 +684,19 @@ - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *) - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { + BITFeedbackMessage *message = [self.manager messageAtIndex:indexPath.row]; + BOOL messageHasAttachments = ([message attachments].count > 0); + if ([_manager deleteMessageAtIndex:indexPath.row]) { if ([_manager numberOfMessages] > 0) { [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } else { [tableView reloadData]; } + + if (messageHasAttachments) { + [self refreshPreviewItems]; + } } } } @@ -779,7 +791,7 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn if (buttonIndex == actionSheet.cancelButtonIndex) { return; } - + if ([actionSheet tag] == 0) { if (buttonIndex == [actionSheet destructiveButtonIndex]) { [self deleteAllMessages]; @@ -794,60 +806,66 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn } } + #pragma mark - ListViewCellDelegate - (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment { - QLPreviewController *previewController = [[QLPreviewController alloc] init]; - previewController.dataSource = self; - - [self presentViewController:previewController animated:YES completion:nil]; - if (self.cachedPreviewItems.count > [self.cachedPreviewItems indexOfObject:attachment]){ + QLPreviewController *previewController = [[QLPreviewController alloc] init]; + previewController.dataSource = self; + + [self presentViewController:previewController animated:YES completion:nil]; + + if (self.cachedPreviewItems.count > [self.cachedPreviewItems indexOfObject:attachment]) { [previewController setCurrentPreviewItemIndex:[self.cachedPreviewItems indexOfObject:attachment]]; } - } + - (void)refreshPreviewItems { self.cachedPreviewItems = nil; NSMutableArray *collectedAttachments = [NSMutableArray new]; - for (int i = 0; i) previewController: (QLPreviewController *) controller previewItemAtIndex: (NSInteger) index { - if (index>=0){ +- (id )previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index { + if (index >= 0) { __weak QLPreviewController* blockController = controller; BITFeedbackMessageAttachment *attachment = self.cachedPreviewItems[index]; - if (attachment.needsLoadingFromURL && !attachment.isLoading){ + + if (attachment.needsLoadingFromURL && !attachment.isLoading) { attachment.isLoading = YES; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]]; [NSURLConnection sendAsynchronousRequest:request queue:self.thumbnailQueue completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) { attachment.isLoading = NO; - if (responseData.length){ + if (responseData.length) { [attachment replaceData:responseData]; [blockController reloadData]; - [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; + [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; } else { [blockController reloadData]; } }]; + return attachment; } else { return self.cachedPreviewItems[index]; } } + return nil; } diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 01ad9a46..435a1f2e 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -61,20 +61,19 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { }; /** - * Available modes for collecting automated feedback. + * Available modes for opening the feedback compose interface with a screenshot attached */ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { /** - * No automatic feedback gathering. + * No SDK provided trigger is active. */ BITFeedbackObservationNone = 0, /** - * Feedback compose form will open once a screenshot is taken. + * Triggeres when the user takes a screenshot. Requires iOS 7 or later! */ BITFeedbackObservationModeOnScreenshot = 1, /** - * Feedback compose will open with a generated screenshot if the screen is tapped - * three fingers for three seconds. + * Triggers when the user tapps with three fingers for three seconds on the screen. */ BITFeedbackObservationModeThreeFingerTap = 2 }; @@ -212,6 +211,26 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { @property (nonatomic, readwrite) BOOL showAlertOnIncomingMessages; +/** + Define the trigger that opens the feedback composer and attaches a screenshot + + The following modes are available: + + - `BITFeedbackObservationNone`: No SDK based trigger is active. You can implement your + own trigger and then call `[[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot];` to handle your custom events + that should trigger this. + - `BITFeedbackObservationModeOnScreenshot`: Triggeres when the user takes a screenshot. + Requires iOS 7 or later! + - `BITFeedbackObservationModeThreeFingerTap`: Triggers when the user tapps with three fingers + for three seconds on the screen. + + Default is `BITFeedbackObservationNone` + + @see showFeedbackComposeViewWithGeneratedScreenshot + */ +@property (nonatomic, readwrite) BITFeedbackObservationMode feedbackObservationMode; + + ///----------------------------------------------------------------------------- /// @name User Interface ///----------------------------------------------------------------------------- @@ -269,8 +288,13 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { - (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items; /** - Present the modal feedback compose message user interface with a screenshot that is taken - at the time of calling this method. + Presents a modal feedback compose interface with a screenshot attached which is taken at the time of calling this method. + + This should be used when your own trigger fires. The following code should be used: + + [[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot]; + + @see feedbackObservationMode */ - (void)showFeedbackComposeViewWithGeneratedScreenshot; @@ -295,14 +319,5 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { */ - (BITFeedbackComposeViewController *)feedbackComposeViewController; -/** - Set the so-called feedback observation mode. Depending on the chosen mode, - the feedback manager will automatically launch once the event has been detected. - - You can choose from the modes in BITFeedbackObservationMode. The default mode is - BITFeedbackObservationNone. - */ -- (void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode; - @end diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 5c89cf1c..056266fc 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -740,7 +740,7 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { message.id = [(NSDictionary *)objMessage objectForKey:@"id"]; message.status = BITFeedbackMessageStatusUnread; - for (NSDictionary *attachmentData in objMessage[@"attachments"]){ + for (NSDictionary *attachmentData in objMessage[@"attachments"]) { BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; newAttachment.originalFilename = attachmentData[@"file_name"]; newAttachment.id = attachmentData[@"id"]; @@ -805,45 +805,11 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { [self markSendInProgressMessagesAsPending]; } - // we'll load the images on demand. - //[self synchronizeMissingAttachments]; - [self saveMessages]; return; } -/** - Load all attachments without any local data to have them available. - */ --(BOOL)synchronizeMissingAttachments { - // Extract all Attachments. - NSMutableArray *allAttachments = [NSMutableArray new]; - for (int i = 0; i < [self numberOfMessages]; i++){ - BITFeedbackMessage *message = [self messageAtIndex:i]; - for (BITFeedbackMessageAttachment *attachment in message.attachments){ - if (attachment.needsLoadingFromURL){ - [allAttachments addObject:attachment]; - } - } - } - - for (BITFeedbackMessageAttachment *attachment in allAttachments){ - // we will just update the objects here and perform a save after each successful load operation. - - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]]; - __weak BITFeedbackManager *weakSelf = self; - [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) { - if (responseData.length){ - [attachment replaceData:responseData]; - [weakSelf saveMessages]; - - } - }]; - - } - return NO; -} - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BITFeedbackMessage *)message completionHandler:(void (^)(NSError *err))completionHandler { NSString *boundary = @"----FOO"; @@ -1092,37 +1058,41 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) #pragma mark - Observation Handling --(void)setFeedbackObservationMode:(BITFeedbackObservationMode)mode { - if (mode == BITFeedbackObservationModeOnScreenshot){ - if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; - } else { - BITHockeyLog("Not enabling Screenshot notifications: iOS6.1 and lower is not supported."); - } +- (void)setFeedbackObservationMode:(BITFeedbackObservationMode)feedbackObservationMode { + if (feedbackObservationMode != _feedbackObservationMode) { + _feedbackObservationMode = feedbackObservationMode; - self.screenshotNotificationEnabled = YES; - - if (self.tapRecognizer){ - [[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer]; - self.tapRecognizer = nil; - } - } - - if (mode == BITFeedbackObservationModeThreeFingerTap){ - if (!self.tapRecognizer){ - self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(screenshotTripleTap:)]; - self.tapRecognizer.numberOfTouchesRequired = 3; - self.tapRecognizer.delegate = self; + if (feedbackObservationMode == BITFeedbackObservationModeOnScreenshot){ + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + } else { + BITHockeyLog("WARNING: BITFeedbackObservationModeOnScreenshot requires iOS 7 or later."); + } - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication].keyWindow addGestureRecognizer:self.tapRecognizer]; - }); + self.screenshotNotificationEnabled = YES; + + if (self.tapRecognizer){ + [[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer]; + self.tapRecognizer = nil; + } } - if (self.screenshotNotificationEnabled){ - if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; - self.screenshotNotificationEnabled = NO; + if (feedbackObservationMode == BITFeedbackObservationModeThreeFingerTap){ + if (!self.tapRecognizer){ + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(screenshotTripleTap:)]; + self.tapRecognizer.numberOfTouchesRequired = 3; + self.tapRecognizer.delegate = self; + + dispatch_async(dispatch_get_main_queue(), ^{ + [[UIApplication sharedApplication].keyWindow addGestureRecognizer:self.tapRecognizer]; + }); + } + + if (self.screenshotNotificationEnabled){ + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + self.screenshotNotificationEnabled = NO; + } } } } diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index 4587449d..908e4fc8 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -76,18 +76,24 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { @property (nonatomic) BOOL userMessage; /** - * This method must be called before a feedback message is deleted. It handles the - * deletion of any data stored on the device in association with the feedback message. + Delete local cached attachment data + + @warning This method must be called before a feedback message is deleted. */ --(void)deleteContents; +- (void)deleteContents; +/** + Add an attachment to a message + + @param object BITFeedbackMessageAttachment instance representing the attachment that should be added + */ -(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object; /** - * Returns an array of attachment objects that may be previewed on this device. - */ + Return the attachments that can be viewed + + @return NSArray containing the attachment objects that can be previewed + */ - (NSArray *)previewableAttachments; - - @end diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index 42da4e60..7f8d60de 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -35,7 +35,7 @@ @implementation BITFeedbackMessage #pragma mark - NSObject -- (id) init { +- (instancetype) init { if ((self = [super init])) { _text = nil; _userID = nil; @@ -67,8 +67,8 @@ - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:self.token forKey:@"token"]; } -- (id)initWithCoder:(NSCoder *)decoder { - if ((self = [super init])) { +- (instancetype)initWithCoder:(NSCoder *)decoder { + if ((self = [self init])) { self.text = [decoder decodeObjectForKey:@"text"]; self.userID = [decoder decodeObjectForKey:@"userID"]; self.name = [decoder decodeObjectForKey:@"name"]; diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index 96adde85..dbb398b1 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -2,7 +2,6 @@ * Author: Moritz Haarmann * * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. - * Copyright (c) 2011 Andreas Linde & Kent Sutherland. * All rights reserved. * * Permission is hereby granted, free of charge, to any person diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index ac855101..1315e702 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -32,7 +32,7 @@ #import "HockeySDKPrivate.h" #import -#define kCacheFolderName @"hockey_attachments" +#define kCacheFolderName @"attachments" @interface BITFeedbackMessageAttachment() @@ -43,47 +43,63 @@ @interface BITFeedbackMessageAttachment() @end -@implementation BITFeedbackMessageAttachment +@implementation BITFeedbackMessageAttachment { + NSString *_tempFilename; + + NSString *_cachePath; + + NSFileManager *_fm; +} + + (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType { static NSDateFormatter *formatter; - if(!formatter){ + if(!formatter) { formatter = [NSDateFormatter new]; formatter.dateStyle = NSDateFormatterShortStyle; formatter.timeStyle = NSDateFormatterShortStyle; - } BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; newAttachment.contentType = contentType; newAttachment.data = data; newAttachment.originalFilename = [NSString stringWithFormat:@"Attachment: %@", [formatter stringFromDate:[NSDate date]]]; + return newAttachment; } --(id)init { - self = [super init]; - if (self){ +- (instancetype)init { + if ((self = [super init])) { self.isLoading = NO; self.thumbnailRepresentations = [NSMutableDictionary new]; + + _fm = [[NSFileManager alloc] init]; + _cachePath = [bit_settingsDir() stringByAppendingPathComponent:kCacheFolderName]; + + BOOL isDirectory; + + if (![_fm fileExistsAtPath:_cachePath isDirectory:&isDirectory]){ + [_fm createDirectoryAtPath:_cachePath withIntermediateDirectories:YES attributes:nil error:nil]; + } + } return self; } --(void)setData:(NSData *)data { +- (void)setData:(NSData *)data { self->_internalData = data; self.filename = [self possibleFilename]; [self->_internalData writeToFile:self.filename atomically:NO]; } --(NSData *)data { - if (!self->_internalData && self.filename){ +- (NSData *)data { + if (!self->_internalData && self.filename) { self.internalData = [NSData dataWithContentsOfFile:self.filename]; } - if (self.internalData){ + if (self.internalData) { return self.internalData; } @@ -95,8 +111,8 @@ - (void)replaceData:(NSData *)data { self.thumbnailRepresentations = [NSMutableDictionary new]; } --(BOOL)needsLoadingFromURL { - return (self.sourceURL && ![[NSFileManager defaultManager] fileExistsAtPath:self.localURL.absoluteString]); +- (BOOL)needsLoadingFromURL { + return (self.sourceURL && ![_fm fileExistsAtPath:[self.localURL path]]); } - (BOOL)isImage { @@ -104,12 +120,14 @@ - (BOOL)isImage { } - (NSURL *)localURL { - if (self.filename){ + if (self.filename && [_fm fileExistsAtPath:self.filename]) { return [NSURL fileURLWithPath:self.filename]; - } else - { return nil;} + } + + return nil; } + #pragma mark NSCoding - (void)encodeWithCoder:(NSCoder *)aCoder { @@ -117,29 +135,25 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.filename forKey:@"filename"]; [aCoder encodeObject:self.originalFilename forKey:@"originalFilename"]; [aCoder encodeObject:self.sourceURL forKey:@"url"]; - - } -- (id)initWithCoder:(NSCoder *)aDecoder { - self = [super init]; - - if (self){ +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if ((self = [self init])) { self.contentType = [aDecoder decodeObjectForKey:@"contentType"]; self.filename = [aDecoder decodeObjectForKey:@"filename"]; self.thumbnailRepresentations = [NSMutableDictionary new]; self.originalFilename = [aDecoder decodeObjectForKey:@"originalFilename"]; self.sourceURL = [aDecoder decodeObjectForKey:@"url"]; - } return self; } + #pragma mark - Thubmnails / Image Representation - (UIImage *)imageRepresentation { - if ([self.contentType rangeOfString:@"image"].location != NSNotFound && self.filename ){ + if ([self.contentType rangeOfString:@"image"].location != NSNotFound && self.filename ) { return [UIImage imageWithData:self.data]; } else { // Create a Icon .. @@ -157,21 +171,22 @@ - (UIImage *)imageRepresentation { - (UIImage *)thumbnailWithSize:(CGSize)size { id cacheKey = [NSValue valueWithCGSize:size]; - if (!self.thumbnailRepresentations[cacheKey]){ + if (!self.thumbnailRepresentations[cacheKey]) { UIImage *image = self.imageRepresentation; // consider the scale. - if (!image) + if (!image) { return nil; + } CGFloat scale = [UIScreen mainScreen].scale; - if (scale != image.scale){ + if (scale != image.scale) { CGSize scaledSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale)); UIImage *thumbnail = bit_imageToFitSize(image, scaledSize, YES) ; UIImage *scaledTumbnail = [UIImage imageWithCGImage:thumbnail.CGImage scale:scale orientation:thumbnail.imageOrientation]; - if (thumbnail){ + if (thumbnail) { [self.thumbnailRepresentations setObject:scaledTumbnail forKey:cacheKey]; } @@ -191,42 +206,36 @@ - (UIImage *)thumbnailWithSize:(CGSize)size { #pragma mark - Persistence Helpers - (NSString *)possibleFilename { - NSArray* cachePathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - NSString* cachePath = [cachePathArray lastObject]; - cachePath = [cachePath stringByAppendingPathComponent:kCacheFolderName]; - - BOOL isDirectory; - - if (![[NSFileManager defaultManager] fileExistsAtPath:cachePath isDirectory:&isDirectory]){ - [[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:nil]; + if (_tempFilename) { + return _tempFilename; } NSString *uniqueString = bit_UUID(); - cachePath = [cachePath stringByAppendingPathComponent:uniqueString]; + _tempFilename = [_cachePath stringByAppendingPathComponent:uniqueString]; // File extension that suits the Content type. CFStringRef mimeType = (__bridge CFStringRef)self.contentType; CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); - if (extension){ - cachePath = [cachePath stringByAppendingPathExtension:(__bridge NSString *)(extension)]; + if (extension) { + _tempFilename = [_tempFilename stringByAppendingPathExtension:(__bridge NSString *)(extension)]; CFRelease(extension); - } CFRelease(uti); - return cachePath; + return _tempFilename; } - (void)deleteContents { - if (self.filename){ - [[NSFileManager defaultManager] removeItemAtPath:self.filename error:nil]; + if (self.filename) { + [_fm removeItemAtPath:self.filename error:nil]; self.filename = nil; } } + #pragma mark - QLPreviewItem - (NSString *)previewItemTitle { @@ -236,9 +245,14 @@ - (NSString *)previewItemTitle { - (NSURL *)previewItemURL { if (self.localURL){ return self.localURL; - } else { - return [NSURL fileURLWithPath:self.possibleFilename]; + } else if (self.sourceURL) { + NSString *filename = self.possibleFilename; + if (filename) { + return [NSURL fileURLWithPath:filename]; + } } + + return nil; } @end From 4f18945133e9647165e820032e5f938214d66a91 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 25 May 2014 19:18:34 +0200 Subject: [PATCH 139/206] Add support for attaching NSData objects to a feedback compose view --- Classes/BITFeedbackComposeViewController.h | 5 +++-- Classes/BITFeedbackComposeViewController.m | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.h b/Classes/BITFeedbackComposeViewController.h index 6bc132e6..2ef4f846 100644 --- a/Classes/BITFeedbackComposeViewController.h +++ b/Classes/BITFeedbackComposeViewController.h @@ -71,9 +71,10 @@ - NSString - NSURL - UIImage + - NSData - These are automatically concatenated to one text string, while any image attachments are - added as attachments to the feedback. + These are automatically concatenated to one text string, while any images and NSData + objects are added as attachments to the feedback. @param items Array of data objects to prefill the feedback text message. */ diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 7a5dff7e..014348e9 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -103,6 +103,8 @@ - (void)prepareWithItems:(NSArray *)items { } else if ([item isKindOfClass:[UIImage class]]) { UIImage *image = item; [self.attachments addObject:[BITFeedbackMessageAttachment attachmentWithData:UIImageJPEGRepresentation(image, 0.7f) contentType:@"image/jpeg"]]; + } else if ([item isKindOfClass:[NSData class]]) { + [self.attachments addObject:[BITFeedbackMessageAttachment attachmentWithData:item contentType:@"'application/octet-stream'"]]; } else { BITHockeyLog(@"Unknown item type %@", item); } From 429af075bc68ddbc98041a9e5f263553c4c15d8e Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 25 May 2014 20:25:30 +0200 Subject: [PATCH 140/206] Improve handling of attachments in compose view - Only UIImage attachments can be edited - All others can only be deleted - Remove scrollview when the last attachment is removed (and avoid a crash if the user clicks on an attachment that doesn't exist any longer) - Add action sheet button texts to localization files (not localized!) - Delete local attachment files if compose view is cancelled - Only show image attachments in the compose view --- Classes/BITFeedbackComposeViewController.m | 71 +++++++++++++--------- Resources/de.lproj/HockeySDK.strings | 12 ++++ Resources/en.lproj/HockeySDK.strings | 12 ++++ Resources/es.lproj/HockeySDK.strings | 12 ++++ Resources/fr.lproj/HockeySDK.strings | 12 ++++ Resources/hr.lproj/HockeySDK.strings | 12 ++++ Resources/hu.lproj/HockeySDK.strings | 12 ++++ Resources/it.lproj/HockeySDK.strings | 12 ++++ Resources/ja.lproj/HockeySDK.strings | 12 ++++ Resources/nl.lproj/HockeySDK.strings | 12 ++++ Resources/pt-PT.lproj/HockeySDK.strings | 12 ++++ Resources/pt.lproj/HockeySDK.strings | 12 ++++ Resources/ro.lproj/HockeySDK.strings | 12 ++++ Resources/ru.lproj/HockeySDK.strings | 12 ++++ Resources/zh-Hans.lproj/HockeySDK.strings | 12 ++++ 15 files changed, 211 insertions(+), 28 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 014348e9..23b5cea7 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -58,6 +58,7 @@ @interface BITFeedbackComposeViewController () 0; - if (!alreadySetup){ + if (alreadySetup && self.imageAttachments.count == 0) { + textViewFrame.size.width += 100; + self.textView.frame = textViewFrame; + scrollViewFrame.size.width = 0; + self.attachmentScrollView.frame = scrollViewFrame; + return; + } + + if (!alreadySetup) { textViewFrame.size.width -= scrollViewWidth; scrollViewFrame = CGRectMake(CGRectGetMaxX(textViewFrame), self.view.frame.origin.y, scrollViewWidth, CGRectGetHeight(self.view.bounds)); self.textView.frame = textViewFrame; self.attachmentScrollView.frame = scrollViewFrame; self.attachmentScrollView.contentInset = self.textView.contentInset; - } - if (self.attachments.count > self.attachmentScrollViewImageViews.count){ - NSInteger numberOfViewsToCreate = self.attachments.count - self.attachmentScrollViewImageViews.count; - for (int i = 0;i self.attachmentScrollViewImageViews.count){ + NSInteger numberOfViewsToCreate = self.imageAttachments.count - self.attachmentScrollViewImageViews.count; + for (int i = 0; i 2){ + if (self.imageAttachments.count > 2){ self.textView.inputAccessoryView = nil; } else { self.textView.inputAccessoryView = self.textAccessoryView; @@ -372,6 +379,10 @@ - (void)setUserDataAction { #pragma mark - Actions - (void)dismissAction:(id)sender { + for (BITFeedbackMessageAttachment *attachment in self.attachments){ + [attachment deleteContents]; + } + [self dismissWithResult:BITFeedbackComposeResultCancelled]; } @@ -430,6 +441,7 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking NSString *imageName = [imagePath lastPathComponent]; newAttachment.originalFilename = imageName; [self.attachments addObject:newAttachment]; + [self.imageAttachments addObject:newAttachment]; } [picker dismissViewControllerAnimated:YES completion:nil]; @@ -442,16 +454,19 @@ - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { - (void)imageButtonAction:(UIButton *)sender { // determine the index of the feedback NSInteger index = [self.attachmentScrollViewImageViews indexOfObject:sender]; + self.selectedAttachmentIndex = index; - UIActionSheet * actionSheet = [[UIActionSheet alloc] initWithTitle: nil - delegate: self - cancelButtonTitle:@"Cancel" - destructiveButtonTitle: @"Delete Attachment" - otherButtonTitles: @"Edit Attachment", nil]; + + UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle: nil + delegate: self + cancelButtonTitle: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentCancel") + destructiveButtonTitle: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentDelete") + otherButtonTitles: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentEdit"), nil]; [actionSheet showFromRect: sender.frame inView: self.attachmentScrollView animated: YES]; } + #pragma mark - BITFeedbackUserDataDelegate - (void)userDataUpdateCancelled { @@ -485,14 +500,16 @@ - (void)textViewDidChange:(UITextView *)textView { [self updateBarButtonState]; } + #pragma mark - UIActionSheet Delegate - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex { if (buttonIndex == [actionSheet destructiveButtonIndex]){ if (self.selectedAttachmentIndex != NSNotFound){ - BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; + BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; [attachment deleteContents]; // mandatory call to delete the files associatd. + [self.imageAttachments removeObject:attachment]; [self.attachments removeObject:attachment]; } self.selectedAttachmentIndex = NSNotFound; @@ -501,24 +518,22 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn } else if(buttonIndex != [actionSheet cancelButtonIndex]){ if (self.selectedAttachmentIndex != NSNotFound){ - BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; + BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; BITImageAnnotationViewController *annotationEditor = [[BITImageAnnotationViewController alloc ] init]; annotationEditor.delegate = self; UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:annotationEditor]; annotationEditor.image = attachment.imageRepresentation; [self presentViewController:navController animated:YES completion:nil]; } - } - - } + #pragma mark - Image Annotation Delegate - (void)annotationController:(BITImageAnnotationViewController *)annotationController didFinishWithImage:(UIImage *)image { if (self.selectedAttachmentIndex != NSNotFound){ - BITFeedbackMessageAttachment* attachment = [self.attachments objectAtIndex:self.selectedAttachmentIndex]; + BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; [attachment replaceData:UIImageJPEGRepresentation(image, 0.7f)]; } diff --git a/Resources/de.lproj/HockeySDK.strings b/Resources/de.lproj/HockeySDK.strings index 8a4b45b7..67a9dbcd 100644 --- a/Resources/de.lproj/HockeySDK.strings +++ b/Resources/de.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Senden"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Bild Hinzufügen"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Anhang Bearbeiten"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Anhang Löschen"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Abbrechen"; + /* Set User Data */ diff --git a/Resources/en.lproj/HockeySDK.strings b/Resources/en.lproj/HockeySDK.strings index 25229c84..97cfa878 100644 --- a/Resources/en.lproj/HockeySDK.strings +++ b/Resources/en.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Send"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/es.lproj/HockeySDK.strings b/Resources/es.lproj/HockeySDK.strings index 514fd282..935c961c 100644 --- a/Resources/es.lproj/HockeySDK.strings +++ b/Resources/es.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Enviar"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/fr.lproj/HockeySDK.strings b/Resources/fr.lproj/HockeySDK.strings index a231bf4f..4cae9d64 100644 --- a/Resources/fr.lproj/HockeySDK.strings +++ b/Resources/fr.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Envoyer"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/hr.lproj/HockeySDK.strings b/Resources/hr.lproj/HockeySDK.strings index 9f5a7520..142e4100 100644 --- a/Resources/hr.lproj/HockeySDK.strings +++ b/Resources/hr.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Šalji"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/hu.lproj/HockeySDK.strings b/Resources/hu.lproj/HockeySDK.strings index 7ecaea35..0b248eb5 100644 --- a/Resources/hu.lproj/HockeySDK.strings +++ b/Resources/hu.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Küldés"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/it.lproj/HockeySDK.strings b/Resources/it.lproj/HockeySDK.strings index 75753bab..e3c965de 100644 --- a/Resources/it.lproj/HockeySDK.strings +++ b/Resources/it.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Invia"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/ja.lproj/HockeySDK.strings b/Resources/ja.lproj/HockeySDK.strings index 4532f8a5..8dbb471b 100644 --- a/Resources/ja.lproj/HockeySDK.strings +++ b/Resources/ja.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "送信"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/nl.lproj/HockeySDK.strings b/Resources/nl.lproj/HockeySDK.strings index 0d37d472..854d8916 100644 --- a/Resources/nl.lproj/HockeySDK.strings +++ b/Resources/nl.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Verstuur"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/pt-PT.lproj/HockeySDK.strings b/Resources/pt-PT.lproj/HockeySDK.strings index 70928c4d..61df2895 100644 --- a/Resources/pt-PT.lproj/HockeySDK.strings +++ b/Resources/pt-PT.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Enviar"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/pt.lproj/HockeySDK.strings b/Resources/pt.lproj/HockeySDK.strings index 1e09c896..8f41112c 100644 --- a/Resources/pt.lproj/HockeySDK.strings +++ b/Resources/pt.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Enviar"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/ro.lproj/HockeySDK.strings b/Resources/ro.lproj/HockeySDK.strings index 6c0225cf..2cb78d86 100644 --- a/Resources/ro.lproj/HockeySDK.strings +++ b/Resources/ro.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Trimite"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/ru.lproj/HockeySDK.strings b/Resources/ru.lproj/HockeySDK.strings index da77e3f8..abaa8e14 100644 --- a/Resources/ru.lproj/HockeySDK.strings +++ b/Resources/ru.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Отправить"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/zh-Hans.lproj/HockeySDK.strings b/Resources/zh-Hans.lproj/HockeySDK.strings index fc8859be..fffe641d 100644 --- a/Resources/zh-Hans.lproj/HockeySDK.strings +++ b/Resources/zh-Hans.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "发送"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ From 0f142f0ec5acf04465aa0ba549caf42ebb0820c0 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 25 May 2014 20:37:14 +0200 Subject: [PATCH 141/206] Improvements for feedback compose view on iPad When the actionsheet is visible, disable the add image button and remove the first responder from the textview --- Classes/BITFeedbackComposeViewController.m | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 23b5cea7..d6dd242c 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -68,7 +68,9 @@ @interface BITFeedbackComposeViewController () Date: Sun, 25 May 2014 20:44:54 +0200 Subject: [PATCH 142/206] Improve feedback compose view on iPad further (for iOS 6.1) --- Classes/BITFeedbackComposeViewController.m | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index d6dd242c..16775362 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -514,7 +514,7 @@ - (void)textViewDidChange:(UITextView *)textView { #pragma mark - UIActionSheet Delegate - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex { - if (buttonIndex == [actionSheet destructiveButtonIndex]){ + if (buttonIndex == [actionSheet destructiveButtonIndex]) { if (self.selectedAttachmentIndex != NSNotFound){ BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; @@ -526,7 +526,10 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn [self refreshAttachmentScrollview]; - } else if(buttonIndex != [actionSheet cancelButtonIndex]){ + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + [self.textView becomeFirstResponder]; + } + } else if (buttonIndex != [actionSheet cancelButtonIndex]) { if (self.selectedAttachmentIndex != NSNotFound){ BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; BITImageAnnotationViewController *annotationEditor = [[BITImageAnnotationViewController alloc ] init]; @@ -535,11 +538,12 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn annotationEditor.image = attachment.imageRepresentation; [self presentViewController:navController animated:YES completion:nil]; } + } else { + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + [self.textView becomeFirstResponder]; + } } _actionSheetVisible = NO; - if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { - [self.textView becomeFirstResponder]; - } } From 32bd3e93b17c391e8392cc7dd99a6f768af06583 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Mon, 26 May 2014 00:11:59 +0200 Subject: [PATCH 143/206] Make it clear how the screenshot trigger gets the image --- Classes/BITFeedbackManager.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 435a1f2e..2056d148 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -69,7 +69,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { */ BITFeedbackObservationNone = 0, /** - * Triggeres when the user takes a screenshot. Requires iOS 7 or later! + * Triggeres when the user takes a screenshot. This will grab the latest image from the camera roll. Requires iOS 7 or later! */ BITFeedbackObservationModeOnScreenshot = 1, /** @@ -220,7 +220,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { own trigger and then call `[[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot];` to handle your custom events that should trigger this. - `BITFeedbackObservationModeOnScreenshot`: Triggeres when the user takes a screenshot. - Requires iOS 7 or later! + This will grab the latest image from the camera roll. Requires iOS 7 or later! - `BITFeedbackObservationModeThreeFingerTap`: Triggers when the user tapps with three fingers for three seconds on the screen. From 3cf56aa9d696fd0c69ab6058db7f804ba35742ad Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 27 May 2014 23:43:37 +0200 Subject: [PATCH 144/206] Various fixes and documentation improvements --- Classes/BITAuthenticator.h | 12 ++- Classes/BITCrashAttachment.h | 6 +- Classes/BITCrashDetails.h | 5 +- Classes/BITCrashManager.h | 23 +++++- Classes/BITCrashMetaData.h | 3 + Classes/BITFeedbackComposeViewController.h | 5 ++ Classes/BITFeedbackComposeViewController.m | 2 - Classes/BITFeedbackManager.h | 11 +++ Classes/BITFeedbackManager.m | 4 +- Classes/BITFeedbackMessage.h | 3 + Classes/BITFeedbackMessageAttachment.h | 3 + Classes/BITUpdateManager.h | 5 ++ Support/HockeySDK.xcodeproj/project.pbxproj | 2 +- docs/Guide-Installation-Mac-App-template.md | 4 +- .../HowTo-Integrate-Atlassian-JMC-template.md | 37 ---------- ...To-Set-Custom-AlertViewHandler-template.md | 73 +++++++++++++++++++ docs/index.md | 4 +- 17 files changed, 144 insertions(+), 58 deletions(-) delete mode 100644 docs/HowTo-Integrate-Atlassian-JMC-template.md create mode 100644 docs/HowTo-Set-Custom-AlertViewHandler-template.md diff --git a/Classes/BITAuthenticator.h b/Classes/BITAuthenticator.h index 240e415a..2a96e49e 100644 --- a/Classes/BITAuthenticator.h +++ b/Classes/BITAuthenticator.h @@ -199,10 +199,14 @@ typedef NS_ENUM(NSUInteger, BITAuthenticatorAppRestrictionEnforcementFrequency) @property (nonatomic, copy) NSString *authenticationSecret; /** - * Delegate that can be used to do any last minute configurations on the - * presented viewController. - * - * @see BITAuthenticatorDelegate + Delegate that can be used to do any last minute configurations on the + presented viewController. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` + @see BITAuthenticatorDelegate */ @property (nonatomic, weak) id delegate; diff --git a/Classes/BITCrashAttachment.h b/Classes/BITCrashAttachment.h index 73217a2a..e3bd6875 100644 --- a/Classes/BITCrashAttachment.h +++ b/Classes/BITCrashAttachment.h @@ -53,9 +53,9 @@ /** * Create an BITCrashAttachment instance with a given filename and NSData object * - * @param filename The filename the attachment should get - * @param attachmentData The attachment data as NSData - * @param contentType The content type of your data as MIME type + * @param filename The filename the attachment should get + * @param crashAttachmentData The attachment data as NSData + * @param contentType The content type of your data as MIME type * * @return An instsance of BITCrashAttachment */ diff --git a/Classes/BITCrashDetails.h b/Classes/BITCrashDetails.h index 4cd8fb73..aa176453 100644 --- a/Classes/BITCrashDetails.h +++ b/Classes/BITCrashDetails.h @@ -28,6 +28,9 @@ #import +/** + * Provides details about the crash that occured in the previous app session + */ @interface BITCrashDetails : NSObject /** @@ -80,7 +83,7 @@ took too long to startup or blocks the main thread for too long, or other reasons. See Apple documentation: https://developer.apple.com/library/ios/qa/qa1693/_index.html - See `[BITCrashManager enableDectionAppKillWhileInForeground]` for more details about which kind of kills can be detected. + See `[BITCrashManager enableAppNotTerminatingCleanlyDetection]` for more details about which kind of kills can be detected. @warning This property only has a correct value, once `[BITHockeyManager startManager]` was invoked! In addition, it is automatically disabled while a debugger session is active! diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index 3a72a5fe..a3dc213b 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -163,6 +163,11 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { /** Sets the optional `BITCrashManagerDelegate` delegate. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` */ @property (nonatomic, weak) id delegate; @@ -290,8 +295,8 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { * * Documentation taken from PLCrashReporter: https://www.plcrashreporter.org/documentation/api/v1.2-rc2/async_safety.html * - * @see `BITCrashManagerPostCrashSignalCallback` - * @see `BITCrashManagerCallbacks` + * @see BITCrashManagerPostCrashSignalCallback + * @see BITCrashManagerCallbacks * * @param callbacks A pointer to an initialized PLCrashReporterCallback structure, see https://www.plcrashreporter.org/documentation/api/v1.2-rc2/struct_p_l_crash_reporter_callbacks.html */ @@ -349,11 +354,21 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { Lets you set a custom block which handles showing a custom UI and asking the user whether he wants to send the crash report. + This replaces the default alert the SDK would show! + + You can use this to present any kind of user interface which asks the user for additional information, + e.g. what they did in the app before the app crashed. + + In addition to this you should always ask your users if they agree to send crash reports, send them + always or not and return the result when calling `handleUserInput:withUserProvidedCrashDescription`. + @param alertViewHandler A block that is responsible for loading, presenting and and dismissing your custom user interface which prompts the user if he wants to send crash reports. The block is also responsible for triggering further processing of the crash reports. - @warning Block needs to call the `handleUserInput:withUserProvidedCrashDescription` method! + @warning Block needs to call the `[BITCrashManager handleUserInput:withUserProvidedMetaData:]` method! + + @warning This needs to be set before calling `[BITHockeyManager startManager]`! */ -- (void) setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler; +- (void)setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler; /** * Provides details about the crash that occured in the last app session diff --git a/Classes/BITCrashMetaData.h b/Classes/BITCrashMetaData.h index ac6bb2b6..a7c5a96c 100644 --- a/Classes/BITCrashMetaData.h +++ b/Classes/BITCrashMetaData.h @@ -29,6 +29,9 @@ #import +/** + * This class provides properties that can be attached to a crash report via a custom alert view flow + */ @interface BITCrashMetaData : NSObject /** diff --git a/Classes/BITFeedbackComposeViewController.h b/Classes/BITFeedbackComposeViewController.h index 2ef4f846..9d328512 100644 --- a/Classes/BITFeedbackComposeViewController.h +++ b/Classes/BITFeedbackComposeViewController.h @@ -55,6 +55,11 @@ /** Sets the `BITFeedbackComposeViewControllerDelegate` delegate. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:`] */ @property (nonatomic, weak) id delegate; diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 16775362..d7dbb896 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -237,8 +237,6 @@ - (void)viewWillAppear:(BOOL)animated { [[UIApplication sharedApplication] setStatusBarStyle:(self.navigationController.navigationBar.barStyle == UIBarStyleDefault) ? UIStatusBarStyleDefault : UIStatusBarStyleBlackOpaque]; #endif - // [self.textView setFrame:self.view.frame]; - if (_text) { self.textView.text = _text; } diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 2056d148..66ad75e2 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -88,6 +88,11 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { This is the HockeySDK module for letting your users to communicate directly with you via the app and an integrated user interface. It provides to have a single threaded discussion with a user running your app. + + You should never create your own instance of `BITFeedbackManager` but use the one provided + by the `[BITHockeyManager sharedHockeyManager]`: + + [BITHockeyManager sharedHockeyManager].feedbackManager The user interface provides a list view than can be presented modally using `[BITFeedbackManager showFeedbackListView]` modally or adding @@ -140,6 +145,11 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { Sets the `BITFeedbackManagerDelegate` delegate. Can be set to be notified when new feedback is received from the server. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:`] */ @property (nonatomic, weak) id delegate; @@ -282,6 +292,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { /** Present the modal feedback compose message user interface with the items given. + All NSString-Content in the array will be concatenated and result in the message, while all UIImage and NSData-instances will be turned into attachments. */ diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 056266fc..3357f92f 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -29,10 +29,10 @@ #import "HockeySDK.h" -#import - #if HOCKEYSDK_FEATURE_FEEDBACK +#import + #import "HockeySDKPrivate.h" #import "BITFeedbackManager.h" diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index 908e4fc8..f754e30c 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -61,6 +61,9 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { BITFeedbackMessageStatusArchived = 5 }; +/** + * An individual feedback message + */ @interface BITFeedbackMessage : NSObject { } diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index dbb398b1..d7997139 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -30,6 +30,9 @@ #import #import +/** + * An individual feedback message attachment + */ @interface BITFeedbackMessageAttachment : NSObject @property (nonatomic, copy) NSNumber *id; diff --git a/Classes/BITUpdateManager.h b/Classes/BITUpdateManager.h index 4425363d..f10f8c30 100644 --- a/Classes/BITUpdateManager.h +++ b/Classes/BITUpdateManager.h @@ -82,6 +82,11 @@ typedef NS_ENUM (NSUInteger, BITUpdateSetting) { /** Sets the `BITUpdateManagerDelegate` delegate. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` */ @property (nonatomic, weak) id delegate; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 751e04ef..f5e47d0d 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -987,7 +987,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/usr/local/bin/appledoc \\\n --output \"${SOURCE_ROOT}/../documentation\" \\\n --ignore Vendor \\\n --ignore Products \\\n --ignore Support \\\n --ignore .m \\\n --ignore *Private.h \\\n --ignore BITAttributedLabel.h \\\n --ignore BITStoreButton.h \\\n --ignore BITFeedbackListViewCell.h \\\n --ignore BITAppStoreHeader.h \\\n --ignore BITFeedbackMessage.h \\\n --ignore BITAuthenticationViewController.h \\\n --create-html \\\n --create-docset \\\n --install-docset \\\n --keep-intermediate-files \\\n --project-name \"${HOCKEYSDK_DOCSET_NAME} ${VERSION_STRING}\" \\\n --project-version \"${VERSION_STRING}\" \\\n --project-company \"Bit Stadium GmbH\" \\\n --company-id \"de.bitstadium\" \\\n --docset-bundle-name \"${HOCKEYSDK_DOCSET_NAME} ${VERSION_STRING}\" \\\n --docset-feed-name \"${HOCKEYSDK_DOCSET_NAME}\" \\\n --docset-desc \"\" \\\n --docset-platform-family \"iphoneos\" \\\n --index-desc \"${SOURCE_ROOT}/../docs/index.md\" \\\n --include \"${SOURCE_ROOT}/../docs/index.html\" \\\n --include \"${SOURCE_ROOT}/../docs/\" \\\n --merge-categories \\\n --no-repeat-first-par \\\n --warn-undocumented-object \\\n --warn-undocumented-member \\\n --warn-empty-description \\\n --warn-unknown-directive \\\n --warn-invalid-crossref \\\n --warn-missing-arg \\\n --logformat xcode \\\n --exit-threshold 2 \\\n \"${SOURCE_ROOT}/../\"\n"; + shellScript = "/usr/local/bin/appledoc \\\n --output \"${SOURCE_ROOT}/../documentation\" \\\n --ignore Vendor \\\n --ignore Products \\\n --ignore Support \\\n --ignore .m \\\n --ignore *Private.h \\\n --ignore BITAttributedLabel.h \\\n --ignore BITStoreButton.h \\\n --ignore BITFeedbackListViewCell.h \\\n --ignore BITAppStoreHeader.h \\\n --ignore BITFeedbackMessage.h \\\n --ignore BITFeedbackMessageAttachment.h \\\n --ignore BITAuthenticationViewController.h \\\n --ignore BITHockeyAppClient.h \\\n --create-html \\\n --create-docset \\\n --install-docset \\\n --keep-intermediate-files \\\n --project-name \"${HOCKEYSDK_DOCSET_NAME} ${VERSION_STRING}\" \\\n --project-version \"${VERSION_STRING}\" \\\n --project-company \"Bit Stadium GmbH\" \\\n --company-id \"de.bitstadium\" \\\n --docset-bundle-name \"${HOCKEYSDK_DOCSET_NAME} ${VERSION_STRING}\" \\\n --docset-feed-name \"${HOCKEYSDK_DOCSET_NAME}\" \\\n --docset-desc \"\" \\\n --docset-platform-family \"iphoneos\" \\\n --index-desc \"${SOURCE_ROOT}/../docs/index.md\" \\\n --include \"${SOURCE_ROOT}/../docs/index.html\" \\\n --include \"${SOURCE_ROOT}/../docs/\" \\\n --merge-categories \\\n --no-repeat-first-par \\\n --warn-undocumented-object \\\n --warn-undocumented-member \\\n --warn-empty-description \\\n --warn-unknown-directive \\\n --warn-invalid-crossref \\\n --warn-missing-arg \\\n --logformat xcode \\\n --exit-threshold 2 \\\n \"${SOURCE_ROOT}/../\"\n"; }; 1EE9071A16F6871F003DDE1D /* ShellScript */ = { isa = PBXShellScriptBuildPhase; diff --git a/docs/Guide-Installation-Mac-App-template.md b/docs/Guide-Installation-Mac-App-template.md index 2cd95e34..e9114875 100644 --- a/docs/Guide-Installation-Mac-App-template.md +++ b/docs/Guide-Installation-Mac-App-template.md @@ -1,7 +1,7 @@ ## Introduction -HockeyMac is a simple client application for Mac OS 10.6 or higher to upload files to HockeyApp. After the installation, you can drag & drop either .ipa files or .xcarchive bundles to the dock or menu icon. HockeyMac will then open a window to enter release notes and set the download flag of the version. The upload is shown with a progress bar and there is some minimal error handling. +HockeyMac is a simple client application for Mac OS 10.8 or higher to upload files to HockeyApp. After the installation, you can drag & drop either .ipa files or .xcarchive bundles to the dock or menu icon. HockeyMac will then open a window to enter release notes and set the download flag of the version. The upload is shown with a progress bar and there is some minimal error handling. This document contains the following sections: @@ -14,7 +14,7 @@ This document contains the following sections: ## Prerequisites -1. Mac OS X 10.6 +1. Mac OS X 10.8 2. Xcode 4 diff --git a/docs/HowTo-Integrate-Atlassian-JMC-template.md b/docs/HowTo-Integrate-Atlassian-JMC-template.md deleted file mode 100644 index 347fbf20..00000000 --- a/docs/HowTo-Integrate-Atlassian-JMC-template.md +++ /dev/null @@ -1,37 +0,0 @@ -## Introduction - -The SDK provides integrated support to automatically configure the Atlassian JIRA Mobile Connect (JMC). It will take the JIRA configuration that is associated to your app on HockeyApp and use that to configure JMC. - -## Requirements - -The binary distribution of HockeySDK does not provide this integration. You need to follow the [Installation & Setup Advanced](Guide-Installation-Setup-Advanced) Guide and activate the JMC integration as described below. - -## HowTo - -1. Select `HockeySDK.xcodeproj` project in the Xcode navigator -2. Select `HockeySDKLib` target -3. Search for `Preprocessor Macros` -4. Double tab in the `HockeySDKLib` column and add the following two values - - $(inherited) - HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT=1 - -5. Setup JMC as described in the [JMC instructions](https://developer.atlassian.com/display/JMC/Enabling+JIRA+Mobile+Connect) -6. Sign in to HockeyApp -7. Select the app and edit the apps bug tracker -8. Choose `JIRA` or `JIRA5`. -9. Enter your JIRA credentials. Make sure you supply the credentials for a user with admin rights, otherwise HockeyApp cannot fetch the JMC token from JIRA. -10. Download the latest JMC client file: [https://bitbucket.org/atlassian/jiraconnect-ios/downloads](https://bitbucket.org/atlassian/jiraconnect-ios/downloads) -11. Unzip the file and move the folder into your project directory. -12. Drag & drop the JMC folder from your project directory to your Xcode project. Select `Create groups for any added folders` and set the checkmark for your target. Then click `Finish`. -13. The class `BITHockeyManager` automatically fetches the API token and project key for JMC from HockeyApp, so you don't need to adjust the configuration in your AppDelegate.m file. The only thing you need to do is find a place in your UI to open the feedback view, such as a button and table view cell. You can then open the feedback view as follows: - - In SomeViewController.m: - - [self presentModalViewController:[[JMC sharedInstance] viewController] animated:YES]; - -14. You can customize the options of JMC like this: - - In AppDelegate.m: - - [[[JMC sharedInstance] options] setBarStyle:UIBarStyleBlack]; diff --git a/docs/HowTo-Set-Custom-AlertViewHandler-template.md b/docs/HowTo-Set-Custom-AlertViewHandler-template.md new file mode 100644 index 00000000..cf70b001 --- /dev/null +++ b/docs/HowTo-Set-Custom-AlertViewHandler-template.md @@ -0,0 +1,73 @@ +## Introduction + +HockeySDK lets the user decide wether to send a crash report or lets the developer send crash reports automatically without user interaction. In addition it is possible to attach more data like logs, a binary, or the users name, email or a user ID if this is already known. + +Starting HockeySDK version 3.6 it is now possible to customize this even further and implement your own flow to e.g. ask the user for more details about what happened or his name and email address if your app doesn't know that yet. + +The following example shows how this could be implemented. We'll present a custom UIAlertView asking the user for more details and attaching that to the crash report. + +## HowTo + +1. Setup the SDK +2. Configure HockeySDK to use your custom alertview handler using the `[[BITHockeyManager sharedHockeyManager].crashManager setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler;` method in your AppDelegate. +3. Implement your handler in a way that it calls `[[BITHockeyManager sharedHockeyManager].crashManagerhandleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData]` with the input provided by the user. +4. Dismiss your custom view. + +## Example + + @interface BITAppDelegate () + @end + + + @implementation BITAppDelegate + + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [self.window makeKeyAndVisible]; + + [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"<>" + delegate:nil]; + + // optionally enable logging to get more information about states. + [BITHockeyManager sharedHockeyManager].debugLogEnabled = YES; + + [[BITHockeyManager sharedHockeyManager].crashManager setAlertViewHandler:^(){ + NSString *exceptionReason = [[BITHockeyManager sharedHockeyManager].crashManager lastSessionCrashDetails].exceptionReason; + UIAlertView *customAlertView = [[UIAlertView alloc] initWithTitle: @"Oh no! The App crashed" + message: nil + delegate: self + cancelButtonTitle: @"Don't send" + otherButtonTitles: @"Send", @"Always send", nil]; + if (exceptionReason) { + customAlertView.message = @"We would like to send a crash report to the developers. Please enter a short description of what happened:"; + customAlertView.alertViewStyle = UIAlertViewStylePlainTextInput; + } else { + customAlertView.message = @"We would like to send a crash report to the developers"; + } + + [customAlertView show]; + }]; + + [[BITHockeyManager sharedHockeyManager].authenticator authenticateInstallation]; + + return YES; + } + + - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { + BITCrashMetaData *crashMetaData = [BITCrashMetaData new]; + if (alertView.alertViewStyle != UIAlertViewStyleDefault) { + crashMetaData.userDescription = [alertView textFieldAtIndex:0].text; + } + switch (buttonIndex) { + case 0: + [[BITHockeyManager sharedHockeyManager].crashManager handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedMetaData:nil]; + break; + case 1: + [[BITHockeyManager sharedHockeyManager].crashManager handleUserInput:BITCrashManagerUserInputSend withUserProvidedMetaData:crashMetaData]; + break; + case 2: + [[BITHockeyManager sharedHockeyManager].crashManager handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedMetaData:crashMetaData]; + break; + } + } + + @end diff --git a/docs/index.md b/docs/index.md index b8efea06..ddeb0128 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,11 +36,11 @@ The main SDK class is `BITHockeyManager`. It initializes all modules and provide - [How to upload symbols for crash reporting](HowTo-Upload-Symbols) - [How to handle crashes on startup](HowTo-Handle-Crashes-On-Startup) - [How to add application specific log data](HowTo-Add-Application-Log) -- [How to integrate Atlassian JMC](HowTo-Integrate-Atlassian-JMC) +- [How to ask the user for more details about a crash](HowTo-Set-Custom-AlertViewHandler) ## Troubleshooting -- [Symbolication doesn't work](Symbolication-Doesnt-Work) (Or the rules of binary UUIDs and dSYMs) +- [Symbolication doesn't work](Troubleshooting-Symbolication-Doesnt-Work) (Or the rules of binary UUIDs and dSYMs) - [Crash Reporting is not working](Troubleshooting-Crash-Reporting-Not-Working) ## Xcode Documentation From 038b2b49918e549fd96958b001c3e1e815145f8d Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 28 May 2014 00:48:55 +0200 Subject: [PATCH 145/206] Another small fix --- Classes/BITFeedbackManager.h | 1 - 1 file changed, 1 deletion(-) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 66ad75e2..1dff5e5c 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -32,7 +32,6 @@ #import "BITHockeyBaseManager.h" #import "BITFeedbackListViewController.h" #import "BITFeedbackComposeViewController.h" -#import "HockeySDKPrivate.h" // Notification message which tells that loading messages finished From 88ed46be02a3a643943620fa177235a41115ff46 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 28 May 2014 00:50:56 +0200 Subject: [PATCH 146/206] More documentation fixes --- Classes/BITFeedbackManager.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 1dff5e5c..d9d87865 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -148,7 +148,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You should not need to set this delegate individually. - @see `[BITHockeyManager setDelegate:`] + @see `[BITHockeyManager setDelegate:]` */ @property (nonatomic, weak) id delegate; @@ -294,6 +294,8 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { All NSString-Content in the array will be concatenated and result in the message, while all UIImage and NSData-instances will be turned into attachments. + + @param items an NSArray with objects that should be attached */ - (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items; From c8b053bc14d4a1da5aaa1e7e5fe741d0be1d43cb Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 28 May 2014 20:50:12 +0200 Subject: [PATCH 147/206] Update version and files to v3.6 Beta 1 --- HockeySDK.podspec | 10 +++++----- README.md | 16 +++++++-------- Support/HockeySDK.xcodeproj/project.pbxproj | 20 +++++++++---------- Support/buildnumber.xcconfig | 4 ++-- docs/Changelog-template.md | 17 ++++++++++++++++ ...de-Installation-Setup-Advanced-template.md | 11 ++++++---- docs/Guide-Installation-Setup-template.md | 11 ++++++---- 7 files changed, 56 insertions(+), 33 deletions(-) diff --git a/HockeySDK.podspec b/HockeySDK.podspec index 75107724..f0efbd95 100644 --- a/HockeySDK.podspec +++ b/HockeySDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'HockeySDK' - s.version = '3.5.5' + s.version = '3.6-b.1' s.summary = 'Collect live crash reports, get feedback from your users, distribute your betas, and analyze your test coverage with HockeyApp.' s.description = <<-DESC @@ -12,19 +12,19 @@ Pod::Spec.new do |s| DESC s.homepage = 'http://hockeyapp.net/' - s.documentation_url = 'http://hockeyapp.net/help/sdk/ios/3.5.5/' + s.documentation_url = 'http://hockeyapp.net/help/sdk/ios/3.6-b.1/' s.license = 'MIT' s.author = { 'Andreas Linde' => 'mail@andreaslinde.de', 'Thomas Dohmke' => "thomas@dohmke.de" } s.source = { :git => 'https://github.com/bitstadium/HockeySDK-iOS.git', :tag => s.version.to_s } - s.platform = :ios, '5.0' + s.platform = :ios, '6.0' s.source_files = 'Classes', "Vendor/CrashReporter.framework/Versions/A/Headers/*.h" s.requires_arc = true - s.frameworks = 'CoreText', 'QuartzCore', 'SystemConfiguration', 'CoreGraphics', 'UIKit', 'Security' + s.frameworks = 'CoreText', 'QuartzCore', 'SystemConfiguration', 'CoreGraphics', 'UIKit', 'Security', 'AssetsLibrary', 'MobileCoreServices', 'QuickLook' s.ios.vendored_frameworks = 'Vendor/CrashReporter.framework' - s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => %{$(inherited) BITHOCKEY_VERSION="@\\"#{s.version}\\"" BITHOCKEY_C_VERSION="\\"#{s.version}\\"" BITHOCKEY_BUILD="@\\"28\\"" BITHOCKEY_C_BUILD="\\"28\\""} } + s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => %{$(inherited) BITHOCKEY_VERSION="@\\"#{s.version}\\"" BITHOCKEY_C_VERSION="\\"#{s.version}\\"" BITHOCKEY_BUILD="@\\"29\\"" BITHOCKEY_C_BUILD="\\"29\\""} } s.resource_bundle = { 'HockeySDKResources' => ['Resources/*.png', 'Resources/*.lproj'] } s.preserve_paths = 'Resources', 'Support' diff --git a/README.md b/README.md index 31833fb6..4134a1f9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -## Version 3.5.5 +## Version 3.6 Beta 1 -- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.5.5/docs/docs/Changelog.html) +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Changelog.html) ## Introduction @@ -26,15 +26,15 @@ The main SDK class is `BITHockeyManager`. It initializes all modules and provide 1. Before you integrate HockeySDK into your own app, you should add the app to HockeyApp if you haven't already. Read [this how-to](http://support.hockeyapp.net/kb/how-tos/how-to-create-a-new-app) on how to do it. 2. We also assume that you already have a project in Xcode and that this project is opened in Xcode 4. -3. The SDK supports iOS 5.0 or newer. +3. The SDK supports iOS 6.0 or newer. ## Installation & Setup -- [Installation & Setup](http://www.hockeyapp.net/help/sdk/ios/3.5.5/docs/docs/Guide-Installation-Setup.html) (Recommended) -- [Installation & Setup Advanced](http://www.hockeyapp.net/help/sdk/ios/3.5.5/docs/docs/Guide-Installation-Setup-Advanced.html) (Using Git submodule and Xcode sub-project) -- [Identify and authenticate users of Ad-Hoc or Enterprise builds](http://www.hockeyapp.net/help/sdk/ios/3.5.5/docs/docs/HowTo-Authenticating-Users-on-iOS.html) -- [Migration from previous SDK Versions](http://www.hockeyapp.net/help/sdk/ios/3.5.5/docs/docs/Guide-Migration-Kits.html) +- [Installation & Setup](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Guide-Installation-Setup.html) (Recommended) +- [Installation & Setup Advanced](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Guide-Installation-Setup-Advanced.html) (Using Git submodule and Xcode sub-project) +- [Identify and authenticate users of Ad-Hoc or Enterprise builds](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/HowTo-Authenticating-Users-on-iOS.html) +- [Migration from previous SDK Versions](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Guide-Migration-Kits.html) - [Mac Desktop Uploader](http://support.hockeyapp.net/kb/how-tos/how-to-upload-to-hockeyapp-on-a-mac) @@ -48,4 +48,4 @@ This documentation provides integrated help in Xcode for all public APIs and a s 3. Copy the content into ~`/Library/Developer/Shared/Documentation/DocSets` -The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.5.5/](http://hockeyapp.net/help/sdk/ios/3.5.5/) +The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.1/](http://hockeyapp.net/help/sdk/ios/3.6-b.1/) diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index f5e47d0d..1115044e 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -130,7 +130,7 @@ 1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */; }; 1EB52FD5167B766100C801D5 /* HockeySDK.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E59555F15B6F80E00A03429 /* HockeySDK.strings */; }; 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */; }; - 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */; }; + 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1ECA8F52192B6954006B9416 /* BITCrashMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ECA8F50192B6954006B9416 /* BITCrashMetaData.m */; }; 1ED570C718BF878C00AB3350 /* BITCrashAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1ED570C818BF878C00AB3350 /* BITCrashAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ED570C618BF878C00AB3350 /* BITCrashAttachment.m */; }; @@ -753,25 +753,28 @@ 1E59559B15B6FDA500A03429 /* HockeySDK.h in Headers */, 1E59559A15B6FDA500A03429 /* BITHockeyManager.h in Headers */, 1E5955FD15B7877B00A03429 /* BITHockeyManagerDelegate.h in Headers */, + E48A3DEC17B3ED1C00924C3D /* BITAuthenticator.h in Headers */, 1E754E5C1621FBB70070AB92 /* BITCrashManager.h in Headers */, 1E754E5E1621FBB70070AB92 /* BITCrashManagerDelegate.h in Headers */, + 1ED570C718BF878C00AB3350 /* BITCrashAttachment.h in Headers */, + 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */, 1E90FD7318EDB86400CF0417 /* BITCrashDetails.h in Headers */, 1E49A4731612226D00463151 /* BITUpdateManager.h in Headers */, 1E49A4791612226D00463151 /* BITUpdateManagerDelegate.h in Headers */, 1E49A44E1612223B00463151 /* BITFeedbackManager.h in Headers */, + E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */, 973EC8BF18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h in Headers */, E4B4DB7D17B435550099C67F /* BITAuthenticationViewController.h in Headers */, 1E49A4481612223B00463151 /* BITFeedbackListViewController.h in Headers */, 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */, + 1E94F9E116E91330006570AD /* BITStoreUpdateManager.h in Headers */, 1E49A47F1612226D00463151 /* BITUpdateViewController.h in Headers */, 1E49A43C1612223B00463151 /* BITFeedbackComposeViewController.h in Headers */, E40E0B0C17DA1AFF005E38C1 /* BITHockeyAppClient.h in Headers */, 1EF95CAA162CB314000AE3AD /* BITFeedbackComposeViewControllerDelegate.h in Headers */, 1EF95CA6162CB037000AE3AD /* BITFeedbackActivity.h in Headers */, - 1E49A4AF161222B900463151 /* BITHockeyBaseManager.h in Headers */, 1E49A4B8161222B900463151 /* BITHockeyBaseViewController.h in Headers */, - 1E94F9E116E91330006570AD /* BITStoreUpdateManager.h in Headers */, - 1ED570C718BF878C00AB3350 /* BITCrashAttachment.h in Headers */, + 1E49A4AF161222B900463151 /* BITHockeyBaseManager.h in Headers */, 1E0829001708F69A0073050E /* BITStoreUpdateManagerDelegate.h in Headers */, 1E49A4421612223B00463151 /* BITFeedbackListViewCell.h in Headers */, 1E49A4541612223B00463151 /* BITFeedbackManagerPrivate.h in Headers */, @@ -780,7 +783,6 @@ 1E49A46D1612226D00463151 /* BITAppVersionMetaInfo.h in Headers */, 1E49A47C1612226D00463151 /* BITUpdateManagerPrivate.h in Headers */, 1E49A4851612226D00463151 /* BITUpdateViewControllerPrivate.h in Headers */, - 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */, 1E49A4B5161222B900463151 /* BITHockeyBaseManagerPrivate.h in Headers */, 9774BCFF192CB20A00085EB5 /* BITActivityIndicatorButton.h in Headers */, E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */, @@ -789,10 +791,8 @@ 1E49A4C4161222B900463151 /* BITAppStoreHeader.h in Headers */, 1E49A4CA161222B900463151 /* BITStoreButton.h in Headers */, 973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */, - E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */, 1E49A4D0161222B900463151 /* BITWebTableViewCell.h in Headers */, 1E49A4D8161222D400463151 /* HockeySDKPrivate.h in Headers */, - E48A3DEC17B3ED1C00924C3D /* BITAuthenticator.h in Headers */, 1E754E601621FBB70070AB92 /* BITCrashReportTextFormatter.h in Headers */, 1E84DB3417E099BA00AC83FD /* HockeySDKFeatureConfig.h in Headers */, 1EACC97B162F041E007578C5 /* BITAttributedLabel.h in Headers */, @@ -1303,7 +1303,7 @@ GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "../Resources/HockeySDK-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 5.0; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; "IPHONEOS_DEPLOYMENT_TARGET[arch=arm64]" = 7.0; MACOSX_DEPLOYMENT_TARGET = 10.5; OTHER_CFLAGS = ( @@ -1440,7 +1440,7 @@ GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "../Resources/HockeySDK-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 5.0; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; "IPHONEOS_DEPLOYMENT_TARGET[arch=arm64]" = 7.0; MACOSX_DEPLOYMENT_TARGET = 10.5; OTHER_CFLAGS = ( @@ -1475,7 +1475,7 @@ GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "../Resources/HockeySDK-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 5.0; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; "IPHONEOS_DEPLOYMENT_TARGET[arch=arm64]" = 7.0; MACOSX_DEPLOYMENT_TARGET = 10.5; OTHER_CFLAGS = ( diff --git a/Support/buildnumber.xcconfig b/Support/buildnumber.xcconfig index b4f00c3b..afb98dbd 100644 --- a/Support/buildnumber.xcconfig +++ b/Support/buildnumber.xcconfig @@ -1,7 +1,7 @@ #include "HockeySDK.xcconfig" -BUILD_NUMBER = 28 -VERSION_STRING = 3.5.5 +BUILD_NUMBER = 29 +VERSION_STRING = 3.6-b.1 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) BITHOCKEY_VERSION="@\""$(VERSION_STRING)"\"" BITHOCKEY_BUILD="@\""$(BUILD_NUMBER)"\"" BITHOCKEY_C_VERSION="\""$(VERSION_STRING)"\"" BITHOCKEY_C_BUILD="\""$(BUILD_NUMBER)"\"" BIT_ARM_ARCHS = armv7 armv7s arm64 BIT_SIM_ARCHS = x86_64 i386 diff --git a/docs/Changelog-template.md b/docs/Changelog-template.md index 5cc2d0a7..4f0125ac 100644 --- a/docs/Changelog-template.md +++ b/docs/Changelog-template.md @@ -1,3 +1,20 @@ +## Version 3.6.0 Beta 1 + +- [NEW] Minimum iOS Deployment version is now iOS 6.0 +- [NEW] Requires to link additional frameworks: `AssetLibrary`, `MobileCoreServices`, `QuickLook` +- [NEW] `BITFeedbackManager`: Attach and annotate images and screenshots +- [NEW] `BITFeedbackManager`: Attach any binary data to compose message view (see `showFeedbackComposeViewWithPreparedItems:`) +- [NEW] `BITFeedbackManager`: Show a compose message with a screenshot image attached using predefined triggers (see `feedbackObservationMode`) or your own custom triggers (see `showFeedbackComposeViewWithGeneratedScreenshot`) +- [NEW] `BITCrashManager`: Option to add a custom UI flow before sending a crash report, e.g. to ask users for more details (see `setAlertViewHandler:`) +- [NEW] `BITCrashManager`: Provide details on a crash report (see `lastSessionCrashDetails`) +- [NEW] `BITCrashManager`: Experimental support for detecting app kills triggered by iOS while the app is in foreground (see `enableAppNotTerminatingCleanlyDetection`) +- [NEW] `BITCrashManager`: Added `didReceiveMemoryWarningInLastSession` which indicates if the last app session did get a memory warning by iOS +- [UPDATE] `BITCrashManager`: Updated `setCrashCallbacks` handling now using `BITCrashManagerCallbacks` instead of `PLCrashReporterCallbacks` (which is no longer public) +- [UPDATE] `BITCrashManager`: Crash reports are now send individually if there are multiple pending +- [UPDATE] Removed support for Atlassian JMC +- [BUGFIX] Fixed an incorrect submission warning about referencing non-public selector `attachmentData` +

+ ## Version 3.5.5 - [NEW] `BITCrashManager`: Added support for adding a binary attachment to crash reports diff --git a/docs/Guide-Installation-Setup-Advanced-template.md b/docs/Guide-Installation-Setup-Advanced-template.md index 19b54a67..4ad17fa5 100644 --- a/docs/Guide-Installation-Setup-Advanced-template.md +++ b/docs/Guide-Installation-Setup-Advanced-template.md @@ -1,6 +1,6 @@ -## Version 3.5.5 +## Version 3.5 Beta 1 -- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.5.5/docs/docs/Changelog.html) +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Changelog.html) ## Introduction @@ -17,7 +17,7 @@ This document contains the following sections: ## Requirements -The SDK runs on devices with iOS 5.0 or higher. +The SDK runs on devices with iOS 6.0 or higher. ## Set up Git submodule @@ -58,10 +58,13 @@ The SDK runs on devices with iOS 5.0 or higher. 10. Add the following system frameworks, if they are missing: + - `AssetsLibrary` - `CoreText` - `CoreGraphics` - `Foundation` + - `MobileCoreServices` - `QuartzCore` + - `QuickLook` - `Security` - `SystemConfiguration` - `UIKit` @@ -116,7 +119,7 @@ This documentation provides integrated help in Xcode for all public APIs and a s 3. Copy the content into ~`/Library/Developer/Shared/Documentation/DocSets` -The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.5.5/](http://hockeyapp.net/help/sdk/ios/3.5.5/) +The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.1/](http://hockeyapp.net/help/sdk/ios/3.6-b.1/) ### Set up with xcconfig diff --git a/docs/Guide-Installation-Setup-template.md b/docs/Guide-Installation-Setup-template.md index 68d8356f..680bc3d1 100644 --- a/docs/Guide-Installation-Setup-template.md +++ b/docs/Guide-Installation-Setup-template.md @@ -1,6 +1,6 @@ -## Version 3.5.5 +## Version 3.6 Beta 1 -- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.5.5/docs/docs/Changelog.html) +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Changelog.html) ## Introduction @@ -17,7 +17,7 @@ This document contains the following sections: ## Requirements -The SDK runs on devices with iOS 5.0 or higher. +The SDK runs on devices with iOS 6.0 or higher. ## Download & Extract @@ -48,10 +48,13 @@ The SDK runs on devices with iOS 5.0 or higher. 7. Expand `Link Binary With Libraries`. 8. Add the following system frameworks, if they are missing: + - `AssetsLibrary` - `CoreText` - `CoreGraphics` - `Foundation` + - `MobileCoreServices` - `QuartzCore` + - `QuickLook` - `Security` - `SystemConfiguration` - `UIKit` @@ -92,7 +95,7 @@ This documentation provides integrated help in Xcode for all public APIs and a s 1. Copy `de.bitstadium.HockeySDK-iOS-3.5.5.docset` into ~`/Library/Developer/Shared/Documentation/DocSets` -The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.5.5/](http://hockeyapp.net/help/sdk/ios/3.5.5/) +The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.1/](http://hockeyapp.net/help/sdk/ios/3.6-b.1/) ### Set up with xcconfig From 0066815e205b5d04f3e1a3d67f97a6d7e480f96e Mon Sep 17 00:00:00 2001 From: Matthias Wenz Date: Fri, 30 May 2014 16:19:15 +0200 Subject: [PATCH 148/206] fix some typos --- Classes/BITFeedbackManager.h | 45 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index d9d87865..fc84da1b 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -42,7 +42,7 @@ /** - * Defines if behavior of the user data field + * Defines behavior of the user data field */ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { /** @@ -68,11 +68,11 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { */ BITFeedbackObservationNone = 0, /** - * Triggeres when the user takes a screenshot. This will grab the latest image from the camera roll. Requires iOS 7 or later! + * Triggers when the user takes a screenshot. This will grab the latest image from the camera roll. Requires iOS 7 or later! */ BITFeedbackObservationModeOnScreenshot = 1, /** - * Triggers when the user tapps with three fingers for three seconds on the screen. + * Triggers when the user taps with three fingers for three seconds on the screen. */ BITFeedbackObservationModeThreeFingerTap = 2 }; @@ -84,8 +84,8 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { /** The feedback module. - This is the HockeySDK module for letting your users to communicate directly with you via - the app and an integrated user interface. It provides to have a single threaded + This is the HockeySDK module for letting your users communicate directly with you via + the app and an integrated user interface. It provides a single threaded discussion with a user running your app. You should never create your own instance of `BITFeedbackManager` but use the one provided @@ -94,20 +94,20 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { [BITHockeyManager sharedHockeyManager].feedbackManager The user interface provides a list view than can be presented modally using - `[BITFeedbackManager showFeedbackListView]` modally or adding + `[BITFeedbackManager showFeedbackListView]` or adding `[BITFeedbackManager feedbackListViewController:]` to push onto a navigation stack. - This list integrates all features to load new messages, write new messages, view message + This list integrates all features to load new messages, write new messages, view messages and ask the user for additional (optional) data like name and email. - If the user provides the email address, all responses from the server will also be send - to the user via email and the user is also able to respond directly via email too. + If the user provides the email address, all responses from the server will also be sent + to the user via email and the user is also able to respond directly via email, too. The message list interface also contains options to locally delete single messages by swiping over them, or deleting all messages. This will not delete the messages - on the server though! + on the server, though! - It is also integrates actions to invoke the user interface to compose a new messages, - reload the list content from the server and changing the users name or email if these + It also integrates actions to invoke the user interface to compose a new message, + reload the list content from the server and change the users name or email if these are allowed to be set. It is also possible to invoke the user interface to compose a new message in your @@ -121,9 +121,9 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { A third option is to include the `BITFeedbackActivity` into an UIActivityViewController. This can be useful if you present some data that users can not only share but also report back to the developer because they have some problems, e.g. webcams not working - any more. The activity provide a default title and image that can be also be customized. + any more. The activity provides a default title and image that can also be customized. - New message are automatically loaded on startup, when the app becomes active again + New messages are automatically loaded on startup, when the app becomes active again or when the notification `BITHockeyNetworkDidBecomeReachableNotification` is fired. This only happens if the user ever did initiate a conversation by writing the first feedback message. @@ -202,12 +202,12 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { /** - Indicates if an alert should be shown when new messages arrived + Indicates if an alert should be shown when new messages have arrived - This lets the user to view the new feedback by choosing the appropriate option + This lets the user view the new feedback by choosing the appropriate option in the alert sheet, and the `BITFeedbackListViewController` will be shown. - The alert is only shown, if the newest message is not originated from the current user. + The alert is only shown, if the newest message didn't originate from the current user. This requires the users email address to be present! The optional userid property cannot be used, because users could also answer via email and then this information is not available. @@ -228,10 +228,9 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { - `BITFeedbackObservationNone`: No SDK based trigger is active. You can implement your own trigger and then call `[[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot];` to handle your custom events that should trigger this. - - `BITFeedbackObservationModeOnScreenshot`: Triggeres when the user takes a screenshot. + - `BITFeedbackObservationModeOnScreenshot`: Triggers when the user takes a screenshot. This will grab the latest image from the camera roll. Requires iOS 7 or later! - - `BITFeedbackObservationModeThreeFingerTap`: Triggers when the user tapps with three fingers - for three seconds on the screen. + - `BITFeedbackObservationModeThreeFingerTap`: Triggers when the user taps on the screen for three seconds with three fingers. Default is `BITFeedbackObservationNone` @@ -246,7 +245,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { /** - Indicates if an forced user data UI presentation is shown modal + Indicates if a forced user data UI presentation is shown modal If `requireUserName` and/or `requireUserEmail` are enabled, the first presentation of `feedbackListViewController:` and subsequent `feedbackComposeViewController:` @@ -256,7 +255,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { If you want the SDK to push this UI onto the navigation stack in this specific scenario, then change the property to `NO`. - @warning If you presenting the `BITFeedbackListViewController` in a popover, this property should not be changed! + @warning If you are presenting the `BITFeedbackListViewController` in a popover, this property should not be changed! Default is `YES` @see requireUserName @@ -312,7 +311,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { /** - Create an feedback compose view + Create a feedback compose view Example to show a modal feedback compose UI with prefilled text From 97031cb442eb265959a0ea2b421dc15e49b70d08 Mon Sep 17 00:00:00 2001 From: Matthias Wenz Date: Fri, 30 May 2014 16:46:03 +0200 Subject: [PATCH 149/206] fix some typos in german localization --- Resources/de.lproj/HockeySDK.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/de.lproj/HockeySDK.strings b/Resources/de.lproj/HockeySDK.strings index 67a9dbcd..3611397a 100644 --- a/Resources/de.lproj/HockeySDK.strings +++ b/Resources/de.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Senden"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Bild Hinzufügen"; +"HockeyFeedbackComposeAttachmentAddImage" = "+ Bild hinzufügen"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Anhang Bearbeiten"; +"HockeyFeedbackComposeAttachmentEdit" = "Anhang bearbeiten"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Anhang Löschen"; +"HockeyFeedbackComposeAttachmentDelete" = "Anhang löschen"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Abbrechen"; From 315fee5f921813fc73f77777b13eb9bd353fc491 Mon Sep 17 00:00:00 2001 From: Matthias Wenz Date: Mon, 2 Jun 2014 10:38:53 +0200 Subject: [PATCH 150/206] remove unused initializer --- Classes/BITImageAnnotation.m | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Classes/BITImageAnnotation.m b/Classes/BITImageAnnotation.m index 428e8b11..4870c8ec 100644 --- a/Classes/BITImageAnnotation.m +++ b/Classes/BITImageAnnotation.m @@ -30,17 +30,6 @@ @implementation BITImageAnnotation -- (id)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self) { - // Initialization code - //self.backgroundColor = [UIColor redColor]; - } - return self; -} - - -(BOOL)resizable { return NO; } From d18183f3ae68696d820f765c92c7343575ca940b Mon Sep 17 00:00:00 2001 From: Matthias Wenz Date: Mon, 2 Jun 2014 10:39:45 +0200 Subject: [PATCH 151/206] cleanup/normalize method naming and braces --- Classes/BITArrowImageAnnotation.m | 5 ++--- Classes/BITBlurImageAnnotation.m | 7 +++---- Classes/BITFeedbackComposeViewController.m | 4 ++-- Classes/BITFeedbackMessage.h | 2 +- Classes/BITFeedbackMessage.m | 4 ++-- Classes/BITImageAnnotation.h | 2 +- Classes/BITImageAnnotationViewController.m | 13 +++++-------- Classes/BITRectangleImageAnnotation.m | 5 ++--- 8 files changed, 18 insertions(+), 24 deletions(-) diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index 2d739b4f..bf6b3454 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -40,8 +40,7 @@ @interface BITArrowImageAnnotation() @implementation BITArrowImageAnnotation -- (id)initWithFrame:(CGRect)frame -{ +- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.shapeLayer = [CAShapeLayer layer]; @@ -196,7 +195,7 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { } --(void)layoutSubviews{ +- (void)layoutSubviews{ [super layoutSubviews]; [self buildShape]; diff --git a/Classes/BITBlurImageAnnotation.m b/Classes/BITBlurImageAnnotation.m index 4760229d..0e1eda8d 100644 --- a/Classes/BITBlurImageAnnotation.m +++ b/Classes/BITBlurImageAnnotation.m @@ -39,8 +39,7 @@ @interface BITBlurImageAnnotation() @implementation BITBlurImageAnnotation -- (id)initWithFrame:(CGRect)frame -{ +- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.clipsToBounds = YES; @@ -57,7 +56,7 @@ - (id)initWithFrame:(CGRect)frame return self; } --(void)setSourceImage:(UIImage *)sourceImage { +- (void)setSourceImage:(UIImage *)sourceImage { CGSize size = CGSizeMake(sourceImage.size.width/30, sourceImage.size.height/30); UIGraphicsBeginImageContext(size); @@ -95,7 +94,7 @@ - (void)layoutSubviews { [CATransaction commit]; } --(BOOL)resizable { +- (BOOL)resizable { return YES; } diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index d7dbb896..a5d61e25 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -274,7 +274,7 @@ - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; } --(void)refreshAttachmentScrollview { +- (void)refreshAttachmentScrollview { CGFloat scrollViewWidth = 0; if (self.imageAttachments.count){ @@ -411,7 +411,7 @@ - (void)dismissWithResult:(BITFeedbackComposeResult) result { } } --(void)addPhotoAction:(id)sender { +- (void)addPhotoAction:(id)sender { if (_actionSheetVisible) return; // add photo. diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index f754e30c..b8165f67 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -90,7 +90,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { @param object BITFeedbackMessageAttachment instance representing the attachment that should be added */ --(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object; +- (void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object; /** Return the attachments that can be viewed diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index 7f8d60de..342da85d 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -85,7 +85,7 @@ - (instancetype)initWithCoder:(NSCoder *)decoder { #pragma mark - Deletion --(void)deleteContents { +- (void)deleteContents { for (BITFeedbackMessageAttachment *attachment in self.attachments){ [attachment deleteContents]; } @@ -103,7 +103,7 @@ - (NSArray *)previewableAttachments { return returnArray; } --(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object{ +- (void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object{ if (!self.attachments){ self.attachments = [NSArray array]; } diff --git a/Classes/BITImageAnnotation.h b/Classes/BITImageAnnotation.h index 8f1354a6..563ccc61 100644 --- a/Classes/BITImageAnnotation.h +++ b/Classes/BITImageAnnotation.h @@ -36,7 +36,7 @@ @property (nonatomic, weak) UIImage *sourceImage; @property (nonatomic) CGRect imageFrame; --(BOOL)resizable; +- (BOOL)resizable; - (void)setSelected:(BOOL)selected; - (BOOL)isSelected; diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index e9420da0..61077c16 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -65,8 +65,7 @@ @implementation BITImageAnnotationViewController #pragma mark - UIViewController -- (void)viewDidLoad -{ +- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; @@ -163,7 +162,7 @@ - (void)fitImageViewFrame { self.imageView.frame = baseFrame; } --(void)editingAction:(id)sender { +- (void)editingAction:(id)sender { } @@ -246,8 +245,6 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { self.panStart = [gestureRecognizer locationInView:self.imageView]; - // [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; - } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ CGPoint bla = [gestureRecognizer locationInView:self.imageView]; self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x - self.panStart.x, bla.y - self.panStart.y); @@ -293,7 +290,7 @@ - (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { } } --(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { +- (void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ // try to figure out which view we are talking about. BITImageAnnotation *candidate = nil; @@ -346,7 +343,7 @@ -(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { } } --(void)tapped:(UIGestureRecognizer *)tapRecognizer { +- (void)tapped:(UIGestureRecognizer *)tapRecognizer { // This toggles the nav and status bar. Since iOS7 and pre-iOS7 behave weirdly different, // this might look rather hacky, but hiding the navbar under iOS6 leads to some ugly // animation effect which is avoided by simply hiding the navbar setting it's alpha to 0. // moritzh @@ -388,7 +385,7 @@ -(void)tapped:(UIGestureRecognizer *)tapRecognizer { #pragma mark - Helpers --(UIView *)firstAnnotationThatIsNotBlur { +- (UIView *)firstAnnotationThatIsNotBlur { for (BITImageAnnotation *annotation in self.imageView.subviews){ if (![annotation isKindOfClass:[BITBlurImageAnnotation class]]){ return annotation; diff --git a/Classes/BITRectangleImageAnnotation.m b/Classes/BITRectangleImageAnnotation.m index b08f5d30..fd3ddc8c 100644 --- a/Classes/BITRectangleImageAnnotation.m +++ b/Classes/BITRectangleImageAnnotation.m @@ -38,8 +38,7 @@ @interface BITRectangleImageAnnotation() @implementation BITRectangleImageAnnotation -- (id)initWithFrame:(CGRect)frame -{ +- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.shapeLayer = [CAShapeLayer layer]; @@ -79,7 +78,7 @@ - (void)layoutSubviews { [CATransaction commit]; } --(BOOL)resizable { +- (BOOL)resizable { return YES; } From 4465bb402f2a9a49c6cb987d379314ab3d83b3d2 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Mon, 9 Jun 2014 15:49:46 +0200 Subject: [PATCH 152/206] Add osVersion and osBuild to BITCrashDetails --- Classes/BITCrashDetails.h | 12 ++++++++++++ Classes/BITCrashDetails.m | 4 ++++ Classes/BITCrashDetailsPrivate.h | 2 ++ Classes/BITCrashManager.m | 26 +++++++++++++++++++++++++- 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Classes/BITCrashDetails.h b/Classes/BITCrashDetails.h index aa176453..989001c4 100644 --- a/Classes/BITCrashDetails.h +++ b/Classes/BITCrashDetails.h @@ -68,6 +68,18 @@ */ @property (nonatomic, readonly, strong) NSDate *crashTime; +/** + * Operation System version string the app was running on when it crashed. + */ +@property (nonatomic, readonly, strong) NSString *osVersion; + +/** + * Operation System build string the app was running on when it crashed + * + * This may be unavailable. + */ +@property (nonatomic, readonly, strong) NSString *osBuild; + /** * CFBundleVersion value of the app that crashed */ diff --git a/Classes/BITCrashDetails.m b/Classes/BITCrashDetails.m index b7f5b52c..73cfa80f 100644 --- a/Classes/BITCrashDetails.m +++ b/Classes/BITCrashDetails.m @@ -40,6 +40,8 @@ - (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier exceptionReason:(NSString *)exceptionReason appStartTime:(NSDate *)appStartTime crashTime:(NSDate *)crashTime + osVersion:(NSString *)osVersion + osBuild:(NSString *)osBuild appBuild:(NSString *)appBuild { if ((self = [super init])) { @@ -50,6 +52,8 @@ - (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier _exceptionReason = exceptionReason; _appStartTime = appStartTime; _crashTime = crashTime; + _osVersion = osVersion; + _osBuild = osBuild; _appBuild = appBuild; } return self; diff --git a/Classes/BITCrashDetailsPrivate.h b/Classes/BITCrashDetailsPrivate.h index 6c6fa241..1916f22f 100644 --- a/Classes/BITCrashDetailsPrivate.h +++ b/Classes/BITCrashDetailsPrivate.h @@ -41,6 +41,8 @@ extern NSString *const __attribute__((unused)) kBITCrashKillSignal; exceptionReason:(NSString *)exceptionReason appStartTime:(NSDate *)appStartTime crashTime:(NSDate *)crashTime + osVersion:(NSString *)osVersion + osBuild:(NSString *)osBuild appBuild:(NSString *)appBuild; @end diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index bb7190f2..c51ec54b 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -67,6 +67,7 @@ NSString *const kBITAppDidReceiveLowMemoryNotification = @"BITAppDidReceiveLowMemoryNotification"; NSString *const kBITAppVersion = @"BITAppVersion"; NSString *const kBITAppOSVersion = @"BITAppOSVersion"; +NSString *const kBITAppOSBuild = @"BITAppOSBuild"; NSString *const kBITAppUUIDs = @"BITAppUUIDs"; NSString *const kBITFakeCrashUUID = @"BITFakeCrashUUID"; @@ -478,6 +479,7 @@ - (void)appEnteredForeground { if (bundleVersion && [bundleVersion isKindOfClass:[NSString class]]) [[NSUserDefaults standardUserDefaults] setObject:bundleVersion forKey:kBITAppVersion]; [[NSUserDefaults standardUserDefaults] setObject:[[UIDevice currentDevice] systemVersion] forKey:kBITAppOSVersion]; + [[NSUserDefaults standardUserDefaults] setObject:[self osBuild] forKey:kBITAppOSBuild]; NSString *uuidString =[NSString stringWithFormat:@"%@", [self deviceArchitecture], @@ -508,6 +510,18 @@ - (NSString *)deviceArchitecture { return archName; } +- (NSString *)osBuild { + size_t size; + sysctlbyname("kern.osversion", NULL, &size, NULL, 0); + char *answer = (char*)malloc(size); + if (answer == NULL) + return nil; + sysctlbyname("kern.osversion", answer, &size, NULL, 0); + NSString *osBuild = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding]; + free(answer); + return osBuild; +} + /** * Get the userID from the delegate which should be stored with the crash report * @@ -807,6 +821,8 @@ - (void) handleCrashReport { exceptionReason:report.exceptionInfo.exceptionReason appStartTime:appStartTime crashTime:appCrashTime + osVersion:report.systemInfo.operatingSystemVersion + osBuild:report.systemInfo.operatingSystemBuild appBuild:report.applicationInfo.applicationVersion ]; } @@ -1098,6 +1114,12 @@ - (void)createCrashReportForAppKill { return; NSString *fakeReportOSVersion = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppOSVersion] ?: [[UIDevice currentDevice] systemVersion]; + NSString *fakeReportOSVersionString = fakeReportOSVersion; + NSString *fakeReportOSBuild = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppOSBuild] ?: [self osBuild]; + if (fakeReportOSBuild) { + fakeReportOSVersionString = [NSString stringWithFormat:@"%@ (%@)", fakeReportOSVersion, fakeReportOSBuild]; + } + NSString *fakeReportAppBundleIdentifier = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; NSString *fakeReportDeviceModel = [self getDevicePlatform] ?: @"Unknown"; NSString *fakeReportAppUUIDs = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppUUIDs] ?: @""; @@ -1123,7 +1145,7 @@ - (void)createCrashReportForAppKill { // we use the current date, since we don't know when the kill actually happened [fakeReportString appendFormat:@"Date/Time: %@\n", fakeCrashTimestamp]; - [fakeReportString appendFormat:@"OS Version: %@\n", fakeReportOSVersion]; + [fakeReportString appendFormat:@"OS Version: %@\n", fakeReportOSVersionString]; [fakeReportString appendString:@"Report Version: 104\n"]; [fakeReportString appendString:@"\n"]; [fakeReportString appendFormat:@"Exception Type: %@\n", fakeSignalName]; @@ -1156,6 +1178,8 @@ - (void)createCrashReportForAppKill { exceptionReason:nil appStartTime:nil crashTime:nil + osVersion:fakeReportOSVersion + osBuild:fakeReportOSBuild appBuild:fakeReportAppVersion ]; From f51a0a8b869a174325f31d120675f7691b784a38 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 10 Jun 2014 01:00:53 +0200 Subject: [PATCH 153/206] Fix using the correct meta filename when sending crash reports --- Classes/BITCrashManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index c51ec54b..4cc387a8 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -1269,7 +1269,7 @@ - (void)sendNextCrashReport { if (report.uuidRef != NULL) { crashUUID = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); } - metaFilename = [filename stringByAppendingPathExtension:@"meta"]; + metaFilename = [cacheFilename stringByAppendingPathExtension:@"meta"]; crashLogString = [BITCrashReportTextFormatter stringValueForCrashReport:report crashReporterKey:installString]; appBundleIdentifier = report.applicationInfo.applicationIdentifier; appBundleVersion = report.applicationInfo.applicationVersion; From 05e0e7ccd68ad699105428d5adef4eae8f605058 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 10 Jun 2014 12:36:41 +0200 Subject: [PATCH 154/206] Make sure checking for non approved crash reports also works when there is no crash in the last session --- Classes/BITCrashManager.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 4cc387a8..7ea67097 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -769,8 +769,6 @@ - (void) handleCrashReport { if (!self.plCrashReporter) return; - [self loadSettings]; - // check if the next call ran successfully the last time if (![_fileManager fileExistsAtPath:_analyzerInProgressFile]) { // mark the start of the routine @@ -988,6 +986,8 @@ - (void)startManager { [self registerObservers]; + [self loadSettings]; + if (!_isSetup) { static dispatch_once_t plcrPredicate; dispatch_once(&plcrPredicate, ^{ From 12682d9e3d77923ec24d69bdec5cee476817807f Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 10 Jun 2014 15:37:13 +0200 Subject: [PATCH 155/206] Fix handling of some edge cases in relation to sending approved crash reports --- Classes/BITCrashManager.m | 30 ++++++++++++++++++++---------- Classes/BITHTTPOperation.m | 1 + 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 7ea67097..a9500ecf 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -839,20 +839,22 @@ - (void) handleCrashReport { } /** - * Check if there are any crash reports available which the user did not approve yet - * - * @return `YES` if there are crash reports pending that are not approved, `NO` otherwise + Get the filename of the first not approved crash report + + @return NSString Filename of the first found not approved crash report */ -- (BOOL)hasNonApprovedCrashReports { - if ((!_approvedCrashReports || [_approvedCrashReports count] == 0) && [_crashFiles count] > 0) return YES; +- (NSString *)firstNotApprovedCrashReport { + if ((!_approvedCrashReports || [_approvedCrashReports count] == 0) && [_crashFiles count] > 0) { + return [_crashFiles objectAtIndex:0]; + } for (NSUInteger i=0; i < [_crashFiles count]; i++) { NSString *filename = [_crashFiles objectAtIndex:i]; - if (![_approvedCrashReports objectForKey:filename]) return YES; + if (![_approvedCrashReports objectForKey:filename]) return filename; } - return NO; + return nil; } /** @@ -935,9 +937,17 @@ - (void)invokeDelayedProcessing { if (!_sendingInProgress && [self hasPendingCrashReport]) { _sendingInProgress = YES; + + NSString *notApprovedReportFilename = [self firstNotApprovedCrashReport]; + + // this can happen in case there is a non approved crash report but it didn't happen in the previous app session + if (notApprovedReportFilename && !_lastCrashFilename) { + _lastCrashFilename = [notApprovedReportFilename lastPathComponent]; + } + if (!BITHockeyBundle()) { [self sendNextCrashReport]; - } else if (_crashManagerStatus != BITCrashManagerStatusAutoSend && [self hasNonApprovedCrashReports]) { + } else if (_crashManagerStatus != BITCrashManagerStatusAutoSend && notApprovedReportFilename) { if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillShowSubmitCrashReportAlert:)]) { [self.delegate crashManagerWillShowSubmitCrashReportAlert:self]; @@ -1335,6 +1345,8 @@ - (void)sendNextCrashReport { // store this crash report as user approved, so if it fails it will retry automatically [_approvedCrashReports setObject:[NSNumber numberWithBool:YES] forKey:filename]; + + [self saveSettings]; BITHockeyLog(@"INFO: Sending crash reports:\n%@", crashXML); [self sendCrashReportWithFilename:filename xml:crashXML attachment:attachment]; @@ -1342,8 +1354,6 @@ - (void)sendNextCrashReport { // we cannot do anything with this report, so delete it [self cleanCrashReportWithFilename:filename]; } - - [self saveSettings]; } diff --git a/Classes/BITHTTPOperation.m b/Classes/BITHTTPOperation.m index 322f9769..a9a95ff3 100644 --- a/Classes/BITHTTPOperation.m +++ b/Classes/BITHTTPOperation.m @@ -65,6 +65,7 @@ - (void) start { if (![[NSThread currentThread] isMainThread]) { [self performSelector:@selector(start) onThread:NSThread.mainThread withObject:nil waitUntilDone:NO]; + return; } if(self.isCancelled) { From de211b2d6ef13bb848627e2190fc3421970fad64 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 10 Jun 2014 15:53:21 +0200 Subject: [PATCH 156/206] Fix unit tests --- Classes/BITCrashManagerPrivate.h | 2 +- Support/HockeySDKTests/BITCrashManagerTests.m | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index f42062d0..9c9194f9 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -76,7 +76,7 @@ - (void)handleCrashReport; - (BOOL)hasPendingCrashReport; -- (BOOL)hasNonApprovedCrashReports; +- (NSString *)firstNotApprovedCrashReport; - (void)persistUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData; - (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename; diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index e2768130..0cb0840d 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -209,9 +209,9 @@ - (void)testHasPendingCrashReportWithNoFiles { assertThatBool([_sut hasPendingCrashReport], equalToBool(NO)); } -- (void)testHasNonApprovedCrashReportsWithNoFiles { +- (void)testFirstNotApprovedCrashReportWithNoFiles { _sut.crashManagerStatus = BITCrashManagerStatusAutoSend; - assertThatBool([_sut hasNonApprovedCrashReports], equalToBool(NO)); + assertThat([_sut firstNotApprovedCrashReport], equalTo(nil)); } @@ -247,7 +247,7 @@ - (void)testStartManagerWithAutoSend { // No files at startup assertThatBool([_sut hasPendingCrashReport], equalToBool(NO)); - assertThatBool([_sut hasNonApprovedCrashReports], equalToBool(NO)); + assertThat([_sut firstNotApprovedCrashReport], equalTo(nil)); [_sut invokeDelayedProcessing]; @@ -258,7 +258,7 @@ - (void)testStartManagerWithAutoSend { // we should have 0 pending crash report assertThatBool([_sut hasPendingCrashReport], equalToBool(NO)); - assertThatBool([_sut hasNonApprovedCrashReports], equalToBool(NO)); + assertThat([_sut firstNotApprovedCrashReport], equalTo(nil)); [_sut cleanCrashReports]; @@ -272,7 +272,7 @@ - (void)testStartManagerWithAutoSend { // we should have now 1 pending crash report assertThatBool([_sut hasPendingCrashReport], equalToBool(YES)); - assertThatBool([_sut hasNonApprovedCrashReports], equalToBool(YES)); + assertThat([_sut firstNotApprovedCrashReport], notNilValue()); // this is currently sending blindly, needs refactoring to test properly [_sut sendNextCrashReport]; @@ -287,7 +287,7 @@ - (void)testStartManagerWithAutoSend { // we should have now 1 pending crash report assertThatBool([_sut hasPendingCrashReport], equalToBool(YES)); - assertThatBool([_sut hasNonApprovedCrashReports], equalToBool(YES)); + assertThat([_sut firstNotApprovedCrashReport], notNilValue()); [_sut cleanCrashReports]; } From 42738397a6c0a9524f42380aea7b23d2f670f157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Spie=C3=9F?= Date: Wed, 11 Jun 2014 15:35:45 +0200 Subject: [PATCH 157/206] Replace initialiser return types of id with new instancetype --- Classes/BITAppStoreHeader.m | 2 +- Classes/BITAppVersionMetaInfo.m | 2 +- Classes/BITArrowImageAnnotation.m | 2 +- Classes/BITAttributedLabel.m | 4 ++-- Classes/BITBlurImageAnnotation.m | 2 +- Classes/BITCrashAttachment.m | 2 +- Classes/BITCrashManager.m | 2 +- Classes/BITFeedbackActivity.m | 2 +- Classes/BITFeedbackComposeViewController.m | 2 +- Classes/BITFeedbackListViewCell.m | 2 +- Classes/BITFeedbackManager.m | 2 +- Classes/BITFeedbackUserDataViewController.m | 2 +- Classes/BITHockeyBaseManager.m | 4 ++-- Classes/BITHockeyBaseManagerPrivate.h | 2 +- Classes/BITImageAnnotation.m | 2 +- Classes/BITRectangleImageAnnotation.m | 2 +- Classes/BITStoreButton.h | 4 ++-- Classes/BITStoreButton.m | 6 +++--- Classes/BITStoreUpdateManager.m | 2 +- Classes/BITUpdateManager.m | 2 +- Classes/BITUpdateViewController.m | 2 +- Classes/BITWebTableViewCell.m | 2 +- Support/HockeySDKTests/BITAuthenticatorTests.m | 2 +- 23 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Classes/BITAppStoreHeader.m b/Classes/BITAppStoreHeader.m index 08a48cc0..87a967f4 100644 --- a/Classes/BITAppStoreHeader.m +++ b/Classes/BITAppStoreHeader.m @@ -53,7 +53,7 @@ @implementation BITAppStoreHeader { #pragma mark - NSObject -- (id)initWithFrame:(CGRect)frame { +- (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { self.autoresizingMask = UIViewAutoresizingFlexibleWidth; self.backgroundColor = kWhiteBackgroundColorDefault; diff --git a/Classes/BITAppVersionMetaInfo.m b/Classes/BITAppVersionMetaInfo.m index 1689ac99..24b42160 100644 --- a/Classes/BITAppVersionMetaInfo.m +++ b/Classes/BITAppVersionMetaInfo.m @@ -139,7 +139,7 @@ - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:self.uuids forKey:@"uuids"]; } -- (id)initWithCoder:(NSCoder *)decoder { +- (instancetype)initWithCoder:(NSCoder *)decoder { if ((self = [super init])) { self.name = [decoder decodeObjectForKey:@"name"]; self.version = [decoder decodeObjectForKey:@"version"]; diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m index 2d739b4f..8e89bdac 100644 --- a/Classes/BITArrowImageAnnotation.m +++ b/Classes/BITArrowImageAnnotation.m @@ -40,7 +40,7 @@ @interface BITArrowImageAnnotation() @implementation BITArrowImageAnnotation -- (id)initWithFrame:(CGRect)frame +- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { diff --git a/Classes/BITAttributedLabel.m b/Classes/BITAttributedLabel.m index 6c2ddedf..64f9795d 100644 --- a/Classes/BITAttributedLabel.m +++ b/Classes/BITAttributedLabel.m @@ -240,7 +240,7 @@ @implementation BITAttributedLabel { @synthesize verticalAlignment = _verticalAlignment; @synthesize activeLink = _activeLink; -- (id)initWithFrame:(CGRect)frame { +- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (!self) { return nil; @@ -251,7 +251,7 @@ - (id)initWithFrame:(CGRect)frame { return self; } -- (id)initWithCoder:(NSCoder *)coder { +- (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (!self) { return nil; diff --git a/Classes/BITBlurImageAnnotation.m b/Classes/BITBlurImageAnnotation.m index 4760229d..b1bbea2e 100644 --- a/Classes/BITBlurImageAnnotation.m +++ b/Classes/BITBlurImageAnnotation.m @@ -39,7 +39,7 @@ @interface BITBlurImageAnnotation() @implementation BITBlurImageAnnotation -- (id)initWithFrame:(CGRect)frame +- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { diff --git a/Classes/BITCrashAttachment.m b/Classes/BITCrashAttachment.m index a4f2b17b..eca2946c 100644 --- a/Classes/BITCrashAttachment.m +++ b/Classes/BITCrashAttachment.m @@ -52,7 +52,7 @@ - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:self.contentType forKey:@"contentType"]; } -- (id)initWithCoder:(NSCoder *)decoder { +- (instancetype)initWithCoder:(NSCoder *)decoder { if ((self = [super init])) { _filename = [decoder decodeObjectForKey:@"filename"]; _crashAttachmentData = [decoder decodeObjectForKey:@"data"]; diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index bb7190f2..0584eb87 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -123,7 +123,7 @@ @implementation BITCrashManager { } -- (id)init { +- (instancetype)init { if ((self = [super init])) { _delegate = nil; _showAlwaysButton = YES; diff --git a/Classes/BITFeedbackActivity.m b/Classes/BITFeedbackActivity.m index 814dc0c6..a5876801 100644 --- a/Classes/BITFeedbackActivity.m +++ b/Classes/BITFeedbackActivity.m @@ -54,7 +54,7 @@ @implementation BITFeedbackActivity #pragma mark - NSObject -- (id)init { +- (instancetype)init { if ((self = [super init])) { _customActivityImage = nil; _customActivityTitle = nil; diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index d7dbb896..d8f9ed9a 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -76,7 +76,7 @@ @implementation BITFeedbackComposeViewController { #pragma mark - NSObject -- (id)init { +- (instancetype)init { self = [super init]; if (self) { self.title = BITHockeyLocalizedString(@"HockeyFeedbackComposeTitle"); diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 367721ac..9b0c1de1 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -76,7 +76,7 @@ @interface BITFeedbackListViewCell () @implementation BITFeedbackListViewCell -- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { // Initialization code diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 3357f92f..03b9b326 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -77,7 +77,7 @@ @implementation BITFeedbackManager { #pragma mark - Initialization -- (id)init { +- (instancetype)init { if ((self = [super init])) { _currentFeedbackListViewController = nil; _currentFeedbackComposeViewController = nil; diff --git a/Classes/BITFeedbackUserDataViewController.m b/Classes/BITFeedbackUserDataViewController.m index 2f6fed76..50d0536d 100644 --- a/Classes/BITFeedbackUserDataViewController.m +++ b/Classes/BITFeedbackUserDataViewController.m @@ -51,7 +51,7 @@ @interface BITFeedbackUserDataViewController () { @implementation BITFeedbackUserDataViewController -- (id)initWithStyle:(UITableViewStyle)style { +- (instancetype)initWithStyle:(UITableViewStyle)style { self = [super initWithStyle:style]; if (self) { self.title = BITHockeyLocalizedString(@"HockeyFeedbackUserDataTitle"); diff --git a/Classes/BITHockeyBaseManager.m b/Classes/BITHockeyBaseManager.m index a81f9cc3..1f21ff69 100644 --- a/Classes/BITHockeyBaseManager.m +++ b/Classes/BITHockeyBaseManager.m @@ -54,7 +54,7 @@ @implementation BITHockeyBaseManager { } -- (id)init { +- (instancetype)init { if ((self = [super init])) { _serverURL = BITHOCKEYSDK_URL; @@ -75,7 +75,7 @@ - (id)init { return self; } -- (id)initWithAppIdentifier:(NSString *)appIdentifier isAppStoreEnvironment:(BOOL)isAppStoreEnvironment { +- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier isAppStoreEnvironment:(BOOL)isAppStoreEnvironment { if ((self = [self init])) { _appIdentifier = appIdentifier; _isAppStoreEnvironment = isAppStoreEnvironment; diff --git a/Classes/BITHockeyBaseManagerPrivate.h b/Classes/BITHockeyBaseManagerPrivate.h index 938d55e0..2114d222 100644 --- a/Classes/BITHockeyBaseManagerPrivate.h +++ b/Classes/BITHockeyBaseManagerPrivate.h @@ -36,7 +36,7 @@ @property (nonatomic, strong) NSString *appIdentifier; -- (id)initWithAppIdentifier:(NSString *)appIdentifier isAppStoreEnvironment:(BOOL)isAppStoreEnvironment; +- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier isAppStoreEnvironment:(BOOL)isAppStoreEnvironment; - (void)startManager; diff --git a/Classes/BITImageAnnotation.m b/Classes/BITImageAnnotation.m index 428e8b11..0750154d 100644 --- a/Classes/BITImageAnnotation.m +++ b/Classes/BITImageAnnotation.m @@ -30,7 +30,7 @@ @implementation BITImageAnnotation -- (id)initWithFrame:(CGRect)frame +- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { diff --git a/Classes/BITRectangleImageAnnotation.m b/Classes/BITRectangleImageAnnotation.m index b08f5d30..61ad913a 100644 --- a/Classes/BITRectangleImageAnnotation.m +++ b/Classes/BITRectangleImageAnnotation.m @@ -38,7 +38,7 @@ @interface BITRectangleImageAnnotation() @implementation BITRectangleImageAnnotation -- (id)initWithFrame:(CGRect)frame +- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { diff --git a/Classes/BITStoreButton.h b/Classes/BITStoreButton.h index 207b109b..4368ca00 100644 --- a/Classes/BITStoreButton.h +++ b/Classes/BITStoreButton.h @@ -71,8 +71,8 @@ typedef NS_ENUM(NSUInteger, BITStoreButtonStyle) { // The interface is flexible, so there is now fixed order @interface BITStoreButton : UIButton -- (id)initWithFrame:(CGRect)frame; -- (id)initWithPadding:(CGPoint)padding style:(BITStoreButtonStyle)style; +- (instancetype)initWithFrame:(CGRect)frame; +- (instancetype)initWithPadding:(CGPoint)padding style:(BITStoreButtonStyle)style; // action delegate @property (nonatomic, weak) id buttonDelegate; diff --git a/Classes/BITStoreButton.m b/Classes/BITStoreButton.m index cf4f0be3..d48d2b22 100644 --- a/Classes/BITStoreButton.m +++ b/Classes/BITStoreButton.m @@ -43,7 +43,7 @@ @implementation BITStoreButtonData #pragma mark - NSObject -- (id)initWithLabel:(NSString*)aLabel enabled:(BOOL)flag { +- (instancetype)initWithLabel:(NSString*)aLabel enabled:(BOOL)flag { if ((self = [super init])) { self.label = aLabel; self.enabled = flag; @@ -152,7 +152,7 @@ - (void)alignToSuperview { #pragma mark - NSObject -- (id)initWithFrame:(CGRect)frame { +- (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { self.layer.needsDisplayOnBoundsChange = YES; @@ -167,7 +167,7 @@ - (id)initWithFrame:(CGRect)frame { return self; } -- (id)initWithPadding:(CGPoint)padding style:(BITStoreButtonStyle)style { +- (instancetype)initWithPadding:(CGPoint)padding style:(BITStoreButtonStyle)style { CGRect frame = CGRectMake(0, 0, 40, BIT_MIN_HEIGHT); if ((self = [self initWithFrame:frame])) { _customPadding = padding; diff --git a/Classes/BITStoreUpdateManager.m b/Classes/BITStoreUpdateManager.m index 78a0bcef..6b5711fa 100644 --- a/Classes/BITStoreUpdateManager.m +++ b/Classes/BITStoreUpdateManager.m @@ -106,7 +106,7 @@ - (void) unregisterObservers { #pragma mark - Init -- (id)init { +- (instancetype)init { if ((self = [super init])) { _checkInProgress = NO; _updateAvailable = NO; diff --git a/Classes/BITUpdateManager.m b/Classes/BITUpdateManager.m index 61d1b6b0..40371ba1 100644 --- a/Classes/BITUpdateManager.m +++ b/Classes/BITUpdateManager.m @@ -386,7 +386,7 @@ - (void)saveAppCache { #pragma mark - Init -- (id)init { +- (instancetype)init { if ((self = [super init])) { _delegate = nil; _expiryDate = nil; diff --git a/Classes/BITUpdateViewController.m b/Classes/BITUpdateViewController.m index 41633df0..28c99f64 100644 --- a/Classes/BITUpdateViewController.m +++ b/Classes/BITUpdateViewController.m @@ -230,7 +230,7 @@ - (void)configureWebCell:(BITWebTableViewCell *)cell forAppVersion:(BITAppVersio #pragma mark - Init -- (id)initWithStyle:(UITableViewStyle)style { +- (instancetype)initWithStyle:(UITableViewStyle)style { if ((self = [super initWithStyle:UITableViewStylePlain])) { _updateManager = [BITHockeyManager sharedHockeyManager].updateManager ; _isAppStoreEnvironment = [BITHockeyManager sharedHockeyManager].isAppStoreEnvironment; diff --git a/Classes/BITWebTableViewCell.m b/Classes/BITWebTableViewCell.m index 3435bb81..9a93b210 100644 --- a/Classes/BITWebTableViewCell.m +++ b/Classes/BITWebTableViewCell.m @@ -118,7 +118,7 @@ - (void)setWebViewContent:(NSString *)aWebViewContent { #pragma mark - NSObject -- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { self.cellBackgroundColor = [UIColor clearColor]; } diff --git a/Support/HockeySDKTests/BITAuthenticatorTests.m b/Support/HockeySDKTests/BITAuthenticatorTests.m index 023d7302..a96f8fba 100644 --- a/Support/HockeySDKTests/BITAuthenticatorTests.m +++ b/Support/HockeySDKTests/BITAuthenticatorTests.m @@ -33,7 +33,7 @@ @interface MyDeviceWithIdentifierForVendor : MyDevice @end @implementation MyDeviceWithIdentifierForVendor -- (id)init { +- (instancetype)init { self = [super init]; if( self ) { _identifierForVendor = [NSUUID UUID]; From 67f63c118f5f7ddda8a7b3cdb710a8c1f0837bc4 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 11 Jun 2014 22:08:18 +0200 Subject: [PATCH 158/206] Add missing super call --- Classes/BITImageAnnotationViewController.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index 61077c16..ed8aeff6 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -132,6 +132,7 @@ - (void)viewWillAppear:(BOOL)animated { - (void)viewWillDisappear:(BOOL)animated { [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; + [super viewWillDisappear:animated]; } - (BOOL)prefersStatusBarHidden { From 760a8d07b8901840c7263aa3e31da50102337a14 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 12 Jun 2014 14:39:16 +0200 Subject: [PATCH 159/206] Improve fetching the optimal icon of an app for the update view --- Classes/BITHockeyBaseManager.m | 20 +--- Classes/BITHockeyHelper.h | 4 + Classes/BITHockeyHelper.m | 108 ++++++++++++++++++ Classes/BITUpdateViewController.m | 58 ++-------- Support/HockeySDK.xcodeproj/project.pbxproj | 8 ++ Support/HockeySDKTests/BITHockeyHelperTests.m | 58 ++++++++++ Support/HockeySDKTests/Fixtures/AppIcon.png | Bin 0 -> 2790 bytes .../HockeySDKTests/Fixtures/AppIcon@2x.png | Bin 0 -> 3628 bytes 8 files changed, 189 insertions(+), 67 deletions(-) create mode 100644 Support/HockeySDKTests/Fixtures/AppIcon.png create mode 100644 Support/HockeySDKTests/Fixtures/AppIcon@2x.png diff --git a/Classes/BITHockeyBaseManager.m b/Classes/BITHockeyBaseManager.m index 1f21ff69..334daa7e 100644 --- a/Classes/BITHockeyBaseManager.m +++ b/Classes/BITHockeyBaseManager.m @@ -99,25 +99,7 @@ - (NSString *)encodedAppIdentifier { } - (BOOL)isPreiOS7Environment { - static BOOL isPreiOS7Environment = YES; - static dispatch_once_t checkOS; - - dispatch_once(&checkOS, ^{ - // we only perform this runtime check if this is build against at least iOS7 base SDK -#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 - // runtime check according to - // https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { - isPreiOS7Environment = YES; - } else { - isPreiOS7Environment = NO; - } -#else - isPreiOS7Environment = YES; -#endif - }); - - return isPreiOS7Environment; + return bit_isPreiOS7Environment(); } - (NSString *)getDevicePlatform { diff --git a/Classes/BITHockeyHelper.h b/Classes/BITHockeyHelper.h index a508dc64..9a7e1ecf 100644 --- a/Classes/BITHockeyHelper.h +++ b/Classes/BITHockeyHelper.h @@ -46,6 +46,10 @@ NSString *bit_appName(NSString *placeHolderString); NSString *bit_UUIDPreiOS6(void); NSString *bit_UUID(void); NSString *bit_appAnonID(void); +BOOL bit_isPreiOS7Environment(void); + +NSString *bit_validAppIconStringFromIcons(NSArray *icons); +NSString *bit_validAppIconFilename(NSBundle *bundle); /* UIImage helpers */ UIImage *bit_roundedCornerImage(UIImage *inputImage, NSInteger cornerSize, NSInteger borderSize); diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 0ab661e5..da8c13bd 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -216,6 +216,114 @@ NSComparisonResult bit_versionCompare(NSString *stringA, NSString *stringB) { return appAnonID; } +BOOL bit_isPreiOS7Environment(void) { + static BOOL isPreiOS7Environment = YES; + static dispatch_once_t checkOS; + + dispatch_once(&checkOS, ^{ + // we only perform this runtime check if this is build against at least iOS7 base SDK +#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 + // runtime check according to + // https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { + isPreiOS7Environment = YES; + } else { + isPreiOS7Environment = NO; + } +#else + isPreiOS7Environment = YES; +#endif + }); + + return isPreiOS7Environment; +} + +/** + Find a valid app icon filename that points to a proper app icon image + + @param icons NSArray with app icon filenames + + @return NSString with the valid app icon or nil if none found + */ +NSString *bit_validAppIconStringFromIcons(NSArray *icons) { + if (!icons) return nil; + if (![icons isKindOfClass:[NSArray class]]) return nil; + + BOOL useHighResIcon = NO; + BOOL useiPadIcon = NO; + if ([UIScreen mainScreen].scale == 2.0f) useHighResIcon = YES; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) useiPadIcon = YES; + + NSString *currentBestMatch = nil; + float currentBestMatchHeight = 0; + float bestMatchHeight = 0; + + if (bit_isPreiOS7Environment()) { + bestMatchHeight = useiPadIcon ? (useHighResIcon ? 144 : 72) : (useHighResIcon ? 114 : 57); + } else { + bestMatchHeight = useiPadIcon ? (useHighResIcon ? 152 : 76) : 120; + } + + for(NSString *icon in icons) { + // Don't use imageNamed, otherwise unit tests won't find the fixture icon + // and using imageWithContentsOfFile doesn't load @2x files with absolut paths (required in tests) + NSData *imgData = [[NSData alloc] initWithContentsOfFile:icon]; + UIImage *iconImage = [[UIImage alloc] initWithData:imgData]; + + if (iconImage) { + if (iconImage.size.height == bestMatchHeight) { + return icon; + } else if (iconImage.size.height < bestMatchHeight && + iconImage.size.height > currentBestMatchHeight) { + currentBestMatchHeight = iconImage.size.height; + currentBestMatch = icon; + } + } + } + + return currentBestMatch; +} + +NSString *bit_validAppIconFilename(NSBundle *bundle) { + NSString *iconFilename = nil; + NSArray *icons = nil; + + icons = [bundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]; + iconFilename = bit_validAppIconStringFromIcons(icons); + + if (!iconFilename) { + icons = [bundle objectForInfoDictionaryKey:@"CFBundleIcons"]; + if (icons && [icons isKindOfClass:[NSDictionary class]]) { + icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; + } + iconFilename = bit_validAppIconStringFromIcons(icons); + } + + // we test iPad structure anyway and use it if we find a result and don't have another one yet + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + icons = [bundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]; + if (icons && [icons isKindOfClass:[NSDictionary class]]) { + icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; + } + NSString *iPadIconFilename = bit_validAppIconStringFromIcons(icons); + if (iPadIconFilename && !iconFilename) { + iconFilename = iPadIconFilename; + } + } + + if (!iconFilename) { + NSString *tempFilename = [bundle objectForInfoDictionaryKey:@"CFBundleIconFile"]; + if (tempFilename) { + iconFilename = bit_validAppIconStringFromIcons(@[tempFilename]); + } + } + + if (!iconFilename) { + iconFilename = bit_validAppIconStringFromIcons(@[@"Icon.png"]); + } + + return iconFilename; +} #pragma mark UIImage private helpers diff --git a/Classes/BITUpdateViewController.m b/Classes/BITUpdateViewController.m index 28c99f64..e75e22b1 100644 --- a/Classes/BITUpdateViewController.m +++ b/Classes/BITUpdateViewController.m @@ -294,59 +294,21 @@ - (void)viewDidLoad { } [self updateAppStoreHeader]; - NSString *iconString = nil; - NSArray *icons = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFiles"]; - if (!icons) { - icons = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIcons"]; - if ((icons) && ([icons isKindOfClass:[NSDictionary class]])) { - icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; + NSString *iconFilename = bit_validAppIconFilename([NSBundle mainBundle]); + if (iconFilename) { + BOOL addGloss = YES; + NSNumber *prerendered = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIPrerenderedIcon"]; + if (prerendered) { + addGloss = ![prerendered boolValue]; } - if (!icons) { - iconString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFile"]; - if (!iconString) { - iconString = @"Icon.png"; - } - } - } - - if (icons) { - BOOL useHighResIcon = NO; - BOOL useiPadIcon = NO; - if ([UIScreen mainScreen].scale == 2.0f) useHighResIcon = YES; - if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) useiPadIcon = YES; - - for(NSString *icon in icons) { - iconString = icon; - UIImage *iconImage = [UIImage imageNamed:icon]; - - if ( - (iconImage.size.height == 57 && !useHighResIcon && !useiPadIcon) || - (iconImage.size.height == 114 && useHighResIcon && !useiPadIcon) || - (iconImage.size.height == 120 && useHighResIcon && !useiPadIcon) || - (iconImage.size.height == 72 && !useHighResIcon && useiPadIcon) || - (iconImage.size.height == 76 && !useHighResIcon && useiPadIcon) || - (iconImage.size.height == 144 && !useHighResIcon && useiPadIcon) || - (iconImage.size.height == 152 && useHighResIcon && useiPadIcon) - ) { - // found! - break; - } + if (addGloss && [self.updateManager isPreiOS7Environment]) { + _appStoreHeader.iconImage = [self addGlossToImage:[UIImage imageNamed:iconFilename]]; + } else { + _appStoreHeader.iconImage = [UIImage imageNamed:iconFilename]; } } - BOOL addGloss = YES; - NSNumber *prerendered = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIPrerenderedIcon"]; - if (prerendered) { - addGloss = ![prerendered boolValue]; - } - - if (addGloss && [self.updateManager isPreiOS7Environment]) { - _appStoreHeader.iconImage = [self addGlossToImage:[UIImage imageNamed:iconString]]; - } else { - _appStoreHeader.iconImage = [UIImage imageNamed:iconString]; - } - self.tableView.tableHeaderView = _appStoreHeader; BITStoreButtonStyle buttonStyle = BITStoreButtonStyleDefault; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 1115044e..7e4c4843 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ 1E1127C916580C87007067A2 /* buttonRoundedRegular@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E1127C116580C87007067A2 /* buttonRoundedRegular@2x.png */; }; 1E1127CA16580C87007067A2 /* buttonRoundedRegularHighlighted.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E1127C216580C87007067A2 /* buttonRoundedRegularHighlighted.png */; }; 1E1127CB16580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E1127C316580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png */; }; + 1E494AEC19491943001EFF74 /* AppIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E494AEA19491943001EFF74 /* AppIcon.png */; }; + 1E494AED19491943001EFF74 /* AppIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E494AEB19491943001EFF74 /* AppIcon@2x.png */; }; 1E49A43C1612223B00463151 /* BITFeedbackComposeViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E49A42D1612223B00463151 /* BITFeedbackComposeViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1E49A43F1612223B00463151 /* BITFeedbackComposeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E49A42E1612223B00463151 /* BITFeedbackComposeViewController.m */; }; 1E49A4421612223B00463151 /* BITFeedbackListViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E49A42F1612223B00463151 /* BITFeedbackListViewCell.h */; }; @@ -226,6 +228,8 @@ 1E1127C316580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "buttonRoundedRegularHighlighted@2x.png"; sourceTree = ""; }; 1E20A57F181E9D4600D5B770 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/HockeySDK.strings; sourceTree = ""; }; 1E36D8B816667611000B134C /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/HockeySDK.strings; sourceTree = ""; }; + 1E494AEA19491943001EFF74 /* AppIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AppIcon.png; sourceTree = ""; }; + 1E494AEB19491943001EFF74 /* AppIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon@2x.png"; sourceTree = ""; }; 1E49A42D1612223B00463151 /* BITFeedbackComposeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackComposeViewController.h; sourceTree = ""; }; 1E49A42E1612223B00463151 /* BITFeedbackComposeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITFeedbackComposeViewController.m; sourceTree = ""; }; 1E49A42F1612223B00463151 /* BITFeedbackListViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackListViewCell.h; sourceTree = ""; }; @@ -601,6 +605,8 @@ 1EA1170216F53B49001C015C /* Fixtures */ = { isa = PBXGroup; children = ( + 1E494AEA19491943001EFF74 /* AppIcon.png */, + 1E494AEB19491943001EFF74 /* AppIcon@2x.png */, 1EA1170316F53B49001C015C /* StoreBundleIdentifierUnknown.json */, 1EA1170816F53E3A001C015C /* StoreBundleIdentifierKnown.json */, 1E70A22F17F2F982001BB32D /* live_report_empty.plcrash */, @@ -949,12 +955,14 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E494AEC19491943001EFF74 /* AppIcon.png in Resources */, 1EA1170C16F54A64001C015C /* HockeySDKResources.bundle in Resources */, 1E5A459B16F0DFC200B55C04 /* InfoPlist.strings in Resources */, 1E70A23417F2F982001BB32D /* live_report_signal.plcrash in Resources */, 1EA1170416F53B49001C015C /* StoreBundleIdentifierUnknown.json in Resources */, 1E70A23317F2F982001BB32D /* live_report_exception.plcrash in Resources */, 1E70A23217F2F982001BB32D /* live_report_empty.plcrash in Resources */, + 1E494AED19491943001EFF74 /* AppIcon@2x.png in Resources */, 1EA1170916F53E3A001C015C /* StoreBundleIdentifierKnown.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Support/HockeySDKTests/BITHockeyHelperTests.m b/Support/HockeySDKTests/BITHockeyHelperTests.m index 315a7c42..3cd800ad 100644 --- a/Support/HockeySDKTests/BITHockeyHelperTests.m +++ b/Support/HockeySDKTests/BITHockeyHelperTests.m @@ -90,5 +90,63 @@ - (void)testAppAnonID { assertThatInteger([resultString length], equalToInteger(36)); } +- (void)testValidAppIconFilename { + NSString *resultString = nil; + NSBundle *mockBundle = mock([NSBundle class]); + NSString *validIconPath = [[NSBundle bundleForClass:self.class] pathForResource:@"AppIcon" ofType:@"png"]; + NSString *validIconPath2x = [[NSBundle bundleForClass:self.class] pathForResource:@"AppIcon@2x" ofType:@"png"]; + + // No valid icons defined at all + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:@"invalidFilename.png"]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, nilValue()); + + // CFBundleIconFiles contains valid filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[validIconPath, validIconPath2x]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); + + // CFBundleIcons contains valid dictionary filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + // CFBundleIcons contains valid ipad dictionary and valid default dictionary filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); + + // CFBundleIcons contains valid filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@[validIconPath, validIconPath2x]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); + + // CFBundleIcon contains valid filename + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:validIconPath]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); +} + @end diff --git a/Support/HockeySDKTests/Fixtures/AppIcon.png b/Support/HockeySDKTests/Fixtures/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..5473e5e8de977643534715d284906b506c1c58ab GIT binary patch literal 2790 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 literal 0 HcmV?d00001 From deee2db37e29eefddb9890bae468c92ad6061ddc Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 12 Jun 2014 16:10:09 +0200 Subject: [PATCH 160/206] Expose public screenshot method This is useful when trying to connect a compose view that is using a custom UIViewController via the delegate --- Classes/BITFeedbackManager.h | 9 +++++++++ Classes/BITFeedbackManager.m | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index fc84da1b..a67ab7a3 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -267,6 +267,15 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { */ @property (nonatomic, readwrite) BOOL showFirstRequiredPresentationModal; + +/** + Return a screenshot UIImage intance from the current visiable screen + + @return UIImage instance containing a screenshot of the current screen + */ +- (UIImage *)screenshot; + + /** Present the modal feedback list user interface. */ diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 03b9b326..1a98ce9f 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -198,6 +198,10 @@ - (NSString *)uuidAsLowerCaseAndShortened { #pragma mark - Feedback Modal UI +- (UIImage *)screenshot { + return bit_screenshot(); +} + - (BITFeedbackListViewController *)feedbackListViewController:(BOOL)modal { if ([self isPreiOS7Environment]) { return [[BITFeedbackListViewController alloc] initWithModalStyle:modal]; From 4226ac0f2059f5ff93ba6fc6734e2454dcaf81ca Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sat, 14 Jun 2014 01:32:53 +0200 Subject: [PATCH 161/206] Properly consider paragraphs in release notes --- Classes/BITWebTableViewCell.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITWebTableViewCell.m b/Classes/BITWebTableViewCell.m index 9a93b210..22fa446c 100644 --- a/Classes/BITWebTableViewCell.m +++ b/Classes/BITWebTableViewCell.m @@ -38,7 +38,7 @@ @implementation BITWebTableViewCell \ \ \ \ \ From 423915cb0c4c6d326291c8372e5af1c3e7e41e19 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sat, 14 Jun 2014 17:22:03 +0200 Subject: [PATCH 162/206] Add option to include satusbar in screenshots All mehotds in BITFeedbackManager that are screenshot releated are changed to include an option that defines wether the screenshot should include the statusbar (if present) or not: - `- (UIImage *)screenshot` has been changed to `- (UIImage *)screenshotWithStatusBar:(BOOL)includeStatusBar)` - `- (void)showFeedbackComposeViewWithGeneratedScreenshot` has been changed to `- (void)showFeedbackComposeViewWithGeneratedScreenshotWithStatusBar:(BOOL)includeStatusBar` --- Classes/BITFeedbackManager.h | 8 ++++++-- Classes/BITFeedbackManager.m | 10 +++++----- Classes/BITHockeyHelper.h | 2 +- Classes/BITHockeyHelper.m | 7 +++++-- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index a67ab7a3..777ed23e 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -270,10 +270,12 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { /** Return a screenshot UIImage intance from the current visiable screen + + @param includeStatusBar BOOL if the screenshot should include the satusbar (if present) or not @return UIImage instance containing a screenshot of the current screen */ -- (UIImage *)screenshot; +- (UIImage *)screenshotWithStatusBar:(BOOL)includeStatusBar; /** @@ -315,8 +317,10 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { [[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot]; @see feedbackObservationMode + + @param includeStatusBar BOOL if the screenshot should include the satusbar (if present) or not */ -- (void)showFeedbackComposeViewWithGeneratedScreenshot; +- (void)showFeedbackComposeViewWithGeneratedScreenshotWithStatusBar:(BOOL)includeStatusBar; /** diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 1a98ce9f..f708bf72 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -198,8 +198,8 @@ - (NSString *)uuidAsLowerCaseAndShortened { #pragma mark - Feedback Modal UI -- (UIImage *)screenshot { - return bit_screenshot(); +- (UIImage *)screenshotWithStatusBar:(BOOL)includeStatusBar { + return bit_screenshot(includeStatusBar); } - (BITFeedbackListViewController *)feedbackListViewController:(BOOL)modal { @@ -243,8 +243,8 @@ - (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items{ } -- (void)showFeedbackComposeViewWithGeneratedScreenshot { - UIImage *screenshot = bit_screenshot(); +- (void)showFeedbackComposeViewWithGeneratedScreenshotWithStatusBar:(BOOL)includeStatusBar { + UIImage *screenshot = bit_screenshot(includeStatusBar); [self showFeedbackComposeViewWithPreparedItems:@[screenshot]]; } @@ -1132,7 +1132,7 @@ -(void)extractLastPictureFromLibraryAndLaunchFeedback { - (void)screenshotTripleTap:(UITapGestureRecognizer *)tapRecognizer { if (tapRecognizer.state == UIGestureRecognizerStateRecognized){ - [self showFeedbackComposeViewWithGeneratedScreenshot]; + [self showFeedbackComposeViewWithGeneratedScreenshotWithStatusBar:NO]; } } diff --git a/Classes/BITHockeyHelper.h b/Classes/BITHockeyHelper.h index 9a7e1ecf..9348d183 100644 --- a/Classes/BITHockeyHelper.h +++ b/Classes/BITHockeyHelper.h @@ -59,5 +59,5 @@ UIImage *bit_reflectedImageWithHeight(UIImage *inputImage, NSUInteger height, fl UIImage *bit_newWithContentsOfResolutionIndependentFile(NSString * path); UIImage *bit_imageWithContentsOfResolutionIndependentFile(NSString * path); UIImage *bit_imageNamed(NSString *imageName, NSString *bundleName); -UIImage *bit_screenshot(void); +UIImage *bit_screenshot(BOOL includeStatusBar); UIImage *bit_appIcon(void); diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index da8c13bd..d62b0d1a 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -690,7 +690,7 @@ BOOL bit_hasAlpha(UIImage *inputImage) { } } -UIImage *bit_screenshot() { +UIImage *bit_screenshot(BOOL includeStatusBar) { // Create a graphics context with the target size CGSize imageSize = [[UIScreen mainScreen] bounds].size; BOOL isLandscapeLeft = [UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationLandscapeLeft; @@ -722,7 +722,10 @@ BOOL bit_hasAlpha(UIImage *inputImage) { CGContextConcatCTM(context, [window transform]); // Y-offset for the status bar (if it's showing) - NSInteger yOffset = [UIApplication sharedApplication].statusBarHidden ? 0 : -20; + NSInteger yOffset = 0; + if (!includeStatusBar) { + yOffset = [UIApplication sharedApplication].statusBarHidden ? 0 : -20; + } // Offset by the portion of the bounds left of and above the anchor point CGContextTranslateCTM(context, From fbfd4ae0fbe0db148372baf4e23710e042409db8 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Mon, 16 Jun 2014 13:45:02 +0200 Subject: [PATCH 163/206] Include statusbar in screenshots by default --- Classes/BITFeedbackManager.h | 4 +--- Classes/BITFeedbackManager.m | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 777ed23e..e497f5d2 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -317,10 +317,8 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { [[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot]; @see feedbackObservationMode - - @param includeStatusBar BOOL if the screenshot should include the satusbar (if present) or not */ -- (void)showFeedbackComposeViewWithGeneratedScreenshotWithStatusBar:(BOOL)includeStatusBar; +- (void)showFeedbackComposeViewWithGeneratedScreenshot; /** diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index f708bf72..a5043c1b 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -243,8 +243,8 @@ - (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items{ } -- (void)showFeedbackComposeViewWithGeneratedScreenshotWithStatusBar:(BOOL)includeStatusBar { - UIImage *screenshot = bit_screenshot(includeStatusBar); +- (void)showFeedbackComposeViewWithGeneratedScreenshot { + UIImage *screenshot = bit_screenshot(YES); [self showFeedbackComposeViewWithPreparedItems:@[screenshot]]; } @@ -1132,7 +1132,7 @@ -(void)extractLastPictureFromLibraryAndLaunchFeedback { - (void)screenshotTripleTap:(UITapGestureRecognizer *)tapRecognizer { if (tapRecognizer.state == UIGestureRecognizerStateRecognized){ - [self showFeedbackComposeViewWithGeneratedScreenshotWithStatusBar:NO]; + [self showFeedbackComposeViewWithGeneratedScreenshot]; } } From 83b32ffe37656cfbd3aa2976d5da2507e19afbc8 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 18 Jun 2014 15:52:56 +0200 Subject: [PATCH 164/206] Make individual components delegate private The `delegate` property should only be set on `[BITHockeyManager sharedHockeyManager]` which will then propagade it to all components. Setting the delegate individually per component could cause undefined behaviour. --- Classes/BITAuthenticator.h | 12 ------------ Classes/BITAuthenticator_Private.h | 12 ++++++++++++ Classes/BITCrashManager.h | 15 --------------- Classes/BITCrashManagerPrivate.h | 15 +++++++++++++++ Classes/BITFeedbackManager.h | 17 ----------------- Classes/BITFeedbackManagerPrivate.h | 17 +++++++++++++++++ Classes/BITStoreUpdateManager.h | 10 ---------- Classes/BITStoreUpdateManagerPrivate.h | 10 ++++++++++ Classes/BITUpdateManager.h | 16 ---------------- Classes/BITUpdateManagerPrivate.h | 15 +++++++++++++++ 10 files changed, 69 insertions(+), 70 deletions(-) diff --git a/Classes/BITAuthenticator.h b/Classes/BITAuthenticator.h index 2a96e49e..1653de78 100644 --- a/Classes/BITAuthenticator.h +++ b/Classes/BITAuthenticator.h @@ -198,18 +198,6 @@ typedef NS_ENUM(NSUInteger, BITAuthenticatorAppRestrictionEnforcementFrequency) */ @property (nonatomic, copy) NSString *authenticationSecret; -/** - Delegate that can be used to do any last minute configurations on the - presented viewController. - - The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You - should not need to set this delegate individually. - - @see `[BITHockeyManager setDelegate:]` - @see BITAuthenticatorDelegate - */ -@property (nonatomic, weak) id delegate; - #pragma mark - Device based identification diff --git a/Classes/BITAuthenticator_Private.h b/Classes/BITAuthenticator_Private.h index 2836b92b..960279ad 100644 --- a/Classes/BITAuthenticator_Private.h +++ b/Classes/BITAuthenticator_Private.h @@ -34,6 +34,18 @@ @interface BITAuthenticator () +/** + Delegate that can be used to do any last minute configurations on the + presented viewController. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` + @see BITAuthenticatorDelegate + */ +@property (nonatomic, weak) id delegate; + /** * must be set */ diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index a3dc213b..fdc8e2f2 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -157,21 +157,6 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { @interface BITCrashManager : BITHockeyBaseManager -///----------------------------------------------------------------------------- -/// @name Delegate -///----------------------------------------------------------------------------- - -/** - Sets the optional `BITCrashManagerDelegate` delegate. - - The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You - should not need to set this delegate individually. - - @see `[BITHockeyManager setDelegate:]` - */ -@property (nonatomic, weak) id delegate; - - ///----------------------------------------------------------------------------- /// @name Configuration ///----------------------------------------------------------------------------- diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index 9c9194f9..ae45e632 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -38,6 +38,21 @@ @interface BITCrashManager () { } + +///----------------------------------------------------------------------------- +/// @name Delegate +///----------------------------------------------------------------------------- + +/** + Sets the optional `BITCrashManagerDelegate` delegate. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` + */ +@property (nonatomic, weak) id delegate; + /** * must be set */ diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index e497f5d2..c86bd46a 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -136,23 +136,6 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { @interface BITFeedbackManager : BITHockeyBaseManager -///----------------------------------------------------------------------------- -/// @name Delegate -///----------------------------------------------------------------------------- - -/** - Sets the `BITFeedbackManagerDelegate` delegate. - - Can be set to be notified when new feedback is received from the server. - - The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You - should not need to set this delegate individually. - - @see `[BITHockeyManager setDelegate:]` - */ -@property (nonatomic, weak) id delegate; - - ///----------------------------------------------------------------------------- /// @name General settings ///----------------------------------------------------------------------------- diff --git a/Classes/BITFeedbackManagerPrivate.h b/Classes/BITFeedbackManagerPrivate.h index c7ba4330..23ae1d98 100644 --- a/Classes/BITFeedbackManagerPrivate.h +++ b/Classes/BITFeedbackManagerPrivate.h @@ -36,6 +36,23 @@ } +///----------------------------------------------------------------------------- +/// @name Delegate +///----------------------------------------------------------------------------- + +/** + Sets the `BITFeedbackManagerDelegate` delegate. + + Can be set to be notified when new feedback is received from the server. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` + */ +@property (nonatomic, weak) id delegate; + + @property (nonatomic, strong) NSMutableArray *feedbackList; @property (nonatomic, strong) NSString *token; diff --git a/Classes/BITStoreUpdateManager.h b/Classes/BITStoreUpdateManager.h index 4ba81331..1b081b71 100644 --- a/Classes/BITStoreUpdateManager.h +++ b/Classes/BITStoreUpdateManager.h @@ -75,16 +75,6 @@ typedef NS_ENUM(NSInteger, BITStoreUpdateSetting) { @interface BITStoreUpdateManager : BITHockeyBaseManager -///----------------------------------------------------------------------------- -/// @name Delegate -///----------------------------------------------------------------------------- - -/** - Sets the optional `BITStoreUpdateManagerDelegate` delegate. - */ -@property (nonatomic, weak) id delegate; - - ///----------------------------------------------------------------------------- /// @name Update Checking ///----------------------------------------------------------------------------- diff --git a/Classes/BITStoreUpdateManagerPrivate.h b/Classes/BITStoreUpdateManagerPrivate.h index 26e354e7..c614f551 100644 --- a/Classes/BITStoreUpdateManagerPrivate.h +++ b/Classes/BITStoreUpdateManagerPrivate.h @@ -34,6 +34,16 @@ @interface BITStoreUpdateManager () { } +///----------------------------------------------------------------------------- +/// @name Delegate +///----------------------------------------------------------------------------- + +/** + Sets the optional `BITStoreUpdateManagerDelegate` delegate. + */ +@property (nonatomic, weak) id delegate; + + // is an update available? @property (nonatomic, assign, getter=isUpdateAvailable) BOOL updateAvailable; diff --git a/Classes/BITUpdateManager.h b/Classes/BITUpdateManager.h index f10f8c30..dc5cfdbb 100644 --- a/Classes/BITUpdateManager.h +++ b/Classes/BITUpdateManager.h @@ -75,22 +75,6 @@ typedef NS_ENUM (NSUInteger, BITUpdateSetting) { @interface BITUpdateManager : BITHockeyBaseManager - -///----------------------------------------------------------------------------- -/// @name Delegate -///----------------------------------------------------------------------------- - -/** - Sets the `BITUpdateManagerDelegate` delegate. - - The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You - should not need to set this delegate individually. - - @see `[BITHockeyManager setDelegate:]` - */ -@property (nonatomic, weak) id delegate; - - ///----------------------------------------------------------------------------- /// @name Update Checking ///----------------------------------------------------------------------------- diff --git a/Classes/BITUpdateManagerPrivate.h b/Classes/BITUpdateManagerPrivate.h index 75191853..bbc35d0a 100644 --- a/Classes/BITUpdateManagerPrivate.h +++ b/Classes/BITUpdateManagerPrivate.h @@ -39,6 +39,21 @@ @interface BITUpdateManager () { } +///----------------------------------------------------------------------------- +/// @name Delegate +///----------------------------------------------------------------------------- + +/** + Sets the `BITUpdateManagerDelegate` delegate. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` + */ +@property (nonatomic, weak) id delegate; + + // is an update available? @property (nonatomic, assign, getter=isUpdateAvailable) BOOL updateAvailable; From c5cee829549904129f86685a0c087e927e510eb1 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 18 Jun 2014 18:22:35 +0200 Subject: [PATCH 165/206] Added more logging to BITAuthenticator More logging to the console in case of authentication issues, if debugLogEnabled is set --- Classes/BITAuthenticator.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Classes/BITAuthenticator.m b/Classes/BITAuthenticator.m index 6ccc33be..43e5e74f 100644 --- a/Classes/BITAuthenticator.m +++ b/Classes/BITAuthenticator.m @@ -256,6 +256,8 @@ - (void) validate { if(validated) { [self dismissAuthenticationControllerAnimated:YES completion:nil]; } else { + BITHockeyLog(@"Validation failed with error: %@", error); + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:error.localizedDescription delegate:self From c1d75806c7f4c40fd23eefc07315b40359170ecc Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 20 Jun 2014 16:43:58 +0200 Subject: [PATCH 166/206] Minor code cleanup in Feedback component --- Classes/BITFeedbackActivity.m | 2 +- Classes/BITFeedbackComposeViewController.m | 6 ++-- Classes/BITFeedbackListViewCell.m | 36 ++++++++++----------- Classes/BITFeedbackListViewController.m | 8 ++--- Classes/BITFeedbackManager.m | 18 +++++------ Classes/BITFeedbackMessage.h | 2 +- Classes/BITFeedbackMessage.m | 6 ++-- Classes/BITFeedbackMessageAttachment.h | 2 +- Classes/BITFeedbackMessageAttachment.m | 4 +-- Classes/BITFeedbackUserDataViewController.m | 4 +-- 10 files changed, 44 insertions(+), 44 deletions(-) diff --git a/Classes/BITFeedbackActivity.m b/Classes/BITFeedbackActivity.m index a5876801..75fe13ba 100644 --- a/Classes/BITFeedbackActivity.m +++ b/Classes/BITFeedbackActivity.m @@ -59,7 +59,7 @@ - (instancetype)init { _customActivityImage = nil; _customActivityTitle = nil; - self.items = [NSMutableArray array];; + _items = [NSMutableArray array]; } return self; diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 9314cb05..e9885e5d 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -79,7 +79,6 @@ @implementation BITFeedbackComposeViewController { - (instancetype)init { self = [super init]; if (self) { - self.title = BITHockeyLocalizedString(@"HockeyFeedbackComposeTitle"); _blockUserDataScreen = NO; _actionSheetVisible = NO; _delegate = nil; @@ -87,8 +86,8 @@ - (instancetype)init { _attachments = [NSMutableArray new]; _imageAttachments = [NSMutableArray new]; _attachmentScrollViewImageViews = [NSMutableArray new]; - self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewTapped:)]; - [self.attachmentScrollView addGestureRecognizer:self.tapRecognizer]; + _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewTapped:)]; + [_attachmentScrollView addGestureRecognizer:self.tapRecognizer]; _text = nil; } @@ -167,6 +166,7 @@ - (void)keyboardWillBeHidden:(NSNotification*)aNotification { - (void)viewDidLoad { [super viewDidLoad]; + self.title = BITHockeyLocalizedString(@"HockeyFeedbackComposeTitle"); self.view.backgroundColor = [UIColor whiteColor]; // Do any additional setup after loading the view. diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 9b0c1de1..2e9519a0 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -85,28 +85,28 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr _message = nil; - self.dateFormatter = [[NSDateFormatter alloc] init]; - [self.dateFormatter setTimeStyle:NSDateFormatterNoStyle]; - [self.dateFormatter setDateStyle:NSDateFormatterMediumStyle]; - [self.dateFormatter setLocale:[NSLocale currentLocale]]; - [self.dateFormatter setDoesRelativeDateFormatting:YES]; + _dateFormatter = [[NSDateFormatter alloc] init]; + [_dateFormatter setTimeStyle:NSDateFormatterNoStyle]; + [_dateFormatter setDateStyle:NSDateFormatterMediumStyle]; + [_dateFormatter setLocale:[NSLocale currentLocale]]; + [_dateFormatter setDoesRelativeDateFormatting:YES]; - self.timeFormatter = [[NSDateFormatter alloc] init]; - [self.timeFormatter setTimeStyle:NSDateFormatterShortStyle]; - [self.timeFormatter setDateStyle:NSDateFormatterNoStyle]; - [self.timeFormatter setLocale:[NSLocale currentLocale]]; - [self.timeFormatter setDoesRelativeDateFormatting:YES]; + _timeFormatter = [[NSDateFormatter alloc] init]; + [_timeFormatter setTimeStyle:NSDateFormatterShortStyle]; + [_timeFormatter setDateStyle:NSDateFormatterNoStyle]; + [_timeFormatter setLocale:[NSLocale currentLocale]]; + [_timeFormatter setDoesRelativeDateFormatting:YES]; - self.labelTitle = [[UILabel alloc] init]; - self.labelTitle.font = [UIFont systemFontOfSize:TITLE_FONTSIZE]; + _labelTitle = [[UILabel alloc] init]; + _labelTitle.font = [UIFont systemFontOfSize:TITLE_FONTSIZE]; - self.labelText = [[BITAttributedLabel alloc] init]; - self.labelText.font = [UIFont systemFontOfSize:TEXT_FONTSIZE]; - self.labelText.numberOfLines = 0; - self.labelText.textAlignment = kBITTextLabelAlignmentLeft; - self.labelText.dataDetectorTypes = UIDataDetectorTypeAll; + _labelText = [[BITAttributedLabel alloc] init]; + _labelText.font = [UIFont systemFontOfSize:TEXT_FONTSIZE]; + _labelText.numberOfLines = 0; + _labelText.textAlignment = kBITTextLabelAlignmentLeft; + _labelText.dataDetectorTypes = UIDataDetectorTypeAll; - self.attachmentViews = [NSMutableArray new]; + _attachmentViews = [NSMutableArray new]; } return self; } diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index 9397bf85..9bf4bd2f 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -91,10 +91,10 @@ - (instancetype)initWithStyle:(UITableViewStyle)style { _userButtonSection = -1; _userDataComposeFlow = NO; - self.lastUpdateDateFormatter = [[NSDateFormatter alloc] init]; - [self.lastUpdateDateFormatter setDateStyle:NSDateFormatterShortStyle]; - [self.lastUpdateDateFormatter setTimeStyle:NSDateFormatterShortStyle]; - self.lastUpdateDateFormatter.locale = [NSLocale currentLocale]; + _lastUpdateDateFormatter = [[NSDateFormatter alloc] init]; + [_lastUpdateDateFormatter setDateStyle:NSDateFormatterShortStyle]; + [_lastUpdateDateFormatter setTimeStyle:NSDateFormatterShortStyle]; + _lastUpdateDateFormatter.locale = [NSLocale currentLocale]; _thumbnailQueue = [NSOperationQueue new]; } diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index a5043c1b..cc2f9626 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -95,7 +95,7 @@ - (instancetype)init { _token = nil; _lastMessageID = nil; - self.feedbackList = [NSMutableArray array]; + _feedbackList = [NSMutableArray array]; _fileManager = [[NSFileManager alloc] init]; @@ -537,7 +537,7 @@ - (BITFeedbackMessage *)messageWithID:(NSNumber *)messageID { __block BITFeedbackMessage *message = nil; [_feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([[objMessage id] isEqualToNumber:messageID]) { + if ([[objMessage identifier] isEqualToNumber:messageID]) { message = objMessage; *stop = YES; } @@ -562,7 +562,7 @@ - (BITFeedbackMessage *)lastMessageHavingID { __block BITFeedbackMessage *message = nil; [_feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([[objMessage id] integerValue] != 0) { + if ([[objMessage identifier] integerValue] != 0) { message = objMessage; *stop = YES; } @@ -590,8 +590,8 @@ - (void)markSendInProgressMessagesAsInConflict { - (void)updateLastMessageID { BITFeedbackMessage *lastMessageHavingID = [self lastMessageHavingID]; if (lastMessageHavingID) { - if (!self.lastMessageID || [self.lastMessageID compare:[lastMessageHavingID id]] != NSOrderedSame) - self.lastMessageID = [lastMessageHavingID id]; + if (!self.lastMessageID || [self.lastMessageID compare:[lastMessageHavingID identifier]] != NSOrderedSame) + self.lastMessageID = [lastMessageHavingID identifier]; } } @@ -723,13 +723,13 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { if (matchingSendInProgressOrInConflictMessage) { matchingSendInProgressOrInConflictMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]]; - matchingSendInProgressOrInConflictMessage.id = messageID; + matchingSendInProgressOrInConflictMessage.identifier = messageID; matchingSendInProgressOrInConflictMessage.status = BITFeedbackMessageStatusRead; NSArray *feedbackAttachments =[(NSDictionary *)objMessage objectForKey:@"attachments"]; if (matchingSendInProgressOrInConflictMessage.attachments.count == feedbackAttachments.count) { int attachmentIndex = 0; for (BITFeedbackMessageAttachment* attachment in matchingSendInProgressOrInConflictMessage.attachments){ - attachment.id =feedbackAttachments[attachmentIndex][@"id"]; + attachment.identifier =feedbackAttachments[attachmentIndex][@"id"]; attachmentIndex++; } } @@ -741,13 +741,13 @@ - (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { message.email = [(NSDictionary *)objMessage objectForKey:@"email"] ?: @""; message.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]] ?: [NSDate date]; - message.id = [(NSDictionary *)objMessage objectForKey:@"id"]; + message.identifier = [(NSDictionary *)objMessage objectForKey:@"id"]; message.status = BITFeedbackMessageStatusUnread; for (NSDictionary *attachmentData in objMessage[@"attachments"]) { BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; newAttachment.originalFilename = attachmentData[@"file_name"]; - newAttachment.id = attachmentData[@"id"]; + newAttachment.identifier = attachmentData[@"id"]; newAttachment.sourceURL = attachmentData[@"url"]; newAttachment.contentType = attachmentData[@"content_type"]; [message addAttachmentsObject:newAttachment]; diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index b8165f67..c33caa98 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -72,7 +72,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *email; @property (nonatomic, copy) NSDate *date; -@property (nonatomic, copy) NSNumber *id; +@property (nonatomic, copy) NSNumber *identifier; @property (nonatomic, copy) NSString *token; @property (nonatomic, strong) NSArray *attachments; @property (nonatomic) BITFeedbackMessageStatus status; diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index 342da85d..820bc9c6 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -44,7 +44,7 @@ - (instancetype) init { _date = [[NSDate alloc] init]; _token = nil; _attachments = nil; - _id = [[NSNumber alloc] initWithInteger:0]; + _identifier = [[NSNumber alloc] initWithInteger:0]; _status = BITFeedbackMessageStatusSendPending; _userMessage = NO; } @@ -60,7 +60,7 @@ - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:self.name forKey:@"name"]; [encoder encodeObject:self.email forKey:@"email"]; [encoder encodeObject:self.date forKey:@"date"]; - [encoder encodeObject:self.id forKey:@"id"]; + [encoder encodeObject:self.identifier forKey:@"id"]; [encoder encodeObject:self.attachments forKey:@"attachments"]; [encoder encodeInteger:self.status forKey:@"status"]; [encoder encodeBool:self.userMessage forKey:@"userMessage"]; @@ -74,7 +74,7 @@ - (instancetype)initWithCoder:(NSCoder *)decoder { self.name = [decoder decodeObjectForKey:@"name"]; self.email = [decoder decodeObjectForKey:@"email"]; self.date = [decoder decodeObjectForKey:@"date"]; - self.id = [decoder decodeObjectForKey:@"id"]; + self.identifier = [decoder decodeObjectForKey:@"id"]; self.attachments = [decoder decodeObjectForKey:@"attachments"]; self.status = (BITFeedbackMessageStatus)[decoder decodeIntegerForKey:@"status"]; self.userMessage = [decoder decodeBoolForKey:@"userMessage"]; diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h index d7997139..6654c794 100644 --- a/Classes/BITFeedbackMessageAttachment.h +++ b/Classes/BITFeedbackMessageAttachment.h @@ -35,7 +35,7 @@ */ @interface BITFeedbackMessageAttachment : NSObject -@property (nonatomic, copy) NSNumber *id; +@property (nonatomic, copy) NSNumber *identifier; @property (nonatomic, copy) NSString *originalFilename; @property (nonatomic, copy) NSString *contentType; @property (nonatomic, copy) NSString *sourceURL; diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 1315e702..5a5a9312 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -72,8 +72,8 @@ + (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType: - (instancetype)init { if ((self = [super init])) { - self.isLoading = NO; - self.thumbnailRepresentations = [NSMutableDictionary new]; + _isLoading = NO; + _thumbnailRepresentations = [NSMutableDictionary new]; _fm = [[NSFileManager alloc] init]; _cachePath = [bit_settingsDir() stringByAppendingPathComponent:kCacheFolderName]; diff --git a/Classes/BITFeedbackUserDataViewController.m b/Classes/BITFeedbackUserDataViewController.m index 50d0536d..cc80d830 100644 --- a/Classes/BITFeedbackUserDataViewController.m +++ b/Classes/BITFeedbackUserDataViewController.m @@ -54,8 +54,6 @@ @implementation BITFeedbackUserDataViewController - (instancetype)initWithStyle:(UITableViewStyle)style { self = [super initWithStyle:style]; if (self) { - self.title = BITHockeyLocalizedString(@"HockeyFeedbackUserDataTitle"); - _delegate = nil; _manager = [BITHockeyManager sharedHockeyManager].feedbackManager; @@ -68,6 +66,8 @@ - (instancetype)initWithStyle:(UITableViewStyle)style { - (void)viewDidLoad { [super viewDidLoad]; + self.title = BITHockeyLocalizedString(@"HockeyFeedbackUserDataTitle"); + [self.tableView setScrollEnabled:NO]; } From d6aef454baf0be5f69d00e142fc084d5b251a0fe Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 20 Jun 2014 16:46:22 +0200 Subject: [PATCH 167/206] Binary attachments and BITFeedbackActivity Add binary attachment support to BITFeedbackActivity --- Classes/BITFeedbackActivity.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Classes/BITFeedbackActivity.m b/Classes/BITFeedbackActivity.m index 75fe13ba..b5728aa5 100644 --- a/Classes/BITFeedbackActivity.m +++ b/Classes/BITFeedbackActivity.m @@ -96,6 +96,10 @@ - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems { for (UIActivityItemProvider *item in activityItems) { if ([item isKindOfClass:[NSString class]]) { return YES; + } else if ([item isKindOfClass:[UIImage class]]) { + return YES; + } else if ([item isKindOfClass:[NSData class]]) { + return YES; } else if ([item isKindOfClass:[NSURL class]]) { return YES; } @@ -106,6 +110,8 @@ - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems { - (void)prepareWithActivityItems:(NSArray *)activityItems { for (id item in activityItems) { if ([item isKindOfClass:[NSString class]] || + [item isKindOfClass:[UIImage class]] || + [item isKindOfClass:[NSData class]] || [item isKindOfClass:[NSURL class]]) { [_items addObject:item]; } else { From 5fc00143858b4f6b269435730f8cc5cd2bba6d47 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sat, 21 Jun 2014 01:24:33 +0200 Subject: [PATCH 168/206] Set data type specific attachment filenames --- Classes/BITFeedbackComposeViewController.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index e9885e5d..74a10c57 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -107,10 +107,13 @@ - (void)prepareWithItems:(NSArray *)items { } else if ([item isKindOfClass:[UIImage class]]) { UIImage *image = item; BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:UIImageJPEGRepresentation(image, 0.7f) contentType:@"image/jpeg"]; + attachment.originalFilename = [NSString stringWithFormat:@"Image_%li.jpg", (unsigned long)[self.attachments count]]; [self.attachments addObject:attachment]; [self.imageAttachments addObject:attachment]; } else if ([item isKindOfClass:[NSData class]]) { - [self.attachments addObject:[BITFeedbackMessageAttachment attachmentWithData:item contentType:@"'application/octet-stream'"]]; + BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:item contentType:@"application/octet-stream"]; + attachment.originalFilename = [NSString stringWithFormat:@"Attachment_%li.data", (unsigned long)[self.attachments count]]; + [self.attachments addObject:attachment]; } else { BITHockeyLog(@"Unknown item type %@", item); } From cae1226b38ae15d64cec54bf01920482d8541e85 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sat, 21 Jun 2014 16:23:55 +0200 Subject: [PATCH 169/206] Allow customizable attachments to feedback - New `BITHockeyAttachment` class, which `BITCrashAttachment` is now a subclass of - Can also be used in `prepareWithItems:` for BITFeedbackComposeViewController - Allows to set a custom filename and content-type for attachments that can be used with crash reports and with feedback --- Classes/BITCrashAttachment.h | 47 +++++------- Classes/BITCrashAttachment.m | 24 +----- Classes/BITCrashManager.m | 28 ++++--- Classes/BITCrashManagerDelegate.h | 15 ++-- Classes/BITCrashManagerPrivate.h | 4 +- Classes/BITFeedbackActivity.m | 4 + Classes/BITFeedbackComposeViewController.h | 1 + Classes/BITFeedbackComposeViewController.m | 17 +++++ Classes/BITFeedbackManager.h | 1 + Classes/BITHockeyAttachment.h | 68 +++++++++++++++++ Classes/BITHockeyAttachment.m | 75 +++++++++++++++++++ Classes/HockeySDK.h | 4 + Support/HockeySDK.xcodeproj/project.pbxproj | 8 ++ Support/HockeySDKTests/BITCrashManagerTests.m | 6 +- 14 files changed, 228 insertions(+), 74 deletions(-) create mode 100644 Classes/BITHockeyAttachment.h create mode 100644 Classes/BITHockeyAttachment.m diff --git a/Classes/BITCrashAttachment.h b/Classes/BITCrashAttachment.h index e3bd6875..bf6f339b 100644 --- a/Classes/BITCrashAttachment.h +++ b/Classes/BITCrashAttachment.h @@ -26,38 +26,31 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -#import +#import "BITHockeyAttachment.h" /** - * Provides support to add binary attachments to crash reports - * - * This is used by `[BITCrashManagerDelegate attachmentForCrashManager:]` - */ -@interface BITCrashAttachment : NSObject - -/** - * The filename the attachment should get - */ -@property (nonatomic, readonly, strong) NSString *filename; - -/** - * The attachment data as NSData object - */ -@property (nonatomic, readonly, strong) NSData *crashAttachmentData; - -/** - * The content type of your data as MIME type + Deprecated: Provides support to add binary attachments to crash reports + + This class is not needed any longer and exists for compatiblity purposes with + HockeySDK-iOS 3.5.5. + + It is a subclass of `BITHockeyAttachment` which only provides an initializer + that is compatible with the one of HockeySDK-iOS 3.5.5. + + This is used by `[BITCrashManagerDelegate attachmentForCrashManager:]` + + @see BITHockeyAttachment */ -@property (nonatomic, readonly, strong) NSString *contentType; +@interface BITCrashAttachment : BITHockeyAttachment /** - * Create an BITCrashAttachment instance with a given filename and NSData object - * - * @param filename The filename the attachment should get - * @param crashAttachmentData The attachment data as NSData - * @param contentType The content type of your data as MIME type - * - * @return An instsance of BITCrashAttachment + Create an BITCrashAttachment instance with a given filename and NSData object + + @param filename The filename the attachment should get + @param crashAttachmentData The attachment data as NSData + @param contentType The content type of your data as MIME type + + @return An instsance of BITCrashAttachment */ - (instancetype)initWithFilename:(NSString *)filename crashAttachmentData:(NSData *)crashAttachmentData diff --git a/Classes/BITCrashAttachment.m b/Classes/BITCrashAttachment.m index eca2946c..011c87a9 100644 --- a/Classes/BITCrashAttachment.m +++ b/Classes/BITCrashAttachment.m @@ -34,31 +34,9 @@ - (instancetype)initWithFilename:(NSString *)filename crashAttachmentData:(NSData *)crashAttachmentData contentType:(NSString *)contentType { - if (self = [super init]) { - _filename = filename; - _crashAttachmentData = crashAttachmentData; - _contentType = contentType; - } + self = [super initWithFilename:filename hockeyAttachmentData:crashAttachmentData contentType:contentType]; return self; } - -#pragma mark - NSCoder - -- (void)encodeWithCoder:(NSCoder *)encoder { - [encoder encodeObject:self.filename forKey:@"filename"]; - [encoder encodeObject:self.crashAttachmentData forKey:@"data"]; - [encoder encodeObject:self.contentType forKey:@"contentType"]; -} - -- (instancetype)initWithCoder:(NSCoder *)decoder { - if ((self = [super init])) { - _filename = [decoder decodeObjectForKey:@"filename"]; - _crashAttachmentData = [decoder decodeObjectForKey:@"data"]; - _contentType = [decoder decodeObjectForKey:@"contentType"]; - } - return self; -} - @end diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index e8f0c5b7..ff5af9be 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -278,7 +278,7 @@ - (void)cleanCrashReports { } } -- (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename { +- (void)persistAttachment:(BITHockeyAttachment *)attachment withFilename:(NSString *)filename { NSString *attachmentFilename = [filename stringByAppendingString:@".data"]; NSMutableData *data = [[NSMutableData alloc] init]; NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; @@ -318,9 +318,9 @@ - (void)persistUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData { * * @param filename The crash report file path * - * @return an BITCrashAttachment instance or nil + * @return an BITHockeyAttachment instance or nil */ -- (BITCrashAttachment *)attachmentForCrashReport:(NSString *)filename { +- (BITHockeyAttachment *)attachmentForCrashReport:(NSString *)filename { NSString *attachmentFilename = [filename stringByAppendingString:@".data"]; if (![_fileManager fileExistsAtPath:attachmentFilename]) @@ -341,7 +341,7 @@ - (BITCrashAttachment *)attachmentForCrashReport:(NSString *)filename { } if ([unarchiver containsValueForKey:kBITCrashMetaAttachment]) { - BITCrashAttachment *attachment = [unarchiver decodeObjectForKey:kBITCrashMetaAttachment]; + BITHockeyAttachment *attachment = [unarchiver decodeObjectForKey:kBITCrashMetaAttachment]; return attachment; } @@ -701,9 +701,9 @@ - (void)storeMetaDataForCrashReportFilename:(NSString *)filename { [metaDict setObject:applicationLog forKey:kBITCrashMetaApplicationLog]; if (self.delegate != nil && [self.delegate respondsToSelector:@selector(attachmentForCrashManager:)]) { - BITCrashAttachment *attachment = [self.delegate attachmentForCrashManager:self]; + BITHockeyAttachment *attachment = [self.delegate attachmentForCrashManager:self]; - if (attachment) { + if (attachment && attachment.hockeyAttachmentData) { [self persistAttachment:attachment withFilename:[_crashesDir stringByAppendingPathComponent: filename]]; } } @@ -1219,7 +1219,7 @@ - (void)sendNextCrashReport { return; NSString *crashXML = nil; - BITCrashAttachment *attachment = nil; + BITHockeyAttachment *attachment = nil; NSString *filename = [_crashFiles objectAtIndex:0]; NSString *cacheFilename = [filename lastPathComponent]; @@ -1378,7 +1378,7 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) #pragma mark - Networking -- (NSURLRequest *)requestWithXML:(NSString*)xml attachment:(BITCrashAttachment *)attachment { +- (NSURLRequest *)requestWithXML:(NSString*)xml attachment:(BITHockeyAttachment *)attachment { NSString *postCrashPath = [NSString stringWithFormat:@"api/2/apps/%@/crashes", self.encodedAppIdentifier]; NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST" @@ -1413,12 +1413,16 @@ - (NSURLRequest *)requestWithXML:(NSString*)xml attachment:(BITCrashAttachment * boundary:boundary filename:@"crash.xml"]]; - if (attachment) { - [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.crashAttachmentData + if (attachment && attachment.hockeyAttachmentData) { + NSString *attachmentFilename = attachment.filename; + if (!attachmentFilename) { + attachmentFilename = @"Attachment_0"; + } + [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.hockeyAttachmentData forKey:@"attachment0" contentType:attachment.contentType boundary:boundary - filename:attachment.filename]]; + filename:attachmentFilename]]; } [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; @@ -1435,7 +1439,7 @@ - (NSURLRequest *)requestWithXML:(NSString*)xml attachment:(BITCrashAttachment * * * @param xml The XML data that needs to be send to the server */ -- (void)sendCrashReportWithFilename:(NSString *)filename xml:(NSString*)xml attachment:(BITCrashAttachment *)attachment { +- (void)sendCrashReportWithFilename:(NSString *)filename xml:(NSString*)xml attachment:(BITHockeyAttachment *)attachment { NSURLRequest* request = [self requestWithXML:xml attachment:attachment]; diff --git a/Classes/BITCrashManagerDelegate.h b/Classes/BITCrashManagerDelegate.h index 7098e607..ab4d7b5a 100644 --- a/Classes/BITCrashManagerDelegate.h +++ b/Classes/BITCrashManagerDelegate.h @@ -29,7 +29,7 @@ #import @class BITCrashManager; -@class BITCrashAttachment; +@class BITHockeyAttachment; /** The `BITCrashManagerDelegate` formal protocol defines methods further configuring @@ -55,28 +55,29 @@ -(NSString *)applicationLogForCrashManager:(BITCrashManager *)crashManager; -/** Return a BITCrashAttachment object providing an NSData object the crash report +/** Return a BITHockeyAttachment object providing an NSData object the crash report being processed should contain Please limit your attachments to reasonable files to avoid high traffic costs for your users. Example implementation: - - (BITCrashAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManager { + - (BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManager { NSData *data = [NSData dataWithContentsOfURL:@"mydatafile"]; - BITCrashAttachment *attachment = [[BITCrashAttachment alloc] initWithFilename:@"myfile.data" - crashAttachmentData:data - contentType:@"'application/octet-stream"]; + BITHockeyAttachment *attachment = [[BITHockeyAttachment alloc] initWithFilename:@"myfile.data" + hockeyAttachmentData:data + contentType:@"'application/octet-stream"]; return attachment; } @param crashManager The `BITCrashManager` instance invoking this delegate + @see BITHockeyAttachment @see applicationLogForCrashManager: @see userNameForCrashManager: @see userEmailForCrashManager: */ --(BITCrashAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManager; +-(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManager; diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index ae45e632..c1b63621 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -94,9 +94,9 @@ - (NSString *)firstNotApprovedCrashReport; - (void)persistUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData; -- (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename; +- (void)persistAttachment:(BITHockeyAttachment *)attachment withFilename:(NSString *)filename; -- (BITCrashAttachment *)attachmentForCrashReport:(NSString *)filename; +- (BITHockeyAttachment *)attachmentForCrashReport:(NSString *)filename; - (void)invokeDelayedProcessing; - (void)sendNextCrashReport; diff --git a/Classes/BITFeedbackActivity.m b/Classes/BITFeedbackActivity.m index b5728aa5..40aceb13 100644 --- a/Classes/BITFeedbackActivity.m +++ b/Classes/BITFeedbackActivity.m @@ -38,6 +38,7 @@ #import "BITFeedbackManagerPrivate.h" #import "BITHockeyBaseManagerPrivate.h" +#import "BITHockeyAttachment.h" @interface BITFeedbackActivity() @@ -100,6 +101,8 @@ - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems { return YES; } else if ([item isKindOfClass:[NSData class]]) { return YES; + } else if ([item isKindOfClass:[BITHockeyAttachment class]]) { + return YES; } else if ([item isKindOfClass:[NSURL class]]) { return YES; } @@ -112,6 +115,7 @@ - (void)prepareWithActivityItems:(NSArray *)activityItems { if ([item isKindOfClass:[NSString class]] || [item isKindOfClass:[UIImage class]] || [item isKindOfClass:[NSData class]] || + [item isKindOfClass:[BITHockeyAttachment class]] || [item isKindOfClass:[NSURL class]]) { [_items addObject:item]; } else { diff --git a/Classes/BITFeedbackComposeViewController.h b/Classes/BITFeedbackComposeViewController.h index 9d328512..9fff2fd8 100644 --- a/Classes/BITFeedbackComposeViewController.h +++ b/Classes/BITFeedbackComposeViewController.h @@ -77,6 +77,7 @@ - NSURL - UIImage - NSData + - `BITHockeyAttachment` These are automatically concatenated to one text string, while any images and NSData objects are added as attachments to the feedback. diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index 74a10c57..88f95486 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -43,6 +43,7 @@ #import "BITHockeyHelper.h" #import "BITImageAnnotationViewController.h" +#import "BITHockeyAttachment.h" @interface BITFeedbackComposeViewController () { @@ -114,6 +115,22 @@ - (void)prepareWithItems:(NSArray *)items { BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:item contentType:@"application/octet-stream"]; attachment.originalFilename = [NSString stringWithFormat:@"Attachment_%li.data", (unsigned long)[self.attachments count]]; [self.attachments addObject:attachment]; + } else if ([item isKindOfClass:[BITHockeyAttachment class]]) { + BITHockeyAttachment *sourceAttachment = (BITHockeyAttachment *)item; + + if (!sourceAttachment.hockeyAttachmentData) { + BITHockeyLog(@"BITHockeyAttachment instance doesn't contain any data."); + continue; + } + + NSString *filename = [NSString stringWithFormat:@"Attachment_%li.data", (unsigned long)[self.attachments count]]; + if (sourceAttachment.filename) { + filename = sourceAttachment.filename; + } + + BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:sourceAttachment.hockeyAttachmentData contentType:sourceAttachment.contentType]; + attachment.originalFilename = filename; + [self.attachments addObject:attachment]; } else { BITHockeyLog(@"Unknown item type %@", item); } diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index c86bd46a..cb9f2f30 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -289,6 +289,7 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { while all UIImage and NSData-instances will be turned into attachments. @param items an NSArray with objects that should be attached + @see `[BITFeedbackComposeViewController prepareWithItems:]` */ - (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items; diff --git a/Classes/BITHockeyAttachment.h b/Classes/BITHockeyAttachment.h new file mode 100644 index 00000000..ee579e05 --- /dev/null +++ b/Classes/BITHockeyAttachment.h @@ -0,0 +1,68 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2014 HockeyApp, Bit Stadium GmbH. + * 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 + +/** + Provides support to add binary attachments to crash reports and feedback messages + + This is used by `[BITCrashManagerDelegate attachmentForCrashManager:]`, + `[BITFeedbackComposeViewController prepareWithItems:]` and + `[BITFeedbackManager showFeedbackComposeViewWithPreparedItems:]` + */ +@interface BITHockeyAttachment : NSObject + +/** + The filename the attachment should get + */ +@property (nonatomic, readonly, strong) NSString *filename; + +/** + The attachment data as NSData object + */ +@property (nonatomic, readonly, strong) NSData *hockeyAttachmentData; + +/** + The content type of your data as MIME type + */ +@property (nonatomic, readonly, strong) NSString *contentType; + +/** + Create an BITHockeyAttachment instance with a given filename and NSData object + + @param filename The filename the attachment should get. If nil will get a automatically generated filename + @param hockeyAttachmentData The attachment data as NSData. The instance will be ignore if this is set to nil! + @param contentType The content type of your data as MIME type. If nil will be set to "application/octet-stream" + + @return An instsance of BITHockeyAttachment. + */ +- (instancetype)initWithFilename:(NSString *)filename + hockeyAttachmentData:(NSData *)hockeyAttachmentData + contentType:(NSString *)contentType; + +@end diff --git a/Classes/BITHockeyAttachment.m b/Classes/BITHockeyAttachment.m new file mode 100644 index 00000000..4dc6424a --- /dev/null +++ b/Classes/BITHockeyAttachment.m @@ -0,0 +1,75 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2014 HockeyApp, Bit Stadium GmbH. + * 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 "BITHockeyAttachment.h" + +@implementation BITHockeyAttachment + +- (instancetype)initWithFilename:(NSString *)filename + hockeyAttachmentData:(NSData *)hockeyAttachmentData + contentType:(NSString *)contentType +{ + if (self = [super init]) { + _filename = filename; + + _hockeyAttachmentData = hockeyAttachmentData; + + if (contentType) { + _contentType = contentType; + } else { + _contentType = @"application/octet-stream"; + } + + } + + return self; +} + + +#pragma mark - NSCoder + +- (void)encodeWithCoder:(NSCoder *)encoder { + if (self.filename) { + [encoder encodeObject:self.filename forKey:@"filename"]; + } + if (self.hockeyAttachmentData) { + [encoder encodeObject:self.hockeyAttachmentData forKey:@"data"]; + } + [encoder encodeObject:self.contentType forKey:@"contentType"]; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + if ((self = [super init])) { + _filename = [decoder decodeObjectForKey:@"filename"]; + _hockeyAttachmentData = [decoder decodeObjectForKey:@"data"]; + _contentType = [decoder decodeObjectForKey:@"contentType"]; + } + return self; +} + +@end diff --git a/Classes/HockeySDK.h b/Classes/HockeySDK.h index 709333dc..f0069d1d 100644 --- a/Classes/HockeySDK.h +++ b/Classes/HockeySDK.h @@ -35,6 +35,10 @@ #import "BITHockeyManager.h" #import "BITHockeyManagerDelegate.h" +#if HOCKEYSDK_FEATURE_CRASH_REPORTER || HOCKEYSDK_FEATURE_FEEDBACK +#import "BITHockeyAttachment.h" +#endif + #if HOCKEYSDK_FEATURE_CRASH_REPORTER #import "BITCrashManager.h" #import "BITCrashAttachment.h" diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 7e4c4843..0d9e15c6 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -131,6 +131,8 @@ 1EAF20AA162DC0F600957B1D /* feedbackActiviy.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A6162DC0F600957B1D /* feedbackActiviy.png */; }; 1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */; }; 1EB52FD5167B766100C801D5 /* HockeySDK.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E59555F15B6F80E00A03429 /* HockeySDK.strings */; }; + 1EB92E731955C38C0093C8B6 /* BITHockeyAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EB92E711955C38C0093C8B6 /* BITHockeyAttachment.h */; }; + 1EB92E741955C38C0093C8B6 /* BITHockeyAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EB92E721955C38C0093C8B6 /* BITHockeyAttachment.m */; }; 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */; }; 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1ECA8F52192B6954006B9416 /* BITCrashMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ECA8F50192B6954006B9416 /* BITCrashMetaData.m */; }; @@ -325,6 +327,8 @@ 1EAF20A6162DC0F600957B1D /* feedbackActiviy.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = feedbackActiviy.png; sourceTree = ""; }; 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "feedbackActiviy@2x.png"; sourceTree = ""; }; 1EB52FC3167B73D400C801D5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/HockeySDK.strings; sourceTree = ""; }; + 1EB92E711955C38C0093C8B6 /* BITHockeyAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITHockeyAttachment.h; sourceTree = ""; }; + 1EB92E721955C38C0093C8B6 /* BITHockeyAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITHockeyAttachment.m; sourceTree = ""; }; 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashDetailsPrivate.h; sourceTree = ""; }; 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashMetaData.h; sourceTree = ""; }; 1ECA8F50192B6954006B9416 /* BITCrashMetaData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashMetaData.m; sourceTree = ""; }; @@ -522,6 +526,8 @@ 1E49A4AA161222B900463151 /* BITStoreButton.m */, 1E49A4AB161222B900463151 /* BITWebTableViewCell.h */, 1E49A4AC161222B900463151 /* BITWebTableViewCell.m */, + 1EB92E711955C38C0093C8B6 /* BITHockeyAttachment.h */, + 1EB92E721955C38C0093C8B6 /* BITHockeyAttachment.m */, ); name = Helper; sourceTree = ""; @@ -780,6 +786,7 @@ 1EF95CAA162CB314000AE3AD /* BITFeedbackComposeViewControllerDelegate.h in Headers */, 1EF95CA6162CB037000AE3AD /* BITFeedbackActivity.h in Headers */, 1E49A4B8161222B900463151 /* BITHockeyBaseViewController.h in Headers */, + 1EB92E731955C38C0093C8B6 /* BITHockeyAttachment.h in Headers */, 1E49A4AF161222B900463151 /* BITHockeyBaseManager.h in Headers */, 1E0829001708F69A0073050E /* BITStoreUpdateManagerDelegate.h in Headers */, 1E49A4421612223B00463151 /* BITFeedbackListViewCell.h in Headers */, @@ -1044,6 +1051,7 @@ 1E49A4CD161222B900463151 /* BITStoreButton.m in Sources */, 1E90FD7418EDB86400CF0417 /* BITCrashDetails.m in Sources */, 1E49A4D3161222B900463151 /* BITWebTableViewCell.m in Sources */, + 1EB92E741955C38C0093C8B6 /* BITHockeyAttachment.m in Sources */, E48A3DED17B3ED1C00924C3D /* BITAuthenticator.m in Sources */, 1E49A4DB161222D400463151 /* HockeySDKPrivate.m in Sources */, 1E754E5D1621FBB70070AB92 /* BITCrashManager.m in Sources */, diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 0cb0840d..49289f08 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -99,15 +99,15 @@ - (void)testPersistAttachment { NSData *data = [[NSData alloc] initWithBase64Encoding:@"TestData"]; NSString* type = @"text/plain"; - BITCrashAttachment *originalAttachment = [[BITCrashAttachment alloc] initWithFilename:filename crashAttachmentData:data contentType:type]; + BITHockeyAttachment *originalAttachment = [[BITHockeyAttachment alloc] initWithFilename:filename hockeyAttachmentData:data contentType:type]; NSString *attachmentFilename = [[_sut crashesDir] stringByAppendingPathComponent:@"testAttachment"]; [_sut persistAttachment:originalAttachment withFilename:attachmentFilename]; - BITCrashAttachment *decodedAttachment = [_sut attachmentForCrashReport:attachmentFilename]; + BITHockeyAttachment *decodedAttachment = [_sut attachmentForCrashReport:attachmentFilename]; assertThat(decodedAttachment.filename, equalTo(filename)); - assertThat(decodedAttachment.crashAttachmentData, equalTo(data)); + assertThat(decodedAttachment.hockeyAttachmentData, equalTo(data)); assertThat(decodedAttachment.contentType, equalTo(type)); } From b6af4e1d4693e4d78391f61fc4a3eaf0457a6c54 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sat, 21 Jun 2014 17:37:59 +0200 Subject: [PATCH 170/206] Removed JMC support --- Classes/BITHockeyManager.h | 2 +- Classes/BITHockeyManager.m | 143 +----------------------------- Classes/BITUpdateManager.h | 3 +- Classes/BITUpdateManager.m | 23 +---- Classes/BITUpdateManagerPrivate.h | 7 -- Classes/HockeySDK.h | 2 +- Classes/HockeySDKFeatureConfig.h | 12 --- 7 files changed, 7 insertions(+), 185 deletions(-) diff --git a/Classes/BITHockeyManager.h b/Classes/BITHockeyManager.h index 3bf72fcd..7a8a5b80 100644 --- a/Classes/BITHockeyManager.h +++ b/Classes/BITHockeyManager.h @@ -45,7 +45,7 @@ This is the principal SDK class. It represents the entry point for the HockeySDK. The main promises of the class are initializing the SDK modules, providing access to global properties and to all modules. Initialization is divided into several distinct phases: - 1. Setup the [HockeyApp](http://hockeyapp.net/) app identifier and the optional delegate: This is the least required information on setting up the SDK and using it. It does some simple validation of the app identifier and checks if the app is running from the App Store or not. If the [Atlassian JMC framework](http://www.atlassian.com/jmc/) is found, it will disable its Crash Reporting module and configure it with the Jira configuration data from [HockeyApp](http://hockeyapp.net/). + 1. Setup the [HockeyApp](http://hockeyapp.net/) app identifier and the optional delegate: This is the least required information on setting up the SDK and using it. It does some simple validation of the app identifier and checks if the app is running from the App Store or not. 2. Provides access to the SDK modules `BITCrashManager`, `BITUpdateManager`, and `BITFeedbackManager`. This way all modules can be further configured to personal needs, if the defaults don't fit the requirements. 3. Configure each module. 4. Start up all modules. diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index fb74376d..5cd2b8c8 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -75,10 +75,6 @@ @interface BITHockeyManager () - (BOOL)shouldUseLiveIdentifier; -#if HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT -- (void)configureJMC; -#endif /* HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT */ - @end @@ -267,22 +263,15 @@ - (void)startManager { #endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ #if HOCKEYSDK_FEATURE_UPDATES - BOOL jmcIsPresent = NO; BOOL isIdentified = NO; -#if HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT - jmcIsPresent = [[self class] isJMCPresent] -#endif /* HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT */ - #if HOCKEYSDK_FEATURE_AUTHENTICATOR if (![self isAppStoreEnvironment]) isIdentified = [self.authenticator isIdentified]; #endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ // Setup UpdateManager - if ( - (![self isUpdateManagerDisabled] && isIdentified) || - jmcIsPresent) { + if (![self isUpdateManagerDisabled] && isIdentified) { [self invokeStartUpdateManager]; } #endif /* HOCKEYSDK_FEATURE_UPDATES */ @@ -457,20 +446,6 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N [self invokeStartUpdateManager]; } } -#if HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT - } else if (([object trackerConfig]) && ([[object trackerConfig] isKindOfClass:[NSDictionary class]])) { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - NSMutableDictionary *trackerConfig = [[defaults valueForKey:@"BITTrackerConfigurations"] mutableCopy]; - if (!trackerConfig) { - trackerConfig = [NSMutableDictionary dictionaryWithCapacity:1]; - } - - [trackerConfig setValue:[object trackerConfig] forKey:_appIdentifier]; - [defaults setValue:trackerConfig forKey:@"BITTrackerConfigurations"]; - - [defaults synchronize]; - [self configureJMC]; -#endif /* HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT */ } } #endif /* HOCKEYSDK_FEATURE_UPDATES */ @@ -638,21 +613,6 @@ - (void)initializeModules { _authenticator.delegate = _delegate; #endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ -#if HOCKEYSDK_FEATURE_UPDATES - -#if HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT - // Only if JMC is part of the project - if ([[self class] isJMCPresent]) { - BITHockeyLog(@"INFO: Setup JMC"); - [_updateManager setCheckForTracker:YES]; - [_updateManager addObserver:self forKeyPath:@"trackerConfig" options:0 context:nil]; - [[self class] disableJMCCrashReporter]; - [self performSelector:@selector(configureJMC) withObject:nil afterDelay:0]; - } -#endif /* HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT */ - -#endif /* HOCKEYSDK_FEATURE_UPDATES */ - if (![self isAppStoreEnvironment]) { NSString *integrationFlowTime = [self integrationFlowTimeString]; if (integrationFlowTime && [self integrationFlowStartedWithTimeString:integrationFlowTime]) { @@ -664,105 +624,4 @@ - (void)initializeModules { } } -#if HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT - -#pragma mark - JMC - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wundeclared-selector" -+ (id)jmcInstance { - id jmcClass = NSClassFromString(@"JMC"); - if ((jmcClass) && ([jmcClass respondsToSelector:@selector(sharedInstance)])) { - return [jmcClass performSelector:@selector(sharedInstance)]; - } -#ifdef JMC_LEGACY - else if ((jmcClass) && ([jmcClass respondsToSelector:@selector(instance)])) { - return [jmcClass performSelector:@selector(instance)]; // legacy pre (JMC 1.0.11) support - } -#endif - - return nil; -} -#pragma clang diagnostic pop - -+ (BOOL)isJMCActive { - id jmcInstance = [self jmcInstance]; - return (jmcInstance) && ([jmcInstance performSelector:@selector(url)]); -} - -+ (BOOL)isJMCPresent { - return [self jmcInstance] != nil; -} - -#pragma mark - Private Class Methods - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wundeclared-selector" -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" -+ (void)disableJMCCrashReporter { - id jmcInstance = [self jmcInstance]; - SEL optionsSelector = @selector(options); - id jmcOptions = [jmcInstance performSelector:optionsSelector]; - SEL crashReporterSelector = @selector(setCrashReportingEnabled:); - - BOOL value = NO; - - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[jmcOptions methodSignatureForSelector:crashReporterSelector]]; - invocation.target = jmcOptions; - invocation.selector = crashReporterSelector; - [invocation setArgument:&value atIndex:2]; - [invocation invoke]; -} -#pragma clang diagnostic pop - -+ (BOOL)checkJMCConfiguration:(NSDictionary *)configuration { - return (([configuration isKindOfClass:[NSDictionary class]]) && - ([[configuration valueForKey:@"enabled"] boolValue]) && - ([(NSString *)[configuration valueForKey:@"url"] length] > 0) && - ([(NSString *)[configuration valueForKey:@"key"] length] > 0) && - ([(NSString *)[configuration valueForKey:@"project"] length] > 0)); -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wundeclared-selector" -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" -+ (void)applyJMCConfiguration:(NSDictionary *)configuration { - id jmcInstance = [self jmcInstance]; - SEL configureSelector = @selector(configureJiraConnect:projectKey:apiKey:); - - __unsafe_unretained NSString *url = [configuration valueForKey:@"url"]; - __unsafe_unretained NSString *project = [configuration valueForKey:@"project"]; - __unsafe_unretained NSString *key = [configuration valueForKey:@"key"]; - - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[jmcInstance methodSignatureForSelector:configureSelector]]; - invocation.target = jmcInstance; - invocation.selector = configureSelector; - [invocation setArgument:&url atIndex:2]; - [invocation setArgument:&project atIndex:3]; - [invocation setArgument:&key atIndex:4]; - [invocation invoke]; - - SEL pingSelector = NSSelectorFromString(@"ping"); - if ([jmcInstance respondsToSelector:pingSelector]) { - [jmcInstance performSelector:pingSelector]; - } -} -#pragma clang diagnostic pop - -- (void)configureJMC { - // Return if JMC is already configured - if ([[self class] isJMCActive]) { - return; - } - - // Configure JMC from user defaults - NSDictionary *configurations = [[NSUserDefaults standardUserDefaults] valueForKey:@"BITTrackerConfigurations"]; - NSDictionary *configuration = [configurations valueForKey:_appIdentifier]; - if ([[self class] checkJMCConfiguration:configuration]) { - [[self class] applyJMCConfiguration:configuration]; - } -} - -#endif /* HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT */ - @end diff --git a/Classes/BITUpdateManager.h b/Classes/BITUpdateManager.h index dc5cfdbb..9fdf93d9 100644 --- a/Classes/BITUpdateManager.h +++ b/Classes/BITUpdateManager.h @@ -62,8 +62,7 @@ typedef NS_ENUM (NSUInteger, BITUpdateSetting) { This modul handles version updates, presents update and version information in a App Store like user interface, collects usage information and provides additional authorization options when using Ad-Hoc provisioning profiles. - This module automatically disables itself when running in an App Store build by default! If you integrate the - Atlassian JMC client this module is used to automatically configure JMC, but will not do anything else. + This module automatically disables itself when running in an App Store build by default! The protocol `BITUpdateManagerDelegate` provides delegates to inform about events and adjust a few behaviors. diff --git a/Classes/BITUpdateManager.m b/Classes/BITUpdateManager.m index 40371ba1..a34dd94e 100644 --- a/Classes/BITUpdateManager.m +++ b/Classes/BITUpdateManager.m @@ -402,7 +402,6 @@ - (instancetype)init { _versionID = nil; _sendUsageData = YES; _disableUpdateManager = NO; - _checkForTracker = NO; _firstStartAfterInstall = NO; _companyName = nil; _currentAppVersionUsageTime = @0; @@ -652,8 +651,6 @@ - (void)checkForUpdate { [self showCheckForUpdateAlert]; } - [self checkForUpdateShowFeedback:NO]; - } else if ([self checkForTracker]) { [self checkForUpdateShowFeedback:NO]; } } @@ -666,7 +663,7 @@ - (void)checkForUpdateShowFeedback:(BOOL)feedback { self.checkInProgress = YES; // do we need to update? - if (![self checkForTracker] && !_currentHockeyViewController && ![self shouldCheckForUpdates] && _updateSetting != BITUpdateCheckManually) { + if (!_currentHockeyViewController && ![self shouldCheckForUpdates] && _updateSetting != BITUpdateCheckManually) { BITHockeyLog(@"INFO: Update not needed right now"); self.checkInProgress = NO; return; @@ -699,10 +696,6 @@ - (void)checkForUpdateShowFeedback:(BOOL)feedback { ]; } - if ([self checkForTracker]) { - [parameter appendFormat:@"&jmc=yes"]; - } - // build request & send NSString *url = [NSString stringWithFormat:@"%@%@", self.serverURL, parameter]; BITHockeyLog(@"INFO: Sending api request to %@", url); @@ -766,21 +759,12 @@ - (void)startManager { [self checkExpiryDateReached]; if (![self expiryDateReached]) { - if ([self checkForTracker] || ([self isCheckForUpdateOnLaunch] && [self shouldCheckForUpdates])) { + if ([self isCheckForUpdateOnLaunch] && [self shouldCheckForUpdates]) { if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) return; [self performSelector:@selector(checkForUpdate) withObject:nil afterDelay:1.0f]; } } - } else { - if ([self checkForTracker]) { - // if we are in the app store, make sure not to send usage information in any case for now - _sendUsageData = NO; - - if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) return; - - [self performSelector:@selector(checkForUpdate) withObject:nil afterDelay:1.0f]; - } } [self registerObservers]; } @@ -846,8 +830,7 @@ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSError *error = nil; NSDictionary *json = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error]; - - self.trackerConfig = (([self checkForTracker] && [[json valueForKey:@"tracker"] isKindOfClass:[NSDictionary class]]) ? [json valueForKey:@"tracker"] : nil); + self.companyName = (([[json valueForKey:@"company"] isKindOfClass:[NSString class]]) ? [json valueForKey:@"company"] : nil); if (![self isAppStoreEnvironment]) { diff --git a/Classes/BITUpdateManagerPrivate.h b/Classes/BITUpdateManagerPrivate.h index bbc35d0a..562dffa8 100644 --- a/Classes/BITUpdateManagerPrivate.h +++ b/Classes/BITUpdateManagerPrivate.h @@ -83,13 +83,6 @@ @property (nonatomic) BOOL installationIdentified; -// if YES, the API will return an existing JMC config -// if NO, the API will return only version information -@property (nonatomic, assign) BOOL checkForTracker; - -// Contains the tracker config if received from server -@property (nonatomic, strong) NSDictionary *trackerConfig; - // used by BITHockeyManager if disable status is changed @property (nonatomic, getter = isUpdateManagerDisabled) BOOL disableUpdateManager; diff --git a/Classes/HockeySDK.h b/Classes/HockeySDK.h index f0069d1d..0d04c438 100644 --- a/Classes/HockeySDK.h +++ b/Classes/HockeySDK.h @@ -47,7 +47,7 @@ #import "BITCrashMetaData.h" #endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ -#if HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT +#if HOCKEYSDK_FEATURE_UPDATES #import "BITUpdateManager.h" #import "BITUpdateManagerDelegate.h" #import "BITUpdateViewController.h" diff --git a/Classes/HockeySDKFeatureConfig.h b/Classes/HockeySDKFeatureConfig.h index d5cba7e0..1a8b387e 100644 --- a/Classes/HockeySDKFeatureConfig.h +++ b/Classes/HockeySDKFeatureConfig.h @@ -80,16 +80,4 @@ #endif /* HOCKEYSDK_FEATURE_UPDATES */ -/** - * If true, include support for the Jira Mobile Connect SDK. - * - * @warning This requires Crash Reporting and Update Manager to be included! - * - * _Default_: Disabled - */ -#ifndef HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT -# define HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT 0 -#endif /* HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT */ - - #endif /* HockeySDK_HockeySDKFeatureConfig_h */ From 8874ce0f82754b2da323b409f86562aec30ce7a3 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 24 Jun 2014 21:06:51 +0200 Subject: [PATCH 171/206] Minor improvements for feedback list view --- Classes/BITFeedbackListViewController.m | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index 9bf4bd2f..64fce534 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -649,12 +649,11 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]]; [NSURLConnection sendAsynchronousRequest:request queue:self.thumbnailQueue completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) { if (responseData.length) { - [attachment replaceData:responseData]; dispatch_async(dispatch_get_main_queue(), ^{ - [self.tableView reloadData]; + [attachment replaceData:responseData]; + [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; }); - - [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; } }]; } From 85d5480def6bdeac9377ec3f45776637f57eff5a Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 24 Jun 2014 21:21:08 +0200 Subject: [PATCH 172/206] Improve feedback screenshot feature - Use iOS 7 functionality when available (Thanks to Lee for the hint) - Always include status bar area --- Classes/BITFeedbackManager.h | 4 +--- Classes/BITFeedbackManager.m | 6 +++--- Classes/BITHockeyHelper.h | 2 +- Classes/BITHockeyHelper.m | 15 ++++++++------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index cb9f2f30..b7902964 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -254,11 +254,9 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { /** Return a screenshot UIImage intance from the current visiable screen - @param includeStatusBar BOOL if the screenshot should include the satusbar (if present) or not - @return UIImage instance containing a screenshot of the current screen */ -- (UIImage *)screenshotWithStatusBar:(BOOL)includeStatusBar; +- (UIImage *)screenshot; /** diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index cc2f9626..b9fa70cb 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -198,8 +198,8 @@ - (NSString *)uuidAsLowerCaseAndShortened { #pragma mark - Feedback Modal UI -- (UIImage *)screenshotWithStatusBar:(BOOL)includeStatusBar { - return bit_screenshot(includeStatusBar); +- (UIImage *)screenshot { + return bit_screenshot(); } - (BITFeedbackListViewController *)feedbackListViewController:(BOOL)modal { @@ -244,7 +244,7 @@ - (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items{ } - (void)showFeedbackComposeViewWithGeneratedScreenshot { - UIImage *screenshot = bit_screenshot(YES); + UIImage *screenshot = bit_screenshot(); [self showFeedbackComposeViewWithPreparedItems:@[screenshot]]; } diff --git a/Classes/BITHockeyHelper.h b/Classes/BITHockeyHelper.h index 9348d183..9a7e1ecf 100644 --- a/Classes/BITHockeyHelper.h +++ b/Classes/BITHockeyHelper.h @@ -59,5 +59,5 @@ UIImage *bit_reflectedImageWithHeight(UIImage *inputImage, NSUInteger height, fl UIImage *bit_newWithContentsOfResolutionIndependentFile(NSString * path); UIImage *bit_imageWithContentsOfResolutionIndependentFile(NSString * path); UIImage *bit_imageNamed(NSString *imageName, NSString *bundleName); -UIImage *bit_screenshot(BOOL includeStatusBar); +UIImage *bit_screenshot(void); UIImage *bit_appIcon(void); diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index d62b0d1a..8d96fac9 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -690,7 +690,7 @@ BOOL bit_hasAlpha(UIImage *inputImage) { } } -UIImage *bit_screenshot(BOOL includeStatusBar) { +UIImage *bit_screenshot(void) { // Create a graphics context with the target size CGSize imageSize = [[UIScreen mainScreen] bounds].size; BOOL isLandscapeLeft = [UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationLandscapeLeft; @@ -703,7 +703,7 @@ BOOL bit_hasAlpha(UIImage *inputImage) { imageSize.height = temp; } - UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0); + UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0); CGContextRef context = UIGraphicsGetCurrentContext(); @@ -723,9 +723,6 @@ BOOL bit_hasAlpha(UIImage *inputImage) { // Y-offset for the status bar (if it's showing) NSInteger yOffset = 0; - if (!includeStatusBar) { - yOffset = [UIApplication sharedApplication].statusBarHidden ? 0 : -20; - } // Offset by the portion of the bounds left of and above the anchor point CGContextTranslateCTM(context, @@ -740,8 +737,12 @@ BOOL bit_hasAlpha(UIImage *inputImage) { CGContextConcatCTM(context, CGAffineTransformRotate(CGAffineTransformMakeTranslation( imageSize.width, imageSize.height), M_PI)); } - // Render the layer hierarchy to the current context - [[window layer] renderInContext:context]; + if ([window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) { + [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:NO]; + } else { + // Render the layer hierarchy to the current context + [[window layer] renderInContext:context]; + } // Restore the context CGContextRestoreGState(context); From c4486099f9dd49c0e4b774cefb6d551de27d05be Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 24 Jun 2014 21:22:46 +0200 Subject: [PATCH 173/206] Remove code that is not needed any more --- Classes/BITHockeyHelper.m | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 8d96fac9..407bb14b 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -721,13 +721,10 @@ BOOL bit_hasAlpha(UIImage *inputImage) { // Apply the window's transform about the anchor point CGContextConcatCTM(context, [window transform]); - // Y-offset for the status bar (if it's showing) - NSInteger yOffset = 0; - // Offset by the portion of the bounds left of and above the anchor point CGContextTranslateCTM(context, -[window bounds].size.width * [[window layer] anchorPoint].x, - -[window bounds].size.height * [[window layer] anchorPoint].y + yOffset); + -[window bounds].size.height * [[window layer] anchorPoint].y); if (isLandscapeLeft) { CGContextConcatCTM(context, CGAffineTransformRotate(CGAffineTransformMakeTranslation( imageSize.width, 0), M_PI / 2.0)); From 00c90ec6c35bef0e1d7fa6625113043ef9e8e776 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 25 Jun 2014 14:30:53 +0200 Subject: [PATCH 174/206] Make BITHockeyAttachmen.h public --- Support/HockeySDK.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 0d9e15c6..3853064d 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -131,7 +131,7 @@ 1EAF20AA162DC0F600957B1D /* feedbackActiviy.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A6162DC0F600957B1D /* feedbackActiviy.png */; }; 1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */; }; 1EB52FD5167B766100C801D5 /* HockeySDK.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E59555F15B6F80E00A03429 /* HockeySDK.strings */; }; - 1EB92E731955C38C0093C8B6 /* BITHockeyAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EB92E711955C38C0093C8B6 /* BITHockeyAttachment.h */; }; + 1EB92E731955C38C0093C8B6 /* BITHockeyAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EB92E711955C38C0093C8B6 /* BITHockeyAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1EB92E741955C38C0093C8B6 /* BITHockeyAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EB92E721955C38C0093C8B6 /* BITHockeyAttachment.m */; }; 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */; }; 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */; settings = {ATTRIBUTES = (Public, ); }; }; From 57f52c7459aabb7799ce0bd95f40a1ae013f12c8 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 26 Jun 2014 11:56:39 +0200 Subject: [PATCH 175/206] Update PLCrashReporter to v1.2 --- .../Versions/A/CrashReporter | Bin 1432204 -> 1432156 bytes .../Versions/A/Resources/Info.plist | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Vendor/CrashReporter.framework/Versions/A/CrashReporter b/Vendor/CrashReporter.framework/Versions/A/CrashReporter index 3ee803a4c6fc23918b9d09f5598f6794d8c3f4e8..2344126c11fb6d90f5440a9018d4f533bbde95c8 100644 GIT binary patch delta 100643 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 T?pHO=v+h@3H9E7lY<%{A!5k9l delta 100009 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

O{@}*o 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 BuildMachineOSBuild - 13C64 + 13D65 CFBundleDevelopmentRegion English CFBundleExecutable From da8a544fa6c43f5bc76d849c8724197882b5f511 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 26 Jun 2014 15:54:30 +0200 Subject: [PATCH 176/206] Update version and files to v3.6 Beta 2 --- HockeySDK.podspec | 6 +- README.md | 10 +- Support/buildnumber.xcconfig | 4 +- docs/Changelog-template.md | 14 +++ ...de-Installation-Setup-Advanced-template.md | 7 +- docs/Guide-Installation-Setup-template.md | 11 +- docs/HockeyMacGeneral_normal.png | Bin 73103 -> 0 bytes docs/HowTo-Add-Application-Log-template.md | 97 ------------------ ...owTo-Handle-Crashes-On-Startup-template.md | 78 -------------- ...ting-Symbolication-Doesnt-Work-template.md | 40 -------- docs/XcodeArchivePostAction_normal.png | Bin 74258 -> 0 bytes docs/index.md | 8 +- 12 files changed, 40 insertions(+), 235 deletions(-) delete mode 100644 docs/HockeyMacGeneral_normal.png delete mode 100644 docs/HowTo-Add-Application-Log-template.md delete mode 100644 docs/HowTo-Handle-Crashes-On-Startup-template.md delete mode 100644 docs/Troubleshooting-Symbolication-Doesnt-Work-template.md delete mode 100644 docs/XcodeArchivePostAction_normal.png diff --git a/HockeySDK.podspec b/HockeySDK.podspec index f0efbd95..b4ddf65f 100644 --- a/HockeySDK.podspec +++ b/HockeySDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'HockeySDK' - s.version = '3.6-b.1' + s.version = '3.6-b.2' s.summary = 'Collect live crash reports, get feedback from your users, distribute your betas, and analyze your test coverage with HockeyApp.' s.description = <<-DESC @@ -12,7 +12,7 @@ Pod::Spec.new do |s| DESC s.homepage = 'http://hockeyapp.net/' - s.documentation_url = 'http://hockeyapp.net/help/sdk/ios/3.6-b.1/' + s.documentation_url = 'http://hockeyapp.net/help/sdk/ios/3.6-b.2/' s.license = 'MIT' s.author = { 'Andreas Linde' => 'mail@andreaslinde.de', 'Thomas Dohmke' => "thomas@dohmke.de" } @@ -24,7 +24,7 @@ Pod::Spec.new do |s| s.frameworks = 'CoreText', 'QuartzCore', 'SystemConfiguration', 'CoreGraphics', 'UIKit', 'Security', 'AssetsLibrary', 'MobileCoreServices', 'QuickLook' s.ios.vendored_frameworks = 'Vendor/CrashReporter.framework' - s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => %{$(inherited) BITHOCKEY_VERSION="@\\"#{s.version}\\"" BITHOCKEY_C_VERSION="\\"#{s.version}\\"" BITHOCKEY_BUILD="@\\"29\\"" BITHOCKEY_C_BUILD="\\"29\\""} } + s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => %{$(inherited) BITHOCKEY_VERSION="@\\"#{s.version}\\"" BITHOCKEY_C_VERSION="\\"#{s.version}\\"" BITHOCKEY_BUILD="@\\"30\\"" BITHOCKEY_C_BUILD="\\"30\\""} } s.resource_bundle = { 'HockeySDKResources' => ['Resources/*.png', 'Resources/*.lproj'] } s.preserve_paths = 'Resources', 'Support' diff --git a/README.md b/README.md index 4134a1f9..90384ab4 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ The main SDK class is `BITHockeyManager`. It initializes all modules and provide ## Installation & Setup -- [Installation & Setup](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Guide-Installation-Setup.html) (Recommended) -- [Installation & Setup Advanced](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Guide-Installation-Setup-Advanced.html) (Using Git submodule and Xcode sub-project) -- [Identify and authenticate users of Ad-Hoc or Enterprise builds](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/HowTo-Authenticating-Users-on-iOS.html) -- [Migration from previous SDK Versions](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Guide-Migration-Kits.html) +- [Installation & Setup](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Guide-Installation-Setup.html) (Recommended) +- [Installation & Setup Advanced](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Guide-Installation-Setup-Advanced.html) (Using Git submodule and Xcode sub-project) +- [Identify and authenticate users of Ad-Hoc or Enterprise builds](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/HowTo-Authenticating-Users-on-iOS.html) +- [Migration from previous SDK Versions](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Guide-Migration-Kits.html) - [Mac Desktop Uploader](http://support.hockeyapp.net/kb/how-tos/how-to-upload-to-hockeyapp-on-a-mac) @@ -48,4 +48,4 @@ This documentation provides integrated help in Xcode for all public APIs and a s 3. Copy the content into ~`/Library/Developer/Shared/Documentation/DocSets` -The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.1/](http://hockeyapp.net/help/sdk/ios/3.6-b.1/) +The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.2/](http://hockeyapp.net/help/sdk/ios/3.6-b.2/) diff --git a/Support/buildnumber.xcconfig b/Support/buildnumber.xcconfig index afb98dbd..7d717adf 100644 --- a/Support/buildnumber.xcconfig +++ b/Support/buildnumber.xcconfig @@ -1,7 +1,7 @@ #include "HockeySDK.xcconfig" -BUILD_NUMBER = 29 -VERSION_STRING = 3.6-b.1 +BUILD_NUMBER = 30 +VERSION_STRING = 3.6-b.2 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) BITHOCKEY_VERSION="@\""$(VERSION_STRING)"\"" BITHOCKEY_BUILD="@\""$(BUILD_NUMBER)"\"" BITHOCKEY_C_VERSION="\""$(VERSION_STRING)"\"" BITHOCKEY_C_BUILD="\""$(BUILD_NUMBER)"\"" BIT_ARM_ARCHS = armv7 armv7s arm64 BIT_SIM_ARCHS = x86_64 i386 diff --git a/docs/Changelog-template.md b/docs/Changelog-template.md index 4f0125ac..c9a328e9 100644 --- a/docs/Changelog-template.md +++ b/docs/Changelog-template.md @@ -1,3 +1,17 @@ +## Version 3.6.0 Beta 2 + +- [NEW] `BITFeedbackManager`: Screenshot feature is now part of the public API +- [UPDATE] `BITFeedbackManager`: Various improvements for the screenshot feature +- [UPDATE] `BITFeedbackManager`: Added `BITHockeyAttachment` for more customizable attachments to feedback (`content-type`, `filename`) +- [UPDATE] `BITUpdateManager`: Improved algorithm for fetching an optimal sized app icon for the Update View +- [UPDATE] `BITUpdateManager`: Properly consider paragraphs in releases notes when presenting them in the Update View +- [UPDATE] `BITCrashManager`: Updated PLCrashReporter to version 1.2 +- [UPDATE] `BITCrashManager`: Added `osVersion` and `osBuild` properties to `BITCrashDetails` +- [BUGFIX] `BITCrashManager`: Use correct filename for crash report attachments +- [UPDATE] Property `delegate` in all components is now private. Set the delegate on `BITHockeyManager` only! +- [BUGFIX] Various additional fixes +

+ ## Version 3.6.0 Beta 1 - [NEW] Minimum iOS Deployment version is now iOS 6.0 diff --git a/docs/Guide-Installation-Setup-Advanced-template.md b/docs/Guide-Installation-Setup-Advanced-template.md index 4ad17fa5..6b4d0a0d 100644 --- a/docs/Guide-Installation-Setup-Advanced-template.md +++ b/docs/Guide-Installation-Setup-Advanced-template.md @@ -1,6 +1,6 @@ -## Version 3.5 Beta 1 +## Version 3.6 Beta 2 -- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Changelog.html) +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Changelog.html) ## Introduction @@ -150,10 +150,13 @@ Instead of manually adding the missing frameworks, you can also use our bundled **Important note:** Check if you overwrite any of the build settings and add a missing `$(inherited)` entry on the projects build settings level, so the `HockeySDK.xcconfig` settings will be passed through successfully. 4. If you are getting build warnings, then the `.xcconfig` setting wasn't included successfully or its settings in `Other Linker Flags` get ignored because `$(inherited)` is missing on project or target level. Either add `$(inherited)` or link the following frameworks manually in `Link Binary With Libraries` under `Build Phases`: + - `AssetsLibrary` - `CoreText` - `CoreGraphics` - `Foundation` + - `MobileCoreServices` - `QuartzCore` + - `QuickLook` - `Security` - `SystemConfiguration` - `UIKit` diff --git a/docs/Guide-Installation-Setup-template.md b/docs/Guide-Installation-Setup-template.md index 680bc3d1..82afe193 100644 --- a/docs/Guide-Installation-Setup-template.md +++ b/docs/Guide-Installation-Setup-template.md @@ -1,6 +1,6 @@ -## Version 3.6 Beta 1 +## Version 3.6 Beta 2 -- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Changelog.html) +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Changelog.html) ## Introduction @@ -93,9 +93,9 @@ The Mac Desktop Uploader can provide easy uploading of your app versions to Hock This documentation provides integrated help in Xcode for all public APIs and a set of additional tutorials and how-tos. -1. Copy `de.bitstadium.HockeySDK-iOS-3.5.5.docset` into ~`/Library/Developer/Shared/Documentation/DocSets` +1. Copy `de.bitstadium.HockeySDK-iOS-3.6-b.2.docset` into ~`/Library/Developer/Shared/Documentation/DocSets` -The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.1/](http://hockeyapp.net/help/sdk/ios/3.6-b.1/) +The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.2/](http://hockeyapp.net/help/sdk/ios/3.6-b.2/) ### Set up with xcconfig @@ -124,10 +124,13 @@ Instead of manually adding the missing frameworks, you can also use our bundled **Important note:** Check if you overwrite any of the build settings and add a missing `$(inherited)` entry on the projects build settings level, so the `HockeySDK.xcconfig` settings will be passed through successfully. 7. If you are getting build warnings, then the `.xcconfig` setting wasn't included successfully or its settings in `Other Linker Flags` get ignored because `$(inherited)` is missing on project or target level. Either add `$(inherited)` or link the following frameworks manually in `Link Binary With Libraries` under `Build Phases`: + - `AssetsLibrary` - `CoreText` - `CoreGraphics` - `Foundation` + - `MobileCoreServices` - `QuartzCore` + - `QuickLook` - `Security` - `SystemConfiguration` - `UIKit` diff --git a/docs/HockeyMacGeneral_normal.png b/docs/HockeyMacGeneral_normal.png deleted file mode 100644 index b1f3f3f132c1af94c71386d34a62cd16e8d1c52d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73103 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úh^N1atb#PD3IMo{4VQM+2CIf@Q($_^EkY;^bZ<@ z=GfCRnlz2!hT?-1yT!OjD@f;TU$VZ+9~=_K?^y^SW-NR1uHcQ)|J^w^#@qEZba5Yc z(qoCyb!Crc_i&Amjqg!Mj5dSyh@j14#eMAdUB-BRLh{df{wX;Hz}iOu_0~K?F#!q2 zZ8nhQ3G#WK^)0SrGO4sPW+5iuE1bF@PSGFh4TU)9zy3c_byQ;IYtBtJ^89YwCwU5^HqoKCv1)|#{ZIEESB}*_2h?JFp+$CM;t{Wk+tlD{ncY{U{MnF3SA25bQ#=Z?03hfxp-ctlAE~*fS;+_ z+*kQ+>~~B5rHoGAuDl=j;Ttr%J@@uual5?MmE6?e%5>4y{!i|i6|Ll(Q>8SSG ze-$hCpF8r0O&242iDA&Ou2&%zRQq{A$VHFzAsuYZmb(jDM41Tg)V+(WUhMv3|FOuxjmoLHp8{a>{L=B0b6qsm3f zpE+duBQU`O)&VxJPsGEp23bl!tE*KY3Z|LatJI%xPyc54=TNI?GZK)(3lg z7j@jf;(?+VO&k$Fw<>C(%Td%$vveT3?=QvGA%r(}xq=!*w1zzX?O1}k@8*&k!2X*f z-%aGK5@wD{r~YQBHHq+@h)9H|kkbF_@f&y^TWwrD+cPWm@GaEi8&(e}INT2lz~5hM z#fCn6`r1*K28T|jwp(0C!^D9og(BvY85Ydpd6MO-1{942Q%0Dj!Ck6Z%f4(%MW0Rn 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

nz0!lO~pY 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 diff --git a/docs/HowTo-Add-Application-Log-template.md b/docs/HowTo-Add-Application-Log-template.md deleted file mode 100644 index 95b1e6d9..00000000 --- a/docs/HowTo-Add-Application-Log-template.md +++ /dev/null @@ -1,97 +0,0 @@ -## Introduction - -For some types of crashes it is helpful to have more data available, than a crash report itself can provide, e.g. application specific log data. - -Since the crash report can only be send on the next start, you need to store the log data into a log file. And to make that as fast as possible it should not block the main thread. We highly recommend using [CocoaLumberjack](https://github.com/robbiehanson/CocoaLumberjack/) or [NSLogger](https://github.com/fpillet/NSLogger) or even both in combination using the [NSLogger-CocoaLumberjack-connector](https://github.com/steipete/NSLogger-CocoaLumberjack-connector). - -CocoaLumberjack can write log data to multiple destinations non blocking (!!), like the Xcode console or files, and NSLogger has the ability to stream log data over Bonjour to it's Mac application. We do *NOT* recommend to use `NSLog`! - -**Important:** Make sure *NOT* to include personalized data into the log data because of privacy reasons! Also don't send too much data that you will never use. The crash report and the log data should be small in size, so they get send quickly even under bad mobile network conditions. - - -## HowTo - -1. Setup the logging framework of choice -2. Implement `[BITCrashManagerDelegate applicationLogForCrashManager:]` -3. Return the log data - -## Example - -This example code is based on CocoaLumberjack logging into log files: - - @interface BITAppDelegate () {} - @property (nonatomic) DDFileLogger *fileLogger; - @end - - - @implementation BITAppDelegate - - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [self.window makeKeyAndVisible]; - - // initialize before HockeySDK, so the delegate can access the file logger! - _fileLogger = [[DDFileLogger alloc] init]; - _fileLogger.maximumFileSize = (1024 * 64); // 64 KByte - _fileLogger.logFileManager.maximumNumberOfLogFiles = 1; - [_fileLogger rollLogFile]; - [DDLog addLogger:_fileLogger]; - - [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"<>" - delegate:nil]; - - [[BITHockeyManager sharedHockeyManager] startManager]; - - // add Xcode console logger if not running in the App Store - if (![[BITHockeyManager sharedHockeyManager] isAppStoreEnvironment]) { - PSDDFormatter *psLogger = [[[PSDDFormatter alloc] init] autorelease]; - [[DDTTYLogger sharedInstance] setLogFormatter:psLogger]; - - [DDLog addLogger:[DDTTYLogger sharedInstance]]; - - [DDLog addLogger:[DDNSLoggerLogger sharedInstance]]; - } - - return YES; - } - - // get the log content with a maximum byte size - - (NSString *) getLogFilesContentWithMaxSize:(NSInteger)maxSize { - NSMutableString *description = [NSMutableString string]; - - NSArray *sortedLogFileInfos = [[_fileLogger logFileManager] sortedLogFileInfos]; - NSUInteger count = [sortedLogFileInfos count]; - - // we start from the last one - for (NSUInteger index = count - 1; index >= 0; index--) { - DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:index]; - - NSData *logData = [[NSFileManager defaultManager] contentsAtPath:[logFileInfo filePath]]; - if ([logData length] > 0) { - NSString *result = [[NSString alloc] initWithBytes:[logData bytes] - length:[logData length] - encoding: NSUTF8StringEncoding]; - - [description appendString:result]; - [result release]; - } - } - - if ([description length] > maxSize) { - description = (NSMutableString *)[description substringWithRange:NSMakeRange([description length]-maxSize-1, maxSize)]; - } - - return description; - } - - #pragma mark - BITCrashManagerDelegate - - - (NSString *)applicationLogForCrashManager:(BITCrashManager *)crashManager { - NSString *description = [self getLogFilesContentWithMaxSize:5000]; // 5000 bytes should be enough! - if ([description length] == 0) { - return nil; - } else { - return description; - } - } - - @end diff --git a/docs/HowTo-Handle-Crashes-On-Startup-template.md b/docs/HowTo-Handle-Crashes-On-Startup-template.md deleted file mode 100644 index d0116e22..00000000 --- a/docs/HowTo-Handle-Crashes-On-Startup-template.md +++ /dev/null @@ -1,78 +0,0 @@ -## Introduction - -To catch and send crashes that occur while the app is starting up, the app has to get adjusted a little bit to make this work. - -The challenges in this scenario are: - -- Sending crash reports needs to be asynchronous, otherwise it would block the main thread or bad network conditions could make it even worse -- If the startup takes too long or the main thread is blocking too long, the watchdog process will kill the app -- The app might crash again before the crash report could have been send - - -## HowTo - -1. Setup the SDK -2. Check if the app crashed in the last session by checking `[BITCrashManager didCrashInLastSession]` -3. Check if `[BITCrashManager timeintervalCrashInLastSessionOccured]` is below a treshhold that you define. E.g. say your app usually requires 2 seconds for startup, and giving sending a crash report some time, you mighe choose `5` seconds as the treshhold -4. If the crash happened in that timeframe, delay your app initialization and show an intermediate screen -5. Implement the `BITCrashManagerDelegate` protocol methods `- (void)crashManagerWillCancelSendingCrashReport:(BITCrashManager *)crashManager`, `- (void)crashManager:(BITCrashManager *)crashManager didFailWithError:(NSError *)error;` and `- (void)crashManagerDidFinishSendingCrashReport:(BITCrashManager *)crashManager;` and continue app initialization - -## Example - - @interface BITAppDelegate () {} - @end - - - @implementation BITAppDelegate - - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [self.window makeKeyAndVisible]; - - [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"<>" - delegate:nil]; - - // optionally enable logging to get more information about states. - [BITHockeyManager sharedHockeyManager].debugLogEnabled = YES; - - [[BITHockeyManager sharedHockeyManager] startManager]; - - if ([self didCrashInLastSessionOnStartup]) { - // show intermediate UI - } else { - [self setupApplication]; - } - - return YES; - } - - - (BOOL)didCrashInLastSessionOnStartup { - return ([[BITHockeyManager sharedHockeyManager].crashManager didCrashInLastSession] && - [[BITHockeyManager sharedHockeyManager].crashManager timeintervalCrashInLastSessionOccured] < 5); - } - - - (void)setupApplication { - // setup your app specific code - } - - #pragma mark - BITCrashManagerDelegate - - - (void)crashManagerWillCancelSendingCrashReport:(BITCrashManager *)crashManager { - if ([self didCrashInLastSessionOnStartup]) { - [self setupApplication]; - } - } - - - (void)crashManager:(BITCrashManager *)crashManager didFailWithError:(NSError *)error { - if ([self didCrashInLastSessionOnStartup]) { - [self setupApplication]; - } - } - - - (void)crashManagerDidFinishSendingCrashReport:(BITCrashManager *)crashManager { - if ([self didCrashInLastSessionOnStartup]) { - [self setupApplication]; - } - } - - @end - diff --git a/docs/Troubleshooting-Symbolication-Doesnt-Work-template.md b/docs/Troubleshooting-Symbolication-Doesnt-Work-template.md deleted file mode 100644 index 3b835892..00000000 --- a/docs/Troubleshooting-Symbolication-Doesnt-Work-template.md +++ /dev/null @@ -1,40 +0,0 @@ -## Symbolication doesn't work - -In most cases the symbolication process doesn't work, since there is no dSYM uploaded to HockeyApp, or the dSYM doesn't match the application binary. So this is also about the rules of binary UUIDs and dSYMs. - -**IMPORTANT:** Each time you run the build command, your app gets a new unique UUID which is placed into the crash report to idenfiy the build. You also get a new dSYM package which contains the same UUID. So if you upload a new binary to the app store, you also have to upload the new dSYM to HockeyApp! - -Here are some tips on how to find out which UUID is used where: - -1. Find the UUID in the crash report: - - - Scroll down the crash report until you find `Binary Images:`. - - The first line below that shows something like the following: - - 0x1000 - 0x222fff +AppName armv7 <1234567890abcdef1234567890abcdef> /var/mobile/Applications/ABCDEF01-1234-5678-9ABC-DEF012345678/AppName.app/AppName - - `1234567890abcdef1234567890abcdef` is the UUID of your binary for the `armv7` architecture. - -2. Find the UUID in the app binary (1 line for each architecture): - - dwarfdump --uuid AppName.app/AppName - - The result will look like: - - UUID: 12345678-90AB-CDEF-1234-567890ABCDEF (armv7) AppName.app/AppName - -3. Find the UUID in the dSYM (1 line for each architecture): - - dwarfdump --uuid AppName.app.dSYM - - The result will look like: - - UUID: 12345678-90AB-CDEF-1234-567890ABCDEF (armv7) AppName.app.dSYM/Contents/Resources/DWARF/AppName - -4. Find the dSYM for a specific UUID on your computer: - - mdfind "com_apple_xcode_dsym_uuids == 12345678-90AB-CDEF-1234-567890ABCDEF" - - The string "12345678-90AB-CDEF-1234-567890ABCDEF" is the UUID string from the crash report reformatted to uppercase and 8-4-4-4-12 groups. - -If you found the correct dSYM, please upload it again and HockeyApp will process the crash logs a second time. \ No newline at end of file diff --git a/docs/XcodeArchivePostAction_normal.png b/docs/XcodeArchivePostAction_normal.png deleted file mode 100644 index 4dfe971cd8be5d480fa1452e6c9fa980a78e59ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74258 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 Date: Thu, 26 Jun 2014 16:05:27 +0200 Subject: [PATCH 177/206] Fix title in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 90384ab4..caa45c39 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -## Version 3.6 Beta 1 +## Version 3.6 Beta 2 -- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.1/docs/docs/Changelog.html) +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Changelog.html) ## Introduction From 53edf21c5dfd7ea1db23be458eaa12ba32bf1fcd Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 8 Jul 2014 21:03:26 +0200 Subject: [PATCH 178/206] Fix some best app icon finding issues In some cases the algorithm didn't find any icon, even if they are there --- Classes/BITHockeyHelper.h | 4 +- Classes/BITHockeyHelper.m | 45 ++++++++++++------- Classes/BITUpdateViewController.m | 2 +- Support/HockeySDKTests/BITHockeyHelperTests.m | 15 ++++--- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/Classes/BITHockeyHelper.h b/Classes/BITHockeyHelper.h index 9a7e1ecf..0f91ae1f 100644 --- a/Classes/BITHockeyHelper.h +++ b/Classes/BITHockeyHelper.h @@ -48,8 +48,8 @@ NSString *bit_UUID(void); NSString *bit_appAnonID(void); BOOL bit_isPreiOS7Environment(void); -NSString *bit_validAppIconStringFromIcons(NSArray *icons); -NSString *bit_validAppIconFilename(NSBundle *bundle); +NSString *bit_validAppIconStringFromIcons(NSBundle *resourceBundle, NSArray *icons); +NSString *bit_validAppIconFilename(NSBundle *bundle, NSBundle *resourceBundle); /* UIImage helpers */ UIImage *bit_roundedCornerImage(UIImage *inputImage, NSInteger cornerSize, NSInteger borderSize); diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 407bb14b..5c954b43 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -245,7 +245,7 @@ BOOL bit_isPreiOS7Environment(void) { @return NSString with the valid app icon or nil if none found */ -NSString *bit_validAppIconStringFromIcons(NSArray *icons) { +NSString *bit_validAppIconStringFromIcons(NSBundle *resourceBundle, NSArray *icons) { if (!icons) return nil; if (![icons isKindOfClass:[NSArray class]]) return nil; @@ -267,16 +267,29 @@ BOOL bit_isPreiOS7Environment(void) { for(NSString *icon in icons) { // Don't use imageNamed, otherwise unit tests won't find the fixture icon // and using imageWithContentsOfFile doesn't load @2x files with absolut paths (required in tests) - NSData *imgData = [[NSData alloc] initWithContentsOfFile:icon]; - UIImage *iconImage = [[UIImage alloc] initWithData:imgData]; + + NSString *iconPathExtension = ([[icon pathExtension] length] > 0) ? [icon pathExtension] : @"png"; + NSMutableArray *iconFilenameVariants = [NSMutableArray new]; + + [iconFilenameVariants addObject:[icon stringByDeletingPathExtension]]; + [iconFilenameVariants addObject:[NSString stringWithFormat:@"%@@2x", [icon stringByDeletingPathExtension]]]; - if (iconImage) { - if (iconImage.size.height == bestMatchHeight) { - return icon; - } else if (iconImage.size.height < bestMatchHeight && - iconImage.size.height > currentBestMatchHeight) { - currentBestMatchHeight = iconImage.size.height; - currentBestMatch = icon; + for (NSString *iconFilename in iconFilenameVariants) { + // this call already covers "~ipad" files + NSString *iconPath = [resourceBundle pathForResource:iconFilename ofType:iconPathExtension]; + + NSData *imgData = [[NSData alloc] initWithContentsOfFile:iconPath]; + + UIImage *iconImage = [[UIImage alloc] initWithData:imgData]; + + if (iconImage) { + if (iconImage.size.height == bestMatchHeight) { + return iconFilename; + } else if (iconImage.size.height < bestMatchHeight && + iconImage.size.height > currentBestMatchHeight) { + currentBestMatchHeight = iconImage.size.height; + currentBestMatch = iconFilename; + } } } } @@ -284,19 +297,19 @@ BOOL bit_isPreiOS7Environment(void) { return currentBestMatch; } -NSString *bit_validAppIconFilename(NSBundle *bundle) { +NSString *bit_validAppIconFilename(NSBundle *bundle, NSBundle *resourceBundle) { NSString *iconFilename = nil; NSArray *icons = nil; icons = [bundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]; - iconFilename = bit_validAppIconStringFromIcons(icons); + iconFilename = bit_validAppIconStringFromIcons(resourceBundle, icons); if (!iconFilename) { icons = [bundle objectForInfoDictionaryKey:@"CFBundleIcons"]; if (icons && [icons isKindOfClass:[NSDictionary class]]) { icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; } - iconFilename = bit_validAppIconStringFromIcons(icons); + iconFilename = bit_validAppIconStringFromIcons(resourceBundle, icons); } // we test iPad structure anyway and use it if we find a result and don't have another one yet @@ -305,7 +318,7 @@ BOOL bit_isPreiOS7Environment(void) { if (icons && [icons isKindOfClass:[NSDictionary class]]) { icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; } - NSString *iPadIconFilename = bit_validAppIconStringFromIcons(icons); + NSString *iPadIconFilename = bit_validAppIconStringFromIcons(resourceBundle, icons); if (iPadIconFilename && !iconFilename) { iconFilename = iPadIconFilename; } @@ -314,12 +327,12 @@ BOOL bit_isPreiOS7Environment(void) { if (!iconFilename) { NSString *tempFilename = [bundle objectForInfoDictionaryKey:@"CFBundleIconFile"]; if (tempFilename) { - iconFilename = bit_validAppIconStringFromIcons(@[tempFilename]); + iconFilename = bit_validAppIconStringFromIcons(resourceBundle, @[tempFilename]); } } if (!iconFilename) { - iconFilename = bit_validAppIconStringFromIcons(@[@"Icon.png"]); + iconFilename = bit_validAppIconStringFromIcons(resourceBundle, @[@"Icon.png"]); } return iconFilename; diff --git a/Classes/BITUpdateViewController.m b/Classes/BITUpdateViewController.m index e75e22b1..142112ef 100644 --- a/Classes/BITUpdateViewController.m +++ b/Classes/BITUpdateViewController.m @@ -294,7 +294,7 @@ - (void)viewDidLoad { } [self updateAppStoreHeader]; - NSString *iconFilename = bit_validAppIconFilename([NSBundle mainBundle]); + NSString *iconFilename = bit_validAppIconFilename([NSBundle mainBundle], [NSBundle mainBundle]); if (iconFilename) { BOOL addGloss = YES; NSNumber *prerendered = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIPrerenderedIcon"]; diff --git a/Support/HockeySDKTests/BITHockeyHelperTests.m b/Support/HockeySDKTests/BITHockeyHelperTests.m index 3cd800ad..b6532d52 100644 --- a/Support/HockeySDKTests/BITHockeyHelperTests.m +++ b/Support/HockeySDKTests/BITHockeyHelperTests.m @@ -93,8 +93,9 @@ - (void)testAppAnonID { - (void)testValidAppIconFilename { NSString *resultString = nil; NSBundle *mockBundle = mock([NSBundle class]); - NSString *validIconPath = [[NSBundle bundleForClass:self.class] pathForResource:@"AppIcon" ofType:@"png"]; - NSString *validIconPath2x = [[NSBundle bundleForClass:self.class] pathForResource:@"AppIcon@2x" ofType:@"png"]; + NSBundle *resourceBundle = [NSBundle bundleForClass:self.class]; + NSString *validIconPath = @"AppIcon"; + NSString *validIconPath2x = @"AppIcon@2x"; // No valid icons defined at all [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:nil]; @@ -102,7 +103,7 @@ - (void)testValidAppIconFilename { [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:@"invalidFilename.png"]; - resultString = bit_validAppIconFilename(mockBundle); + resultString = bit_validAppIconFilename(mockBundle, resourceBundle); assertThat(resultString, nilValue()); // CFBundleIconFiles contains valid filenames @@ -111,7 +112,7 @@ - (void)testValidAppIconFilename { [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; - resultString = bit_validAppIconFilename(mockBundle); + resultString = bit_validAppIconFilename(mockBundle, resourceBundle); assertThat(resultString, notNilValue()); // CFBundleIcons contains valid dictionary filenames @@ -126,7 +127,7 @@ - (void)testValidAppIconFilename { [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; - resultString = bit_validAppIconFilename(mockBundle); + resultString = bit_validAppIconFilename(mockBundle, resourceBundle); assertThat(resultString, notNilValue()); // CFBundleIcons contains valid filenames @@ -135,7 +136,7 @@ - (void)testValidAppIconFilename { [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; - resultString = bit_validAppIconFilename(mockBundle); + resultString = bit_validAppIconFilename(mockBundle, resourceBundle); assertThat(resultString, notNilValue()); // CFBundleIcon contains valid filename @@ -144,7 +145,7 @@ - (void)testValidAppIconFilename { [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:validIconPath]; - resultString = bit_validAppIconFilename(mockBundle); + resultString = bit_validAppIconFilename(mockBundle, resourceBundle); assertThat(resultString, notNilValue()); } From 3917622aa5915e29efd0da837967fae63e2e7adc Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Mon, 14 Jul 2014 18:13:04 +0200 Subject: [PATCH 179/206] Remove KVO observer on dealloc This case should never happen, since the SDK is supposed to be used via the sharedInstance and never be deallocated. --- Classes/BITHockeyManager.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index 5cd2b8c8..802deaa9 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -162,6 +162,15 @@ - (id) init { return self; } +- (void)dealloc { +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + // start Authenticator + if (![self isAppStoreEnvironment]) { + [_authenticator removeObserver:self forKeyPath:@"identified"]; + } +#endif +} + #pragma mark - Public Instance Methods (Configuration) From 74e4bb7f1a05c0b12f5ac7965c46b8f7c834dcb0 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 17 Jul 2014 12:46:37 +0200 Subject: [PATCH 180/206] Migrate unit tests to XCTest --- Support/HockeySDK.xcodeproj/project.pbxproj | 18 ++++---- .../HockeySDKTests/BITAuthenticatorTests.m | 6 +-- Support/HockeySDKTests/BITCrashManagerTests.m | 6 +-- .../HockeySDKTests/BITFeedbackManagerTests.m | 4 +- .../HockeySDKTests/BITHockeyAppClientTests.m | 6 +-- Support/HockeySDKTests/BITHockeyHelperTests.m | 4 +- .../HockeySDKTests/BITKeychainUtilsTests.m | 4 +- .../BITStoreUpdateManagerTests.m | 42 +++++++++---------- 8 files changed, 43 insertions(+), 47 deletions(-) diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 3853064d..0d9a1ab7 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -96,7 +96,6 @@ 1E5955CF15B71C8600A03429 /* IconGradient.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E5955C415B71C8600A03429 /* IconGradient.png */; }; 1E5955D015B71C8600A03429 /* IconGradient@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E5955C515B71C8600A03429 /* IconGradient@2x.png */; }; 1E5955FD15B7877B00A03429 /* BITHockeyManagerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E5955FA15B7877A00A03429 /* BITHockeyManagerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 1E5A459216F0DFC200B55C04 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E5A459116F0DFC200B55C04 /* SenTestingKit.framework */; }; 1E5A459516F0DFC200B55C04 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E400561D148D79B500EB22B9 /* Foundation.framework */; }; 1E5A459B16F0DFC200B55C04 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E5A459916F0DFC200B55C04 /* InfoPlist.strings */; }; 1E5A459E16F0DFC200B55C04 /* BITStoreUpdateManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E5A459D16F0DFC200B55C04 /* BITStoreUpdateManagerTests.m */; }; @@ -286,8 +285,7 @@ 1E5955C415B71C8600A03429 /* IconGradient.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = IconGradient.png; sourceTree = ""; }; 1E5955C515B71C8600A03429 /* IconGradient@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "IconGradient@2x.png"; sourceTree = ""; }; 1E5955FA15B7877A00A03429 /* BITHockeyManagerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITHockeyManagerDelegate.h; sourceTree = ""; }; - 1E5A459016F0DFC200B55C04 /* HockeySDKTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HockeySDKTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; - 1E5A459116F0DFC200B55C04 /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; + 1E5A459016F0DFC200B55C04 /* HockeySDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HockeySDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 1E5A459816F0DFC200B55C04 /* HockeySDKTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "HockeySDKTests-Info.plist"; sourceTree = ""; }; 1E5A459A16F0DFC200B55C04 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 1E5A459D16F0DFC200B55C04 /* BITStoreUpdateManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BITStoreUpdateManagerTests.m; sourceTree = ""; }; @@ -416,7 +414,6 @@ 97CC11FA1917C0390028768F /* MobileCoreServices.framework in Frameworks */, 97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */, 1EA1170016F4D32C001C015C /* libHockeySDK.a in Frameworks */, - 1E5A459216F0DFC200B55C04 /* SenTestingKit.framework in Frameworks */, 1EA1170116F4D354001C015C /* CrashReporter.framework in Frameworks */, 1E5A459516F0DFC200B55C04 /* Foundation.framework in Frameworks */, 1E7A45FC16F54FB5005B08F1 /* OCHamcrestIOS.framework in Frameworks */, @@ -679,7 +676,7 @@ children = ( 1E5954F215B6F24A00A03429 /* libHockeySDK.a */, 1E59550A15B6F45800A03429 /* HockeySDKResources.bundle */, - 1E5A459016F0DFC200B55C04 /* HockeySDKTests.octest */, + 1E5A459016F0DFC200B55C04 /* HockeySDKTests.xctest */, ); name = Products; sourceTree = ""; @@ -692,7 +689,6 @@ 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */, E41EB48B148D7C4E0015DEDC /* CrashReporter.framework */, E400561D148D79B500EB22B9 /* Foundation.framework */, - 1E5A459116F0DFC200B55C04 /* SenTestingKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -870,8 +866,8 @@ ); name = HockeySDKTests; productName = HockeySDKTests; - productReference = 1E5A459016F0DFC200B55C04 /* HockeySDKTests.octest */; - productType = "com.apple.product-type.bundle"; + productReference = 1E5A459016F0DFC200B55C04 /* HockeySDKTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -1237,6 +1233,7 @@ "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", "\"$(SRCROOT)/../Vendor\"", "\"$(SRCROOT)/HockeySDKTests\"", + "$(inherited)", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "HockeySDKTests/HockeySDKTests-Prefix.pch"; @@ -1253,7 +1250,6 @@ ); ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; - WRAPPER_EXTENSION = octest; }; name = Debug; }; @@ -1273,6 +1269,7 @@ "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", "\"$(SRCROOT)/../Vendor\"", "\"$(SRCROOT)/HockeySDKTests\"", + "$(inherited)", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "HockeySDKTests/HockeySDKTests-Prefix.pch"; @@ -1286,7 +1283,6 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; VALIDATE_PRODUCT = YES; - WRAPPER_EXTENSION = octest; }; name = Release; }; @@ -1392,6 +1388,7 @@ "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", "\"$(SRCROOT)/../Vendor\"", "\"$(SRCROOT)/HockeySDKTests\"", + "$(inherited)", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "HockeySDKTests/HockeySDKTests-Prefix.pch"; @@ -1409,7 +1406,6 @@ ); ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; - WRAPPER_EXTENSION = octest; }; name = CodeCoverage; }; diff --git a/Support/HockeySDKTests/BITAuthenticatorTests.m b/Support/HockeySDKTests/BITAuthenticatorTests.m index a96f8fba..07a6ff65 100644 --- a/Support/HockeySDKTests/BITAuthenticatorTests.m +++ b/Support/HockeySDKTests/BITAuthenticatorTests.m @@ -6,7 +6,7 @@ // // -#import +#import #define HC_SHORTHAND #import @@ -45,7 +45,7 @@ - (instancetype)init { static void *kInstallationIdentification = &kInstallationIdentification; -@interface BITAuthenticatorTests : SenTestCase +@interface BITAuthenticatorTests : XCTestCase @end @implementation BITAuthenticatorTests { @@ -84,7 +84,7 @@ - (NSDictionary *)jsonFromFixture:(NSString *)fixture { #pragma mark - Setup Tests - (void) testThatItInstantiates { - STAssertNotNil(_sut, @"Should be there"); + XCTAssertNotNil(_sut, @"Should be there"); } #pragma mark - Persistence Tests diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 49289f08..51609ccf 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -6,7 +6,7 @@ // // -#import +#import #define HC_SHORTHAND #import @@ -25,7 +25,7 @@ #define kBITCrashMetaAttachment @"BITCrashMetaAttachment" -@interface BITCrashManagerTests : SenTestCase +@interface BITCrashManagerTests : XCTestCase @end @@ -75,7 +75,7 @@ - (void)startManagerAutoSend { #pragma mark - Setup Tests - (void)testThatItInstantiates { - STAssertNotNil(_sut, @"Should be there"); + XCTAssertNotNil(_sut, @"Should be there"); } diff --git a/Support/HockeySDKTests/BITFeedbackManagerTests.m b/Support/HockeySDKTests/BITFeedbackManagerTests.m index 838fdb56..ac00063f 100644 --- a/Support/HockeySDKTests/BITFeedbackManagerTests.m +++ b/Support/HockeySDKTests/BITFeedbackManagerTests.m @@ -6,7 +6,7 @@ // // -#import +#import #define HC_SHORTHAND #import @@ -23,7 +23,7 @@ #import "BITTestHelper.h" -@interface BITFeedbackManagerTests : SenTestCase +@interface BITFeedbackManagerTests : XCTestCase @end diff --git a/Support/HockeySDKTests/BITHockeyAppClientTests.m b/Support/HockeySDKTests/BITHockeyAppClientTests.m index 41a63aac..5f1f66b4 100644 --- a/Support/HockeySDKTests/BITHockeyAppClientTests.m +++ b/Support/HockeySDKTests/BITHockeyAppClientTests.m @@ -6,7 +6,7 @@ // // -#import +#import #define HC_SHORTHAND #import @@ -19,7 +19,7 @@ #import "BITHTTPOperation.h" #import "BITTestHelper.h" -@interface BITHockeyAppClientTests : SenTestCase +@interface BITHockeyAppClientTests : XCTestCase @end @implementation BITHockeyAppClientTests { @@ -57,7 +57,7 @@ - (NSDictionary *)jsonFromFixture:(NSString *)fixture { #pragma mark - Setup Tests - (void) testThatItInstantiates { - STAssertNotNil(_sut, @"Should be there"); + XCTAssertNotNil(_sut, @"Should be there"); } #pragma mark - Networking base tests diff --git a/Support/HockeySDKTests/BITHockeyHelperTests.m b/Support/HockeySDKTests/BITHockeyHelperTests.m index b6532d52..b6a0bf9d 100644 --- a/Support/HockeySDKTests/BITHockeyHelperTests.m +++ b/Support/HockeySDKTests/BITHockeyHelperTests.m @@ -6,7 +6,7 @@ // // -#import +#import #define HC_SHORTHAND #import @@ -19,7 +19,7 @@ #import "BITKeychainUtils.h" -@interface BITHockeyHelperTests : SenTestCase +@interface BITHockeyHelperTests : XCTestCase @end diff --git a/Support/HockeySDKTests/BITKeychainUtilsTests.m b/Support/HockeySDKTests/BITKeychainUtilsTests.m index 9c317be6..dc6ba513 100644 --- a/Support/HockeySDKTests/BITKeychainUtilsTests.m +++ b/Support/HockeySDKTests/BITKeychainUtilsTests.m @@ -6,7 +6,7 @@ // Copyright (c) 2013 __MyCompanyName__. All rights reserved. // -#import +#import #define HC_SHORTHAND #import @@ -17,7 +17,7 @@ #import "HockeySDK.h" #import "BITKeychainUtils.h" -@interface BITKeychainUtilsTests : SenTestCase { +@interface BITKeychainUtilsTests : XCTestCase { } @end diff --git a/Support/HockeySDKTests/BITStoreUpdateManagerTests.m b/Support/HockeySDKTests/BITStoreUpdateManagerTests.m index 897ec761..a9e2d9c1 100644 --- a/Support/HockeySDKTests/BITStoreUpdateManagerTests.m +++ b/Support/HockeySDKTests/BITStoreUpdateManagerTests.m @@ -6,7 +6,7 @@ // // -#import +#import // Uncomment the next two lines to use OCHamcrest for test assertions: #define HC_SHORTHAND @@ -25,7 +25,7 @@ #import "BITTestHelper.h" -@interface BITStoreUpdateManagerTests : SenTestCase +@interface BITStoreUpdateManagerTests : XCTestCase @end @@ -83,7 +83,7 @@ - (void)testUpdateCheckDailyFirstTimeEver { BOOL result = [_storeUpdateManager shouldAutoCheckForUpdates]; - STAssertTrue(result, @"Checking daily first time ever"); + XCTAssertTrue(result, @"Checking daily first time ever"); } - (void)testUpdateCheckDailyFirstTimeTodayLastCheckPreviousDay { @@ -96,7 +96,7 @@ - (void)testUpdateCheckDailyFirstTimeTodayLastCheckPreviousDay { BOOL result = [_storeUpdateManager shouldAutoCheckForUpdates]; - STAssertTrue(result, @"Checking daily first time today with last check done previous day"); + XCTAssertTrue(result, @"Checking daily first time today with last check done previous day"); } - (void)testUpdateCheckDailySecondTimeOfTheDay { @@ -108,7 +108,7 @@ - (void)testUpdateCheckDailySecondTimeOfTheDay { BOOL result = [_storeUpdateManager shouldAutoCheckForUpdates]; - STAssertFalse(result, @"Checking daily second time of the day"); + XCTAssertFalse(result, @"Checking daily second time of the day"); } - (void)testUpdateCheckWeeklyFirstTimeEver { @@ -120,7 +120,7 @@ - (void)testUpdateCheckWeeklyFirstTimeEver { BOOL result = [_storeUpdateManager shouldAutoCheckForUpdates]; - STAssertTrue(result, @"Checking weekly first time ever"); + XCTAssertTrue(result, @"Checking weekly first time ever"); } - (void)testUpdateCheckWeeklyFirstTimeTodayLastCheckPreviousWeek { @@ -133,7 +133,7 @@ - (void)testUpdateCheckWeeklyFirstTimeTodayLastCheckPreviousWeek { BOOL result = [_storeUpdateManager shouldAutoCheckForUpdates]; - STAssertTrue(result, @"Checking weekly first time after one week"); + XCTAssertTrue(result, @"Checking weekly first time after one week"); } - (void)testUpdateCheckWeeklyFirstTimeFiveDaysAfterPreviousCheck { @@ -146,7 +146,7 @@ - (void)testUpdateCheckWeeklyFirstTimeFiveDaysAfterPreviousCheck { BOOL result = [_storeUpdateManager shouldAutoCheckForUpdates]; - STAssertFalse(result, @"Checking weekly first time five days after previous check"); + XCTAssertFalse(result, @"Checking weekly first time five days after previous check"); } - (void)testUpdateCheckManuallyFirstTimeEver { @@ -158,7 +158,7 @@ - (void)testUpdateCheckManuallyFirstTimeEver { BOOL result = [_storeUpdateManager shouldAutoCheckForUpdates]; - STAssertFalse(result, @"Checking manually first time ever"); + XCTAssertFalse(result, @"Checking manually first time ever"); } - (void)testUpdateCheckManuallyFirstTimeTodayLastCheckDonePreviousDay { @@ -171,7 +171,7 @@ - (void)testUpdateCheckManuallyFirstTimeTodayLastCheckDonePreviousDay { BOOL result = [_storeUpdateManager shouldAutoCheckForUpdates]; - STAssertFalse(result, @"Checking manually first time ever"); + XCTAssertFalse(result, @"Checking manually first time ever"); } @@ -180,28 +180,28 @@ - (void)testUpdateCheckManuallyFirstTimeTodayLastCheckDonePreviousDay { - (void)testProcessStoreResponseWithEmptyData { BOOL result = [_storeUpdateManager processStoreResponseWithString:nil]; - STAssertFalse(result, @"Empty data was handled correctly"); + XCTAssertFalse(result, @"Empty data was handled correctly"); } - (void)testProcessStoreResponseWithInvalidData { NSString *invalidString = @"8a@c&)if"; BOOL result = [_storeUpdateManager processStoreResponseWithString:invalidString]; - STAssertFalse(result, @"Invalid JSON data was handled correctly"); + XCTAssertFalse(result, @"Invalid JSON data was handled correctly"); } - (void)testProcessStoreResponseWithUnknownBundleIdentifier { NSString *dataString = [BITTestHelper jsonFixture:@"StoreBundleIdentifierUnknown"]; BOOL result = [_storeUpdateManager processStoreResponseWithString:dataString]; - STAssertFalse(result, @"Valid but empty json data was handled correctly"); + XCTAssertFalse(result, @"Valid but empty json data was handled correctly"); } - (void)testProcessStoreResponseWithKnownBundleIdentifier { NSString *dataString = [BITTestHelper jsonFixture:@"StoreBundleIdentifierKnown"]; BOOL result = [_storeUpdateManager processStoreResponseWithString:dataString]; - STAssertTrue(result, @"Valid and correct JSON data was handled correctly"); + XCTAssertTrue(result, @"Valid and correct JSON data was handled correctly"); } @@ -219,7 +219,7 @@ - (void)testFirstStartHasNewVersionReturnsFalseWithFirstCheck { BOOL result = [_storeUpdateManager hasNewVersion:json]; - STAssertFalse(result, @"There is no udpate available"); + XCTAssertFalse(result, @"There is no udpate available"); } - (void)testFirstStartHasNewVersionReturnsFalseWithSameVersion { @@ -234,7 +234,7 @@ - (void)testFirstStartHasNewVersionReturnsFalseWithSameVersion { BOOL result = [_storeUpdateManager hasNewVersion:json]; - STAssertFalse(result, @"There is no udpate available"); + XCTAssertFalse(result, @"There is no udpate available"); } @@ -250,7 +250,7 @@ - (void)testFirstStartHasNewVersionReturnsFalseWithSameVersionButDifferentUUID { BOOL result = [_storeUpdateManager hasNewVersion:json]; - STAssertFalse(result, @"There is no udpate available"); + XCTAssertFalse(result, @"There is no udpate available"); } - (void)testFirstStartHasNewVersionReturnsTrue { @@ -265,7 +265,7 @@ - (void)testFirstStartHasNewVersionReturnsTrue { BOOL result = [_storeUpdateManager hasNewVersion:json]; - STAssertTrue(result, @"There is an udpate available"); + XCTAssertTrue(result, @"There is an udpate available"); } @@ -281,7 +281,7 @@ - (void)testFirstStartHasNewVersionReturnsFalseBecauseWeHaveANewerVersionInstall BOOL result = [_storeUpdateManager hasNewVersion:json]; - STAssertFalse(result, @"There is no udpate available"); + XCTAssertFalse(result, @"There is no udpate available"); } - (void)testReportedVersionIsBeingIgnored { @@ -297,7 +297,7 @@ - (void)testReportedVersionIsBeingIgnored { BOOL result = [_storeUpdateManager hasNewVersion:json]; - STAssertFalse(result, @"The newer version is being ignored"); + XCTAssertFalse(result, @"The newer version is being ignored"); } - (void)testReportedVersionIsNewerThanTheIgnoredVersion { @@ -313,7 +313,7 @@ - (void)testReportedVersionIsNewerThanTheIgnoredVersion { BOOL result = [_storeUpdateManager hasNewVersion:json]; - STAssertTrue(result, @"The newer version is not ignored"); + XCTAssertTrue(result, @"The newer version is not ignored"); } @end From bb165bdcefba7221402dabb29c2081eb949d79f3 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 24 Jul 2014 18:20:14 +0200 Subject: [PATCH 181/206] Use library caches folder for BITUpdateManager --- Classes/BITUpdateManager.m | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Classes/BITUpdateManager.m b/Classes/BITUpdateManager.m index a34dd94e..f2a9599e 100644 --- a/Classes/BITUpdateManager.m +++ b/Classes/BITUpdateManager.m @@ -430,18 +430,7 @@ - (instancetype)init { _fileManager = [[NSFileManager alloc] init]; - // temporary directory for crashes grabbed from PLCrashReporter - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); - _updateDir = [[paths objectAtIndex:0] stringByAppendingPathComponent:BITHOCKEY_IDENTIFIER]; - - if (![_fileManager fileExistsAtPath:_updateDir]) { - NSDictionary *attributes = [NSDictionary dictionaryWithObject: [NSNumber numberWithUnsignedLong: 0755] forKey: NSFilePosixPermissions]; - NSError *theError = NULL; - - [_fileManager createDirectoryAtPath:_updateDir withIntermediateDirectories: YES attributes: attributes error: &theError]; - } - - _usageDataFile = [_updateDir stringByAppendingPathComponent:BITHOCKEY_USAGE_DATA]; + _usageDataFile = [bit_settingsDir() stringByAppendingPathComponent:BITHOCKEY_USAGE_DATA]; [self loadAppCache]; From 1678d7550a0cc667b14e1efa5608ce3d271bfa42 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 25 Jul 2014 12:24:41 +0200 Subject: [PATCH 182/206] Send install uuid with BITAuthenticator requests --- Classes/BITAuthenticator.m | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Classes/BITAuthenticator.m b/Classes/BITAuthenticator.m index 43e5e74f..1b477a57 100644 --- a/Classes/BITAuthenticator.m +++ b/Classes/BITAuthenticator.m @@ -340,6 +340,12 @@ - (void) validateWithCompletion:(void (^)(BOOL validated, NSError *))completion - (NSDictionary*) validationParameters { NSParameterAssert(self.installationIdentifier); NSParameterAssert(self.installationIdentifierParameterString); + + NSString *installString = bit_appAnonID(); + if (installString) { + return @{self.installationIdentifierParameterString : self.installationIdentifier, @"install_string": installString}; + } + return @{self.installationIdentifierParameterString : self.installationIdentifier}; } @@ -455,6 +461,19 @@ - (NSURLRequest *) requestForAuthenticationEmail:(NSString*) email password:(NSS NSString *authValue = [NSString stringWithFormat:@"Basic %@", bit_base64String(authData, authData.length)]; [request setValue:authValue forHTTPHeaderField:@"Authorization"]; } + + NSMutableData *postBody = [NSMutableData data]; + NSString *boundary = @"----FOO"; + + NSString *installString = bit_appAnonID(); + if (installString) { + [postBody appendData:[BITHockeyAppClient dataWithPostValue:installString forKey:@"install_string" boundary:boundary]]; + } + [postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + + [request setHTTPBody:postBody]; + return request; } From cc34cfece82d810bc21a5e1c4653ce07bd00a8c2 Mon Sep 17 00:00:00 2001 From: Daniel Tull Date: Mon, 4 Aug 2014 19:37:45 +0100 Subject: [PATCH 183/206] Fix missing call to super analyzer warning This is flagged up in Xcode 6's shallow analyzer check. --- Classes/BITImageAnnotationViewController.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m index ed8aeff6..14069964 100644 --- a/Classes/BITImageAnnotationViewController.m +++ b/Classes/BITImageAnnotationViewController.m @@ -130,6 +130,7 @@ - (void)viewWillAppear:(BOOL)animated { } - (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; [super viewWillDisappear:animated]; From a441811a42cdba883b6fd88edcd8362129a20eb0 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 8 Aug 2014 16:20:14 +0200 Subject: [PATCH 184/206] Fix broken auth The commit 1678d7550a0cc667b14e1efa5608ce3d271bfa42 did break BITAuthenticator --- Classes/BITAuthenticator.m | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/Classes/BITAuthenticator.m b/Classes/BITAuthenticator.m index 1b477a57..17586c85 100644 --- a/Classes/BITAuthenticator.m +++ b/Classes/BITAuthenticator.m @@ -440,16 +440,20 @@ - (void)authenticationViewController:(UIViewController *)viewController - (NSURLRequest *) requestForAuthenticationEmail:(NSString*) email password:(NSString*) password { NSString *authenticationPath = [self authenticationPath]; - NSDictionary *params = nil; + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + NSString *installString = bit_appAnonID(); + if (installString) { + params[@"install_string"] = installString; + } + if(BITAuthenticatorIdentificationTypeHockeyAppEmail == self.identificationType) { NSString *authCode = BITHockeyMD5([NSString stringWithFormat:@"%@%@", self.authenticationSecret ? : @"", email ? : @""]); - params = @{ - @"email" : email ? : @"", - @"authcode" : authCode.lowercaseString, - }; + + params[@"email"] = email ? : @""; + params[@"authcode"] = authCode.lowercaseString; } NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST" @@ -462,18 +466,6 @@ - (NSURLRequest *) requestForAuthenticationEmail:(NSString*) email password:(NSS [request setValue:authValue forHTTPHeaderField:@"Authorization"]; } - NSMutableData *postBody = [NSMutableData data]; - NSString *boundary = @"----FOO"; - - NSString *installString = bit_appAnonID(); - if (installString) { - [postBody appendData:[BITHockeyAppClient dataWithPostValue:installString forKey:@"install_string" boundary:boundary]]; - } - [postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - - - [request setHTTPBody:postBody]; - return request; } From 41fab8ae29edd5882029c717fed54f9e0917b15f Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 21 Aug 2014 22:04:39 +0200 Subject: [PATCH 185/206] Add missing framework to xcconfig --- Support/HockeySDK.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Support/HockeySDK.xcconfig b/Support/HockeySDK.xcconfig index 012479d8..7f04f3fd 100644 --- a/Support/HockeySDK.xcconfig +++ b/Support/HockeySDK.xcconfig @@ -1,3 +1,3 @@ -OTHER_LDFLAGS=$(inherited) -framework CoreText -framework CoreGraphics -framework Foundation -framework QuartzCore -framework SystemConfiguration -framework UIKit -framework Security -framework AssetsLibrary +OTHER_LDFLAGS=$(inherited) -framework CoreText -framework CoreGraphics -framework Foundation -framework QuartzCore -framework SystemConfiguration -framework UIKit -framework Security -framework AssetsLibrary -framework MobileCoreServices HOCKEYSDK_DOCSET_NAME=HockeySDK-iOS GCC_PREPROCESSOR_DEFINITIONS=$(inherited) CONFIGURATION_$(CONFIGURATION) From 20a777ccf83fbe0b4bc21f206a71b98139f2d572 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sat, 23 Aug 2014 20:52:41 +0200 Subject: [PATCH 186/206] Fix update view for resizable iPhone Resizable iPhone causes modal presentations not to be full screen any longer, so we always should use the views width and never the device-width for the version update data presentation. This also works just fine when build with iOS <= 7.1.x --- Classes/BITWebTableViewCell.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/BITWebTableViewCell.m b/Classes/BITWebTableViewCell.m index 22fa446c..179d3bf2 100644 --- a/Classes/BITWebTableViewCell.m +++ b/Classes/BITWebTableViewCell.m @@ -81,7 +81,8 @@ - (void)addWebView { else _webView.frame = webViewRect; - NSString *deviceWidth = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? [NSString stringWithFormat:@"%.0f", CGRectGetWidth(self.bounds)] : @"device-width"; + NSString *deviceWidth = [NSString stringWithFormat:@"%.0f", CGRectGetWidth(self.bounds)]; + //HockeySDKLog(@"%@\n%@\%@", PSWebTableViewCellHtmlTemplate, deviceWidth, self.webViewContent); NSString *contentHtml = [NSString stringWithFormat:BITWebTableViewCellHtmlTemplate, deviceWidth, self.webViewContent]; [_webView loadHTMLString:contentHtml baseURL:nil]; From d7e17d2f1cb905f81c31bdbe15aac10c1d3d67a8 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 26 Aug 2014 17:31:57 +0200 Subject: [PATCH 187/206] Make sure multiple setup runs are ignored If the `configure` initializers or `startManager` are invoked multiple times, this can cause undefined behaviour and crashes. Hence we simply ignore if they are invoked multiple times. --- Classes/BITHockeyManager.m | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index 802deaa9..036f86ee 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -88,6 +88,8 @@ @implementation BITHockeyManager { BOOL _startUpdateManagerIsInvoked; + BOOL _managersInitialized; + BITHockeyAppClient *_hockeyAppClient; } @@ -134,6 +136,7 @@ - (id) init { if ((self = [super init])) { _serverURL = nil; _delegate = nil; + _managersInitialized = NO; _hockeyAppClient = nil; @@ -209,6 +212,7 @@ - (void)configureWithBetaIdentifier:(NSString *)betaIdentifier liveIdentifier:(N - (void)startManager { if (!_validAppIdentifier) return; + if (_startManagerIsInvoked) return; if (![self isSetUpOnMainThread]) return; @@ -584,6 +588,8 @@ - (BOOL)shouldUseLiveIdentifier { } - (void)initializeModules { + if (_managersInitialized) return; + _validAppIdentifier = [self checkValidityOfAppIdentifier:_appIdentifier]; if (![self isSetUpOnMainThread]) return; @@ -628,6 +634,7 @@ - (void)initializeModules { [self pingServerForIntegrationStartWorkflowWithTimeString:integrationFlowTime appIdentifier:_appIdentifier]; } } + _managersInitialized = YES; } else { [self logInvalidIdentifier:@"app identifier"]; } From b2a40717c0096e3ca9268c860194082c2fda4af4 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 26 Aug 2014 17:37:24 +0200 Subject: [PATCH 188/206] Add warnings to the console for the last commit Notify the developer about the issues by logging a warning to the console when the SDK is initialized more than once or startManager is invoked multiple times --- Classes/BITHockeyManager.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index 036f86ee..cd0811d4 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -212,7 +212,10 @@ - (void)configureWithBetaIdentifier:(NSString *)betaIdentifier liveIdentifier:(N - (void)startManager { if (!_validAppIdentifier) return; - if (_startManagerIsInvoked) return; + if (_startManagerIsInvoked) { + NSLog(@"[HockeySDK] Warning: startManager should only be invoked once! This call is ignored."); + return; + } if (![self isSetUpOnMainThread]) return; @@ -588,7 +591,10 @@ - (BOOL)shouldUseLiveIdentifier { } - (void)initializeModules { - if (_managersInitialized) return; + if (_managersInitialized) { + NSLog(@"[HockeySDK] Warning: The SDK should only be initialized once! This call is ignored."); + return; + } _validAppIdentifier = [self checkValidityOfAppIdentifier:_appIdentifier]; From 09d2e5089f56620be99b95df675b530baf5e4fa9 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 26 Aug 2014 17:39:48 +0200 Subject: [PATCH 189/206] Some localization updates Change some strings for the new attachment feature. Translations still pending. --- Resources/de.lproj/HockeySDK.strings | 6 +++--- Resources/en.lproj/HockeySDK.strings | 8 ++++---- Resources/es.lproj/HockeySDK.strings | 8 ++++---- Resources/fr.lproj/HockeySDK.strings | 8 ++++---- Resources/hr.lproj/HockeySDK.strings | 8 ++++---- Resources/hu.lproj/HockeySDK.strings | 8 ++++---- Resources/it.lproj/HockeySDK.strings | 8 ++++---- Resources/ja.lproj/HockeySDK.strings | 8 ++++---- Resources/nl.lproj/HockeySDK.strings | 8 ++++---- Resources/pt-PT.lproj/HockeySDK.strings | 8 ++++---- Resources/pt.lproj/HockeySDK.strings | 8 ++++---- Resources/ro.lproj/HockeySDK.strings | 8 ++++---- Resources/ru.lproj/HockeySDK.strings | 8 ++++---- Resources/zh-Hans.lproj/HockeySDK.strings | 8 ++++---- 14 files changed, 55 insertions(+), 55 deletions(-) diff --git a/Resources/de.lproj/HockeySDK.strings b/Resources/de.lproj/HockeySDK.strings index 3611397a..1f7e4748 100644 --- a/Resources/de.lproj/HockeySDK.strings +++ b/Resources/de.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Senden"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Bild hinzufügen"; +"HockeyFeedbackComposeAttachmentAddImage" = "Bild hinzufügen"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Anhang bearbeiten"; +"HockeyFeedbackComposeAttachmentEdit" = "Bild bearbeiten"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Anhang löschen"; +"HockeyFeedbackComposeAttachmentDelete" = "Bild löschen"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Abbrechen"; diff --git a/Resources/en.lproj/HockeySDK.strings b/Resources/en.lproj/HockeySDK.strings index 97cfa878..27edc559 100644 --- a/Resources/en.lproj/HockeySDK.strings +++ b/Resources/en.lproj/HockeySDK.strings @@ -1,4 +1,4 @@ -/* +/* Localization provided by Wordcrafts The Mac OS X and iOS localization experts. http://www.wordcrafts.de @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Send"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Cancel"; diff --git a/Resources/es.lproj/HockeySDK.strings b/Resources/es.lproj/HockeySDK.strings index 935c961c..230ceb31 100644 --- a/Resources/es.lproj/HockeySDK.strings +++ b/Resources/es.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Enviar"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Cancelar"; /* Set User Data */ diff --git a/Resources/fr.lproj/HockeySDK.strings b/Resources/fr.lproj/HockeySDK.strings index 4cae9d64..c781fa2b 100644 --- a/Resources/fr.lproj/HockeySDK.strings +++ b/Resources/fr.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Envoyer"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Annuler"; /* Set User Data */ diff --git a/Resources/hr.lproj/HockeySDK.strings b/Resources/hr.lproj/HockeySDK.strings index 142e4100..e0721bb7 100644 --- a/Resources/hr.lproj/HockeySDK.strings +++ b/Resources/hr.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Šalji"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Prekid"; /* Set User Data */ diff --git a/Resources/hu.lproj/HockeySDK.strings b/Resources/hu.lproj/HockeySDK.strings index 0b248eb5..a30e8210 100644 --- a/Resources/hu.lproj/HockeySDK.strings +++ b/Resources/hu.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Küldés"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Mégsem"; /* Set User Data */ diff --git a/Resources/it.lproj/HockeySDK.strings b/Resources/it.lproj/HockeySDK.strings index e3c965de..5a17d48c 100644 --- a/Resources/it.lproj/HockeySDK.strings +++ b/Resources/it.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Invia"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Annulla"; /* Set User Data */ diff --git a/Resources/ja.lproj/HockeySDK.strings b/Resources/ja.lproj/HockeySDK.strings index 8dbb471b..8845467e 100644 --- a/Resources/ja.lproj/HockeySDK.strings +++ b/Resources/ja.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "送信"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "キャンセル"; /* Set User Data */ diff --git a/Resources/nl.lproj/HockeySDK.strings b/Resources/nl.lproj/HockeySDK.strings index 854d8916..400b90bd 100644 --- a/Resources/nl.lproj/HockeySDK.strings +++ b/Resources/nl.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Verstuur"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Annuleer"; /* Set User Data */ diff --git a/Resources/pt-PT.lproj/HockeySDK.strings b/Resources/pt-PT.lproj/HockeySDK.strings index 61df2895..93870cc7 100644 --- a/Resources/pt-PT.lproj/HockeySDK.strings +++ b/Resources/pt-PT.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Enviar"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Cancelar"; /* Set User Data */ diff --git a/Resources/pt.lproj/HockeySDK.strings b/Resources/pt.lproj/HockeySDK.strings index 8f41112c..f31d58ae 100644 --- a/Resources/pt.lproj/HockeySDK.strings +++ b/Resources/pt.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Enviar"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Cancelar"; /* Set User Data */ diff --git a/Resources/ro.lproj/HockeySDK.strings b/Resources/ro.lproj/HockeySDK.strings index 2cb78d86..6b2cfee4 100644 --- a/Resources/ro.lproj/HockeySDK.strings +++ b/Resources/ro.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Trimite"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Anulează"; /* Set User Data */ diff --git a/Resources/ru.lproj/HockeySDK.strings b/Resources/ru.lproj/HockeySDK.strings index abaa8e14..0ef786bc 100644 --- a/Resources/ru.lproj/HockeySDK.strings +++ b/Resources/ru.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "Отправить"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "Отменить"; /* Set User Data */ diff --git a/Resources/zh-Hans.lproj/HockeySDK.strings b/Resources/zh-Hans.lproj/HockeySDK.strings index fffe641d..04b8f1a5 100644 --- a/Resources/zh-Hans.lproj/HockeySDK.strings +++ b/Resources/zh-Hans.lproj/HockeySDK.strings @@ -213,16 +213,16 @@ "HockeyFeedbackComposeSend" = "发送"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; +"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; +"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; /* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; +"HockeyFeedbackComposeAttachmentCancel" = "取消"; /* Set User Data */ From 7b61bfa2a64edcfd499b4ea4cc5775eb0daa3c3e Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 26 Aug 2014 20:12:50 +0200 Subject: [PATCH 190/206] Make keychain data better accessible Use `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` instead of `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` when storing data into the keychain to allow the data to be also fetched when the app is launched in the background --- Classes/BITHockeyBaseManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITHockeyBaseManager.m b/Classes/BITHockeyBaseManager.m index 334daa7e..818f7158 100644 --- a/Classes/BITHockeyBaseManager.m +++ b/Classes/BITHockeyBaseManager.m @@ -282,7 +282,7 @@ - (BOOL)addStringValueToKeychainForThisDeviceOnly:(NSString *)stringValue forKey andPassword:stringValue forServiceName:bit_keychainHockeySDKServiceName() updateExisting:YES - accessibility:kSecAttrAccessibleWhenUnlockedThisDeviceOnly + accessibility:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly error:&error]; } From ccd04e1bcd670d83822fac26cdb096593d74110e Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 3 Sep 2014 14:13:59 +0200 Subject: [PATCH 191/206] Fix a small typo in an alert of BITAuthenticator --- Classes/BITAuthenticator.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITAuthenticator.m b/Classes/BITAuthenticator.m index 17586c85..93561b66 100644 --- a/Classes/BITAuthenticator.m +++ b/Classes/BITAuthenticator.m @@ -171,7 +171,7 @@ - (void)alertOnFailureStoringTokenInKeychain { - (void) identifyWithCompletion:(void (^)(BOOL identified, NSError *))completion { if(_authenticationController) { - BITHockeyLog(@"Authentication controller already visible. Ingoring identify request"); + BITHockeyLog(@"Authentication controller already visible. Ignoring identify request"); if(completion) completion(NO, nil); return; } From aa5e68536dc7841033df1458d660d77dc63f23b5 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 3 Sep 2014 14:14:34 +0200 Subject: [PATCH 192/206] Fix formatting of appname in crash reports Fixes highlights on HockeyApp backend of apps with special chars in their name --- Classes/BITCrashManager.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index ff5af9be..02d27f60 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -1327,8 +1327,8 @@ - (void)sendNextCrashReport { } } - crashXML = [NSString stringWithFormat:@"%s%@%@%@%@%@%@%@%@%@%@%@", - [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String], + crashXML = [NSString stringWithFormat:@"%@%@%@%@%@%@%@%@%@%@%@%@", + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"], appBinaryUUIDs, appBundleIdentifier, osVersion, From 393a27906e2ff09c0fdd4b3861873ca2fa286876 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 5 Sep 2014 01:33:55 +0200 Subject: [PATCH 193/206] Make swift integration easier iOS Frameworks use an extra `Modules/module.modulemap` file that is used with the `import` statement to get all required information for integration. We simulate this behavior by writing our own file manually. So in apps using Swift the Objective-C bridging header is not required any longer (when using the binary distribution of the framework). --- Support/HockeySDK.xcodeproj/project.pbxproj | 4 +++- Support/module.modulemap | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Support/module.modulemap diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 0d9a1ab7..6912ac8b 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -310,6 +310,7 @@ 1E84DB3317E0977C00AC83FD /* HockeySDKFeatureConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HockeySDKFeatureConfig.h; sourceTree = ""; }; 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashDetails.h; sourceTree = ""; }; 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashDetails.m; sourceTree = ""; }; + 1E91D84619B924E600E9616D /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; 1E94F9DF16E91330006570AD /* BITStoreUpdateManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITStoreUpdateManager.h; sourceTree = ""; }; 1E94F9E016E91330006570AD /* BITStoreUpdateManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITStoreUpdateManager.m; sourceTree = ""; }; 1E94F9E316E9136B006570AD /* BITStoreUpdateManagerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITStoreUpdateManagerPrivate.h; sourceTree = ""; }; @@ -497,6 +498,7 @@ children = ( 1E754DC61621BC170070AB92 /* HockeySDK.xcconfig */, 1E66CA9115D4100500F35BED /* buildnumber.xcconfig */, + 1E91D84619B924E600E9616D /* module.modulemap */, ); name = Support; sourceTree = ""; @@ -985,7 +987,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Sets the target folders and the final framework product.\nFMK_NAME=HockeySDK\nFMK_VERSION=A\nFMK_RESOURCE_BUNDLE=HockeySDKResources\n\n# Documentation\nHOCKEYSDK_DOCSET_VERSION_NAME=\"de.bitstadium.${HOCKEYSDK_DOCSET_NAME}-${VERSION_STRING}\"\n\n# Install dir will be the final output to the framework.\n# The following line create it in the root folder of the current project.\nPRODUCTS_DIR=${SRCROOT}/../Products\nZIP_FOLDER=HockeySDK-iOS\nTEMP_DIR=${PRODUCTS_DIR}/${ZIP_FOLDER}\nINSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.framework\n\n# Working dir will be deleted after the framework creation.\nWRK_DIR=build\nDEVICE_DIR=${WRK_DIR}/Release-iphoneos\nSIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator\nHEADERS_DIR=${WRK_DIR}/Release-iphoneos/usr/local/include\n\n# Building both architectures.\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphoneos\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphonesimulator\n\n# Cleaning the oldest.\nif [ -d \"${TEMP_DIR}\" ]\nthen\nrm -rf \"${TEMP_DIR}\"\nfi\n\n# Creates and renews the final product folder.\nmkdir -p \"${INSTALL_DIR}\"\nmkdir -p \"${INSTALL_DIR}/Versions\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers\"\n\n# Creates the internal links.\n# It MUST uses relative path, otherwise will not work when the folder is copied/moved.\nln -s \"${FMK_VERSION}\" \"${INSTALL_DIR}/Versions/Current\"\nln -s \"Versions/Current/Headers\" \"${INSTALL_DIR}/Headers\"\nln -s \"Versions/Current/Resources\" \"${INSTALL_DIR}/Resources\"\nln -s \"Versions/Current/${FMK_NAME}\" \"${INSTALL_DIR}/${FMK_NAME}\"\n\n# Copies the headers and resources files to the final product folder.\ncp -R \"${SRCROOT}/build/Release-iphoneos/include/HockeySDK/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${DEVICE_DIR}/${FMK_RESOURCE_BUNDLE}.bundle\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\ncp -f \"${SRCROOT}/${FMK_NAME}.xcconfig\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\n\n# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.\nlipo -create \"${DEVICE_DIR}/lib${FMK_NAME}.a\" \"${SIMULATOR_DIR}/lib${FMK_NAME}.a\" -output \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\"\n\n# Combine the CrashReporter static library into a new Hockey static library file if they are not already present and copy the public headers too\nif [ -z $(otool -L \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" | grep 'libCrashReporter') ]\nthen\nlibtool -static -o \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${SRCROOT}/../Vendor/CrashReporter.framework/Versions/A/CrashReporter\"\nfi\n\nrm -r \"${WRK_DIR}\"\n\n# build embeddedframework folder and move framework into it\nmkdir \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework\"\nmv \"${INSTALL_DIR}\" \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework/${FMK_NAME}.framework\"\n\n# link Resources\nNEW_INSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.embeddedframework\nmkdir \"${NEW_INSTALL_DIR}/Resources\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_RESOURCE_BUNDLE}.bundle\" \"${NEW_INSTALL_DIR}/Resources/${FMK_RESOURCE_BUNDLE}.bundle\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_NAME}.xcconfig\" \"${NEW_INSTALL_DIR}/Resources/${FMK_NAME}.xcconfig\"\n\n# copy license, changelog, documentation, integration json\ncp -f \"${SRCROOT}/../Docs/Changelog-template.md\" \"${TEMP_DIR}/CHANGELOG\"\ncp -f \"${SRCROOT}/../Docs/Guide-Installation-Setup-template.md\" \"${TEMP_DIR}/README.md\"\ncp -f \"${SRCROOT}/../LICENSE\" \"${TEMP_DIR}\"\nmkdir \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\ncp -R \"${SRCROOT}/../documentation/docset/Contents\" \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\n\n# build zip\ncd \"${PRODUCTS_DIR}\"\nrm -f \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nzip -yr \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${ZIP_FOLDER}\" -x \\*/.*\n"; + shellScript = "# Sets the target folders and the final framework product.\nFMK_NAME=HockeySDK\nFMK_VERSION=A\nFMK_RESOURCE_BUNDLE=HockeySDKResources\n\n# Documentation\nHOCKEYSDK_DOCSET_VERSION_NAME=\"de.bitstadium.${HOCKEYSDK_DOCSET_NAME}-${VERSION_STRING}\"\n\n# Install dir will be the final output to the framework.\n# The following line create it in the root folder of the current project.\nPRODUCTS_DIR=${SRCROOT}/../Products\nZIP_FOLDER=HockeySDK-iOS\nTEMP_DIR=${PRODUCTS_DIR}/${ZIP_FOLDER}\nINSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.framework\n\n# Working dir will be deleted after the framework creation.\nWRK_DIR=build\nDEVICE_DIR=${WRK_DIR}/Release-iphoneos\nSIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator\nHEADERS_DIR=${WRK_DIR}/Release-iphoneos/usr/local/include\n\n# Building both architectures.\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphoneos\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphonesimulator\n\n# Cleaning the oldest.\nif [ -d \"${TEMP_DIR}\" ]\nthen\nrm -rf \"${TEMP_DIR}\"\nfi\n\n# Creates and renews the final product folder.\nmkdir -p \"${INSTALL_DIR}\"\nmkdir -p \"${INSTALL_DIR}/Modules\"\nmkdir -p \"${INSTALL_DIR}/Versions\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers\"\n\n# Creates the internal links.\n# It MUST uses relative path, otherwise will not work when the folder is copied/moved.\nln -s \"${FMK_VERSION}\" \"${INSTALL_DIR}/Versions/Current\"\nln -s \"Versions/Current/Headers\" \"${INSTALL_DIR}/Headers\"\nln -s \"Versions/Current/Resources\" \"${INSTALL_DIR}/Resources\"\nln -s \"Versions/Current/${FMK_NAME}\" \"${INSTALL_DIR}/${FMK_NAME}\"\n\n# Copy the swift import file\ncp -f \"${SRCROOT}/module.modulemap\" \"${INSTALL_DIR}/Modules/\"\n\n# Copies the headers and resources files to the final product folder.\ncp -R \"${SRCROOT}/build/Release-iphoneos/include/HockeySDK/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${DEVICE_DIR}/${FMK_RESOURCE_BUNDLE}.bundle\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\ncp -f \"${SRCROOT}/${FMK_NAME}.xcconfig\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\n\n# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.\nlipo -create \"${DEVICE_DIR}/lib${FMK_NAME}.a\" \"${SIMULATOR_DIR}/lib${FMK_NAME}.a\" -output \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\"\n\n# Combine the CrashReporter static library into a new Hockey static library file if they are not already present and copy the public headers too\nif [ -z $(otool -L \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" | grep 'libCrashReporter') ]\nthen\nlibtool -static -o \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${SRCROOT}/../Vendor/CrashReporter.framework/Versions/A/CrashReporter\"\nfi\n\nrm -r \"${WRK_DIR}\"\n\n# build embeddedframework folder and move framework into it\nmkdir \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework\"\nmv \"${INSTALL_DIR}\" \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework/${FMK_NAME}.framework\"\n\n# link Resources\nNEW_INSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.embeddedframework\nmkdir \"${NEW_INSTALL_DIR}/Resources\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_RESOURCE_BUNDLE}.bundle\" \"${NEW_INSTALL_DIR}/Resources/${FMK_RESOURCE_BUNDLE}.bundle\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_NAME}.xcconfig\" \"${NEW_INSTALL_DIR}/Resources/${FMK_NAME}.xcconfig\"\n\n# copy license, changelog, documentation, integration json\ncp -f \"${SRCROOT}/../Docs/Changelog-template.md\" \"${TEMP_DIR}/CHANGELOG\"\ncp -f \"${SRCROOT}/../Docs/Guide-Installation-Setup-template.md\" \"${TEMP_DIR}/README.md\"\ncp -f \"${SRCROOT}/../LICENSE\" \"${TEMP_DIR}\"\nmkdir \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\ncp -R \"${SRCROOT}/../documentation/docset/Contents\" \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\n\n# build zip\ncd \"${PRODUCTS_DIR}\"\nrm -f \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nzip -yr \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${ZIP_FOLDER}\" -x \\*/.*\n"; }; 1E8E66B215BC3D8200632A2E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; diff --git a/Support/module.modulemap b/Support/module.modulemap new file mode 100644 index 00000000..73fba672 --- /dev/null +++ b/Support/module.modulemap @@ -0,0 +1,6 @@ +framework module HockeySDK { + umbrella header "HockeySDK.h" + + export * + module * { export * } +} From e9f3d7dde2df29d31c8dd06f4e571e09d5280962 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 5 Sep 2014 01:47:19 +0200 Subject: [PATCH 194/206] Update setup doc for Swift --- ...de-Installation-Setup-Advanced-template.md | 31 +++++++++++++++++-- docs/Guide-Installation-Setup-template.md | 31 +++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/docs/Guide-Installation-Setup-Advanced-template.md b/docs/Guide-Installation-Setup-Advanced-template.md index 6b4d0a0d..4fc2db1f 100644 --- a/docs/Guide-Installation-Setup-Advanced-template.md +++ b/docs/Guide-Installation-Setup-Advanced-template.md @@ -82,11 +82,13 @@ The SDK runs on devices with iOS 6.0 or higher. ## Modify Code +### Objective-C + 1. Open your `AppDelegate.m` file. 2. Add the following line at the top of the file below your own #import statements: - #import "HockeySDK.h" + #import 3. Search for the method `application:didFinishLaunchingWithOptions:` @@ -96,9 +98,32 @@ The SDK runs on devices with iOS 6.0 or higher. [[BITHockeyManager sharedHockeyManager] startManager]; [[BITHockeyManager sharedHockeyManager].authenticator authenticateInstallation]; -5. Replace `APP_IDENTIFIER` with the app identifier of your beta app. If you don't know what the app identifier is or how to find it, please read [this how-to](http://support.hockeyapp.net/kb/how-tos/how-to-find-the-app-identifier). +5. Continue with [General subsection](#generalcode) + +### Swift + +1. Add the following line to your [Objective-C bridging header](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html) file: + + #import + +2. Open your `AppDelegate.swift` file. + +3. Search for the method `application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool` + +4. Add the following lines: + + BITHockeyManager.sharedHockeyManager().configureWithIdentifier("APP_IDENTIFIER"); + BITHockeyManager.sharedHockeyManager().startManager(); + BITHockeyManager.sharedHockeyManager().authenticator.authenticateInstallation(); + +5. Continue with [General subsection](#generalcode) + + +### General + +1. Replace `APP_IDENTIFIER` with the app identifier of your beta app. If you don't know what the app identifier is or how to find it, please read [this how-to](http://support.hockeyapp.net/kb/how-tos/how-to-find-the-app-identifier). -6. If you want to see beta analytics, use the beta distribution feature with in-app updates, restrict versions to specific users, or want to know who is actually testing your app, you need to follow the instructions on our guide [Identify and authenticate users of Ad-Hoc or Enterprise builds](HowTo-Authenticating-Users-on-iOS) +2. If you want to see beta analytics, use the beta distribution feature with in-app updates, restrict versions to specific users, or want to know who is actually testing your app, you need to follow the instructions on our guide [Identify and authenticate users of Ad-Hoc or Enterprise builds](HowTo-Authenticating-Users-on-iOS) *Note:* The SDK is optimized to defer everything possible to a later time while making sure e.g. crashes on startup can also be caught and each module executes other code with a delay some seconds. This ensures that applicationDidFinishLaunching will process as fast as possible and the SDK will not block the startup sequence resulting in a possible kill by the watchdog process. diff --git a/docs/Guide-Installation-Setup-template.md b/docs/Guide-Installation-Setup-template.md index 82afe193..51c0938a 100644 --- a/docs/Guide-Installation-Setup-template.md +++ b/docs/Guide-Installation-Setup-template.md @@ -60,7 +60,9 @@ The SDK runs on devices with iOS 6.0 or higher. - `UIKit` -## Modify Code +## Modify Code + +### Objective-C 1. Open your `AppDelegate.m` file. @@ -76,9 +78,32 @@ The SDK runs on devices with iOS 6.0 or higher. [[BITHockeyManager sharedHockeyManager] startManager]; [[BITHockeyManager sharedHockeyManager].authenticator authenticateInstallation]; -5. Replace `APP_IDENTIFIER` with the app identifier of your app. If you don't know what the app identifier is or how to find it, please read [this how-to](http://support.hockeyapp.net/kb/how-tos/how-to-find-the-app-identifier). +5. Continue with [General subsection](#generalcode) + +### Swift + +1. Open your `AppDelegate.swift` file. + +2. Add the following line at the top of the file below your own #import statements: + + import HockeySDK + +3. Search for the method `application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool` + +4. Add the following lines: + + BITHockeyManager.sharedHockeyManager().configureWithIdentifier("APP_IDENTIFIER"); + BITHockeyManager.sharedHockeyManager().startManager(); + BITHockeyManager.sharedHockeyManager().authenticator.authenticateInstallation(); + +5. Continue with [General subsection](#generalcode) + + +### General + +1. Replace `APP_IDENTIFIER` with the app identifier of your app. If you don't know what the app identifier is or how to find it, please read [this how-to](http://support.hockeyapp.net/kb/how-tos/how-to-find-the-app-identifier). -6. If you want to see beta analytics, use the beta distribution feature with in-app updates, restrict versions to specific users, or want to know who is actually testing your app, you need to follow the instructions on our guide [Identify and authenticate users of Ad-Hoc or Enterprise builds](HowTo-Authenticating-Users-on-iOS) +2. If you want to see beta analytics, use the beta distribution feature with in-app updates, restrict versions to specific users, or want to know who is actually testing your app, you need to follow the instructions on our guide [Identify and authenticate users of Ad-Hoc or Enterprise builds](HowTo-Authenticating-Users-on-iOS) *Note:* The SDK is optimized to defer everything possible to a later time while making sure e.g. crashes on startup can also be caught and each module executes other code with a delay some seconds. This ensures that applicationDidFinishLaunching will process as fast as possible and the SDK will not block the startup sequence resulting in a possible kill by the watchdog process. From 8eaa0295366f4dbc4fca93b68f580007de202973 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sat, 6 Sep 2014 01:55:52 +0200 Subject: [PATCH 195/206] Fix incorrect error log message The warning about `delegate` setting after calling `startManager` being incorrect, should actually only be shown if it was called after it and not before it. Doh. --- Classes/BITHockeyManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index cd0811d4..893148a5 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -342,7 +342,7 @@ - (void)setServerURL:(NSString *)aServerURL { - (void)setDelegate:(id)delegate { if (![self isAppStoreEnvironment]) { - if (!_startManagerIsInvoked) { + if (_startManagerIsInvoked) { NSLog(@"[HockeySDK] ERROR: The `delegate` property has to be set before calling [[BITHockeyManager sharedHockeyManager] startManager] !"); } } From 70d2573cbda8f99ef3debfc6cf3cf588636c76ab Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 12 Sep 2014 14:40:18 +0200 Subject: [PATCH 196/206] Update podspec We don't need to include the PLCR headers any longer, this wasn't needed sincee a few 3.5 releases also. --- HockeySDK.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HockeySDK.podspec b/HockeySDK.podspec index e66d8401..9fc1c071 100644 --- a/HockeySDK.podspec +++ b/HockeySDK.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/bitstadium/HockeySDK-iOS.git', :tag => s.version.to_s } s.platform = :ios, '6.0' - s.source_files = 'Classes', "Vendor/CrashReporter.framework/Versions/A/Headers/*.h" + s.source_files = 'Classes' s.requires_arc = true s.frameworks = 'CoreText', 'QuartzCore', 'SystemConfiguration', 'CoreGraphics', 'UIKit', 'Security', 'AssetsLibrary', 'MobileCoreServices', 'QuickLook' From c53cd6facd347af4dcb9a9db45c41013e53cc8f7 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 19 Sep 2014 15:03:08 +0200 Subject: [PATCH 197/206] Request update of a specific version Even though the UI showed the newest version that the device can install, the download request always triggered downloading the latest version. This commit fixes this bug. --- Classes/BITUpdateManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITUpdateManager.m b/Classes/BITUpdateManager.m index 647592d1..f7e0c127 100644 --- a/Classes/BITUpdateManager.m +++ b/Classes/BITUpdateManager.m @@ -744,7 +744,7 @@ - (BOOL)initiateAppDownload { ]; } - NSString *hockeyAPIURL = [NSString stringWithFormat:@"%@api/2/apps/%@?format=plist%@", self.serverURL, [self encodedAppIdentifier], extraParameter]; + NSString *hockeyAPIURL = [NSString stringWithFormat:@"%@api/2/apps/%@/app_versions/%@?format=plist%@", self.serverURL, [self encodedAppIdentifier], [self.newestAppVersion.versionID stringValue], extraParameter]; NSString *iOSUpdateURL = [NSString stringWithFormat:@"itms-services://?action=download-manifest&url=%@", bit_URLEncodedString(hockeyAPIURL)]; BITHockeyLog(@"INFO: API Server Call: %@, calling iOS with %@", hockeyAPIURL, iOSUpdateURL); From a44d77805d8f2697f3d44d2879a3ee1030ea59b0 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 23 Sep 2014 12:07:35 +0200 Subject: [PATCH 198/206] Update localizations Also removed romanian, since it is not handled by Wordcrafts. --- Resources/de.lproj/HockeySDK.strings | 3 +- Resources/es.lproj/HockeySDK.strings | 7 +- Resources/fr.lproj/HockeySDK.strings | 7 +- Resources/hu.lproj/HockeySDK.strings | 7 +- Resources/it.lproj/HockeySDK.strings | 7 +- Resources/ja.lproj/HockeySDK.strings | 7 +- Resources/nl.lproj/HockeySDK.strings | 7 +- Resources/pt-PT.lproj/HockeySDK.strings | 7 +- Resources/pt.lproj/HockeySDK.strings | 7 +- Resources/ro.lproj/HockeySDK.strings | 280 -------------------- Resources/ru.lproj/HockeySDK.strings | 7 +- Resources/zh-Hans.lproj/HockeySDK.strings | 7 +- Support/HockeySDK.xcodeproj/project.pbxproj | 2 - 13 files changed, 31 insertions(+), 324 deletions(-) delete mode 100644 Resources/ro.lproj/HockeySDK.strings diff --git a/Resources/de.lproj/HockeySDK.strings b/Resources/de.lproj/HockeySDK.strings index 1f7e4748..d25c51bb 100644 --- a/Resources/de.lproj/HockeySDK.strings +++ b/Resources/de.lproj/HockeySDK.strings @@ -234,7 +234,7 @@ "HockeyFeedbackUserDataDescription" = "Bitte geben Sie vor Verfassen Ihres Feedbacks Ihre Daten ein."; /* Name Field */ -"HockeyFeedbackUserDataName" = "Name"; +"HockeyFeedbackUserDataName" = "Max Mustermann"; /* Name Placeholder */ "HockeyFeedbackUserDataNamePlaceHolder" = "Max Mustermann"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "Autorisierung fehlgeschlagen. Bitte erneut versuchen oder an den Entwickler dieser App wenden."; "HockeyAuthenticationNotMember" = "Sie sind nicht berechtigt, diese App zu nutzen. Bitte wenden Sie sich an den Entwickler dieser App."; "HockeyAuthenticationContactDeveloper" = "Autorisierungsfehler. Bitte wenden Sie sich an den Entwickler dieser App."; diff --git a/Resources/es.lproj/HockeySDK.strings b/Resources/es.lproj/HockeySDK.strings index 230ceb31..6b57293c 100644 --- a/Resources/es.lproj/HockeySDK.strings +++ b/Resources/es.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Enviar"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Añadir imagen"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "Editar imagen"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "Eliminar imagen"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Cancelar"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "Error de autorización. Vuelva a intentarlo o póngase en contacto con el desarrollador de la app."; "HockeyAuthenticationNotMember" = "No tiene autorización para usar esta app. Póngase en contacto con el desarrollador de la app."; "HockeyAuthenticationContactDeveloper" = "Error de autorización. Póngase en contacto con el desarrollador de la app."; diff --git a/Resources/fr.lproj/HockeySDK.strings b/Resources/fr.lproj/HockeySDK.strings index c781fa2b..2f727f45 100644 --- a/Resources/fr.lproj/HockeySDK.strings +++ b/Resources/fr.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Envoyer"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Ajouter une image"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "Modifier l’image"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "Supprimer l’image"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Annuler"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "Échec de l'autorisation. Réessayez ou contactez le développeur de l'application."; "HockeyAuthenticationNotMember" = "Vous n'êtes pas autorisé à utiliser cette application. Contactez le développeur de l'application."; "HockeyAuthenticationContactDeveloper" = "Erreur d'autorisation. Contactez le développeur de l'application."; diff --git a/Resources/hu.lproj/HockeySDK.strings b/Resources/hu.lproj/HockeySDK.strings index a30e8210..a13abae8 100644 --- a/Resources/hu.lproj/HockeySDK.strings +++ b/Resources/hu.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Küldés"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Fotó hozzáadása"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "Fotó szerkesztése"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "Fotó törlése"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Mégsem"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "A hitelesítés sikertelen. Kérjük, próbálja meg később újra vagy lépjen kapcsolatba az alkalmazás fejlesztőjével."; "HockeyAuthenticationNotMember" = "Nem jogosult az alkalmazás használatára. Kérjük, lépjen kapcsolatba az alkalmazás fejlesztőjével."; "HockeyAuthenticationContactDeveloper" = "Hitelesítési hiba. Kérjük, lépjen kapcsolatba az alkalmazás fejlesztőjével."; diff --git a/Resources/it.lproj/HockeySDK.strings b/Resources/it.lproj/HockeySDK.strings index 5a17d48c..eaa07e26 100644 --- a/Resources/it.lproj/HockeySDK.strings +++ b/Resources/it.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Invia"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Aggiungi immagine"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "Modifica immagine"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "Elimina immagine"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Annulla"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "Autorizzazione non riuscita. Riprova o contatta lo sviluppatore di questa app."; "HockeyAuthenticationNotMember" = "Non sei autorizzato a usare questa app. Contatta lo sviluppatore di questa app."; "HockeyAuthenticationContactDeveloper" = "Errore di autorizzazione. Contatta lo sviluppatore di questa app."; diff --git a/Resources/ja.lproj/HockeySDK.strings b/Resources/ja.lproj/HockeySDK.strings index 8845467e..a6d25532 100644 --- a/Resources/ja.lproj/HockeySDK.strings +++ b/Resources/ja.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "送信"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "イメージを追加"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "イメージを編集"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "イメージを削除"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "キャンセル"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "認証できませんでした。やり直すか、このAppのデベロッパに連絡してください。"; "HockeyAuthenticationNotMember" = "このAppの使用を認証されていません。このAppのデベロッパに連絡してください。"; "HockeyAuthenticationContactDeveloper" = "認証エラー。このAppのデベロッパに連絡してください。"; diff --git a/Resources/nl.lproj/HockeySDK.strings b/Resources/nl.lproj/HockeySDK.strings index 400b90bd..45d737b3 100644 --- a/Resources/nl.lproj/HockeySDK.strings +++ b/Resources/nl.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Verstuur"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Voeg afbeelding toe"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "Wijzig afbeelding"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "Verwijder afbeelding"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Annuleer"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "Identiteitscontrole mislukt. Probeer opnieuw of neem contact op met de ontwikkelaar van deze app."; "HockeyAuthenticationNotMember" = "U bent niet gemachtigd om deze app te gebruiken. Neem contact op met de ontwikkelaar van deze app."; "HockeyAuthenticationContactDeveloper" = "Fout bij identiteitscontrole. Neem contact op met de ontwikkelaar van deze app."; diff --git a/Resources/pt-PT.lproj/HockeySDK.strings b/Resources/pt-PT.lproj/HockeySDK.strings index 93870cc7..930cb408 100644 --- a/Resources/pt-PT.lproj/HockeySDK.strings +++ b/Resources/pt-PT.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Enviar"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Adicionar imagem"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "Editar imagem"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "Apagar imagem"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Cancelar"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "Falha ao autorizar. Tente novamente ou contacte o programador desta aplicação."; "HockeyAuthenticationNotMember" = "Não está autorizado a utilizar esta aplicação. Contacte o programador desta aplicação."; "HockeyAuthenticationContactDeveloper" = "Erro de autorização. Contacte o programador desta aplicação."; diff --git a/Resources/pt.lproj/HockeySDK.strings b/Resources/pt.lproj/HockeySDK.strings index f31d58ae..5991cf77 100644 --- a/Resources/pt.lproj/HockeySDK.strings +++ b/Resources/pt.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Enviar"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Adicionar imagem"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "Editar imagem"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "Excluir imagem"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Cancelar"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "Falha ao autorizar. Tente novamente ou contate o desenvolvedor deste aplicativo."; "HockeyAuthenticationNotMember" = "Você não tem autorização para usar este aplicativo. Contate o desenvolvedor deste aplicativo."; "HockeyAuthenticationContactDeveloper" = "Erro de autorização. Contate o desenvolvedor deste aplicativo."; diff --git a/Resources/ro.lproj/HockeySDK.strings b/Resources/ro.lproj/HockeySDK.strings deleted file mode 100644 index 6b2cfee4..00000000 --- a/Resources/ro.lproj/HockeySDK.strings +++ /dev/null @@ -1,280 +0,0 @@ -/* - Localization provided by Wordcrafts - The Mac OS X and iOS localization experts. - http://www.wordcrafts.de - */ - - -/* General */ - -/* For dialogs yes buttons */ -"HockeyYes" = "Da"; - -/* For dialogs no buttons */ -"HockeyNo" = "Nu"; - -/* For dialogs ok buttons */ -"HockeyOK" = "OK"; - -/* Replacement for app name, if it could not be detected */ -"HockeyAppNamePlaceholder" = "Aplicația curentă"; - -/* Crash */ - - -/* Crash dialog */ - -/* Title showing in the alert box when crash report data has been found */ -"CrashDataFoundTitle" = "Raportare eroare"; - -/* Description explaining that crash data has been found and ask the user if the data might be uploaded to the developers server */ -"CrashDataFoundAnonymousDescription" = "Doriți să trimiteți un raport anonim ca să putem corecta problema?"; - -/* Description explaining that crash data has been found and ask the user if the non anonymous data might be uplaoded to the developers server */ -"CrashDataFoundDescription" = "Doriți să trimiteți un raport ca să putem corecta problema??"; - -/* Alert box button if the users wants to send crash data always automatically */ -"CrashSendReportAlways" = "Trimite mereu"; - -/* Alert box button to send the crash report once */ -"CrashSendReport" = "Trimite raport"; - -/* Alert box button to decline sending the report */ -"CrashDontSendReport" = "Nu trimite"; - -/* Text showing in a processing box that the crash data is being uploaded to the server */ -"CrashReportSending" = "Se trimite…"; - - -/* Update */ - - -/* Update Alert view */ - -/* Update available */ -"UpdateAvailable" = "Versiune nouă"; - -"UpdateAlertTextWithAppVersion" = "A apărut versiunea %@."; - -"UpdateAlertMandatoryTextWithAppVersion" = "A apărut versiunea %@ și actualizarea e obligatorie!"; - -"UpdateIgnore" = "Ignoră"; - -"UpdateShow" = "Arată"; - -"UpdateInstall" = "Instalează"; - -"UpdateRemindMe" = "Remind me"; - - -/* Update Details */ - -"UpdateScreenTitle" = "Actualizare"; - -"UpdateButtonCheck" = "VERIFICĂ"; - -"UpdateButtonSearching" = "SE VERIFICĂ"; - -"UpdateButtonUpdate" = "ACTUALIZEAZĂ"; - -"UpdateButtonInstalling" = "SE INSTALEAZĂ"; - -"UpdateButtonOffline" = "OFFLINE"; - -"UpdateInstalled" = "INSTALAT"; - -"UpdateVersion" = "Versiune"; - -"UpdateShowPreviousVersions" = "Arată versiunile anterioare..."; - -"UpdateNoUpdateAvailableTitle" = "Nici o actualizare disponibilă"; - -"UpdateNoUpdateAvailableMessage" = "%@ e cea mai recentă versiune."; - -"UpdateError" = "Eroare"; - -"UpdateWarning" = "Avertisment"; - -"UpdateNoReleaseNotesAvailable" = "Nu există note."; - - -/* Update Authorization */ - -"UpdateAuthorizationProgress" = "Se autorizeaza..."; - -"UpdateAuthorizationOffline" = "Necesită conexiune la Internet!"; - -"UpdateAuthorizationDenied" = "Autorizare refuzată. Vă rugăm contactați dezvoltatorul."; - - -/* Update Expiry */ - -"UpdateExpired" = "%@ a expirat și nu mai poate fi folosită."; - - -/* Update Simulator Warning */ - -"UpdateSimulatorMessage" = "Hockey Update nu funcționează în simulator.\nSchema itms-services:// este implementată dar nefuncțională."; - - -/* Feedback */ - - -/* New Message Alert */ - -/* Alert Title */ -"HockeyFeedbackNewMessageTitle" = "Nou răspuns feedback"; - -/* Alert Text */ -"HockeyFeedbackNewMessageText" = "A apărut un nou răspuns la feedback-ul dvs. Doriți să îl vedeți?"; - -/* Alert Ignore Button */ -"HockeyFeedbackIgnore" = "Ignoră"; - -/* Alert Show Button */ -"HockeyFeedbackShow" = "Arată"; - - -/* List View */ - -/* Title */ -"HockeyFeedbackListTitle" = "Feedback"; - -/* Last Updated */ -"HockeyFeedbackListLastUpdated" = "Ultima actualizare: %@"; - -/* Never Updated */ -"HockeyFeedbackListNeverUpdated" = "Niciodată"; - -/* Provide Feedback Button Title */ -"HockeyFeedbackListButonWriteFeedback" = "Furnizează Feedback"; - -/* Add a Response Button Title */ -"HockeyFeedbackListButonWriteResponse" = "Adaugă un răspuns"; - -/* User Data Set Name Button Title */ -"HockeyFeedbackListButonUserDataSetName" = "Introduceți numele"; - -/* User Data Set Email Button Title */ -"HockeyFeedbackListButonUserDataSetEmail" = "Introduceți e-mail"; - -/* User Data With Name Button Title */ -"HockeyFeedbackListButonUserDataWithName" = "Nume: %@"; - -/* User Data With Email Button Title */ -"HockeyFeedbackListButonUserDataWithEmail" = "E-mail: %@"; - -/* Button title for deleting all local messages*/ -"HockeyFeedbackListButonDeleteAllMessages" = "Șterge toate mesajele"; - -/* Message pending to be send */ -"HockeyFeedbackListMessagePending" = "În aștepare"; - - -/* Delete All Messages Action Sheet / Alert View */ - -/* Title for the Action Sheet */ -"HockeyFeedbackListDeleteAllTitle" = "Toate mesajele de pe acest dispozitiv vor fi șterse."; - -/* Button Title to perform delete action */ -"HockeyFeedbackListDeleteAllDelete" = "Șterge"; - -/* Button Title to cancel delete action */ -"HockeyFeedbackListDeleteAllCancel" = "Anulează"; - - -/* Open Link In Safari Action Sheet / Alert View */ - -/* Button Title to cancel */ -"HockeyFeedbackListLinkActionCancel" = "Anulează"; - -/* Button Title to Open the Link */ -"HockeyFeedbackListLinkActionOpen" = "Deschide"; - -/* Button Title to Copy the Link */ -"HockeyFeedbackListLinkActionCopy" = "Copiază"; - - -/* UIActivity */ - -/* Activity Sharing Button Title, App Name will be inserted */ -"HockeyFeedbackActivityButtonTitle" = "Feedback pentru %@"; - -/* if there can no app name be found, use this instead for HockeyFeedbackActivityButtonTitle */ -"HockeyFeedbackActivityAppPlaceholder" = "Aplicație"; - - -/* Compose Message */ - -/* Title */ -"HockeyFeedbackComposeTitle" = "Nou feedback"; - -/* Send button */ -"HockeyFeedbackComposeSend" = "Trimite"; - -/* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; - -/* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; - -/* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; - -/* Cancel button for attachment actions */ -"HockeyFeedbackComposeAttachmentCancel" = "Anulează"; - - -/* Set User Data */ - -/* Title */ -"HockeyFeedbackUserDataTitle" = "Informațiile mele"; - -/* Description On What Should Be Entered */ -"HockeyFeedbackUserDataDescription" = "Vă rugăm introduceți informațiile despre dvs."; - -/* Name Field */ -"HockeyFeedbackUserDataName" = "Nume"; - -/* Name Placeholder */ -"HockeyFeedbackUserDataNamePlaceHolder" = "Ion Popescu"; - -/* Email Field */ -"HockeyFeedbackUserDataEmail" = "E-mail"; - -/* Email Placeholder */ -"HockeyFeedbackUserDataEmailPlaceholder" = "email@exemplu.ro"; - - -/* Authenticator */ - -/* View Controller Title*/ -"HockeyAuthenticatorViewControllerTitle" = "HockeyApp"; - -/* BITAuthenticatorAuthTypeUDIDProvider */ -"HockeyAuthenticationViewControllerWebUDIDLoginDescription" = "The developer has requested this device's UDID. Tap below to open the HockeyApp website and authorize access."; -"HockeyAuthenticationViewControllerWebLoginButtonTitle" = "Open HockeyApp"; - -/* BITAuthenticatorIdentificationTypeWebAuth */ -"HockeyAuthenticationViewControllerWebAuthLoginDescription" = "The developer has requested your HockeyApp account's email address. Tap below to open the HockeyApp website and authorize access."; - -/* BITAuthenticatorAuthTypeEmail and BITAuthenticatorAuthTypeEmailAndPassword */ -"HockeyAuthenticationViewControllerDataEmailAndPasswordDescription" = "Please enter your HockeyApp account's email address and password to authorize access to this app."; -"HockeyAuthenticationViewControllerDataEmailDescription" = "Please enter your HockeyApp account's email address to authorize access to this app."; -"HockeyAuthenticationViewControllerEmailPlaceholder" = "email@exemplu.ro"; -"HockeyAuthenticationViewControllerPasswordPlaceholder" = "Required"; -"HockeyAuthenticationViewControllerEmailDescription" = "E-mail"; -"HockeyAuthenticationViewControllerPasswordDescription" = "Password"; - -/* Error presented to the user if authentication failed because of networking issues */ -"HockeyAuthenticationViewControllerNetworkError" = "Failed to authorize because your device appears to be disconnected from the Internet. Please try again."; - -/* Error presented to the user if authentication could not be stored on the device */ -"HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - - -"HockeyAuthenticationFailedAuthenticate" = "Failed to authorize. Please try again or contact the developer of this app."; -"HockeyAuthenticationNotMember" = "You are not authorized to use this app. Please contact the developer of this app."; -"HockeyAuthenticationContactDeveloper" = "Authorization error. Please contact the developer of this app."; -"HockeyAuthenticationWrongEmailPassword" = "You have entered an invalid email or password. Please check your input and try again."; -"HockeyAuthenticationAuthSecretMissing" = "The HockeySDK authorization secret has not been set. Please contact the developer of this app."; diff --git a/Resources/ru.lproj/HockeySDK.strings b/Resources/ru.lproj/HockeySDK.strings index 0ef786bc..28e66fe3 100644 --- a/Resources/ru.lproj/HockeySDK.strings +++ b/Resources/ru.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "Отправить"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "Добавить фото"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "Изменить фото"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "Удалить фото"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "Отменить"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "Авторизация не удалась. Попробуйте еще раз или свяжитесь с разработчиком приложения."; "HockeyAuthenticationNotMember" = "У вас нет разрешения использовать это приложение. Обратитесь к разработчику приложения."; "HockeyAuthenticationContactDeveloper" = "Ошибка авторизации. Свяжитесь с разработчиком приложения."; diff --git a/Resources/zh-Hans.lproj/HockeySDK.strings b/Resources/zh-Hans.lproj/HockeySDK.strings index 04b8f1a5..21a5aa68 100644 --- a/Resources/zh-Hans.lproj/HockeySDK.strings +++ b/Resources/zh-Hans.lproj/HockeySDK.strings @@ -213,13 +213,13 @@ "HockeyFeedbackComposeSend" = "发送"; /* Add Image button for attachment actions */ -"HockeyFeedbackComposeAttachmentAddImage" = "Add Image"; +"HockeyFeedbackComposeAttachmentAddImage" = "添加图像"; /* Edit button for attachment actions */ -"HockeyFeedbackComposeAttachmentEdit" = "Edit Image"; +"HockeyFeedbackComposeAttachmentEdit" = "编辑图像"; /* Delete button for attachment actions */ -"HockeyFeedbackComposeAttachmentDelete" = "Delete Image"; +"HockeyFeedbackComposeAttachmentDelete" = "删除图像"; /* Cancel button for attachment actions */ "HockeyFeedbackComposeAttachmentCancel" = "取消"; @@ -272,7 +272,6 @@ /* Error presented to the user if authentication could not be stored on the device */ "HockeyAuthenticationViewControllerStorageError" = "Your authentication token could not be stored due to a keychain error. Please contact the developer of the app."; - "HockeyAuthenticationFailedAuthenticate" = "授权错误。请再试一次或联系这个 app 的开发人员。"; "HockeyAuthenticationNotMember" = "你未获授权使用这个程序。请联系这个 app 的开发人员。"; "HockeyAuthenticationContactDeveloper" = "授权错误。请联系这个 app 开发人员。"; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 86e5250b..4f28f8ba 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -292,7 +292,6 @@ 1E5A459F16F0DFC200B55C04 /* HockeySDKTests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "HockeySDKTests-Prefix.pch"; sourceTree = ""; }; 1E61CCAE18E0585A00A5E38E /* BITFeedbackManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITFeedbackManagerTests.m; sourceTree = ""; }; 1E66CA9115D4100500F35BED /* buildnumber.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = buildnumber.xcconfig; sourceTree = ""; }; - 1E6DDCEE169E290C0076C65D /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/HockeySDK.strings; sourceTree = ""; }; 1E6F0450167B5E5600ED1C86 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/HockeySDK.strings"; sourceTree = ""; }; 1E70A22F17F2F982001BB32D /* live_report_empty.plcrash */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = live_report_empty.plcrash; sourceTree = ""; }; 1E70A23017F2F982001BB32D /* live_report_exception.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_exception.plcrash; sourceTree = ""; }; @@ -1126,7 +1125,6 @@ 1E6F0450167B5E5600ED1C86 /* pt-PT */, 1EB52FC3167B73D400C801D5 /* en */, 1EA512DF167F7EF000FC9FBA /* zh-Hans */, - 1E6DDCEE169E290C0076C65D /* ro */, BEE0207C16C5107E004426EA /* hu */, 1E20A57F181E9D4600D5B770 /* nl */, ); From 24a6756ba709d427eba9b471ce3fd8d534503668 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 23 Sep 2014 13:48:23 +0200 Subject: [PATCH 199/206] Update image resources --- Classes/BITUpdateViewController.m | 27 ++++++++++++++- Resources/Arrow.png | Bin 394 -> 340 bytes Resources/Arrow@2x.png | Bin 896 -> 612 bytes Resources/Arrow@3x.png | Bin 0 -> 939 bytes Resources/Blur.png | Bin 169 -> 187 bytes Resources/Blur@2x.png | Bin 250 -> 274 bytes Resources/Blur@3x.png | Bin 0 -> 350 bytes Resources/Cancel.png | Bin 179 -> 197 bytes Resources/Cancel@2x.png | Bin 303 -> 342 bytes Resources/Cancel@3x.png | Bin 0 -> 487 bytes Resources/Ok.png | Bin 245 -> 199 bytes Resources/Ok@2x.png | Bin 363 -> 347 bytes Resources/Ok@3x.png | Bin 0 -> 496 bytes Resources/Rectangle.png | Bin 176 -> 192 bytes Resources/Rectangle@2x.png | Bin 244 -> 364 bytes Resources/Rectangle@3x.png | Bin 0 -> 541 bytes Resources/authorize_denied.png | Bin 2340 -> 3264 bytes Resources/authorize_denied@2x.png | Bin 4384 -> 7462 bytes Resources/authorize_denied@3x.png | Bin 0 -> 12304 bytes Resources/buttonHighlight.png | Bin 1021 -> 0 bytes Resources/buttonHighlight@2x.png | Bin 1101 -> 0 bytes Resources/feedbackActivity.png | Bin 0 -> 487 bytes Resources/feedbackActivity@2x.png | Bin 0 -> 1052 bytes Resources/feedbackActivity@3x.png | Bin 0 -> 1697 bytes Support/HockeySDK.xcodeproj/project.pbxproj | 36 +++++++++++++++----- 25 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 Resources/Arrow@3x.png create mode 100644 Resources/Blur@3x.png create mode 100644 Resources/Cancel@3x.png create mode 100644 Resources/Ok@3x.png create mode 100644 Resources/Rectangle@3x.png create mode 100644 Resources/authorize_denied@3x.png delete mode 100644 Resources/buttonHighlight.png delete mode 100644 Resources/buttonHighlight@2x.png create mode 100644 Resources/feedbackActivity.png create mode 100644 Resources/feedbackActivity@2x.png create mode 100644 Resources/feedbackActivity@3x.png diff --git a/Classes/BITUpdateViewController.m b/Classes/BITUpdateViewController.m index 142112ef..4a5f1877 100644 --- a/Classes/BITUpdateViewController.m +++ b/Classes/BITUpdateViewController.m @@ -146,6 +146,31 @@ - (void)changePreviousVersionButtonBackgroundHighlighted:(id)sender { [(UIButton *)sender setBackgroundColor:BIT_RGBCOLOR(245, 245, 245)]; } +- (UIImage *)gradientButtonHighlightImage { + CGFloat width = 10; + CGFloat height = 70; + + CGSize size = CGSizeMake(width, height); + UIGraphicsBeginImageContextWithOptions(size, NO, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + + NSArray *colors = [NSArray arrayWithObjects:(id)BIT_RGBCOLOR(69, 127, 247).CGColor, (id)BIT_RGBCOLOR(58, 68, 233).CGColor, nil]; + CGGradientRef gradient = CGGradientCreateWithColors(CGColorGetColorSpace((__bridge CGColorRef)[colors objectAtIndex:0]), (__bridge CFArrayRef)colors, (CGFloat[2]){0, 1}); + CGPoint top = CGPointMake(width / 2, 0); + CGPoint bottom = CGPointMake(width / 2, height); + CGContextDrawLinearGradient(context, gradient, top, bottom, 0); + + UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); + + CGGradientRelease(gradient); + CGColorSpaceRelease(colorspace); + UIGraphicsEndImageContext(); + + return theImage; +} + - (void)showHidePreviousVersionsButton { BOOL multipleVersionButtonNeeded = [_updateManager.appVersions count] > 1 && !_showAllVersions; @@ -174,7 +199,7 @@ - (void)showHidePreviousVersionsButton { [footerButton setTitle:BITHockeyLocalizedString(@"UpdateShowPreviousVersions") forState:UIControlStateNormal]; [footerButton setTitleColor:BIT_RGBCOLOR(61, 61, 61) forState:UIControlStateNormal]; [footerButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; - [footerButton setBackgroundImage:bit_imageNamed(@"buttonHighlight.png", BITHOCKEYSDK_BUNDLE) forState:UIControlStateHighlighted]; + [footerButton setBackgroundImage:[self gradientButtonHighlightImage] 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); diff --git a/Resources/Arrow.png b/Resources/Arrow.png index 6d8fac10390585862e4384ef2e86820ba5da9356..92ff5f668ed7be8060cadb20cd5336fa57e41d34 100644 GIT binary patch delta 314 zcmV-A0mc4`1JnYLBYyw^b5ch_0Itp)=>Px$4oO5oR5%f}l0B+IF%X3Xd030j1=#u1 zD_ErztZm(a=n^a~tmIWL!379*S%iqV0l`h^cko^vqIj=)I`EO1WO5EnQYc)*v3St> z)$ph<;Q-1Vd^{YAGAJetliIU7_*i(9=7E|_-!XLb4+4#cvVX_U4mQvVPV*Xi?w#N_ zxc^W}a0F`pg9_MqW$0+s=rxrUZ#Fn1k^BY;W`k ztw@6+eZQ_f2s85N5n7!o#TKe|Gsyov!YP7TCaGtKBi_@% delta 368 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 N002ovPDHLkV1mKr$(#TH literal 0 HcmV?d00001 diff --git a/Resources/Blur.png b/Resources/Blur.png index 7dd64b9fe3e96f609c29338aaf4f35adc8e94ce4..a741c735124010007682c79f91c4f50c9c76ad41 100644 GIT binary patch delta 159 zcmV;Q0AT;A0lNW^B!2;OQb$4nuFf3k0001WNklGo z^(5NjyL-IU3zeKWl^i_z4OKRNR&i)a|NPpP*mD|6&%j?AY8beXz5>{qF?%{bVdww= N002ovPDHLkV1iMlNOS-I delta 141 zcmV;80CNAk0jU9yB!6&8L_t(I5$#h!4!|G?6SG(HK)$I+cEE%x$=pp(+FC>?BqFVV z#muA>x3LW+~8qBpMj#LI0b5|T}kL&s6*RNK2FR59Fnb% v-w&daU7yDe_;o_f=lU*cilvnBaj5TiGckK|>E*Qm0000gN5tcdvV$!|_7;*%8bP4UyF3U*5y zMWA|fQ*?u-+!P!8sh>}qN$z8CanFFxF)iC wPU5QxYyVf}oK!@u`8N0VET=zf{Isd^0X)Sh|17B;P5=M^07*qoM6N<$f>&g4#sB~S delta 222 zcmV<403rX90{Q`vB!9t4L_t(o3GJ3q3WP8WL{&Ux57=w2UD?Qd425KAFfjgDqSJ}5 z!DR2ff40EYTHAlJkbY=yeboO5o3%_K@K>GaqP6+gJPp%e$iHlj zKYx`$K6RcZ_2lv|OI(SQqhA^l@uhgl<|0m@EDeeHQoLky?3|&Q!S?jd)24&F%fD=? YTscXn4_^Nn+tP4;d}|C)S{d)@1(xcbOx zVfEGXKmF?8TN%&KKbvea&&F3$iLKu2^K)>)i17h-*{*o9*TQ&s?l4pn<_< zmXe49!^CAAtQ-zYG@Tfk1Tv9@8K&$gHHi7=+*>jw=HK$y2ainpXUckli*H z@Qn96lv{XTO|o|S(URNztMA|6g}htr+npFSmfPn$R?n!DRW;4K6(7FK#&{3U>R;DI ze(<&6wGZM9hNoLfCjf1oSz-dTRr}zPRX|%G0BzmFc*{joTH#v~FuWKTJYD@<);T3K F0RTr+g(Ls~ literal 0 HcmV?d00001 diff --git a/Resources/Cancel.png b/Resources/Cancel.png index b8112db68fa071f309219c4980b2836cde9fa5ef..a1306b58596dd058f024e27eec318c764983566a 100644 GIT binary patch delta 169 zcmV;a09OCA0mT83B!2;OQb$4nuFf3k0001gNkl3L5U|?tfVtybl1Y%-9KTy#} zkU|jSKa>XX@c|6A7y?*y<5YuF0%!<6Ij|kr6krp>;viJhiPi)&38n#OpfSNzQiF!s z2BMMKc+JM95Sti^A@~&Fl*1{3WHg2v3;`s?=uEg;W}u8E5F%dyVqy~cPM|sgApQsd XoOv@Fw(aUz00000NkvXXu0mjfhZ;Xu delta 151 zcmV;I0BHZk0kZ*+B!7BIL_t(26^+or4S+BR1yIzBySb1~pi6mkAt%sJ4F;tJnjlzs zP1BBuW(2S8N#R%KX~pP;csCr%Gd#J!nh;!ti2(or002ovPDHLk FV1jdKKPx$5J^NqR7ef2lmQaLAPfYnZ}lO4i26Anq8#-chDp*3h8cuH*lm!qZAaN#^Jw*Q zJeK}~3HDAZI`40-mQqGOT;0wAv{lksMt0vuNGheC4CB3=1~5=)WtJQmNYjk;fKSf6HiAKuQdGQX2uIISK9sd0=(RJ#IK|Nh>c3Q|?JqSK-w}`-8}8 zF4ou_lYT~a_g2Sk9Q&9%saDaQ=Q`TvKM-IkA=6o8}|19wJDN~BO^fjz}w*IZviu(}^ ou)t*bx%M_vl^Nk|nJFCG2N1o^?mB+meEA3f$@P#^eJOuqfBk)bTyPmiX#Elq)8SxhT+s#H0zSXvG zCP9c{+J4I>MoZAjyOCZYZ=DK`3PkHWINKQR3pey5wuQUjT_^xDGPx$p-DtRR9Fe!mJLn>Aq<5###4G4uA|<^)0k-A#OFe@lomRJk%TSKpZ85?lI3>0 zk0Q%gna}s5Z7VQo zz%?3oz+l0{+FsS!UMIoQFj#?ScXrfC_?xi1vXmR9q+38$@9APY84Bdw6` zg6z|4rx!c}l8wlO&8$Vf#%2g))M%i3n-!sz+#Zj4J|j4jV?ubH5#dOzS3eZz0r13x zL}!CXAiY~q)*nWQ+k&lcU|es30c3C$u*(0Gean&hURMa znF5)$PJ?9OblCKQBo&)hJFQR!r^21^@s6CNG?d00001b5ch_0Itp)=>Px#fk{L`R45f=WFQ~( zldqM5!49bT9}rI@#}F={raMp!Vv`I&wt{T8C0Qp>!yt7!C^|rx8IPavIvu3>KM-#N zVm>T}5EbevK#jkE_$m-9p&3GSsM`ZIzJX#88w5a6MN)zR*)a}s1;{lyKulJG0fhr7 mJV4<>c7g%L2q>11^@s6pu6rQ0002INkl5QNK?(tJ6o*Y>#o9tvTtIOK zo4J7;!BV+_vUCJjuyF>;1z1|lPGLim?<>VfGyH$`)x4Sc-n@DLs1Tk$YNHtFH}Mv5 z2=PluE7(Cbq`?&F3f&O7hjfQgh+IQTUPr*Wz|9H9k2$33_Y_i}~- g{2foklbPh154SZRmf47@!2kdN07*qoM6N<$g5kAfjQ{`u diff --git a/Resources/Ok@2x.png b/Resources/Ok@2x.png index f1915b84f885ce463de072db6185ffbee702968e..ad7c7603bafee806bd126ae73f15fab9f0866b61 100644 GIT binary patch literal 347 zcmV-h0i^zkP)Px$6-h)vR7ef&l(7xMFc3u}Xz3|a!w3w+8bHASjKc`fpwPhxA;ZpFTG&^4C<7C-q!?$uD0NJBhwNea>xQDZyu8V3X4T=&ylzr8~216{3g2 zBAc>={u}*P1T6KSheoy<(U0gM&}reM+(^AsKatC$`k?0iOGTv~VG9dJW|0n}cRiwJ zfb*sy=S5=oXOF9CYX8Q+-n{N7BhpE7f#DaCFPW>yzwD~ tlO<)soKPT$*{UAS!O?p!vv4Ls{|1=OjwVkWN;Uuh002ovPDHLkV1f?ulfnQ1 literal 363 zcmV-x0hIoUP)`$q!%F{{Kzr)=6uI<578(JD~J6nj3AiqaQ=i}h(lG;#dwnxp{qUzkvgDGs3Yo3 z5II~`gistr?OfQlw{wwpqn(Si6S|)7T#W74s3G>6iwBJLmRrDe4MlXx;q^N*;lX!M zF>f%0DylRWH#o0A#r(iC_{i~)atOs<>hRPQ(nK%BS0`lhnv0_R_bh&Z3fjQ{%H%y4 zWr=Uuum%``rT=^Llo=fdR94DC^U zvEvaMn~Tf}%+2GY+OyW!To~Wn&>q!+T`qU$KP>2|me8xq>ks$^WSTBB^J@SA002ov JPDHLkV1ka>sf_>t diff --git a/Resources/Ok@3x.png b/Resources/Ok@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0d79bf61b9b0572e710aac516293fb963f4105d8 GIT binary patch literal 496 zcmVPx$s!2paR9Fe^m@!VnKoCVi6iD0v#5uSN1=weSdvF3O;2IQMfRYkQO88&Omqx4I z1lF_b9m$qf-*zJB&*c4yos*+j+Xd{tVSVj1mV3B;!TQ;)XDAB_v(0cS^V0Vy?%8Q# zBjeKBCoqd*maqg^4qv~M^_LqjcJ32C+YE8ITLnk$_Yn%cec>Jsol^0oa1V=4 mu8=g`!=sZcLgDWAm*6h|1VCbd45pI+0000&X9=4UW$m&M?p9PRn7wv`K+al;MQTgh~Z&iFu40c{&_j8I1T-^ei-1 zWa!;l@!zLFjaOoETQcvJiwTl55-Rz&OuJ<%62fAW&L))PFo)>~d*csbfnY8Nw(~5H mVilSbmMX2#S&+4ogJF)Zi6q;Vk5)ieFnGH9xvXbV9sh$l zo6j)Kk=Z6);G#7zrFnGH9xvXhB!2;OQb$4nuFf3k0003dNklyd;o^|)q>mkAS(xUw7poL{4-6S>yd~%*wzyQ!w$$x821Xkb=KyrQnXII{9 zEkIadZrc3NpB)CB7L0(moE19M*BAR9cK4fj$L_F+9Is}jaiMVBXjHXn;dnJGjSJy;m*@V=)eYPZ@M{KN=GJqw%5aI9 zt3LNQuKq*-UP_DTga`#zB9(MWCS)n_#1!5FAwsGGO?cH`>^a{Bo(*^v(8PsYaW8V>iRI(8%?500000NkvXXu0mjf6xf%_ delta 216 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 literal 4384 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 H;u85k4!}n< diff --git a/Resources/authorize_denied@3x.png b/Resources/authorize_denied@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..40a2594ddc49d9be22f20fd0319a71c7cfd59131 GIT binary patch literal 12304 zcmd72^;4To*fyM`0Rj|h8J69=pMc@{(8>WEdb22uteyJ0%bZtO)$Rz)*oN^dx_! zfj_XllH?mu>EQDn5Qw8!>fLJ<7w|zUri)Hz>{gMrvrJ9J%jqWkCzzV}I8uMO`iS9i zFY&B3o%((oGk$=O!my(a8LMP8(oR4B@k|nSiZ-b1*_Y)?`(y;sq{Qa@a#rrIrS?e% z`(-o*KD^Bj-7gPodOEHm$8&yGnXTvhEZh0Pq1(;m+R_r*ZcFX%xInA&`9F{Gl0H&V zI4;~foSbiRJk4cyX2g77r|as?;fb=}8oMp@&eR=jwo2GF)C zd&1|j!PM0OJIHJO|herK}AR#`E$vC4KRc;iRAnDw>nBi9bG1a|Y4lD2ko zu`dn``UZJTEecoU1TQAeyOy@oP|?T`+p;gRK%;$Jv4iIu1erXJ z!DDDUFTa%PAvz54r}KKYmcE6FCJr<-dY)@6DHEm_sXzbAD+|FLez{{ce9u{SQV z#mU)uDrCB8V}9kc1M!XYU8&(UESpu?0W0=69aH_&`i`$QL2$==&5w#wRuEjpk}>t9 z(-YL;K=L^DHC8*KiCb>vGSfJToRiUdtGuwgexm(*FH-lP#C%!IW`&_BuzdE!!e2v& zhxgo?a{E;dF|Hz8xBD2fnU;8vH$m~{PUAQl%y;eYvW{bEIxDp80TlxT{HZqw2X`Z( zKxs+osGYuE50NR90T8dcDo4+k*4;EHd!o{YzwheGF%=|U$_2>QVJ#7-a zD(fFT2VPRYUB7$cB^+_0?|_qjJ}794-a41a#J5;_c5F(*z!SpV$+Z{h#G#XmQhD3z z$}egLr^eYrEd)DT#ZpPHb_+MchxRswZ&TqQaH>J(M6G1aS-LWL(G@=WR<%O|;{%_}em+cTVDlWJ6q z=9CtRWKJ^{xVrluaH-XnQZ}-4y%W-i`Oh7W$UAovuy4J*MA$uH*th|`LJ;5%gI3fm z8@L5*6WcGHw}VaqFMs#Cw9DT$)wUbM@_k$v$GKf*_g~!bE$d3WtPbYY(zwf$94*9Kz%{Z+9rSg_=1z=WGmXU6>svscK<+t)=@ z%bU)b9?d39!bst*Fwy&fRrWukG&dpp+#!vY{%-sbEY5+{R3U|kemmab!lR-$4o~Fv zq!B>_GltFwy|MAlC04M88!RvulkY0WLnuu>*GdpkW**07zi)^*$$nzbld*AbrZQ1$_7`Y zfH>%$)w1%8h7r%Bw39=~KvRQ@2V0UZKhg5`!N2@_M$uu%c4{U`>b$!5jiW5tU9wPk z2KNY)aeB&9PyLvYi2oBIc^<5h<5k;1jYg5M10cYIy0VMMvN z)bNMbc`b>1+4mZLL^McR#q@^P)Zpwe(UC7j_MWR)(HKS1UZ#hIFl)aXbG!Gu6|fl>*?Ukh zJKeu{H@50H8J&*{LUlIuY0d@(Nzh@uqCtP2l$`6-XJT65)lP z9nBNb<*O@t6rscpf+p$Xc<1Gxzbjz<;28sg{_Ia1URRyGXVLD5TiwPsuLIFE`(4WHB z?lMcuk=yS#lTHfv)I(8ck9ziQDy^^ECQak}j=DSqDHrY+g9vU2QJk>u+AsCiASn!R zrmW!0RiI!*yvs9uweS(P?O^h8T0ZZ7-B95`Zp|7SM+h@VZ<5Gk7C+2n?)s8#Un|mY>Wp(r zOFr4Lb|d}ENNz-~XWd8BXi#gjTs!|silM=>gR`g`MJ~VVQ1O>H%OYo^(whu$$;*$U z^4XiNy+i}`221m&cDsRAQ@q&m5}kviwJPyA-mpIhhIrVI$!fW+jMzRDvl>8=M`a1>3$b0CVaO>Ze2%R;y)H5Q15wnJ)L-T zwVbZpdPy$W?3HeG7<+(y9!e`)C-b@SP#60p%pA`$8`HM~Rkl6~;k(AvHD4ye`R!K9 z&8<1Ae6d=^d%{bWhz=u~ zF~zu0>Z&BICviWr&C+**quXyJ1{sfU(fu)xQy8{HKRx?iVZxS3VA;%=*EcmZO!6lj zg=#s$93q+FUnL!Bj_tJD@?CfVJ{u(!I;3CUw37JS(8%f=)U(BL`N;^$1N(V4{p%Wn zpP%b1J&Y{wq%F}NE;ri(kelEru^pm{+A%qA8^Y@?7#|S<1OHvwdNy;5D3pL&SA|C-SalWKiO%4oP4s7P=ix&db1g{Z{$KR&D?q)eE z?7EBvb=a5nReH5>d1~4zy}v9Z@g7_stZSzcW55&9<(vv>e9IRb!1b1RmEMsc|J7~0 z?Qhrg*5mq-2CAJGX|h3{oLqL)9b;RRcu0Xn+C-MbDo{U~)pu*=jjkYXh1FP0Y>1#l zM5)`N?3Boczujlilb8j^&LvUNVR9P_i}~#EwKySWdwP2Ydq(?w0&D?^b&ZjpEx1P~ zlkmtuGi>mS)3EyIXCWDLnQj^q?t&va3Yes8*(~0NX>BYTF%SA=L4qRM>A6!rtZmWK zXWM?I6hj=XLPE}I{m<{=Gh^?C#r3tG*W<44sB^%EBKkA?>E(!aF^59GYe)2OMw1KC ztMe53oI}VG59YJSd~jc-t{*ZwU9f8tMIP}KvCNW(_viE%^p~&`t|tToTjTbsjiRTP z2gXwD^QNdt*IRi-TaOwUGr2fIS_+<}5 zckgf3DxOK5Fpd|fc`jDn&70F*ypm%aG4Q8v3URW$=)cbyW2xL>n2wU=y=!Ah=J0CU zFD8r_oK?Owr)$tdJC31HDIE$Dx5jifAgJB6lM@))&^rcgkbF0m`oj0ben=X;@csGa zQ@L()jIcuefQ|1)vKV z;B5o=r3QO7(Q)BxP@Y(-ju}fO&YzZVH)yIGi0y%#`ih*&Nbu#y2|RN%yX8vQkX!-| zC`W-|N8hfEEB>{x$ErmTsViNtrbFKD!=;&jqlsygVIK8#Xn^4WUcUQ*Py2XUWlUps zPlt4n*@&l=E_jBKYn)G|wSulY;ki`}-Jc``hec3XPUwPHp|z2CK;t1}xlTd@DaIW4 z44(-rxc5JL{NT5!%W0fYmI#FFfRr;cYnq)@j?l{?`!3Ir)M(hB7Mh#>A9B(j1L4(4 zuKFsy1;~^SC;BhNjALykO?Y^Q#tOts(w*)q>C?S()#>MV<3*+`0zqsnwGr3nY|Ij} zcVGQ-l(rJ@B@|905ysVLrPkt|(Q3=CMj8K{h|oapO6kabp~BMD7;^hp7vp&viGdPS z-lCN`4=I{F)kAnR8*zo%t{!gdTpu-53o5Zcxg8AjtnYA&Zb^~-VJ>A%qLdGBvN;dw zB9&(N7Vz}=m7|6#N2v}~R?9m+YZaXtQC?ouBD5d9Y@{q1 zkHB1#xT>s#OF>~^eA zP?q3J9t{llb=hkeXi9h%jo4ym;15zSpbAD*60I3o+;-3SwcEHt@hlx{+|OutGPrzvUT9*PwQ z@!gu`xchKR;Ad0|JfwZuDvQOCFZV5Qp083Xu_RIIEyGCVJcPaCjn^$pa|z)K2psLw zLn|!D$!Zva+Ih&qswX#oJV~0LJ`o(ms zxG*2&Bi2MQY~)J5%>_>C63kVZYM-;AXR86Tv4zc932B5gBbCMXqSd3=HnNQHo0NSw z=1!*W&G{>{n!Qm>NVWH1+6_)Xfr?_+Pm@R50q5+(cBe8yqONr~+A zoGY8&4%#I{dl&s{8((?&zV9zts0o$Ij)$~eSZ{@3^pN0zvV<+OH-kPk!hR`68e;&b zvA(3_*p~iFCOb?;8gcP@hM1*#&CEkUP0sZo5D#*_5|GnpepOs`Cu>JvlkY2BZM3r* z$Wr0m0AD5s&k&oYpY`eGIIU~M8rLWZF^}`TK~Nm(ltB8?spiW@oCWQFqd{Zf2oKIF z##mbk!OX=3Hk|Mai1d+AA(2lRGl%~(Aq5Zcu_j#4w z{4*XS5C7KK`TlF9lq0=bWASpi?)IGX7s37Oi`-AHjJ!7N!y#u5U$9%tBzL&>+^#5} zam0K!%H8T-ivIz2JjJqlzNGq_m>fqjy~_DuV7kAvdmUoQ2Wa`xY@#plnTCEuFQ z$1_xmLQ1s%vJ0Xpz7^hKzDpC~$n)HmQ8R!-AvExt{QV+uBF6}8x{zRiOmVK_mlBk5 zqNr!48V&o0)mG$zvBzi;NCX&CFDmU({i{@wmx(8!51vtt96d)=iuqIGzonN9pHY4mIp3f-FxnU%;Rc%LCf&8Y^=?PU+7&3!arz9UbX5!1_#{pbl$b9( zP?hq#w>Ns~tNN!((UB(?_{;GaTPcko1}U8dgFa9qb5r>&<=8muPBH$I}sh zH}+Dgg*@OjF6{Xn-@vIw`sk9|iM&3z-OmGI7&4L4p{8H@BpzRMj z$nMz!u&CL4Jcux?m@lk%Y*?6+# zIEb`@(hKSgDEW~fb9HxSlO=R~yL~ro=le-^DsuCg5s^;X@4)+>h8?uG&Jb)Q0tFhg z)BY;gQO8762;96qnqzlwc3H%Mer&f_*%!-yS730PQMdtAfP`ziX_EDXIAfttV$z@W zg}~wOQaW=`N#D7)UDm+&e>1>Tl99|Fnz0qS7+wd^m|PBB4QZrzERiP6+`!hbt`MrS z(^nXFfWN%Qm=WfY?|13Lk~D}w_-14FP0(e?>#G@mivOj@2hRETp#mokS066EF&_EI z;vmA>U%*rnk^6njvSwuDb6O=Iier&Z49;mqUDxSZt-T)42fPu#peoyA?uXkXIQ%M9 z16K4I*r6}%f;eoi4~-f9n22EcBY6{~Ym0VSu}G!&(z5m>gLcoozwYV3$Uz7DU;o56 zp0}F)iLW@=>DG+;%%Fa2>ebYkTf$#3*d7*kzO&UH@ji<4kknTVB2C<|LuOt_r4@8B zV=IE#gX2C06J6I}7&j3%XraFldEzC{)P|zs3(aHpbGDL(|XV3A!jN89k&`)lxKkyo_$vBYKULA zpv`c&v(Jo2`moX_@FRu(zQzwYcnQoHwi}lO(e+t1gJL(+jBun~eEKR+{&MfiHaCAw zc`>T@-aM}Jaqgk4tcz0VK2;RZmlwW zS~rMTd%clyP{Qps=`hF0o-ql81^x9Er7=cvLf^cHH))_Dt|AhN@1f%j3?!PjYE$ZS zba3bo9v`&9ji?LS>|!^PWzCxSHk6SUybCpUd}UaZtCIWz2LcB{{9(gS*qQLFs0D|~ zvdpgk4L7tk?$~u$H)KB)?f<#=o}8Sd_1{%(7*Gccx?#uC%3@x_RD)yK^~j@alCOfFfPd35K z6n$Hv_RwW2MPuDsaJH#137pM_3j*W*O$O<5d;B$La69de<8Wj zqk|?T^0%&!{S?0=4bT$z^qP${-#Rnh$G!?L5E5{~B#>w9s3PomfeC?U0Q?u6Fk)QbE|soQ;_DOQ=>N85S4&3*HAZQP-rpuZGAyA#BL*VCwfWvO@T+;1FHTmr!ZLEtZAK_xu@VtceB(9I=}1|vsT z%1%PBf6W6tsgBo7nyE2iqmaf6gy^gDlKaX-SGs6`zU!k7x!w_(96vu%I(f zbj`~Dx{3x-60%pIaeWa=^FMC50+gUD;xECYElv1H0h;F@f)@NSB4%TpbQRVgU&4EGC z%acMua@R6TKD93eFIX9XlczU@O-PW%EV1Q7bp5&|A)dTp9u_lO9ybZVisDTWMDP%++X%WT<#)?x_;Ht zp^34P=u4LU5YaUhTS;Po+5(}=QgKv*z+_bNziid>ey?gN^a#Ve%>P%O)^sfvK zgdZ|Nt6=$+X&RF>?Mh{DlwKi!XSrX`@$8_EpE(O>v>$12#va@ZS4;p!ATaIQCRd@n zSUBOglDeJB0gN8;oWoOco$cc2xcq5Ahe$IYHg4QMsg>NGGBWPR&L3aoE|iw8mvRjA z02g_A=!%y}q$Hs89EQ4x4GkJXfJ8ns^TU6vx(iRO zduyrkJ-6XPU!@vXqrzCU?2mPDwa?*22U-ZL-}EdP$)IHuywJH~|Nxig@B ztg0>TIhb?*V`nT|-){O(l%O!xuCOszZ7w~}6x`vH8B%?2=l1u1i$3w6^eg0sX)5j~ zup}v3t!M<>*j5K=w9l3~$U*iC)N6f z+1*F;rE)r#dg#3uBp6$EzQedyR;dS2$0}?k0YaJl7g)q%R*8^78zO13r(= zt*Pla^rDm%EiSoHb#jl1&2XMsGZq3DqglG)WL)k9?N*&^SRqzY5$Z?FT3?CpaGIwO zQUvJBu$7})!d-zf~H`Ui_9;{);lM={19a`~1`XDIk=jKqT`wzhNER)#`Wl9$w}LR*QcLTKDfzXakZ0 z!T(r*Yq}Cp+J&mXUN(u}#TP$91VG-(l)a04dRzf;_T!@+edW4cHJZ;`I5K@RIY_E` zAm9mJ$5&Zf27(wz@;|Ccm34i;noJ?&hULbtzxWU0y|F^-8@76zg zvCUdxv9b*|H)m>73OEl{q#(Ukd~>{5d8+(q6$kJ);~P^>AC}|U<_?fK%_2bFEe`#c zi8aX@jtqTsj^0;u0@tw&s`rAu0@`)Jj=i8?bfFpjlwRhBMzZ5Okh@h+#rBVUTK`w- z*h%#D>WLcJkeLftfk>?sufYy3jCAd=jM3qpeUa{RC87K>CLnXU!Ka;{2bU%i=j+Fb zwV#poRL}-I3yApE4FlG~5eByXee?#b7n^FoSHetI9G1^nz{-Iuh^Bi!Rw86es7fQ+ zBU{N*K^8(T0obnbMg}KP(oCFYDqe5skSx33W!xGgh!PSa`fL0BuKmHm!8Ru4jgJX^ zqAP2J5h}s23{L)fy|>wtctiKL9?C^iCpGcW#dO?r0&q{+ACgogv(u*@p@Z=HLJXwhP5vi_8KQEpGQ z^B|ZcD(($%@r1$F*J~HJFh0TOl4@#5g{j^pKPAFhy+?xQ-opq24>0;HDZ^~r?ki{o zE~A*p*)FM#<0`aqncoy6frq$J#C=ee<@g`f%IVoHlmk!(g-$gCJruo^E9B+ z0-NHu2V4c+Xp+h$pj`m`@f8OpKXb%=JkRl^D8=ebj4XS?fZ2mh!=!60_Wf^3N5G_s zfpE1n^tOvpp-~%(*@xXH0JEb>lo}stIys>45^La2QL^LO5F+}Uu^E`o=e_YKix|}R z@VdIa@c7Yx#kzC+#Xjx--K8DuPdi4~TIcOZZ51vPxWDuT=1Q0_Gcn3S;(WW`rT&g{ z$G=BD5~ICsAQ^4fi1BZ0R_*4U`rtIF;U$uHfB`vGnUuOToYjZ^Y-e)swYEH|j1OCr%1i3-} zGd0bmwJ+>o`Qkm;v?`o5ZwM-0&SCrV%od9R=FyEpz`Xs zX{PSObF@q!oVAwavPW#YxuKF61nmpp$iTSFkbScpbOd~Cq4j?)g{0gN{iT+^Tva-YnaejR7Qy1sPa4Mu1D|2vwoi&t(h{5d@I86mu$zR6}6gq5p6;*3Or(j$h%|m%YpF2b@Rp2CXL14I>YW)Gt}T8u z^lNKPd8XD~|5zIb$cD(tkMMX+&v!Gikt!E*$OF!;})>|oM0T9{S_67dP)A%wehxY_w-|JUBD7Y`|Mb=v*Ii7 z2^KPal?98P>@!tIv6Xa6K+M;5DOZkq#Vv~Wod!GWRuvlMVmd$-vArT6Ymj%Ybv&6! zz&0|6<0`%-(7r2oXlN{nE}Iyl(>jv}3`-@F9(?~x^&WN9??&)SaD}I(061h7>dF5+ z4U!Lzq+Dz@t7VUn(a#xbxWp8jpa+3f`S=7~FOq9&c(sb&Mh+#EwT`rFyA=Fy6ubD& z^=Qu@MJ!EM@t|tq; zCQFrlveXBMbJ;W{c{YTFWU7&#Ynr@qgoIlG5910wFxs&Yu=~bGwzvH}>ufS-X!PHx zRTe5t78k(0<-clAU@uxxx1SpB?B6^!G*qt!=3SBa?68x$XpRKIb{@lPv!y%EUO)&2=$S1=qIh!{0+TU7O8=pzQ)nyqMe*m^K z3b~-RW%gWmb6mv@vFohSK*C;nH2y~;+34TxQ*c0nZ-!;vfJ0Y` z(BHKJF3#NJ>A!sZz!Y7cCbLL#VpxD}R^uvuvt za50c3TYRBT`(IPD9HYyKlAb3AdvKm5z%w%Q5JG5Su6|V@*JxuFTy|bnX&`S*ulIJ7 zou63H_%n3#;?cKU0m&Rw zC(Gi;6I;PCHTwe)n>#bhVsWjDfy-o8-VXCmXjs(1TMXE+6N9&k{SI0B&rAcWY;KC6^-p5(mc&~q7uQ^vd94N@ zuiHo5Sx}q1>dY%Yg;c+IN|*rn0q_aa&S6Lrd2M?wE9K&D{4fqvyk+fcr+Q{!%s8XpYjBP=#fp7o!hX>}NLNb9apF51R;zMs zY`>++Y6(}MEIqb?01!NUm(|uW+KIK+8cE^Zi$&Ak@~Y*D*CX)-g687e`U{EE=E2MX zK9XD)?qP08bPIV51TOruyRB(crWaQ*6NK6Mhwc~d5_@NJJ8JhwZW&yJ4s&{BjO|5z z*Psp)l>t*a)TIn~j;EMwe2-pEOThE|OWn>N31tzni#4dqGYx=GrT4qxyYfH&j|eTM zg-=tk#PK)~hq(3XUfx0+)bI5gSiIx_T#x#wgB&(IvK5Jc+&LvjY}H0PeEi#JeR*s9 z_m1&e>oM`8`Nh=m4i3%TUs&9oLfJ?L{NSh+R&5l#RCMYKZxD<`64?Qi%Ay~rgM@Qo z5N4KA;1Pu{@XRy@+jPf)lIqdRm&r)s&%_13;lUl>`9j47*La3+dv%+i)P6a1PdyEm zM4a|5|L4&5d$@CLsx=9H(laVIl`4(r^yvAoYBol^q4p^^7ROPpongknd9bX8nfj&gkGU(zqiPvYfmFhxVHKPg;Rb@QY&=n<(CnKq(04iNC2awix#{>;^*=y2p> zdo(WJ?^pi&N@_t&$}Htm5eJ5*3s$De-BS4mZp1h3PTit QC;lKQart+pZw!3?KTbTjsQ>@~ literal 0 HcmV?d00001 diff --git a/Resources/buttonHighlight.png b/Resources/buttonHighlight.png deleted file mode 100644 index e2de297538e6243791969ddb6ddda13a4726efdd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1021 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 literal 0 HcmV?d00001 diff --git a/Resources/feedbackActivity@2x.png b/Resources/feedbackActivity@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..83033222943dd0d6bcf6085d9bac79c6da50ab9b GIT binary patch literal 1052 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 D=aEJj literal 0 HcmV?d00001 diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 4f28f8ba..62bac7d2 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -83,6 +83,13 @@ 1E49A4D3161222B900463151 /* BITWebTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E49A4AC161222B900463151 /* BITWebTableViewCell.m */; }; 1E49A4D8161222D400463151 /* HockeySDKPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E49A4D4161222D400463151 /* HockeySDKPrivate.h */; }; 1E49A4DB161222D400463151 /* HockeySDKPrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E49A4D5161222D400463151 /* HockeySDKPrivate.m */; }; + 1E4CD1E419D17E9D00019DD4 /* Arrow@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E4CD1E319D17E9D00019DD4 /* Arrow@3x.png */; }; + 1E4CD1E619D17EA300019DD4 /* Blur@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E4CD1E519D17EA300019DD4 /* Blur@3x.png */; }; + 1E4CD1E819D17EAD00019DD4 /* Cancel@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E4CD1E719D17EAD00019DD4 /* Cancel@3x.png */; }; + 1E4CD1EA19D17EB700019DD4 /* Ok@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E4CD1E919D17EB700019DD4 /* Ok@3x.png */; }; + 1E4CD1EC19D17EBE00019DD4 /* Rectangle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E4CD1EB19D17EBE00019DD4 /* Rectangle@3x.png */; }; + 1E4CD1EE19D17ECF00019DD4 /* feedbackActivity@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E4CD1ED19D17ECF00019DD4 /* feedbackActivity@3x.png */; }; + 1E4CD1F019D17EE400019DD4 /* authorize_denied@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E4CD1EF19D17EE400019DD4 /* authorize_denied@3x.png */; }; 1E5954D315B6F24A00A03429 /* BITHockeyManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E41EB466148D7BF50015DEDC /* BITHockeyManager.m */; }; 1E5954DC15B6F24A00A03429 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E400561D148D79B500EB22B9 /* Foundation.framework */; }; 1E5954DD15B6F24A00A03429 /* CrashReporter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E41EB48B148D7C4E0015DEDC /* CrashReporter.framework */; }; @@ -91,8 +98,6 @@ 1E5955C615B71C8600A03429 /* authorize_denied.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E5955BB15B71C8600A03429 /* authorize_denied.png */; }; 1E5955C715B71C8600A03429 /* authorize_denied@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E5955BC15B71C8600A03429 /* authorize_denied@2x.png */; }; 1E5955CA15B71C8600A03429 /* bg.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E5955BF15B71C8600A03429 /* bg.png */; }; - 1E5955CB15B71C8600A03429 /* buttonHighlight.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E5955C015B71C8600A03429 /* buttonHighlight.png */; }; - 1E5955CC15B71C8600A03429 /* buttonHighlight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E5955C115B71C8600A03429 /* buttonHighlight@2x.png */; }; 1E5955CF15B71C8600A03429 /* IconGradient.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E5955C415B71C8600A03429 /* IconGradient.png */; }; 1E5955D015B71C8600A03429 /* IconGradient@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E5955C515B71C8600A03429 /* IconGradient@2x.png */; }; 1E5955FD15B7877B00A03429 /* BITHockeyManagerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E5955FA15B7877A00A03429 /* BITHockeyManagerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -268,6 +273,13 @@ 1E49A4AC161222B900463151 /* BITWebTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITWebTableViewCell.m; sourceTree = ""; }; 1E49A4D4161222D400463151 /* HockeySDKPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HockeySDKPrivate.h; sourceTree = ""; }; 1E49A4D5161222D400463151 /* HockeySDKPrivate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HockeySDKPrivate.m; sourceTree = ""; }; + 1E4CD1E319D17E9D00019DD4 /* Arrow@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Arrow@3x.png"; sourceTree = ""; }; + 1E4CD1E519D17EA300019DD4 /* Blur@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Blur@3x.png"; sourceTree = ""; }; + 1E4CD1E719D17EAD00019DD4 /* Cancel@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Cancel@3x.png"; sourceTree = ""; }; + 1E4CD1E919D17EB700019DD4 /* Ok@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Ok@3x.png"; sourceTree = ""; }; + 1E4CD1EB19D17EBE00019DD4 /* Rectangle@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Rectangle@3x.png"; sourceTree = ""; }; + 1E4CD1ED19D17ECF00019DD4 /* feedbackActivity@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "feedbackActivity@3x.png"; sourceTree = ""; }; + 1E4CD1EF19D17EE400019DD4 /* authorize_denied@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "authorize_denied@3x.png"; sourceTree = ""; }; 1E5954F215B6F24A00A03429 /* libHockeySDK.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libHockeySDK.a; sourceTree = BUILT_PRODUCTS_DIR; }; 1E59550A15B6F45800A03429 /* HockeySDKResources.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HockeySDKResources.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 1E59556015B6F80E00A03429 /* de */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/HockeySDK.strings; sourceTree = ""; }; @@ -280,8 +292,6 @@ 1E5955BB15B71C8600A03429 /* authorize_denied.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = authorize_denied.png; sourceTree = ""; }; 1E5955BC15B71C8600A03429 /* authorize_denied@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "authorize_denied@2x.png"; sourceTree = ""; }; 1E5955BF15B71C8600A03429 /* bg.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bg.png; sourceTree = ""; }; - 1E5955C015B71C8600A03429 /* buttonHighlight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = buttonHighlight.png; sourceTree = ""; }; - 1E5955C115B71C8600A03429 /* buttonHighlight@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "buttonHighlight@2x.png"; sourceTree = ""; }; 1E5955C415B71C8600A03429 /* IconGradient.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = IconGradient.png; sourceTree = ""; }; 1E5955C515B71C8600A03429 /* IconGradient@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "IconGradient@2x.png"; sourceTree = ""; }; 1E5955FA15B7877A00A03429 /* BITHockeyManagerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITHockeyManagerDelegate.h; sourceTree = ""; }; @@ -429,21 +439,25 @@ children = ( 9782022818F81BFC00A98D8B /* Arrow.png */, 9782022918F81BFC00A98D8B /* Arrow@2x.png */, + 1E4CD1E319D17E9D00019DD4 /* Arrow@3x.png */, 9782022A18F81BFC00A98D8B /* Blur.png */, 9782022B18F81BFC00A98D8B /* Blur@2x.png */, + 1E4CD1E519D17EA300019DD4 /* Blur@3x.png */, 9782022C18F81BFC00A98D8B /* Cancel.png */, 9782022D18F81BFC00A98D8B /* Cancel@2x.png */, + 1E4CD1E719D17EAD00019DD4 /* Cancel@3x.png */, 9782022E18F81BFC00A98D8B /* Ok.png */, 9782022F18F81BFC00A98D8B /* Ok@2x.png */, + 1E4CD1E919D17EB700019DD4 /* Ok@3x.png */, 9782023018F81BFC00A98D8B /* Rectangle.png */, 9782023118F81BFC00A98D8B /* Rectangle@2x.png */, + 1E4CD1EB19D17EBE00019DD4 /* Rectangle@3x.png */, 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */, 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */, 1E5955BB15B71C8600A03429 /* authorize_denied.png */, 1E5955BC15B71C8600A03429 /* authorize_denied@2x.png */, + 1E4CD1EF19D17EE400019DD4 /* authorize_denied@3x.png */, 1E5955BF15B71C8600A03429 /* bg.png */, - 1E5955C015B71C8600A03429 /* buttonHighlight.png */, - 1E5955C115B71C8600A03429 /* buttonHighlight@2x.png */, 1E1127BC16580C87007067A2 /* buttonRoundedDelete.png */, 1E1127BD16580C87007067A2 /* buttonRoundedDelete@2x.png */, 1E1127BE16580C87007067A2 /* buttonRoundedDeleteHighlighted.png */, @@ -458,6 +472,7 @@ 1EAF20A5162DC0F600957B1D /* feedbackActivity~ipad.png */, 1EAF20A6162DC0F600957B1D /* feedbackActiviy.png */, 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */, + 1E4CD1ED19D17ECF00019DD4 /* feedbackActivity@3x.png */, ); name = Images; sourceTree = ""; @@ -925,12 +940,12 @@ 1E5955C715B71C8600A03429 /* authorize_denied@2x.png in Resources */, 9782023B18F81BFC00A98D8B /* Rectangle@2x.png in Resources */, 1E5955CA15B71C8600A03429 /* bg.png in Resources */, + 1E4CD1E819D17EAD00019DD4 /* Cancel@3x.png in Resources */, + 1E4CD1EA19D17EB700019DD4 /* Ok@3x.png in Resources */, 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */, 9782023618F81BFC00A98D8B /* Cancel.png in Resources */, 9782023318F81BFC00A98D8B /* Arrow@2x.png in Resources */, 9782023818F81BFC00A98D8B /* Ok.png in Resources */, - 1E5955CB15B71C8600A03429 /* buttonHighlight.png in Resources */, - 1E5955CC15B71C8600A03429 /* buttonHighlight@2x.png in Resources */, 1E5955CF15B71C8600A03429 /* IconGradient.png in Resources */, 1E5955D015B71C8600A03429 /* IconGradient@2x.png in Resources */, 9782023218F81BFC00A98D8B /* Arrow.png in Resources */, @@ -940,6 +955,7 @@ 9782023918F81BFC00A98D8B /* Ok@2x.png in Resources */, 9782023418F81BFC00A98D8B /* Blur.png in Resources */, 1EAF20AA162DC0F600957B1D /* feedbackActiviy.png in Resources */, + 1E4CD1EE19D17ECF00019DD4 /* feedbackActivity@3x.png in Resources */, 1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */, 9782023518F81BFC00A98D8B /* Blur@2x.png in Resources */, 1E1127C416580C87007067A2 /* buttonRoundedDelete.png in Resources */, @@ -947,11 +963,15 @@ 1E1127C616580C87007067A2 /* buttonRoundedDeleteHighlighted.png in Resources */, 9782023718F81BFC00A98D8B /* Cancel@2x.png in Resources */, 1E1127C716580C87007067A2 /* buttonRoundedDeleteHighlighted@2x.png in Resources */, + 1E4CD1F019D17EE400019DD4 /* authorize_denied@3x.png in Resources */, 1E1127C816580C87007067A2 /* buttonRoundedRegular.png in Resources */, 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */, + 1E4CD1E619D17EA300019DD4 /* Blur@3x.png in Resources */, + 1E4CD1E419D17E9D00019DD4 /* Arrow@3x.png in Resources */, 1E1127C916580C87007067A2 /* buttonRoundedRegular@2x.png in Resources */, 1E1127CA16580C87007067A2 /* buttonRoundedRegularHighlighted.png in Resources */, 1E1127CB16580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png in Resources */, + 1E4CD1EC19D17EBE00019DD4 /* Rectangle@3x.png in Resources */, 1EB52FD5167B766100C801D5 /* HockeySDK.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; From 03767a63ef6ac0a3ea1bb0bbd7001ad63a3526c2 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 24 Sep 2014 14:20:15 +0200 Subject: [PATCH 200/206] Add migration text for BITCrashManagerCallbacks --- docs/Guide-Migration-Kits-template.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/Guide-Migration-Kits-template.md b/docs/Guide-Migration-Kits-template.md index 9a358088..e1e39b4e 100644 --- a/docs/Guide-Migration-Kits-template.md +++ b/docs/Guide-Migration-Kits-template.md @@ -127,6 +127,11 @@ Instead of implementing the individual protocols in your app delegate, you can n The delegate `-(NSString *)customDeviceIdentifierForUpdateManager:(BITUpdateManager *)updateManager` has been removed. To identify the installation please use the new `BITAuthenticator` class. +### HockeySDK-iOS 3.5.x + +If you are using `PLCrashReporterCallbacks`, you now have to use `BITCrashManagerCallbacks` instead. This `struct` doesn't contain `version` any longer, so you have to remove that. Otherwise everything is the same. + + ## Troubleshooting ### ld: warning: directory not found for option '....QuincyKit.....' From 204020610129b8b24fd8a71945fc38fadbcb779d Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 24 Sep 2014 17:06:22 +0200 Subject: [PATCH 201/206] Add support for App Extensions - Added simple detection method to check wether the SDK runs in an extension - BITCrashManager will send crash reports without UI (UIAlertViews aren't allowed in extensions anyway) - Don't start BITUpdateManager, BITStoreUpdateManager, BITFeedbackManager and BITAuthenticator in app extensions --- Classes/BITCrashManager.m | 7 +++++-- Classes/BITHockeyHelper.h | 1 + Classes/BITHockeyHelper.m | 11 +++++++++++ Classes/BITHockeyManager.m | 5 +++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 02d27f60..29082662 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -919,7 +919,10 @@ - (void)triggerDelayedProcessing { * - Send pending approved crash reports */ - (void)invokeDelayedProcessing { - if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) return; + if (!bit_isRunningInAppExtension() && + [[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { + return; + } BITHockeyLog(@"INFO: Start delayed CrashManager processing"); @@ -945,7 +948,7 @@ - (void)invokeDelayedProcessing { _lastCrashFilename = [notApprovedReportFilename lastPathComponent]; } - if (!BITHockeyBundle()) { + if (!BITHockeyBundle() || bit_isRunningInAppExtension()) { [self sendNextCrashReport]; } else if (_crashManagerStatus != BITCrashManagerStatusAutoSend && notApprovedReportFilename) { diff --git a/Classes/BITHockeyHelper.h b/Classes/BITHockeyHelper.h index 8c0dba08..f4d8e9fa 100644 --- a/Classes/BITHockeyHelper.h +++ b/Classes/BITHockeyHelper.h @@ -48,6 +48,7 @@ NSString *bit_UUID(void); NSString *bit_appAnonID(void); BOOL bit_isPreiOS7Environment(void); BOOL bit_isPreiOS8Environment(void); +BOOL bit_isRunningInAppExtension(void); NSString *bit_validAppIconStringFromIcons(NSBundle *resourceBundle, NSArray *icons); NSString *bit_validAppIconFilename(NSBundle *bundle, NSBundle *resourceBundle); diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index d19e8731..09a46414 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -260,6 +260,17 @@ BOOL bit_isPreiOS8Environment(void) { return isPreiOS8Environment; } +BOOL bit_isRunningInAppExtension(void) { + static BOOL isRunningInAppExtension = NO; + static dispatch_once_t checkAppExtension; + + dispatch_once(&checkAppExtension, ^{ + isRunningInAppExtension = [[[NSBundle mainBundle] executablePath] containsString:@".appex/"]; + }); + + return isRunningInAppExtension; +} + /** Find a valid app icon filename that points to a proper app icon image diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index 893148a5..490f0441 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -242,6 +242,11 @@ - (void)startManager { } #endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ + // App Extensions can only use BITCrashManager, so ignore all others automatically + if (bit_isRunningInAppExtension()) { + return; + } + #if HOCKEYSDK_FEATURE_STORE_UPDATES // start StoreUpdateManager if ([self isStoreUpdateManagerEnabled]) { From 4f05060adb2247a97999f0ac7d6d2ff93db1b093 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 25 Sep 2014 10:15:15 +0200 Subject: [PATCH 202/206] Fix extension check code to be iOS 6/7 compatible --- Classes/BITHockeyHelper.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 09a46414..3835c5f9 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -265,7 +265,7 @@ BOOL bit_isRunningInAppExtension(void) { static dispatch_once_t checkAppExtension; dispatch_once(&checkAppExtension, ^{ - isRunningInAppExtension = [[[NSBundle mainBundle] executablePath] containsString:@".appex/"]; + isRunningInAppExtension = ([[[NSBundle mainBundle] executablePath] rangeOfString:@".appex/"].location != NSNotFound); }); return isRunningInAppExtension; From 3b9482c32a5b02d67b42a4242675a2cfd8a5c815 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 25 Sep 2014 10:21:55 +0200 Subject: [PATCH 203/206] Improve iOS version runtime checks Hardcode the iOS 6.1 and iOS 7.1 NSFoundationVersionNumber values, so the checks also work when compiling the source with iOS 6.1 or iOS 7.1 as base iOS version. --- Classes/BITHockeyHelper.m | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 3835c5f9..8da51ca0 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -221,18 +221,16 @@ BOOL bit_isPreiOS7Environment(void) { static dispatch_once_t checkOS; dispatch_once(&checkOS, ^{ - // we only perform this runtime check if this is build against at least iOS7 base SDK -#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 + // NSFoundationVersionNumber_iOS_6_1 = 993.00 + // We hardcode this, so compiling with iOS 6 is possible while still being able to detect the correct environment + // runtime check according to // https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { + if (floor(NSFoundationVersionNumber) <= 993.00) { isPreiOS7Environment = YES; } else { isPreiOS7Environment = NO; } -#else - isPreiOS7Environment = YES; -#endif }); return isPreiOS7Environment; @@ -243,18 +241,16 @@ BOOL bit_isPreiOS8Environment(void) { static dispatch_once_t checkOS8; dispatch_once(&checkOS8, ^{ - // we only perform this runtime check if this is build against at least iOS8 base SDK -#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_7_1 + // NSFoundationVersionNumber_iOS_7_1 = 1047.25 + // We hardcode this, so compiling with iOS 7 is possible while still being able to detect the correct environment + // runtime check according to // https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_7_1) { + if (floor(NSFoundationVersionNumber) <= 1047.25) { isPreiOS8Environment = YES; } else { isPreiOS8Environment = NO; } -#else - isPreiOS8Environment = YES; -#endif }); return isPreiOS8Environment; From 3354a36f114354e948dd2986406552a49629c4b4 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 25 Sep 2014 15:14:23 +0200 Subject: [PATCH 204/206] Don't warn for missing resource with extensions App extensions can't display UIAlerts for crashes anyway, so we don't need the extensions in the extension bundle. --- Classes/BITCrashManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 29082662..21408ba9 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -169,7 +169,7 @@ - (instancetype)init { [_fileManager removeItemAtPath:_analyzerInProgressFile error:&error]; } - if (!BITHockeyBundle()) { + if (!BITHockeyBundle() && !bit_isRunningInAppExtension()) { NSLog(@"[HockeySDK] WARNING: %@ is missing, will send reports automatically!", BITHOCKEYSDK_BUNDLE); } } From cdf6691579f84d7e29edb68e068acfa9e6dd657c Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 25 Sep 2014 17:29:45 +0200 Subject: [PATCH 205/206] More updates on extension support - Added iOS 8 extension setup documentation - Added a crash reporting only framework build to the binary distribution target --- Support/HockeySDK.xcodeproj/project.pbxproj | 128 +++++++++++++++++++- Support/HockeySDKCrashOnlyConfig.h | 90 ++++++++++++++ Support/crashonly.xcconfig | 3 + docs/Guide-Installation-Setup-template.md | 30 ++++- 4 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 Support/HockeySDKCrashOnlyConfig.h create mode 100644 Support/crashonly.xcconfig diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 62bac7d2..4dff5ac6 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -316,6 +316,7 @@ 1E754E5B1621FBB70070AB92 /* BITCrashReportTextFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashReportTextFormatter.m; sourceTree = ""; }; 1E7A45FA16F54FB5005B08F1 /* OCHamcrestIOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCHamcrestIOS.framework; sourceTree = ""; }; 1E7A45FB16F54FB5005B08F1 /* OCMockitoIOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCMockitoIOS.framework; sourceTree = ""; }; + 1E7DE39619D44DC6009AB8E5 /* crashonly.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = crashonly.xcconfig; sourceTree = ""; }; 1E84DB3317E0977C00AC83FD /* HockeySDKFeatureConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HockeySDKFeatureConfig.h; sourceTree = ""; }; 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashDetails.h; sourceTree = ""; }; 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashDetails.m; sourceTree = ""; }; @@ -512,6 +513,7 @@ children = ( 1E754DC61621BC170070AB92 /* HockeySDK.xcconfig */, 1E66CA9115D4100500F35BED /* buildnumber.xcconfig */, + 1E7DE39619D44DC6009AB8E5 /* crashonly.xcconfig */, 1E91D84619B924E600E9616D /* module.modulemap */, ); name = Support; @@ -1007,7 +1009,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Sets the target folders and the final framework product.\nFMK_NAME=HockeySDK\nFMK_VERSION=A\nFMK_RESOURCE_BUNDLE=HockeySDKResources\n\n# Documentation\nHOCKEYSDK_DOCSET_VERSION_NAME=\"de.bitstadium.${HOCKEYSDK_DOCSET_NAME}-${VERSION_STRING}\"\n\n# Install dir will be the final output to the framework.\n# The following line create it in the root folder of the current project.\nPRODUCTS_DIR=${SRCROOT}/../Products\nZIP_FOLDER=HockeySDK-iOS\nTEMP_DIR=${PRODUCTS_DIR}/${ZIP_FOLDER}\nINSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.framework\n\n# Working dir will be deleted after the framework creation.\nWRK_DIR=build\nDEVICE_DIR=${WRK_DIR}/Release-iphoneos\nSIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator\nHEADERS_DIR=${WRK_DIR}/Release-iphoneos/usr/local/include\n\n# Building both architectures.\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphoneos\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphonesimulator\n\n# Cleaning the oldest.\nif [ -d \"${TEMP_DIR}\" ]\nthen\nrm -rf \"${TEMP_DIR}\"\nfi\n\n# Creates and renews the final product folder.\nmkdir -p \"${INSTALL_DIR}\"\nmkdir -p \"${INSTALL_DIR}/Modules\"\nmkdir -p \"${INSTALL_DIR}/Versions\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers\"\n\n# Creates the internal links.\n# It MUST uses relative path, otherwise will not work when the folder is copied/moved.\nln -s \"${FMK_VERSION}\" \"${INSTALL_DIR}/Versions/Current\"\nln -s \"Versions/Current/Headers\" \"${INSTALL_DIR}/Headers\"\nln -s \"Versions/Current/Resources\" \"${INSTALL_DIR}/Resources\"\nln -s \"Versions/Current/${FMK_NAME}\" \"${INSTALL_DIR}/${FMK_NAME}\"\n\n# Copy the swift import file\ncp -f \"${SRCROOT}/module.modulemap\" \"${INSTALL_DIR}/Modules/\"\n\n# Copies the headers and resources files to the final product folder.\ncp -R \"${SRCROOT}/build/Release-iphoneos/include/HockeySDK/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${DEVICE_DIR}/${FMK_RESOURCE_BUNDLE}.bundle\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\ncp -f \"${SRCROOT}/${FMK_NAME}.xcconfig\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\n\n# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.\nlipo -create \"${DEVICE_DIR}/lib${FMK_NAME}.a\" \"${SIMULATOR_DIR}/lib${FMK_NAME}.a\" -output \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\"\n\n# Combine the CrashReporter static library into a new Hockey static library file if they are not already present and copy the public headers too\nif [ -z $(otool -L \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" | grep 'libCrashReporter') ]\nthen\nlibtool -static -o \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${SRCROOT}/../Vendor/CrashReporter.framework/Versions/A/CrashReporter\"\nfi\n\nrm -r \"${WRK_DIR}\"\n\n# build embeddedframework folder and move framework into it\nmkdir \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework\"\nmv \"${INSTALL_DIR}\" \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework/${FMK_NAME}.framework\"\n\n# link Resources\nNEW_INSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.embeddedframework\nmkdir \"${NEW_INSTALL_DIR}/Resources\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_RESOURCE_BUNDLE}.bundle\" \"${NEW_INSTALL_DIR}/Resources/${FMK_RESOURCE_BUNDLE}.bundle\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_NAME}.xcconfig\" \"${NEW_INSTALL_DIR}/Resources/${FMK_NAME}.xcconfig\"\n\n# copy license, changelog, documentation, integration json\ncp -f \"${SRCROOT}/../Docs/Changelog-template.md\" \"${TEMP_DIR}/CHANGELOG\"\ncp -f \"${SRCROOT}/../Docs/Guide-Installation-Setup-template.md\" \"${TEMP_DIR}/README.md\"\ncp -f \"${SRCROOT}/../LICENSE\" \"${TEMP_DIR}\"\nmkdir \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\ncp -R \"${SRCROOT}/../documentation/docset/Contents\" \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\n\n# build zip\ncd \"${PRODUCTS_DIR}\"\nrm -f \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nzip -yr \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${ZIP_FOLDER}\" -x \\*/.*\n"; + shellScript = "# Sets the target folders and the final framework product.\nFMK_NAME=HockeySDK\nFMK_VERSION=A\nFMK_RESOURCE_BUNDLE=HockeySDKResources\n\n# Documentation\nHOCKEYSDK_DOCSET_VERSION_NAME=\"de.bitstadium.${HOCKEYSDK_DOCSET_NAME}-${VERSION_STRING}\"\n\n# Install dir will be the final output to the framework.\n# The following line create it in the root folder of the current project.\nPRODUCTS_DIR=${SRCROOT}/../Products\nZIP_FOLDER=HockeySDK-iOS\nTEMP_DIR=${PRODUCTS_DIR}/${ZIP_FOLDER}\nINSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.framework\n\n# Working dir will be deleted after the framework creation.\nWRK_DIR=build\nDEVICE_DIR=${WRK_DIR}/Release-iphoneos\nSIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator\nDEVICE_CRASH_ONLY_DIR=${WRK_DIR}/ReleaseCrashOnly-iphoneos\nSIMULATOR_CRASH_ONLY_DIR=${WRK_DIR}/ReleaseCrashOnly-iphonesimulator\n\n# Building the full featured SDK\n\n# Building both architectures.\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphoneos\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphonesimulator\n\n# Cleaning the oldest.\nif [ -d \"${TEMP_DIR}\" ]\nthen\nrm -rf \"${TEMP_DIR}\"\nfi\n\n# Creates and renews the final product folder.\nmkdir -p \"${INSTALL_DIR}\"\nmkdir -p \"${INSTALL_DIR}/Modules\"\nmkdir -p \"${INSTALL_DIR}/Versions\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers\"\n\n# Creates the internal links.\n# It MUST uses relative path, otherwise will not work when the folder is copied/moved.\nln -s \"${FMK_VERSION}\" \"${INSTALL_DIR}/Versions/Current\"\nln -s \"Versions/Current/Headers\" \"${INSTALL_DIR}/Headers\"\nln -s \"Versions/Current/Resources\" \"${INSTALL_DIR}/Resources\"\nln -s \"Versions/Current/${FMK_NAME}\" \"${INSTALL_DIR}/${FMK_NAME}\"\n\n# Copy the swift import file\ncp -f \"${SRCROOT}/module.modulemap\" \"${INSTALL_DIR}/Modules/\"\n\n# Copies the headers and resources files to the final product folder.\ncp -R \"${SRCROOT}/build/Release-iphoneos/include/HockeySDK/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${DEVICE_DIR}/${FMK_RESOURCE_BUNDLE}.bundle\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\ncp -f \"${SRCROOT}/${FMK_NAME}.xcconfig\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\n\n# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.\nlipo -create \"${DEVICE_DIR}/lib${FMK_NAME}.a\" \"${SIMULATOR_DIR}/lib${FMK_NAME}.a\" -output \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\"\n\n# Combine the CrashReporter static library into a new Hockey static library file if they are not already present and copy the public headers too\nif [ -z $(otool -L \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" | grep 'libCrashReporter') ]\nthen\nlibtool -static -o \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${SRCROOT}/../Vendor/CrashReporter.framework/Versions/A/CrashReporter\"\nfi\n\nrm -r \"${WRK_DIR}\"\n\n# build embeddedframework folder and move framework into it\nmkdir \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework\"\nmv \"${INSTALL_DIR}\" \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework/${FMK_NAME}.framework\"\n\n# link Resources\nNEW_INSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.embeddedframework\nmkdir \"${NEW_INSTALL_DIR}/Resources\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_RESOURCE_BUNDLE}.bundle\" \"${NEW_INSTALL_DIR}/Resources/${FMK_RESOURCE_BUNDLE}.bundle\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_NAME}.xcconfig\" \"${NEW_INSTALL_DIR}/Resources/${FMK_NAME}.xcconfig\"\n\n\n# Building the crash only SDK without resources\n\n# Building both architectures.\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"ReleaseCrashOnly\" -target \"${FMK_NAME}\" -sdk iphoneos\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"ReleaseCrashOnly\" -target \"${FMK_NAME}\" -sdk iphonesimulator\n\n# Creates and renews the final product folder.\nmkdir -p \"${INSTALL_DIR}\"\nmkdir -p \"${INSTALL_DIR}/Modules\"\nmkdir -p \"${INSTALL_DIR}/Versions\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers\"\n\n# Creates the internal links.\n# It MUST uses relative path, otherwise will not work when the folder is copied/moved.\nln -s \"${FMK_VERSION}\" \"${INSTALL_DIR}/Versions/Current\"\nln -s \"Versions/Current/Headers\" \"${INSTALL_DIR}/Headers\"\nln -s \"Versions/Current/Resources\" \"${INSTALL_DIR}/Resources\"\nln -s \"Versions/Current/${FMK_NAME}\" \"${INSTALL_DIR}/${FMK_NAME}\"\n\n# Copy the swift import file\ncp -f \"${SRCROOT}/module.modulemap\" \"${INSTALL_DIR}/Modules/\"\n\n# Copies the headers and resources files to the final product folder.\ncp -R \"${SRCROOT}\"/build/ReleaseCrashOnly-iphoneos/include/HockeySDK/BITCrash*.h \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${SRCROOT}/\"build/ReleaseCrashOnly-iphoneos/include/HockeySDK/BITHockey*.h \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${SRCROOT}/build/ReleaseCrashOnly-iphoneos/include/HockeySDK/HockeySDK.h\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -f \"${SRCROOT}/${FMK_NAME}.xcconfig\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\n\n# Copy the patched feature header\ncp -f \"${SRCROOT}/HockeySDKCrashOnlyConfig.h\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/HockeySDKFeatureConfig.h\"\n\n# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.\nlipo -create \"${DEVICE_CRASH_ONLY_DIR}/lib${FMK_NAME}.a\" \"${SIMULATOR_CRASH_ONLY_DIR}/lib${FMK_NAME}.a\" -output \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\"\n\n# Combine the CrashReporter static library into a new Hockey static library file if they are not already present and copy the public headers too\nif [ -z $(otool -L \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" | grep 'libCrashReporter') ]\nthen\nlibtool -static -o \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${SRCROOT}/../Vendor/CrashReporter.framework/Versions/A/CrashReporter\"\nfi\n\n# Move the crash reporting only framework into a new folder\nmkdir \"${INSTALL_DIR}/../${FMK_NAME}CrashOnly\"\nmv \"${INSTALL_DIR}\" \"${INSTALL_DIR}/../${FMK_NAME}CrashOnly/${FMK_NAME}.framework\"\n\nrm -r \"${WRK_DIR}\"\n\n\n\n# copy license, changelog, documentation, integration json\ncp -f \"${SRCROOT}/../Docs/Changelog-template.md\" \"${TEMP_DIR}/CHANGELOG\"\ncp -f \"${SRCROOT}/../Docs/Guide-Installation-Setup-template.md\" \"${TEMP_DIR}/README.md\"\ncp -f \"${SRCROOT}/../LICENSE\" \"${TEMP_DIR}\"\nmkdir \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\ncp -R \"${SRCROOT}/../documentation/docset/Contents\" \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\n\n# build zip\ncd \"${PRODUCTS_DIR}\"\nrm -f \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nzip -yr \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${ZIP_FOLDER}\" -x \\*/.*\n"; }; 1E8E66B215BC3D8200632A2E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1430,6 +1432,124 @@ }; name = CodeCoverage; }; + 1E7DE39019D44D88009AB8E5 /* ReleaseCrashOnly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1E7DE39619D44DC6009AB8E5 /* crashonly.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PREPROCESSOR_DEFINITIONS = ( + "BITHOCKEY_STATIC_LIBRARY=1", + "$(inherited)", + ); + GCC_THUMB_SUPPORT = NO; + GCC_VERSION = ""; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_STRICT_SELECTOR_MATCH = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "../Resources/HockeySDK-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + "IPHONEOS_DEPLOYMENT_TARGET[arch=arm64]" = 7.0; + MACOSX_DEPLOYMENT_TARGET = 10.5; + OTHER_CFLAGS = ( + "-Wshorten-64-to-32", + "-Wall", + ); + RUN_CLANG_STATIC_ANALYZER = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = NO; + }; + name = ReleaseCrashOnly; + }; + 1E7DE39119D44D88009AB8E5 /* ReleaseCrashOnly */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphonesimulator*]" = "$(BIT_SIM_ARCHS)"; + DSTROOT = /tmp/HockeySDK.dst; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../Vendor\"", + ); + GCC_THUMB_SUPPORT = NO; + OTHER_CFLAGS = ( + "$(inherited)", + "-DNS_BLOCK_ASSERTIONS=1", + ); + PRODUCT_NAME = HockeySDK; + PUBLIC_HEADERS_FOLDER_PATH = "include/$(PRODUCT_NAME)"; + SKIP_INSTALL = YES; + "VALID_ARCHS[sdk=iphonesimulator*]" = "$(BIT_SIM_ARCHS)"; + }; + name = ReleaseCrashOnly; + }; + 1E7DE39219D44D88009AB8E5 /* ReleaseCrashOnly */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_THUMB_SUPPORT = NO; + INFOPLIST_FILE = "../Resources/HockeySDK-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + WRAPPER_EXTENSION = bundle; + }; + name = ReleaseCrashOnly; + }; + 1E7DE39319D44D88009AB8E5 /* ReleaseCrashOnly */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_THUMB_SUPPORT = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = ReleaseCrashOnly; + }; + 1E7DE39419D44D88009AB8E5 /* ReleaseCrashOnly */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = HockeySDK.framework; + }; + name = ReleaseCrashOnly; + }; + 1E7DE39519D44D88009AB8E5 /* ReleaseCrashOnly */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + FRAMEWORK_SEARCH_PATHS = ( + "\"$(SDKROOT)/Developer/Library/Frameworks\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + "\"$(SRCROOT)/../Vendor\"", + "\"$(SRCROOT)/HockeySDKTests\"", + "$(inherited)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "HockeySDKTests/HockeySDKTests-Prefix.pch"; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + INFOPLIST_FILE = "HockeySDKTests/HockeySDKTests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 6.1; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/HockeySDKTests\"", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + VALIDATE_PRODUCT = YES; + }; + name = ReleaseCrashOnly; + }; 1E8E66AE15BC3D7700632A2E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1531,6 +1651,7 @@ 1E4F61EB1621AD970033EFC5 /* Debug */, 1E68F4AB16F7843F00053706 /* CodeCoverage */, 1E4F61EC1621AD970033EFC5 /* Release */, + 1E7DE39419D44D88009AB8E5 /* ReleaseCrashOnly */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -1541,6 +1662,7 @@ 1E5954F015B6F24A00A03429 /* Debug */, 1E68F4A816F7843F00053706 /* CodeCoverage */, 1E5954F115B6F24A00A03429 /* Release */, + 1E7DE39119D44D88009AB8E5 /* ReleaseCrashOnly */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -1551,6 +1673,7 @@ 1E59551515B6F45800A03429 /* Debug */, 1E68F4A916F7843F00053706 /* CodeCoverage */, 1E59551615B6F45800A03429 /* Release */, + 1E7DE39219D44D88009AB8E5 /* ReleaseCrashOnly */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -1561,6 +1684,7 @@ 1E5A45A016F0DFC200B55C04 /* Debug */, 1E68F4AC16F7843F00053706 /* CodeCoverage */, 1E5A45A116F0DFC200B55C04 /* Release */, + 1E7DE39519D44D88009AB8E5 /* ReleaseCrashOnly */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -1571,6 +1695,7 @@ 1E8E66AE15BC3D7700632A2E /* Debug */, 1E68F4AA16F7843F00053706 /* CodeCoverage */, 1E8E66AF15BC3D7700632A2E /* Release */, + 1E7DE39319D44D88009AB8E5 /* ReleaseCrashOnly */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -1581,6 +1706,7 @@ E400563C148D79B500EB22B9 /* Debug */, 1E68F4A716F7843F00053706 /* CodeCoverage */, E400563D148D79B500EB22B9 /* Release */, + 1E7DE39019D44D88009AB8E5 /* ReleaseCrashOnly */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Support/HockeySDKCrashOnlyConfig.h b/Support/HockeySDKCrashOnlyConfig.h new file mode 100644 index 00000000..1a4a2ee7 --- /dev/null +++ b/Support/HockeySDKCrashOnlyConfig.h @@ -0,0 +1,90 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2013-2014 HockeyApp, Bit Stadium GmbH. + * 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. + */ + +/* + * This file is only used by the binary framework target when building + * and creating the crash reporting only framework + * + * Attention: Do not include this into your projects yourself! + */ + +#ifndef HockeySDK_HockeySDKFeatureConfig_h +#define HockeySDK_HockeySDKFeatureConfig_h + + +/** + * If true, include support for handling crash reports + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_CRASH_REPORTER +# define HOCKEYSDK_FEATURE_CRASH_REPORTER 1 +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ + + +/** + * If true, include support for managing user feedback + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_FEEDBACK +# define HOCKEYSDK_FEATURE_FEEDBACK 0 +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ + + +/** + * If true, include support for informing the user about new updates pending in the App Store + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_STORE_UPDATES +# define HOCKEYSDK_FEATURE_STORE_UPDATES 0 +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ + + +/** + * If true, include support for authentication installations for Ad-Hoc and Enterprise builds + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_AUTHENTICATOR +# define HOCKEYSDK_FEATURE_AUTHENTICATOR 0 +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ + + +/** + * If true, include support for handling in-app udpates for Ad-Hoc and Enterprise builds + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_UPDATES +# define HOCKEYSDK_FEATURE_UPDATES 0 +#endif /* HOCKEYSDK_FEATURE_UPDATES */ + + +#endif /* HockeySDK_HockeySDKFeatureConfig_h */ diff --git a/Support/crashonly.xcconfig b/Support/crashonly.xcconfig new file mode 100644 index 00000000..0593ddb4 --- /dev/null +++ b/Support/crashonly.xcconfig @@ -0,0 +1,3 @@ +#include "buildnumber.xcconfig" + +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) HOCKEYSDK_FEATURE_CRASH_REPORTER=1 HOCKEYSDK_FEATURE_FEEDBACK=0 HOCKEYSDK_FEATURE_STORE_UPDATES=0 HOCKEYSDK_FEATURE_AUTHENTICATOR=0 HOCKEYSDK_FEATURE_UPDATES=0 BITHOCKEY_VERSION="@\""$(VERSION_STRING)"\"" BITHOCKEY_BUILD="@\""$(BUILD_NUMBER)"\"" BITHOCKEY_C_VERSION="\""$(VERSION_STRING)"\"" BITHOCKEY_C_BUILD="\""$(BUILD_NUMBER)"\"" diff --git a/docs/Guide-Installation-Setup-template.md b/docs/Guide-Installation-Setup-template.md index 82efcdad..1e4af487 100644 --- a/docs/Guide-Installation-Setup-template.md +++ b/docs/Guide-Installation-Setup-template.md @@ -10,8 +10,9 @@ This document contains the following sections: - [Requirements](#requirements) - [Download & Extract](#download) -- [Set up Xcode](#xcode) +- [Set up Xcode](#xcode) - [Modify Code](#modify) +- [iOS 8 Extensions](#extension) - [Additional Options](#options) @@ -107,6 +108,33 @@ The SDK runs on devices with iOS 6.0 or higher. *Note:* The SDK is optimized to defer everything possible to a later time while making sure e.g. crashes on startup can also be caught and each module executes other code with a delay some seconds. This ensures that applicationDidFinishLaunching will process as fast as possible and the SDK will not block the startup sequence resulting in a possible kill by the watchdog process. + +## iOS 8 Extensions + +The following points need to be considered to use HockeySDK with iOS 8 Extensions: + +1. Each extension is required to use the same values for version (`CFBundleShortVersionString`) and build number (`CFBundleVersion`) as the main app uses. (This is required only if you are using the same APP_IDENTIFIER for your app and extensions). +2. You need to make sure the SDK setup code is only invoked once. Since there is no `applicationDidFinishLaunching:` equivalent and `viewDidLoad` can run multiple times, you need to use a setup like the following example: + + @interface TodayViewController () + + @property (nonatomic, assign) BOOL didSetupHockeySDK; + + @end + + @implementation TodayViewController + + - (void)viewDidLoad { + [super viewDidLoad]; + if (!self.didSetupHockeySDK) { + [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + [[BITHockeyManager sharedHockeyManager] startManager]; + self.didSetupHockeySDK = YES; + } + } +3. The binary distribution provides a special framework build in the `HockeySDKCrashOnly` folder of the distribution zip file, which only contains crash reporting functionality (also automatic sending crash reports only). You can use this to further slim down the binary size of your extensions. + + ## Additional Options From 476a4de23249181b873d74130cd05e94e0ba2bfa Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 26 Sep 2014 14:07:18 +0200 Subject: [PATCH 206/206] 3.6 release and documentation updates --- HockeySDK.podspec | 8 ++--- README.md | 16 ++++----- Support/buildnumber.xcconfig | 4 +-- docs/Changelog-template.md | 21 ++++++++++++ ...de-Installation-Setup-Advanced-template.md | 34 ++++++++++++++++--- docs/Guide-Installation-Setup-template.md | 8 ++--- docs/Guide-Migration-Kits-template.md | 15 ++++++++ docs/index.md | 4 +-- 8 files changed, 86 insertions(+), 24 deletions(-) diff --git a/HockeySDK.podspec b/HockeySDK.podspec index 9fc1c071..389e7613 100644 --- a/HockeySDK.podspec +++ b/HockeySDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'HockeySDK' - s.version = '3.6-b.2' + s.version = '3.6' s.summary = 'Collect live crash reports, get feedback from your users, distribute your betas, and analyze your test coverage with HockeyApp.' s.description = <<-DESC @@ -12,7 +12,7 @@ Pod::Spec.new do |s| DESC s.homepage = 'http://hockeyapp.net/' - s.documentation_url = 'http://hockeyapp.net/help/sdk/ios/3.6-b.2/' + s.documentation_url = 'http://hockeyapp.net/help/sdk/ios/3.6/' s.license = 'MIT' s.author = { 'Andreas Linde' => 'mail@andreaslinde.de', 'Thomas Dohmke' => "thomas@dohmke.de" } @@ -22,9 +22,9 @@ Pod::Spec.new do |s| s.source_files = 'Classes' s.requires_arc = true - s.frameworks = 'CoreText', 'QuartzCore', 'SystemConfiguration', 'CoreGraphics', 'UIKit', 'Security', 'AssetsLibrary', 'MobileCoreServices', 'QuickLook' + s.frameworks = 'AssetsLibrary', 'CoreText', 'CoreGraphics', 'MobileCoreServices', 'QuartzCore', 'QuickLook', 'Security', 'SystemConfiguration', 'UIKit' s.ios.vendored_frameworks = 'Vendor/CrashReporter.framework' - s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => %{$(inherited) BITHOCKEY_VERSION="@\\"#{s.version}\\"" BITHOCKEY_C_VERSION="\\"#{s.version}\\"" BITHOCKEY_BUILD="@\\"30\\"" BITHOCKEY_C_BUILD="\\"30\\""} } + s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => %{$(inherited) BITHOCKEY_VERSION="@\\"#{s.version}\\"" BITHOCKEY_C_VERSION="\\"#{s.version}\\"" BITHOCKEY_BUILD="@\\"33\\"" BITHOCKEY_C_BUILD="\\"33\\""} } s.resource_bundle = { 'HockeySDKResources' => ['Resources/*.png', 'Resources/*.lproj'] } s.preserve_paths = 'Resources', 'Support' diff --git a/README.md b/README.md index ffc17ef4..5abfbef9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -## Version 3.6 Beta 2 +## Version 3.6 -- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Changelog.html) +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6/docs/docs/Changelog.html) ## Introduction @@ -25,16 +25,16 @@ The main SDK class is `BITHockeyManager`. It initializes all modules and provide ## Prerequisites 1. Before you integrate HockeySDK into your own app, you should add the app to HockeyApp if you haven't already. Read [this how-to](http://support.hockeyapp.net/kb/how-tos/how-to-create-a-new-app) on how to do it. -2. We also assume that you already have a project in Xcode and that this project is opened in Xcode 4. +2. We also assume that you already have a project in Xcode and that this project is opened in Xcode 5. 3. The SDK supports iOS 6.0 or newer. ## Installation & Setup -- [Installation & Setup](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Guide-Installation-Setup.html) (Recommended) -- [Installation & Setup Advanced](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Guide-Installation-Setup-Advanced.html) (Using Git submodule and Xcode sub-project) -- [Identify and authenticate users of Ad-Hoc or Enterprise builds](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/HowTo-Authenticating-Users-on-iOS.html) -- [Migration from previous SDK Versions](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Guide-Migration-Kits.html) +- [Installation & Setup](http://www.hockeyapp.net/help/sdk/ios/3.6/docs/docs/Guide-Installation-Setup.html) (Recommended) +- [Installation & Setup Advanced](http://www.hockeyapp.net/help/sdk/ios/3.6/docs/docs/Guide-Installation-Setup-Advanced.html) (Using Git submodule and Xcode sub-project) +- [Identify and authenticate users of Ad-Hoc or Enterprise builds](http://www.hockeyapp.net/help/sdk/ios/3.6/docs/docs/HowTo-Authenticating-Users-on-iOS.html) +- [Migration from previous SDK Versions](http://www.hockeyapp.net/help/sdk/ios/3.6/docs/docs/Guide-Migration-Kits.html) ## Xcode Documentation @@ -47,4 +47,4 @@ This documentation provides integrated help in Xcode for all public APIs and a s 3. Copy the content into ~`/Library/Developer/Shared/Documentation/DocSets` -The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.2/](http://hockeyapp.net/help/sdk/ios/3.6-b.2/) +The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6/](http://hockeyapp.net/help/sdk/ios/3.6/) diff --git a/Support/buildnumber.xcconfig b/Support/buildnumber.xcconfig index 7d717adf..511c3eee 100644 --- a/Support/buildnumber.xcconfig +++ b/Support/buildnumber.xcconfig @@ -1,7 +1,7 @@ #include "HockeySDK.xcconfig" -BUILD_NUMBER = 30 -VERSION_STRING = 3.6-b.2 +BUILD_NUMBER = 33 +VERSION_STRING = 3.6 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) BITHOCKEY_VERSION="@\""$(VERSION_STRING)"\"" BITHOCKEY_BUILD="@\""$(BUILD_NUMBER)"\"" BITHOCKEY_C_VERSION="\""$(VERSION_STRING)"\"" BITHOCKEY_C_BUILD="\""$(BUILD_NUMBER)"\"" BIT_ARM_ARCHS = armv7 armv7s arm64 BIT_SIM_ARCHS = x86_64 i386 diff --git a/docs/Changelog-template.md b/docs/Changelog-template.md index c91620f8..10a55891 100644 --- a/docs/Changelog-template.md +++ b/docs/Changelog-template.md @@ -1,3 +1,24 @@ +## Version 3.6.0 + +- [NEW] `BITCrashManager`: Added support for iOS 8 Extensions +- [NEW] `BITCrashManager`: Option to add a custom UI flow before sending a crash report, e.g. to ask users for more details (see `setAlertViewHandler:`) +- [NEW] `BITCrashManager`: Provide details on a crash report (see `lastSessionCrashDetails` and `BITCrashDetails`) +- [NEW] `BITCrashManager`: Experimental support for detecting app kills triggered by iOS while the app is in foreground (see `enableAppNotTerminatingCleanlyDetection`) +- [NEW] `BITCrashManager`: Added `didReceiveMemoryWarningInLastSession` which indicates if the last app session did get a memory warning by iOS +- [NEW] `BITFeedbackManager`: Attach and annotate images and screenshots +- [NEW] `BITFeedbackManager`: Attach any binary data to compose message view (see `showFeedbackComposeViewWithPreparedItems:`) +- [NEW] `BITFeedbackManager`: Show a compose message with a screenshot image attached using predefined triggers (see `feedbackObservationMode`) or your own custom triggers (see `showFeedbackComposeViewWithGeneratedScreenshot`) +- [NEW] Minimum iOS Deployment version is now iOS 6.0 +- [NEW] Requires to link additional frameworks: `AssetLibrary`, `MobileCoreServices`, `QuickLook` +- [UPDATE] `BITCrashManager`: Updated `setCrashCallbacks` handling now using `BITCrashManagerCallbacks` instead of `PLCrashReporterCallbacks` (which is no longer public) +- [UPDATE] `BITCrashManager`: Crash reports are now sent individually if there are multiple pending +- [UPDATE] `BITUpdateManager`: Improved algorithm for fetching an optimal sized app icon for the Update View +- [UPDATE] `BITUpdateManager`: Properly consider paragraphs in release notes when presenting them in the Update view +- [UPDATE] Property `delegate` in all components is now private. Set the delegate on `BITHockeyManager` only! +- [UPDATE] Removed support for Atlassian JMC +- [BUGFIX] Various additional fixes +

+ ## Version 3.6.0 Beta 2 - [NEW] `BITFeedbackManager`: Screenshot feature is now part of the public API diff --git a/docs/Guide-Installation-Setup-Advanced-template.md b/docs/Guide-Installation-Setup-Advanced-template.md index 9836044e..2705376f 100644 --- a/docs/Guide-Installation-Setup-Advanced-template.md +++ b/docs/Guide-Installation-Setup-Advanced-template.md @@ -1,6 +1,6 @@ -## Version 3.6 Beta 2 +## Version 3.6 -- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Changelog.html) +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6/docs/docs/Changelog.html) ## Introduction @@ -12,6 +12,7 @@ This document contains the following sections: - [Set up Git submodule](#download) - [Set up Xcode](#xcode) - [Modify Code](#modify) +- [iOS 8 Extensions](#extension) - [Additional Options](#options) @@ -102,7 +103,7 @@ The SDK runs on devices with iOS 6.0 or higher. ### Swift -1. Add the following line to your [Objective-C bridging header](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html) file: +1. Add the following line to your [Objective-C bridging header](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html) file: #import @@ -127,6 +128,31 @@ The SDK runs on devices with iOS 6.0 or higher. *Note:* The SDK is optimized to defer everything possible to a later time while making sure e.g. crashes on startup can also be caught and each module executes other code with a delay some seconds. This ensures that applicationDidFinishLaunching will process as fast as possible and the SDK will not block the startup sequence resulting in a possible kill by the watchdog process. + +## iOS 8 Extensions + +The following points need to be considered to use HockeySDK with iOS 8 Extensions: + +1. Each extension is required to use the same values for version (`CFBundleShortVersionString`) and build number (`CFBundleVersion`) as the main app uses. (This is required only if you are using the same APP_IDENTIFIER for your app and extensions). +2. You need to make sure the SDK setup code is only invoked once. Since there is no `applicationDidFinishLaunching:` equivalent and `viewDidLoad` can run multiple times, you need to use a setup like the following example: + + @interface TodayViewController () + + @property (nonatomic, assign) BOOL didSetupHockeySDK; + + @end + + @implementation TodayViewController + + - (void)viewDidLoad { + [super viewDidLoad]; + if (!self.didSetupHockeySDK) { + [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + [[BITHockeyManager sharedHockeyManager] startManager]; + self.didSetupHockeySDK = YES; + } + } + ## Additional Options @@ -140,7 +166,7 @@ This documentation provides integrated help in Xcode for all public APIs and a s 3. Copy the content into ~`/Library/Developer/Shared/Documentation/DocSets` -The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.1/](http://hockeyapp.net/help/sdk/ios/3.6-b.1/) +The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6/](http://hockeyapp.net/help/sdk/ios/3.6/) ### Set up with xcconfig diff --git a/docs/Guide-Installation-Setup-template.md b/docs/Guide-Installation-Setup-template.md index 1e4af487..4236a901 100644 --- a/docs/Guide-Installation-Setup-template.md +++ b/docs/Guide-Installation-Setup-template.md @@ -1,6 +1,6 @@ -## Version 3.6 Beta 2 +## Version 3.6 -- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6-b.2/docs/docs/Changelog.html) +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/3.6/docs/docs/Changelog.html) ## Introduction @@ -142,9 +142,9 @@ The following points need to be considered to use HockeySDK with iOS 8 Extension This documentation provides integrated help in Xcode for all public APIs and a set of additional tutorials and how-tos. -1. Copy `de.bitstadium.HockeySDK-iOS-3.6-b.2.docset` into ~`/Library/Developer/Shared/Documentation/DocSets` +1. Copy `de.bitstadium.HockeySDK-iOS-3.6.docset` into ~`/Library/Developer/Shared/Documentation/DocSets` -The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6-b.2/](http://hockeyapp.net/help/sdk/ios/3.6-b.2/) +The documentation is also available via the following URL: [http://hockeyapp.net/help/sdk/ios/3.6/](http://hockeyapp.net/help/sdk/ios/3.6/) ### Set up with xcconfig diff --git a/docs/Guide-Migration-Kits-template.md b/docs/Guide-Migration-Kits-template.md index e1e39b4e..2e789295 100644 --- a/docs/Guide-Migration-Kits-template.md +++ b/docs/Guide-Migration-Kits-template.md @@ -131,6 +131,21 @@ The delegate `-(NSString *)customDeviceIdentifierForUpdateManager:(BITUpdateMana If you are using `PLCrashReporterCallbacks`, you now have to use `BITCrashManagerCallbacks` instead. This `struct` doesn't contain `version` any longer, so you have to remove that. Otherwise everything is the same. +If you did set the delegate per component, e.g. `[[BITHockeyManager sharedHockeyManager].crashManager setDelegate:self]`, you need to remove these and set the delegate this way only: `[[BITHockeyManager sharedHockeyManager] setDelegate:self]`. This will propagate the delegate to all SDK components. Make sure to set it before calling `startManager`! + +In addition you need to make sure all of these frameworks are linked: + +- `AssetsLibrary` +- `CoreText` +- `CoreGraphics` +- `Foundation` +- `MobileCoreServices` +- `QuartzCore` +- `QuickLook` +- `Security` +- `SystemConfiguration` +- `UIKit` + ## Troubleshooting diff --git a/docs/index.md b/docs/index.md index 8ac7068a..d32d91bc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,8 +15,8 @@ The main SDK class is `BITHockeyManager`. It initializes all modules and provide ## Prerequisites 1. Before you integrate HockeySDK into your own app, you should add the app to HockeyApp if you haven't already. Read [this how-to](http://support.hockeyapp.net/kb/how-tos/how-to-create-a-new-app) on how to do it. -2. We also assume that you already have a project in Xcode and that this project is opened in Xcode 4. -3. The SDK supports iOS 5.0 or newer. +2. We also assume that you already have a project in Xcode and that this project is opened in Xcode 5. +3. The SDK supports iOS 6.0 or newer. ## Release Notes