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.app.prediction.AppTarget; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.ActivityInfo; 29 import android.content.pm.LabeledIntent; 30 import android.content.pm.PackageManager; 31 import android.content.pm.ResolveInfo; 32 import android.content.pm.ShortcutInfo; 33 import android.graphics.drawable.Drawable; 34 import android.os.AsyncTask; 35 import android.os.UserHandle; 36 import android.os.UserManager; 37 import android.provider.DeviceConfig; 38 import android.service.chooser.ChooserTarget; 39 import android.util.Log; 40 import android.util.Pair; 41 import android.view.View; 42 import android.view.ViewGroup; 43 44 import com.android.internal.R; 45 import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; 46 import com.android.internal.app.chooser.ChooserTargetInfo; 47 import com.android.internal.app.chooser.DisplayResolveInfo; 48 import com.android.internal.app.chooser.MultiDisplayResolveInfo; 49 import com.android.internal.app.chooser.SelectableTargetInfo; 50 import com.android.internal.app.chooser.TargetInfo; 51 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; 52 53 import java.util.ArrayList; 54 import java.util.Collections; 55 import java.util.HashMap; 56 import java.util.HashSet; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.Set; 60 import java.util.stream.Collectors; 61 62 public class ChooserListAdapter extends ResolverListAdapter { 63 private static final String TAG = "ChooserListAdapter"; 64 private static final boolean DEBUG = false; 65 66 private boolean mAppendDirectShareEnabled = DeviceConfig.getBoolean( 67 DeviceConfig.NAMESPACE_SYSTEMUI, 68 SystemUiDeviceConfigFlags.APPEND_DIRECT_SHARE_ENABLED, 69 true); 70 71 private boolean mEnableStackedApps = true; 72 73 public static final int NO_POSITION = -1; 74 public static final int TARGET_BAD = -1; 75 public static final int TARGET_CALLER = 0; 76 public static final int TARGET_SERVICE = 1; 77 public static final int TARGET_STANDARD = 2; 78 public static final int TARGET_STANDARD_AZ = 3; 79 80 private static final int MAX_SUGGESTED_APP_TARGETS = 4; 81 private static final int MAX_CHOOSER_TARGETS_PER_APP = 2; 82 private static final int MAX_SERVICE_TARGET_APP = 8; 83 private static final int DEFAULT_DIRECT_SHARE_RANKING_SCORE = 1000; 84 85 /** {@link #getBaseScore} */ 86 public static final float CALLER_TARGET_SCORE_BOOST = 900.f; 87 /** {@link #getBaseScore} */ 88 public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f; 89 90 private final int mMaxShortcutTargetsPerApp; 91 private final ChooserListCommunicator mChooserListCommunicator; 92 private final SelectableTargetInfo.SelectableTargetInfoCommunicator 93 mSelectableTargetInfoCommunicator; 94 95 private int mNumShortcutResults = 0; 96 private Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>(); 97 98 // Reserve spots for incoming direct share targets by adding placeholders 99 private ChooserTargetInfo 100 mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo(); 101 private int mValidServiceTargetsNum = 0; 102 private int mAvailableServiceTargetsNum = 0; 103 private final Map<ComponentName, Pair<List<ChooserTargetInfo>, Integer>> 104 mParkingDirectShareTargets = new HashMap<>(); 105 private final Map<ComponentName, Map<String, Integer>> mChooserTargetScores = new HashMap<>(); 106 private Set<ComponentName> mPendingChooserTargetService = new HashSet<>(); 107 private Set<ComponentName> mShortcutComponents = new HashSet<>(); 108 private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>(); 109 private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>(); 110 111 private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator = 112 new ChooserActivity.BaseChooserTargetComparator(); 113 private boolean mListViewDataChanged = false; 114 115 // Sorted list of DisplayResolveInfos for the alphabetical app section. 116 private List<DisplayResolveInfo> mSortedList = new ArrayList<>(); 117 private AppPredictor mAppPredictor; 118 private AppPredictor.Callback mAppPredictorCallback; 119 ChooserListAdapter(Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, ChooserListCommunicator chooserListCommunicator, SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator, PackageManager packageManager)120 public ChooserListAdapter(Context context, List<Intent> payloadIntents, 121 Intent[] initialIntents, List<ResolveInfo> rList, 122 boolean filterLastUsed, ResolverListController resolverListController, 123 ChooserListCommunicator chooserListCommunicator, 124 SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator, 125 PackageManager packageManager) { 126 // Don't send the initial intents through the shared ResolverActivity path, 127 // we want to separate them into a different section. 128 super(context, payloadIntents, null, rList, filterLastUsed, 129 resolverListController, chooserListCommunicator, false); 130 131 mMaxShortcutTargetsPerApp = 132 context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp); 133 mChooserListCommunicator = chooserListCommunicator; 134 createPlaceHolders(); 135 mSelectableTargetInfoCommunicator = selectableTargetInfoCommunicator; 136 137 if (initialIntents != null) { 138 for (int i = 0; i < initialIntents.length; i++) { 139 final Intent ii = initialIntents[i]; 140 if (ii == null) { 141 continue; 142 } 143 144 // We reimplement Intent#resolveActivityInfo here because if we have an 145 // implicit intent, we want the ResolveInfo returned by PackageManager 146 // instead of one we reconstruct ourselves. The ResolveInfo returned might 147 // have extra metadata and resolvePackageName set and we want to respect that. 148 ResolveInfo ri = null; 149 ActivityInfo ai = null; 150 final ComponentName cn = ii.getComponent(); 151 if (cn != null) { 152 try { 153 ai = packageManager.getActivityInfo(ii.getComponent(), 0); 154 ri = new ResolveInfo(); 155 ri.activityInfo = ai; 156 } catch (PackageManager.NameNotFoundException ignored) { 157 // ai will == null below 158 } 159 } 160 if (ai == null) { 161 ri = packageManager.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY); 162 ai = ri != null ? ri.activityInfo : null; 163 } 164 if (ai == null) { 165 Log.w(TAG, "No activity found for " + ii); 166 continue; 167 } 168 UserManager userManager = 169 (UserManager) context.getSystemService(Context.USER_SERVICE); 170 if (ii instanceof LabeledIntent) { 171 LabeledIntent li = (LabeledIntent) ii; 172 ri.resolvePackageName = li.getSourcePackage(); 173 ri.labelRes = li.getLabelResource(); 174 ri.nonLocalizedLabel = li.getNonLocalizedLabel(); 175 ri.icon = li.getIconResource(); 176 ri.iconResourceId = ri.icon; 177 } 178 if (userManager.isManagedProfile()) { 179 ri.noResourceId = true; 180 ri.icon = 0; 181 } 182 mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri))); 183 if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break; 184 } 185 } 186 } 187 getAppPredictor()188 AppPredictor getAppPredictor() { 189 return mAppPredictor; 190 } 191 192 @Override handlePackagesChanged()193 public void handlePackagesChanged() { 194 if (DEBUG) { 195 Log.d(TAG, "clearing queryTargets on package change"); 196 } 197 createPlaceHolders(); 198 mChooserListCommunicator.onHandlePackagesChanged(this); 199 200 } 201 202 @Override notifyDataSetChanged()203 public void notifyDataSetChanged() { 204 if (!mListViewDataChanged) { 205 mChooserListCommunicator.sendListViewUpdateMessage(getUserHandle()); 206 mListViewDataChanged = true; 207 } 208 } 209 refreshListView()210 void refreshListView() { 211 if (mListViewDataChanged) { 212 if (mAppendDirectShareEnabled) { 213 appendServiceTargetsWithQuota(); 214 } 215 super.notifyDataSetChanged(); 216 } 217 mListViewDataChanged = false; 218 } 219 220 createPlaceHolders()221 private void createPlaceHolders() { 222 mNumShortcutResults = 0; 223 mServiceTargets.clear(); 224 mValidServiceTargetsNum = 0; 225 mParkingDirectShareTargets.clear(); 226 mPendingChooserTargetService.clear(); 227 mShortcutComponents.clear(); 228 for (int i = 0; i < mChooserListCommunicator.getMaxRankedTargets(); i++) { 229 mServiceTargets.add(mPlaceHolderTargetInfo); 230 } 231 } 232 233 @Override onCreateView(ViewGroup parent)234 View onCreateView(ViewGroup parent) { 235 return mInflater.inflate( 236 com.android.internal.R.layout.resolve_grid_item, parent, false); 237 } 238 239 @Override onBindView(View view, TargetInfo info, int position)240 protected void onBindView(View view, TargetInfo info, int position) { 241 final ViewHolder holder = (ViewHolder) view.getTag(); 242 if (info == null) { 243 holder.icon.setImageDrawable( 244 mContext.getDrawable(R.drawable.resolver_icon_placeholder)); 245 return; 246 } 247 248 if (!(info instanceof DisplayResolveInfo)) { 249 holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); 250 holder.bindIcon(info); 251 252 if (info instanceof SelectableTargetInfo) { 253 // direct share targets should append the application name for a better readout 254 DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo(); 255 CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : ""; 256 CharSequence extendedInfo = info.getExtendedInfo(); 257 String contentDescription = String.join(" ", info.getDisplayLabel(), 258 extendedInfo != null ? extendedInfo : "", appName); 259 holder.updateContentDescription(contentDescription); 260 } 261 } else { 262 DisplayResolveInfo dri = (DisplayResolveInfo) info; 263 holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel()); 264 LoadIconTask task = mIconLoaders.get(dri); 265 if (task == null) { 266 task = new LoadIconTask(dri, holder); 267 mIconLoaders.put(dri, task); 268 task.execute(); 269 } else { 270 // The holder was potentially changed as the underlying items were 271 // reshuffled, so reset the target holder 272 task.setViewHolder(holder); 273 } 274 } 275 276 // If target is loading, show a special placeholder shape in the label, make unclickable 277 if (info instanceof ChooserActivity.PlaceHolderTargetInfo) { 278 final int maxWidth = mContext.getResources().getDimensionPixelSize( 279 R.dimen.chooser_direct_share_label_placeholder_max_width); 280 holder.text.setMaxWidth(maxWidth); 281 holder.text.setBackground(mContext.getResources().getDrawable( 282 R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme())); 283 // Prevent rippling by removing background containing ripple 284 holder.itemView.setBackground(null); 285 } else { 286 holder.text.setMaxWidth(Integer.MAX_VALUE); 287 holder.text.setBackground(null); 288 holder.itemView.setBackground(holder.defaultItemViewBackground); 289 } 290 291 if (info instanceof MultiDisplayResolveInfo) { 292 // If the target is grouped show an indicator 293 Drawable bkg = mContext.getDrawable(R.drawable.chooser_group_background); 294 holder.text.setPaddingRelative(0, 0, bkg.getIntrinsicWidth() /* end */, 0); 295 holder.text.setBackground(bkg); 296 } else if (info.isPinned() && getPositionTargetType(position) == TARGET_STANDARD) { 297 // If the target is pinned and in the suggested row show a pinned indicator 298 Drawable bkg = mContext.getDrawable(R.drawable.chooser_pinned_background); 299 holder.text.setPaddingRelative(bkg.getIntrinsicWidth() /* start */, 0, 0, 0); 300 holder.text.setBackground(bkg); 301 } else { 302 holder.text.setBackground(null); 303 holder.text.setPaddingRelative(0, 0, 0, 0); 304 } 305 } 306 updateAlphabeticalList()307 void updateAlphabeticalList() { 308 new AsyncTask<Void, Void, List<DisplayResolveInfo>>() { 309 @Override 310 protected List<DisplayResolveInfo> doInBackground(Void... voids) { 311 List<DisplayResolveInfo> allTargets = new ArrayList<>(); 312 allTargets.addAll(mDisplayList); 313 allTargets.addAll(mCallerTargets); 314 if (!mEnableStackedApps) { 315 return allTargets; 316 } 317 // Consolidate multiple targets from same app. 318 Map<String, DisplayResolveInfo> consolidated = new HashMap<>(); 319 for (DisplayResolveInfo info : allTargets) { 320 String packageName = info.getResolvedComponentName().getPackageName(); 321 DisplayResolveInfo multiDri = consolidated.get(packageName); 322 if (multiDri == null) { 323 consolidated.put(packageName, info); 324 } else if (multiDri instanceof MultiDisplayResolveInfo) { 325 ((MultiDisplayResolveInfo) multiDri).addTarget(info); 326 } else { 327 // create consolidated target from the single DisplayResolveInfo 328 MultiDisplayResolveInfo multiDisplayResolveInfo = 329 new MultiDisplayResolveInfo(packageName, multiDri); 330 multiDisplayResolveInfo.addTarget(info); 331 consolidated.put(packageName, multiDisplayResolveInfo); 332 } 333 } 334 List<DisplayResolveInfo> groupedTargets = new ArrayList<>(); 335 groupedTargets.addAll(consolidated.values()); 336 Collections.sort(groupedTargets, new ChooserActivity.AzInfoComparator(mContext)); 337 return groupedTargets; 338 } 339 @Override 340 protected void onPostExecute(List<DisplayResolveInfo> newList) { 341 mSortedList = newList; 342 notifyDataSetChanged(); 343 } 344 }.execute(); 345 } 346 347 @Override getCount()348 public int getCount() { 349 return getRankedTargetCount() + getAlphaTargetCount() 350 + getSelectableServiceTargetCount() + getCallerTargetCount(); 351 } 352 353 @Override getUnfilteredCount()354 public int getUnfilteredCount() { 355 int appTargets = super.getUnfilteredCount(); 356 if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) { 357 appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets(); 358 } 359 return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount(); 360 } 361 362 getCallerTargetCount()363 public int getCallerTargetCount() { 364 return mCallerTargets.size(); 365 } 366 367 /** 368 * Filter out placeholders and non-selectable service targets 369 */ getSelectableServiceTargetCount()370 public int getSelectableServiceTargetCount() { 371 int count = 0; 372 for (ChooserTargetInfo info : mServiceTargets) { 373 if (info instanceof SelectableTargetInfo) { 374 count++; 375 } 376 } 377 return count; 378 } 379 getServiceTargetCount()380 public int getServiceTargetCount() { 381 if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent()) 382 && !ActivityManager.isLowRamDeviceStatic()) { 383 return Math.min(mServiceTargets.size(), mChooserListCommunicator.getMaxRankedTargets()); 384 } 385 386 return 0; 387 } 388 getAlphaTargetCount()389 int getAlphaTargetCount() { 390 int groupedCount = mSortedList.size(); 391 int ungroupedCount = mCallerTargets.size() + mDisplayList.size(); 392 return ungroupedCount > mChooserListCommunicator.getMaxRankedTargets() ? groupedCount : 0; 393 } 394 395 /** 396 * Fetch ranked app target count 397 */ getRankedTargetCount()398 public int getRankedTargetCount() { 399 int spacesAvailable = 400 mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount(); 401 return Math.min(spacesAvailable, super.getCount()); 402 } 403 getPositionTargetType(int position)404 public int getPositionTargetType(int position) { 405 int offset = 0; 406 407 final int serviceTargetCount = getServiceTargetCount(); 408 if (position < serviceTargetCount) { 409 return TARGET_SERVICE; 410 } 411 offset += serviceTargetCount; 412 413 final int callerTargetCount = getCallerTargetCount(); 414 if (position - offset < callerTargetCount) { 415 return TARGET_CALLER; 416 } 417 offset += callerTargetCount; 418 419 final int rankedTargetCount = getRankedTargetCount(); 420 if (position - offset < rankedTargetCount) { 421 return TARGET_STANDARD; 422 } 423 offset += rankedTargetCount; 424 425 final int standardTargetCount = getAlphaTargetCount(); 426 if (position - offset < standardTargetCount) { 427 return TARGET_STANDARD_AZ; 428 } 429 430 return TARGET_BAD; 431 } 432 433 @Override getItem(int position)434 public TargetInfo getItem(int position) { 435 return targetInfoForPosition(position, true); 436 } 437 438 439 /** 440 * Find target info for a given position. 441 * Since ChooserActivity displays several sections of content, determine which 442 * section provides this item. 443 */ 444 @Override targetInfoForPosition(int position, boolean filtered)445 public TargetInfo targetInfoForPosition(int position, boolean filtered) { 446 if (position == NO_POSITION) { 447 return null; 448 } 449 450 int offset = 0; 451 452 // Direct share targets 453 final int serviceTargetCount = filtered ? getServiceTargetCount() : 454 getSelectableServiceTargetCount(); 455 if (position < serviceTargetCount) { 456 return mServiceTargets.get(position); 457 } 458 offset += serviceTargetCount; 459 460 // Targets provided by calling app 461 final int callerTargetCount = getCallerTargetCount(); 462 if (position - offset < callerTargetCount) { 463 return mCallerTargets.get(position - offset); 464 } 465 offset += callerTargetCount; 466 467 // Ranked standard app targets 468 final int rankedTargetCount = getRankedTargetCount(); 469 if (position - offset < rankedTargetCount) { 470 return filtered ? super.getItem(position - offset) 471 : getDisplayResolveInfo(position - offset); 472 } 473 offset += rankedTargetCount; 474 475 // Alphabetical complete app target list. 476 if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) { 477 return mSortedList.get(position - offset); 478 } 479 480 return null; 481 } 482 483 // Check whether {@code dri} should be added into mDisplayList. 484 @Override shouldAddResolveInfo(DisplayResolveInfo dri)485 protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) { 486 // Checks if this info is already listed in callerTargets. 487 for (TargetInfo existingInfo : mCallerTargets) { 488 if (mResolverListCommunicator 489 .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) { 490 return false; 491 } 492 } 493 return super.shouldAddResolveInfo(dri); 494 } 495 496 /** 497 * Fetch surfaced direct share target info 498 */ getSurfacedTargetInfo()499 public List<ChooserTargetInfo> getSurfacedTargetInfo() { 500 int maxSurfacedTargets = mChooserListCommunicator.getMaxRankedTargets(); 501 return mServiceTargets.subList(0, 502 Math.min(maxSurfacedTargets, getSelectableServiceTargetCount())); 503 } 504 505 506 /** 507 * Evaluate targets for inclusion in the direct share area. May not be included 508 * if score is too low. 509 */ addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets, @ChooserActivity.ShareTargetType int targetType, Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos, List<ChooserActivity.ChooserTargetServiceConnection> pendingChooserTargetServiceConnections)510 public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets, 511 @ChooserActivity.ShareTargetType int targetType, 512 Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos, 513 List<ChooserActivity.ChooserTargetServiceConnection> 514 pendingChooserTargetServiceConnections) { 515 if (DEBUG) { 516 Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", " 517 + targets.size() 518 + " targets"); 519 } 520 if (mAppendDirectShareEnabled) { 521 parkTargetIntoMemory(origTarget, targets, targetType, directShareToShortcutInfos, 522 pendingChooserTargetServiceConnections); 523 return; 524 } 525 if (targets.size() == 0) { 526 return; 527 } 528 529 final float baseScore = getBaseScore(origTarget, targetType); 530 Collections.sort(targets, mBaseTargetComparator); 531 532 final boolean isShortcutResult = 533 (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER 534 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); 535 final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp 536 : MAX_CHOOSER_TARGETS_PER_APP; 537 float lastScore = 0; 538 boolean shouldNotify = false; 539 for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) { 540 final ChooserTarget target = targets.get(i); 541 float targetScore = target.getScore(); 542 targetScore *= baseScore; 543 if (i > 0 && targetScore >= lastScore) { 544 // Apply a decay so that the top app can't crowd out everything else. 545 // This incents ChooserTargetServices to define what's truly better. 546 targetScore = lastScore * 0.95f; 547 } 548 UserHandle userHandle = getUserHandle(); 549 Context contextAsUser = mContext.createContextAsUser(userHandle, 0 /* flags */); 550 boolean isInserted = insertServiceTarget(new SelectableTargetInfo(contextAsUser, 551 origTarget, target, targetScore, mSelectableTargetInfoCommunicator, 552 (isShortcutResult ? directShareToShortcutInfos.get(target) : null))); 553 554 if (isInserted && isShortcutResult) { 555 mNumShortcutResults++; 556 } 557 558 shouldNotify |= isInserted; 559 560 if (DEBUG) { 561 Log.d(TAG, " => " + target.toString() + " score=" + targetScore 562 + " base=" + target.getScore() 563 + " lastScore=" + lastScore 564 + " baseScore=" + baseScore); 565 } 566 567 lastScore = targetScore; 568 } 569 570 if (shouldNotify) { 571 notifyDataSetChanged(); 572 } 573 } 574 575 /** 576 * Store ChooserTarget ranking scores info wrapped in {@code targets}. 577 */ addChooserTargetRankingScore(List<AppTarget> targets)578 public void addChooserTargetRankingScore(List<AppTarget> targets) { 579 Log.i(TAG, "addChooserTargetRankingScore " + targets.size() + " targets score."); 580 for (AppTarget target : targets) { 581 if (target.getShortcutInfo() == null) { 582 continue; 583 } 584 ShortcutInfo shortcutInfo = target.getShortcutInfo(); 585 if (!shortcutInfo.getId().equals(ChooserActivity.CHOOSER_TARGET) 586 || shortcutInfo.getActivity() == null) { 587 continue; 588 } 589 ComponentName componentName = shortcutInfo.getActivity(); 590 if (!mChooserTargetScores.containsKey(componentName)) { 591 mChooserTargetScores.put(componentName, new HashMap<>()); 592 } 593 mChooserTargetScores.get(componentName).put(shortcutInfo.getShortLabel().toString(), 594 target.getRank()); 595 } 596 mChooserTargetScores.keySet().forEach(key -> rankTargetsWithinComponent(key)); 597 } 598 599 /** 600 * Rank chooserTargets of the given {@code componentName} in mParkingDirectShareTargets as per 601 * available scores stored in mChooserTargetScores. 602 */ rankTargetsWithinComponent(ComponentName componentName)603 private void rankTargetsWithinComponent(ComponentName componentName) { 604 if (!mParkingDirectShareTargets.containsKey(componentName) 605 || !mChooserTargetScores.containsKey(componentName)) { 606 return; 607 } 608 Map<String, Integer> scores = mChooserTargetScores.get(componentName); 609 Collections.sort(mParkingDirectShareTargets.get(componentName).first, (o1, o2) -> { 610 // The score has been normalized between 0 and 2000, the default is 1000. 611 int score1 = scores.getOrDefault( 612 ChooserUtil.md5(o1.getChooserTarget().getTitle().toString()), 613 DEFAULT_DIRECT_SHARE_RANKING_SCORE); 614 int score2 = scores.getOrDefault( 615 ChooserUtil.md5(o2.getChooserTarget().getTitle().toString()), 616 DEFAULT_DIRECT_SHARE_RANKING_SCORE); 617 return score2 - score1; 618 }); 619 } 620 621 /** 622 * Park {@code targets} into memory for the moment to surface them later when view is refreshed. 623 * Components pending on ChooserTargetService query are also recorded. 624 */ parkTargetIntoMemory(DisplayResolveInfo origTarget, List<ChooserTarget> targets, @ChooserActivity.ShareTargetType int targetType, Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos, List<ChooserActivity.ChooserTargetServiceConnection> pendingChooserTargetServiceConnections)625 private void parkTargetIntoMemory(DisplayResolveInfo origTarget, List<ChooserTarget> targets, 626 @ChooserActivity.ShareTargetType int targetType, 627 Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos, 628 List<ChooserActivity.ChooserTargetServiceConnection> 629 pendingChooserTargetServiceConnections) { 630 ComponentName origComponentName = origTarget != null ? origTarget.getResolvedComponentName() 631 : !targets.isEmpty() ? targets.get(0).getComponentName() : null; 632 Log.i(TAG, 633 "parkTargetIntoMemory " + origComponentName + ", " + targets.size() + " targets"); 634 mPendingChooserTargetService = pendingChooserTargetServiceConnections.stream() 635 .map(ChooserActivity.ChooserTargetServiceConnection::getComponentName) 636 .filter(componentName -> !componentName.equals(origComponentName)) 637 .collect(Collectors.toSet()); 638 // Park targets in memory 639 if (!targets.isEmpty()) { 640 final boolean isShortcutResult = 641 (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER 642 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); 643 Context contextAsUser = mContext.createContextAsUser(getUserHandle(), 644 0 /* flags */); 645 List<ChooserTargetInfo> parkingTargetInfos = targets.stream() 646 .map(target -> 647 new SelectableTargetInfo( 648 contextAsUser, origTarget, target, target.getScore(), 649 mSelectableTargetInfoCommunicator, 650 (isShortcutResult ? directShareToShortcutInfos.get(target) 651 : null)) 652 ) 653 .collect(Collectors.toList()); 654 Pair<List<ChooserTargetInfo>, Integer> parkingTargetInfoPair = 655 mParkingDirectShareTargets.getOrDefault(origComponentName, 656 new Pair<>(new ArrayList<>(), 0)); 657 for (ChooserTargetInfo target : parkingTargetInfos) { 658 if (!checkDuplicateTarget(target, parkingTargetInfoPair.first) 659 && !checkDuplicateTarget(target, mServiceTargets)) { 660 parkingTargetInfoPair.first.add(target); 661 mAvailableServiceTargetsNum++; 662 } 663 } 664 mParkingDirectShareTargets.put(origComponentName, parkingTargetInfoPair); 665 rankTargetsWithinComponent(origComponentName); 666 if (isShortcutResult) { 667 mShortcutComponents.add(origComponentName); 668 } 669 } 670 notifyDataSetChanged(); 671 } 672 673 /** 674 * Append targets of top ranked share app into direct share row with quota limit. Remove 675 * appended ones from memory. 676 */ appendServiceTargetsWithQuota()677 private void appendServiceTargetsWithQuota() { 678 int maxRankedTargets = mChooserListCommunicator.getMaxRankedTargets(); 679 List<ComponentName> topComponentNames = getTopComponentNames(maxRankedTargets); 680 float totalScore = 0f; 681 for (ComponentName component : topComponentNames) { 682 if (!mPendingChooserTargetService.contains(component) 683 && !mParkingDirectShareTargets.containsKey(component)) { 684 continue; 685 } 686 totalScore += super.getScore(component); 687 } 688 boolean shouldWaitPendingService = false; 689 for (ComponentName component : topComponentNames) { 690 if (!mPendingChooserTargetService.contains(component) 691 && !mParkingDirectShareTargets.containsKey(component)) { 692 continue; 693 } 694 float score = super.getScore(component); 695 int quota = Math.round(maxRankedTargets * score / totalScore); 696 if (mPendingChooserTargetService.contains(component) && quota >= 1) { 697 shouldWaitPendingService = true; 698 } 699 if (!mParkingDirectShareTargets.containsKey(component)) { 700 continue; 701 } 702 // Append targets into direct share row as per quota. 703 Pair<List<ChooserTargetInfo>, Integer> parkingTargetsItem = 704 mParkingDirectShareTargets.get(component); 705 List<ChooserTargetInfo> parkingTargets = parkingTargetsItem.first; 706 int insertedNum = parkingTargetsItem.second; 707 while (insertedNum < quota && !parkingTargets.isEmpty()) { 708 if (!checkDuplicateTarget(parkingTargets.get(0), mServiceTargets)) { 709 mServiceTargets.add(mValidServiceTargetsNum, parkingTargets.get(0)); 710 mValidServiceTargetsNum++; 711 insertedNum++; 712 } 713 parkingTargets.remove(0); 714 } 715 Log.i(TAG, " appendServiceTargetsWithQuota component=" + component 716 + " appendNum=" + (insertedNum - parkingTargetsItem.second)); 717 if (DEBUG) { 718 Log.d(TAG, " appendServiceTargetsWithQuota component=" + component 719 + " score=" + score 720 + " totalScore=" + totalScore 721 + " quota=" + quota); 722 } 723 mParkingDirectShareTargets.put(component, new Pair<>(parkingTargets, insertedNum)); 724 } 725 if (!shouldWaitPendingService) { 726 fillAllServiceTargets(); 727 } 728 } 729 730 /** 731 * Append all remaining targets (parking in memory) into direct share row as per their ranking. 732 */ fillAllServiceTargets()733 private void fillAllServiceTargets() { 734 if (mParkingDirectShareTargets.isEmpty()) { 735 return; 736 } 737 Log.i(TAG, " fillAllServiceTargets"); 738 List<ComponentName> topComponentNames = getTopComponentNames(MAX_SERVICE_TARGET_APP); 739 // Append all remaining targets of top recommended components into direct share row. 740 for (ComponentName component : topComponentNames) { 741 if (!mParkingDirectShareTargets.containsKey(component)) { 742 continue; 743 } 744 mParkingDirectShareTargets.get(component).first.stream() 745 .filter(target -> !checkDuplicateTarget(target, mServiceTargets)) 746 .forEach(target -> { 747 mServiceTargets.add(mValidServiceTargetsNum, target); 748 mValidServiceTargetsNum++; 749 }); 750 mParkingDirectShareTargets.remove(component); 751 } 752 // Append all remaining shortcuts targets into direct share row. 753 mParkingDirectShareTargets.entrySet().stream() 754 .filter(entry -> mShortcutComponents.contains(entry.getKey())) 755 .map(entry -> entry.getValue()) 756 .map(pair -> pair.first) 757 .forEach(targets -> { 758 for (ChooserTargetInfo target : targets) { 759 if (!checkDuplicateTarget(target, mServiceTargets)) { 760 mServiceTargets.add(mValidServiceTargetsNum, target); 761 mValidServiceTargetsNum++; 762 } 763 } 764 }); 765 mParkingDirectShareTargets.clear(); 766 } 767 checkDuplicateTarget(ChooserTargetInfo target, List<ChooserTargetInfo> destination)768 private boolean checkDuplicateTarget(ChooserTargetInfo target, 769 List<ChooserTargetInfo> destination) { 770 // Check for duplicates and abort if found 771 for (ChooserTargetInfo otherTargetInfo : destination) { 772 if (target.isSimilar(otherTargetInfo)) { 773 return true; 774 } 775 } 776 return false; 777 } 778 779 /** 780 * The return number have to exceed a minimum limit to make direct share area expandable. When 781 * append direct share targets is enabled, return count of all available targets parking in the 782 * memory; otherwise, it is shortcuts count which will help reduce the amount of visible 783 * shuffling due to older-style direct share targets. 784 */ getNumServiceTargetsForExpand()785 int getNumServiceTargetsForExpand() { 786 return mAppendDirectShareEnabled ? mAvailableServiceTargetsNum : mNumShortcutResults; 787 } 788 789 /** 790 * Use the scoring system along with artificial boosts to create up to 4 distinct buckets: 791 * <ol> 792 * <li>App-supplied targets 793 * <li>Shortcuts ranked via App Prediction Manager 794 * <li>Shortcuts ranked via legacy heuristics 795 * <li>Legacy direct share targets 796 * </ol> 797 */ getBaseScore( DisplayResolveInfo target, @ChooserActivity.ShareTargetType int targetType)798 public float getBaseScore( 799 DisplayResolveInfo target, 800 @ChooserActivity.ShareTargetType int targetType) { 801 if (target == null) { 802 return CALLER_TARGET_SCORE_BOOST; 803 } 804 805 if (targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) { 806 return SHORTCUT_TARGET_SCORE_BOOST; 807 } 808 809 float score = super.getScore(target); 810 if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) { 811 return score * SHORTCUT_TARGET_SCORE_BOOST; 812 } 813 814 return score; 815 } 816 817 /** 818 * Calling this marks service target loading complete, and will attempt to no longer 819 * update the direct share area. 820 */ completeServiceTargetLoading()821 public void completeServiceTargetLoading() { 822 mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo); 823 if (mAppendDirectShareEnabled) { 824 fillAllServiceTargets(); 825 } 826 if (mServiceTargets.isEmpty()) { 827 mServiceTargets.add(new ChooserActivity.EmptyTargetInfo()); 828 } 829 notifyDataSetChanged(); 830 } 831 insertServiceTarget(ChooserTargetInfo chooserTargetInfo)832 private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) { 833 // Avoid inserting any potentially late results 834 if (mServiceTargets.size() == 1 835 && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) { 836 return false; 837 } 838 839 // Check for duplicates and abort if found 840 for (ChooserTargetInfo otherTargetInfo : mServiceTargets) { 841 if (chooserTargetInfo.isSimilar(otherTargetInfo)) { 842 return false; 843 } 844 } 845 846 int currentSize = mServiceTargets.size(); 847 final float newScore = chooserTargetInfo.getModifiedScore(); 848 for (int i = 0; i < Math.min(currentSize, mChooserListCommunicator.getMaxRankedTargets()); 849 i++) { 850 final ChooserTargetInfo serviceTarget = mServiceTargets.get(i); 851 if (serviceTarget == null) { 852 mServiceTargets.set(i, chooserTargetInfo); 853 return true; 854 } else if (newScore > serviceTarget.getModifiedScore()) { 855 mServiceTargets.add(i, chooserTargetInfo); 856 return true; 857 } 858 } 859 860 if (currentSize < mChooserListCommunicator.getMaxRankedTargets()) { 861 mServiceTargets.add(chooserTargetInfo); 862 return true; 863 } 864 865 return false; 866 } 867 getChooserTargetForValue(int value)868 public ChooserTarget getChooserTargetForValue(int value) { 869 return mServiceTargets.get(value).getChooserTarget(); 870 } 871 alwaysShowSubLabel()872 protected boolean alwaysShowSubLabel() { 873 // Always show a subLabel for visual consistency across list items. Show an empty 874 // subLabel if the subLabel is the same as the label 875 return true; 876 } 877 878 /** 879 * Rather than fully sorting the input list, this sorting task will put the top k elements 880 * in the head of input list and fill the tail with other elements in undetermined order. 881 */ 882 @Override 883 AsyncTask<List<ResolvedComponentInfo>, 884 Void, createSortingTask(boolean doPostProcessing)885 List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) { 886 return new AsyncTask<List<ResolvedComponentInfo>, 887 Void, 888 List<ResolvedComponentInfo>>() { 889 @Override 890 protected List<ResolvedComponentInfo> doInBackground( 891 List<ResolvedComponentInfo>... params) { 892 mResolverListController.topK(params[0], 893 mChooserListCommunicator.getMaxRankedTargets()); 894 return params[0]; 895 } 896 @Override 897 protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) { 898 processSortedList(sortedComponents, doPostProcessing); 899 if (doPostProcessing) { 900 mChooserListCommunicator.updateProfileViewButton(); 901 notifyDataSetChanged(); 902 } 903 } 904 }; 905 } 906 907 public void setAppPredictor(AppPredictor appPredictor) { 908 mAppPredictor = appPredictor; 909 } 910 911 public void setAppPredictorCallback(AppPredictor.Callback appPredictorCallback) { 912 mAppPredictorCallback = appPredictorCallback; 913 } 914 915 public void destroyAppPredictor() { 916 if (getAppPredictor() != null) { 917 getAppPredictor().unregisterPredictionUpdates(mAppPredictorCallback); 918 getAppPredictor().destroy(); 919 setAppPredictor(null); 920 } 921 } 922 923 /** 924 * Necessary methods to communicate between {@link ChooserListAdapter} 925 * and {@link ChooserActivity}. 926 */ 927 interface ChooserListCommunicator extends ResolverListCommunicator { 928 929 int getMaxRankedTargets(); 930 931 void sendListViewUpdateMessage(UserHandle userHandle); 932 933 boolean isSendAction(Intent targetIntent); 934 } 935 } 936