Skip to content

Commit d1e979c

Browse files
authored
Merge 973955f into e63ad34
2 parents e63ad34 + 973955f commit d1e979c

4 files changed

Lines changed: 223 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
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))
1516

1617
### Dependencies
1718

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

Lines changed: 120 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,118 @@ 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(activity, () -> {
74+
final @Nullable Activity active = activityRef.get();
75+
if (active != null && !active.isFinishing() && !active.isDestroyed()) {
76+
active.runOnUiThread(() -> {
77+
if (!active.isFinishing() && !active.isDestroyed()) {
78+
show();
79+
}
80+
});
81+
}
82+
});
83+
final @NotNull Application app = activity.getApplication();
84+
shakeLifecycleCallbacks = new ShakeLifecycleCallbacks(activityRef);
85+
app.registerActivityLifecycleCallbacks(shakeLifecycleCallbacks);
86+
}
87+
88+
private void stopShakeDetection() {
89+
if (shakeDetector != null) {
90+
shakeDetector.close();
91+
shakeDetector = null;
92+
}
93+
if (shakeLifecycleCallbacks != null) {
94+
final @Nullable Activity activity = getActivity(getContext());
95+
if (activity != null) {
96+
activity.getApplication().unregisterActivityLifecycleCallbacks(shakeLifecycleCallbacks);
97+
}
98+
shakeLifecycleCallbacks = null;
99+
}
100+
}
101+
102+
private static @Nullable Activity getActivity(final @NotNull Context context) {
103+
Context current = context;
104+
while (current instanceof ContextWrapper) {
105+
if (current instanceof Activity) {
106+
return (Activity) current;
107+
}
108+
current = ((ContextWrapper) current).getBaseContext();
109+
}
110+
return null;
111+
}
112+
113+
private class ShakeLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
114+
private final @NotNull WeakReference<Activity> activityRef;
115+
116+
ShakeLifecycleCallbacks(final @NotNull WeakReference<Activity> activityRef) {
117+
this.activityRef = activityRef;
118+
}
119+
120+
@Override
121+
public void onActivityResumed(final @NotNull Activity activity) {
122+
if (activity == activityRef.get() && shakeDetector != null) {
123+
shakeDetector.start(activity, () -> {
124+
final @Nullable Activity active = activityRef.get();
125+
if (active != null && !active.isFinishing() && !active.isDestroyed()) {
126+
active.runOnUiThread(() -> {
127+
if (!active.isFinishing() && !active.isDestroyed()) {
128+
show();
129+
}
130+
});
131+
}
132+
});
133+
}
134+
}
135+
136+
@Override
137+
public void onActivityPaused(final @NotNull Activity activity) {
138+
if (activity == activityRef.get() && shakeDetector != null) {
139+
shakeDetector.stop();
140+
}
141+
}
142+
143+
@Override
144+
public void onActivityDestroyed(final @NotNull Activity activity) {
145+
if (activity == activityRef.get()) {
146+
stopShakeDetection();
147+
}
148+
}
149+
150+
@Override
151+
public void onActivityCreated(
152+
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {}
153+
@Override
154+
public void onActivityStarted(final @NotNull Activity activity) {}
155+
@Override
156+
public void onActivityStopped(final @NotNull Activity activity) {}
157+
@Override
158+
public void onActivitySaveInstanceState(
159+
final @NotNull Activity activity, final @NotNull Bundle outState) {}
45160
}
46161

47162
@Override
@@ -57,14 +172,7 @@ protected void onCreate(Bundle savedInstanceState) {
57172
setContentView(R.layout.sentry_dialog_user_feedback);
58173
setCancelable(isCancelable);
59174

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-
}
175+
final @NotNull SentryFeedbackOptions feedbackOptions = resolvedFeedbackOptions;
68176
final @NotNull TextView lblTitle = findViewById(R.id.sentry_dialog_user_feedback_title);
69177
final @NotNull ImageView imgLogo = findViewById(R.id.sentry_dialog_user_feedback_logo);
70178
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: 101 additions & 2 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,10 @@ 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
8386
import io.sentry.compose.SentryUserFeedbackButton
87+
import io.sentry.protocol.Feedback
8488
import io.sentry.protocol.User
8589
import java.io.File
8690
import java.io.FileOutputStream
@@ -615,8 +619,103 @@ fun UserFeedbackScreen() {
615619
}
616620
}
617621

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

0 commit comments

Comments
 (0)