diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 99ca3e39..8535bf98 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -32,4 +32,4 @@ -keepattributes Signature -keepattributes *Annotation* --keepattributes InnerClasses \ No newline at end of file +-keepattributes InnerClasses diff --git a/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt b/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt index 5ffd968a..0999fc9e 100644 --- a/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt +++ b/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt @@ -38,6 +38,8 @@ interface RemoteConfigDataSource { fun useImagen(): Boolean fun getFineTunedModelName(): String + + fun getImageGenerationEditsModelName(): String } @Singleton @@ -94,4 +96,8 @@ class RemoteConfigDataSourceImpl @Inject constructor() : RemoteConfigDataSource override fun getFineTunedModelName(): String { return remoteConfig.getString("fine_tuned_model_name") } + + override fun getImageGenerationEditsModelName(): String { + return remoteConfig.getString("image_generation_model_edits") + } } diff --git a/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt b/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt index 7ddd3c2b..0a43a4be 100644 --- a/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt +++ b/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt @@ -19,9 +19,9 @@ import android.annotation.SuppressLint import android.content.Context import androidx.startup.Initializer import com.google.firebase.appcheck.FirebaseAppCheck -import com.google.firebase.appcheck.ktx.appCheck +import com.google.firebase.appcheck.appCheck import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory -import com.google.firebase.ktx.Firebase +import com.google.firebase.Firebase /** * Initialize [FirebaseAppCheck] using the App Startup Library. diff --git a/core/network/src/main/java/com/android/developers/androidify/vertexai/FirebaseAiDataSource.kt b/core/network/src/main/java/com/android/developers/androidify/vertexai/FirebaseAiDataSource.kt index 6fbe086a..fef0f431 100644 --- a/core/network/src/main/java/com/android/developers/androidify/vertexai/FirebaseAiDataSource.kt +++ b/core/network/src/main/java/com/android/developers/androidify/vertexai/FirebaseAiDataSource.kt @@ -32,6 +32,7 @@ import com.google.firebase.ai.type.ImagenPersonFilterLevel import com.google.firebase.ai.type.ImagenSafetyFilterLevel import com.google.firebase.ai.type.ImagenSafetySettings import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.ResponseModality import com.google.firebase.ai.type.SafetySetting import com.google.firebase.ai.type.Schema import com.google.firebase.ai.type.asImageOrNull @@ -51,6 +52,7 @@ interface FirebaseAiDataSource { suspend fun generateDescriptivePromptFromImage(image: Bitmap): ValidatedDescription suspend fun generateImageFromPromptAndSkinTone(prompt: String, skinTone: String): Bitmap suspend fun generatePrompt(prompt: String): GeneratedPrompt + suspend fun generateImageWithEdit(image: Bitmap, backgroundPrompt: String): Bitmap } @OptIn(PublicPreviewAPI::class) @@ -58,7 +60,10 @@ interface FirebaseAiDataSource { class FirebaseAiDataSourceImpl @Inject constructor( private val remoteConfigDataSource: RemoteConfigDataSource, ) : FirebaseAiDataSource { - private fun createGenerativeTextModel(jsonSchema: Schema, temperature: Float? = null): GenerativeModel { + private fun createGenerativeTextModel( + jsonSchema: Schema, + temperature: Float? = null, + ): GenerativeModel { return Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel( modelName = remoteConfigDataSource.textModelName(), generationConfig = generationConfig { @@ -139,6 +144,7 @@ class FirebaseAiDataSourceImpl @Inject constructor( image, ) } + private fun createFineTunedModel(): GenerativeModel { return Firebase.ai.generativeModel( remoteConfigDataSource.getFineTunedModelName(), @@ -152,7 +158,10 @@ class FirebaseAiDataSourceImpl @Inject constructor( ) } - override suspend fun generateImageFromPromptAndSkinTone(prompt: String, skinTone: String): Bitmap { + override suspend fun generateImageFromPromptAndSkinTone( + prompt: String, + skinTone: String, + ): Bitmap { val basePromptTemplate = remoteConfigDataSource.promptImageGenerationWithSkinTone() val imageGenerationPrompt = basePromptTemplate .replace("{prompt}", prompt) @@ -237,6 +246,29 @@ class FirebaseAiDataSourceImpl @Inject constructor( return executePromptGeneration(generativeModel, prompt) } + override suspend fun generateImageWithEdit( + image: Bitmap, + backgroundPrompt: String, + ): Bitmap { + val model = Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel( + modelName = remoteConfigDataSource.getImageGenerationEditsModelName(), + generationConfig = generationConfig { + responseModalities = listOf( + ResponseModality.TEXT, + ResponseModality.IMAGE, + ) + }, + ) + val prompt = content { + text(backgroundPrompt) + image(image) + } + val response = model.generateContent(prompt) + val image = response.candidates.firstOrNull() + ?.content?.parts?.firstNotNullOfOrNull { it.asImageOrNull() } + return image ?: throw IllegalStateException("Could not extract image from model response") + } + private suspend fun executePromptGeneration( generativeModel: GenerativeModel, prompt: String, diff --git a/core/network/src/main/res/xml/remote_config_defaults.xml b/core/network/src/main/res/xml/remote_config_defaults.xml index ef27518d..36cfa63b 100644 --- a/core/network/src/main/res/xml/remote_config_defaults.xml +++ b/core/network/src/main/res/xml/remote_config_defaults.xml @@ -15,6 +15,10 @@ limitations under the License. --> + + image_generation_model_edits + gemini-2.0-flash-preview-image-generation + use_imagen true diff --git a/core/testing/src/main/java/com/android/developers/testing/network/TestFirebaseAiDataSource.kt b/core/testing/src/main/java/com/android/developers/testing/network/TestFirebaseAiDataSource.kt index 409e31ab..a12a4357 100644 --- a/core/testing/src/main/java/com/android/developers/testing/network/TestFirebaseAiDataSource.kt +++ b/core/testing/src/main/java/com/android/developers/testing/network/TestFirebaseAiDataSource.kt @@ -45,4 +45,11 @@ class TestFirebaseAiDataSource(val promptOutput: List) : FirebaseAiDataS override suspend fun generatePrompt(prompt: String): GeneratedPrompt { return GeneratedPrompt(true, promptOutput) } + + override suspend fun generateImageWithEdit( + image: Bitmap, + backgroundPrompt: String, + ): Bitmap { + return createBitmap(1, 1) + } } diff --git a/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt b/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt index 7f46caa4..8790c38b 100644 --- a/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt +++ b/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt @@ -67,4 +67,8 @@ class TestRemoteConfigDataSource(private val useGeminiNano: Boolean) : RemoteCon override fun getFineTunedModelName(): String { return "test-fine-tuned-model" } + + override fun getImageGenerationEditsModelName(): String { + return "test_image_model" + } } diff --git a/core/testing/src/main/java/com/android/developers/testing/repository/FakeImageGenerationRepository.kt b/core/testing/src/main/java/com/android/developers/testing/repository/FakeImageGenerationRepository.kt index c6795a26..ab654b84 100644 --- a/core/testing/src/main/java/com/android/developers/testing/repository/FakeImageGenerationRepository.kt +++ b/core/testing/src/main/java/com/android/developers/testing/repository/FakeImageGenerationRepository.kt @@ -57,4 +57,11 @@ class FakeImageGenerationRepository : ImageGenerationRepository { if (exceptionToThrow != null) throw exceptionToThrow!! return imageUri } + + override suspend fun generateImageWithEdit( + image: Bitmap, + editPrompt: String, + ): Bitmap { + return createBitmap(1, 1) + } } diff --git a/core/theme/src/main/java/com/android/developers/androidify/theme/transitions/LoadingShimmerOverlay.kt b/core/theme/src/main/java/com/android/developers/androidify/theme/transitions/LoadingShimmerOverlay.kt new file mode 100644 index 00000000..0cb3c4a8 --- /dev/null +++ b/core/theme/src/main/java/com/android/developers/androidify/theme/transitions/LoadingShimmerOverlay.kt @@ -0,0 +1,89 @@ +/* + * 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.theme.transitions + +import androidx.compose.animation.core.InfiniteRepeatableSpec +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.unit.dp +import kotlin.math.max + +private val ColorScheme.highlightLoading: Color + get() = + Color(0xFFAB9FF6) + +@Composable +fun Modifier.loadingShimmerOverlay( + visible: Boolean, + highlightColor: Color = MaterialTheme.colorScheme.highlightLoading, + clipShape: Shape = RoundedCornerShape(8.dp), + animationSpec: InfiniteRepeatableSpec = infiniteRepeatable( + animation = tween(1500), + ), +): Modifier { + var highlightProgress: Float by remember { mutableFloatStateOf(0f) } + if (visible) { + val infiniteTransition = rememberInfiniteTransition() + highlightProgress = infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = animationSpec, + ).value + } + return drawWithCache { + if (visible) { + val brush = Brush.radialGradient( + colors = listOf( + highlightColor.copy(alpha = 0f), + highlightColor.copy(alpha = 0.8f), + highlightColor.copy(alpha = 0f), + ), + center = Offset(x = 0f, y = 0f), + radius = (max(size.width, size.height) * highlightProgress * 2.5f).coerceAtLeast(0.01f), + ) + onDrawWithContent { + drawContent() + val outline = clipShape.createOutline(size, layoutDirection, this) + + drawOutline( + outline = outline, + brush = brush, + ) + } + } else { + onDrawWithContent { + drawContent() + } + } + } +} diff --git a/data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt b/data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt index a59723c2..710f5d9e 100644 --- a/data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt +++ b/data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt @@ -35,6 +35,7 @@ interface ImageGenerationRepository { suspend fun saveImage(imageBitmap: Bitmap): Uri suspend fun saveImageToExternalStorage(imageBitmap: Bitmap): Uri suspend fun saveImageToExternalStorage(imageUri: Uri): Uri + suspend fun generateImageWithEdit(image: Bitmap, editPrompt: String): Bitmap } @Singleton @@ -124,4 +125,8 @@ internal class ImageGenerationRepositoryImpl @Inject constructor( throw NoInternetException() } } + + override suspend fun generateImageWithEdit(image: Bitmap, editPrompt: String): Bitmap { + return firebaseAiDataSource.generateImageWithEdit(image, editPrompt) + } } diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/BackgroundTool.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/BackgroundTool.kt index ec9cb56b..bd50a4b8 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/BackgroundTool.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/BackgroundTool.kt @@ -21,15 +21,24 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.rememberAsyncImagePainter +import com.android.developers.androidify.results.R import com.android.developers.androidify.theme.AndroidifyTheme @Composable @@ -41,7 +50,8 @@ fun BackgroundTool( singleLine: Boolean = false, ) { GenericTool( - modifier = modifier.wrapContentSize(), + modifier = modifier.wrapContentSize() + .verticalScroll(rememberScrollState()), tools = backgroundOptions, singleLine = singleLine, selectedOption = selectedOption, @@ -73,6 +83,24 @@ fun BackgroundTool( .clip(MaterialTheme.shapes.small), ) } + if (tool.aiBackground) { + Box( + Modifier + .padding(2.dp) + .align(Alignment.BottomEnd) + .size(16.dp) + .background( + color = MaterialTheme.colorScheme.secondaryContainer, + shape = RoundedCornerShape(size = 24.dp), + ), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.spark), + contentDescription = null, + ) + } + } } }, ) @@ -88,6 +116,16 @@ private fun BackgroundToolPreview() { BackgroundOption.Plain, BackgroundOption.Lightspeed, BackgroundOption.IO, + BackgroundOption.MusicLover, + BackgroundOption.PoolMaven, + BackgroundOption.SoccerFanatic, + BackgroundOption.StarGazer, + BackgroundOption.FitnessBuff, + BackgroundOption.Fandroid, + BackgroundOption.GreenThumb, + BackgroundOption.Gamer, + BackgroundOption.Jetsetter, + BackgroundOption.Chef ), selectedOption = BackgroundOption.Lightspeed, onBackgroundOptionSelected = {}, diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt index 8593b756..3d13e5c4 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt @@ -18,6 +18,7 @@ package com.android.developers.androidify.customize import android.Manifest +import android.R.attr.visible import android.graphics.Bitmap import android.net.Uri import android.os.Build @@ -82,6 +83,7 @@ import com.android.developers.androidify.theme.LocalAnimateBoundsScope import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.theme.components.PrimaryButton import com.android.developers.androidify.theme.components.SecondaryOutlinedButton +import com.android.developers.androidify.theme.transitions.loadingShimmerOverlay import com.android.developers.androidify.util.LargeScreensPreview import com.android.developers.androidify.util.PhonePreview import com.android.developers.androidify.util.allowsFullContent @@ -162,23 +164,32 @@ private fun CustomizeExportContents( }, containerColor = MaterialTheme.colorScheme.surface, ) { paddingValues -> - val imageResult = remember { + val imageResult = remember(state.showImageEditProgress) { movableContentWithReceiverOf { - ImageResult( - this, - modifier = Modifier + Box( + Modifier .padding(16.dp), - outerChromeModifier = Modifier - .dropShadow( - RoundedCornerShape(6), - shadow = Shadow( - radius = 26.dp, - spread = 10.dp, - color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.2f), + ) { + ImageResult( + this@movableContentWithReceiverOf, + modifier = Modifier + .padding(16.dp), + outerChromeModifier = Modifier + .dropShadow( + RoundedCornerShape(6), + shadow = Shadow( + radius = 26.dp, + spread = 10.dp, + color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.2f), + ), + ) + .clip(RoundedCornerShape(6)) + .loadingShimmerOverlay( + visible = state.showImageEditProgress, + clipShape = RoundedCornerShape(percent = 6), ), - ) - .clip(RoundedCornerShape(6)), - ) + ) + } } } val toolSelector = @Composable { modifier: Modifier, horizontal: Boolean -> diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt index 1907e18e..adb98869 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt @@ -18,19 +18,24 @@ package com.android.developers.androidify.customize import android.app.Application import android.graphics.Bitmap import android.net.Uri +import android.util.Log import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.Modifier import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.android.developers.androidify.data.DataModule_Companion_IoDispatcherFactory.ioDispatcher import com.android.developers.androidify.data.ImageGenerationRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +import javax.inject.Named @HiltViewModel class CustomizeExportViewModel @Inject constructor( @@ -87,27 +92,77 @@ class CustomizeExportViewModel @Inject constructor( } } fun selectedToolStateChanged(toolState: ToolState) { - _state.update { - it.copy( - toolState = it.toolState + (it.selectedTool to toolState), - exportImageCanvas = - when (toolState.selectedToolOption) { - is BackgroundOption -> { - val backgroundOption = toolState.selectedToolOption as BackgroundOption - it.exportImageCanvas.updateAspectRatioAndBackground( + when (toolState.selectedToolOption) { + is BackgroundOption -> { + val backgroundOption = toolState.selectedToolOption as BackgroundOption + _state.update { + it.copy( + toolState = it.toolState + (it.selectedTool to toolState), + exportImageCanvas = it.exportImageCanvas.updateAspectRatioAndBackground( backgroundOption, it.exportImageCanvas.aspectRatioOption, + ), + ) + } + if (backgroundOption.aiBackground) { + triggerAiBackgroundGeneration(backgroundOption) + } else { + _state.update { + it.copy( + exportImageCanvas = it.exportImageCanvas.copy(imageWithEdit = null), ) } - is SizeOption -> { - it.exportImageCanvas.updateAspectRatioAndBackground( + } + } + is SizeOption -> { + _state.update { + it.copy( + toolState = it.toolState + (it.selectedTool to toolState), + exportImageCanvas = it.exportImageCanvas.updateAspectRatioAndBackground( it.exportImageCanvas.selectedBackgroundOption, (toolState.selectedToolOption as SizeOption), - ) - } - else -> throw IllegalArgumentException("Unknown tool option") - }, - ) + ), + ) + } + } + else -> throw IllegalArgumentException("Unknown tool option") + } + } + + private fun triggerAiBackgroundGeneration(backgroundOption: BackgroundOption) { + viewModelScope.launch { + if (backgroundOption.prompt == null) { + _state.update { + it.copy( + showImageEditProgress = false, + exportImageCanvas = it.exportImageCanvas.copy(imageWithEdit = null), + ) + } + return@launch + } + + val image = state.value.exportImageCanvas.imageBitmap + if (image == null) { + return@launch + } + + _state.update { it.copy(showImageEditProgress = true) } + try { + val bitmap = imageGenerationRepository.generateImageWithEdit( + image, + "Add the input image android bot as the main subject to the result, it should be the most prominent element of the resultant image, large and filling the foreground, standing in the center of the frame with the central focus, and the background just underneath the content. The background is described as follows: \"" + backgroundOption.prompt + "\"", + ) + _state.update { + it.copy( + exportImageCanvas = it.exportImageCanvas.copy(imageWithEdit = bitmap), + ) + } + } catch (e: Exception) { + Log.e("CustomizeExportViewModel", "Image generation failed", e) + snackbarHostState.value.showSnackbar("Background vibe generation failed") + } finally { + _state.update { it.copy(showImageEditProgress = false) } + } } } diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeState.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeState.kt index d9963976..c2c576e5 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeState.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeState.kt @@ -15,12 +15,10 @@ */ package com.android.developers.androidify.customize -import android.R.attr.rotation import android.graphics.Bitmap import android.net.Uri import androidx.annotation.DrawableRes import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.R import androidx.compose.ui.geometry.Size data class CustomizeExportState( @@ -35,7 +33,7 @@ data class CustomizeExportState( CustomizeTool.Background to BackgroundToolState(), ), val exportImageCanvas: ExportImageCanvas = ExportImageCanvas(), - + val showImageEditProgress: Boolean = false, ) interface ToolState { @@ -61,6 +59,16 @@ data class BackgroundToolState( BackgroundOption.Plain, BackgroundOption.Lightspeed, BackgroundOption.IO, + BackgroundOption.MusicLover, + BackgroundOption.PoolMaven, + BackgroundOption.SoccerFanatic, + BackgroundOption.StarGazer, + BackgroundOption.FitnessBuff, + BackgroundOption.Fandroid, + BackgroundOption.GreenThumb, + BackgroundOption.Gamer, + BackgroundOption.Jetsetter, + BackgroundOption.Chef ), ) : ToolState @@ -77,6 +85,7 @@ data class ExportImageCanvas( @param:DrawableRes val selectedBackgroundDrawable: Int? = com.android.developers.androidify.results.R.drawable.background_square_blocks, val includeWatermark: Boolean = true, + val imageWithEdit: Bitmap? = null, ) { fun updateAspectRatioAndBackground( backgroundOption: BackgroundOption, @@ -102,6 +111,12 @@ data class ExportImageCanvas( null } BackgroundOption.Plain -> com.android.developers.androidify.results.R.drawable.background_square_none + else -> { + offset = Offset(0f, 0f) + rotation = 0f + imageSize = Size(newCanvasSize.width, newCanvasSize.height) + null + } } } SizeOption.Banner -> { @@ -119,6 +134,12 @@ data class ExportImageCanvas( null } BackgroundOption.Plain -> com.android.developers.androidify.results.R.drawable.background_banner_plain + else -> { + offset = Offset(0f, 0f) + rotation = 0f + imageSize = Size(newCanvasSize.width, newCanvasSize.height) + null + } } } SizeOption.SocialHeader -> { @@ -135,6 +156,12 @@ data class ExportImageCanvas( null } BackgroundOption.Plain -> com.android.developers.androidify.results.R.drawable.background_social_header_plain + else -> { + offset = Offset(0f, 0f) + rotation = 0f + imageSize = Size(newCanvasSize.width, newCanvasSize.height) + null + } } } @@ -152,6 +179,12 @@ data class ExportImageCanvas( null } BackgroundOption.Plain -> com.android.developers.androidify.results.R.drawable.background_wallpaper_plain + else -> { + offset = Offset(0f, 0f) + rotation = 0f + imageSize = Size(newCanvasSize.width, newCanvasSize.height) + null + } } } @@ -169,6 +202,12 @@ data class ExportImageCanvas( null } BackgroundOption.Plain -> com.android.developers.androidify.results.R.drawable.background_wallpaper_tablet_light + else -> { + offset = Offset(0f, 0f) + rotation = 0f + imageSize = Size(newCanvasSize.width, newCanvasSize.height) + null + } } } } diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeTool.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeTool.kt index d749c78c..2152c36d 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeTool.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeTool.kt @@ -46,6 +46,8 @@ sealed class BackgroundOption( override val displayName: String, override val key: String, val previewDrawableInt: Int?, + val aiBackground: Boolean = false, + val prompt: String? = null, ) : ToolOption { object None : BackgroundOption("None", "None", null) object Plain : BackgroundOption("Plain", "Plain", null) @@ -59,6 +61,192 @@ sealed class BackgroundOption( "IO", R.drawable.background_option_io, ) - // todo add Create with AI background option - /* object Create : BackgroundOption("Create", "Create", R.drawable.background_create)*/ + object MusicLover: BackgroundOption( + "Music lover", + "music", + R.drawable.background_option_music_lover, + aiBackground = true, + prompt = """ + This is a soft, vibrant 3D illustration of a minimalist outdoor DJ stage setup, rendered with a meticulous blend of realism and rounded, toy-like objects, creating a clean aesthetic. The entire scene is characterized by subtle glossiness, and soft, even lighting that casts dynamic shadows, beautifully emphasizing the 3D form and depth of the objects. In the far distance, behind the stage, is rows of stadium seats and cheering crowds that appear blurry. The scene is of a night show. + + The centerpiece is a DJ stage constructed from a metallic truss system, forming a rectangular frame that supports a black canopy overhead. Two large, colorful speaker stacks are standing on either side of the stage. The front of the stage features a large, vibrant LED screen displaying abstract patterns. The DJ booth is empty. + + The scene appears to be at night, with lasers and a strobe lights illuminating the scene. A shallow depth of field keeps the stage and its equipment in sharp focus. The foreground is a deliberate blank space, with only the clean, simple ground surface visible, offering an open area for a future character or object to be added. The overall atmosphere is quiet and calm, with warm, natural light creating long, dynamic shadows that enhance the 3D rendering. + + Crucially, the scene is captured from a very low camera angle, almost at floor level, significantly zoomed out to showcase a much wider view of the room/setup. This low perspective emphasizes the expanse of the floor, which appears to stretch far into the distance, creating a profound sense of depth and length. The objects in the scene should appear much smaller in relation to the overall composition. + + Place the scene items, to the edges of the composition, so that when an object is placed in the middle of the foreground, it does not completely cover what's behind it. + """.trimIndent() + ) + object PoolMaven : BackgroundOption( + "Pool maven", + "pool", + R.drawable.background_option_pool, + aiBackground = true, + prompt = """ + A vibrant, soft 3D illustration of a serene and sun-drenched minimalist swimming pool. The entire scene is rendered with a meticulous blend of realism and rounded, toy-like objects, creating a clean, minimalist aesthetic. Every element is characterized by smooth surfaces, subtle glossiness, and soft, even lighting that creates dynamic shadows, beautifully emphasizing the 3D form and depth of the objects. The scene features an array of vibrant colors, and has a whimsical, playful feel to it. + + The centerpiece is a bean-shaped swimming pool, where the water shimmers with a tranquil, light blue hue, its surface rendered with soft forms and gentle ripples. The tiles around the pool are a light pink color. Floating nearby is a bright, rounded, playful pool floaty that is shaped like an animal. On the sleek, polished tiled deck, a pair of oversized, stylized swimming goggles. There is a playful slide that is on one side of the pool. + + In the extremely blurred background, we can see only vague, indistinct forms of minimalist elements, such as a sleek lounge chair, a simple side table, and a potted palm plant with smooth, rounded leaves. The scene is illuminated by a warm, natural light from the sun, creating a peaceful and inviting atmosphere. A horizontal horizon line distinctly separates the pool scene from the blank blue sky above. + + A shallow depth of field is intensely applied, bringing the pool and props into razor-sharp focus while the background elements appear maximally blurred, almost abstract. The foreground is a deliberate blank space, with just the polished tile floor visible, offering a clean, open area for a future character or object to be added to the scene. + + Crucially, the scene is captured from a very low camera angle, almost at floor level, significantly zoomed out to showcase a much wider view of the room/setup. This low perspective emphasizes the expanse of the floor, which appears to stretch far into the distance, creating a profound sense of depth and length. The objects in the scene should appear much smaller in relation to the overall composition. The foreground has the same pink tiles that surround the pool. + + Place the scene items, to the edges of the composition, so that when an object is placed in the middle of the foreground, it does not completely cover what's behind it. + """.trimIndent() + ) + + object SoccerFanatic : BackgroundOption( + "Soccer fanatic", + "soccer", + R.drawable.background_option_soccer, + aiBackground = true, + prompt = """ + A 3D illustration of a minimalist/clean soccer stadium, depicted from the center of the field looking towards a goal. The entire scene is a 3D rendering where all elements are crafted with soft forms, rounded edges, and smooth surfaces. + + The field itself is a lush, vibrant green. A perfectly spherical, classic black and white soccer ball, with subtle glossiness, rests in the composition. + + In the midground, the white lines of the penalty box and the soccer goal are simplified and rounded. The goal's frame and netting are also rendered with soft forms and a gentle sheen. + + The stadium stands in the background are abstract and softly formed, with many brights colors and flags, as if it is full of excited fans. There is intentional blurring due to a shallow depth of field, creating a pleasing bokeh effect. + + There is blank blue sky visible at the top of the image, a horizontal horizon line is viaible in the middle of the image, created by where the edge of the pitch meets the stadium. + + Dynamic shadows are strategically placed beneath the ball and goal, adding visual interest and grounding these objects within the space, despite the soft lighting. In the immediate foreground, a deliberate blank space of grass if left, perfectly framed and inviting, ready for an object to be placed there. The overall aesthetic is a compelling mixture of realism in its precise rendering and the whimsical, toy-like style suggested by the keywords. + + Crucially, the scene is captured from a very low camera angle, almost at floor level, significantly zoomed out to showcase a much wider view of the room/setup. This low perspective emphasizes the expanse of the floor, which appears to stretch far into the distance, creating a profound sense of depth and length. The objects in the scene should appear much smaller in relation to the overall composition. + + Place the scene items, to the edges of the composition, so that when an object is placed in the middle of the foreground, it does not completely cover what's behind it. + """.trimIndent() + ) + object StarGazer: BackgroundOption( + "StarGazer", + "star", + R.drawable.background_option_stargazer, + aiBackground = true, + prompt = """ + A vibrant, soft 3D illustration of a minimalist stargazing setup. The scene is a mixture of realism and rounded, toy-like objects, rendered in a smooth, clean, and minimalist style. The space is characterized by smooth surfaces, subtle glossiness, and soft, even lighting that casts dynamic shadows, beautifully emphasizing the 3D form and depth. + + The is a small, stylized telescope with soft forms and rounded edges, resting on a clean, grassy hill. Next to it, a few props—such as a cozy blanket and a thermos—are arranged neatly. The atmosphere is tranquil and peaceful, with a palette of deep, vibrant colors against the night sky. There are stars and nebula in the nights sky, the night sky looks like something you might see from the Hubble space telescope. + + A shallow depth of field is used to keep the telescope and props in sharp focus. The foreground is a blank space, with only the grass visible, ready for a character or another object to be added later. + + Crucially, the scene is captured from a very low camera angle, almost at floor level, significantly zoomed out to showcase a much wider view of the room/setup. This low perspective emphasizes the expanse of the floor, which appears to stretch far into the distance, creating a profound sense of depth and length. The objects in the scene should appear much smaller in relation to the overall composition. + + Place the scene items, to the edges of the composition, so that when an object is placed in the middle of the foreground, it does not completely cover what's behind it. + """.trimIndent() + ) + + object FitnessBuff: BackgroundOption( + "Fitness buff", + "fitness", + R.drawable.background_option_fitness, + aiBackground = true, + prompt = """ + A soft, vibrant 3D illustration depicts a simplified, whimsical Synthwave Sweat Sanctuary fitness studio scene. Rounded, brightly colored objects define the smooth, clean, and minimalist aesthetic. The scene has an overall whimsical and playful feeling. The lighting is highly dynamic and dramatic, similar to a Pixar film, with strong, directional key lights creating crisp highlights and deep, expressive shadows that beautifully sculpt the 3D forms and emphasize depth. Contrasting colors in the lighting scheme add visual interest and warmth to the scene. + + The centerpiece is a clean, oversized aerobics stage ts surface stretching expansively into the background. Scattered minimally on its surface are iridescent, futuristic leg warmers and shimmering sweatbands, along with an oversized, bouncy boomboxes with built-in light shows and glowing protein shakers. Alongside these, a few sleek, brightly colored chrome dumbbells with integrated LED light strips and vibrant exercise step platforms. A shallow depth of field keeps these items sharp, while the background is a softly blurred, solid colored wall resembling a giant, glowing synthwave grid (e.g., deep purple to electric blue) a gentle backdrop, subtly illuminated by the same dynamic lighting. + + The immediate foreground is a blank, clean section of the glowing neon aerobics stage's surface, where the play of light and shadow creates intriguing patterns. + + Captured from a very low, zoomed-out angle, the scene emphasizes the vast aerobics stage and its contents, creating profound depth. Objects appear much smaller relative to the wide composition, subtly placed at the edges to leave the center foreground clear. Make sure that no characters appear in the scene, and that no objects are given eyes and mouths. There should be a clear horizon line close to the center of the composition where the floor meets the back wall. + + Crucially, the scene is captured from a very low camera angle, almost at floor level, significantly zoomed out to showcase a much wider view of the aerobics stage and its contents. This low perspective emphasizes the expanse of the stage's surface, which appears to stretch far into the distance, creating a profound sense of depth and length. Place the scene items towards the edges of the composition, ensuring that the vast middle foreground remains clear and open. + """.trimIndent() + ) + + object Fandroid: BackgroundOption( + "Fandroid", + "fandroid", + R.drawable.background_option_fandroid, + aiBackground = true, + prompt = """ + Get ready for a burst of energy with this soft, vibrant 3D illustration of a minimalist Google headquarters entrance scene to a beautiful large office park building. The composition, with rounded, toy-like objects, features chairs and android statues with a big Google logo sign on a manicured grassy and sidewalk area. Subtle glossiness and soft, even lighting cast dynamic shadows, beautifully emphasizing the 3D form and depth of the space and environment. The scene has an overall and playful feeling to it, objects in the scene are brightly colored and elaborate, almost unreal. + + The star of the show is the grass flooring. This isn't just any entrance area; its fun an overly large almost giant. + + A super shallow depth of field keeps all the elements in razor-sharp focus. The foreground is like a welcoming blank canvas of statues and entrance structures on a mix of grass and sidewalk. + + This 3D illustration portrays a whimsical outdoor scene, animated by a collection of toy-like, rounded Android mascots and dessert-themed figures, all rendered with vibrant colors, soft forms, and smooth surfaces. The 3D rendering imbues them with a subtle glossiness and a minimalist/clean, polished appearance. + + The scene is bathed in soft, even lighting, which, coupled with dynamic shadows, effectively emphasizes the 3D form and depth of each figure. Among the playful sculptures, from left to right, are a towering white soft-serve ice cream swirl with colorful berries at its base, a large brown donut adorned with sprinkles, a gingerbread man-like figure, a large orange and yellow archway resembling headphones (with a honeycomb pattern visible within), and another brown Android statue. In the foreground on the right, a prominent bright green Android robot holds a white, marshmallow-like object. + + These figures are set on a meticulously grass, while the background, composed of lush trees and subtle building outlines, is depicted with a gentle depth of field, appearing slightly blurry to direct focus onto the foreground figures. A deliberate blank grass space in the immediate foreground suggests an inviting spot for an additional character or object, seamlessly integrating into this charming blend of realism and 3D illustration. + + Crucially, the scene is captured from a very low camera angle, almost at floor level, significantly zoomed out to showcase a much wider view of the room/setup. This low perspective emphasizes the expanse of the floor, which appears to stretch far into the distance, creating a profound sense of depth and length. The objects in the scene should appear much smaller in relation to the overall composition. + + Place the scene items, to the edges of the composition, so that when an object is placed in the middle of the foreground, it does not completely cover what's behind it. + """.trimIndent() + ) + + object GreenThumb: BackgroundOption( + displayName = "Green thumb", + key = "green_thumb", + previewDrawableInt = R.drawable.background_option_greenthumb, + aiBackground = true, + prompt = """ + A vibrant, 3D illustration of a vibrant outdoor garden with fun plants. The flowers in this scene have a alien-like quality to them, and are brightly colored. The entire scene is rendered with a meticulous mixture of rounded, toy-like objects, creating a clean, minimalist aesthetic. Every element is characterized by smooth surfaces, subtle glossiness, and bright lighting that casts dynamic shadows, except for the ground which is covered in grass, beautifully emphasizing their 3D form and depth. + + There are rounded pots, smooth ceramic planters with cascading colorful unnatural looking greenery. A few gardening tools with toy-like appearances—like a small watering can with a soft form and a miniature trowel. There are only a few items in the scene, giving it a restrained minimal feel. There is a white picket fence in the distance. + + A shallow depth of field is used to keep the central grouping of plants and gardening tools in sharp focus, while the abundant plants and subtle room details in the background are slightly blurry. The foreground is a deliberate blank space, with only the clean, simple floor visible, offering a clean, open area for a future character or object to be added. + + Crucially, the scene is captured from a very low camera angle, almost at ground level, significantly zoomed out to showcase a much wider view of the space. This low perspective emphasizes the expanse of the ground, which appears to stretch far into the distance, creating a profound sense of depth and length. The objects in the scene should appear much smaller in relation to the overall composition. + + Place the scene items, to the edges of the composition, so that when an object is placed in the middle of the foreground, it does not completely cover what's behind it. + """.trimIndent() + ) + + object Gamer: BackgroundOption( + displayName = "Gamer", + key = "gamer", + previewDrawableInt = R.drawable.background_option_gamer, + aiBackground = true, + prompt = """ + Craft a vibrant 3D rendering of a pixelated, retro-futuristic video gaming setup scene that is both minimalist and clean, with a toy-like aesthetic. The composition should feature a living room corner with a gaming chair made of giant, soft-edged pixels, a headset, and various oversized snacks, all constructed with soft forms, rounded edges, and smooth surfaces. Use a palette of vibrant, neon arcade colors (electric blues, vivid purples, glowing greens) with a subtle glossiness to emphasize the 3D form and depth of the gaming equipment and items. + + Include various devices with different screen sizes, like a computer screen, tablet and mobile phone. + + The scene appears to be at night with the lights off, light coming from screens displaying shimmering, low-resolution landscapes, with laser beams forming geometric patterns and strobe lights pulsating like an old CRT television refresh rate illuminating the scene. Illumination should be soft, even lighting, creating dynamic shadows that further highlight the depth of the scene. The overall look should feel smooth and polished. The 3D illustration should have a depth of field, with the background, a blurry, glitching digital landscape, slightly blurry to draw focus. Leave a blank space in the foreground, as if a character or object could be placed there (but leave it blank for now). The style should be a compelling mixture of realism and the described artistic keywords. + + Crucially, the scene is captured from a very low camera angle, almost at floor level, significantly zoomed out to showcase a much wider view of the room/setup. This low perspective emphasizes the expanse of the floor, which appears to stretch far into the distance, creating a profound sense of depth and length. The objects in the scene should appear much smaller in relation to the overall composition. + + Place the scene items, to the edges of the composition, so that when an object is placed in the middle of the foreground, it does not completely cover what's behind it. + """.trimIndent() + ) + object Jetsetter: BackgroundOption( + displayName = "Jetsetter", + key = "jetsetter", + previewDrawableInt = R.drawable.background_option_jetsetter, + aiBackground = true, + prompt = """ + A soft, vibrant 3D illustration depicts a simplified scene featuring a whimsical, but not fantastical scene of 2 planes on the tarmac with luggage. They are facing each other with golden hour, warm, ethereal lighting that bathes the cloud station’s interior. TIt feels like a dream meticulously designed for explorers of the sky, where cloud-ships always depart on time. + + The tarmac and its surroundings are rendered with a meticulous blend of fluffy realism and rounded, toy-like objects, creating a clean aesthetic. The entire scene is characterized by subtle glossiness and soft, even lighting that casts dynamic shadows, beautifully emphasizing the 3D form and depth of the area Polished, dew-kissed surfaces reflect the warm, ethereal sunlight. + + A collection of brightly colored, stylized luggage — including vibrant bags, deep blue cloud-shaped carry-ons, and sunny yellow star-shaped duffel bags — sit neatly arranged. The foreground is a deliberate blank space, with only the clean, subtly glowing cloud floor visible, offering an open area for a future character or object to be added, perhaps a traveler waiting for their destination. The overall atmosphere is serene and anticipatory, with the warm, ethereal light creating long, dynamic shadows that enhance the 3D rendering. + + Crucially, the scene is captured from a very low camera angle, almost at ground level, significantly zoomed out to showcase a much wider view of the waiting area. This low perspective emphasizes the expansive cloud station, creating a profound sense of depth and scale. The area above the horizon line is clean, open, and uncluttered, emphasizing the vastness of the sky. The items and the colorful luggage appear smaller in relation to the overall composition, positioned slightly to the edges to allow the sweeping view of the planes and sky above, ensuring that the middle foreground remains clear and open. + """.trimIndent() + ) + + object Chef: BackgroundOption( + "Masterchef", + "chef", + R.drawable.background_option_chef, + aiBackground = true, + prompt = """ + A soft, vibrant 3D illustration depicts a surreal, bubbling pasta scene in a bright kitchen. Rounded, brightly colored, toy-like objects define the smooth, clean, and minimalist aesthetic. The scene has an overall whimsical and playful feeling. The lighting is highly dynamic, with strong, directional key lights creating crisp highlights and expressive shadows that beautifully sculpt the 3D forms and emphasize depth. Bright colors in the lighting scheme add visual interest and warmth to the scene. + + On its surface are cartoon shaped meat, vibrant and playful pancakes, along with a giant block of cheese, colored pots and pans and a banana and pineapple. There is a single cupcake in the scene. A shallow depth of field keeps these items sharp, while the background is a gentle backdrop, subtly illuminated by the same dynamic lighting. Remember to only include a few items to keep the scene simple. There shouldn't be too many items or clutter in the scene. It feels restrained. + + The immediate foreground is a blank, clean section of marble-like work surface, where the play of light and shadow creates intriguing patterns. + + Captured from a very low, zoomed-out angle, the scene emphasizes the vast work surface and its contents, creating profound depth. Objects appear much smaller relative to the wide composition, subtly placed at the edges to leave the center foreground and upper area clear, creating a parting in the middle of the scene. No characters appear, and no objects are given eyes and mouths. + + Crucially, the scene is captured from a very low camera angle, almost at floor level, significantly zoomed out to showcase a much wider view of the work surface and its contents. This low perspective emphasizes the expanse of the surface, which appears to stretch far into the distance, creating a profound sense of depth and length. Place the scene items towards the edges of the composition, ensuring that the vast middle foreground remains clear and open. + """.trimIndent() + ) } diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/GenericTool.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/GenericTool.kt index 5096b233..d286c83c 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/GenericTool.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/GenericTool.kt @@ -17,16 +17,19 @@ package com.android.developers.androidify.customize import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -39,6 +42,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max import coil3.compose.rememberAsyncImagePainter import com.android.developers.androidify.theme.AndroidifyTheme @@ -96,7 +100,8 @@ fun GenericToolButton( } Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = backgroundModifier.padding(8.dp), + modifier = backgroundModifier.padding(8.dp) + .width(IntrinsicSize.Min), ) { Box( modifier = Modifier @@ -109,6 +114,11 @@ fun GenericToolButton( tool.displayName, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + modifier = Modifier.basicMarquee( + repeatDelayMillis = 0, + iterations = 300 + ) ) } } @@ -123,6 +133,20 @@ private fun GenericToolPreview() { BackgroundOption.None, BackgroundOption.Lightspeed, BackgroundOption.IO, + BackgroundOption.None, + BackgroundOption.Plain, + BackgroundOption.Lightspeed, + BackgroundOption.IO, + BackgroundOption.MusicLover, + BackgroundOption.PoolMaven, + BackgroundOption.SoccerFanatic, + BackgroundOption.StarGazer, + BackgroundOption.FitnessBuff, + BackgroundOption.Fandroid, + BackgroundOption.GreenThumb, + BackgroundOption.Gamer, + BackgroundOption.Jetsetter, + BackgroundOption.Chef ), singleLine = false, selectedOption = BackgroundOption.Lightspeed, diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/ImageRenderer.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/ImageRenderer.kt index 05e7f926..63522b46 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/ImageRenderer.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/ImageRenderer.kt @@ -74,7 +74,15 @@ fun ImageResult( exportImageCanvas, modifier = Modifier.fillMaxSize(), ) { - if (exportImageCanvas.imageBitmap != null) { + if (exportImageCanvas.imageWithEdit != null) { + Image( + bitmap = exportImageCanvas.imageWithEdit.asImageBitmap(), + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } else if (exportImageCanvas.imageBitmap != null) { Image( bitmap = exportImageCanvas.imageBitmap.asImageBitmap(), modifier = Modifier diff --git a/feature/results/src/main/res/drawable-nodpi/background_banner_lightspeed.png b/feature/results/src/main/res/drawable-nodpi/background_banner_lightspeed.png index d7319403..dfccfa9e 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_banner_lightspeed.png and b/feature/results/src/main/res/drawable-nodpi/background_banner_lightspeed.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_banner_plain.png b/feature/results/src/main/res/drawable-nodpi/background_banner_plain.png index db24e0a0..5df0772d 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_banner_plain.png and b/feature/results/src/main/res/drawable-nodpi/background_banner_plain.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_banner_shapes.png b/feature/results/src/main/res/drawable-nodpi/background_banner_shapes.png index d055ce95..aaeac12b 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_banner_shapes.png and b/feature/results/src/main/res/drawable-nodpi/background_banner_shapes.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_banner_square.png b/feature/results/src/main/res/drawable-nodpi/background_banner_square.png index d676f34d..a41f8f46 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_banner_square.png and b/feature/results/src/main/res/drawable-nodpi/background_banner_square.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_chef.png b/feature/results/src/main/res/drawable-nodpi/background_option_chef.png new file mode 100644 index 00000000..56dab474 Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_chef.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_fandroid.png b/feature/results/src/main/res/drawable-nodpi/background_option_fandroid.png new file mode 100644 index 00000000..10c15b75 Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_fandroid.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_fitness.png b/feature/results/src/main/res/drawable-nodpi/background_option_fitness.png new file mode 100644 index 00000000..2ac581a9 Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_fitness.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_gamer.png b/feature/results/src/main/res/drawable-nodpi/background_option_gamer.png new file mode 100644 index 00000000..22e5daff Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_gamer.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_greenthumb.png b/feature/results/src/main/res/drawable-nodpi/background_option_greenthumb.png new file mode 100644 index 00000000..382165ec Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_greenthumb.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_jetsetter.png b/feature/results/src/main/res/drawable-nodpi/background_option_jetsetter.png new file mode 100644 index 00000000..8016bf6f Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_jetsetter.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_music_lover.png b/feature/results/src/main/res/drawable-nodpi/background_option_music_lover.png new file mode 100644 index 00000000..6504dfa2 Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_music_lover.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_pool.png b/feature/results/src/main/res/drawable-nodpi/background_option_pool.png new file mode 100644 index 00000000..a52fc903 Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_pool.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_soccer.png b/feature/results/src/main/res/drawable-nodpi/background_option_soccer.png new file mode 100644 index 00000000..b92bd903 Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_soccer.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_option_stargazer.png b/feature/results/src/main/res/drawable-nodpi/background_option_stargazer.png new file mode 100644 index 00000000..b4e836b6 Binary files /dev/null and b/feature/results/src/main/res/drawable-nodpi/background_option_stargazer.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_social_header_lightspeed.png b/feature/results/src/main/res/drawable-nodpi/background_social_header_lightspeed.png index 6249c640..0657387e 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_social_header_lightspeed.png and b/feature/results/src/main/res/drawable-nodpi/background_social_header_lightspeed.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_social_header_plain.png b/feature/results/src/main/res/drawable-nodpi/background_social_header_plain.png index 97fb831c..a8d6300a 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_social_header_plain.png and b/feature/results/src/main/res/drawable-nodpi/background_social_header_plain.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_social_header_shape.png b/feature/results/src/main/res/drawable-nodpi/background_social_header_shape.png index a83804da..8127b4cc 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_social_header_shape.png and b/feature/results/src/main/res/drawable-nodpi/background_social_header_shape.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_square_blocks.png b/feature/results/src/main/res/drawable-nodpi/background_square_blocks.png index 4d72b39d..1f42d180 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_square_blocks.png and b/feature/results/src/main/res/drawable-nodpi/background_square_blocks.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_square_lightspeed.png b/feature/results/src/main/res/drawable-nodpi/background_square_lightspeed.png index 10d35ccb..0ff36d4c 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_square_lightspeed.png and b/feature/results/src/main/res/drawable-nodpi/background_square_lightspeed.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_square_none.png b/feature/results/src/main/res/drawable-nodpi/background_square_none.png index 87d14c86..2f7d8046 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_square_none.png and b/feature/results/src/main/res/drawable-nodpi/background_square_none.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_lightspeed.png b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_lightspeed.png index 88cb1f9a..2acf464d 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_lightspeed.png and b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_lightspeed.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_plain.png b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_plain.png index b10f16e4..6dba9772 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_plain.png and b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_plain.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_shapes.png b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_shapes.png index d8514fc8..d11ea607 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_shapes.png and b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_shapes.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_light.png b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_light.png index e7748c5b..d18cad0c 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_light.png and b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_light.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_lightspeed.png b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_lightspeed.png index 59e29a35..b34ea075 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_lightspeed.png and b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_lightspeed.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_shapes.png b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_shapes.png index 27474557..4d651473 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_shapes.png and b/feature/results/src/main/res/drawable-nodpi/background_wallpaper_tablet_shapes.png differ diff --git a/feature/results/src/main/res/drawable-nodpi/placeholderbot.png b/feature/results/src/main/res/drawable-nodpi/placeholderbot.png index 11f77d9d..c6fb50d1 100644 Binary files a/feature/results/src/main/res/drawable-nodpi/placeholderbot.png and b/feature/results/src/main/res/drawable-nodpi/placeholderbot.png differ diff --git a/feature/results/src/main/res/drawable/round_auto_awesome_24.xml b/feature/results/src/main/res/drawable/round_auto_awesome_24.xml new file mode 100644 index 00000000..c0b86411 --- /dev/null +++ b/feature/results/src/main/res/drawable/round_auto_awesome_24.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/feature/results/src/main/res/drawable/spark.xml b/feature/results/src/main/res/drawable/spark.xml new file mode 100644 index 00000000..811fca69 --- /dev/null +++ b/feature/results/src/main/res/drawable/spark.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/feature/results/src/test/kotlin/com/android/developers/androidify/customize/CustomizeStateTest.kt b/feature/results/src/test/kotlin/com/android/developers/androidify/customize/CustomizeStateTest.kt index 73ff8cda..18428791 100644 --- a/feature/results/src/test/kotlin/com/android/developers/androidify/customize/CustomizeStateTest.kt +++ b/feature/results/src/test/kotlin/com/android/developers/androidify/customize/CustomizeStateTest.kt @@ -68,6 +68,16 @@ class CustomizeStateTest { BackgroundOption.Plain, BackgroundOption.Lightspeed, BackgroundOption.IO, + BackgroundOption.MusicLover, + BackgroundOption.PoolMaven, + BackgroundOption.SoccerFanatic, + BackgroundOption.StarGazer, + BackgroundOption.FitnessBuff, + BackgroundOption.Fandroid, + BackgroundOption.GreenThumb, + BackgroundOption.Gamer, + BackgroundOption.Jetsetter, + BackgroundOption.Chef ), state.options, ) diff --git a/feature/results/src/test/kotlin/com/android/developers/androidify/customize/CustomizeViewModelTest.kt b/feature/results/src/test/kotlin/com/android/developers/androidify/customize/CustomizeViewModelTest.kt index 73cf7f8c..e34733ad 100644 --- a/feature/results/src/test/kotlin/com/android/developers/androidify/customize/CustomizeViewModelTest.kt +++ b/feature/results/src/test/kotlin/com/android/developers/androidify/customize/CustomizeViewModelTest.kt @@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider import com.android.developers.testing.repository.FakeImageGenerationRepository import com.android.developers.testing.util.FakeComposableBitmapRenderer import com.android.developers.testing.util.MainDispatcherRule +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -34,8 +35,11 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import kotlin.test.DefaultAsserter.assertNotNull +import kotlin.test.assertContains +import kotlin.test.assertFalse import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue @RunWith(RobolectricTestRunner::class) class CustomizeViewModelTest { @@ -135,4 +139,68 @@ class CustomizeViewModelTest { advanceUntilIdle() assertNotNull(values.last().savedUri) } + + @Test + fun changeBackground_NotNull() = runTest { + val viewModel = CustomizeExportViewModel( + FakeImageGenerationRepository(), + composableBitmapRenderer = FakeComposableBitmapRenderer(), + application = ApplicationProvider.getApplicationContext(), + ) + val values = mutableListOf() + // Launch collector on the backgroundScope directly to use runTest's scheduler + backgroundScope.launch(UnconfinedTestDispatcher()) { + viewModel.state.collect { + values.add(it) + } + } + viewModel.setArguments( + fakeBitmap, + originalFakeUri, + ) + advanceUntilIdle() + viewModel.selectedToolStateChanged( + BackgroundToolState( + selectedToolOption = BackgroundOption.Chef, + options = listOf( + BackgroundOption.None, + BackgroundOption.IO, + BackgroundOption.Chef + ), + ), + ) + advanceUntilIdle() + assertFalse { values[values.lastIndex].showImageEditProgress } + // assertTrue(values.any { it.showImageEditProgress }) + assertNotNull(values.last().exportImageCanvas.imageWithEdit) + } + + @Test + fun changeBackground_None() = runTest { + val values = mutableListOf() + // Launch collector on the backgroundScope directly to use runTest's scheduler + backgroundScope.launch(UnconfinedTestDispatcher()) { + viewModel.state.collect { + values.add(it) + } + } + viewModel.setArguments( + fakeBitmap, + originalFakeUri, + ) + advanceUntilIdle() + viewModel.selectedToolStateChanged( + BackgroundToolState( + selectedToolOption = BackgroundOption.None, + options = listOf( + BackgroundOption.None, + BackgroundOption.IO, + BackgroundOption.Chef + ), + ), + ) + advanceUntilIdle() + assertTrue { !values[values.lastIndex].showImageEditProgress } + assertNull(values.last().exportImageCanvas.imageWithEdit) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 08a050d4..3e3a5624 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # build -agp = "8.11.0" +agp = "8.11.1" compileSdk = "36" leakcanaryAndroid = "2.14" minSdk = "26" @@ -13,12 +13,12 @@ activityCompose = "1.10.1" adaptive = "1.1.0" animationAndroid = "1.8.3" appcompat = "1.7.1" -baselineprofile = "1.3.4" -benchmarkMacroJunit4 = "1.3.4" -camerax = "1.5.0-beta01" -coilCompose = "3.2.0" -coilGif = "3.2.0" -composeBom = "2025.06.02" +baselineprofile = "1.4.0" +benchmarkMacroJunit4 = "1.4.0" +camerax = "1.5.0-beta02" +coilCompose = "3.3.0" +coilGif = "3.3.0" +composeBom = "2025.07.01" concurrent = "1.2.0" converterGson = "2.11.0" coreKtx = "1.16.0"