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 createSelectListener(select_id, listener) { 105 select = document.getElementById(select_id); 106 select.addEventListener('change', listener); 107} 108 109function validateMacAddress(val) { 110 var regex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; 111 return (regex.test(val)); 112} 113 114function validateMacWrapper() { 115 let type = document.getElementById('bluetooth-wizard-type').value; 116 let button = document.getElementById("bluetooth-wizard-device"); 117 let macField = document.getElementById('bluetooth-wizard-mac'); 118 if (this.id == 'bluetooth-wizard-type') { 119 if (type == "remote_loopback") { 120 button.disabled = false; 121 macField.setCustomValidity(''); 122 macField.disabled = true; 123 macField.required = false; 124 macField.placeholder = 'N/A'; 125 macField.value = ''; 126 return; 127 } 128 } 129 macField.disabled = false; 130 macField.required = true; 131 macField.placeholder = 'Device MAC'; 132 if (validateMacAddress($(macField).val())) { 133 button.disabled = false; 134 macField.setCustomValidity(''); 135 } else { 136 button.disabled = true; 137 macField.setCustomValidity('MAC address invalid'); 138 } 139} 140 141$('[validate-mac]').bind('input', validateMacWrapper); 142$('[validate-mac]').bind('select', validateMacWrapper); 143 144function parseDevice(device) { 145 let id, name, mac; 146 var regex = /([0-9]+):([^@ ]*)(@(([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})))?/; 147 if (regex.test(device)) { 148 let regexMatches = device.match(regex); 149 id = regexMatches[1]; 150 name = regexMatches[2]; 151 mac = regexMatches[4]; 152 } 153 if (mac === undefined) { 154 mac = ""; 155 } 156 return [id, name, mac]; 157} 158 159function btUpdateAdded(devices) { 160 let deviceArr = devices.split('\r\n'); 161 let [id, name, mac] = parseDevice(deviceArr[0]); 162 if (name) { 163 let div = document.getElementById('bluetooth-wizard-confirm').getElementsByClassName('bluetooth-text')[1]; 164 div.innerHTML = ""; 165 div.innerHTML += "<p>Name: <b>" + id + "</b></p>"; 166 div.innerHTML += "<p>Type: <b>" + name + "</b></p>"; 167 div.innerHTML += "<p>MAC Addr: <b>" + mac + "</b></p>"; 168 return true; 169 } 170 return false; 171} 172 173function parsePhy(phy) { 174 let id = phy.substring(0, phy.indexOf(":")); 175 phy = phy.substring(phy.indexOf(":") + 1); 176 let name = phy.substring(0, phy.indexOf(":")); 177 let devices = phy.substring(phy.indexOf(":") + 1); 178 return [id, name, devices]; 179} 180 181function btParsePhys(phys) { 182 if (phys.indexOf("Phys:") < 0) { 183 return null; 184 } 185 let phyDict = {}; 186 phys = phys.split('Phys:')[1]; 187 let phyArr = phys.split('\r\n'); 188 for (var phy of phyArr.slice(1)) { 189 phy = phy.trim(); 190 if (phy.length == 0 || phy.indexOf("deleted") >= 0) { 191 continue; 192 } 193 let [id, name, devices] = parsePhy(phy); 194 phyDict[name] = id; 195 } 196 return phyDict; 197} 198 199function btUpdateDeviceList(devices) { 200 let deviceArr = devices.split('\r\n'); 201 if (deviceArr[0].indexOf("Devices:") >= 0) { 202 let div = document.getElementById('bluetooth-list').getElementsByClassName('bluetooth-text')[0]; 203 div.innerHTML = ""; 204 let count = 0; 205 for (var device of deviceArr.slice(1)) { 206 if (device.indexOf("Phys:") >= 0) { 207 break; 208 } 209 count++; 210 if (device.indexOf("deleted") >= 0) { 211 continue; 212 } 213 let [id, name, mac] = parseDevice(device); 214 let innerDiv = '<div><button title="Delete" data-device-id="' 215 innerDiv += id; 216 innerDiv += '" class="bluetooth-list-trash material-icons">delete</button>'; 217 innerDiv += name; 218 if (mac) { 219 innerDiv += " | " 220 innerDiv += mac; 221 } 222 innerDiv += '</div>'; 223 div.innerHTML += innerDiv; 224 } 225 return count; 226 } 227 return -1; 228} 229 230function addMouseListeners(button, listener) { 231 // Capture mousedown/up/out commands instead of click to enable 232 // hold detection. mouseout is used to catch if the user moves the 233 // mouse outside the button while holding down. 234 button.addEventListener('mousedown', listener); 235 button.addEventListener('mouseup', listener); 236 button.addEventListener('mouseout', listener); 237} 238 239function createControlPanelButton( 240 title, icon_name, listener, parent_id = 'control-panel-default-buttons') { 241 let button = document.createElement('button'); 242 document.getElementById(parent_id).appendChild(button); 243 button.title = title; 244 button.disabled = true; 245 addMouseListeners(button, listener); 246 // Set the button image using Material Design icons. 247 // See http://google.github.io/material-design-icons 248 // and https://material.io/resources/icons 249 button.classList.add('material-icons'); 250 button.innerHTML = icon_name; 251 return button; 252} 253 254function positionModal(button_id, modal_id) { 255 const modalButton = document.getElementById(button_id); 256 const modalDiv = document.getElementById(modal_id); 257 258 // Position the modal to the right of the show modal button. 259 modalDiv.style.top = modalButton.offsetTop; 260 modalDiv.style.left = modalButton.offsetWidth + 30; 261} 262 263function createModalButton(button_id, modal_id, close_id, hide_id) { 264 const modalButton = document.getElementById(button_id); 265 const modalDiv = document.getElementById(modal_id); 266 const modalHeader = modalDiv.querySelector('.modal-header'); 267 const modalClose = document.getElementById(close_id); 268 const modalDivHide = document.getElementById(hide_id); 269 270 positionModal(button_id, modal_id); 271 272 function showHideModal(show) { 273 if (show) { 274 modalButton.classList.add('modal-button-opened') 275 modalDiv.style.display = 'block'; 276 } else { 277 modalButton.classList.remove('modal-button-opened') 278 modalDiv.style.display = 'none'; 279 } 280 if (modalDivHide != null) { 281 modalDivHide.style.display = 'none'; 282 } 283 } 284 // Allow the show modal button to toggle the modal, 285 modalButton.addEventListener( 286 'click', evt => showHideModal(modalDiv.style.display != 'block')); 287 // but the close button always closes. 288 modalClose.addEventListener('click', evt => showHideModal(false)); 289 290 // Allow the modal to be dragged by the header. 291 let modalOffsets = { 292 midDrag: false, 293 mouseDownOffsetX: null, 294 mouseDownOffsetY: null, 295 }; 296 modalHeader.addEventListener('mousedown', evt => { 297 modalOffsets.midDrag = true; 298 // Store the offset of the mouse location from the 299 // modal's current location. 300 modalOffsets.mouseDownOffsetX = parseInt(modalDiv.style.left) - evt.clientX; 301 modalOffsets.mouseDownOffsetY = parseInt(modalDiv.style.top) - evt.clientY; 302 }); 303 modalHeader.addEventListener('mousemove', evt => { 304 let offsets = modalOffsets; 305 if (offsets.midDrag) { 306 // Move the modal to the mouse location plus the 307 // offset calculated on the initial mouse-down. 308 modalDiv.style.left = evt.clientX + offsets.mouseDownOffsetX; 309 modalDiv.style.top = evt.clientY + offsets.mouseDownOffsetY; 310 } 311 }); 312 document.addEventListener('mouseup', evt => { 313 modalOffsets.midDrag = false; 314 }); 315} 316 317function cmdConsole(consoleViewName, consoleInputName) { 318 let consoleView = document.getElementById(consoleViewName); 319 320 let addString = 321 function(str) { 322 consoleView.value += str; 323 consoleView.scrollTop = consoleView.scrollHeight; 324 } 325 326 let addLine = 327 function(line) { 328 addString(line + '\r\n'); 329 } 330 331 let commandCallbacks = []; 332 333 let addCommandListener = 334 function(f) { 335 commandCallbacks.push(f); 336 } 337 338 let onCommand = 339 function(cmd) { 340 cmd = cmd.trim(); 341 342 if (cmd.length == 0) return; 343 344 commandCallbacks.forEach(f => { 345 f(cmd); 346 }) 347 } 348 349 addCommandListener(cmd => addLine('>> ' + cmd)); 350 351 let consoleInput = document.getElementById(consoleInputName); 352 353 consoleInput.addEventListener('keydown', e => { 354 if ((e.key && e.key == 'Enter') || e.keyCode == 13) { 355 let command = e.target.value; 356 357 e.target.value = ''; 358 359 onCommand(command); 360 } 361 }); 362 363 return { 364 consoleView: consoleView, 365 consoleInput: consoleInput, 366 addLine: addLine, 367 addString: addString, 368 addCommandListener: addCommandListener, 369 }; 370} 371