1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.intentresolver.grid;
18 
19 import android.animation.AnimatorSet;
20 import android.animation.ObjectAnimator;
21 import android.animation.ValueAnimator;
22 import android.app.ActivityManager;
23 import android.content.Context;
24 import android.database.DataSetObserver;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.View.MeasureSpec;
28 import android.view.View.OnClickListener;
29 import android.view.ViewGroup;
30 import android.view.ViewGroup.LayoutParams;
31 import android.view.animation.DecelerateInterpolator;
32 import android.widget.Space;
33 import android.widget.TextView;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.recyclerview.widget.RecyclerView;
38 
39 import com.android.intentresolver.ChooserListAdapter;
40 import com.android.intentresolver.FeatureFlags;
41 import com.android.intentresolver.R;
42 import com.android.intentresolver.ResolverListAdapter.ViewHolder;
43 
44 import com.google.android.collect.Lists;
45 
46 /**
47  * Adapter for all types of items and targets in ShareSheet.
48  * Note that ranked sections like Direct Share - while appearing grid-like - are handled on the
49  * row level by this adapter but not on the item level. Individual targets within the row are
50  * handled by {@link ChooserListAdapter}
51  */
52 public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
53 
54     /**
55      * The transition time between placeholders for direct share to a message
56      * indicating that none are available.
57      */
58     public static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200;
59 
60     /**
61      * Injectable interface for any considerations that should be delegated to other components
62      * in the {@link com.android.intentresolver.ChooserActivity}.
63      * TODO: determine whether any of these methods return parameters that can safely be
64      * precomputed; whether any should be converted to `ChooserGridAdapter` setters to be
65      * invoked by external callbacks; and whether any reflect requirements that should be moved
66      * out of `ChooserGridAdapter` altogether.
67      */
68     public interface ChooserActivityDelegate {
69         /** Notify the client that the item with the selected {@code itemIndex} was selected. */
onTargetSelected(int itemIndex)70         void onTargetSelected(int itemIndex);
71 
72         /**
73          * Notify the client that the item with the selected {@code itemIndex} was
74          * long-pressed.
75          */
onTargetLongPressed(int itemIndex)76         void onTargetLongPressed(int itemIndex);
77     }
78 
79     private static final int VIEW_TYPE_DIRECT_SHARE = 0;
80     private static final int VIEW_TYPE_NORMAL = 1;
81     private static final int VIEW_TYPE_AZ_LABEL = 4;
82     private static final int VIEW_TYPE_CALLER_AND_RANK = 5;
83     private static final int VIEW_TYPE_FOOTER = 6;
84 
85     private final ChooserActivityDelegate mChooserActivityDelegate;
86     private final ChooserListAdapter mChooserListAdapter;
87     private final LayoutInflater mLayoutInflater;
88 
89     private final int mMaxTargetsPerRow;
90     private final boolean mShouldShowContentPreview;
91     private final int mChooserWidthPixels;
92     private final int mChooserRowTextOptionTranslatePixelSize;
93     private final FeatureFlags mFeatureFlags;
94     @Nullable
95     private RecyclerView mRecyclerView;
96 
97     private int mChooserTargetWidth = 0;
98 
99     private int mFooterHeight = 0;
100 
101     private boolean mAzLabelVisibility = false;
102 
ChooserGridAdapter( Context context, ChooserActivityDelegate chooserActivityDelegate, ChooserListAdapter wrappedAdapter, boolean shouldShowContentPreview, int maxTargetsPerRow, FeatureFlags featureFlags)103     public ChooserGridAdapter(
104             Context context,
105             ChooserActivityDelegate chooserActivityDelegate,
106             ChooserListAdapter wrappedAdapter,
107             boolean shouldShowContentPreview,
108             int maxTargetsPerRow,
109             FeatureFlags featureFlags) {
110         super();
111 
112         mChooserActivityDelegate = chooserActivityDelegate;
113 
114         mChooserListAdapter = wrappedAdapter;
115         mLayoutInflater = LayoutInflater.from(context);
116 
117         mShouldShowContentPreview = shouldShowContentPreview;
118         mMaxTargetsPerRow = maxTargetsPerRow;
119 
120         mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width);
121         mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize(
122                 R.dimen.chooser_row_text_option_translate);
123         mFeatureFlags = featureFlags;
124 
125         wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
126             @Override
127             public void onChanged() {
128                 super.onChanged();
129                 notifyDataSetChanged();
130             }
131 
132             @Override
133             public void onInvalidated() {
134                 super.onInvalidated();
135                 notifyDataSetChanged();
136             }
137         });
138     }
139 
140     @Override
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)141     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
142         mRecyclerView = recyclerView;
143     }
144 
145     @Override
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)146     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
147         mRecyclerView = null;
148     }
149 
setFooterHeight(int height)150     public void setFooterHeight(int height) {
151         if (mFooterHeight != height) {
152             mFooterHeight = height;
153             if (mFeatureFlags.fixTargetListFooter()) {
154                 // we always have at least one view, the footer, see getItemCount() and
155                 // getFooterRowCount()
156                 notifyItemChanged(getItemCount() - 1);
157             }
158         }
159     }
160 
161     /**
162      * Calculate the chooser target width to maximize space per item
163      *
164      * @param width The new row width to use for recalculation
165      * @return true if the view width has changed
166      */
calculateChooserTargetWidth(int width)167     public boolean calculateChooserTargetWidth(int width) {
168         if (width == 0) {
169             return false;
170         }
171 
172         // Limit width to the maximum width of the chooser activity, if the maximum width is set
173         if (mChooserWidthPixels >= 0) {
174             width = Math.min(mChooserWidthPixels, width);
175         }
176 
177         int newWidth = width / mMaxTargetsPerRow;
178         if (newWidth != mChooserTargetWidth) {
179             mChooserTargetWidth = newWidth;
180             return true;
181         }
182 
183         return false;
184     }
185 
getRowCount()186     public int getRowCount() {
187         return (int) (
188                 getServiceTargetRowCount()
189                         + getCallerAndRankedTargetRowCount()
190                         + getAzLabelRowCount()
191                         + Math.ceil(
192                         (float) mChooserListAdapter.getAlphaTargetCount()
193                                 / mMaxTargetsPerRow)
194             );
195     }
196 
getFooterRowCount()197     public int getFooterRowCount() {
198         return 1;
199     }
200 
getCallerAndRankedTargetRowCount()201     public int getCallerAndRankedTargetRowCount() {
202         return (int) Math.ceil(
203                 ((float) mChooserListAdapter.getCallerTargetCount()
204                         + mChooserListAdapter.getRankedTargetCount()) / mMaxTargetsPerRow);
205     }
206 
207     // There can be at most one row in the listview, that is internally
208     // a ViewGroup with 2 rows
getServiceTargetRowCount()209     public int getServiceTargetRowCount() {
210         if (mShouldShowContentPreview && !ActivityManager.isLowRamDeviceStatic()) {
211             return 1;
212         }
213         return 0;
214     }
215 
getAzLabelRowCount()216     public int getAzLabelRowCount() {
217         // Only show a label if the a-z list is showing
218         return (mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0;
219     }
220 
getAzLabelRowPosition()221     private int getAzLabelRowPosition() {
222         int azRowCount = getAzLabelRowCount();
223         if (azRowCount == 0) {
224             return -1;
225         }
226 
227         return getServiceTargetRowCount()
228                 + getCallerAndRankedTargetRowCount();
229     }
230 
231     @Override
getItemCount()232     public int getItemCount() {
233         return getServiceTargetRowCount()
234                 + getCallerAndRankedTargetRowCount()
235                 + getAzLabelRowCount()
236                 + mChooserListAdapter.getAlphaTargetCount()
237                 + getFooterRowCount();
238     }
239 
240     @NonNull
241     @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)242     public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
243         switch (viewType) {
244             case VIEW_TYPE_AZ_LABEL:
245                 return new ItemViewHolder(
246                         createAzLabelView(parent),
247                         viewType,
248                         null,
249                         null);
250             case VIEW_TYPE_NORMAL:
251                 return new ItemViewHolder(
252                         mChooserListAdapter.createView(parent),
253                         viewType,
254                         mChooserActivityDelegate::onTargetSelected,
255                         mChooserActivityDelegate::onTargetLongPressed);
256             case VIEW_TYPE_DIRECT_SHARE:
257             case VIEW_TYPE_CALLER_AND_RANK:
258                 return createItemGroupViewHolder(viewType, parent);
259             case VIEW_TYPE_FOOTER:
260                 Space sp = new Space(parent.getContext());
261                 sp.setLayoutParams(new RecyclerView.LayoutParams(
262                         LayoutParams.MATCH_PARENT, mFooterHeight));
263                 return new FooterViewHolder(sp, viewType);
264             default:
265                 // Since we catch all possible viewTypes above, no chance this is being called.
266                 throw new IllegalStateException("unmatched view type");
267         }
268     }
269 
270     /**
271      * Set the app divider's visibility, when it's present.
272      */
setAzLabelVisibility(boolean isVisible)273     public void setAzLabelVisibility(boolean isVisible) {
274         if (mAzLabelVisibility == isVisible) {
275             return;
276         }
277         mAzLabelVisibility = isVisible;
278         int azRowPos = getAzLabelRowPosition();
279         if (azRowPos >= 0) {
280             if (mRecyclerView != null) {
281                 for (int i = 0, size = mRecyclerView.getChildCount(); i < size; i++) {
282                     View child = mRecyclerView.getChildAt(i);
283                     if (mRecyclerView.getChildAdapterPosition(child) == azRowPos) {
284                         child.setVisibility(isVisible ? View.VISIBLE : View.GONE);
285                     }
286                 }
287                 return;
288             }
289             notifyItemChanged(azRowPos);
290         }
291     }
292 
293     @Override
onBindViewHolder(RecyclerView.ViewHolder holder, int position)294     public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
295         if (holder.getItemViewType() == VIEW_TYPE_AZ_LABEL) {
296             holder.itemView.setVisibility(
297                     mAzLabelVisibility ? View.VISIBLE : View.INVISIBLE);
298         }
299         int viewType = ((ViewHolderBase) holder).getViewType();
300         switch (viewType) {
301             case VIEW_TYPE_DIRECT_SHARE:
302             case VIEW_TYPE_CALLER_AND_RANK:
303                 bindItemGroupViewHolder(position, (ItemGroupViewHolder) holder);
304                 break;
305             case VIEW_TYPE_NORMAL:
306                 bindItemViewHolder(position, (ItemViewHolder) holder);
307                 break;
308             default:
309         }
310     }
311 
312     @Override
getItemViewType(int position)313     public int getItemViewType(int position) {
314         int count = 0;
315         int countSum = count;
316 
317         countSum += (count = getServiceTargetRowCount());
318         if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE;
319 
320         countSum += (count = getCallerAndRankedTargetRowCount());
321         if (count > 0 && position < countSum) return VIEW_TYPE_CALLER_AND_RANK;
322 
323         countSum += (count = getAzLabelRowCount());
324         if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL;
325 
326         if (position == getItemCount() - 1) return VIEW_TYPE_FOOTER;
327 
328         return VIEW_TYPE_NORMAL;
329     }
330 
getTargetType(int position)331     public int getTargetType(int position) {
332         return mChooserListAdapter.getPositionTargetType(getListPosition(position));
333     }
334 
createAzLabelView(ViewGroup parent)335     private View createAzLabelView(ViewGroup parent) {
336         return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);
337     }
338 
loadViewsIntoGroup(ItemGroupViewHolder holder)339     private ItemGroupViewHolder loadViewsIntoGroup(ItemGroupViewHolder holder) {
340         final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
341         final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth, MeasureSpec.EXACTLY);
342         int columnCount = holder.getColumnCount();
343 
344         final boolean isDirectShare = holder instanceof DirectShareViewHolder;
345 
346         for (int i = 0; i < columnCount; i++) {
347             final View v = mChooserListAdapter.createView(holder.getRowByIndex(i));
348             final int column = i;
349             v.setOnClickListener(new OnClickListener() {
350                 @Override
351                 public void onClick(View v) {
352                     mChooserActivityDelegate.onTargetSelected(holder.getItemIndex(column));
353                 }
354             });
355 
356             // Show menu for both direct share and app share targets after long click.
357             v.setOnLongClickListener(v1 -> {
358                 mChooserActivityDelegate.onTargetLongPressed(holder.getItemIndex(column));
359                 return true;
360             });
361 
362             holder.addView(i, v);
363 
364             // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
365             // false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be
366             // done before measuring.
367             if (isDirectShare) {
368                 final ViewHolder vh = (ViewHolder) v.getTag();
369                 vh.text.setLines(2);
370                 vh.text.setHorizontallyScrolling(false);
371                 vh.text2.setVisibility(View.GONE);
372             }
373 
374             // Force height to be a given so we don't have visual disruption during scaling.
375             v.measure(exactSpec, spec);
376             setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight());
377         }
378 
379         final ViewGroup viewGroup = holder.getViewGroup();
380 
381         // Pre-measure and fix height so we can scale later.
382         holder.measure();
383         setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight());
384 
385         if (isDirectShare) {
386             DirectShareViewHolder dsvh = (DirectShareViewHolder) holder;
387             setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
388             setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
389         }
390 
391         viewGroup.setTag(holder);
392         return holder;
393     }
394 
setViewBounds(View view, int widthPx, int heightPx)395     private void setViewBounds(View view, int widthPx, int heightPx) {
396         LayoutParams lp = view.getLayoutParams();
397         if (lp == null) {
398             lp = new LayoutParams(widthPx, heightPx);
399             view.setLayoutParams(lp);
400         } else {
401             lp.height = heightPx;
402             lp.width = widthPx;
403         }
404     }
405 
createItemGroupViewHolder(int viewType, ViewGroup parent)406     ItemGroupViewHolder createItemGroupViewHolder(int viewType, ViewGroup parent) {
407         if (viewType == VIEW_TYPE_DIRECT_SHARE) {
408             ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate(
409                     R.layout.chooser_row_direct_share, parent, false);
410             ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(
411                     R.layout.chooser_row, parentGroup, false);
412             ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(
413                     R.layout.chooser_row, parentGroup, false);
414             parentGroup.addView(row1);
415             parentGroup.addView(row2);
416 
417             DirectShareViewHolder directShareViewHolder = new DirectShareViewHolder(parentGroup,
418                     Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType);
419             loadViewsIntoGroup(directShareViewHolder);
420 
421             return directShareViewHolder;
422         } else {
423             ViewGroup row = (ViewGroup) mLayoutInflater.inflate(
424                     R.layout.chooser_row, parent, false);
425             ItemGroupViewHolder holder =
426                     new SingleRowViewHolder(row, mMaxTargetsPerRow, viewType);
427             loadViewsIntoGroup(holder);
428 
429             return holder;
430         }
431     }
432 
433     /**
434      * Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from
435      * showing on top of the AZ list if the AZ label is visible. All other types are placed into
436      * their own row as determined by their target type, and dividers are added in the list to
437      * separate each type.
438      */
getRowType(int rowPosition)439     int getRowType(int rowPosition) {
440         // Merge caller and ranked standard into a single row
441         int positionType = mChooserListAdapter.getPositionTargetType(rowPosition);
442         if (positionType == ChooserListAdapter.TARGET_CALLER) {
443             return ChooserListAdapter.TARGET_STANDARD;
444         }
445 
446         // If an A-Z label is shown, prevent a separator from appearing by making the A-Z
447         // row type the same as the suggestion row type
448         if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) {
449             return ChooserListAdapter.TARGET_STANDARD;
450         }
451 
452         return positionType;
453     }
454 
bindItemViewHolder(int position, ItemViewHolder holder)455     void bindItemViewHolder(int position, ItemViewHolder holder) {
456         View v = holder.itemView;
457         int listPosition = getListPosition(position);
458         holder.setListPosition(listPosition);
459         mChooserListAdapter.bindView(listPosition, v);
460     }
461 
bindItemGroupViewHolder(int position, ItemGroupViewHolder holder)462     void bindItemGroupViewHolder(int position, ItemGroupViewHolder holder) {
463         final ViewGroup viewGroup = (ViewGroup) holder.itemView;
464         int start = getListPosition(position);
465         int startType = getRowType(start);
466 
467         int columnCount = holder.getColumnCount();
468         int end = start + columnCount - 1;
469         while (getRowType(end) != startType && end >= start) {
470             end--;
471         }
472 
473         if (end == start && mChooserListAdapter.getItem(start).isEmptyTargetInfo()) {
474             final TextView textView = viewGroup.findViewById(
475                     com.android.internal.R.id.chooser_row_text_option);
476 
477             if (textView.getVisibility() != View.VISIBLE) {
478                 textView.setAlpha(0.0f);
479                 textView.setVisibility(View.VISIBLE);
480                 textView.setText(R.string.chooser_no_direct_share_targets);
481 
482                 ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f);
483                 fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
484 
485                 textView.setTranslationY(mChooserRowTextOptionTranslatePixelSize);
486                 ValueAnimator translateAnim =
487                         ObjectAnimator.ofFloat(textView, "translationY", 0.0f);
488                 translateAnim.setInterpolator(new DecelerateInterpolator(1.0f));
489 
490                 AnimatorSet animSet = new AnimatorSet();
491                 animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
492                 animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
493                 animSet.playTogether(fadeAnim, translateAnim);
494                 animSet.start();
495             }
496         }
497 
498         for (int i = 0; i < columnCount; i++) {
499             final View v = holder.getView(i);
500 
501             if (start + i <= end) {
502                 holder.setViewVisibility(i, View.VISIBLE);
503                 holder.setItemIndex(i, start + i);
504                 mChooserListAdapter.bindView(holder.getItemIndex(i), v);
505             } else {
506                 holder.setViewVisibility(i, View.INVISIBLE);
507             }
508         }
509     }
510 
getListPosition(int position)511     int getListPosition(int position) {
512         final int serviceCount = mChooserListAdapter.getServiceTargetCount();
513         final int serviceRows = (int) Math.ceil((float) serviceCount / mMaxTargetsPerRow);
514         if (position < serviceRows) {
515             return position * mMaxTargetsPerRow;
516         }
517 
518         position -= serviceRows;
519 
520         final int callerAndRankedCount =
521                 mChooserListAdapter.getCallerTargetCount()
522                 + mChooserListAdapter.getRankedTargetCount();
523         final int callerAndRankedRows = getCallerAndRankedTargetRowCount();
524         if (position < callerAndRankedRows) {
525             return serviceCount + position * mMaxTargetsPerRow;
526         }
527 
528         position -= getAzLabelRowCount() + callerAndRankedRows;
529 
530         return callerAndRankedCount + serviceCount + position;
531     }
532 
getListAdapter()533     public ChooserListAdapter getListAdapter() {
534         return mChooserListAdapter;
535     }
536 
shouldCellSpan(int position)537     public boolean shouldCellSpan(int position) {
538         return getItemViewType(position) == VIEW_TYPE_NORMAL;
539     }
540 }
541