diff --git a/annotations/src/main/java/org/robolectric/annotation/Implementation.java b/annotations/src/main/java/org/robolectric/annotation/Implementation.java
index a041c273cbc..b90a27fb825 100644
--- a/annotations/src/main/java/org/robolectric/annotation/Implementation.java
+++ b/annotations/src/main/java/org/robolectric/annotation/Implementation.java
@@ -21,4 +21,18 @@
/** The annotated shadow method will be invoked only for the specified SDK or lesser. */
int maxSdk() default DEFAULT_SDK;
+
+ /**
+ * The implemented method name.
+ *
+ *
Sometimes internal methods return different types for different SDKs. It's safe because
+ * these methods are internal/private methods, not public methods. To support different return
+ * types of a method for different SDKs, we often use looseSignature method, although all return
+ * types are common types like bool and int. This field/property can be used to fix this issue by
+ * using different real methods for different SDKs.
+ *
+ * @return The expected implemented method name. If it is empty/null, the Robolectric will uses
+ * the method's name that marked by @Implementation as the implemented method name.
+ */
+ String methodName() default "";
}
diff --git a/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java b/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java
index e08b3acd1b7..c7a845c9e97 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/validator/SdkStore.java
@@ -384,7 +384,14 @@ private static String cleanMethodName(ExecutableElement methodElement) {
} else if (STATIC_INITIALIZER_METHOD_NAME.equals(name)) {
return "";
} else {
- return name;
+ Implementation implementation = methodElement.getAnnotation(Implementation.class);
+ String methodName = implementation == null ? "" : implementation.methodName();
+ methodName = methodName == null ? "" : methodName.trim();
+ if (methodName.isEmpty()) {
+ return name;
+ } else {
+ return methodName;
+ }
}
}
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java
index ab480d69a0e..0f18122e5e9 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java
@@ -22,6 +22,7 @@
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Priority;
+import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
import org.robolectric.sandbox.ShadowMatcher;
@@ -308,6 +309,7 @@ private Method findShadowMethod(
Class> shadowClass) {
Method method =
findShadowMethodDeclaredOnClass(shadowClass, name, types, shadowInfo.looseSignatures);
+
if (method != null) {
return method;
} else {
@@ -332,7 +334,9 @@ private Method findShadowMethod(
private Method findShadowMethodDeclaredOnClass(
Class> shadowClass, String methodName, Class>[] paramClasses, boolean looseSignatures) {
Method foundMethod = null;
- for (Method method : shadowClass.getDeclaredMethods()) {
+ // Try to find shadow method with exact method name and looseSignature.
+ Method[] methods = shadowClass.getDeclaredMethods();
+ for (Method method : methods) {
if (!method.getName().equals(methodName)
|| method.getParameterCount() != paramClasses.length) {
continue;
@@ -349,6 +353,7 @@ private Method findShadowMethodDeclaredOnClass(
foundMethod = method;
break;
}
+
if (looseSignatures) {
boolean allParameterTypesAreObject = true;
for (Class> paramClass : method.getParameterTypes()) {
@@ -364,6 +369,24 @@ private Method findShadowMethodDeclaredOnClass(
}
}
+ if (foundMethod == null) {
+ // Try to find shadow method with Implementation#methodName's mapping name
+ for (Method method : methods) {
+ Implementation implementation = method.getAnnotation(Implementation.class);
+ if (implementation == null) {
+ continue;
+ }
+ String mappedMethodName = implementation.methodName().trim();
+ if (mappedMethodName.isEmpty() || !mappedMethodName.equals(methodName)) {
+ continue;
+ }
+ if (Arrays.equals(method.getParameterTypes(), paramClasses)) {
+ foundMethod = method;
+ break;
+ }
+ }
+ }
+
if (foundMethod != null) {
foundMethod.setAccessible(true);
return foundMethod;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
index bff4fe2b772..95846c41483 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
@@ -381,21 +381,21 @@ protected boolean setName(String name) {
* Needs looseSignatures because in Android T the return value of this method was changed from
* bool to int.
*/
- @Implementation
- protected Object setScanMode(int scanMode) {
- boolean result = true;
- if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE
- && scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE
- && scanMode != BluetoothAdapter.SCAN_MODE_NONE) {
- result = false;
- }
-
+ @Implementation(maxSdk = S_V2)
+ protected boolean setScanMode(int scanMode) {
+ boolean result =
+ scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE
+ || scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE
+ || scanMode == BluetoothAdapter.SCAN_MODE_NONE;
this.scanMode = scanMode;
- if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
- return result ? BluetoothStatusCodes.SUCCESS : BluetoothStatusCodes.ERROR_UNKNOWN;
- } else {
- return result;
- }
+ return result;
+ }
+
+ @Implementation(minSdk = TIRAMISU, methodName = "setScanMode")
+ protected int setScanModeFromT(int scanMode) {
+ return setScanMode(scanMode)
+ ? BluetoothStatusCodes.SUCCESS
+ : BluetoothStatusCodes.ERROR_UNKNOWN;
}
@Implementation(maxSdk = Q)