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