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