diff options
author | 2025-01-23 05:47:11 -0800 | |
---|---|---|
committer | 2025-01-23 05:47:11 -0800 | |
commit | af72c8c5e94423a84d715786e4496754bf19e3f7 (patch) | |
tree | 109b27a104dcf14ed6ede2311acc1270f14b6811 | |
parent | 7844c82ddeda9ecf6f69d4edb62053825ece712c (diff) | |
parent | 96a9c32fbd958d9260daf64021eb6549243fa536 (diff) |
Merge "Enable 3 desktop mode education hints to launch independently." into main
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)) } |