/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.allapps; import static android.view.View.GONE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER; import static com.android.launcher3.allapps.PrivateProfileManager.STATE_DISABLED; import static com.android.launcher3.allapps.PrivateProfileManager.STATE_ENABLED; import static com.android.launcher3.allapps.PrivateProfileManager.STATE_TRANSITION; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.LayoutTransition; import android.animation.ValueAnimator; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; import com.android.app.animation.Interpolators; import com.android.launcher3.Flags; import com.android.launcher3.R; import com.android.launcher3.allapps.UserProfileManager.UserProfileState; import com.android.launcher3.anim.AnimatedPropertySetter; import com.android.launcher3.anim.PropertySetter; import com.android.launcher3.views.RecyclerViewFastScroller; import java.util.List; /** * Controller which returns views to be added to Private Space Header based upon * {@link UserProfileState} */ public class PrivateSpaceHeaderViewController { private static final int EXPAND_SCROLL_DURATION = 2000; private static final int EXPAND_COLLAPSE_DURATION = 800; private static final int SETTINGS_OPACITY_DURATION = 160; private final ActivityAllAppsContainerView mAllApps; private final PrivateProfileManager mPrivateProfileManager; public PrivateSpaceHeaderViewController(ActivityAllAppsContainerView allApps, PrivateProfileManager privateProfileManager) { this.mAllApps = allApps; this.mPrivateProfileManager = privateProfileManager; } /** Add Private Space Header view elements based upon {@link UserProfileState} */ public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) { // Set the transition duration for the settings and lock button to animate. ViewGroup settingsAndLockGroup = parent.findViewById(R.id.settingsAndLockGroup); LayoutTransition settingsAndLockTransition = settingsAndLockGroup.getLayoutTransition(); settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING); settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION); //Add quietMode image and action for lock/unlock button ViewGroup lockButton = parent.findViewById(R.id.ps_lock_unlock_button); assert lockButton != null; addLockButton(parent, lockButton); //Trigger lock/unlock action from header. addHeaderOnClickListener(parent); //Add image and action for private space settings button ImageButton settingsButton = parent.findViewById(R.id.ps_settings_button); assert settingsButton != null; addPrivateSpaceSettingsButton(settingsButton); //Add image for private space transitioning view ImageView transitionView = parent.findViewById(R.id.ps_transition_image); assert transitionView != null; addTransitionImage(transitionView); } /** * Adds the quietModeButton and attach onClickListener for the header to animate different * states when clicked. */ private void addLockButton(ViewGroup psHeader, ViewGroup lockButton) { TextView lockText = lockButton.findViewById(R.id.lock_text); switch (mPrivateProfileManager.getCurrentState()) { case STATE_ENABLED -> { lockText.setVisibility(VISIBLE); lockButton.setVisibility(VISIBLE); lockButton.setOnClickListener(view -> lockAction(psHeader)); } case STATE_DISABLED -> { lockText.setVisibility(GONE); lockButton.setVisibility(VISIBLE); lockButton.setOnClickListener(view -> unlockAction(psHeader)); } default -> lockButton.setVisibility(GONE); } } private void addHeaderOnClickListener(RelativeLayout header) { if (mPrivateProfileManager.getCurrentState() == STATE_DISABLED) { header.setOnClickListener(view -> unlockAction(header)); } else { header.setOnClickListener(null); } } private void unlockAction(ViewGroup psHeader) { mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP); mPrivateProfileManager.unlockPrivateProfile((() -> onPrivateProfileUnlocked(psHeader))); } private void lockAction(ViewGroup psHeader) { mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_LOCK_TAP); if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()) { updatePrivateStateAnimator(false, psHeader); } else { mPrivateProfileManager.lockPrivateProfile(); } } private void addPrivateSpaceSettingsButton(ImageButton settingsButton) { if (mPrivateProfileManager.getCurrentState() == STATE_ENABLED && mPrivateProfileManager.isPrivateSpaceSettingsAvailable()) { settingsButton.setVisibility(VISIBLE); settingsButton.setAlpha(1f); settingsButton.setOnClickListener( view -> { mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP); mPrivateProfileManager.openPrivateSpaceSettings(); }); } else { settingsButton.setVisibility(GONE); } } private void addTransitionImage(ImageView transitionImage) { if (mPrivateProfileManager.getCurrentState() == STATE_TRANSITION) { transitionImage.setVisibility(VISIBLE); } else { transitionImage.setVisibility(GONE); } } private void onPrivateProfileUnlocked(ViewGroup header) { // If we are on main adapter view, we apply the PS Container expansion animation and // then scroll down to load the entire container, making animation visible. ActivityAllAppsContainerView.AdapterHolder mainAdapterHolder = (ActivityAllAppsContainerView.AdapterHolder) mAllApps.mAH.get(MAIN); if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation() && mAllApps.getActiveRecyclerView() == mainAdapterHolder.mRecyclerView) { // Animate the text and settings icon. updatePrivateStateAnimator(true, header); mAllApps.getActiveRecyclerView().scrollToBottomWithMotion(EXPAND_SCROLL_DURATION); } } /** Finds the private space header to scroll to and set the private space icons to GONE. */ private void collapse() { AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); for (int i = allAppsRecyclerView.getChildCount() - 1; i > 0; i--) { int adapterPosition = allAppsRecyclerView.getChildAdapterPosition( allAppsRecyclerView.getChildAt(i)); List allAppsAdapters = allAppsRecyclerView.getApps() .getAdapterItems(); if (adapterPosition < 0 || adapterPosition >= allAppsAdapters.size()) { continue; } // Scroll to the private space header. if (allAppsAdapters.get(adapterPosition).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(adapterPosition); RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); if (layoutManager != null) { layoutManager.startSmoothScroll(smoothScroller); } break; } // Make the private space apps gone to "collapse". if (allAppsAdapters.get(adapterPosition).decorationInfo != null) { allAppsRecyclerView.getChildAt(i).setVisibility(GONE); } } } PrivateProfileManager getPrivateProfileManager() { return mPrivateProfileManager; } /** * Scrolls up to the private space header and animates the collapsing of the text. */ private ValueAnimator animateCollapseAnimation(ViewGroup lockButton) { float from = 1; float to = 0; RecyclerViewFastScroller scrollBar = mAllApps.getActiveRecyclerView().getScrollbar(); ValueAnimator collapseAnim = ValueAnimator.ofFloat(from, to); collapseAnim.setDuration(EXPAND_COLLAPSE_DURATION); collapseAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { if (scrollBar != null) { scrollBar.setVisibility(INVISIBLE); } // scroll up collapse(); // Animate the collapsing of the text. lockButton.findViewById(R.id.lock_text).setVisibility(GONE); } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (scrollBar != null) { scrollBar.setThumbOffsetY(-1); scrollBar.setVisibility(VISIBLE); } mPrivateProfileManager.lockPrivateProfile(); } }); return collapseAnim; } /** * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an * animation. At the moment, collapsing, setting alpha changes, and animating the text is done * here. */ private void updatePrivateStateAnimator(boolean expand, ViewGroup psHeader) { PropertySetter setter = new AnimatedPropertySetter(); ViewGroup lockButton = psHeader.findViewById(R.id.ps_lock_unlock_button); ImageButton settingsButton = psHeader.findViewById(R.id.ps_settings_button); updateSettingsGearAlpha(settingsButton, expand, setter); AnimatorSet animatorSet = setter.buildAnim(); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { // Animate the collapsing of the text at the same time while updating lock button. lockButton.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE); } }); // Play the collapsing together of the stateAnimator to avoid being unable to scroll to the // header. Otherwise the smooth scrolling will scroll higher when played with the state // animator. if (!expand) { animatorSet.playTogether(animateCollapseAnimation(lockButton)); } animatorSet.setDuration(EXPAND_COLLAPSE_DURATION); animatorSet.start(); } /** Change the settings gear alpha when expanded or collapsed. */ private void updateSettingsGearAlpha(ImageButton settingsButton, boolean expand, PropertySetter setter) { float toAlpha = expand ? 1 : 0; setter.setFloat(settingsButton, VIEW_ALPHA, toAlpha, Interpolators.LINEAR) .setDuration(SETTINGS_OPACITY_DURATION).setStartDelay(0); } }