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.intentresolver.chooser; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.Activity; 22 import android.app.prediction.AppTarget; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.ResolveInfo; 27 import android.content.pm.ShortcutInfo; 28 import android.os.Bundle; 29 import android.os.UserHandle; 30 import android.util.HashedStringCache; 31 32 import androidx.annotation.VisibleForTesting; 33 34 import com.google.common.collect.ImmutableList; 35 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** 40 * An implementation of {@link TargetInfo} with immutable data. Any modifications must be made by 41 * creating a new instance (e.g., via {@link ImmutableTargetInfo#toBuilder()}). 42 */ 43 public final class ImmutableTargetInfo implements TargetInfo { 44 private static final String TAG = "TargetInfo"; 45 46 /** Delegate interface to implement {@link TargetInfo#getHashedTargetIdForMetrics()}. */ 47 public interface TargetHashProvider { 48 /** Request a hash for the specified {@code target}. */ getHashedTargetIdForMetrics( TargetInfo target, Context context)49 HashedStringCache.HashResult getHashedTargetIdForMetrics( 50 TargetInfo target, Context context); 51 } 52 53 /** Delegate interface to request that the target be launched by a particular API. */ 54 public interface TargetActivityStarter { 55 /** 56 * Request that the delegate use the {@link Activity#startAsCaller()} API to launch the 57 * specified {@code target}. 58 * 59 * @return true if the target was launched successfully. 60 */ startAsCaller(TargetInfo target, Activity activity, Bundle options, int userId)61 boolean startAsCaller(TargetInfo target, Activity activity, Bundle options, int userId); 62 63 /** 64 * Request that the delegate use the {@link Activity#startAsUser()} API to launch the 65 * specified {@code target}. 66 * 67 * @return true if the target was launched successfully. 68 */ startAsUser(TargetInfo target, Activity activity, Bundle options, UserHandle user)69 boolean startAsUser(TargetInfo target, Activity activity, Bundle options, UserHandle user); 70 } 71 72 enum LegacyTargetType { 73 NOT_LEGACY_TARGET, 74 EMPTY_TARGET_INFO, 75 PLACEHOLDER_TARGET_INFO, 76 SELECTABLE_TARGET_INFO, 77 DISPLAY_RESOLVE_INFO, 78 MULTI_DISPLAY_RESOLVE_INFO 79 }; 80 81 /** Builder API to construct {@code ImmutableTargetInfo} instances. */ 82 public static class Builder { 83 @Nullable 84 private ComponentName mResolvedComponentName; 85 86 @Nullable 87 private Intent mResolvedIntent; 88 89 @Nullable 90 private Intent mBaseIntentToSend; 91 92 @Nullable 93 private Intent mTargetIntent; 94 95 @Nullable 96 private ComponentName mChooserTargetComponentName; 97 98 @Nullable 99 private ShortcutInfo mDirectShareShortcutInfo; 100 101 @Nullable 102 private AppTarget mDirectShareAppTarget; 103 104 @Nullable 105 private DisplayResolveInfo mDisplayResolveInfo; 106 107 @Nullable 108 private TargetHashProvider mHashProvider; 109 110 @Nullable 111 private Intent mReferrerFillInIntent; 112 113 @Nullable 114 private TargetActivityStarter mActivityStarter; 115 116 @Nullable 117 private ResolveInfo mResolveInfo; 118 119 @Nullable 120 private CharSequence mDisplayLabel; 121 122 @Nullable 123 private CharSequence mExtendedInfo; 124 125 @Nullable 126 private IconHolder mDisplayIconHolder; 127 128 private boolean mIsSuspended; 129 private boolean mIsPinned; 130 private float mModifiedScore = -0.1f; 131 private LegacyTargetType mLegacyType = LegacyTargetType.NOT_LEGACY_TARGET; 132 133 private ImmutableList<Intent> mAlternateSourceIntents = ImmutableList.of(); 134 private ImmutableList<DisplayResolveInfo> mAllDisplayTargets = ImmutableList.of(); 135 136 /** 137 * Configure an {@link Intent} to be built in to the output target as the resolution for the 138 * requested target data. 139 */ setResolvedIntent(Intent resolvedIntent)140 public Builder setResolvedIntent(Intent resolvedIntent) { 141 mResolvedIntent = resolvedIntent; 142 return this; 143 } 144 145 /** 146 * Configure an {@link Intent} to be built in to the output target as the "base intent to 147 * send," which may be a refinement of any of our source targets. This is private because 148 * it's only used internally by {@link #tryToCloneWithAppliedRefinement()}; if it's ever 149 * expanded, the builder should probably be responsible for enforcing the refinement check. 150 */ setBaseIntentToSend(Intent baseIntent)151 private Builder setBaseIntentToSend(Intent baseIntent) { 152 mBaseIntentToSend = baseIntent; 153 return this; 154 } 155 156 /** 157 * Configure an {@link Intent} to be built in to the output as the "target intent." 158 */ setTargetIntent(Intent targetIntent)159 public Builder setTargetIntent(Intent targetIntent) { 160 mTargetIntent = targetIntent; 161 return this; 162 } 163 164 /** 165 * Configure a fill-in intent provided by the referrer to be used in populating the launch 166 * intent if the output target is ever selected. 167 * 168 * @see android.content.Intent#fillIn(Intent, int) 169 */ setReferrerFillInIntent(@ullable Intent referrerFillInIntent)170 public Builder setReferrerFillInIntent(@Nullable Intent referrerFillInIntent) { 171 mReferrerFillInIntent = referrerFillInIntent; 172 return this; 173 } 174 175 /** 176 * Configure a {@link ComponentName} to be built in to the output target, as the real 177 * component we were able to resolve on this device given the available target data. 178 */ setResolvedComponentName(@ullable ComponentName resolvedComponentName)179 public Builder setResolvedComponentName(@Nullable ComponentName resolvedComponentName) { 180 mResolvedComponentName = resolvedComponentName; 181 return this; 182 } 183 184 /** 185 * Configure a {@link ComponentName} to be built in to the output target, as the component 186 * supposedly associated with a {@link ChooserTarget} from which the builder data is being 187 * derived. 188 */ setChooserTargetComponentName(@ullable ComponentName componentName)189 public Builder setChooserTargetComponentName(@Nullable ComponentName componentName) { 190 mChooserTargetComponentName = componentName; 191 return this; 192 } 193 194 /** Configure the {@link TargetActivityStarter} to be built in to the output target. */ setActivityStarter(TargetActivityStarter activityStarter)195 public Builder setActivityStarter(TargetActivityStarter activityStarter) { 196 mActivityStarter = activityStarter; 197 return this; 198 } 199 200 /** Configure the {@link ResolveInfo} to be built in to the output target. */ setResolveInfo(ResolveInfo resolveInfo)201 public Builder setResolveInfo(ResolveInfo resolveInfo) { 202 mResolveInfo = resolveInfo; 203 return this; 204 } 205 206 /** Configure the display label to be built in to the output target. */ setDisplayLabel(CharSequence displayLabel)207 public Builder setDisplayLabel(CharSequence displayLabel) { 208 mDisplayLabel = displayLabel; 209 return this; 210 } 211 212 /** Configure the extended info to be built in to the output target. */ setExtendedInfo(CharSequence extendedInfo)213 public Builder setExtendedInfo(CharSequence extendedInfo) { 214 mExtendedInfo = extendedInfo; 215 return this; 216 } 217 218 /** Configure the {@link IconHolder} to be built in to the output target. */ setDisplayIconHolder(IconHolder displayIconHolder)219 public Builder setDisplayIconHolder(IconHolder displayIconHolder) { 220 mDisplayIconHolder = displayIconHolder; 221 return this; 222 } 223 224 /** Configure the list of alternate source intents we could resolve for this target. */ setAlternateSourceIntents(List<Intent> sourceIntents)225 public Builder setAlternateSourceIntents(List<Intent> sourceIntents) { 226 mAlternateSourceIntents = immutableCopyOrEmpty(sourceIntents); 227 return this; 228 } 229 230 /** 231 * Configure the full list of source intents we could resolve for this target. This is 232 * effectively the same as calling {@link #setResolvedIntent()} with the first element of 233 * the list, and {@link #setAlternateSourceIntents()} with the remainder (or clearing those 234 * fields on the builder if there are no corresponding elements in the list). 235 */ setAllSourceIntents(List<Intent> sourceIntents)236 public Builder setAllSourceIntents(List<Intent> sourceIntents) { 237 if ((sourceIntents == null) || sourceIntents.isEmpty()) { 238 setResolvedIntent(null); 239 setAlternateSourceIntents(null); 240 return this; 241 } 242 243 setResolvedIntent(sourceIntents.get(0)); 244 setAlternateSourceIntents(sourceIntents.subList(1, sourceIntents.size())); 245 return this; 246 } 247 248 /** Configure the list of display targets to be built in to the output target. */ setAllDisplayTargets(List<DisplayResolveInfo> targets)249 public Builder setAllDisplayTargets(List<DisplayResolveInfo> targets) { 250 mAllDisplayTargets = immutableCopyOrEmpty(targets); 251 return this; 252 } 253 254 /** Configure the is-suspended status to be built in to the output target. */ setIsSuspended(boolean isSuspended)255 public Builder setIsSuspended(boolean isSuspended) { 256 mIsSuspended = isSuspended; 257 return this; 258 } 259 260 /** Configure the is-pinned status to be built in to the output target. */ setIsPinned(boolean isPinned)261 public Builder setIsPinned(boolean isPinned) { 262 mIsPinned = isPinned; 263 return this; 264 } 265 266 /** Configure the modified score to be built in to the output target. */ setModifiedScore(float modifiedScore)267 public Builder setModifiedScore(float modifiedScore) { 268 mModifiedScore = modifiedScore; 269 return this; 270 } 271 272 /** Configure the {@link ShortcutInfo} to be built in to the output target. */ setDirectShareShortcutInfo(@ullable ShortcutInfo shortcutInfo)273 public Builder setDirectShareShortcutInfo(@Nullable ShortcutInfo shortcutInfo) { 274 mDirectShareShortcutInfo = shortcutInfo; 275 return this; 276 } 277 278 /** Configure the {@link AppTarget} to be built in to the output target. */ setDirectShareAppTarget(@ullable AppTarget appTarget)279 public Builder setDirectShareAppTarget(@Nullable AppTarget appTarget) { 280 mDirectShareAppTarget = appTarget; 281 return this; 282 } 283 284 /** Configure the {@link DisplayResolveInfo} to be built in to the output target. */ setDisplayResolveInfo(@ullable DisplayResolveInfo displayResolveInfo)285 public Builder setDisplayResolveInfo(@Nullable DisplayResolveInfo displayResolveInfo) { 286 mDisplayResolveInfo = displayResolveInfo; 287 return this; 288 } 289 290 /** Configure the {@link TargetHashProvider} to be built in to the output target. */ setHashProvider(@ullable TargetHashProvider hashProvider)291 public Builder setHashProvider(@Nullable TargetHashProvider hashProvider) { 292 mHashProvider = hashProvider; 293 return this; 294 } 295 setLegacyType(@onNull LegacyTargetType legacyType)296 Builder setLegacyType(@NonNull LegacyTargetType legacyType) { 297 mLegacyType = legacyType; 298 return this; 299 } 300 301 /** Construct an {@code ImmutableTargetInfo} with the current builder data. */ build()302 public ImmutableTargetInfo build() { 303 List<Intent> sourceIntents = new ArrayList<>(); 304 if (mResolvedIntent != null) { 305 sourceIntents.add(mResolvedIntent); 306 } 307 if (mAlternateSourceIntents != null) { 308 sourceIntents.addAll(mAlternateSourceIntents); 309 } 310 311 Intent baseIntentToSend = mBaseIntentToSend; 312 if ((baseIntentToSend == null) && !sourceIntents.isEmpty()) { 313 baseIntentToSend = sourceIntents.get(0); 314 } 315 if (baseIntentToSend != null) { 316 baseIntentToSend = new Intent(baseIntentToSend); 317 if (mReferrerFillInIntent != null) { 318 baseIntentToSend.fillIn(mReferrerFillInIntent, 0); 319 } 320 } 321 322 return new ImmutableTargetInfo( 323 baseIntentToSend, 324 ImmutableList.copyOf(sourceIntents), 325 mTargetIntent, 326 mReferrerFillInIntent, 327 mResolvedComponentName, 328 mChooserTargetComponentName, 329 mActivityStarter, 330 mResolveInfo, 331 mDisplayLabel, 332 mExtendedInfo, 333 mDisplayIconHolder, 334 mAllDisplayTargets, 335 mIsSuspended, 336 mIsPinned, 337 mModifiedScore, 338 mDirectShareShortcutInfo, 339 mDirectShareAppTarget, 340 mDisplayResolveInfo, 341 mHashProvider, 342 mLegacyType); 343 } 344 } 345 346 @Nullable 347 private final Intent mReferrerFillInIntent; 348 349 @Nullable 350 private final ComponentName mResolvedComponentName; 351 352 @Nullable 353 private final ComponentName mChooserTargetComponentName; 354 355 @Nullable 356 private final ShortcutInfo mDirectShareShortcutInfo; 357 358 @Nullable 359 private final AppTarget mDirectShareAppTarget; 360 361 @Nullable 362 private final DisplayResolveInfo mDisplayResolveInfo; 363 364 @Nullable 365 private final TargetHashProvider mHashProvider; 366 367 private final Intent mBaseIntentToSend; 368 private final ImmutableList<Intent> mSourceIntents; 369 private final Intent mTargetIntent; 370 private final TargetActivityStarter mActivityStarter; 371 private final ResolveInfo mResolveInfo; 372 private final CharSequence mDisplayLabel; 373 private final CharSequence mExtendedInfo; 374 private final IconHolder mDisplayIconHolder; 375 private final ImmutableList<DisplayResolveInfo> mAllDisplayTargets; 376 private final boolean mIsSuspended; 377 private final boolean mIsPinned; 378 private final float mModifiedScore; 379 private final LegacyTargetType mLegacyType; 380 381 /** Construct a {@link Builder}. */ newBuilder()382 public static Builder newBuilder() { 383 return new Builder(); 384 } 385 386 /** Construct a {@link Builder} pre-initialized to match this target. */ toBuilder()387 public Builder toBuilder() { 388 return newBuilder() 389 .setBaseIntentToSend(getBaseIntentToSend()) 390 .setResolvedIntent(getResolvedIntent()) 391 .setTargetIntent(getTargetIntent()) 392 .setReferrerFillInIntent(getReferrerFillInIntent()) 393 .setResolvedComponentName(getResolvedComponentName()) 394 .setChooserTargetComponentName(getChooserTargetComponentName()) 395 .setActivityStarter(mActivityStarter) 396 .setResolveInfo(getResolveInfo()) 397 .setDisplayLabel(getDisplayLabel()) 398 .setExtendedInfo(getExtendedInfo()) 399 .setDisplayIconHolder(getDisplayIconHolder()) 400 .setAllSourceIntents(getAllSourceIntents()) 401 .setAllDisplayTargets(getAllDisplayTargets()) 402 .setIsSuspended(isSuspended()) 403 .setIsPinned(isPinned()) 404 .setModifiedScore(getModifiedScore()) 405 .setDirectShareShortcutInfo(getDirectShareShortcutInfo()) 406 .setDirectShareAppTarget(getDirectShareAppTarget()) 407 .setDisplayResolveInfo(getDisplayResolveInfo()) 408 .setHashProvider(getHashProvider()) 409 .setLegacyType(mLegacyType); 410 } 411 412 @VisibleForTesting getBaseIntentToSend()413 Intent getBaseIntentToSend() { 414 return mBaseIntentToSend; 415 } 416 417 @Override 418 @Nullable tryToCloneWithAppliedRefinement(Intent proposedRefinement)419 public ImmutableTargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) { 420 Intent matchingBase = 421 getAllSourceIntents() 422 .stream() 423 .filter(i -> i.filterEquals(proposedRefinement)) 424 .findFirst() 425 .orElse(null); 426 if (matchingBase == null) { 427 return null; 428 } 429 430 Intent merged = TargetInfo.mergeRefinementIntoMatchingBaseIntent( 431 matchingBase, proposedRefinement); 432 return toBuilder().setBaseIntentToSend(merged).build(); 433 } 434 435 @Override getResolvedIntent()436 public Intent getResolvedIntent() { 437 return (mSourceIntents.isEmpty() ? null : mSourceIntents.get(0)); 438 } 439 440 @Override getTargetIntent()441 public Intent getTargetIntent() { 442 return mTargetIntent; 443 } 444 445 @Nullable getReferrerFillInIntent()446 public Intent getReferrerFillInIntent() { 447 return mReferrerFillInIntent; 448 } 449 450 @Override 451 @Nullable getResolvedComponentName()452 public ComponentName getResolvedComponentName() { 453 return mResolvedComponentName; 454 } 455 456 @Override 457 @Nullable getChooserTargetComponentName()458 public ComponentName getChooserTargetComponentName() { 459 return mChooserTargetComponentName; 460 } 461 462 @Override startAsCaller(Activity activity, Bundle options, int userId)463 public boolean startAsCaller(Activity activity, Bundle options, int userId) { 464 // TODO: make sure that the component name is set in all cases 465 return mActivityStarter.startAsCaller(this, activity, options, userId); 466 } 467 468 @Override startAsUser(Activity activity, Bundle options, UserHandle user)469 public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { 470 // TODO: make sure that the component name is set in all cases 471 return mActivityStarter.startAsUser(this, activity, options, user); 472 } 473 474 @Override getResolveInfo()475 public ResolveInfo getResolveInfo() { 476 return mResolveInfo; 477 } 478 479 @Override getDisplayLabel()480 public CharSequence getDisplayLabel() { 481 return mDisplayLabel; 482 } 483 484 @Override getExtendedInfo()485 public CharSequence getExtendedInfo() { 486 return mExtendedInfo; 487 } 488 489 @Override getDisplayIconHolder()490 public IconHolder getDisplayIconHolder() { 491 return mDisplayIconHolder; 492 } 493 494 @Override getAllSourceIntents()495 public List<Intent> getAllSourceIntents() { 496 return mSourceIntents; 497 } 498 499 @Override getAllDisplayTargets()500 public ArrayList<DisplayResolveInfo> getAllDisplayTargets() { 501 ArrayList<DisplayResolveInfo> targets = new ArrayList<>(); 502 targets.addAll(mAllDisplayTargets); 503 return targets; 504 } 505 506 @Override isSuspended()507 public boolean isSuspended() { 508 return mIsSuspended; 509 } 510 511 @Override isPinned()512 public boolean isPinned() { 513 return mIsPinned; 514 } 515 516 @Override getModifiedScore()517 public float getModifiedScore() { 518 return mModifiedScore; 519 } 520 521 @Override 522 @Nullable getDirectShareShortcutInfo()523 public ShortcutInfo getDirectShareShortcutInfo() { 524 return mDirectShareShortcutInfo; 525 } 526 527 @Override 528 @Nullable getDirectShareAppTarget()529 public AppTarget getDirectShareAppTarget() { 530 return mDirectShareAppTarget; 531 } 532 533 @Override 534 @Nullable getDisplayResolveInfo()535 public DisplayResolveInfo getDisplayResolveInfo() { 536 return mDisplayResolveInfo; 537 } 538 539 @Override getHashedTargetIdForMetrics(Context context)540 public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) { 541 return (mHashProvider == null) 542 ? null : mHashProvider.getHashedTargetIdForMetrics(this, context); 543 } 544 545 @VisibleForTesting 546 @Nullable getHashProvider()547 TargetHashProvider getHashProvider() { 548 return mHashProvider; 549 } 550 551 @Override isEmptyTargetInfo()552 public boolean isEmptyTargetInfo() { 553 return mLegacyType == LegacyTargetType.EMPTY_TARGET_INFO; 554 } 555 556 @Override isPlaceHolderTargetInfo()557 public boolean isPlaceHolderTargetInfo() { 558 return mLegacyType == LegacyTargetType.PLACEHOLDER_TARGET_INFO; 559 } 560 561 @Override isNotSelectableTargetInfo()562 public boolean isNotSelectableTargetInfo() { 563 return isEmptyTargetInfo() || isPlaceHolderTargetInfo(); 564 } 565 566 @Override isSelectableTargetInfo()567 public boolean isSelectableTargetInfo() { 568 return mLegacyType == LegacyTargetType.SELECTABLE_TARGET_INFO; 569 } 570 571 @Override isChooserTargetInfo()572 public boolean isChooserTargetInfo() { 573 return isNotSelectableTargetInfo() || isSelectableTargetInfo(); 574 } 575 576 @Override isMultiDisplayResolveInfo()577 public boolean isMultiDisplayResolveInfo() { 578 return mLegacyType == LegacyTargetType.MULTI_DISPLAY_RESOLVE_INFO; 579 } 580 581 @Override isDisplayResolveInfo()582 public boolean isDisplayResolveInfo() { 583 return (mLegacyType == LegacyTargetType.DISPLAY_RESOLVE_INFO) 584 || isMultiDisplayResolveInfo(); 585 } 586 ImmutableTargetInfo( Intent baseIntentToSend, ImmutableList<Intent> sourceIntents, Intent targetIntent, @Nullable Intent referrerFillInIntent, @Nullable ComponentName resolvedComponentName, @Nullable ComponentName chooserTargetComponentName, TargetActivityStarter activityStarter, ResolveInfo resolveInfo, CharSequence displayLabel, CharSequence extendedInfo, IconHolder iconHolder, ImmutableList<DisplayResolveInfo> allDisplayTargets, boolean isSuspended, boolean isPinned, float modifiedScore, @Nullable ShortcutInfo directShareShortcutInfo, @Nullable AppTarget directShareAppTarget, @Nullable DisplayResolveInfo displayResolveInfo, @Nullable TargetHashProvider hashProvider, LegacyTargetType legacyType)587 private ImmutableTargetInfo( 588 Intent baseIntentToSend, 589 ImmutableList<Intent> sourceIntents, 590 Intent targetIntent, 591 @Nullable Intent referrerFillInIntent, 592 @Nullable ComponentName resolvedComponentName, 593 @Nullable ComponentName chooserTargetComponentName, 594 TargetActivityStarter activityStarter, 595 ResolveInfo resolveInfo, 596 CharSequence displayLabel, 597 CharSequence extendedInfo, 598 IconHolder iconHolder, 599 ImmutableList<DisplayResolveInfo> allDisplayTargets, 600 boolean isSuspended, 601 boolean isPinned, 602 float modifiedScore, 603 @Nullable ShortcutInfo directShareShortcutInfo, 604 @Nullable AppTarget directShareAppTarget, 605 @Nullable DisplayResolveInfo displayResolveInfo, 606 @Nullable TargetHashProvider hashProvider, 607 LegacyTargetType legacyType) { 608 mBaseIntentToSend = baseIntentToSend; 609 mSourceIntents = sourceIntents; 610 mTargetIntent = targetIntent; 611 mReferrerFillInIntent = referrerFillInIntent; 612 mResolvedComponentName = resolvedComponentName; 613 mChooserTargetComponentName = chooserTargetComponentName; 614 mActivityStarter = activityStarter; 615 mResolveInfo = resolveInfo; 616 mDisplayLabel = displayLabel; 617 mExtendedInfo = extendedInfo; 618 mDisplayIconHolder = iconHolder; 619 mAllDisplayTargets = allDisplayTargets; 620 mIsSuspended = isSuspended; 621 mIsPinned = isPinned; 622 mModifiedScore = modifiedScore; 623 mDirectShareShortcutInfo = directShareShortcutInfo; 624 mDirectShareAppTarget = directShareAppTarget; 625 mDisplayResolveInfo = displayResolveInfo; 626 mHashProvider = hashProvider; 627 mLegacyType = legacyType; 628 } 629 immutableCopyOrEmpty(@ullable List<E> source)630 private static <E> ImmutableList<E> immutableCopyOrEmpty(@Nullable List<E> source) { 631 return (source == null) ? ImmutableList.of() : ImmutableList.copyOf(source); 632 } 633 } 634