• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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