1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.allapps;
18 
19 import static android.view.View.GONE;
20 import static android.view.View.INVISIBLE;
21 import static android.view.View.VISIBLE;
22 
23 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN;
24 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ICON;
25 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
26 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER;
27 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
28 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
29 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN;
30 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END;
31 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_TAP;
32 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN;
33 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END;
34 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP;
35 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
36 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
37 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
38 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
39 import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
40 
41 import android.animation.Animator;
42 import android.animation.AnimatorListenerAdapter;
43 import android.animation.AnimatorSet;
44 import android.animation.LayoutTransition;
45 import android.animation.ObjectAnimator;
46 import android.animation.ValueAnimator;
47 import android.content.Context;
48 import android.content.Intent;
49 import android.os.UserHandle;
50 import android.os.UserManager;
51 import android.view.View;
52 import android.view.ViewGroup;
53 import android.widget.ImageView;
54 import android.widget.RelativeLayout;
55 import android.widget.TextView;
56 
57 import androidx.annotation.NonNull;
58 import androidx.annotation.Nullable;
59 import androidx.annotation.VisibleForTesting;
60 import androidx.constraintlayout.widget.ConstraintLayout;
61 import androidx.recyclerview.widget.LinearSmoothScroller;
62 import androidx.recyclerview.widget.RecyclerView;
63 
64 import com.android.app.animation.Interpolators;
65 import com.android.launcher3.BuildConfig;
66 import com.android.launcher3.DeviceProfile;
67 import com.android.launcher3.Flags;
68 import com.android.launcher3.R;
69 import com.android.launcher3.anim.AnimatedPropertySetter;
70 import com.android.launcher3.anim.PropertySetter;
71 import com.android.launcher3.icons.BitmapInfo;
72 import com.android.launcher3.icons.LauncherIcons;
73 import com.android.launcher3.logging.StatsLogManager;
74 import com.android.launcher3.model.data.AppInfo;
75 import com.android.launcher3.model.data.PrivateSpaceInstallAppButtonInfo;
76 import com.android.launcher3.pm.UserCache;
77 import com.android.launcher3.util.ApiWrapper;
78 import com.android.launcher3.util.Preconditions;
79 import com.android.launcher3.util.SettingsCache;
80 import com.android.launcher3.views.ActivityContext;
81 import com.android.launcher3.views.RecyclerViewFastScroller;
82 
83 import java.util.ArrayList;
84 import java.util.List;
85 import java.util.function.Predicate;
86 
87 /**
88  * Companion class for {@link ActivityAllAppsContainerView} to manage private space section related
89  * logic in the Personal tab.
90  */
91 public class PrivateProfileManager extends UserProfileManager {
92     private static final int EXPAND_COLLAPSE_DURATION = 800;
93     private static final int SETTINGS_OPACITY_DURATION = 400;
94     private static final int TEXT_UNLOCK_OPACITY_DURATION = 300;
95     private static final int TEXT_LOCK_OPACITY_DURATION = 50;
96     private static final int APP_OPACITY_DURATION = 400;
97     private static final int MASK_VIEW_DURATION = 200;
98     private static final int APP_OPACITY_DELAY = 400;
99     private static final int SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY = 400;
100     private static final int SETTINGS_OPACITY_DELAY = 400;
101     private static final int LOCK_TEXT_OPACITY_DELAY = 500;
102     private static final int MASK_VIEW_DELAY = 400;
103     private static final int NO_DELAY = 0;
104     private static final int CONTAINER_OPACITY_DURATION = 150;
105     private final ActivityAllAppsContainerView<?> mAllApps;
106     private final Predicate<UserHandle> mPrivateProfileMatcher;
107     private final int mPsHeaderHeight;
108     private final int mFloatingMaskViewCornerRadius;
109     private final RecyclerView.OnScrollListener mOnIdleScrollListener =
110             new RecyclerView.OnScrollListener() {
111         @Override
112         public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
113             super.onScrollStateChanged(recyclerView, newState);
114             if (newState == RecyclerView.SCROLL_STATE_IDLE) {
115                 mIsScrolling = false;
116             }
117         }
118     };
119     private Intent mAppInstallerIntent = new Intent();
120     private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator;
121     private boolean mPrivateSpaceSettingsAvailable;
122     // Returns if the animation is currently running.
123     private boolean mIsAnimationRunning;
124     // mAnimate denotes if private space is ready to be animated.
125     private boolean mReadyToAnimate;
126     // Returns when the recyclerView is currently scrolling.
127     private boolean mIsScrolling;
128     // mIsStateTransitioning indicates that private space is transitioning between states.
129     private boolean mIsStateTransitioning;
130     private Runnable mOnPSHeaderAdded;
131     @Nullable
132     private RelativeLayout mPSHeader;
133     private ConstraintLayout mFloatingMaskView;
134     private final String mLockedStateContentDesc;
135     private final String mUnLockedStateContentDesc;
136 
PrivateProfileManager(UserManager userManager, ActivityAllAppsContainerView<?> allApps, StatsLogManager statsLogManager, UserCache userCache)137     public PrivateProfileManager(UserManager userManager,
138             ActivityAllAppsContainerView<?> allApps,
139             StatsLogManager statsLogManager,
140             UserCache userCache) {
141         super(userManager, statsLogManager, userCache);
142         mAllApps = allApps;
143         mPrivateProfileMatcher = (user) -> userCache.getUserInfo(user).isPrivate();
144 
145         Context appContext = allApps.getContext().getApplicationContext();
146         UI_HELPER_EXECUTOR.post(() -> initializeInBackgroundThread(appContext));
147         mPsHeaderHeight = mAllApps.getContext().getResources().getDimensionPixelSize(
148                 R.dimen.ps_header_height);
149         mLockedStateContentDesc = mAllApps.getContext()
150                 .getString(R.string.ps_container_lock_button_content_description);
151         mUnLockedStateContentDesc = mAllApps.getContext()
152                 .getString(R.string.ps_container_unlock_button_content_description);
153         mFloatingMaskViewCornerRadius = mAllApps.getContext().getResources().getDimensionPixelSize(
154                 R.dimen.ps_floating_mask_corner_radius);
155     }
156 
157     /** Adds Private Space Header to the layout. */
addPrivateSpaceHeader(ArrayList<BaseAllAppsAdapter.AdapterItem> adapterItems)158     public int addPrivateSpaceHeader(ArrayList<BaseAllAppsAdapter.AdapterItem> adapterItems) {
159         adapterItems.add(new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_PRIVATE_SPACE_HEADER));
160         mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1);
161         return adapterItems.size();
162     }
163 
164     /** Adds Private Space System Apps Divider to the layout. */
addSystemAppsDivider(List<BaseAllAppsAdapter.AdapterItem> adapterItems)165     public int addSystemAppsDivider(List<BaseAllAppsAdapter.AdapterItem> adapterItems) {
166         adapterItems.add(new BaseAllAppsAdapter
167                 .AdapterItem(VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER));
168         mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1);
169         return adapterItems.size();
170     }
171 
172     /** Adds Private Space install app button to the layout. */
addPrivateSpaceInstallAppButton(List<BaseAllAppsAdapter.AdapterItem> adapterItems)173     public void addPrivateSpaceInstallAppButton(List<BaseAllAppsAdapter.AdapterItem> adapterItems) {
174         Context context = mAllApps.getContext();
175         // Prepare bitmapInfo
176         Intent.ShortcutIconResource shortcut = Intent.ShortcutIconResource.fromContext(
177                 context, com.android.launcher3.R.drawable.private_space_install_app_icon);
178         BitmapInfo bitmapInfo = LauncherIcons.obtain(context).createIconBitmap(shortcut);
179 
180         PrivateSpaceInstallAppButtonInfo itemInfo = new PrivateSpaceInstallAppButtonInfo();
181         itemInfo.title = context.getResources().getString(R.string.ps_add_button_label);
182         itemInfo.intent = mAppInstallerIntent;
183         itemInfo.bitmap = bitmapInfo;
184         itemInfo.contentDescription = context.getResources().getString(
185                 com.android.launcher3.R.string.ps_add_button_content_description);
186         itemInfo.runtimeStatusFlags |= FLAG_NOT_PINNABLE;
187 
188         BaseAllAppsAdapter.AdapterItem item = new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_ICON);
189         item.itemInfo = itemInfo;
190         item.decorationInfo = new SectionDecorationInfo(context, ROUND_NOTHING,
191                 /* decorateTogether */ true);
192 
193         adapterItems.add(item);
194         mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1);
195     }
196 
197     /** Whether private profile should be hidden on Launcher. */
isPrivateSpaceHidden()198     public boolean isPrivateSpaceHidden() {
199         return getCurrentState() == STATE_DISABLED && SettingsCache.INSTANCE
200                     .get(mAllApps.getContext()).getValue(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, 0);
201     }
202 
203     /**
204      * Resets the current state of Private Profile, w.r.t. to Launcher. The decorator should only
205      * be applied upon expand before animating. When collapsing, reset() will remove the decorator
206      * when animation is not running.
207      */
reset()208     public void reset() {
209         // Ensure the state of the header views is what it should be before animating.
210         updateView();
211         getMainRecyclerView().setChildAttachedConsumer(null);
212         int previousState = getCurrentState();
213         boolean isEnabled = !mAllApps.getAppsStore()
214                 .hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED);
215         int updatedState = isEnabled ? STATE_ENABLED : STATE_DISABLED;
216         setCurrentState(updatedState);
217         if (Flags.privateSpaceAddFloatingMaskView()) {
218             mFloatingMaskView = null;
219         }
220         // It's possible that previousState is 0 when reset is first called.
221         mIsStateTransitioning = previousState != STATE_UNKNOWN && previousState != updatedState;
222         if (previousState == STATE_DISABLED && updatedState == STATE_ENABLED) {
223             postUnlock();
224         } else if (previousState == STATE_ENABLED && updatedState == STATE_DISABLED){
225             executeLock();
226         }
227         addPrivateSpaceDecorator(updatedState);
228     }
229 
230     /** Returns whether or not Private Space Settings Page is available. */
isPrivateSpaceSettingsAvailable()231     public boolean isPrivateSpaceSettingsAvailable() {
232         return mPrivateSpaceSettingsAvailable;
233     }
234 
235     /** Sets whether Private Space Settings Page is available. */
setPrivateSpaceSettingsAvailable(boolean value)236     public boolean setPrivateSpaceSettingsAvailable(boolean value) {
237         return mPrivateSpaceSettingsAvailable = value;
238     }
239 
240     /** Initializes binder call based properties in non-main thread.
241      * <p>
242      * This can cause the Private Space container items to not load/respond correctly sometimes,
243      * when the All Apps Container loads for the first time (device restarts, new profiles
244      * added/removed, etc.), as the properties are being set in non-ui thread whereas the container
245      * loads in the ui thread.
246      * This case should still be ok, as locking the Private Space container and unlocking it,
247      * reloads the values, fixing the incorrect UI.
248      */
initializeInBackgroundThread(Context appContext)249     private void initializeInBackgroundThread(Context appContext) {
250         Preconditions.assertNonUiThread();
251         ApiWrapper apiWrapper = ApiWrapper.INSTANCE.get(appContext);
252         UserHandle profileUser = getProfileUser();
253         if (profileUser != null) {
254             mAppInstallerIntent = apiWrapper
255                     .getAppMarketActivityIntent(BuildConfig.APPLICATION_ID, profileUser);
256         }
257         setPrivateSpaceSettingsAvailable(apiWrapper.getPrivateSpaceSettingsIntent() != null);
258     }
259 
260     /** Adds a private space decorator only when STATE_ENABLED. */
261     @VisibleForTesting
addPrivateSpaceDecorator(int updatedState)262     void addPrivateSpaceDecorator(int updatedState) {
263         ActivityAllAppsContainerView<?>.AdapterHolder mainAdapterHolder = mAllApps.mAH.get(MAIN);
264         if (updatedState == STATE_ENABLED) {
265             // Create a new decorator instance if not already available.
266             if (mPrivateAppsSectionDecorator == null) {
267                 mPrivateAppsSectionDecorator = new PrivateAppsSectionDecorator(
268                         mainAdapterHolder.mAppsList);
269             }
270             for (int i = 0; i < mainAdapterHolder.mRecyclerView.getItemDecorationCount(); i++) {
271                 if (mainAdapterHolder.mRecyclerView.getItemDecorationAt(i)
272                         .equals(mPrivateAppsSectionDecorator)) {
273                     // No need to add another decorator if one is already present in recycler view.
274                     return;
275                 }
276             }
277             // Add Private Space Decorator to the Recycler view.
278             mainAdapterHolder.mRecyclerView.addItemDecoration(mPrivateAppsSectionDecorator);
279         }
280     }
281 
282     @Override
setQuietMode(boolean enable)283     public void setQuietMode(boolean enable) {
284         UI_HELPER_EXECUTOR.post(() ->
285                 mUserCache.getUserProfiles()
286                         .stream()
287                         .filter(getUserMatcher())
288                         .findFirst()
289                         .ifPresent(userHandle -> setQuietModeSafely(enable, userHandle)));
290         mReadyToAnimate = true;
291     }
292 
293     /**
294      * Sets Quiet Mode for Private Profile.
295      * If {@link SecurityException} is thrown, prompts the user to set this launcher as HOME app.
296      */
setQuietModeSafely(boolean enable, UserHandle userHandle)297     private void setQuietModeSafely(boolean enable, UserHandle userHandle) {
298         try {
299             mUserManager.requestQuietModeEnabled(enable, userHandle);
300         } catch (SecurityException ex) {
301             ApiWrapper.INSTANCE.get(mAllApps.mActivityContext)
302                     .assignDefaultHomeRole(mAllApps.mActivityContext);
303         }
304     }
305 
306     /**
307      * Expand the private space after the app list has been added and updated from
308      * {@link AlphabeticalAppsList#onAppsUpdated()}
309      */
postUnlock()310     void postUnlock() {
311         if (mAllApps.isSearching()) {
312             MAIN_EXECUTOR.post(this::exitSearchAndExpand);
313         } else {
314             MAIN_EXECUTOR.post(this::expandPrivateSpace);
315         }
316     }
317 
318     /** Collapses the private space before the app list has been updated. */
executeLock()319     void executeLock() {
320         MAIN_EXECUTOR.execute(() -> updatePrivateStateAnimator(false));
321     }
322 
setAnimationRunning(boolean isAnimationRunning)323     void setAnimationRunning(boolean isAnimationRunning) {
324         if (!isAnimationRunning) {
325             mReadyToAnimate = false;
326         }
327         mIsAnimationRunning = isAnimationRunning;
328     }
329 
getAnimationRunning()330     boolean getAnimationRunning() {
331         return mIsAnimationRunning;
332     }
333 
334     @Override
getUserMatcher()335     public Predicate<UserHandle> getUserMatcher() {
336         return mPrivateProfileMatcher;
337     }
338 
339     /**
340      * Splits private apps into user installed and system apps.
341      * When the list of system apps is empty, all apps are treated as system.
342      */
splitIntoUserInstalledAndSystemApps(Context context)343     public Predicate<AppInfo> splitIntoUserInstalledAndSystemApps(Context context) {
344         List<String> preInstallApps = UserCache.getInstance(context)
345                 .getPreInstallApps(getProfileUser());
346         return appInfo -> !preInstallApps.isEmpty()
347                 && (appInfo.componentName == null
348                 || !(preInstallApps.contains(appInfo.componentName.getPackageName())));
349     }
350 
351     /** Add Private Space Header view elements based upon {@link UserProfileState} */
bindPrivateSpaceHeaderViewElements(RelativeLayout parent)352     public void bindPrivateSpaceHeaderViewElements(RelativeLayout parent) {
353         mPSHeader = parent;
354         if (mOnPSHeaderAdded != null) {
355             MAIN_EXECUTOR.execute(mOnPSHeaderAdded);
356             mOnPSHeaderAdded = null;
357         }
358         // Set the transition duration for the settings and lock button to animate.
359         ViewGroup settingAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
360         if (mReadyToAnimate) {
361             enableLayoutTransition(settingAndLockGroup);
362         } else {
363             // Ensure any unwanted animations to not happen.
364             settingAndLockGroup.setLayoutTransition(null);
365         }
366         updateView();
367     }
368 
369     /** Update the states of the views that make up the header at the state it is called in. */
updateView()370     private void updateView() {
371         if (mPSHeader == null) {
372             return;
373         }
374         mPSHeader.setAlpha(1);
375         ViewGroup lockPill = mPSHeader.findViewById(R.id.ps_lock_unlock_button);
376         assert lockPill != null;
377         TextView lockText = lockPill.findViewById(R.id.lock_text);
378         PrivateSpaceSettingsButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
379         assert settingsButton != null;
380         //Add image for private space transitioning view
381         ImageView transitionView = mPSHeader.findViewById(R.id.ps_transition_image);
382         assert transitionView != null;
383         switch(getCurrentState()) {
384             case STATE_ENABLED -> {
385                 mPSHeader.setOnClickListener(null);
386                 mPSHeader.setClickable(false);
387                 // Remove header from accessibility target when enabled.
388                 mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
389 
390                 lockText.setVisibility(VISIBLE);
391                 lockPill.setVisibility(VISIBLE);
392                 lockPill.setOnClickListener(view -> lockingAction(/* lock */ true));
393                 lockPill.setContentDescription(mUnLockedStateContentDesc);
394 
395                 settingsButton.setVisibility(isPrivateSpaceSettingsAvailable() ? VISIBLE : GONE);
396                 transitionView.setVisibility(GONE);
397             }
398             case STATE_DISABLED -> {
399                 mPSHeader.setOnClickListener(view -> lockingAction(/* lock */ false));
400                 mPSHeader.setClickable(true);
401                 // Add header as accessibility target when disabled.
402                 mPSHeader.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
403                 mPSHeader.setContentDescription(mLockedStateContentDesc);
404 
405                 lockText.setVisibility(GONE);
406                 lockPill.setVisibility(VISIBLE);
407                 lockPill.setOnClickListener(view -> lockingAction(/* lock */ false));
408                 lockPill.setContentDescription(mLockedStateContentDesc);
409 
410                 settingsButton.setVisibility(GONE);
411                 transitionView.setVisibility(GONE);
412             }
413             case STATE_TRANSITION -> {
414                 transitionView.setVisibility(VISIBLE);
415                 lockPill.setVisibility(GONE);
416             }
417         }
418     }
419 
420     /** Sets the enablement of the profile when header or button is clicked. */
lockingAction(boolean lock)421     private void lockingAction(boolean lock) {
422         logEvents(lock ? LAUNCHER_PRIVATE_SPACE_LOCK_TAP : LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP);
423         setQuietMode(lock);
424     }
425 
426     /** Finds the private space header to scroll to and set the private space icons to GONE. */
collapse()427     private void collapse() {
428         AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView();
429         List<BaseAllAppsAdapter.AdapterItem> appListAdapterItems =
430                 allAppsRecyclerView.getApps().getAdapterItems();
431         for (int i = appListAdapterItems.size() - 1; i > 0; i--) {
432             BaseAllAppsAdapter.AdapterItem currentItem = appListAdapterItems.get(i);
433             // Scroll to the private space header.
434             if (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) {
435                 // Note: SmoothScroller is meant to be used once.
436                 RecyclerView.SmoothScroller smoothScroller =
437                         new LinearSmoothScroller(mAllApps.getContext()) {
438                             @Override protected int getVerticalSnapPreference() {
439                                 return LinearSmoothScroller.SNAP_TO_END;
440                             }
441                         };
442                 // If privateSpaceHidden() then the entire container decorator will be invisible and
443                 // we can directly move to an element above the header. There should always be one
444                 // element, as PS is present in the bottom of All Apps.
445                 smoothScroller.setTargetPosition(isPrivateSpaceHidden() ? i - 1 : i);
446                 RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager();
447                 if (layoutManager != null) {
448                     startAnimationScroll(allAppsRecyclerView, layoutManager, smoothScroller);
449                     // Preserve decorator if floating mask view exists.
450                     if (mFloatingMaskView == null) {
451                         currentItem.decorationInfo = null;
452                     }
453                 }
454                 break;
455             }
456             // Make the private space apps gone to "collapse".
457             if (mFloatingMaskView == null && isPrivateSpaceItem(currentItem)) {
458                 RecyclerView.ViewHolder viewHolder =
459                         allAppsRecyclerView.findViewHolderForAdapterPosition(i);
460                 if (viewHolder != null) {
461                     viewHolder.itemView.setVisibility(GONE);
462                     currentItem.decorationInfo = null;
463                 }
464             }
465         }
466     }
467 
468     /**
469      * Upon expanding, only scroll to the item position in the adapter that allows the header to be
470      * visible.
471      */
scrollForHeaderToBeVisibleInContainer( AllAppsRecyclerView allAppsRecyclerView, List<BaseAllAppsAdapter.AdapterItem> appListAdapterItems, int psHeaderHeight, int allAppsCellHeight)472     public int scrollForHeaderToBeVisibleInContainer(
473             AllAppsRecyclerView allAppsRecyclerView,
474             List<BaseAllAppsAdapter.AdapterItem> appListAdapterItems,
475             int psHeaderHeight,
476             int allAppsCellHeight) {
477         int rowToExpandToWithRespectToHeader = -1;
478         int itemToScrollTo = -1;
479         // Looks for the item in the app list to scroll to so that the header is visible.
480         for (int i = 0; i < appListAdapterItems.size(); i++) {
481             BaseAllAppsAdapter.AdapterItem currentItem = appListAdapterItems.get(i);
482             if (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) {
483                 itemToScrollTo = i;
484                 continue;
485             }
486             if (itemToScrollTo != -1) {
487                 itemToScrollTo = i;
488                 if (rowToExpandToWithRespectToHeader == -1) {
489                     rowToExpandToWithRespectToHeader = currentItem.rowIndex;
490                 }
491                 // If there are no tabs, decrease the row to scroll to by 1 since the header
492                 // may be cut off slightly.
493                 int rowToScrollTo =
494                         (int) Math.floor((double) (mAllApps.getHeight() - psHeaderHeight
495                                 - mAllApps.getHeaderProtectionHeight()) / allAppsCellHeight)
496                                 - (mAllApps.isUsingTabs() ? 0 : 1);
497                 int currentRowDistance = currentItem.rowIndex - rowToExpandToWithRespectToHeader;
498                 // rowToScrollTo - 1 since the item to scroll to is 0 indexed.
499                 if (currentRowDistance == rowToScrollTo - 1) {
500                     break;
501                 }
502             }
503         }
504         if (itemToScrollTo != -1) {
505             // Note: SmoothScroller is meant to be used once.
506             RecyclerView.SmoothScroller smoothScroller =
507                     new LinearSmoothScroller(mAllApps.getContext()) {
508                         @Override protected int getVerticalSnapPreference() {
509                             return LinearSmoothScroller.SNAP_TO_ANY;
510                         }
511                     };
512             smoothScroller.setTargetPosition(itemToScrollTo);
513             RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager();
514             if (layoutManager != null) {
515                 startAnimationScroll(allAppsRecyclerView, layoutManager, smoothScroller);
516             }
517         }
518         return itemToScrollTo;
519     }
520 
521     /**
522      * Scrolls up to the private space header and animates the collapsing of the text.
523      */
animateCollapseAnimation()524     private ValueAnimator animateCollapseAnimation() {
525         float from = 1;
526         float to = 0;
527         RecyclerViewFastScroller scrollBar = mAllApps.getActiveRecyclerView().getScrollbar();
528         ValueAnimator collapseAnim = ValueAnimator.ofFloat(from, to);
529         collapseAnim.setDuration(EXPAND_COLLAPSE_DURATION);
530         collapseAnim.addListener(new AnimatorListenerAdapter() {
531             @Override
532             public void onAnimationStart(Animator animation) {
533                 if (scrollBar != null) {
534                     scrollBar.setVisibility(INVISIBLE);
535                 }
536                 // Scroll up to header.
537                 collapse();
538             }
539             @Override
540             public void onAnimationEnd(Animator animation) {
541                 super.onAnimationEnd(animation);
542                 if (scrollBar != null) {
543                     scrollBar.setThumbOffsetY(-1);
544                     scrollBar.setVisibility(VISIBLE);
545                 }
546             }
547         });
548         return collapseAnim;
549     }
550 
animateAlphaOfIcons(boolean isExpanding)551     private ValueAnimator animateAlphaOfIcons(boolean isExpanding) {
552         float from = isExpanding ? 0 : 1;
553         float to = isExpanding ? 1 : 0;
554         AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView();
555         List<BaseAllAppsAdapter.AdapterItem> allAppsAdapterItems =
556                 mAllApps.getActiveRecyclerView().getApps().getAdapterItems();
557         ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
558         alphaAnim.setDuration(APP_OPACITY_DURATION)
559                 .setStartDelay(isExpanding ? APP_OPACITY_DELAY : NO_DELAY);
560         alphaAnim.setInterpolator(Interpolators.LINEAR);
561         alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
562             @Override
563             public void onAnimationUpdate(ValueAnimator valueAnimator) {
564                 float newAlpha = (float) valueAnimator.getAnimatedValue();
565                 for (int i = 0; i < allAppsAdapterItems.size(); i++) {
566                     BaseAllAppsAdapter.AdapterItem currentItem = allAppsAdapterItems.get(i);
567                     // When not hidden: Fade all PS items except header.
568                     // When hidden: Fade all items.
569                     if (isPrivateSpaceItem(currentItem) &&
570                             (currentItem.viewType != VIEW_TYPE_PRIVATE_SPACE_HEADER
571                                     || isPrivateSpaceHidden())) {
572                         RecyclerView.ViewHolder viewHolder =
573                                 allAppsRecyclerView.findViewHolderForAdapterPosition(i);
574                         if (viewHolder != null) {
575                             viewHolder.itemView.setAlpha(newAlpha);
576                         }
577                     }
578                 }
579             }
580         });
581         return alphaAnim;
582     }
583 
584     /**
585      * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an
586      * animation. At the moment, collapsing, setting alpha changes, and animating the text is done
587      * here.
588      */
updatePrivateStateAnimator(boolean expand)589     private void updatePrivateStateAnimator(boolean expand) {
590         if (!Flags.enablePrivateSpace() || !Flags.privateSpaceAnimation()) {
591             return;
592         }
593         if (mPSHeader == null) {
594             mOnPSHeaderAdded = () -> updatePrivateStateAnimator(expand);
595             setAnimationRunning(false);
596             return;
597         }
598         attachFloatingMaskView(expand);
599         ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
600         if (settingsAndLockGroup.getLayoutTransition() == null) {
601             // Set a new transition if the current ViewGroup does not already contain one as each
602             // transition should only happen once when applied.
603             enableLayoutTransition(settingsAndLockGroup);
604         }
605         settingsAndLockGroup.getLayoutTransition().setStartDelay(
606                 LayoutTransition.CHANGING,
607                 expand ? SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY : NO_DELAY);
608         PropertySetter headerSetter = new AnimatedPropertySetter();
609         headerSetter.add(updateSettingsGearAlpha(expand));
610         headerSetter.add(updateLockTextAlpha(expand));
611         AnimatorSet animatorSet = headerSetter.buildAnim();
612         animatorSet.addListener(new AnimatorListenerAdapter() {
613             @Override
614             public void onAnimationStart(Animator animation) {
615                 mStatsLogManager.logger().sendToInteractionJankMonitor(
616                         expand
617                                 ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN
618                                 : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN,
619                         mAllApps.getActiveRecyclerView());
620                 // Animate the collapsing of the text at the same time while updating lock button.
621                 mPSHeader.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE);
622                 setAnimationRunning(true);
623             }
624 
625             @Override
626             public void onAnimationEnd(Animator animation) {
627                 detachFloatingMaskView();
628             }
629         });
630         animatorSet.addListener(forEndCallback(() -> {
631             mIsStateTransitioning = false;
632             setAnimationRunning(false);
633             getMainRecyclerView().setChildAttachedConsumer(child -> child.setAlpha(1));
634             mStatsLogManager.logger().sendToInteractionJankMonitor(
635                     expand
636                             ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END
637                             : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END,
638                     mAllApps.getActiveRecyclerView());
639             if (!expand) {
640                 mAllApps.mAH.get(MAIN).mRecyclerView.removeItemDecoration(
641                         mPrivateAppsSectionDecorator);
642                 // Call onAppsUpdated() because it may be canceled when this animation occurs.
643                 mAllApps.getPersonalAppList().onAppsUpdated();
644                 if (isPrivateSpaceHidden()) {
645                     // TODO (b/325455879): Figure out if we can avoid this.
646                     getMainRecyclerView().getAdapter().notifyDataSetChanged();
647                 }
648             }
649         }));
650         if (expand) {
651             animatorSet.playTogether(animateAlphaOfIcons(true),
652                     translateFloatingMaskView(false));
653         } else {
654             if (isPrivateSpaceHidden()) {
655                 animatorSet.playSequentially(animateAlphaOfIcons(false),
656                         animateAlphaOfPrivateSpaceContainer(),
657                         animateCollapseAnimation());
658             } else {
659                 animatorSet.playSequentially(translateFloatingMaskView(true),
660                         animateAlphaOfIcons(false),
661                         animateCollapseAnimation());
662             }
663         }
664         animatorSet.start();
665     }
666 
667     /** Fades out the private space container (defined by its items' decorators). */
animateAlphaOfPrivateSpaceContainer()668     private ValueAnimator animateAlphaOfPrivateSpaceContainer() {
669         int from = 255; // 100% opacity.
670         int to = 0; // No opacity.
671         ValueAnimator alphaAnim = ObjectAnimator.ofInt(from, to);
672         AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView();
673         List<BaseAllAppsAdapter.AdapterItem> allAppsAdapterItems =
674                 allAppsRecyclerView.getApps().getAdapterItems();
675         alphaAnim.setDuration(CONTAINER_OPACITY_DURATION);
676         alphaAnim.addUpdateListener(valueAnimator -> {
677             for (BaseAllAppsAdapter.AdapterItem currentItem : allAppsAdapterItems) {
678                 if (isPrivateSpaceItem(currentItem)) {
679                     currentItem.setDecorationFillAlpha((int) valueAnimator.getAnimatedValue());
680                 }
681             }
682             // Invalidate the parent view, to redraw the decorations with changed alpha.
683             allAppsRecyclerView.invalidate();
684         });
685         return alphaAnim;
686     }
687 
688     /** Fades out the private space container. */
translateFloatingMaskView(boolean animateIn)689     private ValueAnimator translateFloatingMaskView(boolean animateIn) {
690         if (!Flags.privateSpaceAddFloatingMaskView() || mFloatingMaskView == null) {
691             return new ValueAnimator();
692         }
693         // Translate base on the height amount. Translates out on expand and in on collapse.
694         float floatingMaskViewHeight = getFloatingMaskViewHeight();
695         float from = animateIn ? floatingMaskViewHeight : 0;
696         float to = animateIn ? 0 : floatingMaskViewHeight;
697         ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
698         alphaAnim.setDuration(MASK_VIEW_DURATION);
699         alphaAnim.setStartDelay(MASK_VIEW_DELAY);
700         alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
701             @Override
702             public void onAnimationUpdate(ValueAnimator valueAnimator) {
703                 mFloatingMaskView.setTranslationY((float) valueAnimator.getAnimatedValue());
704             }
705         });
706         return alphaAnim;
707     }
708 
709     /** Animates the layout changes when the text of the button becomes visible/gone. */
enableLayoutTransition(ViewGroup settingsAndLockGroup)710     private void enableLayoutTransition(ViewGroup settingsAndLockGroup) {
711         LayoutTransition settingsAndLockTransition = new LayoutTransition();
712         settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING);
713         settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION);
714         settingsAndLockTransition.setInterpolator(LayoutTransition.CHANGING,
715                 Interpolators.STANDARD);
716         settingsAndLockTransition.addTransitionListener(new LayoutTransition.TransitionListener() {
717             @Override
718             public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
719                     View view, int i) {
720             }
721             @Override
722             public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
723                     View view, int i) {
724                 settingsAndLockGroup.setLayoutTransition(null);
725                 mReadyToAnimate = false;
726             }
727         });
728         settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition);
729     }
730 
731     /** Change the settings gear alpha when expanded or collapsed. */
updateSettingsGearAlpha(boolean expand)732     private ValueAnimator updateSettingsGearAlpha(boolean expand) {
733         if (mPSHeader == null) {
734             return new ValueAnimator();
735         }
736         float from = expand ? 0 : 1;
737         float to = expand ? 1 : 0;
738         ValueAnimator settingsAlphaAnim = ObjectAnimator.ofFloat(from, to);
739         settingsAlphaAnim.setDuration(SETTINGS_OPACITY_DURATION);
740         settingsAlphaAnim.setStartDelay(expand ? SETTINGS_OPACITY_DELAY : NO_DELAY);
741         settingsAlphaAnim.setInterpolator(Interpolators.LINEAR);
742         settingsAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
743             @Override
744             public void onAnimationUpdate(ValueAnimator valueAnimator) {
745                 mPSHeader.findViewById(R.id.ps_settings_button)
746                         .setAlpha((float) valueAnimator.getAnimatedValue());
747             }
748         });
749         return settingsAlphaAnim;
750     }
751 
updateLockTextAlpha(boolean expand)752     private ValueAnimator updateLockTextAlpha(boolean expand) {
753         if (mPSHeader == null) {
754             return new ValueAnimator();
755         }
756         float from = expand ? 0 : 1;
757         float to = expand ? 1 : 0;
758         ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
759         alphaAnim.setDuration(expand ? TEXT_UNLOCK_OPACITY_DURATION : TEXT_LOCK_OPACITY_DURATION);
760         alphaAnim.setStartDelay(expand ? LOCK_TEXT_OPACITY_DELAY : NO_DELAY);
761         alphaAnim.setInterpolator(Interpolators.LINEAR);
762         alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
763             @Override
764             public void onAnimationUpdate(ValueAnimator valueAnimator) {
765                 mPSHeader.findViewById(R.id.lock_text).setAlpha(
766                         (float) valueAnimator.getAnimatedValue());
767             }
768         });
769         return alphaAnim;
770     }
771 
expandPrivateSpace()772     void expandPrivateSpace() {
773         // If we are on main adapter view, we apply the PS Container expansion animation and
774         // scroll down to load the entire container, making animation visible.
775         ActivityAllAppsContainerView<?>.AdapterHolder mainAdapterHolder = mAllApps.mAH.get(MAIN);
776         List<BaseAllAppsAdapter.AdapterItem> adapterItems =
777                 mainAdapterHolder.mAppsList.getAdapterItems();
778         if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()
779                 && mAllApps.isPersonalTab()) {
780             // Animate the text and settings icon.
781             DeviceProfile deviceProfile =
782                     ActivityContext.lookupContext(mAllApps.getContext()).getDeviceProfile();
783             scrollForHeaderToBeVisibleInContainer(mainAdapterHolder.mRecyclerView, adapterItems,
784                     getPsHeaderHeight(), deviceProfile.allAppsCellHeightPx);
785             updatePrivateStateAnimator(true);
786         }
787     }
788 
exitSearchAndExpand()789     private void exitSearchAndExpand() {
790         mAllApps.updateHeaderScroll(0);
791         // Animate to A-Z with 0 time to reset the animation with proper state management.
792         mAllApps.animateToSearchState(false, 0);
793         MAIN_EXECUTOR.post(() -> {
794             mAllApps.mSearchUiManager.resetSearch();
795             mAllApps.switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN);
796             expandPrivateSpace();
797         });
798     }
799 
attachFloatingMaskView(boolean expand)800     private void attachFloatingMaskView(boolean expand) {
801         if (!Flags.privateSpaceAddFloatingMaskView()) {
802             return;
803         }
804         mFloatingMaskView = (FloatingMaskView) mAllApps.getLayoutInflater().inflate(
805                 R.layout.private_space_mask_view, mAllApps, false);
806         mAllApps.addView(mFloatingMaskView);
807         // Translate off the screen first if its collapsing so this header view isn't visible to
808         // user when animation starts.
809         if (!expand) {
810             mFloatingMaskView.setTranslationY(getFloatingMaskViewHeight());
811         }
812         mFloatingMaskView.setVisibility(VISIBLE);
813     }
814 
detachFloatingMaskView()815     private void detachFloatingMaskView() {
816         if (mFloatingMaskView != null) {
817             mAllApps.removeView(mFloatingMaskView);
818         }
819         mFloatingMaskView = null;
820     }
821 
822     /** Starts the smooth scroll with the provided smoothScroller and add idle listener. */
startAnimationScroll(AllAppsRecyclerView allAppsRecyclerView, RecyclerView.LayoutManager layoutManager, RecyclerView.SmoothScroller smoothScroller)823     private void startAnimationScroll(AllAppsRecyclerView allAppsRecyclerView,
824             RecyclerView.LayoutManager layoutManager, RecyclerView.SmoothScroller smoothScroller) {
825         mIsScrolling = true;
826         layoutManager.startSmoothScroll(smoothScroller);
827         allAppsRecyclerView.removeOnScrollListener(mOnIdleScrollListener);
828         allAppsRecyclerView.addOnScrollListener(mOnIdleScrollListener);
829     }
830 
getFloatingMaskViewHeight()831     private float getFloatingMaskViewHeight() {
832         return mFloatingMaskViewCornerRadius + getMainRecyclerView().getPaddingBottom();
833     }
834 
getMainRecyclerView()835     AllAppsRecyclerView getMainRecyclerView() {
836         return mAllApps.mAH.get(ActivityAllAppsContainerView.AdapterHolder.MAIN).mRecyclerView;
837     }
838 
839     /** Returns if private space is readily available to be animated. */
getReadyToAnimate()840     boolean getReadyToAnimate() {
841         return mReadyToAnimate;
842     }
843 
844     /** Returns when a smooth scroll is happening. */
isScrolling()845     boolean isScrolling() {
846         return mIsScrolling;
847     }
848 
849     /**
850      * Returns when private space is in the process of transitioning. This is different from
851      * getAnimate() since mStateTransitioning checks from the time transitioning starts happening
852      * in reset() as oppose to when private space is animating. This should be used to ensure
853      * Private Space state during onBind().
854      */
isStateTransitioning()855     boolean isStateTransitioning() {
856         return mIsStateTransitioning;
857     }
858 
getPsHeaderHeight()859     int getPsHeaderHeight() {
860         return mPsHeaderHeight;
861     }
862 
isPrivateSpaceItem(BaseAllAppsAdapter.AdapterItem item)863     boolean isPrivateSpaceItem(BaseAllAppsAdapter.AdapterItem item) {
864         return getItemInfoMatcher().test(item.itemInfo) || item.decorationInfo != null
865                 || (item.itemInfo instanceof PrivateSpaceInstallAppButtonInfo);
866     }
867 }
868