aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/android
diff options
context:
space:
mode:
authorAng Li <angli@google.com>2017-05-23 16:24:46 -0700
committerGitHub <noreply@github.com>2017-05-23 16:24:46 -0700
commit8a7fe9abd925d8ee471424fcd901a4767aaabf90 (patch)
tree3a03e6d3a98ef508b035b5eaa7d10d81f4cf97c8 /src/main/java/com/google/android
parente9c9480e0e44e223e302d61f3905e1e3aed7bd2d (diff)
downloadmobly-bundled-snippets-8a7fe9abd925d8ee471424fcd901a4767aaabf90.tar.gz
Add basic Bluetooth LE advertising and scan support. (#47)
* Add basic Bluetooth LE advertising and scan support. * Introduce `RpcEnum`, a standard way to handle String-int enum conversion.
Diffstat (limited to 'src/main/java/com/google/android')
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java7
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java178
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java138
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java61
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java110
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java91
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java89
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java1
8 files changed, 618 insertions, 57 deletions
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java
index 67df97b..0f4d3e6 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
+import android.os.Bundle;
import android.support.test.InstrumentationRegistry;
import com.google.android.mobly.snippet.Snippet;
import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
@@ -173,11 +174,11 @@ public class BluetoothAdapterSnippet implements Snippet {
}
@Rpc(description = "Get the list of paired bluetooth devices.")
- public JSONArray btGetPairedDevices()
+ public ArrayList<Bundle> btGetPairedDevices()
throws BluetoothAdapterSnippetException, InterruptedException, JSONException {
- JSONArray pairedDevices = new JSONArray();
+ ArrayList<Bundle> pairedDevices = new ArrayList<>();
for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
- pairedDevices.put(mJsonSerializer.toJson(device));
+ pairedDevices.add(mJsonSerializer.serializeBluetoothDevice(device));
}
return pairedDevices;
}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java
new file mode 100644
index 0000000..e161a5b
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.bundled;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer;
+import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
+import com.google.android.mobly.snippet.bundled.utils.RpcEnum;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.rpc.RpcMinSdk;
+import com.google.android.mobly.snippet.util.Log;
+import java.util.HashMap;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Snippet class exposing Android APIs in WifiManager. */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
+public class BluetoothLeAdvertiserSnippet implements Snippet {
+ private static class BluetoothLeAdvertiserSnippetException extends Exception {
+ private static final long serialVersionUID = 1;
+
+ public BluetoothLeAdvertiserSnippetException(String msg) {
+ super(msg);
+ }
+ }
+
+ private final BluetoothLeAdvertiser mAdvertiser;
+ private static final EventCache sEventCache = EventCache.getInstance();
+
+ private final HashMap<String, AdvertiseCallback> mAdvertiseCallbacks = new HashMap<>();
+
+ public BluetoothLeAdvertiserSnippet() {
+ mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
+ }
+
+ /**
+ * Start Bluetooth LE advertising.
+ *
+ * <p>This can be called multiple times, and each call is associated with a {@link
+ * AdvertiseCallback} object, which is used to stop the advertising.
+ *
+ * @param callbackId
+ * @param advertiseSettings A JSONObject representing a {@link AdvertiseSettings object}. E.g.
+ * <pre>
+ * {
+ * "AdvertiseMode": "ADVERTISE_MODE_BALANCED",
+ * "Timeout": (int, milliseconds),
+ * "Connectable": (bool),
+ * "TxPowerLevel": "ADVERTISE_TX_POWER_LOW"
+ * }
+ * </pre>
+ *
+ * @param advertiseData A JSONObject representing a {@link AdvertiseData} object. E.g.
+ * <pre>
+ * {
+ * "IncludeDeviceName": (bool),
+ * # JSON list, each element representing a set of service data, which is composed of
+ * # a UUID, and an optional string.
+ * "ServiceData": [
+ * {
+ * "UUID": (A string representation of {@link ParcelUuid}),
+ * "Data": (Optional, The string representation of what you want to
+ * advertise, base64 encoded)
+ * # If you want to add a UUID without data, simply omit the "Data"
+ * # field.
+ * }
+ * ]
+ * }
+ * </pre>
+ *
+ * @throws BluetoothLeAdvertiserSnippetException
+ * @throws JSONException
+ */
+ @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
+ @AsyncRpc(description = "Start BLE advertising.")
+ public void bleStartAdvertising(
+ String callbackId, JSONObject advertiseSettings, JSONObject advertiseData)
+ throws BluetoothLeAdvertiserSnippetException, JSONException {
+ if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
+ throw new BluetoothLeAdvertiserSnippetException(
+ "Bluetooth is disabled, cannot start BLE advertising.");
+ }
+ AdvertiseSettings settings = JsonDeserializer.jsonToBleAdvertiseSettings(advertiseSettings);
+ AdvertiseData data = JsonDeserializer.jsonToBleAdvertiseData(advertiseData);
+ AdvertiseCallback advertiseCallback = new DefaultAdvertiseCallback(callbackId);
+ mAdvertiser.startAdvertising(settings, data, advertiseCallback);
+ mAdvertiseCallbacks.put(callbackId, advertiseCallback);
+ }
+
+ /**
+ * Stop a BLE advertising.
+ *
+ * @param callbackId The callbackId corresponding to the {@link
+ * BluetoothLeAdvertiserSnippet#bleStartAdvertising} call that started the advertising.
+ * @throws BluetoothLeScannerSnippet.BluetoothLeScanSnippetException
+ */
+ @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
+ @Rpc(description = "Stop BLE advertising.")
+ public void bleStopAdvertising(String callbackId) throws BluetoothLeAdvertiserSnippetException {
+ AdvertiseCallback callback = mAdvertiseCallbacks.remove(callbackId);
+ if (callback == null) {
+ throw new BluetoothLeAdvertiserSnippetException(
+ "No advertising session found for ID " + callbackId);
+ }
+ mAdvertiser.stopAdvertising(callback);
+ }
+
+ private static class DefaultAdvertiseCallback extends AdvertiseCallback {
+ private final String mCallbackId;
+ public static RpcEnum ADVERTISE_FAILURE_ERROR_CODE =
+ new RpcEnum.Builder()
+ .add("ADVERTISE_FAILED_ALREADY_STARTED", ADVERTISE_FAILED_ALREADY_STARTED)
+ .add("ADVERTISE_FAILED_DATA_TOO_LARGE", ADVERTISE_FAILED_DATA_TOO_LARGE)
+ .add(
+ "ADVERTISE_FAILED_FEATURE_UNSUPPORTED",
+ ADVERTISE_FAILED_FEATURE_UNSUPPORTED)
+ .add("ADVERTISE_FAILED_INTERNAL_ERROR", ADVERTISE_FAILED_INTERNAL_ERROR)
+ .add(
+ "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS",
+ ADVERTISE_FAILED_TOO_MANY_ADVERTISERS)
+ .build();
+
+ public DefaultAdvertiseCallback(String callbackId) {
+ mCallbackId = callbackId;
+ }
+
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ Log.e("Bluetooth LE advertising started with settings: " + settingsInEffect.toString());
+ SnippetEvent event = new SnippetEvent(mCallbackId, "onStartSuccess");
+ Bundle advertiseSettings =
+ JsonSerializer.serializeBleAdvertisingSettings(settingsInEffect);
+ event.getData().putBundle("SettingsInEffect", advertiseSettings);
+ sEventCache.postEvent(event);
+ }
+
+ public void onStartFailure(int errorCode) {
+ Log.e("Bluetooth LE advertising failed to start with error code: " + errorCode);
+ SnippetEvent event = new SnippetEvent(mCallbackId, "onStartFailure");
+ final String errorCodeString = ADVERTISE_FAILURE_ERROR_CODE.getString(errorCode);
+ event.getData().putString("ErrorCode", errorCodeString);
+ sEventCache.postEvent(event);
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ for (AdvertiseCallback callback : mAdvertiseCallbacks.values()) {
+ mAdvertiser.stopAdvertising(callback);
+ }
+ mAdvertiseCallbacks.clear();
+ }
+}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java
new file mode 100644
index 0000000..7e133d1
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.bundled;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanResult;
+import android.os.Build;
+import android.os.Bundle;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
+import com.google.android.mobly.snippet.bundled.utils.MbsEnums;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.rpc.RpcMinSdk;
+import com.google.android.mobly.snippet.util.Log;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/** Snippet class exposing Android APIs in WifiManager. */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
+public class BluetoothLeScannerSnippet implements Snippet {
+ private static class BluetoothLeScanSnippetException extends Exception {
+ private static final long serialVersionUID = 1;
+
+ public BluetoothLeScanSnippetException(String msg) {
+ super(msg);
+ }
+ }
+
+ private final BluetoothLeScanner mScanner;
+ private final EventCache mEventCache = EventCache.getInstance();
+ private final HashMap<String, ScanCallback> mScanCallbacks = new HashMap<>();
+ private final JsonSerializer mJsonSerializer = new JsonSerializer();
+
+ public BluetoothLeScannerSnippet() {
+ mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
+ }
+
+ /**
+ * Start a BLE scan.
+ *
+ * @param callbackId
+ * @throws BluetoothLeScanSnippetException
+ */
+ @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
+ @AsyncRpc(description = "Start BLE scan.")
+ public void bleStartScan(String callbackId) throws BluetoothLeScanSnippetException {
+ if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
+ throw new BluetoothLeScanSnippetException(
+ "Bluetooth is disabled, cannot start BLE scan.");
+ }
+ DefaultScanCallback callback = new DefaultScanCallback(callbackId);
+ mScanner.startScan(callback);
+ mScanCallbacks.put(callbackId, callback);
+ }
+
+ /**
+ * Stop a BLE scan.
+ *
+ * @param callbackId The callbackId corresponding to the {@link
+ * BluetoothLeScannerSnippet#bleStartScan} call that started the scan.
+ * @throws BluetoothLeScanSnippetException
+ */
+ @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
+ @Rpc(description = "Stop a BLE scan.")
+ public void bleStopScan(String callbackId) throws BluetoothLeScanSnippetException {
+ ScanCallback callback = mScanCallbacks.remove(callbackId);
+ if (callback == null) {
+ throw new BluetoothLeScanSnippetException("No ongoing scan with ID: " + callbackId);
+ }
+ mScanner.stopScan(callback);
+ }
+
+ @Override
+ public void shutdown() {
+ for (ScanCallback callback : mScanCallbacks.values()) {
+ mScanner.stopScan(callback);
+ }
+ mScanCallbacks.clear();
+ }
+
+ private class DefaultScanCallback extends ScanCallback {
+ private final String mCallbackId;
+
+ public DefaultScanCallback(String callbackId) {
+ mCallbackId = callbackId;
+ }
+
+ public void onScanResult(int callbackType, ScanResult result) {
+ Log.i("Got Bluetooth LE scan result.");
+ SnippetEvent event = new SnippetEvent(mCallbackId, "onScanResult");
+ String callbackTypeString =
+ MbsEnums.BLE_SCAN_RESULT_CALLBACK_TYPE.getString(callbackType);
+ event.getData().putString("CallbackType", callbackTypeString);
+ event.getData().putBundle("result", mJsonSerializer.serializeBleScanResult(result));
+ mEventCache.postEvent(event);
+ }
+
+ public void onBatchScanResults(List<ScanResult> results) {
+ Log.i("Got Bluetooth LE batch scan results.");
+ SnippetEvent event = new SnippetEvent(mCallbackId, "onBatchScanResult");
+ ArrayList<Bundle> resultList = new ArrayList<>(results.size());
+ for (ScanResult result : results) {
+ resultList.add(mJsonSerializer.serializeBleScanResult(result));
+ }
+ event.getData().putParcelableArrayList("results", resultList);
+ mEventCache.postEvent(event);
+ }
+
+ public void onScanFailed(int errorCode) {
+ Log.e("Bluetooth LE scan failed with error code: " + errorCode);
+ SnippetEvent event = new SnippetEvent(mCallbackId, "onScanFailed");
+ String errorCodeString = MbsEnums.BLE_SCAN_FAILED_ERROR_CODE.getString(errorCode);
+ event.getData().putString("ErrorCode", errorCodeString);
+ mEventCache.postEvent(event);
+ }
+ }
+}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java
index 4fc6b82..2f943e0 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java
@@ -16,7 +16,14 @@
package com.google.android.mobly.snippet.bundled.utils;
+import android.annotation.TargetApi;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
import android.net.wifi.WifiConfiguration;
+import android.os.Build;
+import android.os.ParcelUuid;
+import android.util.Base64;
+import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -40,4 +47,58 @@ public class JsonDeserializer {
}
return config;
}
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static AdvertiseSettings jsonToBleAdvertiseSettings(JSONObject jsonObject)
+ throws JSONException {
+ AdvertiseSettings.Builder builder = new AdvertiseSettings.Builder();
+ if (jsonObject.has("AdvertiseMode")) {
+ int mode = MbsEnums.BLE_ADVERTISE_MODE.getInt(jsonObject.getString("AdvertiseMode"));
+ builder.setAdvertiseMode(mode);
+ }
+ // Timeout in milliseconds.
+ if (jsonObject.has("Timeout")) {
+ builder.setTimeout(jsonObject.getInt("Timeout"));
+ }
+ if (jsonObject.has("Connectable")) {
+ builder.setConnectable(jsonObject.getBoolean("Connectable"));
+ }
+ if (jsonObject.has("TxPowerLevel")) {
+ int txPowerLevel =
+ MbsEnums.BLE_ADVERTISE_TX_POWER.getInt(jsonObject.getString("TxPowerLevel"));
+ builder.setTxPowerLevel(txPowerLevel);
+ }
+ return builder.build();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static AdvertiseData jsonToBleAdvertiseData(JSONObject jsonObject) throws JSONException {
+ AdvertiseData.Builder builder = new AdvertiseData.Builder();
+ if (jsonObject.has("IncludeDeviceName")) {
+ builder.setIncludeDeviceName(jsonObject.getBoolean("IncludeDeviceName"));
+ }
+ if (jsonObject.has("IncludeTxPowerLevel")) {
+ builder.setIncludeTxPowerLevel(jsonObject.getBoolean("IncludeTxPowerLevel"));
+ }
+ if (jsonObject.has("ServiceData")) {
+ JSONArray serviceData = jsonObject.getJSONArray("ServiceData");
+ for (int i = 0; i < serviceData.length(); i++) {
+ JSONObject dataSet = serviceData.getJSONObject(i);
+ ParcelUuid parcelUuid = ParcelUuid.fromString(dataSet.getString("UUID"));
+ builder.addServiceUuid(parcelUuid);
+ if (dataSet.has("Data")) {
+ byte[] data = Base64.decode(dataSet.getString("Data"), Base64.DEFAULT);
+ builder.addServiceData(parcelUuid, data);
+ }
+ }
+ }
+ if (jsonObject.has("ManufacturerData")) {
+ JSONObject manufacturerData = jsonObject.getJSONObject("ManufacturerData");
+ int manufacturerId = manufacturerData.getInt("ManufacturerId");
+ byte[] manufacturerSpecificData =
+ Base64.decode(jsonObject.getString("ManufacturerSpecificData"), Base64.DEFAULT);
+ builder.addManufacturerData(manufacturerId, manufacturerSpecificData);
+ }
+ return builder.build();
+ }
}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
index bd00f45..c555b92 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
@@ -16,12 +16,16 @@
package com.google.android.mobly.snippet.bundled.utils;
+import android.annotation.TargetApi;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.ScanRecord;
import android.net.DhcpInfo;
import android.net.wifi.SupplicantState;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.os.Build;
+import android.os.Bundle;
import android.os.ParcelUuid;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -65,9 +69,7 @@ public class JsonSerializer {
}
public JSONObject toJson(Object object) throws JSONException {
- if (object instanceof BluetoothDevice) {
- return serializeBluetoothDevice((BluetoothDevice) object);
- } else if (object instanceof DhcpInfo) {
+ if (object instanceof DhcpInfo) {
return serializeDhcpInfo((DhcpInfo) object);
} else if (object instanceof WifiConfiguration) {
return serializeWifiConfiguration((WifiConfiguration) object);
@@ -109,13 +111,13 @@ public class JsonSerializer {
private JSONObject serializeWifiConfiguration(WifiConfiguration data) throws JSONException {
JSONObject result = new JSONObject(mGson.toJson(data));
result.put("Status", WifiConfiguration.Status.strings[data.status]);
- guaranteedPut(result, "SSID", trimQuotationMarks(data.SSID));
+ result.put("SSID", trimQuotationMarks(data.SSID));
return result;
}
private JSONObject serializeWifiInfo(WifiInfo data) throws JSONException {
JSONObject result = new JSONObject(mGson.toJson(data));
- guaranteedPut(result, "SSID", trimQuotationMarks(data.getSSID()));
+ result.put("SSID", trimQuotationMarks(data.getSSID()));
for (SupplicantState state : SupplicantState.values()) {
if (data.getSupplicantState().equals(state)) {
result.put("SupplicantState", state.name());
@@ -124,71 +126,71 @@ public class JsonSerializer {
return result;
}
- private JSONObject serializeBluetoothDevice(BluetoothDevice data) throws JSONException {
- JSONObject result = new JSONObject();
- guaranteedPut(result, "Address", data.getAddress());
- final String bondStateFieldName = "BondState";
- switch (data.getBondState()) {
- case BluetoothDevice.BOND_NONE:
- result.put(bondStateFieldName, "BOND_NONE");
- break;
- case BluetoothDevice.BOND_BONDING:
- result.put(bondStateFieldName, "BOND_BONDING");
- break;
- case BluetoothDevice.BOND_BONDED:
- result.put(bondStateFieldName, "BOND_BONDED");
- break;
- }
- guaranteedPut(result, "Name", data.getName());
+ public Bundle serializeBluetoothDevice(BluetoothDevice data) {
+ Bundle result = new Bundle();
+ result.putString("Address", data.getAddress());
+ final String bondState =
+ MbsEnums.BLUETOOTH_DEVICE_BOND_STATE.getString(data.getBondState());
+ result.putString("BondState", bondState);
+ result.putString("Name", data.getName());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- final String deviceTypeFieldName = "DeviceType";
- switch (data.getType()) {
- case BluetoothDevice.DEVICE_TYPE_CLASSIC:
- result.put(deviceTypeFieldName, "DEVICE_TYPE_CLASSIC");
- break;
- case BluetoothDevice.DEVICE_TYPE_LE:
- result.put(deviceTypeFieldName, "DEVICE_TYPE_LE");
- break;
- case BluetoothDevice.DEVICE_TYPE_DUAL:
- result.put(deviceTypeFieldName, "DEVICE_TYPE_DUAL");
- break;
- case BluetoothDevice.DEVICE_TYPE_UNKNOWN:
- result.put(deviceTypeFieldName, "DEVICE_TYPE_UNKNOWN");
- break;
- }
+ String deviceType = MbsEnums.BLUETOOTH_DEVICE_TYPE.getString(data.getType());
+ result.putString("DeviceType", deviceType);
ParcelUuid[] parcelUuids = data.getUuids();
if (parcelUuids != null) {
ArrayList<String> uuidStrings = new ArrayList<>(parcelUuids.length);
for (ParcelUuid parcelUuid : parcelUuids) {
uuidStrings.add(parcelUuid.getUuid().toString());
}
- result.put("UUIDs", uuidStrings);
+ result.putStringArrayList("UUIDs", uuidStrings);
}
}
return result;
}
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public Bundle serializeBleScanResult(android.bluetooth.le.ScanResult scanResult) {
+ Bundle result = new Bundle();
+ result.putBundle("Device", serializeBluetoothDevice(scanResult.getDevice()));
+ result.putInt("Rssi", scanResult.getRssi());
+ result.putBundle("ScanRecord", serializeBleScanRecord(scanResult.getScanRecord()));
+ result.putLong("TimestampNanos", scanResult.getTimestampNanos());
+ return result;
+ }
+
/**
- * Guarantees a field is put into a JSONObject even if it's null.
+ * Serialize ScanRecord for Bluetooth LE.
*
- * <p>By default, if the object of {@link JSONObject#put(String, Object)} is null, the `put`
- * method would either remove the field or do nothing, causing serialized objects to have
- * inconsistent fields.
+ * <p>Not all fields are serialized here. Will add more as we need.
*
- * <p>Use this method to put objects that may be null into the serialized JSONObject so the
- * serialized objects have a consistent set of critical fields, like the SSID field in
- * serialized WifiConfiguration objects.
+ * <pre>The returned {@link Bundle} has the following info:
+ * "DeviceName", String
+ * "TxPowerLevel", String
+ * </pre>
*
- * @param data
- * @param name
- * @param object
- * @throws JSONException
+ * @param record A {@link ScanRecord} object.
+ * @return A {@link Bundle} object.
*/
- private void guaranteedPut(JSONObject data, String name, Object object) throws JSONException {
- if (object == null) {
- data.put(name, JSONObject.NULL);
- } else {
- data.put(name, object);
- }
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private Bundle serializeBleScanRecord(ScanRecord record) {
+ Bundle result = new Bundle();
+ result.putString("DeviceName", record.getDeviceName());
+ result.putString(
+ "TxPowerLevel",
+ MbsEnums.BLE_ADVERTISE_TX_POWER.getString(record.getTxPowerLevel()));
+ return result;
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static Bundle serializeBleAdvertisingSettings(AdvertiseSettings advertiseSettings) {
+ Bundle result = new Bundle();
+ result.putString(
+ "TxPowerLevel",
+ MbsEnums.BLE_ADVERTISE_TX_POWER.getString(advertiseSettings.getTxPowerLevel()));
+ result.putString(
+ "Mode", MbsEnums.BLE_ADVERTISE_MODE.getString(advertiseSettings.getMode()));
+ result.putInt("Timeout", advertiseSettings.getTimeout());
+ result.putBoolean("IsConnectable", advertiseSettings.isConnectable());
+ return result;
}
}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java
new file mode 100644
index 0000000..33c425c
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java
@@ -0,0 +1,91 @@
+package com.google.android.mobly.snippet.bundled.utils;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanSettings;
+import android.os.Build;
+
+/** Mobly Bundled Snippets (MBS)'s {@link RpcEnum} objects representing enums in Android APIs. */
+public class MbsEnums {
+ static final RpcEnum BLE_ADVERTISE_MODE = buildBleAdvertiseModeEnum();
+ static final RpcEnum BLE_ADVERTISE_TX_POWER = buildBleAdvertiseTxPowerEnum();
+ public static final RpcEnum BLE_SCAN_FAILED_ERROR_CODE = buildBleScanFailedErrorCodeEnum();
+ public static final RpcEnum BLE_SCAN_RESULT_CALLBACK_TYPE =
+ buildBleScanResultCallbackTypeEnum();
+ static final RpcEnum BLUETOOTH_DEVICE_BOND_STATE = buildBluetoothDeviceBondState();
+ static final RpcEnum BLUETOOTH_DEVICE_TYPE = buildBluetoothDeviceTypeEnum();
+
+ private static RpcEnum buildBluetoothDeviceBondState() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ return builder.add("BOND_NONE", BluetoothDevice.BOND_NONE)
+ .add("BOND_BONDING", BluetoothDevice.BOND_BONDING)
+ .add("BOND_BONDED", BluetoothDevice.BOND_BONDED)
+ .build();
+ }
+
+ private static RpcEnum buildBluetoothDeviceTypeEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ return builder.build();
+ }
+ return builder.add("DEVICE_TYPE_CLASSIC", BluetoothDevice.DEVICE_TYPE_CLASSIC)
+ .add("DEVICE_TYPE_LE", BluetoothDevice.DEVICE_TYPE_LE)
+ .add("DEVICE_TYPE_DUAL", BluetoothDevice.DEVICE_TYPE_DUAL)
+ .build();
+ }
+
+ private static RpcEnum buildBleAdvertiseTxPowerEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ return builder.add(
+ "ADVERTISE_TX_POWER_ULTRA_LOW",
+ AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW)
+ .add("ADVERTISE_TX_POWER_LOW", AdvertiseSettings.ADVERTISE_TX_POWER_LOW)
+ .add("ADVERTISE_TX_POWER_MEDIUM", AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
+ .add("ADVERTISE_TX_POWER_HIGH", AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
+ .build();
+ }
+
+ private static RpcEnum buildBleAdvertiseModeEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ return builder.add("ADVERTISE_MODE_BALANCED", AdvertiseSettings.ADVERTISE_MODE_BALANCED)
+ .add("ADVERTISE_MODE_LOW_LATENCY", AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
+ .add("ADVERTISE_MODE_LOW_POWER", AdvertiseSettings.ADVERTISE_MODE_LOW_POWER)
+ .build();
+ }
+
+ private static RpcEnum buildBleScanFailedErrorCodeEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ return builder.add("SCAN_FAILED_ALREADY_STARTED", ScanCallback.SCAN_FAILED_ALREADY_STARTED)
+ .add(
+ "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED",
+ ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED)
+ .add(
+ "SCAN_FAILED_FEATURE_UNSUPPORTED",
+ ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED)
+ .add("SCAN_FAILED_INTERNAL_ERROR", ScanCallback.SCAN_FAILED_INTERNAL_ERROR)
+ .build();
+ }
+
+ private static RpcEnum buildBleScanResultCallbackTypeEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ builder.add("CALLBACK_TYPE_ALL_MATCHES", ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ builder.add("CALLBACK_TYPE_FIRST_MATCH", ScanSettings.CALLBACK_TYPE_FIRST_MATCH);
+ builder.add("CALLBACK_TYPE_MATCH_LOST", ScanSettings.CALLBACK_TYPE_MATCH_LOST);
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java
new file mode 100644
index 0000000..d3d95ae
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.bundled.utils;
+
+import com.google.common.collect.ImmutableBiMap;
+
+/**
+ * A container type for handling String-Integer enum conversion in Rpc protocol.
+ *
+ * <p>In Serializing/Deserializing Android API enums, we often need to convert an enum value from
+ * one form to another. This container class makes it easier to do so.
+ *
+ * <p>Once built, an RpcEnum object is immutable.
+ */
+public class RpcEnum {
+ private final ImmutableBiMap<String, Integer> mEnums;
+
+ private RpcEnum(ImmutableBiMap.Builder<String, Integer> builder, int minSdk) {
+ mEnums = builder.build();
+ }
+
+ /**
+ * Get the int value of an enum based on its String value.
+ *
+ * @param enumString
+ * @return
+ */
+ public int getInt(String enumString) {
+ Integer result = mEnums.get(enumString);
+ if (result == null) {
+ throw new NoSuchFieldError("No int value found for: " + enumString);
+ }
+ return result;
+ }
+
+ /**
+ * Get the String value of an enum based on its int value.
+ *
+ * @param enumInt
+ * @return
+ */
+ public String getString(int enumInt) {
+ String result = mEnums.inverse().get(enumInt);
+ if (result == null) {
+ throw new NoSuchFieldError("No String value found for: " + enumInt);
+ }
+ return result;
+ }
+
+ /** Builder for RpcEnum. */
+ public static class Builder {
+ private final ImmutableBiMap.Builder<String, Integer> builder;
+ public int minSdk = 0;
+
+ public Builder() {
+ builder = new ImmutableBiMap.Builder<>();
+ }
+
+ /**
+ * Add an enum String-Integer pair.
+ *
+ * @param enumString
+ * @param enumInt
+ * @return
+ */
+ public Builder add(String enumString, int enumInt) {
+ builder.put(enumString, enumInt);
+ return this;
+ }
+
+ public RpcEnum build() {
+ return new RpcEnum(builder, minSdk);
+ }
+ }
+}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java
index 8736d85..8f4f7d6 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java
@@ -22,6 +22,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public final class Utils {
+
private Utils() {}
/**