OLD | NEW |
(Empty) | |
| 1 // Copyright 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/ui/print/print_controller.h" |
| 6 |
| 7 #import <MobileCoreServices/UTType.h> |
| 8 #import <Webkit/Webkit.h> |
| 9 |
| 10 #include <memory> |
| 11 |
| 12 #include "base/callback_helpers.h" |
| 13 #import "base/ios/ios_util.h" |
| 14 #import "base/ios/weak_nsobject.h" |
| 15 #include "base/location.h" |
| 16 #include "base/logging.h" |
| 17 #include "base/mac/bind_objc_block.h" |
| 18 #include "base/mac/foundation_util.h" |
| 19 #import "base/mac/scoped_nsobject.h" |
| 20 #include "base/memory/ref_counted.h" |
| 21 #include "base/metrics/user_metrics.h" |
| 22 #include "base/metrics/user_metrics_action.h" |
| 23 #include "components/strings/grit/components_strings.h" |
| 24 #import "ios/chrome/browser/ui/alert_coordinator/alert_coordinator.h" |
| 25 #import "ios/chrome/browser/ui/alert_coordinator/loading_alert_coordinator.h" |
| 26 #include "ios/chrome/grit/ios_strings.h" |
| 27 #include "ios/web/public/web_thread.h" |
| 28 #import "net/base/mac/url_conversions.h" |
| 29 #include "net/http/http_response_headers.h" |
| 30 #include "net/url_request/url_fetcher.h" |
| 31 #include "net/url_request/url_fetcher_delegate.h" |
| 32 #include "net/url_request/url_request_context_getter.h" |
| 33 #include "ui/base/l10n/l10n_util_mac.h" |
| 34 |
| 35 using net::URLFetcher; |
| 36 using net::URLFetcherDelegate; |
| 37 using net::URLRequestContextGetter; |
| 38 |
| 39 @interface PrintController () |
| 40 |
| 41 // Presents a UIPrintInteractionController with a default completion handler. |
| 42 // |isPDF| indicates if |printInteractionController| is being presented to print |
| 43 // a PDF. |
| 44 + (void)displayPrintInteractionController: |
| 45 (UIPrintInteractionController*)printInteractionController |
| 46 forPDF:(BOOL)isPDF; |
| 47 |
| 48 // Shows a dialog on |viewController| indicating that the print preview is being |
| 49 // prepared. The dialog will appear only if the download has not completed or |
| 50 // been cancelled |kPDFDownloadDialogDelay| seconds after this method is called. |
| 51 - (void)showPDFDownloadingDialog:(UIViewController*)viewController; |
| 52 |
| 53 // Dismisses the dialog which indicates that the print preview is being |
| 54 // prepared. |
| 55 - (void)dismissPDFDownloadingDialog; |
| 56 |
| 57 // Shows an dialog on |viewController| indicating that there was an error when |
| 58 // preparing the print preview, and providing the ability to retry. |URL| is the |
| 59 // URL of the PDF which had an error. |
| 60 - (void)showPDFDownloadErrorWithURL:(const GURL)URL |
| 61 viewController:(UIViewController*)viewController; |
| 62 |
| 63 // Handles downloading the file at |URL| and presents dialogs on |
| 64 // |viewController| if necessary. |
| 65 - (void)downloadPDFFileWithURL:(const GURL&)URL |
| 66 viewController:(UIViewController*)viewController; |
| 67 |
| 68 // Accesses the result of the PDF download, and if successful, presents the |
| 69 // AirPrint menu with the downloaded PDF. It also ensures that the |
| 70 // PDFDownloadingDialog is dismissed, and presents an error dialog on |
| 71 // |viewController| if the download fails. This method should be called only by |
| 72 // the URLFetcherDelegate. |
| 73 - (void)finishedDownloadingPDF:(UIViewController*)viewController; |
| 74 |
| 75 @end |
| 76 |
| 77 namespace { |
| 78 |
| 79 // The MIME type of a PDF file contained in a Web view. |
| 80 const char kPDFMimeType[] = "application/pdf"; |
| 81 |
| 82 // Delay after downloading begins that the |_PDFDownloadingDialog| appears. |
| 83 const int64_t kPDFDownloadDialogDelay = 1; |
| 84 |
| 85 // A delegate for the URLFetcher to tell owning PrintController that the |
| 86 // download is complete. |
| 87 class PrintPDFFetcherDelegate : public URLFetcherDelegate { |
| 88 public: |
| 89 explicit PrintPDFFetcherDelegate(PrintController* owner) : owner_(owner) {} |
| 90 void OnURLFetchComplete(const URLFetcher* source) override { |
| 91 DCHECK(view_controller_); |
| 92 [owner_ finishedDownloadingPDF:view_controller_]; |
| 93 } |
| 94 |
| 95 // The ViewController used to display an error if the download failed. |
| 96 void SetViewController(UIViewController* view_controller) { |
| 97 view_controller_ = view_controller; |
| 98 } |
| 99 |
| 100 private: |
| 101 __unsafe_unretained PrintController* owner_; |
| 102 __unsafe_unretained UIViewController* view_controller_; |
| 103 DISALLOW_COPY_AND_ASSIGN(PrintPDFFetcherDelegate); |
| 104 }; |
| 105 } // namespace |
| 106 |
| 107 @implementation PrintController { |
| 108 // URLFetcher to download the PDF pointed to by the WKWebView. |
| 109 std::unique_ptr<URLFetcher> _fetcher; |
| 110 |
| 111 // A delegate to bridge between PrintController and the URLFetcher callback. |
| 112 std::unique_ptr<PrintPDFFetcherDelegate> _fetcherDelegate; |
| 113 |
| 114 // Context getter required by the URLFetcher. |
| 115 scoped_refptr<URLRequestContextGetter> _requestContextGetter; |
| 116 |
| 117 // A dialog which indicates that the print preview is being prepared. It |
| 118 // offers a cancel button which will cancel the download. It is created when |
| 119 // downloading begins and is released when downloading ends (either due to |
| 120 // cancellation or completion). |
| 121 base::scoped_nsobject<LoadingAlertCoordinator> _PDFDownloadingDialog; |
| 122 |
| 123 // A dialog which indicates that the print preview failed. |
| 124 base::scoped_nsobject<AlertCoordinator> _PDFDownloadingErrorDialog; |
| 125 } |
| 126 |
| 127 #pragma mark - Class methods. |
| 128 |
| 129 + (void)displayPrintInteractionController: |
| 130 (UIPrintInteractionController*)printInteractionController |
| 131 forPDF:(BOOL)isPDF { |
| 132 void (^completionHandler)(UIPrintInteractionController*, BOOL, NSError*) = ^( |
| 133 UIPrintInteractionController* printInteractionController, BOOL completed, |
| 134 NSError* error) { |
| 135 if (error) |
| 136 DLOG(ERROR) << "Air printing error: " << error.description; |
| 137 |
| 138 // When printing a NSData object given to the |
| 139 // UIPrintInteractionController's |printingItem| object, a PDF file |
| 140 // representing the NSData object is created in the app's tmp directory |
| 141 // by the OS and never deleted. So, this workaround deletes PDF files in |
| 142 // tmp now that printing is done. When iOS9 is deprecated, this can |
| 143 // be removed since PDFs will no longer need to be downloaded to print, |
| 144 // and |printingItem| will no longer be used. |
| 145 if (!base::ios::IsRunningOnIOS10OrLater() && isPDF) { |
| 146 web::WebThread::PostBlockingPoolTask( |
| 147 FROM_HERE, base::BindBlock(^{ |
| 148 NSFileManager* manager = [NSFileManager defaultManager]; |
| 149 NSString* tempDir = NSTemporaryDirectory(); |
| 150 NSError* tempDirError = nil; |
| 151 |
| 152 // Iterate over files in tmp directory. |
| 153 for (NSString* file in |
| 154 [manager contentsOfDirectoryAtPath:tempDir |
| 155 error:&tempDirError]) { |
| 156 // If the file is a PDF file, delete it. |
| 157 if ([[file pathExtension] isEqualToString:@"pdf"]) { |
| 158 NSError* deletionError = nil; |
| 159 NSString* fullFilePath = |
| 160 [tempDir stringByAppendingPathComponent:file]; |
| 161 BOOL success = [manager removeItemAtPath:fullFilePath |
| 162 error:&deletionError]; |
| 163 if (!success) { |
| 164 DLOG(ERROR) << "AirPrint unable to remove tmp file:" << file |
| 165 << " error: " << deletionError.description; |
| 166 } |
| 167 } |
| 168 } |
| 169 if (tempDirError) { |
| 170 DLOG(ERROR) << "AirPrint tmp dir access error:" |
| 171 << tempDirError.description; |
| 172 } |
| 173 })); |
| 174 } |
| 175 }; |
| 176 [printInteractionController presentAnimated:YES |
| 177 completionHandler:completionHandler]; |
| 178 } |
| 179 |
| 180 #pragma mark - Public Methods |
| 181 |
| 182 - (instancetype)initWithContextGetter: |
| 183 (scoped_refptr<net::URLRequestContextGetter>)getter { |
| 184 self = [super init]; |
| 185 if (self) { |
| 186 _requestContextGetter = std::move(getter); |
| 187 _fetcherDelegate.reset(new PrintPDFFetcherDelegate(self)); |
| 188 } |
| 189 return self; |
| 190 } |
| 191 |
| 192 - (instancetype)init { |
| 193 NOTREACHED(); |
| 194 return nil; |
| 195 } |
| 196 |
| 197 - (void)printView:(UIView*)view |
| 198 withTitle:(NSString*)title |
| 199 viewController:(UIViewController*)viewController { |
| 200 base::RecordAction(base::UserMetricsAction("MobilePrintMenuAirPrint")); |
| 201 UIPrintInteractionController* printInteractionController = |
| 202 [UIPrintInteractionController sharedPrintController]; |
| 203 UIPrintInfo* printInfo = [UIPrintInfo printInfo]; |
| 204 printInfo.outputType = UIPrintInfoOutputGeneral; |
| 205 printInfo.jobName = title; |
| 206 printInteractionController.printInfo = printInfo; |
| 207 printInteractionController.showsPageRange = YES; |
| 208 |
| 209 // Print Formatters do not work for PDFs in iOS9 WKWebView, but do in iOS10. |
| 210 // Instead, download the PDF and (eventually) pass it to the |
| 211 // UIPrintInteractionController. Remove this workaround and all associated PDF |
| 212 // specific code when iOS9 is deprecated. |
| 213 BOOL isPDFURL = NO; |
| 214 if (!base::ios::IsRunningOnIOS10OrLater() && |
| 215 [view isMemberOfClass:[WKWebView class]]) { |
| 216 WKWebView* webView = base::mac::ObjCCastStrict<WKWebView>(view); |
| 217 NSURL* URL = webView.URL; |
| 218 CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag( |
| 219 kUTTagClassFilenameExtension, |
| 220 (__bridge CFStringRef)[[URL path] pathExtension], NULL); |
| 221 if (UTI) { |
| 222 CFStringRef MIMEType = |
| 223 UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType); |
| 224 if (MIMEType) { |
| 225 isPDFURL = |
| 226 [@(kPDFMimeType) isEqualToString:(__bridge NSString*)MIMEType]; |
| 227 if (isPDFURL) { |
| 228 [self downloadPDFFileWithURL:net::GURLWithNSURL(URL) |
| 229 viewController:viewController]; |
| 230 } |
| 231 CFRelease(MIMEType); |
| 232 } |
| 233 CFRelease(UTI); |
| 234 } |
| 235 } |
| 236 |
| 237 if (!isPDFURL) { |
| 238 base::scoped_nsobject<UIPrintPageRenderer> renderer( |
| 239 [[UIPrintPageRenderer alloc] init]); |
| 240 [renderer addPrintFormatter:[view viewPrintFormatter] |
| 241 startingAtPageAtIndex:0]; |
| 242 printInteractionController.printPageRenderer = renderer; |
| 243 [PrintController |
| 244 displayPrintInteractionController:printInteractionController |
| 245 forPDF:NO]; |
| 246 } |
| 247 } |
| 248 |
| 249 - (void)dismissAnimated:(BOOL)animated { |
| 250 _fetcher.reset(); |
| 251 [self dismissPDFDownloadingDialog]; |
| 252 [_PDFDownloadingErrorDialog stop]; |
| 253 _PDFDownloadingErrorDialog.reset(); |
| 254 [[UIPrintInteractionController sharedPrintController] |
| 255 dismissAnimated:animated]; |
| 256 } |
| 257 |
| 258 #pragma mark - Private Methods |
| 259 |
| 260 - (void)showPDFDownloadingDialog:(UIViewController*)viewController { |
| 261 if (_PDFDownloadingDialog) |
| 262 return; |
| 263 |
| 264 NSString* title = l10n_util::GetNSString(IDS_IOS_PRINT_PDF_PREPARATION); |
| 265 |
| 266 base::WeakNSObject<PrintController> weakSelf(self); |
| 267 ProceduralBlock cancelHandler = ^{ |
| 268 base::scoped_nsobject<PrintController> strongSelf([weakSelf retain]); |
| 269 if (strongSelf) |
| 270 strongSelf.get()->_fetcher.reset(); |
| 271 }; |
| 272 |
| 273 _PDFDownloadingDialog.reset([[LoadingAlertCoordinator alloc] |
| 274 initWithBaseViewController:viewController |
| 275 title:title |
| 276 cancelHandler:cancelHandler]); |
| 277 |
| 278 dispatch_after( |
| 279 dispatch_time(DISPATCH_TIME_NOW, kPDFDownloadDialogDelay * NSEC_PER_SEC), |
| 280 dispatch_get_main_queue(), ^{ |
| 281 base::scoped_nsobject<PrintController> strongSelf([weakSelf retain]); |
| 282 if (!strongSelf) |
| 283 return; |
| 284 [strongSelf.get()->_PDFDownloadingDialog start]; |
| 285 }); |
| 286 } |
| 287 |
| 288 - (void)dismissPDFDownloadingDialog { |
| 289 [_PDFDownloadingDialog stop]; |
| 290 _PDFDownloadingDialog.reset(); |
| 291 } |
| 292 |
| 293 - (void)showPDFDownloadErrorWithURL:(const GURL)URL |
| 294 viewController:(UIViewController*)viewController { |
| 295 NSString* title = l10n_util::GetNSString(IDS_IOS_PRINT_PDF_ERROR_TITLE); |
| 296 NSString* message = l10n_util::GetNSString(IDS_IOS_PRINT_PDF_ERROR_SUBTITLE); |
| 297 |
| 298 _PDFDownloadingErrorDialog.reset([[AlertCoordinator alloc] |
| 299 initWithBaseViewController:viewController |
| 300 title:title |
| 301 message:message]); |
| 302 |
| 303 base::WeakNSObject<PrintController> weakSelf(self); |
| 304 |
| 305 [_PDFDownloadingErrorDialog |
| 306 addItemWithTitle:l10n_util::GetNSString(IDS_IOS_PRINT_PDF_TRY_AGAIN) |
| 307 action:^{ |
| 308 [weakSelf downloadPDFFileWithURL:URL |
| 309 viewController:viewController]; |
| 310 } |
| 311 style:UIAlertActionStyleDefault]; |
| 312 |
| 313 [_PDFDownloadingErrorDialog |
| 314 addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
| 315 action:nil |
| 316 style:UIAlertActionStyleCancel]; |
| 317 |
| 318 [_PDFDownloadingErrorDialog start]; |
| 319 } |
| 320 |
| 321 - (void)downloadPDFFileWithURL:(const GURL&)URL |
| 322 viewController:(UIViewController*)viewController { |
| 323 DCHECK(!_fetcher); |
| 324 _fetcherDelegate->SetViewController(viewController); |
| 325 _fetcher = URLFetcher::Create(URL, URLFetcher::GET, _fetcherDelegate.get()); |
| 326 _fetcher->SetRequestContext(_requestContextGetter.get()); |
| 327 _fetcher->Start(); |
| 328 [self showPDFDownloadingDialog:viewController]; |
| 329 } |
| 330 |
| 331 - (void)finishedDownloadingPDF:(UIViewController*)viewController { |
| 332 [self dismissPDFDownloadingDialog]; |
| 333 DCHECK(_fetcher); |
| 334 base::ScopedClosureRunner fetcherResetter(base::BindBlock(^{ |
| 335 _fetcher.reset(); |
| 336 })); |
| 337 int responseCode = _fetcher->GetResponseCode(); |
| 338 std::string response; |
| 339 std::string MIMEType; |
| 340 // If the request is not successful or does not match a PDF |
| 341 // MIME type, show an error. |
| 342 if (!_fetcher->GetStatus().is_success() || responseCode != 200 || |
| 343 !_fetcher->GetResponseAsString(&response) || |
| 344 !_fetcher->GetResponseHeaders()->GetMimeType(&MIMEType) || |
| 345 MIMEType != kPDFMimeType) { |
| 346 [self showPDFDownloadErrorWithURL:_fetcher->GetOriginalURL() |
| 347 viewController:viewController]; |
| 348 return; |
| 349 } |
| 350 NSData* data = |
| 351 [NSData dataWithBytes:response.c_str() length:response.length()]; |
| 352 // If the data cannot be printed, show an error. |
| 353 if (![UIPrintInteractionController canPrintData:data]) { |
| 354 [self showPDFDownloadErrorWithURL:_fetcher->GetOriginalURL() |
| 355 viewController:viewController]; |
| 356 return; |
| 357 } |
| 358 UIPrintInteractionController* printInteractionController = |
| 359 [UIPrintInteractionController sharedPrintController]; |
| 360 printInteractionController.printingItem = data; |
| 361 [PrintController displayPrintInteractionController:printInteractionController |
| 362 forPDF:YES]; |
| 363 } |
| 364 |
| 365 @end |
OLD | NEW |