diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index f1f9966f78..15ac9e38fc 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -310,6 +310,13 @@ flag { } } +flag { + name: "enable_container_return_animations" + namespace: "launcher" + description: "Enables the container return animation mirroring launches." + bug: "341017746" +} + flag { name: "floating_search_bar" namespace: "launcher" diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java index 15180efbcc..d973149747 100644 --- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java +++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java @@ -238,5 +238,12 @@ public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat { @Override @UiThread default void onAnimationCancelled() {} + + /** + * Returns whether this animation factory supports a tightly coupled return animation. + */ + default boolean supportsReturnTransition() { + return false; + } } } diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index fae281a70a..5a74f4aafc 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -43,6 +43,7 @@ import static com.android.launcher3.BaseActivity.INVISIBLE_ALL; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS; import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION; +import static com.android.launcher3.Flags.enableContainerReturnAnimations; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; @@ -68,6 +69,7 @@ import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITION import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch; import static com.android.quickstep.util.AnimUtils.clampToDuration; import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius; import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows; @@ -181,6 +183,9 @@ import java.util.List; */ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener { + private static final String TRANSITION_COOKIE_PREFIX = + "com.android.launcher3.QuickstepTransitionManager_activityLaunch"; + private static final boolean ENABLE_SHELL_STARTING_SURFACE = SystemProperties.getBoolean("persist.debug.shell_starting_surface", true); @@ -333,17 +338,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener restartedListener.register(onEndCallback::executeAllAndDestroy); onEndCallback.add(restartedListener::unregister); - mAppLaunchRunner = new AppLaunchAnimationRunner(v, onEndCallback); - ItemInfo tag = (ItemInfo) v.getTag(); - if (tag != null && tag.shouldUseBackgroundAnimation()) { - ContainerAnimationRunner containerAnimationRunner = ContainerAnimationRunner.from( - v, mLauncher, mStartingWindowListener, onEndCallback); - if (containerAnimationRunner != null) { - mAppLaunchRunner = containerAnimationRunner; - } - } - RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner( - mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */); + RemoteAnimationRunnerCompat runner = createAppLaunchRunner(v, onEndCallback); // Note that this duration is a guess as we do not know if the animation will be a // recents launch or not for sure until we know the opening app targets. @@ -360,9 +355,94 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback); options.setOnAnimationAbortListener(endCallback); options.setOnAnimationFinishedListener(endCallback); + + IBinder cookie = mAppLaunchRunner.supportsReturnTransition() + ? ((ContainerAnimationRunner) mAppLaunchRunner).getCookie() : null; + addLaunchCookie(cookie, (ItemInfo) v.getTag(), options); + + // Register the return animation so it can be triggered on back from the app to home. + maybeRegisterAppReturnTransition(v); + return new ActivityOptionsWrapper(options, onEndCallback); } + /** + * Selects the appropriate type of launch runner for the given view, builds it, and returns it. + * {@link QuickstepTransitionManager#mAppLaunchRunner} is updated as a by-product of this + * method. + */ + private RemoteAnimationRunnerCompat createAppLaunchRunner(View v, RunnableList onEndCallback) { + ItemInfo tag = (ItemInfo) v.getTag(); + ContainerAnimationRunner containerRunner = null; + if (tag != null && tag.shouldUseBackgroundAnimation()) { + // The cookie should only override the default used by launcher if container return + // animations are enabled. + ActivityTransitionAnimator.TransitionCookie cookie = + checkReturnAnimationsFlags() + ? new ActivityTransitionAnimator.TransitionCookie( + TRANSITION_COOKIE_PREFIX + tag.id) + : null; + ContainerAnimationRunner launchAnimationRunner = + ContainerAnimationRunner.fromView( + v, cookie, true /* forLaunch */, mLauncher, mStartingWindowListener, + onEndCallback); + + if (launchAnimationRunner != null) { + containerRunner = launchAnimationRunner; + } + } + + mAppLaunchRunner = containerRunner != null + ? containerRunner : new AppLaunchAnimationRunner(v, onEndCallback); + return new LauncherAnimationRunner( + mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */); + } + + /** + * If container return animations are enabled and the current launch runner is itself a + * {@link ContainerAnimationRunner}, registers a matching return animation that de-registers + * itself after it has run once or is made obsolete by the view going away. + */ + private void maybeRegisterAppReturnTransition(View v) { + if (!checkReturnAnimationsFlags() || !mAppLaunchRunner.supportsReturnTransition()) { + return; + } + + ActivityTransitionAnimator.TransitionCookie cookie = + ((ContainerAnimationRunner) mAppLaunchRunner).getCookie(); + RunnableList onEndCallback = new RunnableList(); + ContainerAnimationRunner runner = + ContainerAnimationRunner.fromView( + v, cookie, false /* forLaunch */, mLauncher, mStartingWindowListener, + onEndCallback); + RemoteTransition transition = + new RemoteTransition( + new LauncherAnimationRunner( + mHandler, runner, true /* startAtFrontOfQueue */ + ).toRemoteTransition() + ); + + SystemUiProxy.INSTANCE.get(mLauncher).registerRemoteTransition( + transition, ContainerAnimationRunner.buildBackToHomeFilter(cookie, mLauncher)); + ContainerAnimationRunner.setUpRemoteAnimationCleanup( + v, transition, onEndCallback, mLauncher); + } + + /** + * Adds a new launch cookie for the activity launch if supported. + * Prioritizes the explicitly provided cookie, falling back on extracting one from the given + * {@link ItemInfo} if necessary. + */ + private void addLaunchCookie(IBinder cookie, ItemInfo info, ActivityOptions options) { + if (cookie == null) { + cookie = mLauncher.getLaunchCookie(info); + } + + if (cookie != null) { + options.setLaunchCookie(cookie); + } + } + /** * Whether the launch is a recents app transition and we should do a launch animation * from the recents view. Note that if the remote animation targets are not provided, this @@ -1728,6 +1808,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } + private static boolean checkReturnAnimationsFlags() { + return enableContainerReturnAnimations() && returnAnimationFrameworkLibrary(); + } + /** * Remote animation runner for animation from the app to Launcher, including recents. */ @@ -1844,38 +1928,45 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener /** The delegate runner that handles the actual animation. */ private final RemoteAnimationDelegate mDelegate; + @Nullable + private final ActivityTransitionAnimator.TransitionCookie mCookie; + private ContainerAnimationRunner( - RemoteAnimationDelegate delegate) { + RemoteAnimationDelegate delegate, + ActivityTransitionAnimator.TransitionCookie cookie) { mDelegate = delegate; + mCookie = cookie; } @Nullable - private static ContainerAnimationRunner from(View v, Launcher launcher, - StartingWindowListener startingWindowListener, RunnableList onEndCallback) { - View viewToUse = findLaunchableViewWithBackground(v); - if (viewToUse == null) { - return null; + ActivityTransitionAnimator.TransitionCookie getCookie() { + return mCookie; + } + + @Nullable + static ContainerAnimationRunner fromView( + View v, + ActivityTransitionAnimator.TransitionCookie cookie, + boolean forLaunch, + Launcher launcher, + StartingWindowListener startingWindowListener, + RunnableList onEndCallback) { + if (!forLaunch && !checkReturnAnimationsFlags()) { + throw new IllegalStateException( + "forLaunch cannot be false when the enableContainerReturnAnimations or " + + "returnAnimationFrameworkLibrary flag is disabled"); } - // The CUJ is logged by the click handler, so we don't log it inside the animation - // library. - ActivityTransitionAnimator.Controller controllerDelegate = - ActivityTransitionAnimator.Controller.fromView(viewToUse, null /* cujType */); - - if (controllerDelegate == null) { - return null; - } - - // This wrapper allows us to override the default value, telling the controller that the - // current window is below the animating window. + // First the controller is created. This is used by the runner to animate the + // origin/target view. ActivityTransitionAnimator.Controller controller = - new DelegateTransitionAnimatorController(controllerDelegate) { - @Override - public boolean isBelowAnimatingWindow() { - return true; - } - }; + buildController(v, cookie, forLaunch); + if (controller == null) { + return null; + } + // The callback is used to make sure that we use the right color to fade between view + // and the window. ActivityTransitionAnimator.Callback callback = task -> { final int backgroundColor = startingWindowListener.mBackgroundColor == Color.TRANSPARENT @@ -1894,7 +1985,52 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return new ContainerAnimationRunner( new ActivityTransitionAnimator.AnimationDelegate( - MAIN_EXECUTOR, controller, callback, listener)); + MAIN_EXECUTOR, controller, callback, listener), + cookie); + } + + /** + * Constructs a {@link ActivityTransitionAnimator.Controller} that can be used by a + * {@link ContainerAnimationRunner} to animate a view into an opening window or from a + * closing one. + */ + @Nullable + private static ActivityTransitionAnimator.Controller buildController( + View v, ActivityTransitionAnimator.TransitionCookie cookie, boolean isLaunching) { + View viewToUse = findLaunchableViewWithBackground(v); + if (viewToUse == null) { + return null; + } + + // The CUJ is logged by the click handler, so we don't log it inside the animation + // library. TODO: figure out return CUJ. + ActivityTransitionAnimator.Controller controllerDelegate = + ActivityTransitionAnimator.Controller.fromView(viewToUse, null /* cujType */); + + if (controllerDelegate == null) { + return null; + } + + // This wrapper allows us to override the default value, telling the controller that the + // current window is below the animating window as well as information about the return + // animation. + return new DelegateTransitionAnimatorController(controllerDelegate) { + @Override + public boolean isLaunching() { + return isLaunching; + } + + @Override + public boolean isBelowAnimatingWindow() { + return true; + } + + @Nullable + @Override + public ActivityTransitionAnimator.TransitionCookie getTransitionCookie() { + return cookie; + } + }; } /** @@ -1916,6 +2052,67 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return (T) current; } + /** + * Builds the filter used by WM Shell to match app closing transitions (only back, no home + * button/gesture) to the given launch cookie. + */ + static TransitionFilter buildBackToHomeFilter( + ActivityTransitionAnimator.TransitionCookie cookie, Launcher launcher) { + // Closing activity must include the cookie in its list of launch cookies. + TransitionFilter.Requirement appRequirement = new TransitionFilter.Requirement(); + appRequirement.mActivityType = ACTIVITY_TYPE_STANDARD; + appRequirement.mLaunchCookie = cookie; + appRequirement.mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; + // Opening activity must be Launcher. + TransitionFilter.Requirement launcherRequirement = new TransitionFilter.Requirement(); + launcherRequirement.mActivityType = ACTIVITY_TYPE_HOME; + launcherRequirement.mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + launcherRequirement.mTopActivity = launcher.getComponentName(); + // Transition types CLOSE and TO_BACK match the back button/gesture but not the home + // button/gesture. + TransitionFilter filter = new TransitionFilter(); + filter.mTypeSet = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; + filter.mRequirements = + new TransitionFilter.Requirement[]{appRequirement, launcherRequirement}; + return filter; + } + + /** + * Creates various conditions to ensure that the given transition is cleaned up correctly + * when necessary: + * - if the transition has run, it is the callback that unregisters it; + * - if the associated view is detached before the transition has had an opportunity to run, + * a {@link View.OnAttachStateChangeListener} allows us to do the same (and removes + * itself). + */ + static void setUpRemoteAnimationCleanup( + View v, RemoteTransition transition, RunnableList callback, Launcher launcher) { + View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View v) {} + + @Override + public void onViewDetachedFromWindow(@NonNull View v) { + SystemUiProxy.INSTANCE.get(launcher) + .unregisterRemoteTransition(transition); + v.removeOnAttachStateChangeListener(this); + } + }; + + // Remove the animation as soon as it has run once. + callback.add(() -> { + SystemUiProxy.INSTANCE.get(launcher).unregisterRemoteTransition(transition); + if (v != null) { + v.removeOnAttachStateChangeListener(listener); + } + }); + + // Remove the animation when the view is detached from the hierarchy. + // This is so that if back is not invoked (e.g. if we go back home through the home + // gesture) we don't have obsolete transitions staying registered. + v.addOnAttachStateChangeListener(listener); + } + @Override public void onAnimationStart(int transit, RemoteAnimationTarget[] appTargets, RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets, @@ -1928,6 +2125,11 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public void onAnimationCancelled() { mDelegate.onAnimationCancelled(); } + + @Override + public boolean supportsReturnTransition() { + return true; + } } /** diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java index 7e52ea1372..7cdca746da 100644 --- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java +++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java @@ -120,10 +120,6 @@ public class WidgetPickerActivity extends BaseActivity { WindowInsetsController wc = mDragLayer.getWindowInsetsController(); wc.hide(navigationBars() + statusBars()); - BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true); - widgetSheet.disableNavBarScrim(true); - widgetSheet.addOnCloseListener(this::finish); - parseIntentExtras(); refreshAndBindWidgets(); } @@ -224,9 +220,10 @@ public class WidgetPickerActivity extends BaseActivity { }; } - /** Updates the model with widgets and provides them after applying the provided filter. */ + /** Updates the model with widgets, applies filters and launches the widgets sheet once + * widgets are available */ private void refreshAndBindWidgets() { - MODEL_EXECUTOR.execute(() -> { + MODEL_EXECUTOR.getHandler().postDelayed(() -> { LauncherAppState app = LauncherAppState.getInstance(this); mModel.update(app, null); final List allWidgets = @@ -240,6 +237,9 @@ public class WidgetPickerActivity extends BaseActivity { } ); bindWidgets(allWidgets); + // Open sheet once widgets are available, so that it doesn't interrupt the open + // animation. + openWidgetsSheet(); if (mUiSurface != null) { Map allWidgetItems = allWidgets.stream() .filter(entry -> entry instanceof WidgetsListContentEntry) @@ -253,15 +253,26 @@ public class WidgetPickerActivity extends BaseActivity { mUiSurface, allWidgetItems); mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets); } - }); + }, mDeviceProfile.bottomSheetOpenDuration); } private void bindWidgets(List widgets) { MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets)); } + private void openWidgetsSheet() { + MAIN_EXECUTOR.execute(() -> { + BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true); + widgetSheet.disableNavBarScrim(true); + widgetSheet.addOnCloseListener(this::finish); + }); + } + private void bindRecommendedWidgets(List recommendedWidgets) { - MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets)); + // Bind recommendations once picker has finished open animation. + MAIN_EXECUTOR.getHandler().postDelayed( + () -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets), + mDeviceProfile.bottomSheetOpenDuration); } @Override diff --git a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java index 5730273d88..41fcf61d81 100644 --- a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java +++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java @@ -65,6 +65,7 @@ public class WidgetPredictionsRequester { private final Context mContext; @NonNull private final String mUiSurface; + private boolean mPredictionsAvailable; @NonNull private final Map mAllWidgets; @@ -76,8 +77,8 @@ public class WidgetPredictionsRequester { } /** - * Requests predictions from the app predictions manager and registers the provided callback to - * receive updates when predictions are available. + * Requests one time predictions from the app predictions manager and invokes provided callback + * once predictions are available. * * @param existingWidgets widgets that are currently added to the surface; * @param callback consumer of prediction results to be called when predictions are @@ -159,10 +160,14 @@ public class WidgetPredictionsRequester { @WorkerThread private void bindPredictions(List targets, Predicate filter, Consumer> callback) { - List filteredPredictions = filterPredictions(targets, mAllWidgets, filter); - List mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions); + if (!mPredictionsAvailable) { + mPredictionsAvailable = true; + List filteredPredictions = filterPredictions(targets, mAllWidgets, filter); + List mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions); - MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions)); + MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions)); + MODEL_EXECUTOR.execute(this::clear); + } } /** @@ -214,5 +219,6 @@ public class WidgetPredictionsRequester { mAppPredictor.destroy(); mAppPredictor = null; } + mPredictionsAvailable = false; } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index ce281c30e4..f24bc214f0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -269,8 +269,8 @@ public class TaskbarUIController { foundTaskView, foundTask, taskContainer.getIconView().getDrawable(), - taskContainer.getThumbnailViewDeprecated(), - taskContainer.getThumbnailViewDeprecated().getThumbnail(), + taskContainer.getSnapshotView(), + taskContainer.getThumbnail(), null /* intent */, null /* user */, info); diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 037f2f66f5..be6f69069d 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -1200,7 +1200,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer : Display.DEFAULT_DISPLAY); activityOptions.options.setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - addLaunchCookie(item, activityOptions.options); return activityOptions; } @@ -1224,19 +1223,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer mSplitWithKeyboardShortcutController.enterStageSplit(leftOrTop); } - /** - * Adds a new launch cookie for the activity launch if supported. - * - * @param info the item info for the launch - * @param opts the options to set the launchCookie on. - */ - public void addLaunchCookie(ItemInfo info, ActivityOptions opts) { - IBinder launchCookie = getLaunchCookie(info); - if (launchCookie != null) { - opts.setLaunchCookie(launchCookie); - } - } - /** * Return a new launch cookie for the activity launch if supported. * diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java index 4bc3c1661e..3c7f335325 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java @@ -252,7 +252,7 @@ public abstract class TaskViewTouchController tasks = new ArrayList<>(recentTaskInfo.getTaskInfoList().size()); + int[] minimizedTaskIds = recentTaskInfo.getMinimizedTaskIds(); + if (minimizedTaskIds.length == recentTaskInfo.getTaskInfoList().size()) { + // All Tasks are minimized -> don't create a DesktopTask + return null; + } for (ActivityManager.RecentTaskInfo taskInfo : recentTaskInfo.getTaskInfoList()) { Task.TaskKey key = new Task.TaskKey(taskInfo); Task task = Task.from(key, taskInfo, false); @@ -377,6 +385,8 @@ public class RecentTasksList { task.positionInParent = taskInfo.positionInParent; task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds(); task.isVisible = taskInfo.isVisible; + task.isMinimized = + Arrays.stream(minimizedTaskIds).anyMatch(taskId -> taskId == taskInfo.taskId); tasks.add(task); } return new DesktopTask(tasks); diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index b183ae392f..5d0d074c23 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -44,7 +44,6 @@ import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.OverviewActionsView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsViewContainer; -import com.android.quickstep.views.TaskThumbnailViewDeprecated; import com.android.quickstep.views.TaskView; import com.android.quickstep.views.TaskView.TaskContainer; import com.android.systemui.shared.recents.model.Task; @@ -134,20 +133,20 @@ public class TaskOverlayFactory implements ResourceBasedOverride { mApplicationContext = taskContainer.getTaskView().getContext().getApplicationContext(); mTaskContainer = taskContainer; mImageApi = new ImageActionsApi( - mApplicationContext, mTaskContainer.getThumbnailViewDeprecated()::getThumbnail); + mApplicationContext, mTaskContainer::getThumbnail); } protected T getActionsView() { if (mActionsView == null) { mActionsView = BaseActivity.fromContext( - mTaskContainer.getThumbnailViewDeprecated().getContext()).findViewById( + mTaskContainer.getTaskView().getContext()).findViewById( R.id.overview_actions_view); } return mActionsView; } - public TaskThumbnailViewDeprecated getThumbnailView() { - return mTaskContainer.getThumbnailViewDeprecated(); + public TaskView getTaskView() { + return mTaskContainer.getTaskView(); } /** @@ -159,8 +158,7 @@ public class TaskOverlayFactory implements ResourceBasedOverride { if (thumbnail != null) { getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated); - boolean isAllowedByPolicy = - mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot(); + boolean isAllowedByPolicy = mTaskContainer.isRealSnapshot(); getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task)); } } @@ -172,7 +170,7 @@ public class TaskOverlayFactory implements ResourceBasedOverride { */ public void endLiveTileMode(@NonNull Runnable callback) { RecentsView recentsView = - mTaskContainer.getThumbnailViewDeprecated().getTaskView().getRecentsView(); + mTaskContainer.getTaskView().getRecentsView(); // Task has already been dismissed if (recentsView == null) return; recentsView.switchToScreenshot( @@ -185,8 +183,8 @@ public class TaskOverlayFactory implements ResourceBasedOverride { */ @SuppressLint("NewApi") protected void saveScreenshot(Task task) { - if (mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot()) { - mImageApi.saveScreenshot(mTaskContainer.getThumbnailViewDeprecated().getThumbnail(), + if (mTaskContainer.isRealSnapshot()) { + mImageApi.saveScreenshot(mTaskContainer.getThumbnail(), getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key); } else { showBlockedByPolicyMessage(); @@ -194,17 +192,14 @@ public class TaskOverlayFactory implements ResourceBasedOverride { } protected void enterSplitSelect() { - RecentsView overviewPanel = - mTaskContainer.getThumbnailViewDeprecated().getTaskView().getRecentsView(); + RecentsView overviewPanel = mTaskContainer.getTaskView().getRecentsView(); // Task has already been dismissed if (overviewPanel == null) return; - overviewPanel.initiateSplitSelect( - mTaskContainer.getThumbnailViewDeprecated().getTaskView()); + overviewPanel.initiateSplitSelect(mTaskContainer.getTaskView()); } protected void saveAppPair() { - GroupedTaskView taskView = - (GroupedTaskView) mTaskContainer.getThumbnailViewDeprecated().getTaskView(); + GroupedTaskView taskView = (GroupedTaskView) mTaskContainer.getTaskView(); taskView.getRecentsView().getSplitSelectController().getAppPairsController() .saveAppPair(taskView); } @@ -250,11 +245,11 @@ public class TaskOverlayFactory implements ResourceBasedOverride { */ public Rect getTaskSnapshotBounds() { int[] location = new int[2]; - mTaskContainer.getThumbnailViewDeprecated().getLocationOnScreen(location); + mTaskContainer.getSnapshotView().getLocationOnScreen(location); return new Rect(location[0], location[1], - mTaskContainer.getThumbnailViewDeprecated().getWidth() + location[0], - mTaskContainer.getThumbnailViewDeprecated().getHeight() + location[1]); + mTaskContainer.getSnapshotView().getWidth() + location[0], + mTaskContainer.getSnapshotView().getHeight() + location[1]); } /** @@ -264,7 +259,7 @@ public class TaskOverlayFactory implements ResourceBasedOverride { */ @RequiresApi(api = Build.VERSION_CODES.Q) public Insets getTaskSnapshotInsets() { - return mTaskContainer.getThumbnailViewDeprecated().getScaledInsets(); + return mTaskContainer.getScaledInsets(); } /** @@ -275,14 +270,14 @@ public class TaskOverlayFactory implements ResourceBasedOverride { protected void showBlockedByPolicyMessage() { ActivityContext activityContext = ActivityContext.lookupContext( - mTaskContainer.getThumbnailViewDeprecated().getContext()); + mTaskContainer.getTaskView().getContext()); String message = activityContext.getStringCache() != null ? activityContext.getStringCache().disabledByAdminMessage - : mTaskContainer.getThumbnailViewDeprecated().getContext().getString( + : mTaskContainer.getTaskView().getContext().getString( R.string.blocked_by_policy); Snackbar.show(BaseActivity.fromContext( - mTaskContainer.getThumbnailViewDeprecated().getContext()), message, null); + mTaskContainer.getTaskView().getContext()), message, null); } /** Called when the snapshot has updated its full screen drawing parameters. */ @@ -304,8 +299,7 @@ public class TaskOverlayFactory implements ResourceBasedOverride { @Override public void onClick(View view) { - saveScreenshot( - mTaskContainer.getThumbnailViewDeprecated().getTaskView().getFirstTask()); + saveScreenshot(mTaskContainer.getTaskView().getFirstTask()); dismissTaskMenuView(); } } diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java index 4691ea9420..b4862fdf3c 100644 --- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java +++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.view.Surface.ROTATION_0; +import static com.android.launcher3.Flags.enableRefactorTaskThumbnail; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; import static com.android.window.flags.Flags.enableDesktopWindowingMode; @@ -55,7 +56,6 @@ import com.android.quickstep.util.RecentsOrientedState; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsViewContainer; -import com.android.quickstep.views.TaskThumbnailViewDeprecated; import com.android.quickstep.views.TaskView; import com.android.quickstep.views.TaskView.TaskContainer; import com.android.systemui.shared.recents.model.Task; @@ -174,7 +174,7 @@ public interface TaskShortcutFactory { private Handler mHandler; private final RecentsView mRecentsView; - private final TaskThumbnailViewDeprecated mThumbnailView; + private final TaskContainer mTaskContainer; private final TaskView mTaskView; private final LauncherEvent mLauncherEvent; @@ -186,7 +186,7 @@ public interface TaskShortcutFactory { mHandler = new Handler(Looper.getMainLooper()); mTaskView = taskContainer.getTaskView(); mRecentsView = container.getOverviewPanel(); - mThumbnailView = taskContainer.getThumbnailViewDeprecated(); + mTaskContainer = taskContainer; } @Override @@ -220,20 +220,25 @@ public interface TaskShortcutFactory { }; final int[] position = new int[2]; - mThumbnailView.getLocationOnScreen(position); - final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX()); - final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY()); + View snapShotView = mTaskContainer.getSnapshotView(); + snapShotView.getLocationOnScreen(position); + final int width = (int) (snapShotView.getWidth() * mTaskView.getScaleX()); + final int height = (int) (snapShotView.getHeight() * mTaskView.getScaleY()); final Rect taskBounds = new Rect(position[0], position[1], position[0] + width, position[1] + height); // Take the thumbnail of the task without a scrim and apply it back after - float alpha = mThumbnailView.getDimAlpha(); // TODO(b/348643341) add ability to get override the scrim for this Bitmap retrieval - mThumbnailView.setDimAlpha(0); + float alpha = 0f; + if (!enableRefactorTaskThumbnail()) { + alpha = mTaskContainer.getThumbnailViewDeprecated().getDimAlpha(); + mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(0); + } Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap( - taskBounds.width(), taskBounds.height(), mThumbnailView, 1f, - Color.BLACK); - mThumbnailView.setDimAlpha(alpha); + taskBounds.width(), taskBounds.height(), snapShotView, 1f, Color.BLACK); + if (!enableRefactorTaskThumbnail()) { + mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(alpha); + } AppTransitionAnimationSpecsFuture future = new AppTransitionAnimationSpecsFuture(mHandler) { diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java index ecd84f865f..bd44283d04 100644 --- a/quickstep/src/com/android/quickstep/TaskViewUtils.java +++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java @@ -80,7 +80,6 @@ import com.android.quickstep.util.TransformParams; import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.RecentsView; -import com.android.quickstep.views.TaskThumbnailViewDeprecated; import com.android.quickstep.views.TaskView; import com.android.systemui.animation.RemoteAnimationTargetCompat; import com.android.systemui.shared.recents.model.Task; @@ -334,7 +333,7 @@ public final class TaskViewUtils { // During animation we apply transformation on the thumbnailView (and not the rootView) // to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is: // Mt K(0)` K(t) Mt` - TaskThumbnailViewDeprecated[] thumbnails = v.getThumbnailViews(); + View[] thumbnails = v.getSnapshotViews(); // In case simulator copies and thumbnail size do no match, ensure we get the lesser. // This ensures we do not create arrays with empty elements or attempt to references @@ -344,7 +343,7 @@ public final class TaskViewUtils { Matrix[] mt = new Matrix[matrixSize]; Matrix[] mti = new Matrix[matrixSize]; for (int i = 0; i < matrixSize; i++) { - TaskThumbnailViewDeprecated ttv = thumbnails[i]; + View ttv = thumbnails[i]; RectF localBounds = new RectF(0, 0, ttv.getWidth(), ttv.getHeight()); float[] tvBoundsMapped = new float[]{0, 0, ttv.getWidth(), ttv.getHeight()}; getDescendantCoordRelativeToAncestor(ttv, ttv.getRootView(), tvBoundsMapped, false); @@ -391,7 +390,7 @@ public final class TaskViewUtils { out.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - for (TaskThumbnailViewDeprecated ttv : thumbnails) { + for (View ttv : thumbnails) { ttv.setAnimationMatrix(null); } } diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 7ea04b12a7..876a4eadd8 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -43,6 +43,7 @@ import androidx.annotation.VisibleForTesting import com.android.app.animation.Interpolators import com.android.launcher3.DeviceProfile import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.InsettableFrameLayout import com.android.launcher3.QuickstepTransitionManager import com.android.launcher3.R @@ -118,8 +119,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) { val drawable = getDrawable(container.iconView, splitSelectSource) return SplitAnimInitProps( - container.thumbnailViewDeprecated, - container.thumbnailViewDeprecated.thumbnail, + container.snapshotView, + container.thumbnail, drawable!!, fadeWithThumbnail = true, isStagedTask = true, @@ -137,8 +138,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC taskView.taskContainers.first().let { val drawable = getDrawable(it.iconView, splitSelectSource) return SplitAnimInitProps( - it.thumbnailViewDeprecated, - it.thumbnailViewDeprecated.thumbnail, + it.snapshotView, + it.thumbnail, drawable!!, fadeWithThumbnail = true, isStagedTask = true, @@ -165,27 +166,37 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC /** * When selecting first app from split pair, second app's thumbnail remains. This animates the * second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying it - * with [TaskThumbnailViewDeprecated]'s splashView. Adds animations to the provided builder. - * Note: The app that **was not** selected as the first split app should be the container that's - * passed through. + * with [TaskContainer]'s splashView. Adds animations to the provided builder. Note: The app + * that **was not** selected as the first split app should be the container that's passed + * through. * * @param builder Adds animation to this - * @param taskIdAttributeContainer container of the app that **was not** selected + * @param taskContainer container of the app that **was not** selected * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair - * (opposite of that representing [taskIdAttributeContainer]) + * (opposite of that representing [taskContainer]) */ fun addInitialSplitFromPair( - taskIdAttributeContainer: TaskContainer, + taskContainer: TaskContainer, builder: PendingAnimation, deviceProfile: DeviceProfile, taskViewWidth: Int, taskViewHeight: Int, isPrimaryTaskSplitting: Boolean ) { - val thumbnail = taskIdAttributeContainer.thumbnailViewDeprecated - val iconView: View = taskIdAttributeContainer.iconView.asView() - builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailViewDeprecated.SPLASH_ALPHA, 1f)) - thumbnail.setShowSplashForSplitSelection(true) + val snapshot = taskContainer.snapshotView + val iconView: View = taskContainer.iconView.asView() + // TODO(334826842): Switch to splash state in TaskThumbnailView + if (!enableRefactorTaskThumbnail()) { + val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated + builder.add( + ObjectAnimator.ofFloat( + thumbnailViewDeprecated, + TaskThumbnailViewDeprecated.SPLASH_ALPHA, + 1f + ) + ) + thumbnailViewDeprecated.setShowSplashForSplitSelection(true) + } // With the new `IconAppChipView`, we always want to keep the chip pinned to the // top left of the task / thumbnail. if (enableOverviewIconMenu()) { @@ -202,14 +213,10 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC } if (deviceProfile.isLeftRightSplit) { // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0 - val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f - val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width + val centerThumbnailTranslationX: Float = (taskViewWidth - snapshot.width) / 2f + val finalScaleX: Float = taskViewWidth.toFloat() / snapshot.width builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X, - centerThumbnailTranslationX - ) + ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_X, centerThumbnailTranslationX) ) if (!enableOverviewIconMenu()) { // icons are anchored from Gravity.END, so need to use negative translation @@ -218,21 +225,15 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, -centerIconTranslationX) ) } - builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX)) + builder.add(ObjectAnimator.ofFloat(snapshot, View.SCALE_X, finalScaleX)) // Reset other dimensions // TODO(b/271468547), can't set Y translate to 0, need to account for top space - thumbnail.scaleY = 1f + snapshot.scaleY = 1f val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() - builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y, - translateYResetVal - ) - ) + builder.add(ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_Y, translateYResetVal)) } else { val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0 @@ -247,36 +248,26 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // thumbnail needs to take that into account. We should migrate to only using // translations otherwise this asymmetry causes problems.. if (isPrimaryTaskSplitting) { - centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f + centerThumbnailTranslationY = (thumbnailSize - snapshot.height) / 2f centerThumbnailTranslationY += deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() } else { - centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f + centerThumbnailTranslationY = (thumbnailSize - snapshot.height) / 2f } - val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height + val finalScaleY: Float = thumbnailSize.toFloat() / snapshot.height builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y, - centerThumbnailTranslationY - ) + ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_Y, centerThumbnailTranslationY) ) if (!enableOverviewIconMenu()) { // icons are anchored from Gravity.END, so need to use negative translation builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 0f)) } - builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY)) + builder.add(ObjectAnimator.ofFloat(snapshot, View.SCALE_Y, finalScaleY)) // Reset other dimensions - thumbnail.scaleX = 1f - builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X, - 0f - ) - ) + snapshot.scaleX = 1f + builder.add(ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_X, 0f)) } } diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt index 38bbe601b6..4820c3564f 100644 --- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt +++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt @@ -31,29 +31,13 @@ class SplitScreenUtils { null } else { SplitConfigurationOptions.SplitBounds( - shellSplitBounds.leftTopBounds, shellSplitBounds.rightBottomBounds, - shellSplitBounds.leftTopTaskId, shellSplitBounds.rightBottomTaskId, + shellSplitBounds.leftTopBounds, + shellSplitBounds.rightBottomBounds, + shellSplitBounds.leftTopTaskId, + shellSplitBounds.rightBottomTaskId, shellSplitBounds.snapPosition ) } } - - /** Converts the launcher version of SplitBounds to the shell version */ - @JvmStatic - fun convertLauncherSplitBoundsToShell( - launcherSplitBounds: SplitConfigurationOptions.SplitBounds? - ): SplitBounds? { - return if (launcherSplitBounds == null) { - null - } else { - SplitBounds( - launcherSplitBounds.leftTopBounds, - launcherSplitBounds.rightBottomBounds, - launcherSplitBounds.leftTopTaskId, - launcherSplitBounds.rightBottomTaskId, - launcherSplitBounds.snapPosition - ) - } - } } } diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index 49f4e5f701..d9b7d20eac 100644 --- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java @@ -27,7 +27,6 @@ import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation; import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation; -import static com.android.quickstep.util.SplitScreenUtils.convertLauncherSplitBoundsToShell; import android.animation.TimeInterpolator; import android.content.Context; @@ -247,8 +246,6 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { } else { mStagePosition = runningTarget.taskId == splitInfo.leftTopTaskId ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT; - mPositionHelper.setSplitBounds(convertLauncherSplitBoundsToShell(mSplitBounds), - mStagePosition); } calculateTaskSize(); } diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt index 4c78e21b0a..46ed2eef71 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt @@ -128,7 +128,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu } val thumbWidth = (taskSize.width() * scaleWidth).toInt() val thumbHeight = (taskSize.height() * scaleHeight).toInt() - it.thumbnailViewDeprecated.measure( + it.snapshotView.measure( MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY) ) @@ -139,8 +139,8 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu var taskY = (positionInParent.y * scaleHeight).toInt() // move task down by margin size taskY += thumbnailTopMarginPx - it.thumbnailViewDeprecated.x = taskX.toFloat() - it.thumbnailViewDeprecated.y = taskY.toFloat() + it.snapshotView.x = taskX.toFloat() + it.snapshotView.y = taskY.toFloat() if (DEBUG) { Log.d( TAG, diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java index 4a5b9e4cc1..9f268a0420 100644 --- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java +++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java @@ -332,12 +332,12 @@ public final class DigitalWellBeingToast { (FrameLayout.LayoutParams) mBanner.getLayoutParams(); DeviceProfile deviceProfile = mContainer.getDeviceProfile(); layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams) - mTaskView.getFirstThumbnailViewDeprecated().getLayoutParams()).bottomMargin; + mTaskView.getFirstSnapshotView().getLayoutParams()).bottomMargin; RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler(); Pair translations = orientationHandler .getDwbLayoutTranslations(mTaskView.getMeasuredWidth(), mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile, - mTaskView.getThumbnailViews(), mTask.key.id, mBanner); + mTaskView.getSnapshotViews(), mTask.key.id, mBanner); mSplitOffsetTranslationX = translations.first; mSplitOffsetTranslationY = translations.second; updateTranslationY(); diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt index 6296b0e3ea..b070244304 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt @@ -33,10 +33,8 @@ import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_O import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED import com.android.quickstep.TaskOverlayFactory import com.android.quickstep.util.RecentsOrientedState -import com.android.quickstep.util.SplitScreenUtils.Companion.convertLauncherSplitBoundsToShell import com.android.quickstep.util.SplitSelectStateController import com.android.systemui.shared.recents.model.Task -import com.android.systemui.shared.recents.utilities.PreviewPositionHelper import com.android.systemui.shared.system.InteractionJankMonitorWrapper import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition @@ -70,36 +68,20 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val initSplitTaskId = getThisTaskCurrentlyInSplitSelection() if (initSplitTaskId == INVALID_TASK_ID) { pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds( - taskContainers[0].thumbnailViewDeprecated, - taskContainers[1].thumbnailViewDeprecated, + taskContainers[0].snapshotView, + taskContainers[1].snapshotView, widthSize, heightSize, splitBoundsConfig, container.deviceProfile, layoutDirection == LAYOUT_DIRECTION_RTL ) - // Should we be having a separate translation step apart from the measuring above? - // The following only applies to large screen for now, but for future reference - // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary - // translation directions - taskContainers[0] - .thumbnailViewDeprecated - .applySplitSelectTranslateX(taskContainers[0].thumbnailViewDeprecated.translationX) - taskContainers[0] - .thumbnailViewDeprecated - .applySplitSelectTranslateY(taskContainers[0].thumbnailViewDeprecated.translationY) - taskContainers[1] - .thumbnailViewDeprecated - .applySplitSelectTranslateX(taskContainers[1].thumbnailViewDeprecated.translationX) - taskContainers[1] - .thumbnailViewDeprecated - .applySplitSelectTranslateY(taskContainers[1].thumbnailViewDeprecated.translationY) } else { // Currently being split with this taskView, let the non-split selected thumbnail // take up full thumbnail area taskContainers .firstOrNull { it.task.key.id != initSplitTaskId } - ?.thumbnailViewDeprecated + ?.snapshotView ?.measure( widthMeasureSpec, MeasureSpec.makeMeasureSpec( @@ -147,23 +129,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu ) taskContainers.forEach { it.bind() } - this.splitBoundsConfig = - splitBoundsConfig?.also { - taskContainers[0] - .thumbnailViewDeprecated - .previewPositionHelper - .setSplitBounds( - convertLauncherSplitBoundsToShell(it), - PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT - ) - taskContainers[1] - .thumbnailViewDeprecated - .previewPositionHelper - .setSplitBounds( - convertLauncherSplitBoundsToShell(it), - PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT - ) - } + this.splitBoundsConfig = splitBoundsConfig taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) } setOrientationState(orientedState) } @@ -230,8 +196,8 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu taskContainers[0].iconView.asView(), taskContainers[1].iconView.asView(), taskIconHeight, - taskContainers[0].thumbnailViewDeprecated.measuredWidth, - taskContainers[0].thumbnailViewDeprecated.measuredHeight, + taskContainers[0].snapshotView.measuredWidth, + taskContainers[0].snapshotView.measuredHeight, measuredHeight, measuredWidth, isRtl, @@ -326,7 +292,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu // Check which of the two apps was selected if ( taskContainers[1].iconView.asView().containsPoint(lastTouchDownPosition) || - taskContainers[1].thumbnailViewDeprecated.containsPoint(lastTouchDownPosition) + taskContainers[1].snapshotView.containsPoint(lastTouchDownPosition) ) { return 1 } diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index d806e3ddc6..c1e112a4c9 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -1826,8 +1826,13 @@ public abstract class RecentsView nonMinimizedTasks = + ((DesktopTask) groupTask).tasks.stream() + .filter(task -> !task.isMinimized) + .toList(); + ((DesktopTaskView) taskView).bind(nonMinimizedTasks, mOrientationState, + mTaskOverlayFactory); mDesktopTaskView = (DesktopTaskView) taskView; } else { Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2 diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java index 4f446b203c..8d5ba7766f 100644 --- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java +++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java @@ -238,12 +238,12 @@ public class TaskMenuView extends AbstractFloatingView { mContainer.getDragLayer().getDescendantRectRelativeToSelf( enableOverviewIconMenu() ? getIconView().findViewById(R.id.icon_view_menu_anchor) - : taskContainer.getThumbnailViewDeprecated(), + : taskContainer.getSnapshotView(), sTempRect); Rect insets = mContainer.getDragLayer().getInsets(); BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams(); params.width = orientationHandler.getTaskMenuWidth( - taskContainer.getThumbnailViewDeprecated(), deviceProfile, + taskContainer.getSnapshotView(), deviceProfile, taskContainer.getStagePosition()); // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start params.gravity = Gravity.LEFT; @@ -277,10 +277,10 @@ public class TaskMenuView extends AbstractFloatingView { // Margin that insets the menuView inside the taskView float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin); setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX, - mTaskContainer.getThumbnailViewDeprecated(), deviceProfile, taskInsetMargin, + mTaskContainer.getSnapshotView(), deviceProfile, taskInsetMargin, getIconView())); setTranslationY(orientationHandler.getTaskMenuY( - thumbnailAlignedY, mTaskContainer.getThumbnailViewDeprecated(), + thumbnailAlignedY, mTaskContainer.getSnapshotView(), mTaskContainer.getStagePosition(), this, taskInsetMargin, getIconView())); } @@ -316,7 +316,7 @@ public class TaskMenuView extends AbstractFloatingView { .createRevealAnimator(this, closing, revealAnimationStartProgress); mRevealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED : Interpolators.DECELERATE); - + AnimatorSet.Builder openCloseAnimatorBuilder = mOpenCloseAnimator.play(mRevealAnimator); if (enableOverviewIconMenu()) { IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView(); @@ -334,11 +334,13 @@ public class TaskMenuView extends AbstractFloatingView { closing ? mMenuTranslationYBeforeOpen : mMenuTranslationYBeforeOpen + additionalTranslationY); translationYAnim.setInterpolator(EMPHASIZED); + openCloseAnimatorBuilder.with(translationYAnim); ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat( iconAppChip.getMenuTranslationY(), MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY); menuTranslationYAnim.setInterpolator(EMPHASIZED); + openCloseAnimatorBuilder.with(menuTranslationYAnim); float additionalTranslationX = 0; if (mContainer.getDeviceProfile().isLandscape @@ -354,20 +356,15 @@ public class TaskMenuView extends AbstractFloatingView { closing ? mMenuTranslationXBeforeOpen : mMenuTranslationXBeforeOpen - additionalTranslationX); translationXAnim.setInterpolator(EMPHASIZED); + openCloseAnimatorBuilder.with(translationXAnim); ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat( iconAppChip.getMenuTranslationX(), MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX); menuTranslationXAnim.setInterpolator(EMPHASIZED); - - mOpenCloseAnimator.playTogether(translationYAnim, translationXAnim, - menuTranslationXAnim, menuTranslationYAnim); + openCloseAnimatorBuilder.with(menuTranslationXAnim); } - mOpenCloseAnimator.playTogether(mRevealAnimator, - ObjectAnimator.ofFloat( - mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA, - closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA), - ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); + openCloseAnimatorBuilder.with(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); if (enableRefactorTaskThumbnail()) { mRevealAnimator.addUpdateListener(animation -> { float animatedFraction = animation.getAnimatedFraction(); @@ -375,6 +372,10 @@ public class TaskMenuView extends AbstractFloatingView { mTaskContainer.getTaskContainerData() .getTaskMenuOpenProgress().setValue(openProgress); }); + } else { + openCloseAnimatorBuilder.with(ObjectAnimator.ofFloat( + mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA, + closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA)); } mOpenCloseAnimator.addListener(new AnimationSuccessListener() { @Override diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java index 4283d0e4cf..2afb6a6d52 100644 --- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java @@ -99,36 +99,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab } }; - /** Use to animate thumbnail translationX while first app in split selection is initiated */ - public static final Property SPLIT_SELECT_TRANSLATE_X = - new FloatProperty("splitSelectTranslateX") { - @Override - public void setValue(TaskThumbnailViewDeprecated thumbnail, - float splitSelectTranslateX) { - thumbnail.applySplitSelectTranslateX(splitSelectTranslateX); - } - - @Override - public Float get(TaskThumbnailViewDeprecated thumbnailView) { - return thumbnailView.mSplitSelectTranslateX; - } - }; - - /** Use to animate thumbnail translationY while first app in split selection is initiated */ - public static final Property SPLIT_SELECT_TRANSLATE_Y = - new FloatProperty("splitSelectTranslateY") { - @Override - public void setValue(TaskThumbnailViewDeprecated thumbnail, - float splitSelectTranslateY) { - thumbnail.applySplitSelectTranslateY(splitSelectTranslateY); - } - - @Override - public Float get(TaskThumbnailViewDeprecated thumbnailView) { - return thumbnailView.mSplitSelectTranslateY; - } - }; - private final RecentsViewContainer mContainer; private TaskOverlay mOverlay; private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -160,8 +130,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab private boolean mOverlayEnabled; /** Used as a placeholder when the original thumbnail animates out to. */ private boolean mShowSplashForSplitSelection; - private float mSplitSelectTranslateX; - private float mSplitSelectTranslateY; public TaskThumbnailViewDeprecated(Context context) { this(context, null); @@ -415,31 +383,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab } } - /** See {@link #SPLIT_SELECT_TRANSLATE_X} */ - protected void applySplitSelectTranslateX(float splitSelectTranslateX) { - mSplitSelectTranslateX = splitSelectTranslateX; - applyTranslateX(); - } - - /** See {@link #SPLIT_SELECT_TRANSLATE_Y} */ - protected void applySplitSelectTranslateY(float splitSelectTranslateY) { - mSplitSelectTranslateY = splitSelectTranslateY; - applyTranslateY(); - } - - private void applyTranslateX() { - setTranslationX(mSplitSelectTranslateX); - } - - private void applyTranslateY() { - setTranslationY(mSplitSelectTranslateY); - } - - protected void resetViewTransforms() { - mSplitSelectTranslateX = 0; - mSplitSelectTranslateY = 0; - } - public TaskView getTaskView() { return (TaskView) getParent(); } diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt index 7a3b00f197..d4b0040733 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.kt +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -23,7 +23,9 @@ import android.annotation.IdRes import android.app.ActivityOptions import android.content.Context import android.content.Intent +import android.graphics.Bitmap import android.graphics.Canvas +import android.graphics.Insets import android.graphics.PointF import android.graphics.Rect import android.graphics.drawable.Drawable @@ -138,8 +140,8 @@ constructor( /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */ get() = taskContainers.map { it.task.key.id }.toIntArray() - val thumbnailViews: Array - get() = taskContainers.map { it.thumbnailViewDeprecated }.toTypedArray() + val snapshotViews: Array + get() = taskContainers.map { it.snapshotView }.toTypedArray() val isGridTask: Boolean /** Returns whether the task is part of overview grid and not being focused. */ @@ -170,6 +172,11 @@ constructor( /** Returns the first thumbnailView of the TaskView. */ get() = taskContainers[0].thumbnailViewDeprecated + @get:Deprecated("Use [taskContainers] instead.") + val firstSnapshotView: View + /** Returns the first snapshotView of the TaskView. */ + get() = taskContainers[0].snapshotView + @get:Deprecated("Use [taskContainers] instead.") val firstItemInfo: ItemInfo get() = taskContainers[0].itemInfo @@ -1197,10 +1204,10 @@ constructor( this, container.task, container.iconView.drawable, - container.thumbnailViewDeprecated, - container.thumbnailViewDeprecated.thumbnail, /* intent */ - null, /* user */ - null, + container.snapshotView, + container.thumbnail, + /* intent */ null, + /* user */ null, container.itemInfo ) } @@ -1512,6 +1519,10 @@ constructor( gridTranslationY = 0f boxTranslationY = 0f nonGridPivotTranslationX = 0f + taskContainers.forEach { + it.snapshotView.translationX = 0f + it.snapshotView.translationY = 0f + } resetViewTransforms() } @@ -1537,10 +1548,6 @@ constructor( alpha = stableAlpha setIconScaleAndDim(1f) setColorTint(0f, 0) - if (!enableRefactorTaskThumbnail()) { - // TODO(b/335399428) add split select functionality to new TTV - taskContainers.forEach { it.thumbnailViewDeprecated.resetViewTransforms() } - } } private fun getGridTrans(endTranslation: Float) = @@ -1624,6 +1631,18 @@ constructor( val snapshotView: View get() = thumbnailView ?: thumbnailViewDeprecated + // TODO(b/349120849): Extract ThumbnailData from TaskContainerData/TaskThumbnailViewModel + val thumbnail: Bitmap? + get() = thumbnailViewDeprecated.thumbnail + + // TODO(b/349120849): Extract ThumbnailData from TaskContainerData/TaskThumbnailViewModel + val isRealSnapshot: Boolean + get() = thumbnailViewDeprecated.isRealSnapshot() + + // TODO(b/349120849): Extract ThumbnailData from TaskContainerData/TaskThumbnailViewModel + val scaledInsets: Insets + get() = thumbnailViewDeprecated.scaledInsets + /** Builds proto for logging */ val itemInfo: WorkspaceItemInfo get() = diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt index 1c900b8b77..bbf738e03b 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt @@ -31,14 +31,15 @@ import com.android.quickstep.AllAppsActionManager import com.android.quickstep.TouchInteractionService import com.android.quickstep.TouchInteractionService.TISBinder import org.junit.Assume.assumeTrue -import org.junit.rules.MethodRule -import org.junit.runners.model.FrameworkMethod +import org.junit.rules.TestRule +import org.junit.runner.Description import org.junit.runners.model.Statement /** * Manages the Taskbar lifecycle for unit tests. * - * Tests need to provide their target [context] through the constructor. + * Tests should pass in themselves as [testInstance]. They also need to provide their target + * [context] through the constructor. * * See [InjectController] for grabbing controller(s) under test with minimal boilerplate. * @@ -61,12 +62,11 @@ import org.junit.runners.model.Statement * } * ``` */ -class TaskbarUnitTestRule(private val context: Context) : MethodRule { +class TaskbarUnitTestRule(private val testInstance: Any, private val context: Context) : TestRule { private val instrumentation = InstrumentationRegistry.getInstrumentation() private val serviceTestRule = ServiceTestRule() private lateinit var taskbarManager: TaskbarManager - private lateinit var target: Any val activityContext: TaskbarActivityContext get() { @@ -74,10 +74,9 @@ class TaskbarUnitTestRule(private val context: Context) : MethodRule { ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.") } - override fun apply(base: Statement, method: FrameworkMethod, target: Any): Statement { + override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { - this@TaskbarUnitTestRule.target = target instrumentation.runOnMainSync { assumeTrue( @@ -141,11 +140,11 @@ class TaskbarUnitTestRule(private val context: Context) : MethodRule { private fun injectControllers() { val controllers = activityContext.controllers val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type } - target.javaClass.fields + testInstance.javaClass.fields .filter { it.isAnnotationPresent(InjectController::class.java) } .forEach { it.set( - target, + testInstance, controllerFieldsByType[it.type]?.get(controllers) ?: throw NoSuchElementException("Failed to find controller for ${it.type}"), ) diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt index 9a514bf784..bfad6979cf 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt @@ -42,7 +42,8 @@ import org.junit.runner.RunWith @EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) class TaskbarAllAppsControllerTest { - @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule(getInstrumentation().targetContext) + @get:Rule + val taskbarUnitTestRule = TaskbarUnitTestRule(this, getInstrumentation().targetContext) @get:Rule val animatorTestRule = AnimatorTestRule(this) @InjectController lateinit var allAppsController: TaskbarAllAppsController diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt index 918ec7db7e..72bdc166e2 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt @@ -41,7 +41,8 @@ import org.junit.runner.RunWith @EmulatedDevices(["pixelFoldable2023"]) class TaskbarOverlayControllerTest { - @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule(getInstrumentation().targetContext) + @get:Rule + val taskbarUnitTestRule = TaskbarUnitTestRule(this, getInstrumentation().targetContext) @InjectController lateinit var overlayController: TaskbarOverlayController private val taskbarContext: TaskbarActivityContext diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt index d40f8ab389..c8893ad07e 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt @@ -33,7 +33,6 @@ import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.util.SplitConfigurationOptions import com.android.quickstep.views.GroupedTaskView import com.android.quickstep.views.IconView -import com.android.quickstep.views.TaskThumbnailViewDeprecated import com.android.quickstep.views.TaskView import com.android.quickstep.views.TaskView.TaskContainer import com.android.systemui.shared.recents.model.Task @@ -59,7 +58,7 @@ class SplitAnimationControllerTest { private val mockSplitSelectStateController: SplitSelectStateController = mock() // TaskView private val mockTaskView: TaskView = mock() - private val mockThumbnailView: TaskThumbnailViewDeprecated = mock() + private val mockSnapshotView: View = mock() private val mockBitmap: Bitmap = mock() private val mockIconView: IconView = mock() private val mockTaskViewDrawable: Drawable = mock() @@ -87,8 +86,8 @@ class SplitAnimationControllerTest { @Before fun setup() { - whenever(mockTaskContainer.thumbnailViewDeprecated).thenReturn(mockThumbnailView) - whenever(mockThumbnailView.thumbnail).thenReturn(mockBitmap) + whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView) + whenever(mockTaskContainer.thumbnail).thenReturn(mockBitmap) whenever(mockTaskContainer.iconView).thenReturn(mockIconView) whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable) whenever(mockTaskView.taskContainers).thenReturn(List(1) { mockTaskContainer }) @@ -180,7 +179,6 @@ class SplitAnimationControllerTest { whenever(mockTaskContainer.task).thenReturn(mockTask) whenever(mockTaskContainer.iconView).thenReturn(mockIconView) - whenever(mockTaskContainer.thumbnailViewDeprecated).thenReturn(mockThumbnailView) whenever(mockTask.getKey()).thenReturn(mockTaskKey) whenever(mockTaskKey.getId()).thenReturn(taskId) whenever(mockSplitSelectStateController.initialTaskId).thenReturn(taskId) diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java index 03244eb0bf..ce16b70a1b 100644 --- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java +++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java @@ -32,6 +32,8 @@ import androidx.test.filters.SmallTest; import com.android.launcher3.util.LooperExecutor; import com.android.quickstep.util.GroupTask; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.model.Task; import com.android.wm.shell.util.GroupedRecentTaskInfo; import org.junit.Before; @@ -40,8 +42,11 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @SmallTest public class RecentTasksListTest { @@ -104,4 +109,52 @@ public class RecentTasksListTest { assertEquals(taskDescription, taskList.get(0).task1.taskDescription.getLabel()); assertNull(taskList.get(0).task2.taskDescription.getLabel()); } + + @Test + public void loadTasksInBackground_freeformTask_createsDesktopTask() { + ActivityManager.RecentTaskInfo[] tasks = { + createRecentTaskInfo(1 /* taskId */), + createRecentTaskInfo(4 /* taskId */), + createRecentTaskInfo(5 /* taskId */)}; + GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forFreeformTasks( + tasks, Collections.emptySet() /* minimizedTaskIds */); + when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground( + Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */); + + assertEquals(1, taskList.size()); + assertEquals(TaskView.Type.DESKTOP, taskList.get(0).taskViewType); + List actualFreeformTasks = taskList.get(0).getTasks(); + assertEquals(3, actualFreeformTasks.size()); + assertEquals(1, actualFreeformTasks.get(0).key.id); + assertEquals(4, actualFreeformTasks.get(1).key.id); + assertEquals(5, actualFreeformTasks.get(2).key.id); + } + + @Test + public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask() { + ActivityManager.RecentTaskInfo[] tasks = { + createRecentTaskInfo(1 /* taskId */), + createRecentTaskInfo(4 /* taskId */), + createRecentTaskInfo(5 /* taskId */)}; + Set minimizedTaskIds = + Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet()); + GroupedRecentTaskInfo recentTaskInfos = + GroupedRecentTaskInfo.forFreeformTasks(tasks, minimizedTaskIds); + when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground( + Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */); + + assertEquals(0, taskList.size()); + } + + private ActivityManager.RecentTaskInfo createRecentTaskInfo(int taskId) { + ActivityManager.RecentTaskInfo recentTaskInfo = new ActivityManager.RecentTaskInfo(); + recentTaskInfo.taskId = taskId; + return recentTaskInfo; + } } diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java index 13680846f9..c59e295c5f 100644 --- a/src/com/android/launcher3/widget/BaseWidgetSheet.java +++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java @@ -331,8 +331,21 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView * status bar, into account. */ protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthUsed = getInsetsWidth(); + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + measureChildWithMargins(mContent, widthMeasureSpec, + widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding); + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), + MeasureSpec.getSize(heightMeasureSpec)); + } + + /** + * Returns the width used on left and right by the insets / padding. + */ + protected int getInsetsWidth() { int widthUsed; + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); if (deviceProfile.isTablet) { widthUsed = Math.max(2 * getTabletHorizontalMargin(deviceProfile), 2 * (mInsets.left + mInsets.right)); @@ -343,11 +356,7 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView widthUsed = Math.max(padding.left + padding.right, 2 * (mInsets.left + mInsets.right)); } - - measureChildWithMargins(mContent, widthMeasureSpec, - widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding); - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), - MeasureSpec.getSize(heightMeasureSpec)); + return widthUsed; } /** Returns the horizontal margins to be applied to the widget sheet. **/ diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 9929892e28..fd15677ab7 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -55,7 +55,6 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; -import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; @@ -416,19 +415,18 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int availableWidth = MeasureSpec.getSize(widthMeasureSpec); + updateMaxSpansPerRow(availableWidth); doMeasure(widthMeasureSpec, heightMeasureSpec); - - if (updateMaxSpansPerRow()) { - doMeasure(widthMeasureSpec, heightMeasureSpec); - } } - /** Returns {@code true} if the max spans have been updated. */ - private boolean updateMaxSpansPerRow() { - if (getMeasuredWidth() == 0) return false; - - @Px int maxHorizontalSpan = getContentView().getMeasuredWidth() - - (2 * mContentHorizontalMargin); + /** Returns {@code true} if the max spans have been updated. + * + * @param availableWidth Total width available within parent (includes insets). + */ + private void updateMaxSpansPerRow(int availableWidth) { + @Px int maxHorizontalSpan = getAvailableWidthForSuggestions( + availableWidth - getInsetsWidth()); if (mMaxSpanPerRow != maxHorizontalSpan) { mMaxSpanPerRow = maxHorizontalSpan; mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( @@ -439,16 +437,15 @@ public class WidgetsFullSheet extends BaseWidgetSheet mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow( maxHorizontalSpan); } - onRecommendedWidgetsBound(); - return true; + post(this::onRecommendedWidgetsBound); } - return false; } - protected View getContentView() { - return mHasWorkProfile - ? mViewPager - : mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; + /** + * Returns the width available to display suggestions. + */ + protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) { + return pickerAvailableWidth - (2 * mContentHorizontalMargin); } @Override @@ -493,7 +490,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet .mWidgetsListAdapter.hasVisibleEntries()); if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) { mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded; - onRecommendedWidgetsBound(); + post(this::onRecommendedWidgetsBound); } } @@ -549,7 +546,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE); // Visibility of recommended widgets, recycler views and headers are handled in methods // below. - onRecommendedWidgetsBound(); + post(this::onRecommendedWidgetsBound); onWidgetsBound(); } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java b/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java index 854700fed3..6a1921eb23 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java @@ -16,6 +16,8 @@ package com.android.launcher3.widget.picker; +import static android.animation.ValueAnimator.areAnimatorsEnabled; + import static com.android.launcher3.widget.picker.WidgetsListAdapter.VIEW_TYPE_WIDGETS_LIST; import androidx.recyclerview.widget.DefaultItemAnimator; @@ -26,6 +28,14 @@ public class WidgetsListItemAnimator extends DefaultItemAnimator { public static final int MOVE_DURATION_MS = 90; public static final int ADD_DURATION_MS = 120; + // DefaultItemAnimator runs change and move animations before running add animations (i.e. + // before expanded list item's content start animating to become visible on screen). + public static final int WIDGET_LIST_ITEM_APPEARANCE_START_DELAY = + areAnimatorsEnabled() ? (CHANGE_DURATION_MS + MOVE_DURATION_MS) : 0; + // Delay after which all item animations are ran and list item's content is visible. + public static final int WIDGET_LIST_ITEM_APPEARANCE_DELAY = + WIDGET_LIST_ITEM_APPEARANCE_START_DELAY + ADD_DURATION_MS; + public WidgetsListItemAnimator() { super(); diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java index 45d733a3a5..679b0f566b 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java @@ -15,10 +15,7 @@ */ package com.android.launcher3.widget.picker; -import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.CHANGE_DURATION_MS; -import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.MOVE_DURATION_MS; - -import static android.animation.ValueAnimator.areAnimatorsEnabled; +import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_START_DELAY; import android.content.Context; import android.graphics.Bitmap; @@ -157,8 +154,7 @@ public final class WidgetsListTableViewHolderBinder // Pass resize delay to let the "move" and "change" animations run before resizing the // row. tableRow.setupRow(widgetItems.size(), - /*resizeDelayMs=*/ - areAnimatorsEnabled() ? (CHANGE_DURATION_MS + MOVE_DURATION_MS) : 0); + /*resizeDelayMs=*/ WIDGET_LIST_ITEM_APPEARANCE_START_DELAY); if (tableRow.getChildCount() > widgetItems.size()) { for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) { tableRow.getChildAt(j).setVisibility(View.GONE); diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java index 5d71db6e39..840d98a271 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java @@ -21,6 +21,7 @@ import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER; import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER; import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree; import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree; +import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_DELAY; import android.content.Context; import android.graphics.Rect; @@ -31,6 +32,7 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ScrollView; @@ -281,10 +283,19 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mRightPane.removeAllViews(); mRightPane.addView(mWidgetRecommendationsContainer); mRightPaneScrollView.setScrollY(0); - mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle); mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo); final boolean isChangingHeaders = mSelectedHeader == null || !mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey); + // If the initial focus view is still focused, it is likely a programmatic header + // click. + if (mSelectedHeader != null + && !getAccessibilityInitialFocusView().isAccessibilityFocused()) { + post(() -> { + mRightPaneScrollView.setAccessibilityPaneTitle(suggestionsRightPaneTitle); + mRightPaneScrollView.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + }); + } if (isChangingHeaders) { // If switching from another header, unselect any WidgetCells. This is necessary // because we do not clear/recycle the WidgetCells in the recommendations container @@ -296,7 +307,6 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mSelectedHeader = mSuggestedWidgetsPackageUserKey; }); mSuggestedWidgetsContainer.addView(mSuggestedWidgetsHeader); - mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle); } @Override @@ -312,6 +322,30 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { * RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE; } + @Override + @Px + protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) { + int rightPaneWidth = (int) Math.ceil(0.67 * pickerAvailableWidth); + + if (mDeviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) { + // See onLayout + int leftPaneWidth = (int) (0.33 * pickerAvailableWidth); + @Px int minLeftPaneWidthPx = Utilities.dpToPx(MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP); + @Px int maxLeftPaneWidthPx = Utilities.dpToPx(MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP); + if (leftPaneWidth < minLeftPaneWidthPx) { + leftPaneWidth = minLeftPaneWidthPx; + } else if (leftPaneWidth > maxLeftPaneWidthPx) { + leftPaneWidth = maxLeftPaneWidthPx; + } + rightPaneWidth = pickerAvailableWidth - leftPaneWidth; + } + + // Since suggestions are shown in right pane, the available width is 2/3 of total width of + // bottom sheet. + return rightPaneWidth - getResources().getDimensionPixelSize( + R.dimen.widget_list_horizontal_margin_two_pane); // right pane end margin. + } + @Override public void onActivePageChanged(int currentActivePage) { super.onActivePageChanged(currentActivePage); @@ -323,12 +357,14 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { mActivePage = currentActivePage; - if (mSuggestedWidgetsHeader == null) { - mAdapters.get(currentActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); - mAdapters.get(currentActivePage).mWidgetsRecyclerView.scrollToTop(); - } else if (currentActivePage == PERSONAL_TAB || currentActivePage == WORK_TAB) { - mSuggestedWidgetsHeader.callOnClick(); - } + // When using talkback, swiping left while on right pane, should navigate to the widgets + // list on left. + mAdapters.get(mActivePage).mWidgetsRecyclerView.setAccessibilityTraversalBefore( + mRightPaneScrollView.getId()); + + // On page change, select the first item in the list to show in the right pane. + mAdapters.get(currentActivePage).mWidgetsListAdapter.selectFirstHeaderEntry(); + mAdapters.get(currentActivePage).mWidgetsRecyclerView.scrollToTop(); } @Override @@ -372,17 +408,16 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { } - @Override - protected View getContentView() { - return mRightPane; - } - private HeaderChangeListener getHeaderChangeListener() { return new HeaderChangeListener() { @Override public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) { final boolean isSameHeader = mSelectedHeader != null && mSelectedHeader.equals(selectedHeader); + // If the initial focus view is still focused, it is likely a programmatic header + // click. + final boolean isUserClick = mSelectedHeader != null + && !getAccessibilityInitialFocusView().isAccessibilityFocused(); mSelectedHeader = selectedHeader; WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider() .getSelectedAppWidgets(selectedHeader); @@ -427,11 +462,14 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { }; mRightPane.removeAllViews(); mRightPane.addView(widgetsRowViewHolder.itemView); + if (isUserClick) { + mRightPaneScrollView.setAccessibilityPaneTitle(getContext().getString( + R.string.widget_picker_right_pane_accessibility_title, + contentEntry.mPkgItem.title)); + postDelayed(() -> focusOnFirstWidgetCell(widgetsRowViewHolder.tableContainer), + WIDGET_LIST_ITEM_APPEARANCE_DELAY); + } mRightPaneScrollView.setScrollY(0); - mRightPane.setAccessibilityPaneTitle( - getContext().getString( - R.string.widget_picker_right_pane_accessibility_title, - contentEntry.mPkgItem.title)); } }; } @@ -445,6 +483,18 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { } } + /** + * Requests focus on the first widget cell in the given widget section. + */ + private static void focusOnFirstWidgetCell(ViewGroup parent) { + if (parent == null) return; + WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell); + if (cell != null) { + cell.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + } + private static void unselectWidgetCell(ViewGroup parent, WidgetItem item) { if (parent == null || item == null) return; WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell wc