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