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.core
18 
19 import androidx.compose.foundation.layout.Arrangement
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.Column
22 import androidx.compose.foundation.layout.Row
23 import androidx.compose.foundation.layout.WindowInsets
24 import androidx.compose.foundation.layout.WindowInsetsSides
25 import androidx.compose.foundation.layout.fillMaxHeight
26 import androidx.compose.foundation.layout.fillMaxSize
27 import androidx.compose.foundation.layout.fillMaxWidth
28 import androidx.compose.foundation.layout.offset
29 import androidx.compose.foundation.layout.only
30 import androidx.compose.foundation.layout.padding
31 import androidx.compose.foundation.layout.systemBars
32 import androidx.compose.foundation.layout.windowInsetsPadding
33 import androidx.compose.material3.ExperimentalMaterial3Api
34 import androidx.compose.material3.MaterialTheme
35 import androidx.compose.material3.ModalBottomSheet
36 import androidx.compose.material3.rememberModalBottomSheetState
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.CompositionLocalProvider
39 import androidx.compose.ui.Alignment
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.graphics.Color
42 import androidx.compose.ui.unit.IntOffset
43 import androidx.compose.ui.unit.dp
44 import androidx.navigation.compose.rememberNavController
45 import com.android.photopicker.core.features.LocalFeatureManager
46 import com.android.photopicker.core.features.Location
47 import com.android.photopicker.core.features.LocationParams
48 import com.android.photopicker.core.navigation.LocalNavController
49 import com.android.photopicker.core.navigation.PhotopickerNavGraph
50 import com.android.photopicker.data.model.Media
51 import kotlinx.coroutines.CompletableDeferred
52 import kotlinx.coroutines.flow.Flow
53 
54 private val MEASUREMENT_BOTTOM_SHEET_EDGE_PADDING = 12.dp
55 
56 /**
57  * This is an entrypoint of the Photopicker Compose UI. This is called from the MainActivity and is
58  * the top-most [@Composable] in the activity application. This should not be called except inside
59  * an Activity's [setContent] block.
60  *
61  * @param onDismissRequest handler for when the BottomSheet is dismissed.
62  */
63 @OptIn(ExperimentalMaterial3Api::class)
64 @Composable
PhotopickerAppWithBottomSheetnull65 fun PhotopickerAppWithBottomSheet(
66     onDismissRequest: () -> Unit,
67     onMediaSelectionConfirmed: () -> Unit,
68     preloadMedia: Flow<Set<Media>>,
69     obtainPreloaderDeferred: () -> CompletableDeferred<Boolean>,
70 ) {
71     // Initialize and remember the NavController. This needs to be provided before the call to
72     // the NavigationGraph, so this is done at the top.
73     val navController = rememberNavController()
74     val state = rememberModalBottomSheetState()
75     // Provide the NavController to the rest of the Compose stack.
76     CompositionLocalProvider(LocalNavController provides navController) {
77         Column(
78             modifier =
79                 // Apply WindowInsets to this wrapping column to prevent the Bottom Sheet
80                 // from drawing over the system bars.
81                 Modifier.windowInsetsPadding(
82                     WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
83                 )
84         ) {
85             ModalBottomSheet(
86                 sheetState = state,
87                 onDismissRequest = onDismissRequest,
88                 scrimColor = Color.Transparent,
89                 containerColor = MaterialTheme.colorScheme.surfaceContainer,
90                 contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
91                 contentWindowInsets = { WindowInsets.systemBars },
92             ) {
93                 Box(
94                     modifier = Modifier.fillMaxHeight(),
95                     contentAlignment = Alignment.BottomCenter
96                 ) {
97                     PhotopickerMain()
98                     Column(
99                         modifier =
100                             // Some elements needs to be drawn over the UI inside of the
101                             // BottomSheet A negative y offset will move it from the bottom of the
102                             // content to the bottom of the onscreen BottomSheet.
103                             Modifier.offset {
104                                 IntOffset(x = 0, y = -state.requireOffset().toInt())
105                             },
106                     ) {
107                         LocalFeatureManager.current.composeLocation(
108                             Location.SNACK_BAR,
109                             maxSlots = 1,
110                         )
111                         LocalFeatureManager.current.composeLocation(
112                             Location.SELECTION_BAR,
113                             maxSlots = 1,
114                             params = LocationParams.WithClickAction { onMediaSelectionConfirmed() }
115                         )
116                     }
117                 }
118                 // If a [MEDIA_PRELOADER] is configured in the current session, attach it
119                 // to the compose UI here, so that any dialogs it shows are drawn overtop
120                 // of the application.
121                 LocalFeatureManager.current.composeLocation(
122                     Location.MEDIA_PRELOADER,
123                     maxSlots = 1,
124                     params =
125                         object : LocationParams.WithMediaPreloader {
126                             override fun obtainDeferred(): CompletableDeferred<Boolean> {
127                                 return obtainPreloaderDeferred()
128                             }
129 
130                             override val preloadMedia = preloadMedia
131                         }
132                 )
133             }
134         }
135     }
136 }
137 
138 /**
139  * This is an entrypoint of the Photopicker Compose UI. This is called from a hosting View and is
140  * the top-most [@Composable] in the view based application. This should not be called by any
141  * Activity code, and should only be called inside of the ComposeView [setContent] block.
142  */
143 @OptIn(ExperimentalMaterial3Api::class)
144 @Composable
PhotopickerAppnull145 fun PhotopickerApp() {
146     // Initialize and remember the NavController. This needs to be provided before the call to
147     // the NavigationGraph, so this is done at the top.
148     val navController = rememberNavController()
149 
150     // Provide the NavController to the rest of the Compose stack.
151     CompositionLocalProvider(LocalNavController provides navController) { PhotopickerMain() }
152 }
153 
154 /**
155  * This is the shared entrypoint for the Photopicker compose-UI. Composables above this function
156  * must provide the required dependencies to the compose UI before calling this entrypoint.
157  *
158  * It is presumed after this composable the compose UI can either be running inside of a wrapped
159  * View or an Activity lifecycle.
160  *
161  * By this entrypoint, the expected CompositionLocals should already exist:
162  * - LocalEvents
163  * - LocalFeatureManager
164  * - LocalNavController
165  * - LocalPhotopickerConfiguration
166  * - LocalSelection
167  * - PhotopickerTheme
168  */
169 @Composable
PhotopickerMainnull170 fun PhotopickerMain() {
171     Box(modifier = Modifier.fillMaxSize()) {
172         Column {
173             // The navigation bar and profile switcher are drawn above the navigation graph
174             Row(
175                 modifier =
176                     Modifier.fillMaxWidth()
177                         .padding(horizontal = MEASUREMENT_BOTTOM_SHEET_EDGE_PADDING),
178                 horizontalArrangement = Arrangement.SpaceBetween,
179                 verticalAlignment = Alignment.CenterVertically,
180             ) {
181                 LocalFeatureManager.current.composeLocation(
182                     Location.PROFILE_SELECTOR,
183                     maxSlots = 1,
184                     // Weight should match the overflow menu slot so they are the same size.
185                     modifier = Modifier.weight(1f),
186                 )
187                 LocalFeatureManager.current.composeLocation(
188                     Location.NAVIGATION_BAR,
189                     maxSlots = 1,
190                     modifier = Modifier,
191                 )
192                 LocalFeatureManager.current.composeLocation(
193                     Location.OVERFLOW_MENU,
194                     // Weight should match the profile switcher slot so they are the same size.
195                     modifier = Modifier.weight(1f),
196                 )
197             }
198             // Initialize the navigation graph.
199             PhotopickerNavGraph()
200         }
201     }
202 }
203