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