Skip to content

Commit 4128f36

Browse files
authored
Merge branch 'main' into feat/logs-device-and-os-attributes
2 parents 57d679f + 58769f0 commit 4128f36

File tree

16 files changed

+484
-21
lines changed

16 files changed

+484
-21
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
## Unreleased
44

5+
### Fixes
6+
7+
- Fix Session Replay masking for newer versions of Jetpack Compose (1.8+) ([#4485](https://github.com/getsentry/sentry-java/pull/4485))
8+
59
### Features
610

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
713
- Add New User Feedback form ([#4384](https://github.com/getsentry/sentry-java/pull/4384))
814
- We now introduce SentryUserFeedbackDialog, which extends AlertDialog, inheriting the show() and cancel() methods, among others.
915
To use it, just instantiate it and call show() on the instance (Sentry must be previously initialized).
@@ -20,6 +26,7 @@
2026
```
2127
- Add `user.id`, `user.name` and `user.email` to log attributes ([#4486](https://github.com/getsentry/sentry-java/pull/4486))
2228
- Add device (brand, model and family) and OS (name and version) attributes to logs ([#4493](https://github.com/getsentry/sentry-java/pull/4493))
29+
- Serialize `preContext` and `postContext` in `SentryStackFrame` ([#4482](https://github.com/getsentry/sentry-java/pull/4482))
2330

2431
## 8.13.3
2532

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) {

0 commit comments

Comments
 (0)