blob: 50a4948396f4a11d285c1e7ce00d711bfe309830 [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/print/print_controller.h"
#import <MobileCoreServices/UTType.h>
#import <Webkit/Webkit.h>
#include <memory>
#include "base/callback_helpers.h"
#import "base/ios/ios_util.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/mac/bind_objc_block.h"
#include "base/mac/foundation_util.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/task_scheduler/post_task.h"
#include "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/ui/alert_coordinator/alert_coordinator.h"
#import "ios/chrome/browser/ui/alert_coordinator/loading_alert_coordinator.h"
#include "ios/chrome/grit/ios_strings.h"
#import "net/base/mac/url_conversions.h"
#include "net/http/http_response_headers.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_fetcher_delegate.h"
#include "net/url_request/url_request_context_getter.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using net::URLFetcher;
using net::URLFetcherDelegate;
using net::URLRequestContextGetter;
@interface PrintController ()
// Presents a UIPrintInteractionController with a default completion handler.
// |isPDF| indicates if |printInteractionController| is being presented to print
// a PDF.
+ (void)displayPrintInteractionController:
(UIPrintInteractionController*)printInteractionController
forPDF:(BOOL)isPDF;
// Shows a dialog on |viewController| indicating that the print preview is being
// prepared. The dialog will appear only if the download has not completed or
// been cancelled |kPDFDownloadDialogDelay| seconds after this method is called.
- (void)showPDFDownloadingDialog:(UIViewController*)viewController;
// Dismisses the dialog which indicates that the print preview is being
// prepared.
- (void)dismissPDFDownloadingDialog;
// Shows an dialog on |viewController| indicating that there was an error when
// preparing the print preview, and providing the ability to retry. |URL| is the
// URL of the PDF which had an error.
- (void)showPDFDownloadErrorWithURL:(const GURL)URL
viewController:(UIViewController*)viewController;
// Handles downloading the file at |URL| and presents dialogs on
// |viewController| if necessary.
- (void)downloadPDFFileWithURL:(const GURL&)URL
viewController:(UIViewController*)viewController;
// Accesses the result of the PDF download, and if successful, presents the
// AirPrint menu with the downloaded PDF. It also ensures that the
// PDFDownloadingDialog is dismissed, and presents an error dialog on
// |viewController| if the download fails. This method should be called only by
// the URLFetcherDelegate.
- (void)finishedDownloadingPDF:(UIViewController*)viewController;
@end
namespace {
// The MIME type of a PDF file contained in a Web view.
const char kPDFMimeType[] = "application/pdf";
// Delay after downloading begins that the |_PDFDownloadingDialog| appears.
const int64_t kPDFDownloadDialogDelay = 1;
// A delegate for the URLFetcher to tell owning PrintController that the
// download is complete.
class PrintPDFFetcherDelegate : public URLFetcherDelegate {
public:
explicit PrintPDFFetcherDelegate(PrintController* owner) : owner_(owner) {}
void OnURLFetchComplete(const URLFetcher* source) override {
DCHECK(view_controller_);
[owner_ finishedDownloadingPDF:view_controller_];
}
// The ViewController used to display an error if the download failed.
void SetViewController(UIViewController* view_controller) {
view_controller_ = view_controller;
}
private:
__weak PrintController* owner_;
__weak UIViewController* view_controller_;
DISALLOW_COPY_AND_ASSIGN(PrintPDFFetcherDelegate);
};
} // namespace
@implementation PrintController {
// URLFetcher to download the PDF pointed to by the WKWebView.
std::unique_ptr<URLFetcher> _fetcher;
// A delegate to bridge between PrintController and the URLFetcher callback.
std::unique_ptr<PrintPDFFetcherDelegate> _fetcherDelegate;
// Context getter required by the URLFetcher.
scoped_refptr<URLRequestContextGetter> _requestContextGetter;
// A dialog which indicates that the print preview is being prepared. It
// offers a cancel button which will cancel the download. It is created when
// downloading begins and is released when downloading ends (either due to
// cancellation or completion).
LoadingAlertCoordinator* _PDFDownloadingDialog;
// A dialog which indicates that the print preview failed.
AlertCoordinator* _PDFDownloadingErrorDialog;
}
#pragma mark - Class methods.
+ (void)displayPrintInteractionController:
(UIPrintInteractionController*)printInteractionController
forPDF:(BOOL)isPDF {
void (^completionHandler)(UIPrintInteractionController*, BOOL, NSError*) = ^(
UIPrintInteractionController* printInteractionController, BOOL completed,
NSError* error) {
if (error)
DLOG(ERROR) << "Air printing error: " << error.description;
// When printing a NSData object given to the
// UIPrintInteractionController's |printingItem| object, a PDF file
// representing the NSData object is created in the app's tmp directory
// by the OS and never deleted. So, this workaround deletes PDF files in
// tmp now that printing is done. When iOS9 is deprecated, this can
// be removed since PDFs will no longer need to be downloaded to print,
// and |printingItem| will no longer be used.
if (!base::ios::IsRunningOnIOS10OrLater() && isPDF) {
base::PostTaskWithTraits(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BACKGROUND},
base::BindBlockArc(^{
NSFileManager* manager = [NSFileManager defaultManager];
NSString* tempDir = NSTemporaryDirectory();
NSError* tempDirError = nil;
// Iterate over files in tmp directory.
for (NSString* file in
[manager contentsOfDirectoryAtPath:tempDir
error:&tempDirError]) {
// If the file is a PDF file, delete it.
if ([[file pathExtension] isEqualToString:@"pdf"]) {
NSError* deletionError = nil;
NSString* fullFilePath =
[tempDir stringByAppendingPathComponent:file];
BOOL success = [manager removeItemAtPath:fullFilePath
error:&deletionError];
if (!success) {
DLOG(ERROR) << "AirPrint unable to remove tmp file:" << file
<< " error: " << deletionError.description;
}
}
}
if (tempDirError) {
DLOG(ERROR) << "AirPrint tmp dir access error:"
<< tempDirError.description;
}
}));
}
};
[printInteractionController presentAnimated:YES
completionHandler:completionHandler];
}
#pragma mark - Public Methods
- (instancetype)initWithContextGetter:
(scoped_refptr<net::URLRequestContextGetter>)getter {
self = [super init];
if (self) {
_requestContextGetter = std::move(getter);
_fetcherDelegate.reset(new PrintPDFFetcherDelegate(self));
}
return self;
}
- (instancetype)init {
NOTREACHED();
return nil;
}
- (void)printView:(UIView*)view
withTitle:(NSString*)title
viewController:(UIViewController*)viewController {
base::RecordAction(base::UserMetricsAction("MobilePrintMenuAirPrint"));
UIPrintInteractionController* printInteractionController =
[UIPrintInteractionController sharedPrintController];
UIPrintInfo* printInfo = [UIPrintInfo printInfo];
printInfo.outputType = UIPrintInfoOutputGeneral;
printInfo.jobName = title;
printInteractionController.printInfo = printInfo;
#if !defined(__IPHONE_10_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0
printInteractionController.showsPageRange = YES;
#endif
// Print Formatters do not work for PDFs in iOS9 WKWebView, but do in iOS10.
// Instead, download the PDF and (eventually) pass it to the
// UIPrintInteractionController. Remove this workaround and all associated PDF
// specific code when iOS9 is deprecated.
BOOL isPDFURL = NO;
if (!base::ios::IsRunningOnIOS10OrLater() &&
[view isMemberOfClass:[WKWebView class]]) {
WKWebView* webView = base::mac::ObjCCastStrict<WKWebView>(view);
NSURL* URL = webView.URL;
CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension,
(__bridge CFStringRef)[[URL path] pathExtension], NULL);
if (UTI) {
CFStringRef MIMEType =
UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
if (MIMEType) {
isPDFURL =
[@(kPDFMimeType) isEqualToString:(__bridge NSString*)MIMEType];
if (isPDFURL) {
[self downloadPDFFileWithURL:net::GURLWithNSURL(URL)
viewController:viewController];
}
CFRelease(MIMEType);
}
CFRelease(UTI);
}
}
if (!isPDFURL) {
UIPrintPageRenderer* renderer = [[UIPrintPageRenderer alloc] init];
[renderer addPrintFormatter:[view viewPrintFormatter]
startingAtPageAtIndex:0];
printInteractionController.printPageRenderer = renderer;
[PrintController
displayPrintInteractionController:printInteractionController
forPDF:NO];
}
}
- (void)dismissAnimated:(BOOL)animated {
_fetcher.reset();
[self dismissPDFDownloadingDialog];
[_PDFDownloadingErrorDialog stop];
_PDFDownloadingErrorDialog = nil;
[[UIPrintInteractionController sharedPrintController]
dismissAnimated:animated];
}
#pragma mark - Private Methods
- (void)showPDFDownloadingDialog:(UIViewController*)viewController {
if (_PDFDownloadingDialog)
return;
NSString* title = l10n_util::GetNSString(IDS_IOS_PRINT_PDF_PREPARATION);
__weak PrintController* weakSelf = self;
ProceduralBlock cancelHandler = ^{
PrintController* strongSelf = weakSelf;
if (strongSelf)
strongSelf->_fetcher.reset();
};
_PDFDownloadingDialog = [[LoadingAlertCoordinator alloc]
initWithBaseViewController:viewController
title:title
cancelHandler:cancelHandler];
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, kPDFDownloadDialogDelay * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
PrintController* strongSelf = weakSelf;
if (!strongSelf)
return;
[strongSelf->_PDFDownloadingDialog start];
});
}
- (void)dismissPDFDownloadingDialog {
[_PDFDownloadingDialog stop];
_PDFDownloadingDialog = nil;
}
- (void)showPDFDownloadErrorWithURL:(const GURL)URL
viewController:(UIViewController*)viewController {
NSString* title = l10n_util::GetNSString(IDS_IOS_PRINT_PDF_ERROR_TITLE);
NSString* message = l10n_util::GetNSString(IDS_IOS_PRINT_PDF_ERROR_SUBTITLE);
_PDFDownloadingErrorDialog =
[[AlertCoordinator alloc] initWithBaseViewController:viewController
title:title
message:message];
__weak PrintController* weakSelf = self;
[_PDFDownloadingErrorDialog
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_PRINT_PDF_TRY_AGAIN)
action:^{
[weakSelf downloadPDFFileWithURL:URL
viewController:viewController];
}
style:UIAlertActionStyleDefault];
[_PDFDownloadingErrorDialog
addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
action:nil
style:UIAlertActionStyleCancel];
[_PDFDownloadingErrorDialog start];
}
- (void)downloadPDFFileWithURL:(const GURL&)URL
viewController:(UIViewController*)viewController {
DCHECK(!_fetcher);
_fetcherDelegate->SetViewController(viewController);
_fetcher = URLFetcher::Create(URL, URLFetcher::GET, _fetcherDelegate.get());
_fetcher->SetRequestContext(_requestContextGetter.get());
_fetcher->Start();
[self showPDFDownloadingDialog:viewController];
}
- (void)finishedDownloadingPDF:(UIViewController*)viewController {
[self dismissPDFDownloadingDialog];
DCHECK(_fetcher);
base::ScopedClosureRunner fetcherResetter(base::BindBlockArc(^{
_fetcher.reset();
}));
int responseCode = _fetcher->GetResponseCode();
std::string response;
std::string MIMEType;
// If the request is not successful or does not match a PDF
// MIME type, show an error.
if (!_fetcher->GetStatus().is_success() || responseCode != 200 ||
!_fetcher->GetResponseAsString(&response) ||
!_fetcher->GetResponseHeaders()->GetMimeType(&MIMEType) ||
MIMEType != kPDFMimeType) {
[self showPDFDownloadErrorWithURL:_fetcher->GetOriginalURL()
viewController:viewController];
return;
}
NSData* data =
[NSData dataWithBytes:response.c_str() length:response.length()];
// If the data cannot be printed, show an error.
if (![UIPrintInteractionController canPrintData:data]) {
[self showPDFDownloadErrorWithURL:_fetcher->GetOriginalURL()
viewController:viewController];
return;
}
UIPrintInteractionController* printInteractionController =
[UIPrintInteractionController sharedPrintController];
printInteractionController.printingItem = data;
[PrintController displayPrintInteractionController:printInteractionController
forPDF:YES];
}
@end