1 /* 2 * Copyright (C) 2022 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.allapps; 17 18 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; 19 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD; 20 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD; 21 import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY; 22 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION; 23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB; 25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB; 26 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 27 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; 28 29 import android.animation.Animator; 30 import android.animation.AnimatorListenerAdapter; 31 import android.animation.ValueAnimator; 32 import android.content.Context; 33 import android.graphics.Canvas; 34 import android.graphics.Color; 35 import android.graphics.Outline; 36 import android.graphics.Paint; 37 import android.graphics.Path; 38 import android.graphics.Path.Direction; 39 import android.graphics.Point; 40 import android.graphics.Rect; 41 import android.graphics.RectF; 42 import android.os.Bundle; 43 import android.os.Parcelable; 44 import android.os.Process; 45 import android.os.UserManager; 46 import android.util.AttributeSet; 47 import android.util.FloatProperty; 48 import android.util.Log; 49 import android.util.SparseArray; 50 import android.view.KeyEvent; 51 import android.view.LayoutInflater; 52 import android.view.MotionEvent; 53 import android.view.View; 54 import android.view.ViewGroup; 55 import android.view.ViewOutlineProvider; 56 import android.view.WindowInsets; 57 import android.widget.Button; 58 import android.widget.RelativeLayout; 59 60 import androidx.annotation.NonNull; 61 import androidx.annotation.Nullable; 62 import androidx.annotation.Px; 63 import androidx.annotation.VisibleForTesting; 64 import androidx.core.graphics.ColorUtils; 65 import androidx.recyclerview.widget.RecyclerView; 66 67 import com.android.launcher3.DeviceProfile; 68 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; 69 import com.android.launcher3.DragSource; 70 import com.android.launcher3.DropTarget.DragObject; 71 import com.android.launcher3.Insettable; 72 import com.android.launcher3.InsettableFrameLayout; 73 import com.android.launcher3.R; 74 import com.android.launcher3.Utilities; 75 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; 76 import com.android.launcher3.allapps.search.AllAppsSearchUiDelegate; 77 import com.android.launcher3.allapps.search.SearchAdapterProvider; 78 import com.android.launcher3.config.FeatureFlags; 79 import com.android.launcher3.keyboard.FocusedItemDecorator; 80 import com.android.launcher3.model.StringCache; 81 import com.android.launcher3.model.data.ItemInfo; 82 import com.android.launcher3.util.ItemInfoMatcher; 83 import com.android.launcher3.util.Themes; 84 import com.android.launcher3.views.ActivityContext; 85 import com.android.launcher3.views.BaseDragLayer; 86 import com.android.launcher3.views.RecyclerViewFastScroller; 87 import com.android.launcher3.views.ScrimView; 88 import com.android.launcher3.views.SpringRelativeLayout; 89 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; 90 91 import java.util.ArrayList; 92 import java.util.Arrays; 93 import java.util.List; 94 import java.util.function.Predicate; 95 import java.util.stream.Stream; 96 97 /** 98 * All apps container view with search support for use in a dragging activity. 99 * 100 * @param <T> Type of context inflating all apps. 101 */ 102 public class ActivityAllAppsContainerView<T extends Context & ActivityContext> 103 extends SpringRelativeLayout implements DragSource, Insettable, 104 OnDeviceProfileChangeListener, PersonalWorkSlidingTabStrip.OnActivePageChangedListener, 105 ScrimView.ScrimDrawingController { 106 107 108 public static final FloatProperty<ActivityAllAppsContainerView<?>> BOTTOM_SHEET_ALPHA = 109 new FloatProperty<>("bottomSheetAlpha") { 110 @Override 111 public Float get(ActivityAllAppsContainerView<?> containerView) { 112 return containerView.mBottomSheetAlpha; 113 } 114 115 @Override 116 public void setValue(ActivityAllAppsContainerView<?> containerView, float v) { 117 containerView.setBottomSheetAlpha(v); 118 } 119 }; 120 121 public static final float PULL_MULTIPLIER = .02f; 122 public static final float FLING_VELOCITY_MULTIPLIER = 1200f; 123 protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page"; 124 private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300; 125 // Render the header protection at all times to debug clipping issues. 126 private static final boolean DEBUG_HEADER_PROTECTION = false; 127 /** Context of an activity or window that is inflating this container. */ 128 129 protected final T mActivityContext; 130 protected final List<AdapterHolder> mAH; 131 protected final Predicate<ItemInfo> mPersonalMatcher = ItemInfoMatcher.ofUser( 132 Process.myUserHandle()); 133 protected final WorkProfileManager mWorkManager; 134 protected final Point mFastScrollerOffset = new Point(); 135 protected final int mScrimColor; 136 protected final float mHeaderThreshold; 137 protected final AllAppsSearchUiDelegate mSearchUiDelegate; 138 139 // Used to animate Search results out and A-Z apps in, or vice-versa. 140 private final SearchTransitionController mSearchTransitionController; 141 private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 142 private final Rect mInsets = new Rect(); 143 private final AllAppsStore<T> mAllAppsStore; 144 private final RecyclerView.OnScrollListener mScrollListener = 145 new RecyclerView.OnScrollListener() { 146 @Override 147 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 148 updateHeaderScroll(recyclerView.computeVerticalScrollOffset()); 149 } 150 }; 151 private final Paint mNavBarScrimPaint; 152 private final int mHeaderProtectionColor; 153 private final Path mTmpPath = new Path(); 154 private final RectF mTmpRectF = new RectF(); 155 protected AllAppsPagedView mViewPager; 156 protected FloatingHeaderView mHeader; 157 protected View mBottomSheetBackground; 158 protected RecyclerViewFastScroller mFastScroller; 159 160 /** 161 * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}. 162 */ 163 protected View mSearchContainer; 164 protected SearchUiManager mSearchUiManager; 165 protected boolean mUsingTabs; 166 protected RecyclerViewFastScroller mTouchHandler; 167 168 /** {@code true} when rendered view is in search state instead of the scroll state. */ 169 private boolean mIsSearching; 170 private boolean mRebindAdaptersAfterSearchAnimation; 171 private int mNavBarScrimHeight = 0; 172 private SearchRecyclerView mSearchRecyclerView; 173 protected SearchAdapterProvider<?> mMainAdapterProvider; 174 private View mBottomSheetHandleArea; 175 private boolean mHasWorkApps; 176 private float[] mBottomSheetCornerRadii; 177 private ScrimView mScrimView; 178 private int mHeaderColor; 179 private int mBottomSheetBackgroundColor; 180 private float mBottomSheetAlpha = 1f; 181 private boolean mForceBottomSheetVisible; 182 private int mTabsProtectionAlpha; 183 @Nullable private AllAppsTransitionController mAllAppsTransitionController; 184 ActivityAllAppsContainerView(Context context)185 public ActivityAllAppsContainerView(Context context) { 186 this(context, null); 187 } 188 ActivityAllAppsContainerView(Context context, AttributeSet attrs)189 public ActivityAllAppsContainerView(Context context, AttributeSet attrs) { 190 this(context, attrs, 0); 191 } 192 ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr)193 public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { 194 super(context, attrs, defStyleAttr); 195 mActivityContext = ActivityContext.lookupContext(context); 196 mAllAppsStore = new AllAppsStore<>(mActivityContext); 197 198 mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor); 199 mHeaderThreshold = getResources().getDimensionPixelSize( 200 R.dimen.dynamic_grid_cell_border_spacing); 201 mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor); 202 203 mWorkManager = new WorkProfileManager( 204 mActivityContext.getSystemService(UserManager.class), 205 this, mActivityContext.getStatsLogManager()); 206 mAH = Arrays.asList(null, null, null); 207 mNavBarScrimPaint = new Paint(); 208 mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext)); 209 210 AllAppsStore.OnUpdateListener onAppsUpdated = this::onAppsUpdated; 211 mAllAppsStore.addUpdateListener(onAppsUpdated); 212 213 // This is a focus listener that proxies focus from a view into the list view. This is to 214 // work around the search box from getting first focus and showing the cursor. 215 setOnFocusChangeListener((v, hasFocus) -> { 216 if (hasFocus && getActiveRecyclerView() != null) { 217 getActiveRecyclerView().requestFocus(); 218 } 219 }); 220 mSearchUiDelegate = createSearchUiDelegate(); 221 initContent(); 222 223 mSearchTransitionController = new SearchTransitionController(this); 224 } 225 226 /** Creates the delegate for initializing search. */ createSearchUiDelegate()227 protected AllAppsSearchUiDelegate createSearchUiDelegate() { 228 return new AllAppsSearchUiDelegate(this); 229 } 230 getSearchUiDelegate()231 public AllAppsSearchUiDelegate getSearchUiDelegate() { 232 return mSearchUiDelegate; 233 } 234 235 /** 236 * Initializes the view hierarchy and internal variables. Any initialization which actually uses 237 * these members should be done in {@link #onFinishInflate()}. 238 * In terms of subclass initialization, the following would be parallel order for activity: 239 * initContent -> onPreCreate 240 * constructor/init -> onCreate 241 * onFinishInflate -> onPostCreate 242 */ initContent()243 protected void initContent() { 244 mMainAdapterProvider = mSearchUiDelegate.createMainAdapterProvider(); 245 246 mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN, 247 new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, null))); 248 mAH.set(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK, 249 new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, mWorkManager))); 250 mAH.set(SEARCH, new AdapterHolder(SEARCH, 251 new AlphabeticalAppsList<>(mActivityContext, null, null))); 252 253 getLayoutInflater().inflate(R.layout.all_apps_content, this); 254 mHeader = findViewById(R.id.all_apps_header); 255 mBottomSheetBackground = findViewById(R.id.bottom_sheet_background); 256 mBottomSheetHandleArea = findViewById(R.id.bottom_sheet_handle_area); 257 mSearchRecyclerView = findViewById(R.id.search_results_list_view); 258 mFastScroller = findViewById(R.id.fast_scroller); 259 mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup)); 260 261 mSearchContainer = inflateSearchBar(); 262 if (!isSearchBarFloating()) { 263 // Add the search box above everything else in this container (if the flag is enabled, 264 // it's added to drag layer in onAttach instead). 265 addView(mSearchContainer); 266 } 267 mSearchUiManager = (SearchUiManager) mSearchContainer; 268 } 269 270 @Override onFinishInflate()271 protected void onFinishInflate() { 272 super.onFinishInflate(); 273 274 mAH.get(SEARCH).setup(mSearchRecyclerView, 275 /* Filter out A-Z apps */ itemInfo -> false); 276 rebindAdapters(true /* force */); 277 float cornerRadius = Themes.getDialogCornerRadius(getContext()); 278 mBottomSheetCornerRadii = new float[]{ 279 cornerRadius, 280 cornerRadius, // Top left radius in px 281 cornerRadius, 282 cornerRadius, // Top right radius in px 283 0, 284 0, // Bottom right 285 0, 286 0 // Bottom left 287 }; 288 mBottomSheetBackgroundColor = 289 Themes.getAttrColor(getContext(), R.attr.materialColorSurfaceDim); 290 updateBackgroundVisibility(mActivityContext.getDeviceProfile()); 291 mSearchUiManager.initializeSearch(this); 292 } 293 294 @Override onAttachedToWindow()295 protected void onAttachedToWindow() { 296 super.onAttachedToWindow(); 297 if (isSearchBarFloating()) { 298 // Note: for Taskbar this is removed in TaskbarAllAppsController#cleanUpOverlay when the 299 // panel is closed. Can't do so in onDetach because we are also a child of drag layer 300 // so can't remove its views during that dispatch. 301 mActivityContext.getDragLayer().addView(mSearchContainer); 302 mSearchUiDelegate.onInitializeSearchBar(); 303 } 304 mActivityContext.addOnDeviceProfileChangeListener(this); 305 } 306 307 @Override onDetachedFromWindow()308 protected void onDetachedFromWindow() { 309 super.onDetachedFromWindow(); 310 mActivityContext.removeOnDeviceProfileChangeListener(this); 311 } 312 getSearchUiManager()313 public SearchUiManager getSearchUiManager() { 314 return mSearchUiManager; 315 } 316 getBottomSheetBackground()317 public View getBottomSheetBackground() { 318 return mBottomSheetBackground; 319 } 320 321 /** 322 * Temporarily force the bottom sheet to be visible on non-tablets. 323 * 324 * @param force {@code true} means bottom sheet will be visible on phones until {@code reset()}. 325 */ forceBottomSheetVisible(boolean force)326 public void forceBottomSheetVisible(boolean force) { 327 mForceBottomSheetVisible = force; 328 updateBackgroundVisibility(mActivityContext.getDeviceProfile()); 329 } 330 getSearchView()331 public View getSearchView() { 332 return mSearchContainer; 333 } 334 335 /** Invoke when the current search session is finished. */ onClearSearchResult()336 public void onClearSearchResult() { 337 getMainAdapterProvider().clearHighlightedItem(); 338 animateToSearchState(false); 339 rebindAdapters(); 340 } 341 342 /** 343 * Sets results list for search 344 */ setSearchResults(ArrayList<AdapterItem> results)345 public void setSearchResults(ArrayList<AdapterItem> results) { 346 getMainAdapterProvider().clearHighlightedItem(); 347 if (getSearchResultList().setSearchResults(results)) { 348 getSearchRecyclerView().onSearchResultsChanged(); 349 } 350 if (results != null) { 351 animateToSearchState(true); 352 } 353 } 354 355 /** 356 * Sets results list for search. 357 * 358 * @param searchResultCode indicates if the result is final or intermediate for a given query 359 * since we can get search results from multiple sources. 360 */ setSearchResults(ArrayList<AdapterItem> results, int searchResultCode)361 public void setSearchResults(ArrayList<AdapterItem> results, int searchResultCode) { 362 setSearchResults(results); 363 mSearchUiDelegate.onSearchResultsChanged(results, searchResultCode); 364 } 365 animateToSearchState(boolean goingToSearch)366 private void animateToSearchState(boolean goingToSearch) { 367 animateToSearchState(goingToSearch, DEFAULT_SEARCH_TRANSITION_DURATION_MS); 368 } 369 setAllAppsTransitionController( AllAppsTransitionController allAppsTransitionController)370 public void setAllAppsTransitionController( 371 AllAppsTransitionController allAppsTransitionController) { 372 mAllAppsTransitionController = allAppsTransitionController; 373 } 374 animateToSearchState(boolean goingToSearch, long durationMs)375 private void animateToSearchState(boolean goingToSearch, long durationMs) { 376 if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) { 377 return; 378 } 379 mFastScroller.setVisibility(goingToSearch ? INVISIBLE : VISIBLE); 380 if (goingToSearch) { 381 // Fade out the button to pause work apps. 382 mWorkManager.onActivePageChanged(SEARCH); 383 } else if (mAllAppsTransitionController != null) { 384 // If exiting search, revert predictive back scale on all apps 385 mAllAppsTransitionController.animateAllAppsToNoScale(); 386 } 387 mSearchTransitionController.animateToSearchState(goingToSearch, durationMs, 388 /* onEndRunnable = */ () -> { 389 mIsSearching = goingToSearch; 390 updateSearchResultsVisibility(); 391 int previousPage = getCurrentPage(); 392 if (mRebindAdaptersAfterSearchAnimation) { 393 rebindAdapters(false); 394 mRebindAdaptersAfterSearchAnimation = false; 395 } 396 397 if (goingToSearch) { 398 mSearchUiDelegate.onAnimateToSearchStateCompleted(); 399 } else { 400 setSearchResults(null); 401 if (mViewPager != null) { 402 mViewPager.setCurrentPage(previousPage); 403 } 404 onActivePageChanged(previousPage); 405 } 406 }); 407 } 408 shouldContainerScroll(MotionEvent ev)409 public boolean shouldContainerScroll(MotionEvent ev) { 410 BaseDragLayer dragLayer = mActivityContext.getDragLayer(); 411 // IF the MotionEvent is inside the search box or handle area, and the container keeps on 412 // receiving touch input, container should move down. 413 if (dragLayer.isEventOverView(mSearchContainer, ev) 414 || dragLayer.isEventOverView(mBottomSheetHandleArea, ev)) { 415 return true; 416 } 417 AllAppsRecyclerView rv = getActiveRecyclerView(); 418 if (rv == null) { 419 return true; 420 } 421 if (rv.getScrollbar() != null 422 && rv.getScrollbar().getThumbOffsetY() >= 0 423 && dragLayer.isEventOverView(rv.getScrollbar(), ev)) { 424 return false; 425 } 426 // Scroll if not within the container view (e.g. over large-screen scrim). 427 if (!dragLayer.isEventOverView(getVisibleContainerView(), ev)) { 428 return true; 429 } 430 return rv.shouldContainerScroll(ev, dragLayer); 431 } 432 433 /** 434 * Resets the UI to be ready for fresh interactions in the future. Exits search and returns to 435 * A-Z apps list. 436 * 437 * @param animate Whether to animate the header during the reset (e.g. switching profile tabs). 438 */ reset(boolean animate)439 public void reset(boolean animate) { 440 reset(animate, true); 441 } 442 443 /** 444 * Resets the UI to be ready for fresh interactions in the future. 445 * 446 * @param animate Whether to animate the header during the reset (e.g. switching profile tabs). 447 * @param exitSearch Whether to force exit the search state and return to A-Z apps list. 448 */ reset(boolean animate, boolean exitSearch)449 public void reset(boolean animate, boolean exitSearch) { 450 for (int i = 0; i < mAH.size(); i++) { 451 if (mAH.get(i).mRecyclerView != null) { 452 mAH.get(i).mRecyclerView.scrollToTop(); 453 } 454 } 455 if (mTouchHandler != null) { 456 mTouchHandler.endFastScrolling(); 457 } 458 if (mHeader != null && mHeader.getVisibility() == VISIBLE) { 459 mHeader.reset(animate); 460 } 461 forceBottomSheetVisible(false); 462 // Reset the base recycler view after transitioning home. 463 updateHeaderScroll(0); 464 if (exitSearch) { 465 // Reset the search bar after transitioning home. 466 MAIN_EXECUTOR.getHandler().post(mSearchUiManager::resetSearch); 467 // Animate to A-Z with 0 time to reset the animation with proper state management. 468 animateToSearchState(false, 0); 469 } 470 if (isSearching()) { 471 mWorkManager.reset(); 472 } 473 } 474 475 @Override dispatchKeyEvent(KeyEvent event)476 public boolean dispatchKeyEvent(KeyEvent event) { 477 mSearchUiManager.preDispatchKeyEvent(event); 478 return super.dispatchKeyEvent(event); 479 } 480 getDescription()481 public String getDescription() { 482 if (!mUsingTabs && isSearching()) { 483 return getContext().getString(R.string.all_apps_search_results); 484 } else { 485 StringCache cache = mActivityContext.getStringCache(); 486 if (mUsingTabs) { 487 if (cache != null) { 488 return isPersonalTab() 489 ? cache.allAppsPersonalTabAccessibility 490 : cache.allAppsWorkTabAccessibility; 491 } else { 492 return isPersonalTab() 493 ? getContext().getString(R.string.all_apps_button_personal_label) 494 : getContext().getString(R.string.all_apps_button_work_label); 495 } 496 } 497 return getContext().getString(R.string.all_apps_button_label); 498 } 499 } 500 isSearching()501 public boolean isSearching() { 502 return mIsSearching; 503 } 504 505 @Override onActivePageChanged(int currentActivePage)506 public void onActivePageChanged(int currentActivePage) { 507 if (mSearchTransitionController.isRunning()) { 508 // Will be called at the end of the animation. 509 return; 510 } 511 if (currentActivePage != SEARCH) { 512 mActivityContext.hideKeyboard(); 513 } 514 if (mAH.get(currentActivePage).mRecyclerView != null) { 515 mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller); 516 } 517 // Header keeps track of active recycler view to properly render header protection. 518 mHeader.setActiveRV(currentActivePage); 519 reset(true /* animate */, !isSearching() /* exitSearch */); 520 521 mWorkManager.onActivePageChanged(currentActivePage); 522 } 523 rebindAdapters()524 protected void rebindAdapters() { 525 rebindAdapters(false /* force */); 526 } 527 rebindAdapters(boolean force)528 protected void rebindAdapters(boolean force) { 529 if (mSearchTransitionController.isRunning()) { 530 mRebindAdaptersAfterSearchAnimation = true; 531 return; 532 } 533 updateSearchResultsVisibility(); 534 535 boolean showTabs = shouldShowTabs(); 536 if (showTabs == mUsingTabs && !force) { 537 return; 538 } 539 540 if (!FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) { 541 RecyclerView.ItemDecoration decoration = getMainAdapterProvider().getDecorator(); 542 getSearchRecyclerView().removeItemDecoration(decoration); 543 getSearchRecyclerView().addItemDecoration(decoration); 544 } 545 546 // replaceAppsRVcontainer() needs to use both mUsingTabs value to remove the old view AND 547 // showTabs value to create new view. Hence the mUsingTabs new value assignment MUST happen 548 // after this call. 549 replaceAppsRVContainer(showTabs); 550 mUsingTabs = showTabs; 551 552 mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); 553 mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); 554 mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); 555 556 final AllAppsRecyclerView mainRecyclerView; 557 final AllAppsRecyclerView workRecyclerView; 558 if (mUsingTabs) { 559 mainRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(0); 560 workRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(1); 561 mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher); 562 mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getMatcher()); 563 workRecyclerView.setId(R.id.apps_list_view_work); 564 if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) { 565 mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener( 566 mWorkManager.newScrollListener()); 567 } 568 mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN); 569 findViewById(R.id.tab_personal) 570 .setOnClickListener((View view) -> { 571 if (mViewPager.snapToPage(AdapterHolder.MAIN)) { 572 mActivityContext.getStatsLogManager().logger() 573 .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB); 574 } 575 }); 576 findViewById(R.id.tab_work) 577 .setOnClickListener((View view) -> { 578 if (mViewPager.snapToPage(AdapterHolder.WORK)) { 579 mActivityContext.getStatsLogManager().logger() 580 .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB); 581 } 582 }); 583 setDeviceManagementResources(); 584 if (mHeader.isSetUp()) { 585 onActivePageChanged(mViewPager.getNextPage()); 586 } 587 } else { 588 mainRecyclerView = findViewById(R.id.apps_list_view); 589 workRecyclerView = null; 590 mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, null); 591 mAH.get(AdapterHolder.WORK).mRecyclerView = null; 592 } 593 setUpCustomRecyclerViewPool( 594 mainRecyclerView, 595 workRecyclerView, 596 mAllAppsStore.getRecyclerViewPool()); 597 setupHeader(); 598 599 if (isSearchBarFloating()) { 600 // Keep the scroller above the search bar. 601 RelativeLayout.LayoutParams scrollerLayoutParams = 602 (LayoutParams) mFastScroller.getLayoutParams(); 603 scrollerLayoutParams.bottomMargin = mSearchContainer.getHeight() 604 + getResources().getDimensionPixelSize( 605 R.dimen.fastscroll_bottom_margin_floating_search); 606 } 607 608 mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); 609 mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); 610 mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); 611 } 612 613 /** 614 * If {@link ENABLE_ALL_APPS_RV_PREINFLATION} is enabled, wire custom 615 * {@link RecyclerView.RecycledViewPool} to main and work {@link AllAppsRecyclerView}. 616 * 617 * Then if {@link ALL_APPS_GONE_VISIBILITY} is enabled, update max pool size. This is because 618 * all apps rv's hidden visibility is changed to {@link View#GONE} from {@link View#INVISIBLE), 619 * thus we cannot rely on layout pass to update pool size. 620 */ setUpCustomRecyclerViewPool( @onNull AllAppsRecyclerView mainRecyclerView, @Nullable AllAppsRecyclerView workRecyclerView, @NonNull RecyclerView.RecycledViewPool recycledViewPool)621 private static void setUpCustomRecyclerViewPool( 622 @NonNull AllAppsRecyclerView mainRecyclerView, 623 @Nullable AllAppsRecyclerView workRecyclerView, 624 @NonNull RecyclerView.RecycledViewPool recycledViewPool) { 625 if (!ENABLE_ALL_APPS_RV_PREINFLATION.get()) { 626 return; 627 } 628 mainRecyclerView.setRecycledViewPool(recycledViewPool); 629 if (workRecyclerView != null) { 630 workRecyclerView.setRecycledViewPool(recycledViewPool); 631 } 632 if (ALL_APPS_GONE_VISIBILITY.get()) { 633 mainRecyclerView.updatePoolSize(); 634 } 635 } 636 replaceAppsRVContainer(boolean showTabs)637 private void replaceAppsRVContainer(boolean showTabs) { 638 for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) { 639 AdapterHolder adapterHolder = mAH.get(i); 640 if (adapterHolder.mRecyclerView != null) { 641 adapterHolder.mRecyclerView.setLayoutManager(null); 642 adapterHolder.mRecyclerView.setAdapter(null); 643 } 644 } 645 View oldView = getAppsRecyclerViewContainer(); 646 int index = indexOfChild(oldView); 647 removeView(oldView); 648 int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout; 649 final View rvContainer = getLayoutInflater().inflate(layout, this, false); 650 addView(rvContainer, index); 651 if (showTabs) { 652 mViewPager = (AllAppsPagedView) rvContainer; 653 mViewPager.initParentViews(this); 654 mViewPager.getPageIndicator().setOnActivePageChangedListener(this); 655 mViewPager.setOutlineProvider(new ViewOutlineProvider() { 656 @Override 657 public void getOutline(View view, Outline outline) { 658 @Px final int bottomOffsetPx = 659 (int) (ActivityAllAppsContainerView.this.getMeasuredHeight() 660 * PREDICTIVE_BACK_MIN_SCALE); 661 outline.setRect( 662 0, 663 0, 664 view.getMeasuredWidth(), 665 view.getMeasuredHeight() + bottomOffsetPx); 666 } 667 }); 668 669 mWorkManager.reset(); 670 post(() -> mAH.get(AdapterHolder.WORK).applyPadding()); 671 672 } else { 673 mWorkManager.detachWorkModeSwitch(); 674 mViewPager = null; 675 } 676 677 removeCustomRules(rvContainer); 678 removeCustomRules(getSearchRecyclerView()); 679 if (!isSearchSupported()) { 680 layoutWithoutSearchContainer(rvContainer, showTabs); 681 } else if (isSearchBarFloating()) { 682 alignParentTop(rvContainer, showTabs); 683 alignParentTop(getSearchRecyclerView(), /* tabs= */ false); 684 } else { 685 layoutBelowSearchContainer(rvContainer, showTabs); 686 layoutBelowSearchContainer(getSearchRecyclerView(), /* tabs= */ false); 687 } 688 689 updateSearchResultsVisibility(); 690 } 691 setupHeader()692 void setupHeader() { 693 mHeader.setVisibility(View.VISIBLE); 694 boolean tabsHidden = !mUsingTabs; 695 mHeader.setup( 696 mAH.get(AdapterHolder.MAIN).mRecyclerView, 697 mAH.get(AdapterHolder.WORK).mRecyclerView, 698 (SearchRecyclerView) mAH.get(SEARCH).mRecyclerView, 699 getCurrentPage(), 700 tabsHidden); 701 702 int padding = mHeader.getMaxTranslation(); 703 mAH.forEach(adapterHolder -> { 704 adapterHolder.mPadding.top = padding; 705 adapterHolder.applyPadding(); 706 if (adapterHolder.mRecyclerView != null) { 707 adapterHolder.mRecyclerView.scrollToTop(); 708 } 709 }); 710 711 removeCustomRules(mHeader); 712 if (!isSearchSupported()) { 713 layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */); 714 } else if (isSearchBarFloating()) { 715 alignParentTop(mHeader, false /* includeTabsMargin */); 716 } else { 717 layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */); 718 } 719 } 720 updateHeaderScroll(int scrolledOffset)721 protected void updateHeaderScroll(int scrolledOffset) { 722 float prog1 = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); 723 int headerColor = getHeaderColor(prog1); 724 int tabsAlpha = mHeader.getPeripheralProtectionHeight() == 0 ? 0 725 : (int) (Utilities.boundToRange( 726 (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f) 727 * 255); 728 if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) { 729 mHeaderColor = headerColor; 730 mTabsProtectionAlpha = tabsAlpha; 731 invalidateHeader(); 732 } 733 if (mSearchUiManager.getEditText() == null) { 734 return; 735 } 736 737 float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); 738 boolean bgVisible = mSearchUiManager.getBackgroundVisibility(); 739 if (scrolledOffset == 0 && !isSearching()) { 740 bgVisible = true; 741 } else if (scrolledOffset > mHeaderThreshold) { 742 bgVisible = false; 743 } 744 mSearchUiManager.setBackgroundVisibility(bgVisible, 1 - prog); 745 } 746 getHeaderColor(float blendRatio)747 protected int getHeaderColor(float blendRatio) { 748 return ColorUtils.setAlphaComponent( 749 ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, blendRatio), 750 (int) (mSearchContainer.getAlpha() * 255)); 751 } 752 753 /** 754 * @return true if the search bar is floating above this container (at the bottom of the screen) 755 */ isSearchBarFloating()756 protected boolean isSearchBarFloating() { 757 return mSearchUiDelegate.isSearchBarFloating(); 758 } 759 760 /** 761 * Whether the <em>floating</em> search bar should appear as a small pill when not focused. 762 * <p> 763 * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely 764 * makes sense to use that method to derive an appropriate value for the current/target state. 765 */ shouldFloatingSearchBarBePillWhenUnfocused()766 public boolean shouldFloatingSearchBarBePillWhenUnfocused() { 767 return false; 768 } 769 770 /** 771 * How far from the bottom of the screen the <em>floating</em> search bar should rest when the 772 * IME is not present. 773 * <p> 774 * To hide offscreen, use a negative value. 775 * <p> 776 * Note: if the provided value is non-negative but less than the current bottom insets, the 777 * insets will be applied. As such, you can use 0 to default to this. 778 * <p> 779 * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely 780 * makes sense to use that method to derive an appropriate value for the current/target state. 781 */ getFloatingSearchBarRestingMarginBottom()782 public int getFloatingSearchBarRestingMarginBottom() { 783 return 0; 784 } 785 786 /** 787 * How far from the start of the screen the <em>floating</em> search bar should rest. 788 * <p> 789 * To use original margin, return a negative value. 790 * <p> 791 * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely 792 * makes sense to use that method to derive an appropriate value for the current/target state. 793 */ getFloatingSearchBarRestingMarginStart()794 public int getFloatingSearchBarRestingMarginStart() { 795 DeviceProfile dp = mActivityContext.getDeviceProfile(); 796 return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin(); 797 } 798 799 /** 800 * How far from the end of the screen the <em>floating</em> search bar should rest. 801 * <p> 802 * To use original margin, return a negative value. 803 * <p> 804 * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely 805 * makes sense to use that method to derive an appropriate value for the current/target state. 806 */ getFloatingSearchBarRestingMarginEnd()807 public int getFloatingSearchBarRestingMarginEnd() { 808 DeviceProfile dp = mActivityContext.getDeviceProfile(); 809 return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin(); 810 } 811 layoutBelowSearchContainer(View v, boolean includeTabsMargin)812 private void layoutBelowSearchContainer(View v, boolean includeTabsMargin) { 813 if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { 814 return; 815 } 816 817 RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); 818 layoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.search_container_all_apps); 819 820 int topMargin = getContext().getResources().getDimensionPixelSize( 821 R.dimen.all_apps_header_top_margin); 822 if (includeTabsMargin) { 823 topMargin += getContext().getResources().getDimensionPixelSize( 824 R.dimen.all_apps_header_pill_height); 825 } 826 layoutParams.topMargin = topMargin; 827 } 828 alignParentTop(View v, boolean includeTabsMargin)829 private void alignParentTop(View v, boolean includeTabsMargin) { 830 if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { 831 return; 832 } 833 834 RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); 835 layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); 836 layoutParams.topMargin = 837 includeTabsMargin 838 ? getContext().getResources().getDimensionPixelSize( 839 R.dimen.all_apps_header_pill_height) 840 : 0; 841 } 842 removeCustomRules(View v)843 private void removeCustomRules(View v) { 844 if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { 845 return; 846 } 847 848 RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); 849 layoutParams.removeRule(RelativeLayout.ABOVE); 850 layoutParams.removeRule(RelativeLayout.ALIGN_TOP); 851 layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_TOP); 852 } 853 createAdapter(AlphabeticalAppsList<T> appsList)854 protected BaseAllAppsAdapter<T> createAdapter(AlphabeticalAppsList<T> appsList) { 855 return new AllAppsGridAdapter<>(mActivityContext, getLayoutInflater(), appsList, 856 mMainAdapterProvider); 857 } 858 859 // TODO(b/216683257): Remove when Taskbar All Apps supports search. isSearchSupported()860 protected boolean isSearchSupported() { 861 return true; 862 } 863 layoutWithoutSearchContainer(View v, boolean includeTabsMargin)864 private void layoutWithoutSearchContainer(View v, boolean includeTabsMargin) { 865 if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) { 866 return; 867 } 868 869 RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams(); 870 layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); 871 layoutParams.topMargin = getContext().getResources().getDimensionPixelSize(includeTabsMargin 872 ? R.dimen.all_apps_header_pill_height 873 : R.dimen.all_apps_header_top_margin); 874 } 875 isInAllApps()876 public boolean isInAllApps() { 877 // TODO: Make this abstract 878 return true; 879 } 880 881 /** 882 * Inflates the search bar 883 */ inflateSearchBar()884 protected View inflateSearchBar() { 885 return mSearchUiDelegate.inflateSearchBar(); 886 } 887 888 /** The adapter provider for the main section. */ getMainAdapterProvider()889 public final SearchAdapterProvider<?> getMainAdapterProvider() { 890 return mMainAdapterProvider; 891 } 892 893 @Override dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray)894 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray) { 895 try { 896 // Many slice view id is not properly assigned, and hence throws null 897 // pointer exception in the underneath method. Catching the exception 898 // simply doesn't restore these slice views. This doesn't have any 899 // user visible effect because because we query them again. 900 super.dispatchRestoreInstanceState(sparseArray); 901 } catch (Exception e) { 902 Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e); 903 } 904 905 Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null); 906 if (state != null) { 907 int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0); 908 if (currentPage == AdapterHolder.WORK && mViewPager != null) { 909 mViewPager.setCurrentPage(currentPage); 910 rebindAdapters(); 911 } else { 912 reset(true); 913 } 914 } 915 } 916 917 @Override dispatchSaveInstanceState(SparseArray<Parcelable> container)918 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 919 super.dispatchSaveInstanceState(container); 920 Bundle state = new Bundle(); 921 state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage()); 922 container.put(R.id.work_tab_state_id, state); 923 } 924 getAppsStore()925 public AllAppsStore<T> getAppsStore() { 926 return mAllAppsStore; 927 } 928 getWorkManager()929 public WorkProfileManager getWorkManager() { 930 return mWorkManager; 931 } 932 933 @Override onDeviceProfileChanged(DeviceProfile dp)934 public void onDeviceProfileChanged(DeviceProfile dp) { 935 for (AdapterHolder holder : mAH) { 936 holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns); 937 holder.mAppsList.setNumAppsPerRowAllApps(dp.numShownAllAppsColumns); 938 if (holder.mRecyclerView != null) { 939 // Remove all views and clear the pool, while keeping the data same. After this 940 // call, all the viewHolders will be recreated. 941 holder.mRecyclerView.swapAdapter(holder.mRecyclerView.getAdapter(), true); 942 holder.mRecyclerView.getRecycledViewPool().clear(); 943 } 944 } 945 updateBackgroundVisibility(dp); 946 947 int navBarScrimColor = Themes.getNavBarScrimColor(mActivityContext); 948 if (mNavBarScrimPaint.getColor() != navBarScrimColor) { 949 mNavBarScrimPaint.setColor(navBarScrimColor); 950 invalidate(); 951 } 952 } 953 updateBackgroundVisibility(DeviceProfile deviceProfile)954 protected void updateBackgroundVisibility(DeviceProfile deviceProfile) { 955 boolean visible = deviceProfile.isTablet || mForceBottomSheetVisible; 956 mBottomSheetBackground.setVisibility(visible ? View.VISIBLE : View.GONE); 957 // Note: For tablets, the opaque background and header protection are added in drawOnScrim. 958 // For the taskbar entrypoint, the scrim is drawn by its abstract slide in view container, 959 // so its header protection is derived from this scrim instead. 960 } 961 setBottomSheetAlpha(float alpha)962 private void setBottomSheetAlpha(float alpha) { 963 // Bottom sheet alpha is always 1 for tablets. 964 mBottomSheetAlpha = mActivityContext.getDeviceProfile().isTablet ? 1f : alpha; 965 } 966 onAppsUpdated()967 private void onAppsUpdated() { 968 mHasWorkApps = Stream.of(mAllAppsStore.getApps()).anyMatch(mWorkManager.getMatcher()); 969 if (!isSearching()) { 970 rebindAdapters(); 971 if (mHasWorkApps) { 972 mWorkManager.reset(); 973 } 974 } 975 976 mActivityContext.getStatsLogManager().logger() 977 .withCardinality(mAllAppsStore.getApps().length) 978 .log(LAUNCHER_ALLAPPS_COUNT); 979 } 980 981 @Override onInterceptTouchEvent(MotionEvent ev)982 public boolean onInterceptTouchEvent(MotionEvent ev) { 983 // The AllAppsContainerView houses the QSB and is hence visible from the Workspace 984 // Overview states. We shouldn't intercept for the scrubber in these cases. 985 if (!isInAllApps()) { 986 mTouchHandler = null; 987 return false; 988 } 989 990 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 991 AllAppsRecyclerView rv = getActiveRecyclerView(); 992 if (rv != null && rv.getScrollbar() != null 993 && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { 994 mTouchHandler = rv.getScrollbar(); 995 } else { 996 mTouchHandler = null; 997 } 998 } 999 if (mTouchHandler != null) { 1000 return mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); 1001 } 1002 return false; 1003 } 1004 1005 @Override onTouchEvent(MotionEvent ev)1006 public boolean onTouchEvent(MotionEvent ev) { 1007 if (!isInAllApps()) { 1008 return false; 1009 } 1010 1011 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 1012 AllAppsRecyclerView rv = getActiveRecyclerView(); 1013 if (rv != null && rv.getScrollbar() != null 1014 && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { 1015 mTouchHandler = rv.getScrollbar(); 1016 } else { 1017 mTouchHandler = null; 1018 1019 } 1020 } 1021 if (mTouchHandler != null) { 1022 mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); 1023 return true; 1024 } 1025 if (isSearching() 1026 && mActivityContext.getDragLayer().isEventOverView(getVisibleContainerView(), ev)) { 1027 // if in search state, consume touch event. 1028 return true; 1029 } 1030 return false; 1031 } 1032 1033 /** The current active recycler view (A-Z list from one of the profiles, or search results). */ getActiveRecyclerView()1034 public AllAppsRecyclerView getActiveRecyclerView() { 1035 if (isSearching()) { 1036 return getSearchRecyclerView(); 1037 } 1038 return getActiveAppsRecyclerView(); 1039 } 1040 1041 /** The current apps recycler view in the container. */ getActiveAppsRecyclerView()1042 private AllAppsRecyclerView getActiveAppsRecyclerView() { 1043 if (!mUsingTabs || isPersonalTab()) { 1044 return mAH.get(AdapterHolder.MAIN).mRecyclerView; 1045 } else { 1046 return mAH.get(AdapterHolder.WORK).mRecyclerView; 1047 } 1048 } 1049 1050 /** 1051 * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently 1052 * hidden while searching. 1053 */ getAppsRecyclerViewContainer()1054 public ViewGroup getAppsRecyclerViewContainer() { 1055 return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view); 1056 } 1057 1058 /** The RV for search results, which is hidden while A-Z apps are visible. */ getSearchRecyclerView()1059 public SearchRecyclerView getSearchRecyclerView() { 1060 return mSearchRecyclerView; 1061 } 1062 isPersonalTab()1063 protected boolean isPersonalTab() { 1064 return mViewPager == null || mViewPager.getNextPage() == 0; 1065 } 1066 1067 /** 1068 * Switches the current page to the provided {@code tab} if tabs are supported, otherwise does 1069 * nothing. 1070 */ switchToTab(int tab)1071 public void switchToTab(int tab) { 1072 if (mUsingTabs) { 1073 mViewPager.setCurrentPage(tab); 1074 } 1075 } 1076 getLayoutInflater()1077 public LayoutInflater getLayoutInflater() { 1078 return mSearchUiDelegate.getLayoutInflater(); 1079 } 1080 1081 @Override onDropCompleted(View target, DragObject d, boolean success)1082 public void onDropCompleted(View target, DragObject d, boolean success) {} 1083 1084 @Override setInsets(Rect insets)1085 public void setInsets(Rect insets) { 1086 mInsets.set(insets); 1087 DeviceProfile grid = mActivityContext.getDeviceProfile(); 1088 1089 applyAdapterSideAndBottomPaddings(grid); 1090 1091 MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); 1092 mlp.leftMargin = insets.left; 1093 mlp.rightMargin = insets.right; 1094 setLayoutParams(mlp); 1095 1096 if (grid.isVerticalBarLayout()) { 1097 setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0); 1098 } else { 1099 int topPadding = grid.allAppsTopPadding; 1100 if (isSearchBarFloating() && !grid.isTablet) { 1101 topPadding += getResources().getDimensionPixelSize( 1102 R.dimen.all_apps_additional_top_padding_floating_search); 1103 } 1104 setPadding(grid.allAppsLeftRightMargin, topPadding, grid.allAppsLeftRightMargin, 0); 1105 } 1106 1107 InsettableFrameLayout.dispatchInsets(this, insets); 1108 } 1109 1110 /** 1111 * Returns a padding in case a scrim is shown on the bottom of the view and a padding is needed. 1112 */ computeNavBarScrimHeight(WindowInsets insets)1113 protected int computeNavBarScrimHeight(WindowInsets insets) { 1114 return 0; 1115 } 1116 1117 /** 1118 * Returns the current height of nav bar scrim 1119 */ getNavBarScrimHeight()1120 public int getNavBarScrimHeight() { 1121 return mNavBarScrimHeight; 1122 } 1123 1124 @Override dispatchApplyWindowInsets(WindowInsets insets)1125 public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { 1126 mNavBarScrimHeight = computeNavBarScrimHeight(insets); 1127 applyAdapterSideAndBottomPaddings(mActivityContext.getDeviceProfile()); 1128 return super.dispatchApplyWindowInsets(insets); 1129 } 1130 1131 @Override dispatchDraw(Canvas canvas)1132 protected void dispatchDraw(Canvas canvas) { 1133 super.dispatchDraw(canvas); 1134 1135 if (mNavBarScrimHeight > 0) { 1136 canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), 1137 mNavBarScrimPaint); 1138 } 1139 } 1140 updateSearchResultsVisibility()1141 protected void updateSearchResultsVisibility() { 1142 if (isSearching()) { 1143 getSearchRecyclerView().setVisibility(VISIBLE); 1144 getAppsRecyclerViewContainer().setVisibility(GONE); 1145 mHeader.setVisibility(GONE); 1146 } else { 1147 getSearchRecyclerView().setVisibility(GONE); 1148 getAppsRecyclerViewContainer().setVisibility(VISIBLE); 1149 mHeader.setVisibility(VISIBLE); 1150 } 1151 if (mHeader.isSetUp()) { 1152 mHeader.setActiveRV(getCurrentPage()); 1153 } 1154 } 1155 applyAdapterSideAndBottomPaddings(DeviceProfile grid)1156 private void applyAdapterSideAndBottomPaddings(DeviceProfile grid) { 1157 int bottomPadding = Math.max(mInsets.bottom, mNavBarScrimHeight); 1158 mAH.forEach(adapterHolder -> { 1159 adapterHolder.mPadding.bottom = bottomPadding; 1160 adapterHolder.mPadding.left = 1161 adapterHolder.mPadding.right = grid.allAppsLeftRightPadding; 1162 adapterHolder.applyPadding(); 1163 }); 1164 } 1165 setDeviceManagementResources()1166 private void setDeviceManagementResources() { 1167 if (mActivityContext.getStringCache() != null) { 1168 Button personalTab = findViewById(R.id.tab_personal); 1169 personalTab.setText(mActivityContext.getStringCache().allAppsPersonalTab); 1170 1171 Button workTab = findViewById(R.id.tab_work); 1172 workTab.setText(mActivityContext.getStringCache().allAppsWorkTab); 1173 } 1174 } 1175 1176 /** 1177 * Returns true if the container has work apps. 1178 */ shouldShowTabs()1179 public boolean shouldShowTabs() { 1180 return mHasWorkApps; 1181 } 1182 1183 // Used by tests only isDescendantViewVisible(int viewId)1184 private boolean isDescendantViewVisible(int viewId) { 1185 final View view = findViewById(viewId); 1186 if (view == null) return false; 1187 1188 if (!view.isShown()) return false; 1189 1190 return view.getGlobalVisibleRect(new Rect()); 1191 } 1192 1193 /** Called in Launcher#bindStringCache() to update the UI when cache is updated. */ updateWorkUI()1194 public void updateWorkUI() { 1195 setDeviceManagementResources(); 1196 if (mWorkManager.getWorkModeSwitch() != null) { 1197 mWorkManager.getWorkModeSwitch().updateStringFromCache(); 1198 } 1199 inflateWorkCardsIfNeeded(); 1200 } 1201 inflateWorkCardsIfNeeded()1202 private void inflateWorkCardsIfNeeded() { 1203 AllAppsRecyclerView workRV = mAH.get(AdapterHolder.WORK).mRecyclerView; 1204 if (workRV != null) { 1205 for (int i = 0; i < workRV.getChildCount(); i++) { 1206 View currentView = workRV.getChildAt(i); 1207 int currentItemViewType = workRV.getChildViewHolder(currentView).getItemViewType(); 1208 if (currentItemViewType == VIEW_TYPE_WORK_EDU_CARD) { 1209 ((WorkEduCard) currentView).updateStringFromCache(); 1210 } else if (currentItemViewType == VIEW_TYPE_WORK_DISABLED_CARD) { 1211 ((WorkPausedCard) currentView).updateStringFromCache(); 1212 } 1213 } 1214 } 1215 } 1216 1217 @VisibleForTesting isPersonalTabVisible()1218 public boolean isPersonalTabVisible() { 1219 return isDescendantViewVisible(R.id.tab_personal); 1220 } 1221 1222 @VisibleForTesting isWorkTabVisible()1223 public boolean isWorkTabVisible() { 1224 return isDescendantViewVisible(R.id.tab_work); 1225 } 1226 getSearchResultList()1227 public AlphabeticalAppsList<T> getSearchResultList() { 1228 return mAH.get(SEARCH).mAppsList; 1229 } 1230 getFloatingHeaderView()1231 public FloatingHeaderView getFloatingHeaderView() { 1232 return mHeader; 1233 } 1234 1235 @VisibleForTesting getContentView()1236 public View getContentView() { 1237 return isSearching() ? getSearchRecyclerView() : getAppsRecyclerViewContainer(); 1238 } 1239 1240 /** The current page visible in all apps. */ getCurrentPage()1241 public int getCurrentPage() { 1242 return isSearching() 1243 ? SEARCH 1244 : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage(); 1245 } 1246 1247 /** 1248 * Adds an update listener to animator that adds springs to the animation. 1249 */ addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity , float progress )1250 public void addSpringFromFlingUpdateListener(ValueAnimator animator, 1251 float velocity /* release velocity */, 1252 float progress /* portion of the distance to travel*/) { 1253 animator.addListener(new AnimatorListenerAdapter() { 1254 @Override 1255 public void onAnimationStart(Animator animator) { 1256 float distance = (1 - progress) * getHeight(); // px 1257 float settleVelocity = Math.min(0, distance 1258 / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration()) 1259 + velocity); 1260 absorbSwipeUpVelocity(Math.max(1000, Math.abs( 1261 Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER)))); 1262 } 1263 }); 1264 } 1265 1266 /** Invoked when the container is pulled. */ onPull(float deltaDistance, float displacement)1267 public void onPull(float deltaDistance, float displacement) { 1268 absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement); 1269 // Current motion spec is to actually push and not pull 1270 // on this surface. However, until EdgeEffect.onPush (b/190612804) is 1271 // implemented at view level, we will simply pull 1272 } 1273 1274 @Override getDrawingRect(Rect outRect)1275 public void getDrawingRect(Rect outRect) { 1276 super.getDrawingRect(outRect); 1277 outRect.offset(0, (int) getTranslationY()); 1278 } 1279 1280 @Override setTranslationY(float translationY)1281 public void setTranslationY(float translationY) { 1282 super.setTranslationY(translationY); 1283 invalidateHeader(); 1284 } 1285 setScrimView(ScrimView scrimView)1286 public void setScrimView(ScrimView scrimView) { 1287 mScrimView = scrimView; 1288 } 1289 1290 @Override drawOnScrimWithScale(Canvas canvas, float scale)1291 public void drawOnScrimWithScale(Canvas canvas, float scale) { 1292 final View panel = mBottomSheetBackground; 1293 final boolean hasBottomSheet = panel.getVisibility() == VISIBLE; 1294 final float translationY = ((View) panel.getParent()).getTranslationY(); 1295 1296 final float horizontalScaleOffset = (1 - scale) * panel.getWidth() / 2; 1297 final float verticalScaleOffset = (1 - scale) * (panel.getHeight() - getHeight() / 2); 1298 1299 final float topNoScale = panel.getTop() + translationY; 1300 final float topWithScale = topNoScale + verticalScaleOffset; 1301 final float leftWithScale = panel.getLeft() + horizontalScaleOffset; 1302 final float rightWithScale = panel.getRight() - horizontalScaleOffset; 1303 // Draw full background panel for tablets. 1304 if (hasBottomSheet) { 1305 mHeaderPaint.setColor(mBottomSheetBackgroundColor); 1306 mHeaderPaint.setAlpha((int) (255 * mBottomSheetAlpha)); 1307 1308 mTmpRectF.set( 1309 leftWithScale, 1310 topWithScale, 1311 rightWithScale, 1312 panel.getBottom()); 1313 mTmpPath.reset(); 1314 mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); 1315 canvas.drawPath(mTmpPath, mHeaderPaint); 1316 } 1317 1318 if (DEBUG_HEADER_PROTECTION) { 1319 mHeaderPaint.setColor(Color.MAGENTA); 1320 mHeaderPaint.setAlpha(255); 1321 } else { 1322 mHeaderPaint.setColor(mHeaderColor); 1323 mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor))); 1324 } 1325 if (mHeaderPaint.getColor() == mScrimColor || mHeaderPaint.getColor() == 0) { 1326 return; 1327 } 1328 1329 // Draw header on background panel 1330 final float headerBottomNoScale = 1331 getHeaderBottom() + getVisibleContainerView().getPaddingTop(); 1332 final float headerHeightNoScale = headerBottomNoScale - topNoScale; 1333 final float headerBottomWithScaleOnTablet = topWithScale + headerHeightNoScale * scale; 1334 final float headerBottomOffset = (getVisibleContainerView().getHeight() * (1 - scale) / 2); 1335 final float headerBottomWithScaleOnPhone = headerBottomNoScale * scale + headerBottomOffset; 1336 final FloatingHeaderView headerView = getFloatingHeaderView(); 1337 if (hasBottomSheet) { 1338 // Start adding header protection if search bar or tabs will attach to the top. 1339 if (!isSearchBarFloating() || mUsingTabs) { 1340 mTmpRectF.set( 1341 leftWithScale, 1342 topWithScale, 1343 rightWithScale, 1344 headerBottomWithScaleOnTablet); 1345 mTmpPath.reset(); 1346 mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); 1347 canvas.drawPath(mTmpPath, mHeaderPaint); 1348 } 1349 } else { 1350 canvas.drawRect(0, 0, canvas.getWidth(), headerBottomWithScaleOnPhone, mHeaderPaint); 1351 } 1352 1353 // If tab exist (such as work profile), extend header with tab height 1354 final int tabsHeight = headerView.getPeripheralProtectionHeight(); 1355 if (mTabsProtectionAlpha > 0 && tabsHeight != 0) { 1356 if (DEBUG_HEADER_PROTECTION) { 1357 mHeaderPaint.setColor(Color.BLUE); 1358 mHeaderPaint.setAlpha(255); 1359 } else { 1360 mHeaderPaint.setAlpha((int) (getAlpha() * mTabsProtectionAlpha)); 1361 } 1362 float left = 0f; 1363 float right = canvas.getWidth(); 1364 if (hasBottomSheet) { 1365 left = mBottomSheetBackground.getLeft() + horizontalScaleOffset; 1366 right = mBottomSheetBackground.getRight() - horizontalScaleOffset; 1367 } 1368 1369 final float tabTopWithScale = hasBottomSheet 1370 ? headerBottomWithScaleOnTablet 1371 : headerBottomWithScaleOnPhone; 1372 final float tabBottomWithScale = tabTopWithScale + tabsHeight * scale; 1373 1374 canvas.drawRect( 1375 left, 1376 tabTopWithScale, 1377 right, 1378 tabBottomWithScale, 1379 mHeaderPaint); 1380 } 1381 } 1382 1383 /** 1384 * redraws header protection 1385 */ invalidateHeader()1386 public void invalidateHeader() { 1387 if (mScrimView != null) { 1388 mScrimView.invalidate(); 1389 } 1390 } 1391 1392 /** Returns the position of the bottom edge of the header */ getHeaderBottom()1393 public int getHeaderBottom() { 1394 int bottom = (int) getTranslationY() + mHeader.getClipTop(); 1395 if (isSearchBarFloating()) { 1396 if (mActivityContext.getDeviceProfile().isTablet) { 1397 return bottom + mBottomSheetBackground.getTop(); 1398 } 1399 return bottom; 1400 } 1401 return bottom + mHeader.getTop(); 1402 } 1403 1404 /** 1405 * Returns a view that denotes the visible part of all apps container view. 1406 */ getVisibleContainerView()1407 public View getVisibleContainerView() { 1408 return mBottomSheetBackground.getVisibility() == VISIBLE ? mBottomSheetBackground : this; 1409 } 1410 onInitializeRecyclerView(RecyclerView rv)1411 protected void onInitializeRecyclerView(RecyclerView rv) { 1412 rv.addOnScrollListener(mScrollListener); 1413 mSearchUiDelegate.onInitializeRecyclerView(rv); 1414 } 1415 1416 /** Returns the instance of @{code SearchTransitionController}. */ getSearchTransitionController()1417 public SearchTransitionController getSearchTransitionController() { 1418 return mSearchTransitionController; 1419 } 1420 1421 /** Holds a {@link BaseAllAppsAdapter} and related fields. */ 1422 public class AdapterHolder { 1423 public static final int MAIN = 0; 1424 public static final int WORK = 1; 1425 public static final int SEARCH = 2; 1426 1427 private final int mType; 1428 public final BaseAllAppsAdapter<T> mAdapter; 1429 final RecyclerView.LayoutManager mLayoutManager; 1430 final AlphabeticalAppsList<T> mAppsList; 1431 final Rect mPadding = new Rect(); 1432 AllAppsRecyclerView mRecyclerView; 1433 AdapterHolder(int type, AlphabeticalAppsList<T> appsList)1434 AdapterHolder(int type, AlphabeticalAppsList<T> appsList) { 1435 mType = type; 1436 mAppsList = appsList; 1437 mAdapter = createAdapter(mAppsList); 1438 mAppsList.setAdapter(mAdapter); 1439 mLayoutManager = mAdapter.getLayoutManager(); 1440 } 1441 setup(@onNull View rv, @Nullable Predicate<ItemInfo> matcher)1442 void setup(@NonNull View rv, @Nullable Predicate<ItemInfo> matcher) { 1443 mAppsList.updateItemFilter(matcher); 1444 mRecyclerView = (AllAppsRecyclerView) rv; 1445 mRecyclerView.bindFastScrollbar(mFastScroller); 1446 mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory()); 1447 mRecyclerView.setApps(mAppsList); 1448 mRecyclerView.setLayoutManager(mLayoutManager); 1449 mRecyclerView.setAdapter(mAdapter); 1450 mRecyclerView.setHasFixedSize(true); 1451 // No animations will occur when changes occur to the items in this RecyclerView. 1452 mRecyclerView.setItemAnimator(null); 1453 onInitializeRecyclerView(mRecyclerView); 1454 FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView); 1455 mRecyclerView.addItemDecoration(focusedItemDecorator); 1456 mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); 1457 applyPadding(); 1458 } 1459 applyPadding()1460 void applyPadding() { 1461 if (mRecyclerView != null) { 1462 int bottomOffset = 0; 1463 if (isWork() && mWorkManager.getWorkModeSwitch() != null) { 1464 bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight(); 1465 } 1466 if (isSearchBarFloating()) { 1467 bottomOffset += mSearchContainer.getHeight(); 1468 } 1469 mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right, 1470 mPadding.bottom + bottomOffset); 1471 } 1472 } 1473 isWork()1474 private boolean isWork() { 1475 return mType == WORK; 1476 } 1477 isSearch()1478 private boolean isSearch() { 1479 return mType == SEARCH; 1480 } 1481 } 1482 } 1483