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