From 266b298afcc2b64d1b7227ffbe952d6d93b6e53e Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Wed, 14 Feb 2024 16:19:58 -0500 Subject: Move MPPA and subclasses to subpackage 'profiles' This will let us unpack these files a bit, move some inner classes out and make a home for additional code to improve the patterns here. Pure mechanical refactor. Bug: n/a Test: atest IntentResolver-tests-unit Change-Id: I138313fc0e84b603b6f173a27cb3b96c91ecc194 --- .../android/intentresolver/v2/ChooserActivity.java | 12 +- .../v2/ChooserMultiProfilePagerAdapter.java | 202 ----- .../v2/MultiProfilePagerAdapter.java | 835 --------------------- .../intentresolver/v2/ResolverActivity.java | 11 +- .../v2/ResolverMultiProfilePagerAdapter.java | 112 --- .../intentresolver/v2/profiles/AdapterBinder.java | 31 + .../profiles/ChooserMultiProfilePagerAdapter.java | 202 +++++ .../v2/profiles/MultiProfilePagerAdapter.java | 692 +++++++++++++++++ .../v2/profiles/OnProfileSelectedListener.java | 46 ++ .../profiles/OnSwitchOnWorkSelectedListener.java | 27 + .../v2/profiles/ProfileDescriptor.java | 82 ++ .../profiles/ResolverMultiProfilePagerAdapter.java | 112 +++ .../intentresolver/v2/profiles/TabConfig.java | 38 + 13 files changed, 1245 insertions(+), 1157 deletions(-) delete mode 100644 java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java delete mode 100644 java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java delete mode 100644 java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java create mode 100644 java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java create mode 100644 java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java create mode 100644 java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java create mode 100644 java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/v2/profiles/TabConfig.java (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index cdc05b95..62b79995 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -138,8 +138,6 @@ import com.android.intentresolver.model.AppPredictionServiceResolverComparator; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.shortcuts.AppPredictorFactory; import com.android.intentresolver.shortcuts.ShortcutLoader; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.ProfileType; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.TabConfig; import com.android.intentresolver.v2.data.repository.DevicePolicyResources; import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider; @@ -148,6 +146,12 @@ import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvi import com.android.intentresolver.v2.platform.AppPredictionAvailable; import com.android.intentresolver.v2.platform.ImageEditor; import com.android.intentresolver.v2.platform.NearbyShare; +import com.android.intentresolver.v2.profiles.ChooserMultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.ProfileType; +import com.android.intentresolver.v2.profiles.OnProfileSelectedListener; +import com.android.intentresolver.v2.profiles.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.v2.profiles.TabConfig; import com.android.intentresolver.v2.ui.ActionTitle; import com.android.intentresolver.v2.ui.ShareResultSender; import com.android.intentresolver.v2.ui.ShareResultSenderFactory; @@ -245,7 +249,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements private ResolverActivity.PickTargetOptionRequest mPickOptionRequest; @Nullable - private MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; + private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; ////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// @@ -1169,7 +1173,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements R.layout.resolver_profile_tab_button, com.android.internal.R.id.profile_pager, () -> onProfileTabSelected(viewPager.getCurrentItem()), - new MultiProfilePagerAdapter.OnProfileSelectedListener() { + new OnProfileSelectedListener() { @Override public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {} diff --git a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java deleted file mode 100644 index 42eb077b..00000000 --- a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver.v2; - -import android.content.Context; -import android.os.UserHandle; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager.widget.PagerAdapter; - -import com.android.intentresolver.ChooserListAdapter; -import com.android.intentresolver.ChooserRecyclerViewAccessibilityDelegate; -import com.android.intentresolver.FeatureFlags; -import com.android.intentresolver.R; -import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.intentresolver.grid.ChooserGridAdapter; -import com.android.intentresolver.measurements.Tracer; - -import com.google.common.collect.ImmutableList; - -import java.util.Optional; -import java.util.function.Supplier; - -/** - * A {@link PagerAdapter} which describes the work and personal profile share sheet screens. - */ -public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter< - RecyclerView, ChooserGridAdapter, ChooserListAdapter> { - private static final int SINGLE_CELL_SPAN_SIZE = 1; - - private final ChooserProfileAdapterBinder mAdapterBinder; - private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; - - public ChooserMultiProfilePagerAdapter( - Context context, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - int maxTargetsPerRow, - FeatureFlags featureFlags) { - this( - context, - new ChooserProfileAdapterBinder(maxTargetsPerRow), - tabs, - emptyStateProvider, - workProfileQuietModeChecker, - defaultProfile, - workProfileUserHandle, - cloneProfileUserHandle, - new BottomPaddingOverrideSupplier(context), - featureFlags); - } - - private ChooserMultiProfilePagerAdapter( - Context context, - ChooserProfileAdapterBinder adapterBinder, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier, - FeatureFlags featureFlags) { - super( - gridAdapter -> gridAdapter.getListAdapter(), - adapterBinder, - tabs, - emptyStateProvider, - workProfileQuietModeChecker, - defaultProfile, - workProfileUserHandle, - cloneProfileUserHandle, - () -> makeProfileView(context, featureFlags), - bottomPaddingOverrideSupplier); - mAdapterBinder = adapterBinder; - mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; - } - - public void setMaxTargetsPerRow(int maxTargetsPerRow) { - mAdapterBinder.setMaxTargetsPerRow(maxTargetsPerRow); - } - - public void setEmptyStateBottomOffset(int bottomOffset) { - mBottomPaddingOverrideSupplier.setEmptyStateBottomOffset(bottomOffset); - setupContainerPadding(); - } - - /** - * Notify adapter about the drawer's collapse state. This will affect the app divider's - * visibility. - */ - public void setIsCollapsed(boolean isCollapsed) { - for (int i = 0, size = getItemCount(); i < size; i++) { - getPageAdapterForIndex(i).setAzLabelVisibility(!isCollapsed); - } - } - - private static ViewGroup makeProfileView( - Context context, FeatureFlags featureFlags) { - LayoutInflater inflater = LayoutInflater.from(context); - ViewGroup rootView = featureFlags.scrollablePreview() - ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false) - : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false); - RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list); - recyclerView.setAccessibilityDelegateCompat( - new ChooserRecyclerViewAccessibilityDelegate(recyclerView)); - return rootView; - } - - @Override - public boolean onHandlePackagesChanged( - ChooserListAdapter listAdapter, boolean waitingToEnableWorkProfile) { - // TODO: why do we need to do the extra `notifyDataSetChanged()` in (only) the Chooser case? - getActiveListAdapter().notifyDataSetChanged(); - return super.onHandlePackagesChanged(listAdapter, waitingToEnableWorkProfile); - } - - @Override - protected final boolean rebuildTab(ChooserListAdapter listAdapter, boolean doPostProcessing) { - if (doPostProcessing) { - Tracer.INSTANCE.beginAppTargetLoadingSection(listAdapter.getUserHandle()); - } - return super.rebuildTab(listAdapter, doPostProcessing); - } - - /** Apply the specified {@code height} as the footer in each tab's adapter. */ - public void setFooterHeightInEveryAdapter(int height) { - for (int i = 0; i < getItemCount(); ++i) { - getPageAdapterForIndex(i).setFooterHeight(height); - } - } - - private static class BottomPaddingOverrideSupplier implements Supplier> { - private final Context mContext; - private int mBottomOffset; - - BottomPaddingOverrideSupplier(Context context) { - mContext = context; - } - - public void setEmptyStateBottomOffset(int bottomOffset) { - mBottomOffset = bottomOffset; - } - - @Override - public Optional get() { - int initialBottomPadding = mContext.getResources().getDimensionPixelSize( - R.dimen.resolver_empty_state_container_padding_bottom); - return Optional.of(initialBottomPadding + mBottomOffset); - } - } - - private static class ChooserProfileAdapterBinder implements - AdapterBinder { - private int mMaxTargetsPerRow; - - ChooserProfileAdapterBinder(int maxTargetsPerRow) { - mMaxTargetsPerRow = maxTargetsPerRow; - } - - public void setMaxTargetsPerRow(int maxTargetsPerRow) { - mMaxTargetsPerRow = maxTargetsPerRow; - } - - @Override - public void bind( - RecyclerView recyclerView, ChooserGridAdapter chooserGridAdapter) { - GridLayoutManager glm = (GridLayoutManager) recyclerView.getLayoutManager(); - glm.setSpanCount(mMaxTargetsPerRow); - glm.setSpanSizeLookup( - new GridLayoutManager.SpanSizeLookup() { - @Override - public int getSpanSize(int position) { - return chooserGridAdapter.shouldCellSpan(position) - ? SINGLE_CELL_SPAN_SIZE - : glm.getSpanCount(); - } - }); - } - } -} diff --git a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java deleted file mode 100644 index 79403095..00000000 --- a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java +++ /dev/null @@ -1,835 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.intentresolver.v2; - -import android.annotation.IntDef; -import android.annotation.Nullable; -import android.os.Trace; -import android.os.UserHandle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.TabHost; -import android.widget.TextView; - -import androidx.viewpager.widget.PagerAdapter; -import androidx.viewpager.widget.ViewPager; - -import com.android.intentresolver.ResolverListAdapter; -import com.android.intentresolver.emptystate.EmptyState; -import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.intentresolver.v2.emptystate.EmptyStateUiHelper; -import com.android.internal.annotations.VisibleForTesting; - -import com.google.common.collect.ImmutableList; - -import java.util.HashSet; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet). - *

- * TODO: attempt to further restrict visibility/improve encapsulation in the methods we expose. - *

- * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive" - *

- * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident - * waiting to happen since clients seem to make assumptions about which adapter will be "active" in - * a particular context, and more explicit APIs would make sure those were valid. - *

- * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?) - *

- * TODO: this is part of an in-progress refactor to merge with `GenericMultiProfilePagerAdapter`. - * As originally noted there, we've reduced explicit references to the `ResolverListAdapter` base - * type and may be able to drop the type constraint. - * - * @param the type of the widget that represents the contents of a page in this adapter - * @param the type of a "root" adapter class to be instantiated and included in - * the per-profile records. - * @param the concrete type of a {@link ResolverListAdapter} implementation to - * control the contents of a given per-profile list. This is provided for convenience, since it must - * be possible to get the list adapter from the page adapter via our - * mListAdapterExtractor. - */ -class MultiProfilePagerAdapter< - PageViewT extends ViewGroup, - SinglePageAdapterT, - ListAdapterT extends ResolverListAdapter> extends PagerAdapter { - - /** - * Delegate to set up a given adapter and page view to be used together. - * @param (as in {@link MultiProfilePagerAdapter}). - * @param (as in {@link MultiProfilePagerAdapter}). - */ - public interface AdapterBinder { - /** - * The given {@code view} will be associated with the given {@code adapter}. Do any work - * necessary to configure them compatibly, introduce them to each other, etc. - */ - void bind(PageViewT view, SinglePageAdapterT adapter); - } - - public static final int PROFILE_PERSONAL = 0; - public static final int PROFILE_WORK = 1; - - @IntDef({PROFILE_PERSONAL, PROFILE_WORK}) - public @interface ProfileType {} - - private final Function mListAdapterExtractor; - private final AdapterBinder mAdapterBinder; - private final Supplier mPageViewInflater; - - private final ImmutableList> mItems; - - private final EmptyStateProvider mEmptyStateProvider; - private final UserHandle mWorkProfileUserHandle; - private final UserHandle mCloneProfileUserHandle; - private final Supplier mWorkProfileQuietModeChecker; // True when work is quiet. - - private final Set mLoadedPages; - private int mCurrentPage; - private OnProfileSelectedListener mOnProfileSelectedListener; - - public static class TabConfig { - private final @ProfileType int mProfile; - private final String mTabLabel; - private final String mTabAccessibilityLabel; - private final String mTabTag; - private final PageAdapterT mPageAdapter; - - public TabConfig( - @ProfileType int profile, - String tabLabel, - String tabAccessibilityLabel, - String tabTag, - PageAdapterT pageAdapter) { - mProfile = profile; - mTabLabel = tabLabel; - mTabAccessibilityLabel = tabAccessibilityLabel; - mTabTag = tabTag; - mPageAdapter = pageAdapter; - } - } - - protected MultiProfilePagerAdapter( - Function listAdapterExtractor, - AdapterBinder adapterBinder, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - Supplier pageViewInflater, - Supplier> containerBottomPaddingOverrideSupplier) { - mLoadedPages = new HashSet<>(); - mWorkProfileUserHandle = workProfileUserHandle; - mCloneProfileUserHandle = cloneProfileUserHandle; - mEmptyStateProvider = emptyStateProvider; - mWorkProfileQuietModeChecker = workProfileQuietModeChecker; - - mListAdapterExtractor = listAdapterExtractor; - mAdapterBinder = adapterBinder; - mPageViewInflater = pageViewInflater; - - ImmutableList.Builder> items = - new ImmutableList.Builder<>(); - for (TabConfig tab : tabs) { - // TODO: consider representing tabConfig in a different data structure that can ensure - // uniqueness of their profile assignments (while still respecting the client's - // requested tab order). - items.add( - createProfileDescriptor( - tab.mProfile, - tab.mTabLabel, - tab.mTabAccessibilityLabel, - tab.mTabTag, - tab.mPageAdapter, - containerBottomPaddingOverrideSupplier)); - } - mItems = items.build(); - - mCurrentPage = - hasPageForProfile(defaultProfile) ? getPageNumberForProfile(defaultProfile) : 0; - } - - private ProfileDescriptor createProfileDescriptor( - @ProfileType int profile, - String tabLabel, - String tabAccessibilityLabel, - String tabTag, - SinglePageAdapterT adapter, - Supplier> containerBottomPaddingOverrideSupplier) { - return new ProfileDescriptor<>( - profile, - tabLabel, - tabAccessibilityLabel, - tabTag, - mPageViewInflater.get(), - adapter, - containerBottomPaddingOverrideSupplier); - } - - private boolean hasPageForIndex(int pageIndex) { - return (pageIndex >= 0) && (pageIndex < getCount()); - } - - public final boolean hasPageForProfile(@ProfileType int profile) { - return hasPageForIndex(getPageNumberForProfile(profile)); - } - - private @ProfileType int getProfileForPageNumber(int position) { - if (hasPageForIndex(position)) { - return mItems.get(position).mProfile; - } - return -1; - } - - public int getPageNumberForProfile(@ProfileType int profile) { - for (int i = 0; i < mItems.size(); ++i) { - if (profile == mItems.get(i).mProfile) { - return i; - } - } - return -1; - } - - private ListAdapterT getListAdapterForPageNumber(int pageNumber) { - SinglePageAdapterT pageAdapter = getPageAdapterForIndex(pageNumber); - if (pageAdapter == null) { - return null; - } - return mListAdapterExtractor.apply(pageAdapter); - } - - private @ProfileType int getProfileForUserHandle(UserHandle userHandle) { - if (userHandle.equals(getCloneUserHandle())) { - // TODO: can we push this special case elsewhere -- e.g., when we check against each - // list adapter's user handle in the loop below, could we instead ask the list adapter - // whether it "represents" the queried user handle, and have the personal list adapter - // return true because it knows it's also associated with the clone profile? Or if we - // don't want to make modifications to the list adapter, maybe we could at least specify - // it in our per-page configuration data that we use to build our tabs/pages, and then - // maintain the relevant bookkeeping in our own ProfileDescriptor? - return PROFILE_PERSONAL; - } - for (int i = 0; i < mItems.size(); ++i) { - ListAdapterT listAdapter = getListAdapterForPageNumber(i); - if (listAdapter.getUserHandle().equals(userHandle)) { - return mItems.get(i).mProfile; - } - } - return -1; - } - - private int getPageNumberForUserHandle(UserHandle userHandle) { - return getPageNumberForProfile(getProfileForUserHandle(userHandle)); - } - - /** - * Returns the {@link ListAdapterT} instance of the profile that represents - * userHandle. If there is no such adapter for the specified - * userHandle, returns {@code null}. - *

For example, if there is a work profile on the device with user id 10, calling this method - * with UserHandle.of(10) returns the work profile {@link ListAdapterT}. - */ - @Nullable - public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) { - return getListAdapterForPageNumber(getPageNumberForUserHandle(userHandle)); - } - - @Nullable - private ProfileDescriptor getDescriptorForUserHandle( - UserHandle userHandle) { - return getItem(getPageNumberForUserHandle(userHandle)); - } - - private int getPageNumberForTabTag(String tag) { - for (int i = 0; i < mItems.size(); ++i) { - if (Objects.equals(mItems.get(i).mTabTag, tag)) { - return i; - } - } - return -1; - } - - private void updateActiveTabStyle(TabHost tabHost) { - int currentTab = tabHost.getCurrentTab(); - - for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { - // TODO: can we avoid this downcast by pushing our knowledge of the intended view type - // somewhere else? - TextView tabText = (TextView) tabHost.getTabWidget().getChildAt(pageNumber); - tabText.setSelected(currentTab == pageNumber); - } - } - - public void setupProfileTabs( - LayoutInflater layoutInflater, - TabHost tabHost, - ViewPager viewPager, - int tabButtonLayoutResId, - int tabPageContentViewId, - Runnable onTabChangeListener, - MultiProfilePagerAdapter.OnProfileSelectedListener clientOnProfileSelectedListener) { - tabHost.setup(); - viewPager.setSaveEnabled(false); - - for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { - ProfileDescriptor descriptor = mItems.get(pageNumber); - Button profileButton = (Button) layoutInflater.inflate( - tabButtonLayoutResId, tabHost.getTabWidget(), false); - profileButton.setText(descriptor.mTabLabel); - profileButton.setContentDescription(descriptor.mTabAccessibilityLabel); - - TabHost.TabSpec profileTabSpec = tabHost.newTabSpec(descriptor.mTabTag) - .setContent(tabPageContentViewId) - .setIndicator(profileButton); - tabHost.addTab(profileTabSpec); - } - - tabHost.getTabWidget().setVisibility(View.VISIBLE); - - updateActiveTabStyle(tabHost); - - tabHost.setOnTabChangedListener(tabTag -> { - updateActiveTabStyle(tabHost); - - int pageNumber = getPageNumberForTabTag(tabTag); - if (pageNumber >= 0) { - viewPager.setCurrentItem(pageNumber); - } - onTabChangeListener.run(); - }); - - viewPager.setVisibility(View.VISIBLE); - tabHost.setCurrentTab(getCurrentPage()); - mOnProfileSelectedListener = - new MultiProfilePagerAdapter.OnProfileSelectedListener() { - @Override - public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) { - tabHost.setCurrentTab(pageNumber); - clientOnProfileSelectedListener.onProfilePageSelected( - profileId, pageNumber); - } - - @Override - public void onProfilePageStateChanged(int state) { - clientOnProfileSelectedListener.onProfilePageStateChanged(state); - } - }; - } - - /** - * Sets this instance of this class as {@link ViewPager}'s {@link PagerAdapter} and sets - * an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed - * page and rebuilds the list. - */ - public void setupViewPager(ViewPager viewPager) { - viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { - @Override - public void onPageSelected(int position) { - mCurrentPage = position; - if (!mLoadedPages.contains(position)) { - rebuildActiveTab(true); - mLoadedPages.add(position); - } - if (mOnProfileSelectedListener != null) { - mOnProfileSelectedListener.onProfilePageSelected( - getProfileForPageNumber(position), position); - } - } - - @Override - public void onPageScrollStateChanged(int state) { - if (mOnProfileSelectedListener != null) { - mOnProfileSelectedListener.onProfilePageStateChanged(state); - } - } - }); - viewPager.setAdapter(this); - viewPager.setCurrentItem(mCurrentPage); - mLoadedPages.add(mCurrentPage); - } - - public void clearInactiveProfileCache() { - forEachInactivePage(pageNumber -> mLoadedPages.remove(pageNumber)); - } - - @Override - public final ViewGroup instantiateItem(ViewGroup container, int position) { - setupListAdapter(position); - final ProfileDescriptor descriptor = getItem(position); - container.addView(descriptor.mRootView); - return descriptor.mRootView; - } - - @Override - public void destroyItem(ViewGroup container, int position, Object view) { - container.removeView((View) view); - } - - @Override - public int getCount() { - return getItemCount(); - } - - public int getCurrentPage() { - return mCurrentPage; - } - - public final @ProfileType int getActiveProfile() { - return getProfileForPageNumber(getCurrentPage()); - } - - @VisibleForTesting - public UserHandle getCurrentUserHandle() { - return getActiveListAdapter().getUserHandle(); - } - - @Override - public boolean isViewFromObject(View view, Object object) { - return view == object; - } - - @Override - public CharSequence getPageTitle(int position) { - return null; - } - - public UserHandle getCloneUserHandle() { - return mCloneProfileUserHandle; - } - - /** - * Returns the {@link ProfileDescriptor} relevant to the given pageIndex. - *

    - *
  • For a device with only one user, pageIndex value of - * 0 would return the personal profile {@link ProfileDescriptor}.
  • - *
  • For a device with a work profile, pageIndex value of 0 would - * return the personal profile {@link ProfileDescriptor}, and pageIndex value of - * 1 would return the work profile {@link ProfileDescriptor}.
  • - *
- */ - @Nullable - private ProfileDescriptor getItem(int pageIndex) { - if (!hasPageForIndex(pageIndex)) { - return null; - } - return mItems.get(pageIndex); - } - - private ViewGroup getEmptyStateView(int pageIndex) { - return getItem(pageIndex).getEmptyStateView(); - } - - public ViewGroup getActiveEmptyStateView() { - return getEmptyStateView(getCurrentPage()); - } - - /** - * Returns the number of {@link ProfileDescriptor} objects. - *

For a normal consumer device with only one user returns 1. - *

For a device with a work profile returns 2. - */ - public final int getItemCount() { - return mItems.size(); - } - - public final PageViewT getListViewForIndex(int index) { - return getItem(index).mView; - } - - /** - * Returns the adapter of the list view for the relevant page specified by - * pageIndex. - *

This method is meant to be implemented with an implementation-specific return type - * depending on the adapter type. - */ - @VisibleForTesting - public final SinglePageAdapterT getPageAdapterForIndex(int index) { - if (!hasPageForIndex(index)) { - return null; - } - return getItem(index).mAdapter; - } - - /** - * Performs view-related initialization procedures for the adapter specified - * by pageIndex. - */ - public final void setupListAdapter(int pageIndex) { - mAdapterBinder.bind(getListViewForIndex(pageIndex), getPageAdapterForIndex(pageIndex)); - } - - /** - * Returns the {@link ListAdapterT} instance of the profile that is currently visible - * to the user. - *

For example, if the user is viewing the work tab in the share sheet, this method returns - * the work profile {@link ListAdapterT}. - */ - @VisibleForTesting - public final ListAdapterT getActiveListAdapter() { - return getListAdapterForPageNumber(getCurrentPage()); - } - - public final ListAdapterT getPersonalListAdapter() { - return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_PERSONAL)); - } - - @Nullable - public final ListAdapterT getWorkListAdapter() { - if (!hasPageForProfile(PROFILE_WORK)) { - return null; - } - return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_WORK)); - } - - public final SinglePageAdapterT getCurrentRootAdapter() { - return getPageAdapterForIndex(getCurrentPage()); - } - - public final PageViewT getActiveAdapterView() { - return getListViewForIndex(getCurrentPage()); - } - - private boolean anyAdapterHasItems() { - for (int i = 0; i < mItems.size(); ++i) { - ListAdapterT listAdapter = getListAdapterForPageNumber(i); - if (listAdapter.getCount() > 0) { - return true; - } - } - return false; - } - - public void refreshPackagesInAllTabs() { - // TODO: it's unclear if this legacy logic really requires the active tab to be rebuilt - // first, or if we could just iterate over the tabs in arbitrary order. - getActiveListAdapter().handlePackagesChanged(); - forEachInactivePage(page -> getListAdapterForPageNumber(page).handlePackagesChanged()); - } - - /** - * Notify that there has been a package change which could potentially modify the set of targets - * that should be shown in the specified {@code listAdapter}. This may result in - * "rebuilding" the target list for that adapter. - * - * @param listAdapter an adapter that may need to be updated after the package-change event. - * @param waitingToEnableWorkProfile whether we've turned on the work profile, but haven't yet - * seen an {@code ACTION_USER_UNLOCKED} broadcast. In this case we skip the rebuild of any - * work-profile adapter because we wouldn't expect meaningful results -- but another rebuild - * will be prompted when we eventually get the broadcast. - * - * @return whether we're able to proceed with a Sharesheet session after processing this - * package-change event. If false, we were able to rebuild the targets but determined that there - * aren't any we could present in the UI without the app looking broken, so we should just quit. - */ - public boolean onHandlePackagesChanged( - ListAdapterT listAdapter, boolean waitingToEnableWorkProfile) { - if (listAdapter == getActiveListAdapter()) { - if (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) - && waitingToEnableWorkProfile) { - // We have just turned on the work profile and entered the passcode to start it, - // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no - // point in reloading the list now, since the work profile user is still turning on. - return true; - } - - boolean listRebuilt = rebuildActiveTab(true); - if (listRebuilt) { - listAdapter.notifyDataSetChanged(); - } - - // TODO: shouldn't we check that the inactive tabs are built before declaring that we - // have to quit for lack of items? - return anyAdapterHasItems(); - } else { - clearInactiveProfileCache(); - return true; - } - } - - /** - * Fully-rebuild the active tab and, if specified, partially-rebuild any other inactive tabs. - */ - public boolean rebuildTabs(boolean includePartialRebuildOfInactiveTabs) { - // TODO: we may be able to determine `includePartialRebuildOfInactiveTabs` ourselves as - // a function of our own instance state. OTOH the purpose of this "partial rebuild" is to - // be able to evaluate the intermediate state of one particular profile tab (i.e. work - // profile) that may not generalize well when we have other "inactive tabs." I.e., either we - // rebuild *all* the inactive tabs just to evaluate some auto-launch conditions that only - // depend on personal and/or work tabs, or we have to explicitly specify the ones we care - // about. It's not the pager-adapter's business to know "which ones we care about," so maybe - // they should be rebuilt lazily when-and-if it comes up (e.g. during the evaluation of - // autolaunch conditions). - boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded(); - if (includePartialRebuildOfInactiveTabs) { - // Per legacy logic, avoid short-circuiting (TODO: why? possibly so that we *start* - // loading the inactive tabs even if we're still waiting on the active tab to finish?). - boolean completedRebuildingInactiveTabs = rebuildInactiveTabs(false); - rebuildCompleted = rebuildCompleted && completedRebuildingInactiveTabs; - } - return rebuildCompleted; - } - - /** - * Rebuilds the tab that is currently visible to the user. - *

Returns {@code true} if rebuild has completed. - */ - public final boolean rebuildActiveTab(boolean doPostProcessing) { - Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab"); - boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing); - Trace.endSection(); - return result; - } - - /** - * Rebuilds any tabs that are not currently visible to the user. - *

Returns {@code true} if rebuild has completed in all inactive tabs. - */ - private boolean rebuildInactiveTabs(boolean doPostProcessing) { - Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab"); - AtomicBoolean allRebuildsComplete = new AtomicBoolean(true); - forEachInactivePage(pageNumber -> { - // Evaluate the rebuild for every inactive page, even if we've already seen some adapter - // return an "incomplete" status (i.e., even if `allRebuildsComplete` is already false) - // and so we already know we'll end up returning false for the batch. - // TODO: any particular reason the per-page legacy logic was set up in this order, or - // could we possibly short-circuit the rebuild if the tab is already "loaded"? - ListAdapterT inactiveAdapter = getListAdapterForPageNumber(pageNumber); - boolean rebuildInactivePageCompleted = - rebuildTab(inactiveAdapter, doPostProcessing) || inactiveAdapter.isTabLoaded(); - if (!rebuildInactivePageCompleted) { - allRebuildsComplete.set(false); - } - }); - Trace.endSection(); - return allRebuildsComplete.get(); - } - - protected void forEachPage(Consumer pageNumberHandler) { - for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { - pageNumberHandler.accept(pageNumber); - } - } - - protected void forEachInactivePage(Consumer inactivePageNumberHandler) { - forEachPage(pageNumber -> { - if (pageNumber != getCurrentPage()) { - inactivePageNumberHandler.accept(pageNumber); - } - }); - } - - protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) { - if (shouldSkipRebuild(activeListAdapter)) { - activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); - return false; - } - return activeListAdapter.rebuildList(doPostProcessing); - } - - private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) { - EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter); - return emptyState != null && emptyState.shouldSkipDataRebuild(); - } - - /** - * The empty state screens are shown according to their priority: - *

    - *
  1. (highest priority) cross-profile disabled by policy (handled in - * {@link #rebuildTab(ListAdapterT, boolean)})
  2. - *
  3. no apps available
  4. - *
  5. (least priority) work is off
  6. - *
- * - * The intention is to prevent the user from having to turn - * the work profile on if there will not be any apps resolved - * anyway. - * - * TODO: move this comment to the place where we configure our composite provider. - */ - public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) { - final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter); - - if (emptyState == null) { - return; - } - - emptyState.onEmptyStateShown(); - - View.OnClickListener clickListener = null; - - if (emptyState.getButtonClickListener() != null) { - clickListener = v -> emptyState.getButtonClickListener().onClick(() -> { - ProfileDescriptor descriptor = - getDescriptorForUserHandle(listAdapter.getUserHandle()); - descriptor.mEmptyStateUi.showSpinner(); - }); - } - - showEmptyState(listAdapter, emptyState, clickListener); - } - - /** - * Class to get user id of the current process - */ - public static class MyUserIdProvider { - /** - * @return user id of the current process - */ - public int getMyUserId() { - return UserHandle.myUserId(); - } - } - - private void showEmptyState( - ListAdapterT activeListAdapter, - EmptyState emptyState, - View.OnClickListener buttonOnClick) { - ProfileDescriptor descriptor = - getDescriptorForUserHandle(activeListAdapter.getUserHandle()); - descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick); - activeListAdapter.markTabLoaded(); - } - - /** - * Sets up the padding of the view containing the empty state screens for the current adapter - * view. - */ - protected final void setupContainerPadding() { - getItem(getCurrentPage()).setupContainerPadding(); - } - - public void showListView(ListAdapterT activeListAdapter) { - ProfileDescriptor descriptor = - getDescriptorForUserHandle(activeListAdapter.getUserHandle()); - descriptor.mEmptyStateUi.hide(); - } - - /** - * @return whether any "inactive" tab's adapter would show an empty-state screen in our current - * application state. - */ - public final boolean shouldShowEmptyStateScreenInAnyInactiveAdapter() { - AtomicBoolean anyEmpty = new AtomicBoolean(false); - // TODO: The "inactive" condition is legacy logic. Could we simplify and ask "any"? - forEachInactivePage(pageNumber -> { - if (shouldShowEmptyStateScreen(getListAdapterForPageNumber(pageNumber))) { - anyEmpty.set(true); - } - }); - return anyEmpty.get(); - } - - public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) { - int count = listAdapter.getUnfilteredCount(); - return (count == 0 && listAdapter.getPlaceholderCount() == 0) - || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) - && mWorkProfileQuietModeChecker.get()); - } - - // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager" - // should be the owner of all per-profile data (especially now that the API is generic)? - private static class ProfileDescriptor { - final @ProfileType int mProfile; - final String mTabLabel; - final String mTabAccessibilityLabel; - final String mTabTag; - - final ViewGroup mRootView; - final EmptyStateUiHelper mEmptyStateUi; - - // TODO: post-refactoring, we may not need to retain these ivars directly (since they may - // be encapsulated within the `EmptyStateUiHelper`?). - private final ViewGroup mEmptyStateView; - - private final SinglePageAdapterT mAdapter; - private final PageViewT mView; - - ProfileDescriptor( - @ProfileType int forProfile, - String tabLabel, - String tabAccessibilityLabel, - String tabTag, - ViewGroup rootView, - SinglePageAdapterT adapter, - Supplier> containerBottomPaddingOverrideSupplier) { - mProfile = forProfile; - mTabLabel = tabLabel; - mTabAccessibilityLabel = tabAccessibilityLabel; - mTabTag = tabTag; - mRootView = rootView; - mAdapter = adapter; - mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state); - mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list); - mEmptyStateUi = new EmptyStateUiHelper( - rootView, - com.android.internal.R.id.resolver_list, - containerBottomPaddingOverrideSupplier); - } - - protected ViewGroup getEmptyStateView() { - return mEmptyStateView; - } - - private void setupContainerPadding() { - mEmptyStateUi.setupContainerPadding(); - } - } - - /** Listener interface for changes between the per-profile UI tabs. */ - public interface OnProfileSelectedListener { - /** - * Callback for when the user changes the active tab. - *

This callback is only called when the intent resolver or share sheet shows - * more than one profile. - * @param profileId the ID of the newly-selected profile, e.g. {@link #PROFILE_PERSONAL} - * if the personal profile tab was selected or {@link #PROFILE_WORK} if the work profile tab - * was selected. - */ - void onProfilePageSelected(@ProfileType int profileId, int pageNumber); - - - /** - * Callback for when the scroll state changes. Useful for discovering when the user begins - * dragging, when the pager is automatically settling to the current page, or when it is - * fully stopped/idle. - * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING} - * or {@link ViewPager#SCROLL_STATE_SETTLING} - * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged - */ - void onProfilePageStateChanged(int state); - } - - /** - * Listener for when the user switches on the work profile from the work tab. - */ - public interface OnSwitchOnWorkSelectedListener { - /** - * Callback for when the user switches on the work profile from the work tab. - */ - void onSwitchOnWorkSelected(); - } -} diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java index 77d1dbf5..2d26932f 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java @@ -100,15 +100,18 @@ import com.android.intentresolver.emptystate.EmptyStateProvider; import com.android.intentresolver.icons.DefaultTargetDataLoader; import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.ProfileType; -import com.android.intentresolver.v2.MultiProfilePagerAdapter.TabConfig; import com.android.intentresolver.v2.data.repository.DevicePolicyResources; import com.android.intentresolver.v2.domain.model.Profile; import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvider; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter; +import com.android.intentresolver.v2.profiles.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.ProfileType; +import com.android.intentresolver.v2.profiles.OnProfileSelectedListener; +import com.android.intentresolver.v2.profiles.TabConfig; +import com.android.intentresolver.v2.profiles.ResolverMultiProfilePagerAdapter; import com.android.intentresolver.v2.ui.ActionTitle; import com.android.intentresolver.v2.ui.model.ActivityLaunch; import com.android.intentresolver.v2.ui.model.ResolverRequest; @@ -1865,7 +1868,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements R.layout.resolver_profile_tab_button, com.android.internal.R.id.profile_pager, () -> onProfileTabSelected(viewPager.getCurrentItem()), - new MultiProfilePagerAdapter.OnProfileSelectedListener() { + new OnProfileSelectedListener() { @Override public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) { resetButtonBar(); diff --git a/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java deleted file mode 100644 index c2e1ae07..00000000 --- a/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver.v2; - -import android.content.Context; -import android.os.UserHandle; -import android.view.LayoutInflater; -import android.view.ViewGroup; -import android.widget.ListView; - -import androidx.viewpager.widget.PagerAdapter; - -import com.android.intentresolver.R; -import com.android.intentresolver.ResolverListAdapter; -import com.android.intentresolver.emptystate.EmptyStateProvider; - -import com.google.common.collect.ImmutableList; - -import java.util.Optional; -import java.util.function.Supplier; - -/** - * A {@link PagerAdapter} which describes the work and personal profile intent resolver screens. - */ -public class ResolverMultiProfilePagerAdapter extends - MultiProfilePagerAdapter { - private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; - - public ResolverMultiProfilePagerAdapter(Context context, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle) { - this( - context, - tabs, - emptyStateProvider, - workProfileQuietModeChecker, - defaultProfile, - workProfileUserHandle, - cloneProfileUserHandle, - new BottomPaddingOverrideSupplier()); - } - - private ResolverMultiProfilePagerAdapter( - Context context, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { - super( - listAdapter -> listAdapter, - (listView, bindAdapter) -> listView.setAdapter(bindAdapter), - tabs, - emptyStateProvider, - workProfileQuietModeChecker, - defaultProfile, - workProfileUserHandle, - cloneProfileUserHandle, - () -> (ViewGroup) LayoutInflater.from(context).inflate( - R.layout.resolver_list_per_profile, null, false), - bottomPaddingOverrideSupplier); - mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; - } - - public void setUseLayoutWithDefault(boolean useLayoutWithDefault) { - mBottomPaddingOverrideSupplier.setUseLayoutWithDefault(useLayoutWithDefault); - } - - /** Un-check any item(s) that may be checked in any of our inactive adapter(s). */ - public void clearCheckedItemsInInactiveProfiles() { - // TODO: The "inactive" condition is legacy logic. Could we simplify and clear-all? - forEachInactivePage(pageNumber -> { - ListView inactiveListView = getListViewForIndex(pageNumber); - if (inactiveListView.getCheckedItemCount() > 0) { - inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false); - } - }); - } - - private static class BottomPaddingOverrideSupplier implements Supplier> { - private boolean mUseLayoutWithDefault; - - public void setUseLayoutWithDefault(boolean useLayoutWithDefault) { - mUseLayoutWithDefault = useLayoutWithDefault; - } - - @Override - public Optional get() { - return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0); - } - } -} diff --git a/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java b/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java new file mode 100644 index 00000000..c5b35273 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +/** + * Delegate to set up a given adapter and page view to be used together. + * + * @param (as in {@link MultiProfilePagerAdapter}). + * @param (as in {@link MultiProfilePagerAdapter}). + */ +public interface AdapterBinder { + /** + * The given {@code view} will be associated with the given {@code adapter}. Do any work + * necessary to configure them compatibly, introduce them to each other, etc. + */ + void bind(PageViewT view, SinglePageAdapterT adapter); +} diff --git a/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java new file mode 100644 index 00000000..0ee9d141 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +import android.content.Context; +import android.os.UserHandle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager.widget.PagerAdapter; + +import com.android.intentresolver.ChooserListAdapter; +import com.android.intentresolver.ChooserRecyclerViewAccessibilityDelegate; +import com.android.intentresolver.FeatureFlags; +import com.android.intentresolver.R; +import com.android.intentresolver.emptystate.EmptyStateProvider; +import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.measurements.Tracer; + +import com.google.common.collect.ImmutableList; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * A {@link PagerAdapter} which describes the work and personal profile share sheet screens. + */ +public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter< + RecyclerView, ChooserGridAdapter, ChooserListAdapter> { + private static final int SINGLE_CELL_SPAN_SIZE = 1; + + private final ChooserProfileAdapterBinder mAdapterBinder; + private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; + + public ChooserMultiProfilePagerAdapter( + Context context, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, + int maxTargetsPerRow, + FeatureFlags featureFlags) { + this( + context, + new ChooserProfileAdapterBinder(maxTargetsPerRow), + tabs, + emptyStateProvider, + workProfileQuietModeChecker, + defaultProfile, + workProfileUserHandle, + cloneProfileUserHandle, + new BottomPaddingOverrideSupplier(context), + featureFlags); + } + + private ChooserMultiProfilePagerAdapter( + Context context, + ChooserProfileAdapterBinder adapterBinder, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, + BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier, + FeatureFlags featureFlags) { + super( + gridAdapter -> gridAdapter.getListAdapter(), + adapterBinder, + tabs, + emptyStateProvider, + workProfileQuietModeChecker, + defaultProfile, + workProfileUserHandle, + cloneProfileUserHandle, + () -> makeProfileView(context, featureFlags), + bottomPaddingOverrideSupplier); + mAdapterBinder = adapterBinder; + mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; + } + + public void setMaxTargetsPerRow(int maxTargetsPerRow) { + mAdapterBinder.setMaxTargetsPerRow(maxTargetsPerRow); + } + + public void setEmptyStateBottomOffset(int bottomOffset) { + mBottomPaddingOverrideSupplier.setEmptyStateBottomOffset(bottomOffset); + setupContainerPadding(); + } + + /** + * Notify adapter about the drawer's collapse state. This will affect the app divider's + * visibility. + */ + public void setIsCollapsed(boolean isCollapsed) { + for (int i = 0, size = getItemCount(); i < size; i++) { + getPageAdapterForIndex(i).setAzLabelVisibility(!isCollapsed); + } + } + + private static ViewGroup makeProfileView( + Context context, FeatureFlags featureFlags) { + LayoutInflater inflater = LayoutInflater.from(context); + ViewGroup rootView = featureFlags.scrollablePreview() + ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false) + : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false); + RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list); + recyclerView.setAccessibilityDelegateCompat( + new ChooserRecyclerViewAccessibilityDelegate(recyclerView)); + return rootView; + } + + @Override + public boolean onHandlePackagesChanged( + ChooserListAdapter listAdapter, boolean waitingToEnableWorkProfile) { + // TODO: why do we need to do the extra `notifyDataSetChanged()` in (only) the Chooser case? + getActiveListAdapter().notifyDataSetChanged(); + return super.onHandlePackagesChanged(listAdapter, waitingToEnableWorkProfile); + } + + @Override + protected final boolean rebuildTab(ChooserListAdapter listAdapter, boolean doPostProcessing) { + if (doPostProcessing) { + Tracer.INSTANCE.beginAppTargetLoadingSection(listAdapter.getUserHandle()); + } + return super.rebuildTab(listAdapter, doPostProcessing); + } + + /** Apply the specified {@code height} as the footer in each tab's adapter. */ + public void setFooterHeightInEveryAdapter(int height) { + for (int i = 0; i < getItemCount(); ++i) { + getPageAdapterForIndex(i).setFooterHeight(height); + } + } + + private static class BottomPaddingOverrideSupplier implements Supplier> { + private final Context mContext; + private int mBottomOffset; + + BottomPaddingOverrideSupplier(Context context) { + mContext = context; + } + + public void setEmptyStateBottomOffset(int bottomOffset) { + mBottomOffset = bottomOffset; + } + + @Override + public Optional get() { + int initialBottomPadding = mContext.getResources().getDimensionPixelSize( + R.dimen.resolver_empty_state_container_padding_bottom); + return Optional.of(initialBottomPadding + mBottomOffset); + } + } + + private static class ChooserProfileAdapterBinder implements + AdapterBinder { + private int mMaxTargetsPerRow; + + ChooserProfileAdapterBinder(int maxTargetsPerRow) { + mMaxTargetsPerRow = maxTargetsPerRow; + } + + public void setMaxTargetsPerRow(int maxTargetsPerRow) { + mMaxTargetsPerRow = maxTargetsPerRow; + } + + @Override + public void bind( + RecyclerView recyclerView, ChooserGridAdapter chooserGridAdapter) { + GridLayoutManager glm = (GridLayoutManager) recyclerView.getLayoutManager(); + glm.setSpanCount(mMaxTargetsPerRow); + glm.setSpanSizeLookup( + new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + return chooserGridAdapter.shouldCellSpan(position) + ? SINGLE_CELL_SPAN_SIZE + : glm.getSpanCount(); + } + }); + } + } +} diff --git a/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java new file mode 100644 index 00000000..43785db3 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java @@ -0,0 +1,692 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.profiles; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.os.Trace; +import android.os.UserHandle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TabHost; +import android.widget.TextView; + +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.emptystate.EmptyState; +import com.android.intentresolver.emptystate.EmptyStateProvider; +import com.android.internal.annotations.VisibleForTesting; + +import com.google.common.collect.ImmutableList; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet). + * + * @param the type of the widget that represents the contents of a page in this adapter + * @param the type of a "root" adapter class to be instantiated and included in + * the per-profile records. + * @param the concrete type of a {@link ResolverListAdapter} implementation to + * control the contents of a given per-profile list. This is provided for convenience, since it must + * be possible to get the list adapter from the page adapter via our + * mListAdapterExtractor. + */ +public class MultiProfilePagerAdapter< + PageViewT extends ViewGroup, + SinglePageAdapterT, + ListAdapterT extends ResolverListAdapter> extends PagerAdapter { + + public static final int PROFILE_PERSONAL = 0; + public static final int PROFILE_WORK = 1; + + @IntDef({PROFILE_PERSONAL, PROFILE_WORK}) + public @interface ProfileType {} + + private final Function mListAdapterExtractor; + private final AdapterBinder mAdapterBinder; + private final Supplier mPageViewInflater; + + private final ImmutableList> mItems; + + private final EmptyStateProvider mEmptyStateProvider; + private final UserHandle mWorkProfileUserHandle; + private final UserHandle mCloneProfileUserHandle; + private final Supplier mWorkProfileQuietModeChecker; // True when work is quiet. + + private final Set mLoadedPages; + private int mCurrentPage; + private OnProfileSelectedListener mOnProfileSelectedListener; + + protected MultiProfilePagerAdapter( + Function listAdapterExtractor, + AdapterBinder adapterBinder, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, + Supplier pageViewInflater, + Supplier> containerBottomPaddingOverrideSupplier) { + mLoadedPages = new HashSet<>(); + mWorkProfileUserHandle = workProfileUserHandle; + mCloneProfileUserHandle = cloneProfileUserHandle; + mEmptyStateProvider = emptyStateProvider; + mWorkProfileQuietModeChecker = workProfileQuietModeChecker; + + mListAdapterExtractor = listAdapterExtractor; + mAdapterBinder = adapterBinder; + mPageViewInflater = pageViewInflater; + + ImmutableList.Builder> items = + new ImmutableList.Builder<>(); + for (TabConfig tab : tabs) { + // TODO: consider representing tabConfig in a different data structure that can ensure + // uniqueness of their profile assignments (while still respecting the client's + // requested tab order). + items.add( + createProfileDescriptor( + tab.mProfile, + tab.mTabLabel, + tab.mTabAccessibilityLabel, + tab.mTabTag, + tab.mPageAdapter, + containerBottomPaddingOverrideSupplier)); + } + mItems = items.build(); + + mCurrentPage = + hasPageForProfile(defaultProfile) ? getPageNumberForProfile(defaultProfile) : 0; + } + + private ProfileDescriptor createProfileDescriptor( + @ProfileType int profile, + String tabLabel, + String tabAccessibilityLabel, + String tabTag, + SinglePageAdapterT adapter, + Supplier> containerBottomPaddingOverrideSupplier) { + return new ProfileDescriptor<>( + profile, + tabLabel, + tabAccessibilityLabel, + tabTag, + mPageViewInflater.get(), + adapter, + containerBottomPaddingOverrideSupplier); + } + + private boolean hasPageForIndex(int pageIndex) { + return (pageIndex >= 0) && (pageIndex < getCount()); + } + + public final boolean hasPageForProfile(@ProfileType int profile) { + return hasPageForIndex(getPageNumberForProfile(profile)); + } + + private @ProfileType int getProfileForPageNumber(int position) { + if (hasPageForIndex(position)) { + return mItems.get(position).mProfile; + } + return -1; + } + + public int getPageNumberForProfile(@ProfileType int profile) { + for (int i = 0; i < mItems.size(); ++i) { + if (profile == mItems.get(i).mProfile) { + return i; + } + } + return -1; + } + + private ListAdapterT getListAdapterForPageNumber(int pageNumber) { + SinglePageAdapterT pageAdapter = getPageAdapterForIndex(pageNumber); + if (pageAdapter == null) { + return null; + } + return mListAdapterExtractor.apply(pageAdapter); + } + + private @ProfileType int getProfileForUserHandle(UserHandle userHandle) { + if (userHandle.equals(getCloneUserHandle())) { + // TODO: can we push this special case elsewhere -- e.g., when we check against each + // list adapter's user handle in the loop below, could we instead ask the list adapter + // whether it "represents" the queried user handle, and have the personal list adapter + // return true because it knows it's also associated with the clone profile? Or if we + // don't want to make modifications to the list adapter, maybe we could at least specify + // it in our per-page configuration data that we use to build our tabs/pages, and then + // maintain the relevant bookkeeping in our own ProfileDescriptor? + return PROFILE_PERSONAL; + } + for (int i = 0; i < mItems.size(); ++i) { + ListAdapterT listAdapter = getListAdapterForPageNumber(i); + if (listAdapter.getUserHandle().equals(userHandle)) { + return mItems.get(i).mProfile; + } + } + return -1; + } + + private int getPageNumberForUserHandle(UserHandle userHandle) { + return getPageNumberForProfile(getProfileForUserHandle(userHandle)); + } + + /** + * Returns the {@link ListAdapterT} instance of the profile that represents + * userHandle. If there is no such adapter for the specified + * userHandle, returns {@code null}. + *

For example, if there is a work profile on the device with user id 10, calling this method + * with UserHandle.of(10) returns the work profile {@link ListAdapterT}. + */ + @Nullable + public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) { + return getListAdapterForPageNumber(getPageNumberForUserHandle(userHandle)); + } + + @Nullable + private ProfileDescriptor getDescriptorForUserHandle( + UserHandle userHandle) { + return getItem(getPageNumberForUserHandle(userHandle)); + } + + private int getPageNumberForTabTag(String tag) { + for (int i = 0; i < mItems.size(); ++i) { + if (Objects.equals(mItems.get(i).mTabTag, tag)) { + return i; + } + } + return -1; + } + + private void updateActiveTabStyle(TabHost tabHost) { + int currentTab = tabHost.getCurrentTab(); + + for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { + // TODO: can we avoid this downcast by pushing our knowledge of the intended view type + // somewhere else? + TextView tabText = (TextView) tabHost.getTabWidget().getChildAt(pageNumber); + tabText.setSelected(currentTab == pageNumber); + } + } + + public void setupProfileTabs( + LayoutInflater layoutInflater, + TabHost tabHost, + ViewPager viewPager, + int tabButtonLayoutResId, + int tabPageContentViewId, + Runnable onTabChangeListener, + OnProfileSelectedListener clientOnProfileSelectedListener) { + tabHost.setup(); + viewPager.setSaveEnabled(false); + + for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { + ProfileDescriptor descriptor = mItems.get(pageNumber); + Button profileButton = (Button) layoutInflater.inflate( + tabButtonLayoutResId, tabHost.getTabWidget(), false); + profileButton.setText(descriptor.mTabLabel); + profileButton.setContentDescription(descriptor.mTabAccessibilityLabel); + + TabHost.TabSpec profileTabSpec = tabHost.newTabSpec(descriptor.mTabTag) + .setContent(tabPageContentViewId) + .setIndicator(profileButton); + tabHost.addTab(profileTabSpec); + } + + tabHost.getTabWidget().setVisibility(View.VISIBLE); + + updateActiveTabStyle(tabHost); + + tabHost.setOnTabChangedListener(tabTag -> { + updateActiveTabStyle(tabHost); + + int pageNumber = getPageNumberForTabTag(tabTag); + if (pageNumber >= 0) { + viewPager.setCurrentItem(pageNumber); + } + onTabChangeListener.run(); + }); + + viewPager.setVisibility(View.VISIBLE); + tabHost.setCurrentTab(getCurrentPage()); + mOnProfileSelectedListener = + new OnProfileSelectedListener() { + @Override + public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) { + tabHost.setCurrentTab(pageNumber); + clientOnProfileSelectedListener.onProfilePageSelected( + profileId, pageNumber); + } + + @Override + public void onProfilePageStateChanged(int state) { + clientOnProfileSelectedListener.onProfilePageStateChanged(state); + } + }; + } + + /** + * Sets this instance of this class as {@link ViewPager}'s {@link PagerAdapter} and sets + * an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed + * page and rebuilds the list. + */ + public void setupViewPager(ViewPager viewPager) { + viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + mCurrentPage = position; + if (!mLoadedPages.contains(position)) { + rebuildActiveTab(true); + mLoadedPages.add(position); + } + if (mOnProfileSelectedListener != null) { + mOnProfileSelectedListener.onProfilePageSelected( + getProfileForPageNumber(position), position); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + if (mOnProfileSelectedListener != null) { + mOnProfileSelectedListener.onProfilePageStateChanged(state); + } + } + }); + viewPager.setAdapter(this); + viewPager.setCurrentItem(mCurrentPage); + mLoadedPages.add(mCurrentPage); + } + + public void clearInactiveProfileCache() { + forEachInactivePage(pageNumber -> mLoadedPages.remove(pageNumber)); + } + + @Override + public final ViewGroup instantiateItem(ViewGroup container, int position) { + setupListAdapter(position); + final ProfileDescriptor descriptor = getItem(position); + container.addView(descriptor.mRootView); + return descriptor.mRootView; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object view) { + container.removeView((View) view); + } + + @Override + public int getCount() { + return getItemCount(); + } + + public int getCurrentPage() { + return mCurrentPage; + } + + public final @ProfileType int getActiveProfile() { + return getProfileForPageNumber(getCurrentPage()); + } + + @VisibleForTesting + public UserHandle getCurrentUserHandle() { + return getActiveListAdapter().getUserHandle(); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public CharSequence getPageTitle(int position) { + return null; + } + + public UserHandle getCloneUserHandle() { + return mCloneProfileUserHandle; + } + + /** + * Returns the {@link ProfileDescriptor} relevant to the given pageIndex. + *

    + *
  • For a device with only one user, pageIndex value of + * 0 would return the personal profile {@link ProfileDescriptor}.
  • + *
  • For a device with a work profile, pageIndex value of 0 would + * return the personal profile {@link ProfileDescriptor}, and pageIndex value of + * 1 would return the work profile {@link ProfileDescriptor}.
  • + *
+ */ + @Nullable + private ProfileDescriptor getItem(int pageIndex) { + if (!hasPageForIndex(pageIndex)) { + return null; + } + return mItems.get(pageIndex); + } + + private ViewGroup getEmptyStateView(int pageIndex) { + return getItem(pageIndex).getEmptyStateView(); + } + + public ViewGroup getActiveEmptyStateView() { + return getEmptyStateView(getCurrentPage()); + } + + /** + * Returns the number of {@link ProfileDescriptor} objects. + *

For a normal consumer device with only one user returns 1. + *

For a device with a work profile returns 2. + */ + public final int getItemCount() { + return mItems.size(); + } + + public final PageViewT getListViewForIndex(int index) { + return getItem(index).getView(); + } + + /** + * Returns the adapter of the list view for the relevant page specified by + * pageIndex. + *

This method is meant to be implemented with an implementation-specific return type + * depending on the adapter type. + */ + @VisibleForTesting + public final SinglePageAdapterT getPageAdapterForIndex(int index) { + if (!hasPageForIndex(index)) { + return null; + } + return getItem(index).getAdapter(); + } + + /** + * Performs view-related initialization procedures for the adapter specified + * by pageIndex. + */ + public final void setupListAdapter(int pageIndex) { + mAdapterBinder.bind(getListViewForIndex(pageIndex), getPageAdapterForIndex(pageIndex)); + } + + /** + * Returns the {@link ListAdapterT} instance of the profile that is currently visible + * to the user. + *

For example, if the user is viewing the work tab in the share sheet, this method returns + * the work profile {@link ListAdapterT}. + */ + @VisibleForTesting + public final ListAdapterT getActiveListAdapter() { + return getListAdapterForPageNumber(getCurrentPage()); + } + + public final ListAdapterT getPersonalListAdapter() { + return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_PERSONAL)); + } + + @Nullable + public final ListAdapterT getWorkListAdapter() { + if (!hasPageForProfile(PROFILE_WORK)) { + return null; + } + return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_WORK)); + } + + public final SinglePageAdapterT getCurrentRootAdapter() { + return getPageAdapterForIndex(getCurrentPage()); + } + + public final PageViewT getActiveAdapterView() { + return getListViewForIndex(getCurrentPage()); + } + + private boolean anyAdapterHasItems() { + for (int i = 0; i < mItems.size(); ++i) { + ListAdapterT listAdapter = getListAdapterForPageNumber(i); + if (listAdapter.getCount() > 0) { + return true; + } + } + return false; + } + + public void refreshPackagesInAllTabs() { + // TODO: it's unclear if this legacy logic really requires the active tab to be rebuilt + // first, or if we could just iterate over the tabs in arbitrary order. + getActiveListAdapter().handlePackagesChanged(); + forEachInactivePage(page -> getListAdapterForPageNumber(page).handlePackagesChanged()); + } + + /** + * Notify that there has been a package change which could potentially modify the set of targets + * that should be shown in the specified {@code listAdapter}. This may result in + * "rebuilding" the target list for that adapter. + * + * @param listAdapter an adapter that may need to be updated after the package-change event. + * @param waitingToEnableWorkProfile whether we've turned on the work profile, but haven't yet + * seen an {@code ACTION_USER_UNLOCKED} broadcast. In this case we skip the rebuild of any + * work-profile adapter because we wouldn't expect meaningful results -- but another rebuild + * will be prompted when we eventually get the broadcast. + * + * @return whether we're able to proceed with a Sharesheet session after processing this + * package-change event. If false, we were able to rebuild the targets but determined that there + * aren't any we could present in the UI without the app looking broken, so we should just quit. + */ + public boolean onHandlePackagesChanged( + ListAdapterT listAdapter, boolean waitingToEnableWorkProfile) { + if (listAdapter == getActiveListAdapter()) { + if (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) + && waitingToEnableWorkProfile) { + // We have just turned on the work profile and entered the passcode to start it, + // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no + // point in reloading the list now, since the work profile user is still turning on. + return true; + } + + boolean listRebuilt = rebuildActiveTab(true); + if (listRebuilt) { + listAdapter.notifyDataSetChanged(); + } + + // TODO: shouldn't we check that the inactive tabs are built before declaring that we + // have to quit for lack of items? + return anyAdapterHasItems(); + } else { + clearInactiveProfileCache(); + return true; + } + } + + /** + * Fully-rebuild the active tab and, if specified, partially-rebuild any other inactive tabs. + */ + public boolean rebuildTabs(boolean includePartialRebuildOfInactiveTabs) { + // TODO: we may be able to determine `includePartialRebuildOfInactiveTabs` ourselves as + // a function of our own instance state. OTOH the purpose of this "partial rebuild" is to + // be able to evaluate the intermediate state of one particular profile tab (i.e. work + // profile) that may not generalize well when we have other "inactive tabs." I.e., either we + // rebuild *all* the inactive tabs just to evaluate some auto-launch conditions that only + // depend on personal and/or work tabs, or we have to explicitly specify the ones we care + // about. It's not the pager-adapter's business to know "which ones we care about," so maybe + // they should be rebuilt lazily when-and-if it comes up (e.g. during the evaluation of + // autolaunch conditions). + boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded(); + if (includePartialRebuildOfInactiveTabs) { + // Per legacy logic, avoid short-circuiting (TODO: why? possibly so that we *start* + // loading the inactive tabs even if we're still waiting on the active tab to finish?). + boolean completedRebuildingInactiveTabs = rebuildInactiveTabs(false); + rebuildCompleted = rebuildCompleted && completedRebuildingInactiveTabs; + } + return rebuildCompleted; + } + + /** + * Rebuilds the tab that is currently visible to the user. + *

Returns {@code true} if rebuild has completed. + */ + public final boolean rebuildActiveTab(boolean doPostProcessing) { + Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab"); + boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing); + Trace.endSection(); + return result; + } + + /** + * Rebuilds any tabs that are not currently visible to the user. + *

Returns {@code true} if rebuild has completed in all inactive tabs. + */ + private boolean rebuildInactiveTabs(boolean doPostProcessing) { + Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab"); + AtomicBoolean allRebuildsComplete = new AtomicBoolean(true); + forEachInactivePage(pageNumber -> { + // Evaluate the rebuild for every inactive page, even if we've already seen some adapter + // return an "incomplete" status (i.e., even if `allRebuildsComplete` is already false) + // and so we already know we'll end up returning false for the batch. + // TODO: any particular reason the per-page legacy logic was set up in this order, or + // could we possibly short-circuit the rebuild if the tab is already "loaded"? + ListAdapterT inactiveAdapter = getListAdapterForPageNumber(pageNumber); + boolean rebuildInactivePageCompleted = + rebuildTab(inactiveAdapter, doPostProcessing) || inactiveAdapter.isTabLoaded(); + if (!rebuildInactivePageCompleted) { + allRebuildsComplete.set(false); + } + }); + Trace.endSection(); + return allRebuildsComplete.get(); + } + + protected void forEachPage(Consumer pageNumberHandler) { + for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { + pageNumberHandler.accept(pageNumber); + } + } + + protected void forEachInactivePage(Consumer inactivePageNumberHandler) { + forEachPage(pageNumber -> { + if (pageNumber != getCurrentPage()) { + inactivePageNumberHandler.accept(pageNumber); + } + }); + } + + protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) { + if (shouldSkipRebuild(activeListAdapter)) { + activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); + return false; + } + return activeListAdapter.rebuildList(doPostProcessing); + } + + private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) { + EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter); + return emptyState != null && emptyState.shouldSkipDataRebuild(); + } + + /** + * The empty state screens are shown according to their priority: + *

    + *
  1. (highest priority) cross-profile disabled by policy (handled in + * {@link #rebuildTab(ListAdapterT, boolean)})
  2. + *
  3. no apps available
  4. + *
  5. (least priority) work is off
  6. + *
+ * + * The intention is to prevent the user from having to turn + * the work profile on if there will not be any apps resolved + * anyway. + * + * TODO: move this comment to the place where we configure our composite provider. + */ + public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) { + final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter); + + if (emptyState == null) { + return; + } + + emptyState.onEmptyStateShown(); + + View.OnClickListener clickListener = null; + + if (emptyState.getButtonClickListener() != null) { + clickListener = v -> emptyState.getButtonClickListener().onClick(() -> { + ProfileDescriptor descriptor = + getDescriptorForUserHandle(listAdapter.getUserHandle()); + descriptor.mEmptyStateUi.showSpinner(); + }); + } + + showEmptyState(listAdapter, emptyState, clickListener); + } + + private void showEmptyState( + ListAdapterT activeListAdapter, + EmptyState emptyState, + View.OnClickListener buttonOnClick) { + ProfileDescriptor descriptor = + getDescriptorForUserHandle(activeListAdapter.getUserHandle()); + descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick); + activeListAdapter.markTabLoaded(); + } + + /** + * Sets up the padding of the view containing the empty state screens for the current adapter + * view. + */ + protected final void setupContainerPadding() { + getItem(getCurrentPage()).setupContainerPadding(); + } + + public void showListView(ListAdapterT activeListAdapter) { + ProfileDescriptor descriptor = + getDescriptorForUserHandle(activeListAdapter.getUserHandle()); + descriptor.mEmptyStateUi.hide(); + } + + /** + * @return whether any "inactive" tab's adapter would show an empty-state screen in our current + * application state. + */ + public final boolean shouldShowEmptyStateScreenInAnyInactiveAdapter() { + AtomicBoolean anyEmpty = new AtomicBoolean(false); + // TODO: The "inactive" condition is legacy logic. Could we simplify and ask "any"? + forEachInactivePage(pageNumber -> { + if (shouldShowEmptyStateScreen(getListAdapterForPageNumber(pageNumber))) { + anyEmpty.set(true); + } + }); + return anyEmpty.get(); + } + + public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) { + int count = listAdapter.getUnfilteredCount(); + return (count == 0 && listAdapter.getPlaceholderCount() == 0) + || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) + && mWorkProfileQuietModeChecker.get()); + } + +} diff --git a/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java b/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java new file mode 100644 index 00000000..7bdbec4c --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +import androidx.viewpager.widget.ViewPager; + +/** Listener interface for changes between the per-profile UI tabs. */ +public interface OnProfileSelectedListener { + /** + * Callback for when the user changes the active tab. + *

This callback is only called when the intent resolver or share sheet shows + * more than one profile. + * + * @param profileId the ID of the newly-selected profile, e.g. {@link #PROFILE_PERSONAL} + * if the personal profile tab was selected or {@link #PROFILE_WORK} if the + * work profile tab + * was selected. + */ + void onProfilePageSelected(@MultiProfilePagerAdapter.ProfileType int profileId, int pageNumber); + + + /** + * Callback for when the scroll state changes. Useful for discovering when the user begins + * dragging, when the pager is automatically settling to the current page, or when it is + * fully stopped/idle. + * + * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING} + * or {@link ViewPager#SCROLL_STATE_SETTLING} + * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged + */ + void onProfilePageStateChanged(int state); +} diff --git a/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java b/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java new file mode 100644 index 00000000..3dbbd4d0 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +/** + * Listener for when the user switches on the work profile from the work tab. + */ +public interface OnSwitchOnWorkSelectedListener { + /** + * Callback for when the user switches on the work profile from the work tab. + */ + void onSwitchOnWorkSelected(); +} diff --git a/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java b/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java new file mode 100644 index 00000000..e2e9c19d --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +import android.view.ViewGroup; + +import com.android.intentresolver.v2.emptystate.EmptyStateUiHelper; + +import java.util.Optional; +import java.util.function.Supplier; + +// TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager" +// should be the owner of all per-profile data (especially now that the API is generic)? +class ProfileDescriptor { + final @MultiProfilePagerAdapter.ProfileType int mProfile; + final String mTabLabel; + final String mTabAccessibilityLabel; + final String mTabTag; + + final ViewGroup mRootView; + final EmptyStateUiHelper mEmptyStateUi; + + // TODO: post-refactoring, we may not need to retain these ivars directly (since they may + // be encapsulated within the `EmptyStateUiHelper`?). + private final ViewGroup mEmptyStateView; + + private final SinglePageAdapterT mAdapter; + + public SinglePageAdapterT getAdapter() { + return mAdapter; + } + + public PageViewT getView() { + return mView; + } + + private final PageViewT mView; + + ProfileDescriptor( + @MultiProfilePagerAdapter.ProfileType int forProfile, + String tabLabel, + String tabAccessibilityLabel, + String tabTag, + ViewGroup rootView, + SinglePageAdapterT adapter, + Supplier> containerBottomPaddingOverrideSupplier) { + mProfile = forProfile; + mTabLabel = tabLabel; + mTabAccessibilityLabel = tabAccessibilityLabel; + mTabTag = tabTag; + mRootView = rootView; + mAdapter = adapter; + mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state); + mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list); + mEmptyStateUi = new EmptyStateUiHelper( + rootView, + com.android.internal.R.id.resolver_list, + containerBottomPaddingOverrideSupplier); + } + + protected ViewGroup getEmptyStateView() { + return mEmptyStateView; + } + + public void setupContainerPadding() { + mEmptyStateUi.setupContainerPadding(); + } +} diff --git a/java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java new file mode 100644 index 00000000..e44cf8da --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +import android.content.Context; +import android.os.UserHandle; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.ListView; + +import androidx.viewpager.widget.PagerAdapter; + +import com.android.intentresolver.R; +import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.emptystate.EmptyStateProvider; + +import com.google.common.collect.ImmutableList; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * A {@link PagerAdapter} which describes the work and personal profile intent resolver screens. + */ +public class ResolverMultiProfilePagerAdapter extends + MultiProfilePagerAdapter { + private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; + + public ResolverMultiProfilePagerAdapter(Context context, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle) { + this( + context, + tabs, + emptyStateProvider, + workProfileQuietModeChecker, + defaultProfile, + workProfileUserHandle, + cloneProfileUserHandle, + new BottomPaddingOverrideSupplier()); + } + + private ResolverMultiProfilePagerAdapter( + Context context, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, + BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { + super( + listAdapter -> listAdapter, + (listView, bindAdapter) -> listView.setAdapter(bindAdapter), + tabs, + emptyStateProvider, + workProfileQuietModeChecker, + defaultProfile, + workProfileUserHandle, + cloneProfileUserHandle, + () -> (ViewGroup) LayoutInflater.from(context).inflate( + R.layout.resolver_list_per_profile, null, false), + bottomPaddingOverrideSupplier); + mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; + } + + public void setUseLayoutWithDefault(boolean useLayoutWithDefault) { + mBottomPaddingOverrideSupplier.setUseLayoutWithDefault(useLayoutWithDefault); + } + + /** Un-check any item(s) that may be checked in any of our inactive adapter(s). */ + public void clearCheckedItemsInInactiveProfiles() { + // TODO: The "inactive" condition is legacy logic. Could we simplify and clear-all? + forEachInactivePage(pageNumber -> { + ListView inactiveListView = getListViewForIndex(pageNumber); + if (inactiveListView.getCheckedItemCount() > 0) { + inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false); + } + }); + } + + private static class BottomPaddingOverrideSupplier implements Supplier> { + private boolean mUseLayoutWithDefault; + + public void setUseLayoutWithDefault(boolean useLayoutWithDefault) { + mUseLayoutWithDefault = useLayoutWithDefault; + } + + @Override + public Optional get() { + return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0); + } + } +} diff --git a/java/src/com/android/intentresolver/v2/profiles/TabConfig.java b/java/src/com/android/intentresolver/v2/profiles/TabConfig.java new file mode 100644 index 00000000..994f8aff --- /dev/null +++ b/java/src/com/android/intentresolver/v2/profiles/TabConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.profiles; + +public class TabConfig { + final @MultiProfilePagerAdapter.ProfileType int mProfile; + final String mTabLabel; + final String mTabAccessibilityLabel; + final String mTabTag; + final PageAdapterT mPageAdapter; + + public TabConfig( + @MultiProfilePagerAdapter.ProfileType int profile, + String tabLabel, + String tabAccessibilityLabel, + String tabTag, + PageAdapterT pageAdapter) { + mProfile = profile; + mTabLabel = tabLabel; + mTabAccessibilityLabel = tabAccessibilityLabel; + mTabTag = tabTag; + mPageAdapter = pageAdapter; + } +} -- cgit v1.2.3-59-g8ed1b