aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/android/mobly/snippet/bundled/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/android/mobly/snippet/bundled/utils')
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java104
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java205
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java92
-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.java213
5 files changed, 703 insertions, 0 deletions
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
new file mode 100644
index 0000000..2f943e0
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java
@@ -0,0 +1,104 @@
+/*
+ * 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 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;
+
+/**
+ * A collection of methods used to deserialize JSON strings into data objects defined in Android
+ * API.
+ */
+public class JsonDeserializer {
+
+ private JsonDeserializer() {}
+
+ public static WifiConfiguration jsonToWifiConfig(JSONObject jsonObject) throws JSONException {
+ WifiConfiguration config = new WifiConfiguration();
+ config.SSID = "\"" + jsonObject.getString("SSID") + "\"";
+ config.hiddenSSID = jsonObject.optBoolean("hiddenSSID", false);
+ if (jsonObject.has("password")) {
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+ config.preSharedKey = "\"" + jsonObject.getString("password") + "\"";
+ } else {
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ }
+ 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
new file mode 100644
index 0000000..82e1e4f
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
@@ -0,0 +1,205 @@
+/*
+ * 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 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;
+import java.lang.reflect.Modifier;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A collection of methods used to serialize data types defined in Android API into JSON strings.
+ */
+public class JsonSerializer {
+ private static Gson mGson;
+
+ public JsonSerializer() {
+ GsonBuilder builder = new GsonBuilder();
+ mGson =
+ builder.serializeNulls()
+ .excludeFieldsWithModifiers(Modifier.STATIC)
+ .enableComplexMapKeySerialization()
+ .disableInnerClassSerialization()
+ .create();
+ }
+
+ /**
+ * Remove the extra quotation marks from the beginning and the end of a string.
+ *
+ * <p>This is useful for strings like the SSID field of Android's Wi-Fi configuration.
+ *
+ * @param originalString
+ */
+ public static String trimQuotationMarks(String originalString) {
+ String result = originalString;
+ if (originalString.length() > 2
+ && originalString.charAt(0) == '"'
+ && originalString.charAt(originalString.length() - 1) == '"') {
+ result = originalString.substring(1, originalString.length() - 1);
+ }
+ return result;
+ }
+
+ public JSONObject toJson(Object object) throws JSONException {
+ if (object instanceof DhcpInfo) {
+ return serializeDhcpInfo((DhcpInfo) object);
+ } else if (object instanceof WifiConfiguration) {
+ return serializeWifiConfiguration((WifiConfiguration) object);
+ } else if (object instanceof WifiInfo) {
+ return serializeWifiInfo((WifiInfo) object);
+ }
+ return defaultSerialization(object);
+ }
+
+ /**
+ * By default, we rely on Gson to do the right job.
+ *
+ * @param data An object to serialize
+ * @return A JSONObject that has the info of the serialized data object.
+ * @throws JSONException
+ */
+ private JSONObject defaultSerialization(Object data) throws JSONException {
+ return new JSONObject(mGson.toJson(data));
+ }
+
+ private JSONObject serializeDhcpInfo(DhcpInfo data) throws JSONException {
+ JSONObject result = new JSONObject(mGson.toJson(data));
+ int ipAddress = data.ipAddress;
+ byte[] addressBytes = {
+ (byte) (0xff & ipAddress),
+ (byte) (0xff & (ipAddress >> 8)),
+ (byte) (0xff & (ipAddress >> 16)),
+ (byte) (0xff & (ipAddress >> 24))
+ };
+ try {
+ String addressString = InetAddress.getByAddress(addressBytes).toString();
+ result.put("IpAddress", addressString);
+ } catch (UnknownHostException e) {
+ result.put("IpAddress", ipAddress);
+ }
+ return result;
+ }
+
+ private JSONObject serializeWifiConfiguration(WifiConfiguration data) throws JSONException {
+ JSONObject result = new JSONObject(mGson.toJson(data));
+ result.put("Status", WifiConfiguration.Status.strings[data.status]);
+ result.put("SSID", trimQuotationMarks(data.SSID));
+ return result;
+ }
+
+ private JSONObject serializeWifiInfo(WifiInfo data) throws JSONException {
+ JSONObject result = new JSONObject(mGson.toJson(data));
+ result.put("SSID", trimQuotationMarks(data.getSSID()));
+ for (SupplicantState state : SupplicantState.values()) {
+ if (data.getSupplicantState().equals(state)) {
+ result.put("SupplicantState", state.name());
+ }
+ }
+ return result;
+ }
+
+ 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) {
+ 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.putStringArrayList("UUIDs", uuidStrings);
+ }
+ }
+ return result;
+ }
+
+ public ArrayList<Bundle> serializeBluetoothDeviceList(
+ Collection<BluetoothDevice> bluetoothDevices) {
+ ArrayList<Bundle> results = new ArrayList<>();
+ for (BluetoothDevice device : bluetoothDevices) {
+ results.add(serializeBluetoothDevice(device));
+ }
+ return results;
+ }
+
+ @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;
+ }
+
+ /**
+ * Serialize ScanRecord for Bluetooth LE.
+ *
+ * <p>Not all fields are serialized here. Will add more as we need.
+ *
+ * <pre>The returned {@link Bundle} has the following info:
+ * "DeviceName", String
+ * "TxPowerLevel", String
+ * </pre>
+ *
+ * @param record A {@link ScanRecord} object.
+ * @return A {@link Bundle} object.
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private Bundle serializeBleScanRecord(ScanRecord record) {
+ Bundle result = new Bundle();
+ result.putString("DeviceName", record.getDeviceName());
+ result.putInt("TxPowerLevel", 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..08163b4
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java
@@ -0,0 +1,92 @@
+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)
+ .add("DEVICE_TYPE_UNKNOWN", BluetoothDevice.DEVICE_TYPE_UNKNOWN)
+ .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
new file mode 100644
index 0000000..376bcb5
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java
@@ -0,0 +1,213 @@
+/*
+ * 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.android.mobly.snippet.bundled.SmsSnippet;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.common.primitives.Primitives;
+import com.google.common.reflect.TypeToken;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Locale;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public final class Utils {
+
+ private static final char[] hexArray = "0123456789abcdef".toCharArray();
+
+ private Utils() {}
+
+ /**
+ * Waits util a condition is met.
+ *
+ * <p>This is often used to wait for asynchronous operations to finish and the system to reach a
+ * desired state.
+ *
+ * <p>If the predicate function throws an exception and interrupts the waiting, the exception
+ * will be wrapped in an {@link RuntimeException}.
+ *
+ * @param predicate A lambda function that specifies the condition to wait for. This function
+ * should return true when the desired state has been reached.
+ * @param timeout The number of seconds to wait for before giving up.
+ * @return true if the operation finished before timeout, false otherwise.
+ */
+ public static boolean waitUntil(Utils.Predicate predicate, int timeout) {
+ timeout *= 10;
+ try {
+ while (!predicate.waitCondition() && timeout >= 0) {
+ Thread.sleep(100);
+ timeout -= 1;
+ }
+ if (predicate.waitCondition()) {
+ return true;
+ }
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ return false;
+ }
+
+ /**
+ * Wait on a specific snippet event.
+ *
+ * <p>This allows a snippet to wait on another SnippetEvent as long as they know the name and
+ * callback id. Commonly used to make async calls synchronous, see {@link
+ * SmsSnippet#waitForSms()} waitForSms} for example usage.
+ *
+ * @param callbackId String callbackId that we want to wait on.
+ * @param eventName String event name that we are waiting on.
+ * @param timeout int timeout in milliseconds for how long it will wait for the event.
+ * @return SnippetEvent if one was received.
+ * @throws Throwable if interrupted while polling for event completion. Throws TimeoutException
+ * if no snippet event is received.
+ */
+ public static SnippetEvent waitForSnippetEvent(
+ String callbackId, String eventName, Integer timeout) throws Throwable {
+ String qId = EventCache.getQueueId(callbackId, eventName);
+ LinkedBlockingDeque<SnippetEvent> q = EventCache.getInstance().getEventDeque(qId);
+ SnippetEvent result;
+ try {
+ result = q.pollFirst(timeout, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ throw e.getCause();
+ }
+
+ if (result == null) {
+ throw new TimeoutException(
+ String.format(
+ Locale.ROOT,
+ "Timed out waiting(%d millis) for SnippetEvent: %s",
+ timeout,
+ callbackId));
+ }
+ return result;
+ }
+
+ /**
+ * A function interface that is used by lambda functions signaling an async operation is still
+ * going on.
+ */
+ public interface Predicate {
+ boolean waitCondition() throws Throwable;
+ }
+
+ /**
+ * Simplified API to invoke an instance method by reflection.
+ *
+ * <p>Sample usage:
+ *
+ * <pre>
+ * boolean result = (boolean) Utils.invokeByReflection(
+ * mWifiManager,
+ * "setWifiApEnabled", null /* wifiConfiguration * /, true /* enabled * /);
+ * </pre>
+ *
+ * @param instance Instance of object defining the method to call.
+ * @param methodName Name of the method to call. Can be inherited.
+ * @param args Variadic array of arguments to supply to the method. Their types will be used to
+ * locate a suitable method to call. Subtypes, primitive types, boxed types, and {@code
+ * null} arguments are properly handled.
+ * @return The return value of the method, or {@code null} if no return value.
+ * @throws NoSuchMethodException If no suitable method could be found.
+ * @throws Throwable The exception raised by the method, if any.
+ */
+ public static Object invokeByReflection(Object instance, String methodName, Object... args)
+ throws Throwable {
+ // Java doesn't know if invokeByReflection(instance, name, null) means that the array is
+ // null or that it's a non-null array containing a single null element. We mean the latter.
+ // Silly Java.
+ if (args == null) {
+ args = new Object[] {null};
+ }
+ // Can't use Class#getMethod(Class<?>...) because it expects that the passed in classes
+ // exactly match the parameters of the method, and doesn't handle superclasses.
+ Method method = null;
+ METHOD_SEARCHER:
+ for (Method candidateMethod : instance.getClass().getMethods()) {
+ // getMethods() returns only public methods, so we don't need to worry about checking
+ // whether the method is accessible.
+ if (!candidateMethod.getName().equals(methodName)) {
+ continue;
+ }
+ Class<?>[] declaredParams = candidateMethod.getParameterTypes();
+ if (declaredParams.length != args.length) {
+ continue;
+ }
+ for (int i = 0; i < declaredParams.length; i++) {
+ if (args[i] == null) {
+ // Null is assignable to anything except primitives.
+ if (declaredParams[i].isPrimitive()) {
+ continue METHOD_SEARCHER;
+ }
+ } else {
+ // Allow autoboxing during reflection by wrapping primitives.
+ Class<?> declaredClass = Primitives.wrap(declaredParams[i]);
+ Class<?> actualClass = Primitives.wrap(args[i].getClass());
+ TypeToken<?> declaredParamType = TypeToken.of(declaredClass);
+ TypeToken<?> actualParamType = TypeToken.of(actualClass);
+ if (!declaredParamType.isSupertypeOf(actualParamType)) {
+ continue METHOD_SEARCHER;
+ }
+ }
+ }
+ method = candidateMethod;
+ break;
+ }
+ if (method == null) {
+ StringBuilder methodString =
+ new StringBuilder(instance.getClass().getName())
+ .append('#')
+ .append(methodName)
+ .append('(');
+ for (int i = 0; i < args.length - 1; i++) {
+ methodString.append(args[i].getClass().getSimpleName()).append(", ");
+ }
+ if (args.length > 0) {
+ methodString.append(args[args.length - 1].getClass().getSimpleName());
+ }
+ methodString.append(')');
+ throw new NoSuchMethodException(methodString.toString());
+ }
+ try {
+ Object result = method.invoke(instance, args);
+ return result;
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+
+ /**
+ * Convert a byte array (binary data) to a hexadecimal string (ASCII) representation.
+ *
+ * <p>[\x01\x02] -&gt; "0102"
+ *
+ * @param bytes The array of byte to convert.
+ * @return a String with the ASCII hex representation.
+ */
+ public static String bytesToHexString(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+}