Merge "Add empty state screens."

This commit is contained in:
Antoan Angelov
2020-02-12 14:25:02 +00:00
committed by Android (Google) Code Review
23 changed files with 437 additions and 102 deletions

View File

@@ -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;
} else {
return activeListAdapter.rebuildList(doPostProcessing);
}
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) {

View File

@@ -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());
/**
* 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 updateContentPreview() {
if (shouldShowContentPreview()) {
showContentPreview();
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

View File

@@ -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);

View File

@@ -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,15 +1498,17 @@ 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;
}
/**
* Add a label to signify that the user can pick a different app.
* @param adapter The adapter used to provide data to item views.

View File

@@ -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}.

View File

@@ -57,10 +57,15 @@ public class ResolverViewPager extends ViewPager {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
int height = getMeasuredHeight();
if (getChildCount() > 0) {
View firstChild = getChildAt(0);
firstChild.measure(widthMeasureSpec,
View child = getChildAt(0);
child.measure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
height = firstChild.getMeasuredHeight();
if (height > child.getMeasuredHeight()) {
height = child.getMeasuredHeight();
}
}
if (height < getMinimumHeight()) {
height = getMinimumHeight();
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

View File

@@ -66,6 +66,12 @@ public class ResolverDrawerLayout extends ViewGroup {
*/
private int mMaxCollapsedHeightSmall;
/**
* Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
* inferred by {@code mMaxCollapsedHeight}.
*/
private final boolean mIsMaxCollapsedHeightSmallExplicit;
private boolean mSmallCollapsed;
/**
@@ -146,6 +152,8 @@ public class ResolverDrawerLayout extends ViewGroup {
mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
mMaxCollapsedHeight);
mIsMaxCollapsedHeightSmallExplicit =
a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
a.recycle();
@@ -162,6 +170,21 @@ public class ResolverDrawerLayout extends ViewGroup {
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
/**
* Dynamically set the max collapsed height. Note this also updates the small collapsed
* height if it wasn't specified explicitly.
*/
public void setMaxCollapsedHeight(int heightInPixels) {
if (heightInPixels == mMaxCollapsedHeight) {
return;
}
mMaxCollapsedHeight = heightInPixels;
if (!mIsMaxCollapsedHeightSmallExplicit) {
mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
}
requestLayout();
}
public void setSmallCollapsed(boolean smallCollapsed) {
mSmallCollapsed = smallCollapsed;
requestLayout();