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