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