From 27608554dc385e94aa2ee210be3fe0f04d346434 Mon Sep 17 00:00:00 2001 From: arangelov Date: Fri, 27 Mar 2020 16:57:50 +0000 Subject: [PATCH] Show empty state screens in order of priority. To improve the user experience, this CL changes the order in which the empty state screens are shown: 1. (highest priority) cross-profile disabled by policy 2. no apps available 3. (least priority) work is off The intention is to prevent the user from having to turn the work profile on if there will not be any apps resolved anyway. Fixes: 150936283 Test: atest ResolverActivityTest Test: atest ChooserActivityTest Test: manually tested each possible combination of edge cases (work profile off, no apps found, cross-profile intents disabled) Change-Id: Ic86c90bdda93cf731df2133890081b5583ef35e1 --- .../app/AbstractMultiProfilePagerAdapter.java | 76 +++++++++++++++---- .../internal/app/ResolverActivity.java | 8 +- .../internal/app/ResolverListController.java | 26 ++++++- .../internal/app/ChooserActivityTest.java | 62 ++++++++++++++- .../internal/app/ResolverActivityTest.java | 64 +++++++++++++++- 5 files changed, 208 insertions(+), 28 deletions(-) diff --git a/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java b/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java index c53516389cd95..9fc0da83c504d 100644 --- a/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java +++ b/core/java/com/android/internal/app/AbstractMultiProfilePagerAdapter.java @@ -300,21 +300,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) { UserHandle listUserHandle = activeListAdapter.getUserHandle(); - if (listUserHandle.equals(mWorkProfileUserHandle) - && mInjector.isQuietModeEnabled(mWorkProfileUserHandle)) { - DevicePolicyEventLogger - .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED) - .setStrings(getMetricsCategory()) - .write(); - showWorkProfileOffEmptyState(activeListAdapter, - v -> { - ProfileDescriptor descriptor = getItem( - userHandleToPageIndex(activeListAdapter.getUserHandle())); - showSpinner(descriptor.getEmptyStateView()); - mInjector.requestQuietModeEnabled(false, mWorkProfileUserHandle); - }); - return false; - } + if (UserHandle.myUserId() != listUserHandle.getIdentifier()) { if (!mInjector.hasCrossProfileIntents(activeListAdapter.getIntents(), UserHandle.myUserId(), listUserHandle.getIdentifier())) { @@ -352,7 +338,65 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { protected abstract void showNoWorkToPersonalIntentsEmptyState( ResolverListAdapter activeListAdapter); - void showNoAppsAvailableEmptyState(ResolverListAdapter listAdapter) { + /** + * 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) { + if (maybeShowWorkProfileOffEmptyState(listAdapter)) { + return; + } + maybeShowNoAppsAvailableEmptyState(listAdapter); + } + + /** + * Returns {@code true} if the work profile off empty state screen is shown. + */ + private boolean maybeShowWorkProfileOffEmptyState(ResolverListAdapter listAdapter) { + UserHandle listUserHandle = listAdapter.getUserHandle(); + if (!listUserHandle.equals(mWorkProfileUserHandle) + || !mInjector.isQuietModeEnabled(mWorkProfileUserHandle) + || !hasResolvedAppsInWorkProfile(listAdapter)) { + return false; + } + DevicePolicyEventLogger + .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED) + .setStrings(getMetricsCategory()) + .write(); + showWorkProfileOffEmptyState(listAdapter, + v -> { + ProfileDescriptor descriptor = getItem( + userHandleToPageIndex(listAdapter.getUserHandle())); + showSpinner(descriptor.getEmptyStateView()); + mInjector.requestQuietModeEnabled(false, mWorkProfileUserHandle); + }); + return true; + } + + /** + * Returns {@code true} if there is at least one app resolved in the work profile, + * regardless of whether the work profile is enabled or not. + */ + private boolean hasResolvedAppsInWorkProfile(ResolverListAdapter listAdapter) { + List userStateIndependentWorkResolvers = + listAdapter.mResolverListController.getUserStateIndependentResolversAsUser( + listAdapter.getIntents(), mWorkProfileUserHandle); + return userStateIndependentWorkResolvers.stream() + .anyMatch(resolvedComponentInfo -> + resolvedComponentInfo.getResolveInfoAt(0).targetUserId + == UserHandle.USER_CURRENT); + } + + private void maybeShowNoAppsAvailableEmptyState(ResolverListAdapter listAdapter) { UserHandle listUserHandle = listAdapter.getUserHandle(); if (mWorkProfileUserHandle != null && (UserHandle.myUserId() == listUserHandle.getIdentifier() diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 2352180bcba31..f088ab38658c1 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -994,8 +994,8 @@ public class ResolverActivity extends Activity implements if (isAutolaunching() || maybeAutolaunchActivity()) { return; } - if (shouldShowEmptyState(listAdapter)) { - mMultiProfilePagerAdapter.showNoAppsAvailableEmptyState(listAdapter); + if (isResolverListEmpty(listAdapter)) { + mMultiProfilePagerAdapter.showEmptyResolverListEmptyState(listAdapter); } else { mMultiProfilePagerAdapter.showListView(listAdapter); } @@ -1640,12 +1640,12 @@ public class ResolverActivity extends Activity implements private void setupViewVisibilities() { ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter(); - if (!shouldShowEmptyState(activeListAdapter)) { + if (!isResolverListEmpty(activeListAdapter)) { addUseDifferentAppLabelIfNecessary(activeListAdapter); } } - private boolean shouldShowEmptyState(ResolverListAdapter listAdapter) { + private boolean isResolverListEmpty(ResolverListAdapter listAdapter) { int count = listAdapter.getUnfilteredCount(); return count == 0 && listAdapter.getPlaceholderCount() == 0; } diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java index 51dce55478900..3f897a5f26bfd 100644 --- a/core/java/com/android/internal/app/ResolverListController.java +++ b/core/java/com/android/internal/app/ResolverListController.java @@ -120,12 +120,32 @@ public class ResolverListController { boolean shouldGetActivityMetadata, List intents, UserHandle userHandle) { + int baseFlags = PackageManager.MATCH_DEFAULT_ONLY + | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0) + | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0); + return getResolversForIntentAsUserInternal(intents, userHandle, baseFlags); + } + + /** + * Returns a list of resolved intents which is user state-independent. This means it will + * return the same results regardless of whether the {@code userHandle} user is disabled or not. + */ + public List getUserStateIndependentResolversAsUser( + List intents, + UserHandle userHandle) { + int baseFlags = PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE; + return getResolversForIntentAsUserInternal(intents, userHandle, baseFlags); + } + + private List getResolversForIntentAsUserInternal( + List intents, + UserHandle userHandle, + int baseFlags) { List resolvedComponents = null; for (int i = 0, N = intents.size(); i < N; i++) { final Intent intent = intents.get(i); - int flags = PackageManager.MATCH_DEFAULT_ONLY - | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0) - | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0); + int flags = baseFlags; if (intent.isWebIntent() || (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) { flags |= PackageManager.MATCH_INSTANT; 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 e427421a5a80e..812e2a6c63fd2 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -1330,8 +1330,15 @@ public class ChooserActivityTest { createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); List workResolvedComponentInfos = createResolvedComponentsForTest(workProfileTargets); + when(sOverrides.workResolverListController.getUserStateIndependentResolversAsUser( + Mockito.isA(List.class), + Mockito.isA(UserHandle.class))) + .thenReturn(new ArrayList<>(workResolvedComponentInfos)); sOverrides.isQuietModeEnabled = true; - setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + // When work profile is disabled, we get 0 results when we query the work profile + // intents. + setupResolverControllers(personalResolvedComponentInfos, + /* workResolvedComponentInfos */ new ArrayList<>()); Intent sendIntent = createSendTextIntent(); sendIntent.setType("TestType"); @@ -1348,7 +1355,7 @@ public class ChooserActivityTest { } @Test - public void testWorkTab_noWorkTargets_emptyStateShown() { + public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { // enable the work tab feature flag ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); @@ -1372,6 +1379,57 @@ public class ChooserActivityTest { .check(matches(isDisplayed())); } + @Test + public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + sOverrides.isQuietModeEnabled = true; + sOverrides.hasCrossProfileIntents = false; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_cant_share_with_work_apps)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + sOverrides.isQuietModeEnabled = true; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available_share)) + .check(matches(isDisplayed())); + } + private Intent createSendTextIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); 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 4ec89b7d28177..5a3aff937b792 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java @@ -598,7 +598,7 @@ public class ResolverActivityTest { onView(withId(R.id.contentPanel)) .perform(swipeUp()); - onView(withText(R.string.resolver_cant_share_with_work_apps)) + onView(withText(R.string.resolver_cant_access_work_apps)) .check(matches(isDisplayed())); } @@ -612,8 +612,15 @@ public class ResolverActivityTest { createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); List workResolvedComponentInfos = createResolvedComponentsForTest(workProfileTargets); + when(sOverrides.workResolverListController.getUserStateIndependentResolversAsUser( + Mockito.isA(List.class), + Mockito.isA(UserHandle.class))) + .thenReturn(new ArrayList<>(workResolvedComponentInfos)); sOverrides.isQuietModeEnabled = true; - setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + // When work profile is disabled, we get 0 results when we query the work profile + // intents. + setupResolverControllers(personalResolvedComponentInfos, + /* workResolvedComponentInfos */ new ArrayList<>()); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); @@ -629,7 +636,7 @@ public class ResolverActivityTest { } @Test - public void testWorkTab_noWorkTargets_emptyStateShown() { + public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { // enable the work tab feature flag ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); @@ -652,6 +659,57 @@ public class ResolverActivityTest { .check(matches(isDisplayed())); } + @Test + public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + sOverrides.isQuietModeEnabled = true; + sOverrides.hasCrossProfileIntents = false; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_cant_access_work_apps)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + sOverrides.isQuietModeEnabled = true; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available_resolve)) + .check(matches(isDisplayed())); + } + private Intent createSendImageIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND);