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