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