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 c743582c32641..09f5cf1d31e48 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 @@ -61,6 +61,7 @@ import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeController; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; import com.android.wm.shell.displayareahelper.DisplayAreaHelperController; import com.android.wm.shell.draganddrop.DragAndDropController; @@ -677,7 +678,11 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static Optional provideDesktopMode( - Optional desktopModeController) { + Optional desktopModeController, + Optional desktopTasksController) { + if (DesktopModeStatus.isProto2Enabled()) { + return desktopTasksController.map(DesktopTasksController::asDesktopMode); + } return desktopModeController.map(DesktopModeController::asDesktopMode); } @@ -698,6 +703,23 @@ public abstract class WMShellBaseModule { return Optional.empty(); } + @BindsOptionalOf + @DynamicOverride + abstract DesktopTasksController optionalDesktopTasksController(); + + @WMSingleton + @Provides + static Optional providesDesktopTasksController( + @DynamicOverride Optional> desktopTasksController) { + // Use optional-of-lazy for the dependency that this provider relies on. + // Lazy ensures that this provider will not be the cause the dependency is created + // when it will not be returned due to the condition below. + if (DesktopModeStatus.isProto2Enabled()) { + return desktopTasksController.map(Lazy::get); + } + return Optional.empty(); + } + @BindsOptionalOf @DynamicOverride abstract DesktopModeTaskRepository optionalDesktopModeTaskRepository(); 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 6be83054ae597..8a6fe437e652d 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 @@ -50,6 +50,7 @@ import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.desktopmode.DesktopModeController; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; @@ -613,6 +614,22 @@ public abstract class WMShellModule { mainExecutor); } + @WMSingleton + @Provides + @DynamicOverride + static DesktopTasksController provideDesktopTasksController( + Context context, + ShellInit shellInit, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, + Transitions transitions, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + @ShellMainThread ShellExecutor mainExecutor + ) { + return new DesktopTasksController(context, shellInit, shellController, shellTaskOrganizer, + transitions, desktopModeTaskRepository, mainExecutor); + } + @WMSingleton @Provides @DynamicOverride diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index 67f4a1914c492..055949fd8c89d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -70,9 +70,13 @@ public class DesktopModeStatus { * @return {@code true} if active */ public static boolean isActive(Context context) { - if (!IS_SUPPORTED) { + if (!isAnyEnabled()) { return false; } + if (isProto2Enabled()) { + // Desktop mode is always active in prototype 2 + return true; + } try { int result = Settings.System.getIntForUser(context.getContentResolver(), Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt new file mode 100644 index 0000000000000..736aa7e9aeca3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -0,0 +1,116 @@ +/* + * 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.desktopmode + +import android.content.Context +import androidx.annotation.BinderThread +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.ExternalInterfaceBinder +import com.android.wm.shell.common.RemoteCallable +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.annotations.ExternalThread +import com.android.wm.shell.common.annotations.ShellMainThread +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.sysui.ShellSharedConstants +import com.android.wm.shell.transition.Transitions +import java.util.concurrent.Executor + +/** Handles moving tasks in and out of desktop */ +class DesktopTasksController( + private val context: Context, + shellInit: ShellInit, + private val shellController: ShellController, + private val shellTaskOrganizer: ShellTaskOrganizer, + private val transitions: Transitions, + private val desktopModeTaskRepository: DesktopModeTaskRepository, + @ShellMainThread private val mainExecutor: ShellExecutor +) : RemoteCallable { + + private val desktopMode: DesktopModeImpl + + init { + desktopMode = DesktopModeImpl() + if (DesktopModeStatus.isProto2Enabled()) { + shellInit.addInitCallback({ onInit() }, this) + } + } + + private fun onInit() { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") + shellController.addExternalInterface( + ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, + { createExternalInterface() }, + this + ) + } + + override fun getContext(): Context { + return context + } + + override fun getRemoteCallExecutor(): ShellExecutor { + return mainExecutor + } + + /** Creates a new instance of the external interface to pass to another process. */ + private fun createExternalInterface(): ExternalInterfaceBinder { + return IDesktopModeImpl(this) + } + + /** Get connection interface between sysui and shell */ + fun asDesktopMode(): DesktopMode { + return desktopMode + } + + /** + * Adds a listener to find out about changes in the visibility of freeform tasks. + * + * @param listener the listener to add. + * @param callbackExecutor the executor to call the listener on. + */ + fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) { + desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor) + } + + /** The interface for calls from outside the shell, within the host process. */ + @ExternalThread + private inner class DesktopModeImpl : DesktopMode { + override fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) { + mainExecutor.execute { + this@DesktopTasksController.addListener(listener, callbackExecutor) + } + } + } + + /** The interface for calls from outside the host process. */ + @BinderThread + private class IDesktopModeImpl(private var controller: DesktopTasksController?) : + IDesktopMode.Stub(), ExternalInterfaceBinder { + /** Invalidates this instance, preventing future calls from updating the controller. */ + override fun invalidate() { + controller = null + } + + override fun showDesktopApps() { + // TODO + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt new file mode 100644 index 0000000000000..15155f80247bf --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -0,0 +1,98 @@ +/* + * 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.desktopmode + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.ExtendedMockito.never +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.any +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopTasksControllerTest : ShellTestCase() { + + @Mock + lateinit var testExecutor: ShellExecutor + @Mock + lateinit var shellController: ShellController + @Mock + lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock + lateinit var transitions: Transitions + + lateinit var mockitoSession: StaticMockitoSession + lateinit var controller: DesktopTasksController + lateinit var shellInit: ShellInit + lateinit var desktopModeTaskRepository: DesktopModeTaskRepository + + @Before + fun setUp() { + mockitoSession = mockitoSession().mockStatic(DesktopModeStatus::class.java).startMocking() + whenever(DesktopModeStatus.isProto2Enabled()).thenReturn(true) + + shellInit = Mockito.spy(ShellInit(testExecutor)) + desktopModeTaskRepository = DesktopModeTaskRepository() + + controller = createController() + + shellInit.init() + } + + private fun createController(): DesktopTasksController { + return DesktopTasksController(context, shellInit, shellController, + shellTaskOrganizer, transitions, desktopModeTaskRepository, TestShellExecutor()) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + fun instantiate_addInitCallback() { + verify(shellInit).addInitCallback(any(), any()) + } + + @Test + fun instantiate_flagOff_doNotAddInitCallback() { + whenever(DesktopModeStatus.isProto2Enabled()).thenReturn(false) + clearInvocations(shellInit) + + createController() + + verify(shellInit, never()).addInitCallback(any(), any()) + } +} \ No newline at end of file