Skip to content

Add Drop functionality #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -101,4 +104,8 @@ internal object DataModule {
internetConnectivityManager = internetConnectivityManager,
firebaseAiDataSource = firebaseAiDataSource,
)

@Provides
fun dropBehaviourFactory(localFileProvider: LocalFileProvider,): DropBehaviourFactory =
DropBehaviourFactoryImpl(localFileProvider = localFileProvider)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.android.developers.androidify.data

import android.graphics.BitmapFactory
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 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 createTargetCallback(
activity: ComponentActivity,
onImageDropped: (Uri) -> Unit,
onDropStarted: () -> Unit = {},
onDropEnded: () -> Unit = {},
): DragAndDropTarget
}

class DropBehaviourFactoryImpl @Inject constructor(val localFileProvider: LocalFileProvider) :
DropBehaviourFactory {

override fun shouldStartDragAndDrop(event: DragAndDropEvent): Boolean =
event.mimeTypes().contains("image/jpeg")

override fun createTargetCallback(
activity: ComponentActivity,
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()
}

/**
* 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()

if (targetEvent.clipData.itemCount == 0) {
return false
}

activity.lifecycleScope.launch {
val permission = activity.requestDragAndDropPermissions(targetEvent)
if (permission != null) {
try {
val inputUri = targetEvent.clipData.getItemAt(0).uri
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we always assume it'll be at 0 index?

activity.contentResolver.openInputStream(inputUri)?.use { inputStream ->
val bitmap = BitmapFactory.decodeStream(inputStream)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might make more sense to use LocalFileProvider here instead of imageGenerationRepository as its not generating an image - just copying to internal storage. You probably need to also push this off to a background thread instead of main thread - check if StrictMode logs are complaining.


bitmap?.let {
val cacheFile =
localFileProvider.createCacheFile("dropped_image_${UUID.randomUUID()}.jpg")
localFileProvider.saveBitmapToFile(bitmap, cacheFile)
onImageDropped(localFileProvider.sharingUriForFile(cacheFile))
}
}
} finally {
permission.release()
}
}
}
return true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,6 +46,7 @@ class CreationScreenTest {
SharedElementContextPreview {
EditScreen(
snackbarHostState = SnackbarHostState(),
dropBehaviourFactory = FakeDropImageFactory(),
isExpanded = false,
onCameraPressed = {},
onBackPressed = {},
Expand Down Expand Up @@ -73,6 +75,7 @@ class CreationScreenTest {
SharedElementContextPreview {
EditScreen(
snackbarHostState = SnackbarHostState(),
dropBehaviourFactory = FakeDropImageFactory(),
isExpanded = false,
onCameraPressed = {},
onBackPressed = {},
Expand Down Expand Up @@ -102,6 +105,7 @@ class CreationScreenTest {
SharedElementContextPreview {
EditScreen(
snackbarHostState = SnackbarHostState(),
dropBehaviourFactory = FakeDropImageFactory(),
isExpanded = false,
onCameraPressed = {},
onBackPressed = {},
Expand Down Expand Up @@ -136,6 +140,7 @@ class CreationScreenTest {
SharedElementContextPreview {
EditScreen(
snackbarHostState = SnackbarHostState(),
dropBehaviourFactory = FakeDropImageFactory(),
isExpanded = false,
onCameraPressed = {},
onBackPressed = {},
Expand Down Expand Up @@ -169,6 +174,7 @@ class CreationScreenTest {
SharedElementContextPreview {
EditScreen(
snackbarHostState = SnackbarHostState(),
dropBehaviourFactory = FakeDropImageFactory(),
isExpanded = false,
onCameraPressed = {},
onBackPressed = {},
Expand Down Expand Up @@ -203,6 +209,7 @@ class CreationScreenTest {
SharedElementContextPreview {
EditScreen(
snackbarHostState = SnackbarHostState(),
dropBehaviourFactory = FakeDropImageFactory(),
isExpanded = false,
onCameraPressed = {},
onBackPressed = {},
Expand Down Expand Up @@ -236,6 +243,7 @@ class CreationScreenTest {
SharedElementContextPreview {
EditScreen(
snackbarHostState = SnackbarHostState(),
dropBehaviourFactory = FakeDropImageFactory(),
isExpanded = false,
onCameraPressed = {},
onBackPressed = {},
Expand Down Expand Up @@ -265,6 +273,7 @@ class CreationScreenTest {
SharedElementContextPreview {
EditScreen(
snackbarHostState = SnackbarHostState(),
dropBehaviourFactory = FakeDropImageFactory(),
isExpanded = true, // Expanded mode
onCameraPressed = {},
onBackPressed = {},
Expand Down
Loading