• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}