Skip to content

Commit a372595

Browse files
authored
Merge 929b476 into e63ad34
2 parents e63ad34 + 929b476 commit a372595

4 files changed

Lines changed: 241 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
- All deprecated APIs will be removed in the next major version
1313
- Deprecate `SentryUserFeedbackButton` (View-based and Compose-based) ([#5350](https://github.com/getsentry/sentry-java/pull/5350))
1414
- It will be removed in the next major version
15+
- Add per-form shake-to-show support for `SentryUserFeedbackForm` ([#5353](https://github.com/getsentry/sentry-java/pull/5353))
16+
- Useful for enabling shake-to-report on specific screens instead of globally
17+
```kotlin
18+
SentryUserFeedbackForm.Builder(activity)
19+
.configurator { it.isUseShakeGesture = true }
20+
.create()
21+
```
1522

1623
### Dependencies
1724

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

Lines changed: 129 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package io.sentry.android.core;
22

3+
import android.app.Activity;
34
import android.app.AlertDialog;
5+
import android.app.Application;
46
import android.content.Context;
7+
import android.content.ContextWrapper;
58
import android.os.Bundle;
69
import android.view.View;
710
import android.widget.Button;
@@ -18,6 +21,7 @@
1821
import io.sentry.protocol.Feedback;
1922
import io.sentry.protocol.SentryId;
2023
import io.sentry.protocol.User;
24+
import java.lang.ref.WeakReference;
2125
import org.jetbrains.annotations.NotNull;
2226
import org.jetbrains.annotations.Nullable;
2327

@@ -28,8 +32,10 @@ public class SentryUserFeedbackForm extends AlertDialog {
2832
private final @Nullable SentryId associatedEventId;
2933
private @Nullable OnDismissListener delegate;
3034

31-
private final @Nullable OptionsConfiguration configuration;
32-
private final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator;
35+
private final @NotNull SentryFeedbackOptions resolvedFeedbackOptions;
36+
37+
private @Nullable SentryShakeDetector shakeDetector;
38+
private @Nullable Application.ActivityLifecycleCallbacks shakeLifecycleCallbacks;
3339

3440
SentryUserFeedbackForm(
3541
final @NotNull Context context,
@@ -39,9 +45,127 @@ public class SentryUserFeedbackForm extends AlertDialog {
3945
final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
4046
super(context, themeResId);
4147
this.associatedEventId = associatedEventId;
42-
this.configuration = configuration;
43-
this.configurator = configurator;
48+
this.resolvedFeedbackOptions =
49+
new SentryFeedbackOptions(Sentry.getCurrentScopes().getOptions().getFeedbackOptions());
50+
if (configuration != null) {
51+
configuration.configure(context, resolvedFeedbackOptions);
52+
}
53+
if (configurator != null) {
54+
configurator.configure(resolvedFeedbackOptions);
55+
}
4456
SentryIntegrationPackageStorage.getInstance().addIntegration("UserFeedbackWidget");
57+
maybeStartShakeDetection(context);
58+
}
59+
60+
private void maybeStartShakeDetection(final @NotNull Context context) {
61+
final @NotNull SentryFeedbackOptions globalFeedbackOptions =
62+
Sentry.getCurrentScopes().getOptions().getFeedbackOptions();
63+
if (!resolvedFeedbackOptions.isUseShakeGesture() || globalFeedbackOptions.isUseShakeGesture()) {
64+
return;
65+
}
66+
final @Nullable Activity activity = getActivity(context);
67+
if (activity == null) {
68+
return;
69+
}
70+
final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions();
71+
shakeDetector = new SentryShakeDetector(options.getLogger());
72+
final @NotNull WeakReference<Activity> activityRef = new WeakReference<>(activity);
73+
shakeDetector.start(
74+
activity,
75+
() -> {
76+
final @Nullable Activity active = activityRef.get();
77+
if (active != null && !active.isFinishing() && !active.isDestroyed()) {
78+
active.runOnUiThread(
79+
() -> {
80+
if (!active.isFinishing() && !active.isDestroyed()) {
81+
show();
82+
}
83+
});
84+
}
85+
});
86+
final @NotNull Application app = activity.getApplication();
87+
shakeLifecycleCallbacks = new ShakeLifecycleCallbacks(activityRef);
88+
app.registerActivityLifecycleCallbacks(shakeLifecycleCallbacks);
89+
}
90+
91+
private void stopShakeDetection() {
92+
if (shakeDetector != null) {
93+
shakeDetector.close();
94+
shakeDetector = null;
95+
}
96+
if (shakeLifecycleCallbacks != null) {
97+
final @Nullable Activity activity = getActivity(getContext());
98+
if (activity != null) {
99+
activity.getApplication().unregisterActivityLifecycleCallbacks(shakeLifecycleCallbacks);
100+
}
101+
shakeLifecycleCallbacks = null;
102+
}
103+
}
104+
105+
private static @Nullable Activity getActivity(final @NotNull Context context) {
106+
Context current = context;
107+
while (current instanceof ContextWrapper) {
108+
if (current instanceof Activity) {
109+
return (Activity) current;
110+
}
111+
current = ((ContextWrapper) current).getBaseContext();
112+
}
113+
return null;
114+
}
115+
116+
private class ShakeLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
117+
private final @NotNull WeakReference<Activity> activityRef;
118+
119+
ShakeLifecycleCallbacks(final @NotNull WeakReference<Activity> activityRef) {
120+
this.activityRef = activityRef;
121+
}
122+
123+
@Override
124+
public void onActivityResumed(final @NotNull Activity activity) {
125+
if (activity == activityRef.get() && shakeDetector != null) {
126+
shakeDetector.start(
127+
activity,
128+
() -> {
129+
final @Nullable Activity active = activityRef.get();
130+
if (active != null && !active.isFinishing() && !active.isDestroyed()) {
131+
active.runOnUiThread(
132+
() -> {
133+
if (!active.isFinishing() && !active.isDestroyed()) {
134+
show();
135+
}
136+
});
137+
}
138+
});
139+
}
140+
}
141+
142+
@Override
143+
public void onActivityPaused(final @NotNull Activity activity) {
144+
if (activity == activityRef.get() && shakeDetector != null) {
145+
shakeDetector.stop();
146+
}
147+
}
148+
149+
@Override
150+
public void onActivityDestroyed(final @NotNull Activity activity) {
151+
if (activity == activityRef.get()) {
152+
stopShakeDetection();
153+
}
154+
}
155+
156+
@Override
157+
public void onActivityCreated(
158+
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {}
159+
160+
@Override
161+
public void onActivityStarted(final @NotNull Activity activity) {}
162+
163+
@Override
164+
public void onActivityStopped(final @NotNull Activity activity) {}
165+
166+
@Override
167+
public void onActivitySaveInstanceState(
168+
final @NotNull Activity activity, final @NotNull Bundle outState) {}
45169
}
46170

47171
@Override
@@ -57,14 +181,7 @@ protected void onCreate(Bundle savedInstanceState) {
57181
setContentView(R.layout.sentry_dialog_user_feedback);
58182
setCancelable(isCancelable);
59183

60-
final @NotNull SentryFeedbackOptions feedbackOptions =
61-
new SentryFeedbackOptions(Sentry.getCurrentScopes().getOptions().getFeedbackOptions());
62-
if (configuration != null) {
63-
configuration.configure(getContext(), feedbackOptions);
64-
}
65-
if (configurator != null) {
66-
configurator.configure(feedbackOptions);
67-
}
184+
final @NotNull SentryFeedbackOptions feedbackOptions = resolvedFeedbackOptions;
68185
final @NotNull TextView lblTitle = findViewById(R.id.sentry_dialog_user_feedback_title);
69186
final @NotNull ImageView imgLogo = findViewById(R.id.sentry_dialog_user_feedback_logo);
70187
final @NotNull TextView lblName = findViewById(R.id.sentry_dialog_user_feedback_txt_name);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@
271271
<meta-data
272272
android:name="io.sentry.anr.enable-fingerprinting"
273273
android:value="true" />
274+
<!-- Enable feedback on shake globally -->
274275
<meta-data
275276
android:name="io.sentry.feedback.use-shake-gesture"
276277
android:value="true" />

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

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import androidx.compose.material.icons.filled.Settings
4343
import androidx.compose.material.icons.filled.Speed
4444
import androidx.compose.material.icons.filled.Videocam
4545
import androidx.compose.material3.AlertDialog
46+
import androidx.compose.material3.Button
4647
import androidx.compose.material3.ExperimentalMaterial3Api
4748
import androidx.compose.material3.Icon
4849
import androidx.compose.material3.MaterialTheme
@@ -62,6 +63,7 @@ import androidx.compose.runtime.mutableStateOf
6263
import androidx.compose.runtime.remember
6364
import androidx.compose.runtime.rememberCoroutineScope
6465
import androidx.compose.runtime.setValue
66+
import androidx.compose.ui.Alignment
6567
import androidx.compose.ui.ExperimentalComposeUiApi
6668
import androidx.compose.ui.Modifier
6769
import androidx.compose.ui.draw.rotate
@@ -79,8 +81,9 @@ import io.sentry.MeasurementUnit
7981
import io.sentry.Sentry
8082
import io.sentry.SentryLogLevel
8183
import io.sentry.UpdateStatus
84+
import io.sentry.android.core.SentryUserFeedbackForm
8285
import io.sentry.compose.SentryTraced
83-
import io.sentry.compose.SentryUserFeedbackButton
86+
import io.sentry.protocol.Feedback
8487
import io.sentry.protocol.User
8588
import java.io.File
8689
import java.io.FileOutputStream
@@ -615,8 +618,106 @@ fun UserFeedbackScreen() {
615618
}
616619
}
617620

618-
// SentryUserFeedbackButton as a special item
619-
item(span = { GridItemSpan(maxLineSpan) }) { SentryUserFeedbackButton(modifier = Modifier) }
621+
// Bring up User Feedback Form from a custom button using the global Sentry.feedback() API
622+
item(span = { GridItemSpan(maxLineSpan) }) {
623+
Button(modifier = Modifier, onClick = { Sentry.feedback().show() }) {
624+
Row(
625+
verticalAlignment = Alignment.CenterVertically,
626+
horizontalArrangement = Arrangement.Center,
627+
) {
628+
Icon(
629+
painter =
630+
painterResource(
631+
id = io.sentry.compose.R.drawable.sentry_user_feedback_compose_button_logo_24
632+
),
633+
contentDescription = null,
634+
)
635+
Spacer(Modifier.padding(horizontal = 4.dp))
636+
Text(text = "Report a Bug")
637+
}
638+
}
639+
}
640+
641+
// Create a SentryUserFeedbackForm programmatically and show it
642+
item(span = { GridItemSpan(maxLineSpan) }) {
643+
Button(
644+
modifier = Modifier,
645+
onClick = {
646+
SentryUserFeedbackForm.Builder(activity)
647+
.configurator { options ->
648+
options.formTitle = "Custom Form"
649+
options.submitButtonLabel = "Send"
650+
options.cancelButtonLabel = "Never mind"
651+
options.messageLabel = "What happened?"
652+
options.messagePlaceholder = "Describe the issue..."
653+
options.isShowBranding = false
654+
options.isNameRequired = true
655+
options.isEmailRequired = true
656+
options.setOnSubmitSuccess { feedback ->
657+
Toast.makeText(activity, "Thanks for the feedback!", Toast.LENGTH_SHORT).show()
658+
}
659+
}
660+
.create()
661+
.show()
662+
},
663+
) {
664+
Text(text = "Custom Form (Builder)")
665+
}
666+
}
667+
668+
// Showcases how to manually show and dismiss a form programmatically
669+
item(span = { GridItemSpan(maxLineSpan) }) {
670+
Button(
671+
modifier = Modifier,
672+
onClick = {
673+
val form =
674+
SentryUserFeedbackForm.Builder(activity)
675+
.configurator { options -> options.formTitle = "Quick! You have 2 seconds" }
676+
.create()
677+
form.show()
678+
Handler(Looper.getMainLooper()).postDelayed({ form.dismiss() }, 2000)
679+
},
680+
) {
681+
Text(text = "Auto-dismiss Form (2s)")
682+
}
683+
}
684+
685+
// Send feedback programmatically without showing a form
686+
item(span = { GridItemSpan(maxLineSpan) }) {
687+
Button(
688+
modifier = Modifier,
689+
onClick = {
690+
val feedback =
691+
Feedback("The app crashed when I tapped the button").apply {
692+
name = "Jane Doe"
693+
contactEmail = "jane@example.com"
694+
url = "https://example.com/page"
695+
}
696+
val eventId = Sentry.feedback().capture(feedback)
697+
Toast.makeText(activity, "Feedback sent: $eventId", Toast.LENGTH_SHORT).show()
698+
},
699+
) {
700+
Text(text = "Send Feedback (no form)")
701+
}
702+
}
703+
704+
// Enable shake-to-show for a specific form instance
705+
item(span = { GridItemSpan(maxLineSpan) }) {
706+
Button(
707+
modifier = Modifier,
708+
onClick = {
709+
SentryUserFeedbackForm.Builder(activity)
710+
.configurator { options ->
711+
options.isUseShakeGesture = true
712+
options.formTitle = "Shake Feedback"
713+
}
714+
.create()
715+
Toast.makeText(activity, "Shake your device to open the form!", Toast.LENGTH_SHORT).show()
716+
},
717+
) {
718+
Text(text = "Enable Shake-to-Show")
719+
}
720+
}
620721
}
621722
}
622723

0 commit comments

Comments
 (0)