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