print_dialog_cloud.cc revision dc0f95d653279beabeb9817299e2902918ba123e
1// Copyright (c) 2011 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#include "chrome/browser/printing/print_dialog_cloud.h" 6#include "chrome/browser/printing/print_dialog_cloud_internal.h" 7 8#include "base/base64.h" 9#include "base/file_util.h" 10#include "base/json/json_reader.h" 11#include "base/values.h" 12#include "chrome/browser/browser_list.h" 13#include "chrome/browser/debugger/devtools_manager.h" 14#include "chrome/browser/prefs/pref_service.h" 15#include "chrome/browser/printing/cloud_print/cloud_print_url.h" 16#include "chrome/browser/profiles/profile.h" 17#include "chrome/browser/profiles/profile_manager.h" 18#include "chrome/browser/ui/browser_dialogs.h" 19#include "chrome/common/notification_registrar.h" 20#include "chrome/common/notification_source.h" 21#include "chrome/common/notification_type.h" 22#include "chrome/common/pref_names.h" 23#include "chrome/common/render_messages_params.h" 24#include "chrome/common/url_constants.h" 25#include "content/browser/browser_thread.h" 26#include "content/browser/renderer_host/render_view_host.h" 27#include "content/browser/tab_contents/tab_contents.h" 28#include "content/browser/tab_contents/tab_contents_view.h" 29#include "content/browser/webui/web_ui.h" 30#include "ui/base/l10n/l10n_util.h" 31#include "webkit/glue/webpreferences.h" 32 33#include "grit/generated_resources.h" 34 35// This module implements the UI support in Chrome for cloud printing. 36// This means hosting a dialog containing HTML/JavaScript and using 37// the published cloud print user interface integration APIs to get 38// page setup settings from the dialog contents and provide the 39// generated print PDF to the dialog contents for uploading to the 40// cloud print service. 41 42// Currently, the flow between these classes is as follows: 43 44// PrintDialogCloud::CreatePrintDialogForPdf is called from 45// resource_message_filter_gtk.cc once the renderer has informed the 46// renderer host that PDF generation into the renderer host provided 47// temp file has been completed. That call is on the FILE thread. 48// That, in turn, hops over to the UI thread to create an instance of 49// PrintDialogCloud. 50 51// The constructor for PrintDialogCloud creates a 52// CloudPrintHtmlDialogDelegate and asks the current active browser to 53// show an HTML dialog using that class as the delegate. That class 54// hands in the kCloudPrintResourcesURL as the URL to visit. That is 55// recognized by the GetWebUIFactoryFunction as a signal to create an 56// ExternalHtmlDialogUI. 57 58// CloudPrintHtmlDialogDelegate also temporarily owns a 59// CloudPrintFlowHandler, a class which is responsible for the actual 60// interactions with the dialog contents, including handing in the PDF 61// print data and getting any page setup parameters that the dialog 62// contents provides. As part of bringing up the dialog, 63// HtmlDialogUI::RenderViewCreated is called (an override of 64// WebUI::RenderViewCreated). That routine, in turn, calls the 65// delegate's GetWebUIMessageHandlers routine, at which point the 66// ownership of the CloudPrintFlowHandler is handed over. A pointer 67// to the flow handler is kept to facilitate communication back and 68// forth between the two classes. 69 70// The WebUI continues dialog bring-up, calling 71// CloudPrintFlowHandler::RegisterMessages. This is where the 72// additional object model capabilities are registered for the dialog 73// contents to use. It is also at this time that capabilities for the 74// dialog contents are adjusted to allow the dialog contents to close 75// the window. In addition, the pending URL is redirected to the 76// actual cloud print service URL. The flow controller also registers 77// for notification of when the dialog contents finish loading, which 78// is currently used to send the PDF data to the dialog contents. 79 80// In order to send the PDF data to the dialog contents, the flow 81// handler uses a CloudPrintDataSender. It creates one, letting it 82// know the name of the temporary file containing the PDF data, and 83// posts the task of reading the file 84// (CloudPrintDataSender::ReadPrintDataFile) to the file thread. That 85// routine reads in the file, and then hops over to the IO thread to 86// send that data to the dialog contents. 87 88// When the dialog contents are finished (by either being cancelled or 89// hitting the print button), the delegate is notified, and responds 90// that the dialog should be closed, at which point things are torn 91// down and released. 92 93// TODO(scottbyer): 94// http://code.google.com/p/chromium/issues/detail?id=44093 The 95// high-level flow (where the PDF data is generated before even 96// bringing up the dialog) isn't what we want. 97 98namespace internal_cloud_print_helpers { 99 100bool GetDoubleOrInt(const DictionaryValue& dictionary, 101 const std::string& path, 102 double* out_value) { 103 if (!dictionary.GetDouble(path, out_value)) { 104 int int_value = 0; 105 if (!dictionary.GetInteger(path, &int_value)) 106 return false; 107 *out_value = int_value; 108 } 109 return true; 110} 111 112// From the JSON parsed value, get the entries for the page setup 113// parameters. 114bool GetPageSetupParameters(const std::string& json, 115 ViewMsg_Print_Params& parameters) { 116 scoped_ptr<Value> parsed_value(base::JSONReader::Read(json, false)); 117 DLOG_IF(ERROR, (!parsed_value.get() || 118 !parsed_value->IsType(Value::TYPE_DICTIONARY))) 119 << "PageSetup call didn't have expected contents"; 120 if (!parsed_value.get() || !parsed_value->IsType(Value::TYPE_DICTIONARY)) 121 return false; 122 123 bool result = true; 124 DictionaryValue* params = static_cast<DictionaryValue*>(parsed_value.get()); 125 result &= GetDoubleOrInt(*params, "dpi", ¶meters.dpi); 126 result &= GetDoubleOrInt(*params, "min_shrink", ¶meters.min_shrink); 127 result &= GetDoubleOrInt(*params, "max_shrink", ¶meters.max_shrink); 128 result &= params->GetBoolean("selection_only", ¶meters.selection_only); 129 return result; 130} 131 132void CloudPrintDataSenderHelper::CallJavascriptFunction( 133 const std::wstring& function_name) { 134 web_ui_->CallJavascriptFunction(function_name); 135} 136 137void CloudPrintDataSenderHelper::CallJavascriptFunction( 138 const std::wstring& function_name, const Value& arg) { 139 web_ui_->CallJavascriptFunction(function_name, arg); 140} 141 142void CloudPrintDataSenderHelper::CallJavascriptFunction( 143 const std::wstring& function_name, const Value& arg1, const Value& arg2) { 144 web_ui_->CallJavascriptFunction(function_name, arg1, arg2); 145} 146 147// Clears out the pointer we're using to communicate. Either routine is 148// potentially expensive enough that stopping whatever is in progress 149// is worth it. 150void CloudPrintDataSender::CancelPrintDataFile() { 151 base::AutoLock lock(lock_); 152 // We don't own helper, it was passed in to us, so no need to 153 // delete, just let it go. 154 helper_ = NULL; 155} 156 157CloudPrintDataSender::CloudPrintDataSender(CloudPrintDataSenderHelper* helper, 158 const string16& print_job_title) 159 : helper_(helper), 160 print_job_title_(print_job_title) { 161} 162 163CloudPrintDataSender::~CloudPrintDataSender() {} 164 165// Grab the raw PDF file contents and massage them into shape for 166// sending to the dialog contents (and up to the cloud print server) 167// by encoding it and prefixing it with the appropriate mime type. 168// Once that is done, kick off the next part of the task on the IO 169// thread. 170void CloudPrintDataSender::ReadPrintDataFile(const FilePath& path_to_pdf) { 171 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 172 int64 file_size = 0; 173 if (file_util::GetFileSize(path_to_pdf, &file_size) && file_size != 0) { 174 std::string file_data; 175 if (file_size < kuint32max) { 176 file_data.reserve(static_cast<unsigned int>(file_size)); 177 } else { 178 DLOG(WARNING) << " print data file too large to reserve space"; 179 } 180 if (helper_ && file_util::ReadFileToString(path_to_pdf, &file_data)) { 181 std::string base64_data; 182 base::Base64Encode(file_data, &base64_data); 183 std::string header("data:application/pdf;base64,"); 184 base64_data.insert(0, header); 185 scoped_ptr<StringValue> new_data(new StringValue(base64_data)); 186 print_data_.swap(new_data); 187 BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, 188 NewRunnableMethod( 189 this, 190 &CloudPrintDataSender::SendPrintDataFile)); 191 } 192 } 193} 194 195// We have the data in hand that needs to be pushed into the dialog 196// contents; do so from the IO thread. 197 198// TODO(scottbyer): If the print data ends up being larger than the 199// upload limit (currently 10MB), what we need to do is upload that 200// large data to google docs and set the URL in the printing 201// JavaScript to that location, and make sure it gets deleted when not 202// needed. - 4/1/2010 203void CloudPrintDataSender::SendPrintDataFile() { 204 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 205 base::AutoLock lock(lock_); 206 if (helper_ && print_data_.get()) { 207 StringValue title(print_job_title_); 208 209 // Send the print data to the dialog contents. The JavaScript 210 // function is a preliminary API for prototyping purposes and is 211 // subject to change. 212 const_cast<CloudPrintDataSenderHelper*>(helper_)->CallJavascriptFunction( 213 L"printApp._printDataUrl", *print_data_, title); 214 } 215} 216 217 218CloudPrintFlowHandler::CloudPrintFlowHandler(const FilePath& path_to_pdf, 219 const string16& print_job_title) 220 : path_to_pdf_(path_to_pdf), 221 print_job_title_(print_job_title) { 222} 223 224CloudPrintFlowHandler::~CloudPrintFlowHandler() { 225 // This will also cancel any task in flight. 226 CancelAnyRunningTask(); 227} 228 229 230void CloudPrintFlowHandler::SetDialogDelegate( 231 CloudPrintHtmlDialogDelegate* delegate) { 232 // Even if setting a new WebUI, it means any previous task needs 233 // to be cancelled, it's now invalid. 234 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 235 CancelAnyRunningTask(); 236 dialog_delegate_ = delegate; 237} 238 239// Cancels any print data sender we have in flight and removes our 240// reference to it, so when the task that is calling it finishes and 241// removes it's reference, it goes away. 242void CloudPrintFlowHandler::CancelAnyRunningTask() { 243 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 244 if (print_data_sender_.get()) { 245 print_data_sender_->CancelPrintDataFile(); 246 print_data_sender_ = NULL; 247 } 248} 249 250void CloudPrintFlowHandler::RegisterMessages() { 251 if (!web_ui_) 252 return; 253 254 // TODO(scottbyer) - This is where we will register messages for the 255 // UI JS to use. Needed: Call to update page setup parameters. 256 web_ui_->RegisterMessageCallback( 257 "ShowDebugger", 258 NewCallback(this, &CloudPrintFlowHandler::HandleShowDebugger)); 259 web_ui_->RegisterMessageCallback( 260 "SendPrintData", 261 NewCallback(this, &CloudPrintFlowHandler::HandleSendPrintData)); 262 web_ui_->RegisterMessageCallback( 263 "SetPageParameters", 264 NewCallback(this, &CloudPrintFlowHandler::HandleSetPageParameters)); 265 266 if (web_ui_->tab_contents()) { 267 // Also, take the opportunity to set some (minimal) additional 268 // script permissions required for the web UI. 269 270 // TODO(scottbyer): learn how to make sure we're talking to the 271 // right web site first. 272 RenderViewHost* rvh = web_ui_->tab_contents()->render_view_host(); 273 if (rvh && rvh->delegate()) { 274 WebPreferences webkit_prefs = rvh->delegate()->GetWebkitPrefs(); 275 webkit_prefs.allow_scripts_to_close_windows = true; 276 rvh->UpdateWebPreferences(webkit_prefs); 277 } 278 279 // Register for appropriate notifications, and re-direct the URL 280 // to the real server URL, now that we've gotten an HTML dialog 281 // going. 282 NavigationController* controller = &web_ui_->tab_contents()->controller(); 283 NavigationEntry* pending_entry = controller->pending_entry(); 284 if (pending_entry) 285 pending_entry->set_url(CloudPrintURL( 286 web_ui_->GetProfile()).GetCloudPrintServiceDialogURL()); 287 registrar_.Add(this, NotificationType::LOAD_STOP, 288 Source<NavigationController>(controller)); 289 } 290} 291 292void CloudPrintFlowHandler::Observe(NotificationType type, 293 const NotificationSource& source, 294 const NotificationDetails& details) { 295 if (type == NotificationType::LOAD_STOP) { 296 // Choose one or the other. If you need to debug, bring up the 297 // debugger. You can then use the various chrome.send() 298 // registrations above to kick of the various function calls, 299 // including chrome.send("SendPrintData") in the javaScript 300 // console and watch things happen with: 301 // HandleShowDebugger(NULL); 302 HandleSendPrintData(NULL); 303 } 304} 305 306void CloudPrintFlowHandler::HandleShowDebugger(const ListValue* args) { 307 ShowDebugger(); 308} 309 310void CloudPrintFlowHandler::ShowDebugger() { 311 if (web_ui_) { 312 RenderViewHost* rvh = web_ui_->tab_contents()->render_view_host(); 313 if (rvh) 314 DevToolsManager::GetInstance()->OpenDevToolsWindow(rvh); 315 } 316} 317 318scoped_refptr<CloudPrintDataSender> 319CloudPrintFlowHandler::CreateCloudPrintDataSender() { 320 DCHECK(web_ui_); 321 print_data_helper_.reset(new CloudPrintDataSenderHelper(web_ui_)); 322 return new CloudPrintDataSender(print_data_helper_.get(), print_job_title_); 323} 324 325void CloudPrintFlowHandler::HandleSendPrintData(const ListValue* args) { 326 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 327 // This will cancel any ReadPrintDataFile() or SendPrintDataFile() 328 // requests in flight (this is anticipation of when setting page 329 // setup parameters becomes asynchronous and may be set while some 330 // data is in flight). Then we can clear out the print data. 331 CancelAnyRunningTask(); 332 if (web_ui_) { 333 print_data_sender_ = CreateCloudPrintDataSender(); 334 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, 335 NewRunnableMethod( 336 print_data_sender_.get(), 337 &CloudPrintDataSender::ReadPrintDataFile, 338 path_to_pdf_)); 339 } 340} 341 342void CloudPrintFlowHandler::HandleSetPageParameters(const ListValue* args) { 343 std::string json; 344 args->GetString(0, &json); 345 if (json.empty()) { 346 NOTREACHED() << "Empty json string"; 347 return; 348 } 349 350 // These are backstop default values - 72 dpi to match the screen, 351 // 8.5x11 inch paper with margins subtracted (1/4 inch top, left, 352 // right and 0.56 bottom), and the min page shrink and max page 353 // shrink values appear all over the place with no explanation. 354 355 // TODO(scottbyer): Get a Linux/ChromeOS edge for PrintSettings 356 // working so that we can get the default values from there. Fix up 357 // PrintWebViewHelper to do the same. 358 const int kDPI = 72; 359 const int kWidth = static_cast<int>((8.5-0.25-0.25)*kDPI); 360 const int kHeight = static_cast<int>((11-0.25-0.56)*kDPI); 361 const double kMinPageShrink = 1.25; 362 const double kMaxPageShrink = 2.0; 363 364 ViewMsg_Print_Params default_settings; 365 default_settings.printable_size = gfx::Size(kWidth, kHeight); 366 default_settings.dpi = kDPI; 367 default_settings.min_shrink = kMinPageShrink; 368 default_settings.max_shrink = kMaxPageShrink; 369 default_settings.desired_dpi = kDPI; 370 default_settings.document_cookie = 0; 371 default_settings.selection_only = false; 372 373 if (!GetPageSetupParameters(json, default_settings)) { 374 NOTREACHED(); 375 return; 376 } 377 378 // TODO(scottbyer) - Here is where we would kick the originating 379 // renderer thread with these new parameters in order to get it to 380 // re-generate the PDF and hand it back to us. window.print() is 381 // currently synchronous, so there's a lot of work to do to get to 382 // that point. 383} 384 385void CloudPrintFlowHandler::StoreDialogClientSize() const { 386 if (web_ui_ && web_ui_->tab_contents() && web_ui_->tab_contents()->view()) { 387 gfx::Size size = web_ui_->tab_contents()->view()->GetContainerSize(); 388 web_ui_->GetProfile()->GetPrefs()->SetInteger( 389 prefs::kCloudPrintDialogWidth, size.width()); 390 web_ui_->GetProfile()->GetPrefs()->SetInteger( 391 prefs::kCloudPrintDialogHeight, size.height()); 392 } 393} 394 395CloudPrintHtmlDialogDelegate::CloudPrintHtmlDialogDelegate( 396 const FilePath& path_to_pdf, 397 int width, int height, 398 const std::string& json_arguments, 399 const string16& print_job_title, 400 bool modal) 401 : flow_handler_(new CloudPrintFlowHandler(path_to_pdf, print_job_title)), 402 modal_(modal), 403 owns_flow_handler_(true) { 404 Init(width, height, json_arguments); 405} 406 407// For unit testing. 408CloudPrintHtmlDialogDelegate::CloudPrintHtmlDialogDelegate( 409 CloudPrintFlowHandler* flow_handler, 410 int width, int height, 411 const std::string& json_arguments, 412 bool modal) 413 : flow_handler_(flow_handler), 414 modal_(modal), 415 owns_flow_handler_(true) { 416 Init(width, height, json_arguments); 417} 418 419void CloudPrintHtmlDialogDelegate::Init(int width, int height, 420 const std::string& json_arguments) { 421 // This information is needed to show the dialog HTML content. 422 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 423 params_.url = GURL(chrome::kCloudPrintResourcesURL); 424 params_.height = height; 425 params_.width = width; 426 params_.json_input = json_arguments; 427 428 flow_handler_->SetDialogDelegate(this); 429 // If we're not modal we can show the dialog with no browser. 430 // We need this to keep Chrome alive while our dialog is up. 431 if (!modal_) 432 BrowserList::StartKeepAlive(); 433} 434 435CloudPrintHtmlDialogDelegate::~CloudPrintHtmlDialogDelegate() { 436 // If the flow_handler_ is about to outlive us because we don't own 437 // it anymore, we need to have it remove it's reference to us. 438 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 439 flow_handler_->SetDialogDelegate(NULL); 440 if (owns_flow_handler_) { 441 delete flow_handler_; 442 } 443} 444 445bool CloudPrintHtmlDialogDelegate::IsDialogModal() const { 446 return modal_; 447} 448 449std::wstring CloudPrintHtmlDialogDelegate::GetDialogTitle() const { 450 return std::wstring(); 451} 452 453GURL CloudPrintHtmlDialogDelegate::GetDialogContentURL() const { 454 return params_.url; 455} 456 457void CloudPrintHtmlDialogDelegate::GetWebUIMessageHandlers( 458 std::vector<WebUIMessageHandler*>* handlers) const { 459 handlers->push_back(flow_handler_); 460 // We don't own flow_handler_ anymore, but it sticks around until at 461 // least right after OnDialogClosed() is called (and this object is 462 // destroyed). 463 owns_flow_handler_ = false; 464} 465 466void CloudPrintHtmlDialogDelegate::GetDialogSize(gfx::Size* size) const { 467 size->set_width(params_.width); 468 size->set_height(params_.height); 469} 470 471std::string CloudPrintHtmlDialogDelegate::GetDialogArgs() const { 472 return params_.json_input; 473} 474 475void CloudPrintHtmlDialogDelegate::OnDialogClosed( 476 const std::string& json_retval) { 477 // Get the final dialog size and store it. 478 flow_handler_->StoreDialogClientSize(); 479 // If we're modal we can show the dialog with no browser. 480 // End the keep-alive so that Chrome can exit. 481 if (!modal_) 482 BrowserList::EndKeepAlive(); 483 delete this; 484} 485 486void CloudPrintHtmlDialogDelegate::OnCloseContents(TabContents* source, 487 bool* out_close_dialog) { 488 if (out_close_dialog) 489 *out_close_dialog = true; 490} 491 492bool CloudPrintHtmlDialogDelegate::ShouldShowDialogTitle() const { 493 return false; 494} 495 496} // namespace internal_cloud_print_helpers 497 498// static, called on the IO thread. This is the main entry point into 499// creating the dialog. 500 501// TODO(scottbyer): The signature here will need to change as the 502// workflow through the printing code changes to allow for dynamically 503// changing page setup parameters while the dialog is active. 504void PrintDialogCloud::CreatePrintDialogForPdf(const FilePath& path_to_pdf, 505 const string16& print_job_title, 506 bool modal) { 507 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE) || 508 BrowserThread::CurrentlyOn(BrowserThread::UI)); 509 510 BrowserThread::PostTask( 511 BrowserThread::UI, FROM_HERE, 512 NewRunnableFunction(&PrintDialogCloud::CreateDialogImpl, 513 path_to_pdf, 514 print_job_title, 515 modal)); 516} 517 518// static, called from the UI thread. 519void PrintDialogCloud::CreateDialogImpl(const FilePath& path_to_pdf, 520 const string16& print_job_title, 521 bool modal) { 522 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 523 new PrintDialogCloud(path_to_pdf, print_job_title, modal); 524} 525 526// Initialize the print dialog. Called on the UI thread. 527PrintDialogCloud::PrintDialogCloud(const FilePath& path_to_pdf, 528 const string16& print_job_title, 529 bool modal) 530 : browser_(BrowserList::GetLastActive()) { 531 Init(path_to_pdf, print_job_title, modal); 532} 533 534PrintDialogCloud::~PrintDialogCloud() { 535} 536 537void PrintDialogCloud::Init(const FilePath& path_to_pdf, 538 const string16& print_job_title, 539 bool modal) { 540 // TODO(scottbyer): Verify GAIA login valid, execute GAIA login if not (should 541 // be distilled out of bookmark sync.) 542 const int kDefaultWidth = 497; 543 const int kDefaultHeight = 332; 544 string16 job_title = print_job_title; 545 Profile* profile = NULL; 546 if (modal) { 547 DCHECK(browser_); 548 if (job_title.empty() && browser_->GetSelectedTabContents()) 549 job_title = browser_->GetSelectedTabContents()->GetTitle(); 550 profile = browser_->GetProfile(); 551 } else { 552 profile = ProfileManager::GetDefaultProfile(); 553 } 554 DCHECK(profile); 555 PrefService* pref_service = profile->GetPrefs(); 556 DCHECK(pref_service); 557 if (!pref_service->FindPreference(prefs::kCloudPrintDialogWidth)) { 558 pref_service->RegisterIntegerPref(prefs::kCloudPrintDialogWidth, 559 kDefaultWidth); 560 } 561 if (!pref_service->FindPreference(prefs::kCloudPrintDialogHeight)) { 562 pref_service->RegisterIntegerPref(prefs::kCloudPrintDialogHeight, 563 kDefaultHeight); 564 } 565 566 int width = pref_service->GetInteger(prefs::kCloudPrintDialogWidth); 567 int height = pref_service->GetInteger(prefs::kCloudPrintDialogHeight); 568 569 HtmlDialogUIDelegate* dialog_delegate = 570 new internal_cloud_print_helpers::CloudPrintHtmlDialogDelegate( 571 path_to_pdf, width, height, std::string(), job_title, modal); 572 if (modal) { 573 DCHECK(browser_); 574 browser_->BrowserShowHtmlDialog(dialog_delegate, NULL); 575 } else { 576 browser::ShowHtmlDialog(NULL, profile, dialog_delegate); 577 } 578} 579