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", &parameters.dpi);
124  result &= GetRealOrInt(*params, "min_shrink", &parameters.min_shrink);
125  result &= GetRealOrInt(*params, "max_shrink", &parameters.max_shrink);
126  result &= params->GetBoolean("selection_only", &parameters.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