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