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