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