Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(810)

Side by Side Diff: chrome/browser/chromeos/printer_detector/cups_printer_detector.cc

Issue 2738323003: Implement basic USB setup in the cups printer detector. Factor out (Closed)
Patch Set: Implement basic USB setup in the cups printer detector. Factor out some utility usb functions into… Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2017 The Chromium Authors. All rights reserved. 1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include <stdint.h> 5 #include <stdint.h>
6 6
7 #include <memory> 7 #include <memory>
8 #include <utility> 8 #include <utility>
9 #include <vector> 9 #include <vector>
10 10
11 #include "base/bind.h" 11 #include "base/bind.h"
12 #include "base/bind_helpers.h" 12 #include "base/bind_helpers.h"
13 #include "base/macros.h" 13 #include "base/macros.h"
14 #include "base/scoped_observer.h" 14 #include "base/scoped_observer.h"
15 #include "base/strings/utf_string_conversions.h" 15 #include "base/strings/utf_string_conversions.h"
16 #include "chrome/browser/browser_process.h" 16 #include "chrome/browser/browser_process.h"
17 #include "chrome/browser/chromeos/printer_detector/printer_detector.h" 17 #include "chrome/browser/chromeos/printer_detector/printer_detector.h"
18 #include "chrome/browser/chromeos/printing/ppd_provider_factory.h"
19 #include "chrome/browser/chromeos/printing/printer_configurer.h"
20 #include "chrome/browser/chromeos/printing/printers_manager_factory.h"
21 #include "chrome/browser/chromeos/printing/usb_util.h"
18 #include "chrome/browser/chromeos/profiles/profile_helper.h" 22 #include "chrome/browser/chromeos/profiles/profile_helper.h"
19 #include "chrome/browser/notifications/notification.h" 23 #include "chromeos/dbus/dbus_thread_manager.h"
20 #include "chrome/browser/notifications/notification_delegate.h" 24 #include "chromeos/dbus/debug_daemon_client.h"
21 #include "chrome/browser/notifications/notification_ui_manager.h" 25 #include "chromeos/printing/ppd_provider.h"
22 #include "chrome/browser/ui/browser_navigator.h"
23 #include "chrome/browser/ui/browser_navigator_params.h"
24 #include "chrome/common/url_constants.h"
25 #include "chrome/grit/generated_resources.h"
26 #include "chrome/grit/theme_resources.h"
27 #include "components/user_manager/user.h" 26 #include "components/user_manager/user.h"
28 #include "components/user_manager/user_manager.h" 27 #include "components/user_manager/user_manager.h"
28 #include "content/public/browser/browser_thread.h"
29 #include "device/base/device_client.h" 29 #include "device/base/device_client.h"
30 #include "device/usb/usb_device.h" 30 #include "device/usb/usb_device.h"
31 #include "device/usb/usb_device_filter.h" 31 #include "device/usb/usb_device_filter.h"
32 #include "device/usb/usb_service.h" 32 #include "device/usb/usb_service.h"
33 #include "extensions/browser/extension_registry.h" 33 #include "extensions/browser/extension_registry.h"
34 #include "extensions/browser/extension_system.h" 34 #include "extensions/browser/extension_system.h"
35 #include "extensions/common/one_shot_event.h" 35 #include "extensions/common/one_shot_event.h"
36 #include "ui/base/l10n/l10n_util.h"
37 #include "ui/base/resource/resource_bundle.h"
38 36
39 namespace chromeos { 37 namespace chromeos {
40 namespace { 38 namespace {
41 39
42 const char kUSBPrinterFoundNotificationID[] = 40 using printing::PpdProvider;
43 "chrome://settings/cupsPrinters/printer_found";
44 41
45 // Base class used for printer USB interfaces 42 // We have to carry quite a bit of data through to the callbacks for
46 // (https://www.usb.org/developers/defined_class). 43 // getting a printer set up. Rather than have a bunch of parameters
47 const uint8_t kPrinterInterfaceClass = 7; 44 // in a bunch of functions below, this struct aggregates everything
45 // we need to keep around during the setup.
46 struct SetUpPrinterData {
47 // The configurer running the SetUpPrinter call.
48 std::unique_ptr<PrinterConfigurer> configurer;
48 49
49 // USBPrinterSetupNotificationDelegate takes a pointer to the Impl class, so 50 // The printer being set up.
50 // we have to forward declare it. 51 std::unique_ptr<Printer> printer;
51 class CupsPrinterDetectorImpl;
52 52
53 class USBPrinterSetupNotificationDelegate : public NotificationDelegate { 53 // The usb device causing this setup flow.
54 public: 54 scoped_refptr<device::UsbDevice> device;
55 explicit USBPrinterSetupNotificationDelegate(
56 CupsPrinterDetectorImpl* printer_detector)
57 : printer_detector_(printer_detector) {}
58 55
59 // NotificationDelegate override: 56 // True if this printer is one that the user doesn't already have a
60 std::string id() const override { return kUSBPrinterFoundNotificationID; } 57 // configuration for.
58 bool is_new;
61 59
62 // This is defined out of line because it needs the PrinterDetectorImpl 60 // True if this was a printer that was plugged in during the session, false if
63 // full class declaration, not just the forward declaration. 61 // it was already plugged in when the session started.
64 void ButtonClick(int button_index) override; 62 bool hotplugged;
63 };
65 64
66 bool HasClickedListener() override { return true; } 65 // Given a usb device, guess the make and model for a driver lookup.
67 66 //
68 private: 67 // TODO(justincarlson): Possibly go deeper and query the IEEE1284 fields
69 ~USBPrinterSetupNotificationDelegate() override = default; 68 // for make and model if we determine those are more likely to contain
70 69 // what we want.
71 CupsPrinterDetectorImpl* printer_detector_; 70 std::string GuessEffectiveMakeAndModel(const device::UsbDevice& device) {
72 71 return base::UTF16ToUTF8(device.manufacturer_string()) + " " +
73 DISALLOW_COPY_AND_ASSIGN(USBPrinterSetupNotificationDelegate); 72 base::UTF16ToUTF8(device.product_string());
74 }; 73 }
75 74
76 // The PrinterDetector that drives the flow for setting up a USB printer to use 75 // The PrinterDetector that drives the flow for setting up a USB printer to use
77 // CUPS backend. 76 // CUPS backend.
78 class CupsPrinterDetectorImpl : public PrinterDetector, 77 class CupsPrinterDetectorImpl : public PrinterDetector,
79 public device::UsbService::Observer { 78 public device::UsbService::Observer {
80 public: 79 public:
81 explicit CupsPrinterDetectorImpl(Profile* profile) 80 explicit CupsPrinterDetectorImpl(Profile* profile)
82 : profile_(profile), observer_(this), weak_ptr_factory_(this) { 81 : profile_(profile), observer_(this), weak_ptr_factory_(this) {
83 extensions::ExtensionSystem::Get(profile)->ready().Post( 82 extensions::ExtensionSystem::Get(profile)->ready().Post(
tbarzic 2017/03/14 01:43:32 Is dependency on extensions system needed?
Carlson 2017/03/15 00:31:15 I believe so, though my understanding of this aspe
skau 2017/03/15 04:05:53 In order for services to be cleaned up in the corr
Carlson 2017/03/15 16:45:25 I hadn't seen that, thanks. Added the PrintersMan
84 FROM_HERE, base::Bind(&CupsPrinterDetectorImpl::Initialize, 83 FROM_HERE, base::Bind(&CupsPrinterDetectorImpl::Initialize,
85 weak_ptr_factory_.GetWeakPtr())); 84 weak_ptr_factory_.GetWeakPtr()));
86 } 85 }
87 ~CupsPrinterDetectorImpl() override = default; 86 ~CupsPrinterDetectorImpl() override = default;
88 87
89 void ClickOnNotificationButton(int button_index) {
90 // Remove the old notification first.
91 const ProfileID profile_id = NotificationUIManager::GetProfileID(profile_);
92 g_browser_process->notification_ui_manager()->CancelById(
93 kUSBPrinterFoundNotificationID, profile_id);
94
95 if (command_ == ButtonCommand::SETUP) {
96 OnSetUpUSBPrinterStarted();
97 // TODO(skau/xdai): call the CUPS backend to set up the USB printer and
98 // then call OnSetUpPrinterDone() or OnSetUpPrinterError() depending on
99 // the setup result.
100 } else if (command_ == ButtonCommand::CANCEL_SETUP) {
101 // TODO(skau/xdai): call the CUPS backend to cancel the printer setup.
102 } else if (command_ == ButtonCommand::GET_HELP) {
103 chrome::NavigateParams params(profile_,
104 GURL(chrome::kChromeUIMdCupsSettingsURL),
105 ui::PAGE_TRANSITION_LINK);
106 params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
107 params.window_action = chrome::NavigateParams::SHOW_WINDOW;
108 chrome::Navigate(&params);
109 }
110 }
111
112 private: 88 private:
113 // Action that should be performed when a notification button is clicked. 89 // Called when the extension system is "ready", meaning everything has been
114 enum class ButtonCommand { 90 // created.
115 SETUP, 91 void Initialize() {
116 CANCEL_SETUP,
117 CLOSE,
118 GET_HELP,
119 };
120
121 // UsbService::observer override:
122 void OnDeviceAdded(scoped_refptr<device::UsbDevice> device) override {
123 const user_manager::User* user = 92 const user_manager::User* user =
124 ProfileHelper::Get()->GetUserByProfile(profile_); 93 ProfileHelper::Get()->GetUserByProfile(profile_);
94 // Don't bother to listen for USB events unless we are the active user.
95 //
125 // TODO(justincarlson) - See if it's appropriate to relax any of these 96 // TODO(justincarlson) - See if it's appropriate to relax any of these
126 // constraints. 97 // constraints.
127 if (!user || !user->HasGaiaAccount() || !user_manager::UserManager::Get() || 98 if (!user || !user->HasGaiaAccount() || !user_manager::UserManager::Get() ||
128 user != user_manager::UserManager::Get()->GetActiveUser()) { 99 user != user_manager::UserManager::Get()->GetActiveUser()) {
tbarzic 2017/03/15 18:43:42 I don't think this is right. Active user could cha
Carlson 2017/03/15 23:06:16 Ha, I'd meant to figure this out later, but sure,
129 return; 100 return;
130 } 101 }
131 102 device::UsbService* usb_service =
132 device::UsbDeviceFilter printer_filter; 103 device::DeviceClient::Get()->GetUsbService();
133 printer_filter.interface_class = kPrinterInterfaceClass; 104 if (!usb_service) {
134 if (!printer_filter.Matches(device))
135 return; 105 return;
136 106 }
137 ShowUSBPrinterSetupNotification(device); 107 observer_.Add(usb_service);
108 usb_service->GetDevices(base::Bind(&CupsPrinterDetectorImpl::OnGetDevices,
109 weak_ptr_factory_.GetWeakPtr()));
138 } 110 }
139 111
140 // Initializes the printer detector. 112 // Callback for initial enumeration of usb devices. UsbService invokes
141 void Initialize() { 113 // this on the UI thread.
tbarzic 2017/03/15 18:43:42 nit: I'd remove the second sentence - it's clear f
Carlson 2017/03/15 23:06:16 Not sure I agree completely with this, I think the
142 device::UsbService* usb_service = 114 void OnGetDevices(
143 device::DeviceClient::Get()->GetUsbService(); 115 const std::vector<scoped_refptr<device::UsbDevice>>& devices) {
144 if (!usb_service) 116 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
117 for (const auto& device : devices) {
118 MaybeSetUpDevice(device, false);
119 }
120 }
121
122 // UsbService::observer override. This runs on the UI thread.
123 void OnDeviceAdded(scoped_refptr<device::UsbDevice> device) override {
124 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
125 MaybeSetUpDevice(device, true);
126 }
127
128 // UsbService::observer override. This runs on the UI thread.
129 void OnDeviceRemoved(scoped_refptr<device::UsbDevice> device) override {
130 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
131 if (!UsbDeviceIsPrinter(device)) {
145 return; 132 return;
146 observer_.Add(usb_service); 133 }
134 known_printers_.erase(device->guid());
135
tbarzic 2017/03/14 01:43:32 What if the removed printer is being set up? Shoul
Carlson 2017/03/15 00:31:15 It should be harmless. We'll end up setting up th
136 // If this printer appears in the list of printers we've failed to set up,
137 // remove it.
tbarzic 2017/03/15 18:43:42 nit: I'd remove the comment - the code is clear en
Carlson 2017/03/15 23:06:16 Done.
138 failed_setup_printers_.erase(device->guid());
139 }
140
141 // This is called on the UI thread.
142 void MaybeSetUpDevice(scoped_refptr<device::UsbDevice> device,
143 bool hotplugged) {
144 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
145
146 if (!UsbDeviceIsPrinter(device) ||
147 base::ContainsKey(known_printers_, device->guid())) {
148 return;
149 }
150 known_printers_.insert(device->guid());
151
152 auto data = base::MakeUnique<SetUpPrinterData>();
tbarzic 2017/03/14 01:43:32 I wonder if you could introduce a helper class tha
Carlson 2017/03/15 00:31:15 I really liked this idea and played with implement
153 data->configurer = PrinterConfigurer::Create(profile_);
154 data->printer = base::MakeUnique<Printer>();
155 data->device = device;
156 data->is_new = true;
157 data->hotplugged = hotplugged;
158 if (!UsbDeviceToPrinter(*device, data->printer.get())) {
159 LOG(ERROR)
160 << "Failed to convert usb device to printer, abandoning setup. "
161 << "Usb device details follow:";
162 LOG(ERROR) << UsbPrinterDeviceDetailsAsString(*device);
163 return;
164 }
165 // TODO(justincarlson): add a GetPrinterByURI to PrintersManager.
166 // (bug 700602)
tbarzic 2017/03/14 01:43:32 nit: https://crbug.com/700602 (instead of bug 7006
Carlson 2017/03/15 00:31:15 Done.
167 auto existing_printers =
168 PrintersManagerFactory::GetForBrowserContext(profile_)->GetPrinters();
169 for (std::unique_ptr<Printer>& printer : existing_printers) {
170 if (printer->uri() == data->printer->uri()) {
171 // Found a match, so use the existing configuration.
172 *(data->printer) = *printer;
173 data->is_new = false;
174
175 // Copy fields that will be invalidated by std::move.
176 auto* configurer_tmp = data->configurer.get();
tbarzic 2017/03/14 01:43:32 This is basically the same as what's done in Resol
Carlson 2017/03/15 00:31:15 Done.
177 const Printer& printer_tmp = *(data->printer);
178 // off to the configurer.
179 configurer_tmp->SetUpPrinter(
180 printer_tmp, base::Bind(&CupsPrinterDetectorImpl::SetUpPrinterDone,
181 weak_ptr_factory_.GetWeakPtr(),
182 base::Passed(std::move(data))));
183 return;
184 }
185 }
186
187 // It's not a device we know about. First we see if we can get an exact
188 // driver match based on USB ids.
189 //
190 // TODO(justincarlson): Add a notification that we are attempting to set up
191 // this printer at this point.
192 scoped_refptr<PpdProvider> ppd_provider =
193 printing::CreateProvider(profile_);
194 ppd_provider->ResolveUsbIds(
195 device->vendor_id(), device->product_id(),
196 base::Bind(&CupsPrinterDetectorImpl::ResolveUsbIdsDone,
197 weak_ptr_factory_.GetWeakPtr(), ppd_provider,
198 base::Passed(std::move(data))));
199 }
200
201 // Called when the query for a driver based on usb ids completes.
202 void ResolveUsbIdsDone(scoped_refptr<PpdProvider> provider,
tbarzic 2017/03/14 01:43:32 is it necessary to pass provider here?
Carlson 2017/03/15 00:31:15 Upon a lot of reflection, no. I thought I needed
203 std::unique_ptr<SetUpPrinterData> data,
204 PpdProvider::CallbackResultCode result,
205 const std::string& effective_make_and_model) {
206 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
207 if (result == PpdProvider::SUCCESS) {
208 // Got something based on usb ids. Go with it.
209 data->printer->mutable_ppd_reference()->effective_make_and_model =
210 effective_make_and_model;
211 } else {
212 // Couldn't figure this printer out based on usb ids, fall back to
213 // guessing the make/model string from what the USB system reports.
214 //
215 // TODO(justincarlson): Consider adding a mechanism for aggregating data
216 // about which usb devices are in the wild but unsupported?
217 data->printer->mutable_ppd_reference()->effective_make_and_model =
218 GuessEffectiveMakeAndModel(*data->device);
219 }
220
221 // Have to keep copies of some parameters that will be moved in the
222 // arguments and used in the same statement.
223 auto* configurer = data->configurer.get();
224 auto* printer = data->printer.get();
225
226 configurer->SetUpPrinter(
227 *printer, base::Bind(&CupsPrinterDetectorImpl::SetUpPrinterDone,
228 weak_ptr_factory_.GetWeakPtr(),
229 base::Passed(std::move(data))));
230 }
231
232 // Called on the UI thread with the result of asking to have a printer
233 // configured for CUPS. If |printer_to_register| is non-null and we
234 // successfully configured, then the printer is registered with the printers
235 // manager.
236 void SetUpPrinterDone(std::unique_ptr<SetUpPrinterData> data,
237 PrinterSetupResult result) {
238 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
239 if (result == PrinterSetupResult::SUCCESS) {
240 if (data->is_new) {
241 PrintersManagerFactory::GetForBrowserContext(profile_)->RegisterPrinter(
242 std::move(data->printer));
243 }
244 if (data->hotplugged) {
245 // TODO(justincarlson): Pop a timed notification that says the printer
246 // is now available for printing.
247 }
248 } else {
249 // TODO(justincarlson): Pop a notification that tells the user automatic
250 // setup failed and offers to open the CUPS printer configuration
251 // settings.
252 DCHECK(!base::ContainsKey(failed_setup_printers_, data->device->guid()));
253 failed_setup_printers_.insert({data->device->guid(), *(data->printer)});
254 }
147 } 255 }
148 256
149 void SetNotificationUIManagerForTesting( 257 void SetNotificationUIManagerForTesting(
150 NotificationUIManager* manager) override { 258 NotificationUIManager* manager) override {
151 LOG(FATAL) << "Not implemented for CUPS"; 259 LOG(FATAL) << "Not implemented for CUPS";
152 } 260 }
153 261
154 void ShowUSBPrinterSetupNotification( 262 // USB GUIDs of printers we've already dealt with. There's an inherent race
155 scoped_refptr<device::UsbDevice> device) { 263 // between initially querying all usb devices and receiving a notification
156 // TODO(justincarlson) - Test this notification across a wide variety of 264 // about a new device, so this set lets us guarantee that we handle a given
157 // less-than-sane printers to make sure the notification text stays as 265 // printer exactly once.
158 // intelligible as possible. 266 std::set<std::string> known_printers_;
159 base::string16 printer_name = device->manufacturer_string() +
160 base::UTF8ToUTF16(" ") +
161 device->product_string();
162 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
163 message_center::RichNotificationData data;
164 data.buttons.push_back(message_center::ButtonInfo(l10n_util::GetStringUTF16(
165 IDS_PRINTER_DETECTED_NOTIFICATION_SET_UP_BUTTON)));
166 notification_.reset(new Notification(
167 message_center::NOTIFICATION_TYPE_SIMPLE,
168 l10n_util::GetStringUTF16(
169 IDS_PRINTER_DETECTED_NOTIFICATION_SET_UP_TITLE), // title
170 printer_name, // body
171 bundle.GetImageNamed(IDR_PRINTER_DETECTED_NOTIFICATION), // icon
172 message_center::NotifierId(message_center::NotifierId::SYSTEM_COMPONENT,
173 kUSBPrinterFoundNotificationID),
174 base::string16(), // display_source
175 GURL(), kUSBPrinterFoundNotificationID, data,
176 new USBPrinterSetupNotificationDelegate(this)));
177 notification_->SetSystemPriority();
178 command_ = ButtonCommand::SETUP;
179 267
180 g_browser_process->notification_ui_manager()->Add(*notification_, profile_); 268 // Printers that this class failed to set up automatically. This is provided
181 } 269 // to the PrinterDiscoverer used in the settings flow to determine which
182 270 // printers are available to be set up. This is a map from the USB GUID
183 void OnSetUpUSBPrinterStarted() { 271 // to Printer structure.
184 notification_->set_title(l10n_util::GetStringUTF16( 272 std::unordered_map<std::string, Printer> failed_setup_printers_;
185 IDS_PRINTER_DETECTED_NOTIFICATION_SET_UP_IN_PROGRESS_TITLE));
186 notification_->set_type(message_center::NOTIFICATION_TYPE_PROGRESS);
187 notification_->set_progress(-1);
188 std::vector<message_center::ButtonInfo> buttons;
189 buttons.push_back(message_center::ButtonInfo(l10n_util::GetStringUTF16(
190 IDS_PRINTER_DETECTED_NOTIFICATION_SET_UP_CANCEL_BUTTON)));
191 notification_->set_buttons(buttons);
192 command_ = ButtonCommand::CANCEL_SETUP;
193 g_browser_process->notification_ui_manager()->Add(*notification_, profile_);
194 }
195
196 void OnSetUpUSBPrinterDone() {
197 notification_->set_title(l10n_util::GetStringUTF16(
198 IDS_PRINTER_DETECTED_NOTIFICATION_SET_UP_SUCCESS_TITLE));
199 notification_->set_type(message_center::NOTIFICATION_TYPE_SIMPLE);
200 std::vector<message_center::ButtonInfo> buttons;
201 buttons.push_back(message_center::ButtonInfo(l10n_util::GetStringUTF16(
202 IDS_PRINTER_DETECTED_NOTIFICATION_SET_UP_CLOSE_BUTTON)));
203 notification_->set_buttons(buttons);
204 command_ = ButtonCommand::CLOSE;
205 g_browser_process->notification_ui_manager()->Add(*notification_, profile_);
206 }
207
208 void OnSetUpUSBPrinterError() {
209 notification_->set_title(l10n_util::GetStringUTF16(
210 IDS_PRINTER_DETECTED_NOTIFICATION_SET_UP_FAILED_TITLE));
211 notification_->set_type(message_center::NOTIFICATION_TYPE_SIMPLE);
212 std::vector<message_center::ButtonInfo> buttons;
213 buttons.push_back(message_center::ButtonInfo(l10n_util::GetStringUTF16(
214 IDS_PRINTER_DETECTED_NOTIFICATION_SET_UP_GET_HELP_BUTTON)));
215 notification_->set_buttons(buttons);
216 command_ = ButtonCommand::GET_HELP;
217 g_browser_process->notification_ui_manager()->Add(*notification_, profile_);
218 }
219
220 std::unique_ptr<Notification> notification_;
221 ButtonCommand command_ = ButtonCommand::SETUP;
222 273
223 Profile* profile_; 274 Profile* profile_;
224 ScopedObserver<device::UsbService, device::UsbService::Observer> observer_; 275 ScopedObserver<device::UsbService, device::UsbService::Observer> observer_;
225 base::WeakPtrFactory<CupsPrinterDetectorImpl> weak_ptr_factory_; 276 base::WeakPtrFactory<CupsPrinterDetectorImpl> weak_ptr_factory_;
226 }; 277 };
227 278
228 void USBPrinterSetupNotificationDelegate::ButtonClick(int button_index) {
229 printer_detector_->ClickOnNotificationButton(button_index);
230 }
231
232 } // namespace 279 } // namespace
233 280
234 // static 281 // static
235 std::unique_ptr<PrinterDetector> PrinterDetector::CreateCups(Profile* profile) { 282 std::unique_ptr<PrinterDetector> PrinterDetector::CreateCups(Profile* profile) {
236 return base::MakeUnique<CupsPrinterDetectorImpl>(profile); 283 return base::MakeUnique<CupsPrinterDetectorImpl>(profile);
237 } 284 }
238 285
239 } // namespace chromeos 286 } // namespace chromeos
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698