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