diff options
author | maruel@chromium.org <maruel@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-14 00:38:12 +0000 |
---|---|---|
committer | maruel@chromium.org <maruel@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-14 00:38:12 +0000 |
commit | 73852b8f9c03c6b7a27436f828ed888f71232257 (patch) | |
tree | da07fd53317f13964f93383f9591fcdfdfa61ea7 /chrome/browser/printing | |
parent | 564551a2ece790b22fd2a70aeb8591805fe943be (diff) | |
download | chromium_src-73852b8f9c03c6b7a27436f828ed888f71232257.zip chromium_src-73852b8f9c03c6b7a27436f828ed888f71232257.tar.gz chromium_src-73852b8f9c03c6b7a27436f828ed888f71232257.tar.bz2 |
[Large; Chromium OS] Work to host the cloud print dialog when built
for Chromium OS. Currently disabled by default behind a command line
switch, and containing a non-real URL for now, this code is at
prototype level. It works (when enabled and pointed at a functioning
cloud print service URL), has the beginnings of some unit tests, and
has the beginnings of deeper communication with the dialog contents,
and it shuts off the DOM UI access from the dialog contents.
Patch contributed by Scott Byer
Review URL: http://codereview.chromium.org/1769006
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@47228 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/printing')
-rw-r--r-- | chrome/browser/printing/print_dialog_cloud.cc | 480 | ||||
-rw-r--r-- | chrome/browser/printing/print_dialog_cloud.h | 38 | ||||
-rw-r--r-- | chrome/browser/printing/print_dialog_cloud_internal.h | 172 | ||||
-rw-r--r-- | chrome/browser/printing/print_dialog_cloud_uitest.cc | 176 | ||||
-rw-r--r-- | chrome/browser/printing/print_dialog_cloud_unittest.cc | 189 |
5 files changed, 1055 insertions, 0 deletions
diff --git a/chrome/browser/printing/print_dialog_cloud.cc b/chrome/browser/printing/print_dialog_cloud.cc new file mode 100644 index 0000000..02385d2 --- /dev/null +++ b/chrome/browser/printing/print_dialog_cloud.cc @@ -0,0 +1,480 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/printing/print_dialog_cloud.h" +#include "chrome/browser/printing/print_dialog_cloud_internal.h" + +#include "app/l10n_util.h" +#include "base/base64.h" +#include "base/file_util.h" +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/values.h" +#include "chrome/browser/browser_list.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/dom_ui/dom_ui.h" +#include "chrome/browser/dom_ui/dom_ui_util.h" +#include "chrome/browser/dom_ui/html_dialog_ui.h" +#include "chrome/browser/debugger/devtools_manager.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/renderer_host/render_view_host.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_registrar.h" +#include "chrome/common/notification_source.h" +#include "chrome/common/render_messages.h" +#include "chrome/common/url_constants.h" + +#include "grit/generated_resources.h" + +// This module implements the UI support in Chrome for cloud printing. +// This means hosting a dialog containing HTML/JavaScript and using +// the published cloud print user interface integration APIs to get +// page setup settings from the dialog contents and provide the +// generated print PDF to the dialog contents for uploading to the +// cloud print service. + +// Currently, the flow between these classes is as follows: + +// PrintDialogCloud::CreatePrintDialogForPdf is called from +// resource_message_filter_gtk.cc once the renderer has informed the +// renderer host that PDF generation into the renderer host provided +// temp file has been completed. That call is on the IO thread. +// That, in turn, hops over to the UI thread to create an instance of +// PrintDialogCloud. + +// The constructor for PrintDialogCloud creates a +// CloudPrintHtmlDialogDelegate and asks the current active browser to +// show an HTML dialog using that class as the delegate. That class +// hands in the kCloudPrintResourcesURL as the URL to visit. That is +// recognized by the GetDOMUIFactoryFunction as a signal to create an +// ExternalHtmlDialogUI. + +// CloudPrintHtmlDialogDelegate also temporarily owns a +// CloudPrintFlowHandler, a class which is responsible for the actual +// interactions with the dialog contents, including handing in the PDF +// print data and getting any page setup parameters that the dialog +// contents provides. As part of bringing up the dialog, +// HtmlDialogUI::RenderViewCreated is called (an override of +// DOMUI::RenderViewCreated). That routine, in turn, calls the +// delegate's GetDOMMessageHandlers routine, at which point the +// ownership of the CloudPrintFlowHandler is handed over. A pointer +// to the flow handler is kept to facilitate communication back and +// forth between the two classes. + +// The DOMUI continues dialog bring-up, calling +// CloudPrintFlowHandler::RegisterMessages. This is where the +// additional object model capabilities are registered for the dialog +// contents to use. It is also at this time that capabilities for the +// dialog contents are adjusted to allow the dialog contents to close +// the window. In addition, the pending URL is redirected to the +// actual cloud print service URL. The flow controller also registers +// for notification of when the dialog contents finish loading, which +// is currently used to send the PDF data to the dialog contents. + +// In order to send the PDF data to the dialog contents, the flow +// handler uses a CloudPrintDataSender. It creates one, letting it +// know the name of the temporary file containing the PDF data, and +// posts the task of reading the file +// (CloudPrintDataSender::ReadPrintDataFile) to the file thread. That +// routine reads in the file, and then hops over to the IO thread to +// send that data to the dialog contents. + +// When the dialog contents are finished (by either being cancelled or +// hitting the print button), the delegate is notified, and responds +// that the dialog should be closed, at which point things are torn +// down and released. + +// TODO(scottbyer): +// http://code.google.com/p/chromium/issues/detail?id=44093 The +// high-level flow (where the PDF data is generated before even +// bringing up the dialog) isn't what we want. + + +namespace internal_cloud_print_helpers { + +// TODO(scottbyer): Replace with the real public URL when we have one. +// That, and get it into the profile instead of as a hardwired +// constant. +const char* const kCloudPrintDialogUrl = + "http://placeholderurl.ned/printing/client/dialog.html"; + +bool GetRealOrInt(const DictionaryValue& dictionary, + const std::wstring& path, + double* out_value) { + if (!dictionary.GetReal(path, out_value)) { + int int_value = 0; + if (!dictionary.GetInteger(path, &int_value)) + return false; + *out_value = int_value; + } + return true; +} + +// From the JSON parsed value, get the entries for the page setup +// parameters. +bool GetPageSetupParameters(const std::string& json, + ViewMsg_Print_Params& parameters) { + scoped_ptr<Value> parsed_value(base::JSONReader::Read(json, false)); + DLOG_IF(ERROR, (!parsed_value.get() || + !parsed_value->IsType(Value::TYPE_DICTIONARY))) + << "PageSetup call didn't have expected contents"; + if (!parsed_value.get() || !parsed_value->IsType(Value::TYPE_DICTIONARY)) + return false; + + bool result = true; + DictionaryValue* params = static_cast<DictionaryValue*>(parsed_value.get()); + result &= GetRealOrInt(*params, L"dpi", ¶meters.dpi); + result &= GetRealOrInt(*params, L"min_shrink", ¶meters.min_shrink); + result &= GetRealOrInt(*params, L"max_shrink", ¶meters.max_shrink); + result &= params->GetBoolean(L"selection_only", ¶meters.selection_only); + return result; +} + +void CloudPrintDataSenderHelper::CallJavascriptFunction( + const std::wstring& function_name) { + dom_ui_->CallJavascriptFunction(function_name); +} + +void CloudPrintDataSenderHelper::CallJavascriptFunction( + const std::wstring& function_name, const Value& arg) { + dom_ui_->CallJavascriptFunction(function_name, arg); +} + +void CloudPrintDataSenderHelper::CallJavascriptFunction( + const std::wstring& function_name, const Value& arg1, const Value& arg2) { + dom_ui_->CallJavascriptFunction(function_name, arg1, arg2); +} + +// Clears out the pointer we're using to communicate. Either routine is +// potentially expensive enough that stopping whatever is in progress +// is worth it. +void CloudPrintDataSender::CancelPrintDataFile() { + AutoLock lock(lock_); + // We don't own helper, it was passed in to us, so no need to + // delete, just let it go. + helper_ = NULL; +} + +// Grab the raw PDF file contents and massage them into shape for +// sending to the dialog contents (and up to the cloud print server) +// by encoding it and prefixing it with the appropriate mime type. +// Once that is done, kick off the next part of the task on the IO +// thread. +void CloudPrintDataSender::ReadPrintDataFile(const FilePath& path_to_pdf) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE)); + int64 file_size = 0; + if (file_util::GetFileSize(path_to_pdf, &file_size) && file_size != 0) { + std::string file_data; + if (file_size < kuint32max) { + file_data.reserve(static_cast<unsigned int>(file_size)); + } else { + DLOG(WARNING) << " print data file too large to reserve space"; + } + if (helper_ && file_util::ReadFileToString(path_to_pdf, &file_data)) { + std::string base64_data; + base::Base64Encode(file_data, &base64_data); + std::string header("data:application/pdf;base64,"); + base64_data.insert(0, header); + scoped_ptr<StringValue> new_data(new StringValue(base64_data)); + print_data_.swap(new_data); + ChromeThread::PostTask(ChromeThread::IO, FROM_HERE, + NewRunnableMethod( + this, + &CloudPrintDataSender::SendPrintDataFile)); + } + } +} + +// We have the data in hand that needs to be pushed into the dialog +// contents; do so from the IO thread. + +// TODO(scottbyer): If the print data ends up being larger than the +// upload limit (currently 10MB), what we need to do is upload that +// large data to google docs and set the URL in the printing +// JavaScript to that location, and make sure it gets deleted when not +// needed. - 4/1/2010 +void CloudPrintDataSender::SendPrintDataFile() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + AutoLock lock(lock_); + if (helper_ && print_data_.get()) { + // TODO(scottbyer) - fill this in with the title or URL of the + // original page. + StringValue title("Chrome Print Test"); + + // Send the print data to the dialog contents. The JavaScript + // function is a preliminary API for prototyping purposes and is + // subject to change. + const_cast<CloudPrintDataSenderHelper*>(helper_)->CallJavascriptFunction( + L"printApp._printDataUrl", *print_data_, title); + } +} + + +void CloudPrintFlowHandler::SetDialogDelegate( + CloudPrintHtmlDialogDelegate* delegate) { + // Even if setting a new dom_ui, it means any previous task needs + // to be cancelled, it's now invalid. + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + CancelAnyRunningTask(); + dialog_delegate_ = delegate; +} + +// Cancels any print data sender we have in flight and removes our +// reference to it, so when the task that is calling it finishes and +// removes it's reference, it goes away. +void CloudPrintFlowHandler::CancelAnyRunningTask() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + if (print_data_sender_.get()) { + print_data_sender_->CancelPrintDataFile(); + print_data_sender_ = NULL; + } +} + + +void CloudPrintFlowHandler::RegisterMessages() { + if (!dom_ui_) + return; + + // TODO(scottbyer) - This is where we will register messages for the + // UI JS to use. Needed: Call to update page setup parameters. + dom_ui_->RegisterMessageCallback( + "ShowDebugger", + NewCallback(this, &CloudPrintFlowHandler::HandleShowDebugger)); + dom_ui_->RegisterMessageCallback( + "SendPrintData", + NewCallback(this, &CloudPrintFlowHandler::HandleSendPrintData)); + dom_ui_->RegisterMessageCallback( + "SetPageParameters", + NewCallback(this, &CloudPrintFlowHandler::HandleSetPageParameters)); + + if (dom_ui_->tab_contents()) { + // Also, take the opportunity to set some (minimal) additional + // script permissions required for the web UI. + + // TODO(scottbyer): learn how to make sure we're talking to the + // right web site first. + RenderViewHost* rvh = dom_ui_->tab_contents()->render_view_host(); + if (rvh && rvh->delegate()) { + WebPreferences webkit_prefs = rvh->delegate()->GetWebkitPrefs(); + webkit_prefs.allow_scripts_to_close_windows = true; + rvh->UpdateWebPreferences(webkit_prefs); + } + + // Register for appropriate notifications, and re-direct the URL + // to the real server URL, now that we've gotten an HTML dialog + // going. + NavigationController* controller = &dom_ui_->tab_contents()->controller(); + NavigationEntry* pending_entry = controller->pending_entry(); + if (pending_entry) + pending_entry->set_url(GURL(kCloudPrintDialogUrl)); + registrar_.Add(this, NotificationType::LOAD_STOP, + Source<NavigationController>(controller)); + } +} + +void CloudPrintFlowHandler::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + if (type == NotificationType::LOAD_STOP) { + // Choose one or the other. If you need to debug, bring up the + // debugger. You can then use the various chrome.send() + // registrations above to kick of the various function calls, + // including chrome.send("SendPrintData") in the javaScript + // console and watch things happen with: + // HandleShowDebugger(NULL); + HandleSendPrintData(NULL); + } +} + +void CloudPrintFlowHandler::HandleShowDebugger(const Value* value) { + ShowDebugger(); +} + +void CloudPrintFlowHandler::ShowDebugger() { + if (dom_ui_) { + RenderViewHost* rvh = dom_ui_->tab_contents()->render_view_host(); + if (rvh) + DevToolsManager::GetInstance()->OpenDevToolsWindow(rvh); + } +} + +scoped_refptr<CloudPrintDataSender> +CloudPrintFlowHandler::CreateCloudPrintDataSender() { + DCHECK(dom_ui_); + print_data_helper_.reset(new CloudPrintDataSenderHelper(dom_ui_)); + return new CloudPrintDataSender(print_data_helper_.get()); +} + +void CloudPrintFlowHandler::HandleSendPrintData(const Value* value) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + // This will cancel any ReadPrintDataFile() or SendPrintDataFile() + // requests in flight (this is anticipation of when setting page + // setup parameters becomes asynchronous and may be set while some + // data is in flight). Then we can clear out the print data. + CancelAnyRunningTask(); + if (dom_ui_) { + print_data_sender_ = CreateCloudPrintDataSender(); + ChromeThread::PostTask(ChromeThread::FILE, FROM_HERE, + NewRunnableMethod( + print_data_sender_.get(), + &CloudPrintDataSender::ReadPrintDataFile, + path_to_pdf_)); + } +} + +void CloudPrintFlowHandler::HandleSetPageParameters(const Value* value) { + std::string json(dom_ui_util::GetJsonResponseFromFirstArgumentInList(value)); + if (json.empty()) + return; + + // These are backstop default values - 72 dpi to match the screen, + // 8.5x11 inch paper with margins subtracted (1/4 inch top, left, + // right and 0.56 bottom), and the min page shrink and max page + // shrink values appear all over the place with no explanation. + + // TODO(scottbyer): Get a Linux/ChromeOS edge for PrintSettings + // working so that we can get the default values from there. Fix up + // PrintWebViewHelper to do the same. + const int kDPI = 72; + const int kWidth = static_cast<int>((8.5-0.25-0.25)*kDPI); + const int kHeight = static_cast<int>((11-0.25-0.56)*kDPI); + const double kMinPageShrink = 1.25; + const double kMaxPageShrink = 2.0; + + ViewMsg_Print_Params default_settings; + default_settings.printable_size = gfx::Size(kWidth, kHeight); + default_settings.dpi = kDPI; + default_settings.min_shrink = kMinPageShrink; + default_settings.max_shrink = kMaxPageShrink; + default_settings.desired_dpi = kDPI; + default_settings.document_cookie = 0; + default_settings.selection_only = false; + + if (!GetPageSetupParameters(json, default_settings)) { + NOTREACHED(); + return; + } + + // TODO(scottbyer) - Here is where we would kick the originating + // renderer thread with these new parameters in order to get it to + // re-generate the PDF and hand it back to us. window.print() is + // currently synchronous, so there's a lot of work to do to get to + // that point. +} + +CloudPrintHtmlDialogDelegate::CloudPrintHtmlDialogDelegate( + const FilePath& path_to_pdf, + int width, int height, + const std::string& json_arguments) + : flow_handler_(new CloudPrintFlowHandler(path_to_pdf)), + owns_flow_handler_(true) { + Init(width, height, json_arguments); +} + +CloudPrintHtmlDialogDelegate::CloudPrintHtmlDialogDelegate( + CloudPrintFlowHandler* flow_handler, + int width, int height, + const std::string& json_arguments) + : flow_handler_(flow_handler), + owns_flow_handler_(false) { + Init(width, height, json_arguments); +} + +void CloudPrintHtmlDialogDelegate::Init( + int width, int height, const std::string& json_arguments) { + // This information is needed to show the dialog HTML content. + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + std::string cloud_print_url(chrome::kCloudPrintResourcesURL); + params_.url = GURL(cloud_print_url); + params_.height = height; + params_.width = width; + params_.json_input = json_arguments; + + flow_handler_->SetDialogDelegate(this); +} + +CloudPrintHtmlDialogDelegate::~CloudPrintHtmlDialogDelegate() { + // If the flow_handler_ is about to outlive us because we don't own + // it anymore, we need to have it remove it's reference to us. + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + flow_handler_->SetDialogDelegate(NULL); + if (owns_flow_handler_) { + delete flow_handler_; + } +} + +bool CloudPrintHtmlDialogDelegate::IsDialogModal() const { + return true; +} + +std::wstring CloudPrintHtmlDialogDelegate::GetDialogTitle() const { + return l10n_util::GetString(IDS_CLOUD_PRINT_TITLE); +} + +GURL CloudPrintHtmlDialogDelegate::GetDialogContentURL() const { + return params_.url; +} + +void CloudPrintHtmlDialogDelegate::GetDOMMessageHandlers( + std::vector<DOMMessageHandler*>* handlers) const { + handlers->push_back(flow_handler_); + // We don't own flow_handler_ anymore, but it sticks around until at + // least right after OnDialogClosed() is called (and this object is + // destroyed). + owns_flow_handler_ = false; +} + +void CloudPrintHtmlDialogDelegate::GetDialogSize(gfx::Size* size) const { + size->set_width(params_.width); + size->set_height(params_.height); +} + +std::string CloudPrintHtmlDialogDelegate::GetDialogArgs() const { + return params_.json_input; +} + +void CloudPrintHtmlDialogDelegate::OnDialogClosed( + const std::string& json_retval) { + delete this; +} + +} // end of namespace internal_cloud_print_helpers + +// static, called on the IO thread. This is the main entry point into +// creating the dialog. + +// TODO(scottbyer): The signature here will need to change as the +// workflow through the printing code changes to allow for dynamically +// changing page setup parameters while the dialog is active. +void PrintDialogCloud::CreatePrintDialogForPdf(const FilePath& path_to_pdf) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + + ChromeThread::PostTask( + ChromeThread::UI, FROM_HERE, + NewRunnableFunction(&PrintDialogCloud::CreateDialogImpl, path_to_pdf)); +} + +// static, called from the UI thread. +void PrintDialogCloud::CreateDialogImpl(const FilePath& path_to_pdf) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + new PrintDialogCloud(path_to_pdf); +} + +// Initialize the print dialog. Called on the UI thread. +PrintDialogCloud::PrintDialogCloud(const FilePath& path_to_pdf) + : browser_(BrowserList::GetLastActive()) { + + // TODO(scottbyer): Verify GAIA login valid, execute GAIA login if not (should + // be distilled out of bookmark sync.) + + // TODO(scottbyer): Get the dialog width, height from the dialog + // contents, and take the screen size into account. + HtmlDialogUIDelegate* dialog_delegate = + new internal_cloud_print_helpers::CloudPrintHtmlDialogDelegate( + path_to_pdf, 500, 400, std::string()); + browser_->BrowserShowHtmlDialog(dialog_delegate, NULL); +} + +PrintDialogCloud::~PrintDialogCloud() { +} diff --git a/chrome/browser/printing/print_dialog_cloud.h b/chrome/browser/printing/print_dialog_cloud.h new file mode 100644 index 0000000..7e8515b --- /dev/null +++ b/chrome/browser/printing/print_dialog_cloud.h @@ -0,0 +1,38 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_PRINTING_PRINT_DIALOG_CLOUD_H_ +#define CHROME_BROWSER_PRINTING_PRINT_DIALOG_CLOUD_H_ + +#include "base/basictypes.h" + +#include "testing/gtest/include/gtest/gtest_prod.h" + +class Browser; +class FilePath; +namespace IPC { +class Message; +} + +class PrintDialogCloud { + public: + // Called on the IO thread. + static void CreatePrintDialogForPdf(const FilePath& path_to_pdf); + + private: + FRIEND_TEST(PrintDialogCloudTest, HandlersRegistered); + + explicit PrintDialogCloud(const FilePath& path_to_pdf); + ~PrintDialogCloud(); + + // Called as a task from the UI thread, creates an object instance + // to run the HTML/JS based print dialog for printing through the cloud. + static void CreateDialogImpl(const FilePath& path_to_pdf); + + Browser* browser_; + + DISALLOW_COPY_AND_ASSIGN(PrintDialogCloud); +}; + +#endif // CHROME_BROWSER_PRINTING_PRINT_DIALOG_CLOUD_H_ diff --git a/chrome/browser/printing/print_dialog_cloud_internal.h b/chrome/browser/printing/print_dialog_cloud_internal.h new file mode 100644 index 0000000..1bde98e --- /dev/null +++ b/chrome/browser/printing/print_dialog_cloud_internal.h @@ -0,0 +1,172 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_PRINTING_PRINT_DIALOG_CLOUD_INTERNAL_H_ +#define CHROME_BROWSER_PRINTING_PRINT_DIALOG_CLOUD_INTERNAL_H_ + +#include <string> +#include <vector> + +#include "base/file_path.h" +#include "base/lock.h" +#include "base/scoped_ptr.h" +#include "chrome/browser/dom_ui/dom_ui.h" +#include "chrome/browser/dom_ui/html_dialog_ui.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_registrar.h" + +class StringValue; + +namespace internal_cloud_print_helpers { + +// TODO(scottbyer): this should really be fetched from a profile +// preference. 4/8/10 +extern const char* const kCloudPrintDialogUrl; + +// Small class to virtualize a few functions to aid with unit testing. +class CloudPrintDataSenderHelper { + public: + explicit CloudPrintDataSenderHelper(DOMUI* dom_ui) : dom_ui_(dom_ui) {} + virtual ~CloudPrintDataSenderHelper() {} + + // Virtualize the overrides of these three functions from DOMUI to + // facilitate unit testing. + virtual void CallJavascriptFunction(const std::wstring& function_name); + virtual void CallJavascriptFunction(const std::wstring& function_name, + const Value& arg); + virtual void CallJavascriptFunction(const std::wstring& function_name, + const Value& arg1, + const Value& arg2); + + private: + DOMUI* dom_ui_; + + DISALLOW_COPY_AND_ASSIGN(CloudPrintDataSenderHelper); +}; + +// Small helper class to get the print data loaded in from the PDF +// file (on the FILE thread) and send it to the print dialog contents +// (on the IO thread), allowing for cancellation. +class CloudPrintDataSender + : public base::RefCountedThreadSafe<CloudPrintDataSender> { + public: + // The owner of this object is also expected to own and control the + // lifetime of the helper. + explicit CloudPrintDataSender(CloudPrintDataSenderHelper* helper) + : helper_(helper) {} + + // Calls to read in the PDF file (on the FILE thread) then send that + // information to the dialog renderer (on the IO thread). We know + // that the dom_ui pointer lifetime will outlast us, so we should be + // good. + void ReadPrintDataFile(const FilePath& path_to_pdf); + void SendPrintDataFile(); + + // Cancels any ramining part of the task by clearing out the dom_ui + // helper_ ptr. + void CancelPrintDataFile(); + + private: + friend class base::RefCountedThreadSafe<CloudPrintDataSender>; + ~CloudPrintDataSender() {} + + Lock lock_; + CloudPrintDataSenderHelper* volatile helper_; + scoped_ptr<StringValue> print_data_; + + DISALLOW_COPY_AND_ASSIGN(CloudPrintDataSender); +}; + +class CloudPrintHtmlDialogDelegate; + +// The CloudPrintFlowHandler connects the state machine (the UI delegate) +// to the dialog backing HTML and JS by providing DOMMessageHandler +// functions for the JS to use. This include refreshing the page +// setup parameters (which will cause a re-generation of the PDF in +// the renderer process - do we want a progress throbber shown? +// Probably..), and packing up the PDF and job parameters and sending +// them to the cloud. +class CloudPrintFlowHandler : public DOMMessageHandler, + public NotificationObserver { + public: + explicit CloudPrintFlowHandler(const FilePath& path_to_pdf) + : path_to_pdf_(path_to_pdf) {} + virtual ~CloudPrintFlowHandler() { + // This will also cancel any task in flight. + CancelAnyRunningTask(); + } + + // DOMMessageHandler implementation. + virtual void RegisterMessages(); + + // NotificationObserver implementation. + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); + + // Callbacks from the page. + void HandleShowDebugger(const Value* value); + void HandleSendPrintData(const Value* value); + void HandleSetPageParameters(const Value* value); + + // Call to get the debugger loaded on our hosted dialog page + // specifically. Since we're not in an official browser tab, only + // way to get the debugger going. + void ShowDebugger(); + + void SetDialogDelegate(CloudPrintHtmlDialogDelegate *delegate); + void CancelAnyRunningTask(); + + private: + // For unit testing. + virtual scoped_refptr<CloudPrintDataSender> CreateCloudPrintDataSender(); + + CloudPrintHtmlDialogDelegate* dialog_delegate_; + NotificationRegistrar registrar_; + FilePath path_to_pdf_; + scoped_refptr<CloudPrintDataSender> print_data_sender_; + scoped_ptr<CloudPrintDataSenderHelper> print_data_helper_; + + DISALLOW_COPY_AND_ASSIGN(CloudPrintFlowHandler); +}; + +// State machine used to run the printing dialog. This class is used +// to open and run the html dialog and deletes itself when the dialog +// is closed. +class CloudPrintHtmlDialogDelegate : public HtmlDialogUIDelegate { + public: + CloudPrintHtmlDialogDelegate(const FilePath& path_to_pdf, + int width, int height, + const std::string& json_arguments); + ~CloudPrintHtmlDialogDelegate(); + + // HTMLDialogUIDelegate implementation: + virtual bool IsDialogModal() const; + virtual std::wstring GetDialogTitle() const; + virtual GURL GetDialogContentURL() const; + virtual void GetDOMMessageHandlers( + std::vector<DOMMessageHandler*>* handlers) const; + virtual void GetDialogSize(gfx::Size* size) const; + virtual std::string GetDialogArgs() const; + virtual void OnDialogClosed(const std::string& json_retval); + virtual void OnCloseContents(TabContents* source, bool* out_close_dialog) { } + + private: + CloudPrintHtmlDialogDelegate(CloudPrintFlowHandler* flow_handler, + int width, int height, + const std::string& json_arguments); + void Init(int width, int height, const std::string& json_arguments); + + CloudPrintFlowHandler* flow_handler_; + mutable bool owns_flow_handler_; + + // The parameters needed to display a modal HTML dialog. + HtmlDialogUI::HtmlDialogParams params_; + + DISALLOW_COPY_AND_ASSIGN(CloudPrintHtmlDialogDelegate); +}; + +} // namespace internal_cloud_print_helpers + +#endif // CHROME_BROWSER_PRINTING_PRINT_DIALOG_CLOUD_INTERNAL_H_ diff --git a/chrome/browser/printing/print_dialog_cloud_uitest.cc b/chrome/browser/printing/print_dialog_cloud_uitest.cc new file mode 100644 index 0000000..753f5ae --- /dev/null +++ b/chrome/browser/printing/print_dialog_cloud_uitest.cc @@ -0,0 +1,176 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/printing/print_dialog_cloud.h" +#include "chrome/browser/printing/print_dialog_cloud_internal.h" + +#include <functional> + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/path_service.h" +#include "base/singleton.h" +#include "chrome/browser/browser_list.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/dom_ui/chrome_url_data_manager.h" +#include "chrome/browser/renderer_host/render_view_host.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/url_constants.h" +#include "chrome/test/in_process_browser_test.h" +#include "chrome/test/ui_test_utils.h" +#include "net/url_request/url_request_filter.h" +#include "net/url_request/url_request_test_job.h" + +namespace { + +class TestData { + public: + TestData() {} + + char* GetTestData() { + if (test_data_.empty()) { + FilePath test_data_directory; + PathService::Get(chrome::DIR_TEST_DATA, &test_data_directory); + FilePath test_file = + test_data_directory.AppendASCII("printing/cloud_print_uitest.html"); + file_util::ReadFileToString(test_file, &test_data_); + } + return &test_data_[0]; + } + private: + std::string test_data_; +}; + +// A simple test URLRequestJob. We don't care what it does, only that +// whether it starts and finishes. +class SimpleTestJob : public URLRequestTestJob { + public: + explicit SimpleTestJob(URLRequest* request) + : URLRequestTestJob(request, test_headers(), + Singleton<TestData>()->GetTestData(), true) {} + private: + ~SimpleTestJob() {} +}; + +class TestResult { + public: + TestResult() : result_(false) {} + void SetResult(bool value) { + result_ = value; + } + bool GetResult() { + return result_; + } + private: + bool result_; +}; + +} // namespace + +class PrintDialogCloudTest : public InProcessBrowserTest, + public NotificationObserver { + public: + PrintDialogCloudTest() : handler_added_(false) { + PathService::Get(chrome::DIR_TEST_DATA, &test_data_directory_); + GURL cloud_print_url(internal_cloud_print_helpers::kCloudPrintDialogUrl); + host_name_ = cloud_print_url.host(); + } + + // Must be static for handing into AddHostnameHandler. + static URLRequest::ProtocolFactory Factory; + + virtual void SetUp() { + Singleton<TestResult>()->SetResult(false); + InProcessBrowserTest::SetUp(); + } + + virtual void TearDown() { + if (handler_added_) { + URLRequestFilter* filter = URLRequestFilter::GetInstance(); + filter->RemoveHostnameHandler("http", host_name_); + handler_added_ = false; + } + InProcessBrowserTest::TearDown(); + } + + // Normally this is something I would expect could go into SetUp(), + // but there seems to be some timing or ordering related issue with + // the test harness that made that flaky. Calling this from the + // individual test functions seems to fix that. + void AddTestHandlers() { + if (!handler_added_) { + URLRequestFilter* filter = URLRequestFilter::GetInstance(); + filter->AddHostnameHandler("http", host_name_, + &PrintDialogCloudTest::Factory); + handler_added_ = true; + + registrar_.Add(this, NotificationType::LOAD_STOP, + NotificationService::AllSources()); + } + } + + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + if (type == NotificationType::LOAD_STOP) { + MessageLoop::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); + registrar_.Remove(this, NotificationType::LOAD_STOP, + NotificationService::AllSources()); + } + } + + bool handler_added_; + std::string host_name_; + std::string test_data_; + FilePath test_data_directory_; + NotificationRegistrar registrar_; +}; + +URLRequestJob* PrintDialogCloudTest::Factory(URLRequest* request, + const std::string& scheme) { + if (request && (request->url() == + GURL(internal_cloud_print_helpers::kCloudPrintDialogUrl))) + Singleton<TestResult>()->SetResult(true); + return new SimpleTestJob(request); +} + +IN_PROC_BROWSER_TEST_F(PrintDialogCloudTest, HandlersRegistered) { + BrowserList::SetLastActive(browser()); + ASSERT_TRUE(BrowserList::GetLastActive()); + + AddTestHandlers(); + + FilePath pdf_file = + test_data_directory_.AppendASCII("printing/cloud_print_uitest.pdf"); + new PrintDialogCloud(pdf_file); + + ui_test_utils::RunMessageLoop(); + + ASSERT_TRUE(Singleton<TestResult>()->GetResult()); +} + +#if defined(OS_CHROMEOS) +// Disabled until the extern URL is live so that the Print menu item +// can be enabled for Chromium OS. +IN_PROC_BROWSER_TEST_F(PrintDialogCloudTest, DISABLED_DialogGrabbed) { + BrowserList::SetLastActive(browser()); + ASSERT_TRUE(BrowserList::GetLastActive()); + + AddTestHandlers(); + + // This goes back one step further for the Chrome OS case, to making + // sure 'window.print()' gets to the right place. + ASSERT_TRUE(browser()->GetSelectedTabContents()); + ASSERT_TRUE(browser()->GetSelectedTabContents()->render_view_host()); + + std::wstring window_print(L"window.print()"); + browser()->GetSelectedTabContents()->render_view_host()-> + ExecuteJavascriptInWebFrame(std::wstring(), window_print); + + ui_test_utils::RunMessageLoop(); + + ASSERT_TRUE(Singleton<TestResult>()->GetResult()); +} +#endif diff --git a/chrome/browser/printing/print_dialog_cloud_unittest.cc b/chrome/browser/printing/print_dialog_cloud_unittest.cc new file mode 100644 index 0000000..85a3ce3 --- /dev/null +++ b/chrome/browser/printing/print_dialog_cloud_unittest.cc @@ -0,0 +1,189 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/printing/print_dialog_cloud.h" +#include "chrome/browser/printing/print_dialog_cloud_internal.h" + +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/path_service.h" +#include "base/values.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/notification_details.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_registrar.h" +#include "chrome/common/notification_source.h" +#include "chrome/common/notification_type.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gmock/include/gmock/gmock.h" + +using testing::AtLeast; +using testing::Return; +using testing::_; +using testing::A; + +static const char* const kPDFTestFile = "printing/cloud_print_unittest.pdf"; +static const char* const kEmptyPDFTestFile = + "printing/cloud_print_emptytest.pdf"; + +FilePath GetTestDataFileName() { + FilePath test_data_directory; + PathService::Get(chrome::DIR_TEST_DATA, &test_data_directory); + FilePath test_file = test_data_directory.AppendASCII(kPDFTestFile); + return test_file; +} + +FilePath GetEmptyDataFileName() { + FilePath test_data_directory; + PathService::Get(chrome::DIR_TEST_DATA, &test_data_directory); + FilePath test_file = test_data_directory.AppendASCII(kEmptyPDFTestFile); + return test_file; +} + +char* GetTestData() { + static std::string sTestFileData; + if (sTestFileData.empty()) { + FilePath test_file = GetTestDataFileName(); + file_util::ReadFileToString(test_file, &sTestFileData); + } + return &sTestFileData[0]; +} + +namespace internal_cloud_print_helpers { + +class MockCloudPrintFlowHandler : public CloudPrintFlowHandler { + public: + MOCK_METHOD0(RegisterMessages, + void()); + MOCK_METHOD3(Observe, + void(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details)); + MOCK_METHOD0(CreateCloudPrintDataSender, + scoped_refptr<CloudPrintDataSender>()); +}; + +class MockCloudPrintHtmlDialogDelegate : public CloudPrintHtmlDialogDelegate { + public: + MOCK_CONST_METHOD0(IsDialogModal, + bool()); + MOCK_CONST_METHOD0(GetDialogTitle, + std::wstring()); + MOCK_CONST_METHOD0(GetDialogContentURL, + GURL()); + MOCK_CONST_METHOD1(GetDOMMessageHandlers, + void(std::vector<DOMMessageHandler*>* handlers)); + MOCK_CONST_METHOD1(GetDialogSize, + void(gfx::Size* size)); + MOCK_CONST_METHOD0(GetDialogArgs, + std::string()); + MOCK_METHOD1(OnDialogClosed, + void(const std::string& json_retval)); + MOCK_METHOD2(OnCloseContents, + void(TabContents* source, bool *out_close_dialog)); +}; + +} // namespace internal_cloud_print_helpers + +using internal_cloud_print_helpers::CloudPrintDataSenderHelper; +using internal_cloud_print_helpers::CloudPrintDataSender; + +class MockExternalHtmlDialogUI : public ExternalHtmlDialogUI { + public: + MOCK_METHOD1(RenderViewCreated, + void(RenderViewHost* render_view_host)); +}; + +class MockCloudPrintDataSenderHelper : public CloudPrintDataSenderHelper { + public: + // TODO(scottbyer): At some point this probably wants to use a + // MockTabContents instead of NULL, and to pre-load it with a bunch + // of expects/results. + MockCloudPrintDataSenderHelper() : CloudPrintDataSenderHelper(NULL) {} + MOCK_METHOD1(CallJavascriptFunction, void(const std::wstring&)); + MOCK_METHOD2(CallJavascriptFunction, void(const std::wstring&, + const Value& arg1)); + MOCK_METHOD3(CallJavascriptFunction, void(const std::wstring&, + const Value& arg1, + const Value& arg2)); +}; + +// Testing for CloudPrintDataSender needs a mock DOMUI. +class CloudPrintDataSenderTest : public testing::Test { + public: + CloudPrintDataSenderTest() + : file_thread_(ChromeThread::FILE, &message_loop_), + io_thread_(ChromeThread::IO, &message_loop_) {} + + protected: + virtual void SetUp() { + mock_helper_.reset(new MockCloudPrintDataSenderHelper); + print_data_sender_ = + new CloudPrintDataSender(mock_helper_.get()); + } + + scoped_refptr<CloudPrintDataSender> print_data_sender_; + scoped_ptr<MockCloudPrintDataSenderHelper> mock_helper_; + + MessageLoop message_loop_; + ChromeThread file_thread_; + ChromeThread io_thread_; +}; + +// TODO(scottbyer): DISABLED until the binary test file can get +// checked in separate from the patch. +TEST_F(CloudPrintDataSenderTest, DISABLED_CanSend) { + EXPECT_CALL(*mock_helper_, CallJavascriptFunction(_, _, _)). + WillOnce(Return()); + + FilePath test_data_file_name = GetTestDataFileName(); + ChromeThread::PostTask(ChromeThread::FILE, FROM_HERE, + NewRunnableMethod( + print_data_sender_.get(), + &CloudPrintDataSender::ReadPrintDataFile, + test_data_file_name)); + MessageLoop::current()->RunAllPending(); +} + +TEST_F(CloudPrintDataSenderTest, BadFile) { + EXPECT_CALL(*mock_helper_, CallJavascriptFunction(_, _, _)).Times(0); + +#if defined(OS_WIN) + FilePath bad_data_file_name(L"/some/file/that/isnot/there"); +#else + FilePath bad_data_file_name("/some/file/that/isnot/there"); +#endif + ChromeThread::PostTask(ChromeThread::FILE, FROM_HERE, + NewRunnableMethod( + print_data_sender_.get(), + &CloudPrintDataSender::ReadPrintDataFile, + bad_data_file_name)); + MessageLoop::current()->RunAllPending(); +} + +TEST_F(CloudPrintDataSenderTest, EmptyFile) { + EXPECT_CALL(*mock_helper_, CallJavascriptFunction(_, _, _)).Times(0); + + FilePath empty_data_file_name = GetEmptyDataFileName(); + ChromeThread::PostTask(ChromeThread::FILE, FROM_HERE, + NewRunnableMethod( + print_data_sender_.get(), + &CloudPrintDataSender::ReadPrintDataFile, + empty_data_file_name)); + MessageLoop::current()->RunAllPending(); +} + +// Testing for CloudPrintFlowHandler needs a mock +// CloudPrintHtmlDialogDelegate, mock CloudPrintDataSender, and a mock +// DOMUI. + +// Testing for CloudPrintHtmlDialogDelegate needs a mock +// CloudPrintFlowHandler. + +// Testing for ExternalHtmlDialogUI needs a mock TabContents, mock +// CloudPrintHtmlDialogDelegate (provided through the mock +// tab_contents) + +// Testing for PrintDialogCloud needs a mock Browser. |