blob: 1cf59316a7251e1262df5b677add774f93afccba [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.intentresolver.grid;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.content.Context;
import android.database.DataSetObserver;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.animation.DecelerateInterpolator;
import android.widget.Space;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.android.intentresolver.ChooserListAdapter;
import com.android.intentresolver.R;
import com.android.intentresolver.ResolverListAdapter.ViewHolder;
import com.android.internal.annotations.VisibleForTesting;
import com.google.android.collect.Lists;
/**
* Adapter for all types of items and targets in ShareSheet.
* Note that ranked sections like Direct Share - while appearing grid-like - are handled on the
* row level by this adapter but not on the item level. Individual targets within the row are
* handled by {@link ChooserListAdapter}
*/
@VisibleForTesting
public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
/**
* The transition time between placeholders for direct share to a message
* indicating that none are available.
*/
public static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200;
/**
* Injectable interface for any considerations that should be delegated to other components
* in the {@link ChooserActivity}.
* TODO: determine whether any of these methods return parameters that can safely be
* precomputed; whether any should be converted to `ChooserGridAdapter` setters to be
* invoked by external callbacks; and whether any reflect requirements that should be moved
* out of `ChooserGridAdapter` altogether.
*/
public interface ChooserActivityDelegate {
/** @return whether we're showing a tabbed (multi-profile) UI. */
boolean shouldShowTabs();
/**
* @return a content preview {@link View} that's appropriate for the caller's share
* content, constructed for display in the provided {@code parent} group.
*/
View buildContentPreview(ViewGroup parent);
/** Notify the client that the item with the selected {@code itemIndex} was selected. */
void onTargetSelected(int itemIndex);
/**
* Notify the client that the item with the selected {@code itemIndex} was
* long-pressed.
*/
void onTargetLongPressed(int itemIndex);
/**
* Notify the client that the provided {@code View} should be configured as the new
* "profile view" button. Callers should attach their own click listeners to implement
* behaviors on this view.
*/
void updateProfileViewButton(View newButtonFromProfileRow);
/**
* @return the number of "valid" targets in the active list adapter.
* TODO: define "valid."
*/
int getValidTargetCount();
/**
* Request that the client update our {@code directShareGroup} to match their desired
* state for the "expansion" UI.
*/
void updateDirectShareExpansion(DirectShareViewHolder directShareGroup);
/**
* Request that the client handle a scroll event that should be taken as expanding the
* provided {@code directShareGroup}. Note that this currently never happens due to a
* hard-coded condition in {@link #canExpandDirectShare()}.
*/
void handleScrollToExpandDirectShare(
DirectShareViewHolder directShareGroup, int y, int oldy);
}
private static final int VIEW_TYPE_DIRECT_SHARE = 0;
private static final int VIEW_TYPE_NORMAL = 1;
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 NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
private final ChooserActivityDelegate mChooserActivityDelegate;
private final ChooserListAdapter mChooserListAdapter;
private final LayoutInflater mLayoutInflater;
private final int mMaxTargetsPerRow;
private final boolean mShouldShowContentPreview;
private final int mChooserWidthPixels;
private final int mChooserRowTextOptionTranslatePixelSize;
private final boolean mShowAzLabelIfPoss;
private DirectShareViewHolder mDirectShareViewHolder;
private int mChooserTargetWidth = 0;
private int mFooterHeight = 0;
public ChooserGridAdapter(
Context context,
ChooserActivityDelegate chooserActivityDelegate,
ChooserListAdapter wrappedAdapter,
boolean shouldShowContentPreview,
int maxTargetsPerRow,
int numSheetExpansions) {
super();
mChooserActivityDelegate = chooserActivityDelegate;
mChooserListAdapter = wrappedAdapter;
mLayoutInflater = LayoutInflater.from(context);
mShouldShowContentPreview = shouldShowContentPreview;
mMaxTargetsPerRow = maxTargetsPerRow;
mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width);
mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize(
R.dimen.chooser_row_text_option_translate);
mShowAzLabelIfPoss = numSheetExpansions < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL;
wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
super.onChanged();
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
notifyDataSetChanged();
}
});
}
public void setFooterHeight(int height) {
mFooterHeight = height;
}
/**
* Calculate the chooser target width to maximize space per item
*
* @param width The new row width to use for recalculation
* @return true if the view width has changed
*/
public boolean calculateChooserTargetWidth(int width) {
if (width == 0) {
return false;
}
// Limit width to the maximum width of the chooser activity
int maxWidth = mChooserWidthPixels;
width = Math.min(maxWidth, width);
int newWidth = width / mMaxTargetsPerRow;
if (newWidth != mChooserTargetWidth) {
mChooserTargetWidth = newWidth;
return true;
}
return false;
}
public int getRowCount() {
return (int) (
getSystemRowCount()
+ getProfileRowCount()
+ getServiceTargetRowCount()
+ getCallerAndRankedTargetRowCount()
+ getAzLabelRowCount()
+ Math.ceil(
(float) mChooserListAdapter.getAlphaTargetCount()
/ mMaxTargetsPerRow)
);
}
/**
* Whether the "system" row of targets is displayed.
* This area includes the content preview (if present) and action row.
*/
public int getSystemRowCount() {
// For the tabbed case we show the sticky content preview above the tabs,
// please refer to shouldShowStickyContentPreview
if (mChooserActivityDelegate.shouldShowTabs()) {
return 0;
}
if (!mShouldShowContentPreview) {
return 0;
}
if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
return 0;
}
return 1;
}
public int getProfileRowCount() {
if (mChooserActivityDelegate.shouldShowTabs()) {
return 0;
}
return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
}
public int getFooterRowCount() {
return 1;
}
public int getCallerAndRankedTargetRowCount() {
return (int) Math.ceil(
((float) mChooserListAdapter.getCallerTargetCount()
+ mChooserListAdapter.getRankedTargetCount()) / mMaxTargetsPerRow);
}
// There can be at most one row in the listview, that is internally
// a ViewGroup with 2 rows
public int getServiceTargetRowCount() {
if (mShouldShowContentPreview && !ActivityManager.isLowRamDeviceStatic()) {
return 1;
}
return 0;
}
public int getAzLabelRowCount() {
// Only show a label if the a-z list is showing
return (mShowAzLabelIfPoss && mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0;
}
@Override
public int getItemCount() {
return (int) (
getSystemRowCount()
+ getProfileRowCount()
+ getServiceTargetRowCount()
+ getCallerAndRankedTargetRowCount()
+ getAzLabelRowCount()
+ mChooserListAdapter.getAlphaTargetCount()
+ getFooterRowCount()
);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_CONTENT_PREVIEW:
return new ItemViewHolder(
mChooserActivityDelegate.buildContentPreview(parent),
viewType,
null,
null);
case VIEW_TYPE_PROFILE:
return new ItemViewHolder(
createProfileView(parent),
viewType,
null,
null);
case VIEW_TYPE_AZ_LABEL:
return new ItemViewHolder(
createAzLabelView(parent),
viewType,
null,
null);
case VIEW_TYPE_NORMAL:
return new ItemViewHolder(
mChooserListAdapter.createView(parent),
viewType,
mChooserActivityDelegate::onTargetSelected,
mChooserActivityDelegate::onTargetLongPressed);
case VIEW_TYPE_DIRECT_SHARE:
case VIEW_TYPE_CALLER_AND_RANK:
return createItemGroupViewHolder(viewType, parent);
case VIEW_TYPE_FOOTER:
Space sp = new Space(parent.getContext());
sp.setLayoutParams(new RecyclerView.LayoutParams(
LayoutParams.MATCH_PARENT, mFooterHeight));
return new FooterViewHolder(sp, viewType);
default:
// Since we catch all possible viewTypes above, no chance this is being called.
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int viewType = ((ViewHolderBase) holder).getViewType();
switch (viewType) {
case VIEW_TYPE_DIRECT_SHARE:
case VIEW_TYPE_CALLER_AND_RANK:
bindItemGroupViewHolder(position, (ItemGroupViewHolder) holder);
break;
case VIEW_TYPE_NORMAL:
bindItemViewHolder(position, (ItemViewHolder) holder);
break;
default:
}
}
@Override
public int getItemViewType(int position) {
int count;
int countSum = (count = getSystemRowCount());
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());
if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE;
countSum += (count = getCallerAndRankedTargetRowCount());
if (count > 0 && position < countSum) return VIEW_TYPE_CALLER_AND_RANK;
countSum += (count = getAzLabelRowCount());
if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL;
if (position == getItemCount() - 1) return VIEW_TYPE_FOOTER;
return VIEW_TYPE_NORMAL;
}
public int getTargetType(int position) {
return mChooserListAdapter.getPositionTargetType(getListPosition(position));
}
private View createProfileView(ViewGroup parent) {
View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false);
mChooserActivityDelegate.updateProfileViewButton(profileRow);
return profileRow;
}
private View createAzLabelView(ViewGroup parent) {
return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);
}
private ItemGroupViewHolder loadViewsIntoGroup(ItemGroupViewHolder holder) {
final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth, MeasureSpec.EXACTLY);
int columnCount = holder.getColumnCount();
final boolean isDirectShare = holder instanceof DirectShareViewHolder;
for (int i = 0; i < columnCount; i++) {
final View v = mChooserListAdapter.createView(holder.getRowByIndex(i));
final int column = i;
v.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mChooserActivityDelegate.onTargetSelected(holder.getItemIndex(column));
}
});
// Show menu for both direct share and app share targets after long click.
v.setOnLongClickListener(v1 -> {
mChooserActivityDelegate.onTargetLongPressed(holder.getItemIndex(column));
return true;
});
holder.addView(i, v);
// Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
// false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be
// done before measuring.
if (isDirectShare) {
final ViewHolder vh = (ViewHolder) v.getTag();
vh.text.setLines(2);
vh.text.setHorizontallyScrolling(false);
vh.text2.setVisibility(View.GONE);
}
// Force height to be a given so we don't have visual disruption during scaling.
v.measure(exactSpec, spec);
setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight());
}
final ViewGroup viewGroup = holder.getViewGroup();
// Pre-measure and fix height so we can scale later.
holder.measure();
setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight());
if (isDirectShare) {
DirectShareViewHolder dsvh = (DirectShareViewHolder) holder;
setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
}
viewGroup.setTag(holder);
return holder;
}
private void setViewBounds(View view, int widthPx, int heightPx) {
LayoutParams lp = view.getLayoutParams();
if (lp == null) {
lp = new LayoutParams(widthPx, heightPx);
view.setLayoutParams(lp);
} else {
lp.height = heightPx;
lp.width = widthPx;
}
}
ItemGroupViewHolder createItemGroupViewHolder(int viewType, ViewGroup parent) {
if (viewType == VIEW_TYPE_DIRECT_SHARE) {
ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate(
R.layout.chooser_row_direct_share, parent, false);
ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(
R.layout.chooser_row, parentGroup, false);
ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(
R.layout.chooser_row, parentGroup, false);
parentGroup.addView(row1);
parentGroup.addView(row2);
mDirectShareViewHolder = new DirectShareViewHolder(parentGroup,
Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType,
mChooserActivityDelegate::getValidTargetCount);
loadViewsIntoGroup(mDirectShareViewHolder);
return mDirectShareViewHolder;
} else {
ViewGroup row = (ViewGroup) mLayoutInflater.inflate(
R.layout.chooser_row, parent, false);
ItemGroupViewHolder holder =
new SingleRowViewHolder(row, mMaxTargetsPerRow, viewType);
loadViewsIntoGroup(holder);
return holder;
}
}
/**
* Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from
* showing on top of the AZ list if the AZ label is visible. All other types are placed into
* their own row as determined by their target type, and dividers are added in the list to
* separate each type.
*/
int getRowType(int rowPosition) {
// Merge caller and ranked standard into a single row
int positionType = mChooserListAdapter.getPositionTargetType(rowPosition);
if (positionType == ChooserListAdapter.TARGET_CALLER) {
return ChooserListAdapter.TARGET_STANDARD;
}
// If an A-Z label is shown, prevent a separator from appearing by making the A-Z
// row type the same as the suggestion row type
if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) {
return ChooserListAdapter.TARGET_STANDARD;
}
return positionType;
}
void bindItemViewHolder(int position, ItemViewHolder holder) {
View v = holder.itemView;
int listPosition = getListPosition(position);
holder.setListPosition(listPosition);
mChooserListAdapter.bindView(listPosition, v);
}
void bindItemGroupViewHolder(int position, ItemGroupViewHolder holder) {
final ViewGroup viewGroup = (ViewGroup) holder.itemView;
int start = getListPosition(position);
int startType = getRowType(start);
int columnCount = holder.getColumnCount();
int end = start + columnCount - 1;
while (getRowType(end) != startType && end >= start) {
end--;
}
if (end == start && mChooserListAdapter.getItem(start).isEmptyTargetInfo()) {
final TextView textView = viewGroup.findViewById(
com.android.internal.R.id.chooser_row_text_option);
if (textView.getVisibility() != View.VISIBLE) {
textView.setAlpha(0.0f);
textView.setVisibility(View.VISIBLE);
textView.setText(R.string.chooser_no_direct_share_targets);
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f);
fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
textView.setTranslationY(mChooserRowTextOptionTranslatePixelSize);
ValueAnimator translateAnim =
ObjectAnimator.ofFloat(textView, "translationY", 0.0f);
translateAnim.setInterpolator(new DecelerateInterpolator(1.0f));
AnimatorSet animSet = new AnimatorSet();
animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
animSet.playTogether(fadeAnim, translateAnim);
animSet.start();
}
}
for (int i = 0; i < columnCount; i++) {
final View v = holder.getView(i);
if (start + i <= end) {
holder.setViewVisibility(i, View.VISIBLE);
holder.setItemIndex(i, start + i);
mChooserListAdapter.bindView(holder.getItemIndex(i), v);
} else {
holder.setViewVisibility(i, View.INVISIBLE);
}
}
}
int getListPosition(int position) {
position -= getSystemRowCount() + getProfileRowCount();
final int serviceCount = mChooserListAdapter.getServiceTargetCount();
final int serviceRows = (int) Math.ceil((float) serviceCount / mMaxTargetsPerRow);
if (position < serviceRows) {
return position * mMaxTargetsPerRow;
}
position -= serviceRows;
final int callerAndRankedCount =
mChooserListAdapter.getCallerTargetCount()
+ mChooserListAdapter.getRankedTargetCount();
final int callerAndRankedRows = getCallerAndRankedTargetRowCount();
if (position < callerAndRankedRows) {
return serviceCount + position * mMaxTargetsPerRow;
}
position -= getAzLabelRowCount() + callerAndRankedRows;
return callerAndRankedCount + serviceCount + position;
}
public void handleScroll(View v, int y, int oldy) {
boolean canExpandDirectShare = canExpandDirectShare();
if (mDirectShareViewHolder != null && canExpandDirectShare) {
mChooserActivityDelegate.handleScrollToExpandDirectShare(
mDirectShareViewHolder, y, oldy);
}
}
/** Only expand direct share area if there is a minimum number of targets. */
private boolean canExpandDirectShare() {
// Do not enable until we have confirmed more apps are using sharing shortcuts
// Check git history for enablement logic
return false;
}
public ChooserListAdapter getListAdapter() {
return mChooserListAdapter;
}
public boolean shouldCellSpan(int position) {
return getItemViewType(position) == VIEW_TYPE_NORMAL;
}
public void updateDirectShareExpansion() {
if (mDirectShareViewHolder == null || !canExpandDirectShare()) {
return;
}
mChooserActivityDelegate.updateDirectShareExpansion(mDirectShareViewHolder);
}
}