2 // StorageObjectViewController.m
5 // Created by Mike Mayo on 12/19/10.
6 // The OpenStack project is provided under the Apache 2.0 license.
9 #import "StorageObjectViewController.h"
10 #import "OpenStackAccount.h"
13 #import "StorageObject.h"
14 #import "AccountManager.h"
15 #import "AnimatedProgressView.h"
16 #import "UIViewController+Conveniences.h"
17 #import "MediaViewController.h"
18 #import "FolderViewController.h"
19 #import "UIColor+MoreColors.h"
20 #import "OpenStackAppDelegate.h"
25 // TODO: use etag to reset download
26 // TODO: try downloading directly to the file to save memory. don't use object.data
32 Content Type text/plain
35 Key Value -> tap goes to a metadata item VC to edit or delete
37 Add Metadata... (if max not already reached)
39 CDN URL sections (action sheet to copy, open in safari, and email link)
41 Download File (if downloaded, Open File and Mail File as Attachment)
42 "After you download the file, you'll be able to attempt to open it and mail is as an attachment."
47 @implementation StorageObjectViewController
49 @synthesize account, container, folder, object, tableView, folderViewController;
51 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
52 return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) || (toInterfaceOrientation == UIInterfaceOrientationPortrait);
55 - (void)setBackgroundView {
57 UIImageView *logo = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"cloudfiles-large.png"]];
59 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
60 UIView *backgroundContainer = [[UIView alloc] init];
61 backgroundContainer.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
62 backgroundContainer.backgroundColor = [UIColor iPadTableBackgroundColor];
63 logo.contentMode = UIViewContentModeScaleAspectFit;
64 logo.frame = CGRectMake(100.0, 100.0, 1000.0, 1000.0);
66 [backgroundContainer addSubview:logo];
67 tableView.backgroundView = backgroundContainer;
68 [backgroundContainer release];
70 self.tableView.backgroundView = nil;
77 #pragma mark View lifecycle
81 deleteActionSheet = [[UIActionSheet alloc] initWithTitle:@"Are you sure you want to delete this file? This operation cannot be undone." delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete File" otherButtonTitles:nil];
84 - (void)viewWillAppear:(BOOL)animated {
85 [super viewWillAppear:animated];
86 self.navigationItem.title = object.name;
88 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
89 NSString *documentsDirectory = [paths objectAtIndex:0];
90 NSString *shortPath = [NSString stringWithFormat:@"/%@/%@", self.container.name, self.object.fullPath];
91 NSString *filePath = [documentsDirectory stringByAppendingString:shortPath];
93 NSFileManager *fileManager = [NSFileManager defaultManager];
94 fileDownloaded = [fileManager fileExistsAtPath:filePath];
96 downloadProgressView = [[AnimatedProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
98 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
99 CGRect rect = downloadProgressView.frame;
100 rect.size.width = 440.0;
101 downloadProgressView.frame = rect;
104 [self setBackgroundView];
105 if (self.container.cdnEnabled) {
115 // let's see if we can tweet
116 UIApplication *app = [UIApplication sharedApplication];
117 NSURL *twitterURL = [NSURL URLWithString:@"twitter://post?message=test"];
119 if ([app canOpenURL:twitterURL]) {
120 cdnURLActionSheet = [[UIActionSheet alloc] initWithTitle:[[NSString stringWithFormat:@"%@/%@", self.container.cdnURL, self.object.fullPath] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@"Copy to Pasteboard", @"Open in Safari", @"Email Link to File", @"Tweet Link to File", nil];
122 cdnURLActionSheet = [[UIActionSheet alloc] initWithTitle:[[NSString stringWithFormat:@"%@/%@", self.container.cdnURL, self.object.fullPath] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@"Copy to Pasteboard", @"Open in Safari", @"Email Link to File", nil];
128 #pragma mark Table view data source
130 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
131 return self.container.cdnEnabled ? 4 : 3;
134 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
135 if (section == kDetails) {
137 } else if (section == kMetadata) {
138 return 1 + [object.metadata count];
139 } else if (section == cdnURLSection) {
141 } else if (section == actionsSection) {
142 return fileDownloaded ? 2 : 1;
143 } else if (section == deleteSection) {
151 - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
152 if (section == actionsSection) {
153 return @"After you download the file, you'll be able to attempt to open it and mail it as an attachment.";
160 - (CGFloat)findLabelHeight:(NSString*)text font:(UIFont *)font {
161 CGSize textLabelSize;
162 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
164 textLabelSize = CGSizeMake(596.0, 9000.0f);
166 textLabelSize = CGSizeMake(280.0, 9000.0f);
168 CGSize stringSize = [text sizeWithFont:font constrainedToSize:textLabelSize lineBreakMode:UILineBreakModeCharacterWrap];
169 return stringSize.height;
172 - (CGFloat)tableView:(UITableView *)aTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
173 CGFloat result = aTableView.rowHeight;
175 if (indexPath.section == cdnURLSection) {
176 result = 22.0 + [self findLabelHeight:[[NSString stringWithFormat:@"%@/%@", self.container.cdnURL, self.object.fullPath] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] font:[UIFont systemFontOfSize:18.0]];
177 } else if (indexPath.section == kDetails && indexPath.row == 1) {
178 CGSize textLabelSize;
179 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
180 textLabelSize = CGSizeMake(537.0, 9000.0f);
182 textLabelSize = CGSizeMake(221.0, 9000.0f);
184 CGSize stringSize = [object.fullPath sizeWithFont:[UIFont systemFontOfSize:18.0] constrainedToSize:textLabelSize lineBreakMode:UILineBreakModeWordWrap];
185 return 22.0 + stringSize.height;
186 } else if (indexPath.section == kDetails && indexPath.row == 0) {
187 CGSize textLabelSize;
188 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
189 textLabelSize = CGSizeMake(537.0, 9000.0f);
191 textLabelSize = CGSizeMake(221.0, 9000.0f);
193 CGSize stringSize = [object.name sizeWithFont:[UIFont systemFontOfSize:18.0] constrainedToSize:textLabelSize lineBreakMode:UILineBreakModeWordWrap];
194 return 22.0 + stringSize.height;
197 return MAX(aTableView.rowHeight, result);
200 - (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
202 static NSString *CellIdentifier = @"Cell";
204 UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:CellIdentifier];
206 cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier] autorelease];
207 //cell.backgroundColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.93];
208 cell.textLabel.backgroundColor = [UIColor clearColor];
209 cell.detailTextLabel.backgroundColor = [UIColor clearColor];
210 cell.detailTextLabel.numberOfLines = 0;
211 cell.detailTextLabel.lineBreakMode = UILineBreakModeWordWrap;
214 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
215 cell.backgroundColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.8];
218 cell.detailTextLabel.textAlignment = UITextAlignmentRight;
220 // Configure the cell...
221 if (indexPath.section == kDetails) {
222 cell.accessoryType = UITableViewCellAccessoryNone;
223 cell.selectionStyle = UITableViewCellSelectionStyleNone;
224 cell.accessoryView = nil;
225 if (indexPath.row == 0) {
226 cell.textLabel.text = @"Name";
227 cell.detailTextLabel.text = object.name;
228 } else if (indexPath.row == 1) {
229 cell.textLabel.text = @"Full Path";
230 cell.detailTextLabel.text = object.fullPath;
231 } else if (indexPath.row == 2) {
232 cell.textLabel.text = @"Size";
233 cell.detailTextLabel.text = [object humanizedBytes];
234 } else if (indexPath.row == 3) {
235 cell.textLabel.text = @"Type";
236 cell.detailTextLabel.text = object.contentType;
238 } else if (indexPath.section == kMetadata) {
239 cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
240 cell.selectionStyle = UITableViewCellSelectionStyleBlue;
241 cell.accessoryView = nil;
242 if (indexPath.row == [object.metadata count]) {
243 cell.textLabel.text = @"Add Metadata";
244 cell.detailTextLabel.text = @"";
246 NSString *key = [[object.metadata allKeys] objectAtIndex:indexPath.row];
247 cell.textLabel.text = key;
248 cell.detailTextLabel.text = [object.metadata objectForKey:key];
250 } else if (indexPath.section == cdnURLSection) {
251 cell.detailTextLabel.textAlignment = UITextAlignmentLeft;
252 cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
253 cell.textLabel.text = @"";
254 cell.detailTextLabel.text = [[NSString stringWithFormat:@"%@/%@", self.container.cdnURL, self.object.fullPath] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
255 } else if (indexPath.section == actionsSection) {
256 cell.accessoryView = nil;
257 if (performingAction) {
258 cell.textLabel.textColor = [UIColor grayColor];
259 cell.selectionStyle = UITableViewCellSelectionStyleNone;
260 cell.accessoryType = UITableViewCellAccessoryNone;
262 cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
263 cell.selectionStyle = UITableViewCellSelectionStyleBlue;
266 if (indexPath.row == 0) {
267 if (fileDownloaded) {
268 cell.textLabel.text = @"Open File";
270 if (fileDownloading) {
271 cell.accessoryView = downloadProgressView;
272 // TODO: if you leave this view while downloading, there's EXC_BAD_ACCESS
273 cell.textLabel.text = @"Downloading";
275 cell.textLabel.text = @"Download File";
278 cell.detailTextLabel.text = @"";
280 } else if (indexPath.row == 1) {
281 cell.textLabel.text = @"Email File as Attachment";
282 cell.detailTextLabel.text = @"";
284 } else if (indexPath.section == deleteSection) {
285 cell.textLabel.text = @"Delete Object";
286 cell.detailTextLabel.text = @"";
287 cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
288 cell.selectionStyle = UITableViewCellSelectionStyleBlue;
296 #pragma mark Table view delegate
298 - (void)reloadActionsTitleRow:(NSTimer *)timer {
299 [[timer.userInfo objectForKey:@"tableView"] reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:actionsSection]] withRowAnimation:UITableViewRowAnimationNone];
302 - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
303 if (indexPath.section == cdnURLSection) {
304 [cdnURLActionSheet showInView:self.view];
305 } else if (indexPath.section == actionsSection) {
306 if (indexPath.row == 0) {
307 if (fileDownloaded) {
308 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
309 NSString *documentsDirectory = [paths objectAtIndex:0];
310 NSString *shortPath = [NSString stringWithFormat:@"/%@/%@", self.container.name, self.object.fullPath];
311 NSString *filePath = [documentsDirectory stringByAppendingString:shortPath];
313 UIDocumentInteractionController *vc = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:filePath]];
316 if (![vc presentPreviewAnimated:YES]) {
318 if ([self.object isPlayableMedia]) {
319 MediaViewController *vc = [[MediaViewController alloc] initWithNibName:@"MediaViewController" bundle:nil];
320 vc.container = self.container;
321 vc.object = self.object;
322 [self.navigationController pushViewController:vc animated:YES];
325 [self alert:@"Error" message:@"This file could not be opened."];
326 [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
330 [self.tableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:actionsSection] animated:YES];
333 if (!fileDownloading) {
335 fileDownloading = YES;
336 [self.account.manager getObject:self.container object:self.object downloadProgressDelegate:self];
337 [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:actionsSection]] withRowAnimation:UITableViewRowAnimationNone];
341 } else if (indexPath.row == 1) {
343 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
344 NSString *documentsDirectory = [paths objectAtIndex:0];
345 NSString *shortPath = @"";
347 if (self.container && [self.container respondsToSelector:@selector(name)] && self.object && [self.object respondsToSelector:@selector(fullPath)]) {
348 shortPath = [NSString stringWithFormat:@"/%@/%@", self.container.name, self.object.fullPath];
351 NSString *filePath = [documentsDirectory stringByAppendingString:shortPath];
352 NSData *data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:filePath]];
354 MFMailComposeViewController *vc = [[MFMailComposeViewController alloc] init];
355 vc.mailComposeDelegate = self;
356 [vc setSubject:self.object.name];
357 [vc addAttachmentData:data mimeType:self.object.contentType fileName:self.object.name];
358 [vc setMessageBody:@"" isHTML:NO];
359 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
360 vc.modalPresentationStyle = UIModalPresentationPageSheet;
362 [self presentModalViewController:vc animated:YES];
365 } else if (indexPath.section == deleteSection) {
366 [deleteActionSheet showInView:self.view];
370 - (void)setProgress:(float)newProgress {
371 [downloadProgressView setProgress:newProgress animated:YES];
372 if (newProgress >= 1.0) {
373 fileDownloading = NO;
374 fileDownloaded = YES;
376 [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:1 inSection:actionsSection]] withRowAnimation:UITableViewRowAnimationBottom];
377 [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:actionsSection]] withRowAnimation:UITableViewRowAnimationNone];
382 #pragma mark Document Interation Controller Delegate
384 - (UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *) controller {
385 return self.navigationController;
388 - (void)documentInteractionControllerDidEndPreview:(UIDocumentInteractionController *)controllers {
389 [self.tableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:actionsSection] animated:YES];
393 #pragma mark Action Sheet Delegate
395 - (void)deleteObjectRow {
396 [self.folderViewController.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:selectedIndexPath] withRowAnimation:UITableViewRowAnimationLeft];
399 - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
400 if ([actionSheet isEqual:deleteActionSheet]) {
401 if (buttonIndex == 0) {
402 // delete the file and pop out
403 [self showToolbarActivityMessage:@"Deleting file..."];
405 [self.account.manager deleteObject:self.container object:self.object];
407 deleteSuccessObserver = [[NSNotificationCenter defaultCenter] addObserverForName:@"deleteObjectSucceeded" object:self.object
408 queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* notification)
410 [self hideToolbarActivityMessage];
411 performingAction = NO;
412 [self.folder.objects removeObjectForKey:self.object.name];
413 [self.navigationController popViewControllerAnimated:YES];
414 if ([self.folder.objects count] + [self.folder.folders count] == 0) {
415 [self.folderViewController.tableView reloadData];
417 [self.folderViewController.tableView selectRowAtIndexPath:selectedIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
418 [NSTimer scheduledTimerWithTimeInterval:0.75 target:self selector:@selector(deleteObjectRow) userInfo:nil repeats:NO];
420 [[NSNotificationCenter defaultCenter] removeObserver:deleteSuccessObserver];
423 deleteFailureObserver = [[NSNotificationCenter defaultCenter] addObserverForName:@"deleteObjectFailed" object:self.object
424 queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* notification)
426 [self hideToolbarActivityMessage];
427 performingAction = NO;
428 [self alert:@"There was a problem deleting this file." request:[notification.userInfo objectForKey:@"request"]];
430 [[NSNotificationCenter defaultCenter] removeObserver:deleteFailureObserver];
435 NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:deleteSection];
436 [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
437 } else if ([actionSheet isEqual:cdnURLActionSheet]) {
438 NSURL *url = [NSURL URLWithString:[[NSString stringWithFormat:@"%@/%@", self.container.cdnURL, self.object.fullPath] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
440 if (buttonIndex == 0) {
441 // copy to pasteboard
442 UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
443 [pasteboard setString:[url description]];
444 } else if (buttonIndex == 1) {
446 UIApplication *application = [UIApplication sharedApplication];
447 if ([application canOpenURL:url]) {
448 [application openURL:url];
450 [self alert:@"Error" message:[NSString stringWithFormat:@"This URL cannot be opened.\n%@", url]];
452 } else if (buttonIndex == 2) {
453 // email link to file
454 MFMailComposeViewController *vc = [[MFMailComposeViewController alloc] init];
455 vc.mailComposeDelegate = self;
456 [vc setSubject:self.object.name];
457 [vc setMessageBody:[url description] isHTML:NO];
458 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
459 vc.modalPresentationStyle = UIModalPresentationPageSheet;
461 [self presentModalViewController:vc animated:YES];
463 } else if (buttonIndex == 3) {
464 // tweet link to file
465 UIApplication *app = [UIApplication sharedApplication];
466 NSURL *twitterURL = [NSURL URLWithString:[NSString stringWithFormat:@"twitter://post?message=%@", [[url description] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
467 if ([app canOpenURL:twitterURL]) {
468 [app openURL:twitterURL];
471 [self.tableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:cdnURLSection] animated:YES];
476 #pragma mark Mail Composer Delegate
478 // Dismisses the email composition interface when users tap Cancel or Send.
479 - (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error {
480 [self dismissModalViewControllerAnimated:YES];
481 [self.tableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:actionsSection] animated:YES];
485 #pragma mark Memory management
489 [downloadProgressView release];
490 [deleteActionSheet release];
491 [cdnURLActionSheet release];
493 [folderViewController release];