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