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