| /* |
| * 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<UserHandle> 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<BaseAllAppsAdapter.AdapterItem> 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<BaseAllAppsAdapter.AdapterItem> 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<BaseAllAppsAdapter.AdapterItem> 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. |
| * <p> |
| * 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<UserHandle> 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<AppInfo> splitIntoUserInstalledAndSystemApps(Context context) { |
| List<String> 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<BaseAllAppsAdapter.AdapterItem> 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<BaseAllAppsAdapter.AdapterItem> appListAdapterItems, |
| int psHeaderHeight, |
| int allAppsCellHeight) { |
| int rowToExpandToWithRespectToHeader = -1; |
| int itemToScrollTo = -1; |
| // Looks for the item in the app list to scroll to so that the header is visible. |
| for (int i = 0; i < appListAdapterItems.size(); i++) { |
| BaseAllAppsAdapter.AdapterItem currentItem = appListAdapterItems.get(i); |
| if (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { |
| itemToScrollTo = i; |
| continue; |
| } |
| if (itemToScrollTo != -1) { |
| 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<BaseAllAppsAdapter.AdapterItem> 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<BaseAllAppsAdapter.AdapterItem> 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<BaseAllAppsAdapter.AdapterItem> 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); |
| } |
| } |