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