blob: de5ab6f1c90d1f89555c90c9dc1c06590f0d3dee [file] [log] [blame]
arangelovb0802dc2019-10-18 18:03:44 +01001/*
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
17package com.android.internal.app;
18
19import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
20import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
21
22import android.app.ActivityManager;
arangelov5fc9e7d2020-01-07 17:59:14 +000023import android.app.prediction.AppPredictor;
Song Hu5aab2d32020-04-30 10:07:15 -070024import android.app.prediction.AppTarget;
arangelovb0802dc2019-10-18 18:03:44 +010025import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.content.pm.ActivityInfo;
29import android.content.pm.LabeledIntent;
30import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
arangelov5fc9e7d2020-01-07 17:59:14 +000032import android.content.pm.ShortcutInfo;
Mike Digman438d1322020-05-13 15:59:55 -070033import android.graphics.drawable.Drawable;
arangelovb0802dc2019-10-18 18:03:44 +010034import android.os.AsyncTask;
arangelov5fc9e7d2020-01-07 17:59:14 +000035import android.os.UserHandle;
arangelovb0802dc2019-10-18 18:03:44 +010036import android.os.UserManager;
Song Hue2deffd2020-03-09 15:22:29 -070037import android.provider.DeviceConfig;
arangelovb0802dc2019-10-18 18:03:44 +010038import android.service.chooser.ChooserTarget;
39import android.util.Log;
Song Hue2deffd2020-03-09 15:22:29 -070040import android.util.Pair;
arangelovb0802dc2019-10-18 18:03:44 +010041import android.view.View;
42import android.view.ViewGroup;
43
44import com.android.internal.R;
45import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
46import com.android.internal.app.chooser.ChooserTargetInfo;
47import com.android.internal.app.chooser.DisplayResolveInfo;
Alison Cichowlas19ee2922019-12-16 19:43:12 -050048import com.android.internal.app.chooser.MultiDisplayResolveInfo;
arangelovb0802dc2019-10-18 18:03:44 +010049import com.android.internal.app.chooser.SelectableTargetInfo;
50import com.android.internal.app.chooser.TargetInfo;
Song Hue2deffd2020-03-09 15:22:29 -070051import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
arangelovb0802dc2019-10-18 18:03:44 +010052
53import java.util.ArrayList;
54import java.util.Collections;
Alison Cichowlas19ee2922019-12-16 19:43:12 -050055import java.util.HashMap;
Song Hue2deffd2020-03-09 15:22:29 -070056import java.util.HashSet;
arangelovb0802dc2019-10-18 18:03:44 +010057import java.util.List;
Alison Cichowlas19ee2922019-12-16 19:43:12 -050058import java.util.Map;
Song Hue2deffd2020-03-09 15:22:29 -070059import java.util.Set;
60import java.util.stream.Collectors;
arangelovb0802dc2019-10-18 18:03:44 +010061
62public class ChooserListAdapter extends ResolverListAdapter {
63 private static final String TAG = "ChooserListAdapter";
64 private static final boolean DEBUG = false;
65
Song Hue2deffd2020-03-09 15:22:29 -070066 private boolean mAppendDirectShareEnabled = DeviceConfig.getBoolean(
67 DeviceConfig.NAMESPACE_SYSTEMUI,
68 SystemUiDeviceConfigFlags.APPEND_DIRECT_SHARE_ENABLED,
Song Hu2eb5dbb2020-04-02 15:47:17 -070069 true);
Song Hue2deffd2020-03-09 15:22:29 -070070
Alison Cichowlas19ee2922019-12-16 19:43:12 -050071 private boolean mEnableStackedApps = true;
72
Zhen Zhangbde7b462019-11-11 11:49:33 -080073 public static final int NO_POSITION = -1;
arangelovb0802dc2019-10-18 18:03:44 +010074 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;
Song Hub548df92020-05-16 08:42:54 -070082 private static final int MAX_SERVICE_TARGET_APP = 8;
Song Hu93cefaa2020-05-31 10:53:03 -070083 private static final int DEFAULT_DIRECT_SHARE_RANKING_SCORE = 1000;
arangelovb0802dc2019-10-18 18:03:44 +010084
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
arangelov1b11cc12020-04-27 17:23:04 +010095 mSelectableTargetInfoCommunicator;
arangelovb0802dc2019-10-18 18:03:44 +010096
97 private int mNumShortcutResults = 0;
98
99 // Reserve spots for incoming direct share targets by adding placeholders
100 private ChooserTargetInfo
101 mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
Song Hue2deffd2020-03-09 15:22:29 -0700102 private int mValidServiceTargetsNum = 0;
Song Hub548df92020-05-16 08:42:54 -0700103 private int mAvailableServiceTargetsNum = 0;
Song Hue2deffd2020-03-09 15:22:29 -0700104 private final Map<ComponentName, Pair<List<ChooserTargetInfo>, Integer>>
105 mParkingDirectShareTargets = new HashMap<>();
Song Hu5aab2d32020-04-30 10:07:15 -0700106 private final Map<ComponentName, Map<String, Integer>> mChooserTargetScores = new HashMap<>();
Song Hue2deffd2020-03-09 15:22:29 -0700107 private Set<ComponentName> mPendingChooserTargetService = new HashSet<>();
108 private Set<ComponentName> mShortcutComponents = new HashSet<>();
arangelovb0802dc2019-10-18 18:03:44 +0100109 private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
Song Hu276e3842020-05-13 15:34:29 -0700110 private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>();
arangelovb0802dc2019-10-18 18:03:44 +0100111
112 private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator =
113 new ChooserActivity.BaseChooserTargetComparator();
114 private boolean mListViewDataChanged = false;
115
116 // Sorted list of DisplayResolveInfos for the alphabetical app section.
117 private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
arangelov5fc9e7d2020-01-07 17:59:14 +0000118 private AppPredictor mAppPredictor;
119 private AppPredictor.Callback mAppPredictorCallback;
arangelovb0802dc2019-10-18 18:03:44 +0100120
121 public ChooserListAdapter(Context context, List<Intent> payloadIntents,
122 Intent[] initialIntents, List<ResolveInfo> rList,
123 boolean filterLastUsed, ResolverListController resolverListController,
arangelovb0802dc2019-10-18 18:03:44 +0100124 ChooserListCommunicator chooserListCommunicator,
arangelov1b11cc12020-04-27 17:23:04 +0100125 SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator,
126 PackageManager packageManager) {
arangelovb0802dc2019-10-18 18:03:44 +0100127 // Don't send the initial intents through the shared ResolverActivity path,
128 // we want to separate them into a different section.
129 super(context, payloadIntents, null, rList, filterLastUsed,
Narayan Kamath75418682020-05-07 13:42:05 +0100130 resolverListController, chooserListCommunicator, false);
arangelovb0802dc2019-10-18 18:03:44 +0100131
132 createPlaceHolders();
133 mMaxShortcutTargetsPerApp =
134 context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
135 mChooserListCommunicator = chooserListCommunicator;
arangelov1b11cc12020-04-27 17:23:04 +0100136 mSelectableTargetInfoCommunicator = selectableTargetInfoCommunicator;
arangelovb0802dc2019-10-18 18:03:44 +0100137
138 if (initialIntents != null) {
arangelovb0802dc2019-10-18 18:03:44 +0100139 for (int i = 0; i < initialIntents.length; i++) {
140 final Intent ii = initialIntents[i];
141 if (ii == null) {
142 continue;
143 }
144
145 // We reimplement Intent#resolveActivityInfo here because if we have an
146 // implicit intent, we want the ResolveInfo returned by PackageManager
147 // instead of one we reconstruct ourselves. The ResolveInfo returned might
148 // have extra metadata and resolvePackageName set and we want to respect that.
149 ResolveInfo ri = null;
150 ActivityInfo ai = null;
151 final ComponentName cn = ii.getComponent();
152 if (cn != null) {
153 try {
arangelov1b11cc12020-04-27 17:23:04 +0100154 ai = packageManager.getActivityInfo(ii.getComponent(), 0);
arangelovb0802dc2019-10-18 18:03:44 +0100155 ri = new ResolveInfo();
156 ri.activityInfo = ai;
157 } catch (PackageManager.NameNotFoundException ignored) {
158 // ai will == null below
159 }
160 }
161 if (ai == null) {
arangelov1b11cc12020-04-27 17:23:04 +0100162 ri = packageManager.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY);
arangelovb0802dc2019-10-18 18:03:44 +0100163 ai = ri != null ? ri.activityInfo : null;
164 }
165 if (ai == null) {
166 Log.w(TAG, "No activity found for " + ii);
167 continue;
168 }
169 UserManager userManager =
170 (UserManager) context.getSystemService(Context.USER_SERVICE);
171 if (ii instanceof LabeledIntent) {
172 LabeledIntent li = (LabeledIntent) ii;
173 ri.resolvePackageName = li.getSourcePackage();
174 ri.labelRes = li.getLabelResource();
175 ri.nonLocalizedLabel = li.getNonLocalizedLabel();
176 ri.icon = li.getIconResource();
177 ri.iconResourceId = ri.icon;
178 }
179 if (userManager.isManagedProfile()) {
180 ri.noResourceId = true;
181 ri.icon = 0;
182 }
183 mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
Matt Pietal1245b382020-06-10 09:56:38 -0400184 if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
arangelovb0802dc2019-10-18 18:03:44 +0100185 }
186 }
187 }
188
arangelov5fc9e7d2020-01-07 17:59:14 +0000189 AppPredictor getAppPredictor() {
190 return mAppPredictor;
191 }
192
arangelovb0802dc2019-10-18 18:03:44 +0100193 @Override
194 public void handlePackagesChanged() {
195 if (DEBUG) {
196 Log.d(TAG, "clearing queryTargets on package change");
197 }
198 createPlaceHolders();
arangelov7981b122020-01-16 10:58:27 +0000199 mChooserListCommunicator.onHandlePackagesChanged(this);
arangelovb0802dc2019-10-18 18:03:44 +0100200
201 }
202
203 @Override
204 public void notifyDataSetChanged() {
205 if (!mListViewDataChanged) {
arangelov5fc9e7d2020-01-07 17:59:14 +0000206 mChooserListCommunicator.sendListViewUpdateMessage(getUserHandle());
arangelovb0802dc2019-10-18 18:03:44 +0100207 mListViewDataChanged = true;
208 }
209 }
210
211 void refreshListView() {
212 if (mListViewDataChanged) {
Song Hue2deffd2020-03-09 15:22:29 -0700213 if (mAppendDirectShareEnabled) {
214 appendServiceTargetsWithQuota();
215 }
arangelovb0802dc2019-10-18 18:03:44 +0100216 super.notifyDataSetChanged();
217 }
218 mListViewDataChanged = false;
219 }
220
221
222 private void createPlaceHolders() {
223 mNumShortcutResults = 0;
224 mServiceTargets.clear();
Song Hue2deffd2020-03-09 15:22:29 -0700225 mValidServiceTargetsNum = 0;
226 mParkingDirectShareTargets.clear();
227 mPendingChooserTargetService.clear();
228 mShortcutComponents.clear();
arangelovb0802dc2019-10-18 18:03:44 +0100229 for (int i = 0; i < MAX_SERVICE_TARGETS; i++) {
230 mServiceTargets.add(mPlaceHolderTargetInfo);
231 }
232 }
233
234 @Override
Zhen Zhangbde7b462019-11-11 11:49:33 -0800235 View onCreateView(ViewGroup parent) {
arangelovb0802dc2019-10-18 18:03:44 +0100236 return mInflater.inflate(
237 com.android.internal.R.layout.resolve_grid_item, parent, false);
238 }
239
240 @Override
Mike Digmancaec1a72020-05-18 09:27:15 -0700241 protected void onBindView(View view, TargetInfo info, int position) {
242 super.onBindView(view, info, position);
243 if (info == null) return;
arangelovb0802dc2019-10-18 18:03:44 +0100244
245 // If target is loading, show a special placeholder shape in the label, make unclickable
246 final ViewHolder holder = (ViewHolder) view.getTag();
247 if (info instanceof ChooserActivity.PlaceHolderTargetInfo) {
248 final int maxWidth = mContext.getResources().getDimensionPixelSize(
249 R.dimen.chooser_direct_share_label_placeholder_max_width);
250 holder.text.setMaxWidth(maxWidth);
251 holder.text.setBackground(mContext.getResources().getDrawable(
252 R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme()));
253 // Prevent rippling by removing background containing ripple
254 holder.itemView.setBackground(null);
255 } else {
256 holder.text.setMaxWidth(Integer.MAX_VALUE);
257 holder.text.setBackground(null);
258 holder.itemView.setBackground(holder.defaultItemViewBackground);
259 }
Mike Digman438d1322020-05-13 15:59:55 -0700260
Mike Digman438d1322020-05-13 15:59:55 -0700261 if (info instanceof MultiDisplayResolveInfo) {
Mike Digmancaec1a72020-05-18 09:27:15 -0700262 // If the target is grouped show an indicator
Mike Digman438d1322020-05-13 15:59:55 -0700263 Drawable bkg = mContext.getDrawable(R.drawable.chooser_group_background);
264 holder.text.setPaddingRelative(0, 0, bkg.getIntrinsicWidth() /* end */, 0);
265 holder.text.setBackground(bkg);
Mike Digmancaec1a72020-05-18 09:27:15 -0700266 } else if (info.isPinned() && getPositionTargetType(position) == TARGET_STANDARD) {
267 // If the target is pinned and in the suggested row show a pinned indicator
268 Drawable bkg = mContext.getDrawable(R.drawable.chooser_pinned_background);
269 holder.text.setPaddingRelative(bkg.getIntrinsicWidth() /* start */, 0, 0, 0);
270 holder.text.setBackground(bkg);
Mike Digman438d1322020-05-13 15:59:55 -0700271 } else {
272 holder.text.setBackground(null);
273 holder.text.setPaddingRelative(0, 0, 0, 0);
274 }
arangelovb0802dc2019-10-18 18:03:44 +0100275 }
276
277 void updateAlphabeticalList() {
Matt Pietal0aefc0c2020-06-16 14:04:52 -0400278 new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
279 @Override
280 protected List<DisplayResolveInfo> doInBackground(Void... voids) {
281 List<DisplayResolveInfo> allTargets = new ArrayList<>();
282 allTargets.addAll(mDisplayList);
283 allTargets.addAll(mCallerTargets);
284 if (!mEnableStackedApps) {
285 return allTargets;
Alison Cichowlas19ee2922019-12-16 19:43:12 -0500286 }
Matt Pietal0aefc0c2020-06-16 14:04:52 -0400287 // Consolidate multiple targets from same app.
288 Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
289 for (DisplayResolveInfo info : allTargets) {
290 String packageName = info.getResolvedComponentName().getPackageName();
291 DisplayResolveInfo multiDri = consolidated.get(packageName);
292 if (multiDri == null) {
293 consolidated.put(packageName, info);
294 } else if (multiDri instanceof MultiDisplayResolveInfo) {
295 ((MultiDisplayResolveInfo) multiDri).addTarget(info);
296 } else {
297 // create consolidated target from the single DisplayResolveInfo
298 MultiDisplayResolveInfo multiDisplayResolveInfo =
299 new MultiDisplayResolveInfo(packageName, multiDri);
300 multiDisplayResolveInfo.addTarget(info);
301 consolidated.put(packageName, multiDisplayResolveInfo);
302 }
303 }
304 List<DisplayResolveInfo> groupedTargets = new ArrayList<>();
305 groupedTargets.addAll(consolidated.values());
306 Collections.sort(groupedTargets, new ChooserActivity.AzInfoComparator(mContext));
307 return groupedTargets;
Alison Cichowlas19ee2922019-12-16 19:43:12 -0500308 }
Matt Pietal0aefc0c2020-06-16 14:04:52 -0400309 @Override
310 protected void onPostExecute(List<DisplayResolveInfo> newList) {
311 mSortedList = newList;
312 notifyDataSetChanged();
313 }
314 }.execute();
arangelovb0802dc2019-10-18 18:03:44 +0100315 }
316
317 @Override
arangelovb0802dc2019-10-18 18:03:44 +0100318 public int getCount() {
319 return getRankedTargetCount() + getAlphaTargetCount()
320 + getSelectableServiceTargetCount() + getCallerTargetCount();
321 }
322
323 @Override
324 public int getUnfilteredCount() {
325 int appTargets = super.getUnfilteredCount();
326 if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) {
327 appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets();
328 }
329 return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
330 }
331
332
333 public int getCallerTargetCount() {
Matt Pietal1245b382020-06-10 09:56:38 -0400334 return mCallerTargets.size();
arangelovb0802dc2019-10-18 18:03:44 +0100335 }
336
337 /**
338 * Filter out placeholders and non-selectable service targets
339 */
340 public int getSelectableServiceTargetCount() {
341 int count = 0;
342 for (ChooserTargetInfo info : mServiceTargets) {
343 if (info instanceof SelectableTargetInfo) {
344 count++;
345 }
346 }
347 return count;
348 }
349
350 public int getServiceTargetCount() {
351 if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent())
352 && !ActivityManager.isLowRamDeviceStatic()) {
353 return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS);
354 }
355
356 return 0;
357 }
358
359 int getAlphaTargetCount() {
Matt Pietal1245b382020-06-10 09:56:38 -0400360 int groupedCount = mSortedList.size();
361 int ungroupedCount = mCallerTargets.size() + mDisplayList.size();
362 return ungroupedCount > mChooserListCommunicator.getMaxRankedTargets() ? groupedCount : 0;
arangelovb0802dc2019-10-18 18:03:44 +0100363 }
364
Song Hu276e3842020-05-13 15:34:29 -0700365 /**
366 * Fetch ranked app target count
367 */
368 public int getRankedTargetCount() {
arangelovb0802dc2019-10-18 18:03:44 +0100369 int spacesAvailable =
370 mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount();
371 return Math.min(spacesAvailable, super.getCount());
372 }
373
374 public int getPositionTargetType(int position) {
375 int offset = 0;
376
377 final int serviceTargetCount = getServiceTargetCount();
378 if (position < serviceTargetCount) {
379 return TARGET_SERVICE;
380 }
381 offset += serviceTargetCount;
382
383 final int callerTargetCount = getCallerTargetCount();
384 if (position - offset < callerTargetCount) {
385 return TARGET_CALLER;
386 }
387 offset += callerTargetCount;
388
389 final int rankedTargetCount = getRankedTargetCount();
390 if (position - offset < rankedTargetCount) {
391 return TARGET_STANDARD;
392 }
393 offset += rankedTargetCount;
394
395 final int standardTargetCount = getAlphaTargetCount();
396 if (position - offset < standardTargetCount) {
397 return TARGET_STANDARD_AZ;
398 }
399
400 return TARGET_BAD;
401 }
402
403 @Override
404 public TargetInfo getItem(int position) {
405 return targetInfoForPosition(position, true);
406 }
407
408
409 /**
410 * Find target info for a given position.
411 * Since ChooserActivity displays several sections of content, determine which
412 * section provides this item.
413 */
414 @Override
415 public TargetInfo targetInfoForPosition(int position, boolean filtered) {
Zhen Zhangbde7b462019-11-11 11:49:33 -0800416 if (position == NO_POSITION) {
417 return null;
418 }
419
arangelovb0802dc2019-10-18 18:03:44 +0100420 int offset = 0;
421
422 // Direct share targets
423 final int serviceTargetCount = filtered ? getServiceTargetCount() :
424 getSelectableServiceTargetCount();
425 if (position < serviceTargetCount) {
426 return mServiceTargets.get(position);
427 }
428 offset += serviceTargetCount;
429
430 // Targets provided by calling app
431 final int callerTargetCount = getCallerTargetCount();
432 if (position - offset < callerTargetCount) {
433 return mCallerTargets.get(position - offset);
434 }
435 offset += callerTargetCount;
436
437 // Ranked standard app targets
438 final int rankedTargetCount = getRankedTargetCount();
439 if (position - offset < rankedTargetCount) {
440 return filtered ? super.getItem(position - offset)
441 : getDisplayResolveInfo(position - offset);
442 }
443 offset += rankedTargetCount;
444
445 // Alphabetical complete app target list.
446 if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) {
447 return mSortedList.get(position - offset);
448 }
449
450 return null;
451 }
452
Song Hu276e3842020-05-13 15:34:29 -0700453 // Check whether {@code dri} should be added into mDisplayList.
454 @Override
455 protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
456 // Checks if this info is already listed in callerTargets.
457 for (TargetInfo existingInfo : mCallerTargets) {
458 if (mResolverListCommunicator
459 .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
460 return false;
461 }
462 }
463 return super.shouldAddResolveInfo(dri);
464 }
465
Song Hu5aab2d32020-04-30 10:07:15 -0700466 /**
467 * Fetch surfaced direct share target info
468 */
469 public List<ChooserTargetInfo> getSurfacedTargetInfo() {
470 int maxSurfacedTargets = mChooserListCommunicator.getMaxRankedTargets();
471 return mServiceTargets.subList(0,
472 Math.min(maxSurfacedTargets, getSelectableServiceTargetCount()));
473 }
474
arangelovb0802dc2019-10-18 18:03:44 +0100475
476 /**
477 * Evaluate targets for inclusion in the direct share area. May not be included
478 * if score is too low.
479 */
480 public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
arangelov5fc9e7d2020-01-07 17:59:14 +0000481 @ChooserActivity.ShareTargetType int targetType,
Song Hue2deffd2020-03-09 15:22:29 -0700482 Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
483 List<ChooserActivity.ChooserTargetServiceConnection>
484 pendingChooserTargetServiceConnections) {
arangelovb0802dc2019-10-18 18:03:44 +0100485 if (DEBUG) {
Song Hue2deffd2020-03-09 15:22:29 -0700486 Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", "
487 + targets.size()
arangelovb0802dc2019-10-18 18:03:44 +0100488 + " targets");
489 }
Song Hue2deffd2020-03-09 15:22:29 -0700490 if (mAppendDirectShareEnabled) {
491 parkTargetIntoMemory(origTarget, targets, targetType, directShareToShortcutInfos,
492 pendingChooserTargetServiceConnections);
493 return;
494 }
arangelovb0802dc2019-10-18 18:03:44 +0100495 if (targets.size() == 0) {
496 return;
497 }
498
499 final float baseScore = getBaseScore(origTarget, targetType);
500 Collections.sort(targets, mBaseTargetComparator);
501
502 final boolean isShortcutResult =
503 (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
504 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
505 final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
506 : MAX_CHOOSER_TARGETS_PER_APP;
507 float lastScore = 0;
508 boolean shouldNotify = false;
509 for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) {
510 final ChooserTarget target = targets.get(i);
511 float targetScore = target.getScore();
512 targetScore *= baseScore;
513 if (i > 0 && targetScore >= lastScore) {
514 // Apply a decay so that the top app can't crowd out everything else.
515 // This incents ChooserTargetServices to define what's truly better.
516 targetScore = lastScore * 0.95f;
517 }
arangelov5fc9e7d2020-01-07 17:59:14 +0000518 UserHandle userHandle = getUserHandle();
519 Context contextAsUser = mContext.createContextAsUser(userHandle, 0 /* flags */);
520 boolean isInserted = insertServiceTarget(new SelectableTargetInfo(contextAsUser,
arangelov1b11cc12020-04-27 17:23:04 +0100521 origTarget, target, targetScore, mSelectableTargetInfoCommunicator,
arangelov5fc9e7d2020-01-07 17:59:14 +0000522 (isShortcutResult ? directShareToShortcutInfos.get(target) : null)));
arangelovb0802dc2019-10-18 18:03:44 +0100523
524 if (isInserted && isShortcutResult) {
525 mNumShortcutResults++;
526 }
527
528 shouldNotify |= isInserted;
529
530 if (DEBUG) {
531 Log.d(TAG, " => " + target.toString() + " score=" + targetScore
532 + " base=" + target.getScore()
533 + " lastScore=" + lastScore
534 + " baseScore=" + baseScore);
535 }
536
537 lastScore = targetScore;
538 }
539
540 if (shouldNotify) {
541 notifyDataSetChanged();
542 }
543 }
544
Song Hue2deffd2020-03-09 15:22:29 -0700545 /**
Song Hu5aab2d32020-04-30 10:07:15 -0700546 * Store ChooserTarget ranking scores info wrapped in {@code targets}.
547 */
548 public void addChooserTargetRankingScore(List<AppTarget> targets) {
549 Log.i(TAG, "addChooserTargetRankingScore " + targets.size() + " targets score.");
550 for (AppTarget target : targets) {
551 if (target.getShortcutInfo() == null) {
552 continue;
553 }
554 ShortcutInfo shortcutInfo = target.getShortcutInfo();
555 if (!shortcutInfo.getId().equals(ChooserActivity.CHOOSER_TARGET)
556 || shortcutInfo.getActivity() == null) {
557 continue;
558 }
559 ComponentName componentName = shortcutInfo.getActivity();
560 if (!mChooserTargetScores.containsKey(componentName)) {
561 mChooserTargetScores.put(componentName, new HashMap<>());
562 }
563 mChooserTargetScores.get(componentName).put(shortcutInfo.getShortLabel().toString(),
Song Hu16cfc842020-06-25 04:31:12 -0700564 target.getRank());
Song Hu5aab2d32020-04-30 10:07:15 -0700565 }
566 mChooserTargetScores.keySet().forEach(key -> rankTargetsWithinComponent(key));
567 }
568
569 /**
570 * Rank chooserTargets of the given {@code componentName} in mParkingDirectShareTargets as per
571 * available scores stored in mChooserTargetScores.
572 */
573 private void rankTargetsWithinComponent(ComponentName componentName) {
574 if (!mParkingDirectShareTargets.containsKey(componentName)
575 || !mChooserTargetScores.containsKey(componentName)) {
576 return;
577 }
578 Map<String, Integer> scores = mChooserTargetScores.get(componentName);
579 Collections.sort(mParkingDirectShareTargets.get(componentName).first, (o1, o2) -> {
Song Hu93cefaa2020-05-31 10:53:03 -0700580 // The score has been normalized between 0 and 2000, the default is 1000.
Song Hu5aab2d32020-04-30 10:07:15 -0700581 int score1 = scores.getOrDefault(
Song Hu93cefaa2020-05-31 10:53:03 -0700582 ChooserUtil.md5(o1.getChooserTarget().getTitle().toString()),
583 DEFAULT_DIRECT_SHARE_RANKING_SCORE);
Song Hu5aab2d32020-04-30 10:07:15 -0700584 int score2 = scores.getOrDefault(
Song Hu93cefaa2020-05-31 10:53:03 -0700585 ChooserUtil.md5(o2.getChooserTarget().getTitle().toString()),
586 DEFAULT_DIRECT_SHARE_RANKING_SCORE);
Song Hu5aab2d32020-04-30 10:07:15 -0700587 return score2 - score1;
588 });
589 }
590
591 /**
Song Hue2deffd2020-03-09 15:22:29 -0700592 * Park {@code targets} into memory for the moment to surface them later when view is refreshed.
593 * Components pending on ChooserTargetService query are also recorded.
594 */
595 private void parkTargetIntoMemory(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
596 @ChooserActivity.ShareTargetType int targetType,
597 Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
598 List<ChooserActivity.ChooserTargetServiceConnection>
599 pendingChooserTargetServiceConnections) {
Song Hu36ba1b12020-04-14 15:36:17 -0700600 ComponentName origComponentName = origTarget != null ? origTarget.getResolvedComponentName()
601 : !targets.isEmpty() ? targets.get(0).getComponentName() : null;
Song Hu6f422812020-04-26 21:51:31 -0700602 Log.i(TAG,
603 "parkTargetIntoMemory " + origComponentName + ", " + targets.size() + " targets");
Song Hue2deffd2020-03-09 15:22:29 -0700604 mPendingChooserTargetService = pendingChooserTargetServiceConnections.stream()
605 .map(ChooserActivity.ChooserTargetServiceConnection::getComponentName)
606 .filter(componentName -> !componentName.equals(origComponentName))
607 .collect(Collectors.toSet());
608 // Park targets in memory
Song Hu36ba1b12020-04-14 15:36:17 -0700609 if (!targets.isEmpty()) {
Song Hue2deffd2020-03-09 15:22:29 -0700610 final boolean isShortcutResult =
611 (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
612 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
613 Context contextAsUser = mContext.createContextAsUser(getUserHandle(),
614 0 /* flags */);
615 List<ChooserTargetInfo> parkingTargetInfos = targets.stream()
616 .map(target ->
617 new SelectableTargetInfo(
618 contextAsUser, origTarget, target, target.getScore(),
arangelov1b11cc12020-04-27 17:23:04 +0100619 mSelectableTargetInfoCommunicator,
Song Hue2deffd2020-03-09 15:22:29 -0700620 (isShortcutResult ? directShareToShortcutInfos.get(target)
621 : null))
622 )
623 .collect(Collectors.toList());
Song Hu36ba1b12020-04-14 15:36:17 -0700624 Pair<List<ChooserTargetInfo>, Integer> parkingTargetInfoPair =
625 mParkingDirectShareTargets.getOrDefault(origComponentName,
626 new Pair<>(new ArrayList<>(), 0));
Song Hub548df92020-05-16 08:42:54 -0700627 for (ChooserTargetInfo target : parkingTargetInfos) {
628 if (!checkDuplicateTarget(target, parkingTargetInfoPair.first)
629 && !checkDuplicateTarget(target, mServiceTargets)) {
630 parkingTargetInfoPair.first.add(target);
631 mAvailableServiceTargetsNum++;
632 }
633 }
Song Hu36ba1b12020-04-14 15:36:17 -0700634 mParkingDirectShareTargets.put(origComponentName, parkingTargetInfoPair);
Song Hu5aab2d32020-04-30 10:07:15 -0700635 rankTargetsWithinComponent(origComponentName);
Song Hue2deffd2020-03-09 15:22:29 -0700636 if (isShortcutResult) {
637 mShortcutComponents.add(origComponentName);
638 }
639 }
640 notifyDataSetChanged();
641 }
642
643 /**
644 * Append targets of top ranked share app into direct share row with quota limit. Remove
645 * appended ones from memory.
646 */
647 private void appendServiceTargetsWithQuota() {
648 int maxRankedTargets = mChooserListCommunicator.getMaxRankedTargets();
649 List<ComponentName> topComponentNames = getTopComponentNames(maxRankedTargets);
Song Hu6f422812020-04-26 21:51:31 -0700650 float totalScore = 0f;
Song Hue2deffd2020-03-09 15:22:29 -0700651 for (ComponentName component : topComponentNames) {
652 if (!mPendingChooserTargetService.contains(component)
653 && !mParkingDirectShareTargets.containsKey(component)) {
654 continue;
655 }
Song Hu6f422812020-04-26 21:51:31 -0700656 totalScore += super.getScore(component);
657 }
658 boolean shouldWaitPendingService = false;
659 for (ComponentName component : topComponentNames) {
660 if (!mPendingChooserTargetService.contains(component)
661 && !mParkingDirectShareTargets.containsKey(component)) {
662 continue;
663 }
664 float score = super.getScore(component);
665 int quota = Math.round(maxRankedTargets * score / totalScore);
666 if (mPendingChooserTargetService.contains(component) && quota >= 1) {
667 shouldWaitPendingService = true;
668 }
669 if (!mParkingDirectShareTargets.containsKey(component)) {
670 continue;
671 }
672 // Append targets into direct share row as per quota.
Song Hue2deffd2020-03-09 15:22:29 -0700673 Pair<List<ChooserTargetInfo>, Integer> parkingTargetsItem =
674 mParkingDirectShareTargets.get(component);
Song Hu6f422812020-04-26 21:51:31 -0700675 List<ChooserTargetInfo> parkingTargets = parkingTargetsItem.first;
676 int insertedNum = parkingTargetsItem.second;
677 while (insertedNum < quota && !parkingTargets.isEmpty()) {
Song Hub548df92020-05-16 08:42:54 -0700678 if (!checkDuplicateTarget(parkingTargets.get(0), mServiceTargets)) {
Song Hu6f422812020-04-26 21:51:31 -0700679 mServiceTargets.add(mValidServiceTargetsNum, parkingTargets.get(0));
680 mValidServiceTargetsNum++;
681 insertedNum++;
Song Hue2deffd2020-03-09 15:22:29 -0700682 }
Song Hu6f422812020-04-26 21:51:31 -0700683 parkingTargets.remove(0);
Song Hue2deffd2020-03-09 15:22:29 -0700684 }
Song Hu6f422812020-04-26 21:51:31 -0700685 Log.i(TAG, " appendServiceTargetsWithQuota component=" + component
686 + " appendNum=" + (insertedNum - parkingTargetsItem.second));
687 if (DEBUG) {
688 Log.d(TAG, " appendServiceTargetsWithQuota component=" + component
689 + " score=" + score
690 + " totalScore=" + totalScore
691 + " quota=" + quota);
692 }
Song Hu6f422812020-04-26 21:51:31 -0700693 mParkingDirectShareTargets.put(component, new Pair<>(parkingTargets, insertedNum));
694 }
695 if (!shouldWaitPendingService) {
696 fillAllServiceTargets();
Song Hue2deffd2020-03-09 15:22:29 -0700697 }
698 }
699
700 /**
701 * Append all remaining targets (parking in memory) into direct share row as per their ranking.
702 */
703 private void fillAllServiceTargets() {
Song Hu36ba1b12020-04-14 15:36:17 -0700704 if (mParkingDirectShareTargets.isEmpty()) {
705 return;
706 }
Song Hu6f422812020-04-26 21:51:31 -0700707 Log.i(TAG, " fillAllServiceTargets");
Song Hub548df92020-05-16 08:42:54 -0700708 List<ComponentName> topComponentNames = getTopComponentNames(MAX_SERVICE_TARGET_APP);
Song Hue2deffd2020-03-09 15:22:29 -0700709 // Append all remaining targets of top recommended components into direct share row.
710 for (ComponentName component : topComponentNames) {
711 if (!mParkingDirectShareTargets.containsKey(component)) {
712 continue;
713 }
714 mParkingDirectShareTargets.get(component).first.stream()
Song Hub548df92020-05-16 08:42:54 -0700715 .filter(target -> !checkDuplicateTarget(target, mServiceTargets))
Song Hue2deffd2020-03-09 15:22:29 -0700716 .forEach(target -> {
Song Hu6f422812020-04-26 21:51:31 -0700717 mServiceTargets.add(mValidServiceTargetsNum, target);
718 mValidServiceTargetsNum++;
Song Hue2deffd2020-03-09 15:22:29 -0700719 });
720 mParkingDirectShareTargets.remove(component);
721 }
722 // Append all remaining shortcuts targets into direct share row.
Song Hue2deffd2020-03-09 15:22:29 -0700723 mParkingDirectShareTargets.entrySet().stream()
724 .filter(entry -> mShortcutComponents.contains(entry.getKey()))
725 .map(entry -> entry.getValue())
726 .map(pair -> pair.first)
Song Hu36ba1b12020-04-14 15:36:17 -0700727 .forEach(targets -> {
728 for (ChooserTargetInfo target : targets) {
Song Hub548df92020-05-16 08:42:54 -0700729 if (!checkDuplicateTarget(target, mServiceTargets)) {
Song Hu6f422812020-04-26 21:51:31 -0700730 mServiceTargets.add(mValidServiceTargetsNum, target);
731 mValidServiceTargetsNum++;
Song Hu36ba1b12020-04-14 15:36:17 -0700732 }
733 }
734 });
Song Hue2deffd2020-03-09 15:22:29 -0700735 mParkingDirectShareTargets.clear();
736 }
737
Song Hub548df92020-05-16 08:42:54 -0700738 private boolean checkDuplicateTarget(ChooserTargetInfo target,
739 List<ChooserTargetInfo> destination) {
Song Hue2deffd2020-03-09 15:22:29 -0700740 // Check for duplicates and abort if found
Song Hub548df92020-05-16 08:42:54 -0700741 for (ChooserTargetInfo otherTargetInfo : destination) {
742 if (target.isSimilar(otherTargetInfo)) {
Song Hue2deffd2020-03-09 15:22:29 -0700743 return true;
744 }
745 }
746 return false;
747 }
748
Song Hub548df92020-05-16 08:42:54 -0700749 /**
750 * The return number have to exceed a minimum limit to make direct share area expandable. When
751 * append direct share targets is enabled, return count of all available targets parking in the
752 * memory; otherwise, it is shortcuts count which will help reduce the amount of visible
753 * shuffling due to older-style direct share targets.
754 */
755 int getNumServiceTargetsForExpand() {
756 return mAppendDirectShareEnabled ? mAvailableServiceTargetsNum : mNumShortcutResults;
arangelovb0802dc2019-10-18 18:03:44 +0100757 }
758
759 /**
760 * Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
761 * <ol>
762 * <li>App-supplied targets
763 * <li>Shortcuts ranked via App Prediction Manager
764 * <li>Shortcuts ranked via legacy heuristics
765 * <li>Legacy direct share targets
766 * </ol>
767 */
768 public float getBaseScore(
769 DisplayResolveInfo target,
770 @ChooserActivity.ShareTargetType int targetType) {
771 if (target == null) {
772 return CALLER_TARGET_SCORE_BOOST;
773 }
774
775 if (targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
776 return SHORTCUT_TARGET_SCORE_BOOST;
777 }
778
779 float score = super.getScore(target);
780 if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
781 return score * SHORTCUT_TARGET_SCORE_BOOST;
782 }
783
784 return score;
785 }
786
787 /**
788 * Calling this marks service target loading complete, and will attempt to no longer
789 * update the direct share area.
790 */
791 public void completeServiceTargetLoading() {
792 mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
Song Hue2deffd2020-03-09 15:22:29 -0700793 if (mAppendDirectShareEnabled) {
794 fillAllServiceTargets();
795 }
arangelovb0802dc2019-10-18 18:03:44 +0100796 if (mServiceTargets.isEmpty()) {
797 mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
798 }
799 notifyDataSetChanged();
800 }
801
802 private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
803 // Avoid inserting any potentially late results
804 if (mServiceTargets.size() == 1
805 && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
806 return false;
807 }
808
809 // Check for duplicates and abort if found
810 for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
811 if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
812 return false;
813 }
814 }
815
816 int currentSize = mServiceTargets.size();
817 final float newScore = chooserTargetInfo.getModifiedScore();
818 for (int i = 0; i < Math.min(currentSize, MAX_SERVICE_TARGETS); i++) {
819 final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
820 if (serviceTarget == null) {
821 mServiceTargets.set(i, chooserTargetInfo);
822 return true;
823 } else if (newScore > serviceTarget.getModifiedScore()) {
824 mServiceTargets.add(i, chooserTargetInfo);
825 return true;
826 }
827 }
828
829 if (currentSize < MAX_SERVICE_TARGETS) {
830 mServiceTargets.add(chooserTargetInfo);
831 return true;
832 }
833
834 return false;
835 }
836
837 public ChooserTarget getChooserTargetForValue(int value) {
838 return mServiceTargets.get(value).getChooserTarget();
839 }
840
Matt Pietaldd6ed8e2020-06-17 13:54:57 -0400841 protected boolean alwaysShowSubLabel() {
842 // Always show a subLabel for visual consistency across list items. Show an empty
843 // subLabel if the subLabel is the same as the label
844 return true;
845 }
846
arangelovb0802dc2019-10-18 18:03:44 +0100847 /**
848 * Rather than fully sorting the input list, this sorting task will put the top k elements
849 * in the head of input list and fill the tail with other elements in undetermined order.
850 */
851 @Override
852 AsyncTask<List<ResolvedComponentInfo>,
853 Void,
arangelov7981b122020-01-16 10:58:27 +0000854 List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
arangelovb0802dc2019-10-18 18:03:44 +0100855 return new AsyncTask<List<ResolvedComponentInfo>,
856 Void,
857 List<ResolvedComponentInfo>>() {
858 @Override
859 protected List<ResolvedComponentInfo> doInBackground(
860 List<ResolvedComponentInfo>... params) {
861 mResolverListController.topK(params[0],
862 mChooserListCommunicator.getMaxRankedTargets());
863 return params[0];
864 }
865 @Override
866 protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
arangelov7981b122020-01-16 10:58:27 +0000867 processSortedList(sortedComponents, doPostProcessing);
868 if (doPostProcessing) {
869 mChooserListCommunicator.updateProfileViewButton();
870 notifyDataSetChanged();
871 }
arangelovb0802dc2019-10-18 18:03:44 +0100872 }
873 };
874 }
875
arangelov5fc9e7d2020-01-07 17:59:14 +0000876 public void setAppPredictor(AppPredictor appPredictor) {
877 mAppPredictor = appPredictor;
878 }
879
880 public void setAppPredictorCallback(AppPredictor.Callback appPredictorCallback) {
881 mAppPredictorCallback = appPredictorCallback;
882 }
883
884 public void destroyAppPredictor() {
885 if (getAppPredictor() != null) {
886 getAppPredictor().unregisterPredictionUpdates(mAppPredictorCallback);
887 getAppPredictor().destroy();
888 }
889 }
890
arangelovb0802dc2019-10-18 18:03:44 +0100891 /**
892 * Necessary methods to communicate between {@link ChooserListAdapter}
893 * and {@link ChooserActivity}.
894 */
895 interface ChooserListCommunicator extends ResolverListCommunicator {
896
897 int getMaxRankedTargets();
898
arangelov5fc9e7d2020-01-07 17:59:14 +0000899 void sendListViewUpdateMessage(UserHandle userHandle);
arangelovb0802dc2019-10-18 18:03:44 +0100900
901 boolean isSendAction(Intent targetIntent);
902 }
903}