Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2c6115af authored by arangelov's avatar arangelov
Browse files

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
parent 2bf744ab
Loading
Loading
Loading
Loading
+39 −20
Original line number Diff line number Diff line
@@ -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);
                }
        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.
     */
+45 −11
Original line number Diff line number Diff line
@@ -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 =
+10 −1
Original line number Diff line number Diff line
@@ -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) {
+55 −0
Original line number Diff line number Diff line
@@ -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++) {