1/*
2 * Copyright (C) 2016 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.googlecode.android_scripting.facade.bluetooth;
18
19import java.util.HashMap;
20import java.util.List;
21import java.util.concurrent.Callable;
22
23import android.app.Service;
24import android.bluetooth.BluetoothAdapter;
25import android.bluetooth.le.AdvertiseCallback;
26import android.bluetooth.le.AdvertiseData;
27import android.bluetooth.le.AdvertiseData.Builder;
28import android.bluetooth.le.AdvertiseSettings;
29import android.bluetooth.le.BluetoothLeAdvertiser;
30import android.os.Bundle;
31import android.os.ParcelUuid;
32
33import com.googlecode.android_scripting.Log;
34import com.googlecode.android_scripting.MainThread;
35import com.googlecode.android_scripting.facade.EventFacade;
36import com.googlecode.android_scripting.facade.FacadeManager;
37import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
38import com.googlecode.android_scripting.rpc.Rpc;
39import com.googlecode.android_scripting.rpc.RpcParameter;
40
41/**
42 * BluetoothLe Advertise functions.
43 */
44
45public class BluetoothLeAdvertiseFacade extends RpcReceiver {
46
47    private final EventFacade mEventFacade;
48    private BluetoothAdapter mBluetoothAdapter;
49    private static int BleAdvertiseCallbackCount;
50    private static int BleAdvertiseSettingsCount;
51    private static int BleAdvertiseDataCount;
52    private final HashMap<Integer, myAdvertiseCallback> mAdvertiseCallbackList;
53    private final BluetoothLeAdvertiser mAdvertise;
54    private final Service mService;
55    private Builder mAdvertiseDataBuilder;
56    private android.bluetooth.le.AdvertiseSettings.Builder mAdvertiseSettingsBuilder;
57    private final HashMap<Integer, AdvertiseData> mAdvertiseDataList;
58    private final HashMap<Integer, AdvertiseSettings> mAdvertiseSettingsList;
59
60    public BluetoothLeAdvertiseFacade(FacadeManager manager) {
61        super(manager);
62        mService = manager.getService();
63        mBluetoothAdapter = MainThread.run(mService,
64                new Callable<BluetoothAdapter>() {
65                    @Override
66                    public BluetoothAdapter call() throws Exception {
67                        return BluetoothAdapter.getDefaultAdapter();
68                    }
69                });
70        mEventFacade = manager.getReceiver(EventFacade.class);
71        mAdvertiseCallbackList = new HashMap<Integer, myAdvertiseCallback>();
72        mAdvertise = mBluetoothAdapter.getBluetoothLeAdvertiser();
73        mAdvertiseDataList = new HashMap<Integer, AdvertiseData>();
74        mAdvertiseSettingsList = new HashMap<Integer, AdvertiseSettings>();
75        mAdvertiseDataBuilder = new Builder();
76        mAdvertiseSettingsBuilder = new android.bluetooth.le.AdvertiseSettings.Builder();
77    }
78
79    /**
80     * Constructs a myAdvertiseCallback obj and returns its index
81     *
82     * @return myAdvertiseCallback.index
83     */
84    @Rpc(description = "Generate a new myAdvertisement Object")
85    public Integer bleGenBleAdvertiseCallback() {
86        BleAdvertiseCallbackCount += 1;
87        int index = BleAdvertiseCallbackCount;
88        myAdvertiseCallback mCallback = new myAdvertiseCallback(index);
89        mAdvertiseCallbackList.put(mCallback.index,
90                mCallback);
91        return mCallback.index;
92    }
93
94    /**
95     * Constructs a AdvertiseData obj and returns its index
96     *
97     * @return index
98     */
99    @Rpc(description = "Constructs a new Builder obj for AdvertiseData and returns its index")
100    public Integer bleBuildAdvertiseData() {
101        BleAdvertiseDataCount += 1;
102        int index = BleAdvertiseDataCount;
103        mAdvertiseDataList.put(index,
104                mAdvertiseDataBuilder.build());
105        mAdvertiseDataBuilder = new Builder();
106        return index;
107    }
108
109    /**
110     * Constructs a Advertise Settings obj and returns its index
111     *
112     * @return index
113     */
114    @Rpc(description = "Constructs a new Builder obj for AdvertiseData and returns its index")
115    public Integer bleBuildAdvertiseSettings() {
116        BleAdvertiseSettingsCount += 1;
117        int index = BleAdvertiseSettingsCount;
118        mAdvertiseSettingsList.put(index,
119                mAdvertiseSettingsBuilder.build());
120        mAdvertiseSettingsBuilder = new android.bluetooth.le.AdvertiseSettings.Builder();
121        return index;
122    }
123
124    /**
125     * Stops a ble advertisement
126     *
127     * @param index the id of the advertisement to stop advertising on
128     * @throws Exception
129     */
130    @Rpc(description = "Stops an ongoing ble advertisement")
131    public void bleStopBleAdvertising(
132            @RpcParameter(name = "index")
133            Integer index) throws Exception {
134        if (mAdvertiseCallbackList.get(index) != null) {
135            Log.d("bluetooth_le mAdvertise " + index);
136            mAdvertise.stopAdvertising(mAdvertiseCallbackList
137                    .get(index));
138        } else {
139            throw new Exception("Invalid index input:" + Integer.toString(index));
140        }
141    }
142
143    /**
144     * Starts ble advertising
145     *
146     * @param callbackIndex The advertisementCallback index
147     * @param dataIndex the AdvertiseData index
148     * @param settingsIndex the advertisementsettings index
149     * @throws Exception
150     */
151    @Rpc(description = "Starts ble advertisement")
152    public void bleStartBleAdvertising(
153            @RpcParameter(name = "callbackIndex")
154            Integer callbackIndex,
155            @RpcParameter(name = "dataIndex")
156            Integer dataIndex,
157            @RpcParameter(name = "settingsIndex")
158            Integer settingsIndex
159            ) throws Exception {
160        AdvertiseData mData = new AdvertiseData.Builder().build();
161        AdvertiseSettings mSettings = new AdvertiseSettings.Builder().build();
162        if (mAdvertiseDataList.get(dataIndex) != null) {
163            mData = mAdvertiseDataList.get(dataIndex);
164        } else {
165            throw new Exception("Invalid dataIndex input:" + Integer.toString(dataIndex));
166        }
167        if (mAdvertiseSettingsList.get(settingsIndex) != null) {
168            mSettings = mAdvertiseSettingsList.get(settingsIndex);
169        } else {
170            throw new Exception("Invalid settingsIndex input:" + Integer.toString(settingsIndex));
171        }
172        if (mAdvertiseCallbackList.get(callbackIndex) != null) {
173            Log.d("bluetooth_le starting a background advertisement on callback index: "
174                    + Integer.toString(callbackIndex));
175            mAdvertise
176                    .startAdvertising(mSettings, mData, mAdvertiseCallbackList.get(callbackIndex));
177        } else {
178            throw new Exception("Invalid callbackIndex input" + Integer.toString(callbackIndex));
179        }
180    }
181
182    /**
183     * Starts ble advertising with a scanResponse. ScanResponses are created in the same way
184     * AdvertiseData is created since they share the same object type.
185     *
186     * @param callbackIndex The advertisementCallback index
187     * @param dataIndex the AdvertiseData index
188     * @param settingsIndex the advertisementsettings index
189     * @param scanResponseIndex the scanResponse index
190     * @throws Exception
191     */
192    @Rpc(description = "Starts ble advertisement")
193    public void bleStartBleAdvertisingWithScanResponse(
194            @RpcParameter(name = "callbackIndex")
195            Integer callbackIndex,
196            @RpcParameter(name = "dataIndex")
197            Integer dataIndex,
198            @RpcParameter(name = "settingsIndex")
199            Integer settingsIndex,
200            @RpcParameter(name = "scanResponseIndex")
201            Integer scanResponseIndex
202            ) throws Exception {
203        AdvertiseData mData = new AdvertiseData.Builder().build();
204        AdvertiseSettings mSettings = new AdvertiseSettings.Builder().build();
205        AdvertiseData mScanResponse = new AdvertiseData.Builder().build();
206
207        if (mAdvertiseDataList.get(dataIndex) != null) {
208            mData = mAdvertiseDataList.get(dataIndex);
209        } else {
210            throw new Exception("Invalid dataIndex input:" + Integer.toString(dataIndex));
211        }
212        if (mAdvertiseSettingsList.get(settingsIndex) != null) {
213            mSettings = mAdvertiseSettingsList.get(settingsIndex);
214        } else {
215            throw new Exception("Invalid settingsIndex input:" + Integer.toString(settingsIndex));
216        }
217        if (mAdvertiseDataList.get(scanResponseIndex) != null) {
218            mScanResponse = mAdvertiseDataList.get(scanResponseIndex);
219        } else {
220            throw new Exception("Invalid scanResponseIndex input:"
221                    + Integer.toString(settingsIndex));
222        }
223        if (mAdvertiseCallbackList.get(callbackIndex) != null) {
224            Log.d("bluetooth_le starting a background advertise on callback index: "
225                    + Integer.toString(callbackIndex));
226            mAdvertise
227                    .startAdvertising(mSettings, mData, mScanResponse,
228                            mAdvertiseCallbackList.get(callbackIndex));
229        } else {
230            throw new Exception("Invalid callbackIndex input" + Integer.toString(callbackIndex));
231        }
232    }
233
234    /**
235     * Get ble advertisement settings mode
236     *
237     * @param index the advertise settings object to use
238     * @return the mode of the advertise settings object
239     * @throws Exception
240     */
241    @Rpc(description = "Get ble advertisement settings mode")
242    public int bleGetAdvertiseSettingsMode(
243            @RpcParameter(name = "index")
244            Integer index) throws Exception {
245        if (mAdvertiseSettingsList.get(index) != null) {
246            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
247            return mSettings.getMode();
248        } else {
249            throw new Exception("Invalid index input:" + Integer.toString(index));
250        }
251    }
252
253    /**
254     * Get ble advertisement settings tx power level
255     *
256     * @param index the advertise settings object to use
257     * @return the tx power level of the advertise settings object
258     * @throws Exception
259     */
260    @Rpc(description = "Get ble advertisement settings tx power level")
261    public int bleGetAdvertiseSettingsTxPowerLevel(
262            @RpcParameter(name = "index")
263            Integer index) throws Exception {
264        if (mAdvertiseSettingsList.get(index) != null) {
265            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
266            return mSettings.getTxPowerLevel();
267        } else {
268            throw new Exception("Invalid index input:" + Integer.toString(index));
269        }
270    }
271
272    /**
273     * Get ble advertisement settings isConnectable value
274     *
275     * @param index the advertise settings object to use
276     * @return the boolean value whether the advertisement will indicate
277     * connectable.
278     * @throws Exception
279     */
280    @Rpc(description = "Get ble advertisement settings isConnectable value")
281    public boolean bleGetAdvertiseSettingsIsConnectable(
282            @RpcParameter(name = "index")
283            Integer index) throws Exception {
284        if (mAdvertiseSettingsList.get(index) != null) {
285            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
286            return mSettings.isConnectable();
287        } else {
288            throw new Exception("Invalid index input:" + Integer.toString(index));
289        }
290    }
291
292    /**
293     * Get ble advertisement data include tx power level
294     *
295     * @param index the advertise data object to use
296     * @return True if include tx power level, false otherwise
297     * @throws Exception
298     */
299    @Rpc(description = "Get ble advertisement data include tx power level")
300    public Boolean bleGetAdvertiseDataIncludeTxPowerLevel(
301            @RpcParameter(name = "index")
302            Integer index) throws Exception {
303        if (mAdvertiseDataList.get(index) != null) {
304            AdvertiseData mData = mAdvertiseDataList.get(index);
305            return mData.getIncludeTxPowerLevel();
306        } else {
307            throw new Exception("Invalid index input:" + Integer.toString(index));
308        }
309    }
310
311    /**
312     * Get ble advertisement data manufacturer specific data
313     *
314     * @param index the advertise data object to use
315     * @param manufacturerId the id that corresponds to the manufacturer specific data.
316     * @return the corresponding manufacturer specific data to the manufacturer id.
317     * @throws Exception
318     */
319    @Rpc(description = "Get ble advertisement data manufacturer specific data")
320    public byte[] bleGetAdvertiseDataManufacturerSpecificData(
321            @RpcParameter(name = "index")
322            Integer index,
323            @RpcParameter(name = "manufacturerId")
324            Integer manufacturerId) throws Exception {
325        if (mAdvertiseDataList.get(index) != null) {
326            AdvertiseData mData = mAdvertiseDataList.get(index);
327            if (mData.getManufacturerSpecificData() != null) {
328                return mData.getManufacturerSpecificData().get(manufacturerId);
329            } else {
330                throw new Exception("Invalid manufacturerId input:" + Integer.toString(manufacturerId));
331            }
332        } else {
333            throw new Exception("Invalid index input:" + Integer.toString(index));
334
335        }
336    }
337
338    /**
339     * Get ble advertisement data include device name
340     *
341     * @param index the advertise data object to use
342     * @return the advertisement data's include device name
343     * @throws Exception
344     */
345    @Rpc(description = "Get ble advertisement include device name")
346    public Boolean bleGetAdvertiseDataIncludeDeviceName(
347            @RpcParameter(name = "index")
348            Integer index) throws Exception {
349        if (mAdvertiseDataList.get(index) != null) {
350            AdvertiseData mData = mAdvertiseDataList.get(index);
351            return mData.getIncludeDeviceName();
352        } else {
353            throw new Exception("Invalid index input:" + Integer.toString(index));
354        }
355    }
356
357    /**
358     * Get ble advertisement Service Data
359     *
360     * @param index the advertise data object to use
361     * @param serviceUuid the uuid corresponding to the service data.
362     * @return the advertisement data's service data
363     * @throws Exception
364     */
365    @Rpc(description = "Get ble advertisement Service Data")
366    public byte[] bleGetAdvertiseDataServiceData(
367            @RpcParameter(name = "index")
368            Integer index,
369            @RpcParameter(name = "serviceUuid")
370            String serviceUuid) throws Exception {
371        ParcelUuid uuidKey = ParcelUuid.fromString(serviceUuid);
372        if (mAdvertiseDataList.get(index) != null) {
373            AdvertiseData mData = mAdvertiseDataList.get(index);
374            if (mData.getServiceData().containsKey(uuidKey)) {
375                return mData.getServiceData().get(uuidKey);
376            } else {
377                throw new Exception("Invalid serviceUuid input:" + serviceUuid);
378            }
379        } else {
380            throw new Exception("Invalid index input:" + Integer.toString(index));
381        }
382    }
383
384    /**
385     * Get ble advertisement Service Uuids
386     *
387     * @param index the advertise data object to use
388     * @return the advertisement data's Service Uuids
389     * @throws Exception
390     */
391    @Rpc(description = "Get ble advertisement Service Uuids")
392    public List<ParcelUuid> bleGetAdvertiseDataServiceUuids(
393            @RpcParameter(name = "index")
394            Integer index) throws Exception {
395        if (mAdvertiseDataList.get(index) != null) {
396            AdvertiseData mData = mAdvertiseDataList.get(index);
397            return mData.getServiceUuids();
398        } else {
399            throw new Exception("Invalid index input:" + Integer.toString(index));
400        }
401    }
402
403    /**
404     * Set ble advertisement data service uuids
405     *
406     * @param uuidList
407     * @throws Exception
408     */
409    @Rpc(description = "Set ble advertisement data service uuids")
410    public void bleSetAdvertiseDataSetServiceUuids(
411            @RpcParameter(name = "uuidList")
412            String[] uuidList
413            ) {
414        for (String uuid : uuidList) {
415            mAdvertiseDataBuilder.addServiceUuid(ParcelUuid.fromString(uuid));
416        }
417    }
418
419    /**
420     * Set ble advertise data service uuids
421     *
422     * @param serviceDataUuid
423     * @param serviceData
424     * @throws Exception
425     */
426    @Rpc(description = "Set ble advertise data service uuids")
427    public void bleAddAdvertiseDataServiceData(
428            @RpcParameter(name = "serviceDataUuid")
429            String serviceDataUuid,
430            @RpcParameter(name = "serviceData")
431            byte[] serviceData
432            ) {
433        mAdvertiseDataBuilder.addServiceData(
434                ParcelUuid.fromString(serviceDataUuid),
435                serviceData);
436    }
437
438    /**
439     * Set ble advertise data manufacturer id
440     *
441     * @param manufacturerId the manufacturer id to set
442     * @param manufacturerSpecificData the manufacturer specific data to set
443     * @throws Exception
444     */
445    @Rpc(description = "Set ble advertise data manufacturerId")
446    public void bleAddAdvertiseDataManufacturerId(
447            @RpcParameter(name = "manufacturerId")
448            Integer manufacturerId,
449            @RpcParameter(name = "manufacturerSpecificData")
450            byte[] manufacturerSpecificData
451            ) {
452        mAdvertiseDataBuilder.addManufacturerData(manufacturerId,
453                manufacturerSpecificData);
454    }
455
456    /**
457     * Set ble advertise settings advertise mode
458     *
459     * @param advertiseMode
460     * @throws Exception
461     */
462    @Rpc(description = "Set ble advertise settings advertise mode")
463    public void bleSetAdvertiseSettingsAdvertiseMode(
464            @RpcParameter(name = "advertiseMode")
465            Integer advertiseMode
466            ) {
467        mAdvertiseSettingsBuilder.setAdvertiseMode(advertiseMode);
468    }
469
470    /**
471     * Set ble advertise settings tx power level
472     *
473     * @param txPowerLevel the tx power level to set
474     * @throws Exception
475     */
476    @Rpc(description = "Set ble advertise settings tx power level")
477    public void bleSetAdvertiseSettingsTxPowerLevel(
478            @RpcParameter(name = "txPowerLevel")
479            Integer txPowerLevel
480            ) {
481        mAdvertiseSettingsBuilder.setTxPowerLevel(txPowerLevel);
482    }
483
484    /**
485     * Set ble advertise settings the isConnectable value
486     *
487     * @param type the isConnectable value
488     * @throws Exception
489     */
490    @Rpc(description = "Set ble advertise settings isConnectable value")
491    public void bleSetAdvertiseSettingsIsConnectable(
492            @RpcParameter(name = "value")
493            Boolean value
494            ) {
495        mAdvertiseSettingsBuilder.setConnectable(value);
496    }
497
498    /**
499     * Set ble advertisement data include tx power level
500     *
501     * @param includeTxPowerLevel boolean whether to include the tx power level or not in the
502     *            advertisement
503     */
504    @Rpc(description = "Set ble advertisement data include tx power level")
505    public void bleSetAdvertiseDataIncludeTxPowerLevel(
506            @RpcParameter(name = "includeTxPowerLevel")
507            Boolean includeTxPowerLevel
508            ) {
509        mAdvertiseDataBuilder.setIncludeTxPowerLevel(includeTxPowerLevel);
510    }
511
512    /**
513     * Set ble advertisement settings set timeout
514     *
515     * @param timeoutSeconds Limit advertising to a given amount of time.
516     */
517    @Rpc(description = "Set ble advertisement data include tx power level")
518    public void bleSetAdvertiseSettingsTimeout(
519            @RpcParameter(name = "timeoutSeconds")
520            Integer timeoutSeconds
521            ) {
522        mAdvertiseSettingsBuilder.setTimeout(timeoutSeconds);
523    }
524
525    /**
526     * Set ble advertisement data include device name
527     *
528     * @param includeDeviceName boolean whether to include device name or not in the
529     *            advertisement
530     */
531    @Rpc(description = "Set ble advertisement data include device name")
532    public void bleSetAdvertiseDataIncludeDeviceName(
533            @RpcParameter(name = "includeDeviceName")
534            Boolean includeDeviceName
535            ) {
536        mAdvertiseDataBuilder.setIncludeDeviceName(includeDeviceName);
537    }
538
539    private class myAdvertiseCallback extends AdvertiseCallback {
540        public Integer index;
541        private final Bundle mResults;
542        String mEventType;
543
544        public myAdvertiseCallback(int idx) {
545            index = idx;
546            mEventType = "BleAdvertise";
547            mResults = new Bundle();
548        }
549
550        @Override
551        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
552            Log.d("bluetooth_le_advertisement onSuccess " + mEventType + " "
553                    + index);
554            mResults.putString("Type", "onSuccess");
555            mResults.putParcelable("SettingsInEffect", settingsInEffect);
556            mEventFacade.postEvent(mEventType + index + "onSuccess", mResults.clone());
557            mResults.clear();
558        }
559
560        @Override
561        public void onStartFailure(int errorCode) {
562            String errorString = "UNKNOWN_ERROR_CODE";
563            if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED) {
564                errorString = "ADVERTISE_FAILED_ALREADY_STARTED";
565            } else if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE) {
566                errorString = "ADVERTISE_FAILED_DATA_TOO_LARGE";
567            } else if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED) {
568                errorString = "ADVERTISE_FAILED_FEATURE_UNSUPPORTED";
569            } else if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR) {
570                errorString = "ADVERTISE_FAILED_INTERNAL_ERROR";
571            } else if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) {
572                errorString = "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS";
573            }
574            Log.d("bluetooth_le_advertisement onFailure " + mEventType + " "
575                    + index + " error " + errorString);
576            mResults.putString("Type", "onFailure");
577            mResults.putInt("ErrorCode", errorCode);
578            mResults.putString("Error", errorString);
579            mEventFacade.postEvent(mEventType + index + "onFailure",
580                    mResults.clone());
581            mResults.clear();
582        }
583    }
584
585    @Override
586    public void shutdown() {
587        if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
588            for (myAdvertiseCallback mAdvertise : mAdvertiseCallbackList
589                .values()) {
590                if (mAdvertise != null) {
591                    try{
592                        mBluetoothAdapter.getBluetoothLeAdvertiser()
593                            .stopAdvertising(mAdvertise);
594                    } catch (NullPointerException e) {
595                        Log.e("Failed to stop ble advertising.", e);
596                    }
597                }
598            }
599        }
600        mAdvertiseCallbackList.clear();
601        mAdvertiseSettingsList.clear();
602        mAdvertiseDataList.clear();
603    }
604}
605