local_discovery_ui_handler.cc revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
1// Copyright 2013 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/ui/webui/local_discovery/local_discovery_ui_handler.h"
6
7#include <set>
8
9#include "base/bind.h"
10#include "base/command_line.h"
11#include "base/message_loop/message_loop.h"
12#include "base/prefs/pref_service.h"
13#include "base/strings/stringprintf.h"
14#include "base/strings/utf_string_conversions.h"
15#include "base/values.h"
16#include "chrome/browser/chrome_notification_types.h"
17#include "chrome/browser/local_discovery/privet_confirm_api_flow.h"
18#include "chrome/browser/local_discovery/privet_constants.h"
19#include "chrome/browser/local_discovery/privet_device_lister_impl.h"
20#include "chrome/browser/local_discovery/privet_http_asynchronous_factory.h"
21#include "chrome/browser/local_discovery/privet_http_impl.h"
22#include "chrome/browser/local_discovery/service_discovery_shared_client.h"
23#include "chrome/browser/printing/cloud_print/cloud_print_proxy_service.h"
24#include "chrome/browser/printing/cloud_print/cloud_print_proxy_service_factory.h"
25#include "chrome/browser/printing/cloud_print/cloud_print_url.h"
26#include "chrome/browser/profiles/profile.h"
27#include "chrome/browser/signin/profile_oauth2_token_service.h"
28#include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
29#include "chrome/browser/signin/signin_manager.h"
30#include "chrome/browser/signin/signin_manager_base.h"
31#include "chrome/browser/signin/signin_manager_factory.h"
32#include "chrome/browser/signin/signin_promo.h"
33#include "chrome/browser/ui/browser_finder.h"
34#include "chrome/browser/ui/browser_tabstrip.h"
35#include "chrome/common/chrome_switches.h"
36#include "chrome/common/pref_names.h"
37#include "content/public/browser/notification_source.h"
38#include "content/public/browser/user_metrics.h"
39#include "content/public/browser/user_metrics.h"
40#include "content/public/browser/web_ui.h"
41#include "content/public/common/page_transition_types.h"
42#include "grit/generated_resources.h"
43#include "net/base/host_port_pair.h"
44#include "net/base/net_util.h"
45#include "net/http/http_status_code.h"
46#include "ui/base/l10n/l10n_util.h"
47
48#if defined(ENABLE_FULL_PRINTING) && !defined(OS_CHROMEOS) && \
49  !defined(OS_MACOSX)
50#define CLOUD_PRINT_CONNECTOR_UI_AVAILABLE
51#endif
52
53namespace local_discovery {
54
55namespace {
56const char kPrivetAutomatedClaimURLFormat[] = "%s/confirm?token=%s";
57
58int g_num_visible = 0;
59}  // namespace
60
61LocalDiscoveryUIHandler::LocalDiscoveryUIHandler() : is_visible_(false) {
62#if defined(CLOUD_PRINT_CONNECTOR_UI_AVAILABLE)
63#if !defined(GOOGLE_CHROME_BUILD) && defined(OS_WIN)
64  // On Windows, we need the PDF plugin which is only guaranteed to exist on
65  // Google Chrome builds. Use a command-line switch for Windows non-Google
66  //  Chrome builds.
67  cloud_print_connector_ui_enabled_ =
68      CommandLine::ForCurrentProcess()->HasSwitch(
69      switches::kEnableCloudPrintProxy);
70#elif !defined(OS_CHROMEOS)
71  // Always enabled for Linux and Google Chrome Windows builds.
72  // Never enabled for Chrome OS, we don't even need to indicate it.
73  cloud_print_connector_ui_enabled_ = true;
74#endif
75#endif  // !defined(OS_MACOSX)
76}
77
78LocalDiscoveryUIHandler::~LocalDiscoveryUIHandler() {
79  ResetCurrentRegistration();
80  SetIsVisible(false);
81}
82
83// static
84bool LocalDiscoveryUIHandler::GetHasVisible() {
85  return g_num_visible != 0;
86}
87
88void LocalDiscoveryUIHandler::RegisterMessages() {
89  web_ui()->RegisterMessageCallback("start", base::Bind(
90      &LocalDiscoveryUIHandler::HandleStart,
91      base::Unretained(this)));
92  web_ui()->RegisterMessageCallback("isVisible", base::Bind(
93      &LocalDiscoveryUIHandler::HandleIsVisible,
94      base::Unretained(this)));
95  web_ui()->RegisterMessageCallback("registerDevice", base::Bind(
96      &LocalDiscoveryUIHandler::HandleRegisterDevice,
97      base::Unretained(this)));
98  web_ui()->RegisterMessageCallback("cancelRegistration", base::Bind(
99      &LocalDiscoveryUIHandler::HandleCancelRegistration,
100      base::Unretained(this)));
101  web_ui()->RegisterMessageCallback("requestPrinterList", base::Bind(
102      &LocalDiscoveryUIHandler::HandleRequestPrinterList,
103      base::Unretained(this)));
104  web_ui()->RegisterMessageCallback("openCloudPrintURL", base::Bind(
105      &LocalDiscoveryUIHandler::HandleOpenCloudPrintURL,
106      base::Unretained(this)));
107  web_ui()->RegisterMessageCallback("showSyncUI", base::Bind(
108      &LocalDiscoveryUIHandler::HandleShowSyncUI,
109      base::Unretained(this)));
110
111  // Cloud print connector related messages
112#if defined(CLOUD_PRINT_CONNECTOR_UI_AVAILABLE)
113  if (cloud_print_connector_ui_enabled_) {
114    web_ui()->RegisterMessageCallback(
115        "showCloudPrintSetupDialog",
116        base::Bind(&LocalDiscoveryUIHandler::ShowCloudPrintSetupDialog,
117                   base::Unretained(this)));
118    web_ui()->RegisterMessageCallback(
119        "disableCloudPrintConnector",
120        base::Bind(&LocalDiscoveryUIHandler::HandleDisableCloudPrintConnector,
121                   base::Unretained(this)));
122  }
123#endif  // defined(ENABLE_FULL_PRINTING)
124}
125
126void LocalDiscoveryUIHandler::HandleStart(const base::ListValue* args) {
127  Profile* profile = Profile::FromWebUI(web_ui());
128
129  // If privet_lister_ is already set, it is a mock used for tests or the result
130  // of a reload.
131  if (!privet_lister_) {
132    service_discovery_client_ = ServiceDiscoverySharedClient::GetInstance();
133    privet_lister_.reset(new PrivetDeviceListerImpl(
134        service_discovery_client_.get(), this));
135    privet_http_factory_ =
136        PrivetHTTPAsynchronousFactory::CreateInstance(
137            service_discovery_client_.get(), profile->GetRequestContext());
138  }
139
140  privet_lister_->Start();
141  privet_lister_->DiscoverNewDevices(false);
142
143#if defined(CLOUD_PRINT_CONNECTOR_UI_AVAILABLE)
144  StartCloudPrintConnector();
145#endif
146
147  CheckUserLoggedIn();
148
149  notification_registrar_.RemoveAll();
150  notification_registrar_.Add(this,
151                              chrome::NOTIFICATION_GOOGLE_SIGNIN_SUCCESSFUL,
152                              content::Source<Profile>(profile));
153  notification_registrar_.Add(this, chrome::NOTIFICATION_GOOGLE_SIGNED_OUT,
154                              content::Source<Profile>(profile));
155}
156
157void LocalDiscoveryUIHandler::HandleIsVisible(const base::ListValue* args) {
158  bool is_visible = false;
159  bool rv = args->GetBoolean(0, &is_visible);
160  DCHECK(rv);
161  SetIsVisible(is_visible);
162}
163
164void LocalDiscoveryUIHandler::HandleRegisterDevice(
165    const base::ListValue* args) {
166  std::string device;
167
168  bool rv = args->GetString(0, &device);
169  DCHECK(rv);
170
171  privet_resolution_ = privet_http_factory_->CreatePrivetHTTP(
172      device,
173      device_descriptions_[device].address,
174      base::Bind(&LocalDiscoveryUIHandler::StartRegisterHTTP,
175                 base::Unretained(this)));
176  privet_resolution_->Start();
177}
178
179void LocalDiscoveryUIHandler::HandleCancelRegistration(
180    const base::ListValue* args) {
181  ResetCurrentRegistration();
182}
183
184void LocalDiscoveryUIHandler::HandleRequestPrinterList(
185    const base::ListValue* args) {
186  Profile* profile = Profile::FromWebUI(web_ui());
187  ProfileOAuth2TokenService* token_service =
188      ProfileOAuth2TokenServiceFactory::GetForProfile(profile);
189
190  SigninManagerBase* signin_manager =
191      SigninManagerFactory::GetInstance()->GetForProfile(profile);
192
193  cloud_print_printer_list_.reset(new CloudPrintPrinterList(
194      profile->GetRequestContext(),
195      GetCloudPrintBaseUrl(),
196      token_service,
197      signin_manager->GetAuthenticatedAccountId(),
198      this));
199  cloud_print_printer_list_->Start();
200}
201
202void LocalDiscoveryUIHandler::HandleOpenCloudPrintURL(
203    const base::ListValue* args) {
204  std::string url;
205  bool rv = args->GetString(0, &url);
206  DCHECK(rv);
207
208  GURL url_full(GetCloudPrintBaseUrl() + url);
209
210  Browser* browser = chrome::FindBrowserWithWebContents(
211      web_ui()->GetWebContents());
212  DCHECK(browser);
213
214  chrome::AddSelectedTabWithURL(browser,
215                                url_full,
216                                content::PAGE_TRANSITION_FROM_API);
217}
218
219void LocalDiscoveryUIHandler::HandleShowSyncUI(
220    const base::ListValue* args) {
221  Browser* browser = chrome::FindBrowserWithWebContents(
222      web_ui()->GetWebContents());
223  DCHECK(browser);
224
225  // We use SOURCE_SETTINGS because the URL for SOURCE_SETTINGS is detected on
226  // redirect.
227  GURL url(signin::GetPromoURL(signin::SOURCE_SETTINGS,
228                               true));  // auto close after success.
229
230  browser->OpenURL(
231      content::OpenURLParams(url, content::Referrer(), SINGLETON_TAB,
232                             content::PAGE_TRANSITION_AUTO_BOOKMARK, false));
233}
234
235void LocalDiscoveryUIHandler::StartRegisterHTTP(
236    scoped_ptr<PrivetHTTPClient> http_client) {
237  current_http_client_.swap(http_client);
238
239  std::string user = GetSyncAccount();
240
241  if (!current_http_client_) {
242    SendRegisterError();
243    return;
244  }
245
246  current_register_operation_ =
247      current_http_client_->CreateRegisterOperation(user, this);
248  current_register_operation_->Start();
249}
250
251void LocalDiscoveryUIHandler::OnPrivetRegisterClaimToken(
252    PrivetRegisterOperation* operation,
253    const std::string& token,
254    const GURL& url) {
255  web_ui()->CallJavascriptFunction(
256      "local_discovery.onRegistrationConfirmedOnPrinter");
257  if (device_descriptions_.count(current_http_client_->GetName()) == 0) {
258    SendRegisterError();
259    return;
260  }
261
262  std::string base_url = GetCloudPrintBaseUrl();
263
264  GURL automated_claim_url(base::StringPrintf(
265      kPrivetAutomatedClaimURLFormat,
266      base_url.c_str(),
267      token.c_str()));
268
269  Profile* profile = Profile::FromWebUI(web_ui());
270
271  ProfileOAuth2TokenService* token_service =
272      ProfileOAuth2TokenServiceFactory::GetForProfile(profile);
273
274  if (!token_service) {
275    SendRegisterError();
276    return;
277  }
278
279  SigninManagerBase* signin_manager =
280      SigninManagerFactory::GetInstance()->GetForProfile(profile);
281  if (!signin_manager) {
282    SendRegisterError();
283    return;
284  }
285
286  confirm_api_call_flow_.reset(new PrivetConfirmApiCallFlow(
287      profile->GetRequestContext(),
288      token_service,
289      signin_manager->GetAuthenticatedAccountId(),
290      automated_claim_url,
291      base::Bind(&LocalDiscoveryUIHandler::OnConfirmDone,
292                 base::Unretained(this))));
293  confirm_api_call_flow_->Start();
294}
295
296void LocalDiscoveryUIHandler::OnPrivetRegisterError(
297    PrivetRegisterOperation* operation,
298    const std::string& action,
299    PrivetRegisterOperation::FailureReason reason,
300    int printer_http_code,
301    const base::DictionaryValue* json) {
302  std::string error;
303
304  if (reason == PrivetRegisterOperation::FAILURE_JSON_ERROR &&
305      json->GetString(kPrivetKeyError, &error)) {
306    if (error == kPrivetErrorTimeout) {
307        web_ui()->CallJavascriptFunction(
308            "local_discovery.onRegistrationTimeout");
309      return;
310    } else if (error == kPrivetErrorCancel) {
311      web_ui()->CallJavascriptFunction(
312            "local_discovery.onRegistrationCanceledPrinter");
313      return;
314    }
315  }
316
317  SendRegisterError();
318}
319
320void LocalDiscoveryUIHandler::OnPrivetRegisterDone(
321    PrivetRegisterOperation* operation,
322    const std::string& device_id) {
323  std::string name = operation->GetHTTPClient()->GetName();
324
325  current_register_operation_.reset();
326  current_http_client_.reset();
327
328  // HACK(noamsml): Generate network traffic so the Windows firewall doesn't
329  // block the printer's announcement.
330  privet_lister_->DiscoverNewDevices(false);
331
332  DeviceDescriptionMap::iterator found = device_descriptions_.find(name);
333
334  if (found == device_descriptions_.end()) {
335    // TODO(noamsml): Handle the case where a printer's record is not present at
336    // the end of registration.
337    SendRegisterError();
338    return;
339  }
340
341  SendRegisterDone(found->first, found->second);
342}
343
344void LocalDiscoveryUIHandler::OnConfirmDone(
345    CloudPrintBaseApiFlow::Status status) {
346  if (status == CloudPrintBaseApiFlow::SUCCESS) {
347    confirm_api_call_flow_.reset();
348    current_register_operation_->CompleteRegistration();
349  } else {
350    SendRegisterError();
351  }
352}
353
354void LocalDiscoveryUIHandler::DeviceChanged(
355    bool added,
356    const std::string& name,
357    const DeviceDescription& description) {
358  device_descriptions_[name] = description;
359
360  base::DictionaryValue info;
361
362  base::StringValue service_name(name);
363  scoped_ptr<base::Value> null_value(base::Value::CreateNullValue());
364
365  if (description.id.empty()) {
366    info.SetString("service_name", name);
367    info.SetString("human_readable_name", description.name);
368    info.SetString("description", description.description);
369
370    web_ui()->CallJavascriptFunction(
371        "local_discovery.onUnregisteredDeviceUpdate",
372        service_name, info);
373  } else {
374    web_ui()->CallJavascriptFunction(
375        "local_discovery.onUnregisteredDeviceUpdate",
376        service_name, *null_value);
377  }
378}
379
380void LocalDiscoveryUIHandler::DeviceRemoved(const std::string& name) {
381  device_descriptions_.erase(name);
382  scoped_ptr<base::Value> null_value(base::Value::CreateNullValue());
383  base::StringValue name_value(name);
384
385  web_ui()->CallJavascriptFunction("local_discovery.onUnregisteredDeviceUpdate",
386                                   name_value, *null_value);
387}
388
389void LocalDiscoveryUIHandler::DeviceCacheFlushed() {
390  web_ui()->CallJavascriptFunction("local_discovery.onDeviceCacheFlushed");
391  privet_lister_->DiscoverNewDevices(false);
392}
393
394void LocalDiscoveryUIHandler::OnCloudPrintPrinterListReady() {
395  base::ListValue printer_object_list;
396  std::set<std::string> local_ids;
397
398  for (DeviceDescriptionMap::iterator i = device_descriptions_.begin();
399       i != device_descriptions_.end();
400       i++) {
401    std::string device_id = i->second.id;
402    if (!device_id.empty()) {
403      const CloudPrintPrinterList::PrinterDetails* details =
404          cloud_print_printer_list_->GetDetailsFor(device_id);
405
406      if (details) {
407        local_ids.insert(device_id);
408        printer_object_list.Append(CreatePrinterInfo(*details).release());
409      }
410    }
411  }
412
413  for (CloudPrintPrinterList::iterator i = cloud_print_printer_list_->begin();
414       i != cloud_print_printer_list_->end(); i++) {
415    if (local_ids.count(i->id) == 0) {
416      printer_object_list.Append(CreatePrinterInfo(*i).release());
417    }
418  }
419
420  web_ui()->CallJavascriptFunction(
421      "local_discovery.onCloudDeviceListAvailable", printer_object_list);
422}
423
424void LocalDiscoveryUIHandler::OnCloudPrintPrinterListUnavailable() {
425  web_ui()->CallJavascriptFunction(
426      "local_discovery.onCloudDeviceListUnavailable");
427}
428
429void LocalDiscoveryUIHandler::Observe(
430    int type,
431    const content::NotificationSource& source,
432    const content::NotificationDetails& details) {
433  switch (type) {
434    case chrome::NOTIFICATION_GOOGLE_SIGNIN_SUCCESSFUL:
435    case chrome::NOTIFICATION_GOOGLE_SIGNED_OUT:
436      CheckUserLoggedIn();
437      break;
438    default:
439      NOTREACHED();
440  }
441}
442
443void LocalDiscoveryUIHandler::SendRegisterError() {
444  web_ui()->CallJavascriptFunction("local_discovery.onRegistrationFailed");
445}
446
447void LocalDiscoveryUIHandler::SendRegisterDone(
448    const std::string& service_name, const DeviceDescription& device) {
449  base::DictionaryValue printer_value;
450
451  printer_value.SetString("id", device.id);
452  printer_value.SetString("display_name", device.name);
453  printer_value.SetString("description", device.description);
454  printer_value.SetString("service_name", service_name);
455
456  web_ui()->CallJavascriptFunction("local_discovery.onRegistrationSuccess",
457                                   printer_value);
458}
459
460void LocalDiscoveryUIHandler::SetIsVisible(bool visible) {
461  if (visible != is_visible_) {
462    g_num_visible += visible ? 1 : -1;
463    is_visible_ = visible;
464  }
465}
466
467std::string LocalDiscoveryUIHandler::GetSyncAccount() {
468  Profile* profile = Profile::FromWebUI(web_ui());
469  SigninManagerBase* signin_manager =
470      SigninManagerFactory::GetForProfileIfExists(profile);
471
472  if (!signin_manager) {
473    return "";
474  }
475
476  return signin_manager->GetAuthenticatedUsername();
477}
478
479std::string LocalDiscoveryUIHandler::GetCloudPrintBaseUrl() {
480  CloudPrintURL cloud_print_url(Profile::FromWebUI(web_ui()));
481
482  return cloud_print_url.GetCloudPrintServiceURL().spec();
483}
484
485// TODO(noamsml): Create master object for registration flow.
486void LocalDiscoveryUIHandler::ResetCurrentRegistration() {
487  if (current_register_operation_.get()) {
488    current_register_operation_->Cancel();
489    current_register_operation_.reset();
490  }
491
492  confirm_api_call_flow_.reset();
493  privet_resolution_.reset();
494  current_http_client_.reset();
495}
496
497scoped_ptr<base::DictionaryValue> LocalDiscoveryUIHandler::CreatePrinterInfo(
498    const CloudPrintPrinterList::PrinterDetails& description) {
499  scoped_ptr<base::DictionaryValue> return_value(new base::DictionaryValue);
500
501  return_value->SetString("id", description.id);
502  return_value->SetString("display_name", description.display_name);
503  return_value->SetString("description", description.description);
504
505  return return_value.Pass();
506}
507
508void LocalDiscoveryUIHandler::CheckUserLoggedIn() {
509  base::FundamentalValue logged_in_value(!GetSyncAccount().empty());
510  web_ui()->CallJavascriptFunction("local_discovery.setUserLoggedIn",
511                                   logged_in_value);
512}
513
514#if defined(CLOUD_PRINT_CONNECTOR_UI_AVAILABLE)
515void LocalDiscoveryUIHandler::StartCloudPrintConnector() {
516  Profile* profile = Profile::FromWebUI(web_ui());
517
518  base::Closure cloud_print_callback = base::Bind(
519      &LocalDiscoveryUIHandler::OnCloudPrintPrefsChanged,
520          base::Unretained(this));
521
522  if (cloud_print_connector_email_.GetPrefName().empty()) {
523    cloud_print_connector_email_.Init(
524        prefs::kCloudPrintEmail, profile->GetPrefs(), cloud_print_callback);
525  }
526
527  if (cloud_print_connector_enabled_.GetPrefName().empty()) {
528    cloud_print_connector_enabled_.Init(
529        prefs::kCloudPrintProxyEnabled, profile->GetPrefs(),
530        cloud_print_callback);
531  }
532
533  if (cloud_print_connector_ui_enabled_) {
534    SetupCloudPrintConnectorSection();
535    RefreshCloudPrintStatusFromService();
536  } else {
537    RemoveCloudPrintConnectorSection();
538  }
539}
540
541void LocalDiscoveryUIHandler::OnCloudPrintPrefsChanged() {
542  if (cloud_print_connector_ui_enabled_)
543    SetupCloudPrintConnectorSection();
544}
545
546void LocalDiscoveryUIHandler::ShowCloudPrintSetupDialog(
547    const base::ListValue* args) {
548  content::RecordAction(
549      base::UserMetricsAction("Options_EnableCloudPrintProxy"));
550  // Open the connector enable page in the current tab.
551  Profile* profile = Profile::FromWebUI(web_ui());
552  content::OpenURLParams params(
553      CloudPrintURL(profile).GetCloudPrintServiceEnableURL(
554          CloudPrintProxyServiceFactory::GetForProfile(profile)->proxy_id()),
555      content::Referrer(), CURRENT_TAB, content::PAGE_TRANSITION_LINK, false);
556  web_ui()->GetWebContents()->OpenURL(params);
557}
558
559void LocalDiscoveryUIHandler::HandleDisableCloudPrintConnector(
560    const base::ListValue* args) {
561  content::RecordAction(
562      base::UserMetricsAction("Options_DisableCloudPrintProxy"));
563  CloudPrintProxyServiceFactory::GetForProfile(Profile::FromWebUI(web_ui()))->
564      DisableForUser();
565}
566
567void LocalDiscoveryUIHandler::SetupCloudPrintConnectorSection() {
568  Profile* profile = Profile::FromWebUI(web_ui());
569
570  if (!CloudPrintProxyServiceFactory::GetForProfile(profile)) {
571    cloud_print_connector_ui_enabled_ = false;
572    RemoveCloudPrintConnectorSection();
573    return;
574  }
575
576  bool cloud_print_connector_allowed =
577      !cloud_print_connector_enabled_.IsManaged() ||
578      cloud_print_connector_enabled_.GetValue();
579  base::FundamentalValue allowed(cloud_print_connector_allowed);
580
581  std::string email;
582  if (profile->GetPrefs()->HasPrefPath(prefs::kCloudPrintEmail) &&
583      cloud_print_connector_allowed) {
584    email = profile->GetPrefs()->GetString(prefs::kCloudPrintEmail);
585  }
586  base::FundamentalValue disabled(email.empty());
587
588  base::string16 label_str;
589  if (email.empty()) {
590    label_str = l10n_util::GetStringFUTF16(
591        IDS_LOCAL_DISCOVERY_CLOUD_PRINT_CONNECTOR_DISABLED_LABEL,
592        l10n_util::GetStringUTF16(IDS_GOOGLE_CLOUD_PRINT));
593  } else {
594    label_str = l10n_util::GetStringFUTF16(
595        IDS_OPTIONS_CLOUD_PRINT_CONNECTOR_ENABLED_LABEL,
596        l10n_util::GetStringUTF16(IDS_GOOGLE_CLOUD_PRINT),
597        base::UTF8ToUTF16(email));
598  }
599  base::StringValue label(label_str);
600
601  web_ui()->CallJavascriptFunction(
602      "local_discovery.setupCloudPrintConnectorSection", disabled, label,
603      allowed);
604}
605
606void LocalDiscoveryUIHandler::RemoveCloudPrintConnectorSection() {
607  web_ui()->CallJavascriptFunction(
608      "local_discovery.removeCloudPrintConnectorSection");
609}
610
611void LocalDiscoveryUIHandler::RefreshCloudPrintStatusFromService() {
612  if (cloud_print_connector_ui_enabled_)
613    CloudPrintProxyServiceFactory::GetForProfile(Profile::FromWebUI(web_ui()))->
614        RefreshStatusFromService();
615}
616#endif // cloud print connector option stuff
617
618}  // namespace local_discovery
619