diff options
Diffstat (limited to 'src/main/java/com/google/android/mobly/snippet/bundled/utils')
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] -> "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); + } +} |