• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.intentresolver;
18 
19 import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
20 import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
21 import static com.android.intentresolver.Flags.targetHoverAndKeyboardFocusStates;
22 
23 import android.app.ActivityManager;
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.Trace;
36 import android.os.UserHandle;
37 import android.os.UserManager;
38 import android.provider.DeviceConfig;
39 import android.service.chooser.ChooserTarget;
40 import android.text.Layout;
41 import android.text.TextUtils;
42 import android.util.Log;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.widget.TextView;
46 
47 import androidx.annotation.MainThread;
48 import androidx.annotation.Nullable;
49 import androidx.annotation.WorkerThread;
50 
51 import com.android.intentresolver.chooser.DisplayResolveInfo;
52 import com.android.intentresolver.chooser.DisplayResolveInfoAzInfoComparator;
53 import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
54 import com.android.intentresolver.chooser.NotSelectableTargetInfo;
55 import com.android.intentresolver.chooser.SelectableTargetInfo;
56 import com.android.intentresolver.chooser.TargetInfo;
57 import com.android.intentresolver.icons.TargetDataLoader;
58 import com.android.intentresolver.logging.EventLog;
59 import com.android.intentresolver.widget.BadgeTextView;
60 import com.android.internal.annotations.VisibleForTesting;
61 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
62 
63 import com.google.common.collect.ImmutableList;
64 
65 import java.util.ArrayList;
66 import java.util.HashSet;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.Objects;
70 import java.util.Set;
71 import java.util.concurrent.Executor;
72 import java.util.stream.Collectors;
73 
74 public class ChooserListAdapter extends ResolverListAdapter {
75 
76     /**
77      * Delegate interface for injecting a chooser-specific operation to be performed before handling
78      * a package-change event. This allows the "driver" invoking the package-change to be generic,
79      * with no knowledge specific to the chooser implementation.
80      */
81     public interface PackageChangeCallback {
82         /** Perform any steps necessary before processing the package-change event. */
beforeHandlingPackagesChanged()83         void beforeHandlingPackagesChanged();
84     }
85 
86     private static final String TAG = "ChooserListAdapter";
87     private static final boolean DEBUG = false;
88 
89     public static final int NO_POSITION = -1;
90     public static final int TARGET_BAD = -1;
91     public static final int TARGET_CALLER = 0;
92     public static final int TARGET_SERVICE = 1;
93     public static final int TARGET_STANDARD = 2;
94     public static final int TARGET_STANDARD_AZ = 3;
95 
96     private static final int MAX_SUGGESTED_APP_TARGETS = 4;
97 
98     /** {@link #getBaseScore} */
99     public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
100     /** {@link #getBaseScore} */
101     public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
102 
103     private final Intent mReferrerFillInIntent;
104 
105     private final int mMaxRankedTargets;
106 
107     private final EventLog mEventLog;
108 
109     private final Set<TargetInfo> mRequestedIcons = new HashSet<>();
110 
111     @Nullable
112     private final PackageChangeCallback mPackageChangeCallback;
113 
114     // Reserve spots for incoming direct share targets by adding placeholders
115     private final TargetInfo mPlaceHolderTargetInfo;
116     private final TargetDataLoader mTargetDataLoader;
117     private final List<TargetInfo> mServiceTargets = new ArrayList<>();
118     private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>();
119 
120     private final ShortcutSelectionLogic mShortcutSelectionLogic;
121 
122     // Sorted list of DisplayResolveInfos for the alphabetical app section.
123     private final List<DisplayResolveInfo> mSortedList = new ArrayList<>();
124 
125     private final ItemRevealAnimationTracker mAnimationTracker = new ItemRevealAnimationTracker();
126 
127     /**
128      * Indicates whether the app targets are ready. The flag is reset in
129      * {@link #rebuildList(boolean)} and set to true in {@link #updateAlphabeticalList(Runnable)}'s
130      * onPostExecute.
131      * There's one nuance though, {@link #updateAlphabeticalList(Runnable)} is called by the
132      * {@link ChooserActivity} only when {@link #rebuildList(boolean)} was called with {@code true}
133      * It is called with {@code false} only for inactive tabs in the
134      * MultiProfilePagerAdapter.rebuildTabs which, in turn, is called from either
135      * {@link ChooserActivity#recreatePagerAdapter} or {@link ChooserActivity#configureContentView}
136      * and, in both cases, there are no inactive pages in the MultiProfilePagerAdapter and
137      * {@link #rebuildList(boolean)} will be called with true upon navigation to the missing page.
138      * Yeah.
139      */
140     private boolean mAppTargetsReady = false;
141 
142     // For pinned direct share labels, if the text spans multiple lines, the TextView will consume
143     // the full width, even if the characters actually take up less than that. Measure the actual
144     // line widths and constrain the View's width based upon that so that the pin doesn't end up
145     // very far from the text.
146     private final View.OnLayoutChangeListener mPinTextSpacingListener =
147             new View.OnLayoutChangeListener() {
148                 @Override
149                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
150                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
151                     TextView textView = (TextView) v;
152                     Layout layout = textView.getLayout();
153                     if (layout != null) {
154                         int textWidth = 0;
155                         for (int line = 0; line < layout.getLineCount(); line++) {
156                             textWidth = Math.max((int) Math.ceil(layout.getLineMax(line)),
157                                     textWidth);
158                         }
159                         int desiredWidth = textWidth + textView.getPaddingLeft()
160                                 + textView.getPaddingRight();
161                         if (textView.getWidth() > desiredWidth) {
162                             ViewGroup.LayoutParams params = textView.getLayoutParams();
163                             params.width = desiredWidth;
164                             textView.setLayoutParams(params);
165                             // Need to wait until layout pass is over before requesting layout.
166                             textView.post(() -> textView.requestLayout());
167                         }
168                         textView.removeOnLayoutChangeListener(this);
169                     }
170                 }
171             };
172 
173     private boolean mAnimateItems = true;
174     private boolean mTargetsEnabled = true;
175     private boolean mDirectTargetsEnabled = true;
176 
ChooserListAdapter( Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, UserHandle userHandle, Intent targetIntent, Intent referrerFillInIntent, ResolverListCommunicator resolverListCommunicator, PackageManager packageManager, EventLog eventLog, int maxRankedTargets, UserHandle initialIntentsUserSpace, TargetDataLoader targetDataLoader, @Nullable PackageChangeCallback packageChangeCallback)177     public ChooserListAdapter(
178             Context context,
179             List<Intent> payloadIntents,
180             Intent[] initialIntents,
181             List<ResolveInfo> rList,
182             boolean filterLastUsed,
183             ResolverListController resolverListController,
184             UserHandle userHandle,
185             Intent targetIntent,
186             Intent referrerFillInIntent,
187             ResolverListCommunicator resolverListCommunicator,
188             PackageManager packageManager,
189             EventLog eventLog,
190             int maxRankedTargets,
191             UserHandle initialIntentsUserSpace,
192             TargetDataLoader targetDataLoader,
193             @Nullable PackageChangeCallback packageChangeCallback) {
194         this(
195                 context,
196                 payloadIntents,
197                 initialIntents,
198                 rList,
199                 filterLastUsed,
200                 resolverListController,
201                 userHandle,
202                 targetIntent,
203                 referrerFillInIntent,
204                 resolverListCommunicator,
205                 packageManager,
206                 eventLog,
207                 maxRankedTargets,
208                 initialIntentsUserSpace,
209                 targetDataLoader,
210                 packageChangeCallback,
211                 AsyncTask.SERIAL_EXECUTOR,
212                 context.getMainExecutor()
213         );
214     }
215 
216     @VisibleForTesting
ChooserListAdapter( Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, UserHandle userHandle, Intent targetIntent, Intent referrerFillInIntent, ResolverListCommunicator resolverListCommunicator, PackageManager packageManager, EventLog eventLog, int maxRankedTargets, UserHandle initialIntentsUserSpace, TargetDataLoader targetDataLoader, @Nullable PackageChangeCallback packageChangeCallback, Executor bgExecutor, Executor mainExecutor)217     public ChooserListAdapter(
218             Context context,
219             List<Intent> payloadIntents,
220             Intent[] initialIntents,
221             List<ResolveInfo> rList,
222             boolean filterLastUsed,
223             ResolverListController resolverListController,
224             UserHandle userHandle,
225             Intent targetIntent,
226             Intent referrerFillInIntent,
227             ResolverListCommunicator resolverListCommunicator,
228             PackageManager packageManager,
229             EventLog eventLog,
230             int maxRankedTargets,
231             UserHandle initialIntentsUserSpace,
232             TargetDataLoader targetDataLoader,
233             @Nullable PackageChangeCallback packageChangeCallback,
234             Executor bgExecutor,
235             Executor mainExecutor) {
236         // Don't send the initial intents through the shared ResolverActivity path,
237         // we want to separate them into a different section.
238         super(
239                 context,
240                 payloadIntents,
241                 null,
242                 rList,
243                 filterLastUsed,
244                 resolverListController,
245                 userHandle,
246                 targetIntent,
247                 resolverListCommunicator,
248                 initialIntentsUserSpace,
249                 targetDataLoader,
250                 bgExecutor,
251                 mainExecutor);
252 
253         mMaxRankedTargets = maxRankedTargets;
254         mReferrerFillInIntent = referrerFillInIntent;
255 
256         mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);
257         mTargetDataLoader = targetDataLoader;
258         mPackageChangeCallback = packageChangeCallback;
259         createPlaceHolders();
260         mEventLog = eventLog;
261         mShortcutSelectionLogic = new ShortcutSelectionLogic(
262                 context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp),
263                 DeviceConfig.getBoolean(
264                         DeviceConfig.NAMESPACE_SYSTEMUI,
265                         SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
266                         true)
267         );
268 
269         if (initialIntents != null) {
270             for (int i = 0; i < initialIntents.length; i++) {
271                 final Intent ii = initialIntents[i];
272                 if (ii == null) {
273                     continue;
274                 }
275 
276                 // We reimplement Intent#resolveActivityInfo here because if we have an
277                 // implicit intent, we want the ResolveInfo returned by PackageManager
278                 // instead of one we reconstruct ourselves. The ResolveInfo returned might
279                 // have extra metadata and resolvePackageName set and we want to respect that.
280                 ResolveInfo ri = null;
281                 ActivityInfo ai = null;
282                 final ComponentName cn = ii.getComponent();
283                 if (cn != null) {
284                     try {
285                         ai = packageManager.getActivityInfo(
286                                 ii.getComponent(),
287                                 PackageManager.ComponentInfoFlags.of(PackageManager.GET_META_DATA));
288                         ri = new ResolveInfo();
289                         ri.activityInfo = ai;
290                     } catch (PackageManager.NameNotFoundException ignored) {
291                         // ai will == null below
292                     }
293                 }
294                 if (ai == null) {
295                     // Because of AIDL bug, resolveActivity can't accept subclasses of Intent.
296                     final Intent rii = (ii.getClass() == Intent.class) ? ii : new Intent(ii);
297                     ri = packageManager.resolveActivity(
298                             rii,
299                             PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
300                     ai = ri != null ? ri.activityInfo : null;
301                 }
302                 if (ai == null) {
303                     Log.w(TAG, "No activity found for " + ii);
304                     continue;
305                 }
306                 UserManager userManager =
307                         (UserManager) context.getSystemService(Context.USER_SERVICE);
308                 if (ii instanceof LabeledIntent) {
309                     LabeledIntent li = (LabeledIntent) ii;
310                     ri.resolvePackageName = li.getSourcePackage();
311                     ri.labelRes = li.getLabelResource();
312                     ri.nonLocalizedLabel = li.getNonLocalizedLabel();
313                     ri.icon = li.getIconResource();
314                     ri.iconResourceId = ri.icon;
315                 }
316                 if (userManager.isManagedProfile()) {
317                     ri.noResourceId = true;
318                     ri.icon = 0;
319                 }
320                 ri.userHandle = initialIntentsUserSpace;
321                 DisplayResolveInfo displayResolveInfo =
322                         DisplayResolveInfo.newDisplayResolveInfo(ii, ri, ii);
323                 mCallerTargets.add(displayResolveInfo);
324                 if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
325             }
326         }
327     }
328 
329     /**
330      * @return {@code true} if the app targets are ready.
331      */
areAppTargetsReady()332     public final boolean areAppTargetsReady() {
333         return mAppTargetsReady;
334     }
335 
336     /**
337      * Set the enabled state for all targets.
338      */
setTargetsEnabled(boolean isEnabled)339     public void setTargetsEnabled(boolean isEnabled) {
340         if (mTargetsEnabled != isEnabled) {
341             mTargetsEnabled = isEnabled;
342             notifyDataSetChanged();
343         }
344     }
345 
346     /**
347      * Set the enabled state for direct targets.
348      */
setDirectTargetsEnabled(boolean isEnabled)349     public void setDirectTargetsEnabled(boolean isEnabled) {
350         if (mDirectTargetsEnabled != isEnabled) {
351             mDirectTargetsEnabled = isEnabled;
352             if (!mServiceTargets.isEmpty() && !isDirectTargetRowEmptyState()) {
353                 notifyDataSetChanged();
354             }
355         }
356     }
357 
setAnimateItems(boolean animateItems)358     public void setAnimateItems(boolean animateItems) {
359         mAnimateItems = animateItems;
360     }
361 
362     @Override
handlePackagesChanged()363     public void handlePackagesChanged() {
364         if (mPackageChangeCallback != null) {
365             mPackageChangeCallback.beforeHandlingPackagesChanged();
366         }
367         if (DEBUG) {
368             Log.d(TAG, "clearing queryTargets on package change");
369         }
370         createPlaceHolders();
371         mResolverListCommunicator.onHandlePackagesChanged(this);
372 
373     }
374 
375     @Override
rebuildList(boolean doPostProcessing)376     public boolean rebuildList(boolean doPostProcessing) {
377         mAnimationTracker.reset();
378         mSortedList.clear();
379         mAppTargetsReady = false;
380         boolean result = super.rebuildList(doPostProcessing);
381         notifyDataSetChanged();
382         return result;
383     }
384 
createPlaceHolders()385     private void createPlaceHolders() {
386         mServiceTargets.clear();
387         for (int i = 0; i < mMaxRankedTargets; ++i) {
388             mServiceTargets.add(mPlaceHolderTargetInfo);
389         }
390     }
391 
392     @Override
onCreateView(ViewGroup parent)393     View onCreateView(ViewGroup parent) {
394         int layout = targetHoverAndKeyboardFocusStates()
395                 ? R.layout.chooser_grid_item_hover
396                 : R.layout.chooser_grid_item;
397         return mInflater.inflate(layout, parent, false);
398     }
399 
400     @Override
onDestroy()401     public void onDestroy() {
402         super.onDestroy();
403         notifyDataSetChanged();
404     }
405 
406     @VisibleForTesting
407     @Override
onBindView(View view, TargetInfo info, int position)408     public void onBindView(View view, TargetInfo info, int position) {
409         final boolean isEnabled = !isDestroyed() && mTargetsEnabled;
410         view.setEnabled(isEnabled);
411         final ViewHolder holder = (ViewHolder) view.getTag();
412 
413         resetViewHolder(holder);
414         // Always remove the spacing listener, attach as needed to direct share targets below.
415         holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener);
416 
417         if (info == null) {
418             holder.icon.setImageDrawable(loadIconPlaceholder());
419             return;
420         }
421 
422         final CharSequence displayLabel = Objects.requireNonNullElse(info.getDisplayLabel(), "");
423         final CharSequence extendedInfo = Objects.requireNonNullElse(info.getExtendedInfo(), "");
424         holder.bindLabel(displayLabel, extendedInfo);
425         if (mAnimateItems && !TextUtils.isEmpty(displayLabel)) {
426             mAnimationTracker.animateLabel(holder.text, info);
427         }
428         if (mAnimateItems
429                 && !TextUtils.isEmpty(extendedInfo)
430                 && holder.text2.getVisibility() == View.VISIBLE) {
431             mAnimationTracker.animateLabel(holder.text2, info);
432         }
433 
434         if (info.isSelectableTargetInfo()) {
435             view.setEnabled(isEnabled && mDirectTargetsEnabled);
436             // direct share targets should append the application name for a better readout
437             DisplayResolveInfo rInfo = info.getDisplayResolveInfo();
438             CharSequence appName =
439                     Objects.requireNonNullElse(rInfo == null ? null : rInfo.getDisplayLabel(), "");
440             String contentDescription =
441                     String.join(" ", info.getDisplayLabel(), extendedInfo, appName);
442             if (info.isPinned()) {
443                 contentDescription = String.join(
444                     ". ",
445                     contentDescription,
446                     mContext.getResources().getString(R.string.pinned));
447             }
448             updateContentDescription(holder, contentDescription);
449             if (!info.hasDisplayIcon()) {
450                 loadDirectShareIcon((SelectableTargetInfo) info);
451             }
452         } else if (info.isDisplayResolveInfo()) {
453             if (info.isPinned()) {
454                 updateContentDescription(
455                         holder,
456                         String.join(
457                                 ". ",
458                                 info.getDisplayLabel(),
459                                 mContext.getResources().getString(R.string.pinned)));
460             }
461             DisplayResolveInfo dri = (DisplayResolveInfo) info;
462             if (!dri.hasDisplayIcon()) {
463                 loadIcon(dri);
464             }
465             if (!dri.hasDisplayLabel()) {
466                 loadLabel(dri);
467             }
468         }
469 
470         holder.bindIcon(info, mTargetsEnabled);
471         if (mAnimateItems && info.hasDisplayIcon()) {
472             mAnimationTracker.animateIcon(holder.icon, info);
473         }
474 
475         if (info.isPlaceHolderTargetInfo()) {
476             bindPlaceholder(holder);
477         }
478 
479         if (info.isMultiDisplayResolveInfo()) {
480             // If the target is grouped show an indicator
481             bindGroupIndicator(
482                     holder,
483                     mContext.getDrawable(R.drawable.chooser_group_background));
484         } else if (info.isPinned() && (getPositionTargetType(position) == TARGET_STANDARD
485                 || getPositionTargetType(position) == TARGET_SERVICE)) {
486             // If the appShare or directShare target is pinned and in the suggested row show a
487             // pinned indicator
488             bindPinnedIndicator(holder, mContext.getDrawable(R.drawable.chooser_pinned_background));
489             holder.text.addOnLayoutChangeListener(mPinTextSpacingListener);
490         }
491     }
492 
resetViewHolder(ViewHolder holder)493     private void resetViewHolder(ViewHolder holder) {
494         holder.reset();
495         holder.itemView.setBackground(holder.defaultItemViewBackground);
496 
497         ((BadgeTextView) holder.text).setBadgeDrawable(null);
498         holder.text.setBackground(null);
499         holder.text.setPaddingRelative(0, 0, 0, 0);
500     }
501 
updateContentDescription(ViewHolder holder, String description)502     private void updateContentDescription(ViewHolder holder, String description) {
503         holder.itemView.setContentDescription(description);
504     }
505 
bindPlaceholder(ViewHolder holder)506     private void bindPlaceholder(ViewHolder holder) {
507         holder.itemView.setBackground(null);
508     }
509 
bindGroupIndicator(ViewHolder holder, Drawable indicator)510     private void bindGroupIndicator(ViewHolder holder, Drawable indicator) {
511         ((BadgeTextView) holder.text).setBadgeDrawable(indicator);
512     }
513 
bindPinnedIndicator(ViewHolder holder, Drawable indicator)514     private void bindPinnedIndicator(ViewHolder holder, Drawable indicator) {
515         holder.text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0);
516         holder.text.setBackground(indicator);
517     }
518 
loadDirectShareIcon(SelectableTargetInfo info)519     private void loadDirectShareIcon(SelectableTargetInfo info) {
520         if (mRequestedIcons.add(info)) {
521             Drawable icon = mTargetDataLoader.getOrLoadDirectShareIcon(
522                     info,
523                     getUserHandle(),
524                     (drawable) -> onDirectShareIconLoaded(info, drawable, true));
525             if (icon != null) {
526                 onDirectShareIconLoaded(info, icon, false);
527             }
528         }
529     }
530 
onDirectShareIconLoaded( SelectableTargetInfo mTargetInfo, @Nullable Drawable icon, boolean notify)531     private void onDirectShareIconLoaded(
532             SelectableTargetInfo mTargetInfo, @Nullable Drawable icon, boolean notify) {
533         if (icon != null && !mTargetInfo.hasDisplayIcon()) {
534             mTargetInfo.getDisplayIconHolder().setDisplayIcon(icon);
535             if (notify) {
536                 notifyDataSetChanged();
537             }
538         }
539     }
540 
541     /**
542      * Group application targets
543      */
updateAlphabeticalList(boolean rebuildComplete, Runnable onCompleted)544     public void updateAlphabeticalList(boolean rebuildComplete, Runnable onCompleted) {
545         if (getDisplayResolveInfoCount() == 0) {
546             Log.d(TAG, "getDisplayResolveInfoCount() == 0");
547             if (rebuildComplete) {
548                 mAppTargetsReady = true;
549                 onCompleted.run();
550             }
551             notifyDataSetChanged();
552             return;
553         }
554         final DisplayResolveInfoAzInfoComparator
555                 comparator = new DisplayResolveInfoAzInfoComparator(mContext);
556         ImmutableList<DisplayResolveInfo> displayList = getTargetsInCurrentDisplayList();
557         final List<DisplayResolveInfo> allTargets =
558                 new ArrayList<>(displayList.size() + mCallerTargets.size());
559         allTargets.addAll(displayList);
560         allTargets.addAll(mCallerTargets);
561 
562         new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
563             @Override
564             protected List<DisplayResolveInfo> doInBackground(Void... voids) {
565                 try {
566                     Trace.beginSection("update-alphabetical-list");
567                     return updateList();
568                 } finally {
569                     Trace.endSection();
570                 }
571             }
572 
573             private List<DisplayResolveInfo> updateList() {
574                 loadMissingLabels(allTargets);
575 
576                 // Consolidate multiple targets from same app.
577                 return allTargets
578                         .stream()
579                         .map(appTarget -> {
580                             if (targetHoverAndKeyboardFocusStates()) {
581                                 // Icon drawables are effectively cached per target info.
582                                 // Without cloning target infos, the same target info could be used
583                                 // for two different positions in the grid: once in the ranked
584                                 // targets row (from ResolverListAdapter#mDisplayList or
585                                 // #mCallerTargets, see #getItem()) and again in the all-app-target
586                                 // grid (copied from #mDisplayList and #mCallerTargets to
587                                 // #mSortedList).
588                                 // Using the same drawable for two list items would result in visual
589                                 // effects being applied to both simultaneously.
590                                 DisplayResolveInfo copy = appTarget.copy();
591                                 copy.getDisplayIconHolder().setDisplayIcon(null);
592                                 return copy;
593                             } else {
594                                 return appTarget;
595                             }
596                         })
597                         .collect(Collectors.groupingBy(target ->
598                                 target.getResolvedComponentName().getPackageName()
599                                         + "#" + target.getDisplayLabel()
600                                         + '#' + target.getResolveInfo().userHandle.getIdentifier()
601                         ))
602                         .values()
603                         .stream()
604                         .map(appTargets ->
605                                 (appTargets.size() == 1)
606                                         ? appTargets.get(0)
607                                         : MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
608                                             appTargets))
609                         .sorted(comparator)
610                         .collect(Collectors.toList());
611             }
612 
613             @Override
614             protected void onPostExecute(List<DisplayResolveInfo> newList) {
615                 mSortedList.clear();
616                 mSortedList.addAll(newList);
617                 mAppTargetsReady = true;
618                 notifyDataSetChanged();
619                 onCompleted.run();
620             }
621 
622             private void loadMissingLabels(List<DisplayResolveInfo> targets) {
623                 for (DisplayResolveInfo target: targets) {
624                     mTargetDataLoader.getOrLoadLabel(target);
625                 }
626             }
627         }.execute();
628     }
629 
630     @Override
631     public int getCount() {
632         return getRankedTargetCount() + getAlphaTargetCount()
633                 + getSelectableServiceTargetCount() + getCallerTargetCount();
634     }
635 
636     @Override
637     public int getUnfilteredCount() {
638         int appTargets = super.getUnfilteredCount();
639         if (appTargets > mMaxRankedTargets) {
640             // TODO: what does this condition mean?
641             appTargets = appTargets + mMaxRankedTargets;
642         }
643         return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
644     }
645 
646 
647     public int getCallerTargetCount() {
648         return mCallerTargets.size();
649     }
650 
651     /**
652      * Filter out placeholders and non-selectable service targets
653      */
654     public int getSelectableServiceTargetCount() {
655         int count = 0;
656         for (TargetInfo info : mServiceTargets) {
657             if (info.isSelectableTargetInfo()) {
658                 count++;
659             }
660         }
661         return count;
662     }
663 
664     private static boolean hasSendAction(Intent intent) {
665         String action = intent.getAction();
666         return Intent.ACTION_SEND.equals(action)
667                 || Intent.ACTION_SEND_MULTIPLE.equals(action);
668     }
669 
670     public int getServiceTargetCount() {
671         if (hasSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) {
672             return Math.min(mServiceTargets.size(), mMaxRankedTargets);
673         }
674 
675         return 0;
676     }
677 
678     public int getAlphaTargetCount() {
679         int groupedCount = mSortedList.size();
680         int ungroupedCount = mCallerTargets.size() + getDisplayResolveInfoCount();
681         return (ungroupedCount > mMaxRankedTargets) ? groupedCount : 0;
682     }
683 
684     /**
685      * Fetch ranked app target count
686      */
687     public int getRankedTargetCount() {
688         int spacesAvailable = mMaxRankedTargets - getCallerTargetCount();
689         return Math.min(spacesAvailable, super.getCount());
690     }
691 
692     /** Get all the {@link DisplayResolveInfo} data for our targets. */
693     public DisplayResolveInfo[] getDisplayResolveInfos() {
694         int size = getDisplayResolveInfoCount();
695         DisplayResolveInfo[] resolvedTargets = new DisplayResolveInfo[size];
696         for (int i = 0; i < size; i++) {
697             resolvedTargets[i] = getDisplayResolveInfo(i);
698         }
699         return resolvedTargets;
700     }
701 
702     public int getPositionTargetType(int position) {
703         int offset = 0;
704 
705         final int serviceTargetCount = getServiceTargetCount();
706         if (position < serviceTargetCount) {
707             return TARGET_SERVICE;
708         }
709         offset += serviceTargetCount;
710 
711         final int callerTargetCount = getCallerTargetCount();
712         if (position - offset < callerTargetCount) {
713             return TARGET_CALLER;
714         }
715         offset += callerTargetCount;
716 
717         final int rankedTargetCount = getRankedTargetCount();
718         if (position - offset < rankedTargetCount) {
719             return TARGET_STANDARD;
720         }
721         offset += rankedTargetCount;
722 
723         final int standardTargetCount = getAlphaTargetCount();
724         if (position - offset < standardTargetCount) {
725             return TARGET_STANDARD_AZ;
726         }
727 
728         return TARGET_BAD;
729     }
730 
731     @Override
732     public TargetInfo getItem(int position) {
733         return targetInfoForPosition(position, true);
734     }
735 
736     /**
737      * Find target info for a given position.
738      * Since ChooserActivity displays several sections of content, determine which
739      * section provides this item.
740      */
741     @Override
742     public TargetInfo targetInfoForPosition(int position, boolean filtered) {
743         if (position == NO_POSITION) {
744             return null;
745         }
746 
747         int offset = 0;
748 
749         // Direct share targets
750         final int serviceTargetCount = filtered ? getServiceTargetCount() :
751                 getSelectableServiceTargetCount();
752         if (position < serviceTargetCount) {
753             return mServiceTargets.get(position);
754         }
755         offset += serviceTargetCount;
756 
757         // Targets provided by calling app
758         final int callerTargetCount = getCallerTargetCount();
759         if (position - offset < callerTargetCount) {
760             return mCallerTargets.get(position - offset);
761         }
762         offset += callerTargetCount;
763 
764         // Ranked standard app targets
765         final int rankedTargetCount = getRankedTargetCount();
766         if (position - offset < rankedTargetCount) {
767             return filtered ? super.getItem(position - offset)
768                     : getDisplayResolveInfo(position - offset);
769         }
770         offset += rankedTargetCount;
771 
772         // Alphabetical complete app target list.
773         if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) {
774             return mSortedList.get(position - offset);
775         }
776 
777         return null;
778     }
779 
780     // Check whether {@code dri} should be added into mDisplayList.
781     @Override
782     protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
783         // Checks if this info is already listed in callerTargets.
784         for (TargetInfo existingInfo : mCallerTargets) {
785             if (ResolveInfoHelpers.resolveInfoMatch(
786                     dri.getResolveInfo(), existingInfo.getResolveInfo())) {
787                 return false;
788             }
789         }
790         return super.shouldAddResolveInfo(dri);
791     }
792 
793     /**
794      * Fetch surfaced direct share target info
795      */
796     public List<TargetInfo> getSurfacedTargetInfo() {
797         return mServiceTargets.subList(0,
798                 Math.min(mMaxRankedTargets, getSelectableServiceTargetCount()));
799     }
800 
801 
802     /**
803      * Evaluate targets for inclusion in the direct share area. May not be included
804      * if score is too low.
805      */
806     public void addServiceResults(
807             @Nullable DisplayResolveInfo origTarget,
808             List<ChooserTarget> targets,
809             int targetType,
810             Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
811             Map<ChooserTarget, AppTarget> directShareToAppTargets) {
812         // Avoid inserting any potentially late results.
813         if (isDirectTargetRowEmptyState()) {
814             return;
815         }
816         boolean isShortcutResult = targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
817                 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
818         boolean isUpdated = mShortcutSelectionLogic.addServiceResults(
819                 origTarget,
820                 getBaseScore(origTarget, targetType),
821                 targets,
822                 isShortcutResult,
823                 directShareToShortcutInfos,
824                 directShareToAppTargets,
825                 mContext.createContextAsUser(getUserHandle(), 0),
826                 getTargetIntent(),
827                 mReferrerFillInIntent,
828                 mMaxRankedTargets,
829                 mServiceTargets);
830         if (isUpdated) {
831             notifyDataSetChanged();
832         }
833     }
834 
835     /**
836      * Copy direct targets from another ChooserListAdapter instance
837      */
838     public void copyDirectTargetsFrom(ChooserListAdapter adapter) {
839         if (adapter.isDirectTargetRowEmptyState()) {
840             return;
841         }
842 
843         mServiceTargets.clear();
844         mServiceTargets.addAll(adapter.mServiceTargets);
845     }
846 
847     /**
848      * Reset direct targets
849      */
850     public void resetDirectTargets() {
851         createPlaceHolders();
852     }
853 
854     private boolean isDirectTargetRowEmptyState() {
855         return (mServiceTargets.size() == 1) && mServiceTargets.get(0).isEmptyTargetInfo();
856     }
857 
858     /**
859      * Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
860      * <ol>
861      *   <li>App-supplied targets
862      *   <li>Shortcuts ranked via App Prediction Manager
863      *   <li>Shortcuts ranked via legacy heuristics
864      *   <li>Legacy direct share targets
865      * </ol>
866      */
867     public float getBaseScore(
868             DisplayResolveInfo target,
869             int targetType) {
870         if (target == null) {
871             return CALLER_TARGET_SCORE_BOOST;
872         }
873         float score = super.getScore(target);
874         if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
875                 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
876             return score * SHORTCUT_TARGET_SCORE_BOOST;
877         }
878         return score;
879     }
880 
881     /**
882      * Calling this marks service target loading complete, and will attempt to no longer
883      * update the direct share area.
884      */
885     public void completeServiceTargetLoading() {
886         mServiceTargets.removeIf(o -> o.isPlaceHolderTargetInfo());
887         if (mServiceTargets.isEmpty()) {
888             mServiceTargets.add(NotSelectableTargetInfo.newEmptyTargetInfo());
889             mEventLog.logSharesheetEmptyDirectShareRow();
890         }
891         notifyDataSetChanged();
892     }
893 
894     /**
895      * Rather than fully sorting the input list, this sorting task will put the top k elements
896      * in the head of input list and fill the tail with other elements in undetermined order.
897      */
898     @Override
899     @WorkerThread
900     protected void sortComponents(List<ResolvedComponentInfo> components) {
901         Trace.beginSection("ChooserListAdapter#SortingTask");
902         mResolverListController.topK(components, mMaxRankedTargets);
903         Trace.endSection();
904     }
905 
906     @Override
907     @MainThread
908     protected void onComponentsSorted(
909             @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
910         processSortedList(sortedComponents, doPostProcessing);
911         if (doPostProcessing) {
912             notifyDataSetChanged();
913         }
914     }
915 }
916