Skip to content
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2025 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,21 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntOffset
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.entry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.android.developers.androidify.camera.CameraPreviewScreen
import com.android.developers.androidify.creation.CreationScreen
import com.android.developers.androidify.creation.CreationViewModel
import com.android.developers.androidify.customize.CustomizeAndExportScreen
import com.android.developers.androidify.customize.CustomizeExportViewModel
import com.android.developers.androidify.home.AboutScreen
import com.android.developers.androidify.home.HomeScreen
import com.android.developers.androidify.results.ResultsScreen
import com.android.developers.androidify.results.ResultsViewModel
import com.android.developers.androidify.theme.transitions.ColorSplashTransitionScreen
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity

Expand Down Expand Up @@ -92,14 +98,20 @@ fun MainNavigation() {
CameraPreviewScreen(
onImageCaptured = { uri ->
backStack.removeAll { it is Create }
backStack.add(Create(uri.toString()))
backStack.add(Create(uri))
backStack.removeAll { it is Camera }
},
)
}
entry<Create> { createKey ->
val creationViewModel = hiltViewModel<CreationViewModel, CreationViewModel.Factory>(
creationCallback = { factory ->
factory.create(
originalImageUrl = createKey.fileName,
)
},
)
CreationScreen(
createKey.fileName,
onCameraPressed = {
backStack.removeAll { it is Camera }
backStack.add(Camera)
Expand All @@ -110,6 +122,64 @@ fun MainNavigation() {
onAboutPressed = {
backStack.add(About)
},
onImageCreated = { resultImageUri, prompt, originalImageUri ->
backStack.removeAll { it is Result }
backStack.add(
Result(
resultImageUri = resultImageUri,
prompt = prompt,
originalImageUri = originalImageUri,
),
)
},
creationViewModel = creationViewModel,
)
}
entry<Result> { resultKey ->
val resultsViewModel = hiltViewModel<ResultsViewModel, ResultsViewModel.Factory>(
creationCallback = { factory ->
factory.create(
resultImageUrl = resultKey.resultImageUri,
originalImageUrl = resultKey.originalImageUri,
promptText = resultKey.prompt,
)
},
)
ResultsScreen(
onNextPress = { resultImageUri, originalImageUri ->
backStack.add(
CustomizeExport(
resultImageUri = resultImageUri,
originalImageUri = originalImageUri,
),
)
},
onAboutPress = {
backStack.add(About)
},
onBackPress = {
backStack.removeLastOrNull()
},
viewModel = resultsViewModel,
)
}
entry<CustomizeExport> { shareKey ->
val customizeExportViewModel = hiltViewModel<CustomizeExportViewModel, CustomizeExportViewModel.Factory>(
creationCallback = { factory ->
factory.create(
resultImageUrl = shareKey.resultImageUri,
originalImageUrl = shareKey.originalImageUri,
)
},
)
CustomizeAndExportScreen(
onBackPress = {
backStack.removeLastOrNull()
},
onInfoPress = {
backStack.add(About)
},
viewModel = customizeExportViewModel,
)
}
entry<About> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package com.android.developers.androidify.navigation

import android.net.Uri
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable

Expand All @@ -26,10 +27,39 @@ sealed interface NavigationRoute
data object Home : NavigationRoute

@Serializable
data class Create(val fileName: String? = null, val prompt: String? = null) : NavigationRoute
data class Create(
@Serializable(with = UriSerializer::class) val fileName: Uri? = null,
val prompt: String? = null,
) : NavigationRoute

@Serializable
object Camera : NavigationRoute

@Serializable
object About : NavigationRoute

/**
* Represents the result of an image generation process, used for navigation.
*
* @param resultImageUri The URI of the generated image.
* @param originalImageUri The URI of the original image used as a base for generation, if any.
* @param prompt The text prompt used to generate the image, if any.
*/
@Serializable
data class Result(
@Serializable(with = UriSerializer::class) val resultImageUri: Uri,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I wonder if there is a way to register the UriSerializer for Nav3? @dturner

@Serializable(with = UriSerializer::class) val originalImageUri: Uri? = null,
val prompt: String? = null,
) : NavigationRoute

/**
* Represents the navigation route to the screen for customizing and exporting a generated image.
*
* @param resultImageUri The URI of the generated image to be customized.
* @param originalImageUri The URI of the original image, passed along for context.
*/
@Serializable
data class CustomizeExport(
@Serializable(with = UriSerializer::class) val resultImageUri: Uri,
@Serializable(with = UriSerializer::class) val originalImageUri: Uri?,
) : NavigationRoute
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.developers.androidify.navigation

import android.net.Uri
import androidx.core.net.toUri
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

object UriSerializer : KSerializer<Uri> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: Uri) {
encoder.encodeString(value.toString())
}

override fun deserialize(decoder: Decoder): Uri = decoder.decodeString().toUri()
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface LocalSegmentationDataSource {
}

class LocalSegmentationDataSourceImpl @Inject constructor(
private val moduleInstallClient: ModuleInstallClient
private val moduleInstallClient: ModuleInstallClient,
) : LocalSegmentationDataSource {
private val segmenter by lazy {
val options = SubjectSegmenterOptions.Builder()
Expand All @@ -60,7 +60,7 @@ class LocalSegmentationDataSourceImpl @Inject constructor(
return areModulesAvailable
}
private class CustomInstallStatusListener(
val continuation: CancellableContinuation<Boolean>
val continuation: CancellableContinuation<Boolean>,
) : InstallStatusListener {

override fun onInstallStatusUpdated(update: ModuleInstallStatusUpdate) {
Expand All @@ -70,7 +70,7 @@ class LocalSegmentationDataSourceImpl @Inject constructor(
continuation.resume(true)
} else if (update.installState == STATE_FAILED || update.installState == STATE_CANCELED) {
continuation.resumeWithException(
ImageSegmentationException("Module download failed or was canceled. State: ${update.installState}")
ImageSegmentationException("Module download failed or was canceled. State: ${update.installState}"),
)
} else {
Log.d("LocalSegmentationDataSource", "State update: ${update.installState}")
Expand Down Expand Up @@ -128,4 +128,4 @@ class LocalSegmentationDataSourceImpl @Inject constructor(
}
}

class ImageSegmentationException(message: String? = null): Exception(message)
class ImageSegmentationException(message: String? = null) : Exception(message)
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ class FirebaseAiDataSourceImpl @Inject constructor(
return Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel(
remoteConfigDataSource.imageModelName(),
safetySettings =
ImagenSafetySettings(
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
// Uses `ALLOW_ADULT` filter since `ALLOW_ALL` requires a special approval
// See https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen
personFilterLevel = ImagenPersonFilterLevel.ALLOW_ADULT,
),
ImagenSafetySettings(
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
// Uses `ALLOW_ADULT` filter since `ALLOW_ALL` requires a special approval
// See https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen
personFilterLevel = ImagenPersonFilterLevel.ALLOW_ADULT,
),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.developers.testing.data

import android.graphics.Bitmap

val bitmapSample = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,8 @@ class TestFileProvider : LocalFileProvider {
): Uri {
TODO("Not yet implemented")
}

override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? {
return bitmapSample
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.android.developers.androidify.util
import android.app.Application
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Environment
Expand Down Expand Up @@ -53,6 +54,9 @@ interface LocalFileProvider {

@WorkerThread
suspend fun saveUriToSharedStorage(inputUri: Uri, fileName: String, mimeType: String): Uri

@WorkerThread
suspend fun loadBitmapFromUri(uri: Uri): Bitmap?
}

@Singleton
Expand Down Expand Up @@ -120,6 +124,20 @@ class LocalFileProviderImpl @Inject constructor(
return@withContext newUri
}

override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? {
return withContext(ioDispatcher) {
try {
application.contentResolver.openInputStream(uri)?.use {
return@withContext BitmapFactory.decodeStream(it)
}
null
} catch (e: Exception) {
e.printStackTrace()
null
Comment on lines +134 to +136
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

e.printStackTrace() should be avoided in production code. It prints to the standard error stream and can be lost, especially in release builds where logs might be stripped. Using a structured logging framework like android.util.Log is a better practice for error handling as it allows for tagging and filtering of log messages.

Suggested change
} catch (e: Exception) {
e.printStackTrace()
null
} catch (e: Exception) {
android.util.Log.e("LocalFileProviderImpl", "Error loading bitmap from URI: $uri", e)
null
}

}
}
}

@Throws(IOException::class)
@WorkerThread
private fun saveFileToUri(file: File, uri: Uri) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface ImageGenerationRepository {
suspend fun saveImageToExternalStorage(imageBitmap: Bitmap): Uri
suspend fun saveImageToExternalStorage(imageUri: Uri): Uri

suspend fun addBackgroundToBot(image: Bitmap, backgroundPrompt: String) : Bitmap
suspend fun addBackgroundToBot(image: Bitmap, backgroundPrompt: String): Bitmap
suspend fun removeBackground(image: Bitmap): Bitmap
}

Expand Down Expand Up @@ -134,7 +134,7 @@ internal class ImageGenerationRepositoryImpl @Inject constructor(

override suspend fun addBackgroundToBot(image: Bitmap, backgroundPrompt: String): Bitmap {
val backgroundBotInstructions = remoteConfigDataSource.getBotBackgroundInstructionPrompt() +
"\"" + backgroundPrompt + "\""
"\"" + backgroundPrompt + "\""
return firebaseAiDataSource.generateImageWithEdit(image, backgroundBotInstructions)
}

Expand Down
Loading