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