From d3f8605ab788b72c91fd52a72c742135695bdb48 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 2 Oct 2025 10:28:38 +0200 Subject: [PATCH 1/4] chore(samples): Add CameraX screen --- gradle/libs.versions.toml | 8 + .../sentry-samples-android/build.gradle.kts | 6 + .../src/main/AndroidManifest.xml | 6 +- .../samples/android/CameraXActivity.java | 164 ++++++++++++++++++ .../sentry/samples/android/MainActivity.java | 15 +- .../src/main/res/layout/activity_camerax.xml | 53 ++++++ .../src/main/res/layout/activity_main.xml | 7 + .../src/main/res/values/strings.xml | 3 + 8 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/CameraXActivity.java create mode 100644 sentry-samples/sentry-samples-android/src/main/res/layout/activity_camerax.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 60c163373a7..e54321511a9 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 56f270d235b..4ddc793845b 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) @@ -150,6 +151,11 @@ dependencies { 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(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 9d084ed97e1..03f91c93f78 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -75,7 +75,11 @@ - + + + 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 00000000000..df1d3293a30 --- /dev/null +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/CameraXActivity.java @@ -0,0 +1,164 @@ +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 { + 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 1b9acc3c267..4d5a33e74a1 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 @@ -1,10 +1,12 @@ package io.sentry.samples.android; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.os.Bundle; import android.os.Handler; +import android.provider.MediaStore; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import io.sentry.Attachment; @@ -67,7 +69,13 @@ protected void onCreate(Bundle savedInstanceState) { binding.crashFromJava.setOnClickListener( view -> { - throw new RuntimeException("Uncaught Exception from Java."); + // throw new RuntimeException("Uncaught Exception from Java."); + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + try { + startActivityForResult(takePictureIntent, 1); + } catch (ActivityNotFoundException e) { + // display error state to the user + } }); binding.sendMessage.setOnClickListener(view -> Sentry.captureMessage("Some message.")); @@ -304,6 +312,11 @@ public void run() { Sentry.replay().enableDebugMaskingOverlay(); }); + binding.openCameraActivity.setOnClickListener( + view -> { + startActivity(new Intent(this, CameraXActivity.class)); + }); + 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 00000000000..88c85e7caf1 --- /dev/null +++ b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_camerax.xml @@ -0,0 +1,53 @@ + + + + + +