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