Skip to content

Commit 028a1a6

Browse files
authored
Merge c6d1919 into 202759f
2 parents 202759f + c6d1919 commit 028a1a6

File tree

10 files changed

+302
-9
lines changed

10 files changed

+302
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features
66

7+
- Add New User Feedback Widget ([#4450](https://github.com/getsentry/sentry-java/pull/4450))
8+
- This widget is a custom button that can be used to show the user feedback form
79
- Add debug mode for Session Replay masking ([#4357](https://github.com/getsentry/sentry-java/pull/4357))
810
- Use `Sentry.replay().enableDebugMaskingOverlay()` to overlay the screen with the Session Replay masks.
911
- The masks will be invalidated at most once per `frameRate` (default 1 fps).

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,14 @@ public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app
394394
public fun show ()V
395395
}
396396

397+
public class io/sentry/android/core/SentryUserFeedbackWidget : android/widget/Button {
398+
public fun <init> (Landroid/content/Context;)V
399+
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
400+
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;I)V
401+
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;II)V
402+
public fun setOnClickListener (Landroid/view/View$OnClickListener;)V
403+
}
404+
397405
public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerformanceContinuousCollector, io/sentry/android/core/internal/util/SentryFrameMetricsCollector$FrameMetricsCollectorListener {
398406
protected final field lock Lio/sentry/util/AutoClosableReentrantLock;
399407
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package io.sentry.android.core;
2+
3+
import android.annotation.SuppressLint;
4+
import android.content.Context;
5+
import android.content.res.TypedArray;
6+
import android.os.Build;
7+
import android.util.AttributeSet;
8+
import android.util.TypedValue;
9+
import android.widget.Button;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.annotations.Nullable;
12+
13+
public class SentryUserFeedbackWidget extends Button {
14+
15+
private @Nullable OnClickListener delegate;
16+
17+
public SentryUserFeedbackWidget(Context context) {
18+
super(context);
19+
init(context, null, 0, 0);
20+
}
21+
22+
public SentryUserFeedbackWidget(Context context, AttributeSet attrs) {
23+
super(context, attrs);
24+
init(context, attrs, 0, 0);
25+
}
26+
27+
public SentryUserFeedbackWidget(Context context, AttributeSet attrs, int defStyleAttr) {
28+
super(context, attrs, defStyleAttr);
29+
init(context, attrs, defStyleAttr, 0);
30+
}
31+
32+
public SentryUserFeedbackWidget(
33+
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
34+
super(context, attrs, defStyleAttr, defStyleRes);
35+
init(context, attrs, defStyleAttr, defStyleRes);
36+
}
37+
38+
@SuppressLint("SetTextI18n")
39+
@SuppressWarnings("deprecation")
40+
private void init(
41+
final @NotNull Context context,
42+
final @Nullable AttributeSet attrs,
43+
final int defStyleAttr,
44+
final int defStyleRes) {
45+
try (final @NotNull TypedArray typedArray =
46+
context.obtainStyledAttributes(
47+
attrs, R.styleable.SentryUserFeedbackWidget, defStyleAttr, defStyleRes)) {
48+
final float dimensionScale = context.getResources().getDisplayMetrics().density;
49+
final float drawablePadding =
50+
typedArray.getDimension(R.styleable.SentryUserFeedbackWidget_android_drawablePadding, -1);
51+
final int drawableStart =
52+
typedArray.getResourceId(R.styleable.SentryUserFeedbackWidget_android_drawableStart, -1);
53+
final boolean textAllCaps =
54+
typedArray.getBoolean(R.styleable.SentryUserFeedbackWidget_android_textAllCaps, false);
55+
final int background =
56+
typedArray.getResourceId(R.styleable.SentryUserFeedbackWidget_android_background, -1);
57+
final float padding =
58+
typedArray.getDimension(R.styleable.SentryUserFeedbackWidget_android_padding, -1);
59+
final int textColor =
60+
typedArray.getColor(R.styleable.SentryUserFeedbackWidget_android_textColor, -1);
61+
final @Nullable String text =
62+
typedArray.getString(R.styleable.SentryUserFeedbackWidget_android_text);
63+
64+
// If the drawable padding is not set, set it to 4dp
65+
if (drawablePadding == -1) {
66+
setCompoundDrawablePadding((int) (4 * dimensionScale));
67+
}
68+
69+
// If the drawable start is not set, set it to the default drawable
70+
if (drawableStart == -1) {
71+
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.baseline_campaign_24, 0, 0, 0);
72+
}
73+
74+
// Set the text all caps
75+
setAllCaps(textAllCaps);
76+
77+
// If the background is not set, set it to the default background
78+
if (background == -1) {
79+
setBackgroundResource(R.drawable.oval_button_ripple_background);
80+
}
81+
82+
// If the padding is not set, set it to 12dp
83+
if (padding == -1) {
84+
int defaultPadding = (int) (12 * dimensionScale);
85+
setPadding(defaultPadding, defaultPadding, defaultPadding, defaultPadding);
86+
}
87+
88+
// If the text color is not set, set it to the default text color
89+
if (textColor == -1) {
90+
// We need the TypedValue to resolve the color from the theme
91+
final @NotNull TypedValue typedValue = new TypedValue();
92+
context.getTheme().resolveAttribute(android.R.attr.colorForeground, typedValue, true);
93+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
94+
setTextColor(context.getResources().getColor(typedValue.resourceId, context.getTheme()));
95+
} else {
96+
setTextColor(context.getResources().getColor(typedValue.resourceId));
97+
}
98+
}
99+
100+
// If the text is not set, set it to "Report a Bug"
101+
if (text == null) {
102+
setText("Report a Bug");
103+
}
104+
}
105+
106+
// Set the default ClickListener to open the SentryUserFeedbackDialog
107+
setOnClickListener(delegate);
108+
}
109+
110+
@Override
111+
public void setOnClickListener(final @Nullable OnClickListener listener) {
112+
delegate = listener;
113+
super.setOnClickListener(
114+
v -> {
115+
new SentryUserFeedbackDialog(getContext()).show();
116+
if (delegate != null) {
117+
delegate.onClick(v);
118+
}
119+
});
120+
}
121+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?android:attr/colorForeground" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
2+
3+
<path android:fillColor="?android:attr/colorForeground" android:pathData="M18,11v2h4v-2h-4zM16,17.61c0.96,0.71 2.21,1.65 3.2,2.39 0.4,-0.53 0.8,-1.07 1.2,-1.6 -0.99,-0.74 -2.24,-1.68 -3.2,-2.4 -0.4,0.54 -0.8,1.08 -1.2,1.61zM20.4,5.6c-0.4,-0.53 -0.8,-1.07 -1.2,-1.6 -0.99,0.74 -2.24,1.68 -3.2,2.4 0.4,0.53 0.8,1.07 1.2,1.6 0.96,-0.72 2.21,-1.65 3.2,-2.4zM4,9c-1.1,0 -2,0.9 -2,2v2c0,1.1 0.9,2 2,2h1v4h2v-4h1l5,3L13,6L8,9L4,9zM15.5,12c0,-1.33 -0.58,-2.53 -1.5,-3.35v6.69c0.92,-0.81 1.5,-2.01 1.5,-3.34z"/>
4+
5+
</vector>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:color="?android:attr/colorControlHighlight">
3+
4+
<item>
5+
<shape android:shape="rectangle">
6+
<solid android:color="?android:attr/colorBackground" />
7+
<corners android:radius="50dp" />
8+
</shape>
9+
</item>
10+
</ripple>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<declare-styleable name="SentryUserFeedbackWidget" >
4+
<attr name="android:drawableStart" format="reference" />
5+
<attr name="android:drawablePadding" format="dimension" />
6+
<attr name="android:padding" format="dimension" />
7+
<attr name="android:textAllCaps" format="boolean" />
8+
<attr name="android:background" format="reference|color" />
9+
<attr name="android:textColor" format="reference|color" />
10+
<attr name="android:text" format="string" />
11+
</declare-styleable>
12+
</resources>

sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package io.sentry.uitest.android
22

3+
import android.graphics.Color
4+
import android.util.TypedValue
35
import android.view.View
46
import android.widget.EditText
7+
import android.widget.LinearLayout
58
import androidx.test.core.app.launchActivity
69
import androidx.test.espresso.Espresso.onView
710
import androidx.test.espresso.action.ViewActions.click
@@ -24,8 +27,10 @@ import io.sentry.SentryOptions
2427
import io.sentry.android.core.AndroidLogger
2528
import io.sentry.android.core.R
2629
import io.sentry.android.core.SentryUserFeedbackDialog
30+
import io.sentry.android.core.SentryUserFeedbackWidget
2731
import io.sentry.assertEnvelopeFeedback
2832
import io.sentry.protocol.User
33+
import io.sentry.test.getProperty
2934
import org.hamcrest.Description
3035
import org.hamcrest.Matcher
3136
import org.hamcrest.Matchers.allOf
@@ -34,6 +39,7 @@ import kotlin.test.Test
3439
import kotlin.test.assertEquals
3540
import kotlin.test.assertFalse
3641
import kotlin.test.assertNotNull
42+
import kotlin.test.assertNull
3743
import kotlin.test.assertTrue
3844

3945
@RunWith(AndroidJUnit4::class)
@@ -517,6 +523,95 @@ class UserFeedbackUiTest : BaseUiTest() {
517523
}
518524
}
519525

526+
@Test
527+
fun userFeedbackWidgetDefaults() {
528+
initSentry()
529+
var widgetId = 0
530+
showWidgetAndCheck { widget ->
531+
widgetId = widget.id
532+
val densityScale = context.resources.displayMetrics.density
533+
assertEquals((densityScale * 4).toInt(), widget.compoundDrawablePadding)
534+
535+
assertNotNull(widget.compoundDrawables[0]) // Drawable left
536+
assertNull(widget.compoundDrawables[1]) // Drawable top
537+
assertNull(widget.compoundDrawables[2]) // Drawable right
538+
assertNull(widget.compoundDrawables[3]) // Drawable bottom
539+
540+
// Couldn't find a reliable way to check the drawable, so i'll skip it
541+
542+
assertFalse(widget.isAllCaps)
543+
544+
assertEquals(R.drawable.oval_button_ripple_background, widget.getProperty<Int>("mBackgroundResource"))
545+
546+
assertEquals((densityScale * 12).toInt(), widget.paddingStart)
547+
assertEquals((densityScale * 12).toInt(), widget.paddingEnd)
548+
assertEquals((densityScale * 12).toInt(), widget.paddingTop)
549+
assertEquals((densityScale * 12).toInt(), widget.paddingBottom)
550+
551+
val typedValue = TypedValue()
552+
widget.context.theme.resolveAttribute(android.R.attr.colorForeground, typedValue, true)
553+
assertEquals(typedValue.data, widget.currentTextColor)
554+
555+
assertEquals("Report a Bug", widget.text)
556+
}
557+
558+
onView(withId(widgetId)).perform(click())
559+
// Check that the user feedback dialog is shown
560+
checkViewVisibility(R.id.sentry_dialog_user_feedback_layout)
561+
}
562+
563+
@Test
564+
fun userFeedbackWidgetDefaultsOverridden() {
565+
initSentry()
566+
showWidgetAndCheck({ widget ->
567+
widget.compoundDrawablePadding = 1
568+
widget.setCompoundDrawables(null, null, null, null)
569+
widget.isAllCaps = true
570+
widget.setBackgroundResource(R.drawable.edit_text_border)
571+
widget.setTextColor(Color.RED)
572+
widget.text = "My custom text"
573+
widget.setPadding(0, 0, 0, 0)
574+
}) { widget ->
575+
val densityScale = context.resources.displayMetrics.density
576+
assertEquals(1, widget.compoundDrawablePadding)
577+
578+
assertNull(widget.compoundDrawables[0]) // Drawable left
579+
assertNull(widget.compoundDrawables[1]) // Drawable top
580+
assertNull(widget.compoundDrawables[2]) // Drawable right
581+
assertNull(widget.compoundDrawables[3]) // Drawable bottom
582+
583+
assertTrue(widget.isAllCaps)
584+
585+
assertEquals(R.drawable.edit_text_border, widget.getProperty<Int>("mBackgroundResource"))
586+
587+
assertEquals((densityScale * 0).toInt(), widget.paddingStart)
588+
assertEquals((densityScale * 0).toInt(), widget.paddingEnd)
589+
assertEquals((densityScale * 0).toInt(), widget.paddingTop)
590+
assertEquals((densityScale * 0).toInt(), widget.paddingBottom)
591+
592+
assertEquals(Color.RED, widget.currentTextColor)
593+
594+
assertEquals("My custom text", widget.text)
595+
}
596+
}
597+
598+
@Test
599+
fun userFeedbackWidgetShowsDialogOnClickOverridden() {
600+
initSentry()
601+
var widgetId = 0
602+
var customListenerCalled = false
603+
showWidgetAndCheck { widget ->
604+
widgetId = widget.id
605+
widget.setOnClickListener { customListenerCalled = true }
606+
}
607+
608+
onView(withId(widgetId)).perform(click())
609+
// Check that the user feedback dialog is shown
610+
checkViewVisibility(R.id.sentry_dialog_user_feedback_layout)
611+
// And the custom listener is called, too
612+
assertTrue(customListenerCalled)
613+
}
614+
520615
private fun checkViewVisibility(viewId: Int, isGone: Boolean = false) {
521616
onView(withId(viewId))
522617
.check(matches(withEffectiveVisibility(if (isGone) Visibility.GONE else Visibility.VISIBLE)))
@@ -558,6 +653,29 @@ class UserFeedbackUiTest : BaseUiTest() {
558653
checker(dialog)
559654
}
560655

656+
private fun showWidgetAndCheck(widgetConfig: ((widget: SentryUserFeedbackWidget) -> Unit)? = null, checker: (widget: SentryUserFeedbackWidget) -> Unit = {}) {
657+
val buttonId = Int.MAX_VALUE - 1
658+
val feedbackScenario = launchActivity<EmptyActivity>()
659+
feedbackScenario.onActivity {
660+
val view = LinearLayout(it).apply {
661+
orientation = LinearLayout.VERTICAL
662+
addView(
663+
SentryUserFeedbackWidget(it).apply {
664+
id = buttonId
665+
widgetConfig?.invoke(this)
666+
}
667+
)
668+
}
669+
it.setContentView(view)
670+
}
671+
checkViewVisibility(buttonId)
672+
onView(withId(buttonId))
673+
.check(matches(isDisplayed()))
674+
.check { view, _ ->
675+
checker(view as SentryUserFeedbackWidget)
676+
}
677+
}
678+
561679
fun withError(expectedError: String): Matcher<View> {
562680
return object : BoundedMatcher<View, EditText>(EditText::class.java) {
563681
override fun describeTo(description: Description) {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import io.sentry.ISpan;
99
import io.sentry.MeasurementUnit;
1010
import io.sentry.Sentry;
11-
import io.sentry.android.core.SentryUserFeedbackDialog;
1211
import io.sentry.instrumentation.file.SentryFileOutputStream;
12+
import io.sentry.protocol.Feedback;
1313
import io.sentry.protocol.User;
1414
import io.sentry.samples.android.compose.ComposeActivity;
1515
import io.sentry.samples.android.databinding.ActivityMainBinding;
@@ -69,7 +69,11 @@ protected void onCreate(Bundle savedInstanceState) {
6969

7070
binding.sendUserFeedback.setOnClickListener(
7171
view -> {
72-
new SentryUserFeedbackDialog(this).show();
72+
Feedback feedback =
73+
new Feedback("It broke on Android. I don't know why, but this happens.");
74+
feedback.setContactEmail("[email protected]");
75+
feedback.setName("John Me");
76+
Sentry.captureFeedback(feedback);
7377
});
7478

7579
binding.addAttachment.setOnClickListener(

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
android:layout_height="wrap_content"
2323
android:text="@string/send_message" />
2424

25+
<io.sentry.android.core.SentryUserFeedbackWidget
26+
android:layout_width="wrap_content"
27+
android:layout_height="wrap_content"/>
28+
2529
<Button
2630
android:id="@+id/send_user_feedback"
2731
android:layout_width="wrap_content"

0 commit comments

Comments
 (0)