Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +3 −2 Original line number Diff line number Diff line Loading @@ -252,6 +252,7 @@ public abstract class WMShellModule { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler) { if (DesktopModeStatus.canEnterDesktopMode(context)) { Loading @@ -277,6 +278,7 @@ public abstract class WMShellModule { assistContentRequester, multiInstanceHelper, desktopTasksLimiter, appHandleEducationController, windowDecorCaptionHandleRepository, desktopActivityOrientationHandler); } Loading Loading @@ -869,8 +871,7 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional, AppHandleEducationController appHandleEducationController Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional ) { return new Object(); } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +25 −1 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatasto import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.EducationViewConfig import kotlin.time.Duration.Companion.milliseconds Loading Loading @@ -68,6 +69,9 @@ class AppHandleEducationController( @ShellMainThread private val applicationCoroutineScope: CoroutineScope, @ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher, ) { private lateinit var openHandleMenuCallback: (Int) -> Unit private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit init { runIfEducationFeatureEnabled { applicationCoroutineScope.launch { Loading Loading @@ -114,6 +118,7 @@ class AppHandleEducationController( arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP, onEducationClickAction = { launchWithExceptionHandling { showWindowingImageButtonTooltip() } openHandleMenuCallback(captionState.runningTaskInfo.taskId) }, onDismissAction = { launchWithExceptionHandling { showWindowingImageButtonTooltip() } }, ) Loading Loading @@ -171,6 +176,9 @@ class AppHandleEducationController( DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, onEducationClickAction = { launchWithExceptionHandling { showExitWindowingTooltip() } toDesktopModeCallback( captionState.runningTaskInfo.taskId, DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON) }, onDismissAction = { launchWithExceptionHandling { showExitWindowingTooltip() } }, ) Loading Loading @@ -216,7 +224,9 @@ class AppHandleEducationController( arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, onDismissAction = {}, onEducationClickAction = {}, onEducationClickAction = { openHandleMenuCallback(captionState.runningTaskInfo.taskId) }, ) windowingEducationViewController.showEducationTooltip( taskId = captionState.runningTaskInfo.taskId, Loading @@ -225,6 +235,20 @@ class AppHandleEducationController( } } /** * Setup callbacks for app handle education tooltips. * * @param openHandleMenuCallback callback invoked to open app handle menu or app chip menu. * @param toDesktopModeCallback callback invoked to move task into desktop mode. */ fun setAppHandleEducationTooltipCallbacks( openHandleMenuCallback: (taskId: Int) -> Unit, toDesktopModeCallback: (taskId: Int, DesktopModeTransitionSource) -> Unit ) { this.openHandleMenuCallback = openHandleMenuCallback this.toDesktopModeCallback = toDesktopModeCallback } private inline fun <T> Flow<T>.catchTimeoutAndLog(crossinline block: () -> Unit) = catch { exception -> if (exception is TimeoutCancellationException) block() else throw exception Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +29 −2 Original line number Diff line number Diff line Loading @@ -87,6 +87,7 @@ import android.window.WindowContainerTransaction; import android.window.flags.DesktopModeFlags; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.Cuj; Loading @@ -112,6 +113,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; Loading @@ -134,6 +136,8 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import kotlin.Pair; import kotlin.Unit; import kotlinx.coroutines.ExperimentalCoroutinesApi; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; Loading Loading @@ -167,6 +171,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final MultiInstanceHelper mMultiInstanceHelper; private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter; private final AppHandleEducationController mAppHandleEducationController; private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private boolean mTransitionDragActive; Loading Loading @@ -236,6 +241,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler) { this( Loading Loading @@ -265,6 +271,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new SparseArray<>(), interactionJankMonitor, desktopTasksLimiter, appHandleEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, new TaskPositionerFactory()); Loading Loading @@ -298,6 +305,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId, InteractionJankMonitor interactionJankMonitor, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, TaskPositionerFactory taskPositionerFactory) { Loading Loading @@ -329,6 +337,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { com.android.internal.R.string.config_systemUi); mInteractionJankMonitor = interactionJankMonitor; mDesktopTasksLimiter = desktopTasksLimiter; mAppHandleEducationController = appHandleEducationController; mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; mActivityOrientationChangeHandler = activityOrientationChangeHandler; mAssistContentRequester = assistContentRequester; Loading Loading @@ -362,6 +371,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { shellInit.addInitCallback(this::onInit, this); } @OptIn(markerClass = ExperimentalCoroutinesApi.class) private void onInit() { mShellController.addKeyguardChangeListener(mDesktopModeKeyguardChangeListener); mShellCommandHandler.addDumpCallback(this::dump, this); Loading @@ -378,6 +388,18 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } catch (RemoteException e) { Log.e(TAG, "Failed to register window manager callbacks", e); } if (DesktopModeStatus.canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { mAppHandleEducationController.setAppHandleEducationTooltipCallbacks( /* appHandleTooltipClickCallback= */(taskId) -> { openHandleMenu(taskId); return Unit.INSTANCE; }, /* onToDesktopClickCallback= */(taskId, desktopModeTransitionSource) -> { onToDesktop(taskId, desktopModeTransitionSource); return Unit.INSTANCE; }); } } @Override Loading Loading @@ -495,6 +517,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mWindowDecorByTaskId.remove(taskInfo.taskId); } private void openHandleMenu(int taskId) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) >= MANAGE_WINDOWS_MINIMUM_INSTANCES); } private void onMaximizeOrRestore(int taskId, String source) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { Loading Loading @@ -720,8 +748,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { moveTaskToFront(decoration.mTaskInfo); decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) >= MANAGE_WINDOWS_MINIMUM_INSTANCES); openHandleMenu(mTaskId); } } else if (id == R.id.maximize_window) { // TODO(b/346441962): move click detection logic into the decor's Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +47 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.wm.shell.desktopmode.education.AppHandleEducationController.C import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_TIMEOUT_MILLIS import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.util.createAppHandleState import com.android.wm.shell.util.createAppHeaderState import com.android.wm.shell.util.createWindowingEducationProto Loading @@ -53,6 +54,7 @@ import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify Loading Loading @@ -337,6 +339,51 @@ class AppHandleEducationControllerTest : ShellTestCase() { verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() = testScope.runTest { // App handle is visible. Should show education tooltip. setShouldShowAppHandleEducation(true) val mockOpenHandleMenuCallback: (Int) -> Unit = mock() val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() educationController.setAppHandleEducationTooltipCallbacks( mockOpenHandleMenuCallback, mockToDesktopModeCallback) // Simulate app handle visible. testCaptionStateFlow.value = createAppHandleState() // Wait for first tooltip to showup. waitForBufferDelay() verify(mockTooltipController, atLeastOnce()) .showEducationTooltip(educationConfigCaptor.capture(), any()) educationConfigCaptor.lastValue.onEducationClickAction.invoke() verify(mockOpenHandleMenuCallback, times(1)).invoke(any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() = testScope.runTest { // After first tooltip is dismissed, app handle is expanded. Should show second education // tooltip. showAndDismissFirstTooltip() val mockOpenHandleMenuCallback: (Int) -> Unit = mock() val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() educationController.setAppHandleEducationTooltipCallbacks( mockOpenHandleMenuCallback, mockToDesktopModeCallback) // Simulate app handle expanded. testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) // Wait for next tooltip to showup. waitForBufferDelay() verify(mockTooltipController, atLeastOnce()) .showEducationTooltip(educationConfigCaptor.capture(), any()) educationConfigCaptor.lastValue.onEducationClickAction.invoke() verify(mockToDesktopModeCallback, times(1)).invoke(any(), any()) } private suspend fun TestScope.showAndDismissFirstTooltip() { setShouldShowAppHandleEducation(true) // Simulate app handle visible. Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +83 −0 Original line number Diff line number Diff line Loading @@ -64,6 +64,7 @@ import android.widget.Toast import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession Loading @@ -89,6 +90,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksLimiter import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.AppHandleEducationController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource Loading @@ -105,6 +107,7 @@ import java.util.function.Consumer import java.util.function.Supplier import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before Loading Loading @@ -136,6 +139,7 @@ import org.mockito.quality.Strictness * Tests of [DesktopModeWindowDecorViewModel] * Usage: atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests */ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidTestingRunner::class) @RunWithLooper Loading Loading @@ -184,6 +188,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockTaskPositionerFactory: DesktopModeWindowDecorViewModel.TaskPositionerFactory @Mock private lateinit var mockTaskPositioner: TaskPositioner @Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository private lateinit var spyContext: TestableContext Loading Loading @@ -242,6 +247,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { windowDecorByTaskIdSpy, mockInteractionJankMonitor, Optional.of(mockTasksLimiter), mockAppHandleEducationController, mockCaptionHandleRepository, Optional.of(mockActivityOrientationChangeHandler), mockTaskPositionerFactory Loading Loading @@ -957,6 +963,83 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { }, eq(mockUserHandle)) } @OptIn(ExperimentalCoroutinesApi::class) @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun testDecor_createWindowDecoration_setsAppHandleEducationTooltipClickCallbacks() { whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) shellInit.init() verify( mockAppHandleEducationController, times(1) ).setAppHandleEducationTooltipCallbacks(any(), any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun testDecor_invokeOpenHandleMenuCallback_openHandleMenu() { whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) val decor = setUpMockDecorationForTask(task) val openHandleMenuCallbackCaptor = argumentCaptor<(Int) -> Unit>() // Set task as gmail val gmailPackageName = "com.google.android.gm" val baseComponent = ComponentName(gmailPackageName, /* class */ "") task.baseActivity = baseComponent onTaskOpening(task) verify( mockAppHandleEducationController, times(1) ).setAppHandleEducationTooltipCallbacks(openHandleMenuCallbackCaptor.capture(), any()) openHandleMenuCallbackCaptor.lastValue.invoke(task.taskId) verify(decor, times(1)).createHandleMenu(anyBoolean()) } @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun testDecor_openTaskWithFlagDisabled_doNotOpenHandleMenu() { whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) setUpMockDecorationForTask(task) val openHandleMenuCallbackCaptor = argumentCaptor<(Int) -> Unit>() // Set task as gmail val gmailPackageName = "com.google.android.gm" val baseComponent = ComponentName(gmailPackageName, /* class */ "") task.baseActivity = baseComponent onTaskOpening(task) verify( mockAppHandleEducationController, never() ).setAppHandleEducationTooltipCallbacks(openHandleMenuCallbackCaptor.capture(), any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun testDecor_invokeOnToDesktopCallback_setsAppHandleEducationTooltipClickCallbacks() { whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) setUpMockDecorationsForTasks(task) onTaskOpening(task) val onToDesktopCallbackCaptor = argumentCaptor<(Int, DesktopModeTransitionSource) -> Unit>() verify( mockAppHandleEducationController, times(1) ).setAppHandleEducationTooltipCallbacks(any(), onToDesktopCallbackCaptor.capture()) onToDesktopCallbackCaptor.lastValue.invoke( task.taskId, DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON ) verify(mockDesktopTasksController, times(1)) .moveTaskToDesktop(any(), any(), any()) } @Test fun testOnDisplayRotation_tasksOutOfValidArea_taskBoundsUpdated() { val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +3 −2 Original line number Diff line number Diff line Loading @@ -252,6 +252,7 @@ public abstract class WMShellModule { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler) { if (DesktopModeStatus.canEnterDesktopMode(context)) { Loading @@ -277,6 +278,7 @@ public abstract class WMShellModule { assistContentRequester, multiInstanceHelper, desktopTasksLimiter, appHandleEducationController, windowDecorCaptionHandleRepository, desktopActivityOrientationHandler); } Loading Loading @@ -869,8 +871,7 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional, AppHandleEducationController appHandleEducationController Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional ) { return new Object(); } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +25 −1 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatasto import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.EducationViewConfig import kotlin.time.Duration.Companion.milliseconds Loading Loading @@ -68,6 +69,9 @@ class AppHandleEducationController( @ShellMainThread private val applicationCoroutineScope: CoroutineScope, @ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher, ) { private lateinit var openHandleMenuCallback: (Int) -> Unit private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit init { runIfEducationFeatureEnabled { applicationCoroutineScope.launch { Loading Loading @@ -114,6 +118,7 @@ class AppHandleEducationController( arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP, onEducationClickAction = { launchWithExceptionHandling { showWindowingImageButtonTooltip() } openHandleMenuCallback(captionState.runningTaskInfo.taskId) }, onDismissAction = { launchWithExceptionHandling { showWindowingImageButtonTooltip() } }, ) Loading Loading @@ -171,6 +176,9 @@ class AppHandleEducationController( DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, onEducationClickAction = { launchWithExceptionHandling { showExitWindowingTooltip() } toDesktopModeCallback( captionState.runningTaskInfo.taskId, DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON) }, onDismissAction = { launchWithExceptionHandling { showExitWindowingTooltip() } }, ) Loading Loading @@ -216,7 +224,9 @@ class AppHandleEducationController( arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, onDismissAction = {}, onEducationClickAction = {}, onEducationClickAction = { openHandleMenuCallback(captionState.runningTaskInfo.taskId) }, ) windowingEducationViewController.showEducationTooltip( taskId = captionState.runningTaskInfo.taskId, Loading @@ -225,6 +235,20 @@ class AppHandleEducationController( } } /** * Setup callbacks for app handle education tooltips. * * @param openHandleMenuCallback callback invoked to open app handle menu or app chip menu. * @param toDesktopModeCallback callback invoked to move task into desktop mode. */ fun setAppHandleEducationTooltipCallbacks( openHandleMenuCallback: (taskId: Int) -> Unit, toDesktopModeCallback: (taskId: Int, DesktopModeTransitionSource) -> Unit ) { this.openHandleMenuCallback = openHandleMenuCallback this.toDesktopModeCallback = toDesktopModeCallback } private inline fun <T> Flow<T>.catchTimeoutAndLog(crossinline block: () -> Unit) = catch { exception -> if (exception is TimeoutCancellationException) block() else throw exception Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +29 −2 Original line number Diff line number Diff line Loading @@ -87,6 +87,7 @@ import android.window.WindowContainerTransaction; import android.window.flags.DesktopModeFlags; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.Cuj; Loading @@ -112,6 +113,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; Loading @@ -134,6 +136,8 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import kotlin.Pair; import kotlin.Unit; import kotlinx.coroutines.ExperimentalCoroutinesApi; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; Loading Loading @@ -167,6 +171,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final MultiInstanceHelper mMultiInstanceHelper; private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter; private final AppHandleEducationController mAppHandleEducationController; private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private boolean mTransitionDragActive; Loading Loading @@ -236,6 +241,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler) { this( Loading Loading @@ -265,6 +271,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new SparseArray<>(), interactionJankMonitor, desktopTasksLimiter, appHandleEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, new TaskPositionerFactory()); Loading Loading @@ -298,6 +305,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId, InteractionJankMonitor interactionJankMonitor, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, TaskPositionerFactory taskPositionerFactory) { Loading Loading @@ -329,6 +337,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { com.android.internal.R.string.config_systemUi); mInteractionJankMonitor = interactionJankMonitor; mDesktopTasksLimiter = desktopTasksLimiter; mAppHandleEducationController = appHandleEducationController; mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; mActivityOrientationChangeHandler = activityOrientationChangeHandler; mAssistContentRequester = assistContentRequester; Loading Loading @@ -362,6 +371,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { shellInit.addInitCallback(this::onInit, this); } @OptIn(markerClass = ExperimentalCoroutinesApi.class) private void onInit() { mShellController.addKeyguardChangeListener(mDesktopModeKeyguardChangeListener); mShellCommandHandler.addDumpCallback(this::dump, this); Loading @@ -378,6 +388,18 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } catch (RemoteException e) { Log.e(TAG, "Failed to register window manager callbacks", e); } if (DesktopModeStatus.canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { mAppHandleEducationController.setAppHandleEducationTooltipCallbacks( /* appHandleTooltipClickCallback= */(taskId) -> { openHandleMenu(taskId); return Unit.INSTANCE; }, /* onToDesktopClickCallback= */(taskId, desktopModeTransitionSource) -> { onToDesktop(taskId, desktopModeTransitionSource); return Unit.INSTANCE; }); } } @Override Loading Loading @@ -495,6 +517,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mWindowDecorByTaskId.remove(taskInfo.taskId); } private void openHandleMenu(int taskId) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) >= MANAGE_WINDOWS_MINIMUM_INSTANCES); } private void onMaximizeOrRestore(int taskId, String source) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { Loading Loading @@ -720,8 +748,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { moveTaskToFront(decoration.mTaskInfo); decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) >= MANAGE_WINDOWS_MINIMUM_INSTANCES); openHandleMenu(mTaskId); } } else if (id == R.id.maximize_window) { // TODO(b/346441962): move click detection logic into the decor's Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +47 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.wm.shell.desktopmode.education.AppHandleEducationController.C import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_TIMEOUT_MILLIS import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.util.createAppHandleState import com.android.wm.shell.util.createAppHeaderState import com.android.wm.shell.util.createWindowingEducationProto Loading @@ -53,6 +54,7 @@ import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify Loading Loading @@ -337,6 +339,51 @@ class AppHandleEducationControllerTest : ShellTestCase() { verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() = testScope.runTest { // App handle is visible. Should show education tooltip. setShouldShowAppHandleEducation(true) val mockOpenHandleMenuCallback: (Int) -> Unit = mock() val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() educationController.setAppHandleEducationTooltipCallbacks( mockOpenHandleMenuCallback, mockToDesktopModeCallback) // Simulate app handle visible. testCaptionStateFlow.value = createAppHandleState() // Wait for first tooltip to showup. waitForBufferDelay() verify(mockTooltipController, atLeastOnce()) .showEducationTooltip(educationConfigCaptor.capture(), any()) educationConfigCaptor.lastValue.onEducationClickAction.invoke() verify(mockOpenHandleMenuCallback, times(1)).invoke(any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() = testScope.runTest { // After first tooltip is dismissed, app handle is expanded. Should show second education // tooltip. showAndDismissFirstTooltip() val mockOpenHandleMenuCallback: (Int) -> Unit = mock() val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() educationController.setAppHandleEducationTooltipCallbacks( mockOpenHandleMenuCallback, mockToDesktopModeCallback) // Simulate app handle expanded. testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) // Wait for next tooltip to showup. waitForBufferDelay() verify(mockTooltipController, atLeastOnce()) .showEducationTooltip(educationConfigCaptor.capture(), any()) educationConfigCaptor.lastValue.onEducationClickAction.invoke() verify(mockToDesktopModeCallback, times(1)).invoke(any(), any()) } private suspend fun TestScope.showAndDismissFirstTooltip() { setShouldShowAppHandleEducation(true) // Simulate app handle visible. Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +83 −0 Original line number Diff line number Diff line Loading @@ -64,6 +64,7 @@ import android.widget.Toast import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession Loading @@ -89,6 +90,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksLimiter import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.AppHandleEducationController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource Loading @@ -105,6 +107,7 @@ import java.util.function.Consumer import java.util.function.Supplier import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before Loading Loading @@ -136,6 +139,7 @@ import org.mockito.quality.Strictness * Tests of [DesktopModeWindowDecorViewModel] * Usage: atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests */ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidTestingRunner::class) @RunWithLooper Loading Loading @@ -184,6 +188,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockTaskPositionerFactory: DesktopModeWindowDecorViewModel.TaskPositionerFactory @Mock private lateinit var mockTaskPositioner: TaskPositioner @Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository private lateinit var spyContext: TestableContext Loading Loading @@ -242,6 +247,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { windowDecorByTaskIdSpy, mockInteractionJankMonitor, Optional.of(mockTasksLimiter), mockAppHandleEducationController, mockCaptionHandleRepository, Optional.of(mockActivityOrientationChangeHandler), mockTaskPositionerFactory Loading Loading @@ -957,6 +963,83 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { }, eq(mockUserHandle)) } @OptIn(ExperimentalCoroutinesApi::class) @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun testDecor_createWindowDecoration_setsAppHandleEducationTooltipClickCallbacks() { whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) shellInit.init() verify( mockAppHandleEducationController, times(1) ).setAppHandleEducationTooltipCallbacks(any(), any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun testDecor_invokeOpenHandleMenuCallback_openHandleMenu() { whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) val decor = setUpMockDecorationForTask(task) val openHandleMenuCallbackCaptor = argumentCaptor<(Int) -> Unit>() // Set task as gmail val gmailPackageName = "com.google.android.gm" val baseComponent = ComponentName(gmailPackageName, /* class */ "") task.baseActivity = baseComponent onTaskOpening(task) verify( mockAppHandleEducationController, times(1) ).setAppHandleEducationTooltipCallbacks(openHandleMenuCallbackCaptor.capture(), any()) openHandleMenuCallbackCaptor.lastValue.invoke(task.taskId) verify(decor, times(1)).createHandleMenu(anyBoolean()) } @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun testDecor_openTaskWithFlagDisabled_doNotOpenHandleMenu() { whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) setUpMockDecorationForTask(task) val openHandleMenuCallbackCaptor = argumentCaptor<(Int) -> Unit>() // Set task as gmail val gmailPackageName = "com.google.android.gm" val baseComponent = ComponentName(gmailPackageName, /* class */ "") task.baseActivity = baseComponent onTaskOpening(task) verify( mockAppHandleEducationController, never() ).setAppHandleEducationTooltipCallbacks(openHandleMenuCallbackCaptor.capture(), any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun testDecor_invokeOnToDesktopCallback_setsAppHandleEducationTooltipClickCallbacks() { whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) setUpMockDecorationsForTasks(task) onTaskOpening(task) val onToDesktopCallbackCaptor = argumentCaptor<(Int, DesktopModeTransitionSource) -> Unit>() verify( mockAppHandleEducationController, times(1) ).setAppHandleEducationTooltipCallbacks(any(), onToDesktopCallbackCaptor.capture()) onToDesktopCallbackCaptor.lastValue.invoke( task.taskId, DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON ) verify(mockDesktopTasksController, times(1)) .moveTaskToDesktop(any(), any(), any()) } @Test fun testOnDisplayRotation_tasksOutOfValidArea_taskBoundsUpdated() { val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) Loading