From 5687530256e9727457192c91ead7a66901790ff4 Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Thu, 3 Jul 2025 16:19:57 +0200 Subject: [PATCH 01/11] Enables Dropping an image into Androidify Change-Id: I0b63c1603bc09dcfd5dc7fcbfddaca0846abb2d4 --- .../androidify/creation/CreationScreen.kt | 58 ++++++++++++--- .../androidify/creation/DropBehaviour.kt | 71 +++++++++++++++++++ 2 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/DropBehaviour.kt 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 ca80bde4..3c722fbd 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 @@ -22,7 +22,10 @@ package com.android.developers.androidify.creation +import android.content.ClipDescription import android.net.Uri +import android.util.Log +import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -36,6 +39,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -99,6 +103,10 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.mimeTypes +import androidx.compose.ui.draganddrop.toAndroidDragEvent import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -194,6 +202,7 @@ fun CreationScreen( onPromptGenerationPressed = creationViewModel::onPromptGenerationClicked, onBotColorSelected = creationViewModel::onBotColorChanged, onStartClicked = creationViewModel::startClicked, + onDropCallback = creationViewModel::onImageSelected, ) } @@ -242,6 +251,7 @@ fun EditScreen( onPromptGenerationPressed: () -> Unit, onBotColorSelected: (BotColor) -> Unit, onStartClicked: () -> Unit, + onDropCallback: (Uri) -> Unit = {}, ) { Scaffold( snackbarHost = { @@ -313,6 +323,7 @@ fun EditScreen( onUndoPressed = onUndoPressed, onPromptGenerationPressed = onPromptGenerationPressed, onSelectedPromptOptionChanged = onPromptOptionSelected, + onDropCallback = onDropCallback, ) Box( modifier = Modifier @@ -348,6 +359,7 @@ fun EditScreen( onUndoPressed = onUndoPressed, onPromptGenerationPressed = onPromptGenerationPressed, onSelectedPromptOptionChanged = onPromptOptionSelected, + onDropCallback = onDropCallback ) } @@ -400,12 +412,29 @@ private fun MainCreationPane( onUndoPressed: () -> Unit = {}, onPromptGenerationPressed: () -> Unit, onSelectedPromptOptionChanged: (PromptType) -> Unit, + onDropCallback: (Uri) -> Unit, ) { + val defaultDropAreaBackgroundColor = MaterialTheme.colorScheme.surface + val alternateDropAreaBackgroundColor = MaterialTheme.colorScheme.surfaceVariant + var background by remember { mutableStateOf(defaultDropAreaBackgroundColor) } + + + val activity = LocalContext.current as ComponentActivity + val externalAppCallback = remember { + DropBehaviour(activity).createTargetCallback( + onImageDropped = { uri -> onDropCallback(uri) }, + onDropStarted = { background = alternateDropAreaBackgroundColor }, + onDropEnded = { background = defaultDropAreaBackgroundColor }, + ) + } + + Box( modifier = modifier, ) { val spatialSpec = MaterialTheme.motionScheme.slowSpatialSpec() - val pagerState = rememberPagerState(uiState.selectedPromptOption.ordinal) { PromptType.entries.size } + val pagerState = + rememberPagerState(uiState.selectedPromptOption.ordinal) { PromptType.entries.size } val focusManager = LocalFocusManager.current LaunchedEffect(uiState.selectedPromptOption) { launch { @@ -443,6 +472,21 @@ private fun MainCreationPane( if (imageUri == null) { UploadEmptyState( modifier = Modifier + .background( + color = background, + shape = RoundedCornerShape(28.dp), + ) + .dashedRoundedRectBorder( + 2.dp, + MaterialTheme.colorScheme.outline, + cornerRadius = 28.dp, + ) + .dragAndDropTarget( + shouldStartDragAndDrop = { event -> + event.mimeTypes().contains("image/jpeg") + }, + target = externalAppCallback, + ) .fillMaxSize() .padding(2.dp), onCameraPressed = onCameraPressed, @@ -614,6 +658,9 @@ fun ImagePreview( .clip(MaterialTheme.shapes.large) .fillMaxSize(), contentScale = ContentScale.Crop, + onLoading = { println("ROB-Loading")}, + onError = { println("ROB-Error: ${it.result.throwable}")}, + onSuccess = { println("ROB-Success")} ) Row( @@ -855,15 +902,6 @@ private fun UploadEmptyState( ) { Column( modifier = modifier - .background( - color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(28.dp), - ) - .dashedRoundedRectBorder( - 2.dp, - MaterialTheme.colorScheme.outline, - cornerRadius = 28.dp, - ) .verticalScroll(rememberScrollState()) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/DropBehaviour.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/DropBehaviour.kt new file mode 100644 index 00000000..6d171d5d --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/DropBehaviour.kt @@ -0,0 +1,71 @@ +package com.android.developers.androidify.creation + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.toAndroidDragEvent +import java.io.File +import java.io.FileOutputStream + +class DropBehaviour(val activity: ComponentActivity) { + + fun createTargetCallback( + onImageDropped: (Uri) -> Unit, + onDropStarted: () -> Unit = {}, + onDropEnded: () -> Unit = {}, + ) = + object : DragAndDropTarget { + override fun onStarted(event: DragAndDropEvent) { + super.onStarted(event) + onDropStarted() + } + + override fun onEnded(event: DragAndDropEvent) { + super.onEnded(event) + onDropEnded() + } + + override fun onDrop(event: DragAndDropEvent): Boolean { + val targetEvent = event.toAndroidDragEvent() + val permission = activity.requestDragAndDropPermissions(targetEvent) + if (permission != null) { + try { + val inputUri = targetEvent.clipData.getItemAt(0).uri + processImage(inputUri) + } catch (s: SecurityException) { + s.printStackTrace() + } finally { + permission.release() + } + return true + } else { + return false + } + } + + private fun processImage(input: Uri) { + activity.contentResolver.openInputStream(input)?.use { inputStream -> + val bitmap = BitmapFactory.decodeStream(inputStream) + + if (bitmap != null) { + val outputFileName = "dropped_image_${System.currentTimeMillis()}.jpg" + val outputFile = File( + activity.filesDir, + outputFileName, + ) + + FileOutputStream(outputFile).use { fos -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos) + } + onImageDropped(Uri.fromFile(outputFile)) + } else { + Log.e("DragDrop", "Failed to decode bitmap from URI: $input") + } + } + } + } +} \ No newline at end of file From 72345b822a2b7c5714f289d18fa433c181fb20b6 Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Thu, 3 Jul 2025 16:23:40 +0200 Subject: [PATCH 02/11] Clean imports Change-Id: I039004604086801fa3f99860bb17ecd238bdcc6b --- .../android/developers/androidify/creation/CreationScreen.kt | 5 ----- 1 file changed, 5 deletions(-) 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 3c722fbd..f487a010 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 @@ -22,9 +22,7 @@ package com.android.developers.androidify.creation -import android.content.ClipDescription import android.net.Uri -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult @@ -103,10 +101,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draganddrop.DragAndDropEvent -import androidx.compose.ui.draganddrop.DragAndDropTarget import androidx.compose.ui.draganddrop.mimeTypes -import androidx.compose.ui.draganddrop.toAndroidDragEvent import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color From ca2de4df0031ac73f1007121a6d9211756817032 Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Mon, 7 Jul 2025 14:04:31 +0200 Subject: [PATCH 03/11] Move DropBehaviour to be injected --- .../developers/androidify/data/DropBehaviourFactory.kt | 7 ++++--- .../developers/androidify/creation/CreationScreen.kt | 10 ++++++++-- .../androidify/creation/CreationViewModel.kt | 2 ++ 3 files changed, 14 insertions(+), 5 deletions(-) rename feature/creation/src/main/java/com/android/developers/androidify/creation/DropBehaviour.kt => data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt (93%) diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/DropBehaviour.kt b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt similarity index 93% rename from feature/creation/src/main/java/com/android/developers/androidify/creation/DropBehaviour.kt rename to data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt index 6d171d5d..4afdd2bd 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/DropBehaviour.kt +++ b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt @@ -1,4 +1,4 @@ -package com.android.developers.androidify.creation +package com.android.developers.androidify.data import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -8,12 +8,13 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.draganddrop.DragAndDropEvent import androidx.compose.ui.draganddrop.DragAndDropTarget import androidx.compose.ui.draganddrop.toAndroidDragEvent +import javax.inject.Inject import java.io.File import java.io.FileOutputStream -class DropBehaviour(val activity: ComponentActivity) { +class DropBehaviourFactory @Inject constructor() { - fun createTargetCallback( + fun createTargetCallback(activity: ComponentActivity, onImageDropped: (Uri) -> Unit, onDropStarted: () -> Unit = {}, onDropEnded: () -> Unit = {}, 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 f487a010..1cf6a946 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 @@ -128,6 +128,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade +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 @@ -186,6 +187,7 @@ fun CreationScreen( ScreenState.EDIT -> { EditScreen( snackbarHostState = snackbarHostState, + dropBehaviourFactory = creationViewModel.dropBehaviourFactory, isExpanded = isMedium, onCameraPressed = onCameraPressed, onBackPressed = onBackPressed, @@ -235,6 +237,7 @@ fun CreationScreen( @Composable fun EditScreen( snackbarHostState: SnackbarHostState, + dropBehaviourFactory: DropBehaviourFactory, isExpanded: Boolean, onCameraPressed: () -> Unit, onBackPressed: () -> Unit, @@ -310,6 +313,7 @@ fun EditScreen( ) { MainCreationPane( uiState, + dropBehaviourFactory = dropBehaviourFactory, modifier = Modifier.weight(.6f), onCameraPressed = onCameraPressed, onChooseImageClicked = { @@ -346,6 +350,7 @@ fun EditScreen( } else { MainCreationPane( uiState, + dropBehaviourFactory = dropBehaviourFactory, modifier = Modifier.weight(1f), onCameraPressed = onCameraPressed, onChooseImageClicked = { @@ -401,6 +406,7 @@ fun EditScreen( @OptIn(ExperimentalMaterial3ExpressiveApi::class) private fun MainCreationPane( uiState: CreationState, + dropBehaviourFactory: DropBehaviourFactory, modifier: Modifier = Modifier, onCameraPressed: () -> Unit, onChooseImageClicked: () -> Unit = {}, @@ -413,10 +419,10 @@ private fun MainCreationPane( val alternateDropAreaBackgroundColor = MaterialTheme.colorScheme.surfaceVariant var background by remember { mutableStateOf(defaultDropAreaBackgroundColor) } - val activity = LocalContext.current as ComponentActivity val externalAppCallback = remember { - DropBehaviour(activity).createTargetCallback( + dropBehaviourFactory.createTargetCallback( + activity = activity, onImageDropped = { uri -> onDropCallback(uri) }, onDropStarted = { background = alternateDropAreaBackgroundColor }, onDropEnded = { background = defaultDropAreaBackgroundColor }, 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 7e889ce8..d187c3af 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 @@ -24,6 +24,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.developers.androidify.data.DropBehaviourFactory import com.android.developers.androidify.data.ImageDescriptionFailedGenerationException import com.android.developers.androidify.data.ImageGenerationRepository import com.android.developers.androidify.data.ImageValidationError @@ -48,6 +49,7 @@ class CreationViewModel @Inject constructor( val imageGenerationRepository: ImageGenerationRepository, val textGenerationRepository: TextGenerationRepository, val fileProvider: LocalFileProvider, + val dropBehaviourFactory: DropBehaviourFactory, @ApplicationContext val context: Context, ) : ViewModel() { From eb160389986f541b683eea92216dbc8c1f48efc2 Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Mon, 7 Jul 2025 14:17:24 +0200 Subject: [PATCH 04/11] Use ImageGenerationRepository to process the image dragged to Androidify --- .../androidify/data/DropBehaviourFactory.kt | 59 +++++++------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt index 4afdd2bd..73ae365f 100644 --- a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt +++ b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt @@ -1,20 +1,19 @@ package com.android.developers.androidify.data -import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri -import android.util.Log import androidx.activity.ComponentActivity import androidx.compose.ui.draganddrop.DragAndDropEvent import androidx.compose.ui.draganddrop.DragAndDropTarget import androidx.compose.ui.draganddrop.toAndroidDragEvent +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch import javax.inject.Inject -import java.io.File -import java.io.FileOutputStream -class DropBehaviourFactory @Inject constructor() { +class DropBehaviourFactory @Inject constructor(val imageGenerationRepository: ImageGenerationRepository) { - fun createTargetCallback(activity: ComponentActivity, + fun createTargetCallback( + activity: ComponentActivity, onImageDropped: (Uri) -> Unit, onDropStarted: () -> Unit = {}, onDropEnded: () -> Unit = {}, @@ -32,41 +31,25 @@ class DropBehaviourFactory @Inject constructor() { override fun onDrop(event: DragAndDropEvent): Boolean { val targetEvent = event.toAndroidDragEvent() - val permission = activity.requestDragAndDropPermissions(targetEvent) - if (permission != null) { - try { - val inputUri = targetEvent.clipData.getItemAt(0).uri - processImage(inputUri) - } catch (s: SecurityException) { - s.printStackTrace() - } finally { - permission.release() - } - return true - } else { - return false - } - } - - private fun processImage(input: Uri) { - activity.contentResolver.openInputStream(input)?.use { inputStream -> - val bitmap = BitmapFactory.decodeStream(inputStream) - - if (bitmap != null) { - val outputFileName = "dropped_image_${System.currentTimeMillis()}.jpg" - val outputFile = File( - activity.filesDir, - outputFileName, - ) - - FileOutputStream(outputFile).use { fos -> - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos) + activity.lifecycleScope.launch { + val permission = activity.requestDragAndDropPermissions(targetEvent) + if (permission != null) { + try { + val inputUri = targetEvent.clipData.getItemAt(0).uri + activity.contentResolver.openInputStream(inputUri)?.use { inputStream -> + val bitmap = BitmapFactory.decodeStream(inputStream) + + bitmap?.let { + val uri = imageGenerationRepository.saveImage(bitmap) + onImageDropped(uri) + } + } + } finally { + permission.release() } - onImageDropped(Uri.fromFile(outputFile)) - } else { - Log.e("DragDrop", "Failed to decode bitmap from URI: $input") } } + return true } } } \ No newline at end of file From 2cceaca6be50bd6a937e658fd48bc0881a130c6c Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Mon, 7 Jul 2025 14:34:11 +0200 Subject: [PATCH 05/11] Add comments to clarify the logic --- .../developers/androidify/data/DropBehaviourFactory.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt index 73ae365f..becccc8c 100644 --- a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt +++ b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt @@ -29,6 +29,12 @@ class DropBehaviourFactory @Inject constructor(val imageGenerationRepository: Im onDropEnded() } + /** + * Dropping an image requires the app to obtain the permission to use the image being + * dropped. This permission only lasts until the event is completed. The easiest way + * of being able to display the image being dropped is to temporarily copy it inside + * the app storage and use that copy for the processing. + */ override fun onDrop(event: DragAndDropEvent): Boolean { val targetEvent = event.toAndroidDragEvent() activity.lifecycleScope.launch { From 1ed9702963fcab066c368936f8b66894b10dfd6e Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Mon, 7 Jul 2025 15:04:24 +0200 Subject: [PATCH 06/11] Fix code review comments --- .../developers/androidify/data/DropBehaviourFactory.kt | 8 ++++++++ .../developers/androidify/creation/CreationScreen.kt | 10 ++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt index becccc8c..d9a64fdc 100644 --- a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt +++ b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt @@ -5,6 +5,7 @@ import android.net.Uri import androidx.activity.ComponentActivity import androidx.compose.ui.draganddrop.DragAndDropEvent import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.mimeTypes import androidx.compose.ui.draganddrop.toAndroidDragEvent import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch @@ -12,6 +13,8 @@ import javax.inject.Inject class DropBehaviourFactory @Inject constructor(val imageGenerationRepository: ImageGenerationRepository) { + fun shouldStartDragAndDrop(event: DragAndDropEvent) : Boolean = event.mimeTypes().contains("image/") + fun createTargetCallback( activity: ComponentActivity, onImageDropped: (Uri) -> Unit, @@ -37,6 +40,11 @@ class DropBehaviourFactory @Inject constructor(val imageGenerationRepository: Im */ override fun onDrop(event: DragAndDropEvent): Boolean { val targetEvent = event.toAndroidDragEvent() + + if(targetEvent.clipData.itemCount == 0) { + return false + } + activity.lifecycleScope.launch { val permission = activity.requestDragAndDropPermissions(targetEvent) if (permission != null) { 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 1cf6a946..bc571e44 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 @@ -101,7 +101,6 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draganddrop.mimeTypes import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -359,7 +358,7 @@ fun EditScreen( onUndoPressed = onUndoPressed, onPromptGenerationPressed = onPromptGenerationPressed, onSelectedPromptOptionChanged = onPromptOptionSelected, - onDropCallback = onDropCallback + onDropCallback = onDropCallback, ) } @@ -484,7 +483,9 @@ private fun MainCreationPane( ) .dragAndDropTarget( shouldStartDragAndDrop = { event -> - event.mimeTypes().contains("image/jpeg") + dropBehaviourFactory.shouldStartDragAndDrop( + event, + ) }, target = externalAppCallback, ) @@ -659,9 +660,6 @@ fun ImagePreview( .clip(MaterialTheme.shapes.large) .fillMaxSize(), contentScale = ContentScale.Crop, - onLoading = { println("ROB-Loading")}, - onError = { println("ROB-Error: ${it.result.throwable}")}, - onSuccess = { println("ROB-Success")} ) Row( From 183781f98ef3c43e6b0ac32c410de9b6652f3dc4 Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Mon, 7 Jul 2025 15:21:40 +0200 Subject: [PATCH 07/11] Create Fake for testing --- .../repository/FakeDropImageBehaviour.kt | 24 +++++++++++++++++++ .../developers/androidify/data/DataModule.kt | 9 ++++++- .../androidify/data/DropBehaviourFactory.kt | 18 +++++++++++--- .../creation/CreationViewModelTest.kt | 2 ++ 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 core/testing/src/main/java/com/android/developers/testing/repository/FakeDropImageBehaviour.kt diff --git a/core/testing/src/main/java/com/android/developers/testing/repository/FakeDropImageBehaviour.kt b/core/testing/src/main/java/com/android/developers/testing/repository/FakeDropImageBehaviour.kt new file mode 100644 index 00000000..783d349b --- /dev/null +++ b/core/testing/src/main/java/com/android/developers/testing/repository/FakeDropImageBehaviour.kt @@ -0,0 +1,24 @@ +package com.android.developers.testing.repository + +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import com.android.developers.androidify.data.DropBehaviourFactory + +class FakeDropImageFactory : DropBehaviourFactory { + override fun shouldStartDragAndDrop(event: DragAndDropEvent): Boolean = true + + override fun createTargetCallback( + activity: ComponentActivity, + onImageDropped: (Uri) -> Unit, + onDropStarted: () -> Unit, + onDropEnded: () -> Unit, + ): DragAndDropTarget = object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + return false + } + + } + +} \ No newline at end of file diff --git a/data/src/main/java/com/android/developers/androidify/data/DataModule.kt b/data/src/main/java/com/android/developers/androidify/data/DataModule.kt index 6ffb15d1..99830aee 100644 --- a/data/src/main/java/com/android/developers/androidify/data/DataModule.kt +++ b/data/src/main/java/com/android/developers/androidify/data/DataModule.kt @@ -42,7 +42,10 @@ internal object DataModule { @Provides @Singleton - fun provideLocalFileProvider(@ApplicationContext appContext: Context, @Named("IO") ioDispatcher: CoroutineDispatcher): LocalFileProvider = + fun provideLocalFileProvider( + @ApplicationContext appContext: Context, + @Named("IO") ioDispatcher: CoroutineDispatcher, + ): LocalFileProvider = LocalFileProviderImpl(appContext, ioDispatcher) @Provides @@ -101,4 +104,8 @@ internal object DataModule { internetConnectivityManager = internetConnectivityManager, firebaseAiDataSource = firebaseAiDataSource, ) + + @Provides + fun dropBehaviourFactory(imageGenerationRepository: ImageGenerationRepository): DropBehaviourFactory = + DropBehaviourFactoryImpl(imageGenerationRepository = imageGenerationRepository) } diff --git a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt index d9a64fdc..7c7503e8 100644 --- a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt +++ b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt @@ -11,15 +11,27 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import javax.inject.Inject -class DropBehaviourFactory @Inject constructor(val imageGenerationRepository: ImageGenerationRepository) { - - fun shouldStartDragAndDrop(event: DragAndDropEvent) : Boolean = event.mimeTypes().contains("image/") +interface DropBehaviourFactory { + fun shouldStartDragAndDrop(event: DragAndDropEvent) : Boolean fun createTargetCallback( activity: ComponentActivity, onImageDropped: (Uri) -> Unit, onDropStarted: () -> Unit = {}, onDropEnded: () -> Unit = {}, + ): DragAndDropTarget +} + +class DropBehaviourFactoryImpl @Inject constructor(val imageGenerationRepository: ImageGenerationRepository) : + DropBehaviourFactory { + + override fun shouldStartDragAndDrop(event: DragAndDropEvent) : Boolean = event.mimeTypes().contains("image/") + + override fun createTargetCallback( + activity: ComponentActivity, + onImageDropped: (Uri) -> Unit, + onDropStarted: () -> Unit, + onDropEnded: () -> Unit, ) = object : DragAndDropTarget { override fun onStarted(event: DragAndDropEvent) { 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 ffa74cfa..11105ce9 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 @@ -20,6 +20,7 @@ import androidx.compose.material3.SnackbarHostState import com.android.developers.androidify.data.InsufficientInformationException import com.android.developers.testing.data.TestFileProvider import com.android.developers.testing.data.TestInternetConnectivityManager +import com.android.developers.testing.repository.FakeDropImageFactory import com.android.developers.testing.repository.FakeImageGenerationRepository import com.android.developers.testing.repository.TestTextGenerationRepository import com.android.developers.testing.util.MainDispatcherRule @@ -57,6 +58,7 @@ class CreationViewModelTest { imageGenerationRepository, TestTextGenerationRepository(), TestFileProvider(), + FakeDropImageFactory(), context = RuntimeEnvironment.getApplication(), ) } From b079263d95f2974e16b8c58dcf91742a4fa055d4 Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Mon, 7 Jul 2025 15:24:26 +0200 Subject: [PATCH 08/11] Fix naming --- .../{FakeDropImageBehaviour.kt => FakeDropImageFactory.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/testing/src/main/java/com/android/developers/testing/repository/{FakeDropImageBehaviour.kt => FakeDropImageFactory.kt} (100%) diff --git a/core/testing/src/main/java/com/android/developers/testing/repository/FakeDropImageBehaviour.kt b/core/testing/src/main/java/com/android/developers/testing/repository/FakeDropImageFactory.kt similarity index 100% rename from core/testing/src/main/java/com/android/developers/testing/repository/FakeDropImageBehaviour.kt rename to core/testing/src/main/java/com/android/developers/testing/repository/FakeDropImageFactory.kt From f190ae4bf4cd5650c3fe3874575cc8be1fb9bb99 Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Mon, 7 Jul 2025 15:35:25 +0200 Subject: [PATCH 09/11] Add fake to Android tests --- .../developers/androidify/creation/CreationScreenTest.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/feature/creation/src/androidTest/java/com/android/developers/androidify/creation/CreationScreenTest.kt b/feature/creation/src/androidTest/java/com/android/developers/androidify/creation/CreationScreenTest.kt index 46624d90..3e804199 100644 --- a/feature/creation/src/androidTest/java/com/android/developers/androidify/creation/CreationScreenTest.kt +++ b/feature/creation/src/androidTest/java/com/android/developers/androidify/creation/CreationScreenTest.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.developers.androidify.theme.SharedElementContextPreview +import com.android.developers.testing.repository.FakeDropImageFactory import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -45,6 +46,7 @@ class CreationScreenTest { SharedElementContextPreview { EditScreen( snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = FakeDropImageFactory(), isExpanded = false, onCameraPressed = {}, onBackPressed = {}, @@ -73,6 +75,7 @@ class CreationScreenTest { SharedElementContextPreview { EditScreen( snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = FakeDropImageFactory(), isExpanded = false, onCameraPressed = {}, onBackPressed = {}, @@ -102,6 +105,7 @@ class CreationScreenTest { SharedElementContextPreview { EditScreen( snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = FakeDropImageFactory(), isExpanded = false, onCameraPressed = {}, onBackPressed = {}, @@ -136,6 +140,7 @@ class CreationScreenTest { SharedElementContextPreview { EditScreen( snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = FakeDropImageFactory(), isExpanded = false, onCameraPressed = {}, onBackPressed = {}, @@ -169,6 +174,7 @@ class CreationScreenTest { SharedElementContextPreview { EditScreen( snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = FakeDropImageFactory(), isExpanded = false, onCameraPressed = {}, onBackPressed = {}, @@ -203,6 +209,7 @@ class CreationScreenTest { SharedElementContextPreview { EditScreen( snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = FakeDropImageFactory(), isExpanded = false, onCameraPressed = {}, onBackPressed = {}, @@ -236,6 +243,7 @@ class CreationScreenTest { SharedElementContextPreview { EditScreen( snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = FakeDropImageFactory(), isExpanded = false, onCameraPressed = {}, onBackPressed = {}, @@ -265,6 +273,7 @@ class CreationScreenTest { SharedElementContextPreview { EditScreen( snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = FakeDropImageFactory(), isExpanded = true, // Expanded mode onCameraPressed = {}, onBackPressed = {}, From 59b7b92d3b1bdf27b7e206f0ff1a25f2d865cd3f Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Mon, 7 Jul 2025 16:26:01 +0200 Subject: [PATCH 10/11] Change the shape to be the Material one instead --- .../androidify/creation/CreationScreen.kt | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) 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 bc571e44..e55c684f 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 @@ -274,8 +274,7 @@ fun EditScreen( expandedCenterButtons = { PromptTypeToolbar( uiState.selectedPromptOption, - modifier = Modifier - .padding(start = 16.dp, end = 16.dp), + modifier = Modifier.padding(start = 16.dp, end = 16.dp), onOptionSelected = onPromptOptionSelected, ) }, @@ -382,8 +381,7 @@ fun EditScreen( }, uiState = uiState, onStartClicked = onStartClicked, - modifier = Modifier - .align(Alignment.CenterHorizontally), + modifier = Modifier.align(Alignment.CenterHorizontally), ) } } @@ -474,7 +472,7 @@ private fun MainCreationPane( modifier = Modifier .background( color = background, - shape = RoundedCornerShape(28.dp), + shape = MaterialTheme.shapes.large, ) .dashedRoundedRectBorder( 2.dp, @@ -648,10 +646,7 @@ fun ImagePreview( with(sharedElementScope) { Box(modifier) { AsyncImage( - ImageRequest.Builder(LocalContext.current) - .data(uri) - .crossfade(false) - .build(), + ImageRequest.Builder(LocalContext.current).data(uri).crossfade(false).build(), placeholder = null, contentDescription = stringResource(CreationR.string.cd_selected_image), modifier = Modifier @@ -875,16 +870,14 @@ fun PromptTypeToolbar( private fun UploadEmptyPreview() { AndroidifyTheme { UploadEmptyState( - { - }, + {}, {}, modifier = Modifier .height(300.dp) .fillMaxWidth(), ) UploadEmptyState( - { - }, + {}, {}, modifier = Modifier .height(400.dp) From 3d14b5d95ba30531c971ea090fe943ede812ed24 Mon Sep 17 00:00:00 2001 From: Rob Orgiu Date: Mon, 7 Jul 2025 16:39:30 +0200 Subject: [PATCH 11/11] Move to LocalFileProvider rather than ImageGenerationRepository --- .../developers/androidify/data/DataModule.kt | 4 ++-- .../androidify/data/DropBehaviourFactory.kt | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/data/src/main/java/com/android/developers/androidify/data/DataModule.kt b/data/src/main/java/com/android/developers/androidify/data/DataModule.kt index 99830aee..883bbe4a 100644 --- a/data/src/main/java/com/android/developers/androidify/data/DataModule.kt +++ b/data/src/main/java/com/android/developers/androidify/data/DataModule.kt @@ -106,6 +106,6 @@ internal object DataModule { ) @Provides - fun dropBehaviourFactory(imageGenerationRepository: ImageGenerationRepository): DropBehaviourFactory = - DropBehaviourFactoryImpl(imageGenerationRepository = imageGenerationRepository) + fun dropBehaviourFactory(localFileProvider: LocalFileProvider,): DropBehaviourFactory = + DropBehaviourFactoryImpl(localFileProvider = localFileProvider) } diff --git a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt index 7c7503e8..944caf1d 100644 --- a/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt +++ b/data/src/main/java/com/android/developers/androidify/data/DropBehaviourFactory.kt @@ -8,12 +8,14 @@ import androidx.compose.ui.draganddrop.DragAndDropTarget import androidx.compose.ui.draganddrop.mimeTypes import androidx.compose.ui.draganddrop.toAndroidDragEvent import androidx.lifecycle.lifecycleScope +import com.android.developers.androidify.util.LocalFileProvider import kotlinx.coroutines.launch +import java.util.UUID import javax.inject.Inject interface DropBehaviourFactory { - fun shouldStartDragAndDrop(event: DragAndDropEvent) : Boolean + fun shouldStartDragAndDrop(event: DragAndDropEvent): Boolean fun createTargetCallback( activity: ComponentActivity, onImageDropped: (Uri) -> Unit, @@ -22,10 +24,11 @@ interface DropBehaviourFactory { ): DragAndDropTarget } -class DropBehaviourFactoryImpl @Inject constructor(val imageGenerationRepository: ImageGenerationRepository) : +class DropBehaviourFactoryImpl @Inject constructor(val localFileProvider: LocalFileProvider) : DropBehaviourFactory { - override fun shouldStartDragAndDrop(event: DragAndDropEvent) : Boolean = event.mimeTypes().contains("image/") + override fun shouldStartDragAndDrop(event: DragAndDropEvent): Boolean = + event.mimeTypes().contains("image/jpeg") override fun createTargetCallback( activity: ComponentActivity, @@ -53,7 +56,7 @@ class DropBehaviourFactoryImpl @Inject constructor(val imageGenerationRepository override fun onDrop(event: DragAndDropEvent): Boolean { val targetEvent = event.toAndroidDragEvent() - if(targetEvent.clipData.itemCount == 0) { + if (targetEvent.clipData.itemCount == 0) { return false } @@ -66,8 +69,10 @@ class DropBehaviourFactoryImpl @Inject constructor(val imageGenerationRepository val bitmap = BitmapFactory.decodeStream(inputStream) bitmap?.let { - val uri = imageGenerationRepository.saveImage(bitmap) - onImageDropped(uri) + val cacheFile = + localFileProvider.createCacheFile("dropped_image_${UUID.randomUUID()}.jpg") + localFileProvider.saveBitmapToFile(bitmap, cacheFile) + onImageDropped(localFileProvider.sharingUriForFile(cacheFile)) } } } finally {