Skip to content

Commit 61e40fc

Browse files
authored
Merge pull request #95 from android/krishna/nav3_resultScreenImp
Migrate ResultsScreen to use Nav3
2 parents b573360 + d5ef994 commit 61e40fc

File tree

26 files changed

+446
-266
lines changed

26 files changed

+446
-266
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<?xml version="1.0" encoding="utf-8"?><!--
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
23
Copyright 2025 The Android Open Source Project
34
45
Licensed under the Apache License, Version 2.0 (the "License");

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+
}

core/network/src/main/java/com/android/developers/androidify/ondevice/LocalSegmentationDataSource.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface LocalSegmentationDataSource {
3737
}
3838

3939
class LocalSegmentationDataSourceImpl @Inject constructor(
40-
private val moduleInstallClient: ModuleInstallClient
40+
private val moduleInstallClient: ModuleInstallClient,
4141
) : LocalSegmentationDataSource {
4242
private val segmenter by lazy {
4343
val options = SubjectSegmenterOptions.Builder()
@@ -60,7 +60,7 @@ class LocalSegmentationDataSourceImpl @Inject constructor(
6060
return areModulesAvailable
6161
}
6262
private class CustomInstallStatusListener(
63-
val continuation: CancellableContinuation<Boolean>
63+
val continuation: CancellableContinuation<Boolean>,
6464
) : InstallStatusListener {
6565

6666
override fun onInstallStatusUpdated(update: ModuleInstallStatusUpdate) {
@@ -70,7 +70,7 @@ class LocalSegmentationDataSourceImpl @Inject constructor(
7070
continuation.resume(true)
7171
} else if (update.installState == STATE_FAILED || update.installState == STATE_CANCELED) {
7272
continuation.resumeWithException(
73-
ImageSegmentationException("Module download failed or was canceled. State: ${update.installState}")
73+
ImageSegmentationException("Module download failed or was canceled. State: ${update.installState}"),
7474
)
7575
} else {
7676
Log.d("LocalSegmentationDataSource", "State update: ${update.installState}")
@@ -128,4 +128,4 @@ class LocalSegmentationDataSourceImpl @Inject constructor(
128128
}
129129
}
130130

131-
class ImageSegmentationException(message: String? = null): Exception(message)
131+
class ImageSegmentationException(message: String? = null) : Exception(message)

core/network/src/main/java/com/android/developers/androidify/vertexai/FirebaseAiDataSource.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ class FirebaseAiDataSourceImpl @Inject constructor(
8585
return Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel(
8686
remoteConfigDataSource.imageModelName(),
8787
safetySettings =
88-
ImagenSafetySettings(
89-
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
90-
// Uses `ALLOW_ADULT` filter since `ALLOW_ALL` requires a special approval
91-
// See https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen
92-
personFilterLevel = ImagenPersonFilterLevel.ALLOW_ADULT,
93-
),
88+
ImagenSafetySettings(
89+
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
90+
// Uses `ALLOW_ADULT` filter since `ALLOW_ALL` requires a special approval
91+
// See https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen
92+
personFilterLevel = ImagenPersonFilterLevel.ALLOW_ADULT,
93+
),
9494
)
9595
}
9696

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) {

data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ interface ImageGenerationRepository {
3838
suspend fun saveImageToExternalStorage(imageBitmap: Bitmap): Uri
3939
suspend fun saveImageToExternalStorage(imageUri: Uri): Uri
4040

41-
suspend fun addBackgroundToBot(image: Bitmap, backgroundPrompt: String) : Bitmap
41+
suspend fun addBackgroundToBot(image: Bitmap, backgroundPrompt: String): Bitmap
4242
suspend fun removeBackground(image: Bitmap): Bitmap
4343
}
4444

@@ -134,7 +134,7 @@ internal class ImageGenerationRepositoryImpl @Inject constructor(
134134

135135
override suspend fun addBackgroundToBot(image: Bitmap, backgroundPrompt: String): Bitmap {
136136
val backgroundBotInstructions = remoteConfigDataSource.getBotBackgroundInstructionPrompt() +
137-
"\"" + backgroundPrompt + "\""
137+
"\"" + backgroundPrompt + "\""
138138
return firebaseAiDataSource.generateImageWithEdit(image, backgroundBotInstructions)
139139
}
140140

0 commit comments

Comments
 (0)