1 /*
2  * Copyright 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.photopicker.features.highpriorityuifeature
18 
19 import androidx.compose.foundation.layout.Column
20 import androidx.compose.foundation.layout.fillMaxSize
21 import androidx.compose.material3.Button
22 import androidx.compose.material3.Surface
23 import androidx.compose.material3.Text
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.getValue
26 import androidx.compose.runtime.setValue
27 import androidx.compose.ui.Modifier
28 import androidx.compose.ui.window.DialogProperties
29 import androidx.navigation.NamedNavArgument
30 import androidx.navigation.NavBackStackEntry
31 import androidx.navigation.NavDeepLink
32 import com.android.photopicker.core.banners.Banner
33 import com.android.photopicker.core.banners.BannerDefinitions
34 import com.android.photopicker.core.banners.BannerState
35 import com.android.photopicker.core.configuration.PhotopickerConfiguration
36 import com.android.photopicker.core.events.RegisteredEventClass
37 import com.android.photopicker.core.features.FeatureManager
38 import com.android.photopicker.core.features.FeatureRegistration
39 import com.android.photopicker.core.features.LocalFeatureManager
40 import com.android.photopicker.core.features.Location
41 import com.android.photopicker.core.features.LocationParams
42 import com.android.photopicker.core.features.PhotopickerUiFeature
43 import com.android.photopicker.core.features.PrefetchResultKey
44 import com.android.photopicker.core.features.Priority
45 import com.android.photopicker.core.navigation.LocalNavController
46 import com.android.photopicker.core.navigation.PhotopickerDestinations.ALBUM_GRID
47 import com.android.photopicker.core.navigation.PhotopickerDestinations.PHOTO_GRID
48 import com.android.photopicker.core.navigation.Route
49 import com.android.photopicker.core.user.UserMonitor
50 import com.android.photopicker.data.DataService
51 import com.android.photopicker.features.simpleuifeature.SimpleUiFeature
52 import kotlinx.coroutines.Deferred
53 
54 /**
55  * Test [PhotopickerUiFeature] that renders a simple string to [Location.COMPOSE_TOP] with the
56  * utmost priority.
57  */
58 class HighPriorityUiFeature : PhotopickerUiFeature {
59 
60     companion object Registration : FeatureRegistration {
61         override val TAG: String = "HighPriorityUiFeature"
62 
isEnablednull63         override fun isEnabled(
64             config: PhotopickerConfiguration,
65             deferredPrefetchResultsMap: Map<PrefetchResultKey, Deferred<Any?>>,
66         ) = true
67 
68         override fun build(featureManager: FeatureManager) = HighPriorityUiFeature()
69 
70         val UI_STRING = "I'm super important."
71         val START_ROUTE = "highpriority/start"
72         val START_STRING = "I'm the start location."
73         val DIALOG_ROUTE = "highpriority/dialog"
74         val DIALOG_STRING = "I'm the dialog location."
75     }
76 
77     override val token = TAG
78 
79     /** Only one banner is claimed */
80     override val ownedBanners = setOf(BannerDefinitions.CLOUD_CHOOSE_ACCOUNT)
81 
82     override suspend fun getBannerPriority(
83         banner: BannerDefinitions,
84         bannerState: BannerState?,
85         config: PhotopickerConfiguration,
86         dataService: DataService,
87         userMonitor: UserMonitor,
88     ): Int {
89         // If the banner reports as being dismissed, don't show it.
90         if (bannerState?.dismissed == true) {
91             return Priority.DISABLED.priority
92         }
93 
94         // Otherwise, show it with medium priority.
95         return Priority.HIGH.priority
96     }
97 
buildBannernull98     override suspend fun buildBanner(
99         banner: BannerDefinitions,
100         dataService: DataService,
101         userMonitor: UserMonitor,
102     ): Banner {
103         return object : Banner {
104             override val declaration = BannerDefinitions.CLOUD_CHOOSE_ACCOUNT
105 
106             @Composable override fun buildTitle() = "Choose Account Title"
107 
108             @Composable override fun buildMessage() = "Choose Account Message"
109         }
110     }
111 
112     /** Events consumed by the Photo grid */
113     override val eventsConsumed = emptySet<RegisteredEventClass>()
114 
115     /** Events produced by the Photo grid */
116     override val eventsProduced = emptySet<RegisteredEventClass>()
117 
118     /** Compose Location callback from feature framework */
registerLocationsnull119     override fun registerLocations(): List<Pair<Location, Int>> {
120         return listOf(Pair(Location.COMPOSE_TOP, Priority.HIGH.priority))
121     }
122 
123     /** Navigation registration callback from feature framework */
registerNavigationRoutesnull124     override fun registerNavigationRoutes(): Set<Route> {
125         return setOf(
126             object : Route {
127                 override val route = START_ROUTE
128                 override val initialRoutePriority = Priority.HIGH.priority
129                 override val arguments = emptyList<NamedNavArgument>()
130                 override val deepLinks = emptyList<NavDeepLink>()
131                 override val isDialog = false
132                 override val dialogProperties = null
133                 override val enterTransition = null
134                 override val exitTransition = null
135                 override val popEnterTransition = null
136                 override val popExitTransition = null
137 
138                 @Composable
139                 override fun composable(navBackStackEntry: NavBackStackEntry?) {
140                     start()
141                 }
142             },
143             object : Route {
144                 override val route = DIALOG_ROUTE
145                 override val initialRoutePriority = Priority.REGISTRATION_ORDER.priority
146                 override val arguments = emptyList<NamedNavArgument>()
147                 override val deepLinks = emptyList<NavDeepLink>()
148                 override val isDialog = true
149                 override val dialogProperties = DialogProperties(usePlatformDefaultWidth = false)
150                 override val enterTransition = null
151                 override val exitTransition = null
152                 override val popEnterTransition = null
153                 override val popExitTransition = null
154 
155                 @Composable
156                 override fun composable(navBackStackEntry: NavBackStackEntry?) {
157                     dialog()
158                 }
159             },
160 
161             // This is implemented for PhotopickerNavGraphTest
162             object : Route {
163                 override val route = PHOTO_GRID.route
164                 override val initialRoutePriority = Priority.LAST.priority
165                 override val arguments = emptyList<NamedNavArgument>()
166                 override val deepLinks = emptyList<NavDeepLink>()
167                 override val isDialog = false
168                 override val dialogProperties = null
169                 override val enterTransition = null
170                 override val exitTransition = null
171                 override val popEnterTransition = null
172                 override val popExitTransition = null
173 
174                 @Composable override fun composable(navBackStackEntry: NavBackStackEntry?) {}
175             },
176             // This is implemented for PhotopickerNavGraphTest
177             object : Route {
178                 override val route = ALBUM_GRID.route
179                 override val initialRoutePriority = Priority.LAST.priority
180                 override val arguments = emptyList<NamedNavArgument>()
181                 override val deepLinks = emptyList<NavDeepLink>()
182                 override val isDialog = false
183                 override val dialogProperties = null
184                 override val enterTransition = null
185                 override val exitTransition = null
186                 override val popEnterTransition = null
187                 override val popExitTransition = null
188 
189                 @Composable override fun composable(navBackStackEntry: NavBackStackEntry?) {}
190             },
191         )
192     }
193 
194     /* Feature framework compose-at-location callback */
195     @Composable
composenull196     override fun compose(location: Location, modifier: Modifier, params: LocationParams) {
197         when (location) {
198             Location.COMPOSE_TOP -> composeTop()
199             else -> {}
200         }
201     }
202 
203     /* Private composable used for the [Location.COMPOSE_TOP] location */
204     @Composable
composeTopnull205     private fun composeTop() {
206         Text(UI_STRING)
207     }
208 
209     /** Composes a dialog location, with a button to navigate back to the [START_ROUTE] */
210     @Composable
dialognull211     private fun dialog() {
212         val navController = LocalNavController.current
213         Surface(modifier = Modifier.fillMaxSize()) {
214             Column {
215                 Text(DIALOG_STRING)
216                 Button(onClick = { navController.navigate(START_ROUTE) }) {
217                     Text("navigate to start")
218                 }
219             }
220         }
221     }
222 
223     /**
224      * Composes the test start location, and if the [SimpleUiFeature] is enabled, also includes a
225      * button that navigates to the [SimpleUiFeature.SIMPLE_ROUTE]
226      */
227     @Composable
startnull228     private fun start() {
229         val featureManager = LocalFeatureManager.current
230         val navController = LocalNavController.current
231         Surface(modifier = Modifier.fillMaxSize()) {
232             Column {
233                 Text(START_STRING)
234 
235                 Button(onClick = { navController.navigate(DIALOG_ROUTE) }) {
236                     Text("navigate to dialog")
237                 }
238 
239                 // Optionally add a navigation button if SimpleUiFeature is enabled.
240                 if (featureManager.isFeatureEnabled(SimpleUiFeature::class.java)) {
241                     Button(onClick = { navController.navigate(SimpleUiFeature.SIMPLE_ROUTE) }) {
242                         Text("navigate to simple ui")
243                     }
244                 }
245             }
246         }
247     }
248 }
249