1 /*
2  * Copyright (C) 2017 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 
17 package com.google.android.mobly.snippet.bundled;
18 
19 import android.annotation.TargetApi;
20 import android.bluetooth.BluetoothAdapter;
21 import android.bluetooth.le.AdvertiseCallback;
22 import android.bluetooth.le.AdvertiseData;
23 import android.bluetooth.le.AdvertiseSettings;
24 import android.bluetooth.le.BluetoothLeAdvertiser;
25 import android.os.Build;
26 import android.os.Bundle;
27 import android.os.ParcelUuid;
28 import com.google.android.mobly.snippet.Snippet;
29 import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer;
30 import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
31 import com.google.android.mobly.snippet.bundled.utils.RpcEnum;
32 import com.google.android.mobly.snippet.event.EventCache;
33 import com.google.android.mobly.snippet.event.SnippetEvent;
34 import com.google.android.mobly.snippet.rpc.AsyncRpc;
35 import com.google.android.mobly.snippet.rpc.Rpc;
36 import com.google.android.mobly.snippet.rpc.RpcMinSdk;
37 import com.google.android.mobly.snippet.rpc.RpcOptional;
38 import com.google.android.mobly.snippet.util.Log;
39 import java.util.HashMap;
40 import org.json.JSONException;
41 import org.json.JSONObject;
42 
43 /** Snippet class exposing Android APIs in WifiManager. */
44 @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
45 public class BluetoothLeAdvertiserSnippet implements Snippet {
46     private static class BluetoothLeAdvertiserSnippetException extends Exception {
47         private static final long serialVersionUID = 1;
48 
BluetoothLeAdvertiserSnippetException(String msg)49         public BluetoothLeAdvertiserSnippetException(String msg) {
50             super(msg);
51         }
52     }
53 
54     private final BluetoothLeAdvertiser mAdvertiser;
55     private static final EventCache sEventCache = EventCache.getInstance();
56 
57     private final HashMap<String, AdvertiseCallback> mAdvertiseCallbacks = new HashMap<>();
58 
BluetoothLeAdvertiserSnippet()59     public BluetoothLeAdvertiserSnippet() {
60         mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
61     }
62 
63     /**
64      * Start Bluetooth LE advertising.
65      *
66      * <p>This can be called multiple times, and each call is associated with a {@link
67      * AdvertiseCallback} object, which is used to stop the advertising.
68      *
69      * @param callbackId
70      * @param advertiseSettings A JSONObject representing a {@link AdvertiseSettings object}. E.g.
71      *     <pre>
72      *          {
73      *            "AdvertiseMode": "ADVERTISE_MODE_BALANCED",
74      *            "Timeout": (int, milliseconds),
75      *            "Connectable": (bool),
76      *            "TxPowerLevel": "ADVERTISE_TX_POWER_LOW"
77      *          }
78      *     </pre>
79      *
80      * @param advertiseData A JSONObject representing a {@link AdvertiseData} object will be
81      *     broadcast if the operation succeeds. E.g.
82      *     <pre>
83      *          {
84      *            "IncludeDeviceName": (bool),
85      *            # JSON list, each element representing a set of service data, which is composed of
86      *            # a UUID, and an optional string.
87      *            "ServiceData": [
88      *                      {
89      *                        "UUID": (A string representation of {@link ParcelUuid}),
90      *                        "Data": (Optional, The string representation of what you want to
91      *                                 advertise, base64 encoded)
92      *                        # If you want to add a UUID without data, simply omit the "Data"
93      *                        # field.
94      *                      }
95      *                ]
96      *          }
97      *     </pre>
98      *
99      * @param scanResponse A JSONObject representing a {@link AdvertiseData} object which will
100      *     response the data to the scanning device. E.g.
101      *     <pre>
102      *          {
103      *            "IncludeDeviceName": (bool),
104      *            # JSON list, each element representing a set of service data, which is composed of
105      *            # a UUID, and an optional string.
106      *            "ServiceData": [
107      *                      {
108      *                        "UUID": (A string representation of {@link ParcelUuid}),
109      *                        "Data": (Optional, The string representation of what you want to
110      *                                 advertise, base64 encoded)
111      *                        # If you want to add a UUID without data, simply omit the "Data"
112      *                        # field.
113      *                      }
114      *                ]
115      *          }
116      *     </pre>
117      *
118      * @throws BluetoothLeAdvertiserSnippetException
119      * @throws JSONException
120      */
121     @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
122     @AsyncRpc(description = "Start BLE advertising.")
bleStartAdvertising( String callbackId, JSONObject advertiseSettings, JSONObject advertiseData, @RpcOptional JSONObject scanResponse)123     public void bleStartAdvertising(
124             String callbackId,
125             JSONObject advertiseSettings,
126             JSONObject advertiseData,
127             @RpcOptional JSONObject scanResponse)
128             throws BluetoothLeAdvertiserSnippetException, JSONException {
129         if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
130             throw new BluetoothLeAdvertiserSnippetException(
131                     "Bluetooth is disabled, cannot start BLE advertising.");
132         }
133         AdvertiseSettings settings = JsonDeserializer.jsonToBleAdvertiseSettings(advertiseSettings);
134         AdvertiseData data = JsonDeserializer.jsonToBleAdvertiseData(advertiseData);
135         AdvertiseCallback advertiseCallback = new DefaultAdvertiseCallback(callbackId);
136         if (scanResponse == null) {
137             mAdvertiser.startAdvertising(settings, data, advertiseCallback);
138         } else {
139             AdvertiseData response = JsonDeserializer.jsonToBleAdvertiseData(scanResponse);
140             mAdvertiser.startAdvertising(settings, data, response, advertiseCallback);
141         }
142         mAdvertiseCallbacks.put(callbackId, advertiseCallback);
143     }
144 
145     /**
146      * Stop a BLE advertising.
147      *
148      * @param callbackId The callbackId corresponding to the {@link
149      *     BluetoothLeAdvertiserSnippet#bleStartAdvertising} call that started the advertising.
150      * @throws BluetoothLeScannerSnippet.BluetoothLeScanSnippetException
151      */
152     @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
153     @Rpc(description = "Stop BLE advertising.")
bleStopAdvertising(String callbackId)154     public void bleStopAdvertising(String callbackId) throws BluetoothLeAdvertiserSnippetException {
155         AdvertiseCallback callback = mAdvertiseCallbacks.remove(callbackId);
156         if (callback == null) {
157             throw new BluetoothLeAdvertiserSnippetException(
158                     "No advertising session found for ID " + callbackId);
159         }
160         mAdvertiser.stopAdvertising(callback);
161     }
162 
163     private static class DefaultAdvertiseCallback extends AdvertiseCallback {
164         private final String mCallbackId;
165         public static RpcEnum ADVERTISE_FAILURE_ERROR_CODE =
166                 new RpcEnum.Builder()
167                         .add("ADVERTISE_FAILED_ALREADY_STARTED", ADVERTISE_FAILED_ALREADY_STARTED)
168                         .add("ADVERTISE_FAILED_DATA_TOO_LARGE", ADVERTISE_FAILED_DATA_TOO_LARGE)
169                         .add(
170                                 "ADVERTISE_FAILED_FEATURE_UNSUPPORTED",
171                                 ADVERTISE_FAILED_FEATURE_UNSUPPORTED)
172                         .add("ADVERTISE_FAILED_INTERNAL_ERROR", ADVERTISE_FAILED_INTERNAL_ERROR)
173                         .add(
174                                 "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS",
175                                 ADVERTISE_FAILED_TOO_MANY_ADVERTISERS)
176                         .build();
177 
DefaultAdvertiseCallback(String callbackId)178         public DefaultAdvertiseCallback(String callbackId) {
179             mCallbackId = callbackId;
180         }
181 
182         @Override
onStartSuccess(AdvertiseSettings settingsInEffect)183         public void onStartSuccess(AdvertiseSettings settingsInEffect) {
184             Log.e("Bluetooth LE advertising started with settings: " + settingsInEffect.toString());
185             SnippetEvent event = new SnippetEvent(mCallbackId, "onStartSuccess");
186             Bundle advertiseSettings =
187                     JsonSerializer.serializeBleAdvertisingSettings(settingsInEffect);
188             event.getData().putBundle("SettingsInEffect", advertiseSettings);
189             sEventCache.postEvent(event);
190         }
191 
192         @Override
onStartFailure(int errorCode)193         public void onStartFailure(int errorCode) {
194             Log.e("Bluetooth LE advertising failed to start with error code: " + errorCode);
195             SnippetEvent event = new SnippetEvent(mCallbackId, "onStartFailure");
196             final String errorCodeString = ADVERTISE_FAILURE_ERROR_CODE.getString(errorCode);
197             event.getData().putString("ErrorCode", errorCodeString);
198             sEventCache.postEvent(event);
199         }
200     }
201 
202     @Override
shutdown()203     public void shutdown() {
204         for (AdvertiseCallback callback : mAdvertiseCallbacks.values()) {
205             mAdvertiser.stopAdvertising(callback);
206         }
207         mAdvertiseCallbacks.clear();
208     }
209 }
210