Skip to content

Commit 1792f53

Browse files
authored
Merge f16ac66 into 8696c89
2 parents 8696c89 + f16ac66 commit 1792f53

File tree

23 files changed

+1806
-9
lines changed

23 files changed

+1806
-9
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Add New User Feedback form ([#4384](https://github.com/getsentry/sentry-java/pull/4384))
8+
- We now introduce SentryUserFeedbackDialog, which extends AlertDialog, inheriting the show() and cancel() methods, among others.
9+
To use it, just instantiate it and call show() on the instance (Sentry must be previously initialized).
10+
For customization options, please check the [User Feedback documentation](https://docs.sentry.io/platforms/android/user-feedback/configuration/).
11+
```java
12+
import io.sentry.android.core.SentryUserFeedbackDialog;
13+
14+
new SentryUserFeedbackDialog(context).show();
15+
```
16+
```kotlin
17+
import io.sentry.android.core.SentryUserFeedbackDialog
18+
19+
SentryUserFeedbackDialog(context).show()
20+
```
21+
322
## 8.12.0
423

524
### Features

sentry-android-core/api/sentry-android-core.api

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,15 @@ public final class io/sentry/android/core/SentryPerformanceProvider {
385385
public fun shutdown ()V
386386
}
387387

388+
public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app/AlertDialog {
389+
public fun <init> (Landroid/content/Context;)V
390+
public fun <init> (Landroid/content/Context;I)V
391+
public fun <init> (Landroid/content/Context;ZLandroid/content/DialogInterface$OnCancelListener;)V
392+
public fun setCancelable (Z)V
393+
public fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)V
394+
public fun show ()V
395+
}
396+
388397
public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerformanceContinuousCollector, io/sentry/android/core/internal/util/SentryFrameMetricsCollector$FrameMetricsCollectorListener {
389398
protected final field lock Lio/sentry/util/AutoClosableReentrantLock;
390399
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33
<uses-permission android:name="android.permission.INTERNET"/>
44

5-
<application>
5+
<application
6+
android:supportsRtl="true">
67
<!-- 'android:authorities' must be unique in the device, across all apps -->
78
<provider
89
android:name=".SentryInitProvider"

sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.sentry.ILogger;
77
import io.sentry.InitPriority;
88
import io.sentry.ProfileLifecycle;
9+
import io.sentry.SentryFeedbackOptions;
910
import io.sentry.SentryIntegrationPackageStorage;
1011
import io.sentry.SentryLevel;
1112
import io.sentry.protocol.SdkVersion;
@@ -126,6 +127,18 @@ final class ManifestMetadataReader {
126127
static final String ENABLE_AUTO_TRACE_ID_GENERATION =
127128
"io.sentry.traces.enable-auto-id-generation";
128129

130+
static final String FEEDBACK_NAME_REQUIRED = "io.sentry.feedback.is-name-required";
131+
132+
static final String FEEDBACK_SHOW_NAME = "io.sentry.feedback.show-name";
133+
134+
static final String FEEDBACK_EMAIL_REQUIRED = "io.sentry.feedback.is-email-required";
135+
136+
static final String FEEDBACK_SHOW_EMAIL = "io.sentry.feedback.show-email";
137+
138+
static final String FEEDBACK_USE_SENTRY_USER = "io.sentry.feedback.use-sentry-user";
139+
140+
static final String FEEDBACK_SHOW_BRANDING = "io.sentry.feedback.show-branding";
141+
129142
/** ManifestMetadataReader ctor */
130143
private ManifestMetadataReader() {}
131144

@@ -477,6 +490,21 @@ static void applyMetadata(
477490
options
478491
.getLogs()
479492
.setEnabled(readBool(metadata, logger, ENABLE_LOGS, options.getLogs().isEnabled()));
493+
494+
final @NotNull SentryFeedbackOptions feedbackOptions = options.getFeedbackOptions();
495+
feedbackOptions.setNameRequired(
496+
readBool(metadata, logger, FEEDBACK_NAME_REQUIRED, feedbackOptions.isNameRequired()));
497+
feedbackOptions.setShowName(
498+
readBool(metadata, logger, FEEDBACK_SHOW_NAME, feedbackOptions.isShowName()));
499+
feedbackOptions.setEmailRequired(
500+
readBool(metadata, logger, FEEDBACK_EMAIL_REQUIRED, feedbackOptions.isEmailRequired()));
501+
feedbackOptions.setShowEmail(
502+
readBool(metadata, logger, FEEDBACK_SHOW_EMAIL, feedbackOptions.isShowEmail()));
503+
feedbackOptions.setUseSentryUser(
504+
readBool(
505+
metadata, logger, FEEDBACK_USE_SENTRY_USER, feedbackOptions.isUseSentryUser()));
506+
feedbackOptions.setShowBranding(
507+
readBool(metadata, logger, FEEDBACK_SHOW_BRANDING, feedbackOptions.isShowBranding()));
480508
}
481509
options
482510
.getLogger()
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package io.sentry.android.core;
2+
3+
import android.app.AlertDialog;
4+
import android.content.Context;
5+
import android.os.Bundle;
6+
import android.view.View;
7+
import android.widget.Button;
8+
import android.widget.EditText;
9+
import android.widget.ImageView;
10+
import android.widget.TextView;
11+
import android.widget.Toast;
12+
import io.sentry.IScopes;
13+
import io.sentry.Sentry;
14+
import io.sentry.SentryFeedbackOptions;
15+
import io.sentry.SentryLevel;
16+
import io.sentry.SentryOptions;
17+
import io.sentry.protocol.Feedback;
18+
import io.sentry.protocol.SentryId;
19+
import io.sentry.protocol.User;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.jetbrains.annotations.Nullable;
22+
23+
public final class SentryUserFeedbackDialog extends AlertDialog {
24+
25+
private boolean isCancelable = false;
26+
private @Nullable SentryId currentReplayId;
27+
private @Nullable OnDismissListener delegate;
28+
29+
public SentryUserFeedbackDialog(final @NotNull Context context) {
30+
super(context);
31+
}
32+
33+
public SentryUserFeedbackDialog(
34+
final @NotNull Context context,
35+
final boolean cancelable,
36+
@Nullable final OnCancelListener cancelListener) {
37+
super(context, cancelable, cancelListener);
38+
isCancelable = cancelable;
39+
}
40+
41+
public SentryUserFeedbackDialog(final @NotNull Context context, final int themeResId) {
42+
super(context, themeResId);
43+
}
44+
45+
@Override
46+
public void setCancelable(boolean cancelable) {
47+
super.setCancelable(cancelable);
48+
isCancelable = cancelable;
49+
}
50+
51+
@Override
52+
protected void onCreate(Bundle savedInstanceState) {
53+
super.onCreate(savedInstanceState);
54+
setContentView(R.layout.sentry_dialog_user_feedback);
55+
setCancelable(isCancelable);
56+
57+
final @NotNull SentryFeedbackOptions feedbackOptions =
58+
Sentry.getCurrentScopes().getOptions().getFeedbackOptions();
59+
final @NotNull TextView lblTitle = findViewById(R.id.sentry_dialog_user_feedback_title);
60+
final @NotNull ImageView imgLogo = findViewById(R.id.sentry_dialog_user_feedback_logo);
61+
final @NotNull TextView lblName = findViewById(R.id.sentry_dialog_user_feedback_txt_name);
62+
final @NotNull EditText edtName = findViewById(R.id.sentry_dialog_user_feedback_edt_name);
63+
final @NotNull TextView lblEmail = findViewById(R.id.sentry_dialog_user_feedback_txt_email);
64+
final @NotNull EditText edtEmail = findViewById(R.id.sentry_dialog_user_feedback_edt_email);
65+
final @NotNull TextView lblMessage =
66+
findViewById(R.id.sentry_dialog_user_feedback_txt_description);
67+
final @NotNull EditText edtMessage =
68+
findViewById(R.id.sentry_dialog_user_feedback_edt_description);
69+
final @NotNull Button btnSend = findViewById(R.id.sentry_dialog_user_feedback_btn_send);
70+
final @NotNull Button btnCancel = findViewById(R.id.sentry_dialog_user_feedback_btn_cancel);
71+
72+
if (feedbackOptions.isShowBranding()) {
73+
imgLogo.setVisibility(View.VISIBLE);
74+
} else {
75+
imgLogo.setVisibility(View.GONE);
76+
}
77+
78+
// If name is required, ignore showName flag
79+
if (!feedbackOptions.isShowName() && !feedbackOptions.isNameRequired()) {
80+
lblName.setVisibility(View.GONE);
81+
edtName.setVisibility(View.GONE);
82+
} else {
83+
lblName.setVisibility(View.VISIBLE);
84+
edtName.setVisibility(View.VISIBLE);
85+
lblName.setText(feedbackOptions.getNameLabel());
86+
edtName.setHint(feedbackOptions.getNamePlaceholder());
87+
if (feedbackOptions.isNameRequired()) {
88+
lblName.append(feedbackOptions.getIsRequiredLabel());
89+
}
90+
}
91+
92+
// If email is required, ignore showEmail flag
93+
if (!feedbackOptions.isShowEmail() && !feedbackOptions.isEmailRequired()) {
94+
lblEmail.setVisibility(View.GONE);
95+
edtEmail.setVisibility(View.GONE);
96+
} else {
97+
lblEmail.setVisibility(View.VISIBLE);
98+
edtEmail.setVisibility(View.VISIBLE);
99+
lblEmail.setText(feedbackOptions.getEmailLabel());
100+
edtEmail.setHint(feedbackOptions.getEmailPlaceholder());
101+
if (feedbackOptions.isEmailRequired()) {
102+
lblEmail.append(feedbackOptions.getIsRequiredLabel());
103+
}
104+
}
105+
106+
// If Sentry user is set, and useSentryUser is true, populate the name and email
107+
if (feedbackOptions.isUseSentryUser()) {
108+
final @Nullable User user = Sentry.getCurrentScopes().getScope().getUser();
109+
if (user != null) {
110+
edtName.setText(user.getName());
111+
edtEmail.setText(user.getEmail());
112+
}
113+
}
114+
115+
lblMessage.setText(feedbackOptions.getMessageLabel());
116+
lblMessage.append(feedbackOptions.getIsRequiredLabel());
117+
edtMessage.setHint(feedbackOptions.getMessagePlaceholder());
118+
lblTitle.setText(feedbackOptions.getFormTitle());
119+
120+
btnSend.setText(feedbackOptions.getSubmitButtonLabel());
121+
btnSend.setOnClickListener(
122+
v -> {
123+
// Gather fields and trim them
124+
final @NotNull String name = edtName.getText().toString().trim();
125+
final @NotNull String email = edtEmail.getText().toString().trim();
126+
final @NotNull String message = edtMessage.getText().toString().trim();
127+
128+
// If a required field is missing, shows the error label
129+
if (name.isEmpty() && feedbackOptions.isNameRequired()) {
130+
edtName.setError(lblName.getText());
131+
return;
132+
}
133+
134+
if (email.isEmpty() && feedbackOptions.isEmailRequired()) {
135+
edtEmail.setError(lblEmail.getText());
136+
return;
137+
}
138+
139+
if (message.isEmpty()) {
140+
edtMessage.setError(lblMessage.getText());
141+
return;
142+
}
143+
144+
// Create the feedback object
145+
final @NotNull Feedback feedback = new Feedback(message);
146+
feedback.setName(name);
147+
feedback.setContactEmail(email);
148+
if (currentReplayId != null) {
149+
feedback.setReplayId(currentReplayId);
150+
}
151+
152+
// Capture the feedback. If the ID is empty, it means that the feedback was not sent
153+
final @NotNull SentryId id = Sentry.captureFeedback(feedback);
154+
if (!id.equals(SentryId.EMPTY_ID)) {
155+
Toast.makeText(
156+
getContext(), feedbackOptions.getSuccessMessageText(), Toast.LENGTH_SHORT)
157+
.show();
158+
final @Nullable SentryFeedbackOptions.SentryFeedbackCallback onSubmitSuccess =
159+
feedbackOptions.getOnSubmitSuccess();
160+
if (onSubmitSuccess != null) {
161+
onSubmitSuccess.call(feedback);
162+
}
163+
} else {
164+
final @Nullable SentryFeedbackOptions.SentryFeedbackCallback onSubmitError =
165+
feedbackOptions.getOnSubmitError();
166+
if (onSubmitError != null) {
167+
onSubmitError.call(feedback);
168+
}
169+
}
170+
cancel();
171+
});
172+
173+
btnCancel.setText(feedbackOptions.getCancelButtonLabel());
174+
btnCancel.setOnClickListener(v -> cancel());
175+
setOnDismissListener(delegate);
176+
}
177+
178+
@Override
179+
public void setOnDismissListener(final @Nullable OnDismissListener listener) {
180+
delegate = listener;
181+
// If the user set a custom onDismissListener, we ensure it doesn't override the onFormClose
182+
final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions();
183+
final @Nullable Runnable onFormClose = options.getFeedbackOptions().getOnFormClose();
184+
if (onFormClose != null) {
185+
super.setOnDismissListener(
186+
dialog -> {
187+
onFormClose.run();
188+
currentReplayId = null;
189+
if (delegate != null) {
190+
delegate.onDismiss(dialog);
191+
}
192+
});
193+
} else {
194+
super.setOnDismissListener(delegate);
195+
}
196+
}
197+
198+
@Override
199+
protected void onStart() {
200+
super.onStart();
201+
final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions();
202+
final @NotNull SentryFeedbackOptions feedbackOptions = options.getFeedbackOptions();
203+
final @Nullable Runnable onFormOpen = feedbackOptions.getOnFormOpen();
204+
if (onFormOpen != null) {
205+
onFormOpen.run();
206+
}
207+
options.getReplayController().captureReplay(false);
208+
currentReplayId = options.getReplayController().getReplayId();
209+
}
210+
211+
@Override
212+
public void show() {
213+
// If Sentry is disabled, don't show the dialog, but log a warning
214+
final @NotNull IScopes scopes = Sentry.getCurrentScopes();
215+
final @NotNull SentryOptions options = scopes.getOptions();
216+
if (!scopes.isEnabled() || !options.isEnabled()) {
217+
options
218+
.getLogger()
219+
.log(SentryLevel.WARNING, "Sentry is disabled. Feedback dialog won't be shown.");
220+
return;
221+
}
222+
// Otherwise, show the dialog
223+
super.show();
224+
}
225+
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:shape="rectangle">
4+
5+
<solid android:color="@android:color/transparent" />
6+
<stroke
7+
android:width="1dp"
8+
android:color="#FFAAAAAA" /> <!-- border color -->
9+
<corners android:radius="4dp" />
10+
<padding
11+
android:left="8dp"
12+
android:top="8dp"
13+
android:right="8dp"
14+
android:bottom="8dp" />
15+
</shape>

0 commit comments

Comments
 (0)