Skip to content

Commit d3f8605

Browse files
committed
chore(samples): Add CameraX screen
1 parent 806307f commit d3f8605

File tree

8 files changed

+260
-2
lines changed

8 files changed

+260
-2
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: 6 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)
@@ -150,6 +151,11 @@ dependencies {
150151
implementation(libs.sentry.native.ndk)
151152
implementation(libs.timber)
152153

154+
implementation(libs.camerax.core)
155+
implementation(libs.camerax.camera2)
156+
implementation(libs.camerax.lifecycle)
157+
implementation(libs.camerax.view)
158+
153159
debugImplementation(libs.leakcanary)
154160
}
155161

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@
7575
<activity android:name=".FrameDataForSpansActivity"
7676
android:exported="false"/>
7777

78-
<!-- NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard-->
78+
<activity
79+
android:name=".CameraXActivity"
80+
android:exported="false" />
81+
82+
<!-- 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://1053864c67cc410aa1ffc9701bd6f93d@o447951.ingest.sentry.io/5428559" />
8084

8185
<!-- how to enable Sentry's debug mode-->
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
133+
bindPreview(cameraProvider);
134+
} catch (ExecutionException | InterruptedException e) {
135+
Log.e(TAG, "Error switching camera", e);
136+
Sentry.captureException(e);
137+
}
138+
}
139+
140+
private boolean allPermissionsGranted() {
141+
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
142+
== PackageManager.PERMISSION_GRANTED;
143+
}
144+
145+
@Override
146+
public void onRequestPermissionsResult(
147+
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
148+
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
149+
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
150+
if (allPermissionsGranted()) {
151+
startCamera();
152+
} else {
153+
Toast.makeText(this, "Camera permission is required", Toast.LENGTH_SHORT).show();
154+
finish();
155+
}
156+
}
157+
}
158+
159+
@Override
160+
protected void onDestroy() {
161+
super.onDestroy();
162+
binding = null;
163+
}
164+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package io.sentry.samples.android;
22

3+
import android.content.ActivityNotFoundException;
34
import android.content.Intent;
45
import android.content.pm.ActivityInfo;
56
import android.content.res.Configuration;
67
import android.os.Bundle;
78
import android.os.Handler;
9+
import android.provider.MediaStore;
810
import androidx.appcompat.app.AlertDialog;
911
import androidx.appcompat.app.AppCompatActivity;
1012
import io.sentry.Attachment;
@@ -67,7 +69,13 @@ protected void onCreate(Bundle savedInstanceState) {
6769

6870
binding.crashFromJava.setOnClickListener(
6971
view -> {
70-
throw new RuntimeException("Uncaught Exception from Java.");
72+
// throw new RuntimeException("Uncaught Exception from Java.");
73+
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
74+
try {
75+
startActivityForResult(takePictureIntent, 1);
76+
} catch (ActivityNotFoundException e) {
77+
// display error state to the user
78+
}
7179
});
7280

7381
binding.sendMessage.setOnClickListener(view -> Sentry.captureMessage("Some message."));
@@ -304,6 +312,11 @@ public void run() {
304312
Sentry.replay().enableDebugMaskingOverlay();
305313
});
306314

315+
binding.openCameraActivity.setOnClickListener(
316+
view -> {
317+
startActivity(new Intent(this, CameraXActivity.class));
318+
});
319+
307320
setContentView(binding.getRoot());
308321
}
309322

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
@@ -170,6 +170,13 @@
170170
android:layout_height="wrap_content"
171171
android:text="@string/enable_replay_debug_mode"/>
172172

173+
<Button
174+
android:id="@+id/open_camera_activity"
175+
android:layout_width="wrap_content"
176+
android:layout_height="wrap_content"
177+
android:text="@string/open_camera_activity"
178+
/>
179+
173180
</LinearLayout>
174181

175182
</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
@@ -30,6 +30,8 @@
3030
<string name="enable_replay_debug_mode">Enable Replay Debug Mode</string>
3131
<string name="show_dialog">Show Dialog</string>
3232
<string name="back_main">Back to Main Activity</string>
33+
<string name="back">Back</string>
34+
<string name="switch_preview">Switch Preview</string>
3335
<string name="tap_me">text</string>
3436
<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.
3537

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

0 commit comments

Comments
 (0)