Skip to content

Commit 19aaf3f

Browse files
authored
Merge 6c0bd80 into 8558cac
2 parents 8558cac + 6c0bd80 commit 19aaf3f

5 files changed

Lines changed: 223 additions & 56 deletions

File tree

AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,17 @@ The repository is organized into multiple modules:
144144
4. New features must be **opt-in by default** - extend `SentryOptions` or similar Option classes with getters/setters
145145
5. Consider backwards compatibility
146146

147+
### Third-Party Code Attribution
148+
When adapting code from third-party libraries:
149+
1. Add a license header at the top of the adapted file (before the `package` statement):
150+
```java
151+
// Adapted from <Library Name>.
152+
// Copyright <year> <copyright holder>.
153+
// Licensed under the <License Name>.
154+
// <source URL>
155+
```
156+
2. Add a full attribution entry to `THIRD_PARTY_NOTICES.md` following the existing format (Source, License, Copyright, Scope, full license text)
157+
147158
### Getting PR Information
148159

149160
Use `gh pr view` to get PR details from the current branch. This is needed when adding changelog entries, which require the PR number.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
### Fixes
2424

2525
- Fix soft input keyboard not being shown on the Feedback form ([#5359](https://github.com/getsentry/sentry-java/pull/5359))
26+
- Fix shake-to-report not triggering on some devices due to high acceleration threshold ([#5366](https://github.com/getsentry/sentry-java/pull/5366))
2627

2728
### Dependencies
2829

THIRD_PARTY_NOTICES.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,34 @@ limitations under the License.
118118

119119
---
120120

121+
## Square — Seismic (Apache 2.0)
122+
123+
**Source:** https://github.com/square/seismic<br>
124+
**License:** Apache License 2.0<br>
125+
**Copyright:** Copyright 2010 Square, Inc.
126+
127+
### Scope
128+
129+
The Sentry Java SDK includes an adapted version of Square's Seismic shake detection algorithm. The rolling sample window approach and `SampleQueue`/`SamplePool` data structures in `io.sentry.android.core.SentryShakeDetector` are based on Seismic's `ShakeDetector`.
130+
131+
```
132+
Copyright 2010 Square, Inc.
133+
134+
Licensed under the Apache License, Version 2.0 (the "License");
135+
you may not use this file except in compliance with the License.
136+
You may obtain a copy of the License at
137+
138+
http://www.apache.org/licenses/LICENSE-2.0
139+
140+
Unless required by applicable law or agreed to in writing, software
141+
distributed under the License is distributed on an "AS IS" BASIS,
142+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
143+
See the License for the specific language governing permissions and
144+
limitations under the License.
145+
```
146+
147+
---
148+
121149
## Square — Curtains (Apache 2.0)
122150

123151
**Source:** https://github.com/square/curtains (v1.2.5)<br>

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

Lines changed: 112 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
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
15
package io.sentry.android.core;
26

37
import android.content.Context;
@@ -7,10 +11,8 @@
711
import android.hardware.SensorManager;
812
import android.os.Handler;
913
import android.os.HandlerThread;
10-
import android.os.SystemClock;
1114
import io.sentry.ILogger;
1215
import io.sentry.SentryLevel;
13-
import java.util.concurrent.atomic.AtomicLong;
1416
import org.jetbrains.annotations.ApiStatus;
1517
import org.jetbrains.annotations.NotNull;
1618
import org.jetbrains.annotations.Nullable;
@@ -21,30 +23,25 @@
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
3133
public 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

Comments
 (0)