1 /* 2 * Copyright (C) 2017 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.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; 20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_EXPAND_PRESS; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED; 22 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; 23 import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.WIDGET_SCROLLER; 24 25 import static java.lang.Math.abs; 26 import static java.util.Collections.emptyList; 27 28 import android.animation.Animator; 29 import android.content.Context; 30 import android.content.res.Resources; 31 import android.graphics.Rect; 32 import android.os.Bundle; 33 import android.os.Parcelable; 34 import android.os.Process; 35 import android.os.UserHandle; 36 import android.os.UserManager; 37 import android.util.AttributeSet; 38 import android.util.Pair; 39 import android.util.SparseArray; 40 import android.view.LayoutInflater; 41 import android.view.MotionEvent; 42 import android.view.View; 43 import android.view.ViewConfiguration; 44 import android.view.ViewGroup; 45 import android.view.ViewParent; 46 import android.view.WindowInsets; 47 import android.view.WindowInsetsController; 48 import android.view.animation.AnimationUtils; 49 import android.view.animation.Interpolator; 50 import android.widget.Button; 51 import android.widget.LinearLayout; 52 import android.widget.TextView; 53 54 import androidx.annotation.NonNull; 55 import androidx.annotation.Nullable; 56 import androidx.annotation.Px; 57 import androidx.annotation.VisibleForTesting; 58 import androidx.recyclerview.widget.DefaultItemAnimator; 59 import androidx.recyclerview.widget.RecyclerView; 60 61 import com.android.launcher3.BaseActivity; 62 import com.android.launcher3.DeviceProfile; 63 import com.android.launcher3.R; 64 import com.android.launcher3.anim.PendingAnimation; 65 import com.android.launcher3.compat.AccessibilityManagerCompat; 66 import com.android.launcher3.model.UserManagerState; 67 import com.android.launcher3.model.WidgetItem; 68 import com.android.launcher3.pm.UserCache; 69 import com.android.launcher3.views.RecyclerViewFastScroller; 70 import com.android.launcher3.views.SpringRelativeLayout; 71 import com.android.launcher3.views.StickyHeaderLayout; 72 import com.android.launcher3.widget.BaseWidgetSheet; 73 import com.android.launcher3.widget.WidgetCell; 74 import com.android.launcher3.widget.model.WidgetsListBaseEntry; 75 import com.android.launcher3.widget.picker.model.data.WidgetPickerData; 76 import com.android.launcher3.widget.picker.search.SearchModeListener; 77 import com.android.launcher3.widget.picker.search.WidgetsSearchBar; 78 import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider; 79 import com.android.launcher3.workprofile.PersonalWorkPagedView; 80 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; 81 82 import java.util.ArrayList; 83 import java.util.HashMap; 84 import java.util.List; 85 import java.util.Map; 86 import java.util.function.Predicate; 87 import java.util.stream.IntStream; 88 89 /** 90 * Popup for showing the full list of available widgets 91 */ 92 public class WidgetsFullSheet extends BaseWidgetSheet 93 implements OnActivePageChangedListener, 94 WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener, 95 WidgetsListAdapter.ExpandButtonClickListener { 96 97 private static final long FADE_IN_DURATION = 150; 98 99 // The widget recommendation table can easily take over the entire screen on devices with small 100 // resolution or landscape on phone. This ratio defines the max percentage of content area that 101 // the table can display with respect to bottom sheet's height. 102 private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.45f; 103 private static final String RECOMMENDATIONS_SAVED_STATE_KEY = 104 "widgetsFullSheet:mRecommendationsCurrentPage"; 105 private static final String SUPER_SAVED_STATE_KEY = "widgetsFullSheet:superHierarchyState"; 106 private final UserCache mUserCache; 107 private final UserManagerState mUserManagerState = new UserManagerState(); 108 private final UserHandle mCurrentUser = Process.myUserHandle(); 109 private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter = 110 entry -> mCurrentUser.equals(entry.mPkgItem.user); 111 private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter; 112 protected final boolean mHasWorkProfile; 113 // Number of recommendations displayed 114 protected int mRecommendedWidgetsCount; 115 private List<WidgetItem> mRecommendedWidgets = new ArrayList<>(); 116 private Map<WidgetRecommendationCategory, List<WidgetItem>> mRecommendedWidgetsMap = 117 new HashMap<>(); 118 protected int mRecommendationsCurrentPage = 0; 119 protected final SparseArray<AdapterHolder> mAdapters = new SparseArray(); 120 121 // Helps with removing focus from searchbar by analyzing motion events. 122 private final SearchClearFocusHelper mSearchClearFocusHelper = new SearchClearFocusHelper(); 123 private final float mTouchSlop; // initialized in constructor 124 125 private final OnAttachStateChangeListener mBindScrollbarInSearchMode = 126 new OnAttachStateChangeListener() { 127 @Override 128 public void onViewAttachedToWindow(View view) { 129 WidgetsRecyclerView searchRecyclerView = 130 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; 131 if (mIsInSearchMode && searchRecyclerView != null) { 132 searchRecyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER); 133 } 134 } 135 136 @Override 137 public void onViewDetachedFromWindow(View view) { 138 } 139 }; 140 141 @Px 142 private final int mTabsHeight; 143 144 @Nullable 145 private WidgetsRecyclerView mCurrentWidgetsRecyclerView; 146 @Nullable 147 private WidgetsRecyclerView mCurrentTouchEventRecyclerView; 148 @Nullable 149 PersonalWorkPagedView mViewPager; 150 protected boolean mIsInSearchMode; 151 private boolean mIsNoWidgetsViewNeeded; 152 @Px 153 protected int mMaxSpanPerRow; 154 protected DeviceProfile mDeviceProfile; 155 156 protected TextView mNoWidgetsView; 157 protected LinearLayout mSearchScrollView; 158 // Reference to the mSearchScrollView when it is is a sticky header. 159 private @Nullable StickyHeaderLayout mStickyHeaderLayout; 160 protected WidgetRecommendationsView mWidgetRecommendationsView; 161 protected LinearLayout mWidgetRecommendationsContainer; 162 protected View mTabBar; 163 protected View mSearchBarContainer; 164 protected WidgetsSearchBar mSearchBar; 165 protected TextView mHeaderTitle; 166 protected RecyclerViewFastScroller mFastScroller; 167 protected int mBottomPadding; 168 WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr)169 public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) { 170 super(context, attrs, defStyleAttr); 171 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 172 mDeviceProfile = mActivityContext.getDeviceProfile(); 173 mUserCache = UserCache.INSTANCE.get(context); 174 mHasWorkProfile = mUserCache.getUserProfiles() 175 .stream() 176 .anyMatch(user -> mUserCache.getUserInfo(user).isWork()); 177 mWorkWidgetsFilter = entry -> mHasWorkProfile 178 && mUserCache.getUserInfo(entry.mPkgItem.user).isWork() 179 && !mUserManagerState.isUserQuiet(entry.mPkgItem.user); 180 mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY)); 181 mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); 182 mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); 183 184 Resources resources = getResources(); 185 mUserManagerState.init(UserCache.INSTANCE.get(context), 186 context.getSystemService(UserManager.class)); 187 mTabsHeight = mHasWorkProfile 188 ? resources.getDimensionPixelSize(R.dimen.all_apps_header_pill_height) 189 : 0; 190 } 191 WidgetsFullSheet(Context context, AttributeSet attrs)192 public WidgetsFullSheet(Context context, AttributeSet attrs) { 193 this(context, attrs, 0); 194 } 195 196 @Override onFinishInflate()197 protected void onFinishInflate() { 198 super.onFinishInflate(); 199 200 mContent = findViewById(R.id.container); 201 setContentBackgroundWithParent(getContext().getDrawable(R.drawable.bg_widgets_full_sheet), 202 mContent); 203 mContent.setOutlineProvider(mViewOutlineProvider); 204 mContent.setClipToOutline(true); 205 setupSheet(); 206 } 207 setupSheet()208 protected void setupSheet() { 209 LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 210 int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view 211 : R.layout.widgets_full_sheet_recyclerview; 212 layoutInflater.inflate(contentLayoutRes, mContent, true); 213 214 setupViews(); 215 216 mWidgetRecommendationsContainer = mSearchScrollView.findViewById( 217 R.id.widget_recommendations_container); 218 mWidgetRecommendationsView = mSearchScrollView.findViewById( 219 R.id.widget_recommendations_view); 220 // To save the currently displayed page, so that, it can be requested when rebinding 221 // recommendations with different size constraints. 222 mWidgetRecommendationsView.addPageSwitchListener( 223 newPage -> mRecommendationsCurrentPage = newPage); 224 mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer); 225 mWidgetRecommendationsView.setWidgetCellLongClickListener(this); 226 mWidgetRecommendationsView.setWidgetCellOnClickListener(this); 227 228 mHeaderTitle = mSearchScrollView.findViewById(R.id.title); 229 230 onWidgetsBound(); 231 } 232 setupViews()233 protected void setupViews() { 234 mSearchScrollView = findViewById(R.id.search_and_recommendations_container); 235 if (mSearchScrollView instanceof StickyHeaderLayout) { 236 mStickyHeaderLayout = (StickyHeaderLayout) mSearchScrollView; 237 mStickyHeaderLayout.setCurrentRecyclerView( 238 findViewById(R.id.primary_widgets_list_view)); 239 } 240 mNoWidgetsView = findViewById(R.id.no_widgets_text); 241 mFastScroller = findViewById(R.id.fast_scroller); 242 mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup)); 243 mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view)); 244 mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view)); 245 if (mHasWorkProfile) { 246 mViewPager = findViewById(R.id.widgets_view_pager); 247 mViewPager.setOutlineProvider(mViewOutlineProvider); 248 mViewPager.setClipToOutline(true); 249 mViewPager.setClipChildren(false); 250 mViewPager.initParentViews(this); 251 mViewPager.getPageIndicator().setOnActivePageChangedListener(this); 252 mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY); 253 findViewById(R.id.tab_personal) 254 .setOnClickListener((View view) -> mViewPager.snapToPage(0)); 255 findViewById(R.id.tab_work) 256 .setOnClickListener((View view) -> mViewPager.snapToPage(1)); 257 mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view)); 258 setDeviceManagementResources(); 259 } else { 260 mViewPager = null; 261 } 262 263 mTabBar = mSearchScrollView.findViewById(R.id.tabs); 264 mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container); 265 mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar); 266 267 mSearchBar.initialize(new WidgetsSearchDataProvider() { 268 @Override 269 public List<WidgetsListBaseEntry> getWidgets() { 270 if (enableTieredWidgetsByDefaultInPicker()) { 271 // search all 272 return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets(); 273 } else { 274 // Can be removed when inlining enableTieredWidgetsByDefaultInPicker flag 275 return getWidgetsToDisplay(); 276 } 277 } 278 }, /* searchModeListener= */ this); 279 } 280 setDeviceManagementResources()281 private void setDeviceManagementResources() { 282 if (mActivityContext.getStringCache() != null) { 283 Button personalTab = findViewById(R.id.tab_personal); 284 personalTab.setText(mActivityContext.getStringCache().widgetsPersonalTab); 285 286 Button workTab = findViewById(R.id.tab_work); 287 workTab.setText(mActivityContext.getStringCache().widgetsWorkTab); 288 } 289 } 290 291 @Override onActivePageChanged(int currentActivePage)292 public void onActivePageChanged(int currentActivePage) { 293 AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage); 294 WidgetsRecyclerView currentRecyclerView = 295 mAdapters.get(currentActivePage).mWidgetsRecyclerView; 296 297 updateRecyclerViewVisibility(currentAdapterHolder); 298 attachScrollbarToRecyclerView(currentRecyclerView); 299 } 300 attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView)301 private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) { 302 if (mCurrentWidgetsRecyclerView != recyclerView) { 303 // Bind scrollbar if changing the recycler view. If widgets list updates, since 304 // scrollbar is already attached to the recycler view, it will automatically adjust as 305 // needed with recycler view's onScrollListener. 306 recyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER); 307 // Only reset the scroll position & expanded apps if the currently shown recycler view 308 // has been updated. 309 reset(); 310 resetExpandedHeaders(); 311 mCurrentWidgetsRecyclerView = recyclerView; 312 if (mStickyHeaderLayout != null) { 313 mStickyHeaderLayout.setCurrentRecyclerView(recyclerView); 314 } 315 } 316 } 317 updateRecyclerViewVisibility(AdapterHolder adapterHolder)318 protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) { 319 // The first item is always an empty space entry. Look for any more items. 320 boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries(); 321 322 if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) { 323 mNoWidgetsView.setText(R.string.no_search_results); 324 adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE); 325 } else if (adapterHolder.mAdapterType == AdapterHolder.WORK 326 && mUserCache.getUserProfiles().stream() 327 .filter(userHandle -> mUserCache.getUserInfo(userHandle).isWork()) 328 .anyMatch(mUserManagerState::isUserQuiet) 329 && mActivityContext.getStringCache() != null) { 330 mNoWidgetsView.setText(mActivityContext.getStringCache().workProfilePausedTitle); 331 } else { 332 mNoWidgetsView.setText(R.string.no_widgets_available); 333 } 334 mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE); 335 } 336 reset()337 private void reset() { 338 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop(); 339 if (mHasWorkProfile) { 340 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop(); 341 } 342 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop(); 343 if (mStickyHeaderLayout != null) { 344 mStickyHeaderLayout.reset(/* animate= */ true); 345 } 346 } 347 348 @VisibleForTesting getRecyclerView()349 public WidgetsRecyclerView getRecyclerView() { 350 if (mIsInSearchMode) { 351 return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; 352 } 353 if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) { 354 return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; 355 } 356 return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView; 357 } 358 359 @Override getAccessibilityTarget()360 protected Pair<View, String> getAccessibilityTarget() { 361 return Pair.create(getRecyclerView(), getContext().getString( 362 mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed)); 363 } 364 365 @Override onAttachedToWindow()366 protected void onAttachedToWindow() { 367 super.onAttachedToWindow(); 368 onWidgetsBound(); 369 } 370 371 @Override onDetachedFromWindow()372 protected void onDetachedFromWindow() { 373 super.onDetachedFromWindow(); 374 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView 375 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode); 376 if (mHasWorkProfile) { 377 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView 378 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode); 379 } 380 } 381 382 @Override setInsets(Rect insets)383 public void setInsets(Rect insets) { 384 super.setInsets(insets); 385 mBottomPadding = Math.max(insets.bottom, mNavBarScrimHeight); 386 setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, mBottomPadding); 387 setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, mBottomPadding); 388 if (mHasWorkProfile) { 389 setBottomPadding(mAdapters.get(AdapterHolder.WORK) 390 .mWidgetsRecyclerView, mBottomPadding); 391 } 392 ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = mBottomPadding; 393 394 if (mBottomPadding > 0) { 395 setupNavBarColor(); 396 } else { 397 clearNavBarColor(); 398 } 399 400 requestLayout(); 401 } 402 403 @Override onApplyWindowInsets(WindowInsets insets)404 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 405 WindowInsets w = super.onApplyWindowInsets(insets); 406 if (mInsets.bottom != mNavBarScrimHeight) { 407 setInsets(mInsets); 408 } 409 return w; 410 } 411 setBottomPadding(RecyclerView recyclerView, int bottomPadding)412 private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) { 413 recyclerView.setPadding( 414 recyclerView.getPaddingLeft(), 415 recyclerView.getPaddingTop(), 416 recyclerView.getPaddingRight(), 417 bottomPadding); 418 } 419 420 @Override onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)421 protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) { 422 setContentViewChildHorizontalMargin(mSearchScrollView, contentHorizontalMarginInPx); 423 if (mViewPager == null) { 424 setContentViewChildHorizontalPadding( 425 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, 426 contentHorizontalMarginInPx); 427 } else { 428 setContentViewChildHorizontalPadding( 429 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, 430 contentHorizontalMarginInPx); 431 setContentViewChildHorizontalPadding( 432 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, 433 contentHorizontalMarginInPx); 434 } 435 setContentViewChildHorizontalPadding( 436 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, 437 contentHorizontalMarginInPx); 438 } 439 setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx)440 private static void setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx) { 441 ViewGroup.MarginLayoutParams layoutParams = 442 (ViewGroup.MarginLayoutParams) view.getLayoutParams(); 443 layoutParams.setMarginStart(horizontalMarginInPx); 444 layoutParams.setMarginEnd(horizontalMarginInPx); 445 } 446 setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx)447 private static void setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx) { 448 view.setPadding(horizontalPaddingInPx, view.getPaddingTop(), horizontalPaddingInPx, 449 view.getPaddingBottom()); 450 } 451 452 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)453 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 454 int availableWidth = MeasureSpec.getSize(widthMeasureSpec); 455 updateMaxSpansPerRow(availableWidth); 456 doMeasure(widthMeasureSpec, heightMeasureSpec); 457 } 458 459 /** Returns {@code true} if the max spans have been updated. 460 * 461 * @param availableWidth Total width available within parent (includes insets). 462 */ updateMaxSpansPerRow(int availableWidth)463 private void updateMaxSpansPerRow(int availableWidth) { 464 @Px int maxHorizontalSpan = getAvailableWidthForSuggestions( 465 availableWidth - getInsetsWidth()); 466 if (mMaxSpanPerRow != maxHorizontalSpan) { 467 mMaxSpanPerRow = maxHorizontalSpan; 468 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( 469 maxHorizontalSpan); 470 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( 471 maxHorizontalSpan); 472 if (mHasWorkProfile) { 473 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( 474 maxHorizontalSpan); 475 } 476 post(this::onRecommendedWidgetsBound); 477 } 478 } 479 480 /** 481 * Returns the width available to display suggestions. 482 */ getAvailableWidthForSuggestions(int pickerAvailableWidth)483 protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) { 484 return pickerAvailableWidth - (2 * mContentHorizontalMargin); 485 } 486 487 @Override onLayout(boolean changed, int l, int t, int r, int b)488 protected void onLayout(boolean changed, int l, int t, int r, int b) { 489 int width = r - l; 490 int height = b - t; 491 492 // Content is laid out as center bottom aligned 493 int contentWidth = mContent.getMeasuredWidth(); 494 int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left; 495 mContent.layout(contentLeft, height - mContent.getMeasuredHeight(), 496 contentLeft + contentWidth, height); 497 498 setTranslationShift(mTranslationShift); 499 } 500 501 /** 502 * Returns all displayable widgets. 503 */ 504 // Used by the two pane sheet to show 3-dot menu to toggle between default lists and all lists 505 // when enableTieredWidgetsByDefaultInPicker is OFF. This code path and the 3-dot menu can be 506 // safely deleted when it's alternative "enableTieredWidgetsByDefaultInPicker" flag is inlined. getWidgetsToDisplay()507 protected List<WidgetsListBaseEntry> getWidgetsToDisplay() { 508 return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets(); 509 } 510 511 @Override onWidgetsBound()512 public void onWidgetsBound() { 513 if (mIsInSearchMode) { 514 return; 515 } 516 List<WidgetsListBaseEntry> widgets; 517 List<WidgetsListBaseEntry> defaultWidgets = emptyList(); 518 519 if (enableTieredWidgetsByDefaultInPicker()) { 520 WidgetPickerData dataProvider = 521 mActivityContext.getWidgetPickerDataProvider().get(); 522 widgets = dataProvider.getAllWidgets(); 523 defaultWidgets = dataProvider.getDefaultWidgets(); 524 } else { 525 // This code path can be deleted once enableTieredWidgetsByDefaultInPicker is inlined. 526 widgets = getWidgetsToDisplay(); 527 } 528 529 AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY); 530 primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets, defaultWidgets); 531 532 if (mHasWorkProfile) { 533 mViewPager.setVisibility(VISIBLE); 534 mTabBar.setVisibility(VISIBLE); 535 AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK); 536 workUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets, defaultWidgets); 537 onActivePageChanged(mViewPager.getCurrentPage()); 538 } else { 539 onActivePageChanged(0); 540 } 541 // Update recommended widgets section so that it occupies appropriate space on screen to 542 // leave enough space for presence/absence of mNoWidgetsView. 543 boolean isNoWidgetsViewNeeded = 544 !mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.hasVisibleEntries() 545 || (mHasWorkProfile && mAdapters.get(AdapterHolder.WORK) 546 .mWidgetsListAdapter.hasVisibleEntries()); 547 if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) { 548 mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded; 549 post(this::onRecommendedWidgetsBound); 550 } 551 } 552 553 @Override onWidgetsListExpandButtonClick(View v)554 public void onWidgetsListExpandButtonClick(View v) { 555 AdapterHolder currentAdapterHolder = mAdapters.get(getCurrentAdapterHolderType()); 556 currentAdapterHolder.mWidgetsListAdapter.useExpandedList(); 557 onWidgetsBound(); 558 currentAdapterHolder.mWidgetsRecyclerView.announceForAccessibility( 559 mActivityContext.getString(R.string.widgets_list_expanded)); 560 mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_EXPAND_PRESS); 561 } 562 563 @Override enterSearchMode(boolean shouldLog)564 public void enterSearchMode(boolean shouldLog) { 565 if (mIsInSearchMode) return; 566 setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true); 567 attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView); 568 if (shouldLog) { 569 mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED); 570 } 571 } 572 573 @Override exitSearchMode()574 public void exitSearchMode() { 575 if (!mIsInSearchMode) return; 576 onSearchResults(new ArrayList<>()); 577 // Remove all views when exiting the search mode; this prevents animating from stale results 578 // to new ones the next time we enter search mode. By the time recycler view is hidden, 579 // layout may not have happened to clear up existing results. So, instead of waiting for it 580 // to happen, we clear the views here. 581 mAdapters.get(AdapterHolder.SEARCH).reset(); 582 setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false); 583 if (mHasWorkProfile) { 584 mViewPager.snapToPage(AdapterHolder.PRIMARY); 585 } 586 attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView); 587 } 588 589 @Override onSearchResults(List<WidgetsListBaseEntry> entries)590 public void onSearchResults(List<WidgetsListBaseEntry> entries) { 591 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries); 592 updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH)); 593 } 594 setViewVisibilityBasedOnSearch(boolean isInSearchMode)595 protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { 596 mIsInSearchMode = isInSearchMode; 597 if (isInSearchMode) { 598 mWidgetRecommendationsContainer.setVisibility(GONE); 599 if (mHasWorkProfile) { 600 mViewPager.setVisibility(GONE); 601 mTabBar.setVisibility(GONE); 602 } else { 603 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE); 604 } 605 updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH)); 606 // Hide no search results view to prevent it from flashing on enter search. 607 mNoWidgetsView.setVisibility(GONE); 608 } else { 609 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE); 610 AdapterHolder currentAdapterHolder = mAdapters.get(getCurrentAdapterHolderType()); 611 // Remove all views when exiting the search mode; this prevents animating / flashing old 612 // list position / state. 613 currentAdapterHolder.reset(); 614 currentAdapterHolder.mWidgetsRecyclerView.setVisibility(VISIBLE); 615 post(this::onRecommendedWidgetsBound); 616 // Visibility of recycler views and headers are handled in methods below. 617 onWidgetsBound(); 618 } 619 } 620 resetExpandedHeaders()621 protected void resetExpandedHeaders() { 622 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader(); 623 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader(); 624 } 625 626 @Override onRecommendedWidgetsBound()627 public void onRecommendedWidgetsBound() { 628 if (mIsInSearchMode) { 629 return; 630 } 631 boolean forceUpdate = false; 632 // We avoid applying new recommendations when some are already displayed. 633 if (mRecommendedWidgetsMap.isEmpty()) { 634 mRecommendedWidgetsMap = 635 mActivityContext.getWidgetPickerDataProvider().get().getRecommendations(); 636 forceUpdate = true; 637 } 638 mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations( 639 mRecommendedWidgetsMap, 640 mDeviceProfile, 641 /* availableHeight= */ getMaxAvailableHeightForRecommendations(), 642 /* availableWidth= */ mMaxSpanPerRow, 643 /* cellPadding= */ mWidgetCellHorizontalPadding, 644 /* requestedPage= */ mRecommendationsCurrentPage, 645 /* forceUpdate= */ forceUpdate 646 ); 647 648 mWidgetRecommendationsContainer.setVisibility( 649 mRecommendedWidgetsCount > 0 ? VISIBLE : GONE); 650 } 651 652 @Px getMaxAvailableHeightForRecommendations()653 protected float getMaxAvailableHeightForRecommendations() { 654 // There isn't enough space to show recommendations in landscape orientation on phones with 655 // a full sheet design. Tablets use a two pane picker. 656 if (mDeviceProfile.isLandscape) { 657 return 0f; 658 } 659 660 return (mDeviceProfile.heightPx - mDeviceProfile.bottomSheetTopPadding) 661 * RECOMMENDATION_TABLE_HEIGHT_RATIO; 662 } 663 664 /** b/209579563: "Widgets" header should be focused first. */ 665 @Override getAccessibilityInitialFocusView()666 protected View getAccessibilityInitialFocusView() { 667 return mHeaderTitle; 668 } 669 open(boolean animate)670 private void open(boolean animate) { 671 if (animate) { 672 if (getPopupContainer().getInsets().bottom > 0) { 673 mContent.setAlpha(0); 674 } 675 setUpOpenAnimation(mActivityContext.getDeviceProfile().bottomSheetOpenDuration); 676 Animator animator = mOpenCloseAnimation.getAnimationPlayer(); 677 animator.setInterpolator(AnimationUtils.loadInterpolator( 678 getContext(), android.R.interpolator.linear_out_slow_in)); 679 post(() -> { 680 animator.setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration) 681 .start(); 682 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION); 683 }); 684 } else { 685 setTranslationShift(TRANSLATION_SHIFT_OPENED); 686 post(this::announceAccessibilityChanges); 687 } 688 } 689 690 @Override handleClose(boolean animate)691 protected void handleClose(boolean animate) { 692 handleClose(animate, mActivityContext.getDeviceProfile().bottomSheetCloseDuration); 693 } 694 695 @Override isOfType(int type)696 protected boolean isOfType(int type) { 697 return (type & TYPE_WIDGETS_FULL_SHEET) != 0; 698 } 699 700 @Override onControllerInterceptTouchEvent(MotionEvent ev)701 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 702 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 703 mNoIntercept = shouldScroll(ev); 704 } 705 706 // Clear focus only if user touched outside of search area and handling focus out ourselves 707 // was necessary (e.g. when it's not predictive back, but other user interaction). 708 if (mSearchBar.isSearchBarFocused() 709 && !getPopupContainer().isEventOverView(mSearchBarContainer, ev) 710 && mSearchClearFocusHelper.shouldClearFocus(ev, mTouchSlop)) { 711 mSearchBar.clearSearchBarFocus(); 712 } 713 714 return super.onControllerInterceptTouchEvent(ev); 715 } 716 shouldScroll(MotionEvent ev)717 protected boolean shouldScroll(MotionEvent ev) { 718 boolean intercept = false; 719 WidgetsRecyclerView recyclerView = getRecyclerView(); 720 RecyclerViewFastScroller scroller = recyclerView.getScrollbar(); 721 // Disable swipe down when recycler view is scrolling 722 if (scroller.getThumbOffsetY() >= 0 && getPopupContainer().isEventOverView(scroller, ev)) { 723 intercept = true; 724 } else if (getPopupContainer().isEventOverView(recyclerView, ev)) { 725 intercept = !recyclerView.shouldContainerScroll(ev, getPopupContainer()); 726 } 727 return intercept; 728 } 729 730 /** Shows the {@link WidgetsFullSheet} on the launcher. */ show(BaseActivity activity, boolean animate)731 public static WidgetsFullSheet show(BaseActivity activity, boolean animate) { 732 WidgetsFullSheet sheet = (WidgetsFullSheet) activity.getLayoutInflater().inflate( 733 getWidgetSheetId(activity), 734 activity.getDragLayer(), 735 false); 736 sheet.attachToContainer(); 737 sheet.mIsOpen = true; 738 sheet.open(animate); 739 return sheet; 740 } 741 742 /** 743 * Updates the widget picker's title and description in the header to the provided values (if 744 * present). 745 */ mayUpdateTitleAndDescription(@ullable String title, @Nullable String descriptionRes)746 public void mayUpdateTitleAndDescription(@Nullable String title, 747 @Nullable String descriptionRes) { 748 if (title != null) { 749 mHeaderTitle.setText(title); 750 } 751 // Full sheet doesn't support a description. 752 } 753 754 @Override saveHierarchyState(SparseArray<Parcelable> sparseArray)755 public void saveHierarchyState(SparseArray<Parcelable> sparseArray) { 756 Bundle bundle = new Bundle(); 757 // With widget picker open, when we open shade to switch theme, Launcher re-creates the 758 // picker and calls save/restore hierarchy state. We save the state of recommendations 759 // across those updates. 760 bundle.putInt(RECOMMENDATIONS_SAVED_STATE_KEY, mRecommendationsCurrentPage); 761 mWidgetRecommendationsView.saveState(bundle); 762 SparseArray<Parcelable> superState = new SparseArray<>(); 763 super.saveHierarchyState(superState); 764 bundle.putSparseParcelableArray(SUPER_SAVED_STATE_KEY, superState); 765 sparseArray.put(0, bundle); 766 } 767 768 @Override restoreHierarchyState(SparseArray<Parcelable> sparseArray)769 public void restoreHierarchyState(SparseArray<Parcelable> sparseArray) { 770 Bundle state = (Bundle) sparseArray.get(0); 771 mRecommendationsCurrentPage = state.getInt( 772 RECOMMENDATIONS_SAVED_STATE_KEY, /*defaultValue=*/0); 773 mWidgetRecommendationsView.restoreState(state); 774 super.restoreHierarchyState(state.getSparseParcelableArray(SUPER_SAVED_STATE_KEY)); 775 } 776 getWidgetSheetId(BaseActivity activity)777 private static int getWidgetSheetId(BaseActivity activity) { 778 boolean isTwoPane = activity.getDeviceProfile().isTablet; 779 780 return isTwoPane ? R.layout.widgets_two_pane_sheet : R.layout.widgets_full_sheet; 781 } 782 783 @Override onInterceptTouchEvent(MotionEvent ev)784 public boolean onInterceptTouchEvent(MotionEvent ev) { 785 return isTouchOnScrollbar(ev) || super.onInterceptTouchEvent(ev); 786 } 787 788 @Override onTouchEvent(MotionEvent ev)789 public boolean onTouchEvent(MotionEvent ev) { 790 return maybeHandleTouchEvent(ev) || super.onTouchEvent(ev); 791 } 792 maybeHandleTouchEvent(MotionEvent ev)793 private boolean maybeHandleTouchEvent(MotionEvent ev) { 794 boolean isEventHandled = false; 795 796 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 797 mCurrentTouchEventRecyclerView = isTouchOnScrollbar(ev) ? getRecyclerView() : null; 798 } 799 800 if (mCurrentTouchEventRecyclerView != null) { 801 final float offsetX = mContent.getX(); 802 final float offsetY = mContent.getY(); 803 ev.offsetLocation(-offsetX, -offsetY); 804 isEventHandled = mCurrentTouchEventRecyclerView.dispatchTouchEvent(ev); 805 ev.offsetLocation(offsetX, offsetY); 806 } 807 808 if (ev.getAction() == MotionEvent.ACTION_UP 809 || ev.getAction() == MotionEvent.ACTION_CANCEL) { 810 mCurrentTouchEventRecyclerView = null; 811 } 812 813 return isEventHandled; 814 } 815 isTouchOnScrollbar(MotionEvent ev)816 private boolean isTouchOnScrollbar(MotionEvent ev) { 817 final float offsetX = mContent.getX(); 818 final float offsetY = mContent.getY(); 819 WidgetsRecyclerView rv = getRecyclerView(); 820 821 ev.offsetLocation(-offsetX, -offsetY); 822 boolean isOnScrollBar = rv != null && rv.getScrollbar() != null && rv.isHitOnScrollBar(ev); 823 ev.offsetLocation(offsetX, offsetY); 824 825 return isOnScrollBar; 826 } 827 828 /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */ 829 @VisibleForTesting getWidgetsView(BaseActivity launcher)830 public static WidgetsRecyclerView getWidgetsView(BaseActivity launcher) { 831 return launcher.findViewById(R.id.primary_widgets_list_view); 832 } 833 834 @Override addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target)835 public void addHintCloseAnim( 836 float distanceToMove, Interpolator interpolator, PendingAnimation target) { 837 target.addAnimatedFloat(mSwipeToDismissProgress, 0f, 1f, interpolator); 838 } 839 840 @Override onCloseComplete()841 protected void onCloseComplete() { 842 super.onCloseComplete(); 843 AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL); 844 } 845 846 @Override getHeaderViewHeight()847 public int getHeaderViewHeight() { 848 return measureHeightWithVerticalMargins(mHeaderTitle) 849 + measureHeightWithVerticalMargins(mSearchBarContainer); 850 } 851 852 /** private the height, in pixel, + the vertical margins of a given view. */ measureHeightWithVerticalMargins(View view)853 protected static int measureHeightWithVerticalMargins(View view) { 854 if (view == null || view.getVisibility() != VISIBLE) { 855 return 0; 856 } 857 MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams(); 858 return view.getMeasuredHeight() + marginLayoutParams.bottomMargin 859 + marginLayoutParams.topMargin; 860 } 861 getCurrentAdapterHolderType()862 protected int getCurrentAdapterHolderType() { 863 if (mIsInSearchMode) { 864 return SEARCH; 865 } else if (mViewPager != null) { 866 return mViewPager.getCurrentPage(); 867 } else { 868 return AdapterHolder.PRIMARY; 869 } 870 } 871 restorePreviousAdapterHolderType(int previousAdapterHolderType)872 private void restorePreviousAdapterHolderType(int previousAdapterHolderType) { 873 if (previousAdapterHolderType == AdapterHolder.WORK && mViewPager != null) { 874 mViewPager.setCurrentPage(previousAdapterHolderType); 875 } else if (previousAdapterHolderType == AdapterHolder.SEARCH) { 876 enterSearchMode(false); 877 } 878 } 879 880 @Override onDeviceProfileChanged(DeviceProfile dp)881 public void onDeviceProfileChanged(DeviceProfile dp) { 882 super.onDeviceProfileChanged(dp); 883 884 if (shouldRecreateLayout(/*oldDp=*/ mDeviceProfile, /*newDp=*/ dp)) { 885 SparseArray<Parcelable> widgetsState = new SparseArray<>(); 886 saveHierarchyState(widgetsState); 887 handleClose(false); 888 WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false); 889 sheet.restoreRecommendations(mRecommendedWidgets, mRecommendedWidgetsMap); 890 sheet.restoreHierarchyState(widgetsState); 891 sheet.restoreAdapterStates(mAdapters); 892 sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType()); 893 } else if (!isTwoPane()) { 894 reset(); 895 resetExpandedHeaders(); 896 } 897 898 mDeviceProfile = dp; 899 } 900 restoreRecommendations(List<WidgetItem> recommendedWidgets, Map<WidgetRecommendationCategory, List<WidgetItem>> recommendedWidgetsMap)901 private void restoreRecommendations(List<WidgetItem> recommendedWidgets, 902 Map<WidgetRecommendationCategory, List<WidgetItem>> recommendedWidgetsMap) { 903 mRecommendedWidgets = recommendedWidgets; 904 mRecommendedWidgetsMap = recommendedWidgetsMap; 905 } 906 restoreAdapterStates(SparseArray<AdapterHolder> adapters)907 private void restoreAdapterStates(SparseArray<AdapterHolder> adapters) { 908 if (adapters.contains(AdapterHolder.WORK)) { 909 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.restoreState( 910 adapters.get(AdapterHolder.WORK).mWidgetsListAdapter); 911 } 912 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.restoreState( 913 adapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter); 914 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.restoreState( 915 adapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter); 916 } 917 918 /** 919 * Indicates if layout should be re-created on device profile change - so that a different 920 * layout can be displayed. 921 */ shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp)922 private static boolean shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp) { 923 // When folding/unfolding the foldables, we need to switch between the regular widget picker 924 // and the two pane picker, so we rebuild the picker with the correct layout. 925 return oldDp.isTwoPanels != newDp.isTwoPanels; 926 } 927 928 /** 929 * In widget search mode, we should scale down content inside widget bottom sheet, rather 930 * than the whole bottom sheet, to indicate we will navigate back within the widget 931 * bottom sheet. 932 */ 933 @Override shouldAnimateContentViewInBackSwipe()934 public boolean shouldAnimateContentViewInBackSwipe() { 935 return mIsInSearchMode; 936 } 937 938 @Override onBackInvoked()939 public void onBackInvoked() { 940 if (mIsInSearchMode) { 941 mSearchBar.reset(); 942 // Posting animation to next frame will let widget sheet finish updating UI first, and 943 // make animation smoother. 944 post(this::animateSwipeToDismissProgressToStart); 945 } else { 946 super.onBackInvoked(); 947 } 948 } 949 950 @Override onDragStart(boolean start, float startDisplacement)951 public void onDragStart(boolean start, float startDisplacement) { 952 super.onDragStart(start, startDisplacement); 953 WindowInsetsController insetsController = getWindowInsetsController(); 954 if (insetsController != null) { 955 insetsController.hide(WindowInsets.Type.ime()); 956 } 957 } 958 959 @Nullable getViewToShowEducationTip()960 private View getViewToShowEducationTip() { 961 if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) { 962 return mWidgetRecommendationsView.getViewForEducationTip(); 963 } 964 965 AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode 966 ? AdapterHolder.SEARCH 967 : mViewPager == null 968 ? AdapterHolder.PRIMARY 969 : mViewPager.getCurrentPage()); 970 WidgetsRowViewHolder viewHolderForTip = 971 (WidgetsRowViewHolder) IntStream.range( 972 0, adapterHolder.mWidgetsListAdapter.getItemCount()) 973 .mapToObj(adapterHolder.mWidgetsRecyclerView:: 974 findViewHolderForAdapterPosition) 975 .filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder) 976 .findFirst() 977 .orElse(null); 978 if (viewHolderForTip != null) { 979 return ((ViewGroup) viewHolderForTip.tableContainer.getChildAt(0)).getChildAt(0); 980 } 981 982 return null; 983 } 984 isTwoPane()985 protected boolean isTwoPane() { 986 return false; 987 } 988 989 /** Gets the sheet for widget picker, which is used for testing. */ 990 @VisibleForTesting getSheet()991 public View getSheet() { 992 return mContent; 993 } 994 995 /** Opens the first header in widget picker and scrolls to the top of the RecyclerView. */ 996 @VisibleForTesting openFirstHeader()997 public void openFirstHeader() { 998 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.selectFirstHeaderEntry(); 999 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop(); 1000 } 1001 1002 @Override getHeaderTopClip(@onNull WidgetCell cell)1003 protected int getHeaderTopClip(@NonNull WidgetCell cell) { 1004 StickyHeaderLayout header = findViewById(R.id.search_and_recommendations_container); 1005 if (header == null) { 1006 return 0; 1007 } 1008 Rect cellRect = new Rect(); 1009 boolean cellIsPartiallyVisible = cell.getGlobalVisibleRect(cellRect); 1010 if (cellIsPartiallyVisible) { 1011 Rect occludingRect = new Rect(); 1012 for (View headerChild : header.getStickyChildren()) { 1013 Rect childRect = new Rect(); 1014 boolean childVisible = headerChild.getGlobalVisibleRect(childRect); 1015 if (childVisible && childRect.intersect(cellRect)) { 1016 occludingRect.union(childRect); 1017 } 1018 } 1019 if (!occludingRect.isEmpty() && cellRect.top < occludingRect.bottom) { 1020 return occludingRect.bottom - cellRect.top; 1021 } 1022 } 1023 return 0; 1024 } 1025 1026 @Override scrollCellContainerByY(WidgetCell wc, int scrollByY)1027 protected void scrollCellContainerByY(WidgetCell wc, int scrollByY) { 1028 for (ViewParent parent = wc.getParent(); parent != null; parent = parent.getParent()) { 1029 if (parent instanceof WidgetsRecyclerView recyclerView) { 1030 // Scrollable container for main widget list. 1031 recyclerView.smoothScrollBy(0, scrollByY); 1032 return; 1033 } else if (parent instanceof StickyHeaderLayout header) { 1034 // Scrollable container for recommendations. We still scroll on the recycler (even 1035 // though the recommendations are not in the recycler view) because the 1036 // StickyHeaderLayout scroll is connected to the currently visible recycler view. 1037 WidgetsRecyclerView recyclerView = findVisibleRecyclerView(); 1038 if (recyclerView != null) { 1039 recyclerView.smoothScrollBy(0, scrollByY); 1040 } 1041 return; 1042 } else if (parent == this) { 1043 return; 1044 } 1045 } 1046 } 1047 1048 @Nullable findVisibleRecyclerView()1049 private WidgetsRecyclerView findVisibleRecyclerView() { 1050 if (mViewPager != null) { 1051 return (WidgetsRecyclerView) mViewPager.getPageAt(mViewPager.getCurrentPage()); 1052 } 1053 return findViewById(R.id.primary_widgets_list_view); 1054 } 1055 1056 /** A holder class for holding adapters & their corresponding recycler view. */ 1057 final class AdapterHolder { 1058 static final int PRIMARY = 0; 1059 static final int WORK = 1; 1060 static final int SEARCH = 2; 1061 1062 private final int mAdapterType; 1063 final WidgetsListAdapter mWidgetsListAdapter; 1064 private final DefaultItemAnimator mWidgetsListItemAnimator; 1065 1066 WidgetsRecyclerView mWidgetsRecyclerView; 1067 AdapterHolder(int adapterType)1068 AdapterHolder(int adapterType) { 1069 mAdapterType = adapterType; 1070 Context context = getContext(); 1071 1072 mWidgetsListAdapter = new WidgetsListAdapter( 1073 context, 1074 LayoutInflater.from(context), 1075 this::getEmptySpaceHeight, 1076 /* iconClickListener= */ WidgetsFullSheet.this, 1077 /* iconLongClickListener= */ WidgetsFullSheet.this, 1078 /* expandButtonClickListener= */ WidgetsFullSheet.this, 1079 isTwoPane()); 1080 mWidgetsListAdapter.setHasStableIds(true); 1081 switch (mAdapterType) { 1082 case PRIMARY: 1083 mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter); 1084 break; 1085 case WORK: 1086 mWidgetsListAdapter.setFilter(mWorkWidgetsFilter); 1087 break; 1088 default: 1089 break; 1090 } 1091 mWidgetsListItemAnimator = new WidgetsListItemAnimator(); 1092 } 1093 1094 /** 1095 * Swaps the adapter to existing adapter to prevent the recycler view from using stale view 1096 * to animate in the new visibility update. 1097 * 1098 * <p>For instance, when clearing search text and re-entering search with new list shouldn't 1099 * use stale results to animate in new results. Alternative is setting list animators to 1100 * null, but, we need animations with the default item animator. 1101 */ reset()1102 private void reset() { 1103 mWidgetsRecyclerView.swapAdapter( 1104 mWidgetsListAdapter, 1105 /*removeAndRecycleExistingViews=*/ true 1106 ); 1107 } 1108 getEmptySpaceHeight()1109 private int getEmptySpaceHeight() { 1110 return mStickyHeaderLayout != null ? mStickyHeaderLayout.getHeaderHeight() : 0; 1111 } 1112 setup(WidgetsRecyclerView recyclerView)1113 void setup(WidgetsRecyclerView recyclerView) { 1114 mWidgetsRecyclerView = recyclerView; 1115 mWidgetsRecyclerView.setOutlineProvider(mViewOutlineProvider); 1116 mWidgetsRecyclerView.setClipToOutline(true); 1117 mWidgetsRecyclerView.setClipChildren(false); 1118 mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter); 1119 mWidgetsRecyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER); 1120 mWidgetsRecyclerView.setItemAnimator(isTwoPane() ? null : mWidgetsListItemAnimator); 1121 mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this); 1122 if (!isTwoPane()) { 1123 mWidgetsRecyclerView.setEdgeEffectFactory( 1124 ((SpringRelativeLayout) mContent).createEdgeEffectFactory()); 1125 } 1126 // Recycler view binds to fast scroller when it is attached to screen. Make sure 1127 // search recycler view is bound to fast scroller if user is in search mode at the time 1128 // of attachment. 1129 if (mAdapterType == PRIMARY || mAdapterType == WORK) { 1130 mWidgetsRecyclerView.addOnAttachStateChangeListener(mBindScrollbarInSearchMode); 1131 } 1132 mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(mMaxSpanPerRow); 1133 } 1134 } 1135 1136 /** 1137 * Helper to identify if searchbar's focus can be cleared when user performs an action 1138 * outside search. 1139 */ 1140 private static class SearchClearFocusHelper { 1141 private float mFirstInteractionX = -1f; 1142 private float mFirstInteractionY = -1f; 1143 1144 /** 1145 * For a given [MotionEvent] indicates if we should clear focus from search (and hide IME). 1146 */ shouldClearFocus(MotionEvent ev, float touchSlop)1147 boolean shouldClearFocus(MotionEvent ev, float touchSlop) { 1148 int action = ev.getAction(); 1149 boolean clearFocus = false; 1150 1151 if (action == MotionEvent.ACTION_DOWN) { 1152 mFirstInteractionX = ev.getX(); 1153 mFirstInteractionY = ev.getY(); 1154 } else if (action == MotionEvent.ACTION_CANCEL) { 1155 // This is when user performed a gesture e.g. predictive back 1156 // We don't handle it ourselves and let IME handle the close. 1157 mFirstInteractionY = -1; 1158 mFirstInteractionX = -1; 1159 } else if (action == MotionEvent.ACTION_UP) { 1160 // Its clear that user action wasn't predictive back - but press / scroll etc. that 1161 // should hide the keyboard. 1162 clearFocus = true; 1163 mFirstInteractionY = -1; 1164 mFirstInteractionX = -1; 1165 } else if (action == MotionEvent.ACTION_MOVE) { 1166 // Sometimes, on move, we may not receive ACTION_UP, but if the move was within 1167 // touch slop and we didn't know if its moved or cancelled, we can clear focus. 1168 // Example case: Apps list is small and you do a little scroll on list - in such, we 1169 // want to still hide the keyboard. 1170 if (mFirstInteractionX != -1 && mFirstInteractionY != -1) { 1171 float distY = abs(mFirstInteractionY - ev.getY()); 1172 float distX = abs(mFirstInteractionX - ev.getX()); 1173 if (distY >= touchSlop || distX >= touchSlop) { 1174 clearFocus = true; 1175 mFirstInteractionY = -1; 1176 mFirstInteractionX = -1; 1177 } 1178 } 1179 } 1180 1181 return clearFocus; 1182 } 1183 } 1184 } 1185