/* * 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.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_ANIMATION_BEGIN; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END; 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.ObjectAnimator; 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.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.constraintlayout.widget.ConstraintLayout; 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; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.PrivateSpaceInstallAppButtonInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.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.List; import java.util.function.Predicate; /** * Companion class for {@link ActivityAllAppsContainerView} to manage private space section related * 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 = 400; private static final int TEXT_UNLOCK_OPACITY_DURATION = 300; private static final int TEXT_LOCK_OPACITY_DURATION = 50; private static final int APP_OPACITY_DURATION = 400; private static final int MASK_VIEW_DURATION = 200; private static final int APP_OPACITY_DELAY = 400; private static final int SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY = 400; private static final int SETTINGS_OPACITY_DELAY = 400; private static final int LOCK_TEXT_OPACITY_DELAY = 500; private static final int MASK_VIEW_DELAY = 400; private static final int NO_DELAY = 0; private static final int CONTAINER_OPACITY_DURATION = 150; private final ActivityAllAppsContainerView mAllApps; private final Predicate mPrivateProfileMatcher; private final int mPsHeaderHeight; private final int mFloatingMaskViewCornerRadius; private final RecyclerView.OnScrollListener mOnIdleScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE) { mIsScrolling = false; } } }; private Intent mAppInstallerIntent = new Intent(); private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator; private boolean mPrivateSpaceSettingsAvailable; // Returns if the animation is currently running. private boolean mIsAnimationRunning; // mAnimate denotes if private space is ready to be animated. private boolean mReadyToAnimate; // Returns when the recyclerView is currently scrolling. private boolean mIsScrolling; // mIsStateTransitioning indicates that private space is transitioning between states. private boolean mIsStateTransitioning; private Runnable mOnPSHeaderAdded; @Nullable private RelativeLayout mPSHeader; private ConstraintLayout mFloatingMaskView; private final String mLockedStateContentDesc; private final String mUnLockedStateContentDesc; public PrivateProfileManager(UserManager userManager, ActivityAllAppsContainerView allApps, StatsLogManager statsLogManager, UserCache userCache) { super(userManager, statsLogManager, userCache); mAllApps = allApps; mPrivateProfileMatcher = (user) -> userCache.getUserInfo(user).isPrivate(); Context appContext = allApps.getContext().getApplicationContext(); UI_HELPER_EXECUTOR.post(() -> initializeInBackgroundThread(appContext)); mPsHeaderHeight = mAllApps.getContext().getResources().getDimensionPixelSize( R.dimen.ps_header_height); mLockedStateContentDesc = mAllApps.getContext() .getString(R.string.ps_container_lock_button_content_description); mUnLockedStateContentDesc = mAllApps.getContext() .getString(R.string.ps_container_unlock_button_content_description); mFloatingMaskViewCornerRadius = mAllApps.getContext().getResources().getDimensionPixelSize( R.dimen.ps_floating_mask_corner_radius); } /** Adds Private Space Header to the layout. */ public int addPrivateSpaceHeader(ArrayList adapterItems) { adapterItems.add(new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_PRIVATE_SPACE_HEADER)); mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1); return adapterItems.size(); } /** Adds Private Space System Apps Divider to the layout. */ public int addSystemAppsDivider(List adapterItems) { adapterItems.add(new BaseAllAppsAdapter .AdapterItem(VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER)); mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1); return adapterItems.size(); } /** Adds Private Space install app button to the layout. */ public void addPrivateSpaceInstallAppButton(List adapterItems) { Context context = mAllApps.getContext(); // Prepare bitmapInfo Intent.ShortcutIconResource shortcut = Intent.ShortcutIconResource.fromContext( context, com.android.launcher3.R.drawable.private_space_install_app_icon); BitmapInfo bitmapInfo = LauncherIcons.obtain(context).createIconBitmap(shortcut); PrivateSpaceInstallAppButtonInfo itemInfo = new PrivateSpaceInstallAppButtonInfo(); itemInfo.title = context.getResources().getString(R.string.ps_add_button_label); itemInfo.intent = mAppInstallerIntent; itemInfo.bitmap = bitmapInfo; itemInfo.contentDescription = context.getResources().getString( com.android.launcher3.R.string.ps_add_button_content_description); itemInfo.runtimeStatusFlags |= FLAG_NOT_PINNABLE; BaseAllAppsAdapter.AdapterItem item = new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_ICON); item.itemInfo = itemInfo; item.decorationInfo = new SectionDecorationInfo(context, ROUND_NOTHING, /* decorateTogether */ true); adapterItems.add(item); mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1); } /** Whether private profile should be hidden on Launcher. */ public boolean isPrivateSpaceHidden() { return getCurrentState() == STATE_DISABLED && SettingsCache.INSTANCE .get(mAllApps.getContext()).getValue(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, 0); } /** * Resets the current state of Private Profile, w.r.t. to Launcher. The decorator should only * be applied upon expand before animating. When collapsing, reset() will remove the decorator * when animation is not running. */ public void reset() { // Ensure the state of the header views is what it should be before animating. updateView(); getMainRecyclerView().setChildAttachedConsumer(null); int previousState = getCurrentState(); boolean isEnabled = !mAllApps.getAppsStore() .hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED); int updatedState = isEnabled ? STATE_ENABLED : STATE_DISABLED; setCurrentState(updatedState); if (Flags.privateSpaceAddFloatingMaskView()) { mFloatingMaskView = null; } // It's possible that previousState is 0 when reset is first called. mIsStateTransitioning = previousState != STATE_UNKNOWN && previousState != updatedState; if (previousState == STATE_DISABLED && updatedState == STATE_ENABLED) { postUnlock(); } else if (previousState == STATE_ENABLED && updatedState == STATE_DISABLED){ executeLock(); } addPrivateSpaceDecorator(updatedState); } /** Returns whether or not Private Space Settings Page is available. */ public boolean isPrivateSpaceSettingsAvailable() { return mPrivateSpaceSettingsAvailable; } /** Sets whether Private Space Settings Page is available. */ public boolean setPrivateSpaceSettingsAvailable(boolean value) { return mPrivateSpaceSettingsAvailable = value; } /** Initializes binder call based properties in non-main thread. *

* This can cause the Private Space container items to not load/respond correctly sometimes, * when the All Apps Container loads for the first time (device restarts, new profiles * added/removed, etc.), as the properties are being set in non-ui thread whereas the container * loads in the ui thread. * This case should still be ok, as locking the Private Space container and unlocking it, * reloads the values, fixing the incorrect UI. */ private void initializeInBackgroundThread(Context appContext) { Preconditions.assertNonUiThread(); ApiWrapper apiWrapper = ApiWrapper.INSTANCE.get(appContext); UserHandle profileUser = getProfileUser(); if (profileUser != null) { mAppInstallerIntent = apiWrapper .getAppMarketActivityIntent(BuildConfig.APPLICATION_ID, profileUser); } setPrivateSpaceSettingsAvailable(apiWrapper.getPrivateSpaceSettingsIntent() != null); } /** Adds a private space decorator only when STATE_ENABLED. */ @VisibleForTesting void addPrivateSpaceDecorator(int updatedState) { ActivityAllAppsContainerView.AdapterHolder mainAdapterHolder = mAllApps.mAH.get(MAIN); if (updatedState == STATE_ENABLED) { // Create a new decorator instance if not already available. if (mPrivateAppsSectionDecorator == null) { mPrivateAppsSectionDecorator = new PrivateAppsSectionDecorator( mainAdapterHolder.mAppsList); } for (int i = 0; i < mainAdapterHolder.mRecyclerView.getItemDecorationCount(); i++) { if (mainAdapterHolder.mRecyclerView.getItemDecorationAt(i) .equals(mPrivateAppsSectionDecorator)) { // No need to add another decorator if one is already present in recycler view. return; } } // Add Private Space Decorator to the Recycler view. mainAdapterHolder.mRecyclerView.addItemDecoration(mPrivateAppsSectionDecorator); } } @Override public void setQuietMode(boolean enable) { UI_HELPER_EXECUTOR.post(() -> mUserCache.getUserProfiles() .stream() .filter(getUserMatcher()) .findFirst() .ifPresent(userHandle -> setQuietModeSafely(enable, userHandle))); mReadyToAnimate = true; } /** * Sets Quiet Mode for Private Profile. * If {@link SecurityException} is thrown, prompts the user to set this launcher as HOME app. */ private void setQuietModeSafely(boolean enable, UserHandle userHandle) { try { mUserManager.requestQuietModeEnabled(enable, userHandle); } catch (SecurityException ex) { ApiWrapper.INSTANCE.get(mAllApps.mActivityContext) .assignDefaultHomeRole(mAllApps.mActivityContext); } } /** * 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(() -> updatePrivateStateAnimator(false)); } void setAnimationRunning(boolean isAnimationRunning) { if (!isAnimationRunning) { mReadyToAnimate = false; } mIsAnimationRunning = isAnimationRunning; } boolean getAnimationRunning() { return mIsAnimationRunning; } @Override public Predicate getUserMatcher() { return mPrivateProfileMatcher; } /** * Splits private apps into user installed and system apps. * When the list of system apps is empty, all apps are treated as system. */ public Predicate splitIntoUserInstalledAndSystemApps(Context context) { List preInstallApps = UserCache.getInstance(context) .getPreInstallApps(getProfileUser()); return appInfo -> !preInstallApps.isEmpty() && (appInfo.componentName == null || !(preInstallApps.contains(appInfo.componentName.getPackageName()))); } /** Add Private Space Header view elements based upon {@link UserProfileState} */ public void bindPrivateSpaceHeaderViewElements(RelativeLayout parent) { mPSHeader = parent; if (mOnPSHeaderAdded != null) { MAIN_EXECUTOR.execute(mOnPSHeaderAdded); mOnPSHeaderAdded = null; } // Set the transition duration for the settings and lock button to animate. ViewGroup settingAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup); if (mReadyToAnimate) { enableLayoutTransition(settingAndLockGroup); } else { // Ensure any unwanted animations to not happen. settingAndLockGroup.setLayoutTransition(null); } updateView(); } /** Update the states of the views that make up the header at the state it is called in. */ private void updateView() { if (mPSHeader == null) { return; } mPSHeader.setAlpha(1); ViewGroup lockPill = mPSHeader.findViewById(R.id.ps_lock_unlock_button); assert lockPill != null; TextView lockText = lockPill.findViewById(R.id.lock_text); PrivateSpaceSettingsButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button); assert settingsButton != null; //Add image for private space transitioning view ImageView transitionView = mPSHeader.findViewById(R.id.ps_transition_image); assert transitionView != null; switch(getCurrentState()) { case STATE_ENABLED -> { mPSHeader.setOnClickListener(null); mPSHeader.setClickable(false); // Remove header from accessibility target when enabled. mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); lockText.setVisibility(VISIBLE); lockPill.setVisibility(VISIBLE); lockPill.setOnClickListener(view -> lockingAction(/* lock */ true)); lockPill.setContentDescription(mUnLockedStateContentDesc); settingsButton.setVisibility(isPrivateSpaceSettingsAvailable() ? VISIBLE : GONE); transitionView.setVisibility(GONE); } case STATE_DISABLED -> { mPSHeader.setOnClickListener(view -> lockingAction(/* lock */ false)); mPSHeader.setClickable(true); // Add header as accessibility target when disabled. mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); mPSHeader.setContentDescription(mLockedStateContentDesc); lockText.setVisibility(GONE); lockPill.setVisibility(VISIBLE); lockPill.setOnClickListener(view -> lockingAction(/* lock */ false)); lockPill.setContentDescription(mLockedStateContentDesc); settingsButton.setVisibility(GONE); transitionView.setVisibility(GONE); } case STATE_TRANSITION -> { transitionView.setVisibility(VISIBLE); lockPill.setVisibility(GONE); } } } /** 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); setQuietMode(lock); } /** 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; } }; // If privateSpaceHidden() then the entire container decorator will be invisible and // we can directly move to an element above the header. There should always be one // element, as PS is present in the bottom of All Apps. smoothScroller.setTargetPosition(isPrivateSpaceHidden() ? i - 1 : i); RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); if (layoutManager != null) { startAnimationScroll(allAppsRecyclerView, layoutManager, smoothScroller); // Preserve decorator if floating mask view exists. if (mFloatingMaskView == null) { currentItem.decorationInfo = null; } } break; } // Make the private space apps gone to "collapse". if (mFloatingMaskView == null && isPrivateSpaceItem(currentItem)) { RecyclerView.ViewHolder viewHolder = allAppsRecyclerView.findViewHolderForAdapterPosition(i); if (viewHolder != null) { viewHolder.itemView.setVisibility(GONE); currentItem.decorationInfo = null; } } } } /** * Upon expanding, only scroll to the item position in the adapter that allows the header to be * visible. */ public int scrollForHeaderToBeVisibleInContainer( 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; } // If there are no tabs, decrease the row to scroll to by 1 since the header // may be cut off slightly. int rowToScrollTo = (int) Math.floor((double) (mAllApps.getHeight() - psHeaderHeight - mAllApps.getHeaderProtectionHeight()) / allAppsCellHeight) - (mAllApps.isUsingTabs() ? 0 : 1); 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) { startAnimationScroll(allAppsRecyclerView, layoutManager, 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; } private ValueAnimator animateAlphaOfIcons(boolean isExpanding) { float from = isExpanding ? 0 : 1; float to = isExpanding ? 1 : 0; AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); List allAppsAdapterItems = mAllApps.getActiveRecyclerView().getApps().getAdapterItems(); ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to); alphaAnim.setDuration(APP_OPACITY_DURATION) .setStartDelay(isExpanding ? APP_OPACITY_DELAY : NO_DELAY); alphaAnim.setInterpolator(Interpolators.LINEAR); alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float newAlpha = (float) valueAnimator.getAnimatedValue(); for (int i = 0; i < allAppsAdapterItems.size(); i++) { BaseAllAppsAdapter.AdapterItem currentItem = allAppsAdapterItems.get(i); // When not hidden: Fade all PS items except header. // When hidden: Fade all items. if (isPrivateSpaceItem(currentItem) && (currentItem.viewType != VIEW_TYPE_PRIVATE_SPACE_HEADER || isPrivateSpaceHidden())) { RecyclerView.ViewHolder viewHolder = allAppsRecyclerView.findViewHolderForAdapterPosition(i); if (viewHolder != null) { viewHolder.itemView.setAlpha(newAlpha); } } } } }); return alphaAnim; } /** * 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) { if (!Flags.enablePrivateSpace() || !Flags.privateSpaceAnimation()) { return; } if (mPSHeader == null) { mOnPSHeaderAdded = () -> updatePrivateStateAnimator(expand); setAnimationRunning(false); return; } attachFloatingMaskView(expand); ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup); 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); } settingsAndLockGroup.getLayoutTransition().setStartDelay( LayoutTransition.CHANGING, expand ? SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY : NO_DELAY); PropertySetter headerSetter = new AnimatedPropertySetter(); headerSetter.add(updateSettingsGearAlpha(expand)); headerSetter.add(updateLockTextAlpha(expand)); AnimatorSet animatorSet = headerSetter.buildAnim(); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mStatsLogManager.logger().sendToInteractionJankMonitor( expand ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN, mAllApps.getActiveRecyclerView()); // Animate the collapsing of the text at the same time while updating lock button. mPSHeader.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE); setAnimationRunning(true); } @Override public void onAnimationEnd(Animator animation) { detachFloatingMaskView(); } }); animatorSet.addListener(forEndCallback(() -> { mIsStateTransitioning = false; setAnimationRunning(false); getMainRecyclerView().setChildAttachedConsumer(child -> child.setAlpha(1)); mStatsLogManager.logger().sendToInteractionJankMonitor( expand ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END, mAllApps.getActiveRecyclerView()); if (!expand) { mAllApps.mAH.get(MAIN).mRecyclerView.removeItemDecoration( mPrivateAppsSectionDecorator); // Call onAppsUpdated() because it may be canceled when this animation occurs. mAllApps.getPersonalAppList().onAppsUpdated(); if (isPrivateSpaceHidden()) { // TODO (b/325455879): Figure out if we can avoid this. getMainRecyclerView().getAdapter().notifyDataSetChanged(); } } })); if (expand) { animatorSet.playTogether(animateAlphaOfIcons(true), translateFloatingMaskView(false)); } else { if (isPrivateSpaceHidden()) { animatorSet.playSequentially(animateAlphaOfIcons(false), animateAlphaOfPrivateSpaceContainer(), animateCollapseAnimation()); } else { animatorSet.playSequentially(translateFloatingMaskView(true), animateAlphaOfIcons(false), animateCollapseAnimation()); } } animatorSet.start(); } /** Fades out the private space container (defined by its items' decorators). */ private ValueAnimator animateAlphaOfPrivateSpaceContainer() { int from = 255; // 100% opacity. int to = 0; // No opacity. ValueAnimator alphaAnim = ObjectAnimator.ofInt(from, to); AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); List allAppsAdapterItems = allAppsRecyclerView.getApps().getAdapterItems(); alphaAnim.setDuration(CONTAINER_OPACITY_DURATION); alphaAnim.addUpdateListener(valueAnimator -> { for (BaseAllAppsAdapter.AdapterItem currentItem : allAppsAdapterItems) { if (isPrivateSpaceItem(currentItem)) { currentItem.setDecorationFillAlpha((int) valueAnimator.getAnimatedValue()); } } // Invalidate the parent view, to redraw the decorations with changed alpha. allAppsRecyclerView.invalidate(); }); return alphaAnim; } /** Fades out the private space container. */ private ValueAnimator translateFloatingMaskView(boolean animateIn) { if (!Flags.privateSpaceAddFloatingMaskView() || mFloatingMaskView == null) { return new ValueAnimator(); } // Translate base on the height amount. Translates out on expand and in on collapse. float floatingMaskViewHeight = getFloatingMaskViewHeight(); float from = animateIn ? floatingMaskViewHeight : 0; float to = animateIn ? 0 : floatingMaskViewHeight; ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to); alphaAnim.setDuration(MASK_VIEW_DURATION); alphaAnim.setStartDelay(MASK_VIEW_DELAY); alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mFloatingMaskView.setTranslationY((float) valueAnimator.getAnimatedValue()); } }); return alphaAnim; } /** 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.setInterpolator(LayoutTransition.CHANGING, Interpolators.STANDARD); 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); mReadyToAnimate = false; } }); settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition); } /** Change the settings gear alpha when expanded or collapsed. */ private ValueAnimator updateSettingsGearAlpha(boolean expand) { if (mPSHeader == null) { return new ValueAnimator(); } float from = expand ? 0 : 1; float to = expand ? 1 : 0; ValueAnimator settingsAlphaAnim = ObjectAnimator.ofFloat(from, to); settingsAlphaAnim.setDuration(SETTINGS_OPACITY_DURATION); settingsAlphaAnim.setStartDelay(expand ? SETTINGS_OPACITY_DELAY : NO_DELAY); settingsAlphaAnim.setInterpolator(Interpolators.LINEAR); settingsAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mPSHeader.findViewById(R.id.ps_settings_button) .setAlpha((float) valueAnimator.getAnimatedValue()); } }); return settingsAlphaAnim; } private ValueAnimator updateLockTextAlpha(boolean expand) { if (mPSHeader == null) { return new ValueAnimator(); } float from = expand ? 0 : 1; float to = expand ? 1 : 0; ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to); alphaAnim.setDuration(expand ? TEXT_UNLOCK_OPACITY_DURATION : TEXT_LOCK_OPACITY_DURATION); alphaAnim.setStartDelay(expand ? LOCK_TEXT_OPACITY_DELAY : NO_DELAY); alphaAnim.setInterpolator(Interpolators.LINEAR); alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mPSHeader.findViewById(R.id.lock_text).setAlpha( (float) valueAnimator.getAnimatedValue()); } }); return alphaAnim; } 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(); scrollForHeaderToBeVisibleInContainer(mainAdapterHolder.mRecyclerView, adapterItems, getPsHeaderHeight(), deviceProfile.allAppsCellHeightPx); updatePrivateStateAnimator(true); } } 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 attachFloatingMaskView(boolean expand) { if (!Flags.privateSpaceAddFloatingMaskView()) { return; } mFloatingMaskView = (FloatingMaskView) mAllApps.getLayoutInflater().inflate( R.layout.private_space_mask_view, mAllApps, false); mAllApps.addView(mFloatingMaskView); // Translate off the screen first if its collapsing so this header view isn't visible to // user when animation starts. if (!expand) { mFloatingMaskView.setTranslationY(getFloatingMaskViewHeight()); } mFloatingMaskView.setVisibility(VISIBLE); } private void detachFloatingMaskView() { if (mFloatingMaskView != null) { mAllApps.removeView(mFloatingMaskView); } mFloatingMaskView = null; } /** Starts the smooth scroll with the provided smoothScroller and add idle listener. */ private void startAnimationScroll(AllAppsRecyclerView allAppsRecyclerView, RecyclerView.LayoutManager layoutManager, RecyclerView.SmoothScroller smoothScroller) { mIsScrolling = true; layoutManager.startSmoothScroll(smoothScroller); allAppsRecyclerView.removeOnScrollListener(mOnIdleScrollListener); allAppsRecyclerView.addOnScrollListener(mOnIdleScrollListener); } private float getFloatingMaskViewHeight() { return mFloatingMaskViewCornerRadius + getMainRecyclerView().getPaddingBottom(); } AllAppsRecyclerView getMainRecyclerView() { return mAllApps.mAH.get(ActivityAllAppsContainerView.AdapterHolder.MAIN).mRecyclerView; } /** Returns if private space is readily available to be animated. */ boolean getReadyToAnimate() { return mReadyToAnimate; } /** Returns when a smooth scroll is happening. */ boolean isScrolling() { return mIsScrolling; } /** * Returns when private space is in the process of transitioning. This is different from * getAnimate() since mStateTransitioning checks from the time transitioning starts happening * in reset() as oppose to when private space is animating. This should be used to ensure * Private Space state during onBind(). */ boolean isStateTransitioning() { return mIsStateTransitioning; } int getPsHeaderHeight() { return mPsHeaderHeight; } boolean isPrivateSpaceItem(BaseAllAppsAdapter.AdapterItem item) { return getItemInfoMatcher().test(item.itemInfo) || item.decorationInfo != null || (item.itemInfo instanceof PrivateSpaceInstallAppButtonInfo); } }