From e0cfbc3475b0ea42c25e301cffaec3a0a405083d Mon Sep 17 00:00:00 2001 From: Evan Rosky Date: Sat, 21 Jan 2023 02:20:22 +0000 Subject: [PATCH 1/6] Revert^8 "Enable Shell Transitions" d9cc7d1eb1c6525f310011568832d6f0870a5dee Change-Id: Ic59c027f05e47eb9939c0c65d19892451fd4ae50 --- quickstep/src/com/android/quickstep/TaskAnimationManager.java | 2 +- .../tests/src/com/android/quickstep/AbstractQuickStepTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java index fd7e3673b3..4c0bde7694 100644 --- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java +++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java @@ -50,7 +50,7 @@ import java.util.HashMap; public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener { public static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); + SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS && SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false); diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java index f190e27237..3817cf2300 100644 --- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java +++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java @@ -36,7 +36,7 @@ import org.junit.rules.TestRule; */ public abstract class AbstractQuickStepTest extends AbstractLauncherUiTest { public static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); + SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); @Override protected TestRule getRulesInsideActivityMonitor() { return RuleChain. From ad753607d2316187622891b19b8180f6cfb7e12f Mon Sep 17 00:00:00 2001 From: Andy Wickham Date: Mon, 23 Jan 2023 10:49:15 -0800 Subject: [PATCH 2/6] Fix header padding when search not enabled. This was specifically affecting taskbar all apps with floating search bar enabled. Header placement should not be changed in that case. Fix: 266440950 Test: Manual in taskbar and other all apps surfaces with and without work profile, floating search flag on and off. Change-Id: I2296a079cc15387a517c61e6b8a21fa0081cba23 --- .../launcher3/allapps/ActivityAllAppsContainerView.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 2511cada78..11d93f0143 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -233,7 +233,9 @@ public class ActivityAllAppsContainerView super.setupHeader(); removeCustomRules(mHeader); - if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) { + if (!isSearchSupported()) { + layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */); + } else if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) { alignParentTop(mHeader, false /* includeTabsMargin */); } else { layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */); From 31e90481d163a6391082c3146ce19853e59826df Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Thu, 12 Jan 2023 16:20:46 -0800 Subject: [PATCH 3/6] Do not allow system gestures when dreaming. Currently, TaskBar expansion is allowed when an activity context is available. Since Dreams are implemented as an activity, this leads to being able to expand the TaskBar over a dream. This changelist updates the logic to not allow system gestures when dreaming. Test: manual Bug: 265755700 Change-Id: If58608c1e491da7906a2615681da0514fa49cfc8 --- .../com/android/quickstep/RecentsAnimationDeviceState.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java index f1c0f3e210..600ea5e3fd 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java @@ -32,6 +32,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; @@ -337,7 +338,8 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { && (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0 && (mSystemUiStateFlags & SYSUI_STATE_MAGNIFICATION_OVERLAP) == 0 && ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0 - || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0); + || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0) + && (mSystemUiStateFlags & SYSUI_STATE_DEVICE_DREAMING) == 0; } /** From 34c499dc844bea1706faba9ee9437af4bbdf5319 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Tue, 24 Jan 2023 12:43:18 -0800 Subject: [PATCH 4/6] Merging unnecessary subclass of ActivityAllAppsContainerView Bug: 266605714 Test: Presubmit Change-Id: I87272ee827055f58f91a4ad9fb25d5351d453cdb --- src/com/android/launcher3/Launcher.java | 3 +- .../allapps/ActivityAllAppsContainerView.java | 916 +++++++++++++++- .../allapps/AllAppsRecyclerView.java | 2 +- .../allapps/BaseAllAppsContainerView.java | 996 ------------------ .../launcher3/allapps/FloatingHeaderView.java | 8 +- .../launcher3/allapps/WorkProfileManager.java | 18 +- .../allapps/search/SearchAdapterProvider.java | 7 - 7 files changed, 902 insertions(+), 1048 deletions(-) delete mode 100644 src/com/android/launcher3/allapps/BaseAllAppsContainerView.java diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index de5419eb7b..5b21fb8260 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -132,7 +132,6 @@ import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.allapps.AllAppsRecyclerView; import com.android.launcher3.allapps.AllAppsStore; import com.android.launcher3.allapps.AllAppsTransitionController; -import com.android.launcher3.allapps.BaseAllAppsContainerView; import com.android.launcher3.allapps.BaseSearchConfig; import com.android.launcher3.allapps.DiscoveryBounce; import com.android.launcher3.anim.PropertyListBuilder; @@ -1642,7 +1641,7 @@ public class Launcher extends StatefulActivity private void showAllAppsWorkTabFromIntent(boolean alreadyOnHome) { showAllAppsFromIntent(alreadyOnHome); - mAppsView.switchToTab(BaseAllAppsContainerView.AdapterHolder.WORK); + mAppsView.switchToTab(ActivityAllAppsContainerView.AdapterHolder.WORK); } /** diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 2511cada78..8a02a5452e 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -15,25 +15,74 @@ */ package com.android.launcher3.allapps; -import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.SEARCH; +import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Path.Direction; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.Process; +import android.os.UserManager; import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.util.TypedValue; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.WindowInsets; +import android.widget.Button; import android.widget.RelativeLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.graphics.ColorUtils; import androidx.recyclerview.widget.RecyclerView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.Insettable; +import com.android.launcher3.InsettableFrameLayout; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; +import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider; +import com.android.launcher3.allapps.search.SearchAdapterProvider; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.keyboard.FocusedItemDecorator; +import com.android.launcher3.model.StringCache; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.util.ItemInfoMatcher; +import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.BaseDragLayer; +import com.android.launcher3.views.RecyclerViewFastScroller; +import com.android.launcher3.views.ScrimView; +import com.android.launcher3.views.SpringRelativeLayout; +import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; /** * All apps container view with search support for use in a dragging activity. @@ -41,16 +90,67 @@ import java.util.ArrayList; * @param Type of context inflating all apps. */ public class ActivityAllAppsContainerView - extends BaseAllAppsContainerView { + extends SpringRelativeLayout implements DragSource, Insettable, + OnDeviceProfileChangeListener, PersonalWorkSlidingTabStrip.OnActivePageChangedListener, + ScrimView.ScrimDrawingController { + public static final float PULL_MULTIPLIER = .02f; + public static final float FLING_VELOCITY_MULTIPLIER = 1200f; + protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page"; private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300; + // Render the header protection at all times to debug clipping issues. + private static final boolean DEBUG_HEADER_PROTECTION = false; + /** Context of an activity or window that is inflating this container. */ + + protected final T mActivityContext; + protected final List mAH; + protected final Predicate mPersonalMatcher = ItemInfoMatcher.ofUser( + Process.myUserHandle()); + protected final WorkProfileManager mWorkManager; + protected final Point mFastScrollerOffset = new Point(); + protected final int mScrimColor; + protected final float mHeaderThreshold; // Used to animate Search results out and A-Z apps in, or vice-versa. private final SearchTransitionController mSearchTransitionController; + private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Rect mInsets = new Rect(); + private final AllAppsStore mAllAppsStore = new AllAppsStore(); + private final RecyclerView.OnScrollListener mScrollListener = + new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + updateHeaderScroll(recyclerView.computeVerticalScrollOffset()); + } + }; + private final Paint mNavBarScrimPaint; + private final int mHeaderProtectionColor; + private final Path mTmpPath = new Path(); + private final RectF mTmpRectF = new RectF(); + protected AllAppsPagedView mViewPager; + protected FloatingHeaderView mHeader; + protected View mBottomSheetBackground; + /** + * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}. + */ + protected View mSearchContainer; + protected SearchUiManager mSearchUiManager; + protected boolean mUsingTabs; + protected RecyclerViewFastScroller mTouchHandler; /** {@code true} when rendered view is in search state instead of the scroll state. */ private boolean mIsSearching; private boolean mRebindAdaptersAfterSearchAnimation; + private int mNavBarScrimHeight = 0; + private SearchRecyclerView mSearchRecyclerView; + private SearchAdapterProvider mMainAdapterProvider; + private View mBottomSheetHandleArea; + private boolean mHasWorkApps; + private float[] mBottomSheetCornerRadii; + private ScrimView mScrimView; + private int mHeaderColor; + private int mBottomSheetBackgroundColor; + private int mTabsProtectionAlpha; public ActivityAllAppsContainerView(Context context) { this(context, null); @@ -62,13 +162,84 @@ public class ActivityAllAppsContainerView public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + mActivityContext = ActivityContext.lookupContext(context); + + mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor); + mHeaderThreshold = getResources().getDimensionPixelSize( + R.dimen.dynamic_grid_cell_border_spacing); + mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor); + + mWorkManager = new WorkProfileManager( + mActivityContext.getSystemService(UserManager.class), + this, mActivityContext.getStatsLogManager()); + mAH = Arrays.asList(null, null, null); + mNavBarScrimPaint = new Paint(); + mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor)); + + mAllAppsStore.addUpdateListener(this::onAppsUpdated); + mActivityContext.addOnDeviceProfileChangeListener(this); + + // This is a focus listener that proxies focus from a view into the list view. This is to + // work around the search box from getting first focus and showing the cursor. + setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus && getActiveRecyclerView() != null) { + getActiveRecyclerView().requestFocus(); + } + }); + initContent(); mSearchTransitionController = new SearchTransitionController(this); } + /** + * Initializes the view hierarchy and internal variables. Any initialization which actually uses + * these members should be done in {@link #onFinishInflate()}. + * In terms of subclass initialization, the following would be parallel order for activity: + * initContent -> onPreCreate + * constructor/init -> onCreate + * onFinishInflate -> onPostCreate + */ + protected void initContent() { + mMainAdapterProvider = createMainAdapterProvider(); + + mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN)); + mAH.set(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); + mAH.set(SEARCH, new AdapterHolder(SEARCH)); + + getLayoutInflater().inflate(R.layout.all_apps_content, this); + mHeader = findViewById(R.id.all_apps_header); + mBottomSheetBackground = findViewById(R.id.bottom_sheet_background); + mBottomSheetHandleArea = findViewById(R.id.bottom_sheet_handle_area); + mSearchRecyclerView = findViewById(R.id.search_results_list_view); + + // Add the search box next to the header + mSearchContainer = inflateSearchBox(); + addView(mSearchContainer, indexOfChild(mHeader) + 1); + mSearchUiManager = (SearchUiManager) mSearchContainer; + } + @Override protected void onFinishInflate() { super.onFinishInflate(); + + mAH.get(SEARCH).setup(mSearchRecyclerView, + /* Filter out A-Z apps */ itemInfo -> false); + rebindAdapters(true /* force */); + float cornerRadius = Themes.getDialogCornerRadius(getContext()); + mBottomSheetCornerRadii = new float[]{ + cornerRadius, + cornerRadius, // Top left radius in px + cornerRadius, + cornerRadius, // Top right radius in px + 0, + 0, // Bottom right + 0, + 0 // Bottom left + }; + final TypedValue value = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.colorBackground, value, true); + mBottomSheetBackgroundColor = value.data; + updateBackground(mActivityContext.getDeviceProfile()); mSearchUiManager.initializeSearch(this); } @@ -141,19 +312,43 @@ public class ActivityAllAppsContainerView }); } - @Override public boolean shouldContainerScroll(MotionEvent ev) { + BaseDragLayer dragLayer = mActivityContext.getDragLayer(); // IF the MotionEvent is inside the search box, and the container keeps on receiving // touch input, container should move down. - if (mActivityContext.getDragLayer().isEventOverView(mSearchContainer, ev)) { + if (dragLayer.isEventOverView(mSearchContainer, ev)) { return true; } - return super.shouldContainerScroll(ev); + // Scroll if not within the container view (e.g. over large-screen scrim). + if (!dragLayer.isEventOverView(getVisibleContainerView(), ev)) { + return true; + } + if (dragLayer.isEventOverView(mBottomSheetHandleArea, ev)) { + return true; + } + AllAppsRecyclerView rv = getActiveRecyclerView(); + if (rv == null) { + return true; + } + if (rv.getScrollbar() != null + && rv.getScrollbar().getThumbOffsetY() >= 0 + && dragLayer.isEventOverView(rv.getScrollbar(), ev)) { + return false; + } + return rv.shouldContainerScroll(ev, dragLayer); } - @Override public void reset(boolean animate) { - super.reset(animate); + for (int i = 0; i < mAH.size(); i++) { + if (mAH.get(i).mRecyclerView != null) { + mAH.get(i).mRecyclerView.scrollToTop(); + } + } + if (isHeaderVisible()) { + mHeader.reset(animate); + } + // Reset the base recycler view after transitioning home. + updateHeaderScroll(0); // Reset the search bar after transitioning home. mSearchUiManager.resetSearch(); // Animate to A-Z with 0 time to reset the animation with proper state management. @@ -166,16 +361,26 @@ public class ActivityAllAppsContainerView return super.dispatchKeyEvent(event); } - @Override public String getDescription() { if (!mUsingTabs && isSearching()) { return getContext().getString(R.string.all_apps_search_results); } else { - return super.getDescription(); + StringCache cache = mActivityContext.getStringCache(); + if (mUsingTabs) { + if (cache != null) { + return isPersonalTab() + ? cache.allAppsPersonalTabAccessibility + : cache.allAppsWorkTabAccessibility; + } else { + return isPersonalTab() + ? getContext().getString(R.string.all_apps_button_personal_label) + : getContext().getString(R.string.all_apps_button_work_label); + } + } + return getContext().getString(R.string.all_apps_button_label); } } - @Override public boolean isSearching() { return mIsSearching; } @@ -186,30 +391,112 @@ public class ActivityAllAppsContainerView // Will be called at the end of the animation. return; } - super.onActivePageChanged(currentActivePage); + if (mAH.get(currentActivePage).mRecyclerView != null) { + mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(); + } + // Header keeps track of active recycler view to properly render header protection. + mHeader.setActiveRV(currentActivePage); + reset(true /* animate */); + + mWorkManager.onActivePageChanged(currentActivePage); + } + + protected void rebindAdapters() { + rebindAdapters(false /* force */); } - @Override protected void rebindAdapters(boolean force) { if (mSearchTransitionController.isRunning()) { mRebindAdaptersAfterSearchAnimation = true; return; } - super.rebindAdapters(force); - if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get() - || getMainAdapterProvider().getDecorator() == null - || getSearchRecyclerView() == null) { + updateSearchResultsVisibility(); + + boolean showTabs = shouldShowTabs(); + if (showTabs == mUsingTabs && !force) { return; } - RecyclerView.ItemDecoration decoration = getMainAdapterProvider().getDecorator(); - getSearchRecyclerView().removeItemDecoration(decoration); // In case it is already added. - getSearchRecyclerView().addItemDecoration(decoration); + if (isSearching()) { + mUsingTabs = showTabs; + mWorkManager.detachWorkModeSwitch(); + return; + } + + // replaceAppsRVcontainer() needs to use both mUsingTabs value to remove the old view AND + // showTabs value to create new view. Hence the mUsingTabs new value assignment MUST happen + // after this call. + replaceAppsRVContainer(showTabs); + mUsingTabs = showTabs; + + mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); + mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); + mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); + + if (mUsingTabs) { + mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher); + mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher()); + mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work); + if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) { + mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener( + mWorkManager.newScrollListener()); + } + mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN); + findViewById(R.id.tab_personal) + .setOnClickListener((View view) -> { + if (mViewPager.snapToPage(AdapterHolder.MAIN)) { + mActivityContext.getStatsLogManager().logger() + .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB); + } + mActivityContext.hideKeyboard(); + }); + findViewById(R.id.tab_work) + .setOnClickListener((View view) -> { + if (mViewPager.snapToPage(AdapterHolder.WORK)) { + mActivityContext.getStatsLogManager().logger() + .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB); + } + mActivityContext.hideKeyboard(); + }); + setDeviceManagementResources(); + onActivePageChanged(mViewPager.getNextPage()); + } else { + mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null); + mAH.get(AdapterHolder.WORK).mRecyclerView = null; + } + setupHeader(); + + mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); + mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); + mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); } - @Override protected View replaceAppsRVContainer(boolean showTabs) { - View rvContainer = super.replaceAppsRVContainer(showTabs); + for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) { + AdapterHolder adapterHolder = mAH.get(i); + if (adapterHolder.mRecyclerView != null) { + adapterHolder.mRecyclerView.setLayoutManager(null); + adapterHolder.mRecyclerView.setAdapter(null); + } + } + View oldView = getAppsRecyclerViewContainer(); + int index = indexOfChild(oldView); + removeView(oldView); + int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout; + final View rvContainer = getLayoutInflater().inflate(layout, this, false); + addView(rvContainer, index); + if (showTabs) { + mViewPager = (AllAppsPagedView) rvContainer; + mViewPager.initParentViews(this); + mViewPager.getPageIndicator().setOnActivePageChangedListener(this); + + mWorkManager.reset(); + post(() -> mAH.get(AdapterHolder.WORK).applyPadding()); + + } else { + mWorkManager.detachWorkModeSwitch(); + mViewPager = null; + } removeCustomRules(rvContainer); removeCustomRules(getSearchRecyclerView()); @@ -228,9 +515,24 @@ public class ActivityAllAppsContainerView return rvContainer; } - @Override void setupHeader() { - super.setupHeader(); + mHeader.setVisibility(View.VISIBLE); + boolean tabsHidden = !mUsingTabs; + mHeader.setup( + mAH.get(AdapterHolder.MAIN).mRecyclerView, + mAH.get(AdapterHolder.WORK).mRecyclerView, + (SearchRecyclerView) mAH.get(SEARCH).mRecyclerView, + getCurrentPage(), + tabsHidden); + + int padding = mHeader.getMaxTranslation(); + mAH.forEach(adapterHolder -> { + adapterHolder.mPadding.top = padding; + adapterHolder.applyPadding(); + if (adapterHolder.mRecyclerView != null) { + adapterHolder.mRecyclerView.scrollToTop(); + } + }); removeCustomRules(mHeader); if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) { @@ -240,9 +542,18 @@ public class ActivityAllAppsContainerView } } - @Override protected void updateHeaderScroll(int scrolledOffset) { - super.updateHeaderScroll(scrolledOffset); + float prog1 = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); + int headerColor = getHeaderColor(prog1); + int tabsAlpha = mHeader.getPeripheralProtectionHeight() == 0 ? 0 + : (int) (Utilities.boundToRange( + (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f) + * 255); + if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) { + mHeaderColor = headerColor; + mTabsProtectionAlpha = tabsAlpha; + invalidateHeader(); + } if (mSearchUiManager.getEditText() == null) { return; } @@ -257,10 +568,9 @@ public class ActivityAllAppsContainerView mSearchUiManager.setBackgroundVisibility(bgVisible, 1 - prog); } - @Override protected int getHeaderColor(float blendRatio) { return ColorUtils.setAlphaComponent( - super.getHeaderColor(blendRatio), + ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, blendRatio), (int) (mSearchContainer.getAlpha() * 255)); } @@ -315,7 +625,6 @@ public class ActivityAllAppsContainerView layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_TOP); } - @Override protected BaseAllAppsAdapter createAdapter(AlphabeticalAppsList appsList, BaseAdapterProvider[] adapterProviders) { return new AllAppsGridAdapter<>(mActivityContext, getLayoutInflater(), appsList, @@ -339,9 +648,558 @@ public class ActivityAllAppsContainerView : R.dimen.all_apps_header_top_margin); } - @Override public boolean isInAllApps() { // TODO: Make this abstract return true; } + + /** + * Inflates the search box + */ + protected View inflateSearchBox() { + return getLayoutInflater().inflate(R.layout.search_container_all_apps, this, false); + } + + /** Creates the adapter provider for the main section. */ + protected SearchAdapterProvider createMainAdapterProvider() { + return new DefaultSearchAdapterProvider(mActivityContext); + } + + /** The adapter provider for the main section. */ + public final SearchAdapterProvider getMainAdapterProvider() { + return mMainAdapterProvider; + } + + @Override + protected void dispatchRestoreInstanceState(SparseArray sparseArray) { + try { + // Many slice view id is not properly assigned, and hence throws null + // pointer exception in the underneath method. Catching the exception + // simply doesn't restore these slice views. This doesn't have any + // user visible effect because because we query them again. + super.dispatchRestoreInstanceState(sparseArray); + } catch (Exception e) { + Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e); + } + + Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null); + if (state != null) { + int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0); + if (currentPage == AdapterHolder.WORK && mViewPager != null) { + mViewPager.setCurrentPage(currentPage); + rebindAdapters(); + } else { + reset(true); + } + } + } + + @Override + protected void dispatchSaveInstanceState(SparseArray container) { + super.dispatchSaveInstanceState(container); + Bundle state = new Bundle(); + state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage()); + container.put(R.id.work_tab_state_id, state); + } + + /** + * Sets the long click listener for icons + */ + public void setOnIconLongClickListener(OnLongClickListener listener) { + for (AdapterHolder holder : mAH) { + holder.mAdapter.setOnIconLongClickListener(listener); + } + } + + public AllAppsStore getAppsStore() { + return mAllAppsStore; + } + + public WorkProfileManager getWorkManager() { + return mWorkManager; + } + + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + for (AdapterHolder holder : mAH) { + holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns); + if (holder.mRecyclerView != null) { + // Remove all views and clear the pool, while keeping the data same. After this + // call, all the viewHolders will be recreated. + holder.mRecyclerView.swapAdapter(holder.mRecyclerView.getAdapter(), true); + holder.mRecyclerView.getRecycledViewPool().clear(); + } + } + updateBackground(dp); + } + + protected void updateBackground(DeviceProfile deviceProfile) { + mBottomSheetBackground.setVisibility(deviceProfile.isTablet ? View.VISIBLE : View.GONE); + // Note: For tablets, the opaque background and header protection are added in drawOnScrim. + // For the taskbar entrypoint, the scrim is drawn differently, so a static background is + // added in TaskbarAllAppsContainerView and header protection is not yet supported. + } + + private void onAppsUpdated() { + mHasWorkApps = Stream.of(mAllAppsStore.getApps()).anyMatch(mWorkManager.getMatcher()); + if (!isSearching()) { + rebindAdapters(); + if (mHasWorkApps) { + mWorkManager.reset(); + } + } + + mActivityContext.getStatsLogManager().logger() + .withCardinality(mAllAppsStore.getApps().length) + .log(LAUNCHER_ALLAPPS_COUNT); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // The AllAppsContainerView houses the QSB and is hence visible from the Workspace + // Overview states. We shouldn't intercept for the scrubber in these cases. + if (!isInAllApps()) { + mTouchHandler = null; + return false; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + AllAppsRecyclerView rv = getActiveRecyclerView(); + if (rv != null && rv.getScrollbar() != null + && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { + mTouchHandler = rv.getScrollbar(); + } else { + mTouchHandler = null; + } + } + if (mTouchHandler != null) { + return mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!isInAllApps()) { + return false; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + AllAppsRecyclerView rv = getActiveRecyclerView(); + if (rv != null && rv.getScrollbar() != null + && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { + mTouchHandler = rv.getScrollbar(); + } else { + mTouchHandler = null; + + } + } + if (mTouchHandler != null) { + mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); + return true; + } + if (isSearching() + && mActivityContext.getDragLayer().isEventOverView(getVisibleContainerView(), ev)) { + // if in search state, consume touch event. + return true; + } + return false; + } + + /** The current active recycler view (A-Z list from one of the profiles, or search results). */ + public AllAppsRecyclerView getActiveRecyclerView() { + if (isSearching()) { + return getSearchRecyclerView(); + } + return getActiveAppsRecyclerView(); + } + + /** The current apps recycler view in the container. */ + private AllAppsRecyclerView getActiveAppsRecyclerView() { + if (!mUsingTabs || isPersonalTab()) { + return mAH.get(AdapterHolder.MAIN).mRecyclerView; + } else { + return mAH.get(AdapterHolder.WORK).mRecyclerView; + } + } + + /** + * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently + * hidden while searching. + **/ + protected View getAppsRecyclerViewContainer() { + return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view); + } + + /** The RV for search results, which is hidden while A-Z apps are visible. */ + public SearchRecyclerView getSearchRecyclerView() { + return mSearchRecyclerView; + } + + protected boolean isPersonalTab() { + return mViewPager == null || mViewPager.getNextPage() == 0; + } + + /** + * Switches the current page to the provided {@code tab} if tabs are supported, otherwise does + * nothing. + */ + public void switchToTab(int tab) { + if (mUsingTabs) { + mViewPager.setCurrentPage(tab); + } + } + + public LayoutInflater getLayoutInflater() { + return LayoutInflater.from(getContext()); + } + + @Override + public void onDropCompleted(View target, DragObject d, boolean success) {} + + @Override + public void setInsets(Rect insets) { + mInsets.set(insets); + DeviceProfile grid = mActivityContext.getDeviceProfile(); + + applyAdapterSideAndBottomPaddings(grid); + + MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); + mlp.leftMargin = insets.left; + mlp.rightMargin = insets.right; + setLayoutParams(mlp); + + if (grid.isVerticalBarLayout()) { + setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0); + } else { + int topPadding = grid.allAppsTopPadding; + if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() && !grid.isTablet) { + topPadding += getResources().getDimensionPixelSize( + R.dimen.all_apps_additional_top_padding_floating_search); + } + setPadding(grid.allAppsLeftRightMargin, topPadding, grid.allAppsLeftRightMargin, 0); + } + + InsettableFrameLayout.dispatchInsets(this, insets); + } + + /** + * Returns a padding in case a scrim is shown on the bottom of the view and a padding is needed. + */ + protected int computeNavBarScrimHeight(WindowInsets insets) { + return 0; + } + + /** + * Returns the current height of nav bar scrim + */ + public int getNavBarScrimHeight() { + return mNavBarScrimHeight; + } + + @Override + public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { + mNavBarScrimHeight = computeNavBarScrimHeight(insets); + applyAdapterSideAndBottomPaddings(mActivityContext.getDeviceProfile()); + return super.dispatchApplyWindowInsets(insets); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mNavBarScrimHeight > 0) { + canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), + mNavBarScrimPaint); + } + } + + protected void updateSearchResultsVisibility() { + if (isSearching()) { + getSearchRecyclerView().setVisibility(VISIBLE); + getAppsRecyclerViewContainer().setVisibility(GONE); + mHeader.setVisibility(GONE); + } else { + getSearchRecyclerView().setVisibility(GONE); + getAppsRecyclerViewContainer().setVisibility(VISIBLE); + mHeader.setVisibility(VISIBLE); + } + if (mHeader.isSetUp()) { + mHeader.setActiveRV(getCurrentPage()); + } + } + + private void applyAdapterSideAndBottomPaddings(DeviceProfile grid) { + int bottomPadding = Math.max(mInsets.bottom, mNavBarScrimHeight); + mAH.forEach(adapterHolder -> { + adapterHolder.mPadding.bottom = bottomPadding; + adapterHolder.mPadding.left = + adapterHolder.mPadding.right = grid.allAppsLeftRightPadding; + adapterHolder.applyPadding(); + }); + } + + private void setDeviceManagementResources() { + if (mActivityContext.getStringCache() != null) { + Button personalTab = findViewById(R.id.tab_personal); + personalTab.setText(mActivityContext.getStringCache().allAppsPersonalTab); + + Button workTab = findViewById(R.id.tab_work); + workTab.setText(mActivityContext.getStringCache().allAppsWorkTab); + } + } + + protected boolean shouldShowTabs() { + return mHasWorkApps; + } + + // Used by tests only + private boolean isDescendantViewVisible(int viewId) { + final View view = findViewById(viewId); + if (view == null) return false; + + if (!view.isShown()) return false; + + return view.getGlobalVisibleRect(new Rect()); + } + + @VisibleForTesting + public boolean isPersonalTabVisible() { + return isDescendantViewVisible(R.id.tab_personal); + } + + @VisibleForTesting + public boolean isWorkTabVisible() { + return isDescendantViewVisible(R.id.tab_work); + } + + public AlphabeticalAppsList getSearchResultList() { + return mAH.get(SEARCH).mAppsList; + } + + public FloatingHeaderView getFloatingHeaderView() { + return mHeader; + } + + @VisibleForTesting + public View getContentView() { + return isSearching() ? getSearchRecyclerView() : getAppsRecyclerViewContainer(); + } + + /** The current page visible in all apps. */ + public int getCurrentPage() { + return isSearching() + ? SEARCH + : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage(); + } + + /** The scroll bar for the active apps recycler view. */ + public RecyclerViewFastScroller getScrollBar() { + AllAppsRecyclerView rv = getActiveAppsRecyclerView(); + return rv == null ? null : rv.getScrollbar(); + } + + public boolean isHeaderVisible() { + return mHeader != null && mHeader.getVisibility() == View.VISIBLE; + } + + /** + * Adds an update listener to animator that adds springs to the animation. + */ + public void addSpringFromFlingUpdateListener(ValueAnimator animator, + float velocity /* release velocity */, + float progress /* portion of the distance to travel*/) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + float distance = (1 - progress) * getHeight(); // px + float settleVelocity = Math.min(0, distance + / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration()) + + velocity); + absorbSwipeUpVelocity(Math.max(1000, Math.abs( + Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER)))); + } + }); + } + + /** Invoked when the container is pulled. */ + public void onPull(float deltaDistance, float displacement) { + absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement); + // Current motion spec is to actually push and not pull + // on this surface. However, until EdgeEffect.onPush (b/190612804) is + // implemented at view level, we will simply pull + } + + @Override + public void getDrawingRect(Rect outRect) { + super.getDrawingRect(outRect); + outRect.offset(0, (int) getTranslationY()); + } + + @Override + public void setTranslationY(float translationY) { + super.setTranslationY(translationY); + invalidateHeader(); + } + + public void setScrimView(ScrimView scrimView) { + mScrimView = scrimView; + } + + @Override + public void drawOnScrimWithScale(Canvas canvas, float scale) { + boolean isTablet = mActivityContext.getDeviceProfile().isTablet; + + // Draw full background panel for tablets. + if (isTablet) { + mHeaderPaint.setColor(mBottomSheetBackgroundColor); + View panel = (View) mBottomSheetBackground; + float translationY = ((View) panel.getParent()).getTranslationY(); + mTmpRectF.set(panel.getLeft(), panel.getTop() + translationY, + panel.getRight(), panel.getBottom()); + mTmpPath.reset(); + mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); + canvas.drawPath(mTmpPath, mHeaderPaint); + } + + if (DEBUG_HEADER_PROTECTION) { + mHeaderPaint.setColor(Color.MAGENTA); + mHeaderPaint.setAlpha(255); + } else { + mHeaderPaint.setColor(mHeaderColor); + mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor))); + } + if (mHeaderPaint.getColor() == mScrimColor || mHeaderPaint.getColor() == 0) { + return; + } + final float offset = (getVisibleContainerView().getHeight() * (1 - scale) / 2); + final float bottom = + scale * (getHeaderBottom() + getVisibleContainerView().getPaddingTop()) + offset; + FloatingHeaderView headerView = getFloatingHeaderView(); + if (isTablet) { + // Start adding header protection if search bar or tabs will attach to the top. + if (!FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() || mUsingTabs) { + View panel = (View) mBottomSheetBackground; + float translationY = ((View) panel.getParent()).getTranslationY(); + mTmpRectF.set(panel.getLeft(), panel.getTop() + translationY, panel.getRight(), + bottom); + mTmpPath.reset(); + mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); + canvas.drawPath(mTmpPath, mHeaderPaint); + } + } else { + canvas.drawRect(0, 0, canvas.getWidth(), bottom, mHeaderPaint); + } + int tabsHeight = headerView.getPeripheralProtectionHeight(); + if (mTabsProtectionAlpha > 0 && tabsHeight != 0) { + if (DEBUG_HEADER_PROTECTION) { + mHeaderPaint.setColor(Color.BLUE); + mHeaderPaint.setAlpha(255); + } else { + mHeaderPaint.setAlpha((int) (getAlpha() * mTabsProtectionAlpha)); + } + int left = 0; + int right = canvas.getWidth(); + if (isTablet) { + left = mBottomSheetBackground.getLeft(); + right = mBottomSheetBackground.getRight(); + } + canvas.drawRect(left, bottom, right, bottom + tabsHeight, mHeaderPaint); + } + } + + /** + * redraws header protection + */ + public void invalidateHeader() { + if (mScrimView != null) { + mScrimView.invalidate(); + } + } + + /** Returns the position of the bottom edge of the header */ + public int getHeaderBottom() { + int bottom = (int) getTranslationY() + mHeader.getClipTop(); + if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) { + if (mActivityContext.getDeviceProfile().isTablet) { + return bottom + mBottomSheetBackground.getTop(); + } + return bottom; + } + return bottom + mHeader.getTop(); + } + + /** + * Returns a view that denotes the visible part of all apps container view. + */ + public View getVisibleContainerView() { + return mActivityContext.getDeviceProfile().isTablet ? mBottomSheetBackground : this; + } + + protected void onInitializeRecyclerView(RecyclerView rv) { + rv.addOnScrollListener(mScrollListener); + } + + /** Holds a {@link BaseAllAppsAdapter} and related fields. */ + public class AdapterHolder { + public static final int MAIN = 0; + public static final int WORK = 1; + public static final int SEARCH = 2; + + private final int mType; + public final BaseAllAppsAdapter mAdapter; + final RecyclerView.LayoutManager mLayoutManager; + final AlphabeticalAppsList mAppsList; + final Rect mPadding = new Rect(); + AllAppsRecyclerView mRecyclerView; + + AdapterHolder(int type) { + mType = type; + mAppsList = new AlphabeticalAppsList<>(mActivityContext, + isSearch() ? null : mAllAppsStore, + isWork() ? mWorkManager : null); + BaseAdapterProvider[] adapterProviders = + new BaseAdapterProvider[]{mMainAdapterProvider}; + + mAdapter = createAdapter(mAppsList, adapterProviders); + mAppsList.setAdapter(mAdapter); + mLayoutManager = mAdapter.getLayoutManager(); + } + + void setup(@NonNull View rv, @Nullable Predicate matcher) { + mAppsList.updateItemFilter(matcher); + mRecyclerView = (AllAppsRecyclerView) rv; + mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory()); + mRecyclerView.setApps(mAppsList); + mRecyclerView.setLayoutManager(mLayoutManager); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.setHasFixedSize(true); + // No animations will occur when changes occur to the items in this RecyclerView. + mRecyclerView.setItemAnimator(null); + onInitializeRecyclerView(mRecyclerView); + FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView); + mRecyclerView.addItemDecoration(focusedItemDecorator); + mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); + applyPadding(); + } + + void applyPadding() { + if (mRecyclerView != null) { + int bottomOffset = 0; + if (isWork() && mWorkManager.getWorkModeSwitch() != null) { + bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight(); + } + mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right, + mPadding.bottom + bottomOffset); + } + } + + private boolean isWork() { + return mType == WORK; + } + + private boolean isSearch() { + return mType == SEARCH; + } + } } diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 8cb31fa2e1..7789191370 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -299,7 +299,7 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView { return; } else if (appsView.mViewPager != null) { int currentPage = appsView.mViewPager.getCurrentPage(); - if (currentPage == BaseAllAppsContainerView.AdapterHolder.WORK) { + if (currentPage == ActivityAllAppsContainerView.AdapterHolder.WORK) { // In work A-Z list mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) ? LAUNCHER_WORK_FAB_BUTTON_COLLAPSE diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java deleted file mode 100644 index 9bb825084b..0000000000 --- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java +++ /dev/null @@ -1,996 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.allapps; - -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.Path.Direction; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.RectF; -import android.os.Bundle; -import android.os.Parcelable; -import android.os.Process; -import android.os.UserManager; -import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseArray; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.WindowInsets; -import android.widget.Button; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.core.graphics.ColorUtils; -import androidx.recyclerview.widget.RecyclerView; - -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; -import com.android.launcher3.DragSource; -import com.android.launcher3.DropTarget.DragObject; -import com.android.launcher3.Insettable; -import com.android.launcher3.InsettableFrameLayout; -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider; -import com.android.launcher3.allapps.search.SearchAdapterProvider; -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.keyboard.FocusedItemDecorator; -import com.android.launcher3.model.StringCache; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.util.Themes; -import com.android.launcher3.views.ActivityContext; -import com.android.launcher3.views.BaseDragLayer; -import com.android.launcher3.views.RecyclerViewFastScroller; -import com.android.launcher3.views.ScrimView; -import com.android.launcher3.views.SpringRelativeLayout; -import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; - -import java.util.Arrays; -import java.util.List; -import java.util.function.Predicate; -import java.util.stream.Stream; - -/** - * Base all apps view container. - * - * @param Type of context inflating all apps. - */ -public abstract class BaseAllAppsContainerView - extends SpringRelativeLayout implements DragSource, Insettable, - OnDeviceProfileChangeListener, OnActivePageChangedListener, - ScrimView.ScrimDrawingController { - - protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page"; - - public static final float PULL_MULTIPLIER = .02f; - public static final float FLING_VELOCITY_MULTIPLIER = 1200f; - - // Render the header protection at all times to debug clipping issues. - private static final boolean DEBUG_HEADER_PROTECTION = false; - - private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Rect mInsets = new Rect(); - - /** Context of an activity or window that is inflating this container. */ - protected final T mActivityContext; - protected final List mAH; - protected final Predicate mPersonalMatcher = ItemInfoMatcher.ofUser( - Process.myUserHandle()); - private final AllAppsStore mAllAppsStore = new AllAppsStore(); - - private final RecyclerView.OnScrollListener mScrollListener = - new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - updateHeaderScroll(recyclerView.computeVerticalScrollOffset()); - } - }; - - protected final WorkProfileManager mWorkManager; - - private final Paint mNavBarScrimPaint; - private int mNavBarScrimHeight = 0; - - protected AllAppsPagedView mViewPager; - private SearchRecyclerView mSearchRecyclerView; - private SearchAdapterProvider mMainAdapterProvider; - - protected FloatingHeaderView mHeader; - protected View mBottomSheetBackground; - private View mBottomSheetHandleArea; - - /** - * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}. - */ - protected View mSearchContainer; - protected SearchUiManager mSearchUiManager; - - protected boolean mUsingTabs; - private boolean mHasWorkApps; - - protected RecyclerViewFastScroller mTouchHandler; - protected final Point mFastScrollerOffset = new Point(); - - protected final int mScrimColor; - private final int mHeaderProtectionColor; - protected final float mHeaderThreshold; - private final Path mTmpPath = new Path(); - private final RectF mTmpRectF = new RectF(); - private float[] mBottomSheetCornerRadii; - private ScrimView mScrimView; - private int mHeaderColor; - private int mBottomSheetBackgroundColor; - private int mTabsProtectionAlpha; - - protected BaseAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - mActivityContext = ActivityContext.lookupContext(context); - - mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor); - mHeaderThreshold = getResources().getDimensionPixelSize( - R.dimen.dynamic_grid_cell_border_spacing); - mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor); - - mWorkManager = new WorkProfileManager(mActivityContext.getSystemService(UserManager.class), - this, mActivityContext.getStatsLogManager()); - mAH = Arrays.asList(null, null, null); - mNavBarScrimPaint = new Paint(); - mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor)); - - mAllAppsStore.addUpdateListener(this::onAppsUpdated); - mActivityContext.addOnDeviceProfileChangeListener(this); - - // This is a focus listener that proxies focus from a view into the list view. This is to - // work around the search box from getting first focus and showing the cursor. - setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus && getActiveRecyclerView() != null) { - getActiveRecyclerView().requestFocus(); - } - }); - initContent(); - } - - /** - * Initializes the view hierarchy and internal variables. Any initialization which actually uses - * these members should be done in {@link #onFinishInflate()}. - * In terms of subclass initialization, the following would be parallel order for activity: - * initContent -> onPreCreate - * constructor/init -> onCreate - * onFinishInflate -> onPostCreate - */ - protected void initContent() { - mMainAdapterProvider = createMainAdapterProvider(); - - mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN)); - mAH.set(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); - mAH.set(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); - - getLayoutInflater().inflate(R.layout.all_apps_content, this); - mHeader = findViewById(R.id.all_apps_header); - mBottomSheetBackground = findViewById(R.id.bottom_sheet_background); - mBottomSheetHandleArea = findViewById(R.id.bottom_sheet_handle_area); - mSearchRecyclerView = findViewById(R.id.search_results_list_view); - - // Add the search box next to the header - mSearchContainer = inflateSearchBox(); - addView(mSearchContainer, indexOfChild(mHeader) + 1); - mSearchUiManager = (SearchUiManager) mSearchContainer; - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - mAH.get(AdapterHolder.SEARCH).setup(mSearchRecyclerView, - /* Filter out A-Z apps */ itemInfo -> false); - rebindAdapters(true /* force */); - float cornerRadius = Themes.getDialogCornerRadius(getContext()); - mBottomSheetCornerRadii = new float[]{ - cornerRadius, - cornerRadius, // Top left radius in px - cornerRadius, - cornerRadius, // Top right radius in px - 0, - 0, // Bottom right - 0, - 0 // Bottom left - }; - final TypedValue value = new TypedValue(); - getContext().getTheme().resolveAttribute(android.R.attr.colorBackground, value, true); - mBottomSheetBackgroundColor = value.data; - updateBackground(mActivityContext.getDeviceProfile()); - } - - /** - * Inflates the search box - */ - protected View inflateSearchBox() { - return getLayoutInflater().inflate(R.layout.search_container_all_apps, this, false); - } - - /** Creates the adapter provider for the main section. */ - protected SearchAdapterProvider createMainAdapterProvider() { - return new DefaultSearchAdapterProvider(mActivityContext); - } - - /** The adapter provider for the main section. */ - public final SearchAdapterProvider getMainAdapterProvider() { - return mMainAdapterProvider; - } - - @Override - protected void dispatchRestoreInstanceState(SparseArray sparseArray) { - try { - // Many slice view id is not properly assigned, and hence throws null - // pointer exception in the underneath method. Catching the exception - // simply doesn't restore these slice views. This doesn't have any - // user visible effect because because we query them again. - super.dispatchRestoreInstanceState(sparseArray); - } catch (Exception e) { - Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e); - } - - Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null); - if (state != null) { - int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0); - if (currentPage == AdapterHolder.WORK && mViewPager != null) { - mViewPager.setCurrentPage(currentPage); - rebindAdapters(); - } else { - reset(true); - } - } - } - - @Override - protected void dispatchSaveInstanceState(SparseArray container) { - super.dispatchSaveInstanceState(container); - Bundle state = new Bundle(); - state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage()); - container.put(R.id.work_tab_state_id, state); - } - - /** - * Sets the long click listener for icons - */ - public void setOnIconLongClickListener(OnLongClickListener listener) { - for (AdapterHolder holder : mAH) { - holder.mAdapter.setOnIconLongClickListener(listener); - } - } - - public AllAppsStore getAppsStore() { - return mAllAppsStore; - } - - public WorkProfileManager getWorkManager() { - return mWorkManager; - } - - @Override - public void onDeviceProfileChanged(DeviceProfile dp) { - for (AdapterHolder holder : mAH) { - holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns); - if (holder.mRecyclerView != null) { - // Remove all views and clear the pool, while keeping the data same. After this - // call, all the viewHolders will be recreated. - holder.mRecyclerView.swapAdapter(holder.mRecyclerView.getAdapter(), true); - holder.mRecyclerView.getRecycledViewPool().clear(); - } - } - updateBackground(dp); - } - - protected void updateBackground(DeviceProfile deviceProfile) { - mBottomSheetBackground.setVisibility(deviceProfile.isTablet ? View.VISIBLE : View.GONE); - // Note: For tablets, the opaque background and header protection are added in drawOnScrim. - // For the taskbar entrypoint, the scrim is drawn differently, so a static background is - // added in TaskbarAllAppsContainerView and header protection is not yet supported. - } - - private void onAppsUpdated() { - mHasWorkApps = Stream.of(mAllAppsStore.getApps()).anyMatch(mWorkManager.getMatcher()); - if (!isSearching()) { - rebindAdapters(); - if (mHasWorkApps) { - mWorkManager.reset(); - } - } - - mActivityContext.getStatsLogManager().logger() - .withCardinality(mAllAppsStore.getApps().length) - .log(LAUNCHER_ALLAPPS_COUNT); - } - - /** - * Returns whether the view itself will handle the touch event or not. - */ - public boolean shouldContainerScroll(MotionEvent ev) { - BaseDragLayer dragLayer = mActivityContext.getDragLayer(); - // Scroll if not within the container view (e.g. over large-screen scrim). - if (!dragLayer.isEventOverView(getVisibleContainerView(), ev)) { - return true; - } - if (dragLayer.isEventOverView(mBottomSheetHandleArea, ev)) { - return true; - } - AllAppsRecyclerView rv = getActiveRecyclerView(); - if (rv == null) { - return true; - } - if (rv.getScrollbar() != null - && rv.getScrollbar().getThumbOffsetY() >= 0 - && dragLayer.isEventOverView(rv.getScrollbar(), ev)) { - return false; - } - return rv.shouldContainerScroll(ev, dragLayer); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - // The AllAppsContainerView houses the QSB and is hence visible from the Workspace - // Overview states. We shouldn't intercept for the scrubber in these cases. - if (!isInAllApps()) { - mTouchHandler = null; - return false; - } - - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - AllAppsRecyclerView rv = getActiveRecyclerView(); - if (rv != null && rv.getScrollbar() != null - && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { - mTouchHandler = rv.getScrollbar(); - } else { - mTouchHandler = null; - } - } - if (mTouchHandler != null) { - return mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); - } - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (!isInAllApps()) { - return false; - } - - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - AllAppsRecyclerView rv = getActiveRecyclerView(); - if (rv != null && rv.getScrollbar() != null - && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) { - mTouchHandler = rv.getScrollbar(); - } else { - mTouchHandler = null; - - } - } - if (mTouchHandler != null) { - mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset); - return true; - } - if (isSearching() - && mActivityContext.getDragLayer().isEventOverView(getVisibleContainerView(), ev)) { - // if in search state, consume touch event. - return true; - } - return false; - } - - /** Description of the container view based on its current state. */ - public String getDescription() { - StringCache cache = mActivityContext.getStringCache(); - if (mUsingTabs) { - if (cache != null) { - return isPersonalTab() - ? cache.allAppsPersonalTabAccessibility - : cache.allAppsWorkTabAccessibility; - } else { - return isPersonalTab() - ? getContext().getString(R.string.all_apps_button_personal_label) - : getContext().getString(R.string.all_apps_button_work_label); - } - } - return getContext().getString(R.string.all_apps_button_label); - } - - /** The current active recycler view (A-Z list from one of the profiles, or search results). */ - public AllAppsRecyclerView getActiveRecyclerView() { - if (isSearching()) { - return getSearchRecyclerView(); - } - return getActiveAppsRecyclerView(); - } - - /** The current apps recycler view in the container. */ - private AllAppsRecyclerView getActiveAppsRecyclerView() { - if (!mUsingTabs || isPersonalTab()) { - return mAH.get(AdapterHolder.MAIN).mRecyclerView; - } else { - return mAH.get(AdapterHolder.WORK).mRecyclerView; - } - } - - /** - * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently - * hidden while searching. - **/ - protected View getAppsRecyclerViewContainer() { - return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view); - } - - /** The RV for search results, which is hidden while A-Z apps are visible. */ - public SearchRecyclerView getSearchRecyclerView() { - return mSearchRecyclerView; - } - - protected boolean isPersonalTab() { - return mViewPager == null || mViewPager.getNextPage() == 0; - } - - /** - * Switches the current page to the provided {@code tab} if tabs are supported, otherwise does - * nothing. - */ - public void switchToTab(int tab) { - if (mUsingTabs) { - mViewPager.setCurrentPage(tab); - } - } - - public LayoutInflater getLayoutInflater() { - return LayoutInflater.from(getContext()); - } - - /** - * Resets the state of AllApps. - */ - public void reset(boolean animate) { - for (int i = 0; i < mAH.size(); i++) { - if (mAH.get(i).mRecyclerView != null) { - mAH.get(i).mRecyclerView.scrollToTop(); - } - } - if (isHeaderVisible()) { - mHeader.reset(animate); - } - // Reset the base recycler view after transitioning home. - updateHeaderScroll(0); - } - - @Override - public void onDropCompleted(View target, DragObject d, boolean success) {} - - @Override - public void setInsets(Rect insets) { - mInsets.set(insets); - DeviceProfile grid = mActivityContext.getDeviceProfile(); - - applyAdapterSideAndBottomPaddings(grid); - - MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); - mlp.leftMargin = insets.left; - mlp.rightMargin = insets.right; - setLayoutParams(mlp); - - if (grid.isVerticalBarLayout()) { - setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0); - } else { - int topPadding = grid.allAppsTopPadding; - if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() && !grid.isTablet) { - topPadding += getResources().getDimensionPixelSize( - R.dimen.all_apps_additional_top_padding_floating_search); - } - setPadding(grid.allAppsLeftRightMargin, topPadding, grid.allAppsLeftRightMargin, 0); - } - - InsettableFrameLayout.dispatchInsets(this, insets); - } - - /** - * Returns a padding in case a scrim is shown on the bottom of the view and a padding is needed. - */ - protected int computeNavBarScrimHeight(WindowInsets insets) { - return 0; - } - - /** - * Returns the current height of nav bar scrim - */ - public int getNavBarScrimHeight() { - return mNavBarScrimHeight; - } - - @Override - public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { - mNavBarScrimHeight = computeNavBarScrimHeight(insets); - applyAdapterSideAndBottomPaddings(mActivityContext.getDeviceProfile()); - return super.dispatchApplyWindowInsets(insets); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - - if (mNavBarScrimHeight > 0) { - canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(), - mNavBarScrimPaint); - } - } - - protected void rebindAdapters() { - rebindAdapters(false /* force */); - } - - protected void rebindAdapters(boolean force) { - updateSearchResultsVisibility(); - - boolean showTabs = shouldShowTabs(); - if (showTabs == mUsingTabs && !force) { - return; - } - - if (isSearching()) { - mUsingTabs = showTabs; - mWorkManager.detachWorkModeSwitch(); - return; - } - - // replaceAppsRVcontainer() needs to use both mUsingTabs value to remove the old view AND - // showTabs value to create new view. Hence the mUsingTabs new value assignment MUST happen - // after this call. - replaceAppsRVContainer(showTabs); - mUsingTabs = showTabs; - - mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); - mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); - mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); - - if (mUsingTabs) { - mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher); - mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher()); - mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work); - if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) { - mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener( - mWorkManager.newScrollListener()); - } - mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN); - findViewById(R.id.tab_personal) - .setOnClickListener((View view) -> { - if (mViewPager.snapToPage(AdapterHolder.MAIN)) { - mActivityContext.getStatsLogManager().logger() - .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB); - } - mActivityContext.hideKeyboard(); - }); - findViewById(R.id.tab_work) - .setOnClickListener((View view) -> { - if (mViewPager.snapToPage(AdapterHolder.WORK)) { - mActivityContext.getStatsLogManager().logger() - .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB); - } - mActivityContext.hideKeyboard(); - }); - setDeviceManagementResources(); - onActivePageChanged(mViewPager.getNextPage()); - } else { - mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null); - mAH.get(AdapterHolder.WORK).mRecyclerView = null; - } - setupHeader(); - - mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView); - mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); - mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); - } - - protected void updateSearchResultsVisibility() { - if (isSearching()) { - getSearchRecyclerView().setVisibility(VISIBLE); - getAppsRecyclerViewContainer().setVisibility(GONE); - mHeader.setVisibility(GONE); - } else { - getSearchRecyclerView().setVisibility(GONE); - getAppsRecyclerViewContainer().setVisibility(VISIBLE); - mHeader.setVisibility(VISIBLE); - } - if (mHeader.isSetUp()) { - mHeader.setActiveRV(getCurrentPage()); - } - } - - private void applyAdapterSideAndBottomPaddings(DeviceProfile grid) { - int bottomPadding = Math.max(mInsets.bottom, mNavBarScrimHeight); - mAH.forEach(adapterHolder -> { - adapterHolder.mPadding.bottom = bottomPadding; - adapterHolder.mPadding.left = - adapterHolder.mPadding.right = grid.allAppsLeftRightPadding; - adapterHolder.applyPadding(); - }); - } - - private void setDeviceManagementResources() { - if (mActivityContext.getStringCache() != null) { - Button personalTab = findViewById(R.id.tab_personal); - personalTab.setText(mActivityContext.getStringCache().allAppsPersonalTab); - - Button workTab = findViewById(R.id.tab_work); - workTab.setText(mActivityContext.getStringCache().allAppsWorkTab); - } - } - - protected boolean shouldShowTabs() { - return mHasWorkApps; - } - - protected boolean isSearching() { - return false; - } - - protected View replaceAppsRVContainer(boolean showTabs) { - for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) { - AdapterHolder adapterHolder = mAH.get(i); - if (adapterHolder.mRecyclerView != null) { - adapterHolder.mRecyclerView.setLayoutManager(null); - adapterHolder.mRecyclerView.setAdapter(null); - } - } - View oldView = getAppsRecyclerViewContainer(); - int index = indexOfChild(oldView); - removeView(oldView); - int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout; - View newView = getLayoutInflater().inflate(layout, this, false); - addView(newView, index); - if (showTabs) { - mViewPager = (AllAppsPagedView) newView; - mViewPager.initParentViews(this); - mViewPager.getPageIndicator().setOnActivePageChangedListener(this); - - mWorkManager.reset(); - post(() -> mAH.get(AdapterHolder.WORK).applyPadding()); - - } else { - mWorkManager.detachWorkModeSwitch(); - mViewPager = null; - } - return newView; - } - - @Override - public void onActivePageChanged(int currentActivePage) { - if (mAH.get(currentActivePage).mRecyclerView != null) { - mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(); - } - // Header keeps track of active recycler view to properly render header protection. - mHeader.setActiveRV(currentActivePage); - reset(true /* animate */); - - mWorkManager.onActivePageChanged(currentActivePage); - } - - // Used by tests only - private boolean isDescendantViewVisible(int viewId) { - final View view = findViewById(viewId); - if (view == null) return false; - - if (!view.isShown()) return false; - - return view.getGlobalVisibleRect(new Rect()); - } - - @VisibleForTesting - public boolean isPersonalTabVisible() { - return isDescendantViewVisible(R.id.tab_personal); - } - - @VisibleForTesting - public boolean isWorkTabVisible() { - return isDescendantViewVisible(R.id.tab_work); - } - - public AlphabeticalAppsList getSearchResultList() { - return mAH.get(AdapterHolder.SEARCH).mAppsList; - } - - public FloatingHeaderView getFloatingHeaderView() { - return mHeader; - } - - @VisibleForTesting - public View getContentView() { - return isSearching() ? getSearchRecyclerView() : getAppsRecyclerViewContainer(); - } - - /** The current page visible in all apps. */ - public int getCurrentPage() { - return isSearching() - ? AdapterHolder.SEARCH - : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage(); - } - - /** The scroll bar for the active apps recycler view. */ - public RecyclerViewFastScroller getScrollBar() { - AllAppsRecyclerView rv = getActiveAppsRecyclerView(); - return rv == null ? null : rv.getScrollbar(); - } - - void setupHeader() { - mHeader.setVisibility(View.VISIBLE); - boolean tabsHidden = !mUsingTabs; - mHeader.setup( - mAH.get(AdapterHolder.MAIN).mRecyclerView, - mAH.get(AdapterHolder.WORK).mRecyclerView, - (SearchRecyclerView) mAH.get(AdapterHolder.SEARCH).mRecyclerView, - getCurrentPage(), - tabsHidden); - - int padding = mHeader.getMaxTranslation(); - mAH.forEach(adapterHolder -> { - adapterHolder.mPadding.top = padding; - adapterHolder.applyPadding(); - if (adapterHolder.mRecyclerView != null) { - adapterHolder.mRecyclerView.scrollToTop(); - } - }); - } - - public boolean isHeaderVisible() { - return mHeader != null && mHeader.getVisibility() == View.VISIBLE; - } - - /** - * Adds an update listener to animator that adds springs to the animation. - */ - public void addSpringFromFlingUpdateListener(ValueAnimator animator, - float velocity /* release velocity */, - float progress /* portion of the distance to travel*/) { - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - float distance = (1 - progress) * getHeight(); // px - float settleVelocity = Math.min(0, distance - / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration()) - + velocity); - absorbSwipeUpVelocity(Math.max(1000, Math.abs( - Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER)))); - } - }); - } - - /** Invoked when the container is pulled. */ - public void onPull(float deltaDistance, float displacement) { - absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement); - // Current motion spec is to actually push and not pull - // on this surface. However, until EdgeEffect.onPush (b/190612804) is - // implemented at view level, we will simply pull - } - - @Override - public void getDrawingRect(Rect outRect) { - super.getDrawingRect(outRect); - outRect.offset(0, (int) getTranslationY()); - } - - @Override - public void setTranslationY(float translationY) { - super.setTranslationY(translationY); - invalidateHeader(); - } - - public void setScrimView(ScrimView scrimView) { - mScrimView = scrimView; - } - - @Override - public void drawOnScrimWithScale(Canvas canvas, float scale) { - boolean isTablet = mActivityContext.getDeviceProfile().isTablet; - - // Draw full background panel for tablets. - if (isTablet) { - mHeaderPaint.setColor(mBottomSheetBackgroundColor); - View panel = (View) mBottomSheetBackground; - float translationY = ((View) panel.getParent()).getTranslationY(); - mTmpRectF.set(panel.getLeft(), panel.getTop() + translationY, - panel.getRight(), panel.getBottom()); - mTmpPath.reset(); - mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); - canvas.drawPath(mTmpPath, mHeaderPaint); - } - - if (DEBUG_HEADER_PROTECTION) { - mHeaderPaint.setColor(Color.MAGENTA); - mHeaderPaint.setAlpha(255); - } else { - mHeaderPaint.setColor(mHeaderColor); - mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor))); - } - if (mHeaderPaint.getColor() == mScrimColor || mHeaderPaint.getColor() == 0) { - return; - } - final float offset = (getVisibleContainerView().getHeight() * (1 - scale) / 2); - final float bottom = - scale * (getHeaderBottom() + getVisibleContainerView().getPaddingTop()) + offset; - FloatingHeaderView headerView = getFloatingHeaderView(); - if (isTablet) { - // Start adding header protection if search bar or tabs will attach to the top. - if (!FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() || mUsingTabs) { - View panel = (View) mBottomSheetBackground; - float translationY = ((View) panel.getParent()).getTranslationY(); - mTmpRectF.set(panel.getLeft(), panel.getTop() + translationY, panel.getRight(), - bottom); - mTmpPath.reset(); - mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); - canvas.drawPath(mTmpPath, mHeaderPaint); - } - } else { - canvas.drawRect(0, 0, canvas.getWidth(), bottom, mHeaderPaint); - } - int tabsHeight = headerView.getPeripheralProtectionHeight(); - if (mTabsProtectionAlpha > 0 && tabsHeight != 0) { - if (DEBUG_HEADER_PROTECTION) { - mHeaderPaint.setColor(Color.BLUE); - mHeaderPaint.setAlpha(255); - } else { - mHeaderPaint.setAlpha((int) (getAlpha() * mTabsProtectionAlpha)); - } - int left = 0; - int right = canvas.getWidth(); - if (isTablet) { - left = mBottomSheetBackground.getLeft(); - right = mBottomSheetBackground.getRight(); - } - canvas.drawRect(left, bottom, right, bottom + tabsHeight, mHeaderPaint); - } - } - - /** - * redraws header protection - */ - public void invalidateHeader() { - if (mScrimView != null) { - mScrimView.invalidate(); - } - } - - protected void updateHeaderScroll(int scrolledOffset) { - float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f); - int headerColor = getHeaderColor(prog); - int tabsAlpha = mHeader.getPeripheralProtectionHeight() == 0 ? 0 - : (int) (Utilities.boundToRange( - (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f) - * 255); - if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) { - mHeaderColor = headerColor; - mTabsProtectionAlpha = tabsAlpha; - invalidateHeader(); - } - } - - protected int getHeaderColor(float blendRatio) { - return ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, blendRatio); - } - - protected abstract BaseAllAppsAdapter createAdapter(AlphabeticalAppsList mAppsList, - BaseAdapterProvider[] adapterProviders); - - public int getHeaderBottom() { - int bottom = (int) getTranslationY() + mHeader.getClipTop(); - if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) { - if (mActivityContext.getDeviceProfile().isTablet) { - return bottom + mBottomSheetBackground.getTop(); - } - return bottom; - } - return bottom + mHeader.getTop(); - } - - /** - * Returns a view that denotes the visible part of all apps container view. - */ - public View getVisibleContainerView() { - return mActivityContext.getDeviceProfile().isTablet ? mBottomSheetBackground : this; - } - - protected void onInitializeRecyclerView(RecyclerView rv) { - rv.addOnScrollListener(mScrollListener); - } - - /** - * Returns {@code true} the All Apps UI is currently being displayed on the target surface and - * is interactive. - */ - public abstract boolean isInAllApps(); - - /** Holds a {@link BaseAllAppsAdapter} and related fields. */ - public class AdapterHolder { - public static final int MAIN = 0; - public static final int WORK = 1; - public static final int SEARCH = 2; - - private final int mType; - public final BaseAllAppsAdapter mAdapter; - final RecyclerView.LayoutManager mLayoutManager; - final AlphabeticalAppsList mAppsList; - final Rect mPadding = new Rect(); - AllAppsRecyclerView mRecyclerView; - - AdapterHolder(int type) { - mType = type; - mAppsList = new AlphabeticalAppsList<>(mActivityContext, - isSearch() ? null : mAllAppsStore, - isWork() ? mWorkManager : null); - BaseAdapterProvider[] adapterProviders = - new BaseAdapterProvider[]{mMainAdapterProvider}; - - mAdapter = createAdapter(mAppsList, adapterProviders); - mAppsList.setAdapter(mAdapter); - mLayoutManager = mAdapter.getLayoutManager(); - } - - void setup(@NonNull View rv, @Nullable Predicate matcher) { - mAppsList.updateItemFilter(matcher); - mRecyclerView = (AllAppsRecyclerView) rv; - mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory()); - mRecyclerView.setApps(mAppsList); - mRecyclerView.setLayoutManager(mLayoutManager); - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setHasFixedSize(true); - // No animations will occur when changes occur to the items in this RecyclerView. - mRecyclerView.setItemAnimator(null); - onInitializeRecyclerView(mRecyclerView); - FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView); - mRecyclerView.addItemDecoration(focusedItemDecorator); - mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); - applyPadding(); - } - - void applyPadding() { - if (mRecyclerView != null) { - int bottomOffset = 0; - if (isWork() && mWorkManager.getWorkModeSwitch() != null) { - bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight(); - } - mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right, - mPadding.bottom + bottomOffset); - } - } - - private boolean isWork() { - return mType == WORK; - } - - private boolean isSearch() { - return mType == SEARCH; - } - } -} diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java index c18f9e1884..b3ea3ab225 100644 --- a/src/com/android/launcher3/allapps/FloatingHeaderView.java +++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java @@ -32,7 +32,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.Insettable; import com.android.launcher3.R; -import com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder; +import com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.views.ActivityContext; @@ -72,8 +72,8 @@ public class FloatingHeaderView extends LinearLayout implements moved(current); applyVerticalMove(); if (headerCollapsed != mHeaderCollapsed) { - BaseAllAppsContainerView parent = - (BaseAllAppsContainerView) getParent(); + ActivityAllAppsContainerView parent = + (ActivityAllAppsContainerView) getParent(); parent.invalidateHeader(); } } @@ -191,7 +191,7 @@ public class FloatingHeaderView extends LinearLayout implements updateExpectedHeight(); if (mMaxTranslation != oldMaxHeight || mFloatingRowsCollapsed) { - BaseAllAppsContainerView parent = (BaseAllAppsContainerView) getParent(); + ActivityAllAppsContainerView parent = (ActivityAllAppsContainerView) getParent(); if (parent != null) { parent.setupHeader(); } diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java index fa03905515..f66ea34d7c 100644 --- a/src/com/android/launcher3/allapps/WorkProfileManager.java +++ b/src/com/android/launcher3/allapps/WorkProfileManager.java @@ -16,11 +16,11 @@ package com.android.launcher3.allapps; import static com.android.launcher3.LauncherPrefs.WORK_EDU_STEP; +import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; +import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; +import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.WORK; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD; -import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.MAIN; -import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.SEARCH; -import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.WORK; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TURN_OFF_WORK_APPS_TAP; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION; @@ -54,7 +54,7 @@ import java.util.ArrayList; import java.util.function.Predicate; /** - * Companion class for {@link BaseAllAppsContainerView} to manage work tab and personal tab + * Companion class for {@link ActivityAllAppsContainerView} to manage work tab and personal tab * related * logic based on {@link WorkProfileState}? */ @@ -79,7 +79,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP public @interface WorkProfileState { } private final UserManager mUserManager; - private final BaseAllAppsContainerView mAllApps; + private final ActivityAllAppsContainerView mAllApps; private final Predicate mMatcher; private final StatsLogManager mStatsLogManager; @@ -89,7 +89,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP private int mCurrentState; public WorkProfileManager( - UserManager userManager, BaseAllAppsContainerView allApps, + UserManager userManager, ActivityAllAppsContainerView allApps, StatsLogManager statsLogManager) { mUserManager = userManager; mAllApps = allApps; @@ -152,7 +152,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP } /** - * Creates and attaches for profile toggle button to {@link BaseAllAppsContainerView} + * Creates and attaches for profile toggle button to {@link ActivityAllAppsContainerView} */ public boolean attachWorkModeSwitch() { if (!mAllApps.getAppsStore().hasModelFlag( @@ -177,7 +177,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP return true; } /** - * Removes work profile toggle button from {@link BaseAllAppsContainerView} + * Removes work profile toggle button from {@link ActivityAllAppsContainerView} */ public void detachWorkModeSwitch() { if (mWorkModeSwitch != null && mWorkModeSwitch.getParent() == mAllApps) { @@ -195,7 +195,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP return mWorkModeSwitch; } - private BaseAllAppsContainerView.AdapterHolder getAH() { + private ActivityAllAppsContainerView.AdapterHolder getAH() { return mAllApps.mAH.get(WORK); } diff --git a/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java index 3890741d8e..4f7f9afe39 100644 --- a/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java +++ b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java @@ -16,7 +16,6 @@ package com.android.launcher3.allapps.search; -import android.net.Uri; import android.view.View; import androidx.recyclerview.widget.RecyclerView; @@ -37,12 +36,6 @@ public abstract class SearchAdapterProvider extends B mLauncher = launcher; } - /** - * Called from LiveSearchManager to notify slice status updates. - */ - public void onSliceStatusUpdate(Uri sliceUri) { - } - /** * Handles selection event on search adapter item. Returns false if provider can not handle * event From 6bb8d79549dbcaa479c99ea8dae71addce7ba504 Mon Sep 17 00:00:00 2001 From: Fengjiang Li Date: Thu, 12 Jan 2023 16:20:58 -0800 Subject: [PATCH 5/6] Predictive back: widget to all apps This CL adds a layer of OnBackPressedHanlderRouter to Launcher: 1. 4 OnBackPressedHandler(s) are added in such order: auto cancel action mode handler, drag handler, view handler and state handler 2. first handler who can handle back will handle the entire back gesture 3. Let WidgetsFullSheet to handle widget to all apps transition Bug: b/260956481 Test: manual Change-Id: Idbce3dcec746226dd68aaabaddc8fe01334e9673 --- .../launcher3/taskbar/TaskbarDragLayer.java | 3 +- .../overlay/TaskbarOverlayDragLayer.java | 3 +- .../uioverrides/QuickstepLauncher.java | 36 ++++++++- .../quickstep/views/AllAppsEduView.java | 5 -- .../launcher3/AbstractFloatingView.java | 13 ++- .../launcher3/BaseDraggingActivity.java | 6 +- src/com/android/launcher3/Launcher.java | 80 +++++++++++++------ .../launcher3/OnBackPressedHandler.java | 45 +++++++++++ .../launcher3/allapps/DiscoveryBounce.java | 8 +- src/com/android/launcher3/folder/Folder.java | 9 +-- .../SecondaryDisplayLauncher.java | 3 +- .../widget/picker/WidgetsFullSheet.java | 19 ++++- 12 files changed, 177 insertions(+), 53 deletions(-) create mode 100644 src/com/android/launcher3/OnBackPressedHandler.java diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java index d0059f7207..71148493e8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java @@ -194,7 +194,8 @@ public class TaskbarDragLayer extends BaseDragLayer { public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) { AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); - if (topView != null && topView.onBackPressed()) { + if (topView != null && topView.canHandleBack()) { + topView.onBackInvoked(); // Handled by the floating view. return true; } diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java index 044afd6725..d91b650311 100644 --- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java +++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java @@ -71,7 +71,8 @@ public class TaskbarOverlayDragLayer extends public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) { AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); - if (topView != null && topView.onBackPressed()) { + if (topView != null && topView.canHandleBack()) { + topView.onBackInvoked(); return true; } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 0673dc6e9f..30850b9186 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -84,6 +84,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherState; +import com.android.launcher3.OnBackPressedHandler; import com.android.launcher3.QuickstepAccessibilityDelegate; import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.R; @@ -638,20 +639,49 @@ public class QuickstepLauncher extends Launcher { getOnBackInvokedDispatcher().registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, new OnBackAnimationCallback() { + + @Nullable OnBackPressedHandler mActiveOnBackPressedHandler; + + @Override + public void onBackStarted(@NonNull BackEvent backEvent) { + if (mActiveOnBackPressedHandler != null) { + mActiveOnBackPressedHandler.onBackCancelled(); + } + mActiveOnBackPressedHandler = getOnBackPressedHandler(); + mActiveOnBackPressedHandler.onBackStarted(); + } + @Override public void onBackInvoked() { - onBackPressed(); + // Recreate mActiveOnBackPressedHandler if necessary to avoid NPE because: + // 1. b/260636433: In 3-button-navigation mode, onBackStarted() is not + // called on ACTION_DOWN before onBackInvoked() is called in ACTION_UP. + // 2. Launcher#onBackPressed() will call onBackInvoked() without calling + // onBackInvoked() beforehand. + if (mActiveOnBackPressedHandler == null) { + mActiveOnBackPressedHandler = getOnBackPressedHandler(); + } + mActiveOnBackPressedHandler.onBackInvoked(); + mActiveOnBackPressedHandler = null; TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onBackInvoked"); } @Override public void onBackProgressed(@NonNull BackEvent backEvent) { - QuickstepLauncher.this.onBackProgressed(backEvent.getProgress()); + if (!FeatureFlags.IS_STUDIO_BUILD && mActiveOnBackPressedHandler == null) { + return; + } + mActiveOnBackPressedHandler + .onBackProgressed(backEvent.getProgress()); } @Override public void onBackCancelled() { - QuickstepLauncher.this.onBackCancelled(); + if (!FeatureFlags.IS_STUDIO_BUILD && mActiveOnBackPressedHandler == null) { + return; + } + mActiveOnBackPressedHandler.onBackCancelled(); + mActiveOnBackPressedHandler = null; } }); } diff --git a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java index d79b318023..716d389a2c 100644 --- a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java +++ b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java @@ -111,11 +111,6 @@ public class AllAppsEduView extends AbstractFloatingView { return (type & TYPE_ALL_APPS_EDU) != 0; } - @Override - public boolean onBackPressed() { - return true; - } - @Override public boolean canInterceptEventsInSystemGestureRegion() { return true; diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index 49afc1f170..796fa80b71 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -46,7 +46,8 @@ import java.lang.annotation.RetentionPolicy; /** * Base class for a View which shows a floating UI on top of the launcher UI. */ -public abstract class AbstractFloatingView extends LinearLayout implements TouchController { +public abstract class AbstractFloatingView extends LinearLayout implements TouchController, + OnBackPressedHandler { @IntDef(flag = true, value = { TYPE_FOLDER, @@ -165,12 +166,16 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch protected abstract boolean isOfType(@FloatingViewType int type); - /** @return Whether the back is consumed. If false, Launcher will handle the back as well. */ - public boolean onBackPressed() { - close(true); + /** Return true if this view can consume back press. */ + public boolean canHandleBack() { return true; } + @Override + public void onBackInvoked() { + close(true); + } + @Override public boolean onControllerTouchEvent(MotionEvent ev) { return false; diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java index ca1fe40ed9..6f3e94829c 100644 --- a/src/com/android/launcher3/BaseDraggingActivity.java +++ b/src/com/android/launcher3/BaseDraggingActivity.java @@ -125,9 +125,13 @@ public abstract class BaseDraggingActivity extends BaseActivity mCurrentActionMode = null; } + protected boolean isInAutoCancelActionMode() { + return mCurrentActionMode != null && AUTO_CANCEL_ACTION_MODE == mCurrentActionMode.getTag(); + } + @Override public boolean finishAutoCancelActionMode() { - if (mCurrentActionMode != null && AUTO_CANCEL_ACTION_MODE == mCurrentActionMode.getTag()) { + if (isInAutoCancelActionMode()) { mCurrentActionMode.finish(); return true; } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index de5419eb7b..96dd5692c7 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -560,6 +560,61 @@ public class Launcher extends StatefulActivity setTitle(R.string.home_screen); } + /** + * Provide {@link OnBackPressedHandler} in below order: + *
    + *
  1. auto cancel action mode handler + *
  2. drag handler + *
  3. view handler + *
  4. state handler + *
+ * + * A back gesture (a single click on back button, or a swipe back gesture that contains a series + * of swipe events) should be handled by the same handler from above list. For a new back + * gesture, a new handler should be regenerated. + * + * Note that state handler will always be handling the back press event if the previous 3 don't. + */ + @NonNull + protected OnBackPressedHandler getOnBackPressedHandler() { + // #1 auto cancel action mode handler + if (isInAutoCancelActionMode()) { + return this::finishAutoCancelActionMode; + } + + // #2 drag handler + if (mDragController.isDragging()) { + return mDragController::cancelDrag; + } + + // #3 view handler + AbstractFloatingView topView = + AbstractFloatingView.getTopOpenView(Launcher.this); + if (topView != null && topView.canHandleBack()) { + return topView; + } + + // #4 state handler + return new OnBackPressedHandler() { + @Override + public void onBackInvoked() { + onStateBack(); + } + + @Override + public void onBackProgressed( + @FloatRange(from = 0.0, to = 1.0) float backProgress) { + mStateManager.getState().onBackProgressed( + Launcher.this, backProgress); + } + + @Override + public void onBackCancelled() { + mStateManager.getState().onBackCancelled(Launcher.this); + } + }; + } + protected LauncherOverlayManager getDefaultOverlay() { return new LauncherOverlayManager() { }; } @@ -2047,36 +2102,13 @@ public class Launcher extends StatefulActivity @Override public void onBackPressed() { - if (finishAutoCancelActionMode()) { - return; - } - - if (mDragController.isDragging()) { - mDragController.cancelDrag(); - return; - } - - // Note: There should be at most one log per method call. This is enforced implicitly - // by using if-else statements. - AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(this); - if (topView == null || !topView.onBackPressed()) { - // Not handled by the floating view. - onStateBack(); - } + getOnBackPressedHandler().onBackInvoked(); } protected void onStateBack() { mStateManager.getState().onBackPressed(this); } - protected void onBackProgressed(@FloatRange(from = 0.0, to = 1.0) float backProgress) { - mStateManager.getState().onBackProgressed(this, backProgress); - } - - protected void onBackCancelled() { - mStateManager.getState().onBackCancelled(this); - } - protected void onScreenOnChanged(boolean isOn) { // Reset AllApps to its initial state only if we are not in the middle of // processing a multi-step drop diff --git a/src/com/android/launcher3/OnBackPressedHandler.java b/src/com/android/launcher3/OnBackPressedHandler.java new file mode 100644 index 0000000000..485aa0ae38 --- /dev/null +++ b/src/com/android/launcher3/OnBackPressedHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import androidx.annotation.FloatRange; + +/** + * Interface that mimics {@link android.window.OnBackInvokedCallback} without dependencies on U's + * API such as {@link android.window.BackEvent}. + * + *

Impl can assume below order during a back gesture: + *

    + *
  1. [optional] one {@link #onBackStarted()} will be called to start the gesture + *
  2. zero or multiple {@link #onBackProgressed(float)} will be called during swipe gesture + *
  3. either one of {@link #onBackInvoked()} or {@link #onBackCancelled()} will be called to end + * the gesture + */ +public interface OnBackPressedHandler { + + /** Called when back has started. */ + default void onBackStarted() {} + + /** Called when back is committed. */ + void onBackInvoked(); + + /** Called with back gesture's progress. */ + default void onBackProgressed(@FloatRange(from = 0.0, to = 1.0) float backProgress) {} + + /** Called when user drops the back gesture. */ + default void onBackCancelled() {} +} diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java index be261f76b5..0188a476d6 100644 --- a/src/com/android/launcher3/allapps/DiscoveryBounce.java +++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java @@ -81,10 +81,10 @@ public class DiscoveryBounce extends AbstractFloatingView { } @Override - public boolean onBackPressed() { - super.onBackPressed(); - // Go back to the previous state (from a user's perspective this floating view isn't - // something to go back from). + public boolean canHandleBack() { + // Since DiscoveryBounce doesn't handle back, onBackInvoked() won't be called and we should + // close it without animation. + close(false); return false; } diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index 94eea3598e..9a5d77e3a2 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -1579,17 +1579,14 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo return getOpenView(activityContext, TYPE_FOLDER); } - /** - * Navigation bar back key or hardware input back key has been issued. - */ + /** Navigation bar back key or hardware input back key has been issued. */ @Override - public boolean onBackPressed() { + public void onBackInvoked() { if (isEditingName()) { mFolderName.dispatchBackKey(); } else { - super.onBackPressed(); + super.onBackInvoked(); } - return true; } @Override diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java index a2353d8259..56dffa9b64 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java @@ -175,8 +175,9 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity // Note: There should be at most one log per method call. This is enforced implicitly // by using if-else statements. AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(this); - if (topView != null && topView.onBackPressed()) { + if (topView != null && topView.canHandleBack()) { // Handled by the floating view. + topView.onBackInvoked(); } else { showAppDrawer(false); } diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 38cdb4b500..c78ecf5f51 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -17,7 +17,9 @@ package com.android.launcher3.widget.picker; import static android.view.View.MeasureSpec.makeMeasureSpec; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; +import static com.android.launcher3.allapps.AllAppsTransitionController.SWIPE_ALL_APPS_TO_HOME_MIN_SCALE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED; import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; @@ -45,6 +47,7 @@ import android.view.animation.Interpolator; import android.widget.Button; import android.widget.TextView; +import androidx.annotation.FloatRange; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.DefaultItemAnimator; @@ -54,6 +57,7 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.model.UserManagerState; @@ -264,6 +268,15 @@ public class WidgetsFullSheet extends BaseWidgetSheet attachScrollbarToRecyclerView(currentRecyclerView); } + @Override + public void onBackProgressed(@FloatRange(from = 0.0, to = 1.0) float progress) { + float deceleratedProgress = + Interpolators.PREDICTIVE_BACK_DECELERATED_EASE.getInterpolation(progress); + float scaleProgress = SWIPE_ALL_APPS_TO_HOME_MIN_SCALE + + (1 - SWIPE_ALL_APPS_TO_HOME_MIN_SCALE) * (1 - deceleratedProgress); + SCALE_PROPERTY.set(this, scaleProgress); + } + private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) { recyclerView.bindFastScrollbar(); if (mCurrentWidgetsRecyclerView != recyclerView) { @@ -736,12 +749,12 @@ public class WidgetsFullSheet extends BaseWidgetSheet } @Override - public boolean onBackPressed() { + public void onBackInvoked() { if (mIsInSearchMode) { mSearchBar.reset(); - return true; + } else { + super.onBackInvoked(); } - return super.onBackPressed(); } @Override From 16938d35a9a394f67701d9307b933d68537e55b0 Mon Sep 17 00:00:00 2001 From: Tracy Zhou Date: Wed, 25 Jan 2023 04:44:54 +0000 Subject: [PATCH 6/6] Revert "Swipe-up from the bar area on the home screen should pull up overview" This reverts commit c5985751c433c26f87d2023766a4e164222ddd91. Reason for revert: Confirmed with Manasi that we don't want to have a different behavior for the trackpad gesture even if it starts from the task bar area. It should be treated the same. Change-Id: I5817c477968545e452d7600d43825209e2c75c37 --- .../android/quickstep/RotationTouchHelper.java | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java index f75924b2c7..aab5703ae9 100644 --- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java +++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java @@ -233,15 +233,10 @@ public class RotationTouchHelper implements DisplayInfoChangeListener { * @return whether the coordinates of the {@param event} is in the swipe up gesture region. */ public boolean isInSwipeUpTouchRegion(MotionEvent event, BaseActivityInterface activity) { - boolean inBarArea = mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), - event.getY()); - if (inBarArea) { - return true; - } if (isTrackpadMotionEvent(event)) { return !activity.isResumed(); } - return false; + return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()); } /** @@ -250,15 +245,11 @@ public class RotationTouchHelper implements DisplayInfoChangeListener { */ public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex, BaseActivityInterface activity) { - boolean inBarArea = mOrientationTouchTransformer.touchInValidSwipeRegions( - event.getX(pointerIndex), event.getY(pointerIndex)); - if (inBarArea) { - return true; - } if (isTrackpadMotionEvent(event)) { return !activity.isResumed(); } - return false; + return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex), + event.getY(pointerIndex)); } @Override