Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Features

- Add `Sentry.enableFeedbackOnShake()` and `Sentry.disableFeedbackOnShake()` for runtime control of shake-to-report ([#5232](https://github.com/getsentry/sentry-java/pull/5232))
- Add configurable `IScopesStorageFactory` to `SentryOptions` for providing a custom `IScopesStorage`, e.g. when the default `ThreadLocal`-backed storage is incompatible with non-pinning thread models ([#5199](https://github.com/getsentry/sentry-java/pull/5199))
- Android: Add `beforeErrorSampling` callback to Session Replay ([#5214](https://github.com/getsentry/sentry-java/pull/5214))
- Allows filtering which errors trigger replay capture before the `onErrorSampleRate` is checked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,17 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
: null,
"SentryAndroidOptions is required");

if (!this.options.getFeedbackOptions().isUseShakeGesture()) {
return;
}

shakeDetector.init(application, options.getLogger());

addIntegrationToSdkVersion("FeedbackShake");
application.registerActivityLifecycleCallbacks(this);
options.getLogger().log(SentryLevel.DEBUG, "FeedbackShakeIntegration installed.");

// In case of a deferred init, hook into any already-resumed activity
final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
if (activity != null) {
currentActivityRef = new WeakReference<>(activity);
startShakeDetection(activity);
if (this.options.getFeedbackOptions().isUseShakeGesture()) {
// In case of a deferred init, hook into any already-resumed activity
final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
if (activity != null) {
currentActivityRef = new WeakReference<>(activity);
startShakeDetection(activity);
}
}
}

Expand Down Expand Up @@ -92,7 +88,12 @@ public void onActivityResumed(final @NotNull Activity activity) {
previousOnFormClose = null;
}
currentActivityRef = new WeakReference<>(activity);
startShakeDetection(activity);
// Check dynamically so the flag can be toggled at runtime
if (options != null && options.getFeedbackOptions().isUseShakeGesture()) {
startShakeDetection(activity);
} else {
stopShakeDetection();
}
}

@Override
Expand Down Expand Up @@ -146,6 +147,8 @@ private void startShakeDetection(final @NotNull Activity activity) {
if (options == null) {
return;
}
// Initialize sensor and thread if not already done (idempotent)
shakeDetector.init(application, options.getLogger());
// Stop any existing detection (e.g. when transitioning between activities)
stopShakeDetection();
shakeDetector.start(
Expand All @@ -156,6 +159,7 @@ private void startShakeDetection(final @NotNull Activity activity) {
final Boolean inBackground = AppState.getInstance().isInBackground();
if (active != null
&& options != null
&& options.getFeedbackOptions().isUseShakeGesture()
&& !isDialogShowing
&& !Boolean.TRUE.equals(inBackground)) {
active.runOnUiThread(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import kotlin.test.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

Expand Down Expand Up @@ -50,11 +49,11 @@ class FeedbackShakeIntegrationTest {
}

@Test
fun `when useShakeGesture is disabled does not register activity lifecycle callbacks`() {
fun `when useShakeGesture is disabled still registers activity lifecycle callbacks for runtime toggle`() {
val sut = fixture.getSut(useShakeGesture = false)
sut.register(fixture.scopes, fixture.options)

verify(fixture.application, never()).registerActivityLifecycleCallbacks(any())
verify(fixture.application).registerActivityLifecycleCallbacks(any())
}

@Test
Expand Down Expand Up @@ -103,4 +102,34 @@ class FeedbackShakeIntegrationTest {
val sut = fixture.getSut()
sut.close()
}

@Test
fun `enabling shake at runtime starts detection on next activity resume`() {
val sut = fixture.getSut(useShakeGesture = false)
sut.register(fixture.scopes, fixture.options)

whenever(fixture.activity.getSystemService(any())).thenReturn(null)

// Shake is disabled, onActivityResumed should not crash
sut.onActivityResumed(fixture.activity)

// Now enable at runtime and resume a new activity
fixture.options.feedbackOptions.isUseShakeGesture = true
sut.onActivityResumed(fixture.activity)
// Should not throw โ€” shake detection attempted (fails gracefully with null SensorManager)
}

@Test
fun `disabling shake at runtime stops detection on next activity resume`() {
val sut = fixture.getSut(useShakeGesture = true)
sut.register(fixture.scopes, fixture.options)

whenever(fixture.activity.getSystemService(any())).thenReturn(null)
sut.onActivityResumed(fixture.activity)

// Disable at runtime and resume
fixture.options.feedbackOptions.isUseShakeGesture = false
sut.onActivityResumed(fixture.activity)
// Should not throw โ€” detection stopped gracefully
}
}
2 changes: 2 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -2705,7 +2705,9 @@ public final class io/sentry/Sentry {
public static fun configureScope (Lio/sentry/ScopeCallback;)V
public static fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V
public static fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext;
public static fun disableFeedbackOnShake ()V
public static fun distribution ()Lio/sentry/IDistributionApi;
public static fun enableFeedbackOnShake ()V
public static fun endSession ()V
public static fun flush (J)V
public static fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes;
Expand Down
18 changes: 18 additions & 0 deletions sentry/src/main/java/io/sentry/Sentry.java
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,24 @@ public static void showUserFeedbackDialog(
options.getFeedbackOptions().getDialogHandler().showDialog(associatedEventId, configurator);
}

/**
* Enables shake-to-report for user feedback. When enabled, shaking the device will show the
* feedback dialog. Takes effect on the next activity transition. This can be toggled at runtime.
*/
@ApiStatus.Experimental
public static void enableFeedbackOnShake() {
getCurrentScopes().getOptions().getFeedbackOptions().setUseShakeGesture(true);
}

/**
* Disables shake-to-report for user feedback. Takes effect on the next activity transition. If a
* shake occurs before the transition, the dialog will not be shown.
*/
@ApiStatus.Experimental
public static void disableFeedbackOnShake() {
getCurrentScopes().getOptions().getFeedbackOptions().setUseShakeGesture(false);
}

/**
* Sets an attribute on the scope.
*
Expand Down
Loading