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