diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ef555f0225..19415e87ca 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -38,6 +38,7 @@ compileSdk = "34"
minSdk = "21"
spotless = "7.0.4"
gummyBears = "0.12.0"
+camerax = "1.3.0"
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
@@ -200,6 +201,13 @@ androidx-test-runner = { module = "androidx.test:runner", version = "1.6.2" }
awaitility-kotlin = { module = "org.awaitility:awaitility-kotlin", version = "4.1.1" }
awaitility-kotlin-spring7 = { module = "org.awaitility:awaitility-kotlin", version = "4.3.0" }
awaitility3-kotlin = { module = "org.awaitility:awaitility-kotlin", version = "3.1.6" }
+
+# CameraX dependencies
+camerax-core = { module = "androidx.camera:camera-core", version.ref = "camerax" }
+camerax-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
+camerax-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
+camerax-view = { module = "androidx.camera:camera-view", version.ref = "camerax" }
+
hsqldb = { module = "org.hsqldb:hsqldb", version = "2.6.1" }
javafaker = { module = "com.github.javafaker:javafaker", version = "1.0.2" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts
index acec6ac809..48ac6dda5c 100644
--- a/sentry-samples/sentry-samples-android/build.gradle.kts
+++ b/sentry-samples/sentry-samples-android/build.gradle.kts
@@ -137,6 +137,7 @@ dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.foundation.layout)
@@ -149,6 +150,10 @@ dependencies {
implementation(libs.retrofit.gson)
implementation(libs.sentry.native.ndk)
implementation(libs.timber)
+ implementation(libs.camerax.core)
+ implementation(libs.camerax.camera2)
+ implementation(libs.camerax.lifecycle)
+ implementation(libs.camerax.view)
debugImplementation(projects.sentryAndroidDistribution)
debugImplementation(libs.leakcanary)
diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
index 65445d8570..c6360ca911 100644
--- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
+++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
@@ -75,6 +75,10 @@
+
+
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/CameraXActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/CameraXActivity.java
new file mode 100644
index 0000000000..b8c0491459
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/CameraXActivity.java
@@ -0,0 +1,166 @@
+package io.sentry.samples.android;
+
+import android.Manifest;
+import android.content.ContentValues;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.Camera;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.view.PreviewView;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import com.google.common.util.concurrent.ListenableFuture;
+import io.sentry.Sentry;
+import io.sentry.samples.android.databinding.ActivityCameraxBinding;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+
+public class CameraXActivity extends AppCompatActivity {
+ private static final String TAG = "CameraXActivity";
+ private static final int CAMERA_PERMISSION_REQUEST_CODE = 1001;
+
+ private ActivityCameraxBinding binding;
+ private PreviewView previewView;
+ private ListenableFuture cameraProviderFuture;
+ private ImageCapture imageCapture;
+ private Camera camera;
+ private CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityCameraxBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ previewView = binding.previewView;
+
+ if (allPermissionsGranted()) {
+ startCamera();
+ } else {
+ ActivityCompat.requestPermissions(
+ this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
+ }
+
+ binding.captureButton.setOnClickListener(view -> takePhoto());
+ binding.switchCameraButton.setOnClickListener(view -> switchCamera());
+ binding.backButton.setOnClickListener(view -> finish());
+ }
+
+ private void startCamera() {
+ cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(
+ () -> {
+ try {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ bindPreview(cameraProvider);
+ } catch (ExecutionException | InterruptedException e) {
+ Log.e(TAG, "Error starting camera", e);
+ Sentry.captureException(e);
+ }
+ },
+ ContextCompat.getMainExecutor(this));
+ }
+
+ private void bindPreview(ProcessCameraProvider cameraProvider) {
+ Preview preview = new Preview.Builder().build();
+ imageCapture =
+ new ImageCapture.Builder()
+ .setTargetRotation(previewView.getDisplay().getRotation())
+ .build();
+
+ preview.setSurfaceProvider(previewView.getSurfaceProvider());
+
+ cameraProvider.unbindAll();
+ camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture);
+ }
+
+ private void takePhoto() {
+ if (imageCapture == null) return;
+
+ String timeStamp =
+ new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
+ String fileName = "CameraX_" + timeStamp + ".jpg";
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
+ contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Images");
+
+ ImageCapture.OutputFileOptions outputFileOptions =
+ new ImageCapture.OutputFileOptions.Builder(
+ getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+ .build();
+
+ imageCapture.takePicture(
+ outputFileOptions,
+ ContextCompat.getMainExecutor(this),
+ new ImageCapture.OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
+ String msg = "Photo saved successfully: " + outputFileResults.getSavedUri();
+ Toast.makeText(CameraXActivity.this, "Photo saved!", Toast.LENGTH_SHORT).show();
+ Log.d(TAG, msg);
+ }
+
+ @Override
+ public void onError(@NonNull ImageCaptureException exception) {
+ Log.e(TAG, "Photo capture failed", exception);
+ Toast.makeText(CameraXActivity.this, "Photo capture failed", Toast.LENGTH_SHORT).show();
+ Sentry.captureException(exception);
+ }
+ });
+ }
+
+ private void switchCamera() {
+ cameraSelector =
+ (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA)
+ ? CameraSelector.DEFAULT_FRONT_CAMERA
+ : CameraSelector.DEFAULT_BACK_CAMERA;
+
+ try {
+ if (cameraProviderFuture != null) {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ bindPreview(cameraProvider);
+ }
+ } catch (ExecutionException | InterruptedException e) {
+ Log.e(TAG, "Error switching camera", e);
+ Sentry.captureException(e);
+ }
+ }
+
+ private boolean allPermissionsGranted() {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
+ if (allPermissionsGranted()) {
+ startCamera();
+ } else {
+ Toast.makeText(this, "Camera permission is required", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ binding = null;
+ }
+}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java
index 25907655f7..62280ecd76 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java
@@ -341,6 +341,11 @@ public void run() {
});
});
+ binding.openCameraActivity.setOnClickListener(
+ view -> {
+ startActivity(new Intent(this, CameraXActivity.class));
+ });
+
Sentry.logger().log(SentryLogLevel.INFO, "Creating content view");
setContentView(binding.getRoot());
diff --git a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_camerax.xml b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_camerax.xml
new file mode 100644
index 0000000000..88c85e7caf
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_camerax.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml
index 0083fae8f9..bccb56ca83 100644
--- a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml
+++ b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml
@@ -176,6 +176,13 @@
android:layout_height="wrap_content"
android:text="@string/check_for_update"/>
+
+
diff --git a/sentry-samples/sentry-samples-android/src/main/res/values/strings.xml b/sentry-samples/sentry-samples-android/src/main/res/values/strings.xml
index fe984bbff1..ebe164d3d1 100644
--- a/sentry-samples/sentry-samples-android/src/main/res/values/strings.xml
+++ b/sentry-samples/sentry-samples-android/src/main/res/values/strings.xml
@@ -31,6 +31,8 @@
Show Dialog
Check for Update
Back to Main Activity
+ Back
+ Switch Preview
text
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nibh lorem, venenatis sed nulla vel, venenatis sodales augue. Mauris varius elit eu ligula volutpat, sed tincidunt orci porttitor. Donec et dignissim lacus, sed luctus ipsum. Praesent ornare luctus tortor sit amet ultricies. Cras iaculis et diam et vulputate. Cras ut iaculis mauris, non pellentesque diam. Nunc in laoreet diam, vitae accumsan eros. Morbi non nunc ac eros molestie placerat vitae id dolor. Quisque ornare aliquam ipsum, a dapibus tortor. In eu sodales tellus.
@@ -50,4 +52,5 @@ Nulla interdum gravida augue, vel fringilla lorem bibendum vel. In hac habitasse
No profiling dir path set
Start Profiling
Profile trace file size = %d bytes \nItem payload size = %d bytes \nData sent to Sentry size = %d bytes
+ Open Camera Activity