diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4908d31..a7d15c7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,5 +1,5 @@ --- -name: Bug report +name: 🐛 Bug report about: Create a report to help us improve title: '' labels: '' @@ -7,16 +7,16 @@ assignees: '' --- -**Describe the bug** +**Describe the bug:** A clear and concise description of what the bug is. -**To Reproduce** +**To Reproduce:** Steps to reproduce the behavior: -**Expected behavior** +**Expected behavior:** A clear and concise description of what you expected to happen. -**Screenshots** +**Screenshots:** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** @@ -30,5 +30,5 @@ If applicable, add screenshots to help explain your problem. - Browser [e.g. stock browser, safari] - Version [e.g. 22] -**Additional context** +**Additional context:** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7..ae26d6f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature request +name: ✨ Feature request about: Suggest an idea for this project title: '' labels: '' @@ -7,14 +7,14 @@ assignees: '' --- -**Is your feature request related to a problem? Please describe.** +**Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -**Describe the solution you'd like** +**Describe the solution you'd like** A clear and concise description of what you want to happen. -**Describe alternatives you've considered** +**Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. -**Additional context** +**Additional context** Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 0000000..0119f92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,46 @@ +--- +name: 🆘 Support Request +about: Ask a question or get help with usage. +title: "[Support]: " +labels: ["support", "question"] +assignees: [] +--- + + + +**What do you need help with?** + +Is this a question about how to do something, a configuration problem, or a general issue you can't solve? + +**Describe the issue/question:** + +Clearly describe what you are trying to achieve, what problem you are facing, or what question you have. + +**What have you tried so far? (Optional):** + +List any steps you've already taken to troubleshoot, find information, or attempt a solution. + +**Expected outcome (Optional):** + +If applicable, what did you hope would happen, or what solution are you looking for? + +**Screenshots/Videos (Optional):** + +If applicable, add screenshots or a short video that might help explain your situation. + +**Environment & Details:** + +Please provide details about your operating environment, relevant URLs, or any messages you see. + +- **Operating System:** +- **Browser & Version (if applicable):** +- **Any relevant messages (e.g., from UI, console):** + ``` + PASTE_ANY_MESSAGES_HERE + ``` + +**Any additional context?:** + +Is there anything else that might be useful for us to know? diff --git a/Android/src/app/build.gradle.kts b/Android/src/app/build.gradle.kts index 7bf6c1e..4f8d298 100644 --- a/Android/src/app/build.gradle.kts +++ b/Android/src/app/build.gradle.kts @@ -31,7 +31,7 @@ android { minSdk = 26 targetSdk = 36 versionCode = 1 - versionName = "1.0.2" + versionName = "1.0.3" // Needed for HuggingFace auth workflows. manifestPlaceholders["appAuthRedirectScheme"] = "com.google.ai.edge.gallery.oauth" diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageInputText.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageInputText.kt index 73b1713..49afffd 100644 --- a/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageInputText.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageInputText.kt @@ -23,6 +23,8 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import android.net.Uri +import android.util.Log +import android.util.Size import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -31,6 +33,9 @@ import androidx.camera.core.CameraControl import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture import androidx.camera.core.ImageProxy +import androidx.camera.core.resolutionselector.AspectRatioStrategy +import androidx.camera.core.resolutionselector.ResolutionSelector +import androidx.camera.core.resolutionselector.ResolutionStrategy import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.awaitInstance import androidx.camera.view.PreviewView @@ -75,9 +80,11 @@ import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -104,6 +111,8 @@ import com.google.ai.edge.gallery.ui.theme.GalleryTheme import kotlinx.coroutines.launch import java.util.concurrent.Executors +private const val TAG = "AGMessageInputText" + /** * Composable function to display a text input field for composing chat messages. * @@ -135,7 +144,7 @@ fun MessageInputText( var showAddContentMenu by remember { mutableStateOf(false) } var showTextInputHistorySheet by remember { mutableStateOf(false) } var showCameraCaptureBottomSheet by remember { mutableStateOf(false) } - var cameraCaptureSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val cameraCaptureSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var tempPhotoUri by remember { mutableStateOf(value = Uri.EMPTY) } var pickedImages by remember { mutableStateOf>(listOf()) } val updatePickedImages: (Bitmap) -> Unit = { bitmap -> @@ -150,21 +159,6 @@ fun MessageInputText( checkFrontCamera(context = context, callback = { hasFrontCamera = it }) } - // launches camera - val cameraLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { isImageSaved -> - if (isImageSaved) { - handleImageSelected( - context = context, - uri = tempPhotoUri, - onImageSelected = { bitmap -> - updatePickedImages(bitmap) - }, - rotateForPortrait = true, - ) - } - } - // Permission request when taking picture. val takePicturePermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() @@ -173,7 +167,6 @@ fun MessageInputText( showAddContentMenu = false tempPhotoUri = context.createTempPictureUri() showCameraCaptureBottomSheet = true -// cameraLauncher.launch(tempPhotoUri) } } @@ -270,7 +263,6 @@ fun MessageInputText( showAddContentMenu = false tempPhotoUri = context.createTempPictureUri() showCameraCaptureBottomSheet = true -// cameraLauncher.launch(tempPhotoUri) } // Otherwise, ask for permission @@ -416,28 +408,43 @@ fun MessageInputText( val lifecycleOwner = LocalLifecycleOwner.current val previewUseCase = remember { androidx.camera.core.Preview.Builder().build() } - val imageCaptureUseCase = remember { ImageCapture.Builder().build() } + val imageCaptureUseCase = remember { + // Try to limit the image size. + val preferredSize = Size(512, 512) + val resolutionStrategy = ResolutionStrategy( + preferredSize, + ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER + ) + val resolutionSelector = ResolutionSelector.Builder() + .setResolutionStrategy(resolutionStrategy) + .setAspectRatioStrategy(AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY) + .build() + + ImageCapture.Builder().setResolutionSelector(resolutionSelector).build() + } var cameraProvider by remember { mutableStateOf(null) } var cameraControl by remember { mutableStateOf(null) } val localContext = LocalContext.current - var cameraSide by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) } - + var cameraSide by remember { mutableIntStateOf(CameraSelector.LENS_FACING_BACK) } val executor = remember { Executors.newSingleThreadExecutor() } - val capturedImageUri = remember { mutableStateOf(null) } fun rebindCameraProvider() { cameraProvider?.let { cameraProvider -> val cameraSelector = CameraSelector.Builder() .requireLensFacing(cameraSide) .build() - cameraProvider.unbindAll() - val camera = cameraProvider.bindToLifecycle( - lifecycleOwner = lifecycleOwner, - cameraSelector = cameraSelector, - previewUseCase, - imageCaptureUseCase - ) - cameraControl = camera.cameraControl + try { + cameraProvider.unbindAll() + val camera = cameraProvider.bindToLifecycle( + lifecycleOwner = lifecycleOwner, + cameraSelector = cameraSelector, + previewUseCase, + imageCaptureUseCase + ) + cameraControl = camera.cameraControl + } catch (e: Exception) { + Log.d(TAG, "Failed to bind camera", e) + } } } @@ -450,31 +457,25 @@ fun MessageInputText( rebindCameraProvider() } -// val cameraController = remember { -// LifecycleCameraController(context).apply { -// bindToLifecycle(lifecycleOwner) -// } -// } + DisposableEffect(Unit) { // Or key on lifecycleOwner if it makes more sense + onDispose { + cameraProvider?.unbindAll() // Unbind all use cases from the camera provider + if (!executor.isShutdown) { + executor.shutdown() // Shut down the executor service + } + } + } Box(modifier = Modifier.fillMaxSize()) { // PreviewView for the camera feed. AndroidView( modifier = Modifier.fillMaxSize(), factory = { ctx -> - PreviewView(context).also { + PreviewView(ctx).also { previewUseCase.surfaceProvider = it.surfaceProvider rebindCameraProvider() } -// PreviewView(ctx).apply { -// scaleType = PreviewView.ScaleType.FILL_START -// implementationMode = PreviewView.ImplementationMode.COMPATIBLE -// controller = cameraController // Attach the lifecycle-aware camera controller. -// } }, -// onRelease = { -// // Called when the PreviewView is removed from the composable hierarchy -// cameraController.unbind() // Unbinds the camera to free up resources -// } ) // Close button. @@ -508,25 +509,31 @@ fun MessageInputText( .size(64.dp) .border(2.dp, MaterialTheme.colorScheme.onPrimary, CircleShape), onClick = { - scope.launch { - val callback = object : ImageCapture.OnImageCapturedCallback() { - override fun onCaptureSuccess(image: ImageProxy) { + val callback = object : ImageCapture.OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + try { var bitmap = image.toBitmap() val rotation = image.imageInfo.rotationDegrees bitmap = if (rotation != 0) { val matrix = Matrix().apply { postRotate(rotation.toFloat()) } + Log.d(TAG, "image size: ${bitmap.width}, ${bitmap.height}") Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) } else bitmap updatePickedImages(bitmap) + } catch (e: Exception) { + Log.e(TAG, "Failed to process image", e) + } finally { image.close() + scope.launch { + cameraCaptureSheetState.hide() + showCameraCaptureBottomSheet = false + } } } - imageCaptureUseCase.takePicture(executor, callback) - cameraCaptureSheetState.hide() - showCameraCaptureBottomSheet = false } + imageCaptureUseCase.takePicture(executor, callback) }, ) { Icon( @@ -601,7 +608,7 @@ private fun rotateImageIfNecessary(bitmap: Bitmap, rotateForPortrait: Boolean = private fun checkFrontCamera(context: Context, callback: (Boolean) -> Unit) { val cameraProviderFuture = ProcessCameraProvider.getInstance(context) - cameraProviderFuture.addListener(Runnable { + cameraProviderFuture.addListener({ val cameraProvider = cameraProviderFuture.get() try { // Attempt to select the default front camera