summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Vania Desmonda <vaniadesmonda@google.com> 2025-01-23 05:47:11 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-01-23 05:47:11 -0800
commitaf72c8c5e94423a84d715786e4496754bf19e3f7 (patch)
tree109b27a104dcf14ed6ede2311acc1270f14b6811
parent7844c82ddeda9ecf6f69d4edb62053825ece712c (diff)
parent96a9c32fbd958d9260daf64021eb6549243fa536 (diff)
Merge "Enable 3 desktop mode education hints to launch independently." into main
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt349
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt342
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt104
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt10
9 files changed, 348 insertions, 550 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
index 5757c6afd196..b614b3f4d025 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
@@ -22,7 +22,6 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Point
import android.os.SystemProperties
-import android.util.Slog
import com.android.window.flags.Flags
import com.android.wm.shell.R
import com.android.wm.shell.desktopmode.CaptionState
@@ -32,27 +31,17 @@ 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.common.DecorThemeUtil
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipColorScheme
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipEducationViewConfig
-import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainCoroutineDispatcher
-import kotlinx.coroutines.TimeoutCancellationException
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
/**
@@ -72,59 +61,75 @@ class AppHandleEducationController(
@ShellMainThread private val applicationCoroutineScope: CoroutineScope,
@ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher,
) {
- private val decorThemeUtil = DecorThemeUtil(context)
private lateinit var openHandleMenuCallback: (Int) -> Unit
private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit
+ private val onTertiaryFixedColor =
+ context.getColor(com.android.internal.R.color.materialColorOnTertiaryFixed)
+ private val tertiaryFixedColor =
+ context.getColor(com.android.internal.R.color.materialColorTertiaryFixed)
init {
runIfEducationFeatureEnabled {
+ // Coroutine block for the first hint that appears on a full-screen app's app handle to
+ // encourage users to open the app handle menu.
applicationCoroutineScope.launch {
- // Central block handling the app handle's educational flow end-to-end.
- isAppHandleHintViewedFlow()
- .flatMapLatest { isAppHandleHintViewed ->
- if (isAppHandleHintViewed) {
- // If the education is viewed then return emptyFlow() that completes
- // immediately.
- // This will help us to not listen to [captionHandleStateFlow] after the
- // education
- // has been viewed already.
- emptyFlow()
- } else {
- // Listen for changes to window decor's caption handle.
- windowDecorCaptionHandleRepository.captionStateFlow
- // Wait for few seconds before emitting the latest state.
- .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
- .filter { captionState ->
- captionState is CaptionState.AppHandle &&
- appHandleEducationFilter.shouldShowAppHandleEducation(
- captionState
- )
- }
- }
+ if (isAppHandleHintViewed()) return@launch
+ windowDecorCaptionHandleRepository.captionStateFlow
+ .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
+ .filter { captionState ->
+ captionState is CaptionState.AppHandle &&
+ !captionState.isHandleMenuExpanded &&
+ !isAppHandleHintViewed() &&
+ appHandleEducationFilter.shouldShowDesktopModeEducation(captionState)
}
+ .take(1)
.flowOn(backgroundDispatcher)
.collectLatest { captionState ->
- val tooltipColorScheme = tooltipColorScheme(captionState)
-
- showEducation(captionState, tooltipColorScheme)
- // After showing first tooltip, mark education as viewed
+ showEducation(captionState)
appHandleEducationDatastoreRepository
.updateAppHandleHintViewedTimestampMillis(true)
}
}
+ // Coroutine block for the hint that appears when an app handle is expanded to
+ // encourage users to enter desktop mode.
applicationCoroutineScope.launch {
- if (isAppHandleHintUsed()) return@launch
+ if (isEnterDesktopModeHintViewed()) return@launch
windowDecorCaptionHandleRepository.captionStateFlow
+ .debounce(ENTER_DESKTOP_MODE_EDUCATION_DELAY_MILLIS)
.filter { captionState ->
- captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded
+ captionState is CaptionState.AppHandle &&
+ captionState.isHandleMenuExpanded &&
+ !isEnterDesktopModeHintViewed() &&
+ appHandleEducationFilter.shouldShowDesktopModeEducation(captionState)
}
.take(1)
.flowOn(backgroundDispatcher)
- .collect {
- // If user expands app handle, mark user has used the app handle hint
+ .collectLatest { captionState ->
+ showWindowingImageButtonTooltip(captionState as CaptionState.AppHandle)
appHandleEducationDatastoreRepository
- .updateAppHandleHintUsedTimestampMillis(true)
+ .updateEnterDesktopModeHintViewedTimestampMillis(true)
+ }
+ }
+
+ // Coroutine block for the hint that appears on the window app header in freeform mode
+ // to let users know how to exit desktop mode.
+ applicationCoroutineScope.launch {
+ if (isExitDesktopModeHintViewed()) return@launch
+ windowDecorCaptionHandleRepository.captionStateFlow
+ .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
+ .filter { captionState ->
+ captionState is CaptionState.AppHeader &&
+ !captionState.isHeaderMenuExpanded &&
+ !isExitDesktopModeHintViewed() &&
+ appHandleEducationFilter.shouldShowDesktopModeEducation(captionState)
+ }
+ .take(1)
+ .flowOn(backgroundDispatcher)
+ .collectLatest { captionState ->
+ showExitWindowingTooltip(captionState as CaptionState.AppHeader)
+ appHandleEducationDatastoreRepository
+ .updateExitDesktopModeHintViewedTimestampMillis(true)
}
}
}
@@ -135,7 +140,7 @@ class AppHandleEducationController(
block()
}
- private fun showEducation(captionState: CaptionState, tooltipColorScheme: TooltipColorScheme) {
+ private fun showEducation(captionState: CaptionState) {
val appHandleBounds = (captionState as CaptionState.AppHandle).globalAppHandleBounds
val tooltipGlobalCoordinates =
Point(appHandleBounds.left + appHandleBounds.width() / 2, appHandleBounds.bottom)
@@ -145,21 +150,21 @@ class AppHandleEducationController(
val appHandleTooltipConfig =
TooltipEducationViewConfig(
tooltipViewLayout = R.layout.desktop_windowing_education_top_arrow_tooltip,
- tooltipColorScheme = tooltipColorScheme,
+ tooltipColorScheme =
+ TooltipColorScheme(
+ tertiaryFixedColor,
+ onTertiaryFixedColor,
+ onTertiaryFixedColor,
+ ),
tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
tooltipText = getString(R.string.windowing_app_handle_education_tooltip),
arrowDirection =
DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP,
onEducationClickAction = {
- launchWithExceptionHandling {
- showWindowingImageButtonTooltip(tooltipColorScheme)
- }
openHandleMenuCallback(captionState.runningTaskInfo.taskId)
},
onDismissAction = {
- launchWithExceptionHandling {
- showWindowingImageButtonTooltip(tooltipColorScheme)
- }
+ // TODO: b/341320146 - Log previous tooltip was dismissed
},
)
@@ -170,7 +175,7 @@ class AppHandleEducationController(
}
/** Show tooltip that points to windowing image button in app handle menu */
- private suspend fun showWindowingImageButtonTooltip(tooltipColorScheme: TooltipColorScheme) {
+ private suspend fun showWindowingImageButtonTooltip(captionState: CaptionState.AppHandle) {
val appInfoPillHeight = getSize(R.dimen.desktop_mode_handle_menu_app_info_pill_height)
val windowingOptionPillHeight =
getSize(R.dimen.desktop_mode_handle_menu_windowing_pill_height)
@@ -181,128 +186,81 @@ class AppHandleEducationController(
getSize(R.dimen.desktop_mode_handle_menu_margin_top) +
getSize(R.dimen.desktop_mode_handle_menu_pill_spacing_margin)
- windowDecorCaptionHandleRepository.captionStateFlow
- // After the first tooltip was dismissed, wait for 400 ms and see if the app handle menu
- // has been expanded.
- .timeout(APP_HANDLE_EDUCATION_TIMEOUT_MILLIS.milliseconds)
- .catchTimeoutAndLog {
- // TODO: b/341320146 - Log previous tooltip was dismissed
- }
- // Wait for few milliseconds before emitting the latest state.
- .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
- .filter { captionState ->
- // Filter out states when app handle is not visible or not expanded.
- captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded
- }
- // Before showing this tooltip, stop listening to further emissions to avoid
- // accidentally
- // showing the same tooltip on future emissions.
- .take(1)
- .flowOn(backgroundDispatcher)
- .collectLatest { captionState ->
- captionState as CaptionState.AppHandle
- val appHandleBounds = captionState.globalAppHandleBounds
- val tooltipGlobalCoordinates =
- Point(
- appHandleBounds.left + appHandleBounds.width() / 2 + appHandleMenuWidth / 2,
- appHandleBounds.top +
- appHandleMenuMargins +
- appInfoPillHeight +
- windowingOptionPillHeight / 2,
- )
- // Populate information important to inflate windowing image button education
- // tooltip.
- val windowingImageButtonTooltipConfig =
- TooltipEducationViewConfig(
- tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
- tooltipColorScheme = tooltipColorScheme,
- tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
- tooltipText =
- getString(
- R.string.windowing_desktop_mode_image_button_education_tooltip
- ),
- arrowDirection =
- DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
- onEducationClickAction = {
- launchWithExceptionHandling {
- showExitWindowingTooltip(tooltipColorScheme)
- }
- toDesktopModeCallback(
- captionState.runningTaskInfo.taskId,
- DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON,
- )
- },
- onDismissAction = {
- launchWithExceptionHandling {
- showExitWindowingTooltip(tooltipColorScheme)
- }
- },
+ val appHandleBounds = captionState.globalAppHandleBounds
+ val tooltipGlobalCoordinates =
+ Point(
+ appHandleBounds.left + appHandleBounds.width() / 2 + appHandleMenuWidth / 2,
+ appHandleBounds.top +
+ appHandleMenuMargins +
+ appInfoPillHeight +
+ windowingOptionPillHeight / 2,
+ )
+ // Populate information important to inflate windowing image button education
+ // tooltip.
+ val windowingImageButtonTooltipConfig =
+ TooltipEducationViewConfig(
+ tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
+ tooltipColorScheme =
+ TooltipColorScheme(
+ tertiaryFixedColor,
+ onTertiaryFixedColor,
+ onTertiaryFixedColor,
+ ),
+ tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
+ tooltipText =
+ getString(R.string.windowing_desktop_mode_image_button_education_tooltip),
+ arrowDirection =
+ DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
+ onEducationClickAction = {
+ toDesktopModeCallback(
+ captionState.runningTaskInfo.taskId,
+ DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON,
)
+ },
+ onDismissAction = {
+ // TODO: b/341320146 - Log previous tooltip was dismissed
+ },
+ )
- windowingEducationViewController.showEducationTooltip(
- taskId = captionState.runningTaskInfo.taskId,
- tooltipViewConfig = windowingImageButtonTooltipConfig,
- )
- }
+ windowingEducationViewController.showEducationTooltip(
+ taskId = captionState.runningTaskInfo.taskId,
+ tooltipViewConfig = windowingImageButtonTooltipConfig,
+ )
}
/** Show tooltip that points to app chip button and educates user on how to exit desktop mode */
- private suspend fun showExitWindowingTooltip(tooltipColorScheme: TooltipColorScheme) {
- windowDecorCaptionHandleRepository.captionStateFlow
- // After the previous tooltip was dismissed, wait for 400 ms and see if the user entered
- // desktop mode.
- .timeout(APP_HANDLE_EDUCATION_TIMEOUT_MILLIS.milliseconds)
- .catchTimeoutAndLog {
- // TODO: b/341320146 - Log previous tooltip was dismissed
- }
- // Wait for few milliseconds before emitting the latest state.
- .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
- .filter { captionState ->
- // Filter out states when app header is not visible or expanded.
- captionState is CaptionState.AppHeader && !captionState.isHeaderMenuExpanded
- }
- // Before showing this tooltip, stop listening to further emissions to avoid
- // accidentally
- // showing the same tooltip on future emissions.
- .take(1)
- .flowOn(backgroundDispatcher)
- .collectLatest { captionState ->
- captionState as CaptionState.AppHeader
- val globalAppChipBounds = captionState.globalAppChipBounds
- val tooltipGlobalCoordinates =
- Point(
- globalAppChipBounds.right,
- globalAppChipBounds.top + globalAppChipBounds.height() / 2,
- )
- // Populate information important to inflate exit desktop mode education tooltip.
- val exitWindowingTooltipConfig =
- TooltipEducationViewConfig(
- tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
- tooltipColorScheme = tooltipColorScheme,
- tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
- tooltipText =
- getString(R.string.windowing_desktop_mode_exit_education_tooltip),
- arrowDirection =
- DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
- onDismissAction = {},
- onEducationClickAction = {
- openHandleMenuCallback(captionState.runningTaskInfo.taskId)
- },
- )
- windowingEducationViewController.showEducationTooltip(
- taskId = captionState.runningTaskInfo.taskId,
- tooltipViewConfig = exitWindowingTooltipConfig,
- )
- }
- }
-
- private fun tooltipColorScheme(captionState: CaptionState): TooltipColorScheme {
- val onTertiaryFixed =
- context.getColor(com.android.internal.R.color.materialColorOnTertiaryFixed)
- val tertiaryFixed =
- context.getColor(com.android.internal.R.color.materialColorTertiaryFixed)
-
- return TooltipColorScheme(tertiaryFixed, onTertiaryFixed, onTertiaryFixed)
+ private suspend fun showExitWindowingTooltip(captionState: CaptionState.AppHeader) {
+ val globalAppChipBounds = captionState.globalAppChipBounds
+ val tooltipGlobalCoordinates =
+ Point(
+ globalAppChipBounds.right,
+ globalAppChipBounds.top + globalAppChipBounds.height() / 2,
+ )
+ // Populate information important to inflate exit desktop mode education tooltip.
+ val exitWindowingTooltipConfig =
+ TooltipEducationViewConfig(
+ tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
+ tooltipColorScheme =
+ TooltipColorScheme(
+ tertiaryFixedColor,
+ onTertiaryFixedColor,
+ onTertiaryFixedColor,
+ ),
+ tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
+ tooltipText = getString(R.string.windowing_desktop_mode_exit_education_tooltip),
+ arrowDirection =
+ DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
+ onDismissAction = {
+ // TODO: b/341320146 - Log previous tooltip was dismissed
+ },
+ onEducationClickAction = {
+ openHandleMenuCallback(captionState.runningTaskInfo.taskId)
+ },
+ )
+ windowingEducationViewController.showEducationTooltip(
+ taskId = captionState.runningTaskInfo.taskId,
+ tooltipViewConfig = exitWindowingTooltipConfig,
+ )
}
/**
@@ -319,43 +277,20 @@ class AppHandleEducationController(
this.toDesktopModeCallback = toDesktopModeCallback
}
- private inline fun <T> Flow<T>.catchTimeoutAndLog(crossinline block: () -> Unit) =
- catch { exception ->
- if (exception is TimeoutCancellationException) block() else throw exception
- }
-
- private fun launchWithExceptionHandling(block: suspend () -> Unit) =
- applicationCoroutineScope.launch {
- try {
- block()
- } catch (e: Throwable) {
- Slog.e(TAG, "Error: ", e)
- }
- }
+ private suspend fun isAppHandleHintViewed(): Boolean =
+ appHandleEducationDatastoreRepository.dataStoreFlow
+ .first()
+ .hasAppHandleHintViewedTimestampMillis() && !FORCE_SHOW_DESKTOP_MODE_EDUCATION
- /**
- * Listens to the changes to [WindowingEducationProto#hasAppHandleHintViewedTimestampMillis()]
- * in datastore proto object.
- *
- * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this flow will always emit false. That
- * means it will always emit app handle hint has not been viewed yet.
- */
- private fun isAppHandleHintViewedFlow(): Flow<Boolean> =
+ private suspend fun isEnterDesktopModeHintViewed(): Boolean =
appHandleEducationDatastoreRepository.dataStoreFlow
- .map { preferences ->
- preferences.hasAppHandleHintViewedTimestampMillis() &&
- !SHOULD_OVERRIDE_EDUCATION_CONDITIONS
- }
- .distinctUntilChanged()
+ .first()
+ .hasEnterDesktopModeHintViewedTimestampMillis() && !FORCE_SHOW_DESKTOP_MODE_EDUCATION
- /**
- * Listens to the changes to [WindowingEducationProto#hasAppHandleHintUsedTimestampMillis()] in
- * datastore proto object.
- */
- private suspend fun isAppHandleHintUsed(): Boolean =
+ private suspend fun isExitDesktopModeHintViewed(): Boolean =
appHandleEducationDatastoreRepository.dataStoreFlow
.first()
- .hasAppHandleHintUsedTimestampMillis()
+ .hasExitDesktopModeHintViewedTimestampMillis() && !FORCE_SHOW_DESKTOP_MODE_EDUCATION
private fun getSize(@DimenRes resourceId: Int): Int {
if (resourceId == Resources.ID_NULL) return 0
@@ -369,13 +304,17 @@ class AppHandleEducationController(
val APP_HANDLE_EDUCATION_DELAY_MILLIS: Long
get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L)
- val APP_HANDLE_EDUCATION_TIMEOUT_MILLIS: Long
- get() = SystemProperties.getLong("persist.windowing_app_handle_education_timeout", 400L)
+ val ENTER_DESKTOP_MODE_EDUCATION_DELAY_MILLIS: Long
+ get() =
+ SystemProperties.getLong(
+ "persist.windowing_enter_desktop_mode_education_timeout",
+ 400L,
+ )
- val SHOULD_OVERRIDE_EDUCATION_CONDITIONS: Boolean
+ val FORCE_SHOW_DESKTOP_MODE_EDUCATION: Boolean
get() =
SystemProperties.getBoolean(
- "persist.desktop_windowing_app_handle_education_override_conditions",
+ "persist.windowing_force_show_desktop_mode_education",
false,
)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt
index 9990846fc92e..4d219b5544aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt
@@ -17,13 +17,14 @@
package com.android.wm.shell.desktopmode.education
import android.annotation.IntegerRes
+import android.app.ActivityManager.RunningTaskInfo
import android.app.usage.UsageStatsManager
import android.content.Context
import android.os.SystemClock
import android.provider.Settings.Secure
import com.android.wm.shell.R
import com.android.wm.shell.desktopmode.CaptionState
-import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.SHOULD_OVERRIDE_EDUCATION_CONDITIONS
+import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.FORCE_SHOW_DESKTOP_MODE_EDUCATION
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository
import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto
import java.time.Duration
@@ -37,26 +38,28 @@ class AppHandleEducationFilter(
private val usageStatsManager =
context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
+ suspend fun shouldShowDesktopModeEducation(captionState: CaptionState.AppHeader): Boolean =
+ shouldShowDesktopModeEducation(captionState.runningTaskInfo)
+
+ suspend fun shouldShowDesktopModeEducation(captionState: CaptionState.AppHandle): Boolean =
+ shouldShowDesktopModeEducation(captionState.runningTaskInfo)
+
/**
- * Returns true if conditions to show app handle education are met, returns false otherwise.
+ * Returns true if conditions to show app handle, enter desktop mode and exit desktop mode
+ * education are met based on the app info and usage, returns false otherwise.
*
- * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this method will always return
- * ![captionState.isHandleMenuExpanded].
+ * If [FORCE_SHOW_DESKTOP_MODE_EDUCATION] is true, this method will always return true.
*/
- suspend fun shouldShowAppHandleEducation(captionState: CaptionState): Boolean {
- if ((captionState as CaptionState.AppHandle).isHandleMenuExpanded) return false
- if (SHOULD_OVERRIDE_EDUCATION_CONDITIONS) return true
+ private suspend fun shouldShowDesktopModeEducation(taskInfo: RunningTaskInfo): Boolean {
+ if (FORCE_SHOW_DESKTOP_MODE_EDUCATION) return true
- val focusAppPackageName =
- captionState.runningTaskInfo.topActivityInfo?.packageName ?: return false
+ val focusAppPackageName = taskInfo.topActivityInfo?.packageName ?: return false
val windowingEducationProto =
appHandleEducationDatastoreRepository.windowingEducationProto()
return isFocusAppInAllowlist(focusAppPackageName) &&
!isOtherEducationShowing() &&
hasSufficientTimeSinceSetup() &&
- !isAppHandleHintViewedBefore(windowingEducationProto) &&
- !isAppHandleHintUsedBefore(windowingEducationProto) &&
hasMinAppUsage(windowingEducationProto, focusAppPackageName)
}
@@ -79,14 +82,6 @@ class AppHandleEducationFilter(
R.integer.desktop_windowing_education_required_time_since_setup_seconds
)
- private fun isAppHandleHintViewedBefore(
- windowingEducationProto: WindowingEducationProto
- ): Boolean = windowingEducationProto.hasAppHandleHintViewedTimestampMillis()
-
- private fun isAppHandleHintUsedBefore(
- windowingEducationProto: WindowingEducationProto
- ): Boolean = windowingEducationProto.hasAppHandleHintUsedTimestampMillis()
-
private suspend fun hasMinAppUsage(
windowingEducationProto: WindowingEducationProto,
focusAppPackageName: String,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
index 3e120b09a0b6..d061e03b9be5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
@@ -91,6 +91,40 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) {
}
/**
+ * Updates [WindowingEducationProto.enterDesktopModeHintViewedTimestampMillis_] field in
+ * datastore with current timestamp if [isViewed] is true, if not then clears the field.
+ */
+ suspend fun updateEnterDesktopModeHintViewedTimestampMillis(isViewed: Boolean) {
+ dataStore.updateData { preferences ->
+ if (isViewed) {
+ preferences
+ .toBuilder()
+ .setEnterDesktopModeHintViewedTimestampMillis(System.currentTimeMillis())
+ .build()
+ } else {
+ preferences.toBuilder().clearEnterDesktopModeHintViewedTimestampMillis().build()
+ }
+ }
+ }
+
+ /**
+ * Updates [WindowingEducationProto.exitDesktopModeHintViewedTimestampMillis_] field in
+ * datastore with current timestamp if [isViewed] is true, if not then clears the field.
+ */
+ suspend fun updateExitDesktopModeHintViewedTimestampMillis(isViewed: Boolean) {
+ dataStore.updateData { preferences ->
+ if (isViewed) {
+ preferences
+ .toBuilder()
+ .setExitDesktopModeHintViewedTimestampMillis(System.currentTimeMillis())
+ .build()
+ } else {
+ preferences.toBuilder().clearExitDesktopModeHintViewedTimestampMillis().build()
+ }
+ }
+ }
+
+ /**
* Updates [WindowingEducationProto.appHandleHintUsedTimestampMillis_] field in datastore with
* current timestamp if [isViewed] is true, if not then clears the field.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index b179741b1259..fe9bdb027d18 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -542,6 +542,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
if (appHeader != null) {
appHeader.setAppName(name);
appHeader.setAppIcon(icon);
+ if (canEnterDesktopMode(mContext) && isEducationEnabled()) {
+ notifyCaptionStateChanged();
+ }
}
});
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index dc4fa3788778..79c5eff01b4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -329,11 +329,6 @@ class AppHeaderViewHolder(
}
fun runOnAppChipGlobalLayout(runnable: () -> Unit) {
- if (openMenuButton.isAttachedToWindow) {
- // App chip is already inflated.
- runnable()
- return
- }
// Wait for app chip to be inflated before notifying repository.
openMenuButton.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
index 5475032f35a9..493a8c83c48e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
@@ -29,7 +29,6 @@ import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.desktopmode.CaptionState
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_DELAY_MILLIS
-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
@@ -47,7 +46,6 @@ import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -113,10 +111,10 @@ class AppHandleEducationControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun init_appHandleVisible_shouldCallShowEducationTooltip() =
+ fun init_appHandleVisible_shouldCallShowEducationTooltipAndMarkAsViewed() =
testScope.runTest {
// App handle is visible. Should show education tooltip.
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState()
@@ -124,6 +122,38 @@ class AppHandleEducationControllerTest : ShellTestCase() {
waitForBufferDelay()
verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+ verify(mockDataStoreRepository, times(1))
+ .updateAppHandleHintViewedTimestampMillis(eq(true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ fun init_appHandleVisibleAndMenuExpanded_shouldCallShowEducationTooltipAndMarkAsViewed() =
+ testScope.runTest {
+ setShouldShowDesktopModeEducation(true)
+
+ // Simulate app handle visible and handle menu is expanded.
+ testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+ waitForBufferDelay()
+
+ verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+ verify(mockDataStoreRepository, times(1))
+ .updateEnterDesktopModeHintViewedTimestampMillis(eq(true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ fun init_appHeaderVisible_shouldCallShowEducationTooltipAndMarkAsViewed() =
+ testScope.runTest {
+ setShouldShowDesktopModeEducation(true)
+
+ // Simulate app header visible.
+ testCaptionStateFlow.value = createAppHeaderState()
+ waitForBufferDelay()
+
+ verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+ verify(mockDataStoreRepository, times(1))
+ .updateExitDesktopModeHintViewedTimestampMillis(eq(true))
}
@Test
@@ -133,7 +163,7 @@ class AppHandleEducationControllerTest : ShellTestCase() {
// App handle visible but education aconfig flag disabled, should not show education
// tooltip.
whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false)
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState()
@@ -145,12 +175,11 @@ class AppHandleEducationControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun init_shouldShowAppHandleEducationReturnsFalse_shouldNotCallShowEducationTooltip() =
+ fun init_shouldShowDesktopModeEducationReturnsFalse_shouldNotCallShowEducationTooltip() =
testScope.runTest {
- // App handle is visible but [shouldShowAppHandleEducation] api returns false, should
- // not
- // show education tooltip.
- setShouldShowAppHandleEducation(false)
+ // App handle is visible but [shouldShowDesktopModeEducation] api returns false, should
+ // not show education tooltip.
+ setShouldShowDesktopModeEducation(false)
// Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState()
@@ -165,7 +194,7 @@ class AppHandleEducationControllerTest : ShellTestCase() {
fun init_appHandleNotVisible_shouldNotCallShowEducationTooltip() =
testScope.runTest {
// App handle is not visible, should not show education tooltip.
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle is not visible.
testCaptionStateFlow.value = CaptionState.NoCaption
@@ -184,7 +213,7 @@ class AppHandleEducationControllerTest : ShellTestCase() {
// Mark app handle hint viewed.
testDataStoreFlow.value =
createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
@@ -196,231 +225,95 @@ class AppHandleEducationControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() =
+ fun init_enterDesktopModeHintViewedAlready_shouldNotCallShowEducationTooltip() =
testScope.runTest {
- // App handle is visible but app handle hint has been viewed before.
- // But as we are overriding prerequisite conditions, we should show app
- // handle tooltip.
+ // App handle is visible but app handle hint has been viewed before,
+ // should not show education tooltip.
// Mark app handle hint viewed.
testDataStoreFlow.value =
- createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
- val systemPropertiesKey =
- "persist.desktop_windowing_app_handle_education_override_conditions"
- whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean()))
- .thenReturn(true)
- setShouldShowAppHandleEducation(true)
+ createWindowingEducationProto(enterDesktopModeHintViewedTimestampMillis = 123L)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
- // Wait for first tooltip to showup.
- waitForBufferDelay()
-
- verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun init_appHandleExpanded_shouldMarkAppHandleHintUsed() =
- testScope.runTest {
- setShouldShowAppHandleEducation(false)
-
- // Simulate app handle visible and expanded.
testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for some time before verifying
+ // Wait for first tooltip to showup.
waitForBufferDelay()
- verify(mockDataStoreRepository, times(1))
- .updateAppHandleHintUsedTimestampMillis(eq(true))
+ verify(mockTooltipController, never()).showEducationTooltip(any(), any())
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun init_showFirstTooltip_shouldMarkAppHandleHintViewed() =
+ fun init_exitDesktopModeHintViewedAlready_shouldNotCallShowEducationTooltip() =
testScope.runTest {
- // App handle is visible. Should show education tooltip.
- setShouldShowAppHandleEducation(true)
+ // App handle is visible but app handle hint has been viewed before,
+ // should not show education tooltip.
+ // Mark app handle hint viewed.
+ testDataStoreFlow.value =
+ createWindowingEducationProto(exitDesktopModeHintViewedTimestampMillis = 123L)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
- testCaptionStateFlow.value = createAppHandleState()
+ testCaptionStateFlow.value = createAppHeaderState()
// Wait for first tooltip to showup.
waitForBufferDelay()
- verify(mockDataStoreRepository, times(1))
- .updateAppHandleHintViewedTimestampMillis(eq(true))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() =
- testScope.runTest {
- // After first tooltip is dismissed, app handle is expanded. Should show second
- // education
- // tooltip.
- showAndDismissFirstTooltip()
-
- // Simulate app handle expanded.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
-
- // [showEducationTooltip] should be called twice, once for each tooltip.
- verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showWindowingImageButtonTooltip_appHandleExpandedAfterTimeout_shouldCallShowEducationTooltipOnce() =
- testScope.runTest {
- // After first tooltip is dismissed, app handle is expanded after timeout. Should not
- // show
- // second education tooltip.
- showAndDismissFirstTooltip()
-
- // Wait for timeout to occur, after this timeout we should not listen for further
- // triggers
- // anymore.
- advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS)
- runCurrent()
- // Simulate app handle expanded.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
-
- // [showEducationTooltip] should be called once, just for the first tooltip.
- verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showWindowingImageButtonTooltip_appHandleExpandedTwice_shouldCallShowEducationTooltipTwice() =
- testScope.runTest {
- // After first tooltip is dismissed, app handle is expanded twice. Should show second
- // education tooltip only once.
- showAndDismissFirstTooltip()
-
- // Simulate app handle expanded.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
- // Simulate app handle being expanded twice.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- waitForBufferDelay()
-
- // [showEducationTooltip] should not be called thrice, even if app handle was expanded
- // twice. Should be called twice, once for each tooltip.
- verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
+ verify(mockTooltipController, never()).showEducationTooltip(any(), any())
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showWindowingImageButtonTooltip_appHandleNotExpanded_shouldCallShowEducationTooltipOnce() =
+ fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() =
testScope.runTest {
- // After first tooltip is dismissed, app handle is not expanded. Should not show second
- // education tooltip.
- showAndDismissFirstTooltip()
+ // App handle is visible but app handle hint has been viewed before.
+ // But as we are overriding prerequisite conditions, we should show app
+ // handle tooltip.
+ // Mark app handle hint viewed.
+ testDataStoreFlow.value =
+ createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
+ val systemPropertiesKey = "persist.windowing_force_show_desktop_mode_education"
+ whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean()))
+ .thenReturn(true)
+ setShouldShowDesktopModeEducation(true)
- // Simulate app handle visible but not expanded.
+ // Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
- // Wait for next tooltip to showup.
+ // Wait for first tooltip to showup.
waitForBufferDelay()
- // [showEducationTooltip] should be called once, just for the first tooltip.
verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showExitWindowingButtonTooltip_appHeaderVisible_shouldCallShowEducationTooltipThrice() =
- testScope.runTest {
- // After first two tooltips are dismissed, app header is visible. Should show third
- // education tooltip.
- showAndDismissFirstTooltip()
- showAndDismissSecondTooltip()
-
- // Simulate app header visible.
- testCaptionStateFlow.value = createAppHeaderState()
- // Wait for next tooltip to showup.
- waitForBufferDelay()
-
- verify(mockTooltipController, times(3)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showExitWindowingButtonTooltip_appHeaderVisibleAfterTimeout_shouldCallShowEducationTooltipTwice() =
- testScope.runTest {
- // After first two tooltips are dismissed, app header is visible after timeout. Should
- // not
- // show third education tooltip.
- showAndDismissFirstTooltip()
- showAndDismissSecondTooltip()
-
- // Wait for timeout to occur, after this timeout we should not listen for further
- // triggers
- // anymore.
- advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS)
- runCurrent()
- // Simulate app header visible.
- testCaptionStateFlow.value = createAppHeaderState()
- // Wait for next tooltip to showup.
- waitForBufferDelay()
-
- verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showExitWindowingButtonTooltip_appHeaderVisibleTwice_shouldCallShowEducationTooltipThrice() =
+ fun clickAppHandleHint_openHandleMenuCallbackInvoked() =
testScope.runTest {
- // After first two tooltips are dismissed, app header is visible twice. Should show
- // third
- // education tooltip only once.
- showAndDismissFirstTooltip()
- showAndDismissSecondTooltip()
-
- // Simulate app header visible.
- testCaptionStateFlow.value = createAppHeaderState()
- // Wait for next tooltip to showup.
- waitForBufferDelay()
- testCaptionStateFlow.value = createAppHeaderState()
- // Wait for next tooltip to showup.
+ // App handle is visible. Should show education tooltip.
+ setShouldShowDesktopModeEducation(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, times(3)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showExitWindowingButtonTooltip_appHeaderExpanded_shouldCallShowEducationTooltipTwice() =
- testScope.runTest {
- // After first two tooltips are dismissed, app header is visible but expanded. Should
- // not
- // show third education tooltip.
- showAndDismissFirstTooltip()
- showAndDismissSecondTooltip()
-
- // Simulate app header visible.
- testCaptionStateFlow.value = createAppHeaderState(isHeaderMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
+ verify(mockTooltipController, atLeastOnce())
+ .showEducationTooltip(educationConfigCaptor.capture(), any())
+ educationConfigCaptor.lastValue.onEducationClickAction.invoke()
- verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
+ verify(mockOpenHandleMenuCallback, times(1)).invoke(any())
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() =
+ fun clickEnterDesktopModeHint_toDesktopModeCallbackInvoked() =
testScope.runTest {
// App handle is visible. Should show education tooltip.
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
val mockOpenHandleMenuCallback: (Int) -> Unit = mock()
val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock()
educationController.setAppHandleEducationTooltipCallbacks(
@@ -428,7 +321,7 @@ class AppHandleEducationControllerTest : ShellTestCase() {
mockToDesktopModeCallback,
)
// Simulate app handle visible.
- testCaptionStateFlow.value = createAppHandleState()
+ testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
// Wait for first tooltip to showup.
waitForBufferDelay()
@@ -436,68 +329,41 @@ class AppHandleEducationControllerTest : ShellTestCase() {
.showEducationTooltip(educationConfigCaptor.capture(), any())
educationConfigCaptor.lastValue.onEducationClickAction.invoke()
- verify(mockOpenHandleMenuCallback, times(1)).invoke(any())
+ verify(mockToDesktopModeCallback, times(1))
+ .invoke(any(), eq(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON))
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() =
+ fun clickExitDesktopModeHint_openHandleMenuCallbackInvoked() =
testScope.runTest {
- // After first tooltip is dismissed, app handle is expanded. Should show second
- // education
- // tooltip.
- showAndDismissFirstTooltip()
+ // App handle is visible. Should show education tooltip.
+ setShouldShowDesktopModeEducation(true)
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.
+ // Simulate app handle visible.
+ testCaptionStateFlow.value = createAppHeaderState()
+ // Wait for first tooltip to showup.
waitForBufferDelay()
verify(mockTooltipController, atLeastOnce())
.showEducationTooltip(educationConfigCaptor.capture(), any())
educationConfigCaptor.lastValue.onEducationClickAction.invoke()
- verify(mockToDesktopModeCallback, times(1)).invoke(any(), any())
+ verify(mockOpenHandleMenuCallback, times(1)).invoke(any())
}
- private suspend fun TestScope.showAndDismissFirstTooltip() {
- setShouldShowAppHandleEducation(true)
- // Simulate app handle visible.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
- // Wait for first tooltip to showup.
- waitForBufferDelay()
- // [shouldShowAppHandleEducation] should return false as education has been viewed
- // before.
- setShouldShowAppHandleEducation(false)
- // Dismiss previous tooltip, after this we should listen for next tooltip's trigger.
- captureAndInvokeOnDismissAction()
+ private suspend fun setShouldShowDesktopModeEducation(shouldShowDesktopModeEducation: Boolean) {
+ whenever(mockEducationFilter.shouldShowDesktopModeEducation(any<CaptionState.AppHandle>()))
+ .thenReturn(shouldShowDesktopModeEducation)
+ whenever(mockEducationFilter.shouldShowDesktopModeEducation(any<CaptionState.AppHeader>()))
+ .thenReturn(shouldShowDesktopModeEducation)
}
- private fun TestScope.showAndDismissSecondTooltip() {
- // Simulate app handle expanded.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
- // Dismiss previous tooltip, after this we should listen for next tooltip's trigger.
- captureAndInvokeOnDismissAction()
- }
-
- private fun captureAndInvokeOnDismissAction() {
- verify(mockTooltipController, atLeastOnce())
- .showEducationTooltip(educationConfigCaptor.capture(), any())
- educationConfigCaptor.lastValue.onDismissAction.invoke()
- }
-
- private suspend fun setShouldShowAppHandleEducation(shouldShowAppHandleEducation: Boolean) =
- whenever(mockEducationFilter.shouldShowAppHandleEducation(any()))
- .thenReturn(shouldShowAppHandleEducation)
-
/**
* Class under test waits for some time before showing education, simulate advance time before
* verifying or moving forward
@@ -510,7 +376,5 @@ class AppHandleEducationControllerTest : ShellTestCase() {
private companion object {
val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long =
APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L
- val APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS: Long =
- APP_HANDLE_EDUCATION_TIMEOUT_MILLIS + 1000L
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
index 4db883d13551..31dfc78902b2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
@@ -123,6 +123,24 @@ class AppHandleEducationDatastoreRepositoryTest {
}
@Test
+ fun updateEnterDesktopModeHintViewedTimestampMillis_updatesDatastoreProto() =
+ runTest(StandardTestDispatcher()) {
+ datastoreRepository.updateEnterDesktopModeHintViewedTimestampMillis(true)
+
+ val result = testDatastore.data.first().hasEnterDesktopModeHintViewedTimestampMillis()
+ assertThat(result).isEqualTo(true)
+ }
+
+ @Test
+ fun updateExitDesktopModeHintViewedTimestampMillis_updatesDatastoreProto() =
+ runTest(StandardTestDispatcher()) {
+ datastoreRepository.updateExitDesktopModeHintViewedTimestampMillis(true)
+
+ val result = testDatastore.data.first().hasExitDesktopModeHintViewedTimestampMillis()
+ assertThat(result).isEqualTo(true)
+ }
+
+ @Test
fun updateAppHandleHintUsedTimestampMillis_updatesDatastoreProto() =
runTest(StandardTestDispatcher()) {
datastoreRepository.updateAppHandleHintUsedTimestampMillis(true)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
index 2fc36efb1a41..218226240c0f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
@@ -89,9 +89,9 @@ class AppHandleEducationFilterTest : ShellTestCase() {
}
@Test
- fun shouldShowAppHandleEducation_isTriggerValid_returnsTrue() = runTest {
- // setup() makes sure that all of the conditions satisfy and #shouldShowAppHandleEducation
- // should return true
+ fun shouldShowDesktopModeEducation_isTriggerValid_returnsTrue() = runTest {
+ // setup() makes sure that all of the conditions satisfy and
+ // [shouldShowDesktopModeEducation] should return true
val windowingEducationProto =
createWindowingEducationProto(
appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
@@ -99,16 +99,15 @@ class AppHandleEducationFilterTest : ShellTestCase() {
)
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
assertThat(result).isTrue()
}
@Test
- fun shouldShowAppHandleEducation_focusAppNotInAllowlist_returnsFalse() = runTest {
+ fun shouldShowDesktopModeEducation_focusAppNotInAllowlist_returnsFalse() = runTest {
// Pass Youtube as current focus app, it is not in allowlist hence
- // #shouldShowAppHandleEducation
- // should return false
+ // [shouldShowDesktopModeEducation] should return false
testableResources.addOverride(
R.array.desktop_windowing_app_handle_education_allowlist_apps,
arrayOf(GMAIL_PACKAGE_NAME),
@@ -122,16 +121,15 @@ class AppHandleEducationFilterTest : ShellTestCase() {
createAppHandleState(createTaskInfo(runningTaskPackageName = YOUTUBE_PACKAGE_NAME))
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(captionState)
+ val result = educationFilter.shouldShowDesktopModeEducation(captionState)
assertThat(result).isFalse()
}
@Test
- fun shouldShowAppHandleEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest {
- // Time required to have passed setup is > 100 years, hence #shouldShowAppHandleEducation
- // should
- // return false
+ fun shouldShowDesktopModeEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest {
+ // Time required to have passed setup is > 100 years, hence [shouldShowDesktopModeEducation]
+ // should return false
testableResources.addOverride(
R.integer.desktop_windowing_education_required_time_since_setup_seconds,
MAX_VALUE,
@@ -143,50 +141,15 @@ class AppHandleEducationFilterTest : ShellTestCase() {
)
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
-
- assertThat(result).isFalse()
- }
-
- @Test
- fun shouldShowAppHandleEducation_appHandleHintViewedBefore_returnsFalse() = runTest {
- // App handle hint has been viewed before, hence #shouldShowAppHandleEducation should return
- // false
- val windowingEducationProto =
- createWindowingEducationProto(
- appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
- appHandleHintViewedTimestampMillis = 123L,
- appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
- )
- `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
-
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
-
- assertThat(result).isFalse()
- }
-
- @Test
- fun shouldShowAppHandleEducation_appHandleHintUsedBefore_returnsFalse() = runTest {
- // App handle hint has been used before, hence #shouldShowAppHandleEducation should return
- // false
- val windowingEducationProto =
- createWindowingEducationProto(
- appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
- appHandleHintUsedTimestampMillis = 123L,
- appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
- )
- `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
-
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
assertThat(result).isFalse()
}
@Test
- fun shouldShowAppHandleEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest {
+ fun shouldShowDesktopModeEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest {
// Simulate that gmail app has been launched twice before, minimum app launch count is 3,
- // hence
- // #shouldShowAppHandleEducation should return false
+ // hence [shouldShowDesktopModeEducation] should return false
testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
val windowingEducationProto =
createWindowingEducationProto(
@@ -195,13 +158,13 @@ class AppHandleEducationFilterTest : ShellTestCase() {
)
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
assertThat(result).isFalse()
}
@Test
- fun shouldShowAppHandleEducation_appUsageStatsStale_queryAppUsageStats() = runTest {
+ fun shouldShowDesktopModeEducation_appUsageStatsStale_queryAppUsageStats() = runTest {
// UsageStats caching interval is set to 0ms, that means caching should happen very
// frequently
testableResources.addOverride(
@@ -209,8 +172,7 @@ class AppHandleEducationFilterTest : ShellTestCase() {
0,
)
// The DataStore currently holds a proto object where Gmail's app launch count is recorded
- // as 4.
- // This value exceeds the minimum required count of 3.
+ // as 4. This value exceeds the minimum required count of 3.
testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
val windowingEducationProto =
createWindowingEducationProto(
@@ -223,40 +185,20 @@ class AppHandleEducationFilterTest : ShellTestCase() {
.thenReturn(mapOf(GMAIL_PACKAGE_NAME to UsageStats().apply { mAppLaunchCount = 2 }))
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
// Result should be false as queried usage stats should be considered to determine the
- // result
- // instead of cached stats
- assertThat(result).isFalse()
- }
-
- @Test
- fun shouldShowAppHandleEducation_appHandleMenuExpanded_returnsFalse() = runTest {
- val windowingEducationProto =
- createWindowingEducationProto(
- appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
- appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
- )
- // Simulate app handle menu is expanded
- val captionState = createAppHandleState(isHandleMenuExpanded = true)
- `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
-
- val result = educationFilter.shouldShowAppHandleEducation(captionState)
-
- // We should not show app handle education if app menu is expanded
+ // result instead of cached stats
assertThat(result).isFalse()
}
@Test
- fun shouldShowAppHandleEducation_overridePrerequisite_returnsTrue() = runTest {
+ fun shouldShowDesktopModeEducation_overridePrerequisite_returnsTrue() = runTest {
// Simulate that gmail app has been launched twice before, minimum app launch count is 3,
- // hence
- // #shouldShowAppHandleEducation should return false. But as we are overriding prerequisite
- // conditions, #shouldShowAppHandleEducation should return true.
+ // hence [shouldShowDesktopModeEducation] should return false. But as we are overriding
+ // prerequisite conditions, [shouldShowDesktopModeEducation] should return true.
testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
- val systemPropertiesKey =
- "persist.desktop_windowing_app_handle_education_override_conditions"
+ val systemPropertiesKey = "persist.windowing_force_show_desktop_mode_education"
whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean()))
.thenReturn(true)
val windowingEducationProto =
@@ -266,7 +208,7 @@ class AppHandleEducationFilterTest : ShellTestCase() {
)
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
assertThat(result).isTrue()
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt
index b9d91e7895db..546848421302 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt
@@ -81,7 +81,9 @@ fun createWindowingEducationProto(
appHandleHintViewedTimestampMillis: Long? = null,
appHandleHintUsedTimestampMillis: Long? = null,
appUsageStats: Map<String, Int>? = null,
- appUsageStatsLastUpdateTimestampMillis: Long? = null
+ appUsageStatsLastUpdateTimestampMillis: Long? = null,
+ enterDesktopModeHintViewedTimestampMillis: Long? = null,
+ exitDesktopModeHintViewedTimestampMillis: Long? = null,
): WindowingEducationProto =
WindowingEducationProto.newBuilder()
.apply {
@@ -91,6 +93,12 @@ fun createWindowingEducationProto(
if (appHandleHintUsedTimestampMillis != null) {
setAppHandleHintUsedTimestampMillis(appHandleHintUsedTimestampMillis)
}
+ if (enterDesktopModeHintViewedTimestampMillis != null) {
+ setEnterDesktopModeHintViewedTimestampMillis(enterDesktopModeHintViewedTimestampMillis)
+ }
+ if (exitDesktopModeHintViewedTimestampMillis != null) {
+ setExitDesktopModeHintViewedTimestampMillis(exitDesktopModeHintViewedTimestampMillis)
+ }
setAppHandleEducation(
createAppHandleEducationProto(appUsageStats, appUsageStatsLastUpdateTimestampMillis))
}