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