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