• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17// Creates a "toggle control", which is a stylized checkbox with an icon. The
18// onToggleCb callback is called every time the control changes state with the
19// new toggle position (true for ON) and is expected to return a promise of the
20// new toggle position which can resolve to the opposite position of the one
21// received if there was error.
22function createToggleControl(elm, iconName, onToggleCb, initialState = false) {
23  let icon = document.createElement('span');
24  icon.classList.add('toggle-control-icon');
25  icon.classList.add('material-icons-outlined');
26  if (iconName) {
27    icon.appendChild(document.createTextNode(iconName));
28  }
29  elm.appendChild(icon);
30  let toggle = document.createElement('label');
31  toggle.classList.add('toggle-control-switch');
32  let input = document.createElement('input');
33  input.type = 'checkbox';
34  input.checked = !!initialState;
35  input.onchange = e => {
36    let nextPr = onToggleCb(e.target.checked);
37    if (nextPr && 'then' in nextPr) {
38      nextPr.then(checked => {
39        e.target.checked = !!checked;
40      });
41    }
42  };
43  toggle.appendChild(input);
44  let slider = document.createElement('span');
45  slider.classList.add('toggle-control-slider');
46  toggle.appendChild(slider);
47  elm.classList.add('toggle-control');
48  elm.appendChild(toggle);
49  return {
50    // Sets the state of the toggle control. This only affects the
51    // visible state of the control in the UI, it doesn't affect the
52    // state of the underlying resources. It's most useful to make
53    // changes of said resources visible to the user.
54    Set: checked => input.checked = !!checked,
55  };
56}
57
58function createButtonListener(button_id_class, func,
59  deviceConnection, listener) {
60  let buttons = [];
61  let ele = document.getElementById(button_id_class);
62  if (ele != null) {
63    buttons.push(ele);
64  } else {
65    buttons = document.getElementsByClassName(button_id_class);
66  }
67  for (var button of buttons) {
68    if (func != null) {
69      button.onclick = func;
70    }
71    button.addEventListener('mousedown', listener);
72  }
73}
74
75function createInputListener(input_id, func, listener) {
76  input = document.getElementById(input_id);
77  if (func != null) {
78    input.oninput = func;
79  }
80  input.addEventListener('input', listener);
81}
82
83function validateMacAddress(val) {
84  var regex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
85  return (regex.test(val));
86}
87
88function validateMacWrapper() {
89  let type = document.getElementById('bluetooth-wizard-type').value;
90  let button = document.getElementById("bluetooth-wizard-device");
91  let macField = document.getElementById('bluetooth-wizard-mac');
92  if (this.id == 'bluetooth-wizard-type') {
93    if (type == "remote_loopback") {
94      button.disabled = false;
95      macField.setCustomValidity('');
96      macField.disabled = true;
97      macField.required = false;
98      macField.placeholder = 'N/A';
99      macField.value = '';
100      return;
101    }
102  }
103  macField.disabled = false;
104  macField.required = true;
105  macField.placeholder = 'Device MAC';
106  if (validateMacAddress($(macField).val())) {
107    button.disabled = false;
108    macField.setCustomValidity('');
109  } else {
110    button.disabled = true;
111    macField.setCustomValidity('MAC address invalid');
112  }
113}
114
115$('[validate-mac]').bind('input', validateMacWrapper);
116$('[validate-mac]').bind('select', validateMacWrapper);
117
118function parseDevice(device) {
119  let id, name, mac;
120  var regex = /([0-9]+):([^@ ]*)(@(([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})))?/;
121  if (regex.test(device)) {
122    let regexMatches = device.match(regex);
123    id = regexMatches[1];
124    name = regexMatches[2];
125    mac = regexMatches[4];
126  }
127  if (mac === undefined) {
128    mac = "";
129  }
130  return [id, name, mac];
131}
132
133function btUpdateAdded(devices) {
134  let deviceArr = devices.split('\r\n');
135  let [id, name, mac] = parseDevice(deviceArr[0]);
136  if (name) {
137    let div = document.getElementById('bluetooth-wizard-confirm').getElementsByClassName('bluetooth-text')[1];
138    div.innerHTML = "";
139    div.innerHTML += "<p>Name: <b>" + id + "</b></p>";
140    div.innerHTML += "<p>Type: <b>" + name + "</b></p>";
141    div.innerHTML += "<p>MAC Addr: <b>" + mac + "</b></p>";
142    return true;
143  }
144  return false;
145}
146
147function parsePhy(phy) {
148  let id = phy.substring(0, phy.indexOf(":"));
149  phy = phy.substring(phy.indexOf(":") + 1);
150  let name = phy.substring(0, phy.indexOf(":"));
151  let devices = phy.substring(phy.indexOf(":") + 1);
152  return [id, name, devices];
153}
154
155function btParsePhys(phys) {
156  if (phys.indexOf("Phys:") < 0) {
157    return null;
158  }
159  let phyDict = {};
160  phys = phys.split('Phys:')[1];
161  let phyArr = phys.split('\r\n');
162  for (var phy of phyArr.slice(1)) {
163    phy = phy.trim();
164    if (phy.length == 0 || phy.indexOf("deleted") >= 0) {
165      continue;
166    }
167    let [id, name, devices] = parsePhy(phy);
168    phyDict[name] = id;
169  }
170  return phyDict;
171}
172
173function btUpdateDeviceList(devices) {
174  let deviceArr = devices.split('\r\n');
175  if (deviceArr[0].indexOf("Devices:") >= 0) {
176    let div = document.getElementById('bluetooth-list').getElementsByClassName('bluetooth-text')[0];
177    div.innerHTML = "";
178    let count = 0;
179    for (var device of deviceArr.slice(1)) {
180      if (device.indexOf("Phys:") >= 0) {
181        break;
182      }
183      count++;
184      if (device.indexOf("deleted") >= 0) {
185        continue;
186      }
187      let [id, name, mac] = parseDevice(device);
188      let innerDiv = '<div><button title="Delete" data-device-id="'
189      innerDiv += id;
190      innerDiv += '" class="bluetooth-list-trash material-icons">delete</button>';
191      innerDiv += name;
192      if (mac) {
193        innerDiv += " | "
194        innerDiv += mac;
195      }
196      innerDiv += '</div>';
197      div.innerHTML += innerDiv;
198    }
199    return count;
200  }
201  return -1;
202}
203
204function createControlPanelButton(
205    command, title, icon_name, listener,
206    parent_id = 'control-panel-default-buttons') {
207  let button = document.createElement('button');
208  document.getElementById(parent_id).appendChild(button);
209  button.title = title;
210  button.dataset.command = command;
211  button.disabled = true;
212  // Capture mousedown/up/out commands instead of click to enable
213  // hold detection. mouseout is used to catch if the user moves the
214  // mouse outside the button while holding down.
215  button.addEventListener('mousedown', listener);
216  button.addEventListener('mouseup', listener);
217  button.addEventListener('mouseout', listener);
218  // Set the button image using Material Design icons.
219  // See http://google.github.io/material-design-icons
220  // and https://material.io/resources/icons
221  button.classList.add('material-icons');
222  button.innerHTML = icon_name;
223  return button;
224}
225
226function positionModal(button_id, modal_id) {
227  const modalButton = document.getElementById(button_id);
228  const modalDiv = document.getElementById(modal_id);
229
230  // Position the modal to the right of the show modal button.
231  modalDiv.style.top = modalButton.offsetTop;
232  modalDiv.style.left = modalButton.offsetWidth + 30;
233}
234
235function createModalButton(button_id, modal_id, close_id, hide_id) {
236  const modalButton = document.getElementById(button_id);
237  const modalDiv = document.getElementById(modal_id);
238  const modalHeader = modalDiv.querySelector('.modal-header');
239  const modalClose = document.getElementById(close_id);
240  const modalDivHide = document.getElementById(hide_id);
241
242  positionModal(button_id, modal_id);
243
244  function showHideModal(show) {
245    if (show) {
246      modalButton.classList.add('modal-button-opened')
247      modalDiv.style.display = 'block';
248    } else {
249      modalButton.classList.remove('modal-button-opened')
250      modalDiv.style.display = 'none';
251    }
252    if (modalDivHide != null) {
253      modalDivHide.style.display = 'none';
254    }
255  }
256  // Allow the show modal button to toggle the modal,
257  modalButton.addEventListener(
258      'click', evt => showHideModal(modalDiv.style.display != 'block'));
259  // but the close button always closes.
260  modalClose.addEventListener('click', evt => showHideModal(false));
261
262  // Allow the modal to be dragged by the header.
263  let modalOffsets = {
264    midDrag: false,
265    mouseDownOffsetX: null,
266    mouseDownOffsetY: null,
267  };
268  modalHeader.addEventListener('mousedown', evt => {
269    modalOffsets.midDrag = true;
270    // Store the offset of the mouse location from the
271    // modal's current location.
272    modalOffsets.mouseDownOffsetX = parseInt(modalDiv.style.left) - evt.clientX;
273    modalOffsets.mouseDownOffsetY = parseInt(modalDiv.style.top) - evt.clientY;
274  });
275  modalHeader.addEventListener('mousemove', evt => {
276    let offsets = modalOffsets;
277    if (offsets.midDrag) {
278      // Move the modal to the mouse location plus the
279      // offset calculated on the initial mouse-down.
280      modalDiv.style.left = evt.clientX + offsets.mouseDownOffsetX;
281      modalDiv.style.top = evt.clientY + offsets.mouseDownOffsetY;
282    }
283  });
284  document.addEventListener('mouseup', evt => {
285    modalOffsets.midDrag = false;
286  });
287}
288
289function cmdConsole(consoleViewName, consoleInputName) {
290  let consoleView = document.getElementById(consoleViewName);
291
292  let addString =
293      function(str) {
294    consoleView.value += str;
295    consoleView.scrollTop = consoleView.scrollHeight;
296  }
297
298  let addLine =
299      function(line) {
300    addString(line + '\r\n');
301  }
302
303  let commandCallbacks = [];
304
305  let addCommandListener =
306      function(f) {
307    commandCallbacks.push(f);
308  }
309
310  let onCommand =
311      function(cmd) {
312    cmd = cmd.trim();
313
314    if (cmd.length == 0) return;
315
316    commandCallbacks.forEach(f => {
317      f(cmd);
318    })
319  }
320
321  addCommandListener(cmd => addLine('>> ' + cmd));
322
323  let consoleInput = document.getElementById(consoleInputName);
324
325  consoleInput.addEventListener('keydown', e => {
326    if ((e.key && e.key == 'Enter') || e.keyCode == 13) {
327      let command = e.target.value;
328
329      e.target.value = '';
330
331      onCommand(command);
332    }
333  });
334
335  return {
336    consoleView: consoleView,
337    consoleInput: consoleInput,
338    addLine: addLine,
339    addString: addString,
340    addCommandListener: addCommandListener,
341  };
342}
343