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