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 | |
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.
6 files changed, 246 insertions, 100 deletions
diff --git a/build.gradle b/build.gradle index c5ef718..028777c 100644 --- a/build.gradle +++ b/build.gradle @@ -54,14 +54,24 @@ task sourcesJar(type: Jar) { classifier = 'src' } +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files( + android.getBootClasspath().join(File.pathSeparator)) +} + artifacts { archives sourcesJar } dependencies { - compile 'com.google.android.mobly:mobly-snippet-lib:1.0.0' compile 'com.android.support.test:runner:0.5' + compile 'com.google.android.mobly:mobly-snippet-lib:1.0.0' compile 'com.google.code.gson:gson:2.6.2' + compile 'com.google.guava:guava:20.0' + + testCompile 'com.google.truth:truth:0.32' + testCompile 'junit:junit:4.12' } googleJavaFormat { 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(); + } + } } diff --git a/src/test/java/UtilsTest.java b/src/test/java/UtilsTest.java new file mode 100644 index 0000000..b9a70d2 --- /dev/null +++ b/src/test/java/UtilsTest.java @@ -0,0 +1,120 @@ +/* + * 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. + */ + +import static com.google.android.mobly.snippet.bundled.utils.Utils.invokeByReflection; + +import com.google.android.mobly.snippet.bundled.utils.Utils; +import com.google.common.truth.Truth; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link com.google.android.mobly.snippet.bundled.utils.Utils} */ +public class UtilsTest { + public static final class ReflectionTest_HostClass { + public Object returnSame(List<String> arg) { + return arg; + } + + public Object returnSame(int arg) { + return arg; + } + + public Object multiArgCall(Object arg1, Object arg2, boolean returnArg1) { + if (returnArg1) { + return arg1; + } + return arg2; + } + + public boolean returnTrue() { + return true; + } + + public void throwsException() throws IOException { + throw new IOException("Example exception"); + } + } + + @Test + public void testInvokeByReflection_Obj() throws Throwable { + List<?> sampleList = Collections.singletonList("sampleList"); + ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); + Object ret = invokeByReflection(hostClass, "returnSame", sampleList); + Truth.assertThat(ret).isSameAs(sampleList); + } + + @Test + public void testInvokeByReflection_Null() throws Throwable { + ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); + Object ret = invokeByReflection(hostClass, "returnSame", (Object) null); + Truth.assertThat(ret).isNull(); + } + + @Test + public void testInvokeByReflection_NoArg() throws Throwable { + ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); + boolean ret = (boolean) invokeByReflection(hostClass, "returnTrue"); + Truth.assertThat(ret).isTrue(); + } + + @Test + public void testInvokeByReflection_Primitive() throws Throwable { + ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); + Object ret = invokeByReflection(hostClass, "returnSame", 5); + Truth.assertThat(ret).isEqualTo(5); + } + + @Test + public void testInvokeByReflection_MultiArg() throws Throwable { + ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); + Object arg1 = new Object(); + Object arg2 = new Object(); + Object ret = + invokeByReflection(hostClass, "multiArgCall", arg1, arg2, true /* returnArg1 */); + Truth.assertThat(ret).isSameAs(arg1); + ret = + Utils.invokeByReflection( + hostClass, "multiArgCall", arg1, arg2, false /* returnArg1 */); + Truth.assertThat(ret).isSameAs(arg2); + } + + @Test + public void testInvokeByReflection_NoMatch() throws Throwable { + ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); + Truth.assertThat(List.class.isAssignableFrom(Object.class)).isFalse(); + try { + invokeByReflection(hostClass, "returnSame", new Object()); + Assert.fail(); + } catch (NoSuchMethodException e) { + Truth.assertThat(e.getMessage()) + .contains("UtilsTest$ReflectionTest_HostClass#returnSame(Object)"); + } + } + + @Test + public void testInvokeByReflection_UnwrapException() throws Throwable { + ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); + try { + invokeByReflection(hostClass, "throwsException"); + Assert.fail(); + } catch (IOException e) { + Truth.assertThat(e.getMessage()).isEqualTo("Example exception"); + } + } +} |