From 1e37f5519ba2ff66525fd7d33c4313ffbcf0c9e2 Mon Sep 17 00:00:00 2001 From: Garfield Tan Date: Fri, 8 Jul 2022 13:29:03 -0700 Subject: [PATCH] Add FreeformTaskTransitionHandler This change ties the window decoration lifecycle to shell transitions if it's enabled, so that we can leverage the new transition handler to hand over window decorations. This change exposes the transfers of management of tasks to task listeners to implement maximizing. Eventually I want to use it to implement the passover of window decorations across listeners. This change lets freeform code handle maximizing transitions because there isn't anything special to do for fullscreen tasks with window decorations so we can keep FullscreenTaskListener simple. Currently all animations related to freeform tasks are jumpcuts because there isn't any spec defined. Bug: 205580520 Test: Open, close, maximize freeform tasks with the shell transition and the caption in shell enabled. Test: atest WindowDecorationTests Change-Id: Ie7bb92e0b5946b006fa685f4438815627c72dffe --- .../wm/shell/dagger/WMShellBaseModule.java | 20 +- .../wm/shell/dagger/WMShellModule.java | 23 +- .../wm/shell/freeform/FreeformComponents.java | 56 +++++ .../shell/freeform/FreeformTaskListener.java | 178 +++++++++---- .../FreeformTaskTransitionHandler.java | 234 ++++++++++++++++++ .../FreeformTaskTransitionStarter.java | 34 +++ .../com/android/wm/shell/sysui/ShellInit.java | 14 +- .../wm/shell/transition/Transitions.java | 6 + .../CaptionWindowDecorViewModel.java | 48 +++- .../windowdecor/CaptionWindowDecoration.java | 20 +- .../windowdecor/WindowDecorViewModel.java | 41 ++- .../shell/windowdecor/WindowDecoration.java | 20 +- .../com/android/wm/shell/ShellInitTest.java | 6 +- .../windowdecor/WindowDecorationTests.java | 30 +-- 14 files changed, 630 insertions(+), 100 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 5dd51499ef191..a168cb22f6ad7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -29,8 +29,6 @@ import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; -import com.android.wm.shell.sysui.ShellCommandHandler; -import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.TaskViewFactory; import com.android.wm.shell.TaskViewFactoryController; @@ -59,7 +57,7 @@ import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; import com.android.wm.shell.displayareahelper.DisplayAreaHelperController; import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; @@ -78,7 +76,9 @@ import com.android.wm.shell.startingsurface.StartingSurface; import com.android.wm.shell.startingsurface.StartingWindowController; import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; import com.android.wm.shell.startingsurface.phone.PhoneStartingWindowTypeAlgorithm; +import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.sysui.ShellInterface; import com.android.wm.shell.transition.ShellTransitions; import com.android.wm.shell.transition.Transitions; @@ -339,15 +339,15 @@ public abstract class WMShellBaseModule { // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} @BindsOptionalOf @DynamicOverride - abstract FreeformTaskListener optionalFreeformTaskListener(); + abstract FreeformComponents optionalFreeformComponents(); @WMSingleton @Provides - static Optional> provideFreeformTaskListener( - @DynamicOverride Optional> freeformTaskListener, + static Optional provideFreeformTaskListener( + @DynamicOverride Optional freeformComponents, Context context) { - if (FreeformTaskListener.isFreeformEnabled(context)) { - return freeformTaskListener; + if (FreeformComponents.isFreeformEnabled(context)) { + return freeformComponents; } return Optional.empty(); } @@ -636,7 +636,7 @@ public abstract class WMShellBaseModule { FullscreenTaskListener fullscreenTaskListener, Optional unfoldAnimationController, Optional unfoldTransitionHandler, - Optional> freeformTaskListener, + Optional freeformComponents, Optional recentTasksOptional, Optional activityEmbeddingOptional, Transitions transitions, @@ -655,7 +655,7 @@ public abstract class WMShellBaseModule { fullscreenTaskListener, unfoldAnimationController, unfoldTransitionHandler, - freeformTaskListener, + freeformComponents, recentTasksOptional, activityEmbeddingOptional, transitions, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 0dc293b0265f6..588a1aa57a3a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -49,7 +49,9 @@ import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.draganddrop.DragAndDropController; +import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.freeform.FreeformTaskTransitionHandler; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; @@ -194,10 +196,27 @@ public abstract class WMShellModule { @WMSingleton @Provides @DynamicOverride + static FreeformComponents provideFreeformComponents( + FreeformTaskListener taskListener, + FreeformTaskTransitionHandler transitionHandler) { + return new FreeformComponents(taskListener, Optional.of(transitionHandler)); + } + + @WMSingleton + @Provides static FreeformTaskListener provideFreeformTaskListener( + WindowDecorViewModel windowDecorViewModel) { + return new FreeformTaskListener<>(windowDecorViewModel); + } + + @WMSingleton + @Provides + static FreeformTaskTransitionHandler provideTaskTransitionHandler( + Transitions transitions, WindowDecorViewModel windowDecorViewModel, - SyncTransactionQueue syncQueue) { - return new FreeformTaskListener<>(windowDecorViewModel, syncQueue); + FreeformTaskListener freeformTaskListener) { + return new FreeformTaskTransitionHandler(transitions, windowDecorViewModel, + freeformTaskListener); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java new file mode 100644 index 0000000000000..41e1b1de25463 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.freeform; + +import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; +import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT; + +import android.content.Context; +import android.provider.Settings; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.transition.Transitions; + +import java.util.Optional; + +/** + * Class that holds freeform related classes. It serves as the single injection point of + * all freeform classes to avoid leaking implementation details to the base Dagger module. + */ +public class FreeformComponents { + public final ShellTaskOrganizer.TaskListener mTaskListener; + public final Optional mTaskTransitionHandler; + + /** + * Creates an instance with the given components. + */ + public FreeformComponents( + ShellTaskOrganizer.TaskListener taskListener, + Optional taskTransitionHandler) { + mTaskListener = taskListener; + mTaskTransitionHandler = taskTransitionHandler; + } + + /** + * Returns if this device supports freeform. + */ + public static boolean isFreeformEnabled(Context context) { + return context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT) + || Settings.Global.getInt(context.getContentResolver(), + DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 692e6acb540c5..ba093a5fcc058 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -16,20 +16,18 @@ package com.android.wm.shell.freeform; -import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; -import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT; - import android.app.ActivityManager.RunningTaskInfo; -import android.content.Context; -import android.provider.Settings; -import android.util.Slog; +import android.util.Log; import android.util.SparseArray; import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import java.io.PrintWriter; @@ -45,9 +43,9 @@ public class FreeformTaskListener private static final String TAG = "FreeformTaskListener"; private final WindowDecorViewModel mWindowDecorationViewModel; - private final SyncTransactionQueue mSyncQueue; private final SparseArray> mTasks = new SparseArray<>(); + private final SparseArray mWindowDecorOfVanishedTasks = new SparseArray<>(); private static class State { RunningTaskInfo mTaskInfo; @@ -55,57 +53,78 @@ public class FreeformTaskListener T mWindowDecoration; } - public FreeformTaskListener( - WindowDecorViewModel windowDecorationViewModel, - SyncTransactionQueue syncQueue) { + public FreeformTaskListener(WindowDecorViewModel windowDecorationViewModel) { mWindowDecorationViewModel = windowDecorationViewModel; - mSyncQueue = syncQueue; } @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { - if (mTasks.get(taskInfo.taskId) != null) { - throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId); - } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d", taskInfo.taskId); - final State state = new State<>(); + final State state = createOrUpdateTaskState(taskInfo, leash); + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + state.mWindowDecoration = + mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash, t, t); + t.apply(); + } + } + + private State createOrUpdateTaskState(RunningTaskInfo taskInfo, SurfaceControl leash) { + State state = mTasks.get(taskInfo.taskId); + if (state != null) { + updateTaskInfo(taskInfo); + return state; + } + + state = new State<>(); state.mTaskInfo = taskInfo; state.mLeash = leash; - state.mWindowDecoration = - mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash); mTasks.put(taskInfo.taskId, state); + + return state; } @Override public void onTaskVanished(RunningTaskInfo taskInfo) { - State state = mTasks.get(taskInfo.taskId); + final State state = mTasks.get(taskInfo.taskId); if (state == null) { - Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); + // This is possible if the transition happens before this method. return; } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d", taskInfo.taskId); mTasks.remove(taskInfo.taskId); - try { - state.mWindowDecoration.close(); - } catch (Exception e) { - Slog.e(TAG, "Failed to release window decoration.", e); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + // Save window decorations of closing tasks so that we can hand them over to the + // transition system if this method happens before the transition. In case where the + // transition didn't happen, it'd be cleared when the next transition finished. + if (state.mWindowDecoration != null) { + mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration); + } + return; } + releaseWindowDecor(state.mWindowDecoration); } @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { - State state = mTasks.get(taskInfo.taskId); - if (state == null) { - throw new RuntimeException( - "Task info changed before appearing: #" + taskInfo.taskId); - } + final State state = updateTaskInfo(taskInfo); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d", taskInfo.taskId); + if (state.mWindowDecoration != null) { + mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo, state.mWindowDecoration); + } + } + + private State updateTaskInfo(RunningTaskInfo taskInfo) { + final State state = mTasks.get(taskInfo.taskId); + if (state == null) { + throw new RuntimeException("Task info changed before appearing: #" + taskInfo.taskId); + } state.mTaskInfo = taskInfo; - mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo, state.mWindowDecoration); + return state; } @Override @@ -126,6 +145,93 @@ public class FreeformTaskListener return mTasks.get(taskId).mLeash; } + /** + * Creates a window decoration for a transition. + * + * @param change the change of this task transition that needs to have the task layer as the + * leash + * @return {@code true} if it adopts the window decoration; {@code false} otherwise + */ + void createWindowDecoration( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + final State state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); + state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration( + state.mTaskInfo, state.mLeash, startT, finishT); + } + + /** + * Gives out the ownership of the task's window decoration. The given task is leaving (of has + * left) this task listener. This is the transition system asking for the ownership. + * + * @param taskInfo the maximizing task + * @return the window decor of the maximizing task if any + */ + T giveWindowDecoration( + RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + T windowDecor = mWindowDecorOfVanishedTasks.get(taskInfo.taskId); + mWindowDecorOfVanishedTasks.remove(taskInfo.taskId); + final State state = mTasks.get(taskInfo.taskId); + if (state != null) { + windowDecor = windowDecor == null ? state.mWindowDecoration : windowDecor; + state.mWindowDecoration = null; + } + mWindowDecorationViewModel.setupWindowDecorationForTransition( + taskInfo, startT, finishT, windowDecor); + return windowDecor; + } + + /** + * Adopt the incoming window decoration and lets the window decoration prepare for a transition. + * + * @param change the change of this task transition that needs to have the task layer as the + * leash + * @param startT the start transaction of this transition + * @param finishT the finish transaction of this transition + * @param windowDecor the window decoration to adopt + * @return {@code true} if it adopts the window decoration; {@code false} otherwise + */ + boolean adoptWindowDecoration( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, + @Nullable AutoCloseable windowDecor) { + final State state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); + state.mWindowDecoration = mWindowDecorationViewModel.adoptWindowDecoration(windowDecor); + if (state.mWindowDecoration != null) { + mWindowDecorationViewModel.setupWindowDecorationForTransition( + state.mTaskInfo, startT, finishT, state.mWindowDecoration); + return true; + } else { + state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration( + state.mTaskInfo, state.mLeash, startT, finishT); + return false; + } + } + + void onTaskTransitionFinished() { + if (mWindowDecorOfVanishedTasks.size() == 0) { + return; + } + Log.w(TAG, "Clearing window decors of vanished tasks. There could be visual defects " + + "if any of them is used later in transitions."); + for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) { + releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i)); + } + mWindowDecorOfVanishedTasks.clear(); + } + + private void releaseWindowDecor(T windowDecor) { + try { + windowDecor.close(); + } catch (Exception e) { + Log.e(TAG, "Failed to release window decoration.", e); + } + } + @Override public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; @@ -137,16 +243,4 @@ public class FreeformTaskListener public String toString() { return TAG; } - - /** - * Checks if freeform support is enabled in system. - * - * @param context context used to check settings and package manager. - * @return {@code true} if freeform is enabled, {@code false} if not. - */ - public static boolean isFreeformEnabled(Context context) { - return context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT) - || Settings.Global.getInt(context.getContentResolver(), - DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0; - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java new file mode 100644 index 0000000000000..8731eb604cbc9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.freeform; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.os.IBinder; +import android.util.Log; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; + +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link Transitions.TransitionHandler} that handles freeform task launches, closes, + * maximizing and restoring transitions. It also reports transitions so that window decorations can + * be a part of transitions. + */ +public class FreeformTaskTransitionHandler + implements Transitions.TransitionHandler, FreeformTaskTransitionStarter { + private static final String TAG = "FreeformTH"; + + private final Transitions mTransitions; + private final FreeformTaskListener mFreeformTaskListener; + + private final List mPendingTransitionTokens = new ArrayList<>(); + + public FreeformTaskTransitionHandler( + Transitions transitions, + WindowDecorViewModel windowDecorViewModel, + FreeformTaskListener freeformTaskListener) { + mTransitions = transitions; + mFreeformTaskListener = freeformTaskListener; + + windowDecorViewModel.setFreeformTaskTransitionStarter(this); + } + + @Override + public void startWindowingModeTransition( + int targetWindowingMode, WindowContainerTransaction wct) { + final int type; + switch (targetWindowingMode) { + case WINDOWING_MODE_FULLSCREEN: + type = Transitions.TRANSIT_MAXIMIZE; + break; + case WINDOWING_MODE_FREEFORM: + type = Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE; + break; + default: + throw new IllegalArgumentException("Unexpected target windowing mode " + + WindowConfiguration.windowingModeToString(targetWindowingMode)); + } + final IBinder token = mTransitions.startTransition(type, wct, this); + mPendingTransitionTokens.add(token); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + boolean transitionHandled = false; + final ArrayList windowDecorsInCloseTransitions = new ArrayList<>(); + for (TransitionInfo.Change change : info.getChanges()) { + if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) { + continue; + } + + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null || taskInfo.taskId == -1) { + continue; + } + + switch (change.getMode()) { + case WindowManager.TRANSIT_OPEN: + transitionHandled |= startOpenTransition(change, startT, finishT); + break; + case WindowManager.TRANSIT_CLOSE: + transitionHandled |= startCloseTransition( + change, windowDecorsInCloseTransitions, startT, finishT); + break; + case WindowManager.TRANSIT_CHANGE: + transitionHandled |= startChangeTransition( + transition, info.getType(), change, startT, finishT); + break; + case WindowManager.TRANSIT_TO_BACK: + case WindowManager.TRANSIT_TO_FRONT: + transitionHandled = true; + break; + } + } + + mPendingTransitionTokens.remove(transition); + + if (!transitionHandled) { + return false; + } + + startT.apply(); + mTransitions.getMainExecutor().execute( + () -> finishTransition(windowDecorsInCloseTransitions, finishCallback)); + return true; + } + + private boolean startOpenTransition( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + if (change.getTaskInfo().getWindowingMode() != WINDOWING_MODE_FREEFORM) { + return false; + } + mFreeformTaskListener.createWindowDecoration(change, startT, finishT); + return true; + } + + private boolean startCloseTransition( + TransitionInfo.Change change, + ArrayList windowDecors, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + if (change.getTaskInfo().getWindowingMode() != WINDOWING_MODE_FREEFORM) { + return false; + } + final AutoCloseable windowDecor = + mFreeformTaskListener.giveWindowDecoration(change.getTaskInfo(), startT, finishT); + if (windowDecor != null) { + windowDecors.add(windowDecor); + } + + return true; + } + + private boolean startChangeTransition( + IBinder transition, + int type, + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + AutoCloseable windowDecor = null; + + if (!mPendingTransitionTokens.contains(transition)) { + return false; + } + + boolean handled = false; + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (type == Transitions.TRANSIT_MAXIMIZE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + handled = true; + windowDecor = mFreeformTaskListener.giveWindowDecoration( + change.getTaskInfo(), startT, finishT); + // TODO(b/235638450): Let fullscreen task listener adopt the window decor. + } + + if (type == Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE + && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + handled = true; + // TODO(b/235638450): Let fullscreen task listener transfer the window decor. + mFreeformTaskListener.adoptWindowDecoration(change, startT, finishT, windowDecor); + } + + releaseWindowDecor(windowDecor); + + return handled; + } + + private void finishTransition( + ArrayList windowDecorsInCloseTransitions, + Transitions.TransitionFinishCallback finishCallback) { + for (AutoCloseable windowDecor : windowDecorsInCloseTransitions) { + releaseWindowDecor(windowDecor); + } + mFreeformTaskListener.onTaskTransitionFinished(); + // TODO(b/235638450): Dispatch it to fullscreen task listener. + finishCallback.onTransitionFinished(null, null); + } + + private void releaseWindowDecor(AutoCloseable windowDecor) { + if (windowDecor == null) { + return; + } + try { + windowDecor.close(); + } catch (Exception e) { + Log.e(TAG, "Failed to release window decoration.", e); + } + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + final ActivityManager.RunningTaskInfo taskInfo = request.getTriggerTask(); + if (taskInfo == null || taskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) { + return null; + } + switch (request.getType()) { + case WindowManager.TRANSIT_OPEN: + case WindowManager.TRANSIT_CLOSE: + case WindowManager.TRANSIT_TO_FRONT: + case WindowManager.TRANSIT_TO_BACK: + return new WindowContainerTransaction(); + default: + return null; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java new file mode 100644 index 0000000000000..25eaa0e05a099 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.freeform; + +import android.window.WindowContainerTransaction; + +/** + * The interface around {@link FreeformTaskTransitionHandler} for task listeners to start freeform + * task transitions. + */ +public interface FreeformTaskTransitionStarter { + + /** + * Starts a windowing mode transition. + * + * @param targetWindowingMode the target windowing mode + * @param wct the {@link WindowContainerTransaction} that changes the windowing mode + */ + void startWindowingModeTransition(int targetWindowingMode, WindowContainerTransaction wct); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java index 2619b37b67d8c..dd7fab7c5f461 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java @@ -34,7 +34,7 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.pip.phone.PipTouchHandler; @@ -68,7 +68,7 @@ public class ShellInit { private final FullscreenTaskListener mFullscreenTaskListener; private final Optional mUnfoldController; private final Optional mUnfoldTransitionHandler; - private final Optional> mFreeformTaskListenerOptional; + private final Optional mFreeformComponentsOptional; private final ShellExecutor mMainExecutor; private final Transitions mTransitions; private final StartingWindowController mStartingWindow; @@ -93,7 +93,7 @@ public class ShellInit { FullscreenTaskListener fullscreenTaskListener, Optional unfoldAnimationController, Optional unfoldTransitionHandler, - Optional> freeformTaskListenerOptional, + Optional freeformComponentsOptional, Optional recentTasks, Optional activityEmbeddingOptional, Transitions transitions, @@ -111,7 +111,7 @@ public class ShellInit { mPipTouchHandlerOptional = pipTouchHandlerOptional; mUnfoldController = unfoldAnimationController; mUnfoldTransitionHandler = unfoldTransitionHandler; - mFreeformTaskListenerOptional = freeformTaskListenerOptional; + mFreeformComponentsOptional = freeformComponentsOptional; mRecentTasks = recentTasks; mActivityEmbeddingOptional = activityEmbeddingOptional; mTransitions = transitions; @@ -143,6 +143,8 @@ public class ShellInit { mTransitions.register(mShellTaskOrganizer); mActivityEmbeddingOptional.ifPresent(ActivityEmbeddingController::init); mUnfoldTransitionHandler.ifPresent(UnfoldTransitionHandler::init); + mFreeformComponentsOptional.flatMap(f -> f.mTaskTransitionHandler) + .ifPresent(mTransitions::addHandler); if (mSplitScreenOptional.isPresent() && mPipTouchHandlerOptional.isPresent()) { final DefaultMixedHandler mixedHandler = new DefaultMixedHandler(mTransitions, mPipTouchHandlerOptional.get().getTransitionHandler(), @@ -158,9 +160,9 @@ public class ShellInit { mPipTouchHandlerOptional.ifPresent((handler) -> handler.init()); // Initialize optional freeform - mFreeformTaskListenerOptional.ifPresent(f -> + mFreeformComponentsOptional.ifPresent(f -> mShellTaskOrganizer.addListenerForType( - f, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM)); + f.mTaskListener, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM)); mUnfoldController.ifPresent(UnfoldAnimationController::init); mRecentTasks.ifPresent(RecentTasksController::init); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index fa22c7ca94d23..8e36154d10e7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -98,6 +98,12 @@ public class Transitions implements RemoteCallable { /** Transition type for dismissing split-screen. */ public static final int TRANSIT_SPLIT_DISMISS = TRANSIT_FIRST_CUSTOM + 7; + /** Transition type for freeform to maximize transition. */ + public static final int TRANSIT_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 8; + + /** Transition type for maximize to freeform transition. */ + public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9; + private final WindowOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index fe3724b4ec1b4..08d6c50f94b43 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -36,6 +36,8 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.transition.Transitions; /** * View model for the window decoration with a caption and shadows. Works with @@ -50,6 +52,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { + transaction.merge(t); + t.close(); + }); + } + + void relayout(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { final int shadowRadiusDp = taskInfo.isFocused ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP; final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode() @@ -110,18 +120,12 @@ public class CaptionWindowDecoration extends WindowDecoration { - transaction.merge(t); - t.close(); - - mTaskOrganizer.applyTransaction(wct); - }); + mTaskOrganizer.applyTransaction(wct); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index 6f9ceff722aca..c234949572bfa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -19,6 +19,10 @@ package com.android.wm.shell.windowdecor; import android.app.ActivityManager; import android.view.SurfaceControl; +import androidx.annotation.Nullable; + +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; + /** * The interface used by some {@link com.android.wm.shell.ShellTaskOrganizer.TaskListener} to help * customize {@link WindowDecoration}. Its implementations are responsible to interpret user's @@ -29,14 +33,35 @@ import android.view.SurfaceControl; */ public interface WindowDecorViewModel { + /** + * Sets the transition starter that starts freeform task transitions. + * + * @param transitionStarter the transition starter that starts freeform task transitions + */ + void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter); + /** * Creates a window decoration for the given task. * * @param taskInfo the initial task info of the task * @param taskSurface the surface of the task + * @param startT the start transaction to be applied before the transition + * @param finishT the finish transaction to restore states after the transition * @return the window decoration object */ - T createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface); + T createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskSurface, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT); + + /** + * Adopts the window decoration if possible. + * + * @param windowDecor the potential window decoration to adopt + * @return the window decoration if it can be adopted, or {@code null} otherwise. + */ + T adoptWindowDecoration(@Nullable AutoCloseable windowDecor); /** * Notifies a task info update on the given task, with the window decoration created previously @@ -46,4 +71,18 @@ public interface WindowDecorViewModel { * @param windowDecoration the window decoration created for the task */ void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo, T windowDecoration); + + /** + * Notifies a transition is about to start about the given task to give the window decoration a + * chance to prepare for this transition. + * + * @param startT the start transaction to be applied before the transition + * @param finishT the finish transaction to restore states after the transition + * @param windowDecoration the window decoration created for the task + */ + void setupWindowDecorationForTransition( + ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, + T windowDecoration); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 4855fbd5ba7a7..4380bdc364174 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -140,8 +140,9 @@ public abstract class WindowDecoration abstract void relayout(RunningTaskInfo taskInfo); void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp, - Rect outsetsDp, float shadowRadiusDp, SurfaceControl.Transaction t, - WindowContainerTransaction wct, RelayoutResult outResult) { + Rect outsetsDp, float shadowRadiusDp, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, WindowContainerTransaction wct, + RelayoutResult outResult) { outResult.reset(); final Configuration oldTaskConfig = mTaskInfo.getConfiguration(); @@ -151,7 +152,7 @@ public abstract class WindowDecoration if (!mTaskInfo.isVisible) { releaseViews(); - t.hide(mTaskSurface); + finishT.hide(mTaskSurface); return; } @@ -192,7 +193,7 @@ public abstract class WindowDecoration .setParent(mTaskSurface) .build(); - t.setTrustedOverlay(mDecorationContainerSurface, true); + startT.setTrustedOverlay(mDecorationContainerSurface, true); } final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); @@ -205,7 +206,8 @@ public abstract class WindowDecoration outResult.mHeight = taskBounds.height() + (int) (outsetsDp.bottom * outResult.mDensity) - decorContainerOffsetY; - t.setPosition(mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) + startT.setPosition( + mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1) .show(mDecorationContainerSurface); @@ -225,7 +227,7 @@ public abstract class WindowDecoration mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f; mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f; mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f; - t.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height()) + startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height()) .setShadowRadius(mTaskBackgroundSurface, shadowRadius) .setColor(mTaskBackgroundSurface, mTmpColor); @@ -264,9 +266,9 @@ public abstract class WindowDecoration decorContainerOffsetY, outResult.mWidth + decorContainerOffsetX, outResult.mHeight + decorContainerOffsetY); - t.setPosition(mTaskSurface, taskPosition.x, taskPosition.y) - .setCrop(mTaskSurface, mTaskSurfaceCrop) - .show(mTaskSurface); + startT.show(mTaskSurface); + finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y) + .setCrop(mTaskSurface, mTaskSurfaceCrop); } /** diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java index 219e5ab6c6512..6cd5677db2c37 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java @@ -31,7 +31,7 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.pip.phone.PipTouchHandler; @@ -71,7 +71,7 @@ public class ShellInitTest extends ShellTestCase { @Mock private FullscreenTaskListener mFullscreenTaskListener; @Mock private Optional mUnfoldAnimationController; @Mock private Optional mUnfoldTransitionHandler; - @Mock private Optional> mFreeformTaskListenerOptional; + @Mock private Optional mFreeformComponentsOptional; @Mock private Optional mRecentTasks; @Mock private Optional mActivityEmbeddingController; @Mock private Transitions mTransitions; @@ -87,7 +87,7 @@ public class ShellInitTest extends ShellTestCase { mDisplayInsetsController, mDragAndDropController, mShellTaskOrganizer, mKidsModeTaskOrganizer, mBubblesOptional, mSplitScreenOptional, mPipTouchHandlerOptional, mFullscreenTaskListener, mUnfoldAnimationController, - mUnfoldTransitionHandler, mFreeformTaskListenerOptional, mRecentTasks, + mUnfoldTransitionHandler, mFreeformComponentsOptional, mRecentTasks, mActivityEmbeddingController, mTransitions, mStartingWindow, mMainExecutor); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index d1b837ef42f18..b318bb26a51d8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -98,11 +98,13 @@ public class WindowDecorationTests extends ShellTestCase { private WindowContainerTransaction mMockWindowContainerTransaction; private final List mMockSurfaceControlBuilders = new ArrayList<>(); - private SurfaceControl.Transaction mMockSurfaceControlTransaction; + private SurfaceControl.Transaction mMockSurfaceControlStartT; + private SurfaceControl.Transaction mMockSurfaceControlFinishT; @Before public void setUp() { - mMockSurfaceControlTransaction = createMockSurfaceControlTransaction(); + mMockSurfaceControlStartT = createMockSurfaceControlTransaction(); + mMockSurfaceControlFinishT = createMockSurfaceControlTransaction(); doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory) .create(any(), any(), any(), anyBoolean()); @@ -149,7 +151,7 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlViewHostFactory, never()) .create(any(), any(), any(), anyBoolean()); - verify(mMockSurfaceControlTransaction).hide(taskSurface); + verify(mMockSurfaceControlFinishT).hide(taskSurface); assertNull(mRelayoutResult.mRootView); } @@ -192,16 +194,16 @@ public class WindowDecorationTests extends ShellTestCase { verify(decorContainerSurfaceBuilder).setParent(taskSurface); verify(decorContainerSurfaceBuilder).setContainerLayer(); - verify(mMockSurfaceControlTransaction).setTrustedOverlay(decorContainerSurface, true); - verify(mMockSurfaceControlTransaction).setPosition(decorContainerSurface, -20, -40); - verify(mMockSurfaceControlTransaction).setWindowCrop(decorContainerSurface, 380, 220); + verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true); + verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40); + verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220); verify(taskBackgroundSurfaceBuilder).setParent(taskSurface); verify(taskBackgroundSurfaceBuilder).setEffectLayer(); - verify(mMockSurfaceControlTransaction).setWindowCrop(taskBackgroundSurface, 300, 100); - verify(mMockSurfaceControlTransaction) + verify(mMockSurfaceControlStartT).setWindowCrop(taskBackgroundSurface, 300, 100); + verify(mMockSurfaceControlStartT) .setColor(taskBackgroundSurface, new float[] {1.f, 1.f, 0.f}); - verify(mMockSurfaceControlTransaction).setShadowRadius(taskBackgroundSurface, 10); + verify(mMockSurfaceControlStartT).setShadowRadius(taskBackgroundSurface, 10); verify(mMockSurfaceControlViewHostFactory) .create(any(), eq(defaultDisplay), any(), anyBoolean()); @@ -218,11 +220,11 @@ public class WindowDecorationTests extends ShellTestCase { new int[] { InsetsState.ITYPE_CAPTION_BAR }); } - verify(mMockSurfaceControlTransaction) + verify(mMockSurfaceControlFinishT) .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y); - verify(mMockSurfaceControlTransaction) + verify(mMockSurfaceControlFinishT) .setCrop(taskSurface, new Rect(-20, -40, 360, 180)); - verify(mMockSurfaceControlTransaction) + verify(mMockSurfaceControlStartT) .show(taskSurface); assertEquals(380, mRelayoutResult.mWidth); @@ -319,8 +321,8 @@ public class WindowDecorationTests extends ShellTestCase { @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP, - mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlTransaction, - mMockWindowContainerTransaction, mRelayoutResult); + mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT, + mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult); } } }