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