Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +9 −0 Original line number Original line Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt 0 → 100644 +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 } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt 0 → 100644 +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) } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +9 −0 Original line number Original line Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading
libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt 0 → 100644 +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 } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt 0 → 100644 +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) } }