Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 417094ee authored by Jorge Gil's avatar Jorge Gil
Browse files

[1/N] (Max Menu) Better encapsulation of window decor's views

Moves gesture & click detection for the Maximize Menu out of the
DesktopModeWindowDecorViewModel and into the MaximizeMenuView.

The goal here is to keep the implementation details of the menu view
encapsulated (resource ids, gesture detection, styling - even the fact
that the menu UI happens to be built with xml/android.view and not
Compose), and provide the decoration/viewmodel with abstractions for
actions taken by the user on the view (e.g.
DesktopModeWindowDecoration#setOnLeftSnapClickLitener). The ViewModel
shouldn't care about which specific maximize gesture was triggered
(button in header vs menu vs double-tap), so ideally those others should
be hooked up to these same abstractions later on.

Flag: EXEMPT refactor
Bug: 346441962
Test: atest WMShellUnitTests; maximize menu's hover / click actions work
as usual

Change-Id: I4b5476e157e43f98fc935bc1216e879b354c7e07
parent f0237201
Loading
Loading
Loading
Loading
+79 −67
Original line number Diff line number Diff line
@@ -14,18 +14,22 @@
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
    android:id="@+id/maximize_menu"
    style="?android:attr/buttonBarStyle"
    android:layout_width="@dimen/desktop_mode_maximize_menu_width"
    android:layout_height="@dimen/desktop_mode_maximize_menu_height"
    android:orientation="horizontal"
    android:gravity="center"
    android:padding="16dp"
    android:background="@drawable/desktop_mode_maximize_menu_background"
    android:elevation="1dp">

    <LinearLayout
        android:id="@+id/container"
        android:layout_width="@dimen/desktop_mode_maximize_menu_width"
        android:layout_height="@dimen/desktop_mode_maximize_menu_height"
        android:orientation="horizontal"
        android:padding="16dp"
        android:gravity="center">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
@@ -99,3 +103,11 @@
        </LinearLayout>
    </LinearLayout>

    <!-- Empty view intentionally placed in front of everything else and matching the menu size
     used to monitor input events over the entire menu. -->
    <View
        android:id="@+id/maximize_menu_overlay"
        android:layout_width="@dimen/desktop_mode_maximize_menu_width"
        android:layout_height="@dimen/desktop_mode_maximize_menu_height"/>
</FrameLayout>
+52 −67
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@ import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_HOVER_ENTER;
import static android.view.MotionEvent.ACTION_HOVER_EXIT;
import static android.view.MotionEvent.ACTION_HOVER_MOVE;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.WindowInsets.Type.statusBars;
@@ -103,6 +102,7 @@ import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener;
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;

import java.io.PrintWriter;
import java.util.Objects;
@@ -383,10 +383,32 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
        mWindowDecorByTaskId.remove(taskInfo.taskId);
    }

    private void onMaximizeOrRestore(int taskId, String tag) {
        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
        if (decoration == null) {
            return;
        }
        InteractionJankMonitorUtils.beginTracing(
                Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext, decoration.mTaskSurface, tag);
        mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
        decoration.closeHandleMenu();
        decoration.closeMaximizeMenu();
    }

    private void onSnapResize(int taskId, boolean left) {
        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
        if (decoration == null) {
            return;
        }
        mDesktopTasksController.snapToHalfScreen(decoration.mTaskInfo,
                left ? SnapPosition.LEFT : SnapPosition.RIGHT);
        decoration.closeHandleMenu();
        decoration.closeMaximizeMenu();
    }

    private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
            implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
            View.OnGenericMotionListener, DragDetector.MotionEventHandler {
        private static final int CLOSE_MAXIMIZE_MENU_DELAY_MS = 150;

        private final int mTaskId;
        private final WindowContainerToken mTaskToken;
@@ -405,7 +427,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
        private boolean mTouchscreenInUse;
        private boolean mHasLongClicked;
        private int mDragPointerId = -1;
        private final Runnable mCloseMaximizeWindowRunnable;

        private DesktopModeTouchEventListener(
                RunningTaskInfo taskInfo,
@@ -416,11 +437,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
            mDragDetector = new DragDetector(this);
            mGestureDetector = new GestureDetector(mContext, this);
            mDisplayId = taskInfo.displayId;
            mCloseMaximizeWindowRunnable = () -> {
                final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
                if (decoration == null) return;
                decoration.closeMaximizeMenu();
            };
        }

        @Override
@@ -472,31 +488,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
            } else if (id == R.id.collapse_menu_button) {
                decoration.closeHandleMenu();
            } else if (id == R.id.maximize_window) {
                InteractionJankMonitorUtils.beginTracing(
                        Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
                        /* tag= */ "caption_bar_button");
                final RunningTaskInfo taskInfo = decoration.mTaskInfo;
                decoration.closeHandleMenu();
                decoration.closeMaximizeMenu();
                mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
            } else if (id == R.id.maximize_menu_maximize_button) {
                InteractionJankMonitorUtils.beginTracing(
                        Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
                        /* tag= */ "maximize_menu_option");
                final RunningTaskInfo taskInfo = decoration.mTaskInfo;
                mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
                decoration.closeHandleMenu();
                decoration.closeMaximizeMenu();
            } else if (id == R.id.maximize_menu_snap_left_button) {
                final RunningTaskInfo taskInfo = decoration.mTaskInfo;
                mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.LEFT);
                decoration.closeHandleMenu();
                decoration.closeMaximizeMenu();
            } else if (id == R.id.maximize_menu_snap_right_button) {
                final RunningTaskInfo taskInfo = decoration.mTaskInfo;
                mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.RIGHT);
                decoration.closeHandleMenu();
                decoration.closeMaximizeMenu();
                // TODO(b/346441962): move click detection logic into the decor's
                //  {@link AppHeaderViewHolder}. Let it encapsulate the that and have it report
                //  back to the decoration using
                //  {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which
                //  should shared with the maximize menu's maximize/restore actions.
                onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
            }
        }

@@ -578,40 +575,26 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
            return false;
        }

        /**
         * TODO(b/346441962): move this hover detection logic into the decor's
         * {@link AppHeaderViewHolder}.
         */
        @Override
        public boolean onGenericMotion(View v, MotionEvent ev) {
            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
            final int id = v.getId();
            if (ev.getAction() == ACTION_HOVER_ENTER) {
                if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
                    decoration.onMaximizeWindowHoverEnter();
                } else if (id == R.id.maximize_window
                        || MaximizeMenu.Companion.isMaximizeMenuView(id)) {
                    // Re-hovering over any of the maximize menu views should keep the menu open by
                    // cancelling any attempts to close the menu.
                    mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
                    if (id != R.id.maximize_window) {
                        decoration.onMaximizeMenuHoverEnter(id, ev);
                    }
            if (ev.getAction() == ACTION_HOVER_ENTER && id == R.id.maximize_window) {
                decoration.setAppHeaderMaximizeButtonHovered(true);
                if (!decoration.isMaximizeMenuActive()) {
                    decoration.onMaximizeButtonHoverEnter();
                }
                return true;
            } else if (ev.getAction() == ACTION_HOVER_MOVE
                    && MaximizeMenu.Companion.isMaximizeMenuView(id)) {
                decoration.onMaximizeMenuHoverMove(id, ev);
                mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
            } else if (ev.getAction() == ACTION_HOVER_EXIT) {
                if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
                    decoration.onMaximizeWindowHoverExit();
                } else if (id == R.id.maximize_window
                        || MaximizeMenu.Companion.isMaximizeMenuView(id)) {
                    // Close menu if not hovering over maximize menu or maximize button after a
                    // delay to give user a chance to re-enter view or to move from one maximize
                    // menu view to another.
                    mMainHandler.postDelayed(mCloseMaximizeWindowRunnable,
                            CLOSE_MAXIMIZE_MENU_DELAY_MS);
                    if (id != R.id.maximize_window) {
                        decoration.onMaximizeMenuHoverExit(id, ev);
            }
            if (ev.getAction() == ACTION_HOVER_EXIT && id == R.id.maximize_window) {
                decoration.setAppHeaderMaximizeButtonHovered(false);
                decoration.onMaximizeHoverStateChanged();
                if (!decoration.isMaximizeMenuActive()) {
                    decoration.onMaximizeButtonHoverExit();
                }
                return true;
            }
@@ -719,11 +702,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
                    && action != MotionEvent.ACTION_CANCEL)) {
                return false;
            }
            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
            InteractionJankMonitorUtils.beginTracing(
                    Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext,
                    /* surface= */ decoration.mTaskSurface, /* tag= */ "double_tap");
            mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
            onMaximizeOrRestore(mTaskId, "double_tap");
            return true;
        }
    }
@@ -1105,7 +1084,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {

        final DesktopModeTouchEventListener touchEventListener =
                new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback);

        windowDecoration.setOnMaximizeOrRestoreClickListener(this::onMaximizeOrRestore);
        windowDecoration.setOnLeftSnapClickListener((taskId, tag) -> {
            onSnapResize(taskId, true /* isLeft */);
        });
        windowDecoration.setOnRightSnapClickListener((taskId, tag) -> {
            onSnapResize(taskId, false /* isLeft */);
        });
        windowDecoration.setCaptionListeners(
                touchEventListener, touchEventListener, touchEventListener, touchEventListener);
        windowDecoration.setExclusionRegionListener(mExclusionRegionListener);
+88 −26
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder;
import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
@@ -87,6 +88,9 @@ import java.util.function.Supplier;
public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
    private static final String TAG = "DesktopModeWindowDecoration";

    @VisibleForTesting
    static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L;

    private final Handler mHandler;
    private final Choreographer mChoreographer;
    private final SyncTransactionQueue mSyncQueue;
@@ -96,6 +100,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
    private View.OnTouchListener mOnCaptionTouchListener;
    private View.OnLongClickListener mOnCaptionLongClickListener;
    private View.OnGenericMotionListener mOnCaptionGenericMotionListener;
    private OnTaskActionClickListener mOnMaximizeOrRestoreClickListener;
    private OnTaskActionClickListener mOnLeftSnapClickListener;
    private OnTaskActionClickListener mOnRightSnapClickListener;
    private DragPositioningCallback mDragPositioningCallback;
    private DragResizeInputListener mDragResizeListener;
    private DragDetector mDragDetector;
@@ -120,6 +127,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
    private ExclusionRegionListener mExclusionRegionListener;

    private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
    private final MaximizeMenuFactory mMaximizeMenuFactory;

    // Hover state for the maximize menu and button. The menu will remain open as long as either of
    // these is true. See {@link #onMaximizeHoverStateChanged()}.
    private boolean mIsAppHeaderMaximizeButtonHovered = false;
    private boolean mIsMaximizeMenuHovered = false;
    // Used to schedule the closing of the maximize menu when neither of the button or menu are
    // being hovered. There's a small delay after stopping the hover, to allow a quick reentry
    // to cancel the close.
    private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu;

    DesktopModeWindowDecoration(
            Context context,
@@ -135,7 +152,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
                handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
                SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
                WindowContainerTransaction::new, SurfaceControl::new,
                new SurfaceControlViewHostFactory() {});
                new SurfaceControlViewHostFactory() {},
                DefaultMaximizeMenuFactory.INSTANCE);
    }

    DesktopModeWindowDecoration(
@@ -152,7 +170,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
            Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
            Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
            Supplier<SurfaceControl> surfaceControlSupplier,
            SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
            SurfaceControlViewHostFactory surfaceControlViewHostFactory,
            MaximizeMenuFactory maximizeMenuFactory) {
        super(context, displayController, taskOrganizer, taskInfo, taskSurface,
                surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                windowContainerTransactionSupplier, surfaceControlSupplier,
@@ -161,6 +180,31 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
        mChoreographer = choreographer;
        mSyncQueue = syncQueue;
        mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
        mMaximizeMenuFactory = maximizeMenuFactory;
    }

    /**
     * Register a listener to be called back when one of the tasks' maximize/restore action is
     * triggered.
     * TODO(b/346441962): hook this up to double-tap and the header's maximize button, instead of
     *  having the ViewModel deal with parsing motion events.
     */
    void setOnMaximizeOrRestoreClickListener(OnTaskActionClickListener listener) {
        mOnMaximizeOrRestoreClickListener = listener;
    }

    /**
     * Register a listener to be called back when one of the tasks snap-left action is triggered.
     */
    void setOnLeftSnapClickListener(OnTaskActionClickListener listener) {
        mOnLeftSnapClickListener = listener;
    }

    /**
     * Register a listener to be called back when one of the tasks' snap-right action is triggered.
     */
    void setOnRightSnapClickListener(OnTaskActionClickListener listener) {
        mOnRightSnapClickListener = listener;
    }

    void setCaptionListeners(
@@ -714,11 +758,41 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
     * Create and display maximize menu window
     */
    void createMaximizeMenu() {
        mMaximizeMenu = new MaximizeMenu(mSyncQueue, mRootTaskDisplayAreaOrganizer,
                mDisplayController, mTaskInfo, mOnCaptionButtonClickListener,
                mOnCaptionGenericMotionListener, mOnCaptionTouchListener, mContext,
        mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer,
                mDisplayController, mTaskInfo, mContext,
                calculateMaximizeMenuPosition(), mSurfaceControlTransactionSupplier);
        mMaximizeMenu.show();
        mMaximizeMenu.show(
                mOnMaximizeOrRestoreClickListener,
                mOnLeftSnapClickListener,
                mOnRightSnapClickListener,
                hovered -> {
                    mIsMaximizeMenuHovered = hovered;
                    onMaximizeHoverStateChanged();
                    return null;
                }
        );
    }

    /** Set whether the app header's maximize button is hovered. */
    void setAppHeaderMaximizeButtonHovered(boolean hovered) {
        mIsAppHeaderMaximizeButtonHovered = hovered;
        onMaximizeHoverStateChanged();
    }

    /**
     * Called when either one of the maximize button in the app header or the maximize menu has
     * changed its hover state.
     */
    void onMaximizeHoverStateChanged() {
        if (!mIsMaximizeMenuHovered && !mIsAppHeaderMaximizeButtonHovered) {
            // Neither is hovered, close the menu.
            if (isMaximizeMenuActive()) {
                mHandler.postDelayed(mCloseMaximizeWindowRunnable, CLOSE_MAXIMIZE_MENU_DELAY_MS);
            }
            return;
        }
        // At least one of the two is hovered, cancel the close if needed.
        mHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
    }

    /**
@@ -992,34 +1066,22 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
                .setAnimatingTaskResize(animatingTaskResize);
    }

    /** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */
    void onMaximizeWindowHoverExit() {
    /**
     * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button.
     */
    void onMaximizeButtonHoverExit() {
        ((AppHeaderViewHolder) mWindowDecorViewHolder)
                .onMaximizeWindowHoverExit();
    }

    /** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */
    void onMaximizeWindowHoverEnter() {
    /**
     * Called when there is a {@link MotionEvent#ACTION_HOVER_ENTER} on the maximize window button.
     */
    void onMaximizeButtonHoverEnter() {
        ((AppHeaderViewHolder) mWindowDecorViewHolder)
                .onMaximizeWindowHoverEnter();
    }

    /** Called when there is a {@Link ACTION_HOVER_ENTER} on a view in the maximize menu. */
    void onMaximizeMenuHoverEnter(int id, MotionEvent ev) {
        mMaximizeMenu.onMaximizeMenuHoverEnter(id, ev);
    }

    /** Called when there is a {@Link ACTION_HOVER_MOVE} on a view in the maximize menu. */
    void onMaximizeMenuHoverMove(int id, MotionEvent ev) {
        mMaximizeMenu.onMaximizeMenuHoverMove(id, ev);
    }

    /** Called when there is a {@Link ACTION_HOVER_EXIT} on a view in the maximize menu. */
    void onMaximizeMenuHoverExit(int id, MotionEvent ev) {
        mMaximizeMenu.onMaximizeMenuHoverExit(id, ev);
    }


    @Override
    public String toString() {
        return "{"
+128 −97

File changed.

Preview size limit exceeded, changes collapsed.

+27 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.windowdecor.common

/** A callback to be invoked when a Task's window decor element is clicked. */
fun interface OnTaskActionClickListener {
    /**
     * Called when a task's decor element has been clicked.
     *
     * @param taskId the id of the task.
     * @param tag a readable identifier for the element.
     */
    fun onClick(taskId: Int, tag: String)
}
Loading