1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.desktopmode.education 18 19 import android.annotation.DimenRes 20 import android.annotation.StringRes 21 import android.app.ActivityManager.RunningTaskInfo 22 import android.content.Context 23 import android.content.res.Resources 24 import android.graphics.Point 25 import android.os.SystemProperties 26 import androidx.compose.ui.graphics.toArgb 27 import com.android.window.flags.Flags 28 import com.android.wm.shell.R 29 import com.android.wm.shell.desktopmode.CaptionState 30 import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository 31 import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository 32 import com.android.wm.shell.shared.annotations.ShellBackgroundThread 33 import com.android.wm.shell.shared.annotations.ShellMainThread 34 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode 35 import com.android.wm.shell.windowdecor.common.DecorThemeUtil 36 import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController 37 import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController.EducationColorScheme 38 import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController.EducationViewConfig 39 import kotlinx.coroutines.CoroutineScope 40 import kotlinx.coroutines.MainCoroutineDispatcher 41 import kotlinx.coroutines.flow.Flow 42 import kotlinx.coroutines.flow.collectLatest 43 import kotlinx.coroutines.flow.debounce 44 import kotlinx.coroutines.flow.distinctUntilChanged 45 import kotlinx.coroutines.flow.emptyFlow 46 import kotlinx.coroutines.flow.filter 47 import kotlinx.coroutines.flow.first 48 import kotlinx.coroutines.flow.flatMapLatest 49 import kotlinx.coroutines.flow.flowOn 50 import kotlinx.coroutines.flow.map 51 import kotlinx.coroutines.launch 52 53 /** 54 * Controls App-to-Web education end to end. 55 * 56 * Listen to usages of App-to-Web, calls an api to check if the education should be shown and 57 * controls education UI. 58 */ 59 @OptIn(kotlinx.coroutines.FlowPreview::class) 60 @kotlinx.coroutines.ExperimentalCoroutinesApi 61 class AppToWebEducationController( 62 private val context: Context, 63 private val appToWebEducationFilter: AppToWebEducationFilter, 64 private val appToWebEducationDatastoreRepository: AppToWebEducationDatastoreRepository, 65 private val windowDecorCaptionHandleRepository: WindowDecorCaptionHandleRepository, 66 private val windowingEducationViewController: DesktopWindowingEducationPromoController, 67 @ShellMainThread private val applicationCoroutineScope: CoroutineScope, 68 @ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher, 69 ) { 70 private val decorThemeUtil = DecorThemeUtil(context) 71 72 init { 73 runIfEducationFeatureEnabled { 74 applicationCoroutineScope.launch { 75 // Central block handling the App-to-Web's educational flow end-to-end. 76 isEducationViewLimitReachedFlow() 77 .flatMapLatest { countExceedsMaximum -> 78 if (countExceedsMaximum) { 79 // If the education has been viewed the maximum amount of times then 80 // return emptyFlow() that completes immediately. This will help us to 81 // not listen to [captionHandleStateFlow] after the education should 82 // not be shown. 83 emptyFlow() 84 } else { 85 // Listen for changes to window decor's caption. 86 windowDecorCaptionHandleRepository.captionStateFlow 87 // Wait for few seconds before emitting the latest state. 88 .debounce(APP_TO_WEB_EDUCATION_DELAY_MILLIS) 89 .filter { captionState -> 90 captionState !is CaptionState.NoCaption && 91 appToWebEducationFilter.shouldShowAppToWebEducation( 92 captionState 93 ) 94 } 95 } 96 } 97 .flowOn(backgroundDispatcher) 98 .collectLatest { captionState -> 99 val educationColorScheme = educationColorScheme(captionState) 100 showEducation(captionState, educationColorScheme!!) 101 // After showing first tooltip, increase count of education views 102 appToWebEducationDatastoreRepository.updateEducationShownCount() 103 } 104 } 105 106 applicationCoroutineScope.launch { 107 if (isFeatureUsed()) return@launch 108 windowDecorCaptionHandleRepository.appToWebUsageFlow.collect { 109 // If user utilizes App-to-Web, mark user has used the feature 110 appToWebEducationDatastoreRepository.updateFeatureUsedTimestampMillis( 111 isViewed = true 112 ) 113 } 114 } 115 } 116 } 117 118 private inline fun runIfEducationFeatureEnabled(block: () -> Unit) { 119 if ( 120 canEnterDesktopMode(context) && 121 Flags.enableDesktopWindowingAppToWebEducationIntegration() 122 ) { 123 block() 124 } 125 } 126 127 private fun showEducation(captionState: CaptionState, colorScheme: EducationColorScheme) { 128 val educationGlobalCoordinates: Point 129 val taskId: Int 130 when (captionState) { 131 is CaptionState.AppHandle -> { 132 val appHandleBounds = captionState.globalAppHandleBounds 133 val educationWidth = 134 loadDimensionPixelSize(R.dimen.desktop_windowing_education_promo_width) 135 educationGlobalCoordinates = 136 Point(appHandleBounds.centerX() - educationWidth / 2, appHandleBounds.bottom) 137 taskId = captionState.runningTaskInfo.taskId 138 } 139 140 is CaptionState.AppHeader -> { 141 val taskBounds = 142 captionState.runningTaskInfo.configuration.windowConfiguration.bounds 143 educationGlobalCoordinates = 144 Point(taskBounds.left, captionState.globalAppChipBounds.bottom) 145 taskId = captionState.runningTaskInfo.taskId 146 } 147 148 else -> return 149 } 150 151 // Populate information important to inflate education promo. 152 val educationConfig = 153 EducationViewConfig( 154 viewLayout = R.layout.desktop_windowing_education_promo, 155 educationColorScheme = colorScheme, 156 viewGlobalCoordinates = educationGlobalCoordinates, 157 educationText = getString(R.string.desktop_windowing_app_to_web_education_text), 158 widthId = R.dimen.desktop_windowing_education_promo_width, 159 heightId = R.dimen.desktop_windowing_education_promo_height, 160 ) 161 162 windowingEducationViewController.showEducation( 163 viewConfig = educationConfig, 164 taskId = taskId, 165 ) 166 } 167 168 private fun educationColorScheme(captionState: CaptionState): EducationColorScheme? { 169 val taskInfo: RunningTaskInfo = 170 when (captionState) { 171 is CaptionState.AppHandle -> captionState.runningTaskInfo 172 is CaptionState.AppHeader -> captionState.runningTaskInfo 173 else -> return null 174 } 175 176 val colorScheme = decorThemeUtil.getColorScheme(taskInfo) 177 val tooltipContainerColor = colorScheme.surfaceBright.toArgb() 178 val tooltipTextColor = colorScheme.onSurface.toArgb() 179 return EducationColorScheme(tooltipContainerColor, tooltipTextColor) 180 } 181 182 /** 183 * Listens to changes in the number of times the education has been viewed, mapping the count to 184 * true if the education has been viewed the maximum amount of times. 185 */ 186 private fun isEducationViewLimitReachedFlow(): Flow<Boolean> = 187 appToWebEducationDatastoreRepository.dataStoreFlow 188 .map { preferences -> appToWebEducationFilter.isEducationViewLimitReached(preferences) } 189 .distinctUntilChanged() 190 191 /** 192 * Listens to the changes to [WindowingEducationProto#hasFeatureUsedTimestampMillis()] in 193 * datastore proto object. 194 */ 195 private suspend fun isFeatureUsed(): Boolean = 196 appToWebEducationDatastoreRepository.dataStoreFlow.first().hasFeatureUsedTimestampMillis() 197 198 private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { 199 if (resourceId == Resources.ID_NULL) return 0 200 return context.resources.getDimensionPixelSize(resourceId) 201 } 202 203 private fun getString(@StringRes resId: Int): String = context.resources.getString(resId) 204 205 companion object { 206 const val TAG = "AppToWebEducationController" 207 val APP_TO_WEB_EDUCATION_DELAY_MILLIS: Long 208 get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L) 209 } 210 } 211