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