blob: ea39d29aa4f4e462135df9f1c7868cb96f1e78a9 [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/open_in/open_in_controller.h"
#import <QuickLook/QuickLook.h>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/sequence_checker.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/ui/alert_coordinator/alert_coordinator.h"
#import "ios/chrome/browser/ui/open_in/open_in_activity_delegate.h"
#import "ios/chrome/browser/ui/open_in/open_in_activity_view_controller.h"
#import "ios/chrome/browser/ui/open_in/open_in_controller_testing.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_state.h"
#include "net/base/load_flags.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "ui/base/device_form_factor.h"
#include "ui/base/l10n/l10n_util_mac.h"
#import "ui/gfx/ios/NSString+CrStringDrawing.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// The path in the temp directory containing documents that are to be opened in
// other applications.
static NSString* const kDocumentsTemporaryPath = @"OpenIn";
// Duration of the show/hide animation for the `openInToolbar_`.
const NSTimeInterval kOpenInToolbarAnimationDuration = 0.2;
// Duration to show or hide the `overlayedView_`.
const NSTimeInterval kOverlayViewAnimationDuration = 0.3;
// Time interval after which the `openInToolbar_` is automatically hidden.
const NSTimeInterval kOpenInToolbarDisplayDuration = 2.0;
// Alpha value for the background view of `overlayedView_`.
const CGFloat kOverlayedViewBackgroundAlpha = 0.6;
// Width of the label displayed on the `overlayedView_` as a percentage of the
// `overlayedView_`'s width.
const CGFloat kOverlayedViewLabelWidthPercentage = 0.7;
// Bottom margin for the label displayed on the `overlayedView_`.
const CGFloat kOverlayedViewLabelBottomMargin = 60;
// Logs the result of the download process after the user taps "open in" button.
void LogOpenInDownloadResult(const OpenInDownloadResult result) {
UMA_HISTOGRAM_ENUMERATION("IOS.OpenIn.DownloadResult", result);
}
// Returns true if the file located at `url` can be previewed.
bool HasValidFileAtUrl(NSURL* url) {
if (!url)
return false;
if (![[NSFileManager defaultManager] isReadableFileAtPath:url.path])
return false;
NSString* extension = [url.path pathExtension];
if ([extension isEqualToString:@"pdf"]) {
base::ScopedCFTypeRef<CGPDFDocumentRef> document(
CGPDFDocumentCreateWithURL((__bridge CFURLRef)url));
return document;
}
return [QLPreviewController canPreviewItem:url];
}
// Returns the temporary path where documents are stored.
NSString* GetTemporaryDocumentDirectory() {
return [NSTemporaryDirectory()
stringByAppendingPathComponent:kDocumentsTemporaryPath];
}
// Removes the file at `file_url`.
void RemoveDocumentAtPath(NSURL* file_url) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
if (!file_url.path)
return;
NSError* error = nil;
if (![[NSFileManager defaultManager] removeItemAtPath:file_url.path
error:&error]) {
DLOG(ERROR) << "Failed to remove file: "
<< base::SysNSStringToUTF8([error description]);
}
}
// Removes all the stored files at `path`.
void RemoveAllStoredDocumentsAtPath(NSString* path) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
NSFileManager* file_manager = [NSFileManager defaultManager];
NSError* error = nil;
NSArray<NSString*>* document_files =
[file_manager contentsOfDirectoryAtPath:path error:&error];
if (!document_files) {
DLOG(ERROR) << "Failed to get content of directory at path: "
<< base::SysNSStringToUTF8([error description]);
return;
}
for (NSString* filename in document_files) {
NSString* file_path = [path stringByAppendingPathComponent:filename];
if (![file_manager removeItemAtPath:file_path error:&error]) {
DLOG(ERROR) << "Failed to remove file: "
<< base::SysNSStringToUTF8([error description]);
}
}
}
// Ensures the destination directory is created and any contained obsolete files
// are deleted. Returns YES if the directory is created successfully.
BOOL CreateDestinationDirectoryAndRemoveObsoleteFiles() {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
NSString* temporary_directory_path = GetTemporaryDocumentDirectory();
NSFileManager* file_manager = [NSFileManager defaultManager];
NSError* error = nil;
BOOL is_directory = NO;
if (![file_manager fileExistsAtPath:temporary_directory_path
isDirectory:&is_directory]) {
BOOL created = [file_manager createDirectoryAtPath:temporary_directory_path
withIntermediateDirectories:YES
attributes:nil
error:&error];
DCHECK(created);
if (!created) {
DLOG(ERROR) << "Error creating destination dir: "
<< base::SysNSStringToUTF8([error description]);
return NO;
}
} else {
if (!is_directory) {
DLOG(ERROR) << "Destination Directory already exists and is a file.";
return NO;
}
// Remove all documents that might be still on temporary storage.
RemoveAllStoredDocumentsAtPath(temporary_directory_path);
}
return YES;
}
} // anonymous namespace
@interface OpenInController () <CRWWebViewScrollViewProxyObserver,
OpenInActivityDelegate> {
// AlertCoordinator for showing an alert if no applications were found to open
// the current document.
AlertCoordinator* _alertCoordinator;
}
// Property storing the Y content offset the scroll view the last time it was
// updated. Used to know in which direction the scroll view is scrolling.
@property(nonatomic, assign) CGFloat previousScrollViewOffset;
// The base view controller from which to present UI.
@property(nonatomic, assign) UIViewController* baseViewController;
// Task runner on which file operations should happen.
@property(nonatomic, assign) scoped_refptr<base::SequencedTaskRunner>
sequencedTaskRunner;
// SimpleURLLoader completion callback, when `urlLoader_` completes a request.
- (void)urlLoadDidComplete:(const base::FilePath&)file_path;
// Starts downloading the file at path `kDocumentsTemporaryPath` with the name
// `suggestedFilename_`.
- (void)startDownload;
// Shows the overlayed toolbar `openInToolbar_`. If `withTimer` is YES, it would
// be hidden after a certain amount of time.
- (void)showOpenInToolbarWithTimer:(BOOL)withTimer;
// Hides the overlayed toolbar `openInToolbar_`.
- (void)hideOpenInToolbar;
// Called when there is a tap on the `webState_`'s view to display the
// overlayed toolbar `openInToolbar_` if necessary and (re)schedule the
// `openInTimer_`.
- (void)handleTapFrom:(UIGestureRecognizer*)gestureRecognizer;
// Downloads the file at `documentURL_` and presents the OpenIn menu for opening
// it in other applications.
- (void)exportFileWithOpenInMenuAnchoredAt:(id)sender;
// Called when there is a tap on the `overlayedView_` to cancel the file
// download.
- (void)handleTapOnOverlayedView:(UIGestureRecognizer*)gestureRecognizer;
// Removes `overlayedView_` from the top view of the application.
- (void)removeOverlayedView;
// Shows an alert with the given error message.
- (void)showErrorWithMessage:(NSString*)message;
// Presents the OpenIn menu for the file at `fileURL`.
- (void)presentOpenInMenuForFileAtURL:(NSURL*)fileURL;
// Shows an overlayed spinner on the top view to indicate that a file download
// is in progress.
- (void)showDownloadOverlayView;
// Returns a toolbar with an "Open in..." button to be overlayed on a document
// on tap.
- (OpenInToolbar*)openInToolbar;
@end
@implementation OpenInController {
// To check that callbacks are executed on the correct sequence.
SEQUENCE_CHECKER(_sequenceChecker);
// URL of the document.
GURL _documentURL;
// Controller for opening documents in other applications.
OpenInActivityViewController* activityViewController;
// Toolbar overlay to be displayed on tap.
OpenInToolbar* _openInToolbar;
// Timer used to automatically hide the `openInToolbar_` after a period.
NSTimer* _openInTimer;
// Gesture recognizer to catch taps on the document.
UITapGestureRecognizer* _tapRecognizer;
// Suggested filename for the document.
NSString* _suggestedFilename;
// Loader used to redownload the document and save it in the sandbox.
std::unique_ptr<network::SimpleURLLoader> _urlLoader;
// WebState used to check if the tap is not on a link and the
// `openInToolbar_` should be displayed.
web::WebState* _webState;
// Browser used to display errors.
Browser* _browser;
// URLLoaderFactory instance needed for URLLoader.
scoped_refptr<network::SharedURLLoaderFactory> _urlLoaderFactory;
// Spinner view displayed while the file is downloading.
UIView* _overlayedView;
// The location where the "Open in..." menu is anchored.
CGRect _anchorLocation;
// YES if the file download was canceled.
BOOL _downloadCanceled;
// YES if the toolbar is displayed.
BOOL _isOpenInToolbarDisplayed;
// YES if the workflow has been canceled.
BOOL _disabled;
}
@synthesize baseView = _baseView;
@synthesize previousScrollViewOffset = _previousScrollViewOffset;
- (instancetype)initWithBaseViewController:(UIViewController*)baseViewController
URLLoaderFactory:
(scoped_refptr<network::SharedURLLoaderFactory>)
urlLoaderFactory
webState:(web::WebState*)webState
browser:(Browser*)browser {
self = [super init];
if (self) {
_baseViewController = baseViewController;
_urlLoaderFactory = std::move(urlLoaderFactory);
_webState = webState;
_tapRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleTapFrom:)];
[_tapRecognizer setDelegate:self];
_sequencedTaskRunner = base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
_previousScrollViewOffset = 0;
_browser = browser;
}
return self;
}
- (void)enableWithDocumentURL:(const GURL&)documentURL
suggestedFilename:(NSString*)suggestedFilename {
_disabled = NO;
_documentURL = GURL(documentURL);
_suggestedFilename = suggestedFilename;
[self.baseView addGestureRecognizer:_tapRecognizer];
[self openInToolbar].alpha = 0.0f;
[self.baseView addSubview:[self openInToolbar]];
if (_webState)
[[_webState->GetWebViewProxy() scrollViewProxy] addObserver:self];
[self showOpenInToolbarWithTimer:NO];
}
- (void)disable {
_disabled = YES;
[self removeOverlayedView];
[self openInToolbar].alpha = 0.0f;
[_openInTimer invalidate];
[self.baseView removeGestureRecognizer:_tapRecognizer];
if (_webState)
[[_webState->GetWebViewProxy() scrollViewProxy] removeObserver:self];
self.previousScrollViewOffset = 0;
[[self openInToolbar] removeFromSuperview];
_documentURL = GURL();
_suggestedFilename = nil;
_urlLoader.reset();
}
- (void)detachFromWebState {
[self disable];
// Animation blocks may be keeping this object alive; don't keep a
// potentially dangling pointer to WebState and Browser.
_webState = nullptr;
_browser = nullptr;
}
- (void)dealloc {
[self disable];
}
- (void)handleTapFrom:(UIGestureRecognizer*)gestureRecognizer {
if ([gestureRecognizer state] == UIGestureRecognizerStateEnded) {
if (_isOpenInToolbarDisplayed) {
[self hideOpenInToolbar];
} else {
[self showOpenInToolbarWithTimer:YES];
}
}
}
- (void)showOpenInToolbarWithTimer:(BOOL)withTimer {
if (withTimer) {
if ([_openInTimer isValid]) {
[_openInTimer setFireDate:([NSDate dateWithTimeIntervalSinceNow:
kOpenInToolbarDisplayDuration])];
} else {
_openInTimer =
[NSTimer scheduledTimerWithTimeInterval:kOpenInToolbarDisplayDuration
target:self
selector:@selector(hideOpenInToolbar)
userInfo:nil
repeats:NO];
}
} else {
[_openInTimer invalidate];
}
OpenInToolbar* openInToolbar = [self openInToolbar];
if (!_isOpenInToolbarDisplayed) {
[UIView animateWithDuration:kOpenInToolbarAnimationDuration
animations:^{
[openInToolbar setAlpha:1.0];
}];
}
_isOpenInToolbarDisplayed = YES;
}
- (void)hideOpenInToolbar {
if (!_openInToolbar)
return;
[_openInTimer invalidate];
UIView* openInToolbar = [self openInToolbar];
[UIView animateWithDuration:kOpenInToolbarAnimationDuration
animations:^{
[openInToolbar setAlpha:0.0];
}];
_isOpenInToolbarDisplayed = NO;
}
- (void)exportFileWithOpenInMenuAnchoredAt:(UIView*)view {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
DCHECK([view isKindOfClass:[UIView class]]);
base::RecordAction(base::UserMetricsAction("IOS.OpenIn.Tapped"));
if (!_webState)
return;
_anchorLocation = [[self openInToolbar] convertRect:view.frame
toView:self.baseView];
[_openInTimer invalidate];
// Creating the directory can block the main thread, so perform it on a
// background sequence, then on current sequence complete the workflow.
__weak OpenInController* weakSelf = self;
_sequencedTaskRunner->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&CreateDestinationDirectoryAndRemoveObsoleteFiles),
base::BindOnce(^(BOOL directoryCreated) {
[weakSelf onDestinationDirectoryCreated:directoryCreated];
}));
}
- (void)onDestinationDirectoryCreated:(BOOL)directoryCreated {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (_disabled)
return;
if (!directoryCreated) {
[self hideOpenInToolbar];
} else {
[self startDownload];
}
}
- (void)startDownload {
NSString* tempDirPath = GetTemporaryDocumentDirectory();
NSString* filePath =
[tempDirPath stringByAppendingPathComponent:_suggestedFilename];
// In iPad the toolbar has to be displayed to anchor the "Open in" menu.
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET)
[self hideOpenInToolbar];
// Show an overlayed view to indicate a download is in progress. On tap this
// view can be dismissed and the download canceled.
[self showDownloadOverlayView];
_downloadCanceled = NO;
// Download the document and save it at `filePath`.
auto resourceRequest = std::make_unique<network::ResourceRequest>();
resourceRequest->url = _documentURL;
resourceRequest->load_flags = net::LOAD_SKIP_CACHE_VALIDATION;
_urlLoader = network::SimpleURLLoader::Create(std::move(resourceRequest),
NO_TRAFFIC_ANNOTATION_YET);
_urlLoader->DownloadToFile(_urlLoaderFactory.get(),
base::BindOnce(^(base::FilePath filePath) {
[self urlLoadDidComplete:filePath];
}),
base::FilePath(base::SysNSStringToUTF8(filePath)));
}
- (void)handleTapOnOverlayedView:(UIGestureRecognizer*)gestureRecognizer {
if ([gestureRecognizer state] != UIGestureRecognizerStateEnded)
return;
[self removeOverlayedView];
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET)
[self hideOpenInToolbar];
_downloadCanceled = YES;
}
- (void)removeOverlayedView {
if (!_overlayedView)
return;
UIView* overlayedView = _overlayedView;
[UIView animateWithDuration:kOverlayViewAnimationDuration
animations:^{
[overlayedView setAlpha:0.0];
}
completion:^(BOOL finished) {
[overlayedView removeFromSuperview];
}];
_overlayedView = nil;
}
- (void)showErrorWithMessage:(NSString*)message {
UIViewController* topViewController = [GetAnyKeyWindow() rootViewController];
_alertCoordinator =
[[AlertCoordinator alloc] initWithBaseViewController:topViewController
browser:_browser
title:nil
message:message];
[_alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_OK)
action:nil
style:UIAlertActionStyleDefault];
[_alertCoordinator start];
}
- (void)presentOpenInMenuForFileAtURL:(NSURL*)fileURL {
if (!_webState)
return;
activityViewController =
[[OpenInActivityViewController alloc] initWithURL:fileURL];
activityViewController.delegate = self;
// UIActivityViewController is presented in a popover on iPad.
activityViewController.popoverPresentationController.sourceView =
self.baseView;
activityViewController.popoverPresentationController.sourceRect =
_anchorLocation;
[self removeOverlayedView];
[self.baseViewController presentViewController:activityViewController
animated:YES
completion:nil];
}
- (void)completedPresentOpenInMenuForFileAtURL:(NSURL*)fileURL {
_sequencedTaskRunner->PostTask(FROM_HERE, base::BindOnce(^{
RemoveDocumentAtPath(fileURL);
}));
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
_openInTimer =
[NSTimer scheduledTimerWithTimeInterval:kOpenInToolbarDisplayDuration
target:self
selector:@selector(hideOpenInToolbar)
userInfo:nil
repeats:NO];
}
}
- (void)showDownloadOverlayView {
_overlayedView = [[UIView alloc] initWithFrame:[self.baseView bounds]];
[_overlayedView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight)];
UIView* grayBackgroundView =
[[UIView alloc] initWithFrame:[_overlayedView frame]];
[grayBackgroundView setBackgroundColor:[UIColor darkGrayColor]];
[grayBackgroundView setAlpha:kOverlayedViewBackgroundAlpha];
[grayBackgroundView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight)];
[_overlayedView addSubview:grayBackgroundView];
UIActivityIndicatorView* spinner = GetLargeUIActivityIndicatorView();
[spinner setFrame:[_overlayedView frame]];
[spinner setHidesWhenStopped:YES];
[spinner setUserInteractionEnabled:NO];
[spinner startAnimating];
[spinner setAutoresizingMask:(UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight)];
[_overlayedView addSubview:spinner];
UILabel* label = [[UILabel alloc] init];
[label setTextColor:[UIColor whiteColor]];
[label setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]];
[label setNumberOfLines:0];
[label setShadowColor:[UIColor blackColor]];
[label setShadowOffset:CGSizeMake(0.0, 1.0)];
[label setBackgroundColor:[UIColor clearColor]];
[label setText:l10n_util::GetNSString(IDS_IOS_OPEN_IN_FILE_DOWNLOAD_CANCEL)];
[label setLineBreakMode:NSLineBreakByWordWrapping];
[label setTextAlignment:NSTextAlignmentCenter];
CGFloat labelWidth =
[_overlayedView frame].size.width * kOverlayedViewLabelWidthPercentage;
CGFloat originX = ([_overlayedView frame].size.width - labelWidth) / 2;
CGFloat labelHeight =
[[label text] cr_boundingSizeWithSize:CGSizeMake(labelWidth, CGFLOAT_MAX)
font:[label font]]
.height;
CGFloat originY =
[_overlayedView center].y - labelHeight - kOverlayedViewLabelBottomMargin;
[label setFrame:CGRectMake(originX, originY, labelWidth, labelHeight)];
[_overlayedView addSubview:label];
UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleTapOnOverlayedView:)];
[tapRecognizer setDelegate:self];
[_overlayedView addGestureRecognizer:tapRecognizer];
[_overlayedView setAlpha:0.0];
[self.baseView addSubview:_overlayedView];
UIView* overlayedView = _overlayedView;
[UIView animateWithDuration:kOverlayViewAnimationDuration
animations:^{
[overlayedView setAlpha:1.0];
}];
}
- (OpenInToolbar*)openInToolbar {
if (!_openInToolbar) {
_openInToolbar = [[OpenInToolbar alloc]
initWithTarget:self
action:@selector(exportFileWithOpenInMenuAnchoredAt:)];
}
return _openInToolbar;
}
#pragma mark - OpenInActivityDelegate
- (void)openInActivityWillDisappearForFileAtURL:(NSURL*)fileURL {
[self completedPresentOpenInMenuForFileAtURL:fileURL];
}
#pragma mark File management
- (void)urlLoadDidComplete:(const base::FilePath&)filePath {
NSURL* fileURL = nil;
if (!filePath.empty())
fileURL = [NSURL fileURLWithPath:base::SysUTF8ToNSString(filePath.value())];
if (!_downloadCanceled && HasValidFileAtUrl(fileURL)) {
LogOpenInDownloadResult(OpenInDownloadResult::kSucceeded);
[self presentOpenInMenuForFileAtURL:fileURL];
return;
}
_sequencedTaskRunner->PostTask(FROM_HERE, base::BindOnce(^{
RemoveDocumentAtPath(fileURL);
}));
OpenInDownloadResult download_result = OpenInDownloadResult::kCanceled;
if (!_downloadCanceled) {
download_result = OpenInDownloadResult::kFailed;
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET)
[self hideOpenInToolbar];
[self removeOverlayedView];
[self showErrorWithMessage:l10n_util::GetNSStringWithFixup(
IDS_IOS_OPEN_IN_FILE_DOWNLOAD_FAILED)];
}
LogOpenInDownloadResult(download_result);
}
#pragma mark - UIGestureRecognizerDelegate Methods
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer*)otherGestureRecognizer {
return YES;
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer {
if ([gestureRecognizer.view isEqual:_overlayedView])
return YES;
CGPoint location = [gestureRecognizer locationInView:[self openInToolbar]];
return ![[self openInToolbar] pointInside:location withEvent:nil];
}
#pragma mark - CRWWebViewScrollViewProxyObserver
- (void)webViewScrollViewDidScroll:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
// Store the values.
CGFloat previousScrollOffset = self.previousScrollViewOffset;
CGFloat currentScrollOffset = webViewScrollViewProxy.contentOffset.y;
self.previousScrollViewOffset = currentScrollOffset;
if (previousScrollOffset - currentScrollOffset > 0) {
if (!_isOpenInToolbarDisplayed ||
(_isOpenInToolbarDisplayed && [_openInTimer isValid])) {
// Shows the OpenInToolbar only if it isn't displayed, or if it is
// displayed with a timer to have the timer reset.
[self showOpenInToolbarWithTimer:YES];
}
} else if (webViewScrollViewProxy.dragging) {
[self hideOpenInToolbar];
}
}
#pragma mark - TestingAditions
- (NSString*)suggestedFilename {
return _suggestedFilename;
}
@end