From 258e41f0b7b29dd85063a01ce98f75a332ef86d4 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Tue, 22 Aug 2023 19:17:20 +0000 Subject: Merge the `MultiProfilePagerAdapter` base classes That is, this CL consolidates `GenericMultiProfilePagerAdapter` up into the base `AbstractMultiProfilePagerAdapter` (and renames to drop the prefixes). This advances an in-progress refactoring to simplify these classes (as in an earlier `GenericMultiProfilePagerAdapter` TODO comment when that class was first introduced). This prepares for other upcoming work in the base class (especially in service of "private space" support) since the responsibilities were unclear and arbitrarily split between these two bases, which had no other direct clients/subclasses (i.e., this CL is exactly equivalent to the earlier code, but removes a class from the hierarchy). I've also started a little bit of cleanup around method visibility but left much of that work out-of-scope for now. Test: IntentResolverUnitTests / CtsSharesheetDeviceTest Change-Id: I073e0bec5764ea16736af585af0bc9f744089d03 --- .../AbstractMultiProfilePagerAdapter.java | 582 ---------------- .../android/intentresolver/ChooserActivity.java | 6 +- .../ChooserMultiProfilePagerAdapter.java | 7 +- .../GenericMultiProfilePagerAdapter.java | 235 ------- .../intentresolver/MultiProfilePagerAdapter.java | 734 +++++++++++++++++++++ .../NoAppsAvailableEmptyStateProvider.java | 4 +- .../NoCrossProfileEmptyStateProvider.java | 6 +- .../android/intentresolver/ResolverActivity.java | 28 +- .../ResolverMultiProfilePagerAdapter.java | 3 +- .../WorkProfilePausedEmptyStateProvider.java | 6 +- 10 files changed, 763 insertions(+), 848 deletions(-) delete mode 100644 java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java delete mode 100644 java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/MultiProfilePagerAdapter.java (limited to 'java/src/com') diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java deleted file mode 100644 index 4b06db3b..00000000 --- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java +++ /dev/null @@ -1,582 +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; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.UserIdInt; -import android.app.AppGlobals; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.IPackageManager; -import android.os.Trace; -import android.os.UserHandle; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.TextView; - -import androidx.viewpager.widget.PagerAdapter; -import androidx.viewpager.widget.ViewPager; - -import com.android.internal.annotations.VisibleForTesting; - -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.function.Supplier; - -/** - * Skeletal {@link PagerAdapter} implementation of a work or personal profile page for - * intent resolution (including share sheet). - */ -public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { - - private static final String TAG = "AbstractMultiProfilePagerAdapter"; - static final int PROFILE_PERSONAL = 0; - static final int PROFILE_WORK = 1; - - @IntDef({PROFILE_PERSONAL, PROFILE_WORK}) - @interface Profile {} - - private final Context mContext; - private int mCurrentPage; - private OnProfileSelectedListener mOnProfileSelectedListener; - - private Set mLoadedPages; - private final EmptyStateProvider mEmptyStateProvider; - private final UserHandle mWorkProfileUserHandle; - private final UserHandle mCloneProfileUserHandle; - private final Supplier mWorkProfileQuietModeChecker; // True when work is quiet. - - AbstractMultiProfilePagerAdapter( - Context context, - int currentPage, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle) { - mContext = Objects.requireNonNull(context); - mCurrentPage = currentPage; - mLoadedPages = new HashSet<>(); - mWorkProfileUserHandle = workProfileUserHandle; - mCloneProfileUserHandle = cloneProfileUserHandle; - mEmptyStateProvider = emptyStateProvider; - mWorkProfileQuietModeChecker = workProfileQuietModeChecker; - } - - void setOnProfileSelectedListener(OnProfileSelectedListener listener) { - mOnProfileSelectedListener = listener; - } - - Context getContext() { - return mContext; - } - - /** - * 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. - */ - 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.onProfileSelected(position); - } - } - - @Override - public void onPageScrollStateChanged(int state) { - if (mOnProfileSelectedListener != null) { - mOnProfileSelectedListener.onProfilePageStateChanged(state); - } - } - }); - viewPager.setAdapter(this); - viewPager.setCurrentItem(mCurrentPage); - mLoadedPages.add(mCurrentPage); - } - - void clearInactiveProfileCache() { - if (mLoadedPages.size() == 1) { - return; - } - mLoadedPages.remove(1 - mCurrentPage); - } - - @Override - public ViewGroup instantiateItem(ViewGroup container, int position) { - final ProfileDescriptor profileDescriptor = getItem(position); - container.addView(profileDescriptor.rootView); - return profileDescriptor.rootView; - } - - @Override - public void destroyItem(ViewGroup container, int position, Object view) { - container.removeView((View) view); - } - - @Override - public int getCount() { - return getItemCount(); - } - - protected int getCurrentPage() { - return mCurrentPage; - } - - @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}.
  • - *
- */ - abstract ProfileDescriptor getItem(int pageIndex); - - protected ViewGroup getEmptyStateView(int pageIndex) { - return getItem(pageIndex).getEmptyStateView(); - } - - /** - * 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. - */ - abstract int getItemCount(); - - /** - * Performs view-related initialization procedures for the adapter specified - * by pageIndex. - */ - abstract void setupListAdapter(int pageIndex); - - /** - * 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 abstract Object getAdapterForIndex(int pageIndex); - - /** - * Returns the {@link ResolverListAdapter} 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 ResolverListAdapter}. - */ - @Nullable - abstract ResolverListAdapter getListAdapterForUserHandle(UserHandle userHandle); - - /** - * Returns the {@link ResolverListAdapter} 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 ResolverListAdapter}. - * @see #getInactiveListAdapter() - */ - @VisibleForTesting - public abstract ResolverListAdapter getActiveListAdapter(); - - /** - * If this is a device with a work profile, returns the {@link ResolverListAdapter} instance - * of the profile that is not currently visible to the user. Otherwise returns - * {@code null}. - *

For example, if the user is viewing the work tab in the share sheet, this method returns - * the personal profile {@link ResolverListAdapter}. - * @see #getActiveListAdapter() - */ - @VisibleForTesting - public abstract @Nullable ResolverListAdapter getInactiveListAdapter(); - - public abstract ResolverListAdapter getPersonalListAdapter(); - - public abstract @Nullable ResolverListAdapter getWorkListAdapter(); - - abstract Object getCurrentRootAdapter(); - - abstract ViewGroup getActiveAdapterView(); - - abstract @Nullable ViewGroup getInactiveAdapterView(); - - /** - * Rebuilds the tab that is currently visible to the user. - *

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

Returns {@code true} if rebuild has completed. - */ - boolean rebuildInactiveTab(boolean doPostProcessing) { - Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab"); - if (getItemCount() == 1) { - Trace.endSection(); - return false; - } - boolean result = rebuildTab(getInactiveListAdapter(), doPostProcessing); - Trace.endSection(); - return result; - } - - private int userHandleToPageIndex(UserHandle userHandle) { - if (userHandle.equals(getPersonalListAdapter().getUserHandle())) { - return PROFILE_PERSONAL; - } else { - return PROFILE_WORK; - } - } - - private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) { - if (shouldSkipRebuild(activeListAdapter)) { - activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); - return false; - } - return activeListAdapter.rebuildList(doPostProcessing); - } - - private boolean shouldSkipRebuild(ResolverListAdapter 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(ResolverListAdapter, 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. - */ - void showEmptyResolverListEmptyState(ResolverListAdapter 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 = getItem( - userHandleToPageIndex(listAdapter.getUserHandle())); - AbstractMultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView()); - }); - } - - 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(); - } - } - - /** - * Utility class to check if there are cross profile intents, it is in a separate class so - * it could be mocked in tests - */ - public static class CrossProfileIntentsChecker { - - private final ContentResolver mContentResolver; - - public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) { - mContentResolver = contentResolver; - } - - /** - * Returns {@code true} if at least one of the provided {@code intents} can be forwarded - * from {@code source} (user id) to {@code target} (user id). - */ - public boolean hasCrossProfileIntents(List intents, @UserIdInt int source, - @UserIdInt int target) { - IPackageManager packageManager = AppGlobals.getPackageManager(); - - return intents.stream().anyMatch(intent -> - null != IntentForwarderActivity.canForward(intent, source, target, - packageManager, mContentResolver)); - } - } - - protected void showEmptyState(ResolverListAdapter activeListAdapter, EmptyState emptyState, - View.OnClickListener buttonOnClick) { - ProfileDescriptor descriptor = getItem( - userHandleToPageIndex(activeListAdapter.getUserHandle())); - descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE); - ViewGroup emptyStateView = descriptor.getEmptyStateView(); - resetViewVisibilitiesForEmptyState(emptyStateView); - emptyStateView.setVisibility(View.VISIBLE); - - View container = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_container); - setupContainerPadding(container); - - TextView titleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title); - String title = emptyState.getTitle(); - if (title != null) { - titleView.setVisibility(View.VISIBLE); - titleView.setText(title); - } else { - titleView.setVisibility(View.GONE); - } - - TextView subtitleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle); - String subtitle = emptyState.getSubtitle(); - if (subtitle != null) { - subtitleView.setVisibility(View.VISIBLE); - subtitleView.setText(subtitle); - } else { - subtitleView.setVisibility(View.GONE); - } - - View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty); - defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE); - - Button button = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button); - button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE); - button.setOnClickListener(buttonOnClick); - - activeListAdapter.markTabLoaded(); - } - - /** - * Sets up the padding of the view containing the empty state screens. - *

This method is meant to be overridden so that subclasses can customize the padding. - */ - protected void setupContainerPadding(View container) {} - - private void showSpinner(View emptyStateView) { - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.INVISIBLE); - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE); - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.VISIBLE); - emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE); - } - - private void resetViewVisibilitiesForEmptyState(View emptyStateView) { - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.VISIBLE); - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.VISIBLE); - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE); - emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE); - emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE); - } - - protected void showListView(ResolverListAdapter activeListAdapter) { - ProfileDescriptor descriptor = getItem( - userHandleToPageIndex(activeListAdapter.getUserHandle())); - descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE); - View emptyStateView = descriptor.rootView.findViewById(com.android.internal.R.id.resolver_empty_state); - emptyStateView.setVisibility(View.GONE); - } - - boolean shouldShowEmptyStateScreen(ResolverListAdapter listAdapter) { - int count = listAdapter.getUnfilteredCount(); - return (count == 0 && listAdapter.getPlaceholderCount() == 0) - || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) - && mWorkProfileQuietModeChecker.get()); - } - - protected static class ProfileDescriptor { - final ViewGroup rootView; - private final ViewGroup mEmptyStateView; - ProfileDescriptor(ViewGroup rootView) { - this.rootView = rootView; - mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state); - } - - protected ViewGroup getEmptyStateView() { - return mEmptyStateView; - } - } - - public interface OnProfileSelectedListener { - /** - * Callback for when the user changes the active tab from personal to work or vice versa. - *

This callback is only called when the intent resolver or share sheet shows - * the work and personal profiles. - * @param profileIndex {@link #PROFILE_PERSONAL} if the personal profile was selected or - * {@link #PROFILE_WORK} if the work profile was selected. - */ - void onProfileSelected(int profileIndex); - - - /** - * 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); - } - - /** - * Returns an empty state to show for the current profile page (tab) if necessary. - * This could be used e.g. to show a blocker on a tab if device management policy doesn't - * allow to use it or there are no apps available. - */ - public interface EmptyStateProvider { - /** - * When a non-null empty state is returned the corresponding profile page will show - * this empty state - * @param resolverListAdapter the current adapter - */ - @Nullable - default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { - return null; - } - } - - /** - * Empty state provider that combines multiple providers. Providers earlier in the list have - * priority, that is if there is a provider that returns non-null empty state then all further - * providers will be ignored. - */ - public static class CompositeEmptyStateProvider implements EmptyStateProvider { - - private final EmptyStateProvider[] mProviders; - - public CompositeEmptyStateProvider(EmptyStateProvider... providers) { - mProviders = providers; - } - - @Nullable - @Override - public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { - for (EmptyStateProvider provider : mProviders) { - EmptyState emptyState = provider.getEmptyState(resolverListAdapter); - if (emptyState != null) { - return emptyState; - } - } - return null; - } - } - - /** - * Describes how the blocked empty state should look like for a profile tab - */ - public interface EmptyState { - /** - * Title that will be shown on the empty state - */ - @Nullable - default String getTitle() { return null; } - - /** - * Subtitle that will be shown underneath the title on the empty state - */ - @Nullable - default String getSubtitle() { return null; } - - /** - * If non-null then a button will be shown and this listener will be called - * when the button is clicked - */ - @Nullable - default ClickListener getButtonClickListener() { return null; } - - /** - * If true then default text ('No apps can perform this action') and style for the empty - * state will be applied, title and subtitle will be ignored. - */ - default boolean useDefaultEmptyView() { return false; } - - /** - * Returns true if for this empty state we should skip rebuilding of the apps list - * for this tab. - */ - default boolean shouldSkipDataRebuild() { return false; } - - /** - * Called when empty state is shown, could be used e.g. to track analytics events - */ - default void onEmptyStateShown() {} - - interface ClickListener { - void onClick(TabControl currentTab); - } - - interface TabControl { - void showSpinner(); - } - } - - - /** - * Listener for when the user switches on the work profile from the work tab. - */ - 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/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index f455be4c..7b4f4827 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -73,8 +73,8 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.ViewPager; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.MultiProfilePagerAdapter.EmptyState; +import com.android.intentresolver.MultiProfilePagerAdapter.EmptyStateProvider; import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.MultiDisplayResolveInfo; @@ -418,7 +418,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } @Override - protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( + protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter( Intent[] initialIntents, List rList, boolean filterLastUsed, diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java index ba35ae5d..75ff3a7f 100644 --- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java @@ -38,7 +38,7 @@ import java.util.function.Supplier; * A {@link PagerAdapter} which describes the work and personal profile share sheet screens. */ @VisibleForTesting -public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAdapter< +public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter< RecyclerView, ChooserGridAdapter, ChooserListAdapter> { private static final int SINGLE_CELL_SPAN_SIZE = 1; @@ -103,7 +103,6 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier, FeatureFlags featureFlags) { super( - context, gridAdapter -> gridAdapter.getListAdapter(), adapterBinder, gridAdapters, @@ -149,7 +148,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda } @Override - boolean rebuildActiveTab(boolean doPostProcessing) { + public boolean rebuildActiveTab(boolean doPostProcessing) { if (doPostProcessing) { Tracer.INSTANCE.beginAppTargetLoadingSection(getActiveListAdapter().getUserHandle()); } @@ -157,7 +156,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda } @Override - boolean rebuildInactiveTab(boolean doPostProcessing) { + public boolean rebuildInactiveTab(boolean doPostProcessing) { if (getItemCount() != 1 && doPostProcessing) { Tracer.INSTANCE.beginAppTargetLoadingSection(getInactiveListAdapter().getUserHandle()); } diff --git a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java deleted file mode 100644 index a1c53402..00000000 --- a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (C) 2022 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; - -import android.annotation.Nullable; -import android.content.Context; -import android.os.UserHandle; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; - -import com.android.internal.annotations.VisibleForTesting; - -import com.google.common.collect.ImmutableList; - -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * Implementation of {@link AbstractMultiProfilePagerAdapter} that consolidates the variation in - * existing implementations; most overrides were only to vary type signatures (which are better - * represented via generic types), and a few minor behavioral customizations are now implemented - * through small injectable delegate classes. - * TODO: now that the existing implementations are shown to be expressible in terms of this new - * generic type, merge up into the base class and simplify the public APIs. - * TODO: attempt to further restrict visibility 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"?) - * - * @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 {@link mListAdapterExtractor}. - * - * TODO: this class doesn't make any explicit usage of the {@link ResolverListAdapter} API, so the - * type constraint can probably be dropped once the API is merged upwards and cleaned. - */ -class GenericMultiProfilePagerAdapter< - PageViewT extends ViewGroup, - SinglePageAdapterT, - ListAdapterT extends ResolverListAdapter> extends AbstractMultiProfilePagerAdapter { - - /** Delegate to set up a given adapter and page view to be used together. */ - 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); - } - - private final Function mListAdapterExtractor; - private final AdapterBinder mAdapterBinder; - private final Supplier mPageViewInflater; - private final Supplier> mContainerBottomPaddingOverrideSupplier; - - private final ImmutableList> mItems; - - GenericMultiProfilePagerAdapter( - Context context, - Function listAdapterExtractor, - AdapterBinder adapterBinder, - ImmutableList adapters, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @Profile int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - Supplier pageViewInflater, - Supplier> containerBottomPaddingOverrideSupplier) { - super( - context, - /* currentPage= */ defaultProfile, - emptyStateProvider, - workProfileQuietModeChecker, - workProfileUserHandle, - cloneProfileUserHandle); - - mListAdapterExtractor = listAdapterExtractor; - mAdapterBinder = adapterBinder; - mPageViewInflater = pageViewInflater; - mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier; - - ImmutableList.Builder> items = - new ImmutableList.Builder<>(); - for (SinglePageAdapterT adapter : adapters) { - items.add(createProfileDescriptor(adapter)); - } - mItems = items.build(); - } - - private GenericProfileDescriptor - createProfileDescriptor(SinglePageAdapterT adapter) { - return new GenericProfileDescriptor<>(mPageViewInflater.get(), adapter); - } - - @Override - protected GenericProfileDescriptor getItem(int pageIndex) { - return mItems.get(pageIndex); - } - - @Override - public int getItemCount() { - return mItems.size(); - } - - public PageViewT getListViewForIndex(int index) { - return getItem(index).mView; - } - - @Override - @VisibleForTesting - public SinglePageAdapterT getAdapterForIndex(int index) { - return getItem(index).mAdapter; - } - - @Override - protected void setupListAdapter(int pageIndex) { - mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex)); - } - - @Override - public ViewGroup instantiateItem(ViewGroup container, int position) { - setupListAdapter(position); - return super.instantiateItem(container, position); - } - - @Override - @Nullable - protected ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) { - if (getPersonalListAdapter().getUserHandle().equals(userHandle) - || userHandle.equals(getCloneUserHandle())) { - return getPersonalListAdapter(); - } else if (getWorkListAdapter() != null - && getWorkListAdapter().getUserHandle().equals(userHandle)) { - return getWorkListAdapter(); - } - return null; - } - - @Override - @VisibleForTesting - public ListAdapterT getActiveListAdapter() { - return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage())); - } - - @Override - @VisibleForTesting - public ListAdapterT getInactiveListAdapter() { - if (getCount() < 2) { - return null; - } - return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage())); - } - - @Override - public ListAdapterT getPersonalListAdapter() { - return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL)); - } - - @Override - public ListAdapterT getWorkListAdapter() { - if (!hasAdapterForIndex(PROFILE_WORK)) { - return null; - } - return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK)); - } - - @Override - protected SinglePageAdapterT getCurrentRootAdapter() { - return getAdapterForIndex(getCurrentPage()); - } - - @Override - protected PageViewT getActiveAdapterView() { - return getListViewForIndex(getCurrentPage()); - } - - @Override - protected PageViewT getInactiveAdapterView() { - if (getCount() < 2) { - return null; - } - return getListViewForIndex(1 - getCurrentPage()); - } - - @Override - protected void setupContainerPadding(View container) { - Optional bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get(); - bottomPaddingOverride.ifPresent(paddingBottom -> - container.setPadding( - container.getPaddingLeft(), - container.getPaddingTop(), - container.getPaddingRight(), - paddingBottom)); - } - - private boolean hasAdapterForIndex(int pageIndex) { - return (pageIndex < getCount()); - } - - // 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 GenericProfileDescriptor extends - ProfileDescriptor { - private final SinglePageAdapterT mAdapter; - private final PageViewT mView; - - GenericProfileDescriptor(ViewGroup rootView, SinglePageAdapterT adapter) { - super(rootView); - mAdapter = adapter; - mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list); - } - } -} diff --git a/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java new file mode 100644 index 00000000..cc079a87 --- /dev/null +++ b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java @@ -0,0 +1,734 @@ +/* + * 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; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.AppGlobals; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.IPackageManager; +import android.os.Trace; +import android.os.UserHandle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.android.internal.annotations.VisibleForTesting; + +import com.google.common.collect.ImmutableList; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +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"?) + * + * @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 {@link mListAdapterExtractor}. + * + * 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. + */ +public 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); + } + + static final int PROFILE_PERSONAL = 0; + static final int PROFILE_WORK = 1; + + @IntDef({PROFILE_PERSONAL, PROFILE_WORK}) + @interface Profile {} + + private final Function mListAdapterExtractor; + private final AdapterBinder mAdapterBinder; + private final Supplier mPageViewInflater; + private final Supplier> mContainerBottomPaddingOverrideSupplier; + + 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 Set mLoadedPages; + private int mCurrentPage; + private OnProfileSelectedListener mOnProfileSelectedListener; + + protected MultiProfilePagerAdapter( + Function listAdapterExtractor, + AdapterBinder adapterBinder, + ImmutableList adapters, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @Profile int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, + Supplier pageViewInflater, + Supplier> containerBottomPaddingOverrideSupplier) { + mCurrentPage = defaultProfile; + mLoadedPages = new HashSet<>(); + mWorkProfileUserHandle = workProfileUserHandle; + mCloneProfileUserHandle = cloneProfileUserHandle; + mEmptyStateProvider = emptyStateProvider; + mWorkProfileQuietModeChecker = workProfileQuietModeChecker; + + mListAdapterExtractor = listAdapterExtractor; + mAdapterBinder = adapterBinder; + mPageViewInflater = pageViewInflater; + mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier; + + ImmutableList.Builder> items = + new ImmutableList.Builder<>(); + for (SinglePageAdapterT adapter : adapters) { + items.add(createProfileDescriptor(adapter)); + } + mItems = items.build(); + } + + private ProfileDescriptor createProfileDescriptor( + SinglePageAdapterT adapter) { + return new ProfileDescriptor<>(mPageViewInflater.get(), adapter); + } + + public void setOnProfileSelectedListener(OnProfileSelectedListener listener) { + mOnProfileSelectedListener = listener; + } + + /** + * 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.onProfileSelected(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() { + if (mLoadedPages.size() == 1) { + return; + } + mLoadedPages.remove(1 - mCurrentPage); + } + + @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(); + } + + protected int getCurrentPage() { + return mCurrentPage; + } + + @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}.
  • + *
+ */ + private ProfileDescriptor getItem(int pageIndex) { + return mItems.get(pageIndex); + } + + protected ViewGroup getEmptyStateView(int pageIndex) { + return getItem(pageIndex).getEmptyStateView(); + } + + /** + * 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 getAdapterForIndex(int index) { + return getItem(index).mAdapter; + } + + /** + * Performs view-related initialization procedures for the adapter specified + * by pageIndex. + */ + protected final void setupListAdapter(int pageIndex) { + mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex)); + } + + /** + * 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 + protected final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) { + if (getPersonalListAdapter().getUserHandle().equals(userHandle) + || userHandle.equals(getCloneUserHandle())) { + return getPersonalListAdapter(); + } else if ((getWorkListAdapter() != null) + && getWorkListAdapter().getUserHandle().equals(userHandle)) { + return getWorkListAdapter(); + } + return null; + } + + /** + * 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}. + * @see #getInactiveListAdapter() + */ + @VisibleForTesting + protected final ListAdapterT getActiveListAdapter() { + return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage())); + } + + /** + * If this is a device with a work profile, returns the {@link ListAdapterT} instance + * of the profile that is not currently visible to the user. Otherwise returns + * {@code null}. + *

For example, if the user is viewing the work tab in the share sheet, this method returns + * the personal profile {@link ListAdapterT}. + * @see #getActiveListAdapter() + */ + @VisibleForTesting + @Nullable + protected final ListAdapterT getInactiveListAdapter() { + if (getCount() < 2) { + return null; + } + return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage())); + } + + public final ListAdapterT getPersonalListAdapter() { + return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL)); + } + + @Nullable + public final ListAdapterT getWorkListAdapter() { + if (!hasAdapterForIndex(PROFILE_WORK)) { + return null; + } + return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK)); + } + + protected final SinglePageAdapterT getCurrentRootAdapter() { + return getAdapterForIndex(getCurrentPage()); + } + + protected final PageViewT getActiveAdapterView() { + return getListViewForIndex(getCurrentPage()); + } + + @Nullable + protected final PageViewT getInactiveAdapterView() { + if (getCount() < 2) { + return null; + } + return getListViewForIndex(1 - getCurrentPage()); + } + + /** + * Rebuilds the tab that is currently visible to the user. + *

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

Returns {@code true} if rebuild has completed. + */ + public boolean rebuildInactiveTab(boolean doPostProcessing) { + Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab"); + if (getItemCount() == 1) { + Trace.endSection(); + return false; + } + boolean result = rebuildTab(getInactiveListAdapter(), doPostProcessing); + Trace.endSection(); + return result; + } + + private int userHandleToPageIndex(UserHandle userHandle) { + if (userHandle.equals(getPersonalListAdapter().getUserHandle())) { + return PROFILE_PERSONAL; + } else { + return PROFILE_WORK; + } + } + + private 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(); + } + + private boolean hasAdapterForIndex(int pageIndex) { + return (pageIndex < getCount()); + } + + /** + * 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. + */ + 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 = getItem( + userHandleToPageIndex(listAdapter.getUserHandle())); + MultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView()); + }); + } + + 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(); + } + } + + /** + * Utility class to check if there are cross profile intents, it is in a separate class so + * it could be mocked in tests + */ + public static class CrossProfileIntentsChecker { + + private final ContentResolver mContentResolver; + + public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) { + mContentResolver = contentResolver; + } + + /** + * Returns {@code true} if at least one of the provided {@code intents} can be forwarded + * from {@code source} (user id) to {@code target} (user id). + */ + public boolean hasCrossProfileIntents(List intents, @UserIdInt int source, + @UserIdInt int target) { + IPackageManager packageManager = AppGlobals.getPackageManager(); + + return intents.stream().anyMatch(intent -> + null != IntentForwarderActivity.canForward(intent, source, target, + packageManager, mContentResolver)); + } + } + + protected void showEmptyState( + ListAdapterT activeListAdapter, + EmptyState emptyState, + View.OnClickListener buttonOnClick) { + ProfileDescriptor descriptor = getItem( + userHandleToPageIndex(activeListAdapter.getUserHandle())); + descriptor.mRootView.findViewById( + com.android.internal.R.id.resolver_list).setVisibility(View.GONE); + ViewGroup emptyStateView = descriptor.getEmptyStateView(); + resetViewVisibilitiesForEmptyState(emptyStateView); + emptyStateView.setVisibility(View.VISIBLE); + + View container = emptyStateView.findViewById( + com.android.internal.R.id.resolver_empty_state_container); + setupContainerPadding(container); + + TextView titleView = emptyStateView.findViewById( + com.android.internal.R.id.resolver_empty_state_title); + String title = emptyState.getTitle(); + if (title != null) { + titleView.setVisibility(View.VISIBLE); + titleView.setText(title); + } else { + titleView.setVisibility(View.GONE); + } + + TextView subtitleView = emptyStateView.findViewById( + com.android.internal.R.id.resolver_empty_state_subtitle); + String subtitle = emptyState.getSubtitle(); + if (subtitle != null) { + subtitleView.setVisibility(View.VISIBLE); + subtitleView.setText(subtitle); + } else { + subtitleView.setVisibility(View.GONE); + } + + View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty); + defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE); + + Button button = emptyStateView.findViewById( + com.android.internal.R.id.resolver_empty_state_button); + button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE); + button.setOnClickListener(buttonOnClick); + + activeListAdapter.markTabLoaded(); + } + + /** + * Sets up the padding of the view containing the empty state screens. + *

This method is meant to be overridden so that subclasses can customize the padding. + */ + public void setupContainerPadding(View container) { + Optional bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get(); + bottomPaddingOverride.ifPresent(paddingBottom -> + container.setPadding( + container.getPaddingLeft(), + container.getPaddingTop(), + container.getPaddingRight(), + paddingBottom)); + } + + private void showSpinner(View emptyStateView) { + emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title) + .setVisibility(View.INVISIBLE); + emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button) + .setVisibility(View.INVISIBLE); + emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress) + .setVisibility(View.VISIBLE); + emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE); + } + + private void resetViewVisibilitiesForEmptyState(View emptyStateView) { + emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title) + .setVisibility(View.VISIBLE); + emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle) + .setVisibility(View.VISIBLE); + emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button) + .setVisibility(View.INVISIBLE); + emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress) + .setVisibility(View.GONE); + emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE); + } + + protected void showListView(ListAdapterT activeListAdapter) { + ProfileDescriptor descriptor = getItem( + userHandleToPageIndex(activeListAdapter.getUserHandle())); + descriptor.mRootView.findViewById( + com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE); + View emptyStateView = descriptor.mRootView.findViewById( + com.android.internal.R.id.resolver_empty_state); + emptyStateView.setVisibility(View.GONE); + } + + 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 ViewGroup mRootView; + private final ViewGroup mEmptyStateView; + + private final SinglePageAdapterT mAdapter; + private final PageViewT mView; + + ProfileDescriptor(ViewGroup rootView, SinglePageAdapterT adapter) { + 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); + } + + protected ViewGroup getEmptyStateView() { + return mEmptyStateView; + } + } + + /** Listener interface for changes between the per-profile UI tabs. */ + public interface OnProfileSelectedListener { + /** + * Callback for when the user changes the active tab from personal to work or vice versa. + *

This callback is only called when the intent resolver or share sheet shows + * the work and personal profiles. + * @param profileIndex {@link #PROFILE_PERSONAL} if the personal profile was selected or + * {@link #PROFILE_WORK} if the work profile was selected. + */ + void onProfileSelected(int profileIndex); + + + /** + * 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); + } + + /** + * Returns an empty state to show for the current profile page (tab) if necessary. + * This could be used e.g. to show a blocker on a tab if device management policy doesn't + * allow to use it or there are no apps available. + */ + public interface EmptyStateProvider { + /** + * When a non-null empty state is returned the corresponding profile page will show + * this empty state + * @param resolverListAdapter the current adapter + */ + @Nullable + default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { + return null; + } + } + + /** + * Empty state provider that combines multiple providers. Providers earlier in the list have + * priority, that is if there is a provider that returns non-null empty state then all further + * providers will be ignored. + */ + public static class CompositeEmptyStateProvider implements EmptyStateProvider { + + private final EmptyStateProvider[] mProviders; + + public CompositeEmptyStateProvider(EmptyStateProvider... providers) { + mProviders = providers; + } + + @Nullable + @Override + public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { + for (EmptyStateProvider provider : mProviders) { + EmptyState emptyState = provider.getEmptyState(resolverListAdapter); + if (emptyState != null) { + return emptyState; + } + } + return null; + } + } + + /** + * Describes how the blocked empty state should look like for a profile tab + */ + public interface EmptyState { + /** + * Title that will be shown on the empty state + */ + @Nullable + default String getTitle() { + return null; + } + + /** + * Subtitle that will be shown underneath the title on the empty state + */ + @Nullable + default String getSubtitle() { + return null; + } + + /** + * If non-null then a button will be shown and this listener will be called + * when the button is clicked + */ + @Nullable + default ClickListener getButtonClickListener() { + return null; + } + + /** + * If true then default text ('No apps can perform this action') and style for the empty + * state will be applied, title and subtitle will be ignored. + */ + default boolean useDefaultEmptyView() { + return false; + } + + /** + * Returns true if for this empty state we should skip rebuilding of the apps list + * for this tab. + */ + default boolean shouldSkipDataRebuild() { + return false; + } + + /** + * Called when empty state is shown, could be used e.g. to track analytics events + */ + default void onEmptyStateShown() {} + + interface ClickListener { + void onClick(TabControl currentTab); + } + + interface TabControl { + void showSpinner(); + } + } + + /** + * 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/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java index a7b50f38..1900abee 100644 --- a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java @@ -28,8 +28,8 @@ import android.content.pm.ResolveInfo; import android.os.UserHandle; import android.stats.devicepolicy.nano.DevicePolicyEnums; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.MultiProfilePagerAdapter.EmptyState; +import com.android.intentresolver.MultiProfilePagerAdapter.EmptyStateProvider; import com.android.internal.R; import java.util.List; diff --git a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java index 6f72bb00..ad262f0e 100644 --- a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java @@ -24,9 +24,9 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.os.UserHandle; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.MultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.MultiProfilePagerAdapter.EmptyState; +import com.android.intentresolver.MultiProfilePagerAdapter.EmptyStateProvider; /** * Empty state provider that does not allow cross profile sharing, it will return a blocker diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index 1161ca81..d1d86aff 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -98,12 +98,12 @@ import android.widget.Toast; import androidx.fragment.app.FragmentActivity; import androidx.viewpager.widget.ViewPager; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CompositeEmptyStateProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile; +import com.android.intentresolver.MultiProfilePagerAdapter.CompositeEmptyStateProvider; +import com.android.intentresolver.MultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.MultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.MultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.MultiProfilePagerAdapter.Profile; import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; @@ -201,7 +201,7 @@ public class ResolverActivity extends FragmentActivity implements private TargetDataLoader mTargetDataLoader; @VisibleForTesting - protected AbstractMultiProfilePagerAdapter mMultiProfilePagerAdapter; + protected MultiProfilePagerAdapter mMultiProfilePagerAdapter; protected WorkProfileAvailabilityManager mWorkProfileAvailability; @@ -228,8 +228,8 @@ public class ResolverActivity extends FragmentActivity implements static final String EXTRA_CALLING_USER = "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER"; - protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL; - protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK; + protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL; + protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK; private UserHandle mHeaderCreatorUser; @@ -496,12 +496,12 @@ public class ResolverActivity extends FragmentActivity implements + (categories != null ? Arrays.toString(categories.toArray()) : "")); } - protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( + protected MultiProfilePagerAdapter createMultiProfilePagerAdapter( Intent[] initialIntents, List resolutionList, boolean filterLastUsed, TargetDataLoader targetDataLoader) { - AbstractMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null; + MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null; if (shouldShowTabs()) { resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForTwoProfiles( @@ -521,7 +521,7 @@ public class ResolverActivity extends FragmentActivity implements return new EmptyStateProvider() {}; } - final AbstractMultiProfilePagerAdapter.EmptyState + final MultiProfilePagerAdapter.EmptyState noWorkToPersonalEmptyState = new DevicePolicyBlockerEmptyState(/* context= */ this, /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, @@ -533,7 +533,7 @@ public class ResolverActivity extends FragmentActivity implements /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_RESOLVER); - final AbstractMultiProfilePagerAdapter.EmptyState noPersonalToWorkEmptyState = + final MultiProfilePagerAdapter.EmptyState noPersonalToWorkEmptyState = new DevicePolicyBlockerEmptyState(/* context= */ this, /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE, /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked, @@ -2080,7 +2080,7 @@ public class ResolverActivity extends FragmentActivity implements viewPager.setVisibility(View.VISIBLE); tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage()); mMultiProfilePagerAdapter.setOnProfileSelectedListener( - new AbstractMultiProfilePagerAdapter.OnProfileSelectedListener() { + new MultiProfilePagerAdapter.OnProfileSelectedListener() { @Override public void onProfileSelected(int index) { tabHost.setCurrentTab(index); diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java index 85d97ad5..9fb35948 100644 --- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java @@ -36,7 +36,7 @@ import java.util.function.Supplier; */ @VisibleForTesting public class ResolverMultiProfilePagerAdapter extends - GenericMultiProfilePagerAdapter { + MultiProfilePagerAdapter { private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; ResolverMultiProfilePagerAdapter( @@ -86,7 +86,6 @@ public class ResolverMultiProfilePagerAdapter extends UserHandle cloneProfileUserHandle, BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { super( - context, listAdapter -> listAdapter, (listView, bindAdapter) -> listView.setAdapter(bindAdapter), listAdapters, diff --git a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java index 2f3dfbd5..9ea7ceee 100644 --- a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java @@ -26,9 +26,9 @@ import android.content.Context; import android.os.UserHandle; import android.stats.devicepolicy.nano.DevicePolicyEnums; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; +import com.android.intentresolver.MultiProfilePagerAdapter.EmptyState; +import com.android.intentresolver.MultiProfilePagerAdapter.EmptyStateProvider; +import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; /** * Chooser/ResolverActivity empty state provider that returns empty state which is shown when -- cgit v1.2.3-59-g8ed1b