• 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.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     protected void onIconLoaded(DisplayResolveInfo info) {
684         notifyDataSetChanged();
685     }
686 
687     private void loadLabel(DisplayResolveInfo info) {
688         LoadLabelTask task = mLabelLoaders.get(info);
689         if (task == null) {
690             task = createLoadLabelTask(info);
691             mLabelLoaders.put(info, task);
692             task.execute();
693         }
694     }
695 
696     protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
697         return new LoadLabelTask(info);
698     }
699 
700     public void onDestroy() {
701         if (mPostListReadyRunnable != null) {
702             mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
703             mPostListReadyRunnable = null;
704         }
705         if (mResolverListController != null) {
706             mResolverListController.destroy();
707         }
708         cancelTasks(mIconLoaders.values());
709         cancelTasks(mLabelLoaders.values());
710         mIconLoaders.clear();
711         mLabelLoaders.clear();
712     }
713 
714     private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) {
715         for (T task: tasks) {
716             task.cancel(false);
717         }
718     }
719 
720     private static ColorMatrixColorFilter getSuspendedColorMatrix() {
721         if (sSuspendedMatrixColorFilter == null) {
722 
723             int grayValue = 127;
724             float scale = 0.5f; // half bright
725 
726             ColorMatrix tempBrightnessMatrix = new ColorMatrix();
727             float[] mat = tempBrightnessMatrix.getArray();
728             mat[0] = scale;
729             mat[6] = scale;
730             mat[12] = scale;
731             mat[4] = grayValue;
732             mat[9] = grayValue;
733             mat[14] = grayValue;
734 
735             ColorMatrix matrix = new ColorMatrix();
736             matrix.setSaturation(0.0f);
737             matrix.preConcat(tempBrightnessMatrix);
738             sSuspendedMatrixColorFilter = new ColorMatrixColorFilter(matrix);
739         }
740         return sSuspendedMatrixColorFilter;
741     }
742 
743     ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) {
744         return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai);
745     }
746 
747     ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) {
748         return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri);
749     }
750 
751     Drawable loadIconForResolveInfo(ResolveInfo ri) {
752         // Load icons based on userHandle from ResolveInfo. If in work profile/clone profile, icons
753         // should be badged.
754         return makePresentationGetter(ri).getIcon(ri.userHandle);
755     }
756 
757     void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
758         final DisplayResolveInfo iconInfo = getFilteredItem();
759         if (iconView != null && iconInfo != null) {
760             new AsyncTask<Void, Void, Drawable>() {
761                 @Override
762                 protected Drawable doInBackground(Void... params) {
763                     return loadIconForResolveInfo(iconInfo.getResolveInfo());
764                 }
765 
766                 @Override
767                 protected void onPostExecute(Drawable d) {
768                     iconView.setImageDrawable(d);
769                 }
770             }.execute();
771         }
772     }
773 
774     @VisibleForTesting
775     public UserHandle getUserHandle() {
776         return mResolverListController.getUserHandle();
777     }
778 
779     protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
780         return mResolverListController.getResolversForIntentAsUser(true,
781                 mResolverListCommunicator.shouldGetActivityMetadata(),
782                 mResolverListCommunicator.shouldGetOnlyDefaultActivities(),
783                 mIntents, userHandle);
784     }
785 
786     protected List<Intent> getIntents() {
787         return mIntents;
788     }
789 
790     protected boolean isTabLoaded() {
791         return mIsTabLoaded;
792     }
793 
794     protected void markTabLoaded() {
795         mIsTabLoaded = true;
796     }
797 
798     protected boolean alwaysShowSubLabel() {
799         return false;
800     }
801 
802     /**
803      * Find the first element in a list of {@code ResolvedComponentInfo} objects whose
804      * {@code ResolveInfo} specifies a {@code targetUserId} other than the current user.
805      * @return the first ResolvedComponentInfo targeting a non-current user, or null if there are
806      * none (or if the list itself is null).
807      */
808     private static ResolvedComponentInfo getFirstNonCurrentUserResolvedComponentInfo(
809             @Nullable List<ResolvedComponentInfo> resolveList) {
810         if (resolveList == null) {
811             return null;
812         }
813 
814         for (ResolvedComponentInfo info : resolveList) {
815             ResolveInfo resolveInfo = info.getResolveInfoAt(0);
816             if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
817                 return info;
818             }
819         }
820         return null;
821     }
822 
823     /**
824      * Set up a {@code DisplayResolveInfo} to provide "special treatment" for the first "other"
825      * profile in the resolve list (i.e., the first non-current profile to appear as the target user
826      * of an element in the resolve list).
827      */
828     private static DisplayResolveInfo makeOtherProfileDisplayResolveInfo(
829             Context context,
830             ResolvedComponentInfo resolvedComponentInfo,
831             PackageManager pm,
832             ResolverListCommunicator resolverListCommunicator,
833             int iconDpi) {
834         ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0);
835 
836         Intent pOrigIntent = resolverListCommunicator.getReplacementIntent(
837                 resolveInfo.activityInfo,
838                 resolvedComponentInfo.getIntentAt(0));
839         Intent replacementIntent = resolverListCommunicator.getReplacementIntent(
840                 resolveInfo.activityInfo,
841                 resolverListCommunicator.getTargetIntent());
842 
843         ResolveInfoPresentationGetter presentationGetter =
844                 new ResolveInfoPresentationGetter(context, iconDpi, resolveInfo);
845 
846         return new DisplayResolveInfo(
847                 resolvedComponentInfo.getIntentAt(0),
848                 resolveInfo,
849                 resolveInfo.loadLabel(pm),
850                 resolveInfo.loadLabel(pm),
851                 pOrigIntent != null ? pOrigIntent : replacementIntent,
852                 presentationGetter);
853     }
854 
855     /**
856      * Necessary methods to communicate between {@link ResolverListAdapter}
857      * and {@link ResolverActivity}.
858      */
859     interface ResolverListCommunicator {
860 
861         boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs);
862 
863         Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
864 
865         void onPostListReady(ResolverListAdapter listAdapter, boolean updateUi,
866                 boolean rebuildCompleted);
867 
868         void sendVoiceChoicesIfNeeded();
869 
870         void updateProfileViewButton();
871 
872         boolean useLayoutWithDefault();
873 
874         boolean shouldGetActivityMetadata();
875 
876         /**
877          * @return true to filter only apps that can handle
878          *     {@link android.content.Intent#CATEGORY_DEFAULT} intents
879          */
880         default boolean shouldGetOnlyDefaultActivities() { return true; };
881 
882         Intent getTargetIntent();
883 
884         void onHandlePackagesChanged(ResolverListAdapter listAdapter);
885     }
886 
887     /**
888      * A view holder keeps a reference to a list view and provides functionality for managing its
889      * state.
890      */
891     @VisibleForTesting
892     public static class ViewHolder {
893         public View itemView;
894         public Drawable defaultItemViewBackground;
895 
896         public TextView text;
897         public TextView text2;
898         public ImageView icon;
899 
900         @VisibleForTesting
901         public ViewHolder(View view) {
902             itemView = view;
903             defaultItemViewBackground = view.getBackground();
904             text = (TextView) view.findViewById(com.android.internal.R.id.text1);
905             text2 = (TextView) view.findViewById(com.android.internal.R.id.text2);
906             icon = (ImageView) view.findViewById(R.id.icon);
907         }
908 
909         public void bindLabel(CharSequence label, CharSequence subLabel, boolean showSubLabel) {
910             text.setText(label);
911 
912             if (TextUtils.equals(label, subLabel)) {
913                 subLabel = null;
914             }
915 
916             text2.setText(subLabel);
917             if (showSubLabel || subLabel != null) {
918                 text2.setVisibility(View.VISIBLE);
919             } else {
920                 text2.setVisibility(View.GONE);
921             }
922 
923             itemView.setContentDescription(null);
924         }
925 
926         public void updateContentDescription(String description) {
927             itemView.setContentDescription(description);
928         }
929 
930         public void bindIcon(TargetInfo info) {
931             icon.setImageDrawable(info.getDisplayIcon(itemView.getContext()));
932             if (info.isSuspended()) {
933                 icon.setColorFilter(getSuspendedColorMatrix());
934             } else {
935                 icon.setColorFilter(null);
936             }
937         }
938     }
939 
940     protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
941         private final DisplayResolveInfo mDisplayResolveInfo;
942 
943         protected LoadLabelTask(DisplayResolveInfo dri) {
944             mDisplayResolveInfo = dri;
945         }
946 
947         @Override
948         protected CharSequence[] doInBackground(Void... voids) {
949             ResolveInfoPresentationGetter pg =
950                     makePresentationGetter(mDisplayResolveInfo.getResolveInfo());
951 
952             if (mIsAudioCaptureDevice) {
953                 // This is an audio capture device, so check record permissions
954                 ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo;
955                 String packageName = activityInfo.packageName;
956 
957                 int uid = activityInfo.applicationInfo.uid;
958                 boolean hasRecordPermission =
959                         PermissionChecker.checkPermissionForPreflight(
960                                 mContext,
961                                 android.Manifest.permission.RECORD_AUDIO, -1, uid,
962                                 packageName)
963                                 == android.content.pm.PackageManager.PERMISSION_GRANTED;
964 
965                 if (!hasRecordPermission) {
966                     // Doesn't have record permission, so warn the user
967                     return new CharSequence[] {
968                             pg.getLabel(),
969                             mContext.getString(R.string.usb_device_resolve_prompt_warn)
970                     };
971                 }
972             }
973 
974             return new CharSequence[] {
975                     pg.getLabel(),
976                     pg.getSubLabel()
977             };
978         }
979 
980         @Override
981         protected void onPostExecute(CharSequence[] result) {
982             if (mDisplayResolveInfo.hasDisplayLabel()) {
983                 return;
984             }
985             mDisplayResolveInfo.setDisplayLabel(result[0]);
986             mDisplayResolveInfo.setExtendedInfo(result[1]);
987             notifyDataSetChanged();
988         }
989     }
990 
991     class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
992         protected final DisplayResolveInfo mDisplayResolveInfo;
993         private final ResolveInfo mResolveInfo;
994 
995         LoadIconTask(DisplayResolveInfo dri) {
996             mDisplayResolveInfo = dri;
997             mResolveInfo = dri.getResolveInfo();
998         }
999 
1000         @Override
1001         protected Drawable doInBackground(Void... params) {
1002             return loadIconForResolveInfo(mResolveInfo);
1003         }
1004 
1005         @Override
1006         protected void onPostExecute(Drawable d) {
1007             if (getOtherProfile() == mDisplayResolveInfo) {
1008                 mResolverListCommunicator.updateProfileViewButton();
1009             } else if (!mDisplayResolveInfo.hasDisplayIcon()) {
1010                 mDisplayResolveInfo.setDisplayIcon(d);
1011                 onIconLoaded(mDisplayResolveInfo);
1012             }
1013         }
1014     }
1015 
1016     /**
1017      * Loads the icon and label for the provided ResolveInfo.
1018      */
1019     @VisibleForTesting
1020     public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter {
1021         private final ResolveInfo mRi;
1022         public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) {
1023             super(ctx, iconDpi, ri.activityInfo);
1024             mRi = ri;
1025         }
1026 
1027         @Override
1028         Drawable getIconSubstituteInternal() {
1029             Drawable dr = null;
1030             try {
1031                 // Do not use ResolveInfo#getIconResource() as it defaults to the app
1032                 if (mRi.resolvePackageName != null && mRi.icon != 0) {
1033                     dr = loadIconFromResource(
1034                             mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon);
1035                 }
1036             } catch (PackageManager.NameNotFoundException e) {
1037                 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
1038                         + "couldn't find resources for package", e);
1039             }
1040 
1041             // Fall back to ActivityInfo if no icon is found via ResolveInfo
1042             if (dr == null) dr = super.getIconSubstituteInternal();
1043 
1044             return dr;
1045         }
1046 
1047         @Override
1048         String getAppSubLabelInternal() {
1049             // Will default to app name if no intent filter or activity label set, make sure to
1050             // check if subLabel matches label before final display
1051             return mRi.loadLabel(mPm).toString();
1052         }
1053 
1054         @Override
1055         String getAppLabelForSubstitutePermission() {
1056             // Will default to app name if no activity label set
1057             return mRi.getComponentInfo().loadLabel(mPm).toString();
1058         }
1059     }
1060 
1061     /**
1062      * Loads the icon and label for the provided ActivityInfo.
1063      */
1064     @VisibleForTesting
1065     public static class ActivityInfoPresentationGetter extends
1066             TargetPresentationGetter {
1067         private final ActivityInfo mActivityInfo;
1068         public ActivityInfoPresentationGetter(Context ctx, int iconDpi,
1069                 ActivityInfo activityInfo) {
1070             super(ctx, iconDpi, activityInfo.applicationInfo);
1071             mActivityInfo = activityInfo;
1072         }
1073 
1074         @Override
1075         Drawable getIconSubstituteInternal() {
1076             Drawable dr = null;
1077             try {
1078                 // Do not use ActivityInfo#getIconResource() as it defaults to the app
1079                 if (mActivityInfo.icon != 0) {
1080                     dr = loadIconFromResource(
1081                             mPm.getResourcesForApplication(mActivityInfo.applicationInfo),
1082                             mActivityInfo.icon);
1083                 }
1084             } catch (PackageManager.NameNotFoundException e) {
1085                 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
1086                         + "couldn't find resources for package", e);
1087             }
1088 
1089             return dr;
1090         }
1091 
1092         @Override
1093         String getAppSubLabelInternal() {
1094             // Will default to app name if no activity label set, make sure to check if subLabel
1095             // matches label before final display
1096             return (String) mActivityInfo.loadLabel(mPm);
1097         }
1098 
1099         @Override
1100         String getAppLabelForSubstitutePermission() {
1101             return getAppSubLabelInternal();
1102         }
1103     }
1104 
1105     /**
1106      * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application
1107      * icon and label over any IntentFilter or Activity icon to increase user understanding, with an
1108      * exception for applications that hold the right permission. Always attempts to use available
1109      * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
1110      * Strings to strip creative formatting.
1111      */
1112     private abstract static class TargetPresentationGetter {
1113         @Nullable abstract Drawable getIconSubstituteInternal();
1114         @Nullable abstract String getAppSubLabelInternal();
1115         @Nullable abstract String getAppLabelForSubstitutePermission();
1116 
1117         private Context mCtx;
1118         private final int mIconDpi;
1119         private final boolean mHasSubstitutePermission;
1120         private final ApplicationInfo mAi;
1121 
1122         protected PackageManager mPm;
1123 
1124         TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) {
1125             mCtx = ctx;
1126             mPm = ctx.getPackageManager();
1127             mAi = ai;
1128             mIconDpi = iconDpi;
1129             mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission(
1130                     android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON,
1131                     mAi.packageName);
1132         }
1133 
1134         public Drawable getIcon(UserHandle userHandle) {
1135             return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle));
1136         }
1137 
1138         public Bitmap getIconBitmap(@Nullable UserHandle userHandle) {
1139             Drawable dr = null;
1140             if (mHasSubstitutePermission) {
1141                 dr = getIconSubstituteInternal();
1142             }
1143 
1144             if (dr == null) {
1145                 try {
1146                     if (mAi.icon != 0) {
1147                         dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon);
1148                     }
1149                 } catch (PackageManager.NameNotFoundException ignore) {
1150                 }
1151             }
1152 
1153             // Fall back to ApplicationInfo#loadIcon if nothing has been loaded
1154             if (dr == null) {
1155                 dr = mAi.loadIcon(mPm);
1156             }
1157 
1158             SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx);
1159             Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle);
1160             sif.recycle();
1161 
1162             return icon;
1163         }
1164 
1165         public String getLabel() {
1166             String label = null;
1167             // Apps with the substitute permission will always show the activity label as the
1168             // app label if provided
1169             if (mHasSubstitutePermission) {
1170                 label = getAppLabelForSubstitutePermission();
1171             }
1172 
1173             if (label == null) {
1174                 label = (String) mAi.loadLabel(mPm);
1175             }
1176 
1177             return label;
1178         }
1179 
1180         public String getSubLabel() {
1181             // Apps with the substitute permission will always show the resolve info label as the
1182             // sublabel if provided
1183             if (mHasSubstitutePermission){
1184                 String appSubLabel = getAppSubLabelInternal();
1185                 // Use the resolve info label as sublabel if it is set
1186                 if(!TextUtils.isEmpty(appSubLabel)
1187                     && !TextUtils.equals(appSubLabel, getLabel())){
1188                     return appSubLabel;
1189                 }
1190                 return null;
1191             }
1192             return getAppSubLabelInternal();
1193         }
1194 
1195         protected String loadLabelFromResource(Resources res, int resId) {
1196             return res.getString(resId);
1197         }
1198 
1199         @Nullable
1200         protected Drawable loadIconFromResource(Resources res, int resId) {
1201             try {
1202                 return res.getDrawableForDensity(resId, mIconDpi);
1203             } catch (Resources.NotFoundException e) {
1204                 Log.e(TAG, "Resource not found", e);
1205                 return null;
1206             }
1207         }
1208 
1209     }
1210 }
1211