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