blob: a06c3d64536b06c6ebcea3b82dd8524024c9179a [file] [log] [blame]
// Copyright 2015 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.
/**
* @constructor
* @extends {WebInspector.VBox}
*/
WebInspector.DevicesView = function()
{
WebInspector.VBox.call(this, true);
this.registerRequiredCSS("devices/devicesView.css");
this.contentElement.classList.add("devices-view");
var hbox = this.contentElement.createChild("div", "hbox devices-container");
var sidebar = hbox.createChild("div", "devices-sidebar");
sidebar.createChild("div", "devices-view-title").createTextChild(WebInspector.UIString("Devices"));
this._sidebarList = sidebar.createChild("div", "devices-sidebar-list");
this._discoveryView = new WebInspector.DevicesView.DiscoveryView();
this._sidebarListSpacer = this._sidebarList.createChild("div", "devices-sidebar-spacer");
this._discoveryListItem = this._sidebarList.createChild("div", "devices-sidebar-item");
this._discoveryListItem.textContent = WebInspector.UIString("Settings");
this._discoveryListItem.addEventListener("click", this._selectSidebarListItem.bind(this, this._discoveryListItem, this._discoveryView));
/** @type {!Map<string, !WebInspector.DevicesView.DeviceView>} */
this._viewById = new Map();
/** @type {!Array<!Adb.Device>} */
this._devices = [];
/** @type {!Map<string, !Element>} */
this._listItemById = new Map();
this._viewContainer = hbox.createChild("div", "flex-auto vbox");
var discoveryFooter = this.contentElement.createChild("div", "devices-footer");
this._deviceCountSpan = discoveryFooter.createChild("span");
discoveryFooter.createChild("span").textContent = WebInspector.UIString(" Read ");
discoveryFooter.appendChild(WebInspector.linkifyURLAsNode("https://developers.google.com/chrome-developer-tools/docs/remote-debugging", WebInspector.UIString("remote debugging documentation"), undefined, true));
discoveryFooter.createChild("span").textContent = WebInspector.UIString(" for more information.");
this._updateFooter();
this._selectSidebarListItem(this._discoveryListItem, this._discoveryView);
InspectorFrontendHost.events.addEventListener(InspectorFrontendHostAPI.Events.DevicesUpdated, this._devicesUpdated, this);
InspectorFrontendHost.events.addEventListener(InspectorFrontendHostAPI.Events.DevicesDiscoveryConfigChanged, this._devicesDiscoveryConfigChanged, this);
InspectorFrontendHost.events.addEventListener(InspectorFrontendHostAPI.Events.DevicesPortForwardingStatusChanged, this._devicesPortForwardingStatusChanged, this);
this.contentElement.tabIndex = 0;
this.setDefaultFocusedElement(this.contentElement);
}
WebInspector.DevicesView.prototype = {
/**
* @param {!Element} listItem
* @param {!WebInspector.Widget} view
*/
_selectSidebarListItem: function(listItem, view)
{
if (this._selectedListItem === listItem)
return;
if (this._selectedListItem) {
this._selectedListItem.classList.remove("selected");
this._visibleView.detach();
}
this._visibleView = view;
this._selectedListItem = listItem;
this._visibleView.show(this._viewContainer);
this._selectedListItem.classList.add("selected");
},
/**
* @param {!WebInspector.Event} event
*/
_devicesUpdated: function(event)
{
this._devices = /** @type {!Array.<!Adb.Device>} */ (event.data).slice().filter(d => d.adbSerial.toUpperCase() !== "WEBRTC");
for (var device of this._devices) {
if (!device.adbConnected)
device.adbModel = WebInspector.UIString("Unknown");
}
var ids = new Set();
for (var device of this._devices)
ids.add(device.id);
var selectedRemoved = false;
for (var deviceId of this._viewById.keys()) {
if (!ids.has(deviceId)) {
var listItem = /** @type {!Element} */ (this._listItemById.get(deviceId));
this._listItemById.remove(deviceId);
this._viewById.remove(deviceId);
listItem.remove();
if (listItem === this._selectedListItem)
selectedRemoved = true;
}
}
for (var device of this._devices) {
var view = this._viewById.get(device.id);
var listItem = this._listItemById.get(device.id);
if (!view) {
view = new WebInspector.DevicesView.DeviceView();
this._viewById.set(device.id, view);
listItem = this._createSidebarListItem(view);
this._listItemById.set(device.id, listItem);
this._sidebarList.insertBefore(listItem, this._sidebarListSpacer);
}
listItem._title.textContent = device.adbModel;
listItem._status.textContent = device.adbConnected ? WebInspector.UIString("Connected") : WebInspector.UIString("Pending Authorization");
listItem.classList.toggle("device-connected", device.adbConnected);
view.update(device);
}
if (selectedRemoved)
this._selectSidebarListItem(this._discoveryListItem, this._discoveryView);
this._updateFooter();
},
/**
* @param {!WebInspector.Widget} view
* @return {!Element}
*/
_createSidebarListItem: function(view)
{
var listItem = createElementWithClass("div", "devices-sidebar-item");
listItem.addEventListener("click", this._selectSidebarListItem.bind(this, listItem, view));
listItem._title = listItem.createChild("div", "devices-sidebar-item-title");
listItem._status = listItem.createChild("div", "devices-sidebar-item-status");
return listItem;
},
/**
* @param {!WebInspector.Event} event
*/
_devicesDiscoveryConfigChanged: function(event)
{
var discoverUsbDevices = /** @type {boolean} */ (event.data["discoverUsbDevices"]);
var portForwardingEnabled = /** @type {boolean} */ (event.data["portForwardingEnabled"]);
var portForwardingConfig = /** @type {!Adb.PortForwardingConfig} */ (event.data["portForwardingConfig"]);
this._discoveryView.discoveryConfigChanged(discoverUsbDevices, portForwardingEnabled, portForwardingConfig);
},
/**
* @param {!WebInspector.Event} event
*/
_devicesPortForwardingStatusChanged: function(event)
{
var status = /** @type {!Adb.PortForwardingStatus} */ (event.data);
for (var deviceId in status) {
var view = this._viewById.get(deviceId);
if (view)
view.portForwardingStatusChanged(status[deviceId]);
}
for (var deviceId of this._viewById.keys()) {
var view = this._viewById.get(deviceId);
if (view && !(deviceId in status))
view.portForwardingStatusChanged({ports: {}, browserId: ""});
}
},
_updateFooter: function()
{
this._deviceCountSpan.textContent =
!this._devices.length ? WebInspector.UIString("No devices detected.") :
this._devices.length === 1 ? WebInspector.UIString("1 device detected.") : WebInspector.UIString("%d devices detected.", this._devices.length);
},
/**
* @override
*/
wasShown: function()
{
WebInspector.PanelWithSidebar.prototype.wasShown.call(this);
InspectorFrontendHost.setDevicesUpdatesEnabled(true);
},
/**
* @override
*/
willHide: function()
{
WebInspector.PanelWithSidebar.prototype.wasShown.call(this);
InspectorFrontendHost.setDevicesUpdatesEnabled(false);
},
__proto__: WebInspector.VBox.prototype
}
/**
* @return {!WebInspector.DevicesView}
*/
WebInspector.DevicesView._instance = function()
{
if (!WebInspector.DevicesView._instanceObject)
WebInspector.DevicesView._instanceObject = new WebInspector.DevicesView();
return WebInspector.DevicesView._instanceObject;
}
/**
* @constructor
* @extends {WebInspector.VBox}
* @implements {WebInspector.ListWidget.Delegate}
*/
WebInspector.DevicesView.DiscoveryView = function()
{
WebInspector.VBox.call(this);
this.setMinimumSize(100, 100);
this.element.classList.add("discovery-view");
this.contentElement.createChild("div", "hbox device-text-row").createChild("div", "view-title").textContent = WebInspector.UIString("Settings");
var discoverUsbDevicesCheckbox = createCheckboxLabel(WebInspector.UIString("Discover USB devices"));
discoverUsbDevicesCheckbox.classList.add("usb-checkbox");
this.element.appendChild(discoverUsbDevicesCheckbox);
this._discoverUsbDevicesCheckbox = discoverUsbDevicesCheckbox.checkboxElement;
this._discoverUsbDevicesCheckbox.addEventListener("click", this._updateDiscoveryConfig.bind(this), false);
var help = this.element.createChild("div", "discovery-help");
help.createChild("span").textContent = WebInspector.UIString("Need help? Read Chrome ");
help.appendChild(WebInspector.linkifyURLAsNode("https://developers.google.com/chrome-developer-tools/docs/remote-debugging", WebInspector.UIString("remote debugging documentation."), undefined, true));
var portForwardingHeader = this.element.createChild("div", "port-forwarding-header");
var portForwardingEnabledCheckbox = createCheckboxLabel(WebInspector.UIString("Port forwarding"));
portForwardingEnabledCheckbox.classList.add("port-forwarding-checkbox");
portForwardingHeader.appendChild(portForwardingEnabledCheckbox);
this._portForwardingEnabledCheckbox = portForwardingEnabledCheckbox.checkboxElement;
this._portForwardingEnabledCheckbox.addEventListener("click", this._updateDiscoveryConfig.bind(this), false);
var portForwardingFooter = this.element.createChild("div", "port-forwarding-footer");
portForwardingFooter.createChild("span").textContent = WebInspector.UIString("Define the listening port on your device that maps to a port accessible from your development machine. ");
portForwardingFooter.appendChild(WebInspector.linkifyURLAsNode("https://developer.chrome.com/devtools/docs/remote-debugging#port-forwarding", WebInspector.UIString("Learn more"), undefined, true));
this._list = new WebInspector.ListWidget(this);
this._list.registerRequiredCSS("devices/devicesView.css");
this._list.element.classList.add("port-forwarding-list");
var placeholder = createElementWithClass("div", "port-forwarding-list-empty");
placeholder.textContent = WebInspector.UIString("No rules");
this._list.setEmptyPlaceholder(placeholder);
this._list.show(this.element);
this.element.appendChild(createTextButton(WebInspector.UIString("Add rule"), this._addRuleButtonClicked.bind(this), "add-rule-button"));
/** @type {!Array<!Adb.PortForwardingRule>} */
this._portForwardingConfig = [];
}
WebInspector.DevicesView.DiscoveryView.prototype = {
_addRuleButtonClicked: function()
{
this._list.addNewItem(this._portForwardingConfig.length, {port: "", address: ""});
},
/**
* @param {boolean} discoverUsbDevices
* @param {boolean} portForwardingEnabled
* @param {!Adb.PortForwardingConfig} portForwardingConfig
*/
discoveryConfigChanged: function(discoverUsbDevices, portForwardingEnabled, portForwardingConfig)
{
this._discoverUsbDevicesCheckbox.checked = discoverUsbDevices;
this._portForwardingEnabledCheckbox.checked = portForwardingEnabled;
this._portForwardingConfig = [];
this._list.clear();
for (var key of Object.keys(portForwardingConfig)) {
var rule = /** @type {!Adb.PortForwardingRule} */ ({port: key, address: portForwardingConfig[key]});
this._portForwardingConfig.push(rule);
this._list.appendItem(rule, true);
}
},
/**
* @override
* @param {*} item
* @param {boolean} editable
* @return {!Element}
*/
renderItem: function(item, editable)
{
var rule = /** @type {!Adb.PortForwardingRule} */ (item);
var element = createElementWithClass("div", "port-forwarding-list-item");
var port = element.createChild("div", "port-forwarding-value port-forwarding-port");
port.createChild("span", "port-localhost").textContent = WebInspector.UIString("localhost:");
port.createTextChild(rule.port);
element.createChild("div", "port-forwarding-separator");
element.createChild("div", "port-forwarding-value").textContent = rule.address;
return element;
},
/**
* @override
* @param {*} item
* @param {number} index
*/
removeItemRequested: function(item, index)
{
this._portForwardingConfig.splice(index, 1);
this._list.removeItem(index);
this._updateDiscoveryConfig();
},
/**
* @override
* @param {*} item
* @param {!WebInspector.ListWidget.Editor} editor
* @param {boolean} isNew
*/
commitEdit: function(item, editor, isNew)
{
var rule = /** @type {!Adb.PortForwardingRule} */ (item);
rule.port = editor.control("port").value.trim();
rule.address = editor.control("address").value.trim();
if (isNew)
this._portForwardingConfig.push(rule);
this._updateDiscoveryConfig();
},
/**
* @override
* @param {*} item
* @return {!WebInspector.ListWidget.Editor}
*/
beginEdit: function(item)
{
var rule = /** @type {!Adb.PortForwardingRule} */ (item);
var editor = this._createEditor();
editor.control("port").value = rule.port;
editor.control("address").value = rule.address;
return editor;
},
/**
* @return {!WebInspector.ListWidget.Editor}
*/
_createEditor: function()
{
if (this._editor)
return this._editor;
var editor = new WebInspector.ListWidget.Editor();
this._editor = editor;
var content = editor.contentElement();
var fields = content.createChild("div", "port-forwarding-edit-row");
fields.createChild("div", "port-forwarding-value port-forwarding-port").appendChild(editor.createInput("port", "text", "Device port (3333)", portValidator.bind(this)));
fields.createChild("div", "port-forwarding-separator port-forwarding-separator-invisible");
fields.createChild("div", "port-forwarding-value").appendChild(editor.createInput("address", "text", "Local address (dev.example.corp:3333)", addressValidator));
return editor;
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @this {WebInspector.DevicesView.DiscoveryView}
* @return {boolean}
*/
function portValidator(item, index, input)
{
var value = input.value.trim();
var match = value.match(/^(\d+)$/);
if (!match)
return false;
var port = parseInt(match[1], 10);
if (port < 1024 || port > 65535)
return false;
for (var i = 0; i < this._portForwardingConfig.length; ++i) {
if (i !== index && this._portForwardingConfig[i].port === value)
return false;
}
return true;
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @return {boolean}
*/
function addressValidator(item, index, input)
{
var match = input.value.trim().match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/);
if (!match)
return false;
var port = parseInt(match[2], 10);
return port <= 65535;
}
},
_updateDiscoveryConfig: function()
{
var configMap = /** @type {!Adb.PortForwardingConfig} */ ({});
for (var rule of this._portForwardingConfig)
configMap[rule.port] = rule.address;
InspectorFrontendHost.setDevicesDiscoveryConfig(this._discoverUsbDevicesCheckbox.checked, this._portForwardingEnabledCheckbox.checked, configMap);
},
__proto__: WebInspector.VBox.prototype
}
/**
* @constructor
* @extends {WebInspector.VBox}
*/
WebInspector.DevicesView.DeviceView = function()
{
WebInspector.VBox.call(this);
this.setMinimumSize(100, 100);
this.contentElement.classList.add("device-view");
var topRow = this.contentElement.createChild("div", "hbox device-text-row");
this._deviceTitle = topRow.createChild("div", "view-title");
this._deviceSerial = topRow.createChild("div", "device-serial");
this._portStatus = this.contentElement.createChild("div", "device-port-status hidden");
this._deviceOffline = this.contentElement.createChild("div");
this._deviceOffline.textContent = WebInspector.UIString("Pending authentication: please accept debugging session on the device.");
this._noBrowsers = this.contentElement.createChild("div");
this._noBrowsers.textContent = WebInspector.UIString("No browsers detected.");
this._browsers = this.contentElement.createChild("div", "device-browser-list vbox");
/** @type {!Map<string, !WebInspector.DevicesView.BrowserSection>} */
this._browserById = new Map();
this._device = null;
}
/** @typedef {!{browser: ?Adb.Browser, element: !Element, title: !Element, pages: !Element, viewMore: !Element, newTab: !Element, pageSections: !Map<string, !WebInspector.DevicesView.PageSection>}} */
WebInspector.DevicesView.BrowserSection;
/** @typedef {!{page: ?Adb.Page, element: !Element, title: !Element, url: !Element, inspect: !Element}} */
WebInspector.DevicesView.PageSection;
WebInspector.DevicesView.DeviceView.prototype = {
/**
* @param {!Adb.Device} device
*/
update: function(device)
{
if (!this._device || this._device.adbModel !== device.adbModel)
this._deviceTitle.textContent = device.adbModel;
if (!this._device || this._device.adbSerial !== device.adbSerial)
this._deviceSerial.textContent = "#" + device.adbSerial;
this._deviceOffline.classList.toggle("hidden", device.adbConnected);
this._noBrowsers.classList.toggle("hidden", !device.adbConnected || !!device.browsers.length);
this._browsers.classList.toggle("hidden", !device.adbConnected || !device.browsers.length);
var browserIds = new Set();
for (var browser of device.browsers)
browserIds.add(browser.id);
for (var browserId of this._browserById.keys()) {
if (!browserIds.has(browserId)) {
this._browserById.get(browserId).element.remove();
this._browserById.remove(browserId);
}
}
for (var browser of device.browsers) {
var section = this._browserById.get(browser.id);
if (!section) {
section = this._createBrowserSection();
this._browserById.set(browser.id, section);
this._browsers.appendChild(section.element);
}
this._updateBrowserSection(section, browser);
}
this._device = device;
},
/**
* @return {!WebInspector.DevicesView.BrowserSection}
*/
_createBrowserSection: function()
{
var element = createElementWithClass("div", "vbox flex-none");
var topRow = element.createChild("div", "");
var title = topRow.createChild("div", "device-browser-title");
var newTabRow = element.createChild("div", "device-browser-new-tab");
newTabRow.createChild("div", "").textContent = WebInspector.UIString("New tab:");
var newTabInput = newTabRow.createChild("input", "");
newTabInput.type = "text";
newTabInput.placeholder = WebInspector.UIString("Enter URL");
newTabInput.addEventListener("keydown", newTabKeyDown, false);
var newTabButton = createTextButton(WebInspector.UIString("Open"), openNewTab);
newTabRow.appendChild(newTabButton);
var pages = element.createChild("div", "device-page-list vbox");
var viewMore = element.createChild("div", "device-view-more");
viewMore.addEventListener("click", viewMoreClick, false);
updateViewMoreTitle();
var section = {browser: null, element: element, title: title, pages: pages, viewMore: viewMore, newTab: newTabRow, pageSections: new Map()};
return section;
function viewMoreClick()
{
pages.classList.toggle("device-view-more-toggled");
updateViewMoreTitle();
}
function updateViewMoreTitle()
{
viewMore.textContent = pages.classList.contains("device-view-more-toggled") ? WebInspector.UIString("View less tabs\u2026") : WebInspector.UIString("View more tabs\u2026");
}
/**
* @param {!Event} event
*/
function newTabKeyDown(event)
{
if (event.key === "Enter") {
event.consume(true);
openNewTab();
}
}
function openNewTab()
{
if (section.browser) {
InspectorFrontendHost.openRemotePage(section.browser.id, newTabInput.value.trim() || "about:blank");
newTabInput.value = "";
}
}
},
/**
* @param {!WebInspector.DevicesView.BrowserSection} section
* @param {!Adb.Browser} browser
*/
_updateBrowserSection: function(section, browser)
{
if (!section.browser || section.browser.adbBrowserName !== browser.adbBrowserName || section.browser.adbBrowserVersion !== browser.adbBrowserVersion) {
if (browser.adbBrowserVersion)
section.title.textContent = String.sprintf("%s (%s)", browser.adbBrowserName, browser.adbBrowserVersion);
else
section.title.textContent = browser.adbBrowserName;
}
var pageIds = new Set();
for (var page of browser.pages)
pageIds.add(page.id);
for (var pageId of section.pageSections.keys()) {
if (!pageIds.has(pageId)) {
section.pageSections.get(pageId).element.remove();
section.pageSections.remove(pageId);
}
}
for (var index = 0; index < browser.pages.length; ++index) {
var page = browser.pages[index];
var pageSection = section.pageSections.get(page.id);
if (!pageSection) {
pageSection = this._createPageSection();
section.pageSections.set(page.id, pageSection);
section.pages.appendChild(pageSection.element);
}
this._updatePageSection(pageSection, page);
if (!index && section.pages.firstChild !== pageSection.element)
section.pages.insertBefore(pageSection.element, section.pages.firstChild);
}
var kViewMoreCount = 3;
for (var index = 0, element = section.pages.firstChild; element; element = element.nextSibling, ++index)
element.classList.toggle("device-view-more-page", index >= kViewMoreCount);
section.viewMore.classList.toggle("device-needs-view-more", browser.pages.length > kViewMoreCount);
section.newTab.classList.toggle("hidden", !browser.adbBrowserChromeVersion);
section.browser = browser;
},
/**
* @return {!WebInspector.DevicesView.PageSection}
*/
_createPageSection: function()
{
var element = createElementWithClass("div", "vbox");
var titleRow = element.createChild("div", "device-page-title-row");
var title = titleRow.createChild("div", "device-page-title");
var inspect = createTextButton(WebInspector.UIString("Inspect"), doAction.bind(null, "inspect"), "device-inspect-button");
titleRow.appendChild(inspect);
var toolbar = new WebInspector.Toolbar("");
toolbar.appendToolbarItem(new WebInspector.ToolbarMenuButton(appendActions));
titleRow.appendChild(toolbar.element);
var url = element.createChild("div", "device-page-url");
var section = {page: null, element: element, title: title, url: url, inspect: inspect};
return section;
/**
* @param {!WebInspector.ContextMenu} contextMenu
*/
function appendActions(contextMenu)
{
contextMenu.appendItem(WebInspector.UIString("Reload"), doAction.bind(null, "reload"));
contextMenu.appendItem(WebInspector.UIString("Focus"), doAction.bind(null, "activate"));
contextMenu.appendItem(WebInspector.UIString("Close"), doAction.bind(null, "close"));
}
/**
* @param {string} action
*/
function doAction(action)
{
if (section.page)
InspectorFrontendHost.performActionOnRemotePage(section.page.id, action);
}
},
/**
* @param {!WebInspector.DevicesView.PageSection} section
* @param {!Adb.Page} page
*/
_updatePageSection: function(section, page)
{
if (!section.page || section.page.name !== page.name) {
section.title.textContent = page.name;
section.title.title = page.name;
}
if (!section.page || section.page.url !== page.url) {
section.url.textContent = "";
section.url.appendChild(WebInspector.linkifyURLAsNode(page.url, undefined, undefined, true));
}
section.inspect.disabled = page.adbAttachedForeign;
section.page = page;
},
/**
* @param {!Adb.DevicePortForwardingStatus} status
*/
portForwardingStatusChanged: function(status)
{
var json = JSON.stringify(status);
if (json === this._cachedPortStatus)
return;
this._cachedPortStatus = json;
this._portStatus.removeChildren();
this._portStatus.createChild("div", "device-port-status-text").textContent = WebInspector.UIString("Port Forwarding:");
var connected = [];
var transient = [];
var error = [];
var empty = true;
for (var port in status.ports) {
if (!status.ports.hasOwnProperty(port))
continue;
empty = false;
var portStatus = status.ports[port];
var portNumber = createElementWithClass("div", "device-view-port-number monospace");
portNumber.textContent = ":" + port;
if (portStatus >= 0)
this._portStatus.appendChild(portNumber);
else
this._portStatus.insertBefore(portNumber, this._portStatus.firstChild);
var portIcon = createElementWithClass("div", "device-view-port-icon");
if (portStatus >= 0) {
connected.push(port);
} else if (portStatus === -1 || portStatus === -2) {
portIcon.classList.add("device-view-port-icon-transient");
transient.push(port);
} else if (portStatus < 0) {
portIcon.classList.add("device-view-port-icon-error");
error.push(port);
}
this._portStatus.insertBefore(portIcon, portNumber);
}
var title = [];
if (connected.length)
title.push(WebInspector.UIString("Connected: %s", connected.join(", ")));
if (transient.length)
title.push(WebInspector.UIString("Transient: %s", transient.join(", ")));
if (error.length)
title.push(WebInspector.UIString("Error: %s", error.join(", ")));
this._portStatus.title = title.join("; ");
this._portStatus.classList.toggle("hidden", empty);
},
__proto__: WebInspector.VBox.prototype
}