Add a comprehensive test for Notification.visitUris()

This test uses reflection to find every available setter method on objects and builders and calls them (generating parameters recursively where needed). Thus it's a bit slow and possibly brittle (could be broken by new methods anywhere in the object graph that it doesn't figure out how to handle), but it should be comprehensive.

Fixes: 279441526
Test: atest NotificationVisitUrisTest :)
Change-Id: I77036284d497f6eab705d33f743c6aaad7da4d31
This commit is contained in:
Matías Hernández
2023-05-05 15:49:45 +02:00
parent c51d3ed90c
commit 9d86c08bf9

View File

@@ -0,0 +1,778 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* 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.android.server.notification;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertThat;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Person;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.media.session.MediaSession;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.UiServiceTestCase;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.truth.Expect;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
@RunWith(AndroidJUnit4.class)
public class NotificationVisitUrisTest extends UiServiceTestCase {
private static final String TAG = "VisitUrisTest";
// Methods that are known to add Uris that are *NOT* verified.
// This list should be emptied! Items can be removed as bugs are fixed.
private static final Multimap<Class<?>, String> KNOWN_BAD =
ImmutableMultimap.<Class<?>, String>builder()
.put(Notification.Builder.class, "setPublicVersion") // b/276294099
.putAll(RemoteViews.class, "addView", "addStableView") // b/277740082
.put(RemoteViews.class, "setIcon") // b/281018094
.put(Notification.WearableExtender.class, "addAction") // TODO: b/281044385
.put(Person.Builder.class, "setUri") // TODO: b/281044385
.put(RemoteViews.class, "setRemoteAdapter") // TODO: b/281044385
.build();
// Types that we can't really produce. No methods receiving these parameters will be invoked.
private static final ImmutableSet<Class<?>> UNUSABLE_TYPES =
ImmutableSet.of(Consumer.class, IBinder.class, MediaSession.Token.class, Parcel.class,
PrintWriter.class, Resources.Theme.class, View.class);
// Maximum number of times we allow generating the same class recursively.
// E.g. new RemoteViews.addView(new RemoteViews()) but stop there.
private static final int MAX_RECURSION = 2;
// Number of times a method called addX(X) will be called.
private static final int NUM_ADD_CALLS = 2;
// Number of elements to put in a generated array, e.g. for calling setGloops(Gloop[] gloops).
private static final int NUM_ELEMENTS_IN_ARRAY = 3;
// Constructors that should be used to create instances of specific classes. Overrides scoring.
private static final ImmutableMap<Class<?>, Constructor<?>> PREFERRED_CONSTRUCTORS;
static {
try {
PREFERRED_CONSTRUCTORS = ImmutableMap.of(
Notification.Builder.class,
Notification.Builder.class.getConstructor(Context.class, String.class));
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
private static final Multimap<Class<?>, String> EXCLUDED_SETTERS =
ImmutableMultimap.<Class<?>, String>builder()
// Handled by testAllStyles().
.put(Notification.Builder.class, "setStyle")
// Handled by testAllExtenders().
.put(Notification.Builder.class, "extend")
// Handled by testAllActionExtenders().
.put(Notification.Action.Builder.class, "extend")
// Overwrites icon supplied to constructor.
.put(Notification.BubbleMetadata.Builder.class, "setIcon")
// Discards previously-added actions.
.put(RemoteViews.class, "mergeRemoteViews")
.build();
private Context mContext;
@Rule
public final Expect expect = Expect.create();
@Before
public void setUp() {
mContext = InstrumentationRegistry.getInstrumentation().getContext();
}
@Test // This is a meta-test, checks that the generators are not broken.
public void verifyTest() {
Generated<Notification> notification = buildNotification(mContext, /* styleClass= */ null,
/* extenderClass= */ null, /* actionExtenderClass= */ null,
/* includeRemoteViews= */ true);
assertThat(notification.includedUris.size()).isAtLeast(20);
}
@Test
public void testPlainNotification() throws Exception {
Generated<Notification> notification = buildNotification(mContext, /* styleClass= */ null,
/* extenderClass= */ null, /* actionExtenderClass= */ null,
/* includeRemoteViews= */ false);
verifyAllUrisAreVisited(notification.value, notification.includedUris,
"Plain Notification");
}
@Test
public void testRemoteViews() throws Exception {
Generated<Notification> notification = buildNotification(mContext, /* styleClass= */ null,
/* extenderClass= */ null, /* actionExtenderClass= */ null,
/* includeRemoteViews= */ true);
verifyAllUrisAreVisited(notification.value, notification.includedUris,
"Notification with Remote Views");
}
@Test
public void testAllStyles() throws Exception {
for (Class<?> styleClass : ReflectionUtils.getConcreteSubclasses(Notification.Style.class,
Notification.class)) {
Generated<Notification> notification = buildNotification(mContext, styleClass,
/* extenderClass= */ null, /* actionExtenderClass= */ null,
/* includeRemoteViews= */ false);
verifyAllUrisAreVisited(notification.value, notification.includedUris,
String.format("Style (%s)", styleClass.getSimpleName()));
}
}
@Test
public void testAllExtenders() throws Exception {
for (Class<?> extenderClass : ReflectionUtils.getConcreteSubclasses(
Notification.Extender.class, Notification.class)) {
Generated<Notification> notification = buildNotification(mContext,
/* styleClass= */ null, extenderClass, /* actionExtenderClass= */ null,
/* includeRemoteViews= */ false);
verifyAllUrisAreVisited(notification.value, notification.includedUris,
String.format("Extender (%s)", extenderClass.getSimpleName()));
}
}
@Test
public void testAllActionExtenders() throws Exception {
for (Class<?> actionExtenderClass : ReflectionUtils.getConcreteSubclasses(
Notification.Action.Extender.class, Notification.Action.class)) {
Generated<Notification> notification = buildNotification(mContext,
/* styleClass= */ null, /* extenderClass= */ null, actionExtenderClass,
/* includeRemoteViews= */ false);
verifyAllUrisAreVisited(notification.value, notification.includedUris,
String.format("Action.Extender (%s)", actionExtenderClass.getSimpleName()));
}
}
private void verifyAllUrisAreVisited(Notification notification, List<Uri> includedUris,
String notificationTypeMessage) throws Exception {
Consumer<Uri> visitor = (Consumer<Uri>) Mockito.mock(Consumer.class);
ArgumentCaptor<Uri> visitedUriCaptor = ArgumentCaptor.forClass(Uri.class);
notification.visitUris(visitor);
Mockito.verify(visitor, Mockito.atLeastOnce()).accept(visitedUriCaptor.capture());
List<Uri> visitedUris = new ArrayList<>(visitedUriCaptor.getAllValues());
visitedUris.remove(null);
expect.withMessage(notificationTypeMessage)
.that(visitedUris)
.containsAtLeastElementsIn(includedUris);
expect.that(KNOWN_BAD).isNotEmpty(); // Once empty, switch to containsExactlyElementsIn()
}
private static Generated<Notification> buildNotification(Context context,
@Nullable Class<?> styleClass, @Nullable Class<?> extenderClass,
@Nullable Class<?> actionExtenderClass, boolean includeRemoteViews) {
SpecialParameterGenerator specialGenerator = new SpecialParameterGenerator(context);
Set<Class<?>> excludedClasses = includeRemoteViews
? ImmutableSet.of()
: ImmutableSet.of(RemoteViews.class);
Location location = Location.root(Notification.Builder.class);
Notification.Builder builder = new Notification.Builder(context, "channelId");
invokeAllSetters(builder, location, /* allOverloads= */ false,
/* includingVoidMethods= */ false, excludedClasses, specialGenerator);
if (styleClass != null) {
builder.setStyle((Notification.Style) generateObject(styleClass,
location.plus("setStyle", Notification.Style.class),
excludedClasses, specialGenerator));
}
if (extenderClass != null) {
builder.extend((Notification.Extender) generateObject(extenderClass,
location.plus("extend", Notification.Extender.class),
excludedClasses, specialGenerator));
}
if (actionExtenderClass != null) {
Location actionLocation = location.plus("addAction", Notification.Action.class);
Notification.Action.Builder actionBuilder =
(Notification.Action.Builder) generateObject(
Notification.Action.Builder.class, actionLocation, excludedClasses,
specialGenerator);
actionBuilder.extend((Notification.Action.Extender) generateObject(actionExtenderClass,
actionLocation.plus(
Notification.Action.Builder.class).plus("extend",
Notification.Action.Extender.class),
excludedClasses, specialGenerator));
builder.addAction(actionBuilder.build());
}
return new Generated<>(builder.build(), specialGenerator.getGeneratedUris());
}
private static Object generateObject(Class<?> clazz, Location where,
Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
if (excludingClasses.contains(clazz)) {
throw new IllegalArgumentException(
String.format("Asked to generate a %s but it's part of the excluded set (%s)",
clazz, excludingClasses));
}
if (SpecialParameterGenerator.canGenerate(clazz)) {
return specialGenerator.generate(clazz, where);
}
if (clazz.isEnum()) {
return clazz.getEnumConstants()[0];
}
if (clazz.isArray()) {
Object arrayValue = Array.newInstance(clazz.getComponentType(), NUM_ELEMENTS_IN_ARRAY);
for (int i = 0; i < Array.getLength(arrayValue); i++) {
Array.set(arrayValue, i,
generateObject(clazz.getComponentType(), where, excludingClasses,
specialGenerator));
}
return arrayValue;
}
Log.i(TAG, "About to generate a(n)" + clazz.getName());
// Need to construct one of these. Look for a Builder inner class... and also look for a
// Builder as a "sibling" class; CarExtender.UnreadConversation does this :(
Stream<Class<?>> maybeBuilders =
Stream.concat(Arrays.stream(clazz.getDeclaredClasses()),
clazz.getDeclaringClass() != null
? Arrays.stream(clazz.getDeclaringClass().getDeclaredClasses())
: Stream.empty());
Optional<Class<?>> clazzBuilder =
maybeBuilders
.filter(maybeBuilder -> maybeBuilder.getSimpleName().equals("Builder"))
.filter(maybeBuilder ->
Arrays.stream(maybeBuilder.getMethods()).anyMatch(
m -> m.getName().equals("build")
&& m.getParameterCount() == 0
&& m.getReturnType().equals(clazz)))
.findFirst();
if (clazzBuilder.isPresent()) {
try {
// Found a Builder! Create an instance of it, call its setters, and call build()
// on it.
Object builder = constructEmpty(clazzBuilder.get(), where.plus(clazz),
excludingClasses, specialGenerator);
invokeAllSetters(builder, where.plus(clazz).plus(clazzBuilder.get()),
/* allOverloads= */ false, /* includingVoidMethods= */ false,
excludingClasses, specialGenerator);
Method buildMethod = builder.getClass().getMethod("build");
Object built = buildMethod.invoke(builder);
assertThat(built).isInstanceOf(clazz);
return built;
} catch (Exception e) {
throw new UnsupportedOperationException(
"Error using Builder " + clazzBuilder.get().getName(), e);
}
}
// If no X.Builder, look for X() constructor.
try {
Object instance = constructEmpty(clazz, where, excludingClasses, specialGenerator);
invokeAllSetters(instance, where.plus(clazz), /* allOverloads= */ false,
/* includingVoidMethods= */ false, excludingClasses, specialGenerator);
return instance;
} catch (Exception e) {
throw new UnsupportedOperationException("Error generating a(n) " + clazz.getName(), e);
}
}
private static Object constructEmpty(Class<?> clazz, Location where,
Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
Constructor<?> bestConstructor;
if (PREFERRED_CONSTRUCTORS.containsKey(clazz)) {
// Use the preferred constructor.
bestConstructor = PREFERRED_CONSTRUCTORS.get(clazz);
} else if (Notification.Extender.class.isAssignableFrom(clazz)
|| Notification.Action.Extender.class.isAssignableFrom(clazz)) {
// For extenders, prefer the empty constructors. The others are "partial-copy"
// constructors and do not read all fields from the supplied Notification/Action.
try {
bestConstructor = clazz.getConstructor();
} catch (Exception e) {
throw new UnsupportedOperationException(
String.format("Extender class %s doesn't have a zero-parameter constructor",
clazz.getName()));
}
} else {
// Look for a non-deprecated constructor using any of the "interesting" parameters.
List<Constructor<?>> allConstructors = Arrays.stream(clazz.getConstructors())
.filter(c -> c.getAnnotation(Deprecated.class) == null)
.collect(Collectors.toList());
bestConstructor = ReflectionUtils.chooseBestOverload(allConstructors, where);
}
if (bestConstructor != null) {
try {
Object[] constructorParameters = generateParameters(bestConstructor,
where.plus(clazz), excludingClasses, specialGenerator);
Log.i(TAG, "Invoking " + ReflectionUtils.methodToString(bestConstructor) + " with "
+ Arrays.toString(constructorParameters));
return bestConstructor.newInstance(constructorParameters);
} catch (Exception e) {
throw new UnsupportedOperationException(
String.format("Error invoking constructor %s",
ReflectionUtils.methodToString(bestConstructor)), e);
}
}
// Look for a "static constructor", i.e. some factory method on the same class.
List<Method> factoryMethods = Arrays.stream(clazz.getMethods())
.filter(m -> Modifier.isStatic(m.getModifiers()) && clazz.equals(m.getReturnType()))
.collect(Collectors.toList());
Method bestFactoryMethod = ReflectionUtils.chooseBestOverload(factoryMethods, where);
if (bestFactoryMethod != null) {
try {
Object[] methodParameters = generateParameters(bestFactoryMethod, where.plus(clazz),
excludingClasses, specialGenerator);
Log.i(TAG,
"Invoking " + ReflectionUtils.methodToString(bestFactoryMethod) + " with "
+ Arrays.toString(methodParameters));
return bestFactoryMethod.invoke(null, methodParameters);
} catch (Exception e) {
throw new UnsupportedOperationException(
"Error invoking constructor-like static method "
+ bestFactoryMethod.getName() + " for " + clazz.getName(), e);
}
}
throw new UnsupportedOperationException(
"Couldn't find a way to construct a(n) " + clazz.getName());
}
private static void invokeAllSetters(Object instance, Location where, boolean allOverloads,
boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes,
SpecialParameterGenerator specialGenerator) {
for (Method setter : ReflectionUtils.getAllSetters(instance.getClass(), where,
allOverloads, includingVoidMethods, excludingParameterTypes)) {
try {
int numInvocations = setter.getName().startsWith("add") ? NUM_ADD_CALLS : 1;
for (int i = 0; i < numInvocations; i++) {
// If the method is a "known bad" (i.e. adds Uris that aren't visited later)
// then still call it, but don't add to list of generated Uris. Easiest way is
// to use a throw-away SpecialParameterGenerator instead of the accumulating
// one.
SpecialParameterGenerator specialGeneratorForThisSetter =
KNOWN_BAD.containsEntry(instance.getClass(), setter.getName())
? new SpecialParameterGenerator(specialGenerator.mContext)
: specialGenerator;
Object[] setterParam = generateParameters(setter, where,
excludingParameterTypes, specialGeneratorForThisSetter);
Log.i(TAG, "Invoking " + ReflectionUtils.methodToString(setter) + " with "
+ setterParam[0]);
setter.invoke(instance, setterParam);
}
} catch (Exception e) {
throw new UnsupportedOperationException(
"Error invoking setter " + ReflectionUtils.methodToString(setter), e);
}
}
}
private static Object[] generateParameters(Executable executable, Location where,
Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
Log.i(TAG, "About to generate parameters for " + ReflectionUtils.methodToString(executable)
+ " in " + where);
Class<?>[] parameterTypes = executable.getParameterTypes();
Object[] parameterValues = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
parameterValues[i] = generateObject(
parameterTypes[i],
where.plus(executable,
String.format("[%d,%s]", i, parameterTypes[i].getName())),
excludingClasses,
specialGenerator);
}
return parameterValues;
}
private static class ReflectionUtils {
static Set<Class<?>> getConcreteSubclasses(Class<?> clazz, Class<?> containerClass) {
return Arrays.stream(containerClass.getDeclaredClasses())
.filter(
innerClass -> clazz.isAssignableFrom(innerClass)
&& !Modifier.isAbstract(innerClass.getModifiers()))
.collect(Collectors.toSet());
}
static String methodToString(Executable executable) {
return String.format("%s::%s(%s)",
executable.getDeclaringClass().getName(),
executable.getName(),
Arrays.stream(executable.getParameterTypes()).map(Class::getSimpleName)
.collect(Collectors.joining(", "))
);
}
static List<Method> getAllSetters(Class<?> clazz, Location where, boolean allOverloads,
boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes) {
ListMultimap<String, Method> methods = ArrayListMultimap.create();
// Candidate "setters" are any methods that receive one at least parameter and are
// either void (if acceptable) or return the same type being built.
for (Method method : clazz.getDeclaredMethods()) {
if (Modifier.isPublic(method.getModifiers())
&& !Modifier.isStatic(method.getModifiers())
&& method.getAnnotation(Deprecated.class) == null
&& ((includingVoidMethods && method.getReturnType().equals(Void.TYPE))
|| method.getReturnType().equals(clazz))
&& method.getParameterCount() >= 1
&& !EXCLUDED_SETTERS.containsEntry(clazz, method.getName())
&& Arrays.stream(method.getParameterTypes())
.noneMatch(excludingParameterTypes::contains)) {
methods.put(method.getName(), method);
}
}
// In case of overloads, prefer those with the most interesting parameters.
List<Method> setters = new ArrayList<>();
for (String methodName : methods.keySet()) {
setters.addAll(chooseOverloads(methods.get(methodName), where, allOverloads));
}
// Exclude set(x[]) when there exists add(x).
List<Method> excludedSetters = setters.stream().filter(
m1 -> m1.getName().startsWith("set")
&& setters.stream().anyMatch(
m2 -> {
Class<?> param1 = m1.getParameterTypes()[0];
Class<?> param2 = m2.getParameterTypes()[0];
return m2.getName().startsWith("add")
&& param1.isArray()
&& !param2.isArray() && !param2.isPrimitive()
&& param1.getComponentType().equals(param2);
})).toList();
setters.removeAll(excludedSetters);
return setters;
}
@Nullable
static <T extends Executable> T chooseBestOverload(List<T> executables, Location where) {
ImmutableList<T> chosen = chooseOverloads(executables, where,
/* chooseMultiple= */ false);
return (chosen.isEmpty() ? null : chosen.get(0));
}
static <T extends Executable> ImmutableList<T> chooseOverloads(List<T> executables,
Location where, boolean chooseMultiple) {
// Exclude variants with non-usable parameters and too-deep recursions.
executables = executables.stream()
.filter(e -> Arrays.stream(e.getParameterTypes()).noneMatch(
p -> UNUSABLE_TYPES.contains(p)
|| where.getClassOccurrenceCount(p) >= MAX_RECURSION))
.collect(Collectors.toList());
if (executables.size() <= 1) {
return ImmutableList.copyOf(executables);
}
// Overloads in "builders" usually set the same thing in two different ways (e.g.
// x(Bitmap) and x(Icon)). We choose the one with the most "interesting" parameters
// (from the point of view of containing Uris). In case of ties, LEAST parameters win,
// to use the simplest.
ArrayList<T> sortedCopy = new ArrayList<>(executables);
sortedCopy.sort(
Comparator.comparingInt(ReflectionUtils::getMethodScore)
.thenComparing(Executable::getParameterCount)
.reversed());
return chooseMultiple
? ImmutableList.copyOf(sortedCopy)
: ImmutableList.of(sortedCopy.get(0));
}
/**
* Counts the number of "interesting" parameters in a method. Used to choose the constructor
* or builder-setter overload most suited to this test (e.g. prefer
* {@link Notification.Builder#setLargeIcon(Icon)} to
* {@link Notification.Builder#setLargeIcon(Bitmap)}.
*/
static int getMethodScore(Executable executable) {
return Arrays.stream(executable.getParameterTypes())
.mapToInt(SpecialParameterGenerator::getParameterScore).sum();
}
}
private static class SpecialParameterGenerator {
private static final ImmutableSet<Class<?>> INTERESTING_CLASSES =
ImmutableSet.of(
Person.class, Uri.class, Icon.class, Intent.class, PendingIntent.class,
RemoteViews.class);
private static final ImmutableSet<Class<?>> MOCKED_CLASSES = ImmutableSet.of();
private static final ImmutableMap<Class<?>, Object> PRIMITIVE_VALUES =
ImmutableMap.<Class<?>, Object>builder()
.put(boolean.class, false)
.put(byte.class, (byte) 4)
.put(short.class, (short) 44)
.put(int.class, 1)
.put(long.class, 44444444L)
.put(float.class, 33.33f)
.put(double.class, 3333.3333d)
.put(char.class, 'N')
.build();
private final Context mContext;
private final List<Uri> mGeneratedUris = new ArrayList<>();
private int mNextUriCounter = 1;
SpecialParameterGenerator(Context context) {
mContext = context;
}
static boolean canGenerate(Class<?> clazz) {
return (INTERESTING_CLASSES.contains(clazz) && !clazz.equals(Person.class))
|| MOCKED_CLASSES.contains(clazz)
|| clazz.equals(Context.class)
|| clazz.equals(Bundle.class)
|| clazz.equals(Bitmap.class)
|| clazz.isPrimitive()
|| clazz.equals(CharSequence.class) || clazz.equals(String.class);
}
static int getParameterScore(Class<?> parameterClazz) {
if (parameterClazz.isArray()) {
return getParameterScore(parameterClazz.getComponentType());
} else if (INTERESTING_CLASSES.contains(parameterClazz)) {
return 10;
} else if (parameterClazz.isPrimitive() || parameterClazz.equals(CharSequence.class)
|| parameterClazz.equals(String.class)) {
return 0;
} else {
// No idea. We don't deep inspect, but score them as better than known-useless.
return 1;
}
}
Object generate(Class<?> clazz, Location where) {
if (clazz == Uri.class) {
return generateUri(where);
}
// Interesting parameters
if (clazz == Icon.class) {
Uri iconUri = generateUri(
where.plus(Icon.class).plus("createWithContentUri", Uri.class));
return Icon.createWithContentUri(iconUri);
}
if (clazz == Intent.class) {
// TODO(b/281044385): Are Intent Uris (new Intent(String,Uri)) relevant?
return new Intent("action");
}
if (clazz == PendingIntent.class) {
// PendingIntent can have an Intent with a Uri but those are inaccessible and
// not inspected.
return PendingIntent.getActivity(mContext, 0, new Intent("action"),
PendingIntent.FLAG_IMMUTABLE);
}
if (clazz == RemoteViews.class) {
RemoteViews rv = new RemoteViews(mContext.getPackageName(), /* layoutId= */ 10);
invokeAllSetters(rv, where.plus(RemoteViews.class),
/* allOverloads= */ true, /* includingVoidMethods= */ true,
/* excludingParameterTypes= */ ImmutableSet.of(), this);
return rv;
}
if (MOCKED_CLASSES.contains(clazz)) {
return Mockito.mock(clazz);
}
if (clazz.equals(Context.class)) {
return mContext;
}
if (clazz.equals(Bundle.class)) {
return new Bundle();
}
if (clazz.equals(Bitmap.class)) {
return Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
}
// ~Primitives
if (PRIMITIVE_VALUES.containsKey(clazz)) {
return PRIMITIVE_VALUES.get(clazz);
}
if (clazz.equals(CharSequence.class) || clazz.equals(String.class)) {
return where + "->string";
}
throw new IllegalArgumentException(
"I have no idea how to produce a(n) " + clazz + ", sorry");
}
private Uri generateUri(Location where) {
Uri uri = Uri.parse(String.format("%s - %s", mNextUriCounter++, where));
mGeneratedUris.add(uri);
return uri;
}
public List<Uri> getGeneratedUris() {
return mGeneratedUris;
}
}
private static class Location {
private static class Item {
@Nullable private final Class<?> mMaybeClass;
@Nullable private final Executable mMaybeMethod;
@Nullable private final String mExtra;
Item(@NonNull Class<?> clazz) {
mMaybeClass = checkNotNull(clazz);
mMaybeMethod = null;
mExtra = null;
}
Item(@NonNull Executable executable, @Nullable String extra) {
mMaybeClass = null;
mMaybeMethod = checkNotNull(executable);
mExtra = extra;
}
@NonNull
@Override
public String toString() {
String name = mMaybeClass != null
? "CLASS:" + mMaybeClass.getName()
: "METHOD:" + mMaybeMethod.getName() + "/"
+ mMaybeMethod.getParameterCount();
return name + Strings.nullToEmpty(mExtra);
}
}
private final ImmutableList<Item> mComponents;
private Location(Iterable<Item> components) {
mComponents = ImmutableList.copyOf(components);
}
private Location(Location soFar, Item next) {
// Verify the class->method->class->method ordering.
if (!soFar.mComponents.isEmpty()) {
Item previous = soFar.getLastItem();
if (previous.mMaybeMethod != null && next.mMaybeMethod != null) {
throw new IllegalArgumentException(
String.format("Unexpected sequence: %s ===> %s", soFar, next));
}
}
mComponents = ImmutableList.<Item>builder().addAll(soFar.mComponents).add(next).build();
}
public static Location root(Class<?> clazz) {
return new Location(ImmutableList.of(new Item(clazz)));
}
Location plus(Class<?> clazz) {
return new Location(this, new Item(clazz));
}
Location plus(Executable executable, String extra) {
return new Location(this, new Item(executable, extra));
}
public Location plus(String methodName, Class<?>... methodParameters) {
Item lastClass = getLastItem();
try {
checkNotNull(lastClass.mMaybeClass, "Last item is not a class but %s", lastClass);
Method method = lastClass.mMaybeClass.getMethod(methodName, methodParameters);
return new Location(this, new Item(method, null));
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(
String.format("Method %s not found in class %s",
methodName, lastClass.mMaybeClass.getName()));
}
}
Item getLastItem() {
checkState(!mComponents.isEmpty());
return mComponents.get(mComponents.size() - 1);
}
@NonNull
@Override
public String toString() {
return mComponents.stream().map(Item::toString).collect(Collectors.joining(" -> "));
}
public long getClassOccurrenceCount(Class<?> clazz) {
return mComponents.stream().filter(c -> clazz.equals(c.mMaybeClass)).count();
}
}
private static class Generated<T> {
public final T value;
public final ImmutableList<Uri> includedUris;
private Generated(T value, Iterable<Uri> includedUris) {
this.value = value;
this.includedUris = ImmutableList.copyOf(includedUris);
}
}
}