Skip to content

Commit 61ee4fd

Browse files
- Fix some potential memory leak in image capture sheet.
1 parent 707d5f0 commit 61ee4fd

File tree

2 files changed

+40
-51
lines changed

2 files changed

+40
-51
lines changed

Android/src/app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ android {
3131
minSdk = 26
3232
targetSdk = 35
3333
versionCode = 1
34-
versionName = "1.0.2"
34+
versionName = "1.0.3"
3535

3636
// Needed for HuggingFace auth workflows.
3737
manifestPlaceholders["appAuthRedirectScheme"] = "com.google.ai.edge.gallery.oauth"

Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/common/chat/MessageInputText.kt

Lines changed: 39 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import android.graphics.Bitmap
2323
import android.graphics.BitmapFactory
2424
import android.graphics.Matrix
2525
import android.net.Uri
26+
import android.util.Log
2627
import androidx.activity.compose.rememberLauncherForActivityResult
2728
import androidx.activity.result.PickVisualMediaRequest
2829
import androidx.activity.result.contract.ActivityResultContracts
@@ -75,9 +76,11 @@ import androidx.compose.material3.TextField
7576
import androidx.compose.material3.TextFieldDefaults
7677
import androidx.compose.material3.rememberModalBottomSheetState
7778
import androidx.compose.runtime.Composable
79+
import androidx.compose.runtime.DisposableEffect
7880
import androidx.compose.runtime.LaunchedEffect
7981
import androidx.compose.runtime.collectAsState
8082
import androidx.compose.runtime.getValue
83+
import androidx.compose.runtime.mutableIntStateOf
8184
import androidx.compose.runtime.mutableStateOf
8285
import androidx.compose.runtime.remember
8386
import androidx.compose.runtime.rememberCoroutineScope
@@ -104,6 +107,8 @@ import com.google.ai.edge.gallery.ui.theme.GalleryTheme
104107
import kotlinx.coroutines.launch
105108
import java.util.concurrent.Executors
106109

110+
private const val TAG = "AGMessageInputText"
111+
107112
/**
108113
* Composable function to display a text input field for composing chat messages.
109114
*
@@ -135,7 +140,7 @@ fun MessageInputText(
135140
var showAddContentMenu by remember { mutableStateOf(false) }
136141
var showTextInputHistorySheet by remember { mutableStateOf(false) }
137142
var showCameraCaptureBottomSheet by remember { mutableStateOf(false) }
138-
var cameraCaptureSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
143+
val cameraCaptureSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
139144
var tempPhotoUri by remember { mutableStateOf(value = Uri.EMPTY) }
140145
var pickedImages by remember { mutableStateOf<List<Bitmap>>(listOf()) }
141146
val updatePickedImages: (Bitmap) -> Unit = { bitmap ->
@@ -150,21 +155,6 @@ fun MessageInputText(
150155
checkFrontCamera(context = context, callback = { hasFrontCamera = it })
151156
}
152157

153-
// launches camera
154-
val cameraLauncher =
155-
rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { isImageSaved ->
156-
if (isImageSaved) {
157-
handleImageSelected(
158-
context = context,
159-
uri = tempPhotoUri,
160-
onImageSelected = { bitmap ->
161-
updatePickedImages(bitmap)
162-
},
163-
rotateForPortrait = true,
164-
)
165-
}
166-
}
167-
168158
// Permission request when taking picture.
169159
val takePicturePermissionLauncher = rememberLauncherForActivityResult(
170160
ActivityResultContracts.RequestPermission()
@@ -173,7 +163,6 @@ fun MessageInputText(
173163
showAddContentMenu = false
174164
tempPhotoUri = context.createTempPictureUri()
175165
showCameraCaptureBottomSheet = true
176-
// cameraLauncher.launch(tempPhotoUri)
177166
}
178167
}
179168

@@ -270,7 +259,6 @@ fun MessageInputText(
270259
showAddContentMenu = false
271260
tempPhotoUri = context.createTempPictureUri()
272261
showCameraCaptureBottomSheet = true
273-
// cameraLauncher.launch(tempPhotoUri)
274262
}
275263

276264
// Otherwise, ask for permission
@@ -420,24 +408,26 @@ fun MessageInputText(
420408
var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) }
421409
var cameraControl by remember { mutableStateOf<CameraControl?>(null) }
422410
val localContext = LocalContext.current
423-
var cameraSide by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) }
424-
411+
var cameraSide by remember { mutableIntStateOf(CameraSelector.LENS_FACING_BACK) }
425412
val executor = remember { Executors.newSingleThreadExecutor() }
426-
val capturedImageUri = remember { mutableStateOf<Uri?>(null) }
427413

428414
fun rebindCameraProvider() {
429415
cameraProvider?.let { cameraProvider ->
430416
val cameraSelector = CameraSelector.Builder()
431417
.requireLensFacing(cameraSide)
432418
.build()
433-
cameraProvider.unbindAll()
434-
val camera = cameraProvider.bindToLifecycle(
435-
lifecycleOwner = lifecycleOwner,
436-
cameraSelector = cameraSelector,
437-
previewUseCase,
438-
imageCaptureUseCase
439-
)
440-
cameraControl = camera.cameraControl
419+
try {
420+
cameraProvider.unbindAll()
421+
val camera = cameraProvider.bindToLifecycle(
422+
lifecycleOwner = lifecycleOwner,
423+
cameraSelector = cameraSelector,
424+
previewUseCase,
425+
imageCaptureUseCase
426+
)
427+
cameraControl = camera.cameraControl
428+
} catch (e: Exception) {
429+
Log.d(TAG, "Failed to bind camera", e)
430+
}
441431
}
442432
}
443433

@@ -450,31 +440,25 @@ fun MessageInputText(
450440
rebindCameraProvider()
451441
}
452442

453-
// val cameraController = remember {
454-
// LifecycleCameraController(context).apply {
455-
// bindToLifecycle(lifecycleOwner)
456-
// }
457-
// }
443+
DisposableEffect(Unit) { // Or key on lifecycleOwner if it makes more sense
444+
onDispose {
445+
cameraProvider?.unbindAll() // Unbind all use cases from the camera provider
446+
if (!executor.isShutdown) {
447+
executor.shutdown() // Shut down the executor service
448+
}
449+
}
450+
}
458451

459452
Box(modifier = Modifier.fillMaxSize()) {
460453
// PreviewView for the camera feed.
461454
AndroidView(
462455
modifier = Modifier.fillMaxSize(),
463456
factory = { ctx ->
464-
PreviewView(context).also {
457+
PreviewView(ctx).also {
465458
previewUseCase.surfaceProvider = it.surfaceProvider
466459
rebindCameraProvider()
467460
}
468-
// PreviewView(ctx).apply {
469-
// scaleType = PreviewView.ScaleType.FILL_START
470-
// implementationMode = PreviewView.ImplementationMode.COMPATIBLE
471-
// controller = cameraController // Attach the lifecycle-aware camera controller.
472-
// }
473461
},
474-
// onRelease = {
475-
// // Called when the PreviewView is removed from the composable hierarchy
476-
// cameraController.unbind() // Unbinds the camera to free up resources
477-
// }
478462
)
479463

480464
// Close button.
@@ -508,9 +492,9 @@ fun MessageInputText(
508492
.size(64.dp)
509493
.border(2.dp, MaterialTheme.colorScheme.onPrimary, CircleShape),
510494
onClick = {
511-
scope.launch {
512-
val callback = object : ImageCapture.OnImageCapturedCallback() {
513-
override fun onCaptureSuccess(image: ImageProxy) {
495+
val callback = object : ImageCapture.OnImageCapturedCallback() {
496+
override fun onCaptureSuccess(image: ImageProxy) {
497+
try {
514498
var bitmap = image.toBitmap()
515499
val rotation = image.imageInfo.rotationDegrees
516500
bitmap = if (rotation != 0) {
@@ -520,13 +504,18 @@ fun MessageInputText(
520504
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
521505
} else bitmap
522506
updatePickedImages(bitmap)
507+
} catch (e: Exception) {
508+
Log.e(TAG, "Failed to process image", e)
509+
} finally {
523510
image.close()
511+
scope.launch {
512+
cameraCaptureSheetState.hide()
513+
showCameraCaptureBottomSheet = false
514+
}
524515
}
525516
}
526-
imageCaptureUseCase.takePicture(executor, callback)
527-
cameraCaptureSheetState.hide()
528-
showCameraCaptureBottomSheet = false
529517
}
518+
imageCaptureUseCase.takePicture(executor, callback)
530519
},
531520
) {
532521
Icon(

0 commit comments

Comments
 (0)