1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// Custom binding for the Bluetooth API.
6
7var binding = require('binding').Binding.create('bluetooth');
8
9var chrome = requireNative('chrome').GetChrome();
10var Event = require('event_bindings').Event;
11var lastError = require('lastError');
12var sendRequest = require('sendRequest').sendRequest;
13
14// Use custom binding to create an undocumented event listener that will
15// receive events about device discovery and call the event listener that was
16// provided with the request to begin discovery.
17binding.registerCustomHook(function(api) {
18  var apiFunctions = api.apiFunctions;
19
20  var bluetooth = {};
21
22  function callCallbackIfPresent(name, args, error) {
23    var callback = args[args.length - 1];
24    if (typeof(callback) == 'function')
25      lastError.run(name, error, callback);
26  }
27
28  bluetooth.deviceDiscoveredHandler = null;
29  bluetooth.onDeviceDiscovered = new Event('bluetooth.onDeviceDiscovered');
30  function clearDeviceDiscoveredHandler() {
31    bluetooth.onDeviceDiscovered.removeListener(
32        bluetooth.deviceDiscoveredHandler);
33    bluetooth.deviceDiscoveredHandler = null;
34  }
35  apiFunctions.setHandleRequest('startDiscovery',
36      function() {
37        var args = arguments;
38        if (args.length > 0 && args[0] && args[0].deviceCallback) {
39          if (bluetooth.deviceDiscoveredHandler != null) {
40            callCallbackIfPresent('bluetooth.startDiscovery',
41                                  args,
42                                  'Concurrent discovery is not allowed.');
43            return;
44          }
45
46          bluetooth.deviceDiscoveredHandler = args[0].deviceCallback;
47          bluetooth.onDeviceDiscovered.addListener(
48              bluetooth.deviceDiscoveredHandler);
49          sendRequest(this.name,
50                      args,
51                      this.definition.parameters,
52                      {customCallback:this.customCallback});
53        } else {
54          callCallbackIfPresent(
55            'bluetooth.startDiscovery',
56            args,
57            'deviceCallback is required in the options object');
58          return;
59        }
60      });
61  apiFunctions.setCustomCallback('startDiscovery',
62      function(name, request, response) {
63        if (chrome.runtime.lastError) {
64          clearDeviceDiscoveredHandler();
65          return;
66        }
67      });
68  apiFunctions.setHandleRequest('stopDiscovery',
69      function() {
70        clearDeviceDiscoveredHandler();
71        sendRequest(this.name, arguments, this.definition.parameters);
72      });
73
74  // An object to hold state during one call to getDevices.
75  bluetooth.getDevicesState = null;
76
77  // Hidden events used to deliver getDevices data to the client callbacks
78  bluetooth.onDeviceSearchResult = new Event('bluetooth.onDeviceSearchResult');
79  bluetooth.onDeviceSearchFinished =
80      new Event('bluetooth.onDeviceSearchFinished');
81
82  function deviceSearchResultHandler(device) {
83    bluetooth.getDevicesState.actualEvents++;
84    bluetooth.getDevicesState.deviceCallback(device);
85    maybeFinishDeviceSearch();
86  }
87
88  function deviceSearchFinishedHandler(info) {
89    bluetooth.getDevicesState.expectedEventCount = info.expectedEventCount;
90    maybeFinishDeviceSearch();
91  }
92
93  function addDeviceSearchListeners() {
94    bluetooth.onDeviceSearchResult.addListener(deviceSearchResultHandler);
95    bluetooth.onDeviceSearchFinished.addListener(deviceSearchFinishedHandler);
96  }
97
98  function removeDeviceSearchListeners() {
99    bluetooth.onDeviceSearchResult.removeListener(deviceSearchResultHandler);
100    bluetooth.onDeviceSearchFinished.removeListener(
101        deviceSearchFinishedHandler);
102  }
103
104  function maybeFinishDeviceSearch() {
105    var state = bluetooth.getDevicesState;
106    if (typeof(state.expectedEventCount) != 'undefined' &&
107        state.actualEvents >= state.expectedEventCount) {
108      finishDeviceSearch();
109    }
110  }
111
112  function finishDeviceSearch() {
113    var finalCallback = bluetooth.getDevicesState.finalCallback;
114    removeDeviceSearchListeners();
115    bluetooth.getDevicesState = null;
116
117    if (finalCallback) {
118      finalCallback();
119    }
120  }
121
122  apiFunctions.setUpdateArgumentsPostValidate('getDevices',
123      function() {
124        var args = $Array.slice(arguments);
125
126        if (bluetooth.getDevicesState != null) {
127          throw new Error('Concurrent calls to getDevices are not allowed.');
128        }
129
130        var state = { actualEvents: 0 };
131
132        if (typeof(args[args.length - 1]) == 'function') {
133          state.finalCallback = args.pop();
134          $Array.push(args,
135              function() {
136                if (chrome.runtime.lastError) {
137                  finishDeviceSearch();
138                }
139              });
140        } else {
141          throw new Error('getDevices must have a final callback parameter.');
142        }
143
144        if (typeof(args[0].deviceCallback) == 'function') {
145          state.deviceCallback = args[0].deviceCallback;
146        } else {
147          throw new Error('getDevices must be passed options with a ' +
148              'deviceCallback.');
149        }
150
151        bluetooth.getDevicesState = state;
152        addDeviceSearchListeners();
153
154        return args;
155      });
156});
157
158exports.binding = binding.generate();
159