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