diff options
22 files changed, 871 insertions, 135 deletions
diff --git a/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java b/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java index 08022e983892..b2aa0431251d 100644 --- a/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java +++ b/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java @@ -26,7 +26,9 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.PagerAdapter; import com.android.internal.widget.ViewPager; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; /** * Skeletal {@link PagerAdapter} implementation of a work or personal profile page for @@ -34,6 +36,7 @@ import java.util.Objects; */ 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}) @@ -41,10 +44,17 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { private final Context mContext; private int mCurrentPage; + private OnProfileSelectedListener mOnProfileSelectedListener; + private Set<Integer> mLoadedPages; AbstractMultiProfilePagerAdapter(Context context, int currentPage) { mContext = Objects.requireNonNull(context); mCurrentPage = currentPage; + mLoadedPages = new HashSet<>(); + } + + void setOnProfileSelectedListener(OnProfileSelectedListener listener) { + mOnProfileSelectedListener = listener; } Context getContext() { @@ -57,15 +67,22 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { * page and rebuilds the list. */ void setupViewPager(ViewPager viewPager) { - viewPager.setCurrentItem(mCurrentPage); viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { mCurrentPage = position; - getActiveListAdapter().rebuildList(); + if (!mLoadedPages.contains(position)) { + getActiveListAdapter().rebuildList(); + mLoadedPages.add(position); + } + if (mOnProfileSelectedListener != null) { + mOnProfileSelectedListener.onProfileSelected(position); + } } }); viewPager.setAdapter(this); + viewPager.setCurrentItem(mCurrentPage); + mLoadedPages.add(mCurrentPage); } @Override @@ -90,7 +107,8 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { return mCurrentPage; } - UserHandle getCurrentUserHandle() { + @VisibleForTesting + public UserHandle getCurrentUserHandle() { return getActiveListAdapter().mResolverListController.getUserHandle(); } @@ -135,7 +153,8 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { * <p>This method is meant to be implemented with an implementation-specific return type * depending on the adapter type. */ - abstract Object getAdapterForIndex(int pageIndex); + @VisibleForTesting + public abstract Object getAdapterForIndex(int pageIndex); @VisibleForTesting public abstract ResolverListAdapter getActiveListAdapter(); @@ -152,7 +171,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { abstract Object getCurrentRootAdapter(); - abstract ViewGroup getCurrentAdapterView(); + abstract ViewGroup getActiveAdapterView(); + + abstract @Nullable ViewGroup getInactiveAdapterView(); protected class ProfileDescriptor { final ViewGroup rootView; @@ -160,4 +181,15 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { this.rootView = rootView; } } + + 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); + } }
\ No newline at end of file diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 9532faecb4df..8bbc343fa4ca 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -508,7 +508,6 @@ public class ChooserActivity extends ResolverActivity implements protected void onCreate(Bundle savedInstanceState) { final long intentReceivedTime = System.currentTimeMillis(); // This is the only place this value is being set. Effectively final. - //TODO(arangelov) - should there be a mIsAppPredictorComponentAvailable flag for work tab? mIsAppPredictorComponentAvailable = isAppPredictionServiceAvailable(); mIsSuccessfullySelected = false; @@ -689,29 +688,6 @@ public class ChooserActivity extends ResolverActivity implements mResolverDrawerLayout.setOnScrollChangeListener(this::handleScroll); } - final View chooserHeader = mResolverDrawerLayout.findViewById(R.id.chooser_header); - final float defaultElevation = chooserHeader.getElevation(); - final float chooserHeaderScrollElevation = - getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation); - - mChooserMultiProfilePagerAdapter.getCurrentAdapterView().addOnScrollListener( - new RecyclerView.OnScrollListener() { - public void onScrollStateChanged(RecyclerView view, int scrollState) { - } - - public void onScrolled(RecyclerView view, int dx, int dy) { - if (view.getChildCount() > 0) { - View child = view.getLayoutManager().findViewByPosition(0); - if (child == null || child.getTop() < 0) { - chooserHeader.setElevation(chooserHeaderScrollElevation); - return; - } - } - - chooserHeader.setElevation(defaultElevation); - } - }); - mResolverDrawerLayout.setOnCollapsedChangedListener( new ResolverDrawerLayout.OnCollapsedChangedListener() { @@ -1330,8 +1306,8 @@ public class ChooserActivity extends ResolverActivity implements } @Override - public void onPrepareAdapterView(ResolverListAdapter adapter) { - mChooserMultiProfilePagerAdapter.getCurrentAdapterView().setVisibility(View.VISIBLE); + public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) { + mChooserMultiProfilePagerAdapter.getActiveAdapterView().setVisibility(View.VISIBLE); if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) { mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults( /* origTarget */ null, @@ -2202,7 +2178,7 @@ public class ChooserActivity extends ResolverActivity implements if (mChooserMultiProfilePagerAdapter == null) { return; } - RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getCurrentAdapterView(); + RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter(); if (gridAdapter == null || recyclerView == null) { return; @@ -2328,6 +2304,8 @@ public class ChooserActivity extends ResolverActivity implements @Override public void onListRebuilt(ResolverListAdapter listAdapter) { + setupScrollListener(); + ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter; if (chooserListAdapter.mDisplayList == null || chooserListAdapter.mDisplayList.isEmpty()) { @@ -2368,6 +2346,34 @@ public class ChooserActivity extends ResolverActivity implements } } + private void setupScrollListener() { + if (mResolverDrawerLayout == null) { + return; + } + final View chooserHeader = mResolverDrawerLayout.findViewById(R.id.chooser_header); + final float defaultElevation = chooserHeader.getElevation(); + final float chooserHeaderScrollElevation = + getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation); + + mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener( + new RecyclerView.OnScrollListener() { + public void onScrollStateChanged(RecyclerView view, int scrollState) { + } + + public void onScrolled(RecyclerView view, int dx, int dy) { + if (view.getChildCount() > 0) { + View child = view.getLayoutManager().findViewByPosition(0); + if (child == null || child.getTop() < 0) { + chooserHeader.setElevation(chooserHeaderScrollElevation); + return; + } + } + + chooserHeader.setElevation(defaultElevation); + } + }); + } + @Override // ChooserListCommunicator public boolean isSendAction(Intent targetIntent) { if (targetIntent == null) { @@ -2475,7 +2481,8 @@ public class ChooserActivity extends ResolverActivity implements * row level by this adapter but not on the item level. Individual targets within the row are * handled by {@link ChooserListAdapter} */ - final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + @VisibleForTesting + public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private ChooserListAdapter mChooserListAdapter; private final LayoutInflater mLayoutInflater; @@ -2905,7 +2912,7 @@ public class ChooserActivity extends ResolverActivity implements if (mDirectShareViewHolder != null && canExpandDirectShare) { mDirectShareViewHolder.handleScroll( - mChooserMultiProfilePagerAdapter.getCurrentAdapterView(), y, oldy, + mChooserMultiProfilePagerAdapter.getActiveAdapterView(), y, oldy, getMaxTargetsPerRow()); } } diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index a8a676d03971..6ff844a2eaae 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -246,11 +246,6 @@ public class ChooserListAdapter extends ResolverListAdapter { } @Override - public boolean shouldGetResolvedFilter() { - return true; - } - - @Override public int getCount() { return getRankedTargetCount() + getAlphaTargetCount() + getSelectableServiceTargetCount() + getCallerTargetCount(); diff --git a/core/java/com/android/internal/app/ChooserMultiProfilePagerAdapter.java b/core/java/com/android/internal/app/ChooserMultiProfilePagerAdapter.java index 7d856e1b945d..1c52d0e8f9f1 100644 --- a/core/java/com/android/internal/app/ChooserMultiProfilePagerAdapter.java +++ b/core/java/com/android/internal/app/ChooserMultiProfilePagerAdapter.java @@ -16,6 +16,7 @@ package com.android.internal.app; +import android.annotation.Nullable; import android.content.Context; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -77,7 +78,8 @@ public class ChooserMultiProfilePagerAdapter extends AbstractMultiProfilePagerAd } @Override - ChooserActivity.ChooserGridAdapter getAdapterForIndex(int pageIndex) { + @VisibleForTesting + public ChooserActivity.ChooserGridAdapter getAdapterForIndex(int pageIndex) { return mItems[pageIndex].chooserGridAdapter; } @@ -121,10 +123,19 @@ public class ChooserMultiProfilePagerAdapter extends AbstractMultiProfilePagerAd } @Override - RecyclerView getCurrentAdapterView() { + RecyclerView getActiveAdapterView() { return getListViewForIndex(getCurrentPage()); } + @Override + @Nullable + RecyclerView getInactiveAdapterView() { + if (getCount() == 1) { + return null; + } + return getListViewForIndex(1 - getCurrentPage()); + } + class ChooserProfileDescriptor extends ProfileDescriptor { private ChooserActivity.ChooserGridAdapter chooserGridAdapter; private RecyclerView recyclerView; diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index b2417b2e79cc..68d6e03f654c 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -59,6 +59,7 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import android.util.Slog; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -67,9 +68,12 @@ import android.view.WindowInsets; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ListView; import android.widget.Space; +import android.widget.TabHost; +import android.widget.TabWidget; import android.widget.TextView; import android.widget.Toast; @@ -82,6 +86,7 @@ import com.android.internal.content.PackageMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.widget.ResolverDrawerLayout; +import com.android.internal.widget.ViewPager; import java.util.ArrayList; import java.util.Arrays; @@ -147,7 +152,10 @@ public class ResolverActivity extends Activity implements /** * TODO(arangelov): Remove a couple of weeks after work/personal tabs are finalized. */ - static final boolean ENABLE_TABBED_VIEW = false; + @VisibleForTesting + public static boolean ENABLE_TABBED_VIEW = false; + private static final String TAB_TAG_PERSONAL = "personal"; + private static final String TAB_TAG_WORK = "work"; private final PackageMonitor mPackageMonitor = createPackageMonitor(); @@ -418,12 +426,16 @@ public class ResolverActivity extends Activity implements Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed) { + // We only show the default app for the profile of the current user. The filterLastUsed + // flag determines whether to show a default app and that app is not shown in the + // resolver list. So filterLastUsed should be false for the other profile. ResolverListAdapter personalAdapter = createResolverListAdapter( /* context */ this, /* payloadIntents */ mIntents, initialIntents, rList, - filterLastUsed, + (filterLastUsed && UserHandle.myUserId() + == getPersonalProfileUserHandle().getIdentifier()), mUseLayoutForBrowsables, /* userHandle */ getPersonalProfileUserHandle()); ResolverListAdapter workAdapter = createResolverListAdapter( @@ -431,7 +443,8 @@ public class ResolverActivity extends Activity implements /* payloadIntents */ mIntents, initialIntents, rList, - filterLastUsed, + (filterLastUsed && UserHandle.myUserId() + == getWorkProfileUserHandle().getIdentifier()), mUseLayoutForBrowsables, /* userHandle */ getWorkProfileUserHandle()); return new ResolverMultiProfilePagerAdapter( @@ -495,12 +508,12 @@ public class ResolverActivity extends Activity implements mFooterSpacer = new Space(getApplicationContext()); } else { ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter) - .getCurrentAdapterView().removeFooterView(mFooterSpacer); + .getActiveAdapterView().removeFooterView(mFooterSpacer); } mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, mSystemWindowInsets.bottom)); ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter) - .getCurrentAdapterView().addFooterView(mFooterSpacer); + .getActiveAdapterView().addFooterView(mFooterSpacer); } protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { @@ -817,7 +830,7 @@ public class ResolverActivity extends Activity implements public void onButtonClick(View v) { final int id = v.getId(); - ListView listView = (ListView) mMultiProfilePagerAdapter.getCurrentAdapterView(); + ListView listView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView(); ResolverListAdapter currentListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter(); int which = currentListAdapter.hasFilteredItem() ? currentListAdapter.getFilteredPosition() @@ -898,7 +911,10 @@ public class ResolverActivity extends Activity implements @Override // ResolverListCommunicator public void onPostListReady(ResolverListAdapter listAdapter) { - setHeader(); + if (mMultiProfilePagerAdapter.getCurrentUserHandle().getIdentifier() + == UserHandle.myUserId()) { + setHeader(); + } resetButtonBar(); onListRebuilt(listAdapter); } @@ -913,6 +929,9 @@ public class ResolverActivity extends Activity implements finish(); } } + + final ItemClickListener listener = new ItemClickListener(); + setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener); } protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { @@ -1094,6 +1113,7 @@ public class ResolverActivity extends Activity implements return true; } + @VisibleForTesting public void safelyStartActivity(TargetInfo cti) { // We're dispatching intents that might be coming from legacy apps, so // don't kill ourselves. @@ -1222,9 +1242,6 @@ public class ResolverActivity extends Activity implements + "cannot be null."); } boolean rebuildCompleted = mMultiProfilePagerAdapter.getActiveListAdapter().rebuildList(); - if (mMultiProfilePagerAdapter.getInactiveListAdapter() != null) { - mMultiProfilePagerAdapter.getInactiveListAdapter().rebuildList(); - } if (useLayoutWithDefault()) { mLayoutId = R.layout.resolver_list_with_default; } else { @@ -1272,45 +1289,99 @@ public class ResolverActivity extends Activity implements } } - setupViewVisibilities(count); + setupViewVisibilities(); + + if (hasWorkProfile() && ENABLE_TABBED_VIEW) { + setupProfileTabs(); + } + return false; } - private void setupViewVisibilities(int count) { - if (count == 0 - && mMultiProfilePagerAdapter.getActiveListAdapter().getPlaceholderCount() == 0) { - final TextView emptyView = findViewById(R.id.empty); - emptyView.setVisibility(View.VISIBLE); - findViewById(R.id.profile_pager).setVisibility(View.GONE); - } else { - onPrepareAdapterView(mMultiProfilePagerAdapter.getActiveListAdapter()); + private void setupProfileTabs() { + TabHost tabHost = findViewById(R.id.profile_tabhost); + tabHost.setup(); + ViewPager viewPager = findViewById(R.id.profile_pager); + TabHost.TabSpec tabSpec = tabHost.newTabSpec(TAB_TAG_PERSONAL) + .setContent(R.id.profile_pager) + .setIndicator(getString(R.string.resolver_personal_tab)); + tabHost.addTab(tabSpec); + + tabSpec = tabHost.newTabSpec(TAB_TAG_WORK) + .setContent(R.id.profile_pager) + .setIndicator(getString(R.string.resolver_work_tab)); + tabHost.addTab(tabSpec); + + TabWidget tabWidget = tabHost.getTabWidget(); + tabWidget.setVisibility(View.VISIBLE); + resetTabsHeaderStyle(tabWidget); + updateActiveTabStyle(tabHost); + + tabHost.setOnTabChangedListener(tabId -> { + resetTabsHeaderStyle(tabWidget); + updateActiveTabStyle(tabHost); + if (TAB_TAG_PERSONAL.equals(tabId)) { + viewPager.setCurrentItem(0); + } else { + viewPager.setCurrentItem(1); + } + setupViewVisibilities(); + }); + + viewPager.setVisibility(View.VISIBLE); + tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage()); + mMultiProfilePagerAdapter.setOnProfileSelectedListener(tabHost::setCurrentTab); + } + + private void resetTabsHeaderStyle(TabWidget tabWidget) { + for (int i = 0; i < tabWidget.getChildCount(); i++) { + TextView title = tabWidget.getChildAt(i).findViewById(android.R.id.title); + title.setTextColor(getColor(R.color.resolver_tabs_inactive_color)); + title.setAllCaps(false); + } + } + + private void updateActiveTabStyle(TabHost tabHost) { + TextView title = tabHost.getTabWidget().getChildAt(tabHost.getCurrentTab()) + .findViewById(android.R.id.title); + title.setTextColor(getColor(R.color.resolver_tabs_active_color)); + } + + private void setupViewVisibilities() { + int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); + boolean shouldShowEmptyState = count == 0 + && mMultiProfilePagerAdapter.getActiveListAdapter().getPlaceholderCount() == 0; + //TODO(arangelov): Handle empty state + if (!shouldShowEmptyState) { + addUseDifferentAppLabelIfNecessary(mMultiProfilePagerAdapter.getActiveListAdapter()); } } /** - * Prepare the scrollable view which consumes data in the list adapter. + * Add a label to signify that the user can pick a different app. * @param adapter The adapter used to provide data to item views. */ - public void onPrepareAdapterView(ResolverListAdapter adapter) { - mMultiProfilePagerAdapter.getCurrentAdapterView().setVisibility(View.VISIBLE); + public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) { final boolean useHeader = adapter.hasFilteredItem(); - final ListView listView = (ListView) mMultiProfilePagerAdapter.getCurrentAdapterView(); - final ItemClickListener listener = new ItemClickListener(); + if (useHeader) { + FrameLayout stub = findViewById(R.id.stub); + stub.setVisibility(View.VISIBLE); + TextView textView = (TextView) LayoutInflater.from(this).inflate( + R.layout.resolver_different_item_header, null, false); + if (ENABLE_TABBED_VIEW) { + textView.setGravity(Gravity.CENTER); + } + stub.addView(textView); + } + } + + private void setupAdapterListView(ListView listView, ItemClickListener listener) { listView.setOnItemClickListener(listener); listView.setOnItemLongClickListener(listener); if (mSupportsAlwaysUseOption || mUseLayoutForBrowsables) { listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); } - - // In case this method is called again (due to activity recreation), avoid adding a new - // header if one is already present. - if (useHeader && listView.getHeaderViewsCount() == 0) { - listView.setHeaderDividersEnabled(true); - listView.addHeaderView(LayoutInflater.from(this).inflate( - R.layout.resolver_different_item_header, listView, false), - null, false); - } } /** @@ -1378,7 +1449,7 @@ public class ResolverActivity extends Activity implements } // When the items load in, if an item was already selected, enable the buttons - ListView currentAdapterView = (ListView) mMultiProfilePagerAdapter.getCurrentAdapterView(); + ListView currentAdapterView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView(); if (currentAdapterView != null && currentAdapterView.getCheckedItemPosition() != ListView.INVALID_POSITION) { setAlwaysButtonEnabled(true, currentAdapterView.getCheckedItemPosition(), true); @@ -1388,8 +1459,18 @@ public class ResolverActivity extends Activity implements @Override // ResolverListCommunicator public boolean useLayoutWithDefault() { - return mSupportsAlwaysUseOption - && mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem(); + // We only use the default app layout when the profile of the active user has a + // filtered item. We always show the same default app even in the inactive user profile. + boolean currentUserAdapterHasFilteredItem; + if (mMultiProfilePagerAdapter.getCurrentUserHandle().getIdentifier() + == UserHandle.myUserId()) { + currentUserAdapterHasFilteredItem = + mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem(); + } else { + currentUserAdapterHasFilteredItem = + mMultiProfilePagerAdapter.getInactiveListAdapter().hasFilteredItem(); + } + return mSupportsAlwaysUseOption && currentUserAdapterHasFilteredItem; } /** @@ -1494,9 +1575,8 @@ public class ResolverActivity extends Activity implements .resolveInfoForPosition(position, true) == null) { return; } - ListView currentAdapterView = - (ListView) mMultiProfilePagerAdapter.getCurrentAdapterView(); + (ListView) mMultiProfilePagerAdapter.getActiveAdapterView(); final int checkedPos = currentAdapterView.getCheckedItemPosition(); final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION; if (!useLayoutWithDefault() diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index ef7a347cf7be..d227ec5c1b38 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -193,7 +193,8 @@ public class ResolverListAdapter extends BaseAdapter { mBaseResolveList); } else { currentResolveList = mUnfilteredResolveList = - mResolverListController.getResolversForIntent(shouldGetResolvedFilter(), + mResolverListController.getResolversForIntent( + /* shouldGetResolvedFilter= */ true, mResolverListCommunicator.shouldGetActivityMetadata(), mIntents); if (currentResolveList == null) { @@ -363,10 +364,6 @@ public class ResolverListAdapter extends BaseAdapter { } } - public boolean shouldGetResolvedFilter() { - return mFilterLastUsed; - } - private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) { final int count = rci.getCount(); final Intent intent = rci.getIntentAt(0); diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java index abd3eb2453df..0bfe9eb04d28 100644 --- a/core/java/com/android/internal/app/ResolverListController.java +++ b/core/java/com/android/internal/app/ResolverListController.java @@ -133,7 +133,8 @@ public class ResolverListController { return resolvedComponents; } - UserHandle getUserHandle() { + @VisibleForTesting + public UserHandle getUserHandle() { return mUserHandle; } diff --git a/core/java/com/android/internal/app/ResolverMultiProfilePagerAdapter.java b/core/java/com/android/internal/app/ResolverMultiProfilePagerAdapter.java index d72c52bfe6b6..567ed74670bf 100644 --- a/core/java/com/android/internal/app/ResolverMultiProfilePagerAdapter.java +++ b/core/java/com/android/internal/app/ResolverMultiProfilePagerAdapter.java @@ -16,6 +16,7 @@ package com.android.internal.app; +import android.annotation.Nullable; import android.content.Context; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -81,7 +82,8 @@ public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerA } @Override - ResolverListAdapter getAdapterForIndex(int pageIndex) { + @VisibleForTesting + public ResolverListAdapter getAdapterForIndex(int pageIndex) { return mItems[pageIndex].resolverListAdapter; } @@ -106,10 +108,19 @@ public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerA } @Override - ListView getCurrentAdapterView() { + ListView getActiveAdapterView() { return getListViewForIndex(getCurrentPage()); } + @Override + @Nullable + ViewGroup getInactiveAdapterView() { + if (getCount() == 1) { + return null; + } + return getListViewForIndex(1 - getCurrentPage()); + } + class ResolverProfileDescriptor extends ProfileDescriptor { private ResolverListAdapter resolverListAdapter; final ListView listView; diff --git a/core/java/com/android/internal/app/WrapHeightViewPager.java b/core/java/com/android/internal/app/ResolverViewPager.java index b017bb44d751..8ec94f159725 100644 --- a/core/java/com/android/internal/app/WrapHeightViewPager.java +++ b/core/java/com/android/internal/app/ResolverViewPager.java @@ -18,39 +18,36 @@ package com.android.internal.app; import android.content.Context; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import com.android.internal.widget.ViewPager; /** - * A {@link ViewPager} which wraps around its first child's height. + * A {@link ViewPager} which wraps around its first child's height and has swiping disabled. * <p>Normally {@link ViewPager} instances expand their height to cover all remaining space in * the layout. - * <p>This class is used for the intent resolver picker's tabbed view to maintain - * consistency with the previous behavior. + * <p>This class is used for the intent resolver and share sheet's tabbed view. */ -public class WrapHeightViewPager extends ViewPager { +public class ResolverViewPager extends ViewPager { - public WrapHeightViewPager(Context context) { + public ResolverViewPager(Context context) { super(context); } - public WrapHeightViewPager(Context context, AttributeSet attrs) { + public ResolverViewPager(Context context, AttributeSet attrs) { super(context, attrs); } - public WrapHeightViewPager(Context context, AttributeSet attrs, int defStyleAttr) { + public ResolverViewPager(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } - public WrapHeightViewPager(Context context, AttributeSet attrs, + public ResolverViewPager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } - // TODO(arangelov): When we have multiple pages, the height should wrap to the currently - // displayed page. Investigate whether onMeasure is called when changing a page, and instead - // of getChildAt(0), use the currently displayed one. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); @@ -68,4 +65,14 @@ public class WrapHeightViewPager extends ViewPager { heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return false; + } } diff --git a/core/res/res/layout/chooser_grid.xml b/core/res/res/layout/chooser_grid.xml index 0c45e45e7980..4e8a41f81c48 100644 --- a/core/res/res/layout/chooser_grid.xml +++ b/core/res/res/layout/chooser_grid.xml @@ -61,10 +61,33 @@ android:layout_height="wrap_content" android:visibility="gone" /> - <com.android.internal.widget.ViewPager - android:id="@+id/profile_pager" + <TabHost + android:id="@+id/profile_tabhost" android:layout_width="match_parent" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:background="?attr/colorBackgroundFloating"> + <LinearLayout + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TabWidget + android:id="@android:id/tabs" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + </TabWidget> + <FrameLayout + android:id="@android:id/tabcontent" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <com.android.internal.app.ResolverViewPager + android:id="@+id/profile_pager" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + </FrameLayout> + </LinearLayout> + </TabHost> <TextView android:id="@+id/empty" android:layout_width="match_parent" diff --git a/core/res/res/layout/resolver_list.xml b/core/res/res/layout/resolver_list.xml index c5d891254227..757cd539152d 100644 --- a/core/res/res/layout/resolver_list.xml +++ b/core/res/res/layout/resolver_list.xml @@ -70,14 +70,44 @@ android:background="?attr/colorBackgroundFloating" android:foreground="?attr/dividerVertical" /> - <com.android.internal.app.WrapHeightViewPager - android:id="@+id/profile_pager" + <FrameLayout + android:id="@+id/stub" + android:visibility="gone" android:layout_width="match_parent" android:layout_height="wrap_content" - android:divider="?attr/dividerVertical" - android:footerDividersEnabled="false" - android:headerDividersEnabled="false" - android:dividerHeight="1dp"/> + android:background="?attr/colorBackgroundFloating"/> + + <TabHost + android:id="@+id/profile_tabhost" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:background="?attr/colorBackgroundFloating"> + <LinearLayout + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TabWidget + android:id="@android:id/tabs" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + </TabWidget> + <FrameLayout + android:id="@android:id/tabcontent" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <com.android.internal.app.ResolverViewPager + android:id="@+id/profile_pager" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:divider="?attr/dividerVertical" + android:footerDividersEnabled="false" + android:headerDividersEnabled="false" + android:dividerHeight="1dp"/> + </FrameLayout> + </LinearLayout> + </TabHost> <View android:layout_alwaysShow="true" diff --git a/core/res/res/layout/resolver_list_per_profile.xml b/core/res/res/layout/resolver_list_per_profile.xml index 68b991755e73..6d8d3480dc8c 100644 --- a/core/res/res/layout/resolver_list_per_profile.xml +++ b/core/res/res/layout/resolver_list_per_profile.xml @@ -25,7 +25,7 @@ android:nestedScrollingEnabled="true" android:scrollbarStyle="outsideOverlay" android:scrollIndicators="top|bottom" - android:divider="?attr/dividerVertical" + android:divider="@null" android:footerDividersEnabled="false" android:headerDividersEnabled="false" - android:dividerHeight="1dp" />
\ No newline at end of file + android:dividerHeight="0dp" />
\ No newline at end of file diff --git a/core/res/res/layout/resolver_list_with_default.xml b/core/res/res/layout/resolver_list_with_default.xml index 5b3d929d23a5..b54673834ea9 100644 --- a/core/res/res/layout/resolver_list_with_default.xml +++ b/core/res/res/layout/resolver_list_with_default.xml @@ -151,14 +151,46 @@ android:background="?attr/colorBackgroundFloating" android:foreground="?attr/dividerVertical" /> - <com.android.internal.app.WrapHeightViewPager - android:id="@+id/profile_pager" + <FrameLayout + android:id="@+id/stub" + android:visibility="gone" android:layout_width="match_parent" android:layout_height="wrap_content" - android:dividerHeight="1dp" - android:divider="?attr/dividerVertical" - android:footerDividersEnabled="false" - android:headerDividersEnabled="false"/> + android:background="?attr/colorBackgroundFloating"/> + + <TabHost + android:id="@+id/profile_tabhost" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:background="?attr/colorBackgroundFloating"> + <LinearLayout + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TabWidget + android:id="@android:id/tabs" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="gone"> + </TabWidget> + <FrameLayout + android:id="@android:id/tabcontent" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <com.android.internal.app.ResolverViewPager + android:id="@+id/profile_pager" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:dividerHeight="1dp" + android:divider="?attr/dividerVertical" + android:footerDividersEnabled="false" + android:headerDividersEnabled="false"/> + </FrameLayout> + </LinearLayout> + </TabHost> + <View android:layout_alwaysShow="true" android:layout_width="match_parent" diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index 1dcd389d9d8f..731e2ec95f9e 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -224,4 +224,6 @@ <!-- Resolver/Chooser --> <color name="resolver_text_color_secondary_dark">#ffC4C6C6</color> + <color name="resolver_tabs_active_color">#FF1A73E8</color> + <color name="resolver_tabs_inactive_color">#FF80868B</color> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 0cdd02cc9e0f..be2b678565d3 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5325,4 +5325,8 @@ <!-- Text to tell the user that a package has been forced by themselves in the RESTRICTED bucket. [CHAR LIMIT=NONE] --> <string name="as_app_forced_to_restricted_bucket"> <xliff:g id="package_name" example="com.android.example">%1$s</xliff:g> has been put into the RESTRICTED bucket</string> + + <!-- ResolverActivity - profile tabs --> + <string name="resolver_personal_tab">Personal</string> + <string name="resolver_work_tab">Work</string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8f2174562781..ea8ca9aa1c49 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -249,6 +249,9 @@ <java-symbol type="id" name="app_ops" /> <java-symbol type="id" name="profile_pager" /> <java-symbol type="id" name="content_preview_container" /> + <java-symbol type="id" name="profile_tabhost" /> + <java-symbol type="id" name="tabs" /> + <java-symbol type="id" name="tabcontent" /> <java-symbol type="attr" name="actionModeShareDrawable" /> <java-symbol type="attr" name="alertDialogCenterButtons" /> @@ -3836,4 +3839,12 @@ <java-symbol type="array" name="config_defaultImperceptibleKillingExemptionPkgs" /> <java-symbol type="array" name="config_defaultImperceptibleKillingExemptionProcStates" /> + + <!-- Intent resolver and share sheet --> + <java-symbol type="color" name="resolver_tabs_active_color" /> + <java-symbol type="color" name="resolver_tabs_inactive_color" /> + <java-symbol type="string" name="resolver_personal_tab" /> + <java-symbol type="string" name="resolver_work_tab" /> + <java-symbol type="id" name="stub" /> + </resources> diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index c086421501b5..411868d8befe 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -31,6 +31,7 @@ import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FRO import static com.android.internal.app.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST; import static com.android.internal.app.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST; import static com.android.internal.app.ChooserWrapperActivity.sOverrides; +import static com.android.internal.app.MatcherUtils.first; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; @@ -63,6 +64,8 @@ import android.graphics.Paint; import android.graphics.drawable.Icon; import android.metrics.LogMaker; import android.net.Uri; +import android.os.Bundle; +import android.os.UserHandle; import android.service.chooser.ChooserTarget; import androidx.test.platform.app.InstrumentationRegistry; @@ -74,7 +77,11 @@ import com.android.internal.app.chooser.DisplayResolveInfo; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -302,6 +309,7 @@ public class ChooserActivityTest { assertThat(activity.getIsSelected(), is(true)); } + @Ignore // b/148158199 @Test public void noResultsFromPackageManager() { when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), @@ -346,6 +354,9 @@ public class ChooserActivityTest { @Test public void hasOtherProfileOneOption() throws Exception { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(2); @@ -372,9 +383,7 @@ public class ChooserActivityTest { // Make a stable copy of the components as the original list may be modified List<ResolvedComponentInfo> stableCopy = createResolvedComponentsForTestWithOtherProfile(2); - // Check that the "Other Profile" activity is put in the right spot - onView(withId(R.id.profile_button)).check(matches( - withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); + waitForIdle(); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); waitForIdle(); @@ -383,6 +392,9 @@ public class ChooserActivityTest { @Test public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3); @@ -411,9 +423,6 @@ public class ChooserActivityTest { // Make a stable copy of the components as the original list may be modified List<ResolvedComponentInfo> stableCopy = createResolvedComponentsForTestWithOtherProfile(3); - // Check that the "Other Profile" activity is put in the right spot - onView(withId(R.id.profile_button)).check(matches( - withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); waitForIdle(); @@ -422,6 +431,9 @@ public class ChooserActivityTest { @Test public void hasLastChosenActivityAndOtherProfile() throws Exception { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3); @@ -448,9 +460,6 @@ public class ChooserActivityTest { // Make a stable copy of the components as the original list may be modified List<ResolvedComponentInfo> stableCopy = createResolvedComponentsForTestWithOtherProfile(3); - // Check that the "Other Profile" activity is put in the right spot - onView(withId(R.id.profile_button)).check(matches( - withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); waitForIdle(); @@ -1161,6 +1170,123 @@ public class ChooserActivityTest { .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1)); } + @Test + public void testWorkTab_displayedWhenWorkProfileUserAvailable() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + markWorkProfileUserAvailable(); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + + onView(withId(R.id.tabs)).check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + + onView(withId(R.id.tabs)).check(matches(not(isDisplayed()))); + } + + @Test + public void testWorkTab_eachTabUsesExpectedAdapter() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + int personalProfileTargets = 3; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(personalProfileTargets); + int workProfileTargets = 4; + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest( + workProfileTargets); + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(personalResolvedComponentInfos); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + markWorkProfileUserAvailable(); + + final ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); + // The work list adapter must only be filled when we open the work tab + assertThat(activity.getWorkListAdapter().getCount(), is(0)); + onView(withText(R.string.resolver_work_tab)).perform(click()); + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getPersonalListAdapter().getCount(), is(personalProfileTargets)); + assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); + } + + @Test + public void testWorkTab_workProfileHasExpectedNumberOfTargets() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + + final ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); + } + + @Ignore // b/148156663 + @Test + public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + // wait for the share sheet to expand + Thread.sleep(ChooserActivity.LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS); + + onView(first(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name))) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + private Intent createSendTextIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -1224,6 +1350,15 @@ public class ChooserActivityTest { return infoList; } + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithUserId( + int numberOfResults, int userId) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + } + return infoList; + } + private List<ChooserTarget> createDirectShareTargets(int numberOfResults, String packageName) { Icon icon = Icon.createWithBitmap(createBitmap()); String testTitle = "testTitle"; @@ -1308,4 +1443,8 @@ public class ChooserActivityTest { assertEquals(cn.flattenToString(), ct.getComponentName().flattenToString()); } } + + private void markWorkProfileUserAvailable() { + sOverrides.workProfileUserHandle = UserHandle.of(10); + } } diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java index 2a1044361d42..eee62bb791bf 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java @@ -17,6 +17,7 @@ package com.android.internal.app; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import android.annotation.Nullable; import android.app.usage.UsageStatsManager; @@ -29,6 +30,7 @@ import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; +import android.os.Bundle; import android.os.UserHandle; import android.util.Size; @@ -51,6 +53,19 @@ public class ChooserWrapperActivity extends ChooserActivity { return mChooserMultiProfilePagerAdapter.getActiveListAdapter(); } + ChooserListAdapter getPersonalListAdapter() { + return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0)) + .getListAdapter(); + } + + ChooserListAdapter getWorkListAdapter() { + if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { + return null; + } + return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1)) + .getListAdapter(); + } + boolean getIsSelected() { return mIsSuccessfullySelected; } UsageStatsManager getUsageStatsManager() { @@ -79,7 +94,12 @@ public class ChooserWrapperActivity extends ChooserActivity { @Override protected ResolverListController createListController(UserHandle userHandle) { - return sOverrides.resolverListController; + if (userHandle == UserHandle.SYSTEM) { + when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM); + return sOverrides.resolverListController; + } + when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); + return sOverrides.workResolverListController; } @Override @@ -144,6 +164,15 @@ public class ChooserWrapperActivity extends ChooserActivity { resolveInfoPresentationGetter); } + @Override + protected UserHandle getWorkProfileUserHandle() { + return sOverrides.workProfileUserHandle; + } + + protected UserHandle getCurrentUserHandle() { + return mMultiProfilePagerAdapter.getCurrentUserHandle(); + } + /** * We cannot directly mock the activity created since instrumentation creates it. * <p> @@ -154,6 +183,7 @@ public class ChooserWrapperActivity extends ChooserActivity { public Function<PackageManager, PackageManager> createPackageManager; public Function<TargetInfo, Boolean> onSafelyStartCallback; public ResolverListController resolverListController; + public ResolverListController workResolverListController; public Boolean isVoiceInteraction; public boolean isImageType; public Cursor resolverCursor; @@ -162,6 +192,7 @@ public class ChooserWrapperActivity extends ChooserActivity { public MetricsLogger metricsLogger; public int alternateProfileSetting; public Resources resources; + public UserHandle workProfileUserHandle; public void reset() { onSafelyStartCallback = null; @@ -172,9 +203,11 @@ public class ChooserWrapperActivity extends ChooserActivity { resolverCursor = null; resolverForceException = false; resolverListController = mock(ResolverListController.class); + workResolverListController = mock(ResolverListController.class); metricsLogger = mock(MetricsLogger.class); alternateProfileSetting = 0; resources = null; + workProfileUserHandle = null; } } } diff --git a/core/tests/coretests/src/com/android/internal/app/MatcherUtils.java b/core/tests/coretests/src/com/android/internal/app/MatcherUtils.java new file mode 100644 index 000000000000..a4766318a21e --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/MatcherUtils.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 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.internal.app; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +/** + * Utils for helping with more customized matching options, for example matching the first + * occurrence of a set criteria. + */ +public class MatcherUtils { + + /** + * Returns a {@link Matcher} which only matches the first occurrence of a set criteria. + */ + static <T> Matcher<T> first(final Matcher<T> matcher) { + return new BaseMatcher<T>() { + boolean isFirstMatch = true; + + @Override + public boolean matches(final Object item) { + if (isFirstMatch && matcher.matches(item)) { + isFirstMatch = false; + return true; + } + return false; + } + + @Override + public void describeTo(final Description description) { + description.appendText("Returns the first matching item"); + } + }; + } +} diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java index 923ce3e3935d..42f7736d37b0 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java @@ -19,13 +19,17 @@ package com.android.internal.app; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.android.internal.app.MatcherUtils.first; import static com.android.internal.app.ResolverDataProvider.createPackageManagerMockedInfo; import static com.android.internal.app.ResolverWrapperActivity.sOverrides; +import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; @@ -33,6 +37,7 @@ import static org.mockito.Mockito.when; import android.content.Intent; import android.content.pm.ResolveInfo; +import android.os.UserHandle; import android.text.TextUtils; import android.view.View; import android.widget.RelativeLayout; @@ -49,6 +54,9 @@ import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGett import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter; import com.android.internal.widget.ResolverDrawerLayout; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; @@ -212,6 +220,9 @@ public class ResolverActivityTest { @Test public void hasOtherProfileOneOption() throws Exception { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + Intent sendIntent = createSendImageIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(2); @@ -237,9 +248,6 @@ public class ResolverActivityTest { // Make a stable copy of the components as the original list may be modified List<ResolvedComponentInfo> stableCopy = createResolvedComponentsForTestWithOtherProfile(2); - // Check that the "Other Profile" activity is put in the right spot - onView(withId(R.id.profile_button)).check(matches( - withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); onView(withId(R.id.button_once)) @@ -250,6 +258,9 @@ public class ResolverActivityTest { @Test public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + Intent sendIntent = createSendImageIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3); @@ -279,9 +290,6 @@ public class ResolverActivityTest { List<ResolvedComponentInfo> stableCopy = createResolvedComponentsForTestWithOtherProfile(2); - // Check that the "Other Profile" activity is put in the right spot - onView(withId(R.id.profile_button)).check(matches( - withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); onView(withId(R.id.button_once)).perform(click()); @@ -292,6 +300,9 @@ public class ResolverActivityTest { @Test public void hasLastChosenActivityAndOtherProfile() throws Exception { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + // In this case we prefer the other profile and don't display anything about the last // chosen activity. Intent sendIntent = createSendImageIntent(); @@ -325,9 +336,6 @@ public class ResolverActivityTest { List<ResolvedComponentInfo> stableCopy = createResolvedComponentsForTestWithOtherProfile(2); - // Check that the "Other Profile" activity is put in the right spot - onView(withId(R.id.profile_button)).check(matches( - withText(stableCopy.get(0).getResolveInfoAt(0).activityInfo.name))); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); onView(withId(R.id.button_once)).perform(click()); @@ -379,6 +387,222 @@ public class ResolverActivityTest { TextUtils.isEmpty(pg.getSubLabel())); } + @Test + public void testWorkTab_displayedWhenWorkProfileUserAvailable() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + Intent sendIntent = createSendImageIntent(); + markWorkProfileUserAvailable(); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withId(R.id.tabs)).check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + Intent sendIntent = createSendImageIntent(); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withId(R.id.tabs)).check(matches(not(isDisplayed()))); + } + + @Test + public void testWorkTab_workTabListEmptyBeforeGoingToTab() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(personalResolvedComponentInfos); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + markWorkProfileUserAvailable(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); + // The work list adapter must only be filled when we open the work tab + assertThat(activity.getWorkListAdapter().getCount(), is(0)); + } + + @Test + public void testWorkTab_workTabUsesExpectedAdapter() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(personalResolvedComponentInfos); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + markWorkProfileUserAvailable(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_personalTabUsesExpectedAdapter() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(personalResolvedComponentInfos); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + markWorkProfileUserAvailable(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getPersonalListAdapter().getCount(), is(3)); + } + + @Test + public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(personalResolvedComponentInfos); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + + waitForIdle(); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(personalResolvedComponentInfos); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + // wait for the share sheet to expand + Thread.sleep(ChooserActivity.LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS); + onView(first(allOf(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(R.id.button_once)) + .perform(click()); + + waitForIdle(); + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + + @Test + public void testWorkTab_noPersonalApps_workTabHasExpectedNumberOfTargets() + throws InterruptedException { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + + waitForIdle(); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Ignore // b/148156663 + @Test + public void testWorkTab_noPersonalApps_canStartWorkApps() + throws InterruptedException { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + // wait for the share sheet to expand + Thread.sleep(ChooserActivity.LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS); + onView(first(allOf(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + private Intent createSendImageIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -411,4 +635,8 @@ public class ResolverActivityTest { private void waitForIdle() { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } + + private void markWorkProfileUserAvailable() { + ResolverWrapperActivity.sOverrides.workProfileUserHandle = UserHandle.of(10); + } } diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java b/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java index 59634f6d261c..d7db5f8e46eb 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverDataProvider.java @@ -46,6 +46,12 @@ class ResolverDataProvider { createResolverIntent(i), createResolveInfo(i, USER_SOMEONE_ELSE)); } + static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, + int userId) { + return new ResolverActivity.ResolvedComponentInfo(createComponentName(i), + createResolverIntent(i), createResolveInfo(i, userId)); + } + static ComponentName createComponentName(int i) { final String name = "component" + i; return new ComponentName("foo.bar." + name, name); diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java index c5d2cfaa9512..36c8724e522e 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java @@ -17,12 +17,14 @@ package com.android.internal.app; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import android.app.usage.UsageStatsManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.os.Bundle; import android.os.UserHandle; import com.android.internal.app.chooser.TargetInfo; @@ -49,6 +51,17 @@ public class ResolverWrapperActivity extends ResolverActivity { return (ResolverWrapperAdapter) mMultiProfilePagerAdapter.getActiveListAdapter(); } + ResolverListAdapter getPersonalListAdapter() { + return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0)); + } + + ResolverListAdapter getWorkListAdapter() { + if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { + return null; + } + return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1)); + } + @Override public boolean isVoiceInteraction() { if (sOverrides.isVoiceInteraction != null) { @@ -68,7 +81,12 @@ public class ResolverWrapperActivity extends ResolverActivity { @Override protected ResolverListController createListController(UserHandle userHandle) { - return sOverrides.resolverListController; + if (userHandle == UserHandle.SYSTEM) { + when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM); + return sOverrides.resolverListController; + } + when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); + return sOverrides.workResolverListController; } @Override @@ -79,6 +97,20 @@ public class ResolverWrapperActivity extends ResolverActivity { return super.getPackageManager(); } + protected UserHandle getCurrentUserHandle() { + return mMultiProfilePagerAdapter.getCurrentUserHandle(); + } + + @Override + protected UserHandle getWorkProfileUserHandle() { + return sOverrides.workProfileUserHandle; + } + + @Override + public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { + super.startActivityAsUser(intent, options, user); + } + /** * We cannot directly mock the activity created since instrumentation creates it. * <p> @@ -89,13 +121,17 @@ public class ResolverWrapperActivity extends ResolverActivity { public Function<PackageManager, PackageManager> createPackageManager; public Function<TargetInfo, Boolean> onSafelyStartCallback; public ResolverListController resolverListController; + public ResolverListController workResolverListController; public Boolean isVoiceInteraction; + public UserHandle workProfileUserHandle; public void reset() { onSafelyStartCallback = null; isVoiceInteraction = null; createPackageManager = null; resolverListController = mock(ResolverListController.class); + workResolverListController = mock(ResolverListController.class); + workProfileUserHandle = null; } } }
\ No newline at end of file |