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

Commit 2112cf82 authored by Shivangi Dubey's avatar Shivangi Dubey
Browse files

Setup onclick listeners on the tooltip to match what we designed

When any of the tooltips are clicked/touched then perform action similar to when the element that is pointing it towards is clicked.
That means:
1. for the first tooltip that points towards the app handle: when this tooltip is clicked we should open the app handle menu
2. for second tooltip that points to the windowing image button: when this tooltip is clicked we should move the device to desktop mode
3. for third tooltip that points to the app chip: when this tooltip is clicked we should open the app chip menu

Fixes: 365055493
Test: atest DesktopModeWindowDecorViewModelTests
Test: atest AppHandleEducationControllerTest
Flag: com.android.window.flags.enable_desktop_windowing_app_handle_education
Change-Id: Ida8e015a66daa2909625454958489fb9ec820576
parent 087868c7
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -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)) {
@@ -277,6 +278,7 @@ public abstract class WMShellModule {
                    assistContentRequester,
                    multiInstanceHelper,
                    desktopTasksLimiter,
                    appHandleEducationController,
                    windowDecorCaptionHandleRepository,
                    desktopActivityOrientationHandler);
        }
@@ -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();
    }
+25 −1
Original line number Diff line number Diff line
@@ -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
@@ -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 {
@@ -114,6 +118,7 @@ class AppHandleEducationController(
            arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP,
            onEducationClickAction = {
              launchWithExceptionHandling { showWindowingImageButtonTooltip() }
              openHandleMenuCallback(captionState.runningTaskInfo.taskId)
            },
            onDismissAction = { launchWithExceptionHandling { showWindowingImageButtonTooltip() } },
        )
@@ -171,6 +176,9 @@ class AppHandleEducationController(
                      DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
                  onEducationClickAction = {
                    launchWithExceptionHandling { showExitWindowingTooltip() }
                    toDesktopModeCallback(
                        captionState.runningTaskInfo.taskId,
                        DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON)
                  },
                  onDismissAction = { launchWithExceptionHandling { showExitWindowingTooltip() } },
              )
@@ -216,7 +224,9 @@ class AppHandleEducationController(
                  arrowDirection =
                      DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
                  onDismissAction = {},
                  onEducationClickAction = {},
                  onEducationClickAction = {
                    openHandleMenuCallback(captionState.runningTaskInfo.taskId)
                  },
              )
          windowingEducationViewController.showEducationTooltip(
              taskId = captionState.runningTaskInfo.taskId,
@@ -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
+29 −2
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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;

@@ -236,6 +241,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
            AssistContentRequester assistContentRequester,
            MultiInstanceHelper multiInstanceHelper,
            Optional<DesktopTasksLimiter> desktopTasksLimiter,
            AppHandleEducationController appHandleEducationController,
            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
            Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler) {
        this(
@@ -265,6 +271,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
                new SparseArray<>(),
                interactionJankMonitor,
                desktopTasksLimiter,
                appHandleEducationController,
                windowDecorCaptionHandleRepository,
                activityOrientationChangeHandler,
                new TaskPositionerFactory());
@@ -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) {
@@ -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;
@@ -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);
@@ -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
@@ -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) {
@@ -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
+47 −0
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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.
+83 −0
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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

@@ -242,6 +247,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
                windowDecorByTaskIdSpy,
                mockInteractionJankMonitor,
                Optional.of(mockTasksLimiter),
                mockAppHandleEducationController,
                mockCaptionHandleRepository,
                Optional.of(mockActivityOrientationChangeHandler),
                mockTaskPositionerFactory
@@ -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)