From b48bb8fb12a920f62479cb3521f5a89590dde374 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Sun, 25 May 2014 18:55:24 +0200 Subject: [PATCH] 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 | 98 ++++++++-------------- Classes/BITFeedbackMessage.h | 20 +++-- Classes/BITFeedbackMessage.m | 6 +- Classes/BITFeedbackMessageAttachment.h | 1 - Classes/BITFeedbackMessageAttachment.m | 104 ++++++++++++++---------- 12 files changed, 272 insertions(+), 211 deletions(-) diff --git a/Classes/BITActivityIndicatorButton.h b/Classes/BITActivityIndicatorButton.h index 52f0206b0b..2c633222c9 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 4e30bb0f5f..00966186f6 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 18b3aaf270..ba8b200932 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -602,20 +602,22 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; 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 8ce9c3fcd2..f146b8acf9 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 c0526376cd..367721ac39 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -152,7 +152,7 @@ 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 @@ [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]; + 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]; } - [imageView setContentMode:UIViewContentModeScaleAspectFit]; - [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; - - [self.attachmentViews addObject:imageView]; - //[self addSubview:imageView]; - - } } @@ -286,18 +285,18 @@ 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)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 c7595fa7e8..9397bf856a 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -105,7 +105,7 @@ - (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)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 @@ [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 @@ [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 @@ 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 @@ [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 @@ } else { [self.tableView reloadData]; } - + [super viewDidAppear:animated]; } @@ -272,6 +274,8 @@ - (void)deleteAllMessages { [_manager deleteAllMessages]; + [self refreshPreviewItems]; + [self.tableView reloadData]; } @@ -289,9 +293,9 @@ } 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 @@ 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 @@ 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 @@ - (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 @@ if (buttonIndex == actionSheet.cancelButtonIndex) { return; } - + if ([actionSheet tag] == 0) { if (buttonIndex == [actionSheet destructiveButtonIndex]) { [self deleteAllMessages]; @@ -794,60 +806,66 @@ } } + #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 01ad9a46a6..435a1f2ec2 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 5c89cf1cbd..056266fc23 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -740,7 +740,7 @@ 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 @@ [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 @@ #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; - - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication].keyWindow addGestureRecognizer:self.tapRecognizer]; - }); - } - - if (self.screenshotNotificationEnabled){ + if (feedbackObservationMode == BITFeedbackObservationModeOnScreenshot){ if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; - self.screenshotNotificationEnabled = NO; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + } else { + BITHockeyLog("WARNING: BITFeedbackObservationModeOnScreenshot requires iOS 7 or later."); + } + + self.screenshotNotificationEnabled = YES; + + if (self.tapRecognizer){ + [[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer]; + self.tapRecognizer = nil; + } + } + + 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 4587449d17..908e4fc8fd 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 42da4e602f..7f8d60dea1 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -35,7 +35,7 @@ #pragma mark - NSObject -- (id) init { +- (instancetype) init { if ((self = [super init])) { _text = nil; _userID = nil; @@ -67,8 +67,8 @@ [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 96adde8531..dbb398b1da 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 ac8551011d..1315e702f8 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 @@ @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 @@ 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 @@ } - (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 @@ [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 *)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 @@ #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 @@ - (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