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