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

Commit 39669b21 authored by Winson Chung's avatar Winson Chung Committed by Android (Google) Code Review
Browse files

Merge "Add unhandled drag controller" into main

parents 0468fbe8 e7ec705f
Loading
Loading
Loading
Loading
+9 −0
Original line number Original line Diff line number Diff line
@@ -64,6 +64,7 @@ import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.UnhandledDragController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
@@ -556,6 +557,14 @@ public abstract class WMShellModule {
    // Drag and drop
    // Drag and drop
    //
    //


    @WMSingleton
    @Provides
    static UnhandledDragController provideUnhandledDragController(
            IWindowManager wmService,
            @ShellMainThread ShellExecutor mainExecutor) {
        return new UnhandledDragController(wmService, mainExecutor);
    }

    @WMSingleton
    @WMSingleton
    @Provides
    @Provides
    static DragAndDropController provideDragAndDropController(Context context,
    static DragAndDropController provideDragAndDropController(Context context,
+100 −0
Original line number Original line 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.draganddrop

import android.os.RemoteException
import android.util.Log
import android.view.DragEvent
import android.view.IWindowManager
import android.window.IUnhandledDragCallback
import android.window.IUnhandledDragListener
import androidx.annotation.VisibleForTesting
import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.protolog.ShellProtoLogGroup
import java.util.function.Consumer

/**
 * Manages the listener and callbacks for unhandled global drags.
 */
class UnhandledDragController(
    val wmService: IWindowManager,
    mainExecutor: ShellExecutor
) {
    private var callback: UnhandledDragAndDropCallback? = null

    private val unhandledDragListener: IUnhandledDragListener =
        object : IUnhandledDragListener.Stub() {
            override fun onUnhandledDrop(event: DragEvent, callback: IUnhandledDragCallback) {
                mainExecutor.execute() {
                    this@UnhandledDragController.onUnhandledDrop(event, callback)
                }
            }
        }

    /**
     * Listener called when an unhandled drag is started.
     */
    interface UnhandledDragAndDropCallback {
        /**
         * Called when a global drag is unhandled (ie. dropped outside of all visible windows, or
         * dropped on a window that does not want to handle it).
         *
         * The implementer _must_ call onFinishedCallback, and if it consumes the drop, then it is
         * also responsible for releasing up the drag surface provided via the drag event.
         */
        fun onUnhandledDrop(dragEvent: DragEvent, onFinishedCallback: Consumer<Boolean>) {}
    }

    /**
     * Sets a listener for callbacks when an unhandled drag happens.
     */
    fun setListener(listener: UnhandledDragAndDropCallback?) {
        val updateWm = (callback == null && listener != null)
                || (callback != null && listener == null)
        callback = listener
        if (updateWm) {
            try {
                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
                    "%s unhandled drag listener",
                    if (callback != null) "Registering" else "Unregistering")
                wmService.setUnhandledDragListener(
                    if (callback != null) unhandledDragListener else null)
            } catch (e: RemoteException) {
                Log.e(TAG, "Failed to set unhandled drag listener")
            }
        }
    }

    @VisibleForTesting
    fun onUnhandledDrop(dragEvent: DragEvent, wmCallback: IUnhandledDragCallback) {
        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
            "onUnhandledDrop: %s", dragEvent)
        if (callback == null) {
            wmCallback.notifyUnhandledDropComplete(false)
        }

        callback?.onUnhandledDrop(dragEvent) {
            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
                "Notifying onUnhandledDrop complete: %b", it)
            wmCallback.notifyUnhandledDropComplete(it)
        }
    }

    companion object {
        private val TAG = UnhandledDragController::class.java.simpleName
    }
}
+115 −0
Original line number Original line 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.draganddrop

import android.os.RemoteException
import android.view.DragEvent
import android.view.DragEvent.ACTION_DROP
import android.view.IWindowManager
import android.view.SurfaceControl
import android.window.IUnhandledDragCallback
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.draganddrop.UnhandledDragController.UnhandledDragAndDropCallback
import java.util.function.Consumer
import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

/**
 * Tests for the unhandled drag controller.
 */
@SmallTest
@RunWith(AndroidJUnit4::class)
class UnhandledDragControllerTest : ShellTestCase() {
    @Mock
    private lateinit var mIWindowManager: IWindowManager

    @Mock
    private lateinit var mMainExecutor: ShellExecutor

    private lateinit var mController: UnhandledDragController

    @Before
    @Throws(RemoteException::class)
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        mController = UnhandledDragController(mIWindowManager, mMainExecutor)
    }

    @Test
    fun setListener_registersUnregistersWithWM() {
        mController.setListener(object : UnhandledDragAndDropCallback {})
        mController.setListener(object : UnhandledDragAndDropCallback {})
        mController.setListener(object : UnhandledDragAndDropCallback {})
        verify(mIWindowManager, Mockito.times(1))
                .setUnhandledDragListener(ArgumentMatchers.any())

        reset(mIWindowManager)
        mController.setListener(null)
        mController.setListener(null)
        mController.setListener(null)
        verify(mIWindowManager, Mockito.times(1))
                .setUnhandledDragListener(ArgumentMatchers.isNull())
    }

    @Test
    fun onUnhandledDrop_noListener_expectNotifyUnhandled() {
        // Simulate an unhandled drop
        val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,
            null, null, false)
        val wmCallback = mock(IUnhandledDragCallback::class.java)
        mController.onUnhandledDrop(dropEvent, wmCallback)

        verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(false))
    }

    @Test
    fun onUnhandledDrop_withListener_expectNotifyHandled() {
        val lastDragEvent = arrayOfNulls<DragEvent>(1)

        // Set a listener to listen for unhandled drops
        mController.setListener(object : UnhandledDragAndDropCallback {
            override fun onUnhandledDrop(dragEvent: DragEvent,
                onFinishedCallback: Consumer<Boolean>) {
                lastDragEvent[0] = dragEvent
                onFinishedCallback.accept(true)
                dragEvent.dragSurface.release()
            }
        })

        // Simulate an unhandled drop
        val dragSurface = mock(SurfaceControl::class.java)
        val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,
            dragSurface, null, false)
        val wmCallback = mock(IUnhandledDragCallback::class.java)
        mController.onUnhandledDrop(dropEvent, wmCallback)

        verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(true))
        verify(dragSurface).release()
        assertEquals(lastDragEvent.get(0), dropEvent)
    }
}