1/** 2 * Copyright (c) 2021 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import deviceInfo from '@ohos.deviceInfo'; 17import BaseSettingsController from '../../../../../../../common/component/src/main/ets/default/controller/BaseSettingsController'; 18import BluetoothModel, { BondState, ProfileConnectionState } from '../../model/bluetoothImpl/BluetoothModel'; 19import BluetoothDevice from '../../model/bluetoothImpl/BluetoothDevice'; 20import Log from '../../../../../../../common/utils/src/main/ets/default/baseUtil/LogDecorator'; 21import ConfigData from '../../../../../../../common/utils/src/main/ets/default/baseUtil/ConfigData'; 22import ISettingsController from '../../../../../../../common/component/src/main/ets/default/controller/ISettingsController'; 23import LogUtil from '../../../../../../../common/utils/src/main/ets/default/baseUtil/LogUtil'; 24import AboutDeviceModel from '../../model/aboutDeviceImpl/AboutDeviceModel' 25 26const deviceTypeInfo = deviceInfo.deviceType; 27const DISCOVERY_DURING_TIME: number = 30000; // 30' 28const DISCOVERY_INTERVAL_TIME: number = 3000; // 3' 29const DISCOVERY_DEBOUNCE_TIME: number = 500; 30 31export default class BluetoothDeviceController extends BaseSettingsController { 32 private TAG = ConfigData.TAG + 'BluetoothDeviceController ' 33 34 //state 35 private isOn: boolean = false; 36 private enabled: boolean = false; 37 38 // paired devices 39 private pairedDevices: BluetoothDevice[] = []; 40 41 // available devices 42 private isDeviceDiscovering: boolean = false; 43 private availableDevices: BluetoothDevice[] = []; 44 private pairPinCode: string = ''; 45 private discoveryStartTimeoutId: number = 0; 46 private discoveryStopTimeoutId: number = 0; 47 private debounceTimer: number = 0; 48 49 initData(): ISettingsController { 50 LogUtil.log(this.TAG + 'start to initData bluetooth'); 51 super.initData(); 52 let isOn = BluetoothModel.isStateOn(); 53 LogUtil.log(this.TAG + 'initData bluetooth state isOn ' + isOn + ', typeof isOn = ' + typeof (isOn)) 54 if (isOn) { 55 this.refreshPairedDevices(); 56 } 57 58 LogUtil.log(this.TAG + 'initData save value to app storage. ') 59 this.isOn = new Boolean(isOn).valueOf() 60 this.enabled = true 61 62 AppStorage.SetOrCreate('bluetoothIsOn', this.isOn); 63 AppStorage.SetOrCreate('bluetoothToggleEnabled', this.enabled); 64 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 65 66 return this; 67 } 68 69 subscribe(): ISettingsController { 70 LogUtil.log(this.TAG + 'subscribe bluetooth state isOn ' + this.isOn) 71 this.subscribeStateChange(); 72 this.subscribeBluetoothDeviceFind(); 73 this.subscribeBondStateChange(); 74 this.subscribeDeviceConnectStateChange(); 75 BluetoothModel.subscribePinRequired((pinRequiredParam: { 76 deviceId: string; 77 pinCode: string; 78 }) => { 79 LogUtil.log(this.TAG + 'bluetooth subscribePinRequired callback. pinRequiredParam = ' + pinRequiredParam.pinCode); 80 let pairData = this.getAvailableDevice(pinRequiredParam.deviceId); 81 this.pairPinCode = pinRequiredParam.pinCode; 82 AppStorage.SetOrCreate('pairData', pairData); 83 AppStorage.SetOrCreate('pinRequiredParam', pinRequiredParam); 84 }); 85 return this; 86 } 87 88 unsubscribe(): ISettingsController { 89 LogUtil.log(this.TAG + 'start to unsubscribe bluetooth'); 90 this.stopBluetoothDiscovery(); 91 92 if (this.discoveryStartTimeoutId) { 93 clearTimeout(this.discoveryStartTimeoutId); 94 this.discoveryStartTimeoutId = 0; 95 } 96 97 if (this.discoveryStopTimeoutId) { 98 clearTimeout(this.discoveryStopTimeoutId); 99 this.discoveryStopTimeoutId = 0; 100 } 101 102 BluetoothModel.unsubscribeBluetoothDeviceFind(); 103 BluetoothModel.unsubscribeBondStateChange(); 104 BluetoothModel.unsubscribeDeviceStateChange(); 105 BluetoothModel.unsubscribePinRequired(); 106 AppStorage.Delete('BluetoothFailedDialogFlag'); 107 return this; 108 } 109 110 /** 111 * Set toggle value 112 */ 113 toggleValue(isOn: boolean) { 114 if(this.discoveryStartTimeoutId) { 115 clearTimeout(this.discoveryStartTimeoutId); 116 this.discoveryStartTimeoutId = 0; 117 } 118 if(this.discoveryStopTimeoutId) { 119 clearTimeout(this.discoveryStopTimeoutId); 120 this.discoveryStopTimeoutId = 0; 121 } 122 if(this.debounceTimer) { 123 clearTimeout(this.debounceTimer); 124 this.debounceTimer = 0; 125 } 126 127 this.debounceTimer = setTimeout(() => { 128 let curState = BluetoothModel.getState(); 129 if ((curState === 2) === isOn) { 130 clearTimeout(this.debounceTimer); 131 this.debounceTimer = 0; 132 return; 133 } 134 this.enabled = false 135 AppStorage.SetOrCreate('bluetoothToggleEnabled', this.enabled); 136 LogUtil.log(this.TAG + 'afterCurrentValueChanged bluetooth state isOn = ' + this.isOn) 137 if (isOn) { 138 BluetoothModel.enableBluetooth(); 139 } else { 140 BluetoothModel.disableBluetooth(); 141 clearTimeout(this.debounceTimer); 142 this.debounceTimer = 0; 143 // remove all elements from availableDevices array 144 this.availableDevices.splice(0, this.availableDevices.length); 145 } 146 }, DISCOVERY_DEBOUNCE_TIME); 147 } 148 149 /** 150 * Get Local Name 151 */ 152 getLocalName() { 153 AppStorage.SetOrCreate('bluetoothLocalName', AboutDeviceModel.getSystemName()); 154 } 155 156 /** 157 * Pair device. 158 * 159 * @param deviceId device id 160 * @param success success callback 161 * @param error error callback 162 */ 163 pair(deviceId: string, success?: (pinCode: string) => void, error?: () => void): void { 164 const device: BluetoothDevice = this.getAvailableDevice(deviceId); 165 if (device && device.connectionState === BondState.BOND_STATE_BONDING) { 166 LogUtil.log(this.TAG + `bluetooth no Aavailable device or device is already pairing.`) 167 return; 168 } 169 // start pairing 170 BluetoothModel.pairDevice(deviceId); 171 } 172 173 /** 174 * Confirm pairing. 175 * 176 * @param deviceId device id 177 * @param accept accept or not 178 * @param success success callback 179 * @param error error callback 180 */ 181 confirmPairing(deviceId: string, accept: boolean): void { 182 if (accept) { 183 try { 184 this.getAvailableDevice(deviceId).connectionState = BondState.BOND_STATE_BONDING; 185 } catch (err) { 186 LogUtil.error(this.TAG + 'confirmPairing =' + JSON.stringify(err)); 187 } 188 } 189 // set paring confirmation 190 BluetoothModel.setDevicePairingConfirmation(deviceId, accept); 191 192 } 193 194 /** 195 * Connect device. 196 * @param deviceId device id 197 */ 198 connect(deviceId: string): Array<{ 199 profileId: number; 200 connectRet: boolean; 201 }> { 202 return BluetoothModel.connectDevice(deviceId); 203 } 204 205 /** 206 * disconnect device. 207 * @param deviceId device id 208 */ 209 disconnect(deviceId: string): Array<{ 210 profileId: number; 211 disconnectRet: boolean; 212 }> { 213 return BluetoothModel.disconnectDevice(deviceId); 214 } 215 216 /** 217 * Unpair device. 218 * @param deviceId device id 219 */ 220 unpair(deviceId: string): boolean { 221 AppStorage.SetOrCreate('BluetoothFailedDialogFlag', false); 222 const result = BluetoothModel.unpairDevice(deviceId); 223 LogUtil.log(this.TAG + 'bluetooth paired device unpair. result = ' + result) 224 this.refreshPairedDevices() 225 return result; 226 } 227 228 /** 229 * Refresh paired devices. 230 */ 231 refreshPairedDevices() { 232 let deviceIds: string[] = BluetoothModel.getPairedDeviceIds(); 233 let list: BluetoothDevice[] = [] 234 deviceIds.forEach(deviceId => { 235 list.push(this.getDevice(deviceId)); 236 }); 237 this.pairedDevices = list; 238 this.sortPairedDevices(); 239 AppStorage.SetOrCreate('bluetoothPairedDevices', this.pairedDevices); 240 LogUtil.log(this.TAG + 'bluetooth paired devices. list length = ' + JSON.stringify(list.length)) 241 } 242 243 /** 244 * Paired device should be shown on top of the list. 245 */ 246 private sortPairedDevices() { 247 LogUtil.log(this.TAG + 'sortPairedDevices in.') 248 this.pairedDevices.sort((a: BluetoothDevice, b: BluetoothDevice) => { 249 if (a.connectionState == ProfileConnectionState.STATE_DISCONNECTED && b.connectionState == ProfileConnectionState.STATE_DISCONNECTED) { 250 return 0 251 } else if (b.connectionState == ProfileConnectionState.STATE_DISCONNECTED) { 252 return -1 253 } else if (a.connectionState == ProfileConnectionState.STATE_DISCONNECTED) { 254 return 1 255 } else { 256 return 0 257 } 258 }) 259 LogUtil.log(this.TAG + 'sortPairedDevices out.') 260 } 261 262 //---------------------- subscribe ---------------------- 263 /** 264 * Subscribe bluetooth state change 265 */ 266 private subscribeStateChange() { 267 BluetoothModel.subscribeStateChange((isOn: boolean) => { 268 LogUtil.log(this.TAG + 'bluetooth state changed. isOn = ' + isOn) 269 this.isOn = new Boolean(isOn).valueOf(); 270 this.enabled = true; 271 272 LogUtil.log(this.TAG + 'bluetooth state changed. save value.') 273 this.getLocalName() 274 AppStorage.SetOrCreate('bluetoothIsOn', this.isOn); 275 AppStorage.SetOrCreate('bluetoothToggleEnabled', this.enabled); 276 277 if (isOn) { 278 LogUtil.log(this.TAG + 'bluetooth state changed. unsubscribe') 279 this.startBluetoothDiscovery(); 280 } else { 281 LogUtil.log(this.TAG + 'bluetooth state changed. subscribe') 282 this.mStopBluetoothDiscovery(); 283 } 284 }); 285 } 286 287 /** 288 * Subscribe device find 289 */ 290 private subscribeBluetoothDeviceFind() { 291 BluetoothModel.subscribeBluetoothDeviceFind((deviceIds: Array<string>) => { 292 LogUtil.log(ConfigData.TAG + 'available bluetooth devices changed.'); 293 294 deviceIds?.forEach(deviceId => { 295 let device = this.availableDevices.find((availableDevice) => { 296 return availableDevice.deviceId === deviceId 297 }) 298 LogUtil.log(this.TAG + 'available bluetooth find'); 299 if (!device) { 300 let pairedDevice = this.pairedDevices.find((pairedDevice) => { 301 return pairedDevice.deviceId === deviceId 302 }) 303 if (pairedDevice) { 304 LogUtil.log(this.TAG + `available bluetooth is paried.`); 305 let indexDeviceID = 0; 306 for (let i = 0; i < this.pairedDevices.length; i++) { 307 if (this.pairedDevices[i].deviceId === deviceId) { 308 indexDeviceID = i; 309 break; 310 } 311 } 312 let existDevice = this.getDevice(deviceId); 313 if(existDevice.deviceName !== this.pairedDevices[indexDeviceID].deviceName){ 314 this.pairedDevices.splice(indexDeviceID,1,existDevice); 315 AppStorage.setOrCreate('bluetoothPairedDevices', this.pairedDevices); 316 } 317 } else { 318 LogUtil.log(this.TAG + 'available bluetooth new device found. availableDevices length = ' + this.availableDevices.length); 319 let newDevice = this.getDevice(deviceId); 320 if (!!newDevice.deviceName) { 321 this.availableDevices.push(newDevice); 322 } 323 324 LogUtil.log(this.TAG + 'available bluetooth new device pushed. availableDevices length = ' + this.availableDevices.length); 325 } 326 } else { 327 LogUtil.log(this.TAG + 'bluetooth already exist!'); 328 let indexDeviceID = 0; 329 for (let i = 0; i < this.availableDevices.length; i++) { 330 if (this.availableDevices[i].deviceId === deviceId) { 331 indexDeviceID = i; 332 break; 333 } 334 } 335 let existDevice = this.getDevice(deviceId); 336 if(existDevice.deviceName !== this.availableDevices[indexDeviceID].deviceName){ 337 this.availableDevices.splice(indexDeviceID,1,existDevice); 338 } 339 } 340 }) 341 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 342 }); 343 } 344 345 /** 346 * Subscribe bond state change 347 */ 348 private subscribeBondStateChange() { 349 BluetoothModel.subscribeBondStateChange((data: { 350 deviceId: string; 351 bondState: number; 352 }) => { 353 LogUtil.info(this.TAG + "data.bondState" + JSON.stringify(data.bondState)) 354 //paired devices 355 if (data.bondState !== BondState.BOND_STATE_BONDING) { 356 AppStorage.SetOrCreate("controlPairing", true) 357 this.refreshPairedDevices(); 358 } 359 360 //available devices 361 if (data.bondState == BondState.BOND_STATE_BONDING) { 362 AppStorage.SetOrCreate("controlPairing", false) 363 // case bonding 364 // do nothing and still listening 365 LogUtil.log(this.TAG + 'bluetooth continue listening bondStateChange.'); 366 if (this.getAvailableDevice(data.deviceId) != null) { 367 this.getAvailableDevice(data.deviceId).connectionState = ProfileConnectionState.STATE_CONNECTING; 368 } 369 370 } else if (data.bondState == BondState.BOND_STATE_INVALID) { 371 AppStorage.SetOrCreate("controlPairing", true) 372 // case failed 373 if (this.getAvailableDevice(data.deviceId) != null) { 374 this.getAvailableDevice(data.deviceId).connectionState = ProfileConnectionState.STATE_DISCONNECTED; 375 } 376 this.forceRefresh(this.availableDevices); 377 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 378 let showFlag = AppStorage.Get('BluetoothFailedDialogFlag'); 379 if (showFlag == false) { 380 AppStorage.SetOrCreate('BluetoothFailedDialogFlag', true); 381 return; 382 } 383 this.showConnectFailedDialog(this.getDevice(data.deviceId).deviceName); 384 } else if (data.bondState == BondState.BOND_STATE_BONDED) { 385 // case success 386 LogUtil.log(this.TAG + 'bluetooth bonded : remove device.'); 387 this.removeAvailableDevice(data.deviceId); 388 BluetoothModel.connectDevice(data.deviceId); 389 } 390 391 }); 392 } 393 394 /** 395 * Subscribe device connect state change 396 */ 397 private subscribeDeviceConnectStateChange() { 398 BluetoothModel.subscribeDeviceStateChange((data: { 399 profileId: number; 400 deviceId: string; 401 profileConnectionState: number; 402 }) => { 403 LogUtil.log(this.TAG + 'device connection state changed. profileId:' + JSON.stringify(data.profileId) 404 + ' profileConnectionState: ' + JSON.stringify(data.profileConnectionState)); 405 for (let device of this.pairedDevices) { 406 if (device.deviceId === data.deviceId) { 407 device.setProfile(data); 408 this.sortPairedDevices(); 409 AppStorage.SetOrCreate('bluetoothPairedDevices', this.pairedDevices); 410 break; 411 } 412 }; 413 LogUtil.log(this.TAG + 'device connection state changed. pairedDevices length = ' 414 + JSON.stringify(this.pairedDevices.length)) 415 LogUtil.log(this.TAG + 'device connection state changed. availableDevices length = ' 416 + JSON.stringify(this.availableDevices.length)) 417 this.removeAvailableDevice(data.deviceId); 418 }); 419 } 420 421 //---------------------- private ---------------------- 422 /** 423 * Get device by device id. 424 * @param deviceId device id 425 */ 426 protected getDevice(deviceId: string): BluetoothDevice { 427 let device = new BluetoothDevice(); 428 device.deviceId = deviceId; 429 device.deviceName = BluetoothModel.getDeviceName(deviceId); 430 device.deviceType = BluetoothModel.getDeviceType(deviceId); 431 device.setProfiles(BluetoothModel.getDeviceState(deviceId)); 432 return device; 433 } 434 435 /** 436 * Force refresh array. 437 * Note: the purpose of this function is just trying to fix page (ets) level's bug below, 438 * and should be useless if fixed by the future sdk. 439 * Bug Details: 440 * @State is not supported well for Array<CustomClass> type. 441 * In the case that the array item's field value changed, while not its length, 442 * the build method on page will not be triggered! 443 */ 444 protected forceRefresh(arr: BluetoothDevice[]): void { 445 arr.push(new BluetoothDevice()) 446 arr.pop(); 447 } 448 449 /** 450 * Start bluetooth discovery. 451 */ 452 @Log 453 public startBluetoothDiscovery() { 454 this.isDeviceDiscovering = true; 455 BluetoothModel.startBluetoothDiscovery(); 456 if(this.discoveryStopTimeoutId) { 457 clearTimeout(this.discoveryStopTimeoutId); 458 this.discoveryStopTimeoutId = 0; 459 } 460 this.discoveryStopTimeoutId = setTimeout(() => { 461 this.stopBluetoothDiscovery(); 462 clearTimeout(this.discoveryStopTimeoutId); 463 this.discoveryStopTimeoutId = 0; 464 }, DISCOVERY_DURING_TIME); 465 } 466 467 /** 468 * Stop bluetooth discovery. 469 */ 470 @Log 471 private stopBluetoothDiscovery() { 472 this.isDeviceDiscovering = false; 473 BluetoothModel.stopBluetoothDiscovery(); 474 if(this.discoveryStartTimeoutId) { 475 clearTimeout(this.discoveryStartTimeoutId); 476 this.discoveryStartTimeoutId = 0; 477 } 478 this.discoveryStartTimeoutId = setTimeout(() => { 479 this.startBluetoothDiscovery(); 480 clearTimeout(this.discoveryStartTimeoutId); 481 this.discoveryStartTimeoutId = 0; 482 }, DISCOVERY_INTERVAL_TIME); 483 } 484 485 /** 486 * Stop bluetooth discovery. 487 */ 488 private mStopBluetoothDiscovery() { 489 this.isDeviceDiscovering = false; 490 BluetoothModel.stopBluetoothDiscovery(); 491 if (this.discoveryStartTimeoutId) { 492 clearTimeout(this.discoveryStartTimeoutId); 493 this.discoveryStartTimeoutId = 0; 494 } 495 496 if (this.discoveryStopTimeoutId) { 497 clearTimeout(this.discoveryStopTimeoutId); 498 this.discoveryStopTimeoutId = 0; 499 } 500 } 501 502 503 /** 504 * Get available device. 505 * 506 * @param deviceId device id 507 */ 508 private getAvailableDevice(deviceIds: string): BluetoothDevice { 509 LogUtil.log(this.TAG + 'getAvailableDevice length = ' + this.availableDevices.length); 510 let temp = this.availableDevices; 511 for (let i = 0; i < temp.length; i++) { 512 if (temp[i].deviceId === deviceIds) { 513 return temp[i]; 514 } 515 } 516 return null; 517 } 518 519 /** 520 * Remove available device. 521 * 522 * @param deviceId device id 523 */ 524 private removeAvailableDevice(deviceId: string): void { 525 LogUtil.log(this.TAG + 'removeAvailableDevice : before : availableDevices length = ' + this.availableDevices.length); 526 this.availableDevices = this.availableDevices.filter((device) => device.deviceId !== deviceId) 527 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 528 LogUtil.log(this.TAG + 'removeAvailableDevice : after : availableDevices length = ' + this.availableDevices.length); 529 } 530 531 /** 532 * Connect Failed Dialog 533 */ 534 private showConnectFailedDialog(deviceName: string) { 535 AlertDialog.show({ 536 title: $r("app.string.bluetooth_connect_failed"), 537 message: $r("app.string.bluetooth_connect_failed_msg", deviceName), 538 confirm: { 539 value: $r("app.string.bluetooth_know_button"), 540 action: () => { 541 LogUtil.info('Button-clicking callback') 542 } 543 }, 544 cancel: () => { 545 LogUtil.info('Closed callbacks') 546 }, 547 alignment: deviceTypeInfo === 'phone' || deviceTypeInfo === 'default' ? DialogAlignment.Bottom : DialogAlignment.Center, 548 offset: ({ 549 dx: 0, dy: deviceTypeInfo === 'phone' || deviceTypeInfo === 'default' ? '-24dp' : 0 550 }) 551 }) 552 553 } 554}