/* * 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 { /** * 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); } }