Show other profile tab if 0 apps in current profile and >1 in the other.

Currently if we have 0 browser apps in work profile and >1 in personal,
and we try open a link from the  work profile, the personal profile
intent resolver is launched. We handle this gracefully by showing the
work tab and the no apps found empty state screen, instead of the
cross profile intents disabled.

Other changes in this commit:
- Handle all empty state screens from a single method
- All empty state screens are now shown from ResolveActivity#onPostListReady.
This was needed to guarantee consistency when showing the header.
- Always create the header exactly once for the lifetime of the intent
resolver/share sheet. Before it was re-created when swiping back to
the initial tab.
- Always show the header text when the tabbed view is enabled.

Fixes: 148536209
Test: manual
Test: atest ChooserActivityTest
Test: atest ResolverActivityTest

Change-Id: I737f1e8f864ae1108af7ec3ba8a1fa0f10b383b0
This commit is contained in:
arangelov
2020-04-08 13:26:13 +01:00
parent 2bf744abe1
commit 2c6115af27
4 changed files with 149 additions and 32 deletions

View File

@@ -300,30 +300,26 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) {
UserHandle listUserHandle = activeListAdapter.getUserHandle();
if (UserHandle.myUserId() != listUserHandle.getIdentifier()) {
if (!mInjector.hasCrossProfileIntents(activeListAdapter.getIntents(),
UserHandle.myUserId(), listUserHandle.getIdentifier())) {
if (listUserHandle.equals(mPersonalProfileUserHandle)) {
DevicePolicyEventLogger.createEvent(
DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL)
.setStrings(getMetricsCategory())
.write();
showNoWorkToPersonalIntentsEmptyState(activeListAdapter);
} else {
DevicePolicyEventLogger.createEvent(
DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK)
.setStrings(getMetricsCategory())
.write();
showNoPersonalToWorkIntentsEmptyState(activeListAdapter);
}
return false;
}
if (shouldShowNoCrossProfileIntentsEmptyState(activeListAdapter)) {
activeListAdapter.postListReadyRunnable(doPostProcessing);
return false;
}
return activeListAdapter.rebuildList(doPostProcessing);
}
private boolean shouldShowNoCrossProfileIntentsEmptyState(
ResolverListAdapter activeListAdapter) {
UserHandle listUserHandle = activeListAdapter.getUserHandle();
return UserHandle.myUserId() != listUserHandle.getIdentifier()
&& allowShowNoCrossProfileIntentsEmptyState()
&& !mInjector.hasCrossProfileIntents(activeListAdapter.getIntents(),
UserHandle.myUserId(), listUserHandle.getIdentifier());
}
boolean allowShowNoCrossProfileIntentsEmptyState() {
return true;
}
protected abstract void showWorkProfileOffEmptyState(
ResolverListAdapter activeListAdapter, View.OnClickListener listener);
@@ -353,12 +349,35 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* anyway.
*/
void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) {
if (maybeShowNoCrossProfileIntentsEmptyState(listAdapter)) {
return;
}
if (maybeShowWorkProfileOffEmptyState(listAdapter)) {
return;
}
maybeShowNoAppsAvailableEmptyState(listAdapter);
}
private boolean maybeShowNoCrossProfileIntentsEmptyState(ResolverListAdapter listAdapter) {
if (!shouldShowNoCrossProfileIntentsEmptyState(listAdapter)) {
return false;
}
if (listAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) {
DevicePolicyEventLogger.createEvent(
DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL)
.setStrings(getMetricsCategory())
.write();
showNoWorkToPersonalIntentsEmptyState(listAdapter);
} else {
DevicePolicyEventLogger.createEvent(
DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK)
.setStrings(getMetricsCategory())
.write();
showNoPersonalToWorkIntentsEmptyState(listAdapter);
}
return true;
}
/**
* Returns {@code true} if the work profile off empty state screen is shown.
*/

View File

@@ -179,6 +179,7 @@ public class ResolverActivity extends Activity implements
public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
private BroadcastReceiver mWorkProfileStateReceiver;
private boolean mIsHeaderCreated;
/**
* Get the string resource to be used as a label for the link to the resolver activity for an
@@ -479,13 +480,42 @@ public class ResolverActivity extends Activity implements
== workProfileUserHandle.getIdentifier()),
mUseLayoutForBrowsables,
/* userHandle */ workProfileUserHandle);
// In the edge case when we have 0 apps in the current profile and >1 apps in the other,
// the intent resolver is started in the other profile. Since this is the only case when
// this happens, we check for it here and set the current profile's tab.
int selectedProfile = getCurrentProfile();
UserHandle intentUser = UserHandle.of(getLaunchingUserId());
if (!getUser().equals(intentUser)) {
if (getPersonalProfileUserHandle().equals(intentUser)) {
selectedProfile = PROFILE_PERSONAL;
} else if (getWorkProfileUserHandle().equals(intentUser)) {
selectedProfile = PROFILE_WORK;
}
}
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
personalAdapter,
workAdapter,
/* defaultProfile */ getCurrentProfile(),
selectedProfile,
getPersonalProfileUserHandle(),
getWorkProfileUserHandle());
getWorkProfileUserHandle(),
/* shouldShowNoCrossProfileIntentsEmptyState= */ getUser().equals(intentUser));
}
/**
* Returns the user id of the user that the starting intent originated from.
* <p>This is not necessarily equal to {@link #getUserId()} or {@link UserHandle#myUserId()},
* as there are edge cases when the intent resolver is launched in the other profile.
* For example, when we have 0 resolved apps in current profile and multiple resolved apps
* in the other profile, opening a link from the current profile launches the intent resolver
* in the other one. b/148536209 for more info.
*/
private int getLaunchingUserId() {
int contentUserHint = getIntent().getContentUserHint();
if (contentUserHint == UserHandle.USER_CURRENT) {
return UserHandle.myUserId();
}
return contentUserHint;
}
protected @Profile int getCurrentProfile() {
@@ -856,7 +886,7 @@ public class ResolverActivity extends Activity implements
private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos,
boolean filtered) {
if (mMultiProfilePagerAdapter.getCurrentUserHandle() != getUser()) {
if (!mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getUser())) {
// Never allow the inactive profile to always open an app.
mAlwaysButton.setEnabled(false);
return;
@@ -995,10 +1025,7 @@ public class ResolverActivity extends Activity implements
mMultiProfilePagerAdapter.showListView(listAdapter);
}
if (doPostProcessing) {
if (mMultiProfilePagerAdapter.getCurrentUserHandle().getIdentifier()
== UserHandle.myUserId()) {
setHeader();
}
maybeCreateHeader(listAdapter);
resetButtonBar();
onListRebuilt(listAdapter);
}
@@ -1679,10 +1706,15 @@ public class ResolverActivity extends Activity implements
/**
* Configure the area above the app selection list (title, content preview, etc).
* <p>The header is created once when first launching the activity and whenever a package is
* installed or uninstalled.
*/
public void setHeader() {
if (mMultiProfilePagerAdapter.getActiveListAdapter().getCount() == 0
&& mMultiProfilePagerAdapter.getActiveListAdapter().getPlaceholderCount() == 0) {
private void maybeCreateHeader(ResolverListAdapter listAdapter) {
if (mIsHeaderCreated) {
return;
}
if (!shouldShowTabs()
&& listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) {
final TextView titleView = findViewById(R.id.title);
if (titleView != null) {
titleView.setVisibility(View.GONE);
@@ -1703,8 +1735,9 @@ public class ResolverActivity extends Activity implements
final ImageView iconView = findViewById(R.id.icon);
if (iconView != null) {
mMultiProfilePagerAdapter.getActiveListAdapter().loadFilteredItemIconTaskAsync(iconView);
listAdapter.loadFilteredItemIconTaskAsync(iconView);
}
mIsHeaderCreated = true;
}
protected void resetButtonBar() {
@@ -1804,6 +1837,7 @@ public class ResolverActivity extends Activity implements
// turning on.
return;
}
mIsHeaderCreated = false;
boolean listRebuilt = mMultiProfilePagerAdapter.rebuildActiveTab(true);
if (listRebuilt) {
ResolverListAdapter activeListAdapter =

View File

@@ -35,6 +35,7 @@ import com.android.internal.widget.PagerAdapter;
public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerAdapter {
private final ResolverProfileDescriptor[] mItems;
private final boolean mShouldShowNoCrossProfileIntentsEmptyState;
ResolverMultiProfilePagerAdapter(Context context,
ResolverListAdapter adapter,
@@ -44,6 +45,7 @@ public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerA
mItems = new ResolverProfileDescriptor[] {
createProfileDescriptor(adapter)
};
mShouldShowNoCrossProfileIntentsEmptyState = true;
}
ResolverMultiProfilePagerAdapter(Context context,
@@ -51,13 +53,15 @@ public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerA
ResolverListAdapter workAdapter,
@Profile int defaultProfile,
UserHandle personalProfileUserHandle,
UserHandle workProfileUserHandle) {
UserHandle workProfileUserHandle,
boolean shouldShowNoCrossProfileIntentsEmptyState) {
super(context, /* currentPage */ defaultProfile, personalProfileUserHandle,
workProfileUserHandle);
mItems = new ResolverProfileDescriptor[] {
createProfileDescriptor(personalAdapter),
createProfileDescriptor(workAdapter)
};
mShouldShowNoCrossProfileIntentsEmptyState = shouldShowNoCrossProfileIntentsEmptyState;
}
private ResolverProfileDescriptor createProfileDescriptor(
@@ -162,6 +166,11 @@ public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerA
return ResolverActivity.METRICS_CATEGORY_RESOLVER;
}
@Override
boolean allowShowNoCrossProfileIntentsEmptyState() {
return mShouldShowNoCrossProfileIntentsEmptyState;
}
@Override
protected void showWorkProfileOffEmptyState(ResolverListAdapter activeListAdapter,
View.OnClickListener listener) {

View File

@@ -38,13 +38,16 @@ import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertFalse;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.UserHandle;
import android.text.TextUtils;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.Espresso;
@@ -543,6 +546,51 @@ public class ResolverActivityTest {
assertThat(activity.getWorkListAdapter().getCount(), is(4));
}
@Test
public void testWorkTab_headerIsVisibleInPersonalTab() {
// enable the work tab feature flag
ResolverActivity.ENABLE_TABBED_VIEW = true;
markWorkProfileUserAvailable();
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(1);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createOpenWebsiteIntent();
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
waitForIdle();
TextView headerText = activity.findViewById(R.id.title);
String initialText = headerText.getText().toString();
assertFalse(initialText.isEmpty(), "Header text is empty.");
assertThat(headerText.getVisibility(), is(View.VISIBLE));
}
@Test
public void testWorkTab_switchTabs_headerStaysSame() {
// enable the work tab feature flag
ResolverActivity.ENABLE_TABBED_VIEW = true;
markWorkProfileUserAvailable();
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(1);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createOpenWebsiteIntent();
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
waitForIdle();
TextView headerText = activity.findViewById(R.id.title);
String initialText = headerText.getText().toString();
onView(withText(R.string.resolver_work_tab))
.perform(click());
waitForIdle();
String currentText = headerText.getText().toString();
assertThat(headerText.getVisibility(), is(View.VISIBLE));
assertThat(String.format("Header text is not the same when switching tabs, personal profile"
+ " header was %s but work profile header is %s", initialText, currentText),
TextUtils.equals(initialText, currentText));
}
@Ignore // b/148156663
@Test
public void testWorkTab_noPersonalApps_canStartWorkApps()
@@ -761,6 +809,13 @@ public class ResolverActivityTest {
return sendIntent;
}
private Intent createOpenWebsiteIntent() {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_VIEW);
sendIntent.setData(Uri.parse("https://google.com"));
return sendIntent;
}
private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) {
List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
for (int i = 0; i < numberOfResults; i++) {