From e3866a8d00e11ed63e2521e653d23a5b3a258d4f Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Tue, 9 Sep 2025 14:26:33 +0530 Subject: [PATCH 01/17] Switched back to Image from AsyncImage in ImageRenderer.kt for displaying the Image --- .../developers/androidify/customize/ImageRenderer.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 dc26cac7..11e4fede 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 @@ -44,7 +44,6 @@ import androidx.compose.ui.layout.layout import androidx.compose.ui.res.imageResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastRoundToInt -import coil3.compose.AsyncImage import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.LocalAnimateBoundsScope @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalSharedTransitionApi::class) @@ -90,9 +89,9 @@ fun ImageResult( contentScale = ContentScale.Crop, contentDescription = null, ) - } else if (exportImageCanvas.imageUri != null) { - AsyncImage( - model = exportImageCanvas.imageUri, + } else if (exportImageCanvas.imageBitmap != null) { + Image( + bitmap = exportImageCanvas.imageBitmap.asImageBitmap(), modifier = Modifier .fillMaxSize(), contentScale = ContentScale.Crop, From 3285cfac723dcf93685c46229109289ae57c210d Mon Sep 17 00:00:00 2001 From: srikrishnasakunia Date: Mon, 11 Aug 2025 16:25:27 +0530 Subject: [PATCH 02/17] Moved ResultsScreen.kt & CustomizeExportScreen.kt to open from MainNavigation.kt using Nav3 instead of driving them from CreationScreen.kt --- .../androidify/navigation/MainNavigation.kt | 48 +++++++++++++++++++ .../androidify/navigation/NavigationRoutes.kt | 6 +++ .../androidify/creation/CreationScreen.kt | 37 ++------------ .../androidify/creation/CreationViewModel.kt | 44 ++++++++++------- .../creation/CreationViewModelTest.kt | 4 +- .../androidify/results/ResultsScreenTest.kt | 14 +++--- .../customize/CustomizeExportScreen.kt | 16 +++---- .../customize/CustomizeExportViewModel.kt | 4 +- .../androidify/customize/CustomizeState.kt | 2 +- .../androidify/customize/ImageRenderer.kt | 33 ++++++------- .../androidify/results/BotResultCard.kt | 24 ++-------- .../androidify/results/ResultsScreen.kt | 31 +++++++----- .../androidify/results/ResultsViewModel.kt | 4 +- 13 files changed, 149 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt b/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt index 8eab6f36..2d302a21 100644 --- a/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt +++ b/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.IntOffset +import androidx.core.net.toUri import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entryProvider @@ -39,8 +40,10 @@ 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.customize.CustomizeAndExportScreen 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.theme.transitions.ColorSplashTransitionScreen import com.google.android.gms.oss.licenses.OssLicensesMenuActivity @@ -110,6 +113,51 @@ fun MainNavigation() { onAboutPressed = { backStack.add(About) }, + onImageCreated = { resultImageUri, prompt, originalImageUri -> + backStack.removeAll{ it is ImageResult} + backStack.add( + ImageResult( + result = resultImageUri.toString(), + prompt = prompt, + originalImageUri = originalImageUri?.toString() + ) + ) + } + ) + } + entry { resultKey -> + ResultsScreen( + resultImageUri = resultKey.result.toUri(), + originalImageUri = resultKey.originalImageUri?.toUri(), + onNextPress = { resultImageUri, originalImageUri -> + backStack.removeAll{ it is ImageResult} + backStack.add( + ShareResult( + resultUri = resultImageUri.toString(), + originalImageUri = originalImageUri?.toString() + ) + ) + }, + promptText = resultKey.prompt, + onAboutPress = { + backStack.add(About) + }, + onBackPress = { + backStack.removeLastOrNull() + backStack.add(Create(fileName = resultKey.originalImageUri, prompt = resultKey.prompt)) + } + ) + } + entry { shareKey -> + CustomizeAndExportScreen( + resultImageUri = shareKey.resultUri.toUri(), + originalImageUri = shareKey.originalImageUri?.toUri(), + onBackPress = { + backStack.removeLastOrNull() + }, + onInfoPress = { + backStack.add(About) + } ) } entry { diff --git a/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt b/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt index 81c8338f..46f5f943 100644 --- a/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt +++ b/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt @@ -33,3 +33,9 @@ object Camera : NavigationRoute @Serializable object About : NavigationRoute + +@Serializable +data class ImageResult(val originalImageUri: String? = null, val prompt: String? = null, val result: String) : NavigationRoute + +@Serializable +data class ShareResult(val resultUri: String, val originalImageUri: String?) : NavigationRoute \ No newline at end of file diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt index c16c1de6..b27f371d 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt @@ -165,6 +165,7 @@ fun CreationScreen( onCameraPressed: () -> Unit = {}, onBackPressed: () -> Unit, onAboutPressed: () -> Unit, + onImageCreated: (resultImageUri: Uri, prompt: String?, originalImageUri: Uri?) -> Unit, ) { val uiState by creationViewModel.uiState.collectAsStateWithLifecycle() BackHandler( @@ -214,44 +215,16 @@ fun CreationScreen( } ScreenState.RESULT -> { - val prompt = uiState.descriptionText.text.toString() - val key = if (uiState.descriptionText.text.isBlank()) { - uiState.imageUri.toString() - } else { - prompt - } - ResultsScreen( - uiState.resultBitmap!!, + onImageCreated( + uiState.resultBitmapUri!!, + uiState.descriptionText.text.toString(), if (uiState.selectedPromptOption == PromptType.PHOTO) { uiState.imageUri } else { null - }, - promptText = prompt, - viewModel = hiltViewModel(key = key), - onAboutPress = onAboutPressed, - onBackPress = onBackPressed, - onNextPress = creationViewModel::customizeExportClicked, + } ) } - - ScreenState.CUSTOMIZE -> { - val prompt = uiState.descriptionText.text.toString() - val key = if (uiState.descriptionText.text.isBlank()) { - uiState.imageUri.toString() - } else { - prompt - } - uiState.resultBitmap?.let { bitmap -> - CustomizeAndExportScreen( - resultImage = bitmap, - originalImageUri = uiState.imageUri, - onBackPress = onBackPressed, - onInfoPress = onAboutPressed, - viewModel = hiltViewModel(key = key), - ) - } - } } } diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt index b9e34558..45e0f3a2 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt @@ -36,11 +36,15 @@ import com.android.developers.androidify.data.TextGenerationRepository import com.android.developers.androidify.util.LocalFileProvider import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream import javax.inject.Inject @HiltViewModel @@ -153,7 +157,7 @@ class CreationViewModel @Inject constructor( ) } _uiState.update { - it.copy(resultBitmap = bitmap, screenState = ScreenState.RESULT) + it.copy(resultBitmapUri = saveBitmapToCache(context, bitmap), screenState = ScreenState.RESULT) } } catch (e: Exception) { handleImageGenerationError(e) @@ -220,25 +224,13 @@ class CreationViewModel @Inject constructor( ScreenState.RESULT -> { _uiState.update { - it.copy(screenState = ScreenState.EDIT, resultBitmap = null) + it.copy(screenState = ScreenState.EDIT, resultBitmapUri = null) } } ScreenState.EDIT -> { // do nothing, back press handled outside } - - ScreenState.CUSTOMIZE -> { - _uiState.update { - it.copy(screenState = ScreenState.RESULT) - } - } - } - } - - fun customizeExportClicked() { - _uiState.update { - it.copy(screenState = ScreenState.CUSTOMIZE) } } } @@ -252,14 +244,13 @@ data class CreationState( val generatedPrompt: String? = null, val promptGenerationInProgress: Boolean = false, val screenState: ScreenState = ScreenState.EDIT, - val resultBitmap: Bitmap? = null, + val resultBitmapUri: Uri? = null, ) enum class ScreenState { EDIT, LOADING, RESULT, - CUSTOMIZE, } data class BotColor( @@ -301,3 +292,24 @@ enum class PromptType(val displayName: String) { PHOTO("Photo"), TEXT("Prompt"), } + +suspend fun saveBitmapToCache( + context: Context, + bitmap: Bitmap, + compressionFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, + quality: Int = 100 +): Uri? = withContext(Dispatchers.IO) { + + val cacheDir = context.cacheDir + val fileName = File(cacheDir, "temp_image_${System.currentTimeMillis()}.jpg") + try { + FileOutputStream(fileName).use { outputStream -> + bitmap.compress(compressionFormat, quality, outputStream) + } + Uri.fromFile(fileName) + } catch (e: Exception) { + e.printStackTrace() + null + } + +} \ No newline at end of file diff --git a/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt index 574fda1e..1bc07544 100644 --- a/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt +++ b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt @@ -137,7 +137,7 @@ class CreationViewModelTest { viewModel.onSelectedPromptOptionChanged(PromptType.PHOTO) viewModel.startClicked() assertEquals(ScreenState.RESULT, viewModel.uiState.value.screenState) - assertNotNull(viewModel.uiState.value.resultBitmap) + assertNotNull(viewModel.uiState.value.resultBitmapUri) } @Test @@ -198,7 +198,7 @@ class CreationViewModelTest { } viewModel.startClicked() assertEquals(ScreenState.RESULT, viewModel.uiState.value.screenState) - assertNotNull(viewModel.uiState.value.resultBitmap) + assertNotNull(viewModel.uiState.value.resultBitmapUri) } @Test diff --git a/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt b/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt index cad23c8c..5dba07c0 100644 --- a/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt +++ b/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt @@ -42,14 +42,14 @@ class ResultsScreenTest { val composeTestRule = createAndroidComposeRule() // Create a dummy bitmap for testing - private val testBitmap: Bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) + val dummyUri = android.net.Uri.parse("dummy://image") @Test fun resultsScreenContents_displaysActionButtons() { val shareButtonText = composeTestRule.activity.getString(R.string.customize_and_share) // Note: Download button is identified by icon, harder to test reliably without tags/desc - val initialState = ResultState(resultImageBitmap = testBitmap, promptText = "test") + val initialState = ResultState(resultImageUri = dummyUri, promptText = "test") val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -78,7 +78,7 @@ class ResultsScreenTest { val frontCardDesc = composeTestRule.activity.getString(R.string.resultant_android_bot) // Ensure promptText is non-null when bitmap is present - val initialState = ResultState(resultImageBitmap = testBitmap, promptText = "test") + val initialState = ResultState(resultImageUri = dummyUri, promptText = "test") val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -108,7 +108,7 @@ class ResultsScreenTest { val backCardDesc = composeTestRule.activity.getString(R.string.original_image) val dummyUri = android.net.Uri.parse("dummy://image") - val initialState = ResultState(resultImageBitmap = testBitmap, originalImageUrl = dummyUri) + val initialState = ResultState(resultImageUri = dummyUri, originalImageUrl = dummyUri) val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -143,7 +143,7 @@ class ResultsScreenTest { val promptText = "test prompt" val promptPrefix = composeTestRule.activity.getString(R.string.my_bot_is_wearing) - val initialState = ResultState(resultImageBitmap = testBitmap, promptText = promptText) // No original image URI + val initialState = ResultState(resultImageUri = dummyUri, promptText = promptText) // No original image URI val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -175,7 +175,7 @@ class ResultsScreenTest { val frontCardDesc = composeTestRule.activity.getString(R.string.resultant_android_bot) val dummyUri = android.net.Uri.parse("dummy://image") - val initialState = ResultState(resultImageBitmap = testBitmap, originalImageUrl = dummyUri) + val initialState = ResultState(resultImageUri = dummyUri, originalImageUrl = dummyUri) val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -210,7 +210,7 @@ class ResultsScreenTest { var shareClicked = false // Ensure promptText is non-null when bitmap is present - val initialState = ResultState(resultImageBitmap = testBitmap, promptText = "test") + val initialState = ResultState(resultImageUri = dummyUri, promptText = "test") val state = mutableStateOf(initialState) composeTestRule.setContent { 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 9b5f8736..06b8ca4e 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 @@ -63,7 +63,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.dropShadow import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.LookaheadScope @@ -73,6 +72,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.ui.LocalNavAnimatedContentScope @@ -100,15 +100,15 @@ import com.android.developers.androidify.theme.R as ThemeR @OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomizeAndExportScreen( - resultImage: Bitmap, + resultImageUri: Uri, originalImageUri: Uri?, onBackPress: () -> Unit, onInfoPress: () -> Unit, isMediumWindowSize: Boolean = isAtLeastMedium(), viewModel: CustomizeExportViewModel = hiltViewModel(), ) { - LaunchedEffect(resultImage, originalImageUri) { - viewModel.setArguments(resultImage, originalImageUri) + LaunchedEffect(resultImageUri, originalImageUri) { + viewModel.setArguments(resultImageUri, originalImageUri) } val state = viewModel.state.collectAsStateWithLifecycle() @@ -494,9 +494,9 @@ fun CustomizeExportPreview() { displayName = "Pixel Watch 3", hasAndroidify = true, ) - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() val state = CustomizeExportState( - exportImageCanvas = ExportImageCanvas(imageBitmap = bitmap.asAndroidBitmap()), + exportImageCanvas = ExportImageCanvas(imageUri = imageUri), connectedWatch = connectedWatch, ) CustomizeExportContents( @@ -526,7 +526,7 @@ fun CustomizeExportPreviewLarge() { AnimatedContent(true) { targetState -> targetState CompositionLocalProvider(LocalNavAnimatedContentScope provides this@AnimatedContent) { - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() val connectedWatch = ConnectedWatch( nodeId = "1234", displayName = "Pixel Watch 3", @@ -534,7 +534,7 @@ fun CustomizeExportPreviewLarge() { ) val state = CustomizeExportState( exportImageCanvas = ExportImageCanvas( - imageBitmap = bitmap.asAndroidBitmap(), + imageUri = imageUri, aspectRatioOption = SizeOption.Square, ), selectedTool = CustomizeTool.Background, 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 4753afae..cd9441ce 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 @@ -120,13 +120,13 @@ class CustomizeExportViewModel @Inject constructor( } fun setArguments( - resultImageUrl: Bitmap, + resultImageUrl: Uri, originalImageUrl: Uri?, ) { _state.update { _state.value.copy( originalImageUrl, - exportImageCanvas = it.exportImageCanvas.copy(imageBitmap = resultImageUrl), + exportImageCanvas = it.exportImageCanvas.copy(imageUri = resultImageUrl), ) } } 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 06c9cc1a..61dfc918 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 @@ -70,7 +70,7 @@ data class BackgroundToolState( ) : ToolState data class ExportImageCanvas( - val imageBitmap: Bitmap? = null, + val imageUri: Uri? = null, val imageBitmapRemovedBackground: Bitmap? = null, val aspectRatioOption: SizeOption = SizeOption.Square, val canvasSize: Size = Size(1000f, 1000f), 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 18cd1bef..2a43798d 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 @@ -38,13 +38,14 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layout import androidx.compose.ui.res.imageResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastRoundToInt +import androidx.core.net.toUri +import coil3.compose.AsyncImage import com.android.developers.androidify.results.R import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.LocalAnimateBoundsScope @@ -91,9 +92,9 @@ fun ImageResult( contentScale = ContentScale.Crop, contentDescription = null, ) - } else if (exportImageCanvas.imageBitmap != null) { - Image( - bitmap = exportImageCanvas.imageBitmap.asImageBitmap(), + } else if (exportImageCanvas.imageUri != null) { + AsyncImage( + model = exportImageCanvas.imageUri, modifier = Modifier .fillMaxSize(), contentScale = ContentScale.Crop, @@ -209,12 +210,12 @@ private fun Modifier.safeAnimateBounds(): Modifier { @Preview @Composable private fun ImageRendererPreviewSquare() { - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageBitmap = bitmap.asAndroidBitmap(), + imageUri = imageUri, canvasSize = Size(1000f, 1000f), aspectRatioOption = SizeOption.Square, selectedBackgroundOption = BackgroundOption.IO, @@ -232,11 +233,11 @@ private fun ImageRendererPreviewSquare() { @Preview @Composable private fun ImageRendererPreviewBanner() { - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageBitmap = bitmap.asAndroidBitmap(), + imageUri = imageUri, canvasSize = Size(1000f, 1000f), aspectRatioOption = SizeOption.Banner, selectedBackgroundOption = BackgroundOption.Lightspeed, @@ -254,11 +255,11 @@ private fun ImageRendererPreviewBanner() { @Preview @Composable private fun ImageRendererPreviewWallpaper() { - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageBitmap = bitmap.asAndroidBitmap(), + imageUri = imageUri, canvasSize = Size(1000f, 1000f), aspectRatioOption = SizeOption.Wallpaper, selectedBackgroundOption = BackgroundOption.Lightspeed, @@ -276,11 +277,11 @@ private fun ImageRendererPreviewWallpaper() { @Preview(widthDp = 1280, heightDp = 800) @Composable private fun ImageRendererPreviewWallpaperTablet() { - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageBitmap = bitmap.asAndroidBitmap(), + imageUri = imageUri, canvasSize = Size(1280f, 800f), aspectRatioOption = SizeOption.WallpaperTablet, selectedBackgroundOption = BackgroundOption.Lightspeed, @@ -298,11 +299,11 @@ private fun ImageRendererPreviewWallpaperTablet() { @Preview @Composable private fun ImageRendererPreviewWallpaperSocial() { - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageBitmap = bitmap.asAndroidBitmap(), + imageUri = imageUri, canvasSize = Size(1600f, 900f), aspectRatioOption = SizeOption.SocialHeader, selectedBackgroundOption = BackgroundOption.Lightspeed, @@ -320,11 +321,11 @@ private fun ImageRendererPreviewWallpaperSocial() { @Preview @Composable fun ImageRendererPreviewWallpaperIO() { - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageBitmap = bitmap.asAndroidBitmap(), + imageUri = imageUri, canvasSize = Size(1600f, 900f), aspectRatioOption = SizeOption.SocialHeader, selectedBackgroundOption = BackgroundOption.IO, diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt b/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt index 0934b485..4f78fd3d 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt @@ -50,7 +50,7 @@ import coil3.compose.AsyncImage @Composable fun BotResultCard( - resultImage: Bitmap, + resultImageUri: Uri, originalImageUrl: Uri?, promptText: String?, flippableState: FlippableState, @@ -66,11 +66,11 @@ fun BotResultCard( flippableState = flippableState, onFlipStateChanged = onFlipStateChanged, front = { - FrontCard(resultImage) + ImageCard(resultImageUri, isBack = false) }, back = { if (originalImageUrl != null) { - BackCard(originalImageUrl) + ImageCard(originalImageUrl, isBack = true) } else { BackCardPrompt(promptText!!) } @@ -79,24 +79,10 @@ fun BotResultCard( } @Composable -private fun FrontCard(bitmap: Bitmap) { - AsyncImage( - model = bitmap, - contentDescription = stringResource(R.string.resultant_android_bot), - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .aspectRatio(BOT_ASPECT_RATIO) - .shadow(8.dp, shape = MaterialTheme.shapes.large) - .clip(MaterialTheme.shapes.large), - ) -} - -@Composable -private fun BackCard(originalImageUrl: Uri) { +private fun ImageCard(originalImageUrl: Uri, isBack: Boolean) { AsyncImage( model = originalImageUrl, - contentDescription = stringResource(R.string.original_image), + contentDescription = if (isBack)stringResource(R.string.original_image) else stringResource(R.string.resultant_android_bot), contentScale = ContentScale.Crop, modifier = Modifier .fillMaxSize() diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt index a6c2036c..fd9df9ca 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt @@ -17,7 +17,6 @@ package com.android.developers.androidify.results -import android.graphics.Bitmap import android.net.Uri import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.EaseOutBack @@ -75,22 +74,23 @@ import com.android.developers.androidify.util.SmallPhonePreview import com.android.developers.androidify.util.allowsFullContent import com.android.developers.androidify.util.isAtLeastMedium import com.google.accompanist.permissions.ExperimentalPermissionsApi +import androidx.core.net.toUri @Composable fun ResultsScreen( - resultImage: Bitmap, + resultImageUri: Uri, originalImageUri: Uri?, promptText: String?, modifier: Modifier = Modifier, verboseLayout: Boolean = allowsFullContent(), onBackPress: () -> Unit, onAboutPress: () -> Unit, - onNextPress: () -> Unit, - viewModel: ResultsViewModel = hiltViewModel(), + onNextPress: (resultImageUri:Uri, originalImageUri:Uri?) -> Unit, + viewModel: ResultsViewModel = hiltViewModel(), ) { val state = viewModel.state.collectAsStateWithLifecycle() - LaunchedEffect(resultImage, originalImageUri, promptText) { - viewModel.setArguments(resultImage, originalImageUri, promptText) + LaunchedEffect(resultImageUri, originalImageUri, promptText) { + viewModel.setArguments(resultImageUri, originalImageUri, promptText) } val snackbarHostState by viewModel.snackbarHostState.collectAsStateWithLifecycle() Scaffold( @@ -121,7 +121,12 @@ fun ResultsScreen( contentPadding, state, verboseLayout = verboseLayout, - onCustomizeShareClicked = onNextPress, + onCustomizeShareClicked = { + onNextPress( + resultImageUri, + originalImageUri, + ) + }, ) } } @@ -132,11 +137,11 @@ fun ResultsScreen( @Composable private fun ResultsScreenPreview() { AndroidifyTheme { - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() val state = remember { mutableStateOf( ResultState( - resultImageBitmap = bitmap.asAndroidBitmap(), + resultImageUri = imageUri, promptText = "wearing a hat with straw hair", ), ) @@ -154,11 +159,11 @@ private fun ResultsScreenPreview() { @Composable private fun ResultsScreenPreviewSmall() { AndroidifyTheme { - val bitmap = ImageBitmap.imageResource(R.drawable.placeholderbot) + val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() val state = remember { mutableStateOf( ResultState( - resultImageBitmap = bitmap.asAndroidBitmap(), + resultImageUri = imageUri, promptText = "wearing a hat with straw hair", ), ) @@ -182,7 +187,7 @@ fun ResultsScreenContents( defaultSelectedResult: ResultOption = ResultOption.ResultImage, ) { ResultsBackground() - val showResult = state.value.resultImageBitmap != null + val showResult = state.value.resultImageUri != null var selectedResultOption by remember { mutableStateOf(defaultSelectedResult) } @@ -210,7 +215,7 @@ fun ResultsScreenContents( .fillMaxSize(), ) { BotResultCard( - state.value.resultImageBitmap!!, + state.value.resultImageUri!!, state.value.originalImageUrl, state.value.promptText, modifier = Modifier.align(Alignment.Center), diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt index a54af8ea..70c6ca5b 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt @@ -38,7 +38,7 @@ class ResultsViewModel @Inject constructor() : ViewModel() { get() = _snackbarHostState fun setArguments( - resultImageUrl: Bitmap, + resultImageUrl: Uri?, originalImageUrl: Uri?, promptText: String?, ) { @@ -49,7 +49,7 @@ class ResultsViewModel @Inject constructor() : ViewModel() { } data class ResultState( - val resultImageBitmap: Bitmap? = null, + val resultImageUri: Uri? = null, val originalImageUrl: Uri? = null, val promptText: String? = null, ) From 92c777469572753d164e87abdf6e7fa363374b70 Mon Sep 17 00:00:00 2001 From: srikrishnasakunia Date: Mon, 11 Aug 2025 18:45:30 +0530 Subject: [PATCH 03/17] Refactor image saving and loading in Creation and Customize features --- .../androidify/creation/CreationViewModel.kt | 28 +------------------ .../customize/CustomizeExportViewModel.kt | 23 ++++++++++++++- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt index 45e0f3a2..499881cb 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt @@ -16,7 +16,6 @@ package com.android.developers.androidify.creation import android.content.Context -import android.graphics.Bitmap import android.net.Uri import android.util.Log import androidx.compose.foundation.text.input.TextFieldState @@ -36,15 +35,11 @@ import com.android.developers.androidify.data.TextGenerationRepository import com.android.developers.androidify.util.LocalFileProvider import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream import javax.inject.Inject @HiltViewModel @@ -157,7 +152,7 @@ class CreationViewModel @Inject constructor( ) } _uiState.update { - it.copy(resultBitmapUri = saveBitmapToCache(context, bitmap), screenState = ScreenState.RESULT) + it.copy(resultBitmapUri = imageGenerationRepository.saveImage(bitmap), screenState = ScreenState.RESULT) } } catch (e: Exception) { handleImageGenerationError(e) @@ -291,25 +286,4 @@ private fun getBotColors(): List { enum class PromptType(val displayName: String) { PHOTO("Photo"), TEXT("Prompt"), -} - -suspend fun saveBitmapToCache( - context: Context, - bitmap: Bitmap, - compressionFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, - quality: Int = 100 -): Uri? = withContext(Dispatchers.IO) { - - val cacheDir = context.cacheDir - val fileName = File(cacheDir, "temp_image_${System.currentTimeMillis()}.jpg") - try { - FileOutputStream(fileName).use { outputStream -> - bitmap.compress(compressionFormat, quality, outputStream) - } - Uri.fromFile(fileName) - } catch (e: Exception) { - e.printStackTrace() - null - } - } \ No newline at end of file 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 cd9441ce..a64fee2a 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 @@ -17,12 +17,14 @@ package com.android.developers.androidify.customize import android.app.Application import android.graphics.Bitmap +import android.graphics.BitmapFactory 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.application import androidx.lifecycle.viewModelScope import com.android.developers.androidify.RemoteConfigDataSource import com.android.developers.androidify.data.ImageGenerationRepository @@ -43,6 +45,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel @@ -264,7 +267,7 @@ class CustomizeExportViewModel @Inject constructor( return@launch } - val image = state.value.exportImageCanvas.imageBitmap + val image = state.value.exportImageCanvas.imageUri?.let { uri -> convertUriToBitmap(uri) } if (image == null) { return@launch } @@ -394,4 +397,22 @@ class CustomizeExportViewModel @Inject constructor( watchfaceInstallationRepository.resetInstallationStatus() } } + + suspend fun convertUriToBitmap(uri: Uri): Bitmap? { + return withContext(ioDispatcher()) { + try { + val inputStream = application.contentResolver.openInputStream(uri) + if (inputStream != null) { + val bitmap = BitmapFactory.decodeStream(inputStream) + inputStream.close() + bitmap + } else { + null + } + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } } From 887aac4d9ed3d29e9410aa3f890775e90dcbfb4a Mon Sep 17 00:00:00 2001 From: srikrishnasakunia Date: Sun, 17 Aug 2025 23:57:43 +0530 Subject: [PATCH 04/17] Fixed the PR Comments --- .../androidify/navigation/MainNavigation.kt | 67 +++++++++++++------ .../androidify/navigation/NavigationRoutes.kt | 30 ++++++++- .../androidify/navigation/UriSerializer.kt | 21 ++++++ .../androidify/util/LocalFileProvider.kt | 18 +++++ .../androidify/creation/CreationScreen.kt | 44 ++++++------ .../androidify/creation/CreationViewModel.kt | 44 +++++++----- .../androidify/results/ResultsScreenTest.kt | 21 +++--- .../customize/CustomizeExportScreen.kt | 12 ++-- .../customize/CustomizeExportViewModel.kt | 40 +++++------ .../androidify/customize/ImageRenderer.kt | 14 ++-- .../androidify/results/BotResultCard.kt | 22 ++++-- .../androidify/results/ResultsScreen.kt | 33 +++++---- .../androidify/results/ResultsViewModel.kt | 25 +++++-- .../results/ResultsScreenScreenshotTest.kt | 7 +- 14 files changed, 256 insertions(+), 142 deletions(-) create mode 100644 app/src/main/java/com/android/developers/androidify/navigation/UriSerializer.kt diff --git a/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt b/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt index 2d302a21..bc076d5c 100644 --- a/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt +++ b/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt @@ -32,7 +32,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.IntOffset -import androidx.core.net.toUri +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.entry import androidx.navigation3.runtime.entryProvider @@ -40,10 +40,13 @@ 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 @@ -95,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 { createKey -> + val creationViewModel = hiltViewModel( + creationCallback = { factory -> + factory.create( + originalImageUrl = createKey.fileName + ) + } + ) CreationScreen( - createKey.fileName, onCameraPressed = { backStack.removeAll { it is Camera } backStack.add(Camera) @@ -114,50 +123,64 @@ fun MainNavigation() { backStack.add(About) }, onImageCreated = { resultImageUri, prompt, originalImageUri -> - backStack.removeAll{ it is ImageResult} + backStack.removeAll{ it is Result} backStack.add( - ImageResult( - result = resultImageUri.toString(), + Result( + resultImageUri = resultImageUri, prompt = prompt, - originalImageUri = originalImageUri?.toString() + originalImageUri = originalImageUri ) ) - } + }, + creationViewModel = creationViewModel ) } - entry { resultKey -> + entry { resultKey -> + val resultsViewModel = hiltViewModel( + creationCallback = { factory -> + factory.create( + resultImageUrl = resultKey.resultImageUri, + originalImageUrl = resultKey.originalImageUri, + promptText = resultKey.prompt + ) + } + ) ResultsScreen( - resultImageUri = resultKey.result.toUri(), - originalImageUri = resultKey.originalImageUri?.toUri(), onNextPress = { resultImageUri, originalImageUri -> - backStack.removeAll{ it is ImageResult} + backStack.removeAll{ it is Result} backStack.add( - ShareResult( - resultUri = resultImageUri.toString(), - originalImageUri = originalImageUri?.toString() + CustomizeExport( + resultImageUri = resultImageUri, + originalImageUri = originalImageUri ) ) }, - promptText = resultKey.prompt, onAboutPress = { backStack.add(About) }, onBackPress = { backStack.removeLastOrNull() - backStack.add(Create(fileName = resultKey.originalImageUri, prompt = resultKey.prompt)) - } + }, + viewModel = resultsViewModel ) } - entry { shareKey -> + entry { shareKey -> + val customizeExportViewModel = hiltViewModel( + creationCallback = { factory -> + factory.create( + resultImageUrl = shareKey.resultImageUri, + originalImageUrl = shareKey.originalImageUri + ) + } + ) CustomizeAndExportScreen( - resultImageUri = shareKey.resultUri.toUri(), - originalImageUri = shareKey.originalImageUri?.toUri(), onBackPress = { backStack.removeLastOrNull() }, onInfoPress = { backStack.add(About) - } + }, + viewModel = customizeExportViewModel ) } entry { diff --git a/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt b/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt index 46f5f943..50930b0c 100644 --- a/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt +++ b/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt @@ -17,6 +17,7 @@ package com.android.developers.androidify.navigation +import android.net.Uri import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable @@ -26,7 +27,10 @@ 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 @@ -34,8 +38,28 @@ 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 ImageResult(val originalImageUri: String? = null, val prompt: String? = null, val result: String) : NavigationRoute +data class Result( + @Serializable(with = UriSerializer::class) val resultImageUri: Uri, + @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 ShareResult(val resultUri: String, val originalImageUri: String?) : NavigationRoute \ No newline at end of file +data class CustomizeExport( + @Serializable(with = UriSerializer::class) val resultImageUri: Uri, + @Serializable(with = UriSerializer::class) val originalImageUri: Uri? +) : NavigationRoute \ No newline at end of file diff --git a/app/src/main/java/com/android/developers/androidify/navigation/UriSerializer.kt b/app/src/main/java/com/android/developers/androidify/navigation/UriSerializer.kt new file mode 100644 index 00000000..02733c09 --- /dev/null +++ b/app/src/main/java/com/android/developers/androidify/navigation/UriSerializer.kt @@ -0,0 +1,21 @@ +package com.android.developers.androidify.navigation + +import android.net.Uri +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 +import androidx.core.net.toUri + +object UriSerializer: KSerializer { + 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() +} \ No newline at end of file diff --git a/core/util/src/main/java/com/android/developers/androidify/util/LocalFileProvider.kt b/core/util/src/main/java/com/android/developers/androidify/util/LocalFileProvider.kt index 150fe769..4c2812d7 100644 --- a/core/util/src/main/java/com/android/developers/androidify/util/LocalFileProvider.kt +++ b/core/util/src/main/java/com/android/developers/androidify/util/LocalFileProvider.kt @@ -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 @@ -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 @@ -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 + } + } + } + @Throws(IOException::class) @WorkerThread private fun saveFileToUri(file: File, uri: Uri) { diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt index b27f371d..059cb578 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt @@ -92,6 +92,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.ripple import androidx.compose.material3.toShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -124,6 +125,11 @@ import androidx.core.net.toUri import androidx.graphics.shapes.RoundedPolygon import androidx.graphics.shapes.rectangle import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import coil3.request.ImageRequest @@ -159,8 +165,7 @@ import com.android.developers.androidify.creation.R as CreationR @Composable fun CreationScreen( - fileName: String? = null, - creationViewModel: CreationViewModel = hiltViewModel(), + creationViewModel: CreationViewModel, isMedium: Boolean = isAtLeastMedium(), onCameraPressed: () -> Unit = {}, onBackPressed: () -> Unit, @@ -173,19 +178,28 @@ fun CreationScreen( ) { creationViewModel.onBackPress() } - LaunchedEffect(Unit) { - if (fileName != null) { - creationViewModel.onImageSelected(fileName.toUri()) - } else { - creationViewModel.onImageSelected(null) - } - } val pickMedia = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> if (uri != null) { creationViewModel.onImageSelected(uri) } } val snackbarHostState by creationViewModel.snackbarHostState.collectAsStateWithLifecycle() + + LaunchedEffect(uiState.resultBitmapUri) { + uiState.resultBitmapUri?.let { resultBitmapUri -> + onImageCreated( + resultBitmapUri, + uiState.descriptionText.text.toString(), + if (uiState.selectedPromptOption == PromptType.PHOTO) { + uiState.imageUri + } else { + null + } + ) + creationViewModel.onResultDisplayed() + } + } + when (uiState.screenState) { ScreenState.EDIT -> { EditScreen( @@ -213,18 +227,6 @@ fun CreationScreen( }, ) } - - ScreenState.RESULT -> { - onImageCreated( - uiState.resultBitmapUri!!, - uiState.descriptionText.text.toString(), - if (uiState.selectedPromptOption == PromptType.PHOTO) { - uiState.imageUri - } else { - null - } - ) - } } } diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt index 499881cb..603e0ba9 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt @@ -33,6 +33,9 @@ import com.android.developers.androidify.data.InternetConnectivityManager import com.android.developers.androidify.data.NoInternetException import com.android.developers.androidify.data.TextGenerationRepository import com.android.developers.androidify.util.LocalFileProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Job @@ -42,8 +45,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject -@HiltViewModel -class CreationViewModel @Inject constructor( +@HiltViewModel(assistedFactory = CreationViewModel.Factory::class) +class CreationViewModel @AssistedInject constructor( + @Assisted("originalImageUrl") originalImageUrl: Uri?, val internetConnectivityManager: InternetConnectivityManager, val imageGenerationRepository: ImageGenerationRepository, val textGenerationRepository: TextGenerationRepository, @@ -53,11 +57,9 @@ class CreationViewModel @Inject constructor( val context: Context, ) : ViewModel() { - init { - viewModelScope.launch { - imageGenerationRepository.initialize() - textGenerationRepository.initialize() - } + @AssistedFactory + interface Factory { + fun create(@Assisted("originalImageUrl") originalImageUrl: Uri?): CreationViewModel } private var _uiState = MutableStateFlow(CreationState()) @@ -73,6 +75,14 @@ class CreationViewModel @Inject constructor( private var promptGenerationJob: Job? = null private var imageGenerationJob: Job? = null + init { + onImageSelected(originalImageUrl) + viewModelScope.launch { + imageGenerationRepository.initialize() + textGenerationRepository.initialize() + } + } + fun onImageSelected(uri: Uri?) { _uiState.update { it.copy( @@ -152,7 +162,10 @@ class CreationViewModel @Inject constructor( ) } _uiState.update { - it.copy(resultBitmapUri = imageGenerationRepository.saveImage(bitmap), screenState = ScreenState.RESULT) + it.copy( + resultBitmapUri = imageGenerationRepository.saveImage(bitmap), + screenState = ScreenState.EDIT + ) } } catch (e: Exception) { handleImageGenerationError(e) @@ -217,17 +230,17 @@ class CreationViewModel @Inject constructor( cancelInProgressTask() } - ScreenState.RESULT -> { - _uiState.update { - it.copy(screenState = ScreenState.EDIT, resultBitmapUri = null) - } - } - ScreenState.EDIT -> { // do nothing, back press handled outside } } } + + fun onResultDisplayed() { + _uiState.update { + it.copy(resultBitmapUri = null) + } + } } data class CreationState( @@ -245,7 +258,6 @@ data class CreationState( enum class ScreenState { EDIT, LOADING, - RESULT, } data class BotColor( @@ -286,4 +298,4 @@ private fun getBotColors(): List { enum class PromptType(val displayName: String) { PHOTO("Photo"), TEXT("Prompt"), -} \ No newline at end of file +} diff --git a/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt b/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt index 5dba07c0..9f1f312a 100644 --- a/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt +++ b/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt @@ -41,15 +41,15 @@ class ResultsScreenTest { @get:Rule val composeTestRule = createAndroidComposeRule() - // Create a dummy bitmap for testing - val dummyUri = android.net.Uri.parse("dummy://image") + // Create a test bitmap for testing + val testUri = android.net.Uri.parse("placeholder://image") @Test fun resultsScreenContents_displaysActionButtons() { val shareButtonText = composeTestRule.activity.getString(R.string.customize_and_share) // Note: Download button is identified by icon, harder to test reliably without tags/desc - val initialState = ResultState(resultImageUri = dummyUri, promptText = "test") + val initialState = ResultState(resultImageUri = testUri, promptText = "test") val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -78,7 +78,7 @@ class ResultsScreenTest { val frontCardDesc = composeTestRule.activity.getString(R.string.resultant_android_bot) // Ensure promptText is non-null when bitmap is present - val initialState = ResultState(resultImageUri = dummyUri, promptText = "test") + val initialState = ResultState(resultImageUri = testUri, promptText = "test") val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -106,9 +106,9 @@ class ResultsScreenTest { val photoOptionText = composeTestRule.activity.getString(R.string.photo) val frontCardDesc = composeTestRule.activity.getString(R.string.resultant_android_bot) val backCardDesc = composeTestRule.activity.getString(R.string.original_image) - val dummyUri = android.net.Uri.parse("dummy://image") + val testUri = android.net.Uri.parse("placeholder://image") - val initialState = ResultState(resultImageUri = dummyUri, originalImageUrl = dummyUri) + val initialState = ResultState(resultImageUri = testUri, originalImageUrl = testUri) val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -143,7 +143,7 @@ class ResultsScreenTest { val promptText = "test prompt" val promptPrefix = composeTestRule.activity.getString(R.string.my_bot_is_wearing) - val initialState = ResultState(resultImageUri = dummyUri, promptText = promptText) // No original image URI + val initialState = ResultState(resultImageUri = testUri, promptText = promptText) // No original image URI val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -173,9 +173,10 @@ class ResultsScreenTest { val botOptionText = composeTestRule.activity.getString(R.string.bot) val photoOptionText = composeTestRule.activity.getString(R.string.photo) val frontCardDesc = composeTestRule.activity.getString(R.string.resultant_android_bot) - val dummyUri = android.net.Uri.parse("dummy://image") + val testUri = android.net.Uri.parse("placeholder://image") - val initialState = ResultState(resultImageUri = dummyUri, originalImageUrl = dummyUri) + + val initialState = ResultState(resultImageUri = testUri, originalImageUrl = testUri) val state = mutableStateOf(initialState) composeTestRule.setContent { @@ -210,7 +211,7 @@ class ResultsScreenTest { var shareClicked = false // Ensure promptText is non-null when bitmap is present - val initialState = ResultState(resultImageUri = dummyUri, promptText = "test") + val initialState = ResultState(resultImageUri = testUri, promptText = "test") val state = mutableStateOf(initialState) composeTestRule.setContent { 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 06b8ca4e..3be29477 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 @@ -96,20 +96,16 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.shouldShowRationale import com.android.developers.androidify.theme.R as ThemeR +import android.content.ContentResolver @OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomizeAndExportScreen( - resultImageUri: Uri, - originalImageUri: Uri?, onBackPress: () -> Unit, onInfoPress: () -> Unit, isMediumWindowSize: Boolean = isAtLeastMedium(), - viewModel: CustomizeExportViewModel = hiltViewModel(), + viewModel: CustomizeExportViewModel, ) { - LaunchedEffect(resultImageUri, originalImageUri) { - viewModel.setArguments(resultImageUri, originalImageUri) - } val state = viewModel.state.collectAsStateWithLifecycle() val context = LocalContext.current @@ -494,7 +490,7 @@ fun CustomizeExportPreview() { displayName = "Pixel Watch 3", hasAndroidify = true, ) - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() val state = CustomizeExportState( exportImageCanvas = ExportImageCanvas(imageUri = imageUri), connectedWatch = connectedWatch, @@ -526,7 +522,7 @@ fun CustomizeExportPreviewLarge() { AnimatedContent(true) { targetState -> targetState CompositionLocalProvider(LocalNavAnimatedContentScope provides this@AnimatedContent) { - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() val connectedWatch = ConnectedWatch( nodeId = "1234", displayName = "Pixel Watch 3", 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 a64fee2a..0e55f453 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 @@ -16,15 +16,12 @@ package com.android.developers.androidify.customize import android.app.Application -import android.graphics.Bitmap -import android.graphics.BitmapFactory 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.application import androidx.lifecycle.viewModelScope import com.android.developers.androidify.RemoteConfigDataSource import com.android.developers.androidify.data.ImageGenerationRepository @@ -45,11 +42,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject -@HiltViewModel -class CustomizeExportViewModel @Inject constructor( +@HiltViewModel(assistedFactory = CustomizeExportViewModel.Factory::class) +class CustomizeExportViewModel @AssistedInject constructor( + @Assisted("resultImageUrl") val resultImageUrl: Uri, + @Assisted("originalImageUrl") val originalImageUrl: Uri?, val imageGenerationRepository: ImageGenerationRepository, val composableBitmapRenderer: ComposableBitmapRenderer, val watchfaceInstallationRepository: WatchFaceInstallationRepository, @@ -58,6 +56,14 @@ class CustomizeExportViewModel @Inject constructor( application: Application, ) : AndroidViewModel(application) { + @AssistedFactory + interface Factory{ + fun create( + @Assisted("resultImageUrl") resultImageUrl: Uri, + @Assisted("originalImageUrl")originalImageUrl: Uri? + ): CustomizeExportViewModel + } + private val _state = MutableStateFlow(CustomizeExportState()) val state: StateFlow = combine( _state, @@ -108,6 +114,8 @@ class CustomizeExportViewModel @Inject constructor( _state.update { CustomizeExportState( + originalImageUrl = originalImageUrl, + exportImageCanvas = it.exportImageCanvas.copy(imageUri = resultImageUrl), toolState = mapOf( CustomizeTool.Size to AspectRatioToolState(), CustomizeTool.Background to BackgroundToolState( @@ -267,7 +275,7 @@ class CustomizeExportViewModel @Inject constructor( return@launch } - val image = state.value.exportImageCanvas.imageUri?.let { uri -> convertUriToBitmap(uri) } + val image = state.value.exportImageCanvas.imageUri?.let { uri -> localFileProvider.loadBitmapFromUri(uri) } if (image == null) { return@launch } @@ -397,22 +405,4 @@ class CustomizeExportViewModel @Inject constructor( watchfaceInstallationRepository.resetInstallationStatus() } } - - suspend fun convertUriToBitmap(uri: Uri): Bitmap? { - return withContext(ioDispatcher()) { - try { - val inputStream = application.contentResolver.openInputStream(uri) - if (inputStream != null) { - val bitmap = BitmapFactory.decodeStream(inputStream) - inputStream.close() - bitmap - } else { - null - } - } catch (e: Exception) { - e.printStackTrace() - null - } - } - } } 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 2a43798d..f837ae54 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 @@ -17,6 +17,7 @@ package com.android.developers.androidify.customize +import android.content.ContentResolver import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.animateBounds import androidx.compose.animation.core.animateFloatAsState @@ -41,6 +42,7 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.imageResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastRoundToInt @@ -210,7 +212,7 @@ private fun Modifier.safeAnimateBounds(): Modifier { @Preview @Composable private fun ImageRendererPreviewSquare() { - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() AndroidifyTheme { ImageResult( @@ -233,7 +235,7 @@ private fun ImageRendererPreviewSquare() { @Preview @Composable private fun ImageRendererPreviewBanner() { - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( @@ -255,7 +257,7 @@ private fun ImageRendererPreviewBanner() { @Preview @Composable private fun ImageRendererPreviewWallpaper() { - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( @@ -277,7 +279,7 @@ private fun ImageRendererPreviewWallpaper() { @Preview(widthDp = 1280, heightDp = 800) @Composable private fun ImageRendererPreviewWallpaperTablet() { - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( @@ -299,7 +301,7 @@ private fun ImageRendererPreviewWallpaperTablet() { @Preview @Composable private fun ImageRendererPreviewWallpaperSocial() { - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( @@ -321,7 +323,7 @@ private fun ImageRendererPreviewWallpaperSocial() { @Preview @Composable fun ImageRendererPreviewWallpaperIO() { - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() AndroidifyTheme { ImageResult( ExportImageCanvas( diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt b/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt index 4f78fd3d..fd772d8d 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt @@ -66,11 +66,11 @@ fun BotResultCard( flippableState = flippableState, onFlipStateChanged = onFlipStateChanged, front = { - ImageCard(resultImageUri, isBack = false) + FrontCard(resultImageUri) }, back = { if (originalImageUrl != null) { - ImageCard(originalImageUrl, isBack = true) + BackCard(originalImageUrl) } else { BackCardPrompt(promptText!!) } @@ -79,10 +79,24 @@ fun BotResultCard( } @Composable -private fun ImageCard(originalImageUrl: Uri, isBack: Boolean) { +private fun FrontCard(resultImageUri: Uri) { + AsyncImage( + model = resultImageUri, + contentDescription = stringResource(R.string.resultant_android_bot), + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .aspectRatio(BOT_ASPECT_RATIO) + .shadow(8.dp, shape = MaterialTheme.shapes.large) + .clip(MaterialTheme.shapes.large), + ) +} + +@Composable +private fun BackCard(originalImageUrl: Uri) { AsyncImage( model = originalImageUrl, - contentDescription = if (isBack)stringResource(R.string.original_image) else stringResource(R.string.resultant_android_bot), + contentDescription = stringResource(R.string.original_image), contentScale = ContentScale.Crop, modifier = Modifier .fillMaxSize() diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt index fd9df9ca..c8e51dab 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt @@ -17,6 +17,7 @@ package com.android.developers.androidify.results +import android.content.ContentResolver import android.net.Uri import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.EaseOutBack @@ -43,7 +44,6 @@ import androidx.compose.material3.SnackbarDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -51,11 +51,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -63,7 +61,7 @@ import androidx.compose.ui.text.font.FontWeight.Companion.Bold import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.components.AndroidifyTopAppBar @@ -74,24 +72,23 @@ import com.android.developers.androidify.util.SmallPhonePreview import com.android.developers.androidify.util.allowsFullContent import com.android.developers.androidify.util.isAtLeastMedium import com.google.accompanist.permissions.ExperimentalPermissionsApi -import androidx.core.net.toUri @Composable fun ResultsScreen( - resultImageUri: Uri, + /*resultImageUri: Uri, originalImageUri: Uri?, - promptText: String?, + promptText: String?,*/ modifier: Modifier = Modifier, verboseLayout: Boolean = allowsFullContent(), onBackPress: () -> Unit, onAboutPress: () -> Unit, onNextPress: (resultImageUri:Uri, originalImageUri:Uri?) -> Unit, - viewModel: ResultsViewModel = hiltViewModel(), + viewModel: ResultsViewModel, ) { val state = viewModel.state.collectAsStateWithLifecycle() - LaunchedEffect(resultImageUri, originalImageUri, promptText) { + /*LaunchedEffect(resultImageUri, originalImageUri, promptText) { viewModel.setArguments(resultImageUri, originalImageUri, promptText) - } + }*/ val snackbarHostState by viewModel.snackbarHostState.collectAsStateWithLifecycle() Scaffold( snackbarHost = { @@ -122,10 +119,12 @@ fun ResultsScreen( state, verboseLayout = verboseLayout, onCustomizeShareClicked = { - onNextPress( - resultImageUri, - originalImageUri, - ) + viewModel.state.value.resultImageUri?.let { + onNextPress( + it, + viewModel.state.value.originalImageUrl, + ) + } }, ) } @@ -137,7 +136,7 @@ fun ResultsScreen( @Composable private fun ResultsScreenPreview() { AndroidifyTheme { - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() val state = remember { mutableStateOf( ResultState( @@ -159,7 +158,7 @@ private fun ResultsScreenPreview() { @Composable private fun ResultsScreenPreviewSmall() { AndroidifyTheme { - val imageUri = ("android.resource://com.android.developers.androidify.results/" + R.drawable.placeholderbot).toUri() + val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() val state = remember { mutableStateOf( ResultState( diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt index 70c6ca5b..2b226d61 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt @@ -19,6 +19,9 @@ import android.graphics.Bitmap import android.net.Uri import androidx.compose.material3.SnackbarHostState import androidx.lifecycle.ViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,9 +29,21 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import javax.inject.Inject -@HiltViewModel -class ResultsViewModel @Inject constructor() : ViewModel() { +@HiltViewModel(assistedFactory = ResultsViewModel.Factory::class) +class ResultsViewModel @AssistedInject constructor( + @Assisted("resultImageUrl") val resultImageUrl: Uri?, + @Assisted("originalImageUrl") val originalImageUrl: Uri?, + @Assisted("promptText") val promptText: String? +) : ViewModel() { + @AssistedFactory + interface Factory { + fun create( + @Assisted("resultImageUrl") resultImageUrl: Uri?, + @Assisted("originalImageUrl") originalImageUrl: Uri?, + @Assisted("promptText") promptText: String?, + ): ResultsViewModel + } private val _state = MutableStateFlow(ResultState()) val state = _state.asStateFlow() @@ -37,11 +52,7 @@ class ResultsViewModel @Inject constructor() : ViewModel() { val snackbarHostState: StateFlow get() = _snackbarHostState - fun setArguments( - resultImageUrl: Uri?, - originalImageUrl: Uri?, - promptText: String?, - ) { + init{ _state.update { ResultState(resultImageUrl, originalImageUrl, promptText = promptText) } diff --git a/feature/results/src/screenshotTest/java/com/android/developers/androidify/results/ResultsScreenScreenshotTest.kt b/feature/results/src/screenshotTest/java/com/android/developers/androidify/results/ResultsScreenScreenshotTest.kt index f03cfbd3..ff33b851 100644 --- a/feature/results/src/screenshotTest/java/com/android/developers/androidify/results/ResultsScreenScreenshotTest.kt +++ b/feature/results/src/screenshotTest/java/com/android/developers/androidify/results/ResultsScreenScreenshotTest.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.util.AdaptivePreview import com.android.developers.androidify.util.SmallPhonePreview +import androidx.core.net.toUri class ResultsScreenScreenshotTest { @@ -43,7 +44,7 @@ class ResultsScreenScreenshotTest { val state = remember { mutableStateOf( ResultState( - resultImageBitmap = mockBitmap, + resultImageUri = "test://mockbitmap/${mockBitmap.hashCode()}".toUri(), promptText = "wearing a hat with straw hair", ), ) @@ -68,7 +69,7 @@ class ResultsScreenScreenshotTest { val state = remember { mutableStateOf( ResultState( - resultImageBitmap = mockBitmap, + resultImageUri = "test://mockbitmap/${mockBitmap.hashCode()}".toUri(), promptText = "wearing a hat with straw hair", ), ) @@ -92,7 +93,7 @@ class ResultsScreenScreenshotTest { val state = remember { mutableStateOf( ResultState( - resultImageBitmap = mockBitmap, + resultImageUri = "test://mockbitmap/${mockBitmap.hashCode()}".toUri(), promptText = "wearing a hat with straw hair", ), ) From 43760ace11a6945b143ee7861a948de3649e3c08 Mon Sep 17 00:00:00 2001 From: srikrishnasakunia Date: Mon, 18 Aug 2025 12:24:08 +0530 Subject: [PATCH 05/17] Handling the process death scenario for Bitmap --- .../customize/CustomizeExportViewModel.kt | 23 +++++++++++++++---- .../androidify/customize/CustomizeState.kt | 1 + 2 files changed, 20 insertions(+), 4 deletions(-) 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 0e55f453..3d978a43 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 @@ -16,6 +16,7 @@ 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 @@ -42,7 +43,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel(assistedFactory = CustomizeExportViewModel.Factory::class) class CustomizeExportViewModel @AssistedInject constructor( @@ -113,7 +113,7 @@ class CustomizeExportViewModel @AssistedInject constructor( } _state.update { - CustomizeExportState( + it.copy( originalImageUrl = originalImageUrl, exportImageCanvas = it.exportImageCanvas.copy(imageUri = resultImageUrl), toolState = mapOf( @@ -124,6 +124,7 @@ class CustomizeExportViewModel @AssistedInject constructor( ), ) } + loadInitialBitmap(resultImageUrl) } override fun onCleared() { @@ -274,8 +275,7 @@ class CustomizeExportViewModel @AssistedInject constructor( } return@launch } - - val image = state.value.exportImageCanvas.imageUri?.let { uri -> localFileProvider.loadBitmapFromUri(uri) } + val image = state.value.exportImageCanvas.imageBitmap if (image == null) { return@launch } @@ -405,4 +405,19 @@ class CustomizeExportViewModel @AssistedInject constructor( watchfaceInstallationRepository.resetInstallationStatus() } } + + private fun loadInitialBitmap(uri: Uri) { + viewModelScope.launch { + try { + val bitmap = localFileProvider.loadBitmapFromUri(uri) + _state.update { + it.copy( + exportImageCanvas = it.exportImageCanvas.copy(imageBitmap = bitmap) + ) + } + } catch (e: Exception) { + _snackbarHostState.value.showSnackbar("Could not load image.") + } + } + } } 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 61dfc918..f2866c6c 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 @@ -71,6 +71,7 @@ data class BackgroundToolState( data class ExportImageCanvas( val imageUri: Uri? = null, + val imageBitmap: Bitmap? = null, val imageBitmapRemovedBackground: Bitmap? = null, val aspectRatioOption: SizeOption = SizeOption.Square, val canvasSize: Size = Size(1000f, 1000f), From f4f7c4d38b8248091bf3223a75dc992eddae24e5 Mon Sep 17 00:00:00 2001 From: srikrishnasakunia Date: Mon, 18 Aug 2025 14:39:04 +0530 Subject: [PATCH 06/17] Test Cases handling --- .../testing/data/TestFileProvider.kt | 4 ++++ .../creation/CreationViewModelTest.kt | 6 +++-- .../customize/CustomizeViewModelTest.kt | 24 +++++++++++++------ .../results/ResultsViewModelTest.kt | 22 +++++++---------- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt b/core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt index bc10d9e7..5a9ca1a7 100644 --- a/core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt +++ b/core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt @@ -64,4 +64,8 @@ class TestFileProvider : LocalFileProvider { ): Uri { TODO("Not yet implemented") } + + override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? { + TODO("Not yet implemented") + } } diff --git a/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt index 1bc07544..dba889b4 100644 --- a/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt +++ b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt @@ -54,7 +54,9 @@ class CreationViewModelTest { private val fakeUri = Uri.parse("test.jpeg") @Before fun setup() { + val fakeUri = Uri.parse("content://test/image.jpg") viewModel = CreationViewModel( + originalImageUrl = fakeUri, internetConnectivityManager, imageGenerationRepository, TestTextGenerationRepository(), @@ -136,7 +138,7 @@ class CreationViewModelTest { viewModel.onImageSelected(Uri.parse("content://test/image.jpg")) viewModel.onSelectedPromptOptionChanged(PromptType.PHOTO) viewModel.startClicked() - assertEquals(ScreenState.RESULT, viewModel.uiState.value.screenState) + assertEquals(ScreenState.EDIT, viewModel.uiState.value.screenState) assertNotNull(viewModel.uiState.value.resultBitmapUri) } @@ -197,7 +199,7 @@ class CreationViewModelTest { "testing input description" } viewModel.startClicked() - assertEquals(ScreenState.RESULT, viewModel.uiState.value.screenState) + assertEquals(ScreenState.EDIT, viewModel.uiState.value.screenState) assertNotNull(viewModel.uiState.value.resultBitmapUri) } 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 f15b6af0..a00e7f29 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 @@ -53,11 +53,15 @@ class CustomizeViewModelTest { private val fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) private val originalFakeUri = Uri.parse("content://com.example.app/images/original.jpg") + val fakeUri = Uri.parse("content://test/image.jpg") + @Before fun setup() { val remoteConfigDataSource = TestRemoteConfigDataSource(true) remoteConfigDataSource.backgroundVibeEnabled = false viewModel = CustomizeExportViewModel( + fakeUri, + originalFakeUri, FakeImageGenerationRepository(), composableBitmapRenderer = FakeComposableBitmapRenderer(), watchfaceInstallationRepository = FakeWatchFaceInstallationRepository(), @@ -80,7 +84,7 @@ class CustomizeViewModelTest { val initialState = viewModel.state.value viewModel.setArguments( - fakeBitmap, + fakeUri, originalFakeUri, ) @@ -103,6 +107,8 @@ class CustomizeViewModelTest { val initialState = viewModel.state.value val viewModel = CustomizeExportViewModel( + fakeUri, + null, FakeImageGenerationRepository(), composableBitmapRenderer = FakeComposableBitmapRenderer(), application = ApplicationProvider.getApplicationContext(), @@ -112,7 +118,7 @@ class CustomizeViewModelTest { ) viewModel.setArguments( - fakeBitmap, + fakeUri, null, ) @@ -139,7 +145,7 @@ class CustomizeViewModelTest { } viewModel.setArguments( - fakeBitmap, + fakeUri, originalFakeUri, ) @@ -161,7 +167,7 @@ class CustomizeViewModelTest { } } viewModel.setArguments( - fakeBitmap, + fakeUri, originalFakeUri, ) advanceUntilIdle() @@ -174,6 +180,8 @@ class CustomizeViewModelTest { @Test fun changeBackground_NotNull() = runTest { val viewModel = CustomizeExportViewModel( + fakeUri, + originalFakeUri, FakeImageGenerationRepository(), composableBitmapRenderer = FakeComposableBitmapRenderer(), watchfaceInstallationRepository = FakeWatchFaceInstallationRepository(), @@ -189,7 +197,7 @@ class CustomizeViewModelTest { } } viewModel.setArguments( - fakeBitmap, + fakeUri, originalFakeUri, ) advanceUntilIdle() @@ -219,7 +227,7 @@ class CustomizeViewModelTest { } } viewModel.setArguments( - fakeBitmap, + fakeUri, originalFakeUri, ) advanceUntilIdle() @@ -243,6 +251,8 @@ class CustomizeViewModelTest { val remoteConfigDataSource = TestRemoteConfigDataSource(true) remoteConfigDataSource.backgroundVibeEnabled = true val viewModel = CustomizeExportViewModel( + fakeUri, + originalFakeUri, FakeImageGenerationRepository(), composableBitmapRenderer = FakeComposableBitmapRenderer(), application = ApplicationProvider.getApplicationContext(), @@ -254,7 +264,7 @@ class CustomizeViewModelTest { val initialState = viewModel.state.value viewModel.setArguments( - fakeBitmap, + fakeUri, null, ) diff --git a/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt b/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt index 5f425658..e8828609 100644 --- a/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt +++ b/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt @@ -40,9 +40,15 @@ class ResultsViewModelTest { private val fakePromptText = "Pink Hair, plaid shirt, jeans" private val originalFakeUri = Uri.parse("content://com.example.app/images/original.jpg") + private val fakeUri = Uri.parse("content://test/image.jpg") + @Before fun setup() { - viewModel = ResultsViewModel() + viewModel = ResultsViewModel( + fakeUri, + originalFakeUri, + fakePromptText + ) } @Test @@ -55,14 +61,9 @@ class ResultsViewModelTest { @Test fun setArgumentsWithOriginalImage() = runTest { - viewModel.setArguments( - fakeBitmap, - originalFakeUri, - promptText = null, - ) assertEquals( ResultState( - resultImageBitmap = fakeBitmap, + resultImageUri = fakeUri, originalImageUrl = originalFakeUri, ), viewModel.state.value, @@ -71,14 +72,9 @@ class ResultsViewModelTest { @Test fun setArgumentsWithPrompt() = runTest { - viewModel.setArguments( - fakeBitmap, - null, - promptText = fakePromptText, - ) assertEquals( ResultState( - resultImageBitmap = fakeBitmap, + resultImageUri = fakeUri, originalImageUrl = null, promptText = fakePromptText, ), From b01f805eb2a962f3143576cb8d22e8660f5885f8 Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Thu, 4 Sep 2025 15:29:06 +0530 Subject: [PATCH 07/17] Test Cases handling and comments resolved. --- .../androidify/navigation/MainNavigation.kt | 31 ++++++----- .../androidify/navigation/UriSerializer.kt | 21 ++++++-- .../developers/testing/data/BitmapSample.kt | 20 +++++++ .../testing/data/TestFileProvider.kt | 2 +- .../customize/CustomizeExportScreen.kt | 4 +- .../customize/CustomizeExportViewModel.kt | 12 ----- .../androidify/customize/ImageRenderer.kt | 16 +++--- .../developers/androidify/customize/Utils.kt | 27 ++++++++++ .../androidify/results/ResultsScreen.kt | 16 ++---- .../results/ResultsScreenScreenshotTest.kt | 2 +- .../customize/CustomizeStateTest.kt | 2 +- .../customize/CustomizeViewModelTest.kt | 52 +++---------------- .../results/ResultsViewModelTest.kt | 19 ++----- 13 files changed, 108 insertions(+), 116 deletions(-) create mode 100644 core/testing/src/main/java/com/android/developers/testing/data/BitmapSample.kt create mode 100644 feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt diff --git a/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt b/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt index bc076d5c..2a8fbbc5 100644 --- a/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt +++ b/app/src/main/java/com/android/developers/androidify/navigation/MainNavigation.kt @@ -105,11 +105,11 @@ fun MainNavigation() { } entry { createKey -> val creationViewModel = hiltViewModel( - creationCallback = { factory -> + creationCallback = { factory -> factory.create( - originalImageUrl = createKey.fileName + originalImageUrl = createKey.fileName, ) - } + }, ) CreationScreen( onCameraPressed = { @@ -123,16 +123,16 @@ fun MainNavigation() { backStack.add(About) }, onImageCreated = { resultImageUri, prompt, originalImageUri -> - backStack.removeAll{ it is Result} + backStack.removeAll { it is Result } backStack.add( Result( resultImageUri = resultImageUri, prompt = prompt, - originalImageUri = originalImageUri - ) + originalImageUri = originalImageUri, + ), ) }, - creationViewModel = creationViewModel + creationViewModel = creationViewModel, ) } entry { resultKey -> @@ -141,18 +141,17 @@ fun MainNavigation() { factory.create( resultImageUrl = resultKey.resultImageUri, originalImageUrl = resultKey.originalImageUri, - promptText = resultKey.prompt + promptText = resultKey.prompt, ) - } + }, ) ResultsScreen( onNextPress = { resultImageUri, originalImageUri -> - backStack.removeAll{ it is Result} backStack.add( CustomizeExport( resultImageUri = resultImageUri, - originalImageUri = originalImageUri - ) + originalImageUri = originalImageUri, + ), ) }, onAboutPress = { @@ -161,7 +160,7 @@ fun MainNavigation() { onBackPress = { backStack.removeLastOrNull() }, - viewModel = resultsViewModel + viewModel = resultsViewModel, ) } entry { shareKey -> @@ -169,9 +168,9 @@ fun MainNavigation() { creationCallback = { factory -> factory.create( resultImageUrl = shareKey.resultImageUri, - originalImageUrl = shareKey.originalImageUri + originalImageUrl = shareKey.originalImageUri, ) - } + }, ) CustomizeAndExportScreen( onBackPress = { @@ -180,7 +179,7 @@ fun MainNavigation() { onInfoPress = { backStack.add(About) }, - viewModel = customizeExportViewModel + viewModel = customizeExportViewModel, ) } entry { diff --git a/app/src/main/java/com/android/developers/androidify/navigation/UriSerializer.kt b/app/src/main/java/com/android/developers/androidify/navigation/UriSerializer.kt index 02733c09..69950d33 100644 --- a/app/src/main/java/com/android/developers/androidify/navigation/UriSerializer.kt +++ b/app/src/main/java/com/android/developers/androidify/navigation/UriSerializer.kt @@ -1,15 +1,30 @@ +/* + * 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 -import androidx.core.net.toUri -object UriSerializer: KSerializer { +object UriSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) @@ -18,4 +33,4 @@ object UriSerializer: KSerializer { } override fun deserialize(decoder: Decoder): Uri = decoder.decodeString().toUri() -} \ No newline at end of file +} diff --git a/core/testing/src/main/java/com/android/developers/testing/data/BitmapSample.kt b/core/testing/src/main/java/com/android/developers/testing/data/BitmapSample.kt new file mode 100644 index 00000000..ceec4d74 --- /dev/null +++ b/core/testing/src/main/java/com/android/developers/testing/data/BitmapSample.kt @@ -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) diff --git a/core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt b/core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt index 5a9ca1a7..75761a89 100644 --- a/core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt +++ b/core/testing/src/main/java/com/android/developers/testing/data/TestFileProvider.kt @@ -66,6 +66,6 @@ class TestFileProvider : LocalFileProvider { } override suspend fun loadBitmapFromUri(uri: Uri): Bitmap? { - TODO("Not yet implemented") + return bitmapSample } } 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 3be29477..dd2a972e 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 @@ -490,7 +490,7 @@ fun CustomizeExportPreview() { displayName = "Pixel Watch 3", hasAndroidify = true, ) - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() val state = CustomizeExportState( exportImageCanvas = ExportImageCanvas(imageUri = imageUri), connectedWatch = connectedWatch, @@ -522,7 +522,7 @@ fun CustomizeExportPreviewLarge() { AnimatedContent(true) { targetState -> targetState CompositionLocalProvider(LocalNavAnimatedContentScope provides this@AnimatedContent) { - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() val connectedWatch = ConnectedWatch( nodeId = "1234", displayName = "Pixel Watch 3", 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 3d978a43..f783c6b4 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 @@ -131,18 +131,6 @@ class CustomizeExportViewModel @AssistedInject constructor( super.onCleared() } - fun setArguments( - resultImageUrl: Uri, - originalImageUrl: Uri?, - ) { - _state.update { - _state.value.copy( - originalImageUrl, - exportImageCanvas = it.exportImageCanvas.copy(imageUri = resultImageUrl), - ) - } - } - fun shareClicked() { viewModelScope.launch { val exportImageCanvas = state.value.exportImageCanvas 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 f837ae54..dc26cac7 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 @@ -17,7 +17,6 @@ package com.android.developers.androidify.customize -import android.content.ContentResolver import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.animateBounds import androidx.compose.animation.core.animateFloatAsState @@ -42,13 +41,10 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layout -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.imageResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastRoundToInt -import androidx.core.net.toUri import coil3.compose.AsyncImage -import com.android.developers.androidify.results.R import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.LocalAnimateBoundsScope @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalSharedTransitionApi::class) @@ -212,7 +208,7 @@ private fun Modifier.safeAnimateBounds(): Modifier { @Preview @Composable private fun ImageRendererPreviewSquare() { - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() AndroidifyTheme { ImageResult( @@ -235,7 +231,7 @@ private fun ImageRendererPreviewSquare() { @Preview @Composable private fun ImageRendererPreviewBanner() { - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() AndroidifyTheme { ImageResult( ExportImageCanvas( @@ -257,7 +253,7 @@ private fun ImageRendererPreviewBanner() { @Preview @Composable private fun ImageRendererPreviewWallpaper() { - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() AndroidifyTheme { ImageResult( ExportImageCanvas( @@ -279,7 +275,7 @@ private fun ImageRendererPreviewWallpaper() { @Preview(widthDp = 1280, heightDp = 800) @Composable private fun ImageRendererPreviewWallpaperTablet() { - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() AndroidifyTheme { ImageResult( ExportImageCanvas( @@ -301,7 +297,7 @@ private fun ImageRendererPreviewWallpaperTablet() { @Preview @Composable private fun ImageRendererPreviewWallpaperSocial() { - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() AndroidifyTheme { ImageResult( ExportImageCanvas( @@ -323,7 +319,7 @@ private fun ImageRendererPreviewWallpaperSocial() { @Preview @Composable fun ImageRendererPreviewWallpaperIO() { - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() AndroidifyTheme { ImageResult( ExportImageCanvas( diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt new file mode 100644 index 00000000..40886c77 --- /dev/null +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt @@ -0,0 +1,27 @@ +/* + * 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.customize + +import android.content.ContentResolver +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.core.net.toUri +import com.android.developers.androidify.results.R + +@Composable +fun getPlaceholderBotUri(): Uri = + ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt index c8e51dab..589ce799 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt @@ -17,7 +17,6 @@ package com.android.developers.androidify.results -import android.content.ContentResolver import android.net.Uri import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.EaseOutBack @@ -52,7 +51,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource @@ -61,8 +59,8 @@ import androidx.compose.ui.text.font.FontWeight.Companion.Bold import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.developers.androidify.customize.getPlaceholderBotUri import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.theme.components.PrimaryButton @@ -75,20 +73,14 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi @Composable fun ResultsScreen( - /*resultImageUri: Uri, - originalImageUri: Uri?, - promptText: String?,*/ modifier: Modifier = Modifier, verboseLayout: Boolean = allowsFullContent(), onBackPress: () -> Unit, onAboutPress: () -> Unit, - onNextPress: (resultImageUri:Uri, originalImageUri:Uri?) -> Unit, + onNextPress: (resultImageUri: Uri, originalImageUri: Uri?) -> Unit, viewModel: ResultsViewModel, ) { val state = viewModel.state.collectAsStateWithLifecycle() - /*LaunchedEffect(resultImageUri, originalImageUri, promptText) { - viewModel.setArguments(resultImageUri, originalImageUri, promptText) - }*/ val snackbarHostState by viewModel.snackbarHostState.collectAsStateWithLifecycle() Scaffold( snackbarHost = { @@ -136,7 +128,7 @@ fun ResultsScreen( @Composable private fun ResultsScreenPreview() { AndroidifyTheme { - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() val state = remember { mutableStateOf( ResultState( @@ -158,7 +150,7 @@ private fun ResultsScreenPreview() { @Composable private fun ResultsScreenPreviewSmall() { AndroidifyTheme { - val imageUri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + val imageUri = getPlaceholderBotUri() val state = remember { mutableStateOf( ResultState( diff --git a/feature/results/src/screenshotTest/java/com/android/developers/androidify/results/ResultsScreenScreenshotTest.kt b/feature/results/src/screenshotTest/java/com/android/developers/androidify/results/ResultsScreenScreenshotTest.kt index ff33b851..56e6b586 100644 --- a/feature/results/src/screenshotTest/java/com/android/developers/androidify/results/ResultsScreenScreenshotTest.kt +++ b/feature/results/src/screenshotTest/java/com/android/developers/androidify/results/ResultsScreenScreenshotTest.kt @@ -29,10 +29,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.util.AdaptivePreview import com.android.developers.androidify.util.SmallPhonePreview -import androidx.core.net.toUri class ResultsScreenScreenshotTest { 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 fee29d60..8f4214ae 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 @@ -77,7 +77,7 @@ class CustomizeStateTest { @Test fun exportImageCanvas_defaultValues() { val canvas = ExportImageCanvas() - Assert.assertNull(canvas.imageBitmap) + Assert.assertNull(canvas.imageUri) Assert.assertEquals(SizeOption.Square, canvas.aspectRatioOption) Assert.assertEquals(Size(1000f, 1000f), canvas.canvasSize) Assert.assertNull(canvas.mainImageUri) 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 a00e7f29..3d235331 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 @@ -21,6 +21,7 @@ import android.graphics.Bitmap import android.net.Uri import androidx.test.core.app.ApplicationProvider import com.android.developers.testing.data.TestFileProvider +import com.android.developers.testing.data.bitmapSample import com.android.developers.testing.network.TestRemoteConfigDataSource import com.android.developers.testing.repository.FakeImageGenerationRepository import com.android.developers.testing.repository.FakeWatchFaceInstallationRepository @@ -49,11 +50,9 @@ class CustomizeViewModelTest { val mainDispatcherRule = MainDispatcherRule() private lateinit var viewModel: CustomizeExportViewModel - - private val fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) private val originalFakeUri = Uri.parse("content://com.example.app/images/original.jpg") - val fakeUri = Uri.parse("content://test/image.jpg") + private val fakeUri = Uri.parse("content://com.example.app/images/original.jpg") @Before fun setup() { @@ -72,28 +71,21 @@ class CustomizeViewModelTest { } @Test - fun stateInitialEmpty() = runTest { - assertEquals( - CustomizeExportState(), - viewModel.state.value, + fun stateResultUri_NotNull() = runTest { + assertNotNull( + viewModel.state.value.exportImageCanvas.imageUri, ) } @Test fun setArgumentsWithOriginalImage() = runTest { val initialState = viewModel.state.value - - viewModel.setArguments( - fakeUri, - originalFakeUri, - ) - // Ensure state has changed - view model uses combine to combine state flows so state // update is not immediate val newState = viewModel.state.first { it != initialState } assertEquals( CustomizeExportState( - exportImageCanvas = ExportImageCanvas(imageBitmap = fakeBitmap), + exportImageCanvas = ExportImageCanvas(imageUri = fakeUri, imageBitmap = bitmapSample), originalImageUrl = originalFakeUri, ), newState, @@ -116,19 +108,13 @@ class CustomizeViewModelTest { watchfaceInstallationRepository = FakeWatchFaceInstallationRepository(), remoteConfigDataSource = remoteConfigDataSource, ) - - viewModel.setArguments( - fakeUri, - null, - ) - // Ensure state has changed - view model uses combine to combine state flows so state // update is not immediate val newState = viewModel.state.first { it != initialState } assertEquals( CustomizeExportState( - exportImageCanvas = ExportImageCanvas(imageBitmap = fakeBitmap), + exportImageCanvas = ExportImageCanvas(imageUri = fakeUri, imageBitmap = bitmapSample), originalImageUrl = null, ), newState, @@ -144,10 +130,6 @@ class CustomizeViewModelTest { } } - viewModel.setArguments( - fakeUri, - originalFakeUri, - ) viewModel.downloadClicked() assertNotNull(values.last().externalOriginalSavedUri) @@ -166,10 +148,6 @@ class CustomizeViewModelTest { values.add(it) } } - viewModel.setArguments( - fakeUri, - originalFakeUri, - ) advanceUntilIdle() viewModel.shareClicked() // Ensure all coroutines on the test scheduler complete @@ -181,7 +159,7 @@ class CustomizeViewModelTest { fun changeBackground_NotNull() = runTest { val viewModel = CustomizeExportViewModel( fakeUri, - originalFakeUri, + null, FakeImageGenerationRepository(), composableBitmapRenderer = FakeComposableBitmapRenderer(), watchfaceInstallationRepository = FakeWatchFaceInstallationRepository(), @@ -196,10 +174,6 @@ class CustomizeViewModelTest { values.add(it) } } - viewModel.setArguments( - fakeUri, - originalFakeUri, - ) advanceUntilIdle() viewModel.selectedToolStateChanged( BackgroundToolState( @@ -226,10 +200,6 @@ class CustomizeViewModelTest { values.add(it) } } - viewModel.setArguments( - fakeUri, - originalFakeUri, - ) advanceUntilIdle() viewModel.selectedToolStateChanged( BackgroundToolState( @@ -262,12 +232,6 @@ class CustomizeViewModelTest { ) val initialState = viewModel.state.value - - viewModel.setArguments( - fakeUri, - null, - ) - val newState = viewModel.state.first { it != initialState } val toolState = newState.toolState[CustomizeTool.Background] as BackgroundToolState diff --git a/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt b/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt index e8828609..810a2edf 100644 --- a/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt +++ b/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt @@ -34,25 +34,14 @@ class ResultsViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() - private lateinit var viewModel: ResultsViewModel - - private val fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) private val fakePromptText = "Pink Hair, plaid shirt, jeans" private val originalFakeUri = Uri.parse("content://com.example.app/images/original.jpg") private val fakeUri = Uri.parse("content://test/image.jpg") - @Before - fun setup() { - viewModel = ResultsViewModel( - fakeUri, - originalFakeUri, - fakePromptText - ) - } - @Test fun stateInitialEmpty() = runTest { + val viewModel = ResultsViewModel(null,null,null) assertEquals( ResultState(), viewModel.state.value, @@ -60,7 +49,8 @@ class ResultsViewModelTest { } @Test - fun setArgumentsWithOriginalImage() = runTest { + fun setArgumentsWithOriginalImage_isCorrect() = runTest { + val viewModel = ResultsViewModel(fakeUri,originalFakeUri,null) assertEquals( ResultState( resultImageUri = fakeUri, @@ -71,7 +61,8 @@ class ResultsViewModelTest { } @Test - fun setArgumentsWithPrompt() = runTest { + fun initialState_withPrompt_isCorrect() = runTest { + val viewModel = ResultsViewModel(fakeUri,null,fakePromptText) assertEquals( ResultState( resultImageUri = fakeUri, From 453f8e88bc7fbea7db3afb504bcd5b0918126442 Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Thu, 4 Sep 2025 15:45:54 +0530 Subject: [PATCH 08/17] ./gradlew spotlessApply --- .../androidify/navigation/NavigationRoutes.kt | 8 ++++---- .../androidify/creation/CreationScreen.kt | 13 +------------ .../androidify/creation/CreationViewModel.kt | 3 +-- .../androidify/results/ResultsScreenTest.kt | 2 -- .../androidify/customize/CustomizeExportScreen.kt | 8 -------- .../customize/CustomizeExportViewModel.kt | 6 +++--- .../developers/androidify/results/BotResultCard.kt | 1 - .../androidify/results/ResultsViewModel.kt | 6 ++---- .../androidify/customize/CustomizeViewModelTest.kt | 2 -- .../androidify/results/ResultsViewModelTest.kt | 8 +++----- 10 files changed, 14 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt b/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt index 50930b0c..8322c46f 100644 --- a/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt +++ b/app/src/main/java/com/android/developers/androidify/navigation/NavigationRoutes.kt @@ -29,7 +29,7 @@ data object Home : NavigationRoute @Serializable data class Create( @Serializable(with = UriSerializer::class) val fileName: Uri? = null, - val prompt: String? = null + val prompt: String? = null, ) : NavigationRoute @Serializable @@ -49,7 +49,7 @@ object About : NavigationRoute data class Result( @Serializable(with = UriSerializer::class) val resultImageUri: Uri, @Serializable(with = UriSerializer::class) val originalImageUri: Uri? = null, - val prompt: String? = null + val prompt: String? = null, ) : NavigationRoute /** @@ -61,5 +61,5 @@ data class Result( @Serializable data class CustomizeExport( @Serializable(with = UriSerializer::class) val resultImageUri: Uri, - @Serializable(with = UriSerializer::class) val originalImageUri: Uri? -) : NavigationRoute \ No newline at end of file + @Serializable(with = UriSerializer::class) val originalImageUri: Uri?, +) : NavigationRoute diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt index 059cb578..8cdd2681 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt @@ -92,7 +92,6 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.ripple import androidx.compose.material3.toShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -121,23 +120,13 @@ 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.sp -import androidx.core.net.toUri import androidx.graphics.shapes.RoundedPolygon import androidx.graphics.shapes.rectangle -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.compose.LifecycleResumeEffect -import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade -import com.android.developers.androidify.customize.CustomizeAndExportScreen -import com.android.developers.androidify.customize.CustomizeExportViewModel import com.android.developers.androidify.data.DropBehaviourFactory -import com.android.developers.androidify.results.ResultsScreen import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.LimeGreen import com.android.developers.androidify.theme.LocalSharedTransitionScope @@ -194,7 +183,7 @@ fun CreationScreen( uiState.imageUri } else { null - } + }, ) creationViewModel.onResultDisplayed() } diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt index 603e0ba9..632300f2 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt @@ -43,7 +43,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel(assistedFactory = CreationViewModel.Factory::class) class CreationViewModel @AssistedInject constructor( @@ -164,7 +163,7 @@ class CreationViewModel @AssistedInject constructor( _uiState.update { it.copy( resultBitmapUri = imageGenerationRepository.saveImage(bitmap), - screenState = ScreenState.EDIT + screenState = ScreenState.EDIT, ) } } catch (e: Exception) { diff --git a/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt b/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt index 9f1f312a..f478f44b 100644 --- a/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt +++ b/feature/results/src/androidTest/java/com/android/developers/androidify/results/ResultsScreenTest.kt @@ -15,7 +15,6 @@ */ package com.android.developers.androidify.results -import android.graphics.Bitmap import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.CompositionLocalProvider @@ -175,7 +174,6 @@ class ResultsScreenTest { val frontCardDesc = composeTestRule.activity.getString(R.string.resultant_android_bot) val testUri = android.net.Uri.parse("placeholder://image") - val initialState = ResultState(resultImageUri = testUri, originalImageUrl = testUri) val state = mutableStateOf(initialState) 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 dd2a972e..e3b53abb 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,9 +18,6 @@ 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 import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalSharedTransitionApi @@ -62,18 +59,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.dropShadow -import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.LookaheadScope import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.android.developers.androidify.results.PermissionRationaleDialog @@ -96,7 +89,6 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.shouldShowRationale import com.android.developers.androidify.theme.R as ThemeR -import android.content.ContentResolver @OptIn(ExperimentalMaterial3Api::class) @Composable 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 f783c6b4..0cf8c7b4 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 @@ -57,10 +57,10 @@ class CustomizeExportViewModel @AssistedInject constructor( ) : AndroidViewModel(application) { @AssistedFactory - interface Factory{ + interface Factory { fun create( @Assisted("resultImageUrl") resultImageUrl: Uri, - @Assisted("originalImageUrl")originalImageUrl: Uri? + @Assisted("originalImageUrl")originalImageUrl: Uri?, ): CustomizeExportViewModel } @@ -400,7 +400,7 @@ class CustomizeExportViewModel @AssistedInject constructor( val bitmap = localFileProvider.loadBitmapFromUri(uri) _state.update { it.copy( - exportImageCanvas = it.exportImageCanvas.copy(imageBitmap = bitmap) + exportImageCanvas = it.exportImageCanvas.copy(imageBitmap = bitmap), ) } } catch (e: Exception) { diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt b/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt index fd772d8d..3f4139be 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/BotResultCard.kt @@ -15,7 +15,6 @@ */ package com.android.developers.androidify.results -import android.graphics.Bitmap import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt index 2b226d61..5005ecc4 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsViewModel.kt @@ -15,7 +15,6 @@ */ package com.android.developers.androidify.results -import android.graphics.Bitmap import android.net.Uri import androidx.compose.material3.SnackbarHostState import androidx.lifecycle.ViewModel @@ -27,13 +26,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import javax.inject.Inject @HiltViewModel(assistedFactory = ResultsViewModel.Factory::class) class ResultsViewModel @AssistedInject constructor( @Assisted("resultImageUrl") val resultImageUrl: Uri?, @Assisted("originalImageUrl") val originalImageUrl: Uri?, - @Assisted("promptText") val promptText: String? + @Assisted("promptText") val promptText: String?, ) : ViewModel() { @AssistedFactory @@ -52,7 +50,7 @@ class ResultsViewModel @AssistedInject constructor( val snackbarHostState: StateFlow get() = _snackbarHostState - init{ + init { _state.update { ResultState(resultImageUrl, originalImageUrl, promptText = promptText) } 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 3d235331..dec8f90c 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 @@ -17,7 +17,6 @@ package com.android.developers.androidify.customize -import android.graphics.Bitmap import android.net.Uri import androidx.test.core.app.ApplicationProvider import com.android.developers.testing.data.TestFileProvider @@ -130,7 +129,6 @@ class CustomizeViewModelTest { } } - viewModel.downloadClicked() assertNotNull(values.last().externalOriginalSavedUri) assertEquals( diff --git a/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt b/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt index 810a2edf..43bf653c 100644 --- a/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt +++ b/feature/results/src/test/kotlin/com/android/developers/androidify/results/ResultsViewModelTest.kt @@ -17,13 +17,11 @@ package com.android.developers.androidify.results -import android.graphics.Bitmap import android.net.Uri import com.android.developers.testing.util.MainDispatcherRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals -import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -41,7 +39,7 @@ class ResultsViewModelTest { @Test fun stateInitialEmpty() = runTest { - val viewModel = ResultsViewModel(null,null,null) + val viewModel = ResultsViewModel(null, null, null) assertEquals( ResultState(), viewModel.state.value, @@ -50,7 +48,7 @@ class ResultsViewModelTest { @Test fun setArgumentsWithOriginalImage_isCorrect() = runTest { - val viewModel = ResultsViewModel(fakeUri,originalFakeUri,null) + val viewModel = ResultsViewModel(fakeUri, originalFakeUri, null) assertEquals( ResultState( resultImageUri = fakeUri, @@ -62,7 +60,7 @@ class ResultsViewModelTest { @Test fun initialState_withPrompt_isCorrect() = runTest { - val viewModel = ResultsViewModel(fakeUri,null,fakePromptText) + val viewModel = ResultsViewModel(fakeUri, null, fakePromptText) assertEquals( ResultState( resultImageUri = fakeUri, From 1eccbae77263f9318a2fd07faf4ac10f313e334a Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Thu, 4 Sep 2025 16:40:34 +0530 Subject: [PATCH 09/17] Fixed the failed TestCase # Conflicts: # feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt --- .../developers/androidify/creation/CreationViewModelTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt index dba889b4..1894e2bd 100644 --- a/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt +++ b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt @@ -50,11 +50,10 @@ class CreationViewModelTest { private val internetConnectivityManager = TestInternetConnectivityManager(true) private val imageGenerationRepository = FakeImageGenerationRepository() + private val fakeUri = Uri.parse("content://test/image.jpg") - private val fakeUri = Uri.parse("test.jpeg") @Before fun setup() { - val fakeUri = Uri.parse("content://test/image.jpg") viewModel = CreationViewModel( originalImageUrl = fakeUri, internetConnectivityManager, @@ -81,6 +80,7 @@ class CreationViewModelTest { @Test fun stateInitialEdit_WithOutImage() = runTest { viewModel = CreationViewModel( + null, internetConnectivityManager, imageGenerationRepository, TestTextGenerationRepository(), @@ -93,7 +93,7 @@ class CreationViewModelTest { viewModel.uiState.value.screenState, ) assertEquals(false, viewModel.uiState.value.promptGenerationInProgress) - assertEquals(null, viewModel.uiState.value.imageUri) + assertEquals( null, viewModel.uiState.value.imageUri) } @Test From b13fca1a0f8232ccee794befa6720c189749006a Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Wed, 10 Sep 2025 11:32:10 +0530 Subject: [PATCH 10/17] Test Cases handling and animation bug resolved. --- .../androidify/customize/CustomizeExportViewModel.kt | 5 +++-- .../developers/androidify/customize/ImageRenderer.kt | 7 +++---- .../developers/androidify/results/ResultsScreen.kt | 8 +++++++- .../androidify/customize/CustomizeViewModelTest.kt | 1 + 4 files changed, 14 insertions(+), 7 deletions(-) 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 0cf8c7b4..d0596660 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 @@ -37,7 +37,7 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn @@ -77,9 +77,10 @@ class CustomizeExportViewModel @AssistedInject constructor( ) }.stateIn( scope = viewModelScope, - started = WhileSubscribed(5000), + started = SharingStarted.Eagerly, initialValue = _state.value, ) + //val state = _state.asStateFlow() private var transferJob: Job? = null 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 dc26cac7..11e4fede 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 @@ -44,7 +44,6 @@ import androidx.compose.ui.layout.layout import androidx.compose.ui.res.imageResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastRoundToInt -import coil3.compose.AsyncImage import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.LocalAnimateBoundsScope @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalSharedTransitionApi::class) @@ -90,9 +89,9 @@ fun ImageResult( contentScale = ContentScale.Crop, contentDescription = null, ) - } else if (exportImageCanvas.imageUri != null) { - AsyncImage( - model = exportImageCanvas.imageUri, + } else if (exportImageCanvas.imageBitmap != null) { + Image( + bitmap = exportImageCanvas.imageBitmap.asImageBitmap(), modifier = Modifier .fillMaxSize(), contentScale = ContentScale.Crop, diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt index 6fe8f456..6fd96175 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.material3.SnackbarDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -71,6 +72,7 @@ import com.android.developers.androidify.util.SmallPhonePreview import com.android.developers.androidify.util.allowsFullContent import com.android.developers.androidify.util.isAtLeastMedium import com.google.accompanist.permissions.ExperimentalPermissionsApi +import kotlinx.coroutines.delay @Composable fun ResultsScreen( @@ -181,7 +183,11 @@ fun ResultsScreenContents( defaultSelectedResult: ResultOption = ResultOption.ResultImage, ) { ResultsBackground() - val showResult = state.value.resultImageUri != null + var showResult by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + delay(100) + showResult = state.value.resultImageUri != null + } var selectedResultOption by remember { mutableStateOf(defaultSelectedResult) } 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 dec8f90c..5ff620de 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 @@ -71,6 +71,7 @@ class CustomizeViewModelTest { @Test fun stateResultUri_NotNull() = runTest { + println(viewModel.state.value.exportImageCanvas) assertNotNull( viewModel.state.value.exportImageCanvas.imageUri, ) From dda5cda1efedf942dd685a09fd3bcfc8164988fb Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Wed, 10 Sep 2025 11:44:03 +0530 Subject: [PATCH 11/17] Test Cases handling and animation bug resolved. --- .../android/developers/androidify/results/ResultsScreen.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt index 6fd96175..5d377f9c 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt @@ -72,7 +72,6 @@ import com.android.developers.androidify.util.SmallPhonePreview import com.android.developers.androidify.util.allowsFullContent import com.android.developers.androidify.util.isAtLeastMedium import com.google.accompanist.permissions.ExperimentalPermissionsApi -import kotlinx.coroutines.delay @Composable fun ResultsScreen( @@ -184,8 +183,7 @@ fun ResultsScreenContents( ) { ResultsBackground() var showResult by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - delay(100) + LaunchedEffect(state.value.resultImageUri) { showResult = state.value.resultImageUri != null } var selectedResultOption by remember { From 10a64335767033222d53cfc43fd915f6914ac9a6 Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Wed, 10 Sep 2025 12:23:47 +0530 Subject: [PATCH 12/17] Test Cases handling --- .../androidify/customize/CustomizeExportViewModel.kt | 5 ++--- .../androidify/customize/CustomizeViewModelTest.kt | 10 +++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) 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 d0596660..0cf8c7b4 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 @@ -37,7 +37,7 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn @@ -77,10 +77,9 @@ class CustomizeExportViewModel @AssistedInject constructor( ) }.stateIn( scope = viewModelScope, - started = SharingStarted.Eagerly, + started = WhileSubscribed(5000), initialValue = _state.value, ) - //val state = _state.asStateFlow() private var transferJob: Job? = null 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 5ff620de..15cc5475 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 @@ -71,10 +71,14 @@ class CustomizeViewModelTest { @Test fun stateResultUri_NotNull() = runTest { - println(viewModel.state.value.exportImageCanvas) - assertNotNull( + val firstState = viewModel.state.first() + + // Assert + assertNotNull(firstState.exportImageCanvas.imageUri) + assertEquals(fakeUri, firstState.exportImageCanvas.imageUri) + /*assertNotNull( viewModel.state.value.exportImageCanvas.imageUri, - ) + )*/ } @Test From 6378aac5d1d7e3306c6b31709d5317ebc4a199f1 Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Fri, 12 Sep 2025 12:53:38 +0530 Subject: [PATCH 13/17] Used imageBitmap of ExportImageCanvas to load Image in ImageRenderer.kt and removed imageUri as it wasn't required anymore. --- .../customize/CustomizeExportScreen.kt | 8 +++---- .../customize/CustomizeExportViewModel.kt | 1 - .../androidify/customize/CustomizeState.kt | 1 - .../androidify/customize/ImageRenderer.kt | 24 +++++++++---------- .../developers/androidify/customize/Utils.kt | 8 +++++++ .../customize/CustomizeStateTest.kt | 4 ++-- .../customize/CustomizeViewModelTest.kt | 8 +++---- 7 files changed, 30 insertions(+), 24 deletions(-) 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 f35d50b3..995022ee 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 @@ -485,9 +485,9 @@ fun CustomizeExportPreview() { displayName = "Pixel Watch 3", hasAndroidify = true, ) - val imageUri = getPlaceholderBotUri() + val bitmap = getPlaceholderBotBitmap() val state = CustomizeExportState( - exportImageCanvas = ExportImageCanvas(imageUri = imageUri), + exportImageCanvas = ExportImageCanvas(imageBitmap = bitmap), connectedWatch = connectedWatch, ) CustomizeExportContents( @@ -517,7 +517,7 @@ fun CustomizeExportPreviewLarge() { AnimatedContent(true) { targetState -> targetState CompositionLocalProvider(LocalNavAnimatedContentScope provides this@AnimatedContent) { - val imageUri = getPlaceholderBotUri() + val bitmap = getPlaceholderBotBitmap() val connectedWatch = ConnectedWatch( nodeId = "1234", displayName = "Pixel Watch 3", @@ -525,7 +525,7 @@ fun CustomizeExportPreviewLarge() { ) val state = CustomizeExportState( exportImageCanvas = ExportImageCanvas( - imageUri = imageUri, + imageBitmap = bitmap, aspectRatioOption = SizeOption.Square, ), selectedTool = CustomizeTool.Background, 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 0cf8c7b4..60714b13 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 @@ -115,7 +115,6 @@ class CustomizeExportViewModel @AssistedInject constructor( _state.update { it.copy( originalImageUrl = originalImageUrl, - exportImageCanvas = it.exportImageCanvas.copy(imageUri = resultImageUrl), toolState = mapOf( CustomizeTool.Size to AspectRatioToolState(), CustomizeTool.Background to BackgroundToolState( 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 f2866c6c..06c9cc1a 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 @@ -70,7 +70,6 @@ data class BackgroundToolState( ) : ToolState data class ExportImageCanvas( - val imageUri: Uri? = null, val imageBitmap: Bitmap? = null, val imageBitmapRemovedBackground: Bitmap? = null, val aspectRatioOption: SizeOption = SizeOption.Square, 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 11e4fede..761599f0 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 @@ -207,12 +207,12 @@ private fun Modifier.safeAnimateBounds(): Modifier { @Preview @Composable private fun ImageRendererPreviewSquare() { - val imageUri = getPlaceholderBotUri() + val imageBitmap = getPlaceholderBotBitmap() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageUri = imageUri, + imageBitmap = imageBitmap, canvasSize = Size(1000f, 1000f), aspectRatioOption = SizeOption.Square, selectedBackgroundOption = BackgroundOption.IO, @@ -230,11 +230,11 @@ private fun ImageRendererPreviewSquare() { @Preview @Composable private fun ImageRendererPreviewBanner() { - val imageUri = getPlaceholderBotUri() + val imageBitmap = getPlaceholderBotBitmap() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageUri = imageUri, + imageBitmap = imageBitmap, canvasSize = Size(1000f, 1000f), aspectRatioOption = SizeOption.Banner, selectedBackgroundOption = BackgroundOption.Lightspeed, @@ -252,11 +252,11 @@ private fun ImageRendererPreviewBanner() { @Preview @Composable private fun ImageRendererPreviewWallpaper() { - val imageUri = getPlaceholderBotUri() + val imageBitmap = getPlaceholderBotBitmap() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageUri = imageUri, + imageBitmap = imageBitmap, canvasSize = Size(1000f, 1000f), aspectRatioOption = SizeOption.Wallpaper, selectedBackgroundOption = BackgroundOption.Lightspeed, @@ -274,11 +274,11 @@ private fun ImageRendererPreviewWallpaper() { @Preview(widthDp = 1280, heightDp = 800) @Composable private fun ImageRendererPreviewWallpaperTablet() { - val imageUri = getPlaceholderBotUri() + val imageBitmap = getPlaceholderBotBitmap() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageUri = imageUri, + imageBitmap = imageBitmap, canvasSize = Size(1280f, 800f), aspectRatioOption = SizeOption.WallpaperTablet, selectedBackgroundOption = BackgroundOption.Lightspeed, @@ -296,11 +296,11 @@ private fun ImageRendererPreviewWallpaperTablet() { @Preview @Composable private fun ImageRendererPreviewWallpaperSocial() { - val imageUri = getPlaceholderBotUri() + val imageBitmap = getPlaceholderBotBitmap() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageUri = imageUri, + imageBitmap = imageBitmap, canvasSize = Size(1600f, 900f), aspectRatioOption = SizeOption.SocialHeader, selectedBackgroundOption = BackgroundOption.Lightspeed, @@ -318,11 +318,11 @@ private fun ImageRendererPreviewWallpaperSocial() { @Preview @Composable fun ImageRendererPreviewWallpaperIO() { - val imageUri = getPlaceholderBotUri() + val imageBitmap = getPlaceholderBotBitmap() AndroidifyTheme { ImageResult( ExportImageCanvas( - imageUri = imageUri, + imageBitmap = imageBitmap, canvasSize = Size(1600f, 900f), aspectRatioOption = SizeOption.SocialHeader, selectedBackgroundOption = BackgroundOption.IO, diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt index 40886c77..42d11a00 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt @@ -18,10 +18,18 @@ package com.android.developers.androidify.customize import android.content.ContentResolver import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.imageResource import androidx.core.net.toUri +import coil3.Bitmap import com.android.developers.androidify.results.R @Composable fun getPlaceholderBotUri(): Uri = ("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${LocalContext.current.packageName}/${R.drawable.placeholderbot}").toUri() + +@Composable +fun getPlaceholderBotBitmap(): Bitmap = + ImageBitmap.imageResource(id = R.drawable.placeholderbot).asAndroidBitmap() \ No newline at end of file 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 8f4214ae..14717a48 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 @@ -77,7 +77,7 @@ class CustomizeStateTest { @Test fun exportImageCanvas_defaultValues() { val canvas = ExportImageCanvas() - Assert.assertNull(canvas.imageUri) + Assert.assertNull(canvas.imageBitmap) Assert.assertEquals(SizeOption.Square, canvas.aspectRatioOption) Assert.assertEquals(Size(1000f, 1000f), canvas.canvasSize) Assert.assertNull(canvas.mainImageUri) @@ -86,7 +86,7 @@ class CustomizeStateTest { Assert.assertEquals(0f, canvas.imageRotation) Assert.assertEquals(Size(1024f, 1024f), canvas.imageOriginalBitmapSize) Assert.assertEquals(BackgroundOption.IO, canvas.selectedBackgroundOption) - Assert.assertEquals(com.android.developers.androidify.results.R.drawable.background_square_blocks, canvas.selectedBackgroundDrawable) + Assert.assertEquals(R.drawable.background_square_blocks, canvas.selectedBackgroundDrawable) Assert.assertTrue(canvas.includeWatermark) } 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 15cc5475..3cb22978 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 @@ -74,8 +74,8 @@ class CustomizeViewModelTest { val firstState = viewModel.state.first() // Assert - assertNotNull(firstState.exportImageCanvas.imageUri) - assertEquals(fakeUri, firstState.exportImageCanvas.imageUri) + assertNotNull(firstState.exportImageCanvas.imageBitmap) + assertEquals(bitmapSample, firstState.exportImageCanvas.imageBitmap) /*assertNotNull( viewModel.state.value.exportImageCanvas.imageUri, )*/ @@ -89,7 +89,7 @@ class CustomizeViewModelTest { val newState = viewModel.state.first { it != initialState } assertEquals( CustomizeExportState( - exportImageCanvas = ExportImageCanvas(imageUri = fakeUri, imageBitmap = bitmapSample), + exportImageCanvas = ExportImageCanvas(imageBitmap = bitmapSample), originalImageUrl = originalFakeUri, ), newState, @@ -118,7 +118,7 @@ class CustomizeViewModelTest { assertEquals( CustomizeExportState( - exportImageCanvas = ExportImageCanvas(imageUri = fakeUri, imageBitmap = bitmapSample), + exportImageCanvas = ExportImageCanvas(imageBitmap = bitmapSample), originalImageUrl = null, ), newState, From c886f0dc4f9fdb7db14c9772d87d395caf57375a Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Fri, 12 Sep 2025 12:54:45 +0530 Subject: [PATCH 14/17] Used imageBitmap of ExportImageCanvas to load Image in ImageRenderer.kt and removed imageUri as it wasn't required anymore. --- .../java/com/android/developers/androidify/customize/Utils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt index 42d11a00..4e662541 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/Utils.kt @@ -16,6 +16,7 @@ package com.android.developers.androidify.customize import android.content.ContentResolver +import android.graphics.Bitmap import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.ImageBitmap @@ -23,7 +24,6 @@ import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.imageResource import androidx.core.net.toUri -import coil3.Bitmap import com.android.developers.androidify.results.R @Composable From 7cfd2b2eae862bd4a9bcb93ce77c243375295eff Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Fri, 12 Sep 2025 17:45:26 +0530 Subject: [PATCH 15/17] Removed Commented Code --- .../androidify/customize/CustomizeViewModelTest.kt | 6 ------ 1 file changed, 6 deletions(-) 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 3cb22978..d096119b 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 @@ -72,13 +72,8 @@ class CustomizeViewModelTest { @Test fun stateResultUri_NotNull() = runTest { val firstState = viewModel.state.first() - - // Assert assertNotNull(firstState.exportImageCanvas.imageBitmap) assertEquals(bitmapSample, firstState.exportImageCanvas.imageBitmap) - /*assertNotNull( - viewModel.state.value.exportImageCanvas.imageUri, - )*/ } @Test @@ -190,7 +185,6 @@ class CustomizeViewModelTest { ) advanceUntilIdle() assertFalse { values[values.lastIndex].showImageEditProgress } - // assertTrue(values.any { it.showImageEditProgress }) assertNotNull(values.last().exportImageCanvas.imageWithEdit) } From 09bb0090aa06aa6b762de51be0d9e8bab34338e9 Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Fri, 12 Sep 2025 17:58:45 +0530 Subject: [PATCH 16/17] Added Timber exception --- .../developers/androidify/customize/CustomizeExportViewModel.kt | 1 + 1 file changed, 1 insertion(+) 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 4e474618..dad37c49 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 @@ -409,6 +409,7 @@ class CustomizeExportViewModel @AssistedInject constructor( ) } } catch (e: Exception) { + Timber.e(e, "Could not load Bitmap from the URI due to ${e.message}") _snackbarHostState.value.showSnackbar("Could not load image.") } } From 944c054c294bebc7f7173058a3079299ea622d4d Mon Sep 17 00:00:00 2001 From: Srikrishna Sakunia Date: Fri, 12 Sep 2025 17:59:15 +0530 Subject: [PATCH 17/17] Added Timber exception --- .../developers/androidify/customize/CustomizeExportViewModel.kt | 1 - 1 file changed, 1 deletion(-) 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 dad37c49..c8444670 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 @@ -43,7 +43,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject import kotlin.collections.isNotEmpty @HiltViewModel(assistedFactory = CustomizeExportViewModel.Factory::class)