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