Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d0bec45d authored by Brandon Dayauon's avatar Brandon Dayauon
Browse files

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
parent 4b127628
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -189,6 +189,7 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
    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<T extends Context & ActivityContext>
                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<T extends Context & ActivityContext>
                    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
     */
+60 −2
Original line number Diff line number Diff line
@@ -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<BaseAllAppsAdapter.AdapterItem> 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;
    }
+135 −0
Original line number Diff line number Diff line
@@ -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<BaseAllAppsAdapter.AdapterItem> adapterItemList) {
        BaseAllAppsAdapter.AdapterItem privateSpaceHeader =
                new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_PRIVATE_SPACE_HEADER);
        adapterItemList.add(privateSpaceHeader);
        return adapterItemList.size();
    }

    private AppInfo[] createAppInfoList() {
        List<AppInfo> 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);
    }
}