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 android.view.View.MeasureSpec.makeMeasureSpec; 19 20 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED; 22 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.PropertyValuesHolder; 27 import android.content.Context; 28 import android.content.pm.LauncherApps; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.graphics.Rect; 32 import android.os.Process; 33 import android.os.UserHandle; 34 import android.util.AttributeSet; 35 import android.util.Pair; 36 import android.util.SparseArray; 37 import android.view.LayoutInflater; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.WindowInsets; 42 import android.view.animation.AnimationUtils; 43 import android.view.animation.Interpolator; 44 import android.widget.TextView; 45 46 import androidx.annotation.Nullable; 47 import androidx.annotation.VisibleForTesting; 48 import androidx.recyclerview.widget.DefaultItemAnimator; 49 import androidx.recyclerview.widget.RecyclerView; 50 51 import com.android.launcher3.Launcher; 52 import com.android.launcher3.LauncherAppState; 53 import com.android.launcher3.R; 54 import com.android.launcher3.Utilities; 55 import com.android.launcher3.anim.PendingAnimation; 56 import com.android.launcher3.compat.AccessibilityManagerCompat; 57 import com.android.launcher3.model.WidgetItem; 58 import com.android.launcher3.views.ArrowTipView; 59 import com.android.launcher3.views.RecyclerViewFastScroller; 60 import com.android.launcher3.views.SpringRelativeLayout; 61 import com.android.launcher3.views.WidgetsEduView; 62 import com.android.launcher3.widget.BaseWidgetSheet; 63 import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener; 64 import com.android.launcher3.widget.model.WidgetsListBaseEntry; 65 import com.android.launcher3.widget.picker.search.SearchModeListener; 66 import com.android.launcher3.widget.util.WidgetsTableUtils; 67 import com.android.launcher3.workprofile.PersonalWorkPagedView; 68 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; 69 70 import java.util.ArrayList; 71 import java.util.List; 72 import java.util.function.Predicate; 73 import java.util.stream.IntStream; 74 75 /** 76 * Popup for showing the full list of available widgets 77 */ 78 public class WidgetsFullSheet extends BaseWidgetSheet 79 implements ProviderChangedListener, OnActivePageChangedListener, 80 WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener { 81 82 private static final long DEFAULT_OPEN_DURATION = 267; 83 private static final long FADE_IN_DURATION = 150; 84 private static final long EDUCATION_TIP_DELAY_MS = 200; 85 private static final long EDUCATION_DIALOG_DELAY_MS = 500; 86 private static final float VERTICAL_START_POSITION = 0.3f; 87 // The widget recommendation table can easily take over the entire screen on devices with small 88 // resolution or landscape on phone. This ratio defines the max percentage of content area that 89 // the table can display. 90 private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f; 91 92 private static final String KEY_WIDGETS_EDUCATION_DIALOG_SEEN = 93 "launcher.widgets_education_dialog_seen"; 94 95 private final Rect mInsets = new Rect(); 96 private final boolean mHasWorkProfile; 97 private final SparseArray<AdapterHolder> mAdapters = new SparseArray(); 98 private final UserHandle mCurrentUser = Process.myUserHandle(); 99 private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter = 100 entry -> mCurrentUser.equals(entry.mPkgItem.user); 101 private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter = 102 mPrimaryWidgetsFilter.negate(); 103 @Nullable private ArrowTipView mLatestEducationalTip; 104 private final OnLayoutChangeListener mLayoutChangeListenerToShowTips = 105 new OnLayoutChangeListener() { 106 @Override 107 public void onLayoutChange(View v, int left, int top, int right, int bottom, 108 int oldLeft, int oldTop, int oldRight, int oldBottom) { 109 if (hasSeenEducationTip()) { 110 removeOnLayoutChangeListener(this); 111 return; 112 } 113 114 // Widgets are loaded asynchronously, We are adding a delay because we only want 115 // to show the tip when the widget preview has finished loading and rendering in 116 // this view. 117 removeCallbacks(mShowEducationTipTask); 118 postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS); 119 } 120 }; 121 122 private final Runnable mShowEducationTipTask = () -> { 123 if (hasSeenEducationTip()) { 124 removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 125 return; 126 } 127 mLatestEducationalTip = showEducationTipOnViewIfPossible(getViewToShowEducationTip()); 128 if (mLatestEducationalTip != null) { 129 removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 130 } 131 }; 132 133 private final OnAttachStateChangeListener mBindScrollbarInSearchMode = 134 new OnAttachStateChangeListener() { 135 @Override 136 public void onViewAttachedToWindow(View view) { 137 WidgetsRecyclerView searchRecyclerView = 138 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; 139 if (mIsInSearchMode && searchRecyclerView != null) { 140 searchRecyclerView.bindFastScrollbar(); 141 } 142 } 143 144 @Override 145 public void onViewDetachedFromWindow(View view) { 146 } 147 }; 148 149 private final int mTabsHeight; 150 private final int mWidgetSheetContentHorizontalPadding; 151 152 @Nullable private WidgetsRecyclerView mCurrentWidgetsRecyclerView; 153 @Nullable private PersonalWorkPagedView mViewPager; 154 private boolean mIsInSearchMode; 155 private boolean mIsNoWidgetsViewNeeded; 156 private int mMaxSpansPerRow = DEFAULT_MAX_HORIZONTAL_SPANS; 157 private TextView mNoWidgetsView; 158 private SearchAndRecommendationsScrollController mSearchScrollController; 159 WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr)160 public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) { 161 super(context, attrs, defStyleAttr); 162 mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1; 163 mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY)); 164 mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); 165 mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); 166 167 Resources resources = getResources(); 168 mTabsHeight = mHasWorkProfile 169 ? resources.getDimensionPixelSize(R.dimen.all_apps_header_pill_height) 170 : 0; 171 mWidgetSheetContentHorizontalPadding = 2 * resources.getDimensionPixelSize( 172 R.dimen.widget_cell_horizontal_padding); 173 } 174 WidgetsFullSheet(Context context, AttributeSet attrs)175 public WidgetsFullSheet(Context context, AttributeSet attrs) { 176 this(context, attrs, 0); 177 } 178 179 @Override onFinishInflate()180 protected void onFinishInflate() { 181 super.onFinishInflate(); 182 mContent = findViewById(R.id.container); 183 184 LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 185 int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view 186 : R.layout.widgets_full_sheet_recyclerview; 187 layoutInflater.inflate(contentLayoutRes, mContent, true); 188 189 RecyclerViewFastScroller fastScroller = findViewById(R.id.fast_scroller); 190 mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view)); 191 mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view)); 192 if (mHasWorkProfile) { 193 mViewPager = findViewById(R.id.widgets_view_pager); 194 mViewPager.initParentViews(this); 195 mViewPager.getPageIndicator().setOnActivePageChangedListener(this); 196 mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY); 197 findViewById(R.id.tab_personal) 198 .setOnClickListener((View view) -> mViewPager.snapToPage(0)); 199 findViewById(R.id.tab_work) 200 .setOnClickListener((View view) -> mViewPager.snapToPage(1)); 201 mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view)); 202 } else { 203 mViewPager = null; 204 } 205 206 mNoWidgetsView = findViewById(R.id.no_widgets_text); 207 mSearchScrollController = new SearchAndRecommendationsScrollController( 208 findViewById(R.id.search_and_recommendations_container)); 209 mSearchScrollController.setCurrentRecyclerView( 210 findViewById(R.id.primary_widgets_list_view)); 211 mSearchScrollController.mRecommendedWidgetsTable.setWidgetCellLongClickListener(this); 212 mSearchScrollController.mRecommendedWidgetsTable.setWidgetCellOnClickListener(this); 213 214 onRecommendedWidgetsBound(); 215 onWidgetsBound(); 216 217 mSearchScrollController.mSearchBar.initialize( 218 mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this); 219 220 setUpEducationViewsIfNeeded(); 221 } 222 223 @Override onActivePageChanged(int currentActivePage)224 public void onActivePageChanged(int currentActivePage) { 225 AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage); 226 WidgetsRecyclerView currentRecyclerView = 227 mAdapters.get(currentActivePage).mWidgetsRecyclerView; 228 229 updateRecyclerViewVisibility(currentAdapterHolder); 230 attachScrollbarToRecyclerView(currentRecyclerView); 231 } 232 attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView)233 private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) { 234 recyclerView.bindFastScrollbar(); 235 if (mCurrentWidgetsRecyclerView != recyclerView) { 236 // Only reset the scroll position & expanded apps if the currently shown recycler view 237 // has been updated. 238 reset(); 239 resetExpandedHeaders(); 240 mCurrentWidgetsRecyclerView = recyclerView; 241 mSearchScrollController.setCurrentRecyclerView(recyclerView); 242 } 243 } 244 updateRecyclerViewVisibility(AdapterHolder adapterHolder)245 private void updateRecyclerViewVisibility(AdapterHolder adapterHolder) { 246 // The first item is always an empty space entry. Look for any more items. 247 boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries(); 248 adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE); 249 250 mNoWidgetsView.setText( 251 adapterHolder.mAdapterType == AdapterHolder.SEARCH 252 ? R.string.no_search_results 253 : R.string.no_widgets_available); 254 mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE); 255 } 256 reset()257 private void reset() { 258 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop(); 259 if (mHasWorkProfile) { 260 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop(); 261 } 262 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop(); 263 mSearchScrollController.reset(/* animate= */ true); 264 } 265 266 @VisibleForTesting getRecyclerView()267 public WidgetsRecyclerView getRecyclerView() { 268 if (mIsInSearchMode) { 269 return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; 270 } 271 if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) { 272 return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; 273 } 274 return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView; 275 } 276 277 @Override getAccessibilityTarget()278 protected Pair<View, String> getAccessibilityTarget() { 279 return Pair.create(getRecyclerView(), getContext().getString( 280 mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed)); 281 } 282 283 @Override onAttachedToWindow()284 protected void onAttachedToWindow() { 285 super.onAttachedToWindow(); 286 mActivityContext.getAppWidgetHost().addProviderChangeListener(this); 287 notifyWidgetProvidersChanged(); 288 onRecommendedWidgetsBound(); 289 } 290 291 @Override onDetachedFromWindow()292 protected void onDetachedFromWindow() { 293 super.onDetachedFromWindow(); 294 mActivityContext.getAppWidgetHost().removeProviderChangeListener(this); 295 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView 296 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode); 297 if (mHasWorkProfile) { 298 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView 299 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode); 300 } 301 } 302 303 @Override setInsets(Rect insets)304 public void setInsets(Rect insets) { 305 super.setInsets(insets); 306 307 setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, insets.bottom); 308 setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, insets.bottom); 309 if (mHasWorkProfile) { 310 setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, insets.bottom); 311 } 312 ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = insets.bottom; 313 314 if (insets.bottom > 0) { 315 setupNavBarColor(); 316 } else { 317 clearNavBarColor(); 318 } 319 320 requestLayout(); 321 } 322 setBottomPadding(RecyclerView recyclerView, int bottomPadding)323 private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) { 324 recyclerView.setPadding( 325 recyclerView.getPaddingLeft(), 326 recyclerView.getPaddingTop(), 327 recyclerView.getPaddingRight(), 328 bottomPadding); 329 } 330 331 @Override onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)332 protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) { 333 setContentViewChildHorizontalMargin(mSearchScrollController.mContainer, 334 contentHorizontalMarginInPx); 335 if (mViewPager == null) { 336 setContentViewChildHorizontalPadding( 337 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, 338 contentHorizontalMarginInPx); 339 } else { 340 setContentViewChildHorizontalPadding( 341 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, 342 contentHorizontalMarginInPx); 343 setContentViewChildHorizontalPadding( 344 mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, 345 contentHorizontalMarginInPx); 346 } 347 setContentViewChildHorizontalPadding( 348 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, 349 contentHorizontalMarginInPx); 350 } 351 setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx)352 private static void setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx) { 353 ViewGroup.MarginLayoutParams layoutParams = 354 (ViewGroup.MarginLayoutParams) view.getLayoutParams(); 355 layoutParams.setMarginStart(horizontalMarginInPx); 356 layoutParams.setMarginEnd(horizontalMarginInPx); 357 } 358 setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx)359 private static void setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx) { 360 view.setPadding(horizontalPaddingInPx, view.getPaddingTop(), horizontalPaddingInPx, 361 view.getPaddingBottom()); 362 } 363 364 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)365 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 366 doMeasure(widthMeasureSpec, heightMeasureSpec); 367 368 if (mSearchScrollController.updateHeaderHeight()) { 369 doMeasure(widthMeasureSpec, heightMeasureSpec); 370 } 371 372 if (updateMaxSpansPerRow()) { 373 doMeasure(widthMeasureSpec, heightMeasureSpec); 374 375 if (mSearchScrollController.updateHeaderHeight()) { 376 doMeasure(widthMeasureSpec, heightMeasureSpec); 377 } 378 } 379 } 380 381 /** Returns {@code true} if the max spans have been updated. */ updateMaxSpansPerRow()382 private boolean updateMaxSpansPerRow() { 383 if (getMeasuredWidth() == 0) return false; 384 385 View content = mHasWorkProfile 386 ? mViewPager 387 : mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; 388 int maxHorizontalSpans = computeMaxHorizontalSpans(content, 389 mWidgetSheetContentHorizontalPadding); 390 if (mMaxSpansPerRow != maxHorizontalSpans) { 391 mMaxSpansPerRow = maxHorizontalSpans; 392 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( 393 mMaxSpansPerRow); 394 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( 395 mMaxSpansPerRow); 396 if (mHasWorkProfile) { 397 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( 398 mMaxSpansPerRow); 399 } 400 onRecommendedWidgetsBound(); 401 return true; 402 } 403 return false; 404 } 405 406 @Override onLayout(boolean changed, int l, int t, int r, int b)407 protected void onLayout(boolean changed, int l, int t, int r, int b) { 408 int width = r - l; 409 int height = b - t; 410 411 // Content is laid out as center bottom aligned 412 int contentWidth = mContent.getMeasuredWidth(); 413 int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left; 414 mContent.layout(contentLeft, height - mContent.getMeasuredHeight(), 415 contentLeft + contentWidth, height); 416 417 setTranslationShift(mTranslationShift); 418 } 419 420 @Override notifyWidgetProvidersChanged()421 public void notifyWidgetProvidersChanged() { 422 mActivityContext.refreshAndBindWidgetsForPackageUser(null); 423 } 424 425 @Override onWidgetsBound()426 public void onWidgetsBound() { 427 if (mIsInSearchMode) { 428 return; 429 } 430 List<WidgetsListBaseEntry> allWidgets = 431 mActivityContext.getPopupDataProvider().getAllWidgets(); 432 433 AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY); 434 primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); 435 436 if (mHasWorkProfile) { 437 mViewPager.setVisibility(VISIBLE); 438 mSearchScrollController.mTabBar.setVisibility(VISIBLE); 439 AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK); 440 workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); 441 onActivePageChanged(mViewPager.getCurrentPage()); 442 } else { 443 updateRecyclerViewVisibility(primaryUserAdapterHolder); 444 } 445 // Update recommended widgets section so that it occupies appropriate space on screen to 446 // leave enough space for presence/absence of mNoWidgetsView. 447 boolean isNoWidgetsViewNeeded = 448 !mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.hasVisibleEntries() 449 || (mHasWorkProfile && mAdapters.get(AdapterHolder.WORK) 450 .mWidgetsListAdapter.hasVisibleEntries()); 451 if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) { 452 mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded; 453 onRecommendedWidgetsBound(); 454 } 455 } 456 457 @Override enterSearchMode()458 public void enterSearchMode() { 459 if (mIsInSearchMode) return; 460 setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true); 461 attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView); 462 mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED); 463 } 464 465 @Override exitSearchMode()466 public void exitSearchMode() { 467 if (!mIsInSearchMode) return; 468 onSearchResults(new ArrayList<>()); 469 setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false); 470 if (mHasWorkProfile) { 471 mViewPager.snapToPage(AdapterHolder.PRIMARY); 472 } 473 attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView); 474 } 475 476 @Override onSearchResults(List<WidgetsListBaseEntry> entries)477 public void onSearchResults(List<WidgetsListBaseEntry> entries) { 478 mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries); 479 updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH)); 480 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop(); 481 } 482 setViewVisibilityBasedOnSearch(boolean isInSearchMode)483 private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { 484 mIsInSearchMode = isInSearchMode; 485 if (isInSearchMode) { 486 mSearchScrollController.mRecommendedWidgetsTable.setVisibility(GONE); 487 if (mHasWorkProfile) { 488 mViewPager.setVisibility(GONE); 489 mSearchScrollController.mTabBar.setVisibility(GONE); 490 } else { 491 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE); 492 } 493 updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH)); 494 // Hide no search results view to prevent it from flashing on enter search. 495 mNoWidgetsView.setVisibility(GONE); 496 } else { 497 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE); 498 // Visibility of recommended widgets, recycler views and headers are handled in methods 499 // below. 500 onRecommendedWidgetsBound(); 501 onWidgetsBound(); 502 } 503 } 504 resetExpandedHeaders()505 private void resetExpandedHeaders() { 506 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader(); 507 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader(); 508 } 509 510 @Override onRecommendedWidgetsBound()511 public void onRecommendedWidgetsBound() { 512 if (mIsInSearchMode) { 513 return; 514 } 515 List<WidgetItem> recommendedWidgets = 516 mActivityContext.getPopupDataProvider().getRecommendedWidgets(); 517 WidgetsRecommendationTableLayout table = mSearchScrollController.mRecommendedWidgetsTable; 518 if (recommendedWidgets.size() > 0) { 519 float noWidgetsViewHeight = 0; 520 if (mIsNoWidgetsViewNeeded) { 521 // Make sure recommended section leaves enough space for noWidgetsView. 522 Rect noWidgetsViewTextBounds = new Rect(); 523 mNoWidgetsView.getPaint() 524 .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0, 525 mNoWidgetsView.getText().length(), noWidgetsViewTextBounds); 526 noWidgetsViewHeight = noWidgetsViewTextBounds.height(); 527 } 528 doMeasure( 529 makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx, 530 MeasureSpec.EXACTLY), 531 makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx, 532 MeasureSpec.EXACTLY)); 533 float maxTableHeight = (mContent.getMeasuredHeight() 534 - mTabsHeight - getHeaderViewHeight() 535 - noWidgetsViewHeight) * RECOMMENDATION_TABLE_HEIGHT_RATIO; 536 537 List<ArrayList<WidgetItem>> recommendedWidgetsInTable = 538 WidgetsTableUtils.groupWidgetItemsIntoTableWithoutReordering( 539 recommendedWidgets, mMaxSpansPerRow); 540 table.setRecommendedWidgets(recommendedWidgetsInTable, maxTableHeight); 541 } else { 542 table.setVisibility(GONE); 543 } 544 } 545 open(boolean animate)546 private void open(boolean animate) { 547 if (animate) { 548 if (getPopupContainer().getInsets().bottom > 0) { 549 mContent.setAlpha(0); 550 setTranslationShift(VERTICAL_START_POSITION); 551 } 552 mOpenCloseAnimator.setValues( 553 PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); 554 mOpenCloseAnimator 555 .setDuration(DEFAULT_OPEN_DURATION) 556 .setInterpolator(AnimationUtils.loadInterpolator( 557 getContext(), android.R.interpolator.linear_out_slow_in)); 558 mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { 559 @Override 560 public void onAnimationEnd(Animator animation) { 561 mOpenCloseAnimator.removeListener(this); 562 } 563 }); 564 post(() -> { 565 mOpenCloseAnimator.start(); 566 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION); 567 }); 568 } else { 569 setTranslationShift(TRANSLATION_SHIFT_OPENED); 570 post(this::announceAccessibilityChanges); 571 } 572 } 573 574 @Override handleClose(boolean animate)575 protected void handleClose(boolean animate) { 576 handleClose(animate, DEFAULT_OPEN_DURATION); 577 } 578 579 @Override isOfType(int type)580 protected boolean isOfType(int type) { 581 return (type & TYPE_WIDGETS_FULL_SHEET) != 0; 582 } 583 584 @Override onControllerInterceptTouchEvent(MotionEvent ev)585 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 586 // Disable swipe down when recycler view is scrolling 587 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 588 mNoIntercept = false; 589 RecyclerViewFastScroller scroller = getRecyclerView().getScrollbar(); 590 if (scroller.getThumbOffsetY() >= 0 591 && getPopupContainer().isEventOverView(scroller, ev)) { 592 mNoIntercept = true; 593 } else if (getPopupContainer().isEventOverView(mContent, ev)) { 594 mNoIntercept = !getRecyclerView().shouldContainerScroll(ev, getPopupContainer()); 595 } 596 597 if (mSearchScrollController.mSearchBar.isSearchBarFocused() 598 && !getPopupContainer().isEventOverView( 599 mSearchScrollController.mSearchBarContainer, ev)) { 600 mSearchScrollController.mSearchBar.clearSearchBarFocus(); 601 } 602 } 603 return super.onControllerInterceptTouchEvent(ev); 604 } 605 606 /** Shows the {@link WidgetsFullSheet} on the launcher. */ show(Launcher launcher, boolean animate)607 public static WidgetsFullSheet show(Launcher launcher, boolean animate) { 608 WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater() 609 .inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false); 610 sheet.attachToContainer(); 611 sheet.mIsOpen = true; 612 sheet.open(animate); 613 return sheet; 614 } 615 616 /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */ 617 @VisibleForTesting getWidgetsView(Launcher launcher)618 public static WidgetsRecyclerView getWidgetsView(Launcher launcher) { 619 return launcher.findViewById(R.id.primary_widgets_list_view); 620 } 621 622 @Override addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target)623 public void addHintCloseAnim( 624 float distanceToMove, Interpolator interpolator, PendingAnimation target) { 625 target.setFloat(getRecyclerView(), VIEW_TRANSLATE_Y, -distanceToMove, interpolator); 626 target.setViewAlpha(getRecyclerView(), 0.5f, interpolator); 627 } 628 629 @Override onCloseComplete()630 protected void onCloseComplete() { 631 super.onCloseComplete(); 632 removeCallbacks(mShowEducationTipTask); 633 if (mLatestEducationalTip != null) { 634 mLatestEducationalTip.close(false); 635 } 636 AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL); 637 } 638 639 @Override getHeaderViewHeight()640 public int getHeaderViewHeight() { 641 return measureHeightWithVerticalMargins(mSearchScrollController.mHeaderTitle) 642 + measureHeightWithVerticalMargins(mSearchScrollController.mSearchBarContainer); 643 } 644 645 /** private the height, in pixel, + the vertical margins of a given view. */ measureHeightWithVerticalMargins(View view)646 private static int measureHeightWithVerticalMargins(View view) { 647 if (view.getVisibility() != VISIBLE) { 648 return 0; 649 } 650 MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams(); 651 return view.getMeasuredHeight() + marginLayoutParams.bottomMargin 652 + marginLayoutParams.topMargin; 653 } 654 655 @Override onConfigurationChanged(Configuration newConfig)656 protected void onConfigurationChanged(Configuration newConfig) { 657 super.onConfigurationChanged(newConfig); 658 if (mIsInSearchMode) { 659 mSearchScrollController.mSearchBar.reset(); 660 } 661 } 662 663 @Override onBackPressed()664 public boolean onBackPressed() { 665 if (mIsInSearchMode) { 666 mSearchScrollController.mSearchBar.reset(); 667 return true; 668 } 669 return super.onBackPressed(); 670 } 671 672 @Override onDragStart(boolean start, float startDisplacement)673 public void onDragStart(boolean start, float startDisplacement) { 674 super.onDragStart(start, startDisplacement); 675 getWindowInsetsController().hide(WindowInsets.Type.ime()); 676 } 677 getViewToShowEducationTip()678 @Nullable private View getViewToShowEducationTip() { 679 if (mSearchScrollController.mRecommendedWidgetsTable.getVisibility() == VISIBLE 680 && mSearchScrollController.mRecommendedWidgetsTable.getChildCount() > 0) { 681 return ((ViewGroup) mSearchScrollController.mRecommendedWidgetsTable.getChildAt(0)) 682 .getChildAt(0); 683 } 684 685 AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode 686 ? AdapterHolder.SEARCH 687 : mViewPager == null 688 ? AdapterHolder.PRIMARY 689 : mViewPager.getCurrentPage()); 690 WidgetsRowViewHolder viewHolderForTip = 691 (WidgetsRowViewHolder) IntStream.range( 692 0, adapterHolder.mWidgetsListAdapter.getItemCount()) 693 .mapToObj(adapterHolder.mWidgetsRecyclerView:: 694 findViewHolderForAdapterPosition) 695 .filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder) 696 .findFirst() 697 .orElse(null); 698 if (viewHolderForTip != null) { 699 return ((ViewGroup) viewHolderForTip.tableContainer.getChildAt(0)).getChildAt(0); 700 } 701 702 return null; 703 } 704 705 /** Shows education dialog for widgets. */ showEducationDialog()706 private WidgetsEduView showEducationDialog() { 707 mActivityContext.getSharedPrefs().edit() 708 .putBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, true).apply(); 709 return WidgetsEduView.showEducationDialog(mActivityContext); 710 } 711 712 /** Returns {@code true} if education dialog has previously been shown. */ hasSeenEducationDialog()713 protected boolean hasSeenEducationDialog() { 714 return mActivityContext.getSharedPrefs() 715 .getBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, false) 716 || Utilities.IS_RUNNING_IN_TEST_HARNESS; 717 } 718 setUpEducationViewsIfNeeded()719 private void setUpEducationViewsIfNeeded() { 720 if (!hasSeenEducationDialog()) { 721 postDelayed(() -> { 722 WidgetsEduView eduDialog = showEducationDialog(); 723 eduDialog.addOnCloseListener(() -> { 724 if (!hasSeenEducationTip()) { 725 addOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 726 // Call #requestLayout() to trigger layout change listener in order to show 727 // arrow tip immediately if there is a widget to show it on. 728 requestLayout(); 729 } 730 }); 731 }, EDUCATION_DIALOG_DELAY_MS); 732 } else if (!hasSeenEducationTip()) { 733 addOnLayoutChangeListener(mLayoutChangeListenerToShowTips); 734 } 735 } 736 737 /** A holder class for holding adapters & their corresponding recycler view. */ 738 private final class AdapterHolder { 739 static final int PRIMARY = 0; 740 static final int WORK = 1; 741 static final int SEARCH = 2; 742 743 private final int mAdapterType; 744 private final WidgetsListAdapter mWidgetsListAdapter; 745 private final DefaultItemAnimator mWidgetsListItemAnimator; 746 747 private WidgetsRecyclerView mWidgetsRecyclerView; 748 AdapterHolder(int adapterType)749 AdapterHolder(int adapterType) { 750 mAdapterType = adapterType; 751 752 Context context = getContext(); 753 LauncherAppState apps = LauncherAppState.getInstance(context); 754 mWidgetsListAdapter = new WidgetsListAdapter( 755 context, 756 LayoutInflater.from(context), 757 apps.getIconCache(), 758 this::getEmptySpaceHeight, 759 /* iconClickListener= */ WidgetsFullSheet.this, 760 /* iconLongClickListener= */ WidgetsFullSheet.this); 761 mWidgetsListAdapter.setHasStableIds(true); 762 switch (mAdapterType) { 763 case PRIMARY: 764 mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter); 765 break; 766 case WORK: 767 mWidgetsListAdapter.setFilter(mWorkWidgetsFilter); 768 break; 769 default: 770 break; 771 } 772 mWidgetsListItemAnimator = new DefaultItemAnimator(); 773 // Disable change animations because it disrupts the item focus upon adapter item 774 // change. 775 mWidgetsListItemAnimator.setSupportsChangeAnimations(false); 776 } 777 getEmptySpaceHeight()778 private int getEmptySpaceHeight() { 779 return mSearchScrollController.getHeaderHeight(); 780 } 781 setup(WidgetsRecyclerView recyclerView)782 void setup(WidgetsRecyclerView recyclerView) { 783 mWidgetsRecyclerView = recyclerView; 784 mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter); 785 mWidgetsRecyclerView.setItemAnimator(mWidgetsListItemAnimator); 786 mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this); 787 mWidgetsRecyclerView.setEdgeEffectFactory( 788 ((SpringRelativeLayout) mContent).createEdgeEffectFactory()); 789 // Recycler view binds to fast scroller when it is attached to screen. Make sure 790 // search recycler view is bound to fast scroller if user is in search mode at the time 791 // of attachment. 792 if (mAdapterType == PRIMARY || mAdapterType == WORK) { 793 mWidgetsRecyclerView.addOnAttachStateChangeListener(mBindScrollbarInSearchMode); 794 } 795 mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow); 796 } 797 } 798 } 799