1 /* 2 * Copyright (C) 2023 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 package com.android.launcher3.widget.picker; 17 18 import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker; 19 import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER; 20 import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER; 21 import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree; 22 import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree; 23 import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG; 24 import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_DELAY; 25 import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser; 26 27 import android.content.Context; 28 import android.graphics.Rect; 29 import android.os.Process; 30 import android.os.UserHandle; 31 import android.util.AttributeSet; 32 import android.view.Gravity; 33 import android.view.LayoutInflater; 34 import android.view.MenuItem; 35 import android.view.MotionEvent; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.ViewParent; 39 import android.view.accessibility.AccessibilityNodeInfo; 40 import android.widget.FrameLayout; 41 import android.widget.LinearLayout; 42 import android.widget.PopupMenu; 43 import android.widget.ScrollView; 44 import android.widget.TextView; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 import androidx.annotation.Px; 49 50 import com.android.launcher3.DeviceProfile; 51 import com.android.launcher3.R; 52 import com.android.launcher3.Utilities; 53 import com.android.launcher3.icons.cache.CacheLookupFlag; 54 import com.android.launcher3.model.WidgetItem; 55 import com.android.launcher3.model.data.PackageItemInfo; 56 import com.android.launcher3.recyclerview.ViewHolderBinder; 57 import com.android.launcher3.util.PackageUserKey; 58 import com.android.launcher3.widget.WidgetCell; 59 import com.android.launcher3.widget.model.WidgetsListBaseEntry; 60 import com.android.launcher3.widget.model.WidgetsListContentEntry; 61 import com.android.launcher3.widget.model.WidgetsListHeaderEntry; 62 63 import java.util.Collections; 64 import java.util.List; 65 66 /** 67 * Popup for showing the full list of available widgets with a two-pane layout. 68 */ 69 public class WidgetsTwoPaneSheet extends WidgetsFullSheet { 70 private static final int MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 268; 71 private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395; 72 private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry"; 73 74 // This ratio defines the max percentage of content area that the recommendations can display 75 // with respect to the bottom sheet's height. 76 private static final float RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE = 0.70f; 77 private FrameLayout mSuggestedWidgetsContainer; 78 private WidgetsListHeader mSuggestedWidgetsHeader; 79 private PackageUserKey mSuggestedWidgetsPackageUserKey; 80 private View mPrimaryWidgetListView; 81 private LinearLayout mRightPane; 82 83 private ScrollView mRightPaneScrollView; 84 private WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder; 85 86 private boolean mOldIsSwipeToDismissInProgress; 87 private int mActivePage = -1; 88 @Nullable 89 private PackageUserKey mSelectedHeader; 90 private TextView mHeaderDescription; 91 92 /** 93 * A menu displayed for options (e.g. "show all widgets" filter) around widget lists in the 94 * picker. 95 */ 96 protected View mWidgetOptionsMenu; 97 /** 98 * State of the options in the menu (if displayed to the user). 99 */ 100 @Nullable 101 protected WidgetOptionsMenuState mWidgetOptionsMenuState = null; 102 WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr)103 public WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr) { 104 super(context, attrs, defStyleAttr); 105 } 106 WidgetsTwoPaneSheet(Context context, AttributeSet attrs)107 public WidgetsTwoPaneSheet(Context context, AttributeSet attrs) { 108 super(context, attrs); 109 } 110 111 @Override setupSheet()112 protected void setupSheet() { 113 // Set the header change listener in the adapter 114 mAdapters.get(AdapterHolder.PRIMARY) 115 .mWidgetsListAdapter.setHeaderChangeListener(getHeaderChangeListener()); 116 mAdapters.get(AdapterHolder.WORK) 117 .mWidgetsListAdapter.setHeaderChangeListener(getHeaderChangeListener()); 118 mAdapters.get(AdapterHolder.SEARCH) 119 .mWidgetsListAdapter.setHeaderChangeListener(getHeaderChangeListener()); 120 121 LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 122 123 int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_two_pane_sheet_paged_view 124 : R.layout.widgets_two_pane_sheet_recyclerview; 125 layoutInflater.inflate(contentLayoutRes, findViewById(R.id.recycler_view_container), true); 126 127 setupViews(); 128 129 mWidgetsListTableViewHolderBinder = 130 new WidgetsListTableViewHolderBinder(mActivityContext, layoutInflater, this, this); 131 132 mWidgetRecommendationsContainer = mContent.findViewById( 133 R.id.widget_recommendations_container); 134 mWidgetRecommendationsView = mContent.findViewById( 135 R.id.widget_recommendations_view); 136 mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer); 137 mWidgetRecommendationsView.setWidgetCellLongClickListener(this); 138 mWidgetRecommendationsView.setWidgetCellOnClickListener(this); 139 if (!mDeviceProfile.isTwoPanels) { 140 mWidgetRecommendationsView.enableFullPageViewIfLowDensity(); 141 } 142 // To save the currently displayed page, so that, it can be requested when rebinding 143 // recommendations with different size constraints. 144 mWidgetRecommendationsView.addPageSwitchListener( 145 newPage -> mRecommendationsCurrentPage = newPage); 146 147 mHeaderTitle = mContent.findViewById(R.id.title); 148 mHeaderDescription = mContent.findViewById(R.id.widget_picker_description); 149 150 mWidgetOptionsMenu = mContent.findViewById(R.id.widget_picker_widget_options_menu); 151 if (!enableTieredWidgetsByDefaultInPicker()) { 152 setupWidgetOptionsMenu(); 153 } 154 155 mRightPane = mContent.findViewById(R.id.right_pane); 156 mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view); 157 mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); 158 159 mPrimaryWidgetListView = findViewById(R.id.primary_widgets_list_view); 160 mPrimaryWidgetListView.setOutlineProvider(mViewOutlineProvider); 161 mPrimaryWidgetListView.setClipToOutline(true); 162 163 onWidgetsBound(); 164 165 // Set the fast scroller as not visible for two pane layout. 166 mFastScroller.setVisibility(GONE); 167 } 168 169 @Override mayUpdateTitleAndDescription(@ullable String title, @Nullable String description)170 public void mayUpdateTitleAndDescription(@Nullable String title, @Nullable String description) { 171 if (title != null) { 172 mHeaderTitle.setText(title); 173 } 174 if (description != null) { 175 mHeaderDescription.setText(description); 176 mHeaderDescription.setVisibility(VISIBLE); 177 } 178 } 179 setupWidgetOptionsMenu()180 protected void setupWidgetOptionsMenu() { 181 mWidgetOptionsMenu.setOnClickListener(new OnClickListener() { 182 @Override 183 public void onClick(View v) { 184 if (mWidgetOptionsMenuState != null) { 185 PopupMenu popupMenu = new PopupMenu(mActivityContext, /*anchor=*/ v, 186 Gravity.END); 187 MenuItem menuItem = popupMenu.getMenu().add( 188 R.string.widget_picker_show_all_widgets_menu_item_title); 189 menuItem.setCheckable(true); 190 menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets); 191 menuItem.setOnMenuItemClickListener( 192 item -> onShowAllWidgetsMenuItemClick(item)); 193 popupMenu.show(); 194 } 195 } 196 }); 197 } 198 onShowAllWidgetsMenuItemClick(MenuItem menuItem)199 private boolean onShowAllWidgetsMenuItemClick(MenuItem menuItem) { 200 mWidgetOptionsMenuState.showAllWidgets = !mWidgetOptionsMenuState.showAllWidgets; 201 menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets); 202 203 // Refresh widgets 204 onWidgetsBound(); 205 if (mIsInSearchMode) { 206 mSearchBar.reset(); 207 } else if (!mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader)) { 208 mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); 209 mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop(); 210 } 211 return true; 212 } 213 214 @Override getTabletHorizontalMargin(DeviceProfile deviceProfile)215 protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) { 216 // two pane picker is full width for fold as well as tablet. 217 return getResources().getDimensionPixelSize( 218 R.dimen.widget_picker_two_panels_left_right_margin); 219 } 220 221 @Override onUserSwipeToDismissProgressChanged()222 protected void onUserSwipeToDismissProgressChanged() { 223 super.onUserSwipeToDismissProgressChanged(); 224 boolean isSwipeToDismissInProgress = mSwipeToDismissProgress.value > 0; 225 if (isSwipeToDismissInProgress == mOldIsSwipeToDismissInProgress) { 226 return; 227 } 228 mOldIsSwipeToDismissInProgress = isSwipeToDismissInProgress; 229 if (isSwipeToDismissInProgress) { 230 modifyAttributesOnViewTree(mPrimaryWidgetListView, (ViewParent) mContent, 231 CLIP_CHILDREN_FALSE_MODIFIER); 232 modifyAttributesOnViewTree(mRightPaneScrollView, (ViewParent) mContent, 233 CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER); 234 } else { 235 restoreAttributesOnViewTree(mPrimaryWidgetListView, mContent, 236 CLIP_CHILDREN_FALSE_MODIFIER); 237 restoreAttributesOnViewTree(mRightPaneScrollView, mContent, 238 CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER); 239 } 240 } 241 242 @Override onLayout(boolean changed, int l, int t, int r, int b)243 protected void onLayout(boolean changed, int l, int t, int r, int b) { 244 super.onLayout(changed, l, t, r, b); 245 if (changed && mDeviceProfile.isTwoPanels) { 246 LinearLayout layout = mContent.findViewById(R.id.linear_layout_container); 247 FrameLayout leftPane = layout.findViewById(R.id.recycler_view_container); 248 LinearLayout.LayoutParams layoutParams = (LayoutParams) leftPane.getLayoutParams(); 249 // Width is 1/3 of the sheet unless it's less than min width or max width 250 int leftPaneWidth = layout.getMeasuredWidth() / 3; 251 @Px int minLeftPaneWidthPx = Utilities.dpToPx(MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP); 252 @Px int maxLeftPaneWidthPx = Utilities.dpToPx(MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP); 253 if (leftPaneWidth < minLeftPaneWidthPx) { 254 layoutParams.width = minLeftPaneWidthPx; 255 } else if (leftPaneWidth > maxLeftPaneWidthPx) { 256 layoutParams.width = maxLeftPaneWidthPx; 257 } else { 258 layoutParams.width = 0; 259 } 260 layoutParams.weight = layoutParams.width == 0 ? 0.33F : 0; 261 262 post(() -> { 263 // The following calls all trigger requestLayout, so we post them to avoid 264 // calling requestLayout during a layout pass. This also fixes the related warnings 265 // in logcat. 266 leftPane.setLayoutParams(layoutParams); 267 requestApplyInsets(); 268 if (mSelectedHeader != null) { 269 if (mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey)) { 270 mSuggestedWidgetsHeader.callOnClick(); 271 } else { 272 getHeaderChangeListener().onHeaderChanged(mSelectedHeader); 273 } 274 } 275 }); 276 } 277 } 278 279 // Used by the two pane sheet to show 3-dot menu to toggle between default lists and all lists 280 // when enableTieredWidgetsByDefaultInPicker is OFF. This code path and the 3-dot menu can be 281 // safely deleted when it's alternative "enableTieredWidgetsByDefaultInPicker" flag is inlined. 282 @Override getWidgetsToDisplay()283 protected List<WidgetsListBaseEntry> getWidgetsToDisplay() { 284 List<WidgetsListBaseEntry> allWidgets = 285 mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets(); 286 List<WidgetsListBaseEntry> defaultWidgets = 287 mActivityContext.getWidgetPickerDataProvider().get().getDefaultWidgets(); 288 289 if (allWidgets.isEmpty() || defaultWidgets.isEmpty()) { 290 // no menu if there are no default widgets to show 291 mWidgetOptionsMenuState = null; 292 mWidgetOptionsMenu.setVisibility(GONE); 293 } else { 294 if (mWidgetOptionsMenuState == null) { 295 mWidgetOptionsMenuState = new WidgetOptionsMenuState(); 296 } 297 298 mWidgetOptionsMenu.setVisibility(VISIBLE); 299 return mWidgetOptionsMenuState.showAllWidgets ? allWidgets : defaultWidgets; 300 } 301 302 return allWidgets; 303 } 304 305 @Override onWidgetsBound()306 public void onWidgetsBound() { 307 super.onWidgetsBound(); 308 if (mRecommendedWidgetsCount == 0 && mSelectedHeader == null) { 309 mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); 310 mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop(); 311 } 312 } 313 314 @Override onWidgetsListExpandButtonClick(View v)315 public void onWidgetsListExpandButtonClick(View v) { 316 super.onWidgetsListExpandButtonClick(v); 317 // Refresh right pane with updated data for the selected header. 318 if (mSelectedHeader != null && mSelectedHeader != mSuggestedWidgetsPackageUserKey) { 319 getHeaderChangeListener().onHeaderChanged(mSelectedHeader); 320 } 321 } 322 323 @Override onRecommendedWidgetsBound()324 public void onRecommendedWidgetsBound() { 325 super.onRecommendedWidgetsBound(); 326 327 if (mSuggestedWidgetsContainer == null && mRecommendedWidgetsCount > 0) { 328 setupSuggestedWidgets(LayoutInflater.from(getContext())); 329 mSuggestedWidgetsHeader.callOnClick(); 330 } else if (mSelectedHeader != null 331 && mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey)) { 332 // Reselect widget if we are reloading recommendations while it is currently showing. 333 selectWidgetCell(mWidgetRecommendationsContainer, getLastSelectedWidgetItem()); 334 } 335 } 336 setupSuggestedWidgets(LayoutInflater layoutInflater)337 private void setupSuggestedWidgets(LayoutInflater layoutInflater) { 338 // Add suggested widgets. 339 mSuggestedWidgetsContainer = mSearchScrollView.findViewById(R.id.suggestions_header); 340 341 // Inflate the suggestions header. 342 mSuggestedWidgetsHeader = (WidgetsListHeader) layoutInflater.inflate( 343 R.layout.widgets_list_row_header_two_pane, 344 mSuggestedWidgetsContainer, 345 false); 346 mSuggestedWidgetsHeader.setExpanded(true); 347 348 PackageItemInfo packageItemInfo = new HighresPackageItemInfo( 349 /* packageName= */ SUGGESTIONS_PACKAGE_NAME, 350 Process.myUserHandle()); 351 String suggestionsHeaderTitle = getContext().getString( 352 R.string.suggested_widgets_header_title); 353 String suggestionsRightPaneTitle = getContext().getString( 354 R.string.widget_picker_right_pane_accessibility_title, suggestionsHeaderTitle); 355 packageItemInfo.title = suggestionsHeaderTitle; 356 // Suggestions may update at run time. The widgets count on suggestions doesn't add any 357 // value, so, we don't show the count. 358 WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create( 359 packageItemInfo, 360 /*titleSectionName=*/ suggestionsHeaderTitle, 361 /*items=*/ List.of(), // not necessary 362 /*visibleWidgetsCount=*/ 0) 363 .withWidgetListShown(); 364 365 mSuggestedWidgetsHeader.applyFromItemInfoWithIcon(widgetsListHeaderEntry); 366 mSuggestedWidgetsHeader.setIcon( 367 getContext().getDrawable(R.drawable.widget_suggestions_icon)); 368 mSuggestedWidgetsHeader.setOnClickListener(view -> { 369 mSuggestedWidgetsHeader.setExpanded(true); 370 resetExpandedHeaders(); 371 mRightPane.removeAllViews(); 372 mRightPane.addView(mWidgetRecommendationsContainer); 373 mRightPaneScrollView.setScrollY(0); 374 mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo); 375 final boolean isChangingHeaders = mSelectedHeader == null 376 || !mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey); 377 if (isChangingHeaders) { 378 // If the initial focus view is still focused or widget picker is still opening, it 379 // is likely a programmatic header click. 380 if (mSelectedHeader != null && !mOpenCloseAnimation.getAnimationPlayer().isRunning() 381 && !getAccessibilityInitialFocusView().isAccessibilityFocused()) { 382 mRightPaneScrollView.setAccessibilityPaneTitle(suggestionsRightPaneTitle); 383 focusOnFirstWidgetCell(mWidgetRecommendationsView); 384 } 385 // If switching from another header, unselect any WidgetCells. This is necessary 386 // because we do not clear/recycle the WidgetCells in the recommendations container 387 // when the header is clicked, only when onRecommendationsBound is called. That 388 // means a WidgetCell in the recommendations container may still be selected from 389 // the last time the recommendations were shown. 390 unselectWidgetCell(mWidgetRecommendationsContainer, getLastSelectedWidgetItem()); 391 } 392 mSelectedHeader = mSuggestedWidgetsPackageUserKey; 393 }); 394 mSuggestedWidgetsContainer.addView(mSuggestedWidgetsHeader); 395 } 396 397 @Override 398 @Px getMaxAvailableHeightForRecommendations()399 protected float getMaxAvailableHeightForRecommendations() { 400 if (mRecommendedWidgetsCount > 0) { 401 // If widgets were already selected for display, we show them all on orientation change 402 // in a two pane picker 403 return Float.MAX_VALUE; 404 } 405 406 return (mDeviceProfile.heightPx - mDeviceProfile.bottomSheetTopPadding) 407 * RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE; 408 } 409 410 @Override 411 @Px getAvailableWidthForSuggestions(int pickerAvailableWidth)412 protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) { 413 int rightPaneWidth = (int) Math.ceil(0.67 * pickerAvailableWidth); 414 415 if (mDeviceProfile.isTwoPanels) { 416 // See onLayout 417 int leftPaneWidth = (int) (0.33 * pickerAvailableWidth); 418 @Px int minLeftPaneWidthPx = Utilities.dpToPx(MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP); 419 @Px int maxLeftPaneWidthPx = Utilities.dpToPx(MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP); 420 if (leftPaneWidth < minLeftPaneWidthPx) { 421 leftPaneWidth = minLeftPaneWidthPx; 422 } else if (leftPaneWidth > maxLeftPaneWidthPx) { 423 leftPaneWidth = maxLeftPaneWidthPx; 424 } 425 rightPaneWidth = pickerAvailableWidth - leftPaneWidth; 426 } 427 428 // Since suggestions are shown in right pane, the available width is 2/3 of total width of 429 // bottom sheet. 430 return rightPaneWidth - getResources().getDimensionPixelSize( 431 R.dimen.widget_list_horizontal_margin_two_pane); // right pane end margin. 432 } 433 434 @Override onActivePageChanged(int currentActivePage)435 public void onActivePageChanged(int currentActivePage) { 436 super.onActivePageChanged(currentActivePage); 437 438 // If active page didn't change then we don't want to update the header. 439 if (mActivePage == currentActivePage) { 440 return; 441 } 442 443 mActivePage = currentActivePage; 444 445 // When using talkback, swiping left while on right pane, should navigate to the widgets 446 // list on left. 447 mAdapters.get(mActivePage).mWidgetsRecyclerView.setAccessibilityTraversalBefore( 448 mRightPaneScrollView.getId()); 449 450 // On page change, select the first item in the list to show in the right pane. 451 mAdapters.get(currentActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); 452 mAdapters.get(currentActivePage).mWidgetsRecyclerView.scrollToTop(); 453 } 454 455 @Override updateRecyclerViewVisibility(AdapterHolder adapterHolder)456 protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) { 457 // The first item is always an empty space entry. Look for any more items. 458 boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries(); 459 if (!isWidgetAvailable) { 460 mRightPane.removeAllViews(); 461 mRightPane.addView(mNoWidgetsView); 462 // with no widgets message, no header is selected on left 463 if (mSuggestedWidgetsPackageUserKey != null 464 && mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader) 465 && mSuggestedWidgetsHeader != null) { 466 mSuggestedWidgetsHeader.setExpanded(false); 467 } 468 mSelectedHeader = null; 469 } 470 super.updateRecyclerViewVisibility(adapterHolder); 471 } 472 473 @Override onSearchResults(List<WidgetsListBaseEntry> entries)474 public void onSearchResults(List<WidgetsListBaseEntry> entries) { 475 super.onSearchResults(entries); 476 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.selectFirstHeaderEntry(); 477 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop(); 478 } 479 480 @Override shouldScroll(MotionEvent ev)481 protected boolean shouldScroll(MotionEvent ev) { 482 return getPopupContainer().isEventOverView(mRightPaneScrollView, ev) 483 ? mRightPaneScrollView.canScrollVertically(-1) 484 : super.shouldScroll(ev); 485 } 486 487 @Override setViewVisibilityBasedOnSearch(boolean isInSearchMode)488 protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { 489 super.setViewVisibilityBasedOnSearch(isInSearchMode); 490 491 if (mSuggestedWidgetsHeader != null && mSuggestedWidgetsContainer != null) { 492 if (!isInSearchMode) { 493 mSuggestedWidgetsContainer.setVisibility(VISIBLE); 494 mSuggestedWidgetsHeader.callOnClick(); 495 } else { 496 mSuggestedWidgetsContainer.setVisibility(GONE); 497 } 498 } else if (!isInSearchMode) { 499 mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); 500 } 501 502 } 503 getHeaderChangeListener()504 private HeaderChangeListener getHeaderChangeListener() { 505 return new HeaderChangeListener() { 506 @Override 507 public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) { 508 final boolean isSameHeader = mSelectedHeader != null 509 && mSelectedHeader.equals(selectedHeader); 510 // If the initial focus view is still focused or widget picker is still opening, it 511 // is likely a programmatic header click. 512 final boolean isUserClick = mSelectedHeader != null 513 && !mOpenCloseAnimation.getAnimationPlayer().isRunning() 514 && !getAccessibilityInitialFocusView().isAccessibilityFocused(); 515 mSelectedHeader = selectedHeader; 516 517 WidgetsListContentEntry contentEntry; 518 if (enableTieredWidgetsByDefaultInPicker()) { 519 contentEntry = mAdapters.get( 520 getCurrentAdapterHolderType()).mWidgetsListAdapter.getContentEntry( 521 selectedHeader); 522 } else { // Can be deleted when inlining the "enableTieredWidgetsByDefaultInPicker" 523 // flag 524 final boolean showDefaultWidgets = mWidgetOptionsMenuState != null 525 && !mWidgetOptionsMenuState.showAllWidgets; 526 contentEntry = findContentEntryForPackageUser( 527 mActivityContext.getWidgetPickerDataProvider().get(), 528 selectedHeader, showDefaultWidgets); 529 } 530 531 if (contentEntry == null || mRightPane == null) { 532 return; 533 } 534 535 if (mSuggestedWidgetsHeader != null) { 536 mSuggestedWidgetsHeader.setExpanded(false); 537 } 538 539 WidgetsListContentEntry contentEntryToBind; 540 // Setting max span size enables row to understand how to fit more than one item 541 // in a row. 542 contentEntryToBind = contentEntry.withMaxSpanSize(mMaxSpanPerRow); 543 544 WidgetsRowViewHolder widgetsRowViewHolder = 545 mWidgetsListTableViewHolderBinder.newViewHolder(mRightPane); 546 mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder, 547 contentEntryToBind, 548 ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST, 549 Collections.EMPTY_LIST); 550 if (isSameHeader) { 551 // Reselect the last selected widget if we are reloading the same header. 552 selectWidgetCell(widgetsRowViewHolder.tableContainer, 553 getLastSelectedWidgetItem()); 554 } 555 widgetsRowViewHolder.mDataCallback = data -> { 556 mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder, 557 contentEntryToBind, 558 ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST, 559 Collections.singletonList(data)); 560 if (isSameHeader) { 561 selectWidgetCell(widgetsRowViewHolder.tableContainer, 562 getLastSelectedWidgetItem()); 563 } 564 }; 565 mRightPane.removeAllViews(); 566 mRightPane.addView(widgetsRowViewHolder.itemView); 567 if (isUserClick) { 568 mRightPaneScrollView.setAccessibilityPaneTitle(getContext().getString( 569 R.string.widget_picker_right_pane_accessibility_title, 570 contentEntry.mPkgItem.title)); 571 postDelayed(() -> focusOnFirstWidgetCell(widgetsRowViewHolder.tableContainer), 572 WIDGET_LIST_ITEM_APPEARANCE_DELAY); 573 } 574 mRightPaneScrollView.setScrollY(0); 575 } 576 }; 577 } 578 579 private static void selectWidgetCell(ViewGroup parent, WidgetItem item) { 580 if (parent == null || item == null) return; 581 WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell wc 582 && wc.matchesItem(item)); 583 if (cell != null && !cell.isShowingAddButton()) { 584 cell.callOnClick(); 585 } 586 } 587 588 /** 589 * Requests focus on the first widget cell in the given widget section. 590 */ 591 private static void focusOnFirstWidgetCell(ViewGroup parent) { 592 if (parent == null) return; 593 WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell); 594 if (cell != null) { 595 cell.performAccessibilityAction( 596 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 597 } 598 } 599 600 private static void unselectWidgetCell(ViewGroup parent, WidgetItem item) { 601 if (parent == null || item == null) return; 602 WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell wc 603 && wc.matchesItem(item)); 604 if (cell != null && cell.isShowingAddButton()) { 605 cell.hideAddButton(/* animate= */ false); 606 } 607 } 608 609 @Override 610 public void setInsets(Rect insets) { 611 super.setInsets(insets); 612 FrameLayout rightPaneContainer = mContent.findViewById(R.id.right_pane_container); 613 rightPaneContainer.setPadding( 614 rightPaneContainer.getPaddingLeft(), 615 rightPaneContainer.getPaddingTop(), 616 rightPaneContainer.getPaddingRight(), 617 mBottomPadding); 618 requestLayout(); 619 } 620 621 @Override 622 protected int getWidgetListHorizontalMargin() { 623 return getResources().getDimensionPixelSize( 624 R.dimen.widget_list_left_pane_horizontal_margin); 625 } 626 627 @Override 628 protected boolean isTwoPane() { 629 return true; 630 } 631 632 @Override 633 protected int getHeaderTopClip(@NonNull WidgetCell cell) { 634 return 0; 635 } 636 637 @Override 638 protected void scrollCellContainerByY(WidgetCell wc, int scrollByY) { 639 for (ViewParent parent = wc.getParent(); parent != null; parent = parent.getParent()) { 640 if (parent instanceof ScrollView scrollView) { 641 scrollView.smoothScrollBy(0, scrollByY); 642 return; 643 } else if (parent == this) { 644 return; 645 } 646 } 647 } 648 649 /** 650 * This is a listener for when the selected header gets changed in the left pane. 651 */ 652 public interface HeaderChangeListener { 653 /** 654 * Sets the right pane to have the widgets for the currently selected header from 655 * the left pane. 656 */ 657 void onHeaderChanged(@NonNull PackageUserKey selectedHeader); 658 } 659 660 /** 661 * Holds the selection state of the options menu (if presented to the user). 662 */ 663 protected static class WidgetOptionsMenuState { 664 /** 665 * UI state indicating whether to show default or all widgets. 666 * <p>If true, shows all widgets; else shows the default widgets.</p> 667 */ 668 public boolean showAllWidgets = false; 669 } 670 671 private static class HighresPackageItemInfo extends PackageItemInfo { 672 HighresPackageItemInfo(String packageName, UserHandle user) { 673 super(packageName, user); 674 } 675 676 @Override 677 public CacheLookupFlag getMatchingLookupFlag() { 678 return DEFAULT_LOOKUP_FLAG; 679 } 680 } 681 } 682