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