Skip to content

Commit 58769f0

Browse files
authored
New User Feedback Widget (#4450)
* added SentryUserFeedbackWidget, with styles and icon * MainActivity sendFeedback reverted to call API, with new widget to open dialog * Added widget ui tests
1 parent 5fde6b6 commit 58769f0

File tree

10 files changed

+303
-9
lines changed

10 files changed

+303
-9
lines changed

CHANGELOG.md

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

99
### Features
1010

11+
- Add New User Feedback Widget ([#4450](https://github.com/getsentry/sentry-java/pull/4450))
12+
- This widget is a custom button that can be used to show the user feedback form
1113
- Add New User Feedback form ([#4384](https://github.com/getsentry/sentry-java/pull/4384))
1214
- We now introduce SentryUserFeedbackDialog, which extends AlertDialog, inheriting the show() and cancel() methods, among others.
1315
To use it, just instantiate it and call show() on the instance (Sentry must be previously initialized).

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

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

388+
public class io/sentry/android/core/SentryUserFeedbackButton : android/widget/Button {
389+
public fun <init> (Landroid/content/Context;)V
390+
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
391+
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;I)V
392+
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;II)V
393+
public fun setOnClickListener (Landroid/view/View$OnClickListener;)V
394+
}
395+
388396
public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app/AlertDialog {
389397
public fun setCancelable (Z)V
390398
public fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)V
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 SentryUserFeedbackButton extends Button {
14+
15+
private @Nullable OnClickListener delegate;
16+
17+
public SentryUserFeedbackButton(Context context) {
18+
super(context);
19+
init(context, null, 0, 0);
20+
}
21+
22+
public SentryUserFeedbackButton(Context context, AttributeSet attrs) {
23+
super(context, attrs);
24+
init(context, attrs, 0, 0);
25+
}
26+
27+
public SentryUserFeedbackButton(Context context, AttributeSet attrs, int defStyleAttr) {
28+
super(context, attrs, defStyleAttr);
29+
init(context, attrs, defStyleAttr, 0);
30+
}
31+
32+
public SentryUserFeedbackButton(
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.SentryUserFeedbackButton, defStyleAttr, defStyleRes)) {
48+
final float dimensionScale = context.getResources().getDisplayMetrics().density;
49+
final float drawablePadding =
50+
typedArray.getDimension(R.styleable.SentryUserFeedbackButton_android_drawablePadding, -1);
51+
final int drawableStart =
52+
typedArray.getResourceId(R.styleable.SentryUserFeedbackButton_android_drawableStart, -1);
53+
final boolean textAllCaps =
54+
typedArray.getBoolean(R.styleable.SentryUserFeedbackButton_android_textAllCaps, false);
55+
final int background =
56+
typedArray.getResourceId(R.styleable.SentryUserFeedbackButton_android_background, -1);
57+
final float padding =
58+
typedArray.getDimension(R.styleable.SentryUserFeedbackButton_android_padding, -1);
59+
final int textColor =
60+
typedArray.getColor(R.styleable.SentryUserFeedbackButton_android_textColor, -1);
61+
final @Nullable String text =
62+
typedArray.getString(R.styleable.SentryUserFeedbackButton_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(
72+
R.drawable.sentry_user_feedback_button_logo_24, 0, 0, 0);
73+
}
74+
75+
// Set the text all caps
76+
setAllCaps(textAllCaps);
77+
78+
// If the background is not set, set it to the default background
79+
if (background == -1) {
80+
setBackgroundResource(R.drawable.sentry_oval_button_ripple_background);
81+
}
82+
83+
// If the padding is not set, set it to 12dp
84+
if (padding == -1) {
85+
int defaultPadding = (int) (12 * dimensionScale);
86+
setPadding(defaultPadding, defaultPadding, defaultPadding, defaultPadding);
87+
}
88+
89+
// If the text color is not set, set it to the default text color
90+
if (textColor == -1) {
91+
// We need the TypedValue to resolve the color from the theme
92+
final @NotNull TypedValue typedValue = new TypedValue();
93+
context.getTheme().resolveAttribute(android.R.attr.colorForeground, typedValue, true);
94+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
95+
setTextColor(context.getResources().getColor(typedValue.resourceId, context.getTheme()));
96+
} else {
97+
setTextColor(context.getResources().getColor(typedValue.resourceId));
98+
}
99+
}
100+
101+
// If the text is not set, set it to "Report a Bug"
102+
if (text == null) {
103+
setText("Report a Bug");
104+
}
105+
}
106+
107+
// Set the default ClickListener to open the SentryUserFeedbackDialog
108+
setOnClickListener(delegate);
109+
}
110+
111+
@Override
112+
public void setOnClickListener(final @Nullable OnClickListener listener) {
113+
delegate = listener;
114+
super.setOnClickListener(
115+
v -> {
116+
new SentryUserFeedbackDialog.Builder(getContext()).create().show();
117+
if (delegate != null) {
118+
delegate.onClick(v);
119+
}
120+
});
121+
}
122+
}
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: 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: 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="SentryUserFeedbackButton" >
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
@@ -23,9 +26,11 @@ import io.sentry.SentryFeedbackOptions.SentryFeedbackCallback
2326
import io.sentry.SentryOptions
2427
import io.sentry.android.core.AndroidLogger
2528
import io.sentry.android.core.R
29+
import io.sentry.android.core.SentryUserFeedbackButton
2630
import io.sentry.android.core.SentryUserFeedbackDialog
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.sentry_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.sentry_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.sentry_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: SentryUserFeedbackButton) -> Unit)? = null, checker: (widget: SentryUserFeedbackButton) -> 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+
SentryUserFeedbackButton(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 SentryUserFeedbackButton)
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
@@ -11,8 +11,8 @@
1111
import io.sentry.ISpan;
1212
import io.sentry.MeasurementUnit;
1313
import io.sentry.Sentry;
14-
import io.sentry.android.core.SentryUserFeedbackDialog;
1514
import io.sentry.instrumentation.file.SentryFileOutputStream;
15+
import io.sentry.protocol.Feedback;
1616
import io.sentry.protocol.User;
1717
import io.sentry.samples.android.compose.ComposeActivity;
1818
import io.sentry.samples.android.databinding.ActivityMainBinding;
@@ -74,7 +74,11 @@ protected void onCreate(Bundle savedInstanceState) {
7474

7575
binding.sendUserFeedback.setOnClickListener(
7676
view -> {
77-
new SentryUserFeedbackDialog.Builder(this).create().show();
77+
Feedback feedback =
78+
new Feedback("It broke on Android. I don't know why, but this happens.");
79+
feedback.setContactEmail("john@me.com");
80+
feedback.setName("John Me");
81+
Sentry.captureFeedback(feedback);
7882
});
7983

8084
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.SentryUserFeedbackButton
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)