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