diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 3ef85c4530..902d6dab87 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -219,7 +219,9 @@ typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { This will grab the latest image from the camera roll. Requires iOS 7 or later! It also requires to add a NSPhotoLibraryUsageDescription to your app's Info.plist. - `BITFeedbackObservationModeThreeFingerTap`: Triggers when the user taps on the screen with three fingers. Takes a screenshot and attaches it to the composer. It also requires to add a NSPhotoLibraryUsageDescription to your app's Info.plist. - Default is `BITFeedbackObservationNone` + Default is `BITFeedbackObservationNone`. + If BITFeedbackManger was disabled, setting a new value will be ignored. + @see `[BITHockeyManager disableFeedbackManager]` @see showFeedbackComposeViewWithGeneratedScreenshot */ diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 7dde93471c..5cb6dea24f 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -46,7 +46,7 @@ #import "BITHockeyAppClient.h" #define kBITFeedbackUserDataAsked @"HockeyFeedbackUserDataAsked" -#define kBITFeedbackDateOfLastCheck @"HockeyFeedbackDateOfLastCheck" +#define kBITFeedbackDateOfLastCheck @"HockeyFeedbackDateOfLastCheck" #define kBITFeedbackMessages @"HockeyFeedbackMessages" #define kBITFeedbackToken @"HockeyFeedbackToken" #define kBITFeedbackUserID @"HockeyFeedbackuserID" @@ -59,25 +59,22 @@ NSString *const kBITFeedbackUpdateAttachmentThumbnail = @"BITFeedbackUpdateAttac typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage); -@interface BITFeedbackManager() - -@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; -@property (nonatomic) BOOL screenshotNotificationEnabled; +@interface BITFeedbackManager () @end @implementation BITFeedbackManager { - NSFileManager *_fileManager; - NSString *_settingsFile; - + NSFileManager *_fileManager; + NSString *_settingsFile; + id _appDidBecomeActiveObserver; id _appDidEnterBackgroundObserver; id _networkDidBecomeReachableObserver; - + BOOL _incomingMessagesAlertShowing; BOOL _didEnterBackgroundState; BOOL _networkRequestInProgress; - + BITFeedbackObservationMode _observationMode; } @@ -88,25 +85,25 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage _currentFeedbackListViewController = nil; _currentFeedbackComposeViewController = nil; _didAskUserData = NO; - + _requireUserName = BITFeedbackUserDataElementOptional; _requireUserEmail = BITFeedbackUserDataElementOptional; _showAlertOnIncomingMessages = YES; _showFirstRequiredPresentationModal = YES; - + _disableFeedbackManager = NO; _networkRequestInProgress = NO; _incomingMessagesAlertShowing = NO; _lastCheck = nil; _token = nil; _lastMessageID = nil; - + _feedbackList = [NSMutableArray array]; - + _fileManager = [[NSFileManager alloc] init]; - + _settingsFile = [bit_settingsDir() stringByAppendingPathComponent:BITHOCKEY_FEEDBACK_SETTINGS]; - + _userID = nil; _userName = nil; _userEmail = nil; @@ -121,15 +118,15 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (void)didBecomeActiveActions { if ([self isFeedbackManagerDisabled]) return; if (!_didEnterBackgroundState) return; - + _didEnterBackgroundState = NO; - + if ([_feedbackList count] == 0) { [self loadMessages]; } else { [self updateAppDefinedUserData]; } - + if ([self allowFetchingNewMessages]) { [self updateMessagesList]; } @@ -137,54 +134,55 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (void)didEnterBackgroundActions { _didEnterBackgroundState = NO; - + if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) { _didEnterBackgroundState = YES; } } #pragma mark - Observers -- (void) registerObservers { + +- (void)registerObservers { __weak typeof(self) weakSelf = self; - if(nil == _appDidEnterBackgroundObserver) { + if (nil == _appDidEnterBackgroundObserver) { _appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *note) { - typeof(self) strongSelf = weakSelf; - [strongSelf didEnterBackgroundActions]; + typeof(self) strongSelf = weakSelf; + [strongSelf didEnterBackgroundActions]; }]; } - if(nil == _appDidBecomeActiveObserver) { + if (nil == _appDidBecomeActiveObserver) { _appDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *note) { - typeof(self) strongSelf = weakSelf; - [strongSelf didBecomeActiveActions]; + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; }]; } - if(nil == _networkDidBecomeReachableObserver) { + if (nil == _networkDidBecomeReachableObserver) { _networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *note) { - typeof(self) strongSelf = weakSelf; - [strongSelf didBecomeActiveActions]; + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; }]; } } -- (void) unregisterObservers { - if(_appDidEnterBackgroundObserver) { +- (void)unregisterObservers { + if (_appDidEnterBackgroundObserver) { [[NSNotificationCenter defaultCenter] removeObserver:_appDidEnterBackgroundObserver]; _appDidEnterBackgroundObserver = nil; } - if(_appDidBecomeActiveObserver) { + if (_appDidBecomeActiveObserver) { [[NSNotificationCenter defaultCenter] removeObserver:_appDidBecomeActiveObserver]; _appDidBecomeActiveObserver = nil; } - if(_networkDidBecomeReachableObserver) { + if (_networkDidBecomeReachableObserver) { [[NSNotificationCenter defaultCenter] removeObserver:_networkDidBecomeReachableObserver]; _networkDidBecomeReachableObserver = nil; } @@ -194,9 +192,9 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (NSString *)uuidString { CFUUIDRef theToken = CFUUIDCreate(NULL); - NSString *stringUUID = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, theToken); + NSString * stringUUID = (__bridge_transfer NSString *) CFUUIDCreateString(NULL, theToken); CFRelease(theToken); - + return stringUUID; } @@ -224,14 +222,14 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage return; } dispatch_async(dispatch_get_main_queue(), ^{ - [self showView:[self feedbackListViewController:YES]]; + [self showView:[self feedbackListViewController:YES]]; }); } - (BITFeedbackComposeViewController *)feedbackComposeViewController { BITFeedbackComposeViewController *composeViewController = [[BITFeedbackComposeViewController alloc] init]; - + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" NSArray *preparedItems = self.feedbackComposerPreparedItems ?: [NSArray array]; @@ -239,10 +237,10 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage if ([self.delegate respondsToSelector:@selector(preparedItemsForFeedbackManager:)]) { preparedItems = [preparedItems arrayByAddingObjectsFromArray:[self.delegate preparedItemsForFeedbackManager:self]]; } - + [composeViewController prepareWithItems:preparedItems]; [composeViewController setHideImageAttachmentButton:self.feedbackComposeHideImageAttachmentButton]; - + // by default set the delegate to be identical to the one of BITFeedbackManager [composeViewController setDelegate:self.delegate]; return composeViewController; @@ -252,7 +250,7 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage [self showFeedbackComposeViewWithPreparedItems:nil]; } -- (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items{ +- (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items { if (_currentFeedbackComposeViewController) { BITHockeyLogDebug(@"INFO: Feedback view already visible, aborting"); return; @@ -260,7 +258,7 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage BITFeedbackComposeViewController *composeView = [self feedbackComposeViewController]; [composeView prepareWithItems:items]; dispatch_async(dispatch_get_main_queue(), ^{ - [self showView:composeView]; + [self showView:composeView]; }); } @@ -273,17 +271,17 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (void)startManager { if ([self isFeedbackManagerDisabled]) return; - + [self registerObservers]; - + [self isiOS10PhotoPolicySet]; - + // we are already delayed, so the notification already came in and this won't invoked twice switch ([[UIApplication sharedApplication] applicationState]) { case UIApplicationStateActive: // we did startup, so yes we are coming from background _didEnterBackgroundState = YES; - + [self didBecomeActiveActions]; break; case UIApplicationStateBackground: @@ -297,14 +295,14 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage BOOL fetchNewMessages = YES; if ([[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(allowAutomaticFetchingForNewFeedbackForManager:)]) { fetchNewMessages = [[BITHockeyManager sharedHockeyManager].delegate - allowAutomaticFetchingForNewFeedbackForManager:self]; + allowAutomaticFetchingForNewFeedbackForManager:self]; } return fetchNewMessages; } - (void)updateMessagesList { if (_networkRequestInProgress) return; - + NSArray *pendingMessages = [self messagesWithStatus:BITFeedbackMessageStatusSendPending]; if ([pendingMessages count] > 0) { [self submitPendingMessages]; @@ -322,60 +320,60 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (BOOL)updateUserIDUsingKeychainAndDelegate { BOOL availableViaDelegate = NO; - - NSString *userID = [self stringValueFromKeychainForKey:kBITHockeyMetaUserID]; - + + NSString * userID = [self stringValueFromKeychainForKey:kBITHockeyMetaUserID]; + if ([[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userIDForHockeyManager:componentManager:)]) { userID = [[BITHockeyManager sharedHockeyManager].delegate - userIDForHockeyManager:[BITHockeyManager sharedHockeyManager] - componentManager:self]; + userIDForHockeyManager:[BITHockeyManager sharedHockeyManager] + componentManager:self]; } - + if (userID) { availableViaDelegate = YES; self.userID = userID; } - + return availableViaDelegate; } - (BOOL)updateUserNameUsingKeychainAndDelegate { BOOL availableViaDelegate = NO; - - NSString *userName = [self stringValueFromKeychainForKey:kBITHockeyMetaUserName]; - + + NSString * userName = [self stringValueFromKeychainForKey:kBITHockeyMetaUserName]; + if ([[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; } - (BOOL)updateUserEmailUsingKeychainAndDelegate { BOOL availableViaDelegate = NO; - - NSString *userEmail = [self stringValueFromKeychainForKey:kBITHockeyMetaUserEmail]; - + + NSString * userEmail = [self stringValueFromKeychainForKey:kBITHockeyMetaUserEmail]; + if ([[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userEmailForHockeyManager:componentManager:)]) { userEmail = [[BITHockeyManager sharedHockeyManager].delegate - userEmailForHockeyManager:[BITHockeyManager sharedHockeyManager] - componentManager:self]; + userEmailForHockeyManager:[BITHockeyManager sharedHockeyManager] + componentManager:self]; } - + if (userEmail) { availableViaDelegate = YES; self.userEmail = userEmail; self.requireUserEmail = BITFeedbackUserDataElementDontShow; } - + return availableViaDelegate; } @@ -383,20 +381,20 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage [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) { + self.requireUserEmail == BITFeedbackUserDataElementDontShow) { self.didAskUserData = NO; } } - (BOOL)isiOS10PhotoPolicySet { BOOL isiOS10PhotoPolicySet = [BITHockeyHelper isPhotoAccessPossible]; - if(bit_isDebuggerAttached()) { - if(!isiOS10PhotoPolicySet) { + if (bit_isDebuggerAttached()) { + if (!isiOS10PhotoPolicySet) { BITHockeyLogWarning(@"You are using HockeyApp's Feedback feature in iOS 10 or later. iOS 10 requires you to add the usage strings to your app's info.plist. " - @"Attaching screenshots to feedback is disabled. Please add the String for NSPhotoLibraryUsageDescription to your info.plist to enable screenshot attachments."); + @"Attaching screenshots to feedback is disabled. Please add the String for NSPhotoLibraryUsageDescription to your info.plist to enable screenshot attachments."); } } return isiOS10PhotoPolicySet; @@ -408,22 +406,22 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage BOOL userIDViaDelegate = [self updateUserIDUsingKeychainAndDelegate]; BOOL userNameViaDelegate = [self updateUserNameUsingKeychainAndDelegate]; BOOL userEmailViaDelegate = [self updateUserEmailUsingKeychainAndDelegate]; - + if (![_fileManager fileExistsAtPath:_settingsFile]) return; - + NSData *codedData = [[NSData alloc] initWithContentsOfFile:_settingsFile]; if (codedData == nil) return; - + NSKeyedUnarchiver *unarchiver = nil; - + @try { unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData]; } @catch (NSException *exception) { return; } - + if (!userIDViaDelegate) { if ([unarchiver containsValueForKey:kBITFeedbackUserID]) { self.userID = [unarchiver decodeObjectForKey:kBITFeedbackUserID]; @@ -431,7 +429,7 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage } self.userID = [self stringValueFromKeychainForKey:kBITFeedbackUserID]; } - + if (!userNameViaDelegate) { if ([unarchiver containsValueForKey:kBITFeedbackName]) { self.userName = [unarchiver decodeObjectForKey:kBITFeedbackName]; @@ -439,7 +437,7 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage } self.userName = [self stringValueFromKeychainForKey:kBITFeedbackName]; } - + if (!userEmailViaDelegate) { if ([unarchiver containsValueForKey:kBITFeedbackEmail]) { self.userEmail = [unarchiver decodeObjectForKey:kBITFeedbackEmail]; @@ -447,7 +445,7 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage } self.userEmail = [self stringValueFromKeychainForKey:kBITFeedbackEmail]; } - + if ([unarchiver containsValueForKey:kBITFeedbackUserDataAsked]) _didAskUserData = YES; @@ -456,10 +454,10 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage [self addStringValueToKeychain:self.token forKey:kBITFeedbackToken]; } self.token = [self stringValueFromKeychainForKey:kBITFeedbackToken]; - + if ([unarchiver containsValueForKey:kBITFeedbackAppID]) { - NSString *appID = [unarchiver decodeObjectForKey: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 @@ -474,21 +472,21 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage if ([unarchiver containsValueForKey:kBITFeedbackDateOfLastCheck]) self.lastCheck = [unarchiver decodeObjectForKey:kBITFeedbackDateOfLastCheck]; - + if ([unarchiver containsValueForKey:kBITFeedbackLastMessageID]) self.lastMessageID = [unarchiver decodeObjectForKey:kBITFeedbackLastMessageID]; - + if ([unarchiver containsValueForKey:kBITFeedbackMessages]) { [self.feedbackList setArray:[unarchiver decodeObjectForKey:kBITFeedbackMessages]]; - + [self sortFeedbackList]; - + // 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]; } @@ -497,36 +495,36 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (void)saveMessages { [self sortFeedbackList]; - + NSMutableData *data = [[NSMutableData alloc] init]; NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; - + if (_didAskUserData) [archiver encodeObject:[NSNumber numberWithBool:YES] forKey:kBITFeedbackUserDataAsked]; - + if (self.token) [self addStringValueToKeychain:self.token forKey:kBITFeedbackToken]; - + if (self.appIdentifier) [archiver encodeObject:self.appIdentifier forKey:kBITFeedbackAppID]; - + if (self.userID) [self addStringValueToKeychain:self.userID forKey:kBITFeedbackUserID]; - + if (self.userName) [self addStringValueToKeychain:self.userName forKey:kBITFeedbackName]; - + if (self.userEmail) [self addStringValueToKeychain:self.userEmail forKey:kBITFeedbackEmail]; - + if (self.lastCheck) [archiver encodeObject:self.lastCheck forKey:kBITFeedbackDateOfLastCheck]; - + if (self.lastMessageID) [archiver encodeObject:self.lastMessageID forKey:kBITFeedbackLastMessageID]; - + [archiver encodeObject:self.feedbackList forKey:kBITFeedbackMessages]; - + [archiver finishEncoding]; [data writeToFile:_settingsFile atomically:YES]; } @@ -535,7 +533,7 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (void)updateDidAskUserData { if (!_didAskUserData) { _didAskUserData = YES; - + [self saveMessages]; } } @@ -544,24 +542,24 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (void)sortFeedbackList { [_feedbackList sortUsingComparator:^(BITFeedbackMessage *obj1, BITFeedbackMessage *obj2) { - NSDate *date1 = [obj1 date]; - NSDate *date2 = [obj2 date]; - - // not send, in conflict and send in progress messages on top, sorted by date - // read and unread on bottom, sorted by date - // archived on the very bottom - - if ([obj1 status] >= BITFeedbackMessageStatusSendInProgress && [obj2 status] < BITFeedbackMessageStatusSendInProgress) { - return NSOrderedDescending; - } else if ([obj1 status] < BITFeedbackMessageStatusSendInProgress && [obj2 status] >= BITFeedbackMessageStatusSendInProgress) { - return NSOrderedAscending; - } else if ([obj1 status] == BITFeedbackMessageStatusArchived && [obj2 status] < BITFeedbackMessageStatusArchived) { - return NSOrderedDescending; - } else if ([obj1 status] < BITFeedbackMessageStatusArchived && [obj2 status] == BITFeedbackMessageStatusArchived) { - return NSOrderedAscending; - } else { - return (NSInteger)[date2 compare:date1]; - } + NSDate *date1 = [obj1 date]; + NSDate *date2 = [obj2 date]; + + // not send, in conflict and send in progress messages on top, sorted by date + // read and unread on bottom, sorted by date + // archived on the very bottom + + if ([obj1 status] >= BITFeedbackMessageStatusSendInProgress && [obj2 status] < BITFeedbackMessageStatusSendInProgress) { + return NSOrderedDescending; + } else if ([obj1 status] < BITFeedbackMessageStatusSendInProgress && [obj2 status] >= BITFeedbackMessageStatusSendInProgress) { + return NSOrderedAscending; + } else if ([obj1 status] == BITFeedbackMessageStatusArchived && [obj2 status] < BITFeedbackMessageStatusArchived) { + return NSOrderedDescending; + } else if ([obj1 status] < BITFeedbackMessageStatusArchived && [obj2 status] == BITFeedbackMessageStatusArchived) { + return NSOrderedAscending; + } else { + return (NSInteger) [date2 compare:date1]; + } }]; } @@ -573,64 +571,64 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage if ([_feedbackList count] > index) { return [_feedbackList objectAtIndex:index]; } - + return nil; } - (BITFeedbackMessage *)messageWithID:(NSNumber *)messageID { __block BITFeedbackMessage *message = nil; - + [_feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([[objMessage identifier] isEqualToNumber:messageID]) { - message = objMessage; - *stop = YES; - } + if ([[objMessage identifier] isEqualToNumber:messageID]) { + message = objMessage; + *stop = YES; + } }]; - + return message; } - (NSArray *)messagesWithStatus:(BITFeedbackMessageStatus)status { NSMutableArray *resultMessages = [[NSMutableArray alloc] initWithCapacity:[_feedbackList count]]; - + [_feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([objMessage status] == status) { - [resultMessages addObject: objMessage]; - } + if ([objMessage status] == status) { + [resultMessages addObject:objMessage]; + } }]; - + return [NSArray arrayWithArray:resultMessages];; } - (BITFeedbackMessage *)lastMessageHavingID { __block BITFeedbackMessage *message = nil; - - + + // Note: the logic here is slightly different than in our mac SDK, as _feedbackList is sorted in different order. // Compare the implementation of - (void)sortFeedbackList; in both SDKs. [_feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([[objMessage identifier] integerValue] != 0) { - message = objMessage; - *stop = YES; - } + if ([[objMessage identifier] integerValue] != 0) { + message = objMessage; + *stop = YES; + } }]; - + return message; } - (void)markSendInProgressMessagesAsPending { // make sure message that may have not been send successfully, get back into the right state to be send again [_feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([(BITFeedbackMessage *)objMessage status] == BITFeedbackMessageStatusSendInProgress) - [(BITFeedbackMessage *)objMessage setStatus:BITFeedbackMessageStatusSendPending]; + if ([(BITFeedbackMessage *) objMessage status] == BITFeedbackMessageStatusSendInProgress) + [(BITFeedbackMessage *) objMessage setStatus:BITFeedbackMessageStatusSendPending]; }]; } - (void)markSendInProgressMessagesAsInConflict { // make sure message that may have not been send successfully, get back into the right state to be send again [_feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([(BITFeedbackMessage *)objMessage status] == BITFeedbackMessageStatusSendInProgress) - [(BITFeedbackMessage *)objMessage setStatus:BITFeedbackMessageStatusInConflict]; + if ([(BITFeedbackMessage *) objMessage status] == BITFeedbackMessageStatusSendInProgress) + [(BITFeedbackMessage *) objMessage setStatus:BITFeedbackMessageStatusInConflict]; }]; } @@ -647,25 +645,24 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage BITFeedbackMessage *message = _feedbackList[index]; [message deleteContents]; [_feedbackList removeObjectAtIndex:index]; - + [self saveMessages]; return YES; } - + return NO; } - (void)deleteAllMessages { [_feedbackList removeAllObjects]; - + [self saveMessages]; } - (BOOL)shouldForceNewThread { if (self.delegate && [self.delegate respondsToSelector:@selector(forceNewFeedbackThreadForFeedbackManager:)]) { return [self.delegate forceNewFeedbackThreadForFeedbackManager:self]; - } - else { + } else { return NO; } } @@ -675,33 +672,33 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (BOOL)askManualUserDataAvailable { [self updateAppDefinedUserData]; - + if (self.requireUserName == BITFeedbackUserDataElementDontShow && - self.requireUserEmail == BITFeedbackUserDataElementDontShow) + self.requireUserEmail == BITFeedbackUserDataElementDontShow) return NO; - + return YES; } - (BOOL)requireManualUserDataMissing { [self updateAppDefinedUserData]; - + if (self.requireUserName == BITFeedbackUserDataElementRequired && !self.userName) return YES; - + if (self.requireUserEmail == BITFeedbackUserDataElementRequired && !self.userEmail) return YES; - + return NO; } - (BOOL)isManualUserDataAvailable { [self updateAppDefinedUserData]; - + if ((self.requireUserName != BITFeedbackUserDataElementDontShow && self.userName) || - (self.requireUserEmail != BITFeedbackUserDataElementDontShow && self.userEmail)) + (self.requireUserEmail != BITFeedbackUserDataElementDontShow && self.userEmail)) return YES; - + return NO; } @@ -711,241 +708,241 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage - (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 ([(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 if (pendingMessagesCount > pendingMessagesCountAfterProcessing && pendingMessagesCountAfterProcessing > 0) { [self performSelector:@selector(submitPendingMessages) withObject:nil afterDelay:0.1]; } - + return; } - - NSDictionary *feedback = [jsonDictionary objectForKey:@"feedback"]; - NSString *token = [jsonDictionary objectForKey:@"token"]; - NSDictionary *feedbackObject = [jsonDictionary objectForKey:@"feedback"]; + + NSDictionary * feedback = [jsonDictionary objectForKey:@"feedback"]; + NSString * token = [jsonDictionary objectForKey:@"token"]; + NSDictionary * feedbackObject = [jsonDictionary objectForKey:@"feedback"]; if (feedback && token && feedbackObject) { if ([self shouldForceNewThread]) { - self.token = nil; + self.token = nil; } else { // update the thread token, which is not available until the 1st message was successfully sent self.token = token; } - + self.lastCheck = [NSDate date]; - + // add all new messages NSArray *feedMessages = [feedbackObject objectForKey:@"messages"]; - + // get the message that was currently sent if available NSArray *messagesSendInProgress = [self messagesWithStatus:BITFeedbackMessageStatusSendInProgress]; - + NSInteger pendingMessagesCount = [messagesSendInProgress count] + [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; - + __block BOOL newMessage = NO; NSMutableSet *returnedMessageIDs = [[NSMutableSet alloc] init]; - + [feedMessages enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([(NSDictionary *)objMessage objectForKey:@"id"]) { - NSNumber *messageID = [(NSDictionary *)objMessage objectForKey:@"id"]; - [returnedMessageIDs addObject:messageID]; - - BITFeedbackMessage *thisMessage = [self messageWithID:messageID]; - if (!thisMessage) { - // check if this is a message that was sent right now - __block BITFeedbackMessage *matchingSendInProgressOrInConflictMessage = nil; - - // TODO: match messages in state conflict - - [messagesSendInProgress enumerateObjectsUsingBlock:^(id objSendInProgressMessage, NSUInteger messagesSendInProgressIdx, BOOL *stop2) { - if ([[(NSDictionary *)objMessage objectForKey:@"token"] isEqualToString:[(BITFeedbackMessage *)objSendInProgressMessage token]]) { - matchingSendInProgressOrInConflictMessage = objSendInProgressMessage; - *stop2 = YES; - } - }]; - - if (matchingSendInProgressOrInConflictMessage) { - matchingSendInProgressOrInConflictMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]]; - 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.identifier =feedbackAttachments[attachmentIndex][@"id"]; - attachmentIndex++; + if ([(NSDictionary *) objMessage objectForKey:@"id"]) { + NSNumber *messageID = [(NSDictionary *) objMessage objectForKey:@"id"]; + [returnedMessageIDs addObject:messageID]; + + BITFeedbackMessage *thisMessage = [self messageWithID:messageID]; + if (!thisMessage) { + // check if this is a message that was sent right now + __block BITFeedbackMessage *matchingSendInProgressOrInConflictMessage = nil; + + // TODO: match messages in state conflict + + [messagesSendInProgress enumerateObjectsUsingBlock:^(id objSendInProgressMessage, NSUInteger messagesSendInProgressIdx, BOOL *stop2) { + if ([[(NSDictionary *) objMessage objectForKey:@"token"] isEqualToString:[(BITFeedbackMessage *) objSendInProgressMessage token]]) { + matchingSendInProgressOrInConflictMessage = objSendInProgressMessage; + *stop2 = YES; + } + }]; + + if (matchingSendInProgressOrInConflictMessage) { + matchingSendInProgressOrInConflictMessage.date = [self parseRFC3339Date:[(NSDictionary *) objMessage objectForKey:@"created_at"]]; + 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.identifier = feedbackAttachments[attachmentIndex][@"id"]; + attachmentIndex++; + } + } + } else { + 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"] ?: @""; + message.email = [(NSDictionary *) objMessage objectForKey:@"email"] ?: @""; + + message.date = [self parseRFC3339Date:[(NSDictionary *) objMessage objectForKey:@"created_at"]] ?: [NSDate date]; + 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.identifier = attachmentData[@"id"]; + newAttachment.sourceURL = attachmentData[@"url"]; + newAttachment.contentType = attachmentData[@"content_type"]; + [message addAttachmentsObject:newAttachment]; + } + + [_feedbackList addObject:message]; + + newMessage = YES; } } } else { - 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"] ?: @""; - message.email = [(NSDictionary *)objMessage objectForKey:@"email"] ?: @""; - - message.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]] ?: [NSDate date]; - 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.identifier = attachmentData[@"id"]; - newAttachment.sourceURL = attachmentData[@"url"]; - newAttachment.contentType = attachmentData[@"content_type"]; - [message addAttachmentsObject:newAttachment]; - } - - [_feedbackList addObject:message]; - - newMessage = YES; - } + // we should never get any messages back that are already stored locally, + // since we add the last_message_id to the request } - } else { - // we should never get any messages back that are already stored locally, - // since we add the last_message_id to the request } - } }]; - + [self markSendInProgressMessagesAsPending]; - + [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 BOOL latestMessageFromUser = NO; - + 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:)]) { + if ([self.delegate respondsToSelector:@selector(feedbackManagerDidReceiveNewFeedback:)]) { [self.delegate feedbackManagerDidReceiveNewFeedback:self]; } - - if(self.showAlertOnIncomingMessages && !self.currentFeedbackListViewController && !self.currentFeedbackComposeViewController) { + + if (self.showAlertOnIncomingMessages && !self.currentFeedbackListViewController && !self.currentFeedbackComposeViewController) { dispatch_async(dispatch_get_main_queue(), ^{ - /* - // Requires iOS 8 - id uialertcontrollerClass = NSClassFromString(@"UIAlertController"); - if (uialertcontrollerClass) { - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageTitle") - message:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageText") - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackIgnore") - style:UIAlertActionStyleCancel - handler:nil]; - UIAlertAction *showAction = [UIAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackShow") - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *__nonnull action) { - [self showFeedbackListView]; - }]; - [alertController addAction:cancelAction]; - [alertController addAction:showAction]; - - [self showAlertController:alertController]; - } else { - */ + /* + // Requires iOS 8 + id uialertcontrollerClass = NSClassFromString(@"UIAlertController"); + if (uialertcontrollerClass) { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageTitle") + message:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageText") + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackIgnore") + style:UIAlertActionStyleCancel + handler:nil]; + UIAlertAction *showAction = [UIAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackShow") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *__nonnull action) { + [self showFeedbackListView]; + }]; + [alertController addAction:cancelAction]; + [alertController addAction:showAction]; + + [self showAlertController:alertController]; + } else { + */ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageTitle") - message:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageText") - delegate:self - cancelButtonTitle:BITHockeyLocalizedString(@"HockeyFeedbackIgnore") - otherButtonTitles:BITHockeyLocalizedString(@"HockeyFeedbackShow"), nil - ]; - [alertView setTag:0]; - [alertView show]; + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageTitle") + message:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageText") + delegate:self + cancelButtonTitle:BITHockeyLocalizedString(@"HockeyFeedbackIgnore") + otherButtonTitles:BITHockeyLocalizedString(@"HockeyFeedbackShow"), nil + ]; + [alertView setTag:0]; + [alertView show]; #pragma clang diagnostic pop - /*}*/ - _incomingMessagesAlertShowing = YES; + /*}*/ + _incomingMessagesAlertShowing = YES; }); } } } - + 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]; } - + } else { [self markSendInProgressMessagesAsPending]; } - + [self saveMessages]; - + return; } - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BITFeedbackMessage *)message completionHandler:(void (^)(NSError *error))completionHandler { - NSString *boundary = @"----FOO"; - + 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 = @""; + + NSString * tokenParameter = @""; if ([self token]) { tokenParameter = [NSString stringWithFormat:@"/%@", [self token]]; } NSMutableString *parameter = [NSMutableString stringWithFormat:@"api/2/apps/%@/feedback%@", [self encodedAppIdentifier], tokenParameter]; - - NSString *lastMessageID = @""; + + NSString * lastMessageID = @""; if (!self.lastMessageID) { [self updateLastMessageID]; } if (self.lastMessageID) { - lastMessageID = [NSString stringWithFormat:@"&last_message_id=%li", (long)[self.lastMessageID integerValue]]; + lastMessageID = [NSString stringWithFormat:@"&last_message_id=%li", (long) [self.lastMessageID integerValue]]; } - + [parameter appendFormat:@"?format=json&bundle_version=%@&sdk=%@&sdk_version=%@%@", - bit_URLEncodedString([[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]), - BITHOCKEY_NAME, - BITHOCKEY_VERSION, - lastMessageID - ]; - + bit_URLEncodedString([[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]), + BITHOCKEY_NAME, + BITHOCKEY_VERSION, + lastMessageID + ]; + // build request & send - NSString *url = [NSString stringWithFormat:@"%@%@", self.serverURL, parameter]; + NSString * url = [NSString stringWithFormat:@"%@%@", self.serverURL, parameter]; BITHockeyLogDebug(@"INFO: sending api request to %@", url); - + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:1 timeoutInterval:10.0]; [request setHTTPMethod:httpMethod]; [request setValue:@"Hockey/iOS" forHTTPHeaderField:@"User-Agent"]; [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; - + if (message) { - NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; + NSString * contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; [request setValue:contentType forHTTPHeaderField:@"Content-type"]; - + NSMutableData *postBody = [NSMutableData data]; - + [postBody appendData:[BITHockeyAppClient dataWithPostValue:@"Apple" forKey:@"oem" boundary:boundary]]; [postBody appendData:[BITHockeyAppClient dataWithPostValue:[[UIDevice currentDevice] systemVersion] forKey:@"os_version" boundary:boundary]]; [postBody appendData:[BITHockeyAppClient dataWithPostValue:[self getDevicePlatform] forKey:@"model" boundary:boundary]]; @@ -953,12 +950,12 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage [postBody appendData:[BITHockeyAppClient dataWithPostValue:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] forKey:@"bundle_version" boundary:boundary]]; [postBody appendData:[BITHockeyAppClient dataWithPostValue:[message text] forKey:@"text" boundary:boundary]]; [postBody appendData:[BITHockeyAppClient dataWithPostValue:[message token] forKey:@"message_token" boundary:boundary]]; - - NSString *installString = bit_appAnonID(NO); + + NSString * installString = bit_appAnonID(NO); if (installString) { [postBody appendData:[BITHockeyAppClient dataWithPostValue:installString forKey:@"install_string" boundary:boundary]]; } - + if (self.userID) { [postBody appendData:[BITHockeyAppClient dataWithPostValue:self.userID forKey:@"user_string" boundary:boundary]]; } @@ -968,59 +965,59 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage if (self.userEmail) { [postBody appendData:[BITHockeyAppClient dataWithPostValue:self.userEmail forKey:@"email" boundary:boundary]]; } - - + + NSInteger photoIndex = 0; - - for (BITFeedbackMessageAttachment *attachment in message.attachments){ - NSString *key = [NSString stringWithFormat:@"attachment%ld", (long)photoIndex]; - - NSString *filename = attachment.originalFilename; - + + for (BITFeedbackMessageAttachment *attachment in message.attachments) { + NSString * key = [NSString stringWithFormat:@"attachment%ld", (long) photoIndex]; + + NSString * filename = attachment.originalFilename; + if (!filename) { - filename = [NSString stringWithFormat:@"Attachment %ld", (long)photoIndex]; + filename = [NSString stringWithFormat:@"Attachment %ld", (long) photoIndex]; } - + [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.data forKey:key contentType:attachment.contentType boundary:boundary filename:filename]]; - + photoIndex++; } - + [postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - - + + [request setHTTPBody:postBody]; } - __weak typeof (self) weakSelf = self; + __weak typeof(self) weakSelf = self; if ([BITHockeyHelper isURLSessionSupported]) { NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; - + NSURLSessionDataTask *task = [session dataTaskWithRequest:request - completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { - typeof (self) strongSelf = weakSelf; - - [session finishTasksAndInvalidate]; - - [strongSelf handleFeedbackMessageResponse:response data:data error:error completion:completionHandler]; + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + typeof(self) strongSelf = weakSelf; + + [session finishTasksAndInvalidate]; + + [strongSelf handleFeedbackMessageResponse:response data:data error:error completion:completionHandler]; }]; [task resume]; - + } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *error) { #pragma clang diagnostic pop - typeof (self) strongSelf = weakSelf; - [strongSelf handleFeedbackMessageResponse:response data:responseData error:error completion:completionHandler]; + typeof(self) strongSelf = weakSelf; + [strongSelf handleFeedbackMessageResponse:response data:responseData error:error completion:completionHandler]; }]; } - + } -- (void)handleFeedbackMessageResponse:(NSURLResponse *)response data:(NSData *)responseData error:(NSError * )error completion:(void (^)(NSError *error))completionHandler{ +- (void)handleFeedbackMessageResponse:(NSURLResponse *)response data:(NSData *)responseData error:(NSError *)error completion:(void (^)(NSError *error))completionHandler { _networkRequestInProgress = NO; - + if (error) { [self reportError:error]; [self markSendInProgressMessagesAsPending]; @@ -1028,41 +1025,41 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage completionHandler(error); } } else { - NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; + NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode]; if (statusCode == 404) { // thread has been deleted, we archive it [self updateMessageListFromResponse:nil]; } else if (statusCode == 409) { // we submitted a message that is already on the server, mark it as being in conflict and resolve it with another fetch - + 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 ([(BITFeedbackMessage *) objMessage status] == BITFeedbackMessageStatusSendInProgress) { + token = [(BITFeedbackMessage *) objMessage token]; + *stop = YES; + } }]; - + if (token) { self.token = token; } } - + [self markSendInProgressMessagesAsInConflict]; [self saveMessages]; [self performSelector:@selector(fetchMessageUpdates) withObject:nil afterDelay:0.2]; } else if ([responseData length]) { - NSString *responseString = [[NSString alloc] initWithBytes:[responseData bytes] length:[responseData length] encoding: NSUTF8StringEncoding]; + NSString * responseString = [[NSString alloc] initWithBytes:[responseData bytes] length:[responseData length] encoding:NSUTF8StringEncoding]; BITHockeyLogDebug(@"INFO: Received API response: %@", responseString); - + if (responseString && [responseString dataUsingEncoding:NSUTF8StringEncoding]) { NSError *error = NULL; - - NSDictionary *feedDict = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error]; - + + NSDictionary * feedDict = (NSDictionary *) [NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error]; + // server returned empty response? if (error) { [self reportError:error]; @@ -1072,7 +1069,7 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Server returned empty response.", NSLocalizedDescriptionKey, nil]]]; } else { BITHockeyLogDebug(@"INFO: Received API response: %@", responseString); - NSString *status = [feedDict objectForKey:@"status"]; + NSString * status = [feedDict objectForKey:@"status"]; if ([status compare:@"success"] != NSOrderedSame) { [self reportError:[NSError errorWithDomain:kBITFeedbackErrorDomain code:BITFeedbackAPIServerReturnedInvalidStatus @@ -1083,7 +1080,7 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage } } } - + [self markSendInProgressMessagesAsPending]; if (completionHandler) { completionHandler(error); @@ -1095,15 +1092,15 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage if ([_feedbackList count] == 0 && !self.token) { // inform the UI to update its data in case the list is already showing [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; - + return; } - + [self sendNetworkRequestWithHTTPMethod:@"GET" withMessage:nil - completionHandler:^(NSError *error){ - // inform the UI to update its data in case the list is already showing - [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; + completionHandler:^(NSError *error) { + // inform the UI to update its data in case the list is already showing + [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; }]; } @@ -1113,17 +1110,17 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage [self performSelector:@selector(submitPendingMessages) withObject:nil afterDelay:2.0f]; return; } - + // app defined user data may have changed, update it [self updateAppDefinedUserData]; [self saveMessages]; - + NSArray *pendingMessages = [self messagesWithStatus:BITFeedbackMessageStatusSendPending]; - + if ([pendingMessages count] > 0) { // we send one message at a time BITFeedbackMessage *messageToSend = pendingMessages[0]; - + [messageToSend setStatus:BITFeedbackMessageStatusSendInProgress]; if (self.userID) [messageToSend setUserID:self.userID]; @@ -1131,22 +1128,22 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage [messageToSend setName:self.userName]; if (self.userEmail) [messageToSend setEmail:self.userEmail]; - - NSString *httpMethod = @"POST"; + + NSString * httpMethod = @"POST"; if ([self token]) { httpMethod = @"PUT"; } - + [self sendNetworkRequestWithHTTPMethod:httpMethod withMessage:messageToSend - completionHandler:^(NSError *error){ - if (error) { - [self markSendInProgressMessagesAsPending]; - [self saveMessages]; - } - - // inform the UI to update its data in case the list is already showing - [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; + completionHandler:^(NSError *error) { + if (error) { + [self markSendInProgressMessagesAsPending]; + [self saveMessages]; + } + + // inform the UI to update its data in case the list is already showing + [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; }]; } } @@ -1158,9 +1155,9 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage [message setToken:[self uuidAsLowerCaseAndShortened]]; [message setAttachments:attachments]; [message setUserMessage:YES]; - + [_feedbackList addObject:message]; - + [self submitPendingMessages]; } @@ -1169,85 +1166,128 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" + // invoke the selected action from the action sheet for a location element - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { - + _incomingMessagesAlertShowing = NO; if (buttonIndex == [alertView firstOtherButtonIndex]) { // Show button has been clicked [self showFeedbackListView]; } } + #pragma clang diagnostic pop #pragma mark - Observation Handling - (void)setFeedbackObservationMode:(BITFeedbackObservationMode)feedbackObservationMode { + //Ignore if feedback manager is disabled + if ([self isFeedbackManagerDisabled]) return; + if (feedbackObservationMode != _feedbackObservationMode) { _feedbackObservationMode = feedbackObservationMode; - - if (feedbackObservationMode == BITFeedbackObservationModeOnScreenshot){ - if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; - } else { - BITHockeyLogWarning(@"WARNING: BITFeedbackObservationModeOnScreenshot requires iOS 7 or later."); + + // Reset the other observation modes. + if (feedbackObservationMode == BITFeedbackObservationNone) { + if (self.observationModeOnScreenshotEnabled) { + [self setObservationModeOnScreenshotEnabled:NO]; } - - self.screenshotNotificationEnabled = YES; - - if (self.tapRecognizer){ - [[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer]; - self.tapRecognizer = nil; + if (self.observationModeThreeFingerTapEnabled) { + [self setObservationModeThreeFingerTapEnabled:NO]; + } + BITHockeyLogVerbose(@"Set feedbackObservationMode to BITFeedbackObservationNone"); + } + + if (feedbackObservationMode == BITFeedbackObservationModeOnScreenshot) { + [self setObservationModeOnScreenshotEnabled:YES]; + if(self.observationModeThreeFingerTapEnabled) { + [self setObservationModeThreeFingerTapEnabled: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(), ^{ - if (self.tapRecognizer) { - [[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; - } + + if (feedbackObservationMode == BITFeedbackObservationModeThreeFingerTap) { + [self setObservationModeThreeFingerTapEnabled:YES]; + if (self.observationModeOnScreenshotEnabled) { + [self setObservationModeOnScreenshotEnabled:NO]; } } } } --(void)screenshotNotificationReceived:(NSNotification *)notification { +- (void)setObservationModeOnScreenshotEnabled:(BOOL)observationModeOnScreenshotEnabled { + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) { + // Enable/disable screenshot notification + if (observationModeOnScreenshotEnabled) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + BITHockeyLogVerbose(@"Added observer for UIApplocationUserDidTakeScreenshotNotification."); + } else { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + BITHockeyLogVerbose(@"Removed observer for UIApplocationUserDidTakeScreenshotNotification."); + } + } else { + if (observationModeOnScreenshotEnabled) { + BITHockeyLogWarning(@"WARNING: BITFeedbackObservationModeOnScreenshot requires iOS 7 or later."); + } + } + + _observationModeOnScreenshotEnabled = observationModeOnScreenshotEnabled; + + BITHockeyLogVerbose(@"Enabled BITFeedbackObservationModeOnScreenshot."); +} + +- (void)setObservationModeThreeFingerTapEnabled:(BOOL)observationModeThreeFingerTapEnabled { + _observationModeThreeFingerTapEnabled = observationModeThreeFingerTapEnabled; + + if(observationModeThreeFingerTapEnabled) { + 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(), ^{ + if (self.tapRecognizer) { + [[UIApplication sharedApplication].keyWindow addGestureRecognizer:self.tapRecognizer]; + } + }); + } + + BITHockeyLogVerbose(@"Enabled BITFeedbackObservationModeThreeFingerTap."); + } + else { + [[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer]; + self.tapRecognizer = nil; + BITHockeyLogVerbose(@"Disabled BITFeedbackObservationModeThreeFingerTap."); + } +} + +- (void)screenshotNotificationReceived:(NSNotification *)notification { + // Don't do anything if FeedbackManager was disabled. + if ([self isFeedbackManagerDisabled]) return; + double amountOfSeconds = 1.5; - dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(amountOfSeconds * NSEC_PER_SEC)); - + dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (amountOfSeconds * NSEC_PER_SEC)); + dispatch_after(delayTime, dispatch_get_main_queue(), ^{ - [self extractLastPictureFromLibraryAndLaunchFeedback]; + [self extractLastPictureFromLibraryAndLaunchFeedback]; }); } --(void)extractLastPictureFromLibraryAndLaunchFeedback { +- (void)extractLastPictureFromLibraryAndLaunchFeedback { [self requestLatestImageWithCompletionHandler:^(UIImage *latestImage) { - [self showFeedbackComposeViewWithPreparedItems:@[latestImage]]; + [self showFeedbackComposeViewWithPreparedItems:@[latestImage]]; }]; } - (void)requestLatestImageWithCompletionHandler:(BITLatestImageFetchCompletionBlock)completionHandler { - if (!completionHandler) { return; } - + if (!completionHandler) {return;} + // Safeguard in case the dev hasn't set the NSPhotoLibraryUsageDescription in their Info.plist - if(![self isiOS10PhotoPolicySet]) { return; } - + if (![self isiOS10PhotoPolicySet]) {return;} + // Only available from iOS 8 up - id phimagemanagerClass = NSClassFromString(@"PHImageManager"); - if (phimagemanagerClass) { + id phImageManagerClass = NSClassFromString(@"PHImageManager"); + if (phImageManagerClass) { [self fetchLatestImageUsingPhotoLibraryWithCompletionHandler:completionHandler]; } else { [self fetchLatestImageUsingAssetsLibraryWithCompletionHandler:completionHandler]; @@ -1259,72 +1299,72 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage #pragma clang diagnostic ignored "-Wdeprecated-declarations" 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]]; - - completionHandler(latestPhoto); - - *stop = YES; - *innerStop = YES; - } - }]; - } failureBlock: nil]; + + [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]]; + + completionHandler(latestPhoto); + + *stop = YES; + *innerStop = YES; + } + }]; + } failureBlock:nil]; #pragma clang diagnostic pop } -- (void)fetchLatestImageUsingPhotoLibraryWithCompletionHandler:(BITLatestImageFetchCompletionBlock)completionHandler NS_AVAILABLE_IOS(8_0){ +- (void)fetchLatestImageUsingPhotoLibraryWithCompletionHandler:(BITLatestImageFetchCompletionBlock)completionHandler NS_AVAILABLE_IOS(8_0) { // Safeguard in case the dev hasn't set the NSPhotoLibraryUsageDescription in their Info.plist - if(![self isiOS10PhotoPolicySet]) { return; } - + if (![self isiOS10PhotoPolicySet]) {return;} + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - switch (status) { - case PHAuthorizationStatusDenied: - case PHAuthorizationStatusRestricted: - BITHockeyLogDebug(@"INFO: The latest image could not be fetched, no permissions."); - break; - - case PHAuthorizationStatusAuthorized: - [self loadLatestImageAssetWithCompletionHandler:completionHandler]; - break; - case PHAuthorizationStatusNotDetermined: - BITHockeyLogDebug(@"INFO: The Photo Library authorization status is undetermined. This should not happen."); - break; - } + switch (status) { + case PHAuthorizationStatusDenied: + case PHAuthorizationStatusRestricted: + BITHockeyLogDebug(@"INFO: The latest image could not be fetched, no permissions."); + break; + + case PHAuthorizationStatusAuthorized: + [self loadLatestImageAssetWithCompletionHandler:completionHandler]; + break; + case PHAuthorizationStatusNotDetermined: + BITHockeyLogDebug(@"INFO: The Photo Library authorization status is undetermined. This should not happen."); + break; + } }]; } -- (void)loadLatestImageAssetWithCompletionHandler:(BITLatestImageFetchCompletionBlock)completionHandler NS_AVAILABLE_IOS(8_0){ - +- (void)loadLatestImageAssetWithCompletionHandler:(BITLatestImageFetchCompletionBlock)completionHandler NS_AVAILABLE_IOS(8_0) { + // Safeguard in case the dev hasn't set the NSPhotoLibraryUsageDescription in their Info.plist - if(![self isiOS10PhotoPolicySet]) { return; } - + if (![self isiOS10PhotoPolicySet]) {return;} + PHImageManager *imageManager = PHImageManager.defaultManager; - + PHFetchOptions *fetchOptions = [PHFetchOptions new]; fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]]; - + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:fetchOptions]; - + if (fetchResult.count > 0) { - PHAsset *latestImageAsset = (PHAsset *)fetchResult.lastObject; + PHAsset *latestImageAsset = (PHAsset *) fetchResult.lastObject; if (latestImageAsset) { PHImageRequestOptions *options = [PHImageRequestOptions new]; options.version = PHImageRequestOptionsVersionOriginal; options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; options.resizeMode = PHImageRequestOptionsResizeModeNone; - - [imageManager requestImageDataForAsset:latestImageAsset options:options resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) { - if (imageData) { - completionHandler([UIImage imageWithData:imageData]); - } else { - BITHockeyLogDebug(@"INFO: The latest image could not be fetched, requested image data was empty."); - } + + [imageManager requestImageDataForAsset:latestImageAsset options:options resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, UIImageOrientation orientation, NSDictionary *_Nullable info) { + if (imageData) { + completionHandler([UIImage imageWithData:imageData]); + } else { + BITHockeyLogDebug(@"INFO: The latest image could not be fetched, requested image data was empty."); + } }]; } } else { @@ -1333,7 +1373,10 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage } - (void)screenshotTripleTap:(UITapGestureRecognizer *)tapRecognizer { - if (tapRecognizer.state == UIGestureRecognizerStateRecognized){ + // Don't do anything if FeedbackManager was disabled. + if ([self isFeedbackManagerDisabled]) return; + + if (tapRecognizer.state == UIGestureRecognizerStateRecognized) { [self showFeedbackComposeViewWithGeneratedScreenshot]; } } @@ -1345,5 +1388,4 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage @end - #endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/Classes/BITFeedbackManagerPrivate.h b/Classes/BITFeedbackManagerPrivate.h index 8782a7460a..e89c235792 100644 --- a/Classes/BITFeedbackManagerPrivate.h +++ b/Classes/BITFeedbackManagerPrivate.h @@ -34,6 +34,8 @@ extern NSString *const kBITFeedbackUpdateAttachmentThumbnail; #import "BITFeedbackMessage.h" +@class UITapGestureRecognizer; + @interface BITFeedbackManager () { } @@ -61,6 +63,11 @@ extern NSString *const kBITFeedbackUpdateAttachmentThumbnail; // used by BITHockeyManager if disable status is changed @property (nonatomic, getter = isFeedbackManagerDisabled) BOOL disableFeedbackManager; +// TapRecognizer used in case feedback observation mode is BITFeedbackObservationModeThreeFingerTap is set. +@property(nonatomic, strong) UITapGestureRecognizer *tapRecognizer; +@property(nonatomic) BOOL observationModeOnScreenshotEnabled; +@property(nonatomic) BOOL observationModeThreeFingerTapEnabled; + @property (nonatomic, strong) BITFeedbackListViewController *currentFeedbackListViewController; @property (nonatomic, strong) BITFeedbackComposeViewController *currentFeedbackComposeViewController; @@ -75,6 +82,7 @@ extern NSString *const kBITFeedbackUpdateAttachmentThumbnail; @property (nonatomic, copy) NSString *userEmail; + // Fetch user meta data - (BOOL)updateUserIDUsingKeychainAndDelegate; - (BOOL)updateUserNameUsingKeychainAndDelegate; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 05355d3280..b747dda58a 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -884,6 +884,7 @@ 8034E6B61BA3567B00D83A30 /* BITDataTests.m */, 8034E6B71BA3567B00D83A30 /* BITDeviceTests.m */, 8034E6B81BA3567B00D83A30 /* BITDomainTests.m */, + 1E61CCAE18E0585A00A5E38E /* BITFeedbackManagerTests.m */, 8034E6B91BA3567B00D83A30 /* BITInternalTests.m */, 8034E6BA1BA3567B00D83A30 /* BITPersistenceTests.m */, 8034E6BC1BA3567B00D83A30 /* BITSenderTests.m */, @@ -895,7 +896,6 @@ 8034E6C31BA3567B00D83A30 /* BITUserTests.m */, 8034E6B31BA3567B00D83A30 /* BITAuthenticatorTests.m */, 8034E6BE1BA3567B00D83A30 /* BITStoreUpdateManagerTests.m */, - 1E61CCAE18E0585A00A5E38E /* BITFeedbackManagerTests.m */, E40E0B0817DA19DC005E38C1 /* BITHockeyAppClientTests.m */, 1E70A23517F31B82001BB32D /* BITHockeyHelperTests.m */, E4507E4217F0658F00171A0D /* BITKeychainUtilsTests.m */, @@ -1612,7 +1612,7 @@ isa = PBXProject; attributes = { LastTestingUpgradeCheck = 0600; - LastUpgradeCheck = 0810; + LastUpgradeCheck = 0820; TargetAttributes = { 1EB6173E1B0A30480035A986 = { CreatedOnToolsVersion = 6.3.1; diff --git a/Support/HockeySDK.xcodeproj/xcshareddata/xcschemes/HockeySDK Distribution.xcscheme b/Support/HockeySDK.xcodeproj/xcshareddata/xcschemes/HockeySDK Distribution.xcscheme index 0ca0b31f3c..5560a6f98f 100644 --- a/Support/HockeySDK.xcodeproj/xcshareddata/xcschemes/HockeySDK Distribution.xcscheme +++ b/Support/HockeySDK.xcodeproj/xcshareddata/xcschemes/HockeySDK Distribution.xcscheme @@ -1,6 +1,6 @@