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.unsubscribeStateChange(); 106 BluetoothModel.unsubscribePinRequired(); 107 AppStorage.Delete('BluetoothFailedDialogFlag'); 108 return this; 109 } 110 111 /** 112 * Set toggle value 113 */ 114 toggleValue(isOn: boolean) { 115 if(this.discoveryStartTimeoutId) { 116 clearTimeout(this.discoveryStartTimeoutId); 117 this.discoveryStartTimeoutId = 0; 118 } 119 if(this.discoveryStopTimeoutId) { 120 clearTimeout(this.discoveryStopTimeoutId); 121 this.discoveryStopTimeoutId = 0; 122 } 123 if(this.debounceTimer) { 124 clearTimeout(this.debounceTimer); 125 this.debounceTimer = 0; 126 } 127 128 this.debounceTimer = setTimeout(() => { 129 let curState = BluetoothModel.getState(); 130 if ((curState === 2) === isOn) { 131 clearTimeout(this.debounceTimer); 132 this.debounceTimer = 0; 133 return; 134 } 135 this.enabled = false 136 AppStorage.SetOrCreate('bluetoothToggleEnabled', this.enabled); 137 LogUtil.log(this.TAG + 'afterCurrentValueChanged bluetooth state isOn = ' + this.isOn) 138 if (isOn) { 139 BluetoothModel.enableBluetooth(); 140 } else { 141 BluetoothModel.disableBluetooth(); 142 clearTimeout(this.debounceTimer); 143 this.debounceTimer = 0; 144 // remove all elements from availableDevices array 145 this.availableDevices.splice(0, this.availableDevices.length); 146 } 147 }, DISCOVERY_DEBOUNCE_TIME); 148 } 149 150 /** 151 * Get Local Name 152 */ 153 getLocalName() { 154 AppStorage.SetOrCreate('bluetoothLocalName', AboutDeviceModel.getSystemName()); 155 } 156 157 /** 158 * Pair device. 159 * 160 * @param deviceId device id 161 * @param success success callback 162 * @param error error callback 163 */ 164 pair(deviceId: string, success?: (pinCode: string) => void, error?: () => void): void { 165 const device: BluetoothDevice = this.getAvailableDevice(deviceId); 166 if (device && device.connectionState === BondState.BOND_STATE_BONDING) { 167 LogUtil.log(this.TAG + `bluetooth no Aavailable device or device is already pairing.`) 168 return; 169 } 170 // start pairing 171 BluetoothModel.pairDevice(deviceId); 172 } 173 174 /** 175 * Confirm pairing. 176 * 177 * @param deviceId device id 178 * @param accept accept or not 179 * @param success success callback 180 * @param error error callback 181 */ 182 confirmPairing(deviceId: string, accept: boolean): void { 183 if (accept) { 184 try { 185 this.getAvailableDevice(deviceId).connectionState = BondState.BOND_STATE_BONDING; 186 } catch (err) { 187 LogUtil.error(this.TAG + 'confirmPairing =' + JSON.stringify(err)); 188 } 189 } 190 // set paring confirmation 191 BluetoothModel.setDevicePairingConfirmation(deviceId, accept); 192 193 } 194 195 /** 196 * Connect device. 197 * @param deviceId device id 198 */ 199 connect(deviceId: string): Array<{ 200 profileId: number; 201 connectRet: boolean; 202 }> { 203 return BluetoothModel.connectDevice(deviceId); 204 } 205 206 /** 207 * disconnect device. 208 * @param deviceId device id 209 */ 210 disconnect(deviceId: string): Array<{ 211 profileId: number; 212 disconnectRet: boolean; 213 }> { 214 return BluetoothModel.disconnectDevice(deviceId); 215 } 216 217 /** 218 * Unpair device. 219 * @param deviceId device id 220 */ 221 unpair(deviceId: string): boolean { 222 AppStorage.SetOrCreate('BluetoothFailedDialogFlag', false); 223 const result = BluetoothModel.unpairDevice(deviceId); 224 LogUtil.log(this.TAG + 'bluetooth paired device unpair. result = ' + result) 225 this.refreshPairedDevices() 226 return result; 227 } 228 229 /** 230 * Refresh paired devices. 231 */ 232 refreshPairedDevices() { 233 let deviceIds: string[] = BluetoothModel.getPairedDeviceIds(); 234 let list: BluetoothDevice[] = [] 235 deviceIds.forEach(deviceId => { 236 list.push(this.getDevice(deviceId)); 237 }); 238 this.pairedDevices = list; 239 this.sortPairedDevices(); 240 AppStorage.SetOrCreate('bluetoothPairedDevices', this.pairedDevices); 241 LogUtil.log(this.TAG + 'bluetooth paired devices. list length = ' + JSON.stringify(list.length)) 242 } 243 244 /** 245 * Paired device should be shown on top of the list. 246 */ 247 private sortPairedDevices() { 248 LogUtil.log(this.TAG + 'sortPairedDevices in.') 249 this.pairedDevices.sort((a: BluetoothDevice, b: BluetoothDevice) => { 250 if (a.connectionState == ProfileConnectionState.STATE_DISCONNECTED && b.connectionState == ProfileConnectionState.STATE_DISCONNECTED) { 251 return 0 252 } else if (b.connectionState == ProfileConnectionState.STATE_DISCONNECTED) { 253 return -1 254 } else if (a.connectionState == ProfileConnectionState.STATE_DISCONNECTED) { 255 return 1 256 } else { 257 return 0 258 } 259 }) 260 LogUtil.log(this.TAG + 'sortPairedDevices out.') 261 } 262 263 //---------------------- subscribe ---------------------- 264 /** 265 * Subscribe bluetooth state change 266 */ 267 private subscribeStateChange() { 268 BluetoothModel.subscribeStateChange((isOn: boolean) => { 269 LogUtil.log(this.TAG + 'bluetooth state changed. isOn = ' + isOn) 270 this.isOn = new Boolean(isOn).valueOf(); 271 this.enabled = true; 272 273 LogUtil.log(this.TAG + 'bluetooth state changed. save value.') 274 this.getLocalName() 275 AppStorage.SetOrCreate('bluetoothIsOn', this.isOn); 276 AppStorage.SetOrCreate('bluetoothToggleEnabled', this.enabled); 277 278 if (isOn) { 279 LogUtil.log(this.TAG + 'bluetooth state changed. unsubscribe') 280 this.startBluetoothDiscovery(); 281 } else { 282 LogUtil.log(this.TAG + 'bluetooth state changed. subscribe') 283 this.mStopBluetoothDiscovery(); 284 } 285 }); 286 } 287 288 /** 289 * Subscribe device find 290 */ 291 private subscribeBluetoothDeviceFind() { 292 BluetoothModel.subscribeBluetoothDeviceFind((deviceIds: Array<string>) => { 293 LogUtil.log(ConfigData.TAG + 'available bluetooth devices changed.'); 294 295 deviceIds?.forEach(deviceId => { 296 let device = this.availableDevices.find((availableDevice) => { 297 return availableDevice.deviceId === deviceId 298 }) 299 LogUtil.log(this.TAG + 'available bluetooth find'); 300 if (!device) { 301 let pairedDevice = this.pairedDevices.find((pairedDevice) => { 302 return pairedDevice.deviceId === deviceId 303 }) 304 if (pairedDevice) { 305 LogUtil.log(this.TAG + `available bluetooth is paried.`); 306 } else { 307 LogUtil.log(this.TAG + 'available bluetooth new device found. availableDevices length = ' + this.availableDevices.length); 308 let newDevice = this.getDevice(deviceId); 309 if (!!newDevice.deviceName) { 310 this.availableDevices.push(newDevice); 311 } 312 313 LogUtil.log(this.TAG + 'available bluetooth new device pushed. availableDevices length = ' + this.availableDevices.length); 314 } 315 } else { 316 LogUtil.log(this.TAG + 'bluetooth already exist!'); 317 let indexDeviceID = 0; 318 for (let i = 0; i < this.availableDevices.length; i++) { 319 if (this.availableDevices[i].deviceId === deviceId) { 320 indexDeviceID = i; 321 break; 322 } 323 } 324 let existDevice = this.getDevice(deviceId); 325 if(existDevice.deviceName !== this.availableDevices[indexDeviceID].deviceName){ 326 this.availableDevices.splice(indexDeviceID,1,existDevice); 327 } 328 } 329 }) 330 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 331 }); 332 } 333 334 /** 335 * Subscribe bond state change 336 */ 337 private subscribeBondStateChange() { 338 BluetoothModel.subscribeBondStateChange((data: { 339 deviceId: string; 340 bondState: number; 341 }) => { 342 LogUtil.info(this.TAG + "data.bondState" + JSON.stringify(data.bondState)) 343 //paired devices 344 if (data.bondState !== BondState.BOND_STATE_BONDING) { 345 AppStorage.SetOrCreate("controlPairing", true) 346 this.refreshPairedDevices(); 347 } 348 349 //available devices 350 if (data.bondState == BondState.BOND_STATE_BONDING) { 351 AppStorage.SetOrCreate("controlPairing", false) 352 // case bonding 353 // do nothing and still listening 354 LogUtil.log(this.TAG + 'bluetooth continue listening bondStateChange.'); 355 if (this.getAvailableDevice(data.deviceId) != null) { 356 this.getAvailableDevice(data.deviceId).connectionState = ProfileConnectionState.STATE_CONNECTING; 357 } 358 359 } else if (data.bondState == BondState.BOND_STATE_INVALID) { 360 AppStorage.SetOrCreate("controlPairing", true) 361 // case failed 362 if (this.getAvailableDevice(data.deviceId) != null) { 363 this.getAvailableDevice(data.deviceId).connectionState = ProfileConnectionState.STATE_DISCONNECTED; 364 } 365 this.forceRefresh(this.availableDevices); 366 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 367 let showFlag = AppStorage.Get('BluetoothFailedDialogFlag'); 368 if (showFlag == false) { 369 AppStorage.SetOrCreate('BluetoothFailedDialogFlag', true); 370 return; 371 } 372 this.showConnectFailedDialog(this.getDevice(data.deviceId).deviceName); 373 } else if (data.bondState == BondState.BOND_STATE_BONDED) { 374 // case success 375 LogUtil.log(this.TAG + 'bluetooth bonded : remove device.'); 376 this.removeAvailableDevice(data.deviceId); 377 BluetoothModel.connectDevice(data.deviceId); 378 } 379 380 }); 381 } 382 383 /** 384 * Subscribe device connect state change 385 */ 386 private subscribeDeviceConnectStateChange() { 387 BluetoothModel.subscribeDeviceStateChange((data: { 388 profileId: number; 389 deviceId: string; 390 profileConnectionState: number; 391 }) => { 392 LogUtil.log(this.TAG + 'device connection state changed. profileId:' + JSON.stringify(data.profileId) 393 + ' profileConnectionState: ' + JSON.stringify(data.profileConnectionState)); 394 for (let device of this.pairedDevices) { 395 if (device.deviceId === data.deviceId) { 396 device.setProfile(data); 397 this.sortPairedDevices(); 398 AppStorage.SetOrCreate('bluetoothPairedDevices', this.pairedDevices); 399 break; 400 } 401 }; 402 LogUtil.log(this.TAG + 'device connection state changed. pairedDevices length = ' 403 + JSON.stringify(this.pairedDevices.length)) 404 LogUtil.log(this.TAG + 'device connection state changed. availableDevices length = ' 405 + JSON.stringify(this.availableDevices.length)) 406 this.removeAvailableDevice(data.deviceId); 407 }); 408 } 409 410 //---------------------- private ---------------------- 411 /** 412 * Get device by device id. 413 * @param deviceId device id 414 */ 415 protected getDevice(deviceId: string): BluetoothDevice { 416 let device = new BluetoothDevice(); 417 device.deviceId = deviceId; 418 device.deviceName = BluetoothModel.getDeviceName(deviceId); 419 device.deviceType = BluetoothModel.getDeviceType(deviceId); 420 device.setProfiles(BluetoothModel.getDeviceState(deviceId)); 421 return device; 422 } 423 424 /** 425 * Force refresh array. 426 * Note: the purpose of this function is just trying to fix page (ets) level's bug below, 427 * and should be useless if fixed by the future sdk. 428 * Bug Details: 429 * @State is not supported well for Array<CustomClass> type. 430 * In the case that the array item's field value changed, while not its length, 431 * the build method on page will not be triggered! 432 */ 433 protected forceRefresh(arr: BluetoothDevice[]): void { 434 arr.push(new BluetoothDevice()) 435 arr.pop(); 436 } 437 438 /** 439 * Start bluetooth discovery. 440 */ 441 @Log 442 public startBluetoothDiscovery() { 443 this.isDeviceDiscovering = true; 444 BluetoothModel.startBluetoothDiscovery(); 445 if(this.discoveryStopTimeoutId) { 446 clearTimeout(this.discoveryStopTimeoutId); 447 this.discoveryStopTimeoutId = 0; 448 } 449 this.discoveryStopTimeoutId = setTimeout(() => { 450 this.stopBluetoothDiscovery(); 451 clearTimeout(this.discoveryStopTimeoutId); 452 this.discoveryStopTimeoutId = 0; 453 }, DISCOVERY_DURING_TIME); 454 } 455 456 /** 457 * Stop bluetooth discovery. 458 */ 459 @Log 460 private stopBluetoothDiscovery() { 461 this.isDeviceDiscovering = false; 462 BluetoothModel.stopBluetoothDiscovery(); 463 if(this.discoveryStartTimeoutId) { 464 clearTimeout(this.discoveryStartTimeoutId); 465 this.discoveryStartTimeoutId = 0; 466 } 467 this.discoveryStartTimeoutId = setTimeout(() => { 468 this.startBluetoothDiscovery(); 469 clearTimeout(this.discoveryStartTimeoutId); 470 this.discoveryStartTimeoutId = 0; 471 }, DISCOVERY_INTERVAL_TIME); 472 } 473 474 /** 475 * Stop bluetooth discovery. 476 */ 477 private mStopBluetoothDiscovery() { 478 this.isDeviceDiscovering = false; 479 BluetoothModel.stopBluetoothDiscovery(); 480 if (this.discoveryStartTimeoutId) { 481 clearTimeout(this.discoveryStartTimeoutId); 482 this.discoveryStartTimeoutId = 0; 483 } 484 485 if (this.discoveryStopTimeoutId) { 486 clearTimeout(this.discoveryStopTimeoutId); 487 this.discoveryStopTimeoutId = 0; 488 } 489 } 490 491 492 /** 493 * Get available device. 494 * 495 * @param deviceId device id 496 */ 497 private getAvailableDevice(deviceIds: string): BluetoothDevice { 498 LogUtil.log(this.TAG + 'getAvailableDevice length = ' + this.availableDevices.length); 499 let temp = this.availableDevices; 500 for (let i = 0; i < temp.length; i++) { 501 if (temp[i].deviceId === deviceIds) { 502 return temp[i]; 503 } 504 } 505 return null; 506 } 507 508 /** 509 * Remove available device. 510 * 511 * @param deviceId device id 512 */ 513 private removeAvailableDevice(deviceId: string): void { 514 LogUtil.log(this.TAG + 'removeAvailableDevice : before : availableDevices length = ' + this.availableDevices.length); 515 this.availableDevices = this.availableDevices.filter((device) => device.deviceId !== deviceId) 516 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 517 LogUtil.log(this.TAG + 'removeAvailableDevice : after : availableDevices length = ' + this.availableDevices.length); 518 } 519 520 /** 521 * Connect Failed Dialog 522 */ 523 private showConnectFailedDialog(deviceName: string) { 524 AlertDialog.show({ 525 title: $r("app.string.bluetooth_connect_failed"), 526 message: $r("app.string.bluetooth_connect_failed_msg", deviceName), 527 confirm: { 528 value: $r("app.string.bluetooth_know_button"), 529 action: () => { 530 LogUtil.info('Button-clicking callback') 531 } 532 }, 533 cancel: () => { 534 LogUtil.info('Closed callbacks') 535 }, 536 alignment: deviceTypeInfo === 'phone' || deviceTypeInfo === 'default' ? DialogAlignment.Bottom : DialogAlignment.Center, 537 offset: ({ 538 dx: 0, dy: deviceTypeInfo === 'phone' || deviceTypeInfo === 'default' ? '-24dp' : 0 539 }) 540 }) 541 542 } 543}