This seems to have been asked many times, but most of the answers are old and refer to abandoned/retired packages.
In the iPhone version of my app I'm able to use deviceDidShakeNotification (guidance from here) to detect when the phone is shaken. It works really well and I'm hoping to find something on Android (via package if appropriate) to do the same thing.
Most of the answers I find seem to require some maths; and some of the code I've tried doesn't work well, either too sensitive (activates when the phone is picked up), or takes really specific movements to trigger.
I'm sure there are complications here that I'm not fully appreciating, but surely someone has a simple, reliable solution that just works??
In the end I copied the code directly out of the deprecated Seismic library (as suggested in the readme), from here: https://github.com/square/seismic/blob/master/library/src/main/java/com/squareup/seismic/ShakeDetector.java
The code is in Java, but Android Studio did a good job converting it to Kotlin - here is the code I'm using:
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
// Copyright 2010 Square, Inc.
// https://github.com/square/seismic/tree/master
/**
* Detects phone shaking. If more than 75% of the samples taken in the past 0.5s are
* accelerating, the device is a) shaking, or b) free falling 1.84m (h =
* 1/2*g*t^2*3/4).
*
* @author Bob Lee (bob@squareup.com)
* @author Eric Burke (eric@squareup.com)
*/
class ShakeDetector(private val listener: Listener) : SensorEventListener {
/**
* When the magnitude of total acceleration exceeds this
* value, the phone is accelerating.
*/
private var accelerationThreshold = DEFAULT_ACCELERATION_THRESHOLD
/** Listens for shakes. */
interface Listener {
/** Called on the main thread when the device is shaken. */
fun hearShake()
}
private val queue = SampleQueue()
private var sensorManager: SensorManager? = null
private var accelerometer: Sensor? = null
/**
* Starts listening for shakes on devices with appropriate hardware.
* Allowing to set the sensor delay, available values are:
* SENSOR_DELAY_FASTEST, SENSOR_DELAY_GAME, SENSOR_DELAY_UI, SENSOR_DELAY_NORMAL.
* @see [SensorManager](https://developer.android.com/reference/android/hardware/SensorManager)
*
*
* @return true if the device supports shake detection.
*/
/**
* Starts listening for shakes on devices with appropriate hardware.
*
* @return true if the device supports shake detection.
*/
@JvmOverloads fun start(sensorManager: SensorManager, sensorDelay: Int = SensorManager.SENSOR_DELAY_FASTEST): Boolean {
// Already started?
if (accelerometer != null) {
return true
}
accelerometer = sensorManager.getDefaultSensor(
Sensor.TYPE_ACCELEROMETER
)
// If this phone has an accelerometer, listen to it.
if (accelerometer != null) {
this.sensorManager = sensorManager
sensorManager.registerListener(this, accelerometer, sensorDelay)
}
return accelerometer != null
}
/**
* Stops listening. Safe to call when already stopped. Ignored on devices
* without appropriate hardware.
*/
fun stop() {
if (accelerometer != null) {
queue.clear()
sensorManager!!.unregisterListener(this, accelerometer)
sensorManager = null
accelerometer = null
}
}
override fun onSensorChanged(event: SensorEvent) {
val accelerating = isAccelerating(event)
val timestamp = event.timestamp
queue.add(timestamp, accelerating)
if (queue.isShaking) {
queue.clear()
listener.hearShake()
}
}
/** Returns true if the device is currently accelerating. */
private fun isAccelerating(event: SensorEvent): Boolean {
val ax = event.values[0]
val ay = event.values[1]
val az = event.values[2]
// Instead of comparing magnitude to ACCELERATION_THRESHOLD,
// compare their squares. This is equivalent and doesn't need the
// actual magnitude, which would be computed using (expensive) Math.sqrt().
val magnitudeSquared = (ax * ax + ay * ay + az * az).toDouble()
return magnitudeSquared > accelerationThreshold * accelerationThreshold
}
/** Sets the acceleration threshold sensitivity. */
fun setSensitivity(accelerationThreshold: Int) {
this.accelerationThreshold = accelerationThreshold
}
/** Queue of samples. Keeps a running average. */
internal class SampleQueue {
private val pool = SamplePool()
private var oldest: Sample? = null
private var newest: Sample? = null
private var sampleCount = 0
private var acceleratingCount = 0
/**
* Adds a sample.
*
* @param timestamp in nanoseconds of sample
* @param accelerating true if > [.accelerationThreshold].
*/
fun add(timestamp: Long, accelerating: Boolean) {
// Purge samples that proceed window.
purge(timestamp - MAX_WINDOW_SIZE)
// Add the sample to the queue.
val added = pool.acquire()
added.timestamp = timestamp
added.accelerating = accelerating
added.next = null
if (newest != null) {
newest!!.next = added
}
newest = added
if (oldest == null) {
oldest = added
}
// Update running average.
sampleCount++
if (accelerating) {
acceleratingCount++
}
}
/** Removes all samples from this queue. */
fun clear() {
while (oldest != null) {
val removed: Sample = oldest as Sample
oldest = removed.next
pool.release(removed)
}
newest = null
sampleCount = 0
acceleratingCount = 0
}
/** Purges samples with timestamps older than cutoff. */
fun purge(cutoff: Long) {
while (sampleCount >= MIN_QUEUE_SIZE && oldest != null && cutoff - oldest!!.timestamp > 0) {
// Remove sample.
val removed: Sample = oldest as Sample
if (removed.accelerating) {
acceleratingCount--
}
sampleCount--
oldest = removed.next
if (oldest == null) {
newest = null
}
pool.release(removed)
}
}
/** Copies the samples into a list, with the oldest entry at index 0. */
fun asList(): List<Sample> {
val list: MutableList<Sample> = ArrayList()
var s = oldest
while (s != null) {
list.add(s)
s = s.next
}
return list
}
val isShaking: Boolean
/**
* Returns true if we have enough samples and more than 3/4 of those samples
* are accelerating.
*/
get() = newest != null && oldest != null && newest!!.timestamp - oldest!!.timestamp >= MIN_WINDOW_SIZE && acceleratingCount >= (sampleCount shr 1) + (sampleCount shr 2)
companion object {
/** Window size in ns. Used to compute the average. */
private const val MAX_WINDOW_SIZE: Long = 500000000 // 0.5s
private const val MIN_WINDOW_SIZE = MAX_WINDOW_SIZE shr 1 // 0.25s
/**
* Ensure the queue size never falls below this size, even if the device
* fails to deliver this many events during the time window. The LG Ally
* is one such device.
*/
private const val MIN_QUEUE_SIZE = 4
}
}
/** An accelerometer sample. */
internal class Sample {
/** Time sample was taken. */
var timestamp: Long = 0
/** If acceleration > [.accelerationThreshold]. */
var accelerating = false
/** Next sample in the queue or pool. */
var next: Sample? = null
}
/** Pools samples. Avoids garbage collection. */
internal class SamplePool {
private var head: Sample? = null
/** Acquires a sample from the pool. */
fun acquire(): Sample {
var acquired = head
if (acquired == null) {
acquired = Sample()
} else {
// Remove instance from pool.
head = acquired.next
}
return acquired
}
/** Returns a sample to the pool. */
fun release(sample: Sample) {
sample.next = head
head = sample
}
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
companion object {
const val SENSITIVITY_LIGHT = 11
const val SENSITIVITY_MEDIUM = 13
const val SENSITIVITY_HARD = 15
private const val DEFAULT_ACCELERATION_THRESHOLD = SENSITIVITY_MEDIUM
}
}
Whilst Nguyễn Minh Khoa answer didn't really work, it was a massive help to know how to call the above class:
DisposableEffect(Unit) {
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val shakeDetector = ShakeDetector(object: ShakeDetector.Listener {
override fun hearShake() {
// device shaken
}
})
shakeDetector.start(sensorManager, SensorManager.SENSOR_DELAY_UI)
onDispose {
shakeDetector.stop()
}
}
SensorManager.SENSOR_DELAY_UI
seemed to work well for me; worth noting that SENSOR_DELAY_FASTEST
requires extra permissions on newer devices, so I avoided it!