Commit 28b6988
feat(feedback): implement shake gesture detection (#5150)
* feat(feedback): implement shake gesture detection for user feedback form
Adds SentryShakeDetector (accelerometer-based) and ShakeDetectionIntegration
that shows the feedback dialog when a shake is detected. Controlled by
SentryFeedbackOptions.useShakeGesture (default false).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): improve shake detection robustness and add tests
- Add volatile/AtomicLong for thread-safe cross-thread field access
- Use SystemClock.elapsedRealtime() instead of System.currentTimeMillis()
- Use SENSOR_DELAY_NORMAL for better battery efficiency
- Add multi-shake counting (2+ threshold crossings within 1.5s window)
- Handle deferred init for already-resumed activities
- Wrap showDialog() in try-catch to prevent app crashes
- Improve activity transition handling in onActivityPaused
- Mark SentryShakeDetector as @ApiStatus.Internal
- Add unit tests for SentryShakeDetector and ShakeDetectionIntegration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): prevent stacking multiple feedback dialogs on repeated shakes
Track dialog visibility with an isDialogShowing flag that is set before
showing and cleared via the onFormClose callback when the dialog is
dismissed. Double-checked on both sensor and UI threads to avoid races.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): restore original onFormClose to prevent callback chain growth
Save the user's original onFormClose once during register() and restore
it after each dialog dismiss, instead of wrapping it with a new lambda
each time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): reset isDialogShowing on activity pause to prevent stuck flag
If showDialog silently fails (e.g. activity destroyed between post and
execution), isDialogShowing would stay true forever, permanently
disabling shake-to-feedback. Reset it in onActivityPaused since the
dialog cannot outlive its host activity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): move isDialogShowing reset from onActivityPaused to onActivityDestroyed
AlertDialog survives pause/resume cycles (e.g. screen off/on), so
resetting isDialogShowing in onActivityPaused allowed duplicate dialogs.
Move the reset to onActivityDestroyed where the dialog truly cannot
survive.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): scope dialog flag to hosting activity and restore callback on error
- Only reset isDialogShowing in onActivityDestroyed when it's the
activity that hosts the dialog, not any unrelated activity.
- Restore originalOnFormClose in the catch block when showDialog throws.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Optimise comparison
Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>
* ref(feedback): address review feedback from lucas-zimerman
- Rename ShakeDetectionIntegration to FeedbackShakeIntegration to
clarify its purpose is feedback-specific (#1)
- Avoid Math.sqrt by comparing squared gForce values (#3)
- Null out listener before unregistering sensor to prevent in-flight
callbacks during stop (#4)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): capture onFormClose at shake time instead of registration
Capture the current onFormClose callback just before showing the dialog
rather than caching it during register(). This ensures callbacks set by
the user after SDK init are preserved across shake-triggered dialogs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Reverse sample changes
* fix(feedback): restore onFormClose in onActivityDestroyed fallback path
When the dialog's host activity is destroyed and onDismiss doesn't fire,
onActivityDestroyed now restores the previous onFormClose callback on
global options, preventing a stale wrapper from affecting subsequent
non-shake feedback dialogs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): make previousOnFormClose volatile for thread safety
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): always restore onFormClose in onActivityDestroyed even when null
The previous null check on previousOnFormClose skipped restoration when
no user callback was set, leaving a stale wrapper in global options.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update changelog
* ref(feedback): address review feedback for shake gesture detection
- Reuse single SentryShakeDetector instance across activity transitions
instead of re-creating on every resume (reduces allocations)
- Memoize SensorManager and Sensor lookups to avoid repeated binder calls
- Use getDefaultSensor(TYPE_ACCELEROMETER, false) to avoid wakeup sensor
- Deliver sensor events on a background HandlerThread instead of main thread
- Use SentryUserFeedbackDialog.Builder directly with tracked activity
instead of going through showDialog/CurrentActivityHolder
- Merge dialogActivity into currentActivity, use AppState.isInBackground()
to gate against background shakes
- Fix integration count in SentryAndroidTest (19 -> 20)
Tested manually on Pixel 8 Pro by enabling useShakeGesture in the sample
app's SentryAndroid.init and verifying shake opens the feedback dialog.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Format code
* feat(feedback): add manifest meta-data support for useShakeGesture
Allow enabling shake gesture via AndroidManifest.xml:
<meta-data android:name="io.sentry.feedback.use-shake-gesture" android:value="true" />
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): preserve currentActivity in onActivityPaused when dialog is showing
onActivityPaused always fires before onActivityDestroyed. Without this
fix, currentActivity was set to null in onPause, making the cleanup
condition in onActivityDestroyed (activity == currentActivity) always
false. This left isDialogShowing permanently stuck as true, disabling
shake-to-feedback for the rest of the session.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): pass real logger to SentryShakeDetector on init
The detector was constructed with NoOpLogger and the logger field was
final, so all diagnostic messages (sensor unavailable warnings) were
silently swallowed. Now init(context, logger) updates the logger from
SentryOptions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Format code
* fix(feedback): clear stale activity ref and reset shake state on stop
- Clear currentActivity in onActivityDestroyed to prevent holding a
stale reference to a destroyed activity context
- Reset shakeCount and firstShakeTimestamp in stop() to prevent
cross-session false triggers across pause/resume cycles
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): clean up dialog state when a different activity resumes
When a dialog is showing on Activity A and the user navigates to
Activity B (e.g. via notification), onActivityResumed(B) overwrites
currentActivity. Later onActivityDestroyed(A) can't match and cleanup
never runs, leaving isDialogShowing permanently stuck. Now we detect
this in onActivityResumed and clean up proactively.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): capture onFormClose as local variable in lambda
The onFormClose lambda was reading previousOnFormClose field at dismiss
time. If onActivityResumed or onActivityDestroyed already restored and
nulled the field, the lambda would overwrite onFormClose with null. Now
captured as a local variable at dialog creation time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): restore onFormClose in close() when dialog is showing
When close() is called while a dialog is showing, lifecycle callbacks
are unregistered so onActivityDestroyed cleanup won't fire. Restore
previousOnFormClose and reset dialog state in close() to prevent
the callback from being permanently overwritten.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feedback): check isFinishing/isDestroyed before showing dialog
Add proactive activity validity check inside the runOnUiThread lambda
to avoid hitting the catch block with a BadTokenException when the
activity becomes invalid between the shake callback and UI execution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Format code
* Enable the feature on the demo app for easier testing
* Use a weak reference for activity
* Reuse sentry-shake handler thread
* Add close to the API
* fix(feedback): use instanceof check for SentryAndroidOptions cast
Follow the established defensive pattern used by all other Android
integrations instead of an unchecked cast that could throw
ClassCastException if a hybrid SDK passes a different options type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>1 parent 9c1b406 commit 28b6988
File tree
13 files changed
+716
-1
lines changed- sentry-android-core
- api
- src
- main/java/io/sentry/android/core
- test/java/io/sentry/android/core
- sentry-samples/sentry-samples-android/src/main
- sentry
- api
- src/main/java/io/sentry
13 files changed
+716
-1
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
5 | 11 | | |
6 | 12 | | |
7 | 13 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
269 | 269 | | |
270 | 270 | | |
271 | 271 | | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
272 | 285 | | |
273 | 286 | | |
274 | 287 | | |
| |||
462 | 475 | | |
463 | 476 | | |
464 | 477 | | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
465 | 491 | | |
466 | 492 | | |
467 | 493 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
410 | 410 | | |
411 | 411 | | |
412 | 412 | | |
| 413 | + | |
413 | 414 | | |
414 | 415 | | |
415 | 416 | | |
| |||
Lines changed: 198 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
| 170 | + | |
| 171 | + | |
170 | 172 | | |
171 | 173 | | |
172 | 174 | | |
| |||
661 | 663 | | |
662 | 664 | | |
663 | 665 | | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
664 | 669 | | |
665 | 670 | | |
666 | 671 | | |
| |||
0 commit comments