blob: 6f021eacf7e690d946f4af17fdaf69d7daa5b322 [file] [log] [blame]
/*
* 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);
}
}