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