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