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