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