Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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" }
Expand Down
5 changes: 5 additions & 0 deletions sentry-samples/sentry-samples-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
<activity android:name=".FrameDataForSpansActivity"
android:exported="false"/>

<activity
android:name=".CameraXActivity"
android:exported="false" />

<!-- NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard-->
<meta-data android:name="io.sentry.dsn" android:value="https://[email protected]/5428559" />

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ProcessCameraProvider> 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);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Camera Initialization Timing Issue

The switchCamera() method attempts to access cameraProviderFuture directly. This can lead to a NullPointerException if called before camera permissions are granted and cameraProviderFuture is initialized. Additionally, calling cameraProviderFuture.get() synchronously on the main thread is a blocking operation that may cause ANRs.

Fix in Cursor Fix in Web


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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CameraXActivity"
>

<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>

<Button
android:id="@+id/captureButton"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginBottom="32dp"
android:background="@android:drawable/ic_menu_camera"
android:text=""
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>

<Button
android:id="@+id/switchCameraButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="32dp"
android:text="@string/switch_preview"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>

<Button
android:id="@+id/backButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>

</androidx.constraintlayout.widget.ConstraintLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@
android:layout_height="wrap_content"
android:text="@string/check_for_update"/>

<Button
android:id="@+id/open_camera_activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/open_camera_activity"
/>

</LinearLayout>

</ScrollView>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
<string name="show_dialog">Show Dialog</string>
<string name="check_for_update">Check for Update</string>
<string name="back_main">Back to Main Activity</string>
<string name="back">Back</string>
<string name="switch_preview">Switch Preview</string>
<string name="tap_me">text</string>
<string name="lipsum">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.

Expand All @@ -50,4 +52,5 @@ Nulla interdum gravida augue, vel fringilla lorem bibendum vel. In hac habitasse
<string name="profiling_no_dir_set">No profiling dir path set</string>
<string name="profiling_start">Start Profiling</string>
<string name="profiling_result">Profile trace file size = %d bytes \nItem payload size = %d bytes \nData sent to Sentry size = %d bytes</string>
<string name="open_camera_activity">Open Camera Activity</string>
</resources>
Loading