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,11 +91,12 @@ 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 ;
9994 if (sensorManager != null ) {
10095 sensorManager .unregisterListener (this );
10196 }
97+ synchronized (queue ) {
98+ queue .clear ();
99+ }
102100 }
103101
104102 /** Stops detection and releases the background thread. */
@@ -116,38 +114,120 @@ public void onSensorChanged(final @NotNull SensorEvent event) {
116114 if (event .sensor .getType () != Sensor .TYPE_ACCELEROMETER ) {
117115 return ;
118116 }
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- }
117+ final float ax = event .values [0 ];
118+ final float ay = event .values [1 ];
119+ final float az = event .values [2 ];
120+ final boolean accelerating = Math .sqrt (ax * ax + ay * ay + az * az ) > ACCELERATION_THRESHOLD ;
121+
122+ final @ Nullable Listener currentListener ;
123+ synchronized (queue ) {
124+ queue .add (event .timestamp , accelerating );
125+ if (queue .isShaking ()) {
126+ queue .clear ();
127+ currentListener = listener ;
128+ } else {
129+ currentListener = null ;
145130 }
146131 }
132+ if (currentListener != null ) {
133+ currentListener .onShake ();
134+ }
147135 }
148136
149137 @ Override
150138 public void onAccuracyChanged (final @ NotNull Sensor sensor , final int accuracy ) {
151139 // Not needed for shake detection.
152140 }
141+
142+ static class SampleQueue {
143+ private static final long MAX_WINDOW_SIZE_NS = 500_000_000L ; // 0.5s
144+ private static final long MIN_WINDOW_SIZE_NS = MAX_WINDOW_SIZE_NS >> 1 ; // 0.25s
145+ private static final int MIN_QUEUE_SIZE = 4 ;
146+
147+ private final @ NotNull SamplePool pool = new SamplePool ();
148+ private @ Nullable Sample oldest ;
149+ private @ Nullable Sample newest ;
150+ private int sampleCount ;
151+ private int acceleratingCount ;
152+
153+ void add (final long timestamp , final boolean accelerating ) {
154+ purge (timestamp - MAX_WINDOW_SIZE_NS );
155+
156+ final @ NotNull Sample added = pool .acquire ();
157+ added .timestamp = timestamp ;
158+ added .accelerating = accelerating ;
159+ added .next = null ;
160+ if (newest != null ) {
161+ newest .next = added ;
162+ }
163+ newest = added ;
164+ if (oldest == null ) {
165+ oldest = added ;
166+ }
167+
168+ sampleCount ++;
169+ if (accelerating ) {
170+ acceleratingCount ++;
171+ }
172+ }
173+
174+ void clear () {
175+ while (oldest != null ) {
176+ final @ NotNull Sample removed = oldest ;
177+ oldest = removed .next ;
178+ pool .release (removed );
179+ }
180+ newest = null ;
181+ sampleCount = 0 ;
182+ acceleratingCount = 0 ;
183+ }
184+
185+ private void purge (final long cutoff ) {
186+ while (sampleCount >= MIN_QUEUE_SIZE && oldest != null && cutoff - oldest .timestamp > 0 ) {
187+ final @ NotNull Sample removed = oldest ;
188+ if (removed .accelerating ) {
189+ acceleratingCount --;
190+ }
191+ sampleCount --;
192+ oldest = removed .next ;
193+ if (oldest == null ) {
194+ newest = null ;
195+ }
196+ pool .release (removed );
197+ }
198+ }
199+
200+ boolean isShaking () {
201+ return newest != null
202+ && oldest != null
203+ && newest .timestamp - oldest .timestamp >= MIN_WINDOW_SIZE_NS
204+ && acceleratingCount >= (sampleCount >> 1 ) + (sampleCount >> 2 );
205+ }
206+ }
207+
208+ static class Sample {
209+ long timestamp ;
210+ boolean accelerating ;
211+ @ Nullable Sample next ;
212+ }
213+
214+ static class SamplePool {
215+ private @ Nullable Sample head ;
216+
217+ @ NotNull
218+ Sample acquire () {
219+ Sample acquired = head ;
220+ if (acquired == null ) {
221+ acquired = new Sample ();
222+ } else {
223+ head = acquired .next ;
224+ }
225+ return acquired ;
226+ }
227+
228+ void release (final @ NotNull Sample sample ) {
229+ sample .next = head ;
230+ head = sample ;
231+ }
232+ }
153233}
0 commit comments