• 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.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}