1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.notification;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static com.google.common.base.Preconditions.checkState;
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import android.app.Notification;
24 import android.app.PendingIntent;
25 import android.app.Person;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.res.Resources;
29 import android.graphics.Bitmap;
30 import android.graphics.drawable.Icon;
31 import android.media.session.MediaSession;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.IBinder;
35 import android.os.Parcel;
36 import android.util.Log;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.widget.RemoteViews;
40 
41 import androidx.annotation.NonNull;
42 import androidx.test.InstrumentationRegistry;
43 import androidx.test.runner.AndroidJUnit4;
44 
45 import com.android.server.UiServiceTestCase;
46 
47 import com.google.common.base.Strings;
48 import com.google.common.collect.ArrayListMultimap;
49 import com.google.common.collect.ImmutableList;
50 import com.google.common.collect.ImmutableMap;
51 import com.google.common.collect.ImmutableMultimap;
52 import com.google.common.collect.ImmutableSet;
53 import com.google.common.collect.ListMultimap;
54 import com.google.common.collect.Multimap;
55 import com.google.common.truth.Expect;
56 
57 import org.junit.Before;
58 import org.junit.Rule;
59 import org.junit.Test;
60 import org.junit.runner.RunWith;
61 import org.mockito.ArgumentCaptor;
62 import org.mockito.Mockito;
63 
64 import java.io.PrintWriter;
65 import java.lang.reflect.Array;
66 import java.lang.reflect.Constructor;
67 import java.lang.reflect.Executable;
68 import java.lang.reflect.Method;
69 import java.lang.reflect.Modifier;
70 import java.lang.reflect.ParameterizedType;
71 import java.lang.reflect.Type;
72 import java.util.ArrayList;
73 import java.util.Arrays;
74 import java.util.Comparator;
75 import java.util.List;
76 import java.util.Optional;
77 import java.util.Set;
78 import java.util.function.Consumer;
79 import java.util.stream.Collectors;
80 import java.util.stream.Stream;
81 
82 import javax.annotation.Nullable;
83 
84 @RunWith(AndroidJUnit4.class)
85 public class NotificationVisitUrisTest extends UiServiceTestCase {
86 
87     private static final String TAG = "VisitUrisTest";
88 
89     // Methods that are known to add Uris that are *NOT* verified.
90     // This list should be emptied! Items can be removed as bugs are fixed.
91     private static final Multimap<Class<?>, String> KNOWN_BAD =
92             ImmutableMultimap.<Class<?>, String>builder()
93                     .put(Person.Builder.class, "setUri") // TODO: b/281044385
94                     .put(RemoteViews.class, "setRemoteAdapter") // TODO: b/281044385
95                     .build();
96 
97     // Types that we can't really produce. No methods receiving these parameters will be invoked.
98     private static final ImmutableSet<Class<?>> UNUSABLE_TYPES =
99             ImmutableSet.of(Consumer.class, IBinder.class, MediaSession.Token.class, Parcel.class,
100                     PrintWriter.class, Resources.Theme.class, View.class,
101                     LayoutInflater.Factory2.class);
102 
103     // Maximum number of times we allow generating the same class recursively.
104     // E.g. new RemoteViews.addView(new RemoteViews()) but stop there.
105     private static final int MAX_RECURSION = 2;
106 
107     // Number of times a method called addX(X) will be called.
108     private static final int NUM_ADD_CALLS = 2;
109 
110     // Number of elements to put in a generated array, e.g. for calling setGloops(Gloop[] gloops).
111     private static final int NUM_ELEMENTS_IN_ARRAY = 3;
112 
113     // Constructors that should be used to create instances of specific classes. Overrides scoring.
114     private static final ImmutableMap<Class<?>, Constructor<?>> PREFERRED_CONSTRUCTORS;
115 
116     static {
117         try {
118             PREFERRED_CONSTRUCTORS = ImmutableMap.of(
119                     Notification.Builder.class,
120                     Notification.Builder.class.getConstructor(Context.class, String.class));
121 
122             EXCLUDED_SETTERS_OVERLOADS = ImmutableMultimap.<Class<?>, Method>builder()
123                     .put(RemoteViews.class,
124                             // b/245950570: Tries to connect to service and will crash.
125                             RemoteViews.class.getMethod("setRemoteAdapter",
126                                     int.class, Intent.class))
127                     .build();
128         } catch (NoSuchMethodException e) {
129             throw new RuntimeException(e);
130         }
131     }
132 
133     // Setters that shouldn't be called, for various reasons (but NOT because they are KNOWN_BAD).
134     private static final Multimap<Class<?>, String> EXCLUDED_SETTERS =
135             ImmutableMultimap.<Class<?>, String>builder()
136                     // Handled by testAllStyles().
137                     .put(Notification.Builder.class, "setStyle")
138                     // Handled by testAllExtenders().
139                     .put(Notification.Builder.class, "extend")
140                     // Handled by testAllActionExtenders().
141                     .put(Notification.Action.Builder.class, "extend")
142                     // Overwrites icon supplied to constructor.
143                     .put(Notification.BubbleMetadata.Builder.class, "setIcon")
144                     // Discards previously-added actions.
145                     .put(RemoteViews.class, "mergeRemoteViews")
146                     .build();
147 
148     // Same as above, but specific overloads that should not be called.
149     private static final Multimap<Class<?>, Method> EXCLUDED_SETTERS_OVERLOADS;
150 
151     private Context mContext;
152 
153     @Rule
154     public final Expect expect = Expect.create();
155 
156     @Before
setUp()157     public void setUp() {
158         mContext = InstrumentationRegistry.getInstrumentation().getContext();
159     }
160 
161     @Test // This is a meta-test, checks that the generators are not broken.
verifyTest()162     public void verifyTest() {
163         Generated<Notification> notification = buildNotification(mContext,
164                 /* styleClass= */ Notification.MessagingStyle.class,
165                 /* extenderClass= */ Notification.WearableExtender.class,
166                 /* actionExtenderClass= */ Notification.Action.WearableExtender.class,
167                 /* includeRemoteViews= */ true);
168         assertThat(notification.includedUris.size()).isAtLeast(900);
169     }
170 
171     @Test
testPlainNotification()172     public void testPlainNotification() throws Exception {
173         Generated<Notification> notification = buildNotification(mContext, /* styleClass= */ null,
174                 /* extenderClass= */ null, /* actionExtenderClass= */ null,
175                 /* includeRemoteViews= */ false);
176         verifyAllUrisAreVisited(notification.value, notification.includedUris,
177                 "Plain Notification");
178     }
179 
180     @Test
testRemoteViews()181     public void testRemoteViews() throws Exception {
182         Generated<Notification> notification = buildNotification(mContext, /* styleClass= */ null,
183                 /* extenderClass= */ null, /* actionExtenderClass= */ null,
184                 /* includeRemoteViews= */ true);
185         verifyAllUrisAreVisited(notification.value, notification.includedUris,
186                 "Notification with Remote Views");
187     }
188 
189     @Test
testAllStyles()190     public void testAllStyles() throws Exception {
191         for (Class<?> styleClass : ReflectionUtils.getConcreteSubclasses(Notification.Style.class,
192                 Notification.class)) {
193             Generated<Notification> notification = buildNotification(mContext, styleClass,
194                     /* extenderClass= */ null, /* actionExtenderClass= */ null,
195                     /* includeRemoteViews= */ false);
196             verifyAllUrisAreVisited(notification.value, notification.includedUris,
197                     String.format("Style (%s)", styleClass.getSimpleName()));
198         }
199     }
200 
201     @Test
testAllExtenders()202     public void testAllExtenders() throws Exception {
203         for (Class<?> extenderClass : ReflectionUtils.getConcreteSubclasses(
204                 Notification.Extender.class, Notification.class)) {
205             Generated<Notification> notification = buildNotification(mContext,
206                     /* styleClass= */ null, extenderClass, /* actionExtenderClass= */ null,
207                     /* includeRemoteViews= */ false);
208             verifyAllUrisAreVisited(notification.value, notification.includedUris,
209                     String.format("Extender (%s)", extenderClass.getSimpleName()));
210         }
211     }
212 
213     @Test
testAllActionExtenders()214     public void testAllActionExtenders() throws Exception {
215         for (Class<?> actionExtenderClass : ReflectionUtils.getConcreteSubclasses(
216                 Notification.Action.Extender.class, Notification.Action.class)) {
217             Generated<Notification> notification = buildNotification(mContext,
218                     /* styleClass= */ null, /* extenderClass= */ null, actionExtenderClass,
219                     /* includeRemoteViews= */ false);
220             verifyAllUrisAreVisited(notification.value, notification.includedUris,
221                     String.format("Action.Extender (%s)", actionExtenderClass.getSimpleName()));
222         }
223     }
224 
verifyAllUrisAreVisited(Notification notification, List<Uri> includedUris, String notificationTypeMessage)225     private void verifyAllUrisAreVisited(Notification notification, List<Uri> includedUris,
226             String notificationTypeMessage) throws Exception {
227         Consumer<Uri> visitor = (Consumer<Uri>) Mockito.mock(Consumer.class);
228         ArgumentCaptor<Uri> visitedUriCaptor = ArgumentCaptor.forClass(Uri.class);
229 
230         notification.visitUris(visitor);
231 
232         Mockito.verify(visitor, Mockito.atLeastOnce()).accept(visitedUriCaptor.capture());
233         List<Uri> visitedUris = new ArrayList<>(visitedUriCaptor.getAllValues());
234         visitedUris.remove(null);
235 
236         expect.withMessage(notificationTypeMessage)
237                 .that(visitedUris)
238                 .containsAtLeastElementsIn(includedUris);
239         expect.that(KNOWN_BAD).isNotEmpty(); // Once empty, switch to containsExactlyElementsIn()
240     }
241 
buildNotification(Context context, @Nullable Class<?> styleClass, @Nullable Class<?> extenderClass, @Nullable Class<?> actionExtenderClass, boolean includeRemoteViews)242     private static Generated<Notification> buildNotification(Context context,
243             @Nullable Class<?> styleClass, @Nullable Class<?> extenderClass,
244             @Nullable Class<?> actionExtenderClass, boolean includeRemoteViews) {
245         SpecialParameterGenerator specialGenerator = new SpecialParameterGenerator(context);
246         Set<Class<?>> excludedClasses = includeRemoteViews
247                 ? ImmutableSet.of()
248                 : ImmutableSet.of(RemoteViews.class);
249         Location location = Location.root(Notification.Builder.class);
250 
251         Notification.Builder builder = new Notification.Builder(context, "channelId");
252         invokeAllSetters(builder, location, /* allOverloads= */ false,
253                 /* includingVoidMethods= */ false, excludedClasses, specialGenerator);
254 
255         if (styleClass != null) {
256             builder.setStyle((Notification.Style) generateObject(styleClass,
257                     location.plus("setStyle", Notification.Style.class),
258                     excludedClasses, specialGenerator));
259         }
260         if (extenderClass != null) {
261             builder.extend((Notification.Extender) generateObject(extenderClass,
262                     location.plus("extend", Notification.Extender.class),
263                     excludedClasses, specialGenerator));
264         }
265         if (actionExtenderClass != null) {
266             Location actionLocation = location.plus("addAction", Notification.Action.class);
267             Notification.Action.Builder actionBuilder =
268                     (Notification.Action.Builder) generateObject(
269                             Notification.Action.Builder.class, actionLocation, excludedClasses,
270                             specialGenerator);
271             actionBuilder.extend((Notification.Action.Extender) generateObject(actionExtenderClass,
272                     actionLocation.plus(
273                             Notification.Action.Builder.class).plus("extend",
274                             Notification.Action.Extender.class),
275                     excludedClasses, specialGenerator));
276             builder.addAction(actionBuilder.build());
277         }
278 
279         return new Generated<>(builder.build(), specialGenerator.getGeneratedUris());
280     }
281 
generateObject(Class<?> clazz, Location where, Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator)282     private static Object generateObject(Class<?> clazz, Location where,
283             Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
284         if (excludingClasses.contains(clazz)) {
285             throw new IllegalArgumentException(
286                     String.format("Asked to generate a %s but it's part of the excluded set (%s)",
287                             clazz, excludingClasses));
288         }
289 
290         if (SpecialParameterGenerator.canGenerate(clazz)) {
291             return specialGenerator.generate(clazz, where);
292         }
293         if (clazz.isEnum()) {
294             return clazz.getEnumConstants()[0];
295         }
296         if (clazz.isArray()) {
297             Object arrayValue = Array.newInstance(clazz.getComponentType(), NUM_ELEMENTS_IN_ARRAY);
298             for (int i = 0; i < Array.getLength(arrayValue); i++) {
299                 Array.set(arrayValue, i,
300                         generateObject(clazz.getComponentType(), where, excludingClasses,
301                                 specialGenerator));
302             }
303             return arrayValue;
304         }
305 
306         Log.i(TAG, "About to generate a(n)" + clazz.getName());
307 
308         // Need to construct one of these. Look for a Builder inner class... and also look for a
309         // Builder as a "sibling" class; CarExtender.UnreadConversation does this :(
310         Stream<Class<?>> maybeBuilders =
311                 Stream.concat(Arrays.stream(clazz.getDeclaredClasses()),
312                         clazz.getDeclaringClass() != null
313                                 ? Arrays.stream(clazz.getDeclaringClass().getDeclaredClasses())
314                                 : Stream.empty());
315         Optional<Class<?>> clazzBuilder =
316                 maybeBuilders
317                         .filter(maybeBuilder -> maybeBuilder.getSimpleName().equals("Builder"))
318                         .filter(maybeBuilder ->
319                                 Arrays.stream(maybeBuilder.getMethods()).anyMatch(
320                                         m -> m.getName().equals("build")
321                                                 && m.getParameterCount() == 0
322                                                 && m.getReturnType().equals(clazz)))
323                         .findFirst();
324 
325 
326         if (clazzBuilder.isPresent()) {
327             try {
328                 // Found a Builder! Create an instance of it, call its setters, and call build()
329                 // on it.
330                 Object builder = constructEmpty(clazzBuilder.get(), where.plus(clazz),
331                         excludingClasses, specialGenerator);
332                 invokeAllSetters(builder, where.plus(clazz).plus(clazzBuilder.get()),
333                         /* allOverloads= */ false, /* includingVoidMethods= */ false,
334                         excludingClasses, specialGenerator);
335 
336                 Method buildMethod = builder.getClass().getMethod("build");
337                 Object built = buildMethod.invoke(builder);
338                 assertThat(built).isInstanceOf(clazz);
339                 return built;
340             } catch (Exception e) {
341                 throw new UnsupportedOperationException(
342                         "Error using Builder " + clazzBuilder.get().getName(), e);
343             }
344         }
345 
346         // If no X.Builder, look for X() constructor.
347         try {
348             Object instance = constructEmpty(clazz, where, excludingClasses, specialGenerator);
349             invokeAllSetters(instance, where.plus(clazz), /* allOverloads= */ false,
350                     /* includingVoidMethods= */ false, excludingClasses, specialGenerator);
351             return instance;
352         } catch (Exception e) {
353             throw new UnsupportedOperationException("Error generating a(n) " + clazz.getName(), e);
354         }
355     }
356 
constructEmpty(Class<?> clazz, Location where, Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator)357     private static Object constructEmpty(Class<?> clazz, Location where,
358             Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
359         Constructor<?> bestConstructor;
360         if (PREFERRED_CONSTRUCTORS.containsKey(clazz)) {
361             // Use the preferred constructor.
362             bestConstructor = PREFERRED_CONSTRUCTORS.get(clazz);
363         } else if (Notification.Extender.class.isAssignableFrom(clazz)
364                 || Notification.Action.Extender.class.isAssignableFrom(clazz)) {
365             // For extenders, prefer the empty constructors. The others are "partial-copy"
366             // constructors and do not read all fields from the supplied Notification/Action.
367             try {
368                 bestConstructor = clazz.getConstructor();
369             } catch (Exception e) {
370                 throw new UnsupportedOperationException(
371                         String.format("Extender class %s doesn't have a zero-parameter constructor",
372                                 clazz.getName()));
373             }
374         } else {
375             // Look for a non-deprecated constructor using any of the "interesting" parameters.
376             List<Constructor<?>> allConstructors = Arrays.stream(clazz.getConstructors())
377                     .filter(c -> c.getAnnotation(Deprecated.class) == null)
378                     .collect(Collectors.toList());
379             bestConstructor = ReflectionUtils.chooseBestOverload(allConstructors, where);
380         }
381         if (bestConstructor != null) {
382             try {
383                 Object[] constructorParameters = generateParameters(bestConstructor,
384                         where.plus(clazz), excludingClasses, specialGenerator);
385                 Log.i(TAG, "Invoking " + ReflectionUtils.methodToString(bestConstructor) + " with "
386                         + Arrays.toString(constructorParameters));
387                 return bestConstructor.newInstance(constructorParameters);
388             } catch (Exception e) {
389                 throw new UnsupportedOperationException(
390                         String.format("Error invoking constructor %s",
391                                 ReflectionUtils.methodToString(bestConstructor)), e);
392             }
393         }
394 
395         // Look for a "static constructor", i.e. some factory method on the same class.
396         List<Method> factoryMethods = Arrays.stream(clazz.getMethods())
397                 .filter(m -> Modifier.isStatic(m.getModifiers()) && clazz.equals(m.getReturnType()))
398                 .collect(Collectors.toList());
399         Method bestFactoryMethod = ReflectionUtils.chooseBestOverload(factoryMethods, where);
400         if (bestFactoryMethod != null) {
401             try {
402                 Object[] methodParameters = generateParameters(bestFactoryMethod, where.plus(clazz),
403                         excludingClasses, specialGenerator);
404                 Log.i(TAG,
405                         "Invoking " + ReflectionUtils.methodToString(bestFactoryMethod) + " with "
406                                 + Arrays.toString(methodParameters));
407                 return bestFactoryMethod.invoke(null, methodParameters);
408             } catch (Exception e) {
409                 throw new UnsupportedOperationException(
410                         "Error invoking constructor-like static method "
411                                 + bestFactoryMethod.getName() + " for " + clazz.getName(), e);
412             }
413         }
414 
415         throw new UnsupportedOperationException(
416                 "Couldn't find a way to construct a(n) " + clazz.getName());
417     }
418 
invokeAllSetters(Object instance, Location where, boolean allOverloads, boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes, SpecialParameterGenerator specialGenerator)419     private static void invokeAllSetters(Object instance, Location where, boolean allOverloads,
420             boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes,
421             SpecialParameterGenerator specialGenerator) {
422         for (Method setter : ReflectionUtils.getAllSetters(instance.getClass(), where,
423                 allOverloads, includingVoidMethods, excludingParameterTypes)) {
424             try {
425                 int numInvocations = setter.getName().startsWith("add") ? NUM_ADD_CALLS : 1;
426                 for (int i = 0; i < numInvocations; i++) {
427 
428                     // If the method is a "known bad" (i.e. adds Uris that aren't visited later)
429                     // then still call it, but don't add to list of generated Uris. Easiest way is
430                     // to use a throw-away SpecialParameterGenerator instead of the accumulating
431                     // one.
432                     SpecialParameterGenerator specialGeneratorForThisSetter =
433                             KNOWN_BAD.containsEntry(instance.getClass(), setter.getName())
434                                     ? new SpecialParameterGenerator(specialGenerator.mContext)
435                                     : specialGenerator;
436 
437                     Object[] setterParam = generateParameters(setter, where,
438                             excludingParameterTypes, specialGeneratorForThisSetter);
439                     Log.i(TAG, "Invoking " + ReflectionUtils.methodToString(setter) + " with "
440                             + setterParam[0]);
441                     setter.invoke(instance, setterParam);
442                 }
443             } catch (Exception e) {
444                 throw new UnsupportedOperationException(
445                         "Error invoking setter " + ReflectionUtils.methodToString(setter), e);
446             }
447         }
448     }
449 
generateParameters(Executable executable, Location where, Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator)450     private static Object[] generateParameters(Executable executable, Location where,
451             Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
452         Log.i(TAG, "About to generate parameters for " + ReflectionUtils.methodToString(executable)
453                 + " in " + where);
454         Type[] parameterTypes = executable.getGenericParameterTypes();
455         Object[] parameterValues = new Object[parameterTypes.length];
456         for (int i = 0; i < parameterTypes.length; i++) {
457             parameterValues[i] = generateParameter(
458                     parameterTypes[i],
459                     where.plus(executable,
460                             String.format("[%d,%s]", i, parameterTypes[i].getTypeName())),
461                     excludingClasses,
462                     specialGenerator);
463         }
464         return parameterValues;
465     }
466 
generateParameter(Type parameterType, Location where, Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator)467     private static Object generateParameter(Type parameterType, Location where,
468             Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
469         if (parameterType instanceof Class<?> parameterClass) {
470             return generateObject(
471                     parameterClass,
472                     where,
473                     excludingClasses,
474                     specialGenerator);
475         } else if (parameterType instanceof ParameterizedType parameterizedType) {
476             if (parameterizedType.getRawType().equals(List.class)
477                     && parameterizedType.getActualTypeArguments()[0] instanceof Class<?>) {
478                 ArrayList listValue = new ArrayList();
479                 for (int i = 0; i < NUM_ELEMENTS_IN_ARRAY; i++) {
480                     listValue.add(
481                             generateObject((Class<?>) parameterizedType.getActualTypeArguments()[0],
482                                     where, excludingClasses, specialGenerator));
483                 }
484                 return listValue;
485             }
486         }
487         throw new IllegalArgumentException(
488                 "I have no idea how to produce a(n) " + parameterType + ", sorry");
489     }
490 
491     private static class ReflectionUtils {
getConcreteSubclasses(Class<?> clazz, Class<?> containerClass)492         static Set<Class<?>> getConcreteSubclasses(Class<?> clazz, Class<?> containerClass) {
493             return Arrays.stream(containerClass.getDeclaredClasses())
494                     .filter(
495                             innerClass -> clazz.isAssignableFrom(innerClass)
496                                     && !Modifier.isAbstract(innerClass.getModifiers()))
497                     .collect(Collectors.toSet());
498         }
499 
methodToString(Executable executable)500         static String methodToString(Executable executable) {
501             return String.format("%s::%s(%s)",
502                     executable.getDeclaringClass().getName(),
503                     executable.getName(),
504                     Arrays.stream(executable.getParameterTypes()).map(Class::getSimpleName)
505                             .collect(Collectors.joining(", "))
506             );
507         }
508 
getAllSetters(Class<?> clazz, Location where, boolean allOverloads, boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes)509         static List<Method> getAllSetters(Class<?> clazz, Location where, boolean allOverloads,
510                 boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes) {
511             ListMultimap<String, Method> methods = ArrayListMultimap.create();
512             // Candidate "setters" are any methods that receive one at least parameter and are
513             // either void (if acceptable) or return the same type being built.
514             for (Method method : clazz.getDeclaredMethods()) {
515                 if (Modifier.isPublic(method.getModifiers())
516                         && !Modifier.isStatic(method.getModifiers())
517                         && method.getAnnotation(Deprecated.class) == null
518                         && ((includingVoidMethods && method.getReturnType().equals(Void.TYPE))
519                         || method.getReturnType().equals(clazz))
520                         && method.getParameterCount() >= 1
521                         && !EXCLUDED_SETTERS.containsEntry(clazz, method.getName())
522                         && !EXCLUDED_SETTERS_OVERLOADS.containsEntry(clazz, method)
523                         && Arrays.stream(method.getParameterTypes())
524                             .noneMatch(excludingParameterTypes::contains)) {
525                     methods.put(method.getName(), method);
526                 }
527             }
528 
529             // In case of overloads, prefer those with the most interesting parameters.
530             List<Method> setters = new ArrayList<>();
531             for (String methodName : methods.keySet()) {
532                 setters.addAll(chooseOverloads(methods.get(methodName), where, allOverloads));
533             }
534 
535             // Exclude set(x[]) when there exists add(x).
536             List<Method> excludedSetters = setters.stream().filter(
537                     m1 -> m1.getName().startsWith("set")
538                             && setters.stream().anyMatch(
539                                     m2 -> {
540                                             Class<?> param1 = m1.getParameterTypes()[0];
541                                             Class<?> param2 = m2.getParameterTypes()[0];
542                                             return m2.getName().startsWith("add")
543                                                     && param1.isArray()
544                                                     && !param2.isArray() && !param2.isPrimitive()
545                                                     && param1.getComponentType().equals(param2);
546                                     })).toList();
547 
548             setters.removeAll(excludedSetters);
549             return setters;
550         }
551 
552         @Nullable
chooseBestOverload(List<T> executables, Location where)553         static <T extends Executable> T chooseBestOverload(List<T> executables, Location where) {
554             ImmutableList<T> chosen = chooseOverloads(executables, where,
555                     /* chooseMultiple= */ false);
556             return (chosen.isEmpty() ? null : chosen.get(0));
557         }
558 
chooseOverloads(List<T> executables, Location where, boolean chooseMultiple)559         static <T extends Executable> ImmutableList<T> chooseOverloads(List<T> executables,
560                 Location where, boolean chooseMultiple) {
561             // Exclude variants with non-usable parameters and too-deep recursions.
562             executables = executables.stream()
563                     .filter(e -> Arrays.stream(e.getParameterTypes()).noneMatch(
564                             p -> UNUSABLE_TYPES.contains(p)
565                                     || where.getClassOccurrenceCount(p) >= MAX_RECURSION))
566                     .collect(Collectors.toList());
567 
568             if (executables.size() <= 1) {
569                 return ImmutableList.copyOf(executables);
570             }
571 
572             // Overloads in "builders" usually set the same thing in two different ways (e.g.
573             // x(Bitmap) and x(Icon)). We choose the one with the most "interesting" parameters
574             // (from the point of view of containing Uris). In case of ties, LEAST parameters win,
575             // to use the simplest.
576             ArrayList<T> sortedCopy = new ArrayList<>(executables);
577             sortedCopy.sort(
578                     Comparator.comparingInt(ReflectionUtils::getMethodScore)
579                             .thenComparing(Executable::getParameterCount)
580                             .reversed());
581 
582             return chooseMultiple
583                     ? ImmutableList.copyOf(sortedCopy)
584                     : ImmutableList.of(sortedCopy.get(0));
585         }
586 
587         /**
588          * Counts the number of "interesting" parameters in a method. Used to choose the constructor
589          * or builder-setter overload most suited to this test (e.g. prefer
590          * {@link Notification.Builder#setLargeIcon(Icon)} to
591          * {@link Notification.Builder#setLargeIcon(Bitmap)}.
592          */
getMethodScore(Executable executable)593         static int getMethodScore(Executable executable) {
594             return Arrays.stream(executable.getParameterTypes())
595                     .mapToInt(SpecialParameterGenerator::getParameterScore).sum();
596         }
597     }
598 
599     private static class SpecialParameterGenerator {
600         private static final ImmutableSet<Class<?>> INTERESTING_CLASSES =
601                 ImmutableSet.of(
602                         Person.class, Uri.class, Icon.class, Intent.class, PendingIntent.class,
603                         RemoteViews.class);
604         private static final ImmutableSet<Class<?>> MOCKED_CLASSES = ImmutableSet.of();
605 
606         private static final ImmutableMap<Class<?>, Object> PRIMITIVE_VALUES =
607                 ImmutableMap.<Class<?>, Object>builder()
608                         .put(boolean.class, false)
609                         .put(byte.class, (byte) 4)
610                         .put(short.class, (short) 44)
611                         .put(int.class, 1)
612                         .put(long.class, 44444444L)
613                         .put(float.class, 33.33f)
614                         .put(double.class, 3333.3333d)
615                         .put(char.class, 'N')
616                         .build();
617 
618         private final Context mContext;
619         private final List<Uri> mGeneratedUris = new ArrayList<>();
620         private int mNextUriCounter = 1;
621 
SpecialParameterGenerator(Context context)622         SpecialParameterGenerator(Context context) {
623             mContext = context;
624         }
625 
canGenerate(Class<?> clazz)626         static boolean canGenerate(Class<?> clazz) {
627             return (INTERESTING_CLASSES.contains(clazz) && !clazz.equals(Person.class))
628                     || MOCKED_CLASSES.contains(clazz)
629                     || clazz.equals(Context.class)
630                     || clazz.equals(Bundle.class)
631                     || clazz.equals(Bitmap.class)
632                     || clazz.isPrimitive()
633                     || clazz.equals(CharSequence.class) || clazz.equals(String.class);
634         }
635 
getParameterScore(Class<?> parameterClazz)636         static int getParameterScore(Class<?> parameterClazz) {
637             if (parameterClazz.isArray()) {
638                 return getParameterScore(parameterClazz.getComponentType());
639             } else if (INTERESTING_CLASSES.contains(parameterClazz)) {
640                 return 10;
641             } else if (parameterClazz.isPrimitive() || parameterClazz.equals(CharSequence.class)
642                     || parameterClazz.equals(String.class)) {
643                 return 0;
644             } else {
645                 // No idea. We don't deep inspect, but score them as better than known-useless.
646                 return 1;
647             }
648         }
649 
generate(Class<?> clazz, Location where)650         Object generate(Class<?> clazz, Location where) {
651             if (clazz == Uri.class) {
652                 return generateUri(where);
653             }
654 
655             // Interesting parameters
656             if (clazz == Icon.class) {
657                 Uri iconUri = generateUri(
658                         where.plus(Icon.class).plus("createWithContentUri", Uri.class));
659                 return Icon.createWithContentUri(iconUri);
660             }
661 
662             if (clazz == Intent.class) {
663                 // TODO(b/281044385): Are Intent Uris (new Intent(String,Uri)) relevant?
664                 return new Intent("action");
665             }
666 
667             if (clazz == PendingIntent.class) {
668                 // PendingIntent can have an Intent with a Uri but those are inaccessible and
669                 // not inspected.
670                 return PendingIntent.getActivity(mContext, 0, new Intent("action"),
671                         PendingIntent.FLAG_IMMUTABLE);
672             }
673 
674             if (clazz == RemoteViews.class) {
675                 RemoteViews rv = new RemoteViews(mContext.getPackageName(), /* layoutId= */ 10);
676                 invokeAllSetters(rv, where.plus(RemoteViews.class),
677                         /* allOverloads= */ true, /* includingVoidMethods= */ true,
678                         /* excludingParameterTypes= */ ImmutableSet.of(), this);
679                 return rv;
680             }
681 
682             if (MOCKED_CLASSES.contains(clazz)) {
683                 return Mockito.mock(clazz);
684             }
685             if (clazz.equals(Context.class)) {
686                 return mContext;
687             }
688             if (clazz.equals(Bundle.class)) {
689                 return new Bundle();
690             }
691             if (clazz.equals(Bitmap.class)) {
692                 return Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
693             }
694 
695             // ~Primitives
696             if (PRIMITIVE_VALUES.containsKey(clazz)) {
697                 return PRIMITIVE_VALUES.get(clazz);
698             }
699             if (clazz.equals(CharSequence.class) || clazz.equals(String.class)) {
700                 return where + "->string";
701             }
702 
703             throw new IllegalArgumentException(
704                     "I have no idea how to produce a(n) " + clazz + ", sorry");
705         }
706 
generateUri(Location where)707         private Uri generateUri(Location where) {
708             Uri uri = Uri.parse(String.format("%s - %s", mNextUriCounter++, where));
709             mGeneratedUris.add(uri);
710             return uri;
711         }
712 
getGeneratedUris()713         public List<Uri> getGeneratedUris() {
714             return mGeneratedUris;
715         }
716     }
717 
718     private static class Location {
719 
720         private static class Item {
721             @Nullable private final Class<?> mMaybeClass;
722             @Nullable private final Executable mMaybeMethod;
723             @Nullable private final String mExtra;
724 
Item(@onNull Class<?> clazz)725             Item(@NonNull Class<?> clazz) {
726                 mMaybeClass = checkNotNull(clazz);
727                 mMaybeMethod = null;
728                 mExtra = null;
729             }
730 
Item(@onNull Executable executable, @Nullable String extra)731             Item(@NonNull Executable executable, @Nullable String extra) {
732                 mMaybeClass = null;
733                 mMaybeMethod = checkNotNull(executable);
734                 mExtra = extra;
735             }
736 
737             @NonNull
738             @Override
toString()739             public String toString() {
740                 String name = mMaybeClass != null
741                         ? "CLASS:" + mMaybeClass.getName()
742                         : "METHOD:" + mMaybeMethod.getName() + "/"
743                                 + mMaybeMethod.getParameterCount();
744                 return name + Strings.nullToEmpty(mExtra);
745             }
746         }
747 
748         private final ImmutableList<Item> mComponents;
749 
Location(Iterable<Item> components)750         private Location(Iterable<Item> components) {
751             mComponents = ImmutableList.copyOf(components);
752         }
753 
Location(Location soFar, Item next)754         private Location(Location soFar, Item next) {
755             // Verify the class->method->class->method ordering.
756             if (!soFar.mComponents.isEmpty()) {
757                 Item previous = soFar.getLastItem();
758                 if (previous.mMaybeMethod != null && next.mMaybeMethod != null) {
759                     throw new IllegalArgumentException(
760                             String.format("Unexpected sequence: %s ===> %s", soFar, next));
761                 }
762             }
763             mComponents = ImmutableList.<Item>builder().addAll(soFar.mComponents).add(next).build();
764         }
765 
root(Class<?> clazz)766         public static Location root(Class<?> clazz) {
767             return new Location(ImmutableList.of(new Item(clazz)));
768         }
769 
plus(Class<?> clazz)770         Location plus(Class<?> clazz) {
771             return new Location(this, new Item(clazz));
772         }
773 
plus(Executable executable, String extra)774         Location plus(Executable executable, String extra) {
775             return new Location(this, new Item(executable, extra));
776         }
777 
plus(String methodName, Class<?>... methodParameters)778         public Location plus(String methodName, Class<?>... methodParameters) {
779             Item lastClass = getLastItem();
780             try {
781                 checkNotNull(lastClass.mMaybeClass, "Last item is not a class but %s", lastClass);
782                 Method method = lastClass.mMaybeClass.getMethod(methodName, methodParameters);
783                 return new Location(this, new Item(method, null));
784             } catch (NoSuchMethodException e) {
785                 throw new IllegalArgumentException(
786                         String.format("Method %s not found in class %s",
787                                 methodName, lastClass.mMaybeClass.getName()));
788             }
789         }
790 
getLastItem()791         Item getLastItem() {
792             checkState(!mComponents.isEmpty());
793             return mComponents.get(mComponents.size() - 1);
794         }
795 
796         @NonNull
797         @Override
toString()798         public String toString() {
799             return mComponents.stream().map(Item::toString).collect(Collectors.joining(" -> "));
800         }
801 
getClassOccurrenceCount(Class<?> clazz)802         public long getClassOccurrenceCount(Class<?> clazz) {
803             return mComponents.stream().filter(c -> clazz.equals(c.mMaybeClass)).count();
804         }
805     }
806 
807     private static class Generated<T> {
808         public final T value;
809         public final ImmutableList<Uri> includedUris;
810 
Generated(T value, Iterable<Uri> includedUris)811         private Generated(T value, Iterable<Uri> includedUris) {
812             this.value = value;
813             this.includedUris = ImmutableList.copyOf(includedUris);
814         }
815     }
816 }
817