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.internal.app; 18 19 import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad; 20 21 import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; 22 import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; 23 24 import android.annotation.Nullable; 25 import android.app.prediction.AppPredictor; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.ActivityInfo; 30 import android.content.pm.LabeledIntent; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.content.pm.ShortcutInfo; 34 import android.graphics.drawable.Drawable; 35 import android.os.AsyncTask; 36 import android.os.Trace; 37 import android.os.UserHandle; 38 import android.os.UserManager; 39 import android.provider.DeviceConfig; 40 import android.service.chooser.ChooserTarget; 41 import android.text.Layout; 42 import android.util.Log; 43 import android.view.View; 44 import android.view.ViewGroup; 45 import android.widget.TextView; 46 47 import com.android.internal.R; 48 import com.android.internal.annotations.VisibleForTesting; 49 import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; 50 import com.android.internal.app.chooser.ChooserTargetInfo; 51 import com.android.internal.app.chooser.DisplayResolveInfo; 52 import com.android.internal.app.chooser.MultiDisplayResolveInfo; 53 import com.android.internal.app.chooser.SelectableTargetInfo; 54 import com.android.internal.app.chooser.TargetInfo; 55 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; 56 57 import java.util.ArrayList; 58 import java.util.Collections; 59 import java.util.HashMap; 60 import java.util.List; 61 import java.util.Map; 62 import java.util.function.Consumer; 63 64 public class ChooserListAdapter extends ResolverListAdapter { 65 private static final String TAG = "ChooserListAdapter"; 66 private static final boolean DEBUG = false; 67 68 private boolean mEnableStackedApps = true; 69 70 public static final int NO_POSITION = -1; 71 public static final int TARGET_BAD = -1; 72 public static final int TARGET_CALLER = 0; 73 public static final int TARGET_SERVICE = 1; 74 public static final int TARGET_STANDARD = 2; 75 public static final int TARGET_STANDARD_AZ = 3; 76 77 private static final int MAX_SUGGESTED_APP_TARGETS = 4; 78 private static final int MAX_CHOOSER_TARGETS_PER_APP = 2; 79 80 /** {@link #getBaseScore} */ 81 public static final float CALLER_TARGET_SCORE_BOOST = 900.f; 82 /** {@link #getBaseScore} */ 83 public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f; 84 private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f; 85 86 private final int mMaxShortcutTargetsPerApp; 87 private final ChooserListCommunicator mChooserListCommunicator; 88 private final SelectableTargetInfo.SelectableTargetInfoCommunicator 89 mSelectableTargetInfoCommunicator; 90 private final ChooserActivityLogger mChooserActivityLogger; 91 92 private int mNumShortcutResults = 0; 93 private final Map<SelectableTargetInfo, LoadDirectShareIconTask> mIconLoaders = new HashMap<>(); 94 private boolean mApplySharingAppLimits; 95 96 // Reserve spots for incoming direct share targets by adding placeholders 97 private ChooserTargetInfo 98 mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo(); 99 private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>(); 100 private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>(); 101 102 private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator = 103 new ChooserActivity.BaseChooserTargetComparator(); 104 private boolean mListViewDataChanged = false; 105 106 // Sorted list of DisplayResolveInfos for the alphabetical app section. 107 private List<DisplayResolveInfo> mSortedList = new ArrayList<>(); 108 private AppPredictor mAppPredictor; 109 private ResolverAppPredictorCallback mAppPredictorCallbackWrapper; 110 private AppPredictor.Callback mAppPredictorCallback; 111 112 // Represents the UserSpace in which the Initial Intents should be resolved. 113 private final UserHandle mInitialIntentsUserSpace; 114 115 @Nullable 116 private Consumer<DisplayResolveInfo> mOnIconLoadedListener; 117 118 // For pinned direct share labels, if the text spans multiple lines, the TextView will consume 119 // the full width, even if the characters actually take up less than that. Measure the actual 120 // line widths and constrain the View's width based upon that so that the pin doesn't end up 121 // very far from the text. 122 private final View.OnLayoutChangeListener mPinTextSpacingListener = 123 new View.OnLayoutChangeListener() { 124 @Override 125 public void onLayoutChange(View v, int left, int top, int right, int bottom, 126 int oldLeft, int oldTop, int oldRight, int oldBottom) { 127 TextView textView = (TextView) v; 128 Layout layout = textView.getLayout(); 129 if (layout != null) { 130 int textWidth = 0; 131 for (int line = 0; line < layout.getLineCount(); line++) { 132 textWidth = Math.max((int) Math.ceil(layout.getLineMax(line)), 133 textWidth); 134 } 135 int desiredWidth = textWidth + textView.getPaddingLeft() 136 + textView.getPaddingRight(); 137 if (textView.getWidth() > desiredWidth) { 138 ViewGroup.LayoutParams params = textView.getLayoutParams(); 139 params.width = desiredWidth; 140 textView.setLayoutParams(params); 141 // Need to wait until layout pass is over before requesting layout. 142 textView.post(() -> textView.requestLayout()); 143 } 144 textView.removeOnLayoutChangeListener(this); 145 } 146 } 147 }; 148 ChooserListAdapter(Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, ChooserListCommunicator chooserListCommunicator, SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator, PackageManager packageManager, ChooserActivityLogger chooserActivityLogger, UserHandle initialIntentsUserSpace)149 public ChooserListAdapter(Context context, List<Intent> payloadIntents, 150 Intent[] initialIntents, List<ResolveInfo> rList, 151 boolean filterLastUsed, ResolverListController resolverListController, 152 ChooserListCommunicator chooserListCommunicator, 153 SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator, 154 PackageManager packageManager, 155 ChooserActivityLogger chooserActivityLogger, 156 UserHandle initialIntentsUserSpace) { 157 // Don't send the initial intents through the shared ResolverActivity path, 158 // we want to separate them into a different section. 159 super(context, payloadIntents, null, rList, filterLastUsed, 160 resolverListController, chooserListCommunicator, false, initialIntentsUserSpace); 161 162 mMaxShortcutTargetsPerApp = 163 context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp); 164 mChooserListCommunicator = chooserListCommunicator; 165 createPlaceHolders(); 166 mSelectableTargetInfoCommunicator = selectableTargetInfoCommunicator; 167 mChooserActivityLogger = chooserActivityLogger; 168 mInitialIntentsUserSpace = initialIntentsUserSpace; 169 170 if (initialIntents != null) { 171 for (int i = 0; i < initialIntents.length; i++) { 172 final Intent ii = initialIntents[i]; 173 if (ii == null) { 174 continue; 175 } 176 177 // We reimplement Intent#resolveActivityInfo here because if we have an 178 // implicit intent, we want the ResolveInfo returned by PackageManager 179 // instead of one we reconstruct ourselves. The ResolveInfo returned might 180 // have extra metadata and resolvePackageName set and we want to respect that. 181 ResolveInfo ri = null; 182 ActivityInfo ai = null; 183 final ComponentName cn = ii.getComponent(); 184 if (cn != null) { 185 try { 186 ai = packageManager.getActivityInfo(ii.getComponent(), 0); 187 ri = new ResolveInfo(); 188 ri.activityInfo = ai; 189 } catch (PackageManager.NameNotFoundException ignored) { 190 // ai will == null below 191 } 192 } 193 if (ai == null) { 194 // Because of AIDL bug, resolveActivity can't accept subclasses of Intent. 195 final Intent rii = (ii.getClass() == Intent.class) ? ii : new Intent(ii); 196 ri = packageManager.resolveActivity(rii, PackageManager.MATCH_DEFAULT_ONLY); 197 ai = ri != null ? ri.activityInfo : null; 198 } 199 if (ai == null) { 200 Log.w(TAG, "No activity found for " + ii); 201 continue; 202 } 203 UserManager userManager = 204 (UserManager) context.getSystemService(Context.USER_SERVICE); 205 if (ii instanceof LabeledIntent) { 206 LabeledIntent li = (LabeledIntent) ii; 207 ri.resolvePackageName = li.getSourcePackage(); 208 ri.labelRes = li.getLabelResource(); 209 ri.nonLocalizedLabel = li.getNonLocalizedLabel(); 210 ri.icon = li.getIconResource(); 211 ri.iconResourceId = ri.icon; 212 } 213 if (userManager.isManagedProfile()) { 214 ri.noResourceId = true; 215 ri.icon = 0; 216 } 217 ri.userHandle = mInitialIntentsUserSpace; 218 mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri))); 219 if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break; 220 } 221 } 222 mApplySharingAppLimits = DeviceConfig.getBoolean( 223 DeviceConfig.NAMESPACE_SYSTEMUI, 224 SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, 225 true); 226 } 227 setOnIconLoadedListener(Consumer<DisplayResolveInfo> onIconLoadedListener)228 public void setOnIconLoadedListener(Consumer<DisplayResolveInfo> onIconLoadedListener) { 229 mOnIconLoadedListener = onIconLoadedListener; 230 } 231 getAppPredictor()232 AppPredictor getAppPredictor() { 233 return mAppPredictor; 234 } 235 236 @Override handlePackagesChanged()237 public void handlePackagesChanged() { 238 if (DEBUG) { 239 Log.d(TAG, "clearing queryTargets on package change"); 240 } 241 createPlaceHolders(); 242 mChooserListCommunicator.onHandlePackagesChanged(this); 243 244 } 245 246 @Override notifyDataSetChanged()247 public void notifyDataSetChanged() { 248 if (!mListViewDataChanged) { 249 mChooserListCommunicator.sendListViewUpdateMessage(getUserHandle()); 250 mListViewDataChanged = true; 251 } 252 } 253 refreshListView()254 void refreshListView() { 255 if (mListViewDataChanged) { 256 super.notifyDataSetChanged(); 257 } 258 mListViewDataChanged = false; 259 } 260 createPlaceHolders()261 private void createPlaceHolders() { 262 mNumShortcutResults = 0; 263 mServiceTargets.clear(); 264 for (int i = 0; i < mChooserListCommunicator.getMaxRankedTargets(); i++) { 265 mServiceTargets.add(mPlaceHolderTargetInfo); 266 } 267 } 268 269 @Override onCreateView(ViewGroup parent)270 View onCreateView(ViewGroup parent) { 271 return mInflater.inflate( 272 com.android.internal.R.layout.resolve_grid_item, parent, false); 273 } 274 275 @Override onBindView(View view, TargetInfo info, int position)276 protected void onBindView(View view, TargetInfo info, int position) { 277 final ViewHolder holder = (ViewHolder) view.getTag(); 278 279 if (info == null) { 280 holder.icon.setImageDrawable( 281 mContext.getDrawable(R.drawable.resolver_icon_placeholder)); 282 return; 283 } 284 285 holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); 286 holder.bindIcon(info); 287 if (info instanceof SelectableTargetInfo) { 288 // direct share targets should append the application name for a better readout 289 SelectableTargetInfo sti = (SelectableTargetInfo) info; 290 DisplayResolveInfo rInfo = sti.getDisplayResolveInfo(); 291 CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : ""; 292 CharSequence extendedInfo = info.getExtendedInfo(); 293 String contentDescription = String.join(" ", info.getDisplayLabel(), 294 extendedInfo != null ? extendedInfo : "", appName); 295 holder.updateContentDescription(contentDescription); 296 if (!sti.hasDisplayIcon()) { 297 loadDirectShareIcon(sti); 298 } 299 } else if (info instanceof DisplayResolveInfo) { 300 DisplayResolveInfo dri = (DisplayResolveInfo) info; 301 if (!dri.hasDisplayIcon()) { 302 loadIcon(dri); 303 } 304 } 305 306 // If target is loading, show a special placeholder shape in the label, make unclickable 307 if (info instanceof ChooserActivity.PlaceHolderTargetInfo) { 308 final int maxWidth = mContext.getResources().getDimensionPixelSize( 309 R.dimen.chooser_direct_share_label_placeholder_max_width); 310 holder.text.setMaxWidth(maxWidth); 311 holder.text.setBackground(mContext.getResources().getDrawable( 312 R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme())); 313 // Prevent rippling by removing background containing ripple 314 holder.itemView.setBackground(null); 315 } else { 316 holder.text.setMaxWidth(Integer.MAX_VALUE); 317 holder.text.setBackground(null); 318 holder.itemView.setBackground(holder.defaultItemViewBackground); 319 } 320 321 // Always remove the spacing listener, attach as needed to direct share targets below. 322 holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener); 323 324 if (info instanceof MultiDisplayResolveInfo) { 325 // If the target is grouped show an indicator 326 Drawable bkg = mContext.getDrawable(R.drawable.chooser_group_background); 327 holder.text.setPaddingRelative(0, 0, bkg.getIntrinsicWidth() /* end */, 0); 328 holder.text.setBackground(bkg); 329 } else if (info.isPinned() && (getPositionTargetType(position) == TARGET_STANDARD 330 || getPositionTargetType(position) == TARGET_SERVICE)) { 331 // If the appShare or directShare target is pinned and in the suggested row show a 332 // pinned indicator 333 Drawable bkg = mContext.getDrawable(R.drawable.chooser_pinned_background); 334 holder.text.setPaddingRelative(bkg.getIntrinsicWidth() /* start */, 0, 0, 0); 335 holder.text.setBackground(bkg); 336 holder.text.addOnLayoutChangeListener(mPinTextSpacingListener); 337 } else { 338 holder.text.setBackground(null); 339 holder.text.setPaddingRelative(0, 0, 0, 0); 340 } 341 } 342 343 @Override onIconLoaded(DisplayResolveInfo info)344 protected void onIconLoaded(DisplayResolveInfo info) { 345 if (notifySingleItemChangeOnIconLoad() && mOnIconLoadedListener != null) { 346 mOnIconLoadedListener.accept(info); 347 } else { 348 notifyDataSetChanged(); 349 } 350 } 351 loadDirectShareIcon(SelectableTargetInfo info)352 private void loadDirectShareIcon(SelectableTargetInfo info) { 353 LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info); 354 if (task == null) { 355 task = createLoadDirectShareIconTask(info); 356 mIconLoaders.put(info, task); 357 task.loadIcon(); 358 } 359 } 360 361 @VisibleForTesting createLoadDirectShareIconTask(SelectableTargetInfo info)362 protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) { 363 return new LoadDirectShareIconTask(info); 364 } 365 updateAlphabeticalList()366 void updateAlphabeticalList() { 367 new AsyncTask<Void, Void, List<DisplayResolveInfo>>() { 368 @Override 369 protected List<DisplayResolveInfo> doInBackground(Void... voids) { 370 List<DisplayResolveInfo> allTargets = new ArrayList<>(); 371 allTargets.addAll(mDisplayList); 372 allTargets.addAll(mCallerTargets); 373 if (!mEnableStackedApps) { 374 return allTargets; 375 } 376 // Consolidate multiple targets from same app. 377 Map<String, DisplayResolveInfo> consolidated = new HashMap<>(); 378 for (DisplayResolveInfo info : allTargets) { 379 if (info.getResolveInfo().userHandle == null) { 380 Log.e(TAG, "ResolveInfo with null UserHandle found: " 381 + info.getResolveInfo()); 382 } 383 String resolvedTarget = info.getResolvedComponentName().getPackageName() 384 + '#' + info.getDisplayLabel() 385 + '#' + info.getResolveInfo().userHandle.getIdentifier(); 386 DisplayResolveInfo multiDri = consolidated.get(resolvedTarget); 387 if (multiDri == null) { 388 consolidated.put(resolvedTarget, info); 389 } else if (multiDri instanceof MultiDisplayResolveInfo) { 390 ((MultiDisplayResolveInfo) multiDri).addTarget(info); 391 } else { 392 // create consolidated target from the single DisplayResolveInfo 393 MultiDisplayResolveInfo multiDisplayResolveInfo = 394 new MultiDisplayResolveInfo(resolvedTarget, multiDri); 395 multiDisplayResolveInfo.addTarget(info); 396 consolidated.put(resolvedTarget, multiDisplayResolveInfo); 397 } 398 } 399 List<DisplayResolveInfo> groupedTargets = new ArrayList<>(); 400 groupedTargets.addAll(consolidated.values()); 401 Collections.sort(groupedTargets, 402 new ChooserActivity.AzInfoComparator(mContext)); 403 return groupedTargets; 404 } 405 @Override 406 protected void onPostExecute(List<DisplayResolveInfo> newList) { 407 mSortedList = newList; 408 notifyDataSetChanged(); 409 } 410 }.execute(); 411 } 412 413 @Override getCount()414 public int getCount() { 415 return getRankedTargetCount() + getAlphaTargetCount() 416 + getSelectableServiceTargetCount() + getCallerTargetCount(); 417 } 418 419 @Override getUnfilteredCount()420 public int getUnfilteredCount() { 421 int appTargets = super.getUnfilteredCount(); 422 if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) { 423 appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets(); 424 } 425 return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount(); 426 } 427 428 getCallerTargetCount()429 public int getCallerTargetCount() { 430 return mCallerTargets.size(); 431 } 432 433 /** 434 * Filter out placeholders and non-selectable service targets 435 */ getSelectableServiceTargetCount()436 public int getSelectableServiceTargetCount() { 437 int count = 0; 438 for (ChooserTargetInfo info : mServiceTargets) { 439 if (info instanceof SelectableTargetInfo) { 440 count++; 441 } 442 } 443 return count; 444 } 445 getServiceTargetCount()446 public int getServiceTargetCount() { 447 if (mChooserListCommunicator.shouldShowServiceTargets()) { 448 return Math.min(mServiceTargets.size(), mChooserListCommunicator.getMaxRankedTargets()); 449 } 450 return 0; 451 } 452 getAlphaTargetCount()453 int getAlphaTargetCount() { 454 int groupedCount = mSortedList.size(); 455 int ungroupedCount = mCallerTargets.size() + mDisplayList.size(); 456 return ungroupedCount > mChooserListCommunicator.getMaxRankedTargets() ? groupedCount : 0; 457 } 458 459 /** 460 * Fetch ranked app target count 461 */ getRankedTargetCount()462 public int getRankedTargetCount() { 463 int spacesAvailable = 464 mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount(); 465 return Math.min(spacesAvailable, super.getCount()); 466 } 467 getPositionTargetType(int position)468 public int getPositionTargetType(int position) { 469 int offset = 0; 470 471 final int serviceTargetCount = getServiceTargetCount(); 472 if (position < serviceTargetCount) { 473 return TARGET_SERVICE; 474 } 475 offset += serviceTargetCount; 476 477 final int callerTargetCount = getCallerTargetCount(); 478 if (position - offset < callerTargetCount) { 479 return TARGET_CALLER; 480 } 481 offset += callerTargetCount; 482 483 final int rankedTargetCount = getRankedTargetCount(); 484 if (position - offset < rankedTargetCount) { 485 return TARGET_STANDARD; 486 } 487 offset += rankedTargetCount; 488 489 final int standardTargetCount = getAlphaTargetCount(); 490 if (position - offset < standardTargetCount) { 491 return TARGET_STANDARD_AZ; 492 } 493 494 return TARGET_BAD; 495 } 496 497 @Override getItem(int position)498 public TargetInfo getItem(int position) { 499 return targetInfoForPosition(position, true); 500 } 501 502 503 /** 504 * Find target info for a given position. 505 * Since ChooserActivity displays several sections of content, determine which 506 * section provides this item. 507 */ 508 @Override targetInfoForPosition(int position, boolean filtered)509 public TargetInfo targetInfoForPosition(int position, boolean filtered) { 510 if (position == NO_POSITION) { 511 return null; 512 } 513 514 int offset = 0; 515 516 // Direct share targets 517 final int serviceTargetCount = filtered ? getServiceTargetCount() : 518 getSelectableServiceTargetCount(); 519 if (position < serviceTargetCount) { 520 return mServiceTargets.get(position); 521 } 522 offset += serviceTargetCount; 523 524 // Targets provided by calling app 525 final int callerTargetCount = getCallerTargetCount(); 526 if (position - offset < callerTargetCount) { 527 return mCallerTargets.get(position - offset); 528 } 529 offset += callerTargetCount; 530 531 // Ranked standard app targets 532 final int rankedTargetCount = getRankedTargetCount(); 533 if (position - offset < rankedTargetCount) { 534 return filtered ? super.getItem(position - offset) 535 : getDisplayResolveInfo(position - offset); 536 } 537 offset += rankedTargetCount; 538 539 // Alphabetical complete app target list. 540 if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) { 541 return mSortedList.get(position - offset); 542 } 543 544 return null; 545 } 546 547 // Check whether {@code dri} should be added into mDisplayList. 548 @Override shouldAddResolveInfo(DisplayResolveInfo dri)549 protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) { 550 // Checks if this info is already listed in callerTargets. 551 for (TargetInfo existingInfo : mCallerTargets) { 552 if (mResolverListCommunicator 553 .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) { 554 return false; 555 } 556 } 557 return super.shouldAddResolveInfo(dri); 558 } 559 560 /** 561 * Fetch surfaced direct share target info 562 */ getSurfacedTargetInfo()563 public List<ChooserTargetInfo> getSurfacedTargetInfo() { 564 int maxSurfacedTargets = mChooserListCommunicator.getMaxRankedTargets(); 565 return mServiceTargets.subList(0, 566 Math.min(maxSurfacedTargets, getSelectableServiceTargetCount())); 567 } 568 569 570 /** 571 * Evaluate targets for inclusion in the direct share area. May not be included 572 * if score is too low. 573 */ addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets, @ChooserActivity.ShareTargetType int targetType, Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos)574 public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets, 575 @ChooserActivity.ShareTargetType int targetType, 576 Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos) { 577 if (DEBUG) { 578 Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", " 579 + targets.size() 580 + " targets"); 581 } 582 if (targets.size() == 0) { 583 return; 584 } 585 final float baseScore = getBaseScore(origTarget, targetType); 586 Collections.sort(targets, mBaseTargetComparator); 587 final boolean isShortcutResult = 588 (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER 589 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); 590 final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp 591 : MAX_CHOOSER_TARGETS_PER_APP; 592 final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets) 593 : targets.size(); 594 float lastScore = 0; 595 boolean shouldNotify = false; 596 for (int i = 0, count = targetsLimit; i < count; i++) { 597 final ChooserTarget target = targets.get(i); 598 float targetScore = target.getScore(); 599 if (mApplySharingAppLimits) { 600 targetScore *= baseScore; 601 if (i > 0 && targetScore >= lastScore) { 602 // Apply a decay so that the top app can't crowd out everything else. 603 // This incents ChooserTargetServices to define what's truly better. 604 targetScore = lastScore * 0.95f; 605 } 606 } 607 ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target) 608 : null; 609 if ((shortcutInfo != null) && shortcutInfo.isPinned()) { 610 targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST; 611 } 612 UserHandle userHandle = getUserHandle(); 613 Context contextAsUser = mContext.createContextAsUser(userHandle, 0 /* flags */); 614 boolean isInserted = insertServiceTarget(new SelectableTargetInfo(contextAsUser, 615 origTarget, target, targetScore, mSelectableTargetInfoCommunicator, 616 shortcutInfo)); 617 618 if (isInserted && isShortcutResult) { 619 mNumShortcutResults++; 620 } 621 622 shouldNotify |= isInserted; 623 624 if (DEBUG) { 625 Log.d(TAG, " => " + target.toString() + " score=" + targetScore 626 + " base=" + target.getScore() 627 + " lastScore=" + lastScore 628 + " baseScore=" + baseScore 629 + " applyAppLimit=" + mApplySharingAppLimits); 630 } 631 632 lastScore = targetScore; 633 } 634 635 if (shouldNotify) { 636 notifyDataSetChanged(); 637 } 638 } 639 640 /** 641 * The return number have to exceed a minimum limit to make direct share area expandable. When 642 * append direct share targets is enabled, return count of all available targets parking in the 643 * memory; otherwise, it is shortcuts count which will help reduce the amount of visible 644 * shuffling due to older-style direct share targets. 645 */ getNumServiceTargetsForExpand()646 int getNumServiceTargetsForExpand() { 647 return mNumShortcutResults; 648 } 649 650 /** 651 * Use the scoring system along with artificial boosts to create up to 4 distinct buckets: 652 * <ol> 653 * <li>App-supplied targets 654 * <li>Shortcuts ranked via App Prediction Manager 655 * <li>Shortcuts ranked via legacy heuristics 656 * <li>Legacy direct share targets 657 * </ol> 658 */ getBaseScore( DisplayResolveInfo target, @ChooserActivity.ShareTargetType int targetType)659 public float getBaseScore( 660 DisplayResolveInfo target, 661 @ChooserActivity.ShareTargetType int targetType) { 662 if (target == null) { 663 return CALLER_TARGET_SCORE_BOOST; 664 } 665 float score = super.getScore(target); 666 if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER 667 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) { 668 return score * SHORTCUT_TARGET_SCORE_BOOST; 669 } 670 return score; 671 } 672 673 /** 674 * Calling this marks service target loading complete, and will attempt to no longer 675 * update the direct share area. 676 */ completeServiceTargetLoading()677 public void completeServiceTargetLoading() { 678 mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo); 679 if (mServiceTargets.isEmpty()) { 680 mServiceTargets.add(new ChooserActivity.EmptyTargetInfo()); 681 mChooserActivityLogger.logSharesheetEmptyDirectShareRow(); 682 } 683 notifyDataSetChanged(); 684 } 685 insertServiceTarget(ChooserTargetInfo chooserTargetInfo)686 private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) { 687 // Avoid inserting any potentially late results 688 if (mServiceTargets.size() == 1 689 && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) { 690 return false; 691 } 692 693 // Check for duplicates and abort if found 694 for (ChooserTargetInfo otherTargetInfo : mServiceTargets) { 695 if (chooserTargetInfo.isSimilar(otherTargetInfo)) { 696 return false; 697 } 698 } 699 700 int currentSize = mServiceTargets.size(); 701 final float newScore = chooserTargetInfo.getModifiedScore(); 702 for (int i = 0; i < Math.min(currentSize, mChooserListCommunicator.getMaxRankedTargets()); 703 i++) { 704 final ChooserTargetInfo serviceTarget = mServiceTargets.get(i); 705 if (serviceTarget == null) { 706 mServiceTargets.set(i, chooserTargetInfo); 707 return true; 708 } else if (newScore > serviceTarget.getModifiedScore()) { 709 mServiceTargets.add(i, chooserTargetInfo); 710 return true; 711 } 712 } 713 714 if (currentSize < mChooserListCommunicator.getMaxRankedTargets()) { 715 mServiceTargets.add(chooserTargetInfo); 716 return true; 717 } 718 719 return false; 720 } 721 getChooserTargetForValue(int value)722 public ChooserTarget getChooserTargetForValue(int value) { 723 return mServiceTargets.get(value).getChooserTarget(); 724 } 725 alwaysShowSubLabel()726 protected boolean alwaysShowSubLabel() { 727 // Always show a subLabel for visual consistency across list items. Show an empty 728 // subLabel if the subLabel is the same as the label 729 return true; 730 } 731 732 /** 733 * Rather than fully sorting the input list, this sorting task will put the top k elements 734 * in the head of input list and fill the tail with other elements in undetermined order. 735 */ 736 @Override 737 AsyncTask<List<ResolvedComponentInfo>, 738 Void, createSortingTask(boolean doPostProcessing)739 List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) { 740 return new AsyncTask<List<ResolvedComponentInfo>, 741 Void, 742 List<ResolvedComponentInfo>>() { 743 @Override 744 protected List<ResolvedComponentInfo> doInBackground( 745 List<ResolvedComponentInfo>... params) { 746 Trace.beginSection("ChooserListAdapter#SortingTask"); 747 mResolverListController.topK(params[0], 748 mChooserListCommunicator.getMaxRankedTargets()); 749 Trace.endSection(); 750 return params[0]; 751 } 752 @Override 753 protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) { 754 processSortedList(sortedComponents, doPostProcessing); 755 if (doPostProcessing) { 756 mChooserListCommunicator.updateProfileViewButton(); 757 notifyDataSetChanged(); 758 } 759 } 760 }; 761 } 762 763 public void setAppPredictor(AppPredictor appPredictor) { 764 mAppPredictor = appPredictor; 765 } 766 767 public void setAppPredictorCallback( 768 AppPredictor.Callback appPredictorCallback, 769 ResolverAppPredictorCallback appPredictorCallbackWrapper) { 770 mAppPredictorCallback = appPredictorCallback; 771 mAppPredictorCallbackWrapper = appPredictorCallbackWrapper; 772 } 773 774 public void destroyAppPredictor() { 775 if (getAppPredictor() != null) { 776 getAppPredictor().unregisterPredictionUpdates(mAppPredictorCallback); 777 getAppPredictor().destroy(); 778 setAppPredictor(null); 779 } 780 781 if (mAppPredictorCallbackWrapper != null) { 782 mAppPredictorCallbackWrapper.destroy(); 783 } 784 } 785 786 /** 787 * Necessary methods to communicate between {@link ChooserListAdapter} 788 * and {@link ChooserActivity}. 789 */ 790 @VisibleForTesting 791 public interface ChooserListCommunicator extends ResolverListCommunicator { 792 793 int getMaxRankedTargets(); 794 795 void sendListViewUpdateMessage(UserHandle userHandle); 796 797 boolean isSendAction(Intent targetIntent); 798 799 boolean shouldShowContentPreview(); 800 801 boolean shouldShowServiceTargets(); 802 } 803 804 /** 805 * Loads direct share targets icons. 806 */ 807 @VisibleForTesting 808 public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Boolean> { 809 private final SelectableTargetInfo mTargetInfo; 810 811 private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) { 812 mTargetInfo = targetInfo; 813 } 814 815 @Override 816 protected Boolean doInBackground(Void... voids) { 817 return mTargetInfo.loadIcon(); 818 } 819 820 @Override 821 protected void onPostExecute(Boolean isLoaded) { 822 if (isLoaded) { 823 notifyDataSetChanged(); 824 } 825 } 826 827 /** 828 * An alias for execute to use with unit tests. 829 */ 830 public void loadIcon() { 831 execute(); 832 } 833 } 834 } 835