Merge "Add a comprehensive test for Notification.visitUris()" into udc-dev am: 353e15b09e
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23049101
Change-Id: I5fda57be6f0019826faf22eae52da16802672254
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
new file mode 100644
index 0000000..27677e1
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -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);
+ }
+ }
+}