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