1+ // Adapted from Square's Seismic library.
2+ // Copyright 2010 Square, Inc.
3+ // Licensed under the Apache License, Version 2.0.
4+ // https://github.com/square/seismic
15package io .sentry .android .core ;
26
37import android .content .Context ;
711import android .hardware .SensorManager ;
812import android .os .Handler ;
913import android .os .HandlerThread ;
10- import android .os .SystemClock ;
1114import io .sentry .ILogger ;
1215import io .sentry .SentryLevel ;
13- import java .util .concurrent .atomic .AtomicLong ;
1416import org .jetbrains .annotations .ApiStatus ;
1517import org .jetbrains .annotations .NotNull ;
1618import org .jetbrains .annotations .Nullable ;
2123 * <p>The accelerometer sensor (TYPE_ACCELEROMETER) does NOT require any special permissions on
2224 * Android. The BODY_SENSORS permission is only needed for heart rate and similar body sensors.
2325 *
24- * <p>Requires at least {@link #SHAKE_COUNT_THRESHOLD} accelerometer readings above {@link
25- * #SHAKE_THRESHOLD_GRAVITY} within {@link #SHAKE_WINDOW_MS} to trigger a shake event .
26+ * <p>Uses a rolling sample window: if more than 75% of accelerometer readings in the past 0.5s
27+ * exceed {@link #ACCELERATION_THRESHOLD}, a shake is detected. Based on Square's Seismic library .
2628 *
2729 * <p>Sensor events are delivered on a background {@link HandlerThread} to avoid polluting the main
2830 * thread.
2931 */
3032@ ApiStatus .Internal
3133public final class SentryShakeDetector implements SensorEventListener {
3234
33- private static final float SHAKE_THRESHOLD_GRAVITY = 2.7f ;
34- private static final int SHAKE_WINDOW_MS = 1500 ;
35- private static final int SHAKE_COUNT_THRESHOLD = 2 ;
36- private static final int SHAKE_COOLDOWN_MS = 1000 ;
35+ static final int ACCELERATION_THRESHOLD = 13 ;
3736
3837 private @ Nullable SensorManager sensorManager ;
3938 private @ Nullable Sensor accelerometer ;
4039 private @ Nullable HandlerThread handlerThread ;
4140 private @ Nullable Handler handler ;
42- private final @ NotNull AtomicLong lastShakeTimestamp = new AtomicLong (0 );
4341 private volatile @ Nullable Listener listener ;
4442 private @ NotNull ILogger logger ;
4543
46- private int shakeCount = 0 ;
47- private long firstShakeTimestamp = 0 ;
44+ private final @ NotNull SampleQueue queue = new SampleQueue ();
4845
4946 public interface Listener {
5047 void onShake ();
@@ -94,8 +91,7 @@ public void start(final @NotNull Context context, final @NotNull Listener shakeL
9491
9592 public void stop () {
9693 listener = null ;
97- shakeCount = 0 ;
98- firstShakeTimestamp = 0 ;
94+ queue .clear ();
9995 if (sensorManager != null ) {
10096 sensorManager .unregisterListener (this );
10197 }
@@ -116,32 +112,17 @@ public void onSensorChanged(final @NotNull SensorEvent event) {
116112 if (event .sensor .getType () != Sensor .TYPE_ACCELEROMETER ) {
117113 return ;
118114 }
119- float gX = event .values [0 ] / SensorManager .GRAVITY_EARTH ;
120- float gY = event .values [1 ] / SensorManager .GRAVITY_EARTH ;
121- float gZ = event .values [2 ] / SensorManager .GRAVITY_EARTH ;
122- double gForceSquared = gX * gX + gY * gY + gZ * gZ ;
123- if (gForceSquared > SHAKE_THRESHOLD_GRAVITY * SHAKE_THRESHOLD_GRAVITY ) {
124- long now = SystemClock .elapsedRealtime ();
125-
126- // Reset counter if outside the detection window
127- if (now - firstShakeTimestamp > SHAKE_WINDOW_MS ) {
128- shakeCount = 0 ;
129- firstShakeTimestamp = now ;
130- }
131-
132- shakeCount ++;
133-
134- if (shakeCount >= SHAKE_COUNT_THRESHOLD ) {
135- // Enforce cooldown so we don't fire repeatedly
136- long lastShake = lastShakeTimestamp .get ();
137- if (now - lastShake > SHAKE_COOLDOWN_MS ) {
138- lastShakeTimestamp .set (now );
139- shakeCount = 0 ;
140- final @ Nullable Listener currentListener = listener ;
141- if (currentListener != null ) {
142- currentListener .onShake ();
143- }
144- }
115+ final float ax = event .values [0 ];
116+ final float ay = event .values [1 ];
117+ final float az = event .values [2 ];
118+ final boolean accelerating = Math .sqrt (ax * ax + ay * ay + az * az ) > ACCELERATION_THRESHOLD ;
119+
120+ queue .add (event .timestamp , accelerating );
121+ if (queue .isShaking ()) {
122+ queue .clear ();
123+ final @ Nullable Listener currentListener = listener ;
124+ if (currentListener != null ) {
125+ currentListener .onShake ();
145126 }
146127 }
147128 }
@@ -150,4 +131,96 @@ public void onSensorChanged(final @NotNull SensorEvent event) {
150131 public void onAccuracyChanged (final @ NotNull Sensor sensor , final int accuracy ) {
151132 // Not needed for shake detection.
152133 }
134+
135+ static class SampleQueue {
136+ private static final long MAX_WINDOW_SIZE_NS = 500_000_000L ; // 0.5s
137+ private static final long MIN_WINDOW_SIZE_NS = MAX_WINDOW_SIZE_NS >> 1 ; // 0.25s
138+ private static final int MIN_QUEUE_SIZE = 4 ;
139+
140+ private final @ NotNull SamplePool pool = new SamplePool ();
141+ private @ Nullable Sample oldest ;
142+ private @ Nullable Sample newest ;
143+ private int sampleCount ;
144+ private int acceleratingCount ;
145+
146+ void add (final long timestamp , final boolean accelerating ) {
147+ purge (timestamp - MAX_WINDOW_SIZE_NS );
148+
149+ final @ NotNull Sample added = pool .acquire ();
150+ added .timestamp = timestamp ;
151+ added .accelerating = accelerating ;
152+ added .next = null ;
153+ if (newest != null ) {
154+ newest .next = added ;
155+ }
156+ newest = added ;
157+ if (oldest == null ) {
158+ oldest = added ;
159+ }
160+
161+ sampleCount ++;
162+ if (accelerating ) {
163+ acceleratingCount ++;
164+ }
165+ }
166+
167+ void clear () {
168+ while (oldest != null ) {
169+ final @ NotNull Sample removed = oldest ;
170+ oldest = removed .next ;
171+ pool .release (removed );
172+ }
173+ newest = null ;
174+ sampleCount = 0 ;
175+ acceleratingCount = 0 ;
176+ }
177+
178+ private void purge (final long cutoff ) {
179+ while (sampleCount >= MIN_QUEUE_SIZE && oldest != null && cutoff - oldest .timestamp > 0 ) {
180+ final @ NotNull Sample removed = oldest ;
181+ if (removed .accelerating ) {
182+ acceleratingCount --;
183+ }
184+ sampleCount --;
185+ oldest = removed .next ;
186+ if (oldest == null ) {
187+ newest = null ;
188+ }
189+ pool .release (removed );
190+ }
191+ }
192+
193+ boolean isShaking () {
194+ return newest != null
195+ && oldest != null
196+ && newest .timestamp - oldest .timestamp >= MIN_WINDOW_SIZE_NS
197+ && acceleratingCount >= (sampleCount >> 1 ) + (sampleCount >> 2 );
198+ }
199+ }
200+
201+ static class Sample {
202+ long timestamp ;
203+ boolean accelerating ;
204+ @ Nullable Sample next ;
205+ }
206+
207+ static class SamplePool {
208+ private @ Nullable Sample head ;
209+
210+ @ NotNull
211+ Sample acquire () {
212+ Sample acquired = head ;
213+ if (acquired == null ) {
214+ acquired = new Sample ();
215+ } else {
216+ head = acquired .next ;
217+ }
218+ return acquired ;
219+ }
220+
221+ void release (final @ NotNull Sample sample ) {
222+ sample .next = head ;
223+ head = sample ;
224+ }
225+ }
153226}
0 commit comments