diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 01ea9fb240..fbeab4efd0 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; @@ -1431,9 +1432,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 @@ -1463,9 +1466,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..fdc035ea1f 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,23 +176,24 @@ 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); } } /** 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()) { @@ -198,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); @@ -206,12 +209,67 @@ 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); + } } } } + /** + * 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); + } }