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

Commit e7ec705f authored by Winson Chung's avatar Winson Chung
Browse files

Add unhandled drag controller

- This CL creates a new controller for other components to register
  listeners for unhandled global drags

Bug: 320797628
Test: atest WMShellUnitTests
Change-Id: Ib9363eafd34f5611e7b410904709e8fa83692069
parent 78de7961
Loading
Loading
Loading
Loading
+9 −0
Original line number 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.ToggleResizeDesktopTaskTransitionHandler;
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.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
@@ -556,6 +557,14 @@ public abstract class WMShellModule {
    // Drag and drop
    //

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

    @WMSingleton
    @Provides
    static DragAndDropController provideDragAndDropController(Context context,
+100 −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.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 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)
    }
}