diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index 2c82bd18a7..110d2755bf 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -183,7 +183,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION = "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS"; - private static final long APP_LAUNCH_DURATION = 500; + public static final long APP_LAUNCH_DURATION = 500; private static final long APP_LAUNCH_ALPHA_DURATION = 50; private static final long APP_LAUNCH_ALPHA_START_DELAY = 25; diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java index bd4792358a..5691ecf9fd 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java @@ -19,7 +19,6 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_DENY; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_SEEN; -import android.animation.PropertyValuesHolder; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; @@ -30,7 +29,6 @@ import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; -import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; @@ -164,10 +162,7 @@ public class HotseatEduDialog extends AbstractSlideInView implements I return; } mIsOpen = true; - mOpenCloseAnimator.setValues( - PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); - mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - mOpenCloseAnimator.start(); + setUpDefaultOpenAnimator().start(); } @Override diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java index 7283a184d7..ecf483caa4 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java @@ -27,6 +27,7 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.quickstep.GestureState; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.views.DesktopAppSelectView; import com.android.wm.shell.desktopmode.IDesktopTaskListener; @@ -39,7 +40,8 @@ public class DesktopVisibilityController { private static final String TAG = "DesktopVisController"; private static final boolean DEBUG = false; - + private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_stashing", false); private final Launcher mLauncher; private boolean mFreeformTasksVisible; @@ -73,6 +75,9 @@ public class DesktopVisibilityController { @Override public void onStashedChanged(int displayId, boolean stashed) { + if (!IS_STASHING_ENABLED) { + return; + } MAIN_EXECUTOR.execute(() -> { if (displayId == mLauncher.getDisplayId()) { if (DEBUG) { @@ -166,20 +171,40 @@ public class DesktopVisibilityController { /** * Whether recents gesture is currently in progress. */ - public boolean isGestureInProgress() { + public boolean isRecentsGestureInProgress() { return mGestureInProgress; } /** - * Sets whether recents gesture is in progress. + * Notify controller that recents gesture has started. */ - public void setGestureInProgress(boolean gestureInProgress) { - if (DEBUG) { - Log.d(TAG, "setGestureInProgress: inProgress=" + gestureInProgress); - } + public void setRecentsGestureStart() { if (!isDesktopModeSupported()) { return; } + setRecentsGestureInProgress(true); + } + + /** + * Notify controller that recents gesture finished with the given + * {@link com.android.quickstep.GestureState.GestureEndTarget} + */ + public void setRecentsGestureEnd(@Nullable GestureState.GestureEndTarget endTarget) { + if (!isDesktopModeSupported()) { + return; + } + setRecentsGestureInProgress(false); + + if (endTarget == null) { + // Gesture did not result in a new end target. Ensure launchers gets paused again. + markLauncherPaused(); + } + } + + private void setRecentsGestureInProgress(boolean gestureInProgress) { + if (DEBUG) { + Log.d(TAG, "setGestureInProgress: inProgress=" + gestureInProgress); + } if (gestureInProgress != mGestureInProgress) { mGestureInProgress = gestureInProgress; } @@ -189,7 +214,7 @@ public class DesktopVisibilityController { * Handle launcher moving to home due to home gesture or home button press. */ public void onHomeActionTriggered() { - if (areFreeformTasksVisible()) { + if (IS_STASHING_ENABLED && areFreeformTasksVisible()) { SystemUiProxy.INSTANCE.get(mLauncher).stashDesktopApps(mLauncher.getDisplayId()); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java index 363f915e95..c3ec1e5ad0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java @@ -35,6 +35,8 @@ import android.view.ContextThemeWrapper; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.VisibleForTesting; + import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; @@ -49,7 +51,7 @@ import com.android.launcher3.views.ArrowTipView; */ public class TaskbarHoverToolTipController implements View.OnHoverListener { - private static final int HOVER_TOOL_TIP_REVEAL_START_DELAY = 400; + @VisibleForTesting protected static final int HOVER_TOOL_TIP_REVEAL_START_DELAY = 400; private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 300; private static final int HOVER_TOOL_TIP_EXIT_DURATION = 150; @@ -145,7 +147,6 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener { } private void startRevealHoverToolTip() { - mActivity.setTaskbarWindowFullscreen(true); mHoverToolTipHandler.postDelayed(mRevealHoverToolTipRunnable, HOVER_TOOL_TIP_REVEAL_START_DELAY); } @@ -157,6 +158,7 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener { if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) { return; } + mActivity.setTaskbarWindowFullscreen(true); Rect iconViewBounds = Utilities.getViewBounds(mHoverView); mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(), mTaskbarView.getTop(), /* shouldAutoClose= */ false); diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java index 4f75ef5599..3fe7359573 100644 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java @@ -17,9 +17,6 @@ package com.android.launcher3.taskbar.allapps; import static com.android.app.animation.Interpolators.EMPHASIZED; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.PropertyValuesHolder; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; @@ -31,6 +28,7 @@ import android.window.OnBackInvokedDispatcher; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Insettable; import com.android.launcher3.R; +import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks; import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; @@ -68,16 +66,9 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView mAllAppsCallbacks.onAllAppsTransitionEnd(true))); mOpenCloseAnimator.setDuration(mAllAppsCallbacks.getOpenDuration()).start(); } else { mTranslationShift = TRANSLATION_SHIFT_OPENED; diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index ffd22b89e2..20383f4971 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -664,6 +664,8 @@ public class QuickstepLauncher extends Launcher { @Override public void onAnimationCancel(Animator animation) { getDragLayer().removeView(floatingTaskView); + mSplitSelectStateController.getSplitAnimationController() + .removeSplitInstructionsView(QuickstepLauncher.this); mSplitSelectStateController.resetState(); } }); @@ -865,7 +867,7 @@ public class QuickstepLauncher extends Launcher { if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { DesktopVisibilityController controller = mDesktopVisibilityController; if (controller != null && controller.areFreeformTasksVisible() - && !controller.isGestureInProgress()) { + && !controller.isRecentsGestureInProgress()) { // Return early to skip setting activity to appear as resumed // TODO(b/255649902): shouldn't be needed when we have a separate launcher state // for desktop that we can use to control other parts of launcher diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index 1744b08cf5..06f1f9a6f4 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -159,6 +159,10 @@ public class TaskOverlayFactory implements ResourceBasedOverride { return mActionsView; } + public TaskThumbnailView getThumbnailView() { + return mThumbnailView; + } + /** * Called when the current task is interactive for the user */ diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 56d68570ac..bcb9cecb1d 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -265,6 +265,11 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC return anim } + /** Removes the split instructions view from [launcher] drag layer. */ + fun removeSplitInstructionsView(launcher: StatefulActivity<*>) { + safeRemoveViewFromDragLayer(launcher, splitInstructionsView) + } + private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) { if (view != null) { launcher.dragLayer.removeView(view) diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java index 148a45a386..b36cf5f5b4 100644 --- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java +++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java @@ -169,6 +169,7 @@ public class SplitToWorkspaceController { private void cleanUp() { mLauncher.getDragLayer().removeView(firstFloatingTaskView); mLauncher.getDragLayer().removeView(secondFloatingTaskView); + mController.getSplitAnimationController().removeSplitInstructionsView(mLauncher); mController.resetState(); } }); diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java index 5e488cc66d..4f119c07bc 100644 --- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java +++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java @@ -249,7 +249,7 @@ public class LauncherRecentsView extends RecentsView extends FrameLayo return mMultiValueAlpha.get(INDEX_SCROLL_ALPHA); } + /** + * Returns the visibility of the overview actions buttons. + */ + public @Visibility int getActionsButtonVisibility() { + return findViewById(R.id.action_buttons).getVisibility(); + } + /** * Offsets OverviewActionsView horizontal position based on 3 button nav container in taskbar. */ diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 00719275d1..29d9fa6819 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -4823,6 +4823,8 @@ public abstract class RecentsView mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView); mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); + final AllAppsRecyclerView mainRecyclerView; + final AllAppsRecyclerView workRecyclerView; if (mUsingTabs) { - mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher); - mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher()); - mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work); - if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) { - // Let main and work rv share same view pool. - ((RecyclerView) mViewPager.getChildAt(0)) - .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool()); - ((RecyclerView) mViewPager.getChildAt(1)) - .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool()); - } + mainRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(0); + workRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(1); + mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher); + mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getMatcher()); + workRecyclerView.setId(R.id.apps_list_view_work); if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) { mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener( mWorkManager.newScrollListener()); @@ -587,13 +585,15 @@ public class ActivityAllAppsContainerView onActivePageChanged(mViewPager.getNextPage()); } } else { - mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null); + mainRecyclerView = findViewById(R.id.apps_list_view); + workRecyclerView = null; + mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, null); mAH.get(AdapterHolder.WORK).mRecyclerView = null; - if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) { - mAH.get(AdapterHolder.MAIN).mRecyclerView - .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool()); - } } + setUpCustomRecyclerViewPool( + mainRecyclerView, + workRecyclerView, + mAllAppsStore.getRecyclerViewPool()); setupHeader(); if (isSearchBarFloating()) { @@ -610,6 +610,30 @@ public class ActivityAllAppsContainerView mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView); } + /** + * If {@link ENABLE_ALL_APPS_RV_PREINFLATION} is enabled, wire custom + * {@link RecyclerView.RecycledViewPool} to main and work {@link AllAppsRecyclerView}. + * + * Then if {@link ALL_APPS_GONE_VISIBILITY} is enabled, update max pool size. This is because + * all apps rv's hidden visibility is changed to {@link View#GONE} from {@link View#INVISIBLE), + * thus we cannot rely on layout pass to update pool size. + */ + private static void setUpCustomRecyclerViewPool( + @NonNull AllAppsRecyclerView mainRecyclerView, + @Nullable AllAppsRecyclerView workRecyclerView, + @NonNull RecyclerView.RecycledViewPool recycledViewPool) { + if (!ENABLE_ALL_APPS_RV_PREINFLATION.get()) { + return; + } + mainRecyclerView.setRecycledViewPool(recycledViewPool); + if (workRecyclerView != null) { + workRecyclerView.setRecycledViewPool(recycledViewPool); + } + if (ALL_APPS_GONE_VISIBILITY.get()) { + mainRecyclerView.updatePoolSize(); + } + } + private void replaceAppsRVContainer(boolean showTabs) { for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) { AdapterHolder adapterHolder = mAH.get(i); diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 602d1a38a5..7edbeac71f 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.allapps; +import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY; import static com.android.launcher3.logger.LauncherAtom.ContainerInfo; import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN; @@ -26,6 +27,8 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_COLLAPSE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_EXTEND; +import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.EXTRA_ICONS_COUNT; +import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.PREINFLATE_ICONS_ROW_COUNT; import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING; import android.content.Context; @@ -96,8 +99,18 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView { int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1); pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1); + + // If all apps' hidden visibility is INVISIBLE, we will need to preinflate one page of + // all apps icons for smooth scrolling. + int maxPoolSizeForAppIcons = (approxRows + 1) * grid.numShownAllAppsColumns; + if (ALL_APPS_GONE_VISIBILITY.get()) { + // If all apps' hidden visibility is GONE, we need to increase prefinated icons number + // by [PREINFLATE_ICONS_ROW_COUNT] rows + [EXTRA_ICONS_COUNT] for fast opening all apps. + maxPoolSizeForAppIcons += + PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT; + } pool.setMaxRecycledViews( - AllAppsGridAdapter.VIEW_TYPE_ICON, (approxRows + 1) * grid.numShownAllAppsColumns); + AllAppsGridAdapter.VIEW_TYPE_ICON, maxPoolSizeForAppIcons); } @Override diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index 0d7b736cc8..c09a5b9d93 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -438,7 +438,8 @@ public class AllAppsTransitionController mAppsView = appsView; mAppsView.setScrimView(scrimView); - mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT); + mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT, + FeatureFlags.ALL_APPS_GONE_VISIBILITY.get() ? View.GONE : View.INVISIBLE); mAppsViewAlpha.setUpdateVisibility(true); mAppsViewTranslationY = new MultiPropertyFactory<>( mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum); diff --git a/src/com/android/launcher3/anim/AlphaUpdateListener.java b/src/com/android/launcher3/anim/AlphaUpdateListener.java index 8dad1b4fa5..4382174530 100644 --- a/src/com/android/launcher3/anim/AlphaUpdateListener.java +++ b/src/com/android/launcher3/anim/AlphaUpdateListener.java @@ -53,8 +53,18 @@ public class AlphaUpdateListener extends AnimatorListenerAdapter } public static void updateVisibility(View view) { - if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != View.INVISIBLE) { - view.setVisibility(View.INVISIBLE); + updateVisibility(view, View.INVISIBLE); + } + + /** + * Update view's visibility. + * + * @param view View that needs to update visibility. + * @param hiddenVisibility {@link View#GONE} or {@link View#INVISIBLE} + */ + public static void updateVisibility(View view, int hiddenVisibility) { + if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != hiddenVisibility) { + view.setVisibility(hiddenVisibility); } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != View.VISIBLE) { if (view instanceof ViewGroup) { diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index d2f37fbb5e..cabcabcb28 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -83,11 +83,11 @@ public final class FeatureFlags { */ // TODO(Block 1): Clean up flags public static final BooleanFlag ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES = getReleaseFlag( - 270394041, "ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES", DISABLED, + 270394041, "ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES", TEAMFOOD, "Enable option to replace decorator-based search result backgrounds with drawables"); public static final BooleanFlag ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION = getReleaseFlag( - 270394392, "ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION", DISABLED, + 270394392, "ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION", TEAMFOOD, "Enable option to launch search results using the new view container transitions"); // TODO(Block 2): Clean up flags @@ -395,10 +395,15 @@ public final class FeatureFlags { // TODO(Block 33): Clean up flags public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355, - "ENABLE_ALL_APPS_RV_PREINFLATION", DISABLED, + "ENABLE_ALL_APPS_RV_PREINFLATION", ENABLED, "Enables preinflating all apps icons to avoid scrolling jank."); - // TODO(Block 34): Empty block + // TODO(Block 34): Clean up flags + public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514, + "ALL_APPS_GONE_VISIBILITY", ENABLED, + "Set all apps container view's hidden visibility to GONE instead of INVISIBLE."); + + // TODO(Block 35): Empty block public static class BooleanFlag { diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index ba1547f4be..9afa4593a9 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -49,7 +49,6 @@ import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Animation; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Workspace; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logger.LauncherAtom.AllAppsContainer; import com.android.launcher3.logger.LauncherAtom.ContainerInfo; @@ -323,9 +322,7 @@ public class ItemInfo { * Returns whether this item should use the background animation. */ public boolean shouldUseBackgroundAnimation() { - return animationType == LauncherSettings.Animation.VIEW_BACKGROUND - && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get() - && FeatureFlags.ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION.get(); + return animationType == LauncherSettings.Animation.VIEW_BACKGROUND; } /** diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt index 26dde29d36..3c59c1d63f 100644 --- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt +++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt @@ -22,13 +22,14 @@ import androidx.recyclerview.widget.RecyclerView.RecycledViewPool import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.android.launcher3.BubbleTextView import com.android.launcher3.allapps.BaseAllAppsAdapter +import com.android.launcher3.config.FeatureFlags import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR import com.android.launcher3.views.ActivityContext import java.util.concurrent.Future -private const val PREINFLATE_ICONS_ROW_COUNT = 4 -private const val EXTRA_ICONS_COUNT = 2 +const val PREINFLATE_ICONS_ROW_COUNT = 4 +const val EXTRA_ICONS_COUNT = 2 /** * An [RecycledViewPool] that preinflates app icons ([ViewHolder] of [BubbleTextView]) of all apps @@ -81,11 +82,21 @@ class AllAppsRecyclerViewPool : RecycledViewPool() { * After testing on phone, foldable and tablet, we found [PREINFLATE_ICONS_ROW_COUNT] rows of * app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to * suffice fast scrolling. + * + * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra + * app icons in size of one all apps pages, so that opening all apps don't need to inflate app + * icons. */ fun getPreinflateCount(context: T): Int where T : Context, T : ActivityContext { - val targetPreinflateCount = + var targetPreinflateCount = PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns + EXTRA_ICONS_COUNT + if (FeatureFlags.ALL_APPS_GONE_VISIBILITY.get()) { + val grid = ActivityContext.lookupContext(context).deviceProfile + val approxRows = + Math.ceil((grid.availableHeightPx / grid.allAppsIconSizePx).toDouble()).toInt() + targetPreinflateCount += (approxRows + 1) * grid.numShownAllAppsColumns + } val existingPreinflateCount = getRecycledViewCount(BaseAllAppsAdapter.VIEW_TYPE_ICON) return targetPreinflateCount - existingPreinflateCount } diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java index ac016a8595..a66a9d2d56 100644 --- a/src/com/android/launcher3/util/MultiValueAlpha.java +++ b/src/com/android/launcher3/util/MultiValueAlpha.java @@ -32,8 +32,15 @@ public class MultiValueAlpha extends MultiPropertyFactory { // Whether we should change from INVISIBLE to VISIBLE and vice versa at low alpha values. private boolean mUpdateVisibility; + private final int mHiddenVisibility; + public MultiValueAlpha(View view, int size) { + this(view, size, View.INVISIBLE); + } + + public MultiValueAlpha(View view, int size, int hiddenVisibility) { super(view, VIEW_ALPHA, size, ALPHA_AGGREGATOR, 1f); + this.mHiddenVisibility = hiddenVisibility; } /** Sets whether we should update between INVISIBLE and VISIBLE based on alpha. */ @@ -45,7 +52,7 @@ public class MultiValueAlpha extends MultiPropertyFactory { protected void apply(float value) { super.apply(value); if (mUpdateVisibility) { - AlphaUpdateListener.updateVisibility(mTarget); + AlphaUpdateListener.updateVisibility(mTarget, mHiddenVisibility); } } } diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java index de10fc518b..f69d299cf3 100644 --- a/src/com/android/launcher3/views/AbstractSlideInView.java +++ b/src/com/android/launcher3/views/AbstractSlideInView.java @@ -24,8 +24,7 @@ import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCES import static com.android.launcher3.allapps.AllAppsTransitionController.REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS; import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.content.Context; @@ -51,6 +50,7 @@ import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.touch.BaseSwipeDetector; import com.android.launcher3.touch.SingleAxisSwipeDetector; @@ -82,15 +82,19 @@ public abstract class AbstractSlideInView protected static final float TRANSLATION_SHIFT_CLOSED = 1f; protected static final float TRANSLATION_SHIFT_OPENED = 0f; private static final float VIEW_NO_SCALE = 1f; + private static final int NO_DURATION = -1; protected final T mActivityContext; protected final SingleAxisSwipeDetector mSwipeDetector; - protected final ObjectAnimator mOpenCloseAnimator; + protected @NonNull AnimatorSet mOpenCloseAnimator; + private final ObjectAnimator mTranslationShiftAnimator; protected ViewGroup mContent; protected final View mColorScrim; - protected Interpolator mScrollInterpolator; + + private Interpolator mScrollInterpolator; + private long mScrollDuration; // range [0, 1], 0=> completely open, 1=> completely closed protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED; @@ -104,8 +108,8 @@ public abstract class AbstractSlideInView protected final AnimatedFloat mSlideInViewScale = new AnimatedFloat(this::onScaleProgressChanged, VIEW_NO_SCALE); protected boolean mIsBackProgressing; - @Nullable private Drawable mContentBackground; - @Nullable private View mContentBackgroundParentView; + private @Nullable Drawable mContentBackground; + private @Nullable View mContentBackgroundParentView; protected final ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() { @Override @@ -124,21 +128,52 @@ public abstract class AbstractSlideInView mActivityContext = ActivityContext.lookupContext(context); mScrollInterpolator = Interpolators.SCROLL_CUBIC; + mScrollDuration = NO_DURATION; mSwipeDetector = new SingleAxisSwipeDetector(context, this, SingleAxisSwipeDetector.VERTICAL); - mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this); - mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mSwipeDetector.finishedScrolling(); - announceAccessibilityChanges(); - } - }); + mOpenCloseAnimator = new AnimatorSet(); + mTranslationShiftAnimator = ObjectAnimator.ofPropertyValuesHolder(this); + int scrimColor = getScrimColor(context); mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null; } + /** + * Sets up a {@link #mOpenCloseAnimator} for opening with default parameters. + * + * @see #setUpOpenCloseAnimator(float, Interpolator) + */ + protected final AnimatorSet setUpDefaultOpenAnimator() { + return setUpOpenCloseAnimator(TRANSLATION_SHIFT_OPENED, Interpolators.FAST_OUT_SLOW_IN); + } + + /** + * Initializes a new {@link #mOpenCloseAnimator}. + *

+ * Subclasses should override this method if they want to add more {@code Animator} instances + * to the set. + * + * @param translationShift translation shift to animate to. + * @param translationShiftInterpolator interpolator for {@link #mTranslationShiftAnimator}. + * @return {@link #mOpenCloseAnimator} + */ + protected AnimatorSet setUpOpenCloseAnimator( + float translationShift, Interpolator translationShiftInterpolator) { + mOpenCloseAnimator = new AnimatorSet(); + mOpenCloseAnimator.addListener(AnimatorListeners.forEndCallback(() -> { + mSwipeDetector.finishedScrolling(); + announceAccessibilityChanges(); + })); + + mTranslationShiftAnimator.setValues(PropertyValuesHolder.ofFloat( + TRANSLATION_SHIFT, translationShift)); + mTranslationShiftAnimator.setInterpolator(translationShiftInterpolator); + mOpenCloseAnimator.play(mTranslationShiftAnimator); + + return mOpenCloseAnimator; + } + protected void attachToContainer() { if (mColorScrim != null) { getPopupContainer().addView(mColorScrim); @@ -309,16 +344,13 @@ public abstract class AbstractSlideInView if ((mSwipeDetector.isFling(velocity) && velocity > 0) || mTranslationShift > successfulShiftThreshold) { mScrollInterpolator = scrollInterpolatorForVelocity(velocity); - mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration( - velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift)); + mScrollDuration = BaseSwipeDetector.calculateDuration( + velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift); close(true); } else { - mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat( - TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); - mOpenCloseAnimator.setDuration( - BaseSwipeDetector.calculateDuration(velocity, mTranslationShift)) - .setInterpolator(Interpolators.DECELERATE); - mOpenCloseAnimator.start(); + setUpOpenCloseAnimator(TRANSLATION_SHIFT_OPENED, Interpolators.DECELERATE) + .setDuration(BaseSwipeDetector.calculateDuration(velocity, mTranslationShift)) + .start(); } } @@ -344,23 +376,19 @@ public abstract class AbstractSlideInView onCloseComplete(); return; } - mOpenCloseAnimator.setValues( - PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED)); - mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mOpenCloseAnimator.removeListener(this); - onCloseComplete(); - } - }); + + final Interpolator interpolator; + final long duration; if (mSwipeDetector.isIdleState()) { - mOpenCloseAnimator - .setDuration(defaultDuration) - .setInterpolator(getIdleInterpolator()); + interpolator = getIdleInterpolator(); + duration = defaultDuration; } else { - mOpenCloseAnimator.setInterpolator(mScrollInterpolator); + interpolator = mScrollInterpolator; + duration = mScrollDuration > NO_DURATION ? mScrollDuration : defaultDuration; } - mOpenCloseAnimator.start(); + setUpOpenCloseAnimator(TRANSLATION_SHIFT_CLOSED, interpolator) + .addListener(AnimatorListeners.forEndCallback(this::onCloseComplete)); + mOpenCloseAnimator.setDuration(duration).start(); } protected Interpolator getIdleInterpolator() { diff --git a/src/com/android/launcher3/views/WidgetsEduView.java b/src/com/android/launcher3/views/WidgetsEduView.java index 918078178c..92e048bbf5 100644 --- a/src/com/android/launcher3/views/WidgetsEduView.java +++ b/src/com/android/launcher3/views/WidgetsEduView.java @@ -15,9 +15,6 @@ */ package com.android.launcher3.views; -import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN; - -import android.animation.PropertyValuesHolder; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; @@ -123,10 +120,7 @@ public class WidgetsEduView extends AbstractSlideInView implements Ins return; } mIsOpen = true; - mOpenCloseAnimator.setValues( - PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); - mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN); - mOpenCloseAnimator.start(); + setUpDefaultOpenAnimator().start(); } /** Shows widget education dialog. */ diff --git a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java index 473abf1b1b..c6fc5fef85 100644 --- a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java @@ -16,10 +16,8 @@ package com.android.launcher3.widget; -import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.Utilities.ATLEAST_R; -import android.animation.PropertyValuesHolder; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Insets; @@ -134,10 +132,7 @@ public class AddItemWidgetsBottomSheet extends AbstractSlideInView { - mOpenCloseAnimator.start(); + mOpenCloseAnimator + .setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration) + .start(); mContent.animate().alpha(1).setDuration(FADE_IN_DURATION); }); } else { diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml index 28688fd6fc..bb61fbe440 100644 --- a/tests/AndroidManifest-common.xml +++ b/tests/AndroidManifest-common.xml @@ -32,6 +32,7 @@ @@ -65,6 +66,7 @@ @@ -76,6 +78,7 @@ @@ -87,6 +90,7 @@ diff --git a/tests/res/drawable/test_widget_dynamic_colors_icon.xml b/tests/res/drawable/test_widget_dynamic_colors_icon.xml new file mode 100644 index 0000000000..69f66757f6 --- /dev/null +++ b/tests/res/drawable/test_widget_dynamic_colors_icon.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/tests/res/drawable/test_widget_no_config_icon.xml b/tests/res/drawable/test_widget_no_config_icon.xml new file mode 100644 index 0000000000..e3d012529d --- /dev/null +++ b/tests/res/drawable/test_widget_no_config_icon.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/tests/res/drawable/test_widget_with_config_icon.xml b/tests/res/drawable/test_widget_with_config_icon.xml new file mode 100644 index 0000000000..98b797b870 --- /dev/null +++ b/tests/res/drawable/test_widget_with_config_icon.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/tests/res/drawable/test_widget_with_dialog_icon.xml b/tests/res/drawable/test_widget_with_dialog_icon.xml new file mode 100644 index 0000000000..d2879d281b --- /dev/null +++ b/tests/res/drawable/test_widget_with_dialog_icon.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index b67478f1c9..753d89d8b8 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -318,6 +318,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { } @Test + @Ignore // b/293191790 @PortraitLandscape public void testWidgets() throws Exception { // Test opening widgets. diff --git a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt index b4ad1f3542..f3f9b89b1c 100644 --- a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt +++ b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt @@ -26,8 +26,13 @@ import com.android.app.viewcapture.data.ExportedData import com.android.launcher3.tapl.TestHelpers import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer -import org.junit.Assert.assertTrue +import java.io.BufferedOutputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStreamWriter import java.util.function.Supplier +import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @@ -81,7 +86,7 @@ class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier) : Te MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) } } - analyzeViewCapture() + analyzeViewCapture(description) } private fun startCapturingExistingActivity( @@ -107,16 +112,39 @@ class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier) : Te } } - private fun analyzeViewCapture() { + private fun analyzeViewCapture(description: Description) { // OOP tests don't produce ViewCapture data if (!TestHelpers.isInLauncherProcess()) return - ViewCaptureAnalyzer.assertNoAnomalies(viewCaptureData) - var frameCount = 0 for (i in 0 until viewCaptureData!!.windowDataCount) { frameCount += viewCaptureData!!.getWindowData(i).frameDataCount } assertTrue("Empty ViewCapture data", frameCount > 0) + + val anomalies: Map = ViewCaptureAnalyzer.getAnomalies(viewCaptureData) + if (!anomalies.isEmpty()) { + val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt") + try { + OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer -> + writer.write("View animation anomalies detected.\r\n") + writer.write( + "To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n" + ) + writer.write("List of views with animation anomalies:\r\n") + + for ((viewPath, message) in anomalies) { + writer.write("View: $viewPath\r\n $message\r\n") + } + } + } catch (ex: IOException) { + throw RuntimeException(ex) + } + + val (viewPath, message) = anomalies.entries.first() + fail( + "${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details." + ) + } } } diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java index cb404b966c..7963176828 100644 --- a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java +++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java @@ -113,7 +113,7 @@ final class AlphaJumpDetector extends AnomalyDetector { DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container", DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container", CONTENT + "LauncherRootView:id/launcher|FloatingIconView", - RECENTS_DRAG_LAYER + "ArrowTipView|View:id/arrow", + RECENTS_DRAG_LAYER + "ArrowTipView", DRAG_LAYER + "FallbackRecentsView:id/overview_panel", RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel", DRAG_LAYER @@ -212,24 +212,26 @@ final class AlphaJumpDetector extends AnomalyDetector { } @Override - void detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) { + String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) { // If the view was previously seen, proceed with analysis only if it was present in the // view hierarchy in the previous frame. - if (oldInfo != null && oldInfo.frameN != frameN) return; + if (oldInfo != null && oldInfo.frameN != frameN) return null; final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo; - if (getNodeData(latestInfo).ignoreAlphaJumps) return; + final NodeData nodeData = getNodeData(latestInfo); + if (nodeData.ignoreAlphaJumps) return null; final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0; final float newAlpha = newInfo != null ? newInfo.alpha : 0; final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha); if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) { - throw new AssertionError( - String.format( - "Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)" - + ", threshold: %s, view: %s", - alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo)); + nodeData.ignoreAlphaJumps = true; // No need to report alpha jump in children. + return String.format( + "Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)" + + ", threshold: %s, %s", // ----------- no need to include view? + alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo); } + return null; } } diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java index 2cf38437f5..949c5368ae 100644 --- a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java +++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java @@ -61,8 +61,9 @@ public class ViewCaptureAnalyzer { * the view is not present in the 'currentFrame', but was present in earlier * frames. * @param frameN number of the current frame. + * @return Anomaly diagnostic message if an anomaly has been detected; null otherwise. */ - abstract void detectAnomalies( + abstract String detectAnomalies( @Nullable AnalysisNode oldInfo, @Nullable AnalysisNode newInfo, int frameN); } @@ -101,25 +102,31 @@ public class ViewCaptureAnalyzer { @Override public String toString() { - return String.format("window coordinates: (%s, %s), class path from the root: %s", - left, top, diagPathFromRoot(this)); + return String.format("view window coordinates: (%s, %s)", left, top); } } /** - * Scans a view capture record and throws an error if an anomaly is found. + * Scans a view capture record and searches for view animation anomalies. Can find anomalies for + * multiple views. + * Returns a map from the view path to the anomaly message for the view. Non-empty map means + * that anomalies were detected. */ - public static void assertNoAnomalies(ExportedData viewCaptureData) { + public static Map getAnomalies(ExportedData viewCaptureData) { + final Map anomalies = new HashMap<>(); + final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS); final int windowDataCount = viewCaptureData.getWindowDataCount(); for (int i = 0; i < windowDataCount; ++i) { - analyzeWindowData(viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex); + analyzeWindowData( + viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex, anomalies); } + return anomalies; } private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData, - int scrimClassIndex) { + int scrimClassIndex, Map anomalies) { // View hash code => Last seen node with this hash code. // The view is added when we analyze the first frame where it's visible. // After that, it gets updated for every frame where it's visible. @@ -128,12 +135,13 @@ public class ViewCaptureAnalyzer { for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) { analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes, - scrimClassIndex); + scrimClassIndex, anomalies); } } private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData, - Map lastSeenNodes, int scrimClassIndex) { + Map lastSeenNodes, int scrimClassIndex, + Map anomalies) { // Analyze the node tree starting from the root. analyzeView( frame.getNode(), @@ -143,7 +151,8 @@ public class ViewCaptureAnalyzer { /* topShift = */ 0, viewCaptureData, lastSeenNodes, - scrimClassIndex); + scrimClassIndex, + anomalies); // Analyze transitions when a view visible in the last frame become invisible in the // current one. @@ -151,10 +160,14 @@ public class ViewCaptureAnalyzer { if (info.frameN == frameN - 1) { if (!info.viewCaptureNode.getWillNotDraw()) { Arrays.stream(ANOMALY_DETECTORS).forEach( - detector -> detector.detectAnomalies( - /* oldInfo = */ info, - /* newInfo = */ null, - frameN)); + detector -> + detectAnomaly( + detector, + frameN, + /* oldInfo = */ info, + /* newInfo = */ null, + anomalies) + ); } } } @@ -162,7 +175,8 @@ public class ViewCaptureAnalyzer { private static void analyzeView(ViewNode viewCaptureNode, AnalysisNode parent, int frameN, float leftShift, float topShift, ExportedData viewCaptureData, - Map lastSeenNodes, int scrimClassIndex) { + Map lastSeenNodes, int scrimClassIndex, + Map anomalies) { // Skip analysis of invisible views final float parentAlpha = parent != null ? parent.alpha : 1; final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha); @@ -205,7 +219,10 @@ public class ViewCaptureAnalyzer { final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) { Arrays.stream(ANOMALY_DETECTORS).forEach( - detector -> detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN)); + detector -> + detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode, + anomalies) + ); } lastSeenNodes.put(hashcode, newAnalysisNode); @@ -220,8 +237,20 @@ public class ViewCaptureAnalyzer { if (child.getClassnameIndex() == scrimClassIndex) break; analyzeView(child, newAnalysisNode, frameN, leftShiftForChildren, topShiftForChildren, - viewCaptureData, lastSeenNodes, - scrimClassIndex); + viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies); + } + } + + private static void detectAnomaly(AnomalyDetector detector, int frameN, + AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode, + Map anomalies) { + final String maybeAnomaly = + detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN); + if (maybeAnomaly != null) { + final String viewDiagPath = diagPathFromRoot(newAnalysisNode); + if (!anomalies.containsKey(viewDiagPath)) { + anomalies.put(viewDiagPath, maybeAnomaly); + } } } @@ -235,9 +264,11 @@ public class ViewCaptureAnalyzer { return className.substring(className.lastIndexOf(".") + 1); } - private static String diagPathFromRoot(AnalysisNode nodeBox) { - final StringBuilder path = new StringBuilder(nodeBox.nodeIdentity); - for (AnalysisNode ancestor = nodeBox.parent; ancestor != null; ancestor = ancestor.parent) { + private static String diagPathFromRoot(AnalysisNode analysisNode) { + final StringBuilder path = new StringBuilder(analysisNode.nodeIdentity); + for (AnalysisNode ancestor = analysisNode.parent; + ancestor != null; + ancestor = ancestor.parent) { path.insert(0, ancestor.nodeIdentity + "|"); } return path.toString();