1 /* 2 * Copyright (C) 2019 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; 18 19 import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; 20 import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; 21 import static com.android.intentresolver.Flags.targetHoverAndKeyboardFocusStates; 22 23 import android.app.ActivityManager; 24 import android.app.prediction.AppTarget; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.ActivityInfo; 29 import android.content.pm.LabeledIntent; 30 import android.content.pm.PackageManager; 31 import android.content.pm.ResolveInfo; 32 import android.content.pm.ShortcutInfo; 33 import android.graphics.drawable.Drawable; 34 import android.os.AsyncTask; 35 import android.os.Trace; 36 import android.os.UserHandle; 37 import android.os.UserManager; 38 import android.provider.DeviceConfig; 39 import android.service.chooser.ChooserTarget; 40 import android.text.Layout; 41 import android.text.TextUtils; 42 import android.util.Log; 43 import android.view.View; 44 import android.view.ViewGroup; 45 import android.widget.TextView; 46 47 import androidx.annotation.MainThread; 48 import androidx.annotation.Nullable; 49 import androidx.annotation.WorkerThread; 50 51 import com.android.intentresolver.chooser.DisplayResolveInfo; 52 import com.android.intentresolver.chooser.DisplayResolveInfoAzInfoComparator; 53 import com.android.intentresolver.chooser.MultiDisplayResolveInfo; 54 import com.android.intentresolver.chooser.NotSelectableTargetInfo; 55 import com.android.intentresolver.chooser.SelectableTargetInfo; 56 import com.android.intentresolver.chooser.TargetInfo; 57 import com.android.intentresolver.icons.TargetDataLoader; 58 import com.android.intentresolver.logging.EventLog; 59 import com.android.intentresolver.widget.BadgeTextView; 60 import com.android.internal.annotations.VisibleForTesting; 61 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; 62 63 import com.google.common.collect.ImmutableList; 64 65 import java.util.ArrayList; 66 import java.util.HashSet; 67 import java.util.List; 68 import java.util.Map; 69 import java.util.Objects; 70 import java.util.Set; 71 import java.util.concurrent.Executor; 72 import java.util.stream.Collectors; 73 74 public class ChooserListAdapter extends ResolverListAdapter { 75 76 /** 77 * Delegate interface for injecting a chooser-specific operation to be performed before handling 78 * a package-change event. This allows the "driver" invoking the package-change to be generic, 79 * with no knowledge specific to the chooser implementation. 80 */ 81 public interface PackageChangeCallback { 82 /** Perform any steps necessary before processing the package-change event. */ beforeHandlingPackagesChanged()83 void beforeHandlingPackagesChanged(); 84 } 85 86 private static final String TAG = "ChooserListAdapter"; 87 private static final boolean DEBUG = false; 88 89 public static final int NO_POSITION = -1; 90 public static final int TARGET_BAD = -1; 91 public static final int TARGET_CALLER = 0; 92 public static final int TARGET_SERVICE = 1; 93 public static final int TARGET_STANDARD = 2; 94 public static final int TARGET_STANDARD_AZ = 3; 95 96 private static final int MAX_SUGGESTED_APP_TARGETS = 4; 97 98 /** {@link #getBaseScore} */ 99 public static final float CALLER_TARGET_SCORE_BOOST = 900.f; 100 /** {@link #getBaseScore} */ 101 public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f; 102 103 private final Intent mReferrerFillInIntent; 104 105 private final int mMaxRankedTargets; 106 107 private final EventLog mEventLog; 108 109 private final Set<TargetInfo> mRequestedIcons = new HashSet<>(); 110 111 @Nullable 112 private final PackageChangeCallback mPackageChangeCallback; 113 114 // Reserve spots for incoming direct share targets by adding placeholders 115 private final TargetInfo mPlaceHolderTargetInfo; 116 private final TargetDataLoader mTargetDataLoader; 117 private final List<TargetInfo> mServiceTargets = new ArrayList<>(); 118 private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>(); 119 120 private final ShortcutSelectionLogic mShortcutSelectionLogic; 121 122 // Sorted list of DisplayResolveInfos for the alphabetical app section. 123 private final List<DisplayResolveInfo> mSortedList = new ArrayList<>(); 124 125 private final ItemRevealAnimationTracker mAnimationTracker = new ItemRevealAnimationTracker(); 126 127 /** 128 * Indicates whether the app targets are ready. The flag is reset in 129 * {@link #rebuildList(boolean)} and set to true in {@link #updateAlphabeticalList(Runnable)}'s 130 * onPostExecute. 131 * There's one nuance though, {@link #updateAlphabeticalList(Runnable)} is called by the 132 * {@link ChooserActivity} only when {@link #rebuildList(boolean)} was called with {@code true} 133 * It is called with {@code false} only for inactive tabs in the 134 * MultiProfilePagerAdapter.rebuildTabs which, in turn, is called from either 135 * {@link ChooserActivity#recreatePagerAdapter} or {@link ChooserActivity#configureContentView} 136 * and, in both cases, there are no inactive pages in the MultiProfilePagerAdapter and 137 * {@link #rebuildList(boolean)} will be called with true upon navigation to the missing page. 138 * Yeah. 139 */ 140 private boolean mAppTargetsReady = false; 141 142 // For pinned direct share labels, if the text spans multiple lines, the TextView will consume 143 // the full width, even if the characters actually take up less than that. Measure the actual 144 // line widths and constrain the View's width based upon that so that the pin doesn't end up 145 // very far from the text. 146 private final View.OnLayoutChangeListener mPinTextSpacingListener = 147 new View.OnLayoutChangeListener() { 148 @Override 149 public void onLayoutChange(View v, int left, int top, int right, int bottom, 150 int oldLeft, int oldTop, int oldRight, int oldBottom) { 151 TextView textView = (TextView) v; 152 Layout layout = textView.getLayout(); 153 if (layout != null) { 154 int textWidth = 0; 155 for (int line = 0; line < layout.getLineCount(); line++) { 156 textWidth = Math.max((int) Math.ceil(layout.getLineMax(line)), 157 textWidth); 158 } 159 int desiredWidth = textWidth + textView.getPaddingLeft() 160 + textView.getPaddingRight(); 161 if (textView.getWidth() > desiredWidth) { 162 ViewGroup.LayoutParams params = textView.getLayoutParams(); 163 params.width = desiredWidth; 164 textView.setLayoutParams(params); 165 // Need to wait until layout pass is over before requesting layout. 166 textView.post(() -> textView.requestLayout()); 167 } 168 textView.removeOnLayoutChangeListener(this); 169 } 170 } 171 }; 172 173 private boolean mAnimateItems = true; 174 private boolean mTargetsEnabled = true; 175 private boolean mDirectTargetsEnabled = true; 176 ChooserListAdapter( Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, UserHandle userHandle, Intent targetIntent, Intent referrerFillInIntent, ResolverListCommunicator resolverListCommunicator, PackageManager packageManager, EventLog eventLog, int maxRankedTargets, UserHandle initialIntentsUserSpace, TargetDataLoader targetDataLoader, @Nullable PackageChangeCallback packageChangeCallback)177 public ChooserListAdapter( 178 Context context, 179 List<Intent> payloadIntents, 180 Intent[] initialIntents, 181 List<ResolveInfo> rList, 182 boolean filterLastUsed, 183 ResolverListController resolverListController, 184 UserHandle userHandle, 185 Intent targetIntent, 186 Intent referrerFillInIntent, 187 ResolverListCommunicator resolverListCommunicator, 188 PackageManager packageManager, 189 EventLog eventLog, 190 int maxRankedTargets, 191 UserHandle initialIntentsUserSpace, 192 TargetDataLoader targetDataLoader, 193 @Nullable PackageChangeCallback packageChangeCallback) { 194 this( 195 context, 196 payloadIntents, 197 initialIntents, 198 rList, 199 filterLastUsed, 200 resolverListController, 201 userHandle, 202 targetIntent, 203 referrerFillInIntent, 204 resolverListCommunicator, 205 packageManager, 206 eventLog, 207 maxRankedTargets, 208 initialIntentsUserSpace, 209 targetDataLoader, 210 packageChangeCallback, 211 AsyncTask.SERIAL_EXECUTOR, 212 context.getMainExecutor() 213 ); 214 } 215 216 @VisibleForTesting ChooserListAdapter( Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, UserHandle userHandle, Intent targetIntent, Intent referrerFillInIntent, ResolverListCommunicator resolverListCommunicator, PackageManager packageManager, EventLog eventLog, int maxRankedTargets, UserHandle initialIntentsUserSpace, TargetDataLoader targetDataLoader, @Nullable PackageChangeCallback packageChangeCallback, Executor bgExecutor, Executor mainExecutor)217 public ChooserListAdapter( 218 Context context, 219 List<Intent> payloadIntents, 220 Intent[] initialIntents, 221 List<ResolveInfo> rList, 222 boolean filterLastUsed, 223 ResolverListController resolverListController, 224 UserHandle userHandle, 225 Intent targetIntent, 226 Intent referrerFillInIntent, 227 ResolverListCommunicator resolverListCommunicator, 228 PackageManager packageManager, 229 EventLog eventLog, 230 int maxRankedTargets, 231 UserHandle initialIntentsUserSpace, 232 TargetDataLoader targetDataLoader, 233 @Nullable PackageChangeCallback packageChangeCallback, 234 Executor bgExecutor, 235 Executor mainExecutor) { 236 // Don't send the initial intents through the shared ResolverActivity path, 237 // we want to separate them into a different section. 238 super( 239 context, 240 payloadIntents, 241 null, 242 rList, 243 filterLastUsed, 244 resolverListController, 245 userHandle, 246 targetIntent, 247 resolverListCommunicator, 248 initialIntentsUserSpace, 249 targetDataLoader, 250 bgExecutor, 251 mainExecutor); 252 253 mMaxRankedTargets = maxRankedTargets; 254 mReferrerFillInIntent = referrerFillInIntent; 255 256 mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context); 257 mTargetDataLoader = targetDataLoader; 258 mPackageChangeCallback = packageChangeCallback; 259 createPlaceHolders(); 260 mEventLog = eventLog; 261 mShortcutSelectionLogic = new ShortcutSelectionLogic( 262 context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp), 263 DeviceConfig.getBoolean( 264 DeviceConfig.NAMESPACE_SYSTEMUI, 265 SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, 266 true) 267 ); 268 269 if (initialIntents != null) { 270 for (int i = 0; i < initialIntents.length; i++) { 271 final Intent ii = initialIntents[i]; 272 if (ii == null) { 273 continue; 274 } 275 276 // We reimplement Intent#resolveActivityInfo here because if we have an 277 // implicit intent, we want the ResolveInfo returned by PackageManager 278 // instead of one we reconstruct ourselves. The ResolveInfo returned might 279 // have extra metadata and resolvePackageName set and we want to respect that. 280 ResolveInfo ri = null; 281 ActivityInfo ai = null; 282 final ComponentName cn = ii.getComponent(); 283 if (cn != null) { 284 try { 285 ai = packageManager.getActivityInfo( 286 ii.getComponent(), 287 PackageManager.ComponentInfoFlags.of(PackageManager.GET_META_DATA)); 288 ri = new ResolveInfo(); 289 ri.activityInfo = ai; 290 } catch (PackageManager.NameNotFoundException ignored) { 291 // ai will == null below 292 } 293 } 294 if (ai == null) { 295 // Because of AIDL bug, resolveActivity can't accept subclasses of Intent. 296 final Intent rii = (ii.getClass() == Intent.class) ? ii : new Intent(ii); 297 ri = packageManager.resolveActivity( 298 rii, 299 PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY)); 300 ai = ri != null ? ri.activityInfo : null; 301 } 302 if (ai == null) { 303 Log.w(TAG, "No activity found for " + ii); 304 continue; 305 } 306 UserManager userManager = 307 (UserManager) context.getSystemService(Context.USER_SERVICE); 308 if (ii instanceof LabeledIntent) { 309 LabeledIntent li = (LabeledIntent) ii; 310 ri.resolvePackageName = li.getSourcePackage(); 311 ri.labelRes = li.getLabelResource(); 312 ri.nonLocalizedLabel = li.getNonLocalizedLabel(); 313 ri.icon = li.getIconResource(); 314 ri.iconResourceId = ri.icon; 315 } 316 if (userManager.isManagedProfile()) { 317 ri.noResourceId = true; 318 ri.icon = 0; 319 } 320 ri.userHandle = initialIntentsUserSpace; 321 DisplayResolveInfo displayResolveInfo = 322 DisplayResolveInfo.newDisplayResolveInfo(ii, ri, ii); 323 mCallerTargets.add(displayResolveInfo); 324 if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break; 325 } 326 } 327 } 328 329 /** 330 * @return {@code true} if the app targets are ready. 331 */ areAppTargetsReady()332 public final boolean areAppTargetsReady() { 333 return mAppTargetsReady; 334 } 335 336 /** 337 * Set the enabled state for all targets. 338 */ setTargetsEnabled(boolean isEnabled)339 public void setTargetsEnabled(boolean isEnabled) { 340 if (mTargetsEnabled != isEnabled) { 341 mTargetsEnabled = isEnabled; 342 notifyDataSetChanged(); 343 } 344 } 345 346 /** 347 * Set the enabled state for direct targets. 348 */ setDirectTargetsEnabled(boolean isEnabled)349 public void setDirectTargetsEnabled(boolean isEnabled) { 350 if (mDirectTargetsEnabled != isEnabled) { 351 mDirectTargetsEnabled = isEnabled; 352 if (!mServiceTargets.isEmpty() && !isDirectTargetRowEmptyState()) { 353 notifyDataSetChanged(); 354 } 355 } 356 } 357 setAnimateItems(boolean animateItems)358 public void setAnimateItems(boolean animateItems) { 359 mAnimateItems = animateItems; 360 } 361 362 @Override handlePackagesChanged()363 public void handlePackagesChanged() { 364 if (mPackageChangeCallback != null) { 365 mPackageChangeCallback.beforeHandlingPackagesChanged(); 366 } 367 if (DEBUG) { 368 Log.d(TAG, "clearing queryTargets on package change"); 369 } 370 createPlaceHolders(); 371 mResolverListCommunicator.onHandlePackagesChanged(this); 372 373 } 374 375 @Override rebuildList(boolean doPostProcessing)376 public boolean rebuildList(boolean doPostProcessing) { 377 mAnimationTracker.reset(); 378 mSortedList.clear(); 379 mAppTargetsReady = false; 380 boolean result = super.rebuildList(doPostProcessing); 381 notifyDataSetChanged(); 382 return result; 383 } 384 createPlaceHolders()385 private void createPlaceHolders() { 386 mServiceTargets.clear(); 387 for (int i = 0; i < mMaxRankedTargets; ++i) { 388 mServiceTargets.add(mPlaceHolderTargetInfo); 389 } 390 } 391 392 @Override onCreateView(ViewGroup parent)393 View onCreateView(ViewGroup parent) { 394 int layout = targetHoverAndKeyboardFocusStates() 395 ? R.layout.chooser_grid_item_hover 396 : R.layout.chooser_grid_item; 397 return mInflater.inflate(layout, parent, false); 398 } 399 400 @Override onDestroy()401 public void onDestroy() { 402 super.onDestroy(); 403 notifyDataSetChanged(); 404 } 405 406 @VisibleForTesting 407 @Override onBindView(View view, TargetInfo info, int position)408 public void onBindView(View view, TargetInfo info, int position) { 409 final boolean isEnabled = !isDestroyed() && mTargetsEnabled; 410 view.setEnabled(isEnabled); 411 final ViewHolder holder = (ViewHolder) view.getTag(); 412 413 resetViewHolder(holder); 414 // Always remove the spacing listener, attach as needed to direct share targets below. 415 holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener); 416 417 if (info == null) { 418 holder.icon.setImageDrawable(loadIconPlaceholder()); 419 return; 420 } 421 422 final CharSequence displayLabel = Objects.requireNonNullElse(info.getDisplayLabel(), ""); 423 final CharSequence extendedInfo = Objects.requireNonNullElse(info.getExtendedInfo(), ""); 424 holder.bindLabel(displayLabel, extendedInfo); 425 if (mAnimateItems && !TextUtils.isEmpty(displayLabel)) { 426 mAnimationTracker.animateLabel(holder.text, info); 427 } 428 if (mAnimateItems 429 && !TextUtils.isEmpty(extendedInfo) 430 && holder.text2.getVisibility() == View.VISIBLE) { 431 mAnimationTracker.animateLabel(holder.text2, info); 432 } 433 434 if (info.isSelectableTargetInfo()) { 435 view.setEnabled(isEnabled && mDirectTargetsEnabled); 436 // direct share targets should append the application name for a better readout 437 DisplayResolveInfo rInfo = info.getDisplayResolveInfo(); 438 CharSequence appName = 439 Objects.requireNonNullElse(rInfo == null ? null : rInfo.getDisplayLabel(), ""); 440 String contentDescription = 441 String.join(" ", info.getDisplayLabel(), extendedInfo, appName); 442 if (info.isPinned()) { 443 contentDescription = String.join( 444 ". ", 445 contentDescription, 446 mContext.getResources().getString(R.string.pinned)); 447 } 448 updateContentDescription(holder, contentDescription); 449 if (!info.hasDisplayIcon()) { 450 loadDirectShareIcon((SelectableTargetInfo) info); 451 } 452 } else if (info.isDisplayResolveInfo()) { 453 if (info.isPinned()) { 454 updateContentDescription( 455 holder, 456 String.join( 457 ". ", 458 info.getDisplayLabel(), 459 mContext.getResources().getString(R.string.pinned))); 460 } 461 DisplayResolveInfo dri = (DisplayResolveInfo) info; 462 if (!dri.hasDisplayIcon()) { 463 loadIcon(dri); 464 } 465 if (!dri.hasDisplayLabel()) { 466 loadLabel(dri); 467 } 468 } 469 470 holder.bindIcon(info, mTargetsEnabled); 471 if (mAnimateItems && info.hasDisplayIcon()) { 472 mAnimationTracker.animateIcon(holder.icon, info); 473 } 474 475 if (info.isPlaceHolderTargetInfo()) { 476 bindPlaceholder(holder); 477 } 478 479 if (info.isMultiDisplayResolveInfo()) { 480 // If the target is grouped show an indicator 481 bindGroupIndicator( 482 holder, 483 mContext.getDrawable(R.drawable.chooser_group_background)); 484 } else if (info.isPinned() && (getPositionTargetType(position) == TARGET_STANDARD 485 || getPositionTargetType(position) == TARGET_SERVICE)) { 486 // If the appShare or directShare target is pinned and in the suggested row show a 487 // pinned indicator 488 bindPinnedIndicator(holder, mContext.getDrawable(R.drawable.chooser_pinned_background)); 489 holder.text.addOnLayoutChangeListener(mPinTextSpacingListener); 490 } 491 } 492 resetViewHolder(ViewHolder holder)493 private void resetViewHolder(ViewHolder holder) { 494 holder.reset(); 495 holder.itemView.setBackground(holder.defaultItemViewBackground); 496 497 ((BadgeTextView) holder.text).setBadgeDrawable(null); 498 holder.text.setBackground(null); 499 holder.text.setPaddingRelative(0, 0, 0, 0); 500 } 501 updateContentDescription(ViewHolder holder, String description)502 private void updateContentDescription(ViewHolder holder, String description) { 503 holder.itemView.setContentDescription(description); 504 } 505 bindPlaceholder(ViewHolder holder)506 private void bindPlaceholder(ViewHolder holder) { 507 holder.itemView.setBackground(null); 508 } 509 bindGroupIndicator(ViewHolder holder, Drawable indicator)510 private void bindGroupIndicator(ViewHolder holder, Drawable indicator) { 511 ((BadgeTextView) holder.text).setBadgeDrawable(indicator); 512 } 513 bindPinnedIndicator(ViewHolder holder, Drawable indicator)514 private void bindPinnedIndicator(ViewHolder holder, Drawable indicator) { 515 holder.text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0); 516 holder.text.setBackground(indicator); 517 } 518 loadDirectShareIcon(SelectableTargetInfo info)519 private void loadDirectShareIcon(SelectableTargetInfo info) { 520 if (mRequestedIcons.add(info)) { 521 Drawable icon = mTargetDataLoader.getOrLoadDirectShareIcon( 522 info, 523 getUserHandle(), 524 (drawable) -> onDirectShareIconLoaded(info, drawable, true)); 525 if (icon != null) { 526 onDirectShareIconLoaded(info, icon, false); 527 } 528 } 529 } 530 onDirectShareIconLoaded( SelectableTargetInfo mTargetInfo, @Nullable Drawable icon, boolean notify)531 private void onDirectShareIconLoaded( 532 SelectableTargetInfo mTargetInfo, @Nullable Drawable icon, boolean notify) { 533 if (icon != null && !mTargetInfo.hasDisplayIcon()) { 534 mTargetInfo.getDisplayIconHolder().setDisplayIcon(icon); 535 if (notify) { 536 notifyDataSetChanged(); 537 } 538 } 539 } 540 541 /** 542 * Group application targets 543 */ updateAlphabeticalList(boolean rebuildComplete, Runnable onCompleted)544 public void updateAlphabeticalList(boolean rebuildComplete, Runnable onCompleted) { 545 if (getDisplayResolveInfoCount() == 0) { 546 Log.d(TAG, "getDisplayResolveInfoCount() == 0"); 547 if (rebuildComplete) { 548 mAppTargetsReady = true; 549 onCompleted.run(); 550 } 551 notifyDataSetChanged(); 552 return; 553 } 554 final DisplayResolveInfoAzInfoComparator 555 comparator = new DisplayResolveInfoAzInfoComparator(mContext); 556 ImmutableList<DisplayResolveInfo> displayList = getTargetsInCurrentDisplayList(); 557 final List<DisplayResolveInfo> allTargets = 558 new ArrayList<>(displayList.size() + mCallerTargets.size()); 559 allTargets.addAll(displayList); 560 allTargets.addAll(mCallerTargets); 561 562 new AsyncTask<Void, Void, List<DisplayResolveInfo>>() { 563 @Override 564 protected List<DisplayResolveInfo> doInBackground(Void... voids) { 565 try { 566 Trace.beginSection("update-alphabetical-list"); 567 return updateList(); 568 } finally { 569 Trace.endSection(); 570 } 571 } 572 573 private List<DisplayResolveInfo> updateList() { 574 loadMissingLabels(allTargets); 575 576 // Consolidate multiple targets from same app. 577 return allTargets 578 .stream() 579 .map(appTarget -> { 580 if (targetHoverAndKeyboardFocusStates()) { 581 // Icon drawables are effectively cached per target info. 582 // Without cloning target infos, the same target info could be used 583 // for two different positions in the grid: once in the ranked 584 // targets row (from ResolverListAdapter#mDisplayList or 585 // #mCallerTargets, see #getItem()) and again in the all-app-target 586 // grid (copied from #mDisplayList and #mCallerTargets to 587 // #mSortedList). 588 // Using the same drawable for two list items would result in visual 589 // effects being applied to both simultaneously. 590 DisplayResolveInfo copy = appTarget.copy(); 591 copy.getDisplayIconHolder().setDisplayIcon(null); 592 return copy; 593 } else { 594 return appTarget; 595 } 596 }) 597 .collect(Collectors.groupingBy(target -> 598 target.getResolvedComponentName().getPackageName() 599 + "#" + target.getDisplayLabel() 600 + '#' + target.getResolveInfo().userHandle.getIdentifier() 601 )) 602 .values() 603 .stream() 604 .map(appTargets -> 605 (appTargets.size() == 1) 606 ? appTargets.get(0) 607 : MultiDisplayResolveInfo.newMultiDisplayResolveInfo( 608 appTargets)) 609 .sorted(comparator) 610 .collect(Collectors.toList()); 611 } 612 613 @Override 614 protected void onPostExecute(List<DisplayResolveInfo> newList) { 615 mSortedList.clear(); 616 mSortedList.addAll(newList); 617 mAppTargetsReady = true; 618 notifyDataSetChanged(); 619 onCompleted.run(); 620 } 621 622 private void loadMissingLabels(List<DisplayResolveInfo> targets) { 623 for (DisplayResolveInfo target: targets) { 624 mTargetDataLoader.getOrLoadLabel(target); 625 } 626 } 627 }.execute(); 628 } 629 630 @Override 631 public int getCount() { 632 return getRankedTargetCount() + getAlphaTargetCount() 633 + getSelectableServiceTargetCount() + getCallerTargetCount(); 634 } 635 636 @Override 637 public int getUnfilteredCount() { 638 int appTargets = super.getUnfilteredCount(); 639 if (appTargets > mMaxRankedTargets) { 640 // TODO: what does this condition mean? 641 appTargets = appTargets + mMaxRankedTargets; 642 } 643 return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount(); 644 } 645 646 647 public int getCallerTargetCount() { 648 return mCallerTargets.size(); 649 } 650 651 /** 652 * Filter out placeholders and non-selectable service targets 653 */ 654 public int getSelectableServiceTargetCount() { 655 int count = 0; 656 for (TargetInfo info : mServiceTargets) { 657 if (info.isSelectableTargetInfo()) { 658 count++; 659 } 660 } 661 return count; 662 } 663 664 private static boolean hasSendAction(Intent intent) { 665 String action = intent.getAction(); 666 return Intent.ACTION_SEND.equals(action) 667 || Intent.ACTION_SEND_MULTIPLE.equals(action); 668 } 669 670 public int getServiceTargetCount() { 671 if (hasSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) { 672 return Math.min(mServiceTargets.size(), mMaxRankedTargets); 673 } 674 675 return 0; 676 } 677 678 public int getAlphaTargetCount() { 679 int groupedCount = mSortedList.size(); 680 int ungroupedCount = mCallerTargets.size() + getDisplayResolveInfoCount(); 681 return (ungroupedCount > mMaxRankedTargets) ? groupedCount : 0; 682 } 683 684 /** 685 * Fetch ranked app target count 686 */ 687 public int getRankedTargetCount() { 688 int spacesAvailable = mMaxRankedTargets - getCallerTargetCount(); 689 return Math.min(spacesAvailable, super.getCount()); 690 } 691 692 /** Get all the {@link DisplayResolveInfo} data for our targets. */ 693 public DisplayResolveInfo[] getDisplayResolveInfos() { 694 int size = getDisplayResolveInfoCount(); 695 DisplayResolveInfo[] resolvedTargets = new DisplayResolveInfo[size]; 696 for (int i = 0; i < size; i++) { 697 resolvedTargets[i] = getDisplayResolveInfo(i); 698 } 699 return resolvedTargets; 700 } 701 702 public int getPositionTargetType(int position) { 703 int offset = 0; 704 705 final int serviceTargetCount = getServiceTargetCount(); 706 if (position < serviceTargetCount) { 707 return TARGET_SERVICE; 708 } 709 offset += serviceTargetCount; 710 711 final int callerTargetCount = getCallerTargetCount(); 712 if (position - offset < callerTargetCount) { 713 return TARGET_CALLER; 714 } 715 offset += callerTargetCount; 716 717 final int rankedTargetCount = getRankedTargetCount(); 718 if (position - offset < rankedTargetCount) { 719 return TARGET_STANDARD; 720 } 721 offset += rankedTargetCount; 722 723 final int standardTargetCount = getAlphaTargetCount(); 724 if (position - offset < standardTargetCount) { 725 return TARGET_STANDARD_AZ; 726 } 727 728 return TARGET_BAD; 729 } 730 731 @Override 732 public TargetInfo getItem(int position) { 733 return targetInfoForPosition(position, true); 734 } 735 736 /** 737 * Find target info for a given position. 738 * Since ChooserActivity displays several sections of content, determine which 739 * section provides this item. 740 */ 741 @Override 742 public TargetInfo targetInfoForPosition(int position, boolean filtered) { 743 if (position == NO_POSITION) { 744 return null; 745 } 746 747 int offset = 0; 748 749 // Direct share targets 750 final int serviceTargetCount = filtered ? getServiceTargetCount() : 751 getSelectableServiceTargetCount(); 752 if (position < serviceTargetCount) { 753 return mServiceTargets.get(position); 754 } 755 offset += serviceTargetCount; 756 757 // Targets provided by calling app 758 final int callerTargetCount = getCallerTargetCount(); 759 if (position - offset < callerTargetCount) { 760 return mCallerTargets.get(position - offset); 761 } 762 offset += callerTargetCount; 763 764 // Ranked standard app targets 765 final int rankedTargetCount = getRankedTargetCount(); 766 if (position - offset < rankedTargetCount) { 767 return filtered ? super.getItem(position - offset) 768 : getDisplayResolveInfo(position - offset); 769 } 770 offset += rankedTargetCount; 771 772 // Alphabetical complete app target list. 773 if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) { 774 return mSortedList.get(position - offset); 775 } 776 777 return null; 778 } 779 780 // Check whether {@code dri} should be added into mDisplayList. 781 @Override 782 protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) { 783 // Checks if this info is already listed in callerTargets. 784 for (TargetInfo existingInfo : mCallerTargets) { 785 if (ResolveInfoHelpers.resolveInfoMatch( 786 dri.getResolveInfo(), existingInfo.getResolveInfo())) { 787 return false; 788 } 789 } 790 return super.shouldAddResolveInfo(dri); 791 } 792 793 /** 794 * Fetch surfaced direct share target info 795 */ 796 public List<TargetInfo> getSurfacedTargetInfo() { 797 return mServiceTargets.subList(0, 798 Math.min(mMaxRankedTargets, getSelectableServiceTargetCount())); 799 } 800 801 802 /** 803 * Evaluate targets for inclusion in the direct share area. May not be included 804 * if score is too low. 805 */ 806 public void addServiceResults( 807 @Nullable DisplayResolveInfo origTarget, 808 List<ChooserTarget> targets, 809 int targetType, 810 Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos, 811 Map<ChooserTarget, AppTarget> directShareToAppTargets) { 812 // Avoid inserting any potentially late results. 813 if (isDirectTargetRowEmptyState()) { 814 return; 815 } 816 boolean isShortcutResult = targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER 817 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; 818 boolean isUpdated = mShortcutSelectionLogic.addServiceResults( 819 origTarget, 820 getBaseScore(origTarget, targetType), 821 targets, 822 isShortcutResult, 823 directShareToShortcutInfos, 824 directShareToAppTargets, 825 mContext.createContextAsUser(getUserHandle(), 0), 826 getTargetIntent(), 827 mReferrerFillInIntent, 828 mMaxRankedTargets, 829 mServiceTargets); 830 if (isUpdated) { 831 notifyDataSetChanged(); 832 } 833 } 834 835 /** 836 * Copy direct targets from another ChooserListAdapter instance 837 */ 838 public void copyDirectTargetsFrom(ChooserListAdapter adapter) { 839 if (adapter.isDirectTargetRowEmptyState()) { 840 return; 841 } 842 843 mServiceTargets.clear(); 844 mServiceTargets.addAll(adapter.mServiceTargets); 845 } 846 847 /** 848 * Reset direct targets 849 */ 850 public void resetDirectTargets() { 851 createPlaceHolders(); 852 } 853 854 private boolean isDirectTargetRowEmptyState() { 855 return (mServiceTargets.size() == 1) && mServiceTargets.get(0).isEmptyTargetInfo(); 856 } 857 858 /** 859 * Use the scoring system along with artificial boosts to create up to 4 distinct buckets: 860 * <ol> 861 * <li>App-supplied targets 862 * <li>Shortcuts ranked via App Prediction Manager 863 * <li>Shortcuts ranked via legacy heuristics 864 * <li>Legacy direct share targets 865 * </ol> 866 */ 867 public float getBaseScore( 868 DisplayResolveInfo target, 869 int targetType) { 870 if (target == null) { 871 return CALLER_TARGET_SCORE_BOOST; 872 } 873 float score = super.getScore(target); 874 if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER 875 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) { 876 return score * SHORTCUT_TARGET_SCORE_BOOST; 877 } 878 return score; 879 } 880 881 /** 882 * Calling this marks service target loading complete, and will attempt to no longer 883 * update the direct share area. 884 */ 885 public void completeServiceTargetLoading() { 886 mServiceTargets.removeIf(o -> o.isPlaceHolderTargetInfo()); 887 if (mServiceTargets.isEmpty()) { 888 mServiceTargets.add(NotSelectableTargetInfo.newEmptyTargetInfo()); 889 mEventLog.logSharesheetEmptyDirectShareRow(); 890 } 891 notifyDataSetChanged(); 892 } 893 894 /** 895 * Rather than fully sorting the input list, this sorting task will put the top k elements 896 * in the head of input list and fill the tail with other elements in undetermined order. 897 */ 898 @Override 899 @WorkerThread 900 protected void sortComponents(List<ResolvedComponentInfo> components) { 901 Trace.beginSection("ChooserListAdapter#SortingTask"); 902 mResolverListController.topK(components, mMaxRankedTargets); 903 Trace.endSection(); 904 } 905 906 @Override 907 @MainThread 908 protected void onComponentsSorted( 909 @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) { 910 processSortedList(sortedComponents, doPostProcessing); 911 if (doPostProcessing) { 912 notifyDataSetChanged(); 913 } 914 } 915 } 916