Create a protocol + handler for Background Service recording.

- Create protocol for starting/stopping/querying recording mode
- Add handler which talks to the DevTools context
- Add a SDK model that the view communicates with
- flat_map -> array to avoid multi-thread access issues

Bug: 927726
Change-Id: I11e55a747c7c5c60465508485d6b869b976d40dd
Reviewed-on: https://chromium-review.googlesource.com/c/1477851
Reviewed-by: Dmitry Gozman <[email protected]>
Commit-Queue: Rayan Kanso <[email protected]>
Cr-Original-Commit-Position: refs/heads/master@{#637287}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 31ad82ae99ed2a328d95fa6f73360a7c13561436
diff --git a/front_end/main/Main.js b/front_end/main/Main.js
index 62d2936..3d3214e 100644
--- a/front_end/main/Main.js
+++ b/front_end/main/Main.js
@@ -106,7 +106,7 @@
   _initializeExperiments() {
     // Keep this sorted alphabetically: both keys and values.
     Runtime.experiments.register('applyCustomStylesheet', 'Allow custom UI themes');
-    Runtime.experiments.register('backgroundServices', 'Background web platform feature events');
+    Runtime.experiments.register('backgroundServices', 'Background web platform feature events', true);
     Runtime.experiments.register('blackboxJSFramesOnTimeline', 'Blackbox JavaScript frames on Timeline', true);
     Runtime.experiments.register('emptySourceMapAutoStepping', 'Empty sourcemap auto-stepping');
     Runtime.experiments.register('inputEventsOnTimelineOverview', 'Input events on Timeline overview', true);
diff --git a/front_end/resources/ApplicationPanelSidebar.js b/front_end/resources/ApplicationPanelSidebar.js
index b7929d1..18f1c2b 100644
--- a/front_end/resources/ApplicationPanelSidebar.js
+++ b/front_end/resources/ApplicationPanelSidebar.js
@@ -58,10 +58,10 @@
     this._applicationTreeElement.appendChild(clearStorageTreeElement);
     if (Runtime.experiments.isEnabled('backgroundServices')) {
       this.backgroundFetchTreeElement =
-          new Resources.BackgroundServiceTreeElement(panel, Common.UIString('Background Fetch'));
+          new Resources.BackgroundServiceTreeElement(panel, Protocol.BackgroundService.ServiceName.BackgroundFetch);
       this._applicationTreeElement.appendChild(this.backgroundFetchTreeElement);
       this.backgroundSyncTreeElement =
-          new Resources.BackgroundServiceTreeElement(panel, Common.UIString('Background Sync'));
+          new Resources.BackgroundServiceTreeElement(panel, Protocol.BackgroundService.ServiceName.BackgroundSync);
       this._applicationTreeElement.appendChild(this.backgroundSyncTreeElement);
     }
 
@@ -210,6 +210,11 @@
     this.indexedDBListTreeElement._initialize();
     const serviceWorkerCacheModel = this._target.model(SDK.ServiceWorkerCacheModel);
     this.cacheStorageListTreeElement._initialize(serviceWorkerCacheModel);
+    const backgroundServiceModel = this._target.model(Resources.BackgroundServiceModel);
+    if (Runtime.experiments.isEnabled('backgroundServices')) {
+      this.backgroundFetchTreeElement._initialize(backgroundServiceModel);
+      this.backgroundSyncTreeElement._initialize(backgroundServiceModel);
+    }
   }
 
   /**
@@ -683,22 +688,53 @@
 Resources.BackgroundServiceTreeElement = class extends Resources.BaseStorageTreeElement {
   /**
    * @param {!Resources.ResourcesPanel} storagePanel
-   * @param {string} serviceName
+   * @param {!Protocol.BackgroundService.ServiceName} serviceName
    */
   constructor(storagePanel, serviceName) {
-    super(storagePanel, serviceName, false);
+    super(storagePanel, Resources.BackgroundServiceTreeElement._getUIString(serviceName), false);
 
-    /** @const {string} */
+    /** @const {!Protocol.BackgroundService.ServiceName} */
     this._serviceName = serviceName;
 
+    /** @type {boolean} Whether the element has been selected. */
+    this._selected = false;
+
     /** @type {?Resources.BackgroundServiceView} */
     this._view = null;
 
+    /** @private {?Resources.BackgroundServiceModel} */
+    this._model = null;
+
     const backgroundServiceIcon = UI.Icon.create('mediumicon-table', 'resource-tree-item');
     this.setLeadingIcons([backgroundServiceIcon]);
   }
 
   /**
+   * @param {string} serviceName The name of the background service.
+   * @return {string} The UI String to display.
+   */
+  static _getUIString(serviceName) {
+    switch (serviceName) {
+      case Protocol.BackgroundService.ServiceName.BackgroundFetch:
+        return Common.UIString('Background Fetch');
+      case Protocol.BackgroundService.ServiceName.BackgroundSync:
+        return Common.UIString('Background Sync');
+      default:
+        return '';
+    }
+  }
+
+  /**
+   * @param {?Resources.BackgroundServiceModel} model
+   */
+  _initialize(model) {
+    this._model = model;
+    // Show the view if the model was initialized after selection.
+    if (this._selected && !this._view)
+      this.onselect(false);
+  }
+
+  /**
    * @return {string}
    */
   get itemURL() {
@@ -711,8 +747,13 @@
    */
   onselect(selectedByUser) {
     super.onselect(selectedByUser);
+    this._selected = true;
+
+    if (!this._model)
+      return false;
+
     if (!this._view)
-      this._view = new Resources.BackgroundServiceView(this._serviceName);
+      this._view = new Resources.BackgroundServiceView(this._serviceName, this._model);
     this.showView(this._view);
     return false;
   }
diff --git a/front_end/resources/BackgroundServiceModel.js b/front_end/resources/BackgroundServiceModel.js
new file mode 100644
index 0000000..b701a33
--- /dev/null
+++ b/front_end/resources/BackgroundServiceModel.js
@@ -0,0 +1,54 @@
+// Copyright 2019 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.
+/**
+ * @implements {Protocol.BackgroundServiceDispatcher}
+ * @unrestricted
+ */
+Resources.BackgroundServiceModel = class extends SDK.SDKModel {
+  /**
+   * @param {!SDK.Target} target
+   */
+  constructor(target) {
+    super(target);
+    this._backgroundServiceAgent = target.backgroundServiceAgent();
+    target.registerBackgroundServiceDispatcher(this);
+  }
+
+  /**
+   * @param {!Protocol.BackgroundService.ServiceName} serviceName
+   */
+  enable(serviceName) {
+    this._backgroundServiceAgent.enable(serviceName);
+  }
+
+  /**
+   * @param {boolean} shouldRecord
+   * @param {!Protocol.BackgroundService.ServiceName} serviceName
+   */
+  setRecording(shouldRecord, serviceName) {
+    this._backgroundServiceAgent.setRecording(shouldRecord, serviceName);
+  }
+
+  /**
+   * @override
+   * @param {boolean} isRecording
+   * @param {!Protocol.BackgroundService.ServiceName} serviceName
+   */
+  recordingStateChanged(isRecording, serviceName) {
+    this.dispatchEventToListeners(
+        Resources.BackgroundServiceModel.Events.RecordingStateChanged, {isRecording, serviceName});
+  }
+};
+
+SDK.SDKModel.register(Resources.BackgroundServiceModel, SDK.Target.Capability.Browser, false);
+
+/** @enum {symbol} */
+Resources.BackgroundServiceModel.Events = {
+  RecordingStateChanged: Symbol('RecordingStateChanged'),
+};
+
+/**
+ * @typedef {!{isRecording: boolean, serviceName: !Protocol.BackgroundService.ServiceName}}
+ */
+Resources.BackgroundServiceModel.RecordingState;
diff --git a/front_end/resources/BackgroundServiceView.js b/front_end/resources/BackgroundServiceView.js
index 62c2db9..11b47d8 100644
--- a/front_end/resources/BackgroundServiceView.js
+++ b/front_end/resources/BackgroundServiceView.js
@@ -4,15 +4,25 @@
 
 Resources.BackgroundServiceView = class extends UI.VBox {
   /**
-   * @param {string} serviceName
+   * @param {!Protocol.BackgroundService.ServiceName} serviceName
+   * @param {!Resources.BackgroundServiceModel} model
    */
-  constructor(serviceName) {
+  constructor(serviceName, model) {
     super(true);
     this.registerRequiredCSS('resources/backgroundServiceView.css');
 
-    /** @const {string} */
+    /** @const {!Protocol.BackgroundService.ServiceName} */
     this._serviceName = serviceName;
 
+    /** @const {!Resources.BackgroundServiceModel} */
+    this._model = model;
+    this._model.addEventListener(
+        Resources.BackgroundServiceModel.Events.RecordingStateChanged, this._onRecordingStateChanged, this);
+    this._model.enable(this._serviceName);
+
+    /** @type {?UI.ToolbarToggle} */
+    this._recordButton = null;
+
     /** @const {!UI.Toolbar} */
     this._toolbar = new UI.Toolbar('background-service-toolbar', this.contentElement);
     this._setupToolbar();
@@ -21,13 +31,12 @@
   /**
    * Creates the toolbar UI element.
    */
-  _setupToolbar() {
-    const recordButton =
+  async _setupToolbar() {
+    this._recordButton =
         new UI.ToolbarToggle(Common.UIString('Toggle Record'), 'largeicon-start-recording', 'largeicon-stop-recording');
-    recordButton.addEventListener(
-        UI.ToolbarButton.Events.Click, () => recordButton.setToggled(!recordButton.toggled()));
-    recordButton.setToggleWithRedColor(true);
-    this._toolbar.appendToolbarItem(recordButton);
+    this._recordButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._toggleRecording());
+    this._recordButton.setToggleWithRedColor(true);
+    this._toolbar.appendToolbarItem(this._recordButton);
 
     const refreshButton = new UI.ToolbarButton(Common.UIString('Refresh'), 'largeicon-refresh');
     refreshButton.addEventListener(UI.ToolbarButton.Events.Click, () => {});
@@ -43,4 +52,21 @@
     deleteButton.addEventListener(UI.ToolbarButton.Events.Click, () => {});
     this._toolbar.appendToolbarItem(deleteButton);
   }
+
+  /**
+   * Called when the `Toggle Record` button is clicked.
+   */
+  _toggleRecording() {
+    this._model.setRecording(!this._recordButton.toggled(), this._serviceName);
+  }
+
+  /**
+   * @param {!Common.Event} event
+   */
+  _onRecordingStateChanged(event) {
+    const state = /** @type {!Resources.BackgroundServiceModel.RecordingState} */ (event.data);
+    if (state.serviceName !== this._serviceName)
+      return;
+    this._recordButton.setToggled(state.isRecording);
+  }
 };
diff --git a/front_end/resources/module.json b/front_end/resources/module.json
index fadebf5..ddfece8 100644
--- a/front_end/resources/module.json
+++ b/front_end/resources/module.json
@@ -34,6 +34,7 @@
         "ApplicationCacheModel.js",
         "AppManifestView.js",
         "ApplicationCacheItemsView.js",
+        "BackgroundServiceModel.js",
         "BackgroundServiceView.js",
         "ClearStorageView.js",
         "StorageItemsView.js",