From d0bec45d37c59090387ea5956d8bf3dcab2aab98 Mon Sep 17 00:00:00 2001 From: Brandon Dayauon Date: Mon, 26 Feb 2024 14:10:04 -0800 Subject: [PATCH] Upon expanding, expand just enough so the header shows. This issue only happens when there is a lot of private space apps that scrolling to the bottom will not sure the private space header. Formula to calculate how many rows to scroll to = (appListHeight - privateHeaderHeight - headerProtectionHeight) / cellHeight. bug: 299294792 Test: manually - https://screenshot.googleplex.com/76UJPT2Jnpnp2Ab before: it just scrolls all the way to the bottom after: https://drive.google.com/file/d/1AbprxFm1RWTQKvpt7M4khbUfc1o6-XGF/view?usp=sharing after PHONE WITH TABS: 2x2- https://drive.google.com/file/d/1SLPsWPHenCuZuisiS7HeEy5JwtNPeONs/view?usp=sharing 3x3- https://drive.google.com/file/d/1SK82jeNZMzFJK2odIuHnfTNLYfppne83/view?usp=sharing 4x4- https://drive.google.com/file/d/1T7EhFRq2tDv2zYIvs_FMsTKcFZXwGUaD/view?usp=sharing 4x5- https://drive.google.com/file/d/1SMUPuKjO1Yg36U6P6cDOb6dTkHn6Bh7D/view?usp=sharing 5x5- https://drive.google.com/file/d/1SJCQn1O_Yq5P7C__VUfZHc5I67CEdIpb/view?usp=sharing AFTER PHONE NO TABS: 2x2: https://drive.google.com/file/d/1THU2xrAIt0hTmN5_GwBrgN9Lqj-W4Kfr/view?usp=sharing 3x3: https://drive.google.com/file/d/1TPTUx7PcHW3GsVwVAg_L5rCcn0QYoiY2/view?usp=sharing 4x4: https://drive.google.com/file/d/1TWVWpAX6bZp_JfFKtmXYO0askl4e5qKO/view?usp=sharing 4x5: https://drive.google.com/file/d/1TDJK-swmY3Y3C4ARH_2eljqUkBGEnD3e/view?usp=sharing 5x5- https://drive.google.com/file/d/1TBJtAynwvZrGyOc-29f637wyrJZpMXBJ/view?usp=sharing Tablet: landscape: https://drive.google.com/file/d/1SfyPdoUnCV7e7BWLnpxXWN2HiBOQkRo2/view?usp=sharing portrait: https://drive.google.com/file/d/1SgZq0iE9WMvIFtc8mBb577nYlS9jBa_g/view?usp=sharing Flag: ACONFIG com.android.launcher3.Flags.private_space_animation TRUNKFOOD Change-Id: If70df1299572f8f2edc6376dd2a6df5d74287264 --- .../allapps/ActivityAllAppsContainerView.java | 12 ++ .../PrivateSpaceHeaderViewController.java | 62 +++++++- .../PrivateSpaceHeaderViewControllerTest.java | 135 ++++++++++++++++++ 3 files changed, 207 insertions(+), 2 deletions(-) diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 6acfcd0f11..965e97c53a 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -189,6 +189,7 @@ public class ActivityAllAppsContainerView private float mBottomSheetAlpha = 1f; private boolean mForceBottomSheetVisible; private int mTabsProtectionAlpha; + private float mTotalHeaderProtectionHeight; @Nullable private AllAppsTransitionController mAllAppsTransitionController; private PrivateSpaceHeaderViewController mPrivateSpaceHeaderViewController; @@ -1429,9 +1430,11 @@ public class ActivityAllAppsContainerView mTmpPath.reset(); mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW); canvas.drawPath(mTmpPath, mHeaderPaint); + mTotalHeaderProtectionHeight = headerBottomWithScaleOnTablet; } } else { canvas.drawRect(0, 0, canvas.getWidth(), headerBottomWithScaleOnPhone, mHeaderPaint); + mTotalHeaderProtectionHeight = headerBottomWithScaleOnPhone; } // If tab exist (such as work profile), extend header with tab height @@ -1461,9 +1464,18 @@ public class ActivityAllAppsContainerView right, tabBottomWithScale, mHeaderPaint); + mTotalHeaderProtectionHeight = tabBottomWithScale; } } + /** + * The height of the header protection is dynamically calculated during the time of drawing the + * header. + */ + float getHeaderProtectionHeight() { + return mTotalHeaderProtectionHeight; + } + /** * redraws header protection */ diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java index 6067454812..b151b3a0dc 100644 --- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java +++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java @@ -41,15 +41,18 @@ import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; import com.android.app.animation.Interpolators; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.Flags; import com.android.launcher3.R; import com.android.launcher3.allapps.UserProfileManager.UserProfileState; import com.android.launcher3.anim.AnimatedPropertySetter; import com.android.launcher3.anim.PropertySetter; +import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.RecyclerViewFastScroller; import java.util.List; @@ -59,7 +62,6 @@ import java.util.List; * {@link UserProfileState} */ public class PrivateSpaceHeaderViewController { - private static final int EXPAND_SCROLL_DURATION = 2000; private static final int EXPAND_COLLAPSE_DURATION = 800; private static final int SETTINGS_OPACITY_DURATION = 160; private final ActivityAllAppsContainerView mAllApps; @@ -174,7 +176,12 @@ public class PrivateSpaceHeaderViewController { && mAllApps.getActiveRecyclerView() == mainAdapterHolder.mRecyclerView) { // Animate the text and settings icon. updatePrivateStateAnimator(true, header); - mAllApps.getActiveRecyclerView().scrollToBottomWithMotion(EXPAND_SCROLL_DURATION); + DeviceProfile deviceProfile = + ActivityContext.lookupContext(mAllApps.getContext()).getDeviceProfile(); + AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); + scrollForViewToBeVisibleInContainer(allAppsRecyclerView, + allAppsRecyclerView.getApps().getAdapterItems(), + header.getHeight(), deviceProfile.allAppsCellHeightPx); } } @@ -212,6 +219,57 @@ public class PrivateSpaceHeaderViewController { } } + /** + * Upon expanding, only scroll to the item position in the adapter that allows the header to be + * visible. + */ + @VisibleForTesting + public int scrollForViewToBeVisibleInContainer( + AllAppsRecyclerView allAppsRecyclerView, + List appListAdapterItems, + int psHeaderHeight, + int allAppsCellHeight) { + int rowToExpandToWithRespectToHeader = -1; + int itemToScrollTo = -1; + // Looks for the item in the app list to scroll to so that the header is visible. + for (int i = 0; i < appListAdapterItems.size(); i++) { + BaseAllAppsAdapter.AdapterItem currentItem = appListAdapterItems.get(i); + if (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { + itemToScrollTo = i; + continue; + } + if (itemToScrollTo != -1) { + if (rowToExpandToWithRespectToHeader == -1) { + rowToExpandToWithRespectToHeader = currentItem.rowIndex; + } + int rowToScrollTo = + (int) Math.floor((double) (mAllApps.getHeight() - psHeaderHeight + - mAllApps.getHeaderProtectionHeight()) / allAppsCellHeight); + int currentRowDistance = currentItem.rowIndex - rowToExpandToWithRespectToHeader; + // rowToScrollTo - 1 since the item to scroll to is 0 indexed. + if (currentRowDistance == rowToScrollTo - 1) { + itemToScrollTo = i; + break; + } + } + } + if (itemToScrollTo != -1) { + // Note: SmoothScroller is meant to be used once. + RecyclerView.SmoothScroller smoothScroller = + new LinearSmoothScroller(mAllApps.getContext()) { + @Override protected int getVerticalSnapPreference() { + return LinearSmoothScroller.SNAP_TO_ANY; + } + }; + smoothScroller.setTargetPosition(itemToScrollTo); + RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); + if (layoutManager != null) { + layoutManager.startSmoothScroll(smoothScroller); + } + } + return itemToScrollTo; + } + PrivateProfileManager getPrivateProfileManager() { return mPrivateProfileManager; } diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java index 490cb47eae..043461d4d9 100644 --- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java +++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java @@ -18,6 +18,7 @@ package com.android.launcher3.allapps; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER; import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED; import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED; import static com.android.launcher3.allapps.UserProfileManager.STATE_TRANSITION; @@ -25,13 +26,19 @@ import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalAnswers.answer; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.Process; +import android.os.UserHandle; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageButton; @@ -44,6 +51,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.R; +import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.util.ActivityContextWrapper; import org.junit.Before; @@ -52,32 +60,53 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.List; + @SmallTest @RunWith(AndroidJUnit4.class) public class PrivateSpaceHeaderViewControllerTest { + private static final UserHandle MAIN_HANDLE = Process.myUserHandle(); + private static final UserHandle PRIVATE_HANDLE = new UserHandle(11); private static final int CONTAINER_HEADER_ELEMENT_COUNT = 1; private static final int LOCK_UNLOCK_BUTTON_COUNT = 1; private static final int PS_SETTINGS_BUTTON_COUNT_VISIBLE = 1; private static final int PS_SETTINGS_BUTTON_COUNT_INVISIBLE = 0; private static final int PS_TRANSITION_IMAGE_COUNT = 1; + private static final int NUM_APP_COLS = 4; + private static final int NUM_PRIVATE_SPACE_APPS = 50; + private static final int ALL_APPS_HEIGHT = 10; + private static final int ALL_APPS_CELL_HEIGHT = 1; + private static final int PS_HEADER_HEIGHT = 1; + private static final int BIGGER_PS_HEADER_HEIGHT = 2; + private static final int SCROLL_NO_WHERE = -1; + private static final float HEADER_PROTECTION_HEIGHT = 1F; private Context mContext; private PrivateSpaceHeaderViewController mPsHeaderViewController; private RelativeLayout mPsHeaderLayout; + private AlphabeticalAppsList mAlphabeticalAppsList; @Mock private PrivateProfileManager mPrivateProfileManager; @Mock private ActivityAllAppsContainerView mAllApps; + @Mock + private AllAppsStore mAllAppsStore; @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = new ActivityContextWrapper(getApplicationContext()); + when(mPrivateProfileManager.getItemInfoMatcher()).thenReturn(info -> + info != null && info.user.equals(PRIVATE_HANDLE)); mPsHeaderViewController = new PrivateSpaceHeaderViewController(mAllApps, mPrivateProfileManager); mPsHeaderLayout = (RelativeLayout) LayoutInflater.from(mContext).inflate( R.layout.private_space_header, null); + mAlphabeticalAppsList = new AlphabeticalAppsList<>(mContext, mAllAppsStore, + null, mPrivateProfileManager); + mAlphabeticalAppsList.setNumAppsPerRowAllApps(NUM_APP_COLS); } @Test @@ -223,6 +252,88 @@ public class PrivateSpaceHeaderViewControllerTest { assertEquals(PS_TRANSITION_IMAGE_COUNT, totalLockUnlockButtonView); } + @Test + public void scrollForViewToBeVisibleInContainer_withHeader() { + when(mAllAppsStore.getApps()).thenReturn(createAppInfoList()); + when(mPrivateProfileManager.addPrivateSpaceHeader(any())) + .thenAnswer(answer(this::addPrivateSpaceHeader)); + when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(mPrivateProfileManager.splitIntoUserInstalledAndSystemApps()) + .thenReturn(iteminfo -> iteminfo.componentName == null + || !iteminfo.componentName.getPackageName() + .equals("com.android.launcher3.tests.camera")); + when(mAllApps.getContext()).thenReturn(mContext); + mAlphabeticalAppsList.updateItemFilter(info -> info != null + && info.user.equals(MAIN_HANDLE)); + when(mAllApps.getHeight()).thenReturn(ALL_APPS_HEIGHT); + when(mAllApps.getHeaderProtectionHeight()).thenReturn(HEADER_PROTECTION_HEIGHT); + int rows = (int) (ALL_APPS_HEIGHT - PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT); + int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1; + + // The number of adapterItems should be the private space apps + one main app + header. + assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1, + mAlphabeticalAppsList.getAdapterItems().size()); + assertEquals(position, + mPsHeaderViewController.scrollForViewToBeVisibleInContainer( + new AllAppsRecyclerView(mContext), + mAlphabeticalAppsList.getAdapterItems(), + PS_HEADER_HEIGHT, + ALL_APPS_CELL_HEIGHT)); + } + + @Test + public void scrollForViewToBeVisibleInContainer_withHeaderAndLessAppRowSpace() { + when(mAllAppsStore.getApps()).thenReturn(createAppInfoList()); + when(mPrivateProfileManager.addPrivateSpaceHeader(any())) + .thenAnswer(answer(this::addPrivateSpaceHeader)); + when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(mPrivateProfileManager.splitIntoUserInstalledAndSystemApps()) + .thenReturn(iteminfo -> iteminfo.componentName == null + || !iteminfo.componentName.getPackageName() + .equals("com.android.launcher3.tests.camera")); + when(mAllApps.getContext()).thenReturn(mContext); + mAlphabeticalAppsList.updateItemFilter(info -> info != null + && info.user.equals(MAIN_HANDLE)); + when(mAllApps.getHeight()).thenReturn(ALL_APPS_HEIGHT); + when(mAllApps.getHeaderProtectionHeight()).thenReturn(HEADER_PROTECTION_HEIGHT); + int rows = (int) (ALL_APPS_HEIGHT - BIGGER_PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT); + int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1; + + // The number of adapterItems should be the private space apps + one main app + header. + assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1, + mAlphabeticalAppsList.getAdapterItems().size()); + assertEquals(position, + mPsHeaderViewController.scrollForViewToBeVisibleInContainer( + new AllAppsRecyclerView(mContext), + mAlphabeticalAppsList.getAdapterItems(), + BIGGER_PS_HEADER_HEIGHT, + ALL_APPS_CELL_HEIGHT)); + } + + @Test + public void scrollForViewToBeVisibleInContainer_withNoHeader() { + when(mAllAppsStore.getApps()).thenReturn(createAppInfoList()); + when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(mPrivateProfileManager.splitIntoUserInstalledAndSystemApps()) + .thenReturn(iteminfo -> iteminfo.componentName == null + || !iteminfo.componentName.getPackageName() + .equals("com.android.launcher3.tests.camera")); + when(mAllApps.getContext()).thenReturn(mContext); + mAlphabeticalAppsList.updateItemFilter(info -> info != null + && info.user.equals(MAIN_HANDLE)); + when(mAllApps.getHeight()).thenReturn(ALL_APPS_HEIGHT); + when(mAllApps.getHeaderProtectionHeight()).thenReturn(HEADER_PROTECTION_HEIGHT); + + // The number of adapterItems should be the private space apps + one main app. + assertEquals(NUM_PRIVATE_SPACE_APPS + 1, + mAlphabeticalAppsList.getAdapterItems().size()); + assertEquals(SCROLL_NO_WHERE, mPsHeaderViewController.scrollForViewToBeVisibleInContainer( + new AllAppsRecyclerView(mContext), + mAlphabeticalAppsList.getAdapterItems(), + BIGGER_PS_HEADER_HEIGHT, + ALL_APPS_CELL_HEIGHT)); + } + private Bitmap getBitmap(Drawable drawable) { Bitmap result; if (drawable instanceof BitmapDrawable) { @@ -249,4 +360,28 @@ public class PrivateSpaceHeaderViewControllerTest { private static void awaitTasksCompleted() throws Exception { UI_HELPER_EXECUTOR.submit(() -> null).get(); } + + private int addPrivateSpaceHeader(List adapterItemList) { + BaseAllAppsAdapter.AdapterItem privateSpaceHeader = + new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_PRIVATE_SPACE_HEADER); + adapterItemList.add(privateSpaceHeader); + return adapterItemList.size(); + } + + private AppInfo[] createAppInfoList() { + List appInfos = new ArrayList<>(); + ComponentName gmailComponentName = new ComponentName(mContext, + "com.android.launcher3.tests.Activity" + "Gmail"); + AppInfo gmailAppInfo = new + AppInfo(gmailComponentName, "Gmail", MAIN_HANDLE, new Intent()); + appInfos.add(gmailAppInfo); + ComponentName privateCameraComponentName = new ComponentName( + "com.android.launcher3.tests.camera", "CameraActivity"); + for (int i = 0; i < NUM_PRIVATE_SPACE_APPS; i++) { + AppInfo privateCameraAppInfo = new AppInfo(privateCameraComponentName, + "Private Camera " + i, PRIVATE_HANDLE, new Intent()); + appInfos.add(privateCameraAppInfo); + } + return appInfos.toArray(AppInfo[]::new); + } }