Camera X orientation memory leak
CameraActivity causes a memory leak when using CameraX with Jetpack Compose. LeakCanary reports that the ProcessCameraProvider retains a reference to the destroyed CameraActivity through the LifecycleCameraProviderImpl and its internal lifecycleCameraKeys map. Even after calling unbindAll() in onDestroy(), the activity is not properly garbage collected, leading to a memory leak.
` package com.example.cameraxapp
import android.Manifest import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.CameraController import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.example.cameraxapp.ui.theme.CameraXAppTheme
class MainActivity : ComponentActivity() { lateinit var controller : LifecycleCameraController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
if (!hasRequiredPermission()) {
ActivityCompat.requestPermissions(
this,
CAMERAX_PERMISSIONS,
0
)
}
controller = LifecycleCameraController(applicationContext).apply {
setEnabledUseCases(CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE)
}
setContent {
CameraXAppTheme {
// The main screen content which now manages its own controller.
MainScreenContent(controller = controller)
}
}
}
/**
* Checks if all required permissions for CameraX are granted.
*/
private fun hasRequiredPermission(): Boolean {
return CAMERAX_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
applicationContext,
it
) == PackageManager.PERMISSION_GRANTED
}
}
override fun onDestroy() {
controller.unbind()
ProcessCameraProvider.getInstance(this).get().unbindAll()
super.onDestroy()
}
companion object {
// Define the required permissions for the camera.
private val CAMERAX_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA
)
}
}
@Composable fun MainScreenContent( controller: LifecycleCameraController ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current
// Remember controller once per composition
// Clean up when composable leaves composition
DisposableEffect(controller) {
onDispose {
controller.unbind()
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CameraPreview(controller, lifecycleOwner, Modifier.fillMaxSize())
Text("Camera Preview", Modifier.align(Alignment.BottomCenter))
}
}
@Composable fun CameraPreview( controller: LifecycleCameraController, lifecycleOwner: androidx.lifecycle.LifecycleOwner, modifier: Modifier = Modifier ) { AndroidView( factory = { ctx -> PreviewView(ctx).apply { this.controller = controller controller.bindToLifecycle(lifecycleOwner) } }, modifier = modifier, onRelease = { controller.unbind() } ) }`