diff options
author | Alexander Dorokhine <adorokhine@google.com> | 2017-05-02 19:34:59 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-02 19:34:59 -0700 |
commit | 796a97541ca681478b707c0b9e535e0d201496ac (patch) | |
tree | 9329f582913f12c356a2f694a469bff1162a95b3 /src/main/java/com | |
parent | fec0d88ea2860ef5b595d6f04481cec963350a1d (diff) | |
download | mobly-bundled-snippets-796a97541ca681478b707c0b9e535e0d201496ac.tar.gz |
Simplify API required to call methods by reflection. (#45)
* Implement a cleaner way to call methods by reflection.
* Port all callers to the new reflection API.
Diffstat (limited to 'src/main/java/com')
4 files changed, 115 insertions, 99 deletions
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index 1ca96d0..4f6b0ae 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -78,7 +78,7 @@ public class AudioSnippet implements Snippet { * calling muteAll will throw. */ Class<?> audioSystem = Class.forName("android.media.AudioSystem"); Method getNumStreamTypes = audioSystem.getDeclaredMethod("getNumStreamTypes"); - int numStreams = (int) getNumStreamTypes.invoke(null); + int numStreams = (int) getNumStreamTypes.invoke(null /* instance */); for (int i = 0; i < numStreams; i++) { mAudioManager.setStreamVolume(i /* audio stream */, 0 /* value */, 0 /* flags */); } 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 7a0a15e..6946216 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 @@ -29,7 +29,6 @@ import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; import com.google.android.mobly.snippet.bundled.utils.Utils; import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.rpc.RpcMinSdk; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import org.json.JSONArray; import org.json.JSONException; @@ -146,42 +145,24 @@ public class BluetoothAdapterSnippet implements Snippet { throw new BluetoothAdapterSnippetException( "Bluetooth is not enabled, cannot become discoverable."); } - boolean success; - try { - success = - (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("setScanMode", int.class, int.class) - .invoke( - mBluetoothAdapter, - BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, - duration); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, + duration)) { throw new BluetoothAdapterSnippetException("Failed to become discoverable."); } } @Rpc(description = "Stop being discoverable in Bluetooth.") public void btStopBeingDiscoverable() throws Throwable { - boolean success; - try { - success = - (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("setScanMode", int.class, int.class) - .invoke( - mBluetoothAdapter, - BluetoothAdapter.SCAN_MODE_NONE, - 0 /* duration is not used for this */); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_NONE, + 0 /* duration is not used for this */)) { throw new BluetoothAdapterSnippetException("Failed to stop being discoverable."); } } @@ -206,18 +187,7 @@ public class BluetoothAdapterSnippet implements Snippet { @RpcMinSdk(Build.VERSION_CODES.KITKAT) @Rpc(description = "Enable Bluetooth HCI snoop log for debugging.") public void btEnableHciSnoopLog() throws Throwable { - boolean success; - try { - success = - (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("configHciSnoopLog", boolean.class) - .invoke(mBluetoothAdapter, true); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", true)) { throw new BluetoothAdapterSnippetException("Failed to enable HCI snoop log."); } } @@ -225,18 +195,7 @@ public class BluetoothAdapterSnippet implements Snippet { @RpcMinSdk(Build.VERSION_CODES.KITKAT) @Rpc(description = "Disable Bluetooth HCI snoop log.") public void btDisableHciSnoopLog() throws Throwable { - boolean success; - try { - success = - (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("configHciSnoopLog", boolean.class) - .invoke(mBluetoothAdapter, false); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", false)) { throw new BluetoothAdapterSnippetException("Failed to disable HCI snoop log."); } } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java index bc57ab0..f2ac9eb 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -31,7 +31,6 @@ import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; import com.google.android.mobly.snippet.bundled.utils.Utils; import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.util.Log; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import org.json.JSONArray; import org.json.JSONException; @@ -243,15 +242,7 @@ public class WifiManagerSnippet implements Snippet { @Rpc(description = "Check whether Wi-Fi Soft AP (hotspot) is enabled.") public boolean wifiIsApEnabled() throws Throwable { - try { - return (boolean) - mWifiManager - .getClass() - .getDeclaredMethod("isWifiApEnabled") - .invoke(mWifiManager); - } catch (InvocationTargetException e) { - throw e.getCause(); - } + return (boolean) Utils.invokeByReflection(mWifiManager, "isWifiApEnabled"); } /** @@ -270,21 +261,9 @@ public class WifiManagerSnippet implements Snippet { // WifiConfiguration.SSID literally, unlike the WifiManager connection logic. wifiConfiguration.SSID = JsonSerializer.trimQuotationMarks(wifiConfiguration.SSID); } - boolean success; - try { - success = - (boolean) - mWifiManager - .getClass() - .getDeclaredMethod( - "setWifiApEnabled", - WifiConfiguration.class, - boolean.class) - .invoke(mWifiManager, wifiConfiguration, true); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) + Utils.invokeByReflection( + mWifiManager, "setWifiApEnabled", wifiConfiguration, true)) { throw new WifiManagerSnippetException("Failed to initiate turning on Wi-Fi Soft AP."); } if (!Utils.waitUntil(() -> wifiIsApEnabled() == true, 60)) { @@ -297,24 +276,12 @@ public class WifiManagerSnippet implements Snippet { /** Disables Wi-Fi Soft AP (hotspot). */ @Rpc(description = "Disable Wi-Fi Soft AP (hotspot).") public void wifiDisableSoftAp() throws Throwable { - boolean success; - try { - success = - (boolean) - mWifiManager - .getClass() - .getDeclaredMethod( - "setWifiApEnabled", - WifiConfiguration.class, - boolean.class) - .invoke( - mWifiManager, - null, /* No configuration needed for disabling */ - false); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) + Utils.invokeByReflection( + mWifiManager, + "setWifiApEnabled", + null /* No configuration needed for disabling */, + false)) { throw new WifiManagerSnippetException("Failed to initiate turning off Wi-Fi Soft AP."); } if (!Utils.waitUntil(() -> wifiIsApEnabled() == false, 60)) { 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 0ea5313..8736d85 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 @@ -16,6 +16,11 @@ package com.google.android.mobly.snippet.bundled.utils; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + public final class Utils { private Utils() {} @@ -56,4 +61,89 @@ public final class Utils { 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(); + } + } } |