blob: fe52daf5571c4c6bfc3e082b8eb13285e7678f9c [file] [log] [blame]
Rayan Kanso68904202019-02-21 14:16:251// Copyright 2019 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
5Resources.BackgroundServiceView = class extends UI.VBox {
6 /**
Rayan Kansoc0bfdd82019-04-24 12:32:227 * @param {string} serviceName The name of the background service.
8 * @return {string} The UI String to display.
9 */
10 static getUIString(serviceName) {
11 switch (serviceName) {
12 case Protocol.BackgroundService.ServiceName.BackgroundFetch:
13 return ls`Background Fetch`;
14 case Protocol.BackgroundService.ServiceName.BackgroundSync:
15 return ls`Background Sync`;
Fergus Dall0be4f4c2019-05-21 00:01:2916 case Protocol.BackgroundService.ServiceName.PushMessaging:
17 return ls`Push Messaging`;
18 case Protocol.BackgroundService.ServiceName.Notifications:
19 return ls`Notifications`;
Rayan Kanso8d7918a2019-07-03 21:03:3820 case Protocol.BackgroundService.ServiceName.PaymentHandler:
21 return ls`Payment Handler`;
Rayan Kanso54809672019-07-24 18:40:2822 case Protocol.BackgroundService.ServiceName.PeriodicBackgroundSync:
23 return ls`Periodic Background Sync`;
Rayan Kansoc0bfdd82019-04-24 12:32:2224 default:
25 return '';
26 }
27 }
28
29 /**
Rayan Kanso8fe8ee22019-03-04 14:58:4630 * @param {!Protocol.BackgroundService.ServiceName} serviceName
31 * @param {!Resources.BackgroundServiceModel} model
Rayan Kanso68904202019-02-21 14:16:2532 */
Rayan Kanso8fe8ee22019-03-04 14:58:4633 constructor(serviceName, model) {
Rayan Kanso68904202019-02-21 14:16:2534 super(true);
35 this.registerRequiredCSS('resources/backgroundServiceView.css');
Rayan Kanso6156d222019-04-29 23:40:5536 this.registerRequiredCSS('ui/emptyWidget.css');
Rayan Kanso68904202019-02-21 14:16:2537
Rayan Kanso8fe8ee22019-03-04 14:58:4638 /** @const {!Protocol.BackgroundService.ServiceName} */
Rayan Kanso68904202019-02-21 14:16:2539 this._serviceName = serviceName;
40
Rayan Kanso8fe8ee22019-03-04 14:58:4641 /** @const {!Resources.BackgroundServiceModel} */
42 this._model = model;
43 this._model.addEventListener(
44 Resources.BackgroundServiceModel.Events.RecordingStateChanged, this._onRecordingStateChanged, this);
Rayan Kansof40e3152019-03-11 13:49:4345 this._model.addEventListener(
46 Resources.BackgroundServiceModel.Events.BackgroundServiceEventReceived, this._onEventReceived, this);
Rayan Kanso8fe8ee22019-03-04 14:58:4647 this._model.enable(this._serviceName);
48
Rayan Kansoaca06e72019-03-27 11:57:0649 /** @const {?SDK.ServiceWorkerManager} */
50 this._serviceWorkerManager = this._model.target().model(SDK.ServiceWorkerManager);
51
52 /** @const {?SDK.SecurityOriginManager} */
53 this._securityOriginManager = this._model.target().model(SDK.SecurityOriginManager);
54 this._securityOriginManager.addEventListener(
55 SDK.SecurityOriginManager.Events.MainSecurityOriginChanged, () => this._onOriginChanged());
56
Rayan Kansocc02ea32019-05-02 21:26:5257
58 /** @const {!UI.Action} */
59 this._recordAction = /** @type {!UI.Action} */ (UI.actionRegistry.action('background-service.toggle-recording'));
Joel Einbinder346980e2019-07-16 15:35:4860 /** @type {?UI.ToolbarButton} */
Rayan Kanso8fe8ee22019-03-04 14:58:4661 this._recordButton = null;
62
Rayan Kansoaca06e72019-03-27 11:57:0663 /** @type {?UI.ToolbarCheckbox} */
64 this._originCheckbox = null;
65
Rayan Kansob852ba82019-04-08 13:48:0766 /** @type {?UI.ToolbarButton} */
67 this._saveButton = null;
68
Rayan Kanso68904202019-02-21 14:16:2569 /** @const {!UI.Toolbar} */
70 this._toolbar = new UI.Toolbar('background-service-toolbar', this.contentElement);
71 this._setupToolbar();
Rayan Kanso3252d5e2019-03-27 11:37:2472
Rayan Kansob451b4f2019-04-04 23:12:1173 /**
74 * This will contain the DataGrid for displaying events, and a panel at the bottom for showing
75 * extra metadata related to the selected event.
76 * @const {!UI.SplitWidget}
77 */
78 this._splitWidget = new UI.SplitWidget(/* isVertical= */ false, /* secondIsSidebar= */ true);
79 this._splitWidget.show(this.contentElement);
80
Rayan Kanso3252d5e2019-03-27 11:37:2481 /** @const {!DataGrid.DataGrid} */
82 this._dataGrid = this._createDataGrid();
Rayan Kansob451b4f2019-04-04 23:12:1183
84 /** @const {!UI.VBox} */
85 this._previewPanel = new UI.VBox();
86
Rayan Kansoc0bfdd82019-04-24 12:32:2287 /** @type {?Resources.BackgroundServiceView.EventDataNode} */
88 this._selectedEventNode = null;
89
Rayan Kansob451b4f2019-04-04 23:12:1190 /** @type {?UI.Widget} */
91 this._preview = null;
92
93 this._splitWidget.setMainWidget(this._dataGrid.asWidget());
94 this._splitWidget.setSidebarWidget(this._previewPanel);
95
96 this._showPreview(null);
Rayan Kanso68904202019-02-21 14:16:2597 }
98
99 /**
100 * Creates the toolbar UI element.
101 */
Rayan Kanso8fe8ee22019-03-04 14:58:46102 async _setupToolbar() {
Rayan Kansocc02ea32019-05-02 21:26:52103 this._recordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kanso8fe8ee22019-03-04 14:58:46104 this._toolbar.appendToolbarItem(this._recordButton);
Rayan Kanso68904202019-02-21 14:16:25105
Rayan Kansob852ba82019-04-08 13:48:07106 const clearButton = new UI.ToolbarButton(ls`Clear`, 'largeicon-clear');
Rayan Kansob451b4f2019-04-04 23:12:11107 clearButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._clearEvents());
Rayan Kanso68904202019-02-21 14:16:25108 this._toolbar.appendToolbarItem(clearButton);
109
110 this._toolbar.appendSeparator();
111
Rayan Kansob852ba82019-04-08 13:48:07112 this._saveButton = new UI.ToolbarButton(ls`Save events`, 'largeicon-download');
113 this._saveButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._saveToFile());
114 this._saveButton.setEnabled(false);
115 this._toolbar.appendToolbarItem(this._saveButton);
Rayan Kansoc0bfdd82019-04-24 12:32:22116
117 this._toolbar.appendSeparator();
118
119 this._originCheckbox =
120 new UI.ToolbarCheckbox(ls`Show events from other domains`, undefined, () => this._refreshView());
121 this._toolbar.appendToolbarItem(this._originCheckbox);
Rayan Kanso68904202019-02-21 14:16:25122 }
Rayan Kanso8fe8ee22019-03-04 14:58:46123
124 /**
Rayan Kansob451b4f2019-04-04 23:12:11125 * Displays all available events in the grid.
126 */
127 _refreshView() {
128 this._clearView();
129 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
Tim van der Lippe1d6e57a2019-09-30 11:55:34130 for (const event of events) {
Rayan Kansob451b4f2019-04-04 23:12:11131 this._addEvent(event);
Tim van der Lippe1d6e57a2019-09-30 11:55:34132 }
Rayan Kansob451b4f2019-04-04 23:12:11133 }
134
135 /**
136 * Clears the grid and panel.
137 */
138 _clearView() {
Rayan Kansoc0bfdd82019-04-24 12:32:22139 this._selectedEventNode = null;
Rayan Kansob451b4f2019-04-04 23:12:11140 this._dataGrid.rootNode().removeChildren();
Rayan Kansob852ba82019-04-08 13:48:07141 this._saveButton.setEnabled(false);
Rayan Kansoc0bfdd82019-04-24 12:32:22142 this._showPreview(null);
Rayan Kansob451b4f2019-04-04 23:12:11143 }
144
145 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46146 * Called when the `Toggle Record` button is clicked.
147 */
148 _toggleRecording() {
149 this._model.setRecording(!this._recordButton.toggled(), this._serviceName);
150 }
151
152 /**
Rayan Kanso3252d5e2019-03-27 11:37:24153 * Called when the `Clear` button is clicked.
154 */
Rayan Kansob451b4f2019-04-04 23:12:11155 _clearEvents() {
Rayan Kanso3252d5e2019-03-27 11:37:24156 this._model.clearEvents(this._serviceName);
157 this._clearView();
158 }
159
160 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46161 * @param {!Common.Event} event
162 */
163 _onRecordingStateChanged(event) {
164 const state = /** @type {!Resources.BackgroundServiceModel.RecordingState} */ (event.data);
Tim van der Lippe1d6e57a2019-09-30 11:55:34165 if (state.serviceName !== this._serviceName) {
Rayan Kanso8fe8ee22019-03-04 14:58:46166 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34167 }
Rayan Kansoc0bfdd82019-04-24 12:32:22168
Tim van der Lippe1d6e57a2019-09-30 11:55:34169 if (state.isRecording === this._recordButton.toggled()) {
Rayan Kansoc0bfdd82019-04-24 12:32:22170 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34171 }
Rayan Kansoc0bfdd82019-04-24 12:32:22172
Rayan Kanso0b30aba2019-05-17 13:39:59173 this._recordButton.setToggled(state.isRecording);
Rayan Kansoc0bfdd82019-04-24 12:32:22174 this._showPreview(this._selectedEventNode);
Rayan Kanso8fe8ee22019-03-04 14:58:46175 }
Rayan Kansof40e3152019-03-11 13:49:43176
177 /**
178 * @param {!Common.Event} event
179 */
180 _onEventReceived(event) {
181 const serviceEvent = /** @type {!Protocol.BackgroundService.BackgroundServiceEvent} */ (event.data);
Tim van der Lippe1d6e57a2019-09-30 11:55:34182 if (!this._acceptEvent(serviceEvent)) {
Rayan Kansof40e3152019-03-11 13:49:43183 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34184 }
Rayan Kanso3252d5e2019-03-27 11:37:24185 this._addEvent(serviceEvent);
186 }
187
Rayan Kansoaca06e72019-03-27 11:57:06188 _onOriginChanged() {
189 // No need to refresh the view if we are already showing all events.
Tim van der Lippe1d6e57a2019-09-30 11:55:34190 if (this._originCheckbox.checked()) {
Rayan Kansoaca06e72019-03-27 11:57:06191 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34192 }
Rayan Kansoaca06e72019-03-27 11:57:06193 this._refreshView();
194 }
195
Rayan Kanso3252d5e2019-03-27 11:37:24196 /**
197 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
198 */
199 _addEvent(serviceEvent) {
Rayan Kansoaca06e72019-03-27 11:57:06200 const data = this._createEventData(serviceEvent);
201 const dataNode = new Resources.BackgroundServiceView.EventDataNode(data, serviceEvent.eventMetadata);
Rayan Kanso3252d5e2019-03-27 11:37:24202 this._dataGrid.rootNode().appendChild(dataNode);
Rayan Kansob852ba82019-04-08 13:48:07203
Rayan Kansoc0bfdd82019-04-24 12:32:22204 if (this._dataGrid.rootNode().children.length === 1) {
205 this._saveButton.setEnabled(true);
206 this._showPreview(this._selectedEventNode);
207 }
Rayan Kanso3252d5e2019-03-27 11:37:24208 }
209
210 /**
211 * @return {!DataGrid.DataGrid}
212 */
213 _createDataGrid() {
214 const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([
Rayan Kansob852ba82019-04-08 13:48:07215 {id: 'id', title: ls`#`, weight: 1},
216 {id: 'timestamp', title: ls`Timestamp`, weight: 8},
Rayan Kansoc0bfdd82019-04-24 12:32:22217 {id: 'eventName', title: ls`Event`, weight: 10},
Rayan Kansob852ba82019-04-08 13:48:07218 {id: 'origin', title: ls`Origin`, weight: 10},
Rayan Kansocc02ea32019-05-02 21:26:52219 {id: 'swScope', title: ls`SW Scope`, weight: 2},
Rayan Kansob852ba82019-04-08 13:48:07220 {id: 'instanceId', title: ls`Instance ID`, weight: 10},
Rayan Kanso3252d5e2019-03-27 11:37:24221 ]);
222 const dataGrid = new DataGrid.DataGrid(columns);
223 dataGrid.setStriped(true);
Rayan Kansob451b4f2019-04-04 23:12:11224
225 dataGrid.addEventListener(
226 DataGrid.DataGrid.Events.SelectedNode,
227 event => this._showPreview(/** @type {!Resources.BackgroundServiceView.EventDataNode} */ (event.data)));
228
Rayan Kanso3252d5e2019-03-27 11:37:24229 return dataGrid;
230 }
231
232 /**
Rayan Kansoaca06e72019-03-27 11:57:06233 * Creates the data object to pass to the DataGrid Node.
234 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
235 * @return {!Resources.BackgroundServiceView.EventData}
236 */
237 _createEventData(serviceEvent) {
Rayan Kansocc02ea32019-05-02 21:26:52238 let swScope = '';
Rayan Kansoaca06e72019-03-27 11:57:06239
Rayan Kansocc02ea32019-05-02 21:26:52240 // Try to get the scope of the Service Worker registration to be more user-friendly.
241 const registration = this._serviceWorkerManager.registrations().get(serviceEvent.serviceWorkerRegistrationId);
Tim van der Lippe1d6e57a2019-09-30 11:55:34242 if (registration) {
Rayan Kansocc02ea32019-05-02 21:26:52243 swScope = registration.scopeURL.substr(registration.securityOrigin.length);
Tim van der Lippe1d6e57a2019-09-30 11:55:34244 }
Rayan Kansoaca06e72019-03-27 11:57:06245
246 return {
Rayan Kansoc79a8bb2019-05-29 21:08:38247 id: this._dataGrid.rootNode().children.length + 1,
Rayan Kansoaca06e72019-03-27 11:57:06248 timestamp: UI.formatTimestamp(serviceEvent.timestamp * 1000, /* full= */ true),
249 origin: serviceEvent.origin,
Rayan Kansocc02ea32019-05-02 21:26:52250 swScope,
Rayan Kansoaca06e72019-03-27 11:57:06251 eventName: serviceEvent.eventName,
252 instanceId: serviceEvent.instanceId,
253 };
254 }
255
256 /**
Rayan Kanso3252d5e2019-03-27 11:37:24257 * Filtration function to know whether event should be shown or not.
258 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} event
259 * @return {boolean}
260 */
261 _acceptEvent(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34262 if (event.service !== this._serviceName) {
Rayan Kansoaca06e72019-03-27 11:57:06263 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34264 }
Rayan Kansoaca06e72019-03-27 11:57:06265
Tim van der Lippe1d6e57a2019-09-30 11:55:34266 if (this._originCheckbox.checked()) {
Rayan Kansoaca06e72019-03-27 11:57:06267 return true;
Tim van der Lippe1d6e57a2019-09-30 11:55:34268 }
Rayan Kansoaca06e72019-03-27 11:57:06269
270 // Trim the trailing '/'.
271 const origin = event.origin.substr(0, event.origin.length - 1);
272
273 return this._securityOriginManager.securityOrigins().includes(origin);
Rayan Kanso3252d5e2019-03-27 11:37:24274 }
Rayan Kansob451b4f2019-04-04 23:12:11275
276 /**
Rayan Kanso42c0f9e2019-09-02 12:21:09277 * @return {!Element}
278 */
279 _createLearnMoreLink() {
280 let url =
281 'https://developers.google.com/web/tools/chrome-devtools/javascript/background-services?utm_source=devtools';
282
283 switch (this._serviceName) {
284 case Protocol.BackgroundService.ServiceName.BackgroundFetch:
285 url += '#fetch';
286 break;
287 case Protocol.BackgroundService.ServiceName.BackgroundSync:
288 url += '#sync';
289 break;
290 case Protocol.BackgroundService.ServiceName.PushMessaging:
291 url += '#push';
292 break;
293 case Protocol.BackgroundService.ServiceName.Notifications:
294 url += '#notifications';
295 break;
296 default:
297 break;
298 }
299
300 return UI.XLink.create(url, ls`Learn more`);
301 }
302
303 /**
Rayan Kansob451b4f2019-04-04 23:12:11304 * @param {?Resources.BackgroundServiceView.EventDataNode} dataNode
305 */
306 _showPreview(dataNode) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34307 if (this._selectedEventNode && this._selectedEventNode === dataNode) {
Rayan Kansoc0bfdd82019-04-24 12:32:22308 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34309 }
Rayan Kansoc0bfdd82019-04-24 12:32:22310
311 this._selectedEventNode = dataNode;
312
Tim van der Lippe1d6e57a2019-09-30 11:55:34313 if (this._preview) {
Rayan Kansob451b4f2019-04-04 23:12:11314 this._preview.detach();
Tim van der Lippe1d6e57a2019-09-30 11:55:34315 }
Rayan Kansob451b4f2019-04-04 23:12:11316
Rayan Kansoc0bfdd82019-04-24 12:32:22317 if (this._selectedEventNode) {
318 this._preview = this._selectedEventNode.createPreview();
Rayan Kanso4476ca52019-07-22 11:51:24319 this._preview.show(this._previewPanel.contentElement);
320 return;
321 }
322
323 this._preview = new UI.VBox();
324 this._preview.contentElement.classList.add('background-service-preview', 'fill');
325 const centered = this._preview.contentElement.createChild('div');
326
327 if (this._dataGrid.rootNode().children.length) {
Rayan Kansoc0bfdd82019-04-24 12:32:22328 // Inform users that grid entries are clickable.
Rayan Kanso4476ca52019-07-22 11:51:24329 centered.createChild('p').textContent = ls`Select an entry to view metadata`;
Rayan Kansoc0bfdd82019-04-24 12:32:22330 } else if (this._recordButton.toggled()) {
331 // Inform users that we are recording/waiting for events.
Rayan Kanso4476ca52019-07-22 11:51:24332 const featureName = Resources.BackgroundServiceView.getUIString(this._serviceName);
333 centered.createChild('p').textContent = ls`Recording ${featureName} activity...`;
334 centered.createChild('p').textContent =
335 ls`DevTools will record all ${featureName} activity for up to 3 days, even when closed.`;
Rayan Kansoc0bfdd82019-04-24 12:32:22336 } else {
Rayan Kansocc02ea32019-05-02 21:26:52337 const landingRecordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kansoc0bfdd82019-04-24 12:32:22338
Rayan Kanso6156d222019-04-29 23:40:55339 const recordKey = createElementWithClass('b', 'background-service-shortcut');
340 recordKey.textContent =
341 UI.shortcutRegistry.shortcutDescriptorsForAction('background-service.toggle-recording')[0].name;
342
Rayan Kansof402ef32019-08-12 13:05:18343 const inlineButton = UI.createInlineButton(landingRecordButton);
344 inlineButton.classList.add('background-service-record-inline-button');
345 centered.createChild('p').appendChild(
346 UI.formatLocalized('Click the record button %s or hit %s to start recording.', [inlineButton, recordKey]));
Rayan Kanso42c0f9e2019-09-02 12:21:09347
348 centered.appendChild(this._createLearnMoreLink());
Rayan Kansoc0bfdd82019-04-24 12:32:22349 }
Rayan Kansob451b4f2019-04-04 23:12:11350
351 this._preview.show(this._previewPanel.contentElement);
352 }
Rayan Kansob852ba82019-04-08 13:48:07353
354 /**
355 * Saves all currently displayed events in a file (JSON format).
356 */
357 async _saveToFile() {
358 const fileName = `${this._serviceName}-${new Date().toISO8601Compact()}.json`;
359 const stream = new Bindings.FileOutputStream();
360
361 const accepted = await stream.open(fileName);
Tim van der Lippe1d6e57a2019-09-30 11:55:34362 if (!accepted) {
Rayan Kansob852ba82019-04-08 13:48:07363 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34364 }
Rayan Kansob852ba82019-04-08 13:48:07365
366 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
367 await stream.write(JSON.stringify(events, undefined, 2));
368 stream.close();
369 }
Rayan Kanso3252d5e2019-03-27 11:37:24370};
371
Rayan Kansoaca06e72019-03-27 11:57:06372/**
373 * @typedef {{
374 * id: number,
375 * timestamp: string,
376 * origin: string,
Rayan Kansocc02ea32019-05-02 21:26:52377 * swScope: string,
Rayan Kansoaca06e72019-03-27 11:57:06378 * eventName: string,
379 * instanceId: string,
380 * }}
381 */
382Resources.BackgroundServiceView.EventData;
383
Rayan Kanso3252d5e2019-03-27 11:37:24384Resources.BackgroundServiceView.EventDataNode = class extends DataGrid.DataGridNode {
385 /**
Rayan Kansoaca06e72019-03-27 11:57:06386 * @param {!Object<string, string>} data
387 * @param {!Array<!Protocol.BackgroundService.EventMetadata>} eventMetadata
Rayan Kanso3252d5e2019-03-27 11:37:24388 */
Rayan Kansoaca06e72019-03-27 11:57:06389 constructor(data, eventMetadata) {
390 super(data);
Rayan Kanso3252d5e2019-03-27 11:37:24391
392 /** @const {!Array<!Protocol.BackgroundService.EventMetadata>} */
Rayan Kansoc79a8bb2019-05-29 21:08:38393 this._eventMetadata = eventMetadata.sort((m1, m2) => m1.key.compareTo(m2.key));
Rayan Kanso3252d5e2019-03-27 11:37:24394 }
395
396 /**
Rayan Kansoc0bfdd82019-04-24 12:32:22397 * @return {!UI.VBox}
Rayan Kanso3252d5e2019-03-27 11:37:24398 */
Rayan Kansob451b4f2019-04-04 23:12:11399 createPreview() {
Rayan Kansoc0bfdd82019-04-24 12:32:22400 const preview = new UI.VBox();
401 preview.element.classList.add('background-service-metadata');
402
403 for (const entry of this._eventMetadata) {
404 const div = createElementWithClass('div', 'background-service-metadata-entry');
405 div.createChild('div', 'background-service-metadata-name').textContent = entry.key + ': ';
Rayan Kanso4476ca52019-07-22 11:51:24406 if (entry.value) {
407 div.createChild('div', 'background-service-metadata-value source-code').textContent = entry.value;
408 } else {
409 div.createChild('div', 'background-service-metadata-value background-service-empty-value').textContent =
410 ls`empty`;
411 }
Rayan Kansoc0bfdd82019-04-24 12:32:22412 preview.element.appendChild(div);
413 }
414
415 if (!preview.element.children.length) {
416 const div = createElementWithClass('div', 'background-service-metadata-entry');
417 div.createChild('div', 'background-service-metadata-name').textContent = ls`No metadata for this event`;
418 preview.element.appendChild(div);
419 }
420
421 return preview;
Rayan Kansof40e3152019-03-11 13:49:43422 }
Rayan Kanso68904202019-02-21 14:16:25423};
Rayan Kanso6156d222019-04-29 23:40:55424
425/**
426 * @implements {UI.ActionDelegate}
427 * @unrestricted
428 */
429Resources.BackgroundServiceView.ActionDelegate = class {
430 /**
431 * @override
432 * @param {!UI.Context} context
433 * @param {string} actionId
434 * @return {boolean}
435 */
436 handleAction(context, actionId) {
437 const view = context.flavor(Resources.BackgroundServiceView);
438 switch (actionId) {
439 case 'background-service.toggle-recording':
440 view._toggleRecording();
441 return true;
442 }
443 return false;
444 }
445};