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