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