blob: ca3b7e7a783783e954be628850642b58e8c19a52 [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
arangelov5fc9e7d2020-01-07 17:59:14 +000022import android.annotation.Nullable;
arangelovb0802dc2019-10-18 18:03:44 +010023import android.app.ActivityManager;
arangelov5fc9e7d2020-01-07 17:59:14 +000024import android.app.prediction.AppPredictor;
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;
arangelovb0802dc2019-10-18 18:03:44 +010033import android.os.AsyncTask;
arangelov5fc9e7d2020-01-07 17:59:14 +000034import android.os.UserHandle;
arangelovb0802dc2019-10-18 18:03:44 +010035import android.os.UserManager;
36import android.service.chooser.ChooserTarget;
37import android.util.Log;
38import android.view.View;
39import android.view.ViewGroup;
40
41import com.android.internal.R;
42import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
43import com.android.internal.app.chooser.ChooserTargetInfo;
44import com.android.internal.app.chooser.DisplayResolveInfo;
Alison Cichowlas19ee2922019-12-16 19:43:12 -050045import com.android.internal.app.chooser.MultiDisplayResolveInfo;
arangelovb0802dc2019-10-18 18:03:44 +010046import com.android.internal.app.chooser.SelectableTargetInfo;
47import com.android.internal.app.chooser.TargetInfo;
48
49import java.util.ArrayList;
50import java.util.Collections;
Alison Cichowlas19ee2922019-12-16 19:43:12 -050051import java.util.HashMap;
arangelovb0802dc2019-10-18 18:03:44 +010052import java.util.List;
Alison Cichowlas19ee2922019-12-16 19:43:12 -050053import java.util.Map;
arangelovb0802dc2019-10-18 18:03:44 +010054
55public class ChooserListAdapter extends ResolverListAdapter {
56 private static final String TAG = "ChooserListAdapter";
57 private static final boolean DEBUG = false;
58
Alison Cichowlas19ee2922019-12-16 19:43:12 -050059 private boolean mEnableStackedApps = true;
60
Zhen Zhangbde7b462019-11-11 11:49:33 -080061 public static final int NO_POSITION = -1;
arangelovb0802dc2019-10-18 18:03:44 +010062 public static final int TARGET_BAD = -1;
63 public static final int TARGET_CALLER = 0;
64 public static final int TARGET_SERVICE = 1;
65 public static final int TARGET_STANDARD = 2;
66 public static final int TARGET_STANDARD_AZ = 3;
67
68 private static final int MAX_SUGGESTED_APP_TARGETS = 4;
69 private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
70
71 static final int MAX_SERVICE_TARGETS = 8;
72
73 /** {@link #getBaseScore} */
74 public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
75 /** {@link #getBaseScore} */
76 public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
77
78 private final int mMaxShortcutTargetsPerApp;
79 private final ChooserListCommunicator mChooserListCommunicator;
80 private final SelectableTargetInfo.SelectableTargetInfoCommunicator
81 mSelectableTargetInfoComunicator;
82
83 private int mNumShortcutResults = 0;
84
85 // Reserve spots for incoming direct share targets by adding placeholders
86 private ChooserTargetInfo
87 mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
88 private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
89 private final List<TargetInfo> mCallerTargets = new ArrayList<>();
90
91 private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator =
92 new ChooserActivity.BaseChooserTargetComparator();
93 private boolean mListViewDataChanged = false;
94
95 // Sorted list of DisplayResolveInfos for the alphabetical app section.
96 private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
arangelov5fc9e7d2020-01-07 17:59:14 +000097 private AppPredictor mAppPredictor;
98 private AppPredictor.Callback mAppPredictorCallback;
arangelovb0802dc2019-10-18 18:03:44 +010099
100 public ChooserListAdapter(Context context, List<Intent> payloadIntents,
101 Intent[] initialIntents, List<ResolveInfo> rList,
102 boolean filterLastUsed, ResolverListController resolverListController,
103 boolean useLayoutForBrowsables,
104 ChooserListCommunicator chooserListCommunicator,
105 SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoComunicator) {
106 // Don't send the initial intents through the shared ResolverActivity path,
107 // we want to separate them into a different section.
108 super(context, payloadIntents, null, rList, filterLastUsed,
109 resolverListController, useLayoutForBrowsables,
Paul McLean07425c82019-10-18 12:00:11 -0600110 chooserListCommunicator, false);
arangelovb0802dc2019-10-18 18:03:44 +0100111
112 createPlaceHolders();
113 mMaxShortcutTargetsPerApp =
114 context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
115 mChooserListCommunicator = chooserListCommunicator;
116 mSelectableTargetInfoComunicator = selectableTargetInfoComunicator;
117
118 if (initialIntents != null) {
119 final PackageManager pm = context.getPackageManager();
120 for (int i = 0; i < initialIntents.length; i++) {
121 final Intent ii = initialIntents[i];
122 if (ii == null) {
123 continue;
124 }
125
126 // We reimplement Intent#resolveActivityInfo here because if we have an
127 // implicit intent, we want the ResolveInfo returned by PackageManager
128 // instead of one we reconstruct ourselves. The ResolveInfo returned might
129 // have extra metadata and resolvePackageName set and we want to respect that.
130 ResolveInfo ri = null;
131 ActivityInfo ai = null;
132 final ComponentName cn = ii.getComponent();
133 if (cn != null) {
134 try {
135 ai = pm.getActivityInfo(ii.getComponent(), 0);
136 ri = new ResolveInfo();
137 ri.activityInfo = ai;
138 } catch (PackageManager.NameNotFoundException ignored) {
139 // ai will == null below
140 }
141 }
142 if (ai == null) {
143 ri = pm.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY);
144 ai = ri != null ? ri.activityInfo : null;
145 }
146 if (ai == null) {
147 Log.w(TAG, "No activity found for " + ii);
148 continue;
149 }
150 UserManager userManager =
151 (UserManager) context.getSystemService(Context.USER_SERVICE);
152 if (ii instanceof LabeledIntent) {
153 LabeledIntent li = (LabeledIntent) ii;
154 ri.resolvePackageName = li.getSourcePackage();
155 ri.labelRes = li.getLabelResource();
156 ri.nonLocalizedLabel = li.getNonLocalizedLabel();
157 ri.icon = li.getIconResource();
158 ri.iconResourceId = ri.icon;
159 }
160 if (userManager.isManagedProfile()) {
161 ri.noResourceId = true;
162 ri.icon = 0;
163 }
164 mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
165 }
166 }
167 }
168
arangelov5fc9e7d2020-01-07 17:59:14 +0000169 AppPredictor getAppPredictor() {
170 return mAppPredictor;
171 }
172
arangelovb0802dc2019-10-18 18:03:44 +0100173 @Override
174 public void handlePackagesChanged() {
175 if (DEBUG) {
176 Log.d(TAG, "clearing queryTargets on package change");
177 }
178 createPlaceHolders();
179 mChooserListCommunicator.onHandlePackagesChanged();
180
181 }
182
183 @Override
184 public void notifyDataSetChanged() {
185 if (!mListViewDataChanged) {
arangelov5fc9e7d2020-01-07 17:59:14 +0000186 mChooserListCommunicator.sendListViewUpdateMessage(getUserHandle());
arangelovb0802dc2019-10-18 18:03:44 +0100187 mListViewDataChanged = true;
188 }
189 }
190
191 void refreshListView() {
192 if (mListViewDataChanged) {
193 super.notifyDataSetChanged();
194 }
195 mListViewDataChanged = false;
196 }
197
198
199 private void createPlaceHolders() {
200 mNumShortcutResults = 0;
201 mServiceTargets.clear();
202 for (int i = 0; i < MAX_SERVICE_TARGETS; i++) {
203 mServiceTargets.add(mPlaceHolderTargetInfo);
204 }
205 }
206
207 @Override
Zhen Zhangbde7b462019-11-11 11:49:33 -0800208 View onCreateView(ViewGroup parent) {
arangelovb0802dc2019-10-18 18:03:44 +0100209 return mInflater.inflate(
210 com.android.internal.R.layout.resolve_grid_item, parent, false);
211 }
212
213 @Override
214 protected void onBindView(View view, TargetInfo info) {
215 super.onBindView(view, info);
216
217 // If target is loading, show a special placeholder shape in the label, make unclickable
218 final ViewHolder holder = (ViewHolder) view.getTag();
219 if (info instanceof ChooserActivity.PlaceHolderTargetInfo) {
220 final int maxWidth = mContext.getResources().getDimensionPixelSize(
221 R.dimen.chooser_direct_share_label_placeholder_max_width);
222 holder.text.setMaxWidth(maxWidth);
223 holder.text.setBackground(mContext.getResources().getDrawable(
224 R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme()));
225 // Prevent rippling by removing background containing ripple
226 holder.itemView.setBackground(null);
227 } else {
228 holder.text.setMaxWidth(Integer.MAX_VALUE);
229 holder.text.setBackground(null);
230 holder.itemView.setBackground(holder.defaultItemViewBackground);
231 }
232 }
233
234 void updateAlphabeticalList() {
235 mSortedList.clear();
Alison Cichowlas19ee2922019-12-16 19:43:12 -0500236 if (mEnableStackedApps) {
237 // Consolidate multiple targets from same app.
238 Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
239 for (DisplayResolveInfo info : mDisplayList) {
240 String packageName = info.getResolvedComponentName().getPackageName();
241 if (consolidated.get(packageName) != null) {
242 // create consolidated target
243 MultiDisplayResolveInfo multiDisplayResolveInfo =
244 new MultiDisplayResolveInfo(packageName, info);
245 multiDisplayResolveInfo.addTarget(consolidated.get(packageName));
246 consolidated.put(packageName, multiDisplayResolveInfo);
247 } else {
248 consolidated.put(packageName, info);
249 }
250 }
251 mSortedList.addAll(consolidated.values());
252 } else {
253 mSortedList.addAll(mDisplayList);
254 }
arangelovb0802dc2019-10-18 18:03:44 +0100255 Collections.sort(mSortedList, new ChooserActivity.AzInfoComparator(mContext));
256 }
257
258 @Override
arangelovb0802dc2019-10-18 18:03:44 +0100259 public int getCount() {
260 return getRankedTargetCount() + getAlphaTargetCount()
261 + getSelectableServiceTargetCount() + getCallerTargetCount();
262 }
263
264 @Override
265 public int getUnfilteredCount() {
266 int appTargets = super.getUnfilteredCount();
267 if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) {
268 appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets();
269 }
270 return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
271 }
272
273
274 public int getCallerTargetCount() {
275 return Math.min(mCallerTargets.size(), MAX_SUGGESTED_APP_TARGETS);
276 }
277
278 /**
279 * Filter out placeholders and non-selectable service targets
280 */
281 public int getSelectableServiceTargetCount() {
282 int count = 0;
283 for (ChooserTargetInfo info : mServiceTargets) {
284 if (info instanceof SelectableTargetInfo) {
285 count++;
286 }
287 }
288 return count;
289 }
290
291 public int getServiceTargetCount() {
292 if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent())
293 && !ActivityManager.isLowRamDeviceStatic()) {
294 return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS);
295 }
296
297 return 0;
298 }
299
300 int getAlphaTargetCount() {
Alison Cichowlas19ee2922019-12-16 19:43:12 -0500301 int standardCount = mSortedList.size();
arangelovb0802dc2019-10-18 18:03:44 +0100302 return standardCount > mChooserListCommunicator.getMaxRankedTargets() ? standardCount : 0;
303 }
304
305 int getRankedTargetCount() {
306 int spacesAvailable =
307 mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount();
308 return Math.min(spacesAvailable, super.getCount());
309 }
310
311 public int getPositionTargetType(int position) {
312 int offset = 0;
313
314 final int serviceTargetCount = getServiceTargetCount();
315 if (position < serviceTargetCount) {
316 return TARGET_SERVICE;
317 }
318 offset += serviceTargetCount;
319
320 final int callerTargetCount = getCallerTargetCount();
321 if (position - offset < callerTargetCount) {
322 return TARGET_CALLER;
323 }
324 offset += callerTargetCount;
325
326 final int rankedTargetCount = getRankedTargetCount();
327 if (position - offset < rankedTargetCount) {
328 return TARGET_STANDARD;
329 }
330 offset += rankedTargetCount;
331
332 final int standardTargetCount = getAlphaTargetCount();
333 if (position - offset < standardTargetCount) {
334 return TARGET_STANDARD_AZ;
335 }
336
337 return TARGET_BAD;
338 }
339
340 @Override
341 public TargetInfo getItem(int position) {
342 return targetInfoForPosition(position, true);
343 }
344
345
346 /**
347 * Find target info for a given position.
348 * Since ChooserActivity displays several sections of content, determine which
349 * section provides this item.
350 */
351 @Override
352 public TargetInfo targetInfoForPosition(int position, boolean filtered) {
Zhen Zhangbde7b462019-11-11 11:49:33 -0800353 if (position == NO_POSITION) {
354 return null;
355 }
356
arangelovb0802dc2019-10-18 18:03:44 +0100357 int offset = 0;
358
359 // Direct share targets
360 final int serviceTargetCount = filtered ? getServiceTargetCount() :
361 getSelectableServiceTargetCount();
362 if (position < serviceTargetCount) {
363 return mServiceTargets.get(position);
364 }
365 offset += serviceTargetCount;
366
367 // Targets provided by calling app
368 final int callerTargetCount = getCallerTargetCount();
369 if (position - offset < callerTargetCount) {
370 return mCallerTargets.get(position - offset);
371 }
372 offset += callerTargetCount;
373
374 // Ranked standard app targets
375 final int rankedTargetCount = getRankedTargetCount();
376 if (position - offset < rankedTargetCount) {
377 return filtered ? super.getItem(position - offset)
378 : getDisplayResolveInfo(position - offset);
379 }
380 offset += rankedTargetCount;
381
382 // Alphabetical complete app target list.
383 if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) {
384 return mSortedList.get(position - offset);
385 }
386
387 return null;
388 }
389
390
391 /**
392 * Evaluate targets for inclusion in the direct share area. May not be included
393 * if score is too low.
394 */
395 public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
arangelov5fc9e7d2020-01-07 17:59:14 +0000396 @ChooserActivity.ShareTargetType int targetType,
397 Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos) {
arangelovb0802dc2019-10-18 18:03:44 +0100398 if (DEBUG) {
399 Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size()
400 + " targets");
401 }
402
403 if (targets.size() == 0) {
404 return;
405 }
406
407 final float baseScore = getBaseScore(origTarget, targetType);
408 Collections.sort(targets, mBaseTargetComparator);
409
410 final boolean isShortcutResult =
411 (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
412 || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
413 final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
414 : MAX_CHOOSER_TARGETS_PER_APP;
415 float lastScore = 0;
416 boolean shouldNotify = false;
417 for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) {
418 final ChooserTarget target = targets.get(i);
419 float targetScore = target.getScore();
420 targetScore *= baseScore;
421 if (i > 0 && targetScore >= lastScore) {
422 // Apply a decay so that the top app can't crowd out everything else.
423 // This incents ChooserTargetServices to define what's truly better.
424 targetScore = lastScore * 0.95f;
425 }
arangelov5fc9e7d2020-01-07 17:59:14 +0000426 UserHandle userHandle = getUserHandle();
427 Context contextAsUser = mContext.createContextAsUser(userHandle, 0 /* flags */);
428 boolean isInserted = insertServiceTarget(new SelectableTargetInfo(contextAsUser,
429 origTarget, target, targetScore, mSelectableTargetInfoComunicator,
430 (isShortcutResult ? directShareToShortcutInfos.get(target) : null)));
arangelovb0802dc2019-10-18 18:03:44 +0100431
432 if (isInserted && isShortcutResult) {
433 mNumShortcutResults++;
434 }
435
436 shouldNotify |= isInserted;
437
438 if (DEBUG) {
439 Log.d(TAG, " => " + target.toString() + " score=" + targetScore
440 + " base=" + target.getScore()
441 + " lastScore=" + lastScore
442 + " baseScore=" + baseScore);
443 }
444
445 lastScore = targetScore;
446 }
447
448 if (shouldNotify) {
449 notifyDataSetChanged();
450 }
451 }
452
453 int getNumShortcutResults() {
454 return mNumShortcutResults;
455 }
456
457 /**
458 * Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
459 * <ol>
460 * <li>App-supplied targets
461 * <li>Shortcuts ranked via App Prediction Manager
462 * <li>Shortcuts ranked via legacy heuristics
463 * <li>Legacy direct share targets
464 * </ol>
465 */
466 public float getBaseScore(
467 DisplayResolveInfo target,
468 @ChooserActivity.ShareTargetType int targetType) {
469 if (target == null) {
470 return CALLER_TARGET_SCORE_BOOST;
471 }
472
473 if (targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
474 return SHORTCUT_TARGET_SCORE_BOOST;
475 }
476
477 float score = super.getScore(target);
478 if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
479 return score * SHORTCUT_TARGET_SCORE_BOOST;
480 }
481
482 return score;
483 }
484
485 /**
486 * Calling this marks service target loading complete, and will attempt to no longer
487 * update the direct share area.
488 */
489 public void completeServiceTargetLoading() {
490 mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
491
492 if (mServiceTargets.isEmpty()) {
493 mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
494 }
495 notifyDataSetChanged();
496 }
497
498 private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
499 // Avoid inserting any potentially late results
500 if (mServiceTargets.size() == 1
501 && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
502 return false;
503 }
504
505 // Check for duplicates and abort if found
506 for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
507 if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
508 return false;
509 }
510 }
511
512 int currentSize = mServiceTargets.size();
513 final float newScore = chooserTargetInfo.getModifiedScore();
514 for (int i = 0; i < Math.min(currentSize, MAX_SERVICE_TARGETS); i++) {
515 final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
516 if (serviceTarget == null) {
517 mServiceTargets.set(i, chooserTargetInfo);
518 return true;
519 } else if (newScore > serviceTarget.getModifiedScore()) {
520 mServiceTargets.add(i, chooserTargetInfo);
521 return true;
522 }
523 }
524
525 if (currentSize < MAX_SERVICE_TARGETS) {
526 mServiceTargets.add(chooserTargetInfo);
527 return true;
528 }
529
530 return false;
531 }
532
533 public ChooserTarget getChooserTargetForValue(int value) {
534 return mServiceTargets.get(value).getChooserTarget();
535 }
536
537 /**
538 * Rather than fully sorting the input list, this sorting task will put the top k elements
539 * in the head of input list and fill the tail with other elements in undetermined order.
540 */
541 @Override
542 AsyncTask<List<ResolvedComponentInfo>,
543 Void,
544 List<ResolvedComponentInfo>> createSortingTask() {
545 return new AsyncTask<List<ResolvedComponentInfo>,
546 Void,
547 List<ResolvedComponentInfo>>() {
548 @Override
549 protected List<ResolvedComponentInfo> doInBackground(
550 List<ResolvedComponentInfo>... params) {
551 mResolverListController.topK(params[0],
552 mChooserListCommunicator.getMaxRankedTargets());
553 return params[0];
554 }
555 @Override
556 protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
557 processSortedList(sortedComponents);
558 mChooserListCommunicator.updateProfileViewButton();
559 notifyDataSetChanged();
560 }
561 };
562 }
563
arangelov5fc9e7d2020-01-07 17:59:14 +0000564 public void setAppPredictor(AppPredictor appPredictor) {
565 mAppPredictor = appPredictor;
566 }
567
568 public void setAppPredictorCallback(AppPredictor.Callback appPredictorCallback) {
569 mAppPredictorCallback = appPredictorCallback;
570 }
571
572 public void destroyAppPredictor() {
573 if (getAppPredictor() != null) {
574 getAppPredictor().unregisterPredictionUpdates(mAppPredictorCallback);
575 getAppPredictor().destroy();
576 }
577 }
578
arangelovb0802dc2019-10-18 18:03:44 +0100579 /**
580 * Necessary methods to communicate between {@link ChooserListAdapter}
581 * and {@link ChooserActivity}.
582 */
583 interface ChooserListCommunicator extends ResolverListCommunicator {
584
585 int getMaxRankedTargets();
586
arangelov5fc9e7d2020-01-07 17:59:14 +0000587 void sendListViewUpdateMessage(UserHandle userHandle);
arangelovb0802dc2019-10-18 18:03:44 +0100588
589 boolean isSendAction(Intent targetIntent);
590 }
591}