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