1 /*
<lambda>null2  * 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.core.navigation
18 
19 import android.util.Log
20 import androidx.compose.animation.EnterTransition
21 import androidx.compose.animation.ExitTransition
22 import androidx.compose.runtime.Composable
23 import androidx.compose.ui.window.DialogProperties
24 import androidx.navigation.NamedNavArgument
25 import androidx.navigation.NavBackStackEntry
26 import androidx.navigation.NavDeepLink
27 import androidx.navigation.NavGraphBuilder
28 import androidx.navigation.compose.NavHost
29 import androidx.navigation.compose.composable
30 import androidx.navigation.compose.dialog
31 import com.android.photopicker.MainActivity
32 import com.android.photopicker.core.features.FeatureManager
33 import com.android.photopicker.core.features.LocalFeatureManager
34 import com.android.photopicker.core.features.PhotopickerUiFeature
35 import com.android.photopicker.core.features.Priority
36 
37 /**
38  * The main composable for Photopicker's feature navigation.
39  *
40  * This composable will generate a [NavHost] element with a populated [NavGraph] with all registered
41  * [Route]s that are provided by the currently enabled feature set in the [FeatureManager].
42  */
43 @Composable
44 fun PhotopickerNavGraph() {
45 
46     val featureManager = LocalFeatureManager.current
47     val navController = LocalNavController.current
48 
49     NavHost(
50         navController = navController,
51         startDestination = getStartDestination(featureManager.enabledUiFeatures).route,
52         builder = { this.setupFeatureRoutesForNavigation(featureManager) },
53         // Disable all transitions by default so that routes fully control the transition logic.
54         enterTransition = { EnterTransition.None },
55         exitTransition = { ExitTransition.None },
56         popEnterTransition = { EnterTransition.None },
57         popExitTransition = { ExitTransition.None },
58     )
59 }
60 
61 /**
62  * Extend the NavGraphBuilder to provide a hook for the NavGraph to be dynamically constructed based
63  * on the state present in the [FeatureManager].
64  *
65  * This will construct a navigation graph that contains exposed [Route]s from all enabled
66  * [PhotopickerUiFeature]s at runtime.
67  */
NavGraphBuildernull68 private fun NavGraphBuilder.setupFeatureRoutesForNavigation(
69     featureManager: FeatureManager,
70 ) {
71 
72     // Create a flat set of all registered routes, across all features.
73     var allRoutes = featureManager.enabledUiFeatures.flatMap { it.registerNavigationRoutes() }
74 
75     // This is to ensure there is always at least one route registered in the graph.
76     // In the event no enabled features expose routes, this  will provide an empty default.
77     if (allRoutes.size == 0) {
78         Log.w(
79             MainActivity.TAG,
80             "There were no registered feature routes. Defaulting to an empty NavigationGraph"
81         )
82         allRoutes = listOf(getStartDestination(featureManager.enabledUiFeatures))
83     }
84 
85     // For all of the collected routes, register them into the graph based on their [Route]
86     // declaration.
87     for (route in allRoutes) {
88         if (route.isDialog) {
89             dialog(
90                 route = route.route,
91                 arguments = route.arguments,
92                 deepLinks = route.deepLinks,
93                 dialogProperties = route.dialogProperties ?: DialogProperties()
94             ) { backStackEntry ->
95                 route.composable(backStackEntry)
96             }
97         } else {
98             composable(
99                 route = route.route,
100                 arguments = route.arguments,
101                 deepLinks = route.deepLinks,
102                 enterTransition = route.enterTransition,
103                 exitTransition = route.exitTransition,
104                 popEnterTransition = route.popEnterTransition,
105                 popExitTransition = route.popExitTransition,
106             ) { backStackEntry ->
107                 route.composable(backStackEntry)
108             }
109         }
110     }
111 }
112 
113 /**
114  * Inspects the enabled UI features and locates the [Route] with the highest declared priority. In
115  * the event of a tie, the Registration order will resolve the tie.
116  *
117  * In the event there are no declared routes, this returns an empty route object to prevent the UI
118  * from crashing.
119  *
120  * @return The starting route for initializing the Navigation Graph.
121  */
getStartDestinationnull122 private fun getStartDestination(enabledUiFeatures: Set<PhotopickerUiFeature>): Route {
123 
124     val allRoutes = enabledUiFeatures.flatMap { it.registerNavigationRoutes() }
125 
126     return allRoutes.maxByOrNull { it.initialRoutePriority }
127     // A default blank route in case no route exists.
128     ?: object : Route {
129             override val route = PhotopickerDestinations.DEFAULT.route
130             override val initialRoutePriority = Priority.LOW.priority
131             override val arguments = emptyList<NamedNavArgument>()
132             override val deepLinks = emptyList<NavDeepLink>()
133             override val isDialog = false
134             override val dialogProperties = null
135             override val enterTransition = null
136             override val exitTransition = null
137             override val popEnterTransition = null
138             override val popExitTransition = null
139             @Composable override fun composable(navBackStackEntry: NavBackStackEntry?) {}
140         }
141 }
142