Skip to content

Commit 2dc2ca8

Browse files
authored
chore(samples): Add CameraX screen (#4774)
* chore(samples): Add CameraX screen * Revert crash button * Formatting * Add null check
1 parent bdbe1f4 commit 2dc2ca8

File tree

8 files changed

+251
-0
lines changed

8 files changed

+251
-0
lines changed

gradle/libs.versions.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ compileSdk = "34"
3838
minSdk = "21"
3939
spotless = "7.0.4"
4040
gummyBears = "0.12.0"
41+
camerax = "1.3.0"
4142

4243
[plugins]
4344
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" }
200201
awaitility-kotlin = { module = "org.awaitility:awaitility-kotlin", version = "4.1.1" }
201202
awaitility-kotlin-spring7 = { module = "org.awaitility:awaitility-kotlin", version = "4.3.0" }
202203
awaitility3-kotlin = { module = "org.awaitility:awaitility-kotlin", version = "3.1.6" }
204+
205+
# CameraX dependencies
206+
camerax-core = { module = "androidx.camera:camera-core", version.ref = "camerax" }
207+
camerax-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
208+
camerax-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
209+
camerax-view = { module = "androidx.camera:camera-view", version.ref = "camerax" }
210+
203211
hsqldb = { module = "org.hsqldb:hsqldb", version = "2.6.1" }
204212
javafaker = { module = "com.github.javafaker:javafaker", version = "1.0.2" }
205213
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }

sentry-samples/sentry-samples-android/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ dependencies {
137137

138138
implementation(libs.androidx.activity.compose)
139139
implementation(libs.androidx.appcompat)
140+
implementation(libs.androidx.constraintlayout)
140141
implementation(libs.androidx.fragment.ktx)
141142
implementation(libs.androidx.compose.foundation)
142143
implementation(libs.androidx.compose.foundation.layout)
@@ -149,6 +150,10 @@ dependencies {
149150
implementation(libs.retrofit.gson)
150151
implementation(libs.sentry.native.ndk)
151152
implementation(libs.timber)
153+
implementation(libs.camerax.core)
154+
implementation(libs.camerax.camera2)
155+
implementation(libs.camerax.lifecycle)
156+
implementation(libs.camerax.view)
152157

153158
debugImplementation(projects.sentryAndroidDistribution)
154159
debugImplementation(libs.leakcanary)

sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
<activity android:name=".FrameDataForSpansActivity"
7676
android:exported="false"/>
7777

78+
<activity
79+
android:name=".CameraXActivity"
80+
android:exported="false" />
81+
7882
<!-- NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard-->
7983
<meta-data android:name="io.sentry.dsn" android:value="https://[email protected]/5428559" />
8084

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package io.sentry.samples.android;
2+
3+
import android.Manifest;
4+
import android.content.ContentValues;
5+
import android.content.pm.PackageManager;
6+
import android.os.Bundle;
7+
import android.provider.MediaStore;
8+
import android.util.Log;
9+
import android.widget.Toast;
10+
import androidx.annotation.NonNull;
11+
import androidx.appcompat.app.AppCompatActivity;
12+
import androidx.camera.core.Camera;
13+
import androidx.camera.core.CameraSelector;
14+
import androidx.camera.core.ImageCapture;
15+
import androidx.camera.core.ImageCaptureException;
16+
import androidx.camera.core.Preview;
17+
import androidx.camera.lifecycle.ProcessCameraProvider;
18+
import androidx.camera.view.PreviewView;
19+
import androidx.core.app.ActivityCompat;
20+
import androidx.core.content.ContextCompat;
21+
import com.google.common.util.concurrent.ListenableFuture;
22+
import io.sentry.Sentry;
23+
import io.sentry.samples.android.databinding.ActivityCameraxBinding;
24+
import java.text.SimpleDateFormat;
25+
import java.util.Date;
26+
import java.util.Locale;
27+
import java.util.concurrent.ExecutionException;
28+
29+
public class CameraXActivity extends AppCompatActivity {
30+
private static final String TAG = "CameraXActivity";
31+
private static final int CAMERA_PERMISSION_REQUEST_CODE = 1001;
32+
33+
private ActivityCameraxBinding binding;
34+
private PreviewView previewView;
35+
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
36+
private ImageCapture imageCapture;
37+
private Camera camera;
38+
private CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
39+
40+
@Override
41+
protected void onCreate(Bundle savedInstanceState) {
42+
super.onCreate(savedInstanceState);
43+
binding = ActivityCameraxBinding.inflate(getLayoutInflater());
44+
setContentView(binding.getRoot());
45+
46+
previewView = binding.previewView;
47+
48+
if (allPermissionsGranted()) {
49+
startCamera();
50+
} else {
51+
ActivityCompat.requestPermissions(
52+
this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
53+
}
54+
55+
binding.captureButton.setOnClickListener(view -> takePhoto());
56+
binding.switchCameraButton.setOnClickListener(view -> switchCamera());
57+
binding.backButton.setOnClickListener(view -> finish());
58+
}
59+
60+
private void startCamera() {
61+
cameraProviderFuture = ProcessCameraProvider.getInstance(this);
62+
cameraProviderFuture.addListener(
63+
() -> {
64+
try {
65+
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
66+
bindPreview(cameraProvider);
67+
} catch (ExecutionException | InterruptedException e) {
68+
Log.e(TAG, "Error starting camera", e);
69+
Sentry.captureException(e);
70+
}
71+
},
72+
ContextCompat.getMainExecutor(this));
73+
}
74+
75+
private void bindPreview(ProcessCameraProvider cameraProvider) {
76+
Preview preview = new Preview.Builder().build();
77+
imageCapture =
78+
new ImageCapture.Builder()
79+
.setTargetRotation(previewView.getDisplay().getRotation())
80+
.build();
81+
82+
preview.setSurfaceProvider(previewView.getSurfaceProvider());
83+
84+
cameraProvider.unbindAll();
85+
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture);
86+
}
87+
88+
private void takePhoto() {
89+
if (imageCapture == null) return;
90+
91+
String timeStamp =
92+
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
93+
String fileName = "CameraX_" + timeStamp + ".jpg";
94+
95+
ContentValues contentValues = new ContentValues();
96+
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
97+
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
98+
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Images");
99+
100+
ImageCapture.OutputFileOptions outputFileOptions =
101+
new ImageCapture.OutputFileOptions.Builder(
102+
getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
103+
.build();
104+
105+
imageCapture.takePicture(
106+
outputFileOptions,
107+
ContextCompat.getMainExecutor(this),
108+
new ImageCapture.OnImageSavedCallback() {
109+
@Override
110+
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
111+
String msg = "Photo saved successfully: " + outputFileResults.getSavedUri();
112+
Toast.makeText(CameraXActivity.this, "Photo saved!", Toast.LENGTH_SHORT).show();
113+
Log.d(TAG, msg);
114+
}
115+
116+
@Override
117+
public void onError(@NonNull ImageCaptureException exception) {
118+
Log.e(TAG, "Photo capture failed", exception);
119+
Toast.makeText(CameraXActivity.this, "Photo capture failed", Toast.LENGTH_SHORT).show();
120+
Sentry.captureException(exception);
121+
}
122+
});
123+
}
124+
125+
private void switchCamera() {
126+
cameraSelector =
127+
(cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA)
128+
? CameraSelector.DEFAULT_FRONT_CAMERA
129+
: CameraSelector.DEFAULT_BACK_CAMERA;
130+
131+
try {
132+
if (cameraProviderFuture != null) {
133+
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
134+
bindPreview(cameraProvider);
135+
}
136+
} catch (ExecutionException | InterruptedException e) {
137+
Log.e(TAG, "Error switching camera", e);
138+
Sentry.captureException(e);
139+
}
140+
}
141+
142+
private boolean allPermissionsGranted() {
143+
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
144+
== PackageManager.PERMISSION_GRANTED;
145+
}
146+
147+
@Override
148+
public void onRequestPermissionsResult(
149+
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
150+
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
151+
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
152+
if (allPermissionsGranted()) {
153+
startCamera();
154+
} else {
155+
Toast.makeText(this, "Camera permission is required", Toast.LENGTH_SHORT).show();
156+
finish();
157+
}
158+
}
159+
}
160+
161+
@Override
162+
protected void onDestroy() {
163+
super.onDestroy();
164+
binding = null;
165+
}
166+
}

sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,11 @@ public void run() {
341341
});
342342
});
343343

344+
binding.openCameraActivity.setOnClickListener(
345+
view -> {
346+
startActivity(new Intent(this, CameraXActivity.class));
347+
});
348+
344349
Sentry.logger().log(SentryLogLevel.INFO, "Creating content view");
345350
setContentView(binding.getRoot());
346351

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
xmlns:tools="http://schemas.android.com/tools"
5+
android:layout_width="match_parent"
6+
android:layout_height="match_parent"
7+
tools:context=".CameraXActivity"
8+
>
9+
10+
<androidx.camera.view.PreviewView
11+
android:id="@+id/previewView"
12+
android:layout_width="match_parent"
13+
android:layout_height="match_parent"
14+
app:layout_constraintBottom_toBottomOf="parent"
15+
app:layout_constraintEnd_toEndOf="parent"
16+
app:layout_constraintStart_toStartOf="parent"
17+
app:layout_constraintTop_toTopOf="parent"
18+
/>
19+
20+
<Button
21+
android:id="@+id/captureButton"
22+
android:layout_width="80dp"
23+
android:layout_height="80dp"
24+
android:layout_marginBottom="32dp"
25+
android:background="@android:drawable/ic_menu_camera"
26+
android:text=""
27+
app:layout_constraintBottom_toBottomOf="parent"
28+
app:layout_constraintEnd_toEndOf="parent"
29+
app:layout_constraintStart_toStartOf="parent"
30+
/>
31+
32+
<Button
33+
android:id="@+id/switchCameraButton"
34+
android:layout_width="wrap_content"
35+
android:layout_height="wrap_content"
36+
android:layout_marginEnd="16dp"
37+
android:layout_marginBottom="32dp"
38+
android:text="@string/switch_preview"
39+
app:layout_constraintBottom_toBottomOf="parent"
40+
app:layout_constraintEnd_toEndOf="parent"
41+
/>
42+
43+
<Button
44+
android:id="@+id/backButton"
45+
android:layout_width="wrap_content"
46+
android:layout_height="wrap_content"
47+
android:layout_margin="16dp"
48+
android:text="@string/back"
49+
app:layout_constraintStart_toStartOf="parent"
50+
app:layout_constraintTop_toTopOf="parent"
51+
/>
52+
53+
</androidx.constraintlayout.widget.ConstraintLayout>

sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@
176176
android:layout_height="wrap_content"
177177
android:text="@string/check_for_update"/>
178178

179+
<Button
180+
android:id="@+id/open_camera_activity"
181+
android:layout_width="wrap_content"
182+
android:layout_height="wrap_content"
183+
android:text="@string/open_camera_activity"
184+
/>
185+
179186
</LinearLayout>
180187

181188
</ScrollView>

sentry-samples/sentry-samples-android/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
<string name="show_dialog">Show Dialog</string>
3232
<string name="check_for_update">Check for Update</string>
3333
<string name="back_main">Back to Main Activity</string>
34+
<string name="back">Back</string>
35+
<string name="switch_preview">Switch Preview</string>
3436
<string name="tap_me">text</string>
3537
<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.
3638

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

0 commit comments

Comments
 (0)