blob: 42a29e55404142b77e67febe95418cb924b23a30 [file] [log] [blame]
/*
* 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.os.Trace;
import android.os.UserHandle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.android.intentresolver.emptystate.EmptyState;
import com.android.intentresolver.emptystate.EmptyStateProvider;
import com.android.intentresolver.emptystate.EmptyStateUiHelper;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.util.HashSet;
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 <PageViewT> the type of the widget that represents the contents of a page in this adapter
* @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in
* the per-profile records.
* @param <ListAdapterT> 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 <PageViewT> (as in {@link MultiProfilePagerAdapter}).
* @param <SinglePageAdapterT> (as in {@link MultiProfilePagerAdapter}).
*/
public interface AdapterBinder<PageViewT, SinglePageAdapterT> {
/**
* 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 Profile {}
private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
private final Supplier<ViewGroup> mPageViewInflater;
private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
private final ImmutableList<ProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems;
private final EmptyStateProvider mEmptyStateProvider;
private final UserHandle mWorkProfileUserHandle;
private final UserHandle mCloneProfileUserHandle;
private final Supplier<Boolean> mWorkProfileQuietModeChecker; // True when work is quiet.
private Set<Integer> mLoadedPages;
private int mCurrentPage;
private OnProfileSelectedListener mOnProfileSelectedListener;
protected MultiProfilePagerAdapter(
Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
ImmutableList<SinglePageAdapterT> adapters,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
Supplier<ViewGroup> pageViewInflater,
Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
mCurrentPage = defaultProfile;
mLoadedPages = new HashSet<>();
mWorkProfileUserHandle = workProfileUserHandle;
mCloneProfileUserHandle = cloneProfileUserHandle;
mEmptyStateProvider = emptyStateProvider;
mWorkProfileQuietModeChecker = workProfileQuietModeChecker;
mListAdapterExtractor = listAdapterExtractor;
mAdapterBinder = adapterBinder;
mPageViewInflater = pageViewInflater;
mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
new ImmutableList.Builder<>();
for (SinglePageAdapterT adapter : adapters) {
items.add(createProfileDescriptor(adapter));
}
mItems = items.build();
}
private ProfileDescriptor<PageViewT, SinglePageAdapterT> 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);
}
@NonNull
@Override
public final ViewGroup instantiateItem(ViewGroup container, int position) {
setupListAdapter(position);
final ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(position);
container.addView(descriptor.mRootView);
return descriptor.mRootView;
}
@Override
public void destroyItem(ViewGroup container, int position, @NonNull Object view) {
container.removeView((View) view);
}
@Override
public int getCount() {
return getItemCount();
}
public int getCurrentPage() {
return mCurrentPage;
}
@VisibleForTesting
public UserHandle getCurrentUserHandle() {
return getActiveListAdapter().getUserHandle();
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull 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 <code>pageIndex</code>.
* <ul>
* <li>For a device with only one user, <code>pageIndex</code> value of
* <code>0</code> would return the personal profile {@link ProfileDescriptor}.</li>
* <li>For a device with a work profile, <code>pageIndex</code> value of <code>0</code> would
* return the personal profile {@link ProfileDescriptor}, and <code>pageIndex</code> value of
* <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>
* </ul>
*/
private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
return mItems.get(pageIndex);
}
public ViewGroup getEmptyStateView(int pageIndex) {
return getItem(pageIndex).getEmptyStateView();
}
/**
* Returns the number of {@link ProfileDescriptor} objects.
* <p>For a normal consumer device with only one user returns <code>1</code>.
* <p>For a device with a work profile returns <code>2</code>.
*/
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
* <code>pageIndex</code>.
* <p>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 <code>pageIndex</code>.
*/
public final void setupListAdapter(int pageIndex) {
mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex));
}
/**
* Returns the {@link ListAdapterT} instance of the profile that represents
* <code>userHandle</code>. If there is no such adapter for the specified
* <code>userHandle</code>, returns {@code null}.
* <p>For example, if there is a work profile on the device with user id 10, calling this method
* with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}.
*/
@Nullable
public 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.
* <p>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
public 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 <b><i>not</i></b> currently visible to the user. Otherwise returns
* {@code null}.
* <p>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
public 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));
}
public final SinglePageAdapterT getCurrentRootAdapter() {
return getAdapterForIndex(getCurrentPage());
}
public final PageViewT getActiveAdapterView() {
return getListViewForIndex(getCurrentPage());
}
@Nullable
public final PageViewT getInactiveAdapterView() {
if (getCount() < 2) {
return null;
}
return getListViewForIndex(1 - getCurrentPage());
}
/**
* Rebuilds the tab that is currently visible to the user.
* <p>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.
* <p>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:
* <ol>
* <li>(highest priority) cross-profile disabled by policy (handled in
* {@link #rebuildTab(ListAdapterT, boolean)})</li>
* <li>no apps available</li>
* <li>(least priority) work is off</li>
* </ol>
*
* 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<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(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();
}
}
protected void showEmptyState(
ListAdapterT activeListAdapter,
EmptyState emptyState,
View.OnClickListener buttonOnClick) {
ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
descriptor.mRootView.findViewById(
com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
descriptor.mEmptyStateUi.resetViewVisibilities();
ViewGroup emptyStateView = descriptor.getEmptyStateView();
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.
* <p>This method is meant to be overridden so that subclasses can customize the padding.
*/
public void setupContainerPadding(View container) {
Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get();
bottomPaddingOverride.ifPresent(paddingBottom ->
container.setPadding(
container.getPaddingLeft(),
container.getPaddingTop(),
container.getPaddingRight(),
paddingBottom));
}
public void showListView(ListAdapterT activeListAdapter) {
ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
descriptor.mRootView.findViewById(
com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE);
descriptor.mEmptyStateUi.hide();
}
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<PageViewT, SinglePageAdapterT> {
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(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);
mEmptyStateUi = new EmptyStateUiHelper(rootView);
}
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.
* <p>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);
}
/**
* 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();
}
}