From d0bec45d37c59090387ea5956d8bf3dcab2aab98 Mon Sep 17 00:00:00 2001 From: Brandon Dayauon Date: Mon, 26 Feb 2024 14:10:04 -0800 Subject: [PATCH 1/2] 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); + } } From 5b5110a782716d014e73c13ff2c25d4384244a4a Mon Sep 17 00:00:00 2001 From: Brandon Dayauon Date: Wed, 28 Feb 2024 11:06:52 -0800 Subject: [PATCH 2/2] Change collapse to use adapterItems instead of getting the childCount() Using childCount() doesn't work since it only shows what is visible on the screen. This is an issue because if the header was somewhat slightly off screen, it wouldn't scroll up upon clicking the button. bug: 299294792 Test: before: https://drive.google.com/file/d/1T4rvfR3_rNmR8tRZheBXNbYjCrSubmy7/view?usp=sharing after: https://drive.google.com/file/d/1T4UdddM7MH4onEfNdyhDM-RowKHfvjop/view?usp=sharing Flag: ACONFIG com.android.launcher3.Flags.private_space_animation TRUNKFOOD Change-Id: I209e3ec707bfbdc2ef5e98034149459286903854 --- .../PrivateSpaceHeaderViewController.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java index b151b3a0dc..fdc035ea1f 100644 --- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java +++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java @@ -188,16 +188,12 @@ public class PrivateSpaceHeaderViewController { /** Finds the private space header to scroll to and set the private space icons to GONE. */ private void collapse() { AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); - for (int i = allAppsRecyclerView.getChildCount() - 1; i > 0; i--) { - int adapterPosition = allAppsRecyclerView.getChildAdapterPosition( - allAppsRecyclerView.getChildAt(i)); - List allAppsAdapters = allAppsRecyclerView.getApps() - .getAdapterItems(); - if (adapterPosition < 0 || adapterPosition >= allAppsAdapters.size()) { - continue; - } + List appListAdapterItems = + allAppsRecyclerView.getApps().getAdapterItems(); + for (int i = appListAdapterItems.size() - 1; i > 0; i--) { + BaseAllAppsAdapter.AdapterItem currentItem = appListAdapterItems.get(i); // Scroll to the private space header. - if (allAppsAdapters.get(adapterPosition).viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { + if (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { // Note: SmoothScroller is meant to be used once. RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(mAllApps.getContext()) { @@ -205,7 +201,7 @@ public class PrivateSpaceHeaderViewController { return LinearSmoothScroller.SNAP_TO_END; } }; - smoothScroller.setTargetPosition(adapterPosition); + smoothScroller.setTargetPosition(i); RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); if (layoutManager != null) { layoutManager.startSmoothScroll(smoothScroller); @@ -213,8 +209,12 @@ public class PrivateSpaceHeaderViewController { break; } // Make the private space apps gone to "collapse". - if (allAppsAdapters.get(adapterPosition).decorationInfo != null) { - allAppsRecyclerView.getChildAt(i).setVisibility(GONE); + if (currentItem.decorationInfo != null) { + RecyclerView.ViewHolder viewHolder = + allAppsRecyclerView.findViewHolderForAdapterPosition(i); + if (viewHolder != null) { + viewHolder.itemView.setVisibility(GONE); + } } } }