Skip to content

Commit cfdc59b

Browse files
committed
Merge branch 'main' into feature/xr/creation-screen
2 parents 113bc74 + be1acea commit cfdc59b

File tree

28 files changed

+456
-281
lines changed

28 files changed

+456
-281
lines changed

app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,21 @@ import androidx.compose.runtime.remember
3232
import androidx.compose.runtime.setValue
3333
import androidx.compose.ui.platform.LocalContext
3434
import androidx.compose.ui.unit.IntOffset
35+
import androidx.hilt.navigation.compose.hiltViewModel
3536
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
3637
import androidx.navigation3.runtime.entry
3738
import androidx.navigation3.runtime.entryProvider
3839
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
3940
import androidx.navigation3.ui.NavDisplay
4041
import com.android.developers.androidify.camera.CameraPreviewScreen
4142
import com.android.developers.androidify.creation.CreationScreen
43+
import com.android.developers.androidify.creation.CreationViewModel
44+
import com.android.developers.androidify.customize.CustomizeAndExportScreen
45+
import com.android.developers.androidify.customize.CustomizeExportViewModel
4246
import com.android.developers.androidify.home.AboutScreen
4347
import com.android.developers.androidify.home.HomeScreen
48+
import com.android.developers.androidify.results.ResultsScreen
49+
import com.android.developers.androidify.results.ResultsViewModel
4450
import com.android.developers.androidify.theme.transitions.ColorSplashTransitionScreen
4551
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
4652

@@ -92,14 +98,20 @@ fun MainNavigation() {
9298
CameraPreviewScreen(
9399
onImageCaptured = { uri ->
94100
backStack.removeAll { it is Create }
95-
backStack.add(Create(uri.toString()))
101+
backStack.add(Create(uri))
96102
backStack.removeAll { it is Camera }
97103
},
98104
)
99105
}
100106
entry<Create> { createKey ->
107+
val creationViewModel = hiltViewModel<CreationViewModel, CreationViewModel.Factory>(
108+
creationCallback = { factory ->
109+
factory.create(
110+
originalImageUrl = createKey.fileName,
111+
)
112+
},
113+
)
101114
CreationScreen(
102-
createKey.fileName,
103115
onCameraPressed = {
104116
backStack.removeAll { it is Camera }
105117
backStack.add(Camera)
@@ -110,6 +122,64 @@ fun MainNavigation() {
110122
onAboutPressed = {
111123
backStack.add(About)
112124
},
125+
onImageCreated = { resultImageUri, prompt, originalImageUri ->
126+
backStack.removeAll { it is Result }
127+
backStack.add(
128+
Result(
129+
resultImageUri = resultImageUri,
130+
prompt = prompt,
131+
originalImageUri = originalImageUri,
132+
),
133+
)
134+
},
135+
creationViewModel = creationViewModel,
136+
)
137+
}
138+
entry<Result> { resultKey ->
139+
val resultsViewModel = hiltViewModel<ResultsViewModel, ResultsViewModel.Factory>(
140+
creationCallback = { factory ->
141+
factory.create(
142+
resultImageUrl = resultKey.resultImageUri,
143+
originalImageUrl = resultKey.originalImageUri,
144+
promptText = resultKey.prompt,
145+
)
146+
},
147+
)
148+
ResultsScreen(
149+
onNextPress = { resultImageUri, originalImageUri ->
150+
backStack.add(
151+
CustomizeExport(
152+
resultImageUri = resultImageUri,
153+
originalImageUri = originalImageUri,
154+
),
155+
)
156+
},
157+
onAboutPress = {
158+
backStack.add(About)
159+
},
160+
onBackPress = {
161+
backStack.removeLastOrNull()
162+
},
163+
viewModel = resultsViewModel,
164+
)
165+
}
166+
entry<CustomizeExport> { shareKey ->
167+
val customizeExportViewModel = hiltViewModel<CustomizeExportViewModel, CustomizeExportViewModel.Factory>(
168+
creationCallback = { factory ->
169+
factory.create(
170+
resultImageUrl = shareKey.resultImageUri,
171+
originalImageUrl = shareKey.originalImageUri,
172+
)
173+
},
174+
)
175+
CustomizeAndExportScreen(
176+
onBackPress = {
177+
backStack.removeLastOrNull()
178+
},
179+
onInfoPress = {
180+
backStack.add(About)
181+
},
182+
viewModel = customizeExportViewModel,
113183
)
114184
}
115185
entry<About> {

app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package com.android.developers.androidify.navigation
1919

20+
import android.net.Uri
2021
import kotlinx.serialization.ExperimentalSerializationApi
2122
import kotlinx.serialization.Serializable
2223

@@ -26,10 +27,39 @@ sealed interface NavigationRoute
2627
data object Home : NavigationRoute
2728

2829
@Serializable
29-
data class Create(val fileName: String? = null, val prompt: String? = null) : NavigationRoute
30+
data class Create(
31+
@Serializable(with = UriSerializer::class) val fileName: Uri? = null,
32+
val prompt: String? = null,
33+
) : NavigationRoute
3034

3135
@Serializable
3236
object Camera : NavigationRoute
3337

3438
@Serializable
3539
object About : NavigationRoute
40+
41+
/**
42+
* Represents the result of an image generation process, used for navigation.
43+
*
44+
* @param resultImageUri The URI of the generated image.
45+
* @param originalImageUri The URI of the original image used as a base for generation, if any.
46+
* @param prompt The text prompt used to generate the image, if any.
47+
*/
48+
@Serializable
49+
data class Result(
50+
@Serializable(with = UriSerializer::class) val resultImageUri: Uri,
51+
@Serializable(with = UriSerializer::class) val originalImageUri: Uri? = null,
52+
val prompt: String? = null,
53+
) : NavigationRoute
54+
55+
/**
56+
* Represents the navigation route to the screen for customizing and exporting a generated image.
57+
*
58+
* @param resultImageUri The URI of the generated image to be customized.
59+
* @param originalImageUri The URI of the original image, passed along for context.
60+
*/
61+
@Serializable
62+
data class CustomizeExport(
63+
@Serializable(with = UriSerializer::class) val resultImageUri: Uri,
64+
@Serializable(with = UriSerializer::class) val originalImageUri: Uri?,
65+
) : NavigationRoute
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2025 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+
* https://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+
package com.android.developers.androidify.navigation
17+
18+
import android.net.Uri
19+
import androidx.core.net.toUri
20+
import kotlinx.serialization.KSerializer
21+
import kotlinx.serialization.descriptors.PrimitiveKind
22+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
23+
import kotlinx.serialization.descriptors.SerialDescriptor
24+
import kotlinx.serialization.encoding.Decoder
25+
import kotlinx.serialization.encoding.Encoder
26+
27+
object UriSerializer : KSerializer<Uri> {
28+
override val descriptor: SerialDescriptor =
29+
PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
30+
31+
override fun serialize(encoder: Encoder, value: Uri) {
32+
encoder.encodeString(value.toString())
33+
}
34+
35+
override fun deserialize(decoder: Decoder): Uri = decoder.decodeString().toUri()
36+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2025 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+
* https://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+
package com.android.developers.testing.data
17+
18+
import android.graphics.Bitmap
19+
20+
val bitmapSample = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)

core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,8 @@ class TestFileProvider : LocalFileProvider {
6464
): Uri {
6565
TODO("Not yet implemented")
6666
}
67+
68+
override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? {
69+
return bitmapSample
70+
}
6771
}

core/util/src/main/java/com/android/developers/androidify/util/LocalFileProvider.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.android.developers.androidify.util
1818
import android.app.Application
1919
import android.content.ContentValues
2020
import android.graphics.Bitmap
21+
import android.graphics.BitmapFactory
2122
import android.net.Uri
2223
import android.os.Build
2324
import android.os.Environment
@@ -53,6 +54,9 @@ interface LocalFileProvider {
5354

5455
@WorkerThread
5556
suspend fun saveUriToSharedStorage(inputUri: Uri, fileName: String, mimeType: String): Uri
57+
58+
@WorkerThread
59+
suspend fun loadBitmapFromUri(uri: Uri): Bitmap?
5660
}
5761

5862
@Singleton
@@ -120,6 +124,20 @@ class LocalFileProviderImpl @Inject constructor(
120124
return@withContext newUri
121125
}
122126

127+
override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? {
128+
return withContext(ioDispatcher) {
129+
try {
130+
application.contentResolver.openInputStream(uri)?.use {
131+
return@withContext BitmapFactory.decodeStream(it)
132+
}
133+
null
134+
} catch (e: Exception) {
135+
e.printStackTrace()
136+
null
137+
}
138+
}
139+
}
140+
123141
@Throws(IOException::class)
124142
@WorkerThread
125143
private fun saveFileToUri(file: File, uri: Uri) {

feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
package com.android.developers.androidify.creation
2424

25+
import android.net.Uri
2526
import androidx.activity.compose.BackHandler
2627
import androidx.activity.compose.rememberLauncherForActivityResult
2728
import androidx.activity.result.PickVisualMediaRequest
@@ -33,20 +34,16 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
3334
import androidx.compose.runtime.Composable
3435
import androidx.compose.runtime.LaunchedEffect
3536
import androidx.compose.runtime.getValue
36-
import androidx.core.net.toUri
37-
import androidx.hilt.navigation.compose.hiltViewModel
3837
import androidx.lifecycle.compose.collectAsStateWithLifecycle
39-
import com.android.developers.androidify.customize.CustomizeAndExportScreen
40-
import com.android.developers.androidify.customize.CustomizeExportViewModel
41-
import com.android.developers.androidify.results.ResultsScreen
38+
4239

4340
@Composable
4441
fun CreationScreen(
45-
fileName: String? = null,
46-
creationViewModel: CreationViewModel = hiltViewModel(),
42+
creationViewModel: CreationViewModel,
4743
onCameraPressed: () -> Unit = {},
4844
onBackPressed: () -> Unit,
4945
onAboutPressed: () -> Unit,
46+
onImageCreated: (resultImageUri: Uri, prompt: String?, originalImageUri: Uri?) -> Unit,
5047
) {
5148
val uiState by creationViewModel.uiState.collectAsStateWithLifecycle()
5249
val layoutType = calculateLayoutType(uiState.xrEnabled)
@@ -55,19 +52,28 @@ fun CreationScreen(
5552
) {
5653
creationViewModel.onBackPress()
5754
}
58-
LaunchedEffect(Unit) {
59-
if (fileName != null) {
60-
creationViewModel.onImageSelected(fileName.toUri())
61-
} else {
62-
creationViewModel.onImageSelected(null)
63-
}
64-
}
6555
val pickMedia = rememberLauncherForActivityResult(PickVisualMedia()) { uri ->
6656
if (uri != null) {
6757
creationViewModel.onImageSelected(uri)
6858
}
6959
}
7060
val snackbarHostState by creationViewModel.snackbarHostState.collectAsStateWithLifecycle()
61+
62+
LaunchedEffect(uiState.resultBitmapUri) {
63+
uiState.resultBitmapUri?.let { resultBitmapUri ->
64+
onImageCreated(
65+
resultBitmapUri,
66+
uiState.descriptionText.text.toString(),
67+
if (uiState.selectedPromptOption == PromptType.PHOTO) {
68+
uiState.imageUri
69+
} else {
70+
null
71+
},
72+
)
73+
creationViewModel.onResultDisplayed()
74+
}
75+
}
76+
7177
when (uiState.screenState) {
7278
ScreenState.EDIT -> {
7379
EditScreen(
@@ -95,45 +101,5 @@ fun CreationScreen(
95101
},
96102
)
97103
}
98-
99-
ScreenState.RESULT -> {
100-
val prompt = uiState.descriptionText.text.toString()
101-
val key = if (uiState.descriptionText.text.isBlank()) {
102-
uiState.imageUri.toString()
103-
} else {
104-
prompt
105-
}
106-
ResultsScreen(
107-
uiState.resultBitmap!!,
108-
if (uiState.selectedPromptOption == PromptType.PHOTO) {
109-
uiState.imageUri
110-
} else {
111-
null
112-
},
113-
promptText = prompt,
114-
viewModel = hiltViewModel(key = key),
115-
onAboutPress = onAboutPressed,
116-
onBackPress = onBackPressed,
117-
onNextPress = creationViewModel::customizeExportClicked,
118-
)
119-
}
120-
121-
ScreenState.CUSTOMIZE -> {
122-
val prompt = uiState.descriptionText.text.toString()
123-
val key = if (uiState.descriptionText.text.isBlank()) {
124-
uiState.imageUri.toString()
125-
} else {
126-
prompt
127-
}
128-
uiState.resultBitmap?.let { bitmap ->
129-
CustomizeAndExportScreen(
130-
resultImage = bitmap,
131-
originalImageUri = uiState.imageUri,
132-
onBackPress = onBackPressed,
133-
onInfoPress = onAboutPressed,
134-
viewModel = hiltViewModel<CustomizeExportViewModel>(key = key),
135-
)
136-
}
137-
}
138104
}
139105
}

0 commit comments

Comments
 (0)