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 this.availableDevices.push(newDevice); 310 LogUtil.log(this.TAG + 'available bluetooth new device pushed. availableDevices length = ' + this.availableDevices.length); 311 } 312 } 313 }) 314 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 315 }); 316 } 317 318 /** 319 * Subscribe bond state change 320 */ 321 private subscribeBondStateChange() { 322 BluetoothModel.subscribeBondStateChange((data: { 323 deviceId: string; 324 bondState: number; 325 }) => { 326 LogUtil.info(this.TAG + "data.bondState" + JSON.stringify(data.bondState)) 327 //paired devices 328 if (data.bondState !== BondState.BOND_STATE_BONDING) { 329 AppStorage.SetOrCreate("controlPairing", true) 330 this.refreshPairedDevices(); 331 } 332 333 //available devices 334 if (data.bondState == BondState.BOND_STATE_BONDING) { 335 AppStorage.SetOrCreate("controlPairing", false) 336 // case bonding 337 // do nothing and still listening 338 LogUtil.log(this.TAG + 'bluetooth continue listening bondStateChange.'); 339 if (this.getAvailableDevice(data.deviceId) != null) { 340 this.getAvailableDevice(data.deviceId).connectionState = ProfileConnectionState.STATE_CONNECTING; 341 } 342 343 } else if (data.bondState == BondState.BOND_STATE_INVALID) { 344 AppStorage.SetOrCreate("controlPairing", true) 345 // case failed 346 if (this.getAvailableDevice(data.deviceId) != null) { 347 this.getAvailableDevice(data.deviceId).connectionState = ProfileConnectionState.STATE_DISCONNECTED; 348 } 349 this.forceRefresh(this.availableDevices); 350 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 351 let showFlag = AppStorage.Get('BluetoothFailedDialogFlag'); 352 if (showFlag == false) { 353 AppStorage.SetOrCreate('BluetoothFailedDialogFlag', true); 354 return; 355 } 356 this.showConnectFailedDialog(this.getDevice(data.deviceId).deviceName); 357 } else if (data.bondState == BondState.BOND_STATE_BONDED) { 358 // case success 359 LogUtil.log(this.TAG + 'bluetooth bonded : remove device.'); 360 this.removeAvailableDevice(data.deviceId); 361 BluetoothModel.connectDevice(data.deviceId); 362 } 363 364 }); 365 } 366 367 /** 368 * Subscribe device connect state change 369 */ 370 private subscribeDeviceConnectStateChange() { 371 BluetoothModel.subscribeDeviceStateChange((data: { 372 profileId: number; 373 deviceId: string; 374 profileConnectionState: number; 375 }) => { 376 LogUtil.log(this.TAG + 'device connection state changed. profileId:' + JSON.stringify(data.profileId) 377 + ' profileConnectionState: ' + JSON.stringify(data.profileConnectionState)); 378 for (let device of this.pairedDevices) { 379 if (device.deviceId === data.deviceId) { 380 device.setProfile(data); 381 this.sortPairedDevices(); 382 AppStorage.SetOrCreate('bluetoothPairedDevices', this.pairedDevices); 383 break; 384 } 385 }; 386 LogUtil.log(this.TAG + 'device connection state changed. pairedDevices length = ' 387 + JSON.stringify(this.pairedDevices.length)) 388 LogUtil.log(this.TAG + 'device connection state changed. availableDevices length = ' 389 + JSON.stringify(this.availableDevices.length)) 390 this.removeAvailableDevice(data.deviceId); 391 }); 392 } 393 394 //---------------------- private ---------------------- 395 /** 396 * Get device by device id. 397 * @param deviceId device id 398 */ 399 protected getDevice(deviceId: string): BluetoothDevice { 400 let device = new BluetoothDevice(); 401 device.deviceId = deviceId; 402 device.deviceName = BluetoothModel.getDeviceName(deviceId); 403 device.deviceType = BluetoothModel.getDeviceType(deviceId); 404 device.setProfiles(BluetoothModel.getDeviceState(deviceId)); 405 return device; 406 } 407 408 /** 409 * Force refresh array. 410 * Note: the purpose of this function is just trying to fix page (ets) level's bug below, 411 * and should be useless if fixed by the future sdk. 412 * Bug Details: 413 * @State is not supported well for Array<CustomClass> type. 414 * In the case that the array item's field value changed, while not its length, 415 * the build method on page will not be triggered! 416 */ 417 protected forceRefresh(arr: BluetoothDevice[]): void { 418 arr.push(new BluetoothDevice()) 419 arr.pop(); 420 } 421 422 /** 423 * Start bluetooth discovery. 424 */ 425 @Log 426 public startBluetoothDiscovery() { 427 this.isDeviceDiscovering = true; 428 BluetoothModel.startBluetoothDiscovery(); 429 if(this.discoveryStopTimeoutId) { 430 clearTimeout(this.discoveryStopTimeoutId); 431 this.discoveryStopTimeoutId = 0; 432 } 433 this.discoveryStopTimeoutId = setTimeout(() => { 434 this.stopBluetoothDiscovery(); 435 clearTimeout(this.discoveryStopTimeoutId); 436 this.discoveryStopTimeoutId = 0; 437 }, DISCOVERY_DURING_TIME); 438 } 439 440 /** 441 * Stop bluetooth discovery. 442 */ 443 @Log 444 private stopBluetoothDiscovery() { 445 this.isDeviceDiscovering = false; 446 BluetoothModel.stopBluetoothDiscovery(); 447 if(this.discoveryStartTimeoutId) { 448 clearTimeout(this.discoveryStartTimeoutId); 449 this.discoveryStartTimeoutId = 0; 450 } 451 this.discoveryStartTimeoutId = setTimeout(() => { 452 this.startBluetoothDiscovery(); 453 clearTimeout(this.discoveryStartTimeoutId); 454 this.discoveryStartTimeoutId = 0; 455 }, DISCOVERY_INTERVAL_TIME); 456 } 457 458 /** 459 * Stop bluetooth discovery. 460 */ 461 private mStopBluetoothDiscovery() { 462 this.isDeviceDiscovering = false; 463 BluetoothModel.stopBluetoothDiscovery(); 464 if (this.discoveryStartTimeoutId) { 465 clearTimeout(this.discoveryStartTimeoutId); 466 this.discoveryStartTimeoutId = 0; 467 } 468 469 if (this.discoveryStopTimeoutId) { 470 clearTimeout(this.discoveryStopTimeoutId); 471 this.discoveryStopTimeoutId = 0; 472 } 473 } 474 475 476 /** 477 * Get available device. 478 * 479 * @param deviceId device id 480 */ 481 private getAvailableDevice(deviceIds: string): BluetoothDevice { 482 LogUtil.log(this.TAG + 'getAvailableDevice length = ' + this.availableDevices.length); 483 let temp = this.availableDevices; 484 for (let i = 0; i < temp.length; i++) { 485 if (temp[i].deviceId === deviceIds) { 486 return temp[i]; 487 } 488 } 489 return null; 490 } 491 492 /** 493 * Remove available device. 494 * 495 * @param deviceId device id 496 */ 497 private removeAvailableDevice(deviceId: string): void { 498 LogUtil.log(this.TAG + 'removeAvailableDevice : before : availableDevices length = ' + this.availableDevices.length); 499 this.availableDevices = this.availableDevices.filter((device) => device.deviceId !== deviceId) 500 AppStorage.SetOrCreate('bluetoothAvailableDevices', this.availableDevices); 501 LogUtil.log(this.TAG + 'removeAvailableDevice : after : availableDevices length = ' + this.availableDevices.length); 502 } 503 504 /** 505 * Connect Failed Dialog 506 */ 507 private showConnectFailedDialog(deviceName: string) { 508 AlertDialog.show({ 509 title: $r("app.string.bluetooth_connect_failed"), 510 message: $r("app.string.bluetooth_connect_failed_msg", deviceName), 511 confirm: { 512 value: $r("app.string.bluetooth_know_button"), 513 action: () => { 514 LogUtil.info('Button-clicking callback') 515 } 516 }, 517 cancel: () => { 518 LogUtil.info('Closed callbacks') 519 }, 520 alignment: deviceTypeInfo === 'phone' || deviceTypeInfo === 'default' ? DialogAlignment.Bottom : DialogAlignment.Center, 521 offset: ({ 522 dx: 0, dy: deviceTypeInfo === 'phone' || deviceTypeInfo === 'default' ? '-24dp' : 0 523 }) 524 }) 525 526 } 527}