1 /*
2  * Copyright (C) 2024 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 package com.android.intentresolver.profiles;
17 
18 import android.annotation.Nullable;
19 import android.os.Trace;
20 import android.os.UserHandle;
21 import android.view.LayoutInflater;
22 import android.view.View;
23 import android.view.ViewGroup;
24 import android.widget.Button;
25 import android.widget.TabHost;
26 import android.widget.TextView;
27 
28 import androidx.viewpager.widget.PagerAdapter;
29 import androidx.viewpager.widget.ViewPager;
30 
31 import com.android.intentresolver.ResolverListAdapter;
32 import com.android.intentresolver.emptystate.EmptyState;
33 import com.android.intentresolver.emptystate.EmptyStateProvider;
34 import com.android.intentresolver.shared.model.Profile;
35 import com.android.internal.annotations.VisibleForTesting;
36 
37 import com.google.common.collect.ImmutableList;
38 
39 import java.util.HashSet;
40 import java.util.Objects;
41 import java.util.Optional;
42 import java.util.Set;
43 import java.util.concurrent.atomic.AtomicBoolean;
44 import java.util.function.Consumer;
45 import java.util.function.Function;
46 import java.util.function.Supplier;
47 
48 /**
49  * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet).
50  *
51  * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter
52  * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in
53  * the per-profile records.
54  * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to
55  * control the contents of a given per-profile list. This is provided for convenience, since it must
56  * be possible to get the list adapter from the page adapter via our
57  * <code>mListAdapterExtractor</code>.
58  */
59 public class MultiProfilePagerAdapter<
60         PageViewT extends ViewGroup,
61         SinglePageAdapterT,
62         ListAdapterT extends ResolverListAdapter> extends PagerAdapter {
63 
64     public static final int PROFILE_PERSONAL = Profile.Type.PERSONAL.ordinal();
65     public static final int PROFILE_WORK = Profile.Type.WORK.ordinal();
66 
67     // Removed, must be constants. This is only used for linting anyway.
68     // @IntDef({PROFILE_PERSONAL, PROFILE_WORK})
69     public @interface ProfileType {}
70 
71     private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
72     private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
73     private final Supplier<ViewGroup> mPageViewInflater;
74 
75     private final ImmutableList<ProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems;
76 
77     private final EmptyStateProvider mEmptyStateProvider;
78     private final UserHandle mWorkProfileUserHandle;
79     private final UserHandle mCloneProfileUserHandle;
80     private final Supplier<Boolean> mWorkProfileQuietModeChecker;  // True when work is quiet.
81 
82     private final Set<Integer> mLoadedPages;
83     private int mCurrentPage;
84     private OnProfileSelectedListener mOnProfileSelectedListener;
85 
MultiProfilePagerAdapter( Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor, AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder, ImmutableList<TabConfig<SinglePageAdapterT>> tabs, EmptyStateProvider emptyStateProvider, Supplier<Boolean> workProfileQuietModeChecker, @ProfileType int defaultProfile, UserHandle workProfileUserHandle, UserHandle cloneProfileUserHandle, Supplier<ViewGroup> pageViewInflater, Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier)86     protected MultiProfilePagerAdapter(
87             Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
88             AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
89             ImmutableList<TabConfig<SinglePageAdapterT>> tabs,
90             EmptyStateProvider emptyStateProvider,
91             Supplier<Boolean> workProfileQuietModeChecker,
92             @ProfileType int defaultProfile,
93             UserHandle workProfileUserHandle,
94             UserHandle cloneProfileUserHandle,
95             Supplier<ViewGroup> pageViewInflater,
96             Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
97         mLoadedPages = new HashSet<>();
98         mWorkProfileUserHandle = workProfileUserHandle;
99         mCloneProfileUserHandle = cloneProfileUserHandle;
100         mEmptyStateProvider = emptyStateProvider;
101         mWorkProfileQuietModeChecker = workProfileQuietModeChecker;
102 
103         mListAdapterExtractor = listAdapterExtractor;
104         mAdapterBinder = adapterBinder;
105         mPageViewInflater = pageViewInflater;
106 
107         ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
108                 new ImmutableList.Builder<>();
109         for (TabConfig<SinglePageAdapterT> tab : tabs) {
110             // TODO: consider representing tabConfig in a different data structure that can ensure
111             // uniqueness of their profile assignments (while still respecting the client's
112             // requested tab order).
113             items.add(
114                     createProfileDescriptor(
115                             tab.mProfile,
116                             tab.mTabLabel,
117                             tab.mTabAccessibilityLabel,
118                             tab.mTabTag,
119                             tab.mPageAdapter,
120                             containerBottomPaddingOverrideSupplier));
121         }
122         mItems = items.build();
123 
124         mCurrentPage =
125                 hasPageForProfile(defaultProfile) ? getPageNumberForProfile(defaultProfile) : 0;
126     }
127 
createProfileDescriptor( @rofileType int profile, String tabLabel, String tabAccessibilityLabel, String tabTag, SinglePageAdapterT adapter, Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier)128     private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor(
129             @ProfileType int profile,
130             String tabLabel,
131             String tabAccessibilityLabel,
132             String tabTag,
133             SinglePageAdapterT adapter,
134             Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
135         return new ProfileDescriptor<>(
136                 profile,
137                 tabLabel,
138                 tabAccessibilityLabel,
139                 tabTag,
140                 mPageViewInflater.get(),
141                 adapter,
142                 containerBottomPaddingOverrideSupplier);
143     }
144 
hasPageForIndex(int pageIndex)145     private boolean hasPageForIndex(int pageIndex) {
146         return (pageIndex >= 0) && (pageIndex < getCount());
147     }
148 
hasPageForProfile(@rofileType int profile)149     public final boolean hasPageForProfile(@ProfileType int profile) {
150         return hasPageForIndex(getPageNumberForProfile(profile));
151     }
152 
getProfileForPageNumber(int position)153     private @ProfileType int getProfileForPageNumber(int position) {
154         if (hasPageForIndex(position)) {
155             return mItems.get(position).mProfile;
156         }
157         return -1;
158     }
159 
getPageNumberForProfile(@rofileType int profile)160     public int getPageNumberForProfile(@ProfileType int profile) {
161         for (int i = 0; i < mItems.size(); ++i) {
162             if (profile == mItems.get(i).mProfile) {
163                 return i;
164             }
165         }
166         return -1;
167     }
168 
getListAdapterForPageNumber(int pageNumber)169     private ListAdapterT getListAdapterForPageNumber(int pageNumber) {
170         SinglePageAdapterT pageAdapter = getPageAdapterForIndex(pageNumber);
171         if (pageAdapter == null) {
172             return null;
173         }
174         return mListAdapterExtractor.apply(pageAdapter);
175     }
176 
getProfileForUserHandle(UserHandle userHandle)177     private @ProfileType int getProfileForUserHandle(UserHandle userHandle) {
178         if (userHandle.equals(getCloneUserHandle())) {
179             // TODO: can we push this special case elsewhere -- e.g., when we check against each
180             // list adapter's user handle in the loop below, could we instead ask the list adapter
181             // whether it "represents" the queried user handle, and have the personal list adapter
182             // return true because it knows it's also associated with the clone profile? Or if we
183             // don't want to make modifications to the list adapter, maybe we could at least specify
184             // it in our per-page configuration data that we use to build our tabs/pages, and then
185             // maintain the relevant bookkeeping in our own ProfileDescriptor?
186             return PROFILE_PERSONAL;
187         }
188         for (int i = 0; i < mItems.size(); ++i) {
189             ListAdapterT listAdapter = getListAdapterForPageNumber(i);
190             if (listAdapter.getUserHandle().equals(userHandle)) {
191                 return mItems.get(i).mProfile;
192             }
193         }
194         return -1;
195     }
196 
getPageNumberForUserHandle(UserHandle userHandle)197     private int getPageNumberForUserHandle(UserHandle userHandle) {
198         return getPageNumberForProfile(getProfileForUserHandle(userHandle));
199     }
200 
201     /**
202      * Returns the {@link ListAdapterT} instance of the profile that represents
203      * <code>userHandle</code>. If there is no such adapter for the specified
204      * <code>userHandle</code>, returns {@code null}.
205      * <p>For example, if there is a work profile on the device with user id 10, calling this method
206      * with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}.
207      */
208     @Nullable
getListAdapterForUserHandle(UserHandle userHandle)209     public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
210         return getListAdapterForPageNumber(getPageNumberForUserHandle(userHandle));
211     }
212 
213     @Nullable
getDescriptorForUserHandle( UserHandle userHandle)214     private ProfileDescriptor<PageViewT, SinglePageAdapterT> getDescriptorForUserHandle(
215             UserHandle userHandle) {
216         return getItem(getPageNumberForUserHandle(userHandle));
217     }
218 
getPageNumberForTabTag(String tag)219     private int getPageNumberForTabTag(String tag) {
220         for (int i = 0; i < mItems.size(); ++i) {
221             if (Objects.equals(mItems.get(i).mTabTag, tag)) {
222                 return i;
223             }
224         }
225         return -1;
226     }
227 
updateActiveTabStyle(TabHost tabHost)228     private void updateActiveTabStyle(TabHost tabHost) {
229         int currentTab = tabHost.getCurrentTab();
230 
231         for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) {
232             // TODO: can we avoid this downcast by pushing our knowledge of the intended view type
233             // somewhere else?
234             TextView tabText = (TextView) tabHost.getTabWidget().getChildAt(pageNumber);
235             tabText.setSelected(currentTab == pageNumber);
236         }
237     }
238 
setupProfileTabs( LayoutInflater layoutInflater, TabHost tabHost, ViewPager viewPager, int tabButtonLayoutResId, int tabPageContentViewId, Runnable onTabChangeListener, OnProfileSelectedListener clientOnProfileSelectedListener)239     public void setupProfileTabs(
240             LayoutInflater layoutInflater,
241             TabHost tabHost,
242             ViewPager viewPager,
243             int tabButtonLayoutResId,
244             int tabPageContentViewId,
245             Runnable onTabChangeListener,
246             OnProfileSelectedListener clientOnProfileSelectedListener) {
247         tabHost.setup();
248         tabHost.getTabWidget().removeAllViews();
249         viewPager.setSaveEnabled(false);
250 
251         for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) {
252             ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = mItems.get(pageNumber);
253             Button profileButton = (Button) layoutInflater.inflate(
254                     tabButtonLayoutResId, tabHost.getTabWidget(), false);
255             profileButton.setText(descriptor.mTabLabel);
256             profileButton.setContentDescription(descriptor.mTabAccessibilityLabel);
257 
258             TabHost.TabSpec profileTabSpec = tabHost.newTabSpec(descriptor.mTabTag)
259                     .setContent(tabPageContentViewId)
260                     .setIndicator(profileButton);
261             tabHost.addTab(profileTabSpec);
262         }
263 
264         tabHost.getTabWidget().setVisibility(View.VISIBLE);
265 
266         updateActiveTabStyle(tabHost);
267 
268         tabHost.setOnTabChangedListener(tabTag -> {
269             updateActiveTabStyle(tabHost);
270 
271             int pageNumber = getPageNumberForTabTag(tabTag);
272             if (pageNumber >= 0) {
273                 viewPager.setCurrentItem(pageNumber);
274             }
275             onTabChangeListener.run();
276         });
277 
278         viewPager.setVisibility(View.VISIBLE);
279         tabHost.setCurrentTab(getCurrentPage());
280         mOnProfileSelectedListener =
281                 new OnProfileSelectedListener() {
282                     @Override
283                     public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {
284                         tabHost.setCurrentTab(pageNumber);
285                         clientOnProfileSelectedListener.onProfilePageSelected(
286                                 profileId, pageNumber);
287                     }
288 
289                     @Override
290                     public void onProfilePageStateChanged(int state) {
291                         clientOnProfileSelectedListener.onProfilePageStateChanged(state);
292                     }
293                 };
294     }
295 
296     /**
297      * Sets this instance of this class as {@link ViewPager}'s {@link PagerAdapter} and sets
298      * an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed
299      * page and rebuilds the list.
300      */
setupViewPager(ViewPager viewPager)301     public void setupViewPager(ViewPager viewPager) {
302         viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
303             @Override
304             public void onPageSelected(int position) {
305                 MultiProfilePagerAdapter.this.onPageSelected(position);
306             }
307 
308             @Override
309             public void onPageScrollStateChanged(int state) {
310                 if (mOnProfileSelectedListener != null) {
311                     mOnProfileSelectedListener.onProfilePageStateChanged(state);
312                 }
313             }
314         });
315         viewPager.setAdapter(this);
316         viewPager.setCurrentItem(mCurrentPage);
317         mLoadedPages.add(mCurrentPage);
318     }
319 
onPageSelected(int position)320     private void onPageSelected(int position) {
321         mCurrentPage = position;
322         if (!mLoadedPages.contains(position)) {
323             rebuildActiveTab(true);
324             mLoadedPages.add(position);
325         }
326         if (mOnProfileSelectedListener != null) {
327             mOnProfileSelectedListener.onProfilePageSelected(
328                     getProfileForPageNumber(position), position);
329         }
330     }
331 
clearInactiveProfileCache()332     public void clearInactiveProfileCache() {
333         forEachInactivePage(pageNumber -> mLoadedPages.remove(pageNumber));
334     }
335 
336     @Override
instantiateItem(ViewGroup container, int position)337     public final ViewGroup instantiateItem(ViewGroup container, int position) {
338         setupListAdapter(position);
339         final ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(position);
340         container.addView(descriptor.mRootView);
341         return descriptor.mRootView;
342     }
343 
344     @Override
destroyItem(ViewGroup container, int position, Object view)345     public void destroyItem(ViewGroup container, int position, Object view) {
346         container.removeView((View) view);
347     }
348 
349     @Override
getCount()350     public int getCount() {
351         return getItemCount();
352     }
353 
getCurrentPage()354     public int getCurrentPage() {
355         return mCurrentPage;
356     }
357 
358     /**
359      * Set active adapter page. A support method for the poayload reselection logic.
360      */
setCurrentPage(int page)361     public void setCurrentPage(int page) {
362         onPageSelected(page);
363     }
364 
getActiveProfile()365     public final @ProfileType int getActiveProfile() {
366         return getProfileForPageNumber(getCurrentPage());
367     }
368 
369     @VisibleForTesting
getCurrentUserHandle()370     public UserHandle getCurrentUserHandle() {
371         return getActiveListAdapter().getUserHandle();
372     }
373 
374     @Override
isViewFromObject(View view, Object object)375     public boolean isViewFromObject(View view, Object object) {
376         return view == object;
377     }
378 
379     @Override
getPageTitle(int position)380     public CharSequence getPageTitle(int position) {
381         return null;
382     }
383 
getCloneUserHandle()384     public UserHandle getCloneUserHandle() {
385         return mCloneProfileUserHandle;
386     }
387 
388     /**
389      * Returns the {@link ProfileDescriptor} relevant to the given <code>pageIndex</code>.
390      * <ul>
391      * <li>For a device with only one user, <code>pageIndex</code> value of
392      * <code>0</code> would return the personal profile {@link ProfileDescriptor}.</li>
393      * <li>For a device with a work profile, <code>pageIndex</code> value of <code>0</code> would
394      * return the personal profile {@link ProfileDescriptor}, and <code>pageIndex</code> value of
395      * <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>
396      * </ul>
397      */
398     @Nullable
getItem(int pageIndex)399     private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
400         if (!hasPageForIndex(pageIndex)) {
401             return null;
402         }
403         return mItems.get(pageIndex);
404     }
405 
getEmptyStateView(int pageIndex)406     private ViewGroup getEmptyStateView(int pageIndex) {
407         return getItem(pageIndex).getEmptyStateView();
408     }
409 
getActiveEmptyStateView()410     public ViewGroup getActiveEmptyStateView() {
411         return getEmptyStateView(getCurrentPage());
412     }
413 
414     /**
415      * Returns the number of {@link ProfileDescriptor} objects.
416      * <p>For a normal consumer device with only one user returns <code>1</code>.
417      * <p>For a device with a work profile returns <code>2</code>.
418      */
getItemCount()419     public final int getItemCount() {
420         return mItems.size();
421     }
422 
getListViewForIndex(int index)423     public final PageViewT getListViewForIndex(int index) {
424         return getItem(index).getView();
425     }
426 
427     /**
428      * Returns the adapter of the list view for the relevant page specified by
429      * <code>pageIndex</code>.
430      * <p>This method is meant to be implemented with an implementation-specific return type
431      * depending on the adapter type.
432      */
433     @VisibleForTesting
getPageAdapterForIndex(int index)434     public final SinglePageAdapterT getPageAdapterForIndex(int index) {
435         if (!hasPageForIndex(index)) {
436             return null;
437         }
438         return getItem(index).getAdapter();
439     }
440 
441     /**
442      * Performs view-related initialization procedures for the adapter specified
443      * by <code>pageIndex</code>.
444      */
setupListAdapter(int pageIndex)445     public final void setupListAdapter(int pageIndex) {
446         mAdapterBinder.bind(getListViewForIndex(pageIndex), getPageAdapterForIndex(pageIndex));
447     }
448 
449     /**
450      * Returns the {@link ListAdapterT} instance of the profile that is currently visible
451      * to the user.
452      * <p>For example, if the user is viewing the work tab in the share sheet, this method returns
453      * the work profile {@link ListAdapterT}.
454      */
455     @VisibleForTesting
getActiveListAdapter()456     public final ListAdapterT getActiveListAdapter() {
457         return getListAdapterForPageNumber(getCurrentPage());
458     }
459 
getPersonalListAdapter()460     public final ListAdapterT getPersonalListAdapter() {
461         return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_PERSONAL));
462     }
463 
464     @Nullable
getWorkListAdapter()465     public final ListAdapterT getWorkListAdapter() {
466         if (!hasPageForProfile(PROFILE_WORK)) {
467             return null;
468         }
469         return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_WORK));
470     }
471 
getCurrentRootAdapter()472     public final SinglePageAdapterT getCurrentRootAdapter() {
473         return getPageAdapterForIndex(getCurrentPage());
474     }
475 
getActiveAdapterView()476     public final PageViewT getActiveAdapterView() {
477         return getListViewForIndex(getCurrentPage());
478     }
479 
anyAdapterHasItems()480     private boolean anyAdapterHasItems() {
481         for (int i = 0; i < mItems.size(); ++i) {
482             ListAdapterT listAdapter = getListAdapterForPageNumber(i);
483             if (listAdapter.getCount() > 0) {
484                 return true;
485             }
486         }
487         return false;
488     }
489 
refreshPackagesInAllTabs()490     public void refreshPackagesInAllTabs() {
491         // TODO: it's unclear if this legacy logic really requires the active tab to be rebuilt
492         // first, or if we could just iterate over the tabs in arbitrary order.
493         getActiveListAdapter().handlePackagesChanged();
494         forEachInactivePage(page -> getListAdapterForPageNumber(page).handlePackagesChanged());
495     }
496 
497     /**
498      * Notify that there has been a package change which could potentially modify the set of targets
499      * that should be shown in the specified {@code listAdapter}. This <em>may</em> result in
500      * "rebuilding" the target list for that adapter.
501      *
502      * @param listAdapter an adapter that may need to be updated after the package-change event.
503      * @param waitingToEnableWorkProfile whether we've turned on the work profile, but haven't yet
504      * seen an {@code ACTION_USER_UNLOCKED} broadcast. In this case we skip the rebuild of any
505      * work-profile adapter because we wouldn't expect meaningful results -- but another rebuild
506      * will be prompted when we eventually get the broadcast.
507      *
508      * @return whether we're able to proceed with a Sharesheet session after processing this
509      * package-change event. If false, we were able to rebuild the targets but determined that there
510      * aren't any we could present in the UI without the app looking broken, so we should just quit.
511      */
onHandlePackagesChanged( ListAdapterT listAdapter, boolean waitingToEnableWorkProfile)512     public boolean onHandlePackagesChanged(
513             ListAdapterT listAdapter, boolean waitingToEnableWorkProfile) {
514         if (listAdapter == getActiveListAdapter()) {
515             if (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
516                     && waitingToEnableWorkProfile) {
517                 // We have just turned on the work profile and entered the passcode to start it,
518                 // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no
519                 // point in reloading the list now, since the work profile user is still turning on.
520                 return true;
521             }
522 
523             boolean listRebuilt = rebuildActiveTab(true);
524             if (listRebuilt) {
525                 listAdapter.notifyDataSetChanged();
526             }
527 
528             // TODO: shouldn't we check that the inactive tabs are built before declaring that we
529             // have to quit for lack of items?
530             return anyAdapterHasItems();
531         } else {
532             clearInactiveProfileCache();
533             return true;
534         }
535     }
536 
537     /**
538      * Fully-rebuild the active tab and, if specified, partially-rebuild any other inactive tabs.
539      */
rebuildTabs(boolean includePartialRebuildOfInactiveTabs)540     public boolean rebuildTabs(boolean includePartialRebuildOfInactiveTabs) {
541         // TODO: we may be able to determine `includePartialRebuildOfInactiveTabs` ourselves as
542         // a function of our own instance state. OTOH the purpose of this "partial rebuild" is to
543         // be able to evaluate the intermediate state of one particular profile tab (i.e. work
544         // profile) that may not generalize well when we have other "inactive tabs." I.e., either we
545         // rebuild *all* the inactive tabs just to evaluate some auto-launch conditions that only
546         // depend on personal and/or work tabs, or we have to explicitly specify the ones we care
547         // about. It's not the pager-adapter's business to know "which ones we care about," so maybe
548         // they should be rebuilt lazily when-and-if it comes up (e.g. during the evaluation of
549         // autolaunch conditions).
550         boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded();
551         if (includePartialRebuildOfInactiveTabs) {
552             // Per legacy logic, avoid short-circuiting (TODO: why? possibly so that we *start*
553             // loading the inactive tabs even if we're still waiting on the active tab to finish?).
554             boolean completedRebuildingInactiveTabs = rebuildInactiveTabs(false);
555             rebuildCompleted = rebuildCompleted && completedRebuildingInactiveTabs;
556         }
557         return rebuildCompleted;
558     }
559 
560     /**
561      * Rebuilds the tab that is currently visible to the user.
562      * <p>Returns {@code true} if rebuild has completed.
563      */
rebuildActiveTab(boolean doPostProcessing)564     public final boolean rebuildActiveTab(boolean doPostProcessing) {
565         Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab");
566         boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing);
567         Trace.endSection();
568         return result;
569     }
570 
571     /**
572      * Rebuilds any tabs that are not currently visible to the user.
573      * <p>Returns {@code true} if rebuild has completed in all inactive tabs.
574      */
rebuildInactiveTabs(boolean doPostProcessing)575     private boolean rebuildInactiveTabs(boolean doPostProcessing) {
576         Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab");
577         AtomicBoolean allRebuildsComplete = new AtomicBoolean(true);
578         forEachInactivePage(pageNumber -> {
579             // Evaluate the rebuild for every inactive page, even if we've already seen some adapter
580             // return an "incomplete" status (i.e., even if `allRebuildsComplete` is already false)
581             // and so we already know we'll end up returning false for the batch.
582             // TODO: any particular reason the per-page legacy logic was set up in this order, or
583             // could we possibly short-circuit the rebuild if the tab is already "loaded"?
584             ListAdapterT inactiveAdapter = getListAdapterForPageNumber(pageNumber);
585             boolean rebuildInactivePageCompleted =
586                     rebuildTab(inactiveAdapter, doPostProcessing) || inactiveAdapter.isTabLoaded();
587             if (!rebuildInactivePageCompleted) {
588                 allRebuildsComplete.set(false);
589             }
590         });
591         Trace.endSection();
592         return allRebuildsComplete.get();
593     }
594 
forEachPage(Consumer<Integer> pageNumberHandler)595     protected void forEachPage(Consumer<Integer> pageNumberHandler) {
596         for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) {
597             pageNumberHandler.accept(pageNumber);
598         }
599     }
600 
forEachInactivePage(Consumer<Integer> inactivePageNumberHandler)601     protected void forEachInactivePage(Consumer<Integer> inactivePageNumberHandler) {
602         forEachPage(pageNumber -> {
603             if (pageNumber != getCurrentPage()) {
604                 inactivePageNumberHandler.accept(pageNumber);
605             }
606         });
607     }
608 
rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing)609     protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) {
610         if (shouldSkipRebuild(activeListAdapter)) {
611             activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
612             return false;
613         }
614         return activeListAdapter.rebuildList(doPostProcessing);
615     }
616 
shouldSkipRebuild(ListAdapterT activeListAdapter)617     private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) {
618         EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter);
619         return emptyState != null && emptyState.shouldSkipDataRebuild();
620     }
621 
622     /**
623      * The empty state screens are shown according to their priority:
624      * <ol>
625      * <li>(highest priority) cross-profile disabled by policy (handled in
626      * {@link #rebuildTab(ListAdapterT, boolean)})</li>
627      * <li>no apps available</li>
628      * <li>(least priority) work is off</li>
629      * </ol>
630      *
631      * The intention is to prevent the user from having to turn
632      * the work profile on if there will not be any apps resolved
633      * anyway.
634      *
635      * TODO: move this comment to the place where we configure our composite provider.
636      */
showEmptyResolverListEmptyState(ListAdapterT listAdapter)637     public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) {
638         final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
639 
640         if (emptyState == null) {
641             return;
642         }
643 
644         emptyState.onEmptyStateShown();
645 
646         View.OnClickListener clickListener = null;
647 
648         if (emptyState.getButtonClickListener() != null) {
649             clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
650                 ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor =
651                         getDescriptorForUserHandle(listAdapter.getUserHandle());
652                 descriptor.mEmptyStateUi.showSpinner();
653             });
654         }
655 
656         showEmptyState(listAdapter, emptyState, clickListener);
657     }
658 
showEmptyState( ListAdapterT activeListAdapter, EmptyState emptyState, View.OnClickListener buttonOnClick)659     private void showEmptyState(
660             ListAdapterT activeListAdapter,
661             EmptyState emptyState,
662             View.OnClickListener buttonOnClick) {
663         ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor =
664                 getDescriptorForUserHandle(activeListAdapter.getUserHandle());
665         descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick);
666         activeListAdapter.markTabLoaded();
667     }
668 
669     /**
670      * Sets up the padding of the view containing the empty state screens for the current adapter
671      * view.
672      */
setupContainerPadding()673     protected final void setupContainerPadding() {
674         getItem(getCurrentPage()).setupContainerPadding();
675     }
676 
showListView(ListAdapterT activeListAdapter)677     public void showListView(ListAdapterT activeListAdapter) {
678         ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor =
679                 getDescriptorForUserHandle(activeListAdapter.getUserHandle());
680         descriptor.mEmptyStateUi.hide();
681     }
682 
683     /**
684      * @return whether any "inactive" tab's adapter would show an empty-state screen in our current
685      * application state.
686      */
shouldShowEmptyStateScreenInAnyInactiveAdapter()687     public final boolean shouldShowEmptyStateScreenInAnyInactiveAdapter() {
688         AtomicBoolean anyEmpty = new AtomicBoolean(false);
689         // TODO: The "inactive" condition is legacy logic. Could we simplify and ask "any"?
690         forEachInactivePage(pageNumber -> {
691             if (shouldShowEmptyStateScreen(getListAdapterForPageNumber(pageNumber))) {
692                 anyEmpty.set(true);
693             }
694         });
695         return anyEmpty.get();
696     }
697 
shouldShowEmptyStateScreen(ListAdapterT listAdapter)698     public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) {
699         int count = listAdapter.getUnfilteredCount();
700         return (count == 0 && listAdapter.getPlaceholderCount() == 0)
701                 || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
702                     && mWorkProfileQuietModeChecker.get());
703     }
704 
705 }
706