Skip to content

Commit

Permalink
Replace looseSignature with ClassName
Browse files Browse the repository at this point in the history
Signed-off-by: utzcoz <utzcoz@outlook.com>
  • Loading branch information
utzcoz committed Nov 19, 2022
1 parent 3263174 commit f368cb4
Show file tree
Hide file tree
Showing 34 changed files with 272 additions and 153 deletions.
@@ -0,0 +1,28 @@
package org.robolectric.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicate a real class name of method's input parameter.
*
* <p>For some important Android framework's shadow class, we might bring new APIs to current shadow
* class, but these APIs might be added from newer SDK version, and it will cause compiling error
* when using these shadow classes with lower compileSdk. We can use this annotation and Object type
* to avoid compiling error but with implicit type checking.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface ClassName {
/**
* The class name intended for this parameter.
*
* <p>Use the value as returned from {@link Class#getName()}, not {@link
* Class#getCanonicalName()}; e.g. {@code Foo$Bar} instead of {@code Foo.Bar}.
*/
String value() default "";
}
Expand Up @@ -8,6 +8,7 @@
import static org.robolectric.util.reflector.Reflector.reflector;

import com.google.auto.service.AutoService;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
Expand All @@ -21,6 +22,7 @@
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Priority;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
import org.robolectric.sandbox.ShadowMatcher;
Expand Down Expand Up @@ -291,9 +293,16 @@ private Method findShadowMethod(
Class<?> shadowClass) {
Method method = findShadowMethodDeclaredOnClass(shadowClass, name, types);

if (method == null && shadowInfo.looseSignatures) {
Class<?>[] genericTypes = MethodType.genericMethodType(types.length).parameterArray();
method = findShadowMethodDeclaredOnClass(shadowClass, name, genericTypes);
if (method == null) {
// If user sets looseSignatures for shadow class, we will try to find method with generic
// types again.
if (shadowInfo.looseSignatures) {
Class<?>[] genericTypes = MethodType.genericMethodType(types.length).parameterArray();
method = findShadowMethodDeclaredOnClass(shadowClass, name, genericTypes);
} else {
// Otherwise, we will try to find method with ClassName annotation that can match signature.
method = findShadowMethodWithClassNameDeclaredOnClass(shadowClass, name, types);
}
}

if (method != null) {
Expand All @@ -317,6 +326,76 @@ private Method findShadowMethod(
return method;
}

private ClassName finClassNameAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (ClassName.class.isAssignableFrom(annotation.annotationType())) {
return (ClassName) annotation;
}
}
return null;
}

private boolean hasClassNameAnnotation(Annotation[][] annotations) {
for (Annotation[] parameterAnnotations : annotations) {
for (Annotation annotation : parameterAnnotations) {
if (ClassName.class.isAssignableFrom(annotation.annotationType())) {
return true;
}
}
}
return false;
}

private Method findShadowMethodWithClassNameDeclaredOnClass(
Class<?> shadowClass, String methodName, Class<?>[] paramClasses) {
// We don't process the method without input parameters now.
if (paramClasses == null || paramClasses.length == 0) {
return null;
}
Method[] methods = shadowClass.getDeclaredMethods();
for (Method method : methods) {
if (method == null
|| !method.getName().equals(methodName)
|| method.getParameterCount() != paramClasses.length
|| !isValidShadowMethod(method)) {
continue;
}
Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] allAnnotations = method.getParameterAnnotations();
if (!hasClassNameAnnotation(allAnnotations)) {
continue;
}
boolean matched = true;
for (int i = 0; i < parameterTypes.length; i++) {
// If method's parameter type is superclass of input parameter, we can pass checking for
// this parameter.
if (parameterTypes[i].isAssignableFrom(paramClasses[i])) {
continue;
}
if (allAnnotations.length <= i) {
matched = false;
break;
}
ClassName className = finClassNameAnnotation(allAnnotations[i]);
try {
if (className != null
&& Class.forName(className.value()).isAssignableFrom(paramClasses[i])) {
continue;
}
} catch (ClassNotFoundException ignored) {
// Do nothing
}
matched = false;
break;
}
// TODO identify why above logic will affect __constructor__ without ClassName
if (matched) {
return method;
}
}
return null;
}

private Method findShadowMethodDeclaredOnClass(
Class<?> shadowClass, String methodName, Class<?>[] paramClasses) {
try {
Expand Down
Expand Up @@ -76,7 +76,7 @@
import org.robolectric.util.reflector.WithType;

@SuppressWarnings("NewApi")
@Implements(value = Activity.class, looseSignatures = true)
@Implements(value = Activity.class)
public class ShadowActivity extends ShadowContextThemeWrapper {

@RealObject protected Activity realActivity;
Expand Down
Expand Up @@ -38,7 +38,7 @@
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Reflector;

@Implements(value = ActivityThread.class, isInAndroidSdk = false, looseSignatures = true)
@Implements(value = ActivityThread.class, isInAndroidSdk = false)
public class ShadowActivityThread {
private static ApplicationInfo applicationInfo;
@RealObject protected ActivityThread realActivityThread;
Expand Down
Expand Up @@ -13,7 +13,6 @@
@Implements(
value = AppIntegrityManager.class,
minSdk = R,
looseSignatures = true,
isInAndroidSdk = false)
public class ShadowAppIntegrityManager {

Expand Down
Expand Up @@ -51,6 +51,7 @@
import java.util.Set;
import java.util.stream.IntStream;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
Expand All @@ -61,7 +62,7 @@
import org.robolectric.util.ReflectionHelpers.ClassParameter;

/** Shadow for {@link AppOpsManager}. */
@Implements(value = AppOpsManager.class, minSdk = KITKAT, looseSignatures = true)
@Implements(value = AppOpsManager.class, minSdk = KITKAT)
public class ShadowAppOpsManager {

// OpEntry fields that the shadow doesn't currently allow the test to configure.
Expand Down Expand Up @@ -404,18 +405,18 @@ protected int noteProxyOpNoThrow(
@RequiresApi(api = S)
@Implementation(minSdk = S)
protected int noteProxyOpNoThrow(
Object op, Object attributionSource, Object message, Object ignoredSkipProxyOperation) {
Preconditions.checkArgument(op instanceof Integer);
int op,
@ClassName(value = "android.content.AttributionSource ") Object attributionSource,
String message,
boolean ignoredSkipProxyOperation) {
Preconditions.checkArgument(attributionSource instanceof AttributionSource);
Preconditions.checkArgument(message == null || message instanceof String);
Preconditions.checkArgument(ignoredSkipProxyOperation instanceof Boolean);
AttributionSource castedAttributionSource = (AttributionSource) attributionSource;
return noteProxyOpNoThrow(
(int) op,
op,
castedAttributionSource.getNextPackageName(),
castedAttributionSource.getNextUid(),
castedAttributionSource.getNextAttributionTag(),
(String) message);
message);
}

@Implementation
Expand Down
Expand Up @@ -19,6 +19,7 @@
import java.util.HashMap;
import java.util.Objects;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
Expand All @@ -42,8 +43,7 @@
value = ApkAssets.class,
minSdk = P,
shadowPicker = Picker.class,
isInAndroidSdk = false,
looseSignatures = true)
isInAndroidSdk = false)
public class ShadowArscApkAssets9 extends ShadowApkAssets {
// #define ATRACE_TAG ATRACE_TAG_RESOURCES
//
Expand Down Expand Up @@ -286,11 +286,15 @@ protected static long nativeLoad(

@Implementation(minSdk = R)
protected static Object nativeLoad(
Object format, Object javaPath, Object flags, Object assetsProvider) throws IOException {
boolean system = ((int) flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM;
boolean overlay = ((int) flags & PROPERTY_OVERLAY) == PROPERTY_OVERLAY;
boolean forceSharedLib = ((int) flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC;
return nativeLoad((String) javaPath, system, forceSharedLib, overlay);
int format,
String javaPath,
int flags,
@ClassName(value = "android.content.res.loader.AssetsProvider") Object assetsProvider)
throws IOException {
boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM;
boolean overlay = (flags & PROPERTY_OVERLAY) == PROPERTY_OVERLAY;
boolean forceSharedLib = (flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC;
return nativeLoad(javaPath, system, forceSharedLib, overlay);
}

// static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descriptor,
Expand Down
Expand Up @@ -34,13 +34,14 @@
import java.util.List;
import java.util.Map;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(value = AudioManager.class, looseSignatures = true)
@Implements(value = AudioManager.class)
public class ShadowAudioManager {

public static final int MAX_VOLUME_MUSIC_DTMF = 15;
Expand Down Expand Up @@ -637,7 +638,8 @@ public AudioRecordingConfiguration createActiveRecordingConfiguration(
@HiddenApi
@Implementation(minSdk = P)
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
protected int registerAudioPolicy(@NonNull Object audioPolicy) {
protected int registerAudioPolicy(
@NonNull @ClassName(value = "android.media.audiopolicy.AudioPolicy") Object audioPolicy) {
Preconditions.checkNotNull(audioPolicy, "Illegal null AudioPolicy argument");
AudioPolicy policy = (AudioPolicy) audioPolicy;
String id = getIdForAudioPolicy(audioPolicy);
Expand All @@ -651,7 +653,8 @@ protected int registerAudioPolicy(@NonNull Object audioPolicy) {

@HiddenApi
@Implementation(minSdk = Q)
protected void unregisterAudioPolicy(@NonNull Object audioPolicy) {
protected void unregisterAudioPolicy(
@NonNull @ClassName(value = "android.media.audiopolicy.AudioPolicy") Object audioPolicy) {
Preconditions.checkNotNull(audioPolicy, "Illegal null AudioPolicy argument");
AudioPolicy policy = (AudioPolicy) audioPolicy;
registeredAudioPolicies.remove(getIdForAudioPolicy(policy));
Expand Down
Expand Up @@ -26,7 +26,7 @@
* other methods are expected run through the real class. The two {@link WriteMode} are treated the
* same.
*/
@Implements(value = AudioTrack.class, looseSignatures = true)
@Implements(value = AudioTrack.class)
public class ShadowAudioTrack {

/**
Expand Down
Expand Up @@ -42,6 +42,7 @@
import java.util.concurrent.ConcurrentMap;
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
Expand All @@ -54,7 +55,7 @@
import org.robolectric.util.reflector.Static;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(value = BluetoothAdapter.class, looseSignatures = true)
@Implements(value = BluetoothAdapter.class)
public class ShadowBluetoothAdapter {
@RealObject private BluetoothAdapter realAdapter;

Expand Down Expand Up @@ -121,9 +122,9 @@ protected static BluetoothAdapter getDefaultAdapter() {
return reflector(BluetoothAdapterReflector.class).getDefaultAdapter();
}

/** Requires LooseSignatures because of {@link AttributionSource} parameter */
@Implementation(minSdk = VERSION_CODES.TIRAMISU)
protected static Object createAdapter(Object attributionSource) {
protected static Object createAdapter(
@ClassName(value = "android.content.AttributionSource") Object attributionSource) {
IBluetoothManager service = ReflectionHelpers.createNullProxy(IBluetoothManager.class);
return ReflectionHelpers.callConstructor(
BluetoothAdapter.class,
Expand Down Expand Up @@ -319,10 +320,7 @@ protected boolean setName(String name) {
return true;
}

/**
* Needs looseSignatures because in Android T the return value of this method was changed from
* bool to int.
*/
/** T return value changed from {@code int} to {@link Duration} starting in T. */
@Implementation
protected Object setScanMode(int scanMode) {
boolean result = true;
Expand Down Expand Up @@ -358,10 +356,7 @@ protected int getScanMode() {
return scanMode;
}

/**
* Needs looseSignatures because the return value changed from {@code int} to {@link Duration}
* starting in T.
*/
/** In Android T the return value of this method was changed from bool to int. */
@Implementation
protected Object getDiscoverableTimeout() {
if (RuntimeEnvironment.getApiLevel() <= S_V2) {
Expand Down
Expand Up @@ -23,6 +23,7 @@
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.Bootstrap;
import org.robolectric.android.internal.DisplayConfig;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
Expand All @@ -37,7 +38,7 @@
* For tests, display properties may be changed and devices may be added or removed
* programmatically.
*/
@Implements(value = DisplayManager.class, minSdk = JELLY_BEAN_MR1, looseSignatures = true)
@Implements(value = DisplayManager.class, minSdk = JELLY_BEAN_MR1)
public class ShadowDisplayManager {

@RealObject private DisplayManager realDisplayManager;
Expand Down Expand Up @@ -249,14 +250,17 @@ public void setSaturationLevel(float level) {

@Implementation(minSdk = P)
@HiddenApi
protected void setBrightnessConfiguration(Object config) {
protected void setBrightnessConfiguration(
@ClassName(value = "android.hardware.display.BrightnessConfiguration") Object config) {
setBrightnessConfigurationForUser(config, 0, context.getPackageName());
}

@Implementation(minSdk = P)
@HiddenApi
protected void setBrightnessConfigurationForUser(
Object config, Object userId, Object packageName) {
@ClassName(value = "android.hardware.display.BrightnessConfiguration") Object config,
int userId,
String packageName) {
getShadowDisplayManagerGlobal().setBrightnessConfigurationForUser(config, userId, packageName);
}

Expand Down
Expand Up @@ -40,6 +40,7 @@ protected static long nCreateProxy(
// need to use loose signatures here to account for signature changes
@Implementation(minSdk = S)
protected static long nCreateProxy(Object translucent, Object rootRenderNode) {
// TODO Find an approach to support the same method with disconnected range
return nCreateProxy((boolean) translucent, (long) rootRenderNode);
}

Expand Down
Expand Up @@ -36,7 +36,6 @@
@Implements(
value = ImsMmTelManager.class,
minSdk = VERSION_CODES.Q,
looseSignatures = true,
isInAndroidSdk = false)
@SystemApi
public class ShadowImsMmTelManager {
Expand Down

0 comments on commit f368cb4

Please sign in to comment.