/* * 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.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.android.intentresolver.ChooserListAdapter; import com.android.intentresolver.FeatureFlags; import com.android.intentresolver.R; import com.android.intentresolver.ResolverListAdapter.ViewHolder; 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} */ public final class ChooserGridAdapter extends RecyclerView.Adapter { /** * 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 com.android.intentresolver.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 { /** 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); } private static final int VIEW_TYPE_DIRECT_SHARE = 0; private static final int VIEW_TYPE_NORMAL = 1; 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 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 FeatureFlags mFeatureFlags; @Nullable private RecyclerView mRecyclerView; private int mChooserTargetWidth = 0; private int mFooterHeight = 0; private boolean mAzLabelVisibility = false; public ChooserGridAdapter( Context context, ChooserActivityDelegate chooserActivityDelegate, ChooserListAdapter wrappedAdapter, boolean shouldShowContentPreview, int maxTargetsPerRow, FeatureFlags featureFlags) { 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); mFeatureFlags = featureFlags; wrappedAdapter.registerDataSetObserver(new DataSetObserver() { @Override public void onChanged() { super.onChanged(); notifyDataSetChanged(); } @Override public void onInvalidated() { super.onInvalidated(); notifyDataSetChanged(); } }); } @Override public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { mRecyclerView = recyclerView; } @Override public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { mRecyclerView = null; } public void setFooterHeight(int height) { if (mFooterHeight != height) { mFooterHeight = height; if (mFeatureFlags.fixTargetListFooter()) { // we always have at least one view, the footer, see getItemCount() and // getFooterRowCount() notifyItemChanged(getItemCount() - 1); } } } /** * 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, if the maximum width is set if (mChooserWidthPixels >= 0) { width = Math.min(mChooserWidthPixels, width); } int newWidth = width / mMaxTargetsPerRow; if (newWidth != mChooserTargetWidth) { mChooserTargetWidth = newWidth; return true; } return false; } public int getRowCount() { return (int) ( getServiceTargetRowCount() + getCallerAndRankedTargetRowCount() + getAzLabelRowCount() + Math.ceil( (float) mChooserListAdapter.getAlphaTargetCount() / mMaxTargetsPerRow) ); } 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 (mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0; } private int getAzLabelRowPosition() { int azRowCount = getAzLabelRowCount(); if (azRowCount == 0) { return -1; } return getServiceTargetRowCount() + getCallerAndRankedTargetRowCount(); } @Override public int getItemCount() { return getServiceTargetRowCount() + getCallerAndRankedTargetRowCount() + getAzLabelRowCount() + mChooserListAdapter.getAlphaTargetCount() + getFooterRowCount(); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { 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. throw new IllegalStateException("unmatched view type"); } } /** * Set the app divider's visibility, when it's present. */ public void setAzLabelVisibility(boolean isVisible) { if (mAzLabelVisibility == isVisible) { return; } mAzLabelVisibility = isVisible; int azRowPos = getAzLabelRowPosition(); if (azRowPos >= 0) { if (mRecyclerView != null) { for (int i = 0, size = mRecyclerView.getChildCount(); i < size; i++) { View child = mRecyclerView.getChildAt(i); if (mRecyclerView.getChildAdapterPosition(child) == azRowPos) { child.setVisibility(isVisible ? View.VISIBLE : View.GONE); } } return; } notifyItemChanged(azRowPos); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (holder.getItemViewType() == VIEW_TYPE_AZ_LABEL) { holder.itemView.setVisibility( mAzLabelVisibility ? View.VISIBLE : View.INVISIBLE); } 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 = 0; int countSum = count; 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 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); DirectShareViewHolder directShareViewHolder = new DirectShareViewHolder(parentGroup, Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType); loadViewsIntoGroup(directShareViewHolder); return directShareViewHolder; } 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) { 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 ChooserListAdapter getListAdapter() { return mChooserListAdapter; } public boolean shouldCellSpan(int position) { return getItemViewType(position) == VIEW_TYPE_NORMAL; } }