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

Commit 590fba38 authored by arangelov's avatar arangelov
Browse files

Add empty state screens.

The added screens are for the following scenarios:
- No resolved apps on the visible tab
- Sharing not enabled by admin for the visible tab for both
directions, work->personal and personal->work
- Work profile is disabled

Other changes in this CL:
- Make the intent resolver height suitable for the empty states
- Handle landscape mode gracefully by hiding the content preview
if the tabs are shown
- Show the content preview view as part of the list view if there
are no tabs present, and above the tabs if tabs are present
- Remove elevation if showing the tabbed view

What's left:
- Finalize strings

Test: manually tested all conditions for the empty state screens
Test: atest com.android.internal.app.ChooserActivityTest
Test: atest com.android.internal.app.ResolverActivityTest
Bug: 142537267
Bug: 142538125

Change-Id: I4e5ab5fa368d2b907e3fa74b00a82d8739e4f229
parent 20024529
Loading
Loading
Loading
Loading
+108 −9
Original line number Diff line number Diff line
@@ -15,15 +15,25 @@
 */
package com.android.internal.app;

import android.annotation.DrawableRes;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.app.AppGlobals;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.ResolveInfo;
import android.os.UserHandle;
import android.os.UserManager;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.PagerAdapter;
import com.android.internal.widget.ViewPager;
@@ -213,26 +223,115 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {

    abstract @Nullable ViewGroup getInactiveAdapterView();

    boolean rebuildActiveTab(boolean post) {
        return rebuildTab(getActiveListAdapter(), post);
    /**
     * Rebuilds the tab that is currently visible to the user.
     * <p>Returns {@code true} if rebuild has completed.
     */
    boolean rebuildActiveTab(boolean doPostProcessing) {
        return rebuildTab(getActiveListAdapter(), doPostProcessing);
    }

    boolean rebuildInactiveTab(boolean post) {
    /**
     * Rebuilds the tab that is not currently visible to the user, if such one exists.
     * <p>Returns {@code true} if rebuild has completed.
     */
    boolean rebuildInactiveTab(boolean doPostProcessing) {
        if (getItemCount() == 1) {
            return false;
        }
        return rebuildTab(getInactiveListAdapter(), post);
        return rebuildTab(getInactiveListAdapter(), doPostProcessing);
    }

    private int userHandleToPageIndex(UserHandle userHandle) {
        if (userHandle == getPersonalListAdapter().mResolverListController.getUserHandle()) {
            return PROFILE_PERSONAL;
        } else {
            return PROFILE_WORK;
        }
    }

    private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) {
        UserHandle listUserHandle = activeListAdapter.getUserHandle();
        if (UserHandle.myUserId() != listUserHandle.getIdentifier() &&
                !hasAppsInOtherProfile(activeListAdapter)) {
            // TODO(arangelov): Show empty state UX here
        UserManager userManager = mContext.getSystemService(UserManager.class);
        if (listUserHandle == mWorkProfileUserHandle
                && userManager.isQuietModeEnabled(mWorkProfileUserHandle)) {
            showEmptyState(activeListAdapter,
                    R.drawable.ic_work_apps_off,
                    R.string.resolver_turn_on_work_apps,
                    R.string.resolver_turn_on_work_apps_explanation,
                    (View.OnClickListener) v ->
                            userManager.requestQuietModeEnabled(false, mWorkProfileUserHandle));
            return false;
        }
        if (UserHandle.myUserId() != listUserHandle.getIdentifier()) {
            if (!hasCrossProfileIntents(activeListAdapter.getIntents(),
                    UserHandle.myUserId(), listUserHandle.getIdentifier())) {
                if (listUserHandle == mPersonalProfileUserHandle) {
                    showEmptyState(activeListAdapter,
                            R.drawable.ic_sharing_disabled,
                            R.string.resolver_cant_share_with_personal_apps,
                            R.string.resolver_cant_share_cross_profile_explanation);
                } else {
                    showEmptyState(activeListAdapter,
                            R.drawable.ic_sharing_disabled,
                            R.string.resolver_cant_share_with_work_apps,
                            R.string.resolver_cant_share_cross_profile_explanation);
                }
                return false;
            }
        }
        return activeListAdapter.rebuildList(doPostProcessing);
    }

    void showEmptyState(ResolverListAdapter listAdapter) {
        UserHandle listUserHandle = listAdapter.getUserHandle();
        if (UserHandle.myUserId() == listUserHandle.getIdentifier()
                || !hasAppsInOtherProfile(listAdapter)) {
            showEmptyState(listAdapter,
                    R.drawable.ic_no_apps,
                    R.string.resolver_no_apps_available,
                    R.string.resolver_no_apps_available_explanation);
        }
    }

    private void showEmptyState(ResolverListAdapter activeListAdapter,
            @DrawableRes int iconRes, @StringRes int titleRes, @StringRes int subtitleRes) {
        showEmptyState(activeListAdapter, iconRes, titleRes, subtitleRes, /* buttonOnClick */ null);
    }

    private void showEmptyState(ResolverListAdapter activeListAdapter,
            @DrawableRes int iconRes, @StringRes int titleRes, @StringRes int subtitleRes,
            View.OnClickListener buttonOnClick) {
        ProfileDescriptor descriptor = getItem(
                userHandleToPageIndex(activeListAdapter.getUserHandle()));
        descriptor.rootView.findViewById(R.id.resolver_list).setVisibility(View.GONE);
        View emptyStateView = descriptor.rootView.findViewById(R.id.resolver_empty_state);
        emptyStateView.setVisibility(View.VISIBLE);

        ImageView icon = emptyStateView.findViewById(R.id.resolver_empty_state_icon);
        icon.setImageResource(iconRes);

        TextView title = emptyStateView.findViewById(R.id.resolver_empty_state_title);
        title.setText(titleRes);

        TextView subtitle = emptyStateView.findViewById(R.id.resolver_empty_state_subtitle);
        subtitle.setText(subtitleRes);

        Button button = emptyStateView.findViewById(R.id.resolver_empty_state_button);
        button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
        button.setOnClickListener(buttonOnClick);
    }

    private boolean hasCrossProfileIntents(List<Intent> intents, int source, int target) {
        IPackageManager packageManager = AppGlobals.getPackageManager();
        ContentResolver contentResolver = mContext.getContentResolver();
        for (Intent intent : intents) {
            if (IntentForwarderActivity.canForward(intent, source, target, packageManager,
                    contentResolver) != null) {
                return true;
            }
        }
        return false;
    }

    private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) {
+91 −25
Original line number Diff line number Diff line
@@ -373,7 +373,11 @@ public class ChooserActivity extends ResolverActivity implements
                Log.i(TAG, "Hiding image preview area. Timed out waiting for preview to load"
                        + " within " + mImageLoadTimeoutMillis + "ms.");
                collapseParentView();
                hideContentPreview();
                if (hasWorkProfile() && ENABLE_TABBED_VIEW) {
                    hideStickyContentPreview();
                } else if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) {
                    mChooserMultiProfilePagerAdapter.getCurrentRootAdapter().hideContentPreview();
                }
                mHideParentOnFail = false;
            }
        }
@@ -829,7 +833,12 @@ public class ChooserActivity extends ResolverActivity implements

    @Override
    protected boolean postRebuildList(boolean rebuildCompleted) {
        updateContentPreview();
        updateStickyContentPreview();
        if (shouldShowStickyContentPreview()
                || mChooserMultiProfilePagerAdapter
                        .getCurrentRootAdapter().getContentPreviewRowCount() != 0) {
            logActionShareWithPreview();
        }
        return postRebuildListInternal(rebuildCompleted);
    }

@@ -978,6 +987,8 @@ public class ChooserActivity extends ResolverActivity implements
        updateLayoutWidth(R.id.content_preview_text_layout, width, parent);
        updateLayoutWidth(R.id.content_preview_title_layout, width, parent);
        updateLayoutWidth(R.id.content_preview_file_layout, width, parent);
        findViewById(R.id.content_preview_container)
                .setVisibility(shouldShowStickyContentPreview() ? View.VISIBLE : View.GONE);
    }

    private void updateLayoutWidth(int layoutResourceId, int width, View parent) {
@@ -2413,14 +2424,14 @@ public class ChooserActivity extends ResolverActivity implements

                // still zero? then use a default height and leave, which
                // can happen when there are no targets to show
                if (rowsToShow == 0 && !shouldShowContentPreview()) {
                if (rowsToShow == 0 && !shouldShowStickyContentPreview()) {
                    offset += getResources().getDimensionPixelSize(
                            R.dimen.chooser_max_collapsed_height);
                    mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
                    return;
                }

                if (shouldShowContentPreview()) {
                if (shouldShowStickyContentPreview()) {
                    offset += findViewById(R.id.content_preview_container).getHeight();
                }

@@ -2552,7 +2563,7 @@ public class ChooserActivity extends ResolverActivity implements
    }

    private void setupScrollListener() {
        if (mResolverDrawerLayout == null) {
        if (mResolverDrawerLayout == null || (hasWorkProfile() && ENABLE_TABBED_VIEW)) {
            return;
        }
        final View chooserHeader = mResolverDrawerLayout.findViewById(R.id.chooser_header);
@@ -2597,28 +2608,36 @@ public class ChooserActivity extends ResolverActivity implements
        return false;
    }

    private boolean shouldShowContentPreview() {
        return mMultiProfilePagerAdapter.getActiveListAdapter().getCount() > 0
                && isSendAction(getTargetIntent());
    }

    private void updateContentPreview() {
        if (shouldShowContentPreview()) {
            showContentPreview();
    /**
     * The sticky content preview is shown only when we have a tabbed view. It's shown above
     * the tabs so it is not part of the scrollable list. If we are not in tabbed view,
     * we instead show the content preview as a regular list item.
     */
    private boolean shouldShowStickyContentPreview() {
        return hasWorkProfile()
                && ENABLE_TABBED_VIEW
                && mMultiProfilePagerAdapter.getListAdapterForUserHandle(
                        UserHandle.of(UserHandle.myUserId())).getCount() > 0
                && isSendAction(getTargetIntent())
                && getResources().getBoolean(R.bool.sharesheet_show_content_preview);
    }

    private void updateStickyContentPreview() {
        if (shouldShowStickyContentPreview()) {
            showStickyContentPreview();
        } else {
            hideContentPreview();
            hideStickyContentPreview();
        }
    }

    private void showContentPreview() {
    private void showStickyContentPreview() {
        ViewGroup contentPreviewContainer = findViewById(R.id.content_preview_container);
        contentPreviewContainer.setVisibility(View.VISIBLE);
        ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer);
        contentPreviewContainer.addView(contentPreviewView);
        logActionShareWithPreview();
    }

    private void hideContentPreview() {
    private void hideStickyContentPreview() {
        ViewGroup contentPreviewContainer = findViewById(R.id.content_preview_container);
        contentPreviewContainer.removeAllViews();
        contentPreviewContainer.setVisibility(View.GONE);
@@ -2634,6 +2653,7 @@ public class ChooserActivity extends ResolverActivity implements
    /**
     * Used to bind types of individual item including
     * {@link ChooserGridAdapter#VIEW_TYPE_NORMAL},
     * {@link ChooserGridAdapter#VIEW_TYPE_CONTENT_PREVIEW},
     * {@link ChooserGridAdapter#VIEW_TYPE_PROFILE},
     * and {@link ChooserGridAdapter#VIEW_TYPE_AZ_LABEL}.
     */
@@ -2695,16 +2715,18 @@ public class ChooserActivity extends ResolverActivity implements
        private int mChooserTargetWidth = 0;
        private boolean mShowAzLabelIfPoss;

        private boolean mHideContentPreview = false;
        private boolean mLayoutRequested = false;

        private int mFooterHeight = 0;

        private static final int VIEW_TYPE_DIRECT_SHARE = 0;
        private static final int VIEW_TYPE_NORMAL = 1;
        private static final int VIEW_TYPE_PROFILE = 2;
        private static final int VIEW_TYPE_AZ_LABEL = 3;
        private static final int VIEW_TYPE_CALLER_AND_RANK = 4;
        private static final int VIEW_TYPE_FOOTER = 5;
        private static final int VIEW_TYPE_CONTENT_PREVIEW = 2;
        private static final int VIEW_TYPE_PROFILE = 3;
        private static final int VIEW_TYPE_AZ_LABEL = 4;
        private static final int VIEW_TYPE_CALLER_AND_RANK = 5;
        private static final int VIEW_TYPE_FOOTER = 6;

        private static final int MAX_TARGETS_PER_ROW_PORTRAIT = 4;
        private static final int MAX_TARGETS_PER_ROW_LANDSCAPE = 8;
@@ -2765,6 +2787,17 @@ public class ChooserActivity extends ResolverActivity implements
            return maxTargets;
        }

        /**
         * Hides the list item content preview.
         * <p>Not to be confused with the sticky content preview which is above the
         * personal and work tabs.
         */
        public void hideContentPreview() {
            mHideContentPreview = true;
            mLayoutRequested = true;
            notifyDataSetChanged();
        }

        public boolean consumeLayoutRequest() {
            boolean oldValue = mLayoutRequested;
            mLayoutRequested = false;
@@ -2773,7 +2806,8 @@ public class ChooserActivity extends ResolverActivity implements

        public int getRowCount() {
            return (int) (
                    getProfileRowCount()
                    getContentPreviewRowCount()
                            + getProfileRowCount()
                            + getServiceTargetRowCount()
                            + getCallerAndRankedTargetRowCount()
                            + getAzLabelRowCount()
@@ -2783,7 +2817,33 @@ public class ChooserActivity extends ResolverActivity implements
            );
        }

        /**
         * Returns either {@code 0} or {@code 1} depending on whether we want to show the list item
         * content preview. Not to be confused with the sticky content preview which is above the
         * personal and work tabs.
         */
        public int getContentPreviewRowCount() {
            // For the tabbed case we show the sticky content preview above the tabs,
            // please refer to shouldShowStickyContentPreview
            if (hasWorkProfile() && ENABLE_TABBED_VIEW) {
                return 0;
            }
            if (!isSendAction(getTargetIntent())) {
                return 0;
            }

            if (mHideContentPreview || mChooserListAdapter == null
                    || mChooserListAdapter.getCount() == 0) {
                return 0;
            }

            return 1;
        }

        public int getProfileRowCount() {
            if (hasWorkProfile() && ENABLE_TABBED_VIEW) {
                return 0;
            }
            return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
        }

@@ -2815,7 +2875,8 @@ public class ChooserActivity extends ResolverActivity implements
        @Override
        public int getItemCount() {
            return (int) (
                    getProfileRowCount()
                    getContentPreviewRowCount()
                            + getProfileRowCount()
                            + getServiceTargetRowCount()
                            + getCallerAndRankedTargetRowCount()
                            + getAzLabelRowCount()
@@ -2827,6 +2888,8 @@ public class ChooserActivity extends ResolverActivity implements
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            switch (viewType) {
                case VIEW_TYPE_CONTENT_PREVIEW:
                    return new ItemViewHolder(createContentPreviewView(parent), false);
                case VIEW_TYPE_PROFILE:
                    return new ItemViewHolder(createProfileView(parent), false);
                case VIEW_TYPE_AZ_LABEL:
@@ -2866,7 +2929,10 @@ public class ChooserActivity extends ResolverActivity implements
        public int getItemViewType(int position) {
            int count;

            int countSum = (count = getProfileRowCount());
            int countSum = (count = getContentPreviewRowCount());
            if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW;

            countSum += (count = getProfileRowCount());
            if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE;

            countSum += (count = getServiceTargetRowCount());
@@ -3082,7 +3148,7 @@ public class ChooserActivity extends ResolverActivity implements
        }

        int getListPosition(int position) {
            position -= getProfileRowCount();
            position -= getContentPreviewRowCount() + getProfileRowCount();

            final int serviceCount = mChooserListAdapter.getServiceTargetCount();
            final int serviceRows = (int) Math.ceil((float) serviceCount
+9 −6
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.admin.DevicePolicyManager;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.IPackageManager;
@@ -108,7 +109,8 @@ public class IntentForwarderActivity extends Activity {
        }

        final int callingUserId = getUserId();
        final Intent newIntent = canForward(intentReceived, targetUserId);
        final Intent newIntent = canForward(intentReceived, getUserId(), targetUserId,
                mInjector.getIPackageManager(), getContentResolver());
        if (newIntent != null) {
            if (Intent.ACTION_CHOOSER.equals(newIntent.getAction())) {
                Intent innerIntent = newIntent.getParcelableExtra(Intent.EXTRA_INTENT);
@@ -191,7 +193,8 @@ public class IntentForwarderActivity extends Activity {
     * Check whether the intent can be forwarded to target user. Return the intent used for
     * forwarding if it can be forwarded, {@code null} otherwise.
     */
    Intent canForward(Intent incomingIntent, int targetUserId)  {
    static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,
            IPackageManager packageManager, ContentResolver contentResolver)  {
        Intent forwardIntent = new Intent(incomingIntent);
        forwardIntent.addFlags(
                Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
@@ -220,11 +223,11 @@ public class IntentForwarderActivity extends Activity {
        if (forwardIntent.getSelector() != null) {
            intentToCheck = forwardIntent.getSelector();
        }
        String resolvedType = intentToCheck.resolveTypeIfNeeded(getContentResolver());
        String resolvedType = intentToCheck.resolveTypeIfNeeded(contentResolver);
        sanitizeIntent(intentToCheck);
        try {
            if (mInjector.getIPackageManager().
                    canForwardTo(intentToCheck, resolvedType, getUserId(), targetUserId)) {
            if (packageManager.canForwardTo(
                    intentToCheck, resolvedType, sourceUserId, targetUserId)) {
                return forwardIntent;
            }
        } catch (RemoteException e) {
@@ -267,7 +270,7 @@ public class IntentForwarderActivity extends Activity {
    /**
     * Sanitize the intent in place.
     */
    private void sanitizeIntent(Intent intent) {
    private static void sanitizeIntent(Intent intent) {
        // Apps should not be allowed to target a specific package/ component in the target user.
        intent.setPackage(null);
        intent.setComponent(null);
+17 −14
Original line number Diff line number Diff line
@@ -387,6 +387,11 @@ public class ResolverActivity extends Activity implements
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets);

            rdl.setMaxCollapsedHeight(hasWorkProfile() && ENABLE_TABBED_VIEW
                    ? getResources().getDimensionPixelSize(
                            R.dimen.resolver_empty_state_height_with_tabs)
                    : getResources().getDimensionPixelSize(R.dimen.resolver_empty_state_height));

            mResolverDrawerLayout = rdl;
        }

@@ -548,13 +553,6 @@ public class ResolverActivity extends Activity implements
            applyFooterView(mSystemWindowInsets.bottom);
        }

        View emptyView = findViewById(R.id.empty);
        if (emptyView != null) {
            emptyView.setPadding(0, 0, 0, mSystemWindowInsets.bottom
                                 + getResources().getDimensionPixelSize(
                                         R.dimen.chooser_edge_margin_normal) * 2);
        }

        return insets.consumeSystemWindowInsets();
    }

@@ -941,10 +939,13 @@ public class ResolverActivity extends Activity implements
    }

    @Override // ResolverListCommunicator
    public void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing) {
    public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing) {
        if (isAutolaunching() || maybeAutolaunchActivity()) {
            return;
        }
        if (shouldShowEmptyState(listAdapter)) {
            mMultiProfilePagerAdapter.showEmptyState(listAdapter);
        }
        if (doPostProcessing) {
            if (mMultiProfilePagerAdapter.getCurrentUserHandle().getIdentifier()
                    == UserHandle.myUserId()) {
@@ -1497,13 +1498,15 @@ public class ResolverActivity extends Activity implements
    }

    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());
        ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
        if (!shouldShowEmptyState(activeListAdapter)) {
            addUseDifferentAppLabelIfNecessary(activeListAdapter);
        }
    }

    private boolean shouldShowEmptyState(ResolverListAdapter listAdapter) {
        int count = listAdapter.getUnfilteredCount();
        return count == 0 && listAdapter.getPlaceholderCount() == 0;
    }

    /**
+4 −0
Original line number Diff line number Diff line
@@ -607,6 +607,10 @@ public class ResolverListAdapter extends BaseAdapter {
                mIntents, userHandle);
    }

    protected List<Intent> getIntents() {
        return mIntents;
    }

    /**
     * Necessary methods to communicate between {@link ResolverListAdapter}
     * and {@link ResolverActivity}.
Loading