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.ActivityManager;
23 import android.app.prediction.AppPredictor;
24 import android.app.prediction.AppTarget;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.ActivityInfo;
29 import android.content.pm.LabeledIntent;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ResolveInfo;
32 import android.content.pm.ShortcutInfo;
33 import android.graphics.drawable.Drawable;
34 import android.os.AsyncTask;
35 import android.os.UserHandle;
36 import android.os.UserManager;
37 import android.provider.DeviceConfig;
38 import android.service.chooser.ChooserTarget;
39 import android.util.Log;
40 import android.util.Pair;
41 import android.view.View;
42 import android.view.ViewGroup;
43 
44 import com.android.internal.R;
45 import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
46 import com.android.internal.app.chooser.ChooserTargetInfo;
47 import com.android.internal.app.chooser.DisplayResolveInfo;
48 import com.android.internal.app.chooser.MultiDisplayResolveInfo;
49 import com.android.internal.app.chooser.SelectableTargetInfo;
50 import com.android.internal.app.chooser.TargetInfo;
51 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
52 
53 import java.util.ArrayList;
54 import java.util.Collections;
55 import java.util.HashMap;
56 import java.util.HashSet;
57 import java.util.List;
58 import java.util.Map;
59 import java.util.Set;
60 import java.util.stream.Collectors;
61 
62 public class ChooserListAdapter extends ResolverListAdapter {
63     private static final String TAG = "ChooserListAdapter";
64     private static final boolean DEBUG = false;
65 
66     private boolean mAppendDirectShareEnabled = DeviceConfig.getBoolean(
67             DeviceConfig.NAMESPACE_SYSTEMUI,
68             SystemUiDeviceConfigFlags.APPEND_DIRECT_SHARE_ENABLED,
69             true);
70 
71     private boolean mEnableStackedApps = true;
72 
73     public static final int NO_POSITION = -1;
74     public static final int TARGET_BAD = -1;
75     public static final int TARGET_CALLER = 0;
76     public static final int TARGET_SERVICE = 1;
77     public static final int TARGET_STANDARD = 2;
78     public static final int TARGET_STANDARD_AZ = 3;
79 
80     private static final int MAX_SUGGESTED_APP_TARGETS = 4;
81     private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
82     private static final int MAX_SERVICE_TARGET_APP = 8;
83     private static final int DEFAULT_DIRECT_SHARE_RANKING_SCORE = 1000;
84 
85     static final int MAX_SERVICE_TARGETS = 8;
86 
87     /** {@link #getBaseScore} */
88     public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
89     /** {@link #getBaseScore} */
90     public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
91 
92     private final int mMaxShortcutTargetsPerApp;
93     private final ChooserListCommunicator mChooserListCommunicator;
94     private final SelectableTargetInfo.SelectableTargetInfoCommunicator
95             mSelectableTargetInfoCommunicator;
96 
97     private int mNumShortcutResults = 0;
98     private Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
99 
100     // Reserve spots for incoming direct share targets by adding placeholders
101     private ChooserTargetInfo
102             mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
103     private int mValidServiceTargetsNum = 0;
104     private int mAvailableServiceTargetsNum = 0;
105     private final Map<ComponentName, Pair<List<ChooserTargetInfo>, Integer>>
106             mParkingDirectShareTargets = new HashMap<>();
107     private final Map<ComponentName, Map<String, Integer>> mChooserTargetScores = new HashMap<>();
108     private Set<ComponentName> mPendingChooserTargetService = new HashSet<>();
109     private Set<ComponentName> mShortcutComponents = new HashSet<>();
110     private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
111     private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>();
112 
113     private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator =
114             new ChooserActivity.BaseChooserTargetComparator();
115     private boolean mListViewDataChanged = false;
116 
117     // Sorted list of DisplayResolveInfos for the alphabetical app section.
118     private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
119     private AppPredictor mAppPredictor;
120     private AppPredictor.Callback mAppPredictorCallback;
121 
ChooserListAdapter(Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, ChooserListCommunicator chooserListCommunicator, SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator, PackageManager packageManager)122     public ChooserListAdapter(Context context, List<Intent> payloadIntents,
123             Intent[] initialIntents, List<ResolveInfo> rList,
124             boolean filterLastUsed, ResolverListController resolverListController,
125             ChooserListCommunicator chooserListCommunicator,
126             SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator,
127             PackageManager packageManager) {
128         // Don't send the initial intents through the shared ResolverActivity path,
129         // we want to separate them into a different section.
130         super(context, payloadIntents, null, rList, filterLastUsed,
131                 resolverListController, chooserListCommunicator, false);
132 
133         createPlaceHolders();
134         mMaxShortcutTargetsPerApp =
135                 context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
136         mChooserListCommunicator = chooserListCommunicator;
137         mSelectableTargetInfoCommunicator = selectableTargetInfoCommunicator;
138 
139         if (initialIntents != null) {
140             for (int i = 0; i < initialIntents.length; i++) {
141                 final Intent ii = initialIntents[i];
142                 if (ii == null) {
143                     continue;
144                 }
145 
146                 // We reimplement Intent#resolveActivityInfo here because if we have an
147                 // implicit intent, we want the ResolveInfo returned by PackageManager
148                 // instead of one we reconstruct ourselves. The ResolveInfo returned might
149                 // have extra metadata and resolvePackageName set and we want to respect that.
150                 ResolveInfo ri = null;
151                 ActivityInfo ai = null;
152                 final ComponentName cn = ii.getComponent();
153                 if (cn != null) {
154                     try {
155                         ai = packageManager.getActivityInfo(ii.getComponent(), 0);
156                         ri = new ResolveInfo();
157                         ri.activityInfo = ai;
158                     } catch (PackageManager.NameNotFoundException ignored) {
159                         // ai will == null below
160                     }
161                 }
162                 if (ai == null) {
163                     ri = packageManager.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY);
164                     ai = ri != null ? ri.activityInfo : null;
165                 }
166                 if (ai == null) {
167                     Log.w(TAG, "No activity found for " + ii);
168                     continue;
169                 }
170                 UserManager userManager =
171                         (UserManager) context.getSystemService(Context.USER_SERVICE);
172                 if (ii instanceof LabeledIntent) {
173                     LabeledIntent li = (LabeledIntent) ii;
174                     ri.resolvePackageName = li.getSourcePackage();
175                     ri.labelRes = li.getLabelResource();
176                     ri.nonLocalizedLabel = li.getNonLocalizedLabel();
177                     ri.icon = li.getIconResource();
178                     ri.iconResourceId = ri.icon;
179                 }
180                 if (userManager.isManagedProfile()) {
181                     ri.noResourceId = true;
182                     ri.icon = 0;
183                 }
184                 mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
185                 if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
186             }
187         }
188     }
189 
getAppPredictor()190     AppPredictor getAppPredictor() {
191         return mAppPredictor;
192     }
193 
194     @Override
handlePackagesChanged()195     public void handlePackagesChanged() {
196         if (DEBUG) {
197             Log.d(TAG, "clearing queryTargets on package change");
198         }
199         createPlaceHolders();
200         mChooserListCommunicator.onHandlePackagesChanged(this);
201 
202     }
203 
204     @Override
notifyDataSetChanged()205     public void notifyDataSetChanged() {
206         if (!mListViewDataChanged) {
207             mChooserListCommunicator.sendListViewUpdateMessage(getUserHandle());
208             mListViewDataChanged = true;
209         }
210     }
211 
refreshListView()212     void refreshListView() {
213         if (mListViewDataChanged) {
214             if (mAppendDirectShareEnabled) {
215                 appendServiceTargetsWithQuota();
216             }
217             super.notifyDataSetChanged();
218         }
219         mListViewDataChanged = false;
220     }
221 
222 
createPlaceHolders()223     private void createPlaceHolders() {
224         mNumShortcutResults = 0;
225         mServiceTargets.clear();
226         mValidServiceTargetsNum = 0;
227         mParkingDirectShareTargets.clear();
228         mPendingChooserTargetService.clear();
229         mShortcutComponents.clear();
230         for (int i = 0; i < MAX_SERVICE_TARGETS; i++) {
231             mServiceTargets.add(mPlaceHolderTargetInfo);
232         }
233     }
234 
235     @Override
onCreateView(ViewGroup parent)236     View onCreateView(ViewGroup parent) {
237         return mInflater.inflate(
238                 com.android.internal.R.layout.resolve_grid_item, parent, false);
239     }
240 
241     @Override
onBindView(View view, TargetInfo info, int position)242     protected void onBindView(View view, TargetInfo info, int position) {
243         final ViewHolder holder = (ViewHolder) view.getTag();
244         if (info == null) {
245             holder.icon.setImageDrawable(
246                     mContext.getDrawable(R.drawable.resolver_icon_placeholder));
247             return;
248         }
249 
250         if (!(info instanceof DisplayResolveInfo)) {
251             holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
252             holder.bindIcon(info);
253 
254             if (info instanceof SelectableTargetInfo) {
255                 // direct share targets should append the application name for a better readout
256                 DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
257                 CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
258                 CharSequence extendedInfo = info.getExtendedInfo();
259                 String contentDescription = String.join(" ", info.getDisplayLabel(),
260                         extendedInfo != null ? extendedInfo : "", appName);
261                 holder.updateContentDescription(contentDescription);
262             }
263         } else {
264             DisplayResolveInfo dri = (DisplayResolveInfo) info;
265             holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel());
266             LoadIconTask task = mIconLoaders.get(dri);
267             if (task == null) {
268                 task = new LoadIconTask(dri, holder);
269                 mIconLoaders.put(dri, task);
270                 task.execute();
271             } else {
272                 // The holder was potentially changed as the underlying items were
273                 // reshuffled, so reset the target holder
274                 task.setViewHolder(holder);
275             }
276         }
277 
278         // If target is loading, show a special placeholder shape in the label, make unclickable
279         if (info instanceof ChooserActivity.PlaceHolderTargetInfo) {
280             final int maxWidth = mContext.getResources().getDimensionPixelSize(
281                     R.dimen.chooser_direct_share_label_placeholder_max_width);
282             holder.text.setMaxWidth(maxWidth);
283             holder.text.setBackground(mContext.getResources().getDrawable(
284                     R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme()));
285             // Prevent rippling by removing background containing ripple
286             holder.itemView.setBackground(null);
287         } else {
288             holder.text.setMaxWidth(Integer.MAX_VALUE);
289             holder.text.setBackground(null);
290             holder.itemView.setBackground(holder.defaultItemViewBackground);
291         }
292 
293         if (info instanceof MultiDisplayResolveInfo) {
294             // If the target is grouped show an indicator
295             Drawable bkg = mContext.getDrawable(R.drawable.chooser_group_background);
296             holder.text.setPaddingRelative(0, 0, bkg.getIntrinsicWidth() /* end */, 0);
297             holder.text.setBackground(bkg);
298         } else if (info.isPinned() && getPositionTargetType(position) == TARGET_STANDARD) {
299             // If the target is pinned and in the suggested row show a pinned indicator
300             Drawable bkg = mContext.getDrawable(R.drawable.chooser_pinned_background);
301             holder.text.setPaddingRelative(bkg.getIntrinsicWidth() /* start */, 0, 0, 0);
302             holder.text.setBackground(bkg);
303         } else {
304             holder.text.setBackground(null);
305             holder.text.setPaddingRelative(0, 0, 0, 0);
306         }
307     }
308 
updateAlphabeticalList()309     void updateAlphabeticalList() {
310         new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
311             @Override
312             protected List<DisplayResolveInfo> doInBackground(Void... voids) {
313                 List<DisplayResolveInfo> allTargets = new ArrayList<>();
314                 allTargets.addAll(mDisplayList);
315                 allTargets.addAll(mCallerTargets);
316                 if (!mEnableStackedApps) {
317                     return allTargets;
318                 }
319                 // Consolidate multiple targets from same app.
320                 Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
321                 for (DisplayResolveInfo info : allTargets) {
322                     String packageName = info.getResolvedComponentName().getPackageName();
323                     DisplayResolveInfo multiDri = consolidated.get(packageName);
324                     if (multiDri == null) {
325                         consolidated.put(packageName, info);
326                     } else if (multiDri instanceof MultiDisplayResolveInfo) {
327                         ((MultiDisplayResolveInfo) multiDri).addTarget(info);
328                     } else {
329                         // create consolidated target from the single DisplayResolveInfo
330                         MultiDisplayResolveInfo multiDisplayResolveInfo =
331                             new MultiDisplayResolveInfo(packageName, multiDri);
332                         multiDisplayResolveInfo.addTarget(info);
333                         consolidated.put(packageName, multiDisplayResolveInfo);
334                     }
335                 }
336                 List<DisplayResolveInfo> groupedTargets = new ArrayList<>();
337                 groupedTargets.addAll(consolidated.values());
338                 Collections.sort(groupedTargets, new ChooserActivity.AzInfoComparator(mContext));
339                 return groupedTargets;
340             }
341             @Override
342             protected void onPostExecute(List<DisplayResolveInfo> newList) {
343                 mSortedList = newList;
344                 notifyDataSetChanged();
345             }
346         }.execute();
347     }
348 
349     @Override
getCount()350     public int getCount() {
351         return getRankedTargetCount() + getAlphaTargetCount()
352                 + getSelectableServiceTargetCount() + getCallerTargetCount();
353     }
354 
355     @Override
getUnfilteredCount()356     public int getUnfilteredCount() {
357         int appTargets = super.getUnfilteredCount();
358         if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) {
359             appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets();
360         }
361         return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
362     }
363 
364 
getCallerTargetCount()365     public int getCallerTargetCount() {
366         return mCallerTargets.size();
367     }
368 
369     /**
370      * Filter out placeholders and non-selectable service targets
371      */
getSelectableServiceTargetCount()372     public int getSelectableServiceTargetCount() {
373         int count = 0;
374         for (ChooserTargetInfo info : mServiceTargets) {
375             if (info instanceof SelectableTargetInfo) {
376                 count++;
377             }
378         }
379         return count;
380     }
381 
getServiceTargetCount()382     public int getServiceTargetCount() {
383         if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent())
384                 && !ActivityManager.isLowRamDeviceStatic()) {
385             return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS);
386         }
387 
388         return 0;
389     }
390 
getAlphaTargetCount()391     int getAlphaTargetCount() {
392         int groupedCount = mSortedList.size();
393         int ungroupedCount = mCallerTargets.size() + mDisplayList.size();
394         return ungroupedCount > mChooserListCommunicator.getMaxRankedTargets() ? groupedCount : 0;
395     }
396 
397     /**
398      * Fetch ranked app target count
399      */
getRankedTargetCount()400     public int getRankedTargetCount() {
401         int spacesAvailable =
402                 mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount();
403         return Math.min(spacesAvailable, super.getCount());
404     }
405 
getPositionTargetType(int position)406     public int getPositionTargetType(int position) {
407         int offset = 0;
408 
409         final int serviceTargetCount = getServiceTargetCount();
410         if (position < serviceTargetCount) {
411             return TARGET_SERVICE;
412         }
413         offset += serviceTargetCount;
414 
415         final int callerTargetCount = getCallerTargetCount();
416         if (position - offset < callerTargetCount) {
417             return TARGET_CALLER;
418         }
419         offset += callerTargetCount;
420 
421         final int rankedTargetCount = getRankedTargetCount();
422         if (position - offset < rankedTargetCount) {
423             return TARGET_STANDARD;
424         }
425         offset += rankedTargetCount;
426 
427         final int standardTargetCount = getAlphaTargetCount();
428         if (position - offset < standardTargetCount) {
429             return TARGET_STANDARD_AZ;
430         }
431 
432         return TARGET_BAD;
433     }
434 
435     @Override
getItem(int position)436     public TargetInfo getItem(int position) {
437         return targetInfoForPosition(position, true);
438     }
439 
440 
441     /**
442      * Find target info for a given position.
443      * Since ChooserActivity displays several sections of content, determine which
444      * section provides this item.
445      */
446     @Override
targetInfoForPosition(int position, boolean filtered)447     public TargetInfo targetInfoForPosition(int position, boolean filtered) {
448         if (position == NO_POSITION) {
449             return null;
450         }
451 
452         int offset = 0;
453 
454         // Direct share targets
455         final int serviceTargetCount = filtered ? getServiceTargetCount() :
456                 getSelectableServiceTargetCount();
457         if (position < serviceTargetCount) {
458             return mServiceTargets.get(position);
459         }
460         offset += serviceTargetCount;
461 
462         // Targets provided by calling app
463         final int callerTargetCount = getCallerTargetCount();
464         if (position - offset < callerTargetCount) {
465             return mCallerTargets.get(position - offset);
466         }
467         offset += callerTargetCount;
468 
469         // Ranked standard app targets
470         final int rankedTargetCount = getRankedTargetCount();
471         if (position - offset < rankedTargetCount) {
472             return filtered ? super.getItem(position - offset)
473                     : getDisplayResolveInfo(position - offset);
474         }
475         offset += rankedTargetCount;
476 
477         // Alphabetical complete app target list.
478         if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) {
479             return mSortedList.get(position - offset);
480         }
481 
482         return null;
483     }
484 
485     // Check whether {@code dri} should be added into mDisplayList.
486     @Override
shouldAddResolveInfo(DisplayResolveInfo dri)487     protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
488         // Checks if this info is already listed in callerTargets.
489         for (TargetInfo existingInfo : mCallerTargets) {
490             if (mResolverListCommunicator
491                     .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
492                 return false;
493             }
494         }
495         return super.shouldAddResolveInfo(dri);
496     }
497 
498     /**
499      * Fetch surfaced direct share target info
500      */
getSurfacedTargetInfo()501     public List<ChooserTargetInfo> getSurfacedTargetInfo() {
502         int maxSurfacedTargets = mChooserListCommunicator.getMaxRankedTargets();
503         return mServiceTargets.subList(0,
504                 Math.min(maxSurfacedTargets, getSelectableServiceTargetCount()));
505     }
506 
507 
508     /**
509      * Evaluate targets for inclusion in the direct share area. May not be included
510      * if score is too low.
511      */
addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets, @ChooserActivity.ShareTargetType int targetType, Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos, List<ChooserActivity.ChooserTargetServiceConnection> pendingChooserTargetServiceConnections)512     public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
513             @ChooserActivity.ShareTargetType int targetType,
514             Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
515             List<ChooserActivity.ChooserTargetServiceConnection>
516                     pendingChooserTargetServiceConnections) {
517         if (DEBUG) {
518             Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", "
519                     + targets.size()
520                     + " targets");
521         }
522         if (mAppendDirectShareEnabled) {
523             parkTargetIntoMemory(origTarget, targets, targetType, directShareToShortcutInfos,
524                     pendingChooserTargetServiceConnections);
525             return;
526         }
527         if (targets.size() == 0) {
528             return;
529         }
530 
531         final float baseScore = getBaseScore(origTarget, targetType);
532         Collections.sort(targets, mBaseTargetComparator);
533 
534         final boolean isShortcutResult =
535                 (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
536                         || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
537         final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
538                 : MAX_CHOOSER_TARGETS_PER_APP;
539         float lastScore = 0;
540         boolean shouldNotify = false;
541         for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) {
542             final ChooserTarget target = targets.get(i);
543             float targetScore = target.getScore();
544             targetScore *= baseScore;
545             if (i > 0 && targetScore >= lastScore) {
546                 // Apply a decay so that the top app can't crowd out everything else.
547                 // This incents ChooserTargetServices to define what's truly better.
548                 targetScore = lastScore * 0.95f;
549             }
550             UserHandle userHandle = getUserHandle();
551             Context contextAsUser = mContext.createContextAsUser(userHandle, 0 /* flags */);
552             boolean isInserted = insertServiceTarget(new SelectableTargetInfo(contextAsUser,
553                     origTarget, target, targetScore, mSelectableTargetInfoCommunicator,
554                     (isShortcutResult ? directShareToShortcutInfos.get(target) : null)));
555 
556             if (isInserted && isShortcutResult) {
557                 mNumShortcutResults++;
558             }
559 
560             shouldNotify |= isInserted;
561 
562             if (DEBUG) {
563                 Log.d(TAG, " => " + target.toString() + " score=" + targetScore
564                         + " base=" + target.getScore()
565                         + " lastScore=" + lastScore
566                         + " baseScore=" + baseScore);
567             }
568 
569             lastScore = targetScore;
570         }
571 
572         if (shouldNotify) {
573             notifyDataSetChanged();
574         }
575     }
576 
577     /**
578      * Store ChooserTarget ranking scores info wrapped in {@code targets}.
579      */
addChooserTargetRankingScore(List<AppTarget> targets)580     public void addChooserTargetRankingScore(List<AppTarget> targets) {
581         Log.i(TAG, "addChooserTargetRankingScore " + targets.size() + " targets score.");
582         for (AppTarget target : targets) {
583             if (target.getShortcutInfo() == null) {
584                 continue;
585             }
586             ShortcutInfo shortcutInfo = target.getShortcutInfo();
587             if (!shortcutInfo.getId().equals(ChooserActivity.CHOOSER_TARGET)
588                     || shortcutInfo.getActivity() == null) {
589                 continue;
590             }
591             ComponentName componentName = shortcutInfo.getActivity();
592             if (!mChooserTargetScores.containsKey(componentName)) {
593                 mChooserTargetScores.put(componentName, new HashMap<>());
594             }
595             mChooserTargetScores.get(componentName).put(shortcutInfo.getShortLabel().toString(),
596                     target.getRank());
597         }
598         mChooserTargetScores.keySet().forEach(key -> rankTargetsWithinComponent(key));
599     }
600 
601     /**
602      * Rank chooserTargets of the given {@code componentName} in mParkingDirectShareTargets as per
603      * available scores stored in mChooserTargetScores.
604      */
rankTargetsWithinComponent(ComponentName componentName)605     private void rankTargetsWithinComponent(ComponentName componentName) {
606         if (!mParkingDirectShareTargets.containsKey(componentName)
607                 || !mChooserTargetScores.containsKey(componentName)) {
608             return;
609         }
610         Map<String, Integer> scores = mChooserTargetScores.get(componentName);
611         Collections.sort(mParkingDirectShareTargets.get(componentName).first, (o1, o2) -> {
612             // The score has been normalized between 0 and 2000, the default is 1000.
613             int score1 = scores.getOrDefault(
614                     ChooserUtil.md5(o1.getChooserTarget().getTitle().toString()),
615                     DEFAULT_DIRECT_SHARE_RANKING_SCORE);
616             int score2 = scores.getOrDefault(
617                     ChooserUtil.md5(o2.getChooserTarget().getTitle().toString()),
618                     DEFAULT_DIRECT_SHARE_RANKING_SCORE);
619             return score2 - score1;
620         });
621     }
622 
623     /**
624      * Park {@code targets} into memory for the moment to surface them later when view is refreshed.
625      * Components pending on ChooserTargetService query are also recorded.
626      */
parkTargetIntoMemory(DisplayResolveInfo origTarget, List<ChooserTarget> targets, @ChooserActivity.ShareTargetType int targetType, Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos, List<ChooserActivity.ChooserTargetServiceConnection> pendingChooserTargetServiceConnections)627     private void parkTargetIntoMemory(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
628             @ChooserActivity.ShareTargetType int targetType,
629             Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
630             List<ChooserActivity.ChooserTargetServiceConnection>
631                     pendingChooserTargetServiceConnections) {
632         ComponentName origComponentName = origTarget != null ? origTarget.getResolvedComponentName()
633                 : !targets.isEmpty() ? targets.get(0).getComponentName() : null;
634         Log.i(TAG,
635                 "parkTargetIntoMemory " + origComponentName + ", " + targets.size() + " targets");
636         mPendingChooserTargetService = pendingChooserTargetServiceConnections.stream()
637                 .map(ChooserActivity.ChooserTargetServiceConnection::getComponentName)
638                 .filter(componentName -> !componentName.equals(origComponentName))
639                 .collect(Collectors.toSet());
640         // Park targets in memory
641         if (!targets.isEmpty()) {
642             final boolean isShortcutResult =
643                     (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
644                             || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
645             Context contextAsUser = mContext.createContextAsUser(getUserHandle(),
646                     0 /* flags */);
647             List<ChooserTargetInfo> parkingTargetInfos = targets.stream()
648                     .map(target ->
649                             new SelectableTargetInfo(
650                                     contextAsUser, origTarget, target, target.getScore(),
651                                     mSelectableTargetInfoCommunicator,
652                                     (isShortcutResult ? directShareToShortcutInfos.get(target)
653                                             : null))
654                     )
655                     .collect(Collectors.toList());
656             Pair<List<ChooserTargetInfo>, Integer> parkingTargetInfoPair =
657                     mParkingDirectShareTargets.getOrDefault(origComponentName,
658                             new Pair<>(new ArrayList<>(), 0));
659             for (ChooserTargetInfo target : parkingTargetInfos) {
660                 if (!checkDuplicateTarget(target, parkingTargetInfoPair.first)
661                         && !checkDuplicateTarget(target, mServiceTargets)) {
662                     parkingTargetInfoPair.first.add(target);
663                     mAvailableServiceTargetsNum++;
664                 }
665             }
666             mParkingDirectShareTargets.put(origComponentName, parkingTargetInfoPair);
667             rankTargetsWithinComponent(origComponentName);
668             if (isShortcutResult) {
669                 mShortcutComponents.add(origComponentName);
670             }
671         }
672         notifyDataSetChanged();
673     }
674 
675     /**
676      * Append targets of top ranked share app into direct share row with quota limit. Remove
677      * appended ones from memory.
678      */
appendServiceTargetsWithQuota()679     private void appendServiceTargetsWithQuota() {
680         int maxRankedTargets = mChooserListCommunicator.getMaxRankedTargets();
681         List<ComponentName> topComponentNames = getTopComponentNames(maxRankedTargets);
682         float totalScore = 0f;
683         for (ComponentName component : topComponentNames) {
684             if (!mPendingChooserTargetService.contains(component)
685                     && !mParkingDirectShareTargets.containsKey(component)) {
686                 continue;
687             }
688             totalScore += super.getScore(component);
689         }
690         boolean shouldWaitPendingService = false;
691         for (ComponentName component : topComponentNames) {
692             if (!mPendingChooserTargetService.contains(component)
693                     && !mParkingDirectShareTargets.containsKey(component)) {
694                 continue;
695             }
696             float score = super.getScore(component);
697             int quota = Math.round(maxRankedTargets * score / totalScore);
698             if (mPendingChooserTargetService.contains(component) && quota >= 1) {
699                 shouldWaitPendingService = true;
700             }
701             if (!mParkingDirectShareTargets.containsKey(component)) {
702                 continue;
703             }
704             // Append targets into direct share row as per quota.
705             Pair<List<ChooserTargetInfo>, Integer> parkingTargetsItem =
706                     mParkingDirectShareTargets.get(component);
707             List<ChooserTargetInfo> parkingTargets = parkingTargetsItem.first;
708             int insertedNum = parkingTargetsItem.second;
709             while (insertedNum < quota && !parkingTargets.isEmpty()) {
710                 if (!checkDuplicateTarget(parkingTargets.get(0), mServiceTargets)) {
711                     mServiceTargets.add(mValidServiceTargetsNum, parkingTargets.get(0));
712                     mValidServiceTargetsNum++;
713                     insertedNum++;
714                 }
715                 parkingTargets.remove(0);
716             }
717             Log.i(TAG, " appendServiceTargetsWithQuota component=" + component
718                     + " appendNum=" + (insertedNum - parkingTargetsItem.second));
719             if (DEBUG) {
720                 Log.d(TAG, " appendServiceTargetsWithQuota component=" + component
721                         + " score=" + score
722                         + " totalScore=" + totalScore
723                         + " quota=" + quota);
724             }
725             mParkingDirectShareTargets.put(component, new Pair<>(parkingTargets, insertedNum));
726         }
727         if (!shouldWaitPendingService) {
728             fillAllServiceTargets();
729         }
730     }
731 
732     /**
733      * Append all remaining targets (parking in memory) into direct share row as per their ranking.
734      */
fillAllServiceTargets()735     private void fillAllServiceTargets() {
736         if (mParkingDirectShareTargets.isEmpty()) {
737             return;
738         }
739         Log.i(TAG, " fillAllServiceTargets");
740         List<ComponentName> topComponentNames = getTopComponentNames(MAX_SERVICE_TARGET_APP);
741         // Append all remaining targets of top recommended components into direct share row.
742         for (ComponentName component : topComponentNames) {
743             if (!mParkingDirectShareTargets.containsKey(component)) {
744                 continue;
745             }
746             mParkingDirectShareTargets.get(component).first.stream()
747                     .filter(target -> !checkDuplicateTarget(target, mServiceTargets))
748                     .forEach(target -> {
749                         mServiceTargets.add(mValidServiceTargetsNum, target);
750                         mValidServiceTargetsNum++;
751                     });
752             mParkingDirectShareTargets.remove(component);
753         }
754         // Append all remaining shortcuts targets into direct share row.
755         mParkingDirectShareTargets.entrySet().stream()
756                 .filter(entry -> mShortcutComponents.contains(entry.getKey()))
757                 .map(entry -> entry.getValue())
758                 .map(pair -> pair.first)
759                 .forEach(targets -> {
760                     for (ChooserTargetInfo target : targets) {
761                         if (!checkDuplicateTarget(target, mServiceTargets)) {
762                             mServiceTargets.add(mValidServiceTargetsNum, target);
763                             mValidServiceTargetsNum++;
764                         }
765                     }
766                 });
767         mParkingDirectShareTargets.clear();
768     }
769 
checkDuplicateTarget(ChooserTargetInfo target, List<ChooserTargetInfo> destination)770     private boolean checkDuplicateTarget(ChooserTargetInfo target,
771             List<ChooserTargetInfo> destination) {
772         // Check for duplicates and abort if found
773         for (ChooserTargetInfo otherTargetInfo : destination) {
774             if (target.isSimilar(otherTargetInfo)) {
775                 return true;
776             }
777         }
778         return false;
779     }
780 
781     /**
782      * The return number have to exceed a minimum limit to make direct share area expandable. When
783      * append direct share targets is enabled, return count of all available targets parking in the
784      * memory; otherwise, it is shortcuts count which will help reduce the amount of visible
785      * shuffling due to older-style direct share targets.
786      */
getNumServiceTargetsForExpand()787     int getNumServiceTargetsForExpand() {
788         return mAppendDirectShareEnabled ? mAvailableServiceTargetsNum : mNumShortcutResults;
789     }
790 
791     /**
792      * Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
793      * <ol>
794      *   <li>App-supplied targets
795      *   <li>Shortcuts ranked via App Prediction Manager
796      *   <li>Shortcuts ranked via legacy heuristics
797      *   <li>Legacy direct share targets
798      * </ol>
799      */
getBaseScore( DisplayResolveInfo target, @ChooserActivity.ShareTargetType int targetType)800     public float getBaseScore(
801             DisplayResolveInfo target,
802             @ChooserActivity.ShareTargetType int targetType) {
803         if (target == null) {
804             return CALLER_TARGET_SCORE_BOOST;
805         }
806 
807         if (targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
808             return SHORTCUT_TARGET_SCORE_BOOST;
809         }
810 
811         float score = super.getScore(target);
812         if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
813             return score * SHORTCUT_TARGET_SCORE_BOOST;
814         }
815 
816         return score;
817     }
818 
819     /**
820      * Calling this marks service target loading complete, and will attempt to no longer
821      * update the direct share area.
822      */
completeServiceTargetLoading()823     public void completeServiceTargetLoading() {
824         mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
825         if (mAppendDirectShareEnabled) {
826             fillAllServiceTargets();
827         }
828         if (mServiceTargets.isEmpty()) {
829             mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
830         }
831         notifyDataSetChanged();
832     }
833 
insertServiceTarget(ChooserTargetInfo chooserTargetInfo)834     private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
835         // Avoid inserting any potentially late results
836         if (mServiceTargets.size() == 1
837                 && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
838             return false;
839         }
840 
841         // Check for duplicates and abort if found
842         for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
843             if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
844                 return false;
845             }
846         }
847 
848         int currentSize = mServiceTargets.size();
849         final float newScore = chooserTargetInfo.getModifiedScore();
850         for (int i = 0; i < Math.min(currentSize, MAX_SERVICE_TARGETS); i++) {
851             final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
852             if (serviceTarget == null) {
853                 mServiceTargets.set(i, chooserTargetInfo);
854                 return true;
855             } else if (newScore > serviceTarget.getModifiedScore()) {
856                 mServiceTargets.add(i, chooserTargetInfo);
857                 return true;
858             }
859         }
860 
861         if (currentSize < MAX_SERVICE_TARGETS) {
862             mServiceTargets.add(chooserTargetInfo);
863             return true;
864         }
865 
866         return false;
867     }
868 
getChooserTargetForValue(int value)869     public ChooserTarget getChooserTargetForValue(int value) {
870         return mServiceTargets.get(value).getChooserTarget();
871     }
872 
alwaysShowSubLabel()873     protected boolean alwaysShowSubLabel() {
874         // Always show a subLabel for visual consistency across list items. Show an empty
875         // subLabel if the subLabel is the same as the label
876         return true;
877     }
878 
879     /**
880      * Rather than fully sorting the input list, this sorting task will put the top k elements
881      * in the head of input list and fill the tail with other elements in undetermined order.
882      */
883     @Override
884     AsyncTask<List<ResolvedComponentInfo>,
885                 Void,
createSortingTask(boolean doPostProcessing)886                 List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
887         return new AsyncTask<List<ResolvedComponentInfo>,
888                 Void,
889                 List<ResolvedComponentInfo>>() {
890             @Override
891             protected List<ResolvedComponentInfo> doInBackground(
892                     List<ResolvedComponentInfo>... params) {
893                 mResolverListController.topK(params[0],
894                         mChooserListCommunicator.getMaxRankedTargets());
895                 return params[0];
896             }
897             @Override
898             protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
899                 processSortedList(sortedComponents, doPostProcessing);
900                 if (doPostProcessing) {
901                     mChooserListCommunicator.updateProfileViewButton();
902                     notifyDataSetChanged();
903                 }
904             }
905         };
906     }
907 
908     public void setAppPredictor(AppPredictor appPredictor) {
909         mAppPredictor = appPredictor;
910     }
911 
912     public void setAppPredictorCallback(AppPredictor.Callback appPredictorCallback) {
913         mAppPredictorCallback = appPredictorCallback;
914     }
915 
916     public void destroyAppPredictor() {
917         if (getAppPredictor() != null) {
918             getAppPredictor().unregisterPredictionUpdates(mAppPredictorCallback);
919             getAppPredictor().destroy();
920             setAppPredictor(null);
921         }
922     }
923 
924     /**
925      * Necessary methods to communicate between {@link ChooserListAdapter}
926      * and {@link ChooserActivity}.
927      */
928     interface ChooserListCommunicator extends ResolverListCommunicator {
929 
930         int getMaxRankedTargets();
931 
932         void sendListViewUpdateMessage(UserHandle userHandle);
933 
934         boolean isSendAction(Intent targetIntent);
935     }
936 }
937