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