mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-17 09:38:19 +00:00
Need to create a bug-fix flag since feature flags can't reach nextfood in time for V. We do this by renaming the current flag. According to https://g3doc.corp.google.com/company/teams/android-core-experiments/trunk-stable/gantry-stages/bug-fix.md?cl=head After a flag declaration has been merged, it's no longer possible to change its purpose. If the purpose of a flag changes over time, simply rename the flag; this will create a new workflow and invalidate the previous workflow. In cases where renaming is not possible, please file a bug for the ACE oncall. Even when re-using the name of a flag for a new purpose, please note that all the progress in the pre-existing workflow will be lost; the new bug-fix workflow will start from scratch. bug: 339850589 Test: Presubmit/locally adb shell device_config put launcher_search com.android.launcher3.private_space_add_floating_mask_view true Flag: ACONFIG com.android.launcher3.private_space_add_floating_mask_view STAGING Change-Id: Iffbc6a93c4ef6bbe121e0c8fb83250174f367b9d
907 lines
41 KiB
Java
907 lines
41 KiB
Java
/*
|
|
* 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.LauncherSettings.Favorites.CONTAINER_PRIVATESPACE;
|
|
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_SETTINGS_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.ImageButton;
|
|
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 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) {
|
|
mAnimationScrolling = false;
|
|
}
|
|
}
|
|
};
|
|
private Intent mAppInstallerIntent = new Intent();
|
|
private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator;
|
|
private boolean mPrivateSpaceSettingsAvailable;
|
|
private boolean mIsAnimationRunning;
|
|
private boolean mAnimate;
|
|
private boolean mAnimationScrolling;
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Disables quiet mode for Private Space User Profile.
|
|
* When called from search, a runnable is set and executed in the {@link #reset()} method, when
|
|
* Launcher receives update about profile availability.
|
|
* The runnable is only executed once, and reset after execution.
|
|
* In case the method is called again, before the previously set runnable was executed,
|
|
* the runnable will be updated.
|
|
*/
|
|
public void unlockPrivateProfile() {
|
|
setQuietMode(false);
|
|
}
|
|
|
|
/** Enables quiet mode for Private Space User Profile. */
|
|
void lockPrivateProfile() {
|
|
setQuietMode(true);
|
|
}
|
|
|
|
/** 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() {
|
|
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);
|
|
mFloatingMaskView = null;
|
|
if (mPSHeader != null) {
|
|
mPSHeader.setAlpha(1);
|
|
}
|
|
if (transitioningFromLockedToUnlocked(previousState, updatedState)) {
|
|
postUnlock();
|
|
} else if (transitioningFromUnlockedToLocked(previousState, updatedState)){
|
|
executeLock();
|
|
}
|
|
resetPrivateSpaceDecorator(updatedState);
|
|
}
|
|
|
|
/**
|
|
* Opens the Private Space Settings Page.
|
|
*
|
|
* @param view the view that was clicked to open the settings page and which will be the same
|
|
* view to animate back. Otherwise if there is no view, simply start the activity.
|
|
*/
|
|
public void openPrivateSpaceSettings(View view) {
|
|
if (mPrivateSpaceSettingsAvailable) {
|
|
Context context = mAllApps.getContext();
|
|
Intent intent = ApiWrapper.INSTANCE.get(context).getPrivateSpaceSettingsIntent();
|
|
if (view == null) {
|
|
context.startActivity(intent);
|
|
return;
|
|
}
|
|
ActivityContext activityContext = ActivityContext.lookupContext(context);
|
|
AppInfo itemInfo = new AppInfo();
|
|
itemInfo.id = CONTAINER_PRIVATESPACE;
|
|
itemInfo.componentName = intent.getComponent();
|
|
itemInfo.container = CONTAINER_PRIVATESPACE;
|
|
view.setTag(itemInfo);
|
|
activityContext.startActivitySafely(view, intent, itemInfo);
|
|
}
|
|
}
|
|
|
|
/** 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);
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void resetPrivateSpaceDecorator(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);
|
|
} else {
|
|
// Remove Private Space Decorator from the Recycler view.
|
|
if (mPrivateAppsSectionDecorator != null && !mIsAnimationRunning) {
|
|
mainAdapterHolder.mRecyclerView.removeItemDecoration(mPrivateAppsSectionDecorator);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setQuietMode(boolean enable) {
|
|
super.setQuietMode(enable);
|
|
mAnimate = true;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
mAnimate = false;
|
|
}
|
|
mIsAnimationRunning = isAnimationRunning;
|
|
}
|
|
|
|
boolean getAnimationRunning() {
|
|
return mIsAnimationRunning;
|
|
}
|
|
|
|
private boolean transitioningFromLockedToUnlocked(int previousState, int updatedState) {
|
|
return previousState == STATE_DISABLED && updatedState == STATE_ENABLED;
|
|
}
|
|
|
|
private boolean transitioningFromUnlockedToLocked(int previousState, int updatedState) {
|
|
return previousState == STATE_ENABLED && updatedState == STATE_DISABLED;
|
|
}
|
|
|
|
@Override
|
|
public Predicate<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 addPrivateSpaceHeaderViewElements(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 (mAnimate) {
|
|
enableLayoutTransition(settingAndLockGroup);
|
|
} else {
|
|
// Ensure any unwanted animations to not happen.
|
|
settingAndLockGroup.setLayoutTransition(null);
|
|
}
|
|
|
|
//Add quietMode image and action for lock/unlock button
|
|
ViewGroup lockButton = mPSHeader.findViewById(R.id.ps_lock_unlock_button);
|
|
assert lockButton != null;
|
|
addLockButton(lockButton);
|
|
|
|
//Trigger lock/unlock action from header.
|
|
addHeaderOnClickListener(mPSHeader);
|
|
|
|
//Add image and action for private space settings button
|
|
ImageButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
|
|
assert settingsButton != null;
|
|
addPrivateSpaceSettingsButton(settingsButton);
|
|
|
|
//Add image for private space transitioning view
|
|
ImageView transitionView = parent.findViewById(R.id.ps_transition_image);
|
|
assert transitionView != null;
|
|
addTransitionImage(transitionView);
|
|
}
|
|
|
|
/**
|
|
* Adds the quietModeButton and attach onClickListener for the header to animate different
|
|
* states when clicked.
|
|
*/
|
|
private void addLockButton(ViewGroup lockButton) {
|
|
TextView lockText = lockButton.findViewById(R.id.lock_text);
|
|
switch (getCurrentState()) {
|
|
case STATE_ENABLED -> {
|
|
lockText.setVisibility(VISIBLE);
|
|
lockButton.setVisibility(VISIBLE);
|
|
lockButton.setOnClickListener(view -> lockingAction(/* lock */ true));
|
|
lockButton.setContentDescription(mUnLockedStateContentDesc);
|
|
}
|
|
case STATE_DISABLED -> {
|
|
lockText.setVisibility(GONE);
|
|
lockButton.setVisibility(VISIBLE);
|
|
lockButton.setOnClickListener(view -> lockingAction(/* lock */ false));
|
|
lockButton.setContentDescription(mLockedStateContentDesc);
|
|
}
|
|
default -> lockButton.setVisibility(GONE);
|
|
}
|
|
}
|
|
|
|
private void addHeaderOnClickListener(RelativeLayout header) {
|
|
if (getCurrentState() == STATE_DISABLED) {
|
|
header.setOnClickListener(view -> lockingAction(/* lock */ false));
|
|
header.setClickable(true);
|
|
// Add header as accessibility target when disabled.
|
|
header.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
|
header.setContentDescription(mLockedStateContentDesc);
|
|
} else {
|
|
header.setOnClickListener(null);
|
|
header.setClickable(false);
|
|
// Remove header from accessibility target when enabled.
|
|
header.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
}
|
|
}
|
|
|
|
/** Sets the enablement of the profile when header or button is clicked. */
|
|
private void lockingAction(boolean lock) {
|
|
logEvents(lock ? LAUNCHER_PRIVATE_SPACE_LOCK_TAP : LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP);
|
|
if (lock) {
|
|
lockPrivateProfile();
|
|
} else {
|
|
unlockPrivateProfile();
|
|
}
|
|
}
|
|
|
|
private void addPrivateSpaceSettingsButton(ImageButton settingsButton) {
|
|
if (getCurrentState() == STATE_ENABLED
|
|
&& isPrivateSpaceSettingsAvailable()) {
|
|
settingsButton.setVisibility(VISIBLE);
|
|
settingsButton.setOnClickListener(
|
|
view -> {
|
|
logEvents(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP);
|
|
openPrivateSpaceSettings(view);
|
|
});
|
|
} else {
|
|
settingsButton.setVisibility(GONE);
|
|
}
|
|
}
|
|
|
|
private void addTransitionImage(ImageView transitionImage) {
|
|
if (getCurrentState() == STATE_TRANSITION) {
|
|
transitionImage.setVisibility(VISIBLE);
|
|
} else {
|
|
transitionImage.setVisibility(GONE);
|
|
}
|
|
}
|
|
|
|
/** Finds the private space header to scroll to and set the private space icons to GONE. */
|
|
private void collapse() {
|
|
AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView();
|
|
List<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;
|
|
}
|
|
};
|
|
smoothScroller.setTargetPosition(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);
|
|
if (isPrivateSpaceItem(currentItem) &&
|
|
currentItem.viewType != VIEW_TYPE_PRIVATE_SPACE_HEADER) {
|
|
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(() -> {
|
|
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) {
|
|
// 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(translateFloatingMaskView(false),
|
|
animateAlphaOfIcons(false),
|
|
animateCollapseAnimation(),
|
|
fadeOutHeaderAlpha());
|
|
} else {
|
|
animatorSet.playSequentially(translateFloatingMaskView(true),
|
|
animateAlphaOfIcons(false),
|
|
animateCollapseAnimation());
|
|
}
|
|
}
|
|
animatorSet.setDuration(EXPAND_COLLAPSE_DURATION);
|
|
animatorSet.start();
|
|
}
|
|
|
|
/** Fades out the private space container. */
|
|
private ValueAnimator fadeOutHeaderAlpha() {
|
|
if (mPSHeader == null) {
|
|
return new ValueAnimator();
|
|
}
|
|
float from = 1;
|
|
float to = 0;
|
|
ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
|
|
alphaAnim.setDuration(EXPAND_COLLAPSE_DURATION);
|
|
alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
|
if (mPSHeader != null) {
|
|
mPSHeader.setAlpha((float) valueAnimator.getAnimatedValue());
|
|
}
|
|
}
|
|
});
|
|
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);
|
|
mAnimate = 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) {
|
|
mAnimationScrolling = 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;
|
|
}
|
|
|
|
boolean getAnimate() {
|
|
return mAnimate;
|
|
}
|
|
|
|
boolean getAnimationScrolling() {
|
|
return mAnimationScrolling;
|
|
}
|
|
|
|
int getPsHeaderHeight() {
|
|
return mPsHeaderHeight;
|
|
}
|
|
|
|
boolean isPrivateSpaceItem(BaseAllAppsAdapter.AdapterItem item) {
|
|
return getItemInfoMatcher().test(item.itemInfo) || item.decorationInfo != null
|
|
|| (item.itemInfo instanceof PrivateSpaceInstallAppButtonInfo);
|
|
}
|
|
}
|