• 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.intentresolver;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.ActivityInfo;
24 import android.content.pm.LabeledIntent;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ResolveInfo;
27 import android.graphics.ColorMatrix;
28 import android.graphics.ColorMatrixColorFilter;
29 import android.graphics.drawable.Drawable;
30 import android.os.AsyncTask;
31 import android.os.RemoteException;
32 import android.os.Trace;
33 import android.os.UserHandle;
34 import android.os.UserManager;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.AbsListView;
41 import android.widget.BaseAdapter;
42 import android.widget.ImageView;
43 import android.widget.TextView;
44 
45 import com.android.intentresolver.chooser.DisplayResolveInfo;
46 import com.android.intentresolver.chooser.TargetInfo;
47 import com.android.intentresolver.icons.TargetDataLoader;
48 import com.android.internal.annotations.VisibleForTesting;
49 
50 import com.google.common.collect.ImmutableList;
51 
52 import java.util.ArrayList;
53 import java.util.HashSet;
54 import java.util.List;
55 import java.util.Set;
56 
57 public class ResolverListAdapter extends BaseAdapter {
58     private static final String TAG = "ResolverListAdapter";
59 
60     @Nullable  // TODO: other model for lazy computation? Or just precompute?
61     private static ColorMatrixColorFilter sSuspendedMatrixColorFilter;
62 
63     protected final Context mContext;
64     protected final LayoutInflater mInflater;
65     protected final ResolverListCommunicator mResolverListCommunicator;
66     protected final ResolverListController mResolverListController;
67 
68     private final List<Intent> mIntents;
69     private final Intent[] mInitialIntents;
70     private final List<ResolveInfo> mBaseResolveList;
71     private final PackageManager mPm;
72     private final TargetDataLoader mTargetDataLoader;
73     private final UserHandle mUserHandle;
74     private final Intent mTargetIntent;
75 
76     private final Set<DisplayResolveInfo> mRequestedIcons = new HashSet<>();
77     private final Set<DisplayResolveInfo> mRequestedLabels = new HashSet<>();
78 
79     private ResolveInfo mLastChosen;
80     private DisplayResolveInfo mOtherProfile;
81     private int mPlaceholderCount;
82 
83     // This one is the list that the Adapter will actually present.
84     private final List<DisplayResolveInfo> mDisplayList;
85     private List<ResolvedComponentInfo> mUnfilteredResolveList;
86 
87     private int mLastChosenPosition = -1;
88     private final boolean mFilterLastUsed;
89     private Runnable mPostListReadyRunnable;
90     private boolean mIsTabLoaded;
91     // Represents the UserSpace in which the Initial Intents should be resolved.
92     private final UserHandle mInitialIntentsUserSpace;
93 
ResolverListAdapter( Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, ResolverListController resolverListController, UserHandle userHandle, Intent targetIntent, ResolverListCommunicator resolverListCommunicator, UserHandle initialIntentsUserSpace, TargetDataLoader targetDataLoader)94     public ResolverListAdapter(
95             Context context,
96             List<Intent> payloadIntents,
97             Intent[] initialIntents,
98             List<ResolveInfo> rList,
99             boolean filterLastUsed,
100             ResolverListController resolverListController,
101             UserHandle userHandle,
102             Intent targetIntent,
103             ResolverListCommunicator resolverListCommunicator,
104             UserHandle initialIntentsUserSpace,
105             TargetDataLoader targetDataLoader) {
106         mContext = context;
107         mIntents = payloadIntents;
108         mInitialIntents = initialIntents;
109         mBaseResolveList = rList;
110         mInflater = LayoutInflater.from(context);
111         mPm = context.getPackageManager();
112         mTargetDataLoader = targetDataLoader;
113         mDisplayList = new ArrayList<>();
114         mFilterLastUsed = filterLastUsed;
115         mResolverListController = resolverListController;
116         mUserHandle = userHandle;
117         mTargetIntent = targetIntent;
118         mResolverListCommunicator = resolverListCommunicator;
119         mInitialIntentsUserSpace = initialIntentsUserSpace;
120     }
121 
getFirstDisplayResolveInfo()122     public final DisplayResolveInfo getFirstDisplayResolveInfo() {
123         return mDisplayList.get(0);
124     }
125 
getTargetsInCurrentDisplayList()126     public final ImmutableList<DisplayResolveInfo> getTargetsInCurrentDisplayList() {
127         return ImmutableList.copyOf(mDisplayList);
128     }
129 
handlePackagesChanged()130     public void handlePackagesChanged() {
131         mResolverListCommunicator.onHandlePackagesChanged(this);
132     }
133 
setPlaceholderCount(int count)134     public void setPlaceholderCount(int count) {
135         mPlaceholderCount = count;
136     }
137 
getPlaceholderCount()138     public int getPlaceholderCount() {
139         return mPlaceholderCount;
140     }
141 
142     @Nullable
getFilteredItem()143     public DisplayResolveInfo getFilteredItem() {
144         if (mFilterLastUsed && mLastChosenPosition >= 0) {
145             // Not using getItem since it offsets to dodge this position for the list
146             return mDisplayList.get(mLastChosenPosition);
147         }
148         return null;
149     }
150 
getOtherProfile()151     public DisplayResolveInfo getOtherProfile() {
152         return mOtherProfile;
153     }
154 
getFilteredPosition()155     public int getFilteredPosition() {
156         if (mFilterLastUsed && mLastChosenPosition >= 0) {
157             return mLastChosenPosition;
158         }
159         return AbsListView.INVALID_POSITION;
160     }
161 
hasFilteredItem()162     public boolean hasFilteredItem() {
163         return mFilterLastUsed && mLastChosen != null;
164     }
165 
getScore(DisplayResolveInfo target)166     public float getScore(DisplayResolveInfo target) {
167         return mResolverListController.getScore(target);
168     }
169 
170     /**
171      * Returns the app share score of the given {@code targetInfo}.
172      */
getScore(TargetInfo targetInfo)173     public float getScore(TargetInfo targetInfo) {
174         return mResolverListController.getScore(targetInfo);
175     }
176 
177     /**
178      * Updates the model about the chosen {@code targetInfo}.
179      */
updateModel(TargetInfo targetInfo)180     public void updateModel(TargetInfo targetInfo) {
181         mResolverListController.updateModel(targetInfo);
182     }
183 
184     /**
185      * Updates the model about Chooser Activity selection.
186      */
updateChooserCounts(String packageName, String action, UserHandle userHandle)187     public void updateChooserCounts(String packageName, String action, UserHandle userHandle) {
188         mResolverListController.updateChooserCounts(
189                 packageName, userHandle, action);
190     }
191 
getUnfilteredResolveList()192     List<ResolvedComponentInfo> getUnfilteredResolveList() {
193         return mUnfilteredResolveList;
194     }
195 
196     /**
197      * Rebuild the list of resolvers. When rebuilding is complete, queue the {@code onPostListReady}
198      * callback on the main handler with {@code rebuildCompleted} true.
199      *
200      * In some cases some parts will need some asynchronous work to complete. Then this will first
201      * immediately queue {@code onPostListReady} (on the main handler) with {@code rebuildCompleted}
202      * false; only when the asynchronous work completes will this then go on to queue another
203      * {@code onPostListReady} callback with {@code rebuildCompleted} true.
204      *
205      * The {@code doPostProcessing} parameter is used to specify whether to update the UI and
206      * load additional targets (e.g. direct share) after the list has been rebuilt. We may choose
207      * to skip that step if we're only loading the inactive profile's resolved apps to know the
208      * number of targets.
209      *
210      * @return Whether the list building was completed synchronously. If not, we'll queue the
211      * {@code onPostListReady} callback first with {@code rebuildCompleted} false, and then again
212      * with {@code rebuildCompleted} true at the end of some newly-launched asynchronous work.
213      * Otherwise the callback is only queued once, with {@code rebuildCompleted} true.
214      */
rebuildList(boolean doPostProcessing)215     protected boolean rebuildList(boolean doPostProcessing) {
216         Trace.beginSection("ResolverListAdapter#rebuildList");
217         mDisplayList.clear();
218         mIsTabLoaded = false;
219         mLastChosenPosition = -1;
220 
221         List<ResolvedComponentInfo> currentResolveList = getInitialRebuiltResolveList();
222 
223         /* TODO: this seems like unnecessary extra complexity; why do we need to do this "primary"
224          * (i.e. "eligibility") filtering before evaluating the "other profile" special-treatment,
225          * but the "secondary" (i.e. "priority") filtering after? Are there in fact cases where the
226          * eligibility conditions will filter out a result that would've otherwise gotten the "other
227          * profile" treatment? Or, are there cases where the priority conditions *would* filter out
228          * a result, but we *want* that result to get the "other profile" treatment, so we only
229          * filter *after* evaluating the special-treatment conditions? If the answer to either is
230          * "no," then the filtering steps can be consolidated. (And that also makes the "unfiltered
231          * list" bookkeeping a little cleaner.)
232          */
233         mUnfilteredResolveList = performPrimaryResolveListFiltering(currentResolveList);
234 
235         // So far we only support a single other profile at a time.
236         // The first one we see gets special treatment.
237         ResolvedComponentInfo otherProfileInfo =
238                 getFirstNonCurrentUserResolvedComponentInfo(currentResolveList);
239         updateOtherProfileTreatment(otherProfileInfo);
240         if (otherProfileInfo != null) {
241             currentResolveList.remove(otherProfileInfo);
242             /* TODO: the previous line removed the "other profile info" item from
243              * mUnfilteredResolveList *ONLY IF* that variable is an alias for the same List instance
244              * as currentResolveList (i.e., if no items were filtered out as the result of the
245              * earlier "primary" filtering). It seems wrong for our behavior to depend on that.
246              * Should we:
247              *  A. replicate the above removal to mUnfilteredResolveList (which is idempotent, so we
248              *     don't even have to check whether they're aliases); or
249              *  B. break the alias relationship by copying currentResolveList to a new
250              *  mUnfilteredResolveList instance if necessary before removing otherProfileInfo?
251              * In other words: do we *want* otherProfileInfo in the "unfiltered" results? Either
252              * way, we'll need one of the changes suggested above.
253              */
254         }
255 
256         // If no results have yet been filtered, mUnfilteredResolveList is an alias for the same
257         // List instance as currentResolveList. Then we need to make a copy to store as the
258         // mUnfilteredResolveList if we go on to filter any more items. Otherwise we've already
259         // copied the original unfiltered items to a separate List instance and can now filter
260         // the remainder in-place without any further bookkeeping.
261         boolean needsCopyOfUnfiltered = (mUnfilteredResolveList == currentResolveList);
262         List<ResolvedComponentInfo> originalList = performSecondaryResolveListFiltering(
263                 currentResolveList, needsCopyOfUnfiltered);
264         if (originalList != null) {
265             // Only need the originalList value if there was a modification (otherwise it's null
266             // and shouldn't overwrite mUnfilteredResolveList).
267             mUnfilteredResolveList = originalList;
268         }
269 
270         boolean result =
271                 finishRebuildingListWithFilteredResults(currentResolveList, doPostProcessing);
272         Trace.endSection();
273         return result;
274     }
275 
276     /**
277      * Get the full (unfiltered) set of {@code ResolvedComponentInfo} records for all resolvers
278      * to be considered in a newly-rebuilt list. This list will be filtered and ranked before the
279      * rebuild is complete.
280      */
getInitialRebuiltResolveList()281     List<ResolvedComponentInfo> getInitialRebuiltResolveList() {
282         if (mBaseResolveList != null) {
283             List<ResolvedComponentInfo> currentResolveList = new ArrayList<>();
284             mResolverListController.addResolveListDedupe(currentResolveList,
285                     mTargetIntent,
286                     mBaseResolveList);
287             return currentResolveList;
288         } else {
289             return getResolversForUser(mUserHandle);
290         }
291     }
292 
293     /**
294      * Remove ineligible activities from {@code currentResolveList} (if non-null), in-place. More
295      * broadly, filtering logic should apply in the "primary" stage if it should preclude items from
296      * receiving the "other profile" special-treatment described in {@code rebuildList()}.
297      *
298      * @return A copy of the original {@code currentResolveList}, if any items were removed, or a
299      * (possibly null) reference to the original list otherwise. (That is, this always returns a
300      * list of all the unfiltered items, but if no items were filtered, it's just an alias for the
301      * same list that was passed in).
302      */
303     @Nullable
performPrimaryResolveListFiltering( @ullable List<ResolvedComponentInfo> currentResolveList)304     List<ResolvedComponentInfo> performPrimaryResolveListFiltering(
305             @Nullable List<ResolvedComponentInfo> currentResolveList) {
306         /* TODO: mBaseResolveList appears to be(?) some kind of configured mode. Why is it not
307          * subject to filterIneligibleActivities, even though all the other logic still applies
308          * (including "secondary" filtering)? (This also relates to the earlier question; do we
309          * believe there's an item that would be eligible for "other profile" special treatment,
310          * except we want to filter it out as ineligible... but only if we're not in
311          * "mBaseResolveList mode"? */
312         if ((mBaseResolveList != null) || (currentResolveList == null)) {
313             return currentResolveList;
314         }
315 
316         List<ResolvedComponentInfo> originalList =
317                 mResolverListController.filterIneligibleActivities(currentResolveList, true);
318         return (originalList == null) ? currentResolveList : originalList;
319     }
320 
321     /**
322      * Remove low-priority activities from {@code currentResolveList} (if non-null), in place. More
323      * broadly, filtering logic should apply in the "secondary" stage to prevent items from
324      * appearing in the rebuilt-list results, while still considering those items for the "other
325      * profile" special-treatment described in {@code rebuildList()}.
326      *
327      * @return the same (possibly null) List reference as {@code currentResolveList} if the list is
328      * unmodified as a result of filtering; or, if some item(s) were removed, then either a copy of
329      * the original {@code currentResolveList} (if {@code returnCopyOfOriginalListIfModified} is
330      * true), or null (otherwise).
331      */
332     @Nullable
performSecondaryResolveListFiltering( @ullable List<ResolvedComponentInfo> currentResolveList, boolean returnCopyOfOriginalListIfModified)333     List<ResolvedComponentInfo> performSecondaryResolveListFiltering(
334             @Nullable List<ResolvedComponentInfo> currentResolveList,
335             boolean returnCopyOfOriginalListIfModified) {
336         if ((currentResolveList == null) || currentResolveList.isEmpty()) {
337             return currentResolveList;
338         }
339         return mResolverListController.filterLowPriority(
340                 currentResolveList, returnCopyOfOriginalListIfModified);
341     }
342 
343     /**
344      * Update the special "other profile" UI treatment based on the components resolved for a
345      * newly-built list.
346      *
347      * @param otherProfileInfo the first {@code ResolvedComponentInfo} specifying a
348      * {@code targetUserId} other than {@code USER_CURRENT}, or null if no such component info was
349      * found in the process of rebuilding the list (or if any such candidates were already removed
350      * due to "primary filtering").
351      */
updateOtherProfileTreatment(@ullable ResolvedComponentInfo otherProfileInfo)352     void updateOtherProfileTreatment(@Nullable ResolvedComponentInfo otherProfileInfo) {
353         mLastChosen = null;
354 
355         if (otherProfileInfo != null) {
356             mOtherProfile = makeOtherProfileDisplayResolveInfo(
357                     otherProfileInfo,
358                     mPm,
359                     mTargetIntent,
360                     mResolverListCommunicator,
361                     mTargetDataLoader);
362         } else {
363             mOtherProfile = null;
364             try {
365                 mLastChosen = mResolverListController.getLastChosen();
366                 // TODO: does this also somehow need to update mLastChosenPosition? If so, maybe
367                 // the current method should also take responsibility for re-initializing
368                 // mLastChosenPosition, where it's currently done at the start of rebuildList()?
369                 // (Why is this related to the presence of mOtherProfile in fhe first place?)
370             } catch (RemoteException re) {
371                 Log.d(TAG, "Error calling getLastChosenActivity\n" + re);
372             }
373         }
374     }
375 
376     /**
377      * Prepare the appropriate placeholders to eventually display the final set of resolved
378      * components in a newly-rebuilt list, and spawn an asynchronous sorting task if necessary.
379      * This eventually results in a {@code onPostListReady} callback with {@code rebuildCompleted}
380      * true; if any asynchronous work is required, that will first be preceded by a separate
381      * occurrence of the callback with {@code rebuildCompleted} false (once there are placeholders
382      * set up to represent the pending asynchronous results).
383      * @return Whether we were able to do all the work to prepare the list for display
384      * synchronously; if false, there will eventually be two separate {@code onPostListReady}
385      * callbacks, first with placeholders to represent pending asynchronous results, then later when
386      * the results are ready for presentation.
387      */
finishRebuildingListWithFilteredResults( @ullable List<ResolvedComponentInfo> filteredResolveList, boolean doPostProcessing)388     boolean finishRebuildingListWithFilteredResults(
389             @Nullable List<ResolvedComponentInfo> filteredResolveList, boolean doPostProcessing) {
390         if (filteredResolveList == null || filteredResolveList.size() < 2) {
391             // No asynchronous work to do.
392             setPlaceholderCount(0);
393             processSortedList(filteredResolveList, doPostProcessing);
394             return true;
395         }
396 
397         int placeholderCount = filteredResolveList.size();
398         if (mResolverListCommunicator.useLayoutWithDefault()) {
399             --placeholderCount;
400         }
401         setPlaceholderCount(placeholderCount);
402 
403         // Send an "incomplete" list-ready while the async task is running.
404         postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ false);
405         createSortingTask(doPostProcessing).execute(filteredResolveList);
406         return false;
407     }
408 
409     AsyncTask<List<ResolvedComponentInfo>,
410             Void,
createSortingTask(boolean doPostProcessing)411             List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
412         return new AsyncTask<List<ResolvedComponentInfo>,
413                 Void,
414                 List<ResolvedComponentInfo>>() {
415             @Override
416             protected List<ResolvedComponentInfo> doInBackground(
417                     List<ResolvedComponentInfo>... params) {
418                 mResolverListController.sort(params[0]);
419                 return params[0];
420             }
421             @Override
422             protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
423                 processSortedList(sortedComponents, doPostProcessing);
424                 notifyDataSetChanged();
425                 if (doPostProcessing) {
426                     mResolverListCommunicator.updateProfileViewButton();
427                 }
428             }
429         };
430     }
431 
432     protected void processSortedList(List<ResolvedComponentInfo> sortedComponents,
433             boolean doPostProcessing) {
434         final int n = sortedComponents != null ? sortedComponents.size() : 0;
435         Trace.beginSection("ResolverListAdapter#processSortedList:" + n);
436         if (n != 0) {
437             // First put the initial items at the top.
438             if (mInitialIntents != null) {
439                 for (int i = 0; i < mInitialIntents.length; i++) {
440                     Intent ii = mInitialIntents[i];
441                     if (ii == null) {
442                         continue;
443                     }
444                     // Because of AIDL bug, resolveActivityInfo can't accept subclasses of Intent.
445                     final Intent rii = (ii.getClass() == Intent.class) ? ii : new Intent(ii);
446                     ActivityInfo ai = rii.resolveActivityInfo(mPm, 0);
447                     if (ai == null) {
448                         Log.w(TAG, "No activity found for " + ii);
449                         continue;
450                     }
451                     ResolveInfo ri = new ResolveInfo();
452                     ri.activityInfo = ai;
453                     UserManager userManager =
454                             (UserManager) mContext.getSystemService(Context.USER_SERVICE);
455                     if (ii instanceof LabeledIntent) {
456                         LabeledIntent li = (LabeledIntent) ii;
457                         ri.resolvePackageName = li.getSourcePackage();
458                         ri.labelRes = li.getLabelResource();
459                         ri.nonLocalizedLabel = li.getNonLocalizedLabel();
460                         ri.icon = li.getIconResource();
461                         ri.iconResourceId = ri.icon;
462                     }
463                     if (userManager.isManagedProfile()) {
464                         ri.noResourceId = true;
465                         ri.icon = 0;
466                     }
467 
468                     ri.userHandle = mInitialIntentsUserSpace;
469                     addResolveInfo(DisplayResolveInfo.newDisplayResolveInfo(
470                             ii,
471                             ri,
472                             ri.loadLabel(mPm),
473                             null,
474                             ii,
475                             mTargetDataLoader.createPresentationGetter(ri)));
476                 }
477             }
478 
479 
480             for (ResolvedComponentInfo rci : sortedComponents) {
481                 final ResolveInfo ri = rci.getResolveInfoAt(0);
482                 if (ri != null) {
483                     addResolveInfoWithAlternates(rci);
484                 }
485             }
486         }
487 
488         mResolverListCommunicator.sendVoiceChoicesIfNeeded();
489         postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
490         mIsTabLoaded = true;
491         Trace.endSection();
492     }
493 
494     /**
495      * Some necessary methods for creating the list are initiated in onCreate and will also
496      * determine the layout known. We therefore can't update the UI inline and post to the
497      * handler thread to update after the current task is finished.
498      * @param doPostProcessing Whether to update the UI and load additional direct share targets
499      *                         after the list has been rebuilt
500      * @param rebuildCompleted Whether the list has been completely rebuilt
501      */
502     void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) {
503         if (mPostListReadyRunnable == null) {
504             mPostListReadyRunnable = new Runnable() {
505                 @Override
506                 public void run() {
507                     mResolverListCommunicator.onPostListReady(ResolverListAdapter.this,
508                             doPostProcessing, rebuildCompleted);
509                     mPostListReadyRunnable = null;
510                 }
511             };
512             mContext.getMainThreadHandler().post(mPostListReadyRunnable);
513         }
514     }
515 
516     private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) {
517         final int count = rci.getCount();
518         final Intent intent = rci.getIntentAt(0);
519         final ResolveInfo add = rci.getResolveInfoAt(0);
520         final Intent replaceIntent =
521                 mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent);
522         final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent(
523                 add.activityInfo, mTargetIntent);
524         final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
525                 intent,
526                 add,
527                 (replaceIntent != null) ? replaceIntent : defaultIntent,
528                 mTargetDataLoader.createPresentationGetter(add));
529         dri.setPinned(rci.isPinned());
530         if (rci.isPinned()) {
531             Log.i(TAG, "Pinned item: " + rci.name);
532         }
533         addResolveInfo(dri);
534         if (replaceIntent == intent) {
535             // Only add alternates if we didn't get a specific replacement from
536             // the caller. If we have one it trumps potential alternates.
537             for (int i = 1, n = count; i < n; i++) {
538                 final Intent altIntent = rci.getIntentAt(i);
539                 dri.addAlternateSourceIntent(altIntent);
540             }
541         }
542         updateLastChosenPosition(add);
543     }
544 
545     private void updateLastChosenPosition(ResolveInfo info) {
546         // If another profile is present, ignore the last chosen entry.
547         if (mOtherProfile != null) {
548             mLastChosenPosition = -1;
549             return;
550         }
551         if (mLastChosen != null
552                 && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName)
553                 && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) {
554             mLastChosenPosition = mDisplayList.size() - 1;
555         }
556     }
557 
558     // We assume that at this point we've already filtered out the only intent for a different
559     // targetUserId which we're going to use.
560     private void addResolveInfo(DisplayResolveInfo dri) {
561         if (dri != null && dri.getResolveInfo() != null
562                 && dri.getResolveInfo().targetUserId == UserHandle.USER_CURRENT) {
563             if (shouldAddResolveInfo(dri)) {
564                 mDisplayList.add(dri);
565                 Log.i(TAG, "Add DisplayResolveInfo component: " + dri.getResolvedComponentName()
566                         + ", intent component: " + dri.getResolvedIntent().getComponent());
567             }
568         }
569     }
570 
571     // Check whether {@code dri} should be added into mDisplayList.
572     protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
573         // Checks if this info is already listed in display.
574         for (DisplayResolveInfo existingInfo : mDisplayList) {
575             if (mResolverListCommunicator
576                     .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
577                 return false;
578             }
579         }
580         return true;
581     }
582 
583     @Nullable
584     public ResolveInfo resolveInfoForPosition(int position, boolean filtered) {
585         TargetInfo target = targetInfoForPosition(position, filtered);
586         if (target != null) {
587             return target.getResolveInfo();
588         }
589         return null;
590     }
591 
592     @Nullable
593     public TargetInfo targetInfoForPosition(int position, boolean filtered) {
594         if (filtered) {
595             return getItem(position);
596         }
597         if (mDisplayList.size() > position) {
598             return mDisplayList.get(position);
599         }
600         return null;
601     }
602 
603     public int getCount() {
604         int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount :
605                 mDisplayList.size();
606         if (mFilterLastUsed && mLastChosenPosition >= 0) {
607             totalSize--;
608         }
609         return totalSize;
610     }
611 
612     public int getUnfilteredCount() {
613         return mDisplayList.size();
614     }
615 
616     @Nullable
617     public TargetInfo getItem(int position) {
618         if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) {
619             position++;
620         }
621         if (mDisplayList.size() > position) {
622             return mDisplayList.get(position);
623         } else {
624             return null;
625         }
626     }
627 
628     public long getItemId(int position) {
629         return position;
630     }
631 
632     public final int getDisplayResolveInfoCount() {
633         return mDisplayList.size();
634     }
635 
636     public final boolean allResolveInfosHandleAllWebDataUri() {
637         return mDisplayList.stream().allMatch(t -> t.getResolveInfo().handleAllWebDataURI);
638     }
639 
640     public final DisplayResolveInfo getDisplayResolveInfo(int index) {
641         // Used to query services. We only query services for primary targets, not alternates.
642         return mDisplayList.get(index);
643     }
644 
645     public final View getView(int position, View convertView, ViewGroup parent) {
646         View view = convertView;
647         if (view == null) {
648             view = createView(parent);
649         }
650         onBindView(view, getItem(position), position);
651         return view;
652     }
653 
654     public final View createView(ViewGroup parent) {
655         final View view = onCreateView(parent);
656         final ViewHolder holder = new ViewHolder(view);
657         view.setTag(holder);
658         return view;
659     }
660 
661     View onCreateView(ViewGroup parent) {
662         return mInflater.inflate(
663                 R.layout.resolve_list_item, parent, false);
664     }
665 
666     public final void bindView(int position, View view) {
667         onBindView(view, getItem(position), position);
668     }
669 
670     protected void onBindView(View view, TargetInfo info, int position) {
671         final ViewHolder holder = (ViewHolder) view.getTag();
672         if (info == null) {
673             holder.icon.setImageDrawable(loadIconPlaceholder());
674             holder.bindLabel("", "");
675             return;
676         }
677 
678         if (info.isDisplayResolveInfo()) {
679             DisplayResolveInfo dri = (DisplayResolveInfo) info;
680             if (dri.hasDisplayLabel()) {
681                 holder.bindLabel(
682                         dri.getDisplayLabel(),
683                         dri.getExtendedInfo());
684             } else {
685                 holder.bindLabel("", "");
686                 loadLabel(dri);
687             }
688             holder.bindIcon(info);
689             if (!dri.hasDisplayIcon()) {
690                 loadIcon(dri);
691             }
692         }
693     }
694 
695     protected final void loadIcon(DisplayResolveInfo info) {
696         if (mRequestedIcons.add(info)) {
697             mTargetDataLoader.loadAppTargetIcon(
698                     info,
699                     getUserHandle(),
700                     (drawable) -> onIconLoaded(info, drawable));
701         }
702     }
703 
704     private void onIconLoaded(DisplayResolveInfo displayResolveInfo, Drawable drawable) {
705         if (getOtherProfile() == displayResolveInfo) {
706             mResolverListCommunicator.updateProfileViewButton();
707         } else if (!displayResolveInfo.hasDisplayIcon()) {
708             displayResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable);
709             notifyDataSetChanged();
710         }
711     }
712 
713     private void loadLabel(DisplayResolveInfo info) {
714         if (mRequestedLabels.add(info)) {
715             mTargetDataLoader.loadLabel(info, (result) -> onLabelLoaded(info, result));
716         }
717     }
718 
719     protected final void onLabelLoaded(
720             DisplayResolveInfo displayResolveInfo, CharSequence[] result) {
721         if (displayResolveInfo.hasDisplayLabel()) {
722             return;
723         }
724         displayResolveInfo.setDisplayLabel(result[0]);
725         displayResolveInfo.setExtendedInfo(result[1]);
726         notifyDataSetChanged();
727     }
728 
729     public void onDestroy() {
730         if (mPostListReadyRunnable != null) {
731             mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
732             mPostListReadyRunnable = null;
733         }
734         if (mResolverListController != null) {
735             mResolverListController.destroy();
736         }
737         mRequestedIcons.clear();
738         mRequestedLabels.clear();
739     }
740 
741     private static ColorMatrixColorFilter getSuspendedColorMatrix() {
742         if (sSuspendedMatrixColorFilter == null) {
743 
744             int grayValue = 127;
745             float scale = 0.5f; // half bright
746 
747             ColorMatrix tempBrightnessMatrix = new ColorMatrix();
748             float[] mat = tempBrightnessMatrix.getArray();
749             mat[0] = scale;
750             mat[6] = scale;
751             mat[12] = scale;
752             mat[4] = grayValue;
753             mat[9] = grayValue;
754             mat[14] = grayValue;
755 
756             ColorMatrix matrix = new ColorMatrix();
757             matrix.setSaturation(0.0f);
758             matrix.preConcat(tempBrightnessMatrix);
759             sSuspendedMatrixColorFilter = new ColorMatrixColorFilter(matrix);
760         }
761         return sSuspendedMatrixColorFilter;
762     }
763 
764     protected final Drawable loadIconPlaceholder() {
765         return mContext.getDrawable(R.drawable.resolver_icon_placeholder);
766     }
767 
768     void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
769         final DisplayResolveInfo iconInfo = getFilteredItem();
770         if (iconInfo != null) {
771             mTargetDataLoader.loadAppTargetIcon(
772                     iconInfo, getUserHandle(), iconView::setImageDrawable);
773         }
774     }
775 
776     public UserHandle getUserHandle() {
777         return mUserHandle;
778     }
779 
780     protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
781         return mResolverListController.getResolversForIntentAsUser(
782                 /* shouldGetResolvedFilter= */ true,
783                 mResolverListCommunicator.shouldGetActivityMetadata(),
784                 mResolverListCommunicator.shouldGetOnlyDefaultActivities(),
785                 mIntents,
786                 userHandle);
787     }
788 
789     protected List<Intent> getIntents() {
790         return mIntents;
791     }
792 
793     protected boolean isTabLoaded() {
794         return mIsTabLoaded;
795     }
796 
797     protected void markTabLoaded() {
798         mIsTabLoaded = true;
799     }
800 
801     /**
802      * Find the first element in a list of {@code ResolvedComponentInfo} objects whose
803      * {@code ResolveInfo} specifies a {@code targetUserId} other than the current user.
804      * @return the first ResolvedComponentInfo targeting a non-current user, or null if there are
805      * none (or if the list itself is null).
806      */
807     private static ResolvedComponentInfo getFirstNonCurrentUserResolvedComponentInfo(
808             @Nullable List<ResolvedComponentInfo> resolveList) {
809         if (resolveList == null) {
810             return null;
811         }
812 
813         for (ResolvedComponentInfo info : resolveList) {
814             ResolveInfo resolveInfo = info.getResolveInfoAt(0);
815             if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
816                 return info;
817             }
818         }
819         return null;
820     }
821 
822     /**
823      * Set up a {@code DisplayResolveInfo} to provide "special treatment" for the first "other"
824      * profile in the resolve list (i.e., the first non-current profile to appear as the target user
825      * of an element in the resolve list).
826      */
827     private static DisplayResolveInfo makeOtherProfileDisplayResolveInfo(
828             ResolvedComponentInfo resolvedComponentInfo,
829             PackageManager pm,
830             Intent targetIntent,
831             ResolverListCommunicator resolverListCommunicator,
832             TargetDataLoader targetDataLoader) {
833         ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0);
834 
835         Intent pOrigIntent = resolverListCommunicator.getReplacementIntent(
836                 resolveInfo.activityInfo,
837                 resolvedComponentInfo.getIntentAt(0));
838         Intent replacementIntent = resolverListCommunicator.getReplacementIntent(
839                 resolveInfo.activityInfo, targetIntent);
840 
841         TargetPresentationGetter presentationGetter =
842                 targetDataLoader.createPresentationGetter(resolveInfo);
843 
844         return DisplayResolveInfo.newDisplayResolveInfo(
845                 resolvedComponentInfo.getIntentAt(0),
846                 resolveInfo,
847                 resolveInfo.loadLabel(pm),
848                 resolveInfo.loadLabel(pm),
849                 pOrigIntent != null ? pOrigIntent : replacementIntent,
850                 presentationGetter);
851     }
852 
853     /**
854      * Necessary methods to communicate between {@link ResolverListAdapter}
855      * and {@link ResolverActivity}.
856      */
857     interface ResolverListCommunicator {
858 
859         boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs);
860 
861         Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
862 
863         void onPostListReady(ResolverListAdapter listAdapter, boolean updateUi,
864                 boolean rebuildCompleted);
865 
866         void sendVoiceChoicesIfNeeded();
867 
868         void updateProfileViewButton();
869 
870         boolean useLayoutWithDefault();
871 
872         boolean shouldGetActivityMetadata();
873 
874         /**
875          * @return true to filter only apps that can handle
876          *     {@link android.content.Intent#CATEGORY_DEFAULT} intents
877          */
878         default boolean shouldGetOnlyDefaultActivities() { return true; };
879 
880         void onHandlePackagesChanged(ResolverListAdapter listAdapter);
881     }
882 
883     /**
884      * A view holder keeps a reference to a list view and provides functionality for managing its
885      * state.
886      */
887     @VisibleForTesting
888     public static class ViewHolder {
889         public View itemView;
890         public Drawable defaultItemViewBackground;
891 
892         public TextView text;
893         public TextView text2;
894         public ImageView icon;
895 
896         @VisibleForTesting
897         public ViewHolder(View view) {
898             itemView = view;
899             defaultItemViewBackground = view.getBackground();
900             text = (TextView) view.findViewById(com.android.internal.R.id.text1);
901             text2 = (TextView) view.findViewById(com.android.internal.R.id.text2);
902             icon = (ImageView) view.findViewById(com.android.internal.R.id.icon);
903         }
904 
905         public void bindLabel(CharSequence label, CharSequence subLabel) {
906             text.setText(label);
907 
908             if (TextUtils.equals(label, subLabel)) {
909                 subLabel = null;
910             }
911 
912             if (!TextUtils.isEmpty(subLabel)) {
913                 text.setMaxLines(1);
914                 text2.setText(subLabel);
915                 text2.setVisibility(View.VISIBLE);
916             } else {
917                 text.setMaxLines(2);
918                 text2.setVisibility(View.GONE);
919             }
920 
921             itemView.setContentDescription(null);
922         }
923 
924         public void updateContentDescription(String description) {
925             itemView.setContentDescription(description);
926         }
927 
928         /**
929          * Bind view holder to a TargetInfo.
930          */
931         public void bindIcon(TargetInfo info) {
932             Drawable displayIcon = info.getDisplayIconHolder().getDisplayIcon();
933             icon.setImageDrawable(displayIcon);
934             if (info.isSuspended()) {
935                 icon.setColorFilter(getSuspendedColorMatrix());
936             } else {
937                 icon.setColorFilter(null);
938             }
939         }
940     }
941 }
942