diff options
6 files changed, 560 insertions, 504 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 de9c79ab34fd..c5fca028a1a6 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 @@ -36,8 +36,8 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.Theme import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController -import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipEducationViewConfig 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 @@ -74,293 +74,330 @@ 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 decorThemeUtil = DecorThemeUtil(context) + private lateinit var openHandleMenuCallback: (Int) -> Unit + private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit + + init { + runIfEducationFeatureEnabled { + 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 + ) + } + } + } + .flowOn(backgroundDispatcher) + .collectLatest { captionState -> + val tooltipColorScheme = tooltipColorScheme(captionState) + + showEducation(captionState, tooltipColorScheme) + // After showing first tooltip, mark education as viewed + appHandleEducationDatastoreRepository + .updateAppHandleHintViewedTimestampMillis(true) + } + } - init { - runIfEducationFeatureEnabled { - 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. + applicationCoroutineScope.launch { + if (isAppHandleHintUsed()) return@launch 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) + captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded } - } + .take(1) + .flowOn(backgroundDispatcher) + .collect { + // If user expands app handle, mark user has used the app handle hint + appHandleEducationDatastoreRepository + .updateAppHandleHintUsedTimestampMillis(true) + } + } + } + } + + private inline fun runIfEducationFeatureEnabled(block: () -> Unit) { + if (canEnterDesktopMode(context) && Flags.enableDesktopWindowingAppHandleEducation()) + block() + } + + private fun showEducation(captionState: CaptionState, tooltipColorScheme: TooltipColorScheme) { + val appHandleBounds = (captionState as CaptionState.AppHandle).globalAppHandleBounds + val tooltipGlobalCoordinates = + Point(appHandleBounds.left + appHandleBounds.width() / 2, appHandleBounds.bottom) + // TODO: b/370546801 - Differentiate between user dismissing the tooltip vs following the + // cue. + // Populate information important to inflate app handle education tooltip. + val appHandleTooltipConfig = + TooltipEducationViewConfig( + tooltipViewLayout = R.layout.desktop_windowing_education_top_arrow_tooltip, + tooltipColorScheme = tooltipColorScheme, + 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) + } + }, + ) + + windowingEducationViewController.showEducationTooltip( + tooltipViewConfig = appHandleTooltipConfig, + taskId = captionState.runningTaskInfo.taskId, + ) + } + + /** Show tooltip that points to windowing image button in app handle menu */ + private suspend fun showWindowingImageButtonTooltip(tooltipColorScheme: TooltipColorScheme) { + 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) + val appHandleMenuWidth = + getSize(R.dimen.desktop_mode_handle_menu_width) + + getSize(R.dimen.desktop_mode_handle_menu_pill_spacing_margin) + val appHandleMenuMargins = + 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 -> - val tooltipColorScheme = tooltipColorScheme(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) + } + }, + ) - showEducation(captionState, tooltipColorScheme) - // After showing first tooltip, mark education as viewed - appHandleEducationDatastoreRepository.updateAppHandleHintViewedTimestampMillis(true) + windowingEducationViewController.showEducationTooltip( + taskId = captionState.runningTaskInfo.taskId, + tooltipViewConfig = windowingImageButtonTooltipConfig, + ) } - } + } - applicationCoroutineScope.launch { - if (isAppHandleHintUsed()) return@launch + /** 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 -> - captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded + // 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) - .collect { - // If user expands app handle, mark user has used the app handle hint - appHandleEducationDatastoreRepository.updateAppHandleHintUsedTimestampMillis(true) + .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 inline fun runIfEducationFeatureEnabled(block: () -> Unit) { - if (canEnterDesktopMode(context) && Flags.enableDesktopWindowingAppHandleEducation()) block() - } - - private fun showEducation(captionState: CaptionState, tooltipColorScheme: TooltipColorScheme) { - val appHandleBounds = (captionState as CaptionState.AppHandle).globalAppHandleBounds - val tooltipGlobalCoordinates = - Point(appHandleBounds.left + appHandleBounds.width() / 2, appHandleBounds.bottom) - // TODO: b/370546801 - Differentiate between user dismissing the tooltip vs following the cue. - // Populate information important to inflate app handle education tooltip. - val appHandleTooltipConfig = - TooltipEducationViewConfig( - tooltipViewLayout = R.layout.desktop_windowing_education_top_arrow_tooltip, - tooltipColorScheme = tooltipColorScheme, - 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) } - }, - ) - - windowingEducationViewController.showEducationTooltip( - tooltipViewConfig = appHandleTooltipConfig, taskId = captionState.runningTaskInfo.taskId) - } - - /** Show tooltip that points to windowing image button in app handle menu */ - private suspend fun showWindowingImageButtonTooltip(tooltipColorScheme: TooltipColorScheme) { - 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) - val appHandleMenuWidth = - getSize(R.dimen.desktop_mode_handle_menu_width) + - getSize(R.dimen.desktop_mode_handle_menu_pill_spacing_margin) - val appHandleMenuMargins = - 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) } - }, - ) - windowingEducationViewController.showEducationTooltip( - taskId = captionState.runningTaskInfo.taskId, - tooltipViewConfig = windowingImageButtonTooltipConfig) - } - } + private fun tooltipColorScheme(captionState: CaptionState): TooltipColorScheme { + context.withStyledAttributes( + set = null, + attrs = + intArrayOf( + com.android.internal.R.attr.materialColorOnTertiaryFixed, + com.android.internal.R.attr.materialColorTertiaryFixed, + com.android.internal.R.attr.materialColorTertiaryFixedDim, + ), + defStyleAttr = 0, + defStyleRes = 0, + ) { + val onTertiaryFixed = getColor(/* index= */ 0, /* defValue= */ 0) + val tertiaryFixed = getColor(/* index= */ 1, /* defValue= */ 0) + val tertiaryFixedDim = getColor(/* index= */ 2, /* defValue= */ 0) + val taskInfo = (captionState as CaptionState.AppHandle).runningTaskInfo - /** 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, - ) + val tooltipContainerColor = + if (decorThemeUtil.getAppTheme(taskInfo) == Theme.LIGHT) { + tertiaryFixed + } else { + tertiaryFixedDim + } + return TooltipColorScheme(tooltipContainerColor, onTertiaryFixed, onTertiaryFixed) } - } + return TooltipColorScheme(0, 0, 0) + } - private fun tooltipColorScheme(captionState: CaptionState): TooltipColorScheme { - context.withStyledAttributes( - set = null, - attrs = - intArrayOf( - com.android.internal.R.attr.materialColorOnTertiaryFixed, - com.android.internal.R.attr.materialColorTertiaryFixed, - com.android.internal.R.attr.materialColorTertiaryFixedDim), - defStyleAttr = 0, - defStyleRes = 0) { - val onTertiaryFixed = getColor(/* index= */ 0, /* defValue= */ 0) - val tertiaryFixed = getColor(/* index= */ 1, /* defValue= */ 0) - val tertiaryFixedDim = getColor(/* index= */ 2, /* defValue= */ 0) - val taskInfo = (captionState as CaptionState.AppHandle).runningTaskInfo + /** + * Setup callbacks for app handle education tooltips. + * + * @param openHandleMenuCallback callback invoked to open app handle menu or app chip menu. + * @param toDesktopModeCallback callback invoked to move task into desktop mode. + */ + fun setAppHandleEducationTooltipCallbacks( + openHandleMenuCallback: (taskId: Int) -> Unit, + toDesktopModeCallback: (taskId: Int, DesktopModeTransitionSource) -> Unit, + ) { + this.openHandleMenuCallback = openHandleMenuCallback + this.toDesktopModeCallback = toDesktopModeCallback + } - val tooltipContainerColor = - if (decorThemeUtil.getAppTheme(taskInfo) == Theme.LIGHT) { - tertiaryFixed - } else { - tertiaryFixedDim - } - return TooltipColorScheme(tooltipContainerColor, onTertiaryFixed, onTertiaryFixed) + private inline fun <T> Flow<T>.catchTimeoutAndLog(crossinline block: () -> Unit) = + catch { exception -> + if (exception is TimeoutCancellationException) block() else throw exception } - return TooltipColorScheme(0, 0, 0) - } - /** - * Setup callbacks for app handle education tooltips. - * - * @param openHandleMenuCallback callback invoked to open app handle menu or app chip menu. - * @param toDesktopModeCallback callback invoked to move task into desktop mode. - */ - fun setAppHandleEducationTooltipCallbacks( - openHandleMenuCallback: (taskId: Int) -> Unit, - toDesktopModeCallback: (taskId: Int, DesktopModeTransitionSource) -> Unit - ) { - this.openHandleMenuCallback = openHandleMenuCallback - this.toDesktopModeCallback = toDesktopModeCallback - } - - private inline fun <T> Flow<T>.catchTimeoutAndLog(crossinline block: () -> Unit) = - catch { exception -> - if (exception is TimeoutCancellationException) block() else throw exception - } - - private fun launchWithExceptionHandling(block: suspend () -> Unit) = - applicationCoroutineScope.launch { - try { - block() - } catch (e: Throwable) { - Slog.e(TAG, "Error: ", e) + private fun launchWithExceptionHandling(block: suspend () -> Unit) = + applicationCoroutineScope.launch { + try { + block() + } catch (e: Throwable) { + Slog.e(TAG, "Error: ", e) + } } - } - /** - * 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> = - appHandleEducationDatastoreRepository.dataStoreFlow - .map { preferences -> - preferences.hasAppHandleHintViewedTimestampMillis() && !SHOULD_OVERRIDE_EDUCATION_CONDITIONS - } - .distinctUntilChanged() + /** + * 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> = + appHandleEducationDatastoreRepository.dataStoreFlow + .map { preferences -> + preferences.hasAppHandleHintViewedTimestampMillis() && + !SHOULD_OVERRIDE_EDUCATION_CONDITIONS + } + .distinctUntilChanged() - /** - * Listens to the changes to [WindowingEducationProto#hasAppHandleHintUsedTimestampMillis()] in - * datastore proto object. - */ - private suspend fun isAppHandleHintUsed(): Boolean = - appHandleEducationDatastoreRepository.dataStoreFlow.first().hasAppHandleHintUsedTimestampMillis() + /** + * Listens to the changes to [WindowingEducationProto#hasAppHandleHintUsedTimestampMillis()] in + * datastore proto object. + */ + private suspend fun isAppHandleHintUsed(): Boolean = + appHandleEducationDatastoreRepository.dataStoreFlow + .first() + .hasAppHandleHintUsedTimestampMillis() - private fun getSize(@DimenRes resourceId: Int): Int { - if (resourceId == Resources.ID_NULL) return 0 - return context.resources.getDimensionPixelSize(resourceId) - } + private fun getSize(@DimenRes resourceId: Int): Int { + if (resourceId == Resources.ID_NULL) return 0 + return context.resources.getDimensionPixelSize(resourceId) + } - private fun getString(@StringRes resId: Int): String = context.resources.getString(resId) + private fun getString(@StringRes resId: Int): String = context.resources.getString(resId) - companion object { - const val TAG = "AppHandleEducationController" - val APP_HANDLE_EDUCATION_DELAY_MILLIS: Long - get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L) + companion object { + const val TAG = "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 APP_HANDLE_EDUCATION_TIMEOUT_MILLIS: Long + get() = SystemProperties.getLong("persist.windowing_app_handle_education_timeout", 400L) - val SHOULD_OVERRIDE_EDUCATION_CONDITIONS: Boolean - get() = - SystemProperties.getBoolean( - "persist.desktop_windowing_app_handle_education_override_conditions", false) - } + val SHOULD_OVERRIDE_EDUCATION_CONDITIONS: Boolean + get() = + SystemProperties.getBoolean( + "persist.desktop_windowing_app_handle_education_override_conditions", + 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 7a7829334fb6..9990846fc92e 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 @@ -32,106 +32,114 @@ import java.time.Duration /** Filters incoming app handle education triggers based on set conditions. */ class AppHandleEducationFilter( private val context: Context, - private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository + private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository, ) { - private val usageStatsManager = - context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager - - /** - * Returns true if conditions to show app handle education are met, returns false otherwise. - * - * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this method will always return - * ![captionState.isHandleMenuExpanded]. - */ - suspend fun shouldShowAppHandleEducation(captionState: CaptionState): Boolean { - if ((captionState as CaptionState.AppHandle).isHandleMenuExpanded) return false - if (SHOULD_OVERRIDE_EDUCATION_CONDITIONS) return true - - val focusAppPackageName = - captionState.runningTaskInfo.topActivityInfo?.packageName ?: return false - val windowingEducationProto = appHandleEducationDatastoreRepository.windowingEducationProto() - - return isFocusAppInAllowlist(focusAppPackageName) && - !isOtherEducationShowing() && - hasSufficientTimeSinceSetup() && - !isAppHandleHintViewedBefore(windowingEducationProto) && - !isAppHandleHintUsedBefore(windowingEducationProto) && - hasMinAppUsage(windowingEducationProto, focusAppPackageName) - } - - private fun isFocusAppInAllowlist(focusAppPackageName: String): Boolean = - focusAppPackageName in - context.resources.getStringArray( - R.array.desktop_windowing_app_handle_education_allowlist_apps) - - // TODO: b/350953004 - Add checks based on App compat - // TODO: b/350951797 - Add checks based on PKT tips education - private fun isOtherEducationShowing(): Boolean = isTaskbarEducationShowing() - - private fun isTaskbarEducationShowing(): Boolean = - Secure.getInt(context.contentResolver, Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) == 1 - - private fun hasSufficientTimeSinceSetup(): Boolean = - Duration.ofMillis(SystemClock.elapsedRealtime()) > - convertIntegerResourceToDuration( - 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 - ): Boolean = - (launchCountByPackageName(windowingEducationProto)[focusAppPackageName] ?: 0) >= - context.resources.getInteger(R.integer.desktop_windowing_education_min_app_launch_count) - - private suspend fun launchCountByPackageName( - windowingEducationProto: WindowingEducationProto - ): Map<String, Int> = - if (isAppUsageCacheStale(windowingEducationProto)) { - // Query and return user stats, update cache in datastore - getAndCacheAppUsageStats() - } else { - // Return cached usage stats - windowingEducationProto.appHandleEducation.appUsageStatsMap - } - - private fun isAppUsageCacheStale(windowingEducationProto: WindowingEducationProto): Boolean { - val currentTime = currentTimeInDuration() - val lastUpdateTime = - Duration.ofMillis( - windowingEducationProto.appHandleEducation.appUsageStatsLastUpdateTimestampMillis) - val appUsageStatsCachingInterval = - convertIntegerResourceToDuration( - R.integer.desktop_windowing_education_app_usage_cache_interval_seconds) - return (currentTime - lastUpdateTime) > appUsageStatsCachingInterval - } - - private suspend fun getAndCacheAppUsageStats(): Map<String, Int> { - val currentTime = currentTimeInDuration() - val appUsageStats = queryAppUsageStats() - appHandleEducationDatastoreRepository.updateAppUsageStats(appUsageStats, currentTime) - return appUsageStats - } - - private fun queryAppUsageStats(): Map<String, Int> { - val endTime = currentTimeInDuration() - val appLaunchInterval = - convertIntegerResourceToDuration( - R.integer.desktop_windowing_education_app_launch_interval_seconds) - val startTime = endTime - appLaunchInterval - - return usageStatsManager - .queryAndAggregateUsageStats(startTime.toMillis(), endTime.toMillis()) - .mapValues { it.value.appLaunchCount } - } - - private fun convertIntegerResourceToDuration(@IntegerRes resourceId: Int): Duration = - Duration.ofSeconds(context.resources.getInteger(resourceId).toLong()) - - private fun currentTimeInDuration(): Duration = Duration.ofMillis(System.currentTimeMillis()) + private val usageStatsManager = + context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager + + /** + * Returns true if conditions to show app handle education are met, returns false otherwise. + * + * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this method will always return + * ![captionState.isHandleMenuExpanded]. + */ + suspend fun shouldShowAppHandleEducation(captionState: CaptionState): Boolean { + if ((captionState as CaptionState.AppHandle).isHandleMenuExpanded) return false + if (SHOULD_OVERRIDE_EDUCATION_CONDITIONS) return true + + val focusAppPackageName = + captionState.runningTaskInfo.topActivityInfo?.packageName ?: return false + val windowingEducationProto = + appHandleEducationDatastoreRepository.windowingEducationProto() + + return isFocusAppInAllowlist(focusAppPackageName) && + !isOtherEducationShowing() && + hasSufficientTimeSinceSetup() && + !isAppHandleHintViewedBefore(windowingEducationProto) && + !isAppHandleHintUsedBefore(windowingEducationProto) && + hasMinAppUsage(windowingEducationProto, focusAppPackageName) + } + + private fun isFocusAppInAllowlist(focusAppPackageName: String): Boolean = + focusAppPackageName in + context.resources.getStringArray( + R.array.desktop_windowing_app_handle_education_allowlist_apps + ) + + // TODO: b/350953004 - Add checks based on App compat + // TODO: b/350951797 - Add checks based on PKT tips education + private fun isOtherEducationShowing(): Boolean = isTaskbarEducationShowing() + + private fun isTaskbarEducationShowing(): Boolean = + Secure.getInt(context.contentResolver, Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) == 1 + + private fun hasSufficientTimeSinceSetup(): Boolean = + Duration.ofMillis(SystemClock.elapsedRealtime()) > + convertIntegerResourceToDuration( + 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, + ): Boolean = + (launchCountByPackageName(windowingEducationProto)[focusAppPackageName] ?: 0) >= + context.resources.getInteger(R.integer.desktop_windowing_education_min_app_launch_count) + + private suspend fun launchCountByPackageName( + windowingEducationProto: WindowingEducationProto + ): Map<String, Int> = + if (isAppUsageCacheStale(windowingEducationProto)) { + // Query and return user stats, update cache in datastore + getAndCacheAppUsageStats() + } else { + // Return cached usage stats + windowingEducationProto.appHandleEducation.appUsageStatsMap + } + + private fun isAppUsageCacheStale(windowingEducationProto: WindowingEducationProto): Boolean { + val currentTime = currentTimeInDuration() + val lastUpdateTime = + Duration.ofMillis( + windowingEducationProto.appHandleEducation.appUsageStatsLastUpdateTimestampMillis + ) + val appUsageStatsCachingInterval = + convertIntegerResourceToDuration( + R.integer.desktop_windowing_education_app_usage_cache_interval_seconds + ) + return (currentTime - lastUpdateTime) > appUsageStatsCachingInterval + } + + private suspend fun getAndCacheAppUsageStats(): Map<String, Int> { + val currentTime = currentTimeInDuration() + val appUsageStats = queryAppUsageStats() + appHandleEducationDatastoreRepository.updateAppUsageStats(appUsageStats, currentTime) + return appUsageStats + } + + private fun queryAppUsageStats(): Map<String, Int> { + val endTime = currentTimeInDuration() + val appLaunchInterval = + convertIntegerResourceToDuration( + R.integer.desktop_windowing_education_app_launch_interval_seconds + ) + val startTime = endTime - appLaunchInterval + + return usageStatsManager + .queryAndAggregateUsageStats(startTime.toMillis(), endTime.toMillis()) + .mapValues { it.value.appLaunchCount } + } + + private fun convertIntegerResourceToDuration(@IntegerRes resourceId: Int): Duration = + Duration.ofSeconds(context.resources.getInteger(resourceId).toLong()) + + private fun currentTimeInDuration(): Duration = Duration.ofMillis(System.currentTimeMillis()) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt index 693da81ec4a0..bfe1b12c9605 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt @@ -53,8 +53,8 @@ import kotlinx.coroutines.launch /** * Controls App-to-Web education end to end. * - * Listen to usages of App-to-Web, calls an api to check if the education - * should be shown and controls education UI. + * Listen to usages of App-to-Web, calls an api to check if the education should be shown and + * controls education UI. */ @OptIn(kotlinx.coroutines.FlowPreview::class) @kotlinx.coroutines.ExperimentalCoroutinesApi @@ -88,8 +88,9 @@ class AppToWebEducationController( .debounce(APP_TO_WEB_EDUCATION_DELAY_MILLIS) .filter { captionState -> captionState !is CaptionState.NoCaption && - appToWebEducationFilter - .shouldShowAppToWebEducation(captionState) + appToWebEducationFilter.shouldShowAppToWebEducation( + captionState + ) } } } @@ -104,12 +105,12 @@ class AppToWebEducationController( applicationCoroutineScope.launch { if (isFeatureUsed()) return@launch - windowDecorCaptionHandleRepository.appToWebUsageFlow - .collect { - // If user utilizes App-to-Web, mark user has used the feature - appToWebEducationDatastoreRepository - .updateFeatureUsedTimestampMillis(isViewed = true) - } + windowDecorCaptionHandleRepository.appToWebUsageFlow.collect { + // If user utilizes App-to-Web, mark user has used the feature + appToWebEducationDatastoreRepository.updateFeatureUsedTimestampMillis( + isViewed = true + ) + } } } } @@ -126,10 +127,8 @@ class AppToWebEducationController( val appHandleBounds = captionState.globalAppHandleBounds val educationWidth = loadDimensionPixelSize(R.dimen.desktop_windowing_education_promo_width) - educationGlobalCoordinates = Point( - appHandleBounds.centerX() - educationWidth / 2, - appHandleBounds.bottom - ) + educationGlobalCoordinates = + Point(appHandleBounds.centerX() - educationWidth / 2, appHandleBounds.bottom) taskId = captionState.runningTaskInfo.taskId } @@ -152,19 +151,22 @@ class AppToWebEducationController( viewGlobalCoordinates = educationGlobalCoordinates, educationText = getString(R.string.desktop_windowing_app_to_web_education_text), widthId = R.dimen.desktop_windowing_education_promo_width, - heightId = R.dimen.desktop_windowing_education_promo_height + heightId = R.dimen.desktop_windowing_education_promo_height, ) windowingEducationViewController.showEducation( - viewConfig = educationConfig, taskId = taskId) + viewConfig = educationConfig, + taskId = taskId, + ) } private fun educationColorScheme(captionState: CaptionState): EducationColorScheme? { - val taskInfo: RunningTaskInfo = when (captionState) { - is CaptionState.AppHandle -> captionState.runningTaskInfo - is CaptionState.AppHeader -> captionState.runningTaskInfo - else -> return null - } + val taskInfo: RunningTaskInfo = + when (captionState) { + is CaptionState.AppHandle -> captionState.runningTaskInfo + is CaptionState.AppHeader -> captionState.runningTaskInfo + else -> return null + } val colorScheme = decorThemeUtil.getColorScheme(taskInfo) val tooltipContainerColor = colorScheme.surfaceBright.toArgb() @@ -178,8 +180,7 @@ class AppToWebEducationController( */ private fun isEducationViewLimitReachedFlow(): Flow<Boolean> = appToWebEducationDatastoreRepository.dataStoreFlow - .map { preferences -> - appToWebEducationFilter.isEducationViewLimitReached(preferences)} + .map { preferences -> appToWebEducationFilter.isEducationViewLimitReached(preferences) } .distinctUntilChanged() /** @@ -199,9 +200,6 @@ class AppToWebEducationController( companion object { const val TAG = "AppToWebEducationController" val APP_TO_WEB_EDUCATION_DELAY_MILLIS: Long - get() = SystemProperties.getLong( - "persist.windowing_app_handle_education_delay", - 3000L - ) + get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt index feee6ed86da1..e272b54dd907 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt @@ -30,39 +30,41 @@ import java.time.Duration /** Filters incoming App-to-Web education triggers based on set conditions. */ class AppToWebEducationFilter( private val context: Context, - private val appToWebEducationDatastoreRepository: AppToWebEducationDatastoreRepository + private val appToWebEducationDatastoreRepository: AppToWebEducationDatastoreRepository, ) { /** Returns true if conditions to show App-to-web education are met, returns false otherwise. */ suspend fun shouldShowAppToWebEducation(captionState: CaptionState): Boolean { - val (taskInfo: RunningTaskInfo, isCapturedLinkAvailable: Boolean) = when (captionState) { - is CaptionState.AppHandle -> - Pair(captionState.runningTaskInfo, captionState.isCapturedLinkAvailable) - is CaptionState.AppHeader -> - Pair(captionState.runningTaskInfo, captionState.isCapturedLinkAvailable) - else -> return false - } + val (taskInfo: RunningTaskInfo, isCapturedLinkAvailable: Boolean) = + when (captionState) { + is CaptionState.AppHandle -> + Pair(captionState.runningTaskInfo, captionState.isCapturedLinkAvailable) + is CaptionState.AppHeader -> + Pair(captionState.runningTaskInfo, captionState.isCapturedLinkAvailable) + else -> return false + } val focusAppPackageName = taskInfo.topActivityInfo?.packageName ?: return false val windowingEducationProto = appToWebEducationDatastoreRepository.windowingEducationProto() return !isOtherEducationShowing() && - !isEducationViewLimitReached(windowingEducationProto) && - hasSufficientTimeSinceSetup() && - !isFeatureUsedBefore(windowingEducationProto) && - isCapturedLinkAvailable && - isFocusAppInAllowlist(focusAppPackageName) + !isEducationViewLimitReached(windowingEducationProto) && + hasSufficientTimeSinceSetup() && + !isFeatureUsedBefore(windowingEducationProto) && + isCapturedLinkAvailable && + isFocusAppInAllowlist(focusAppPackageName) } private fun isFocusAppInAllowlist(focusAppPackageName: String): Boolean = focusAppPackageName in - context.resources.getStringArray( - R.array.desktop_windowing_app_to_web_education_allowlist_apps) + context.resources.getStringArray( + R.array.desktop_windowing_app_to_web_education_allowlist_apps + ) // TODO: b/350953004 - Add checks based on App compat // TODO: b/350951797 - Add checks based on PKT tips education - private fun isOtherEducationShowing(): Boolean = isTaskbarEducationShowing() || - isCompatUiEducationShowing() + private fun isOtherEducationShowing(): Boolean = + isTaskbarEducationShowing() || isCompatUiEducationShowing() private fun isTaskbarEducationShowing(): Boolean = Secure.getInt(context.contentResolver, Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) == 1 @@ -72,13 +74,14 @@ class AppToWebEducationFilter( private fun hasSufficientTimeSinceSetup(): Boolean = Duration.ofMillis(SystemClock.elapsedRealtime()) > - convertIntegerResourceToDuration( - R.integer.desktop_windowing_education_required_time_since_setup_seconds) + convertIntegerResourceToDuration( + R.integer.desktop_windowing_education_required_time_since_setup_seconds + ) /** Returns true if education is viewed maximum amount of times it should be shown. */ fun isEducationViewLimitReached(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.getAppToWebEducation().getEducationShownCount() >= - MAXIMUM_TIMES_EDUCATION_SHOWN + MAXIMUM_TIMES_EDUCATION_SHOWN private fun isFeatureUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.hasFeatureUsedTimestampMillis() 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 5e0c0007e2eb..3e120b09a0b6 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 @@ -42,103 +42,109 @@ import kotlinx.coroutines.flow.first class AppHandleEducationDatastoreRepository @VisibleForTesting constructor(private val dataStore: DataStore<WindowingEducationProto>) { - constructor( - context: Context - ) : this( - DataStoreFactory.create( - serializer = WindowingEducationProtoSerializer, - produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) })) + constructor( + context: Context + ) : this( + DataStoreFactory.create( + serializer = WindowingEducationProtoSerializer, + produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) }, + ) + ) - /** Provides dataStore.data flow and handles exceptions thrown during collection */ - val dataStoreFlow: Flow<WindowingEducationProto> = - dataStore.data.catch { exception -> - // dataStore.data throws an IOException when an error is encountered when reading data - if (exception is IOException) { - Log.e( - TAG, - "Error in reading app handle education related data from datastore, data is " + - "stored in a file named $APP_HANDLE_EDUCATION_DATASTORE_FILEPATH", - exception) - } else { - throw exception + /** Provides dataStore.data flow and handles exceptions thrown during collection */ + val dataStoreFlow: Flow<WindowingEducationProto> = + dataStore.data.catch { exception -> + // dataStore.data throws an IOException when an error is encountered when reading data + if (exception is IOException) { + Log.e( + TAG, + "Error in reading app handle education related data from datastore, data is " + + "stored in a file named $APP_HANDLE_EDUCATION_DATASTORE_FILEPATH", + exception, + ) + } else { + throw exception + } } - } - /** - * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the - * DataStore is empty or there's an error reading, it returns the default value of Proto. - */ - suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() + /** + * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the + * DataStore is empty or there's an error reading, it returns the default value of Proto. + */ + suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() - /** - * Updates [WindowingEducationProto.appHandleHintViewedTimestampMillis_] field - * in datastore with current timestamp if [isViewed] is true, if not then - * clears the field. - */ - suspend fun updateAppHandleHintViewedTimestampMillis(isViewed: Boolean) { - dataStore.updateData { preferences -> - if (isViewed) { - preferences - .toBuilder() - .setAppHandleHintViewedTimestampMillis(System.currentTimeMillis()) - .build() - } else { - preferences.toBuilder().clearAppHandleHintViewedTimestampMillis().build() - } + /** + * Updates [WindowingEducationProto.appHandleHintViewedTimestampMillis_] field in datastore with + * current timestamp if [isViewed] is true, if not then clears the field. + */ + suspend fun updateAppHandleHintViewedTimestampMillis(isViewed: Boolean) { + dataStore.updateData { preferences -> + if (isViewed) { + preferences + .toBuilder() + .setAppHandleHintViewedTimestampMillis(System.currentTimeMillis()) + .build() + } else { + preferences.toBuilder().clearAppHandleHintViewedTimestampMillis().build() + } + } } - } - /** - * Updates [WindowingEducationProto.appHandleHintUsedTimestampMillis_] field - * in datastore with current timestamp if [isViewed] is true, if not then - * clears the field. - */ - suspend fun updateAppHandleHintUsedTimestampMillis(isViewed: Boolean) { - dataStore.updateData { preferences -> - if (isViewed) { - preferences.toBuilder().setAppHandleHintUsedTimestampMillis(System.currentTimeMillis()).build() - } else { - preferences.toBuilder().clearAppHandleHintUsedTimestampMillis().build() - } + /** + * Updates [WindowingEducationProto.appHandleHintUsedTimestampMillis_] field in datastore with + * current timestamp if [isViewed] is true, if not then clears the field. + */ + suspend fun updateAppHandleHintUsedTimestampMillis(isViewed: Boolean) { + dataStore.updateData { preferences -> + if (isViewed) { + preferences + .toBuilder() + .setAppHandleHintUsedTimestampMillis(System.currentTimeMillis()) + .build() + } else { + preferences.toBuilder().clearAppHandleHintUsedTimestampMillis().build() + } + } } - } - /** - * Updates [AppHandleEducation.appUsageStats] and - * [AppHandleEducation.appUsageStatsLastUpdateTimestampMillis] fields in datastore with - * [appUsageStats] and [appUsageStatsLastUpdateTimestamp]. - */ - suspend fun updateAppUsageStats( - appUsageStats: Map<String, Int>, - appUsageStatsLastUpdateTimestamp: Duration - ) { - val currentAppHandleProto = windowingEducationProto().appHandleEducation.toBuilder() - currentAppHandleProto - .putAllAppUsageStats(appUsageStats) - .setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestamp.toMillis()) - dataStore.updateData { preferences: WindowingEducationProto -> - preferences.toBuilder().setAppHandleEducation(currentAppHandleProto).build() + /** + * Updates [AppHandleEducation.appUsageStats] and + * [AppHandleEducation.appUsageStatsLastUpdateTimestampMillis] fields in datastore with + * [appUsageStats] and [appUsageStatsLastUpdateTimestamp]. + */ + suspend fun updateAppUsageStats( + appUsageStats: Map<String, Int>, + appUsageStatsLastUpdateTimestamp: Duration, + ) { + val currentAppHandleProto = windowingEducationProto().appHandleEducation.toBuilder() + currentAppHandleProto + .putAllAppUsageStats(appUsageStats) + .setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestamp.toMillis()) + dataStore.updateData { preferences: WindowingEducationProto -> + preferences.toBuilder().setAppHandleEducation(currentAppHandleProto).build() + } } - } - companion object { - private const val TAG = "AppHandleEducationDatastoreRepository" - private const val APP_HANDLE_EDUCATION_DATASTORE_FILEPATH = "app_handle_education.pb" + companion object { + private const val TAG = "AppHandleEducationDatastoreRepository" + private const val APP_HANDLE_EDUCATION_DATASTORE_FILEPATH = "app_handle_education.pb" - object WindowingEducationProtoSerializer : Serializer<WindowingEducationProto> { + object WindowingEducationProtoSerializer : Serializer<WindowingEducationProto> { - override val defaultValue: WindowingEducationProto = - WindowingEducationProto.getDefaultInstance() + override val defaultValue: WindowingEducationProto = + WindowingEducationProto.getDefaultInstance() - override suspend fun readFrom(input: InputStream): WindowingEducationProto = - try { - WindowingEducationProto.parseFrom(input) - } catch (exception: InvalidProtocolBufferException) { - throw CorruptionException("Cannot read proto.", exception) - } + override suspend fun readFrom(input: InputStream): WindowingEducationProto = + try { + WindowingEducationProto.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } - override suspend fun writeTo(windowingProto: WindowingEducationProto, output: OutputStream) = - windowingProto.writeTo(output) + override suspend fun writeTo( + windowingProto: WindowingEducationProto, + output: OutputStream, + ) = windowingProto.writeTo(output) + } } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt index 8be6e6dff6fe..e5ad901d1435 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt @@ -25,12 +25,12 @@ import androidx.datastore.core.Serializer import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.internal.annotations.VisibleForTesting -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.first import java.io.IOException import java.io.InputStream import java.io.OutputStream +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first /** Updates data in App-to-Web's education datastore. */ class AppToWebEducationDatastoreRepository @@ -41,7 +41,9 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { ) : this( DataStoreFactory.create( serializer = WindowingEducationProtoSerializer, - produceFile = { context.dataStoreFile(APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH) })) + produceFile = { context.dataStoreFile(APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH) }, + ) + ) /** Provides dataStore.data flow and handles exceptions thrown during collection */ val dataStoreFlow: Flow<WindowingEducationProto> = @@ -51,8 +53,10 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { Slog.e( TAG, "Error in reading App-to-Web education related data from datastore," + - "data is stored in a file named" + - "$APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH", exception) + "data is stored in a file named" + + "$APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH", + exception, + ) } else { throw exception } @@ -72,26 +76,26 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { dataStore.updateData { preferences -> if (isViewed) { preferences - .toBuilder().setFeatureUsedTimestampMillis(System.currentTimeMillis()).build() + .toBuilder() + .setFeatureUsedTimestampMillis(System.currentTimeMillis()) + .build() } else { preferences.toBuilder().clearFeatureUsedTimestampMillis().build() } } } - /** - * Increases [AppToWebEducation.educationShownCount] field by one. - */ + /** Increases [AppToWebEducation.educationShownCount] field by one. */ suspend fun updateEducationShownCount() { val currentAppHandleProto = windowingEducationProto().appToWebEducation.toBuilder() - currentAppHandleProto - .setEducationShownCount(currentAppHandleProto.getEducationShownCount() + 1) + currentAppHandleProto.setEducationShownCount( + currentAppHandleProto.getEducationShownCount() + 1 + ) dataStore.updateData { preferences -> preferences.toBuilder().setAppToWebEducation(currentAppHandleProto).build() } } - companion object { private const val TAG = "AppToWebEducationDatastoreRepository" private const val APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH = "app_to_web_education.pb" @@ -110,7 +114,7 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { override suspend fun writeTo( windowingProto: WindowingEducationProto, - output: OutputStream + output: OutputStream, ) = windowingProto.writeTo(output) } } |