diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml index 2b5db485de..185207b92f 100644 --- a/res/layout/private_space_header.xml +++ b/res/layout/private_space_header.xml @@ -33,7 +33,7 @@ android:layout_centerVertical="true" android:gravity="center_vertical" android:layout_alignParentEnd="true" - android:animateLayoutChanges="true"> + android:animateLayoutChanges="false"> 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 int SCROLL_TO_BOTTOM_DURATION = 500; 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; @@ -192,8 +190,6 @@ public class ActivityAllAppsContainerView private float mTotalHeaderProtectionHeight; @Nullable private AllAppsTransitionController mAllAppsTransitionController; - private PrivateSpaceHeaderViewController mPrivateSpaceHeaderViewController; - public ActivityAllAppsContainerView(Context context) { this(context, null); } @@ -261,10 +257,6 @@ public class ActivityAllAppsContainerView */ protected void initContent() { mMainAdapterProvider = mSearchUiDelegate.createMainAdapterProvider(); - if (Flags.enablePrivateSpace()) { - mPrivateSpaceHeaderViewController = - new PrivateSpaceHeaderViewController(this, mPrivateProfileManager); - } mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN, new AlphabeticalAppsList<>(mActivityContext, @@ -398,7 +390,7 @@ public class ActivityAllAppsContainerView mAllAppsTransitionController = allAppsTransitionController; } - private void animateToSearchState(boolean goingToSearch, long durationMs) { + void animateToSearchState(boolean goingToSearch, long durationMs) { if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) { return; } @@ -499,9 +491,9 @@ public class ActivityAllAppsContainerView } /** - * Exits search and returns to A-Z apps list. Scroll to the bottom. + * Exits search and returns to A-Z apps list. Scroll to the private space header. */ - public void resetAndScrollToBottom() { + public void resetAndScrollToPrivateSpaceHeader() { if (mTouchHandler != null) { mTouchHandler.endFastScrolling(); } @@ -518,7 +510,13 @@ public class ActivityAllAppsContainerView // Switch to the main tab switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN); // Scroll to bottom - getActiveRecyclerView().scrollToBottomWithMotion(SCROLL_TO_BOTTOM_DURATION); + if (mPrivateProfileManager != null) { + mPrivateProfileManager.scrollForViewToBeVisibleInContainer( + getActiveAppsRecyclerView(), + getPersonalAppList().getAdapterItems(), + mPrivateProfileManager.getPsHeaderHeight(), + mActivityContext.getDeviceProfile().allAppsCellHeightPx); + } }); } @@ -906,7 +904,7 @@ public class ActivityAllAppsContainerView protected BaseAllAppsAdapter createAdapter(AlphabeticalAppsList appsList) { return new AllAppsGridAdapter<>(mActivityContext, getLayoutInflater(), appsList, - mMainAdapterProvider, mPrivateSpaceHeaderViewController); + mMainAdapterProvider); } // TODO(b/216683257): Remove when Taskbar All Apps supports search. diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index 5f002b5790..df383bf9f6 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -73,9 +73,8 @@ public class AllAppsGridAdapter extends public AllAppsGridAdapter(T activityContext, LayoutInflater inflater, - AlphabeticalAppsList apps, SearchAdapterProvider adapterProvider, - PrivateSpaceHeaderViewController privateSpaceHeaderViewController) { - super(activityContext, inflater, apps, adapterProvider, privateSpaceHeaderViewController); + AlphabeticalAppsList apps, SearchAdapterProvider adapterProvider) { + super(activityContext, inflater, apps, adapterProvider); mGridLayoutMgr = new AppsGridLayoutManager(mActivityContext); mGridLayoutMgr.setSpanSizeLookup(new GridSpanSizer()); setAppsPerRow(activityContext.getDeviceProfile().numShownAllAppsColumns); diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 81cfa866ba..4d4b8d282d 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -206,7 +206,10 @@ public class AlphabeticalAppsList implement */ @Override public void onAppsUpdated() { - if (mAllAppsStore == null) { + // Don't update apps when the private profile animations are running, otherwise the motion + // is canceled. + if (mAllAppsStore == null || (mPrivateProviderManager != null && + mPrivateProviderManager.getAnimationRunning())) { return; } // Sort the list of apps @@ -444,6 +447,10 @@ public class AlphabeticalAppsList implement return roundRegion; } + public PrivateProfileManager getPrivateProfileManager() { + return mPrivateProviderManager; + } + private static class MyDiffCallback extends DiffUtil.Callback { private final List mOldList; diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 5e5795d4e1..2190e1a5af 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -169,16 +169,9 @@ public abstract class BaseAllAppsAdapter ex protected final OnClickListener mOnIconClickListener; protected final OnLongClickListener mOnIconLongClickListener; protected OnFocusChangeListener mIconFocusListener; - private final PrivateSpaceHeaderViewController mPrivateSpaceHeaderViewController; public BaseAllAppsAdapter(T activityContext, LayoutInflater inflater, AlphabeticalAppsList apps, SearchAdapterProvider adapterProvider) { - this(activityContext, inflater, apps, adapterProvider, null); - } - - public BaseAllAppsAdapter(T activityContext, LayoutInflater inflater, - AlphabeticalAppsList apps, SearchAdapterProvider adapterProvider, - PrivateSpaceHeaderViewController privateSpaceHeaderViewController) { mActivityContext = activityContext; mApps = apps; mLayoutInflater = inflater; @@ -187,7 +180,6 @@ public abstract class BaseAllAppsAdapter ex mOnIconLongClickListener = mActivityContext.getAllAppsItemLongClickListener(); mAdapterProvider = adapterProvider; - mPrivateSpaceHeaderViewController = privateSpaceHeaderViewController; } /** Checks if the passed viewType represents all apps divider. */ @@ -283,13 +275,10 @@ public abstract class BaseAllAppsAdapter ex case VIEW_TYPE_PRIVATE_SPACE_HEADER: RelativeLayout psHeaderLayout = holder.itemView.findViewById( R.id.ps_header_layout); - assert mPrivateSpaceHeaderViewController != null; - assert psHeaderLayout != null; - mPrivateSpaceHeaderViewController.addPrivateSpaceHeaderViewElements(psHeaderLayout); + mApps.getPrivateProfileManager().addPrivateSpaceHeaderViewElements(psHeaderLayout); AdapterItem adapterItem = mApps.getAdapterItems().get(position); int roundRegions = ROUND_TOP_LEFT | ROUND_TOP_RIGHT; - if (mPrivateSpaceHeaderViewController.getPrivateProfileManager().getCurrentState() - == STATE_DISABLED) { + if (mApps.getPrivateProfileManager().getCurrentState() == STATE_DISABLED) { roundRegions |= (ROUND_BOTTOM_LEFT | ROUND_BOTTOM_RIGHT); } adapterItem.decorationInfo = diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java index e7bb1d0089..eb967bc68e 100644 --- a/src/com/android/launcher3/allapps/PrivateProfileManager.java +++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java @@ -16,26 +16,54 @@ package com.android.launcher3.allapps; +import static android.view.View.GONE; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; + +import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ICON; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_TAP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.LayoutTransition; +import android.animation.ValueAnimator; import android.content.Context; import android.content.Intent; import android.os.UserHandle; import android.os.UserManager; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; +import com.android.app.animation.Interpolators; import com.android.launcher3.BuildConfig; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.R; +import com.android.launcher3.anim.AnimatedPropertySetter; +import com.android.launcher3.anim.PropertySetter; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.logging.StatsLogManager; @@ -45,6 +73,8 @@ import com.android.launcher3.pm.UserCache; import com.android.launcher3.uioverrides.ApiWrapper; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.RecyclerViewFastScroller; import java.util.ArrayList; import java.util.HashSet; @@ -57,14 +87,17 @@ import java.util.function.Predicate; * logic in the Personal tab. */ public class PrivateProfileManager extends UserProfileManager { - + private static final int EXPAND_COLLAPSE_DURATION = 800; + private static final int SETTINGS_OPACITY_DURATION = 160; private final ActivityAllAppsContainerView mAllApps; private final Predicate mPrivateProfileMatcher; private Set mPreInstalledSystemPackages = new HashSet<>(); private Intent mAppInstallerIntent = new Intent(); private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator; private boolean mPrivateSpaceSettingsAvailable; - private Runnable mUnlockRunnable; + private boolean mIsAnimationRunning; + private int mHeaderHeight; + private boolean mAnimate; public PrivateProfileManager(UserManager userManager, ActivityAllAppsContainerView allApps, @@ -118,20 +151,19 @@ public class PrivateProfileManager extends UserProfileManager { /** * Disables quiet mode for Private Space User Profile. - * The runnable passed will be executed in the {@link #reset()} method, - * when Launcher receives update about profile availability. - * The runnable passed is only executed once, and reset after execution. + * When called from search, a runnable is set and executed in the {@link #reset()} method, when + * Launcher receives update about profile availability. + * The runnable is only executed once, and reset after execution. * In case the method is called again, before the previously set runnable was executed, * the runnable will be updated. */ - public void unlockPrivateProfile(Runnable runnable) { - enableQuietMode(false); - mUnlockRunnable = runnable; + public void unlockPrivateProfile() { + setQuietMode(false); } /** Enables quiet mode for Private Space User Profile. */ - public void lockPrivateProfile() { - enableQuietMode(true); + void lockPrivateProfile() { + setQuietMode(true); } /** Whether private profile should be hidden on Launcher. */ @@ -149,7 +181,9 @@ public class PrivateProfileManager extends UserProfileManager { setCurrentState(updatedState); resetPrivateSpaceDecorator(updatedState); if (transitioningFromLockedToUnlocked(previousState, updatedState)) { - applyUnlockRunnable(); + postUnlock(); + } else if (transitioningFromUnlockedToLocked(previousState, updatedState)){ + executeLock(); } } @@ -235,23 +269,45 @@ public class PrivateProfileManager extends UserProfileManager { } } - /** Posts quiet mode enable/disable call for private profile. */ - private void enableQuietMode(boolean enable) { - setQuietMode(enable); + @Override + public void setQuietMode(boolean enable) { + super.setQuietMode(enable); + mAnimate = true; } - void applyUnlockRunnable() { - if (mUnlockRunnable != null) { - // reset the runnable to prevent re-execution. - MAIN_EXECUTOR.post(mUnlockRunnable); - mUnlockRunnable = null; + /** + * Expand the private space after the app list has been added and updated from + * {@link AlphabeticalAppsList#onAppsUpdated()} + */ + void postUnlock() { + if (mAllApps.isSearching()) { + MAIN_EXECUTOR.post(this::exitSearchAndExpand); + } else { + MAIN_EXECUTOR.post(this::expandPrivateSpace); } } + /** Collapses the private space before the app list has been updated. */ + void executeLock() { + MAIN_EXECUTOR.execute(this::collapsePrivateSpace); + } + + void setAnimationRunning(boolean isAnimationRunning) { + mIsAnimationRunning = isAnimationRunning; + } + + boolean getAnimationRunning() { + return mIsAnimationRunning; + } + private boolean transitioningFromLockedToUnlocked(int previousState, int updatedState) { return previousState == STATE_DISABLED && updatedState == STATE_ENABLED; } + private boolean transitioningFromUnlockedToLocked(int previousState, int updatedState) { + return previousState == STATE_ENABLED && updatedState == STATE_DISABLED; + } + @Override public Predicate getUserMatcher() { return mPrivateProfileMatcher; @@ -266,4 +322,349 @@ public class PrivateProfileManager extends UserProfileManager { && (appInfo.componentName == null || !(mPreInstalledSystemPackages.contains(appInfo.componentName.getPackageName()))); } + + /** Add Private Space Header view elements based upon {@link UserProfileState} */ + public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) { + // Set the transition duration for the settings and lock button to animate. + ViewGroup settingAndLockGroup = parent.findViewById(R.id.settingsAndLockGroup); + if (mAnimate) { + enableLayoutTransition(settingAndLockGroup); + } else { + // Ensure any unwanted animations to not happen. + settingAndLockGroup.setLayoutTransition(null); + } + + //Add quietMode image and action for lock/unlock button + ViewGroup lockButton = + parent.findViewById(R.id.ps_lock_unlock_button); + assert lockButton != null; + addLockButton(lockButton); + + //Trigger lock/unlock action from header. + addHeaderOnClickListener(parent); + + //Add image and action for private space settings button + ImageButton settingsButton = parent.findViewById(R.id.ps_settings_button); + assert settingsButton != null; + addPrivateSpaceSettingsButton(settingsButton); + + //Add image for private space transitioning view + ImageView transitionView = parent.findViewById(R.id.ps_transition_image); + assert transitionView != null; + addTransitionImage(transitionView); + mHeaderHeight = parent.getHeight(); + } + + /** + * Adds the quietModeButton and attach onClickListener for the header to animate different + * states when clicked. + */ + private void addLockButton(ViewGroup lockButton) { + TextView lockText = lockButton.findViewById(R.id.lock_text); + switch (getCurrentState()) { + case STATE_ENABLED -> { + lockText.setVisibility(VISIBLE); + lockButton.setVisibility(VISIBLE); + lockButton.setOnClickListener(view -> lockingAction(/* lock */ true)); + } + case STATE_DISABLED -> { + lockText.setVisibility(GONE); + lockButton.setVisibility(VISIBLE); + lockButton.setOnClickListener(view -> lockingAction(/* lock */ false)); + } + default -> lockButton.setVisibility(GONE); + } + } + + private void addHeaderOnClickListener(RelativeLayout header) { + if (getCurrentState() == STATE_DISABLED) { + header.setOnClickListener(view -> lockingAction(/* lock */ false)); + } else { + header.setOnClickListener(null); + } + } + + /** Sets the enablement of the profile when header or button is clicked. */ + private void lockingAction(boolean lock) { + logEvents(lock ? LAUNCHER_PRIVATE_SPACE_LOCK_TAP : LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP); + if (lock) { + lockPrivateProfile(); + } else { + unlockPrivateProfile(); + } + } + + private void addPrivateSpaceSettingsButton(ImageButton settingsButton) { + if (getCurrentState() == STATE_ENABLED + && isPrivateSpaceSettingsAvailable()) { + settingsButton.setVisibility(VISIBLE); + settingsButton.setAlpha(1f); + settingsButton.setOnClickListener( + view -> { + logEvents(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP); + openPrivateSpaceSettings(); + }); + } else { + settingsButton.setVisibility(GONE); + } + } + + private void addTransitionImage(ImageView transitionImage) { + if (getCurrentState() == STATE_TRANSITION) { + transitionImage.setVisibility(VISIBLE); + } else { + transitionImage.setVisibility(GONE); + } + } + + /** Finds the private space header to scroll to and set the private space icons to GONE. */ + private void collapse() { + AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); + 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 (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { + // Note: SmoothScroller is meant to be used once. + RecyclerView.SmoothScroller smoothScroller = + new LinearSmoothScroller(mAllApps.getContext()) { + @Override protected int getVerticalSnapPreference() { + return LinearSmoothScroller.SNAP_TO_END; + } + }; + smoothScroller.setTargetPosition(i); + RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); + if (layoutManager != null) { + layoutManager.startSmoothScroll(smoothScroller); + } + break; + } + // Make the private space apps gone to "collapse". + 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. + */ + 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) { + itemToScrollTo = i; + 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) { + 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; + } + + /** + * Scrolls up to the private space header and animates the collapsing of the text. + */ + private ValueAnimator animateCollapseAnimation() { + float from = 1; + float to = 0; + RecyclerViewFastScroller scrollBar = mAllApps.getActiveRecyclerView().getScrollbar(); + ValueAnimator collapseAnim = ValueAnimator.ofFloat(from, to); + collapseAnim.setDuration(EXPAND_COLLAPSE_DURATION); + collapseAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (scrollBar != null) { + scrollBar.setVisibility(INVISIBLE); + } + // Scroll up to header. + collapse(); + } + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (scrollBar != null) { + scrollBar.setThumbOffsetY(-1); + scrollBar.setVisibility(VISIBLE); + } + } + }); + return collapseAnim; + } + + /** + * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an + * animation. At the moment, collapsing, setting alpha changes, and animating the text is done + * here. + */ + private void updatePrivateStateAnimator(boolean expand, @Nullable ViewGroup psHeader) { + if (psHeader == null) { + return; + } + ViewGroup settingsAndLockGroup = psHeader.findViewById(R.id.settingsAndLockGroup); + ViewGroup lockButton = psHeader.findViewById(R.id.ps_lock_unlock_button); + if (settingsAndLockGroup.getLayoutTransition() == null) { + // Set a new transition if the current ViewGroup does not already contain one as each + // transition should only happen once when applied. + enableLayoutTransition(settingsAndLockGroup); + } + + PropertySetter setter = new AnimatedPropertySetter(); + ImageButton settingsButton = psHeader.findViewById(R.id.ps_settings_button); + updateSettingsGearAlpha(settingsButton, expand, setter); + AnimatorSet animatorSet = setter.buildAnim(); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Animate the collapsing of the text at the same time while updating lock button. + lockButton.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE); + setAnimationRunning(true); + } + }); + animatorSet.addListener(forEndCallback(() -> { + setAnimationRunning(false); + mAnimate = false; + if (!expand) { + // Call onAppsUpdated() because it may be canceled when this animation occurs. + mAllApps.getPersonalAppList().onAppsUpdated(); + } + })); + // Play the collapsing together of the stateAnimator to avoid being unable to scroll to the + // header. Otherwise the smooth scrolling will scroll higher when played with the state + // animator. + if (!expand) { + animatorSet.playTogether(animateCollapseAnimation()); + } + animatorSet.setDuration(EXPAND_COLLAPSE_DURATION); + animatorSet.start(); + } + + /** Animates the layout changes when the text of the button becomes visible/gone. */ + private void enableLayoutTransition(ViewGroup settingsAndLockGroup) { + LayoutTransition settingsAndLockTransition = new LayoutTransition(); + settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING); + settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION); + settingsAndLockTransition.addTransitionListener(new LayoutTransition.TransitionListener() { + @Override + public void startTransition(LayoutTransition transition, ViewGroup viewGroup, + View view, int i) { + } + @Override + public void endTransition(LayoutTransition transition, ViewGroup viewGroup, + View view, int i) { + settingsAndLockGroup.setLayoutTransition(null); + mAnimate = false; + } + }); + settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition); + } + + /** Change the settings gear alpha when expanded or collapsed. */ + private void updateSettingsGearAlpha(ImageButton settingsButton, boolean expand, + PropertySetter setter) { + float toAlpha = expand ? 1 : 0; + setter.setFloat(settingsButton, VIEW_ALPHA, toAlpha, Interpolators.LINEAR) + .setDuration(SETTINGS_OPACITY_DURATION).setStartDelay(0); + } + + void expandPrivateSpace() { + // If we are on main adapter view, we apply the PS Container expansion animation and + // scroll down to load the entire container, making animation visible. + ActivityAllAppsContainerView.AdapterHolder mainAdapterHolder = mAllApps.mAH.get(MAIN); + List adapterItems = + mainAdapterHolder.mAppsList.getAdapterItems(); + if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation() + && mAllApps.isPersonalTab()) { + // Animate the text and settings icon. + DeviceProfile deviceProfile = + ActivityContext.lookupContext(mAllApps.getContext()).getDeviceProfile(); + scrollForViewToBeVisibleInContainer(mainAdapterHolder.mRecyclerView, adapterItems, + getPsHeaderHeight(), deviceProfile.allAppsCellHeightPx); + ViewGroup psHeader = getPsHeader(mainAdapterHolder.mRecyclerView, adapterItems); + updatePrivateStateAnimator(true, psHeader); + } + } + + private void exitSearchAndExpand() { + mAllApps.updateHeaderScroll(0); + // Animate to A-Z with 0 time to reset the animation with proper state management. + mAllApps.animateToSearchState(false, 0); + MAIN_EXECUTOR.post(() -> { + mAllApps.mSearchUiManager.resetSearch(); + mAllApps.switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN); + expandPrivateSpace(); + }); + } + + private void collapsePrivateSpace() { + AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); + AlphabeticalAppsList appList = allAppsRecyclerView.getApps(); + if (appList == null) { + return; + } + ViewGroup psHeader = getPsHeader(allAppsRecyclerView, appList.getAdapterItems()); + assert psHeader != null; + updatePrivateStateAnimator(false, psHeader); + } + + int getPsHeaderHeight() { + return mHeaderHeight; + } + + /** Get the private space header from the adapter items. */ + @Nullable + private ViewGroup getPsHeader(AllAppsRecyclerView allAppsRecyclerView, + List adapterItems){ + ViewGroup psHeader = null; + for (int i = 0; i < adapterItems.size(); i++) { + BaseAllAppsAdapter.AdapterItem currentItem = adapterItems.get(i); + if (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { + RecyclerView.ViewHolder viewHolder = + allAppsRecyclerView.findViewHolderForAdapterPosition(i); + if (viewHolder != null) { + psHeader = (ViewGroup) viewHolder.itemView; + } + } + } + return psHeader; + } } diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java deleted file mode 100644 index fdc035ea1f..0000000000 --- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * 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.allapps; - -import static android.view.View.GONE; -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; - -import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; -import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; -import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER; -import static com.android.launcher3.allapps.PrivateProfileManager.STATE_DISABLED; -import static com.android.launcher3.allapps.PrivateProfileManager.STATE_ENABLED; -import static com.android.launcher3.allapps.PrivateProfileManager.STATE_TRANSITION; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_TAP; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.LayoutTransition; -import android.animation.ValueAnimator; -import android.view.ViewGroup; -import android.widget.ImageButton; -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; - -/** - * Controller which returns views to be added to Private Space Header based upon - * {@link UserProfileState} - */ -public class PrivateSpaceHeaderViewController { - private static final int EXPAND_COLLAPSE_DURATION = 800; - private static final int SETTINGS_OPACITY_DURATION = 160; - private final ActivityAllAppsContainerView mAllApps; - private final PrivateProfileManager mPrivateProfileManager; - - public PrivateSpaceHeaderViewController(ActivityAllAppsContainerView allApps, - PrivateProfileManager privateProfileManager) { - this.mAllApps = allApps; - this.mPrivateProfileManager = privateProfileManager; - } - - /** Add Private Space Header view elements based upon {@link UserProfileState} */ - public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) { - // Set the transition duration for the settings and lock button to animate. - ViewGroup settingsAndLockGroup = parent.findViewById(R.id.settingsAndLockGroup); - LayoutTransition settingsAndLockTransition = settingsAndLockGroup.getLayoutTransition(); - settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING); - settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION); - - //Add quietMode image and action for lock/unlock button - ViewGroup lockButton = - parent.findViewById(R.id.ps_lock_unlock_button); - assert lockButton != null; - addLockButton(parent, lockButton); - - //Trigger lock/unlock action from header. - addHeaderOnClickListener(parent); - - //Add image and action for private space settings button - ImageButton settingsButton = parent.findViewById(R.id.ps_settings_button); - assert settingsButton != null; - addPrivateSpaceSettingsButton(settingsButton); - - //Add image for private space transitioning view - ImageView transitionView = parent.findViewById(R.id.ps_transition_image); - assert transitionView != null; - addTransitionImage(transitionView); - } - - /** - * Adds the quietModeButton and attach onClickListener for the header to animate different - * states when clicked. - */ - private void addLockButton(ViewGroup psHeader, ViewGroup lockButton) { - TextView lockText = lockButton.findViewById(R.id.lock_text); - switch (mPrivateProfileManager.getCurrentState()) { - case STATE_ENABLED -> { - lockText.setVisibility(VISIBLE); - lockButton.setVisibility(VISIBLE); - lockButton.setOnClickListener(view -> lockAction(psHeader)); - } - case STATE_DISABLED -> { - lockText.setVisibility(GONE); - lockButton.setVisibility(VISIBLE); - lockButton.setOnClickListener(view -> unlockAction(psHeader)); - } - default -> lockButton.setVisibility(GONE); - } - } - - private void addHeaderOnClickListener(RelativeLayout header) { - if (mPrivateProfileManager.getCurrentState() == STATE_DISABLED) { - header.setOnClickListener(view -> unlockAction(header)); - } else { - header.setOnClickListener(null); - } - } - - private void unlockAction(ViewGroup psHeader) { - mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP); - mPrivateProfileManager.unlockPrivateProfile((() -> onPrivateProfileUnlocked(psHeader))); - } - - private void lockAction(ViewGroup psHeader) { - mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_LOCK_TAP); - if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()) { - updatePrivateStateAnimator(false, psHeader); - } else { - mPrivateProfileManager.lockPrivateProfile(); - } - } - - private void addPrivateSpaceSettingsButton(ImageButton settingsButton) { - if (mPrivateProfileManager.getCurrentState() == STATE_ENABLED - && mPrivateProfileManager.isPrivateSpaceSettingsAvailable()) { - settingsButton.setVisibility(VISIBLE); - settingsButton.setAlpha(1f); - settingsButton.setOnClickListener( - view -> { - mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP); - mPrivateProfileManager.openPrivateSpaceSettings(); - }); - } else { - settingsButton.setVisibility(GONE); - } - } - - private void addTransitionImage(ImageView transitionImage) { - if (mPrivateProfileManager.getCurrentState() == STATE_TRANSITION) { - transitionImage.setVisibility(VISIBLE); - } else { - transitionImage.setVisibility(GONE); - } - } - - private void onPrivateProfileUnlocked(ViewGroup header) { - // If we are on main adapter view, we apply the PS Container expansion animation and - // then scroll down to load the entire container, making animation visible. - ActivityAllAppsContainerView.AdapterHolder mainAdapterHolder = - (ActivityAllAppsContainerView.AdapterHolder) mAllApps.mAH.get(MAIN); - if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation() - && mAllApps.getActiveRecyclerView() == mainAdapterHolder.mRecyclerView) { - // Animate the text and settings icon. - updatePrivateStateAnimator(true, header); - 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(); - 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 (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { - // Note: SmoothScroller is meant to be used once. - RecyclerView.SmoothScroller smoothScroller = - new LinearSmoothScroller(mAllApps.getContext()) { - @Override protected int getVerticalSnapPreference() { - return LinearSmoothScroller.SNAP_TO_END; - } - }; - smoothScroller.setTargetPosition(i); - RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); - if (layoutManager != null) { - layoutManager.startSmoothScroll(smoothScroller); - } - break; - } - // Make the private space apps gone to "collapse". - 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; - } - - /** - * Scrolls up to the private space header and animates the collapsing of the text. - */ - private ValueAnimator animateCollapseAnimation(ViewGroup lockButton) { - float from = 1; - float to = 0; - RecyclerViewFastScroller scrollBar = mAllApps.getActiveRecyclerView().getScrollbar(); - ValueAnimator collapseAnim = ValueAnimator.ofFloat(from, to); - collapseAnim.setDuration(EXPAND_COLLAPSE_DURATION); - collapseAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - if (scrollBar != null) { - scrollBar.setVisibility(INVISIBLE); - } - // scroll up - collapse(); - // Animate the collapsing of the text. - lockButton.findViewById(R.id.lock_text).setVisibility(GONE); - } - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - if (scrollBar != null) { - scrollBar.setThumbOffsetY(-1); - scrollBar.setVisibility(VISIBLE); - } - mPrivateProfileManager.lockPrivateProfile(); - } - }); - return collapseAnim; - } - - /** - * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an - * animation. At the moment, collapsing, setting alpha changes, and animating the text is done - * here. - */ - private void updatePrivateStateAnimator(boolean expand, ViewGroup psHeader) { - PropertySetter setter = new AnimatedPropertySetter(); - ViewGroup lockButton = psHeader.findViewById(R.id.ps_lock_unlock_button); - ImageButton settingsButton = psHeader.findViewById(R.id.ps_settings_button); - updateSettingsGearAlpha(settingsButton, expand, setter); - AnimatorSet animatorSet = setter.buildAnim(); - animatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - // Animate the collapsing of the text at the same time while updating lock button. - lockButton.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE); - } - }); - // Play the collapsing together of the stateAnimator to avoid being unable to scroll to the - // header. Otherwise the smooth scrolling will scroll higher when played with the state - // animator. - if (!expand) { - animatorSet.playTogether(animateCollapseAnimation(lockButton)); - } - animatorSet.setDuration(EXPAND_COLLAPSE_DURATION); - animatorSet.start(); - } - - /** Change the settings gear alpha when expanded or collapsed. */ - private void updateSettingsGearAlpha(ImageButton settingsButton, boolean expand, - PropertySetter setter) { - float toAlpha = expand ? 1 : 0; - setter.setFloat(settingsButton, VIEW_ALPHA, toAlpha, Interpolators.LINEAR) - .setDuration(SETTINGS_OPACITY_DURATION).setStartDelay(0); - } -} diff --git a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java index eea4fe5f0c..476bda5ada 100644 --- a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java +++ b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java @@ -26,7 +26,7 @@ import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doNothing; @@ -80,7 +80,7 @@ public class PrivateProfileManagerTest { private PrivateProfileManager mPrivateProfileManager; @Mock - private ActivityAllAppsContainerView mActivityAllAppsContainerView; + private ActivityAllAppsContainerView mAllApps; @Mock private StatsLogManager mStatsLogManager; @Mock @@ -90,13 +90,13 @@ public class PrivateProfileManagerTest { @Mock private Context mContext; @Mock - private AllAppsStore mAllAppsStore; + private AllAppsStore mAllAppsStore; @Mock private PackageManager mPackageManager; @Mock private LauncherApps mLauncherApps; - - private boolean mRunnableCalled = false; + @Mock + private AllAppsRecyclerView mAllAppsRecyclerView; @Before public void setUp() { @@ -105,8 +105,9 @@ public class PrivateProfileManagerTest { .thenReturn(Arrays.asList(MAIN_HANDLE, PRIVATE_HANDLE)); when(mUserCache.getUserInfo(Process.myUserHandle())).thenReturn(MAIN_ICON_INFO); when(mUserCache.getUserInfo(PRIVATE_HANDLE)).thenReturn(PRIVATE_ICON_INFO); - when(mActivityAllAppsContainerView.getContext()).thenReturn(mContext); - when(mActivityAllAppsContainerView.getAppsStore()).thenReturn(mAllAppsStore); + when(mAllApps.getContext()).thenReturn(mContext); + when(mAllApps.getAppsStore()).thenReturn(mAllAppsStore); + when(mAllApps.getActiveRecyclerView()).thenReturn(mAllAppsRecyclerView); when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mPackageManager.resolveActivity(any(), any())).thenReturn(new ResolveInfo()); when(mContext.getSystemService(LauncherApps.class)).thenReturn(mLauncherApps); @@ -117,7 +118,7 @@ public class PrivateProfileManagerTest { .thenReturn("com.android.launcher3.tests.privateProfileManager"); when(mLauncherApps.getPreInstalledSystemPackages(any())).thenReturn(new ArrayList<>()); mPrivateProfileManager = new PrivateProfileManager(mUserManager, - mActivityAllAppsContainerView, mStatsLogManager, mUserCache); + mAllApps, mStatsLogManager, mUserCache); } @Test @@ -134,7 +135,7 @@ public class PrivateProfileManagerTest { public void unlockPrivateProfile_requestsQuietModeAsFalse() throws Exception { when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)).thenReturn(true); - mPrivateProfileManager.unlockPrivateProfile(() -> {}); + mPrivateProfileManager.unlockPrivateProfile(); awaitTasksCompleted(); Mockito.verify(mUserManager).requestQuietModeEnabled(false, PRIVATE_HANDLE); @@ -160,20 +161,36 @@ public class PrivateProfileManagerTest { @Test @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/320703862 - public void transitioningToUnlocked_resetCallsPendingRunnable() throws Exception { + public void transitioningToUnlocked_resetCallsPostUnlock() throws Exception { PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); doNothing().when(privateProfileManager).resetPrivateSpaceDecorator(anyInt()); when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)) .thenReturn(false); + doNothing().when(privateProfileManager).expandPrivateSpace(); when(privateProfileManager.getCurrentState()).thenReturn(STATE_DISABLED); - mRunnableCalled = false; - privateProfileManager.unlockPrivateProfile(this::testRunnable); + privateProfileManager.unlockPrivateProfile(); privateProfileManager.reset(); awaitTasksCompleted(); - Mockito.verify(privateProfileManager).applyUnlockRunnable(); - assertTrue("Unlock Runnable not Invoked", mRunnableCalled); + Mockito.verify(privateProfileManager).postUnlock(); + } + + @Test + @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) + public void transitioningToLocked_resetCallsExecuteLock() throws Exception { + PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); + doNothing().when(privateProfileManager).resetPrivateSpaceDecorator(anyInt()); + when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)) + .thenReturn(true); + doNothing().when(privateProfileManager).expandPrivateSpace(); + when(privateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + + privateProfileManager.lockPrivateProfile(); + privateProfileManager.reset(); + + awaitTasksCompleted(); + Mockito.verify(privateProfileManager).executeLock(); } @Test @@ -191,8 +208,4 @@ public class PrivateProfileManagerTest { private static void awaitTasksCompleted() throws Exception { UI_HELPER_EXECUTOR.submit(() -> null).get(); } - - private void testRunnable() { - mRunnableCalled = true; - } } diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java similarity index 76% rename from tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java rename to tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java index 043461d4d9..7ac276acb3 100644 --- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java +++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java @@ -28,6 +28,10 @@ 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.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.content.ComponentName; @@ -39,6 +43,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Process; import android.os.UserHandle; +import android.os.UserManager; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageButton; @@ -51,8 +56,11 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.R; +import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.ActivityContextWrapper; +import com.android.launcher3.util.UserIconInfo; import org.junit.Before; import org.junit.Test; @@ -61,14 +69,20 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) -public class PrivateSpaceHeaderViewControllerTest { +public class PrivateSpaceHeaderViewTest { private static final UserHandle MAIN_HANDLE = Process.myUserHandle(); private static final UserHandle PRIVATE_HANDLE = new UserHandle(11); + private static final UserIconInfo MAIN_ICON_INFO = + new UserIconInfo(MAIN_HANDLE, UserIconInfo.TYPE_MAIN); + private static final UserIconInfo PRIVATE_ICON_INFO = + new UserIconInfo(PRIVATE_HANDLE, UserIconInfo.TYPE_PRIVATE); + private static final String CAMERA_PACKAGE_NAME = "com.android.launcher3.tests.camera"; 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; @@ -84,37 +98,41 @@ public class PrivateSpaceHeaderViewControllerTest { 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; + @Mock + private UserCache mUserCache; + @Mock + private UserManager mUserManager; + @Mock + private StatsLogManager mStatsLogManager; @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); + when(mAllApps.getContext()).thenReturn(mContext); + when(mUserCache.getUserInfo(PRIVATE_HANDLE)).thenReturn(PRIVATE_ICON_INFO); + when(mUserCache.getUserProfiles()) + .thenReturn(Arrays.asList(MAIN_HANDLE, PRIVATE_HANDLE)); + when(mUserCache.getUserInfo(Process.myUserHandle())).thenReturn(MAIN_ICON_INFO); + mPrivateProfileManager = new PrivateProfileManager(mUserManager, + mAllApps, mStatsLogManager, mUserCache); 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 public void privateProfileDisabled_psHeaderContainsLockedView() throws Exception { Bitmap unlockButton = getBitmap(mContext.getDrawable(R.drawable.ic_lock)); - when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_DISABLED); - - mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); + PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); + when(privateProfileManager.getCurrentState()).thenReturn(STATE_DISABLED); + privateProfileManager.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); awaitTasksCompleted(); int totalContainerHeaderView = 0; @@ -137,6 +155,7 @@ public class PrivateSpaceHeaderViewControllerTest { assertEquals(View.GONE, view.getVisibility()); } } + assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView); assertEquals(LOCK_UNLOCK_BUTTON_COUNT, totalLockUnlockButtonView); } @@ -145,10 +164,10 @@ public class PrivateSpaceHeaderViewControllerTest { public void privateProfileEnabled_psHeaderContainsUnlockedView() throws Exception { Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.ic_lock)); Bitmap settingsImage = getBitmap(mContext.getDrawable(R.drawable.ic_ps_settings)); - when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); - when(mPrivateProfileManager.isPrivateSpaceSettingsAvailable()).thenReturn(true); - - mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); + PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); + when(privateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(privateProfileManager.isPrivateSpaceSettingsAvailable()).thenReturn(true); + privateProfileManager.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); awaitTasksCompleted(); int totalContainerHeaderView = 0; @@ -177,6 +196,7 @@ public class PrivateSpaceHeaderViewControllerTest { assertEquals(View.GONE, view.getVisibility()); } } + assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView); assertEquals(LOCK_UNLOCK_BUTTON_COUNT, totalLockUnlockButtonView); assertEquals(PS_SETTINGS_BUTTON_COUNT_VISIBLE, totalSettingsImageView); @@ -186,10 +206,10 @@ public class PrivateSpaceHeaderViewControllerTest { public void privateProfileEnabledAndNoSettingsIntent_psHeaderContainsUnlockedView() throws Exception { Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.ic_lock)); - when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); - when(mPrivateProfileManager.isPrivateSpaceSettingsAvailable()).thenReturn(false); - - mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); + PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); + when(privateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(privateProfileManager.isPrivateSpaceSettingsAvailable()).thenReturn(false); + privateProfileManager.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); awaitTasksCompleted(); int totalContainerHeaderView = 0; @@ -216,6 +236,7 @@ public class PrivateSpaceHeaderViewControllerTest { assertEquals(View.GONE, view.getVisibility()); } } + assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView); assertEquals(LOCK_UNLOCK_BUTTON_COUNT, totalLockUnlockButtonView); assertEquals(PS_SETTINGS_BUTTON_COUNT_INVISIBLE, totalSettingsImageView); @@ -224,9 +245,9 @@ public class PrivateSpaceHeaderViewControllerTest { @Test public void privateProfileTransitioning_psHeaderContainsTransitionView() throws Exception { Bitmap transitionImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_transition_image)); - when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_TRANSITION); - - mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); + PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); + when(privateProfileManager.getCurrentState()).thenReturn(STATE_TRANSITION); + privateProfileManager.addPrivateSpaceHeaderViewElements(mPsHeaderLayout); awaitTasksCompleted(); int totalContainerHeaderView = 0; @@ -248,6 +269,7 @@ public class PrivateSpaceHeaderViewControllerTest { assertEquals(View.GONE, view.getVisibility()); } } + assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView); assertEquals(PS_TRANSITION_IMAGE_COUNT, totalLockUnlockButtonView); } @@ -255,18 +277,25 @@ public class PrivateSpaceHeaderViewControllerTest { @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()) + PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); + when(privateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(privateProfileManager.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)); + .equals(CAMERA_PACKAGE_NAME)); + doReturn(0).when(privateProfileManager).addPrivateSpaceHeader(any()); + doAnswer(answer(this::addPrivateSpaceHeader)).when(privateProfileManager) + .addPrivateSpaceHeader(any()); + doNothing().when(privateProfileManager).addPrivateSpaceInstallAppButton(any()); + doReturn(0).when(privateProfileManager).addSystemAppsDivider(any()); when(mAllApps.getHeight()).thenReturn(ALL_APPS_HEIGHT); when(mAllApps.getHeaderProtectionHeight()).thenReturn(HEADER_PROTECTION_HEIGHT); + mAlphabeticalAppsList = new AlphabeticalAppsList<>(mContext, mAllAppsStore, + null, privateProfileManager); + mAlphabeticalAppsList.setNumAppsPerRowAllApps(NUM_APP_COLS); + mAlphabeticalAppsList.updateItemFilter(info -> info != null + && info.user.equals(MAIN_HANDLE)); + int rows = (int) (ALL_APPS_HEIGHT - PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT); int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1; @@ -274,7 +303,7 @@ public class PrivateSpaceHeaderViewControllerTest { assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1, mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(position, - mPsHeaderViewController.scrollForViewToBeVisibleInContainer( + privateProfileManager.scrollForViewToBeVisibleInContainer( new AllAppsRecyclerView(mContext), mAlphabeticalAppsList.getAdapterItems(), PS_HEADER_HEIGHT, @@ -284,18 +313,25 @@ public class PrivateSpaceHeaderViewControllerTest { @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()) + PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); + when(privateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(privateProfileManager.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)); + .equals(CAMERA_PACKAGE_NAME)); + doReturn(0).when(privateProfileManager).addPrivateSpaceHeader(any()); + doAnswer(answer(this::addPrivateSpaceHeader)).when(privateProfileManager) + .addPrivateSpaceHeader(any()); + doNothing().when(privateProfileManager).addPrivateSpaceInstallAppButton(any()); + doReturn(0).when(privateProfileManager).addSystemAppsDivider(any()); when(mAllApps.getHeight()).thenReturn(ALL_APPS_HEIGHT); when(mAllApps.getHeaderProtectionHeight()).thenReturn(HEADER_PROTECTION_HEIGHT); + mAlphabeticalAppsList = new AlphabeticalAppsList<>(mContext, mAllAppsStore, + null, privateProfileManager); + mAlphabeticalAppsList.setNumAppsPerRowAllApps(NUM_APP_COLS); + mAlphabeticalAppsList.updateItemFilter(info -> info != null + && info.user.equals(MAIN_HANDLE)); + int rows = (int) (ALL_APPS_HEIGHT - BIGGER_PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT); int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1; @@ -303,7 +339,7 @@ public class PrivateSpaceHeaderViewControllerTest { assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1, mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(position, - mPsHeaderViewController.scrollForViewToBeVisibleInContainer( + privateProfileManager.scrollForViewToBeVisibleInContainer( new AllAppsRecyclerView(mContext), mAlphabeticalAppsList.getAdapterItems(), BIGGER_PS_HEADER_HEIGHT, @@ -313,21 +349,27 @@ public class PrivateSpaceHeaderViewControllerTest { @Test public void scrollForViewToBeVisibleInContainer_withNoHeader() { when(mAllAppsStore.getApps()).thenReturn(createAppInfoList()); - when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); - when(mPrivateProfileManager.splitIntoUserInstalledAndSystemApps()) + PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); + when(privateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED); + when(privateProfileManager.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)); + .equals(CAMERA_PACKAGE_NAME)); + doReturn(0).when(privateProfileManager).addPrivateSpaceHeader(any()); + doNothing().when(privateProfileManager).addPrivateSpaceInstallAppButton(any()); + doReturn(0).when(privateProfileManager).addSystemAppsDivider(any()); when(mAllApps.getHeight()).thenReturn(ALL_APPS_HEIGHT); when(mAllApps.getHeaderProtectionHeight()).thenReturn(HEADER_PROTECTION_HEIGHT); + mAlphabeticalAppsList = new AlphabeticalAppsList<>(mContext, mAllAppsStore, + null, privateProfileManager); + mAlphabeticalAppsList.setNumAppsPerRowAllApps(NUM_APP_COLS); + mAlphabeticalAppsList.updateItemFilter(info -> info != null + && info.user.equals(MAIN_HANDLE)); // 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( + assertEquals(SCROLL_NO_WHERE, privateProfileManager.scrollForViewToBeVisibleInContainer( new AllAppsRecyclerView(mContext), mAlphabeticalAppsList.getAdapterItems(), BIGGER_PS_HEADER_HEIGHT, @@ -376,7 +418,7 @@ public class PrivateSpaceHeaderViewControllerTest { AppInfo(gmailComponentName, "Gmail", MAIN_HANDLE, new Intent()); appInfos.add(gmailAppInfo); ComponentName privateCameraComponentName = new ComponentName( - "com.android.launcher3.tests.camera", "CameraActivity"); + CAMERA_PACKAGE_NAME, "CameraActivity"); for (int i = 0; i < NUM_PRIVATE_SPACE_APPS; i++) { AppInfo privateCameraAppInfo = new AppInfo(privateCameraComponentName, "Private Camera " + i, PRIVATE_HANDLE, new Intent());