Skip to content

Commit fff6dca

Browse files
committed
add shake detector
1 parent 4f09830 commit fff6dca

File tree

7 files changed

+257
-35
lines changed

7 files changed

+257
-35
lines changed

README.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,17 @@ Pick a UI implementation and add the dependency:
2727

2828
````java
2929
dependencies {
30-
debugImplementation 'com.github.kernel0x.finch:ui-drawer:2.2.12'
31-
releaseImplementation 'com.github.kernel0x.finch:noop:2.2.12'
30+
debugImplementation 'com.github.kernel0x.finch:ui-drawer:2.3.0'
31+
releaseImplementation 'com.github.kernel0x.finch:noop:2.3.0'
3232
// optional only for OkHttp
33-
debugImplementation 'com.github.kernel0x.finch:log-okhttp:2.2.12'
34-
releaseImplementation 'com.github.kernel0x.finch:log-okhttp-noop:2.2.12'
33+
debugImplementation 'com.github.kernel0x.finch:log-okhttp:2.3.0'
34+
releaseImplementation 'com.github.kernel0x.finch:log-okhttp-noop:2.3.0'
3535
// optional only for GRPC
36-
debugImplementation 'com.github.kernel0x.finch:log-grpc:2.2.12'
37-
releaseImplementation 'com.github.kernel0x.finch:log-grpc-noop:2.2.12'
36+
debugImplementation 'com.github.kernel0x.finch:log-grpc:2.3.0'
37+
releaseImplementation 'com.github.kernel0x.finch:log-grpc-noop:2.3.0'
3838
// optional only for logs
39-
debugImplementation 'com.github.kernel0x.finch:log:2.2.12'
40-
releaseImplementation 'com.github.kernel0x.finch:log-noop:2.2.12'
39+
debugImplementation 'com.github.kernel0x.finch:log:2.3.0'
40+
releaseImplementation 'com.github.kernel0x.finch:log-noop:2.3.0'
4141
}
4242
````
4343

common/src/main/java/com/kernel/finch/common/models/Configuration.kt

+2-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import java.util.*
99

1010
data class Configuration(
1111
@StyleRes val themeResourceId: Int? = DEFAULT_THEME_RESOURCE_ID,
12-
val shakeThreshold: Int? = DEFAULT_SHAKE_THRESHOLD,
13-
val shakeHapticFeedbackDuration: Long = DEFAULT_HAPTIC_FEEDBACK_DURATION,
12+
val shakeDetection: Boolean = DEFAULT_SHAKE_DETECTION,
1413
val excludedPackageNames: List<String> = DEFAULT_EXCLUDED_PACKAGE_NAMES,
1514
val logger: FinchLogger? = DEFAULT_LOGGER,
1615
val networkLoggers: List<FinchNetworkLogger> = DEFAULT_NETWORK_LOGGERS,
@@ -31,9 +30,8 @@ data class Configuration(
3130
val applyInsets: ((windowInsets: Inset) -> Inset)? = DEFAULT_APPLY_INSETS
3231
) {
3332
companion object {
34-
private const val DEFAULT_SHAKE_THRESHOLD = 13
3533
private const val DEFAULT_SHOW_NOTIFICATION_NETWORK_LOGGERS = true
36-
private const val DEFAULT_HAPTIC_FEEDBACK_DURATION = 100L
34+
private const val DEFAULT_SHAKE_DETECTION = true
3735
private val DEFAULT_THEME_RESOURCE_ID: Int? = null
3836
private val DEFAULT_EXCLUDED_PACKAGE_NAMES = emptyList<String>()
3937
private val DEFAULT_LOGGER: FinchLogger? = null

core/src/main/java/com/kernel/finch/core/FinchImpl.kt

+11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.kernel.finch.core
22

33
import android.app.Application
4+
import android.content.Context.SENSOR_SERVICE
45
import android.graphics.Canvas
6+
import android.hardware.SensorManager
57
import android.net.Uri
68
import androidx.fragment.app.FragmentActivity
79
import androidx.lifecycle.Lifecycle
@@ -92,6 +94,15 @@ class FinchImpl(val uiManager: UiManager) : Finch {
9294
this.notificationManager = NotificationManager(application)
9395
this.networkLogDao = FinchDatabase.getInstance(application).networkLog()
9496
this.retentionManager = RetentionManager(application, Period.ONE_WEEK)
97+
if (configuration.shakeDetection) {
98+
(application.getSystemService(SENSOR_SERVICE) as SensorManager?)?.let {
99+
ShakeDetectorManager {
100+
show()
101+
}.apply {
102+
start(it)
103+
}
104+
}
105+
}
95106
debugMenuInjector.register(application)
96107
configuration.logger?.register(::log, ::clearLogs)
97108
configuration.networkLoggers.forEach { it.register(::logNetworkEvent, ::clearNetworkLogs) }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package com.kernel.finch.core.manager
2+
3+
import android.hardware.Sensor
4+
import android.hardware.SensorEvent
5+
import android.hardware.SensorEventListener
6+
import android.hardware.SensorManager
7+
import java.util.*
8+
9+
private const val SENSITIVITY_LIGHT = 11
10+
private const val SENSITIVITY_MEDIUM = 13
11+
private const val SENSITIVITY_HARD = 15
12+
private const val DEFAULT_ACCELERATION_THRESHOLD = SENSITIVITY_MEDIUM
13+
14+
/**
15+
* Detects phone shaking. If more than 75% of the samples taken in the past 0.5s are
16+
* accelerating, the device is a) shaking, or b) free falling 1.84m (h =
17+
* 1/2*g*t^2*3/4)
18+
*/
19+
internal class ShakeDetectorManager(private val listener: () -> Unit) : SensorEventListener {
20+
/**
21+
* When the magnitude of total acceleration exceeds this
22+
* value, the phone is accelerating.
23+
*/
24+
private var accelerationThreshold = DEFAULT_ACCELERATION_THRESHOLD
25+
26+
private val queue = SampleQueue()
27+
private var sensorManager: SensorManager? = null
28+
private var accelerometer: Sensor? = null
29+
30+
/**
31+
* Starts listening for shakes on devices with appropriate hardware.
32+
*
33+
* @return true if the device supports shake detection.
34+
*/
35+
fun start(sensorManager: SensorManager): Boolean {
36+
// Already started?
37+
if (accelerometer != null) {
38+
return true
39+
}
40+
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
41+
42+
// If this phone has an accelerometer, listen to it.
43+
if (accelerometer != null) {
44+
this.sensorManager = sensorManager
45+
sensorManager.registerListener(
46+
this, accelerometer,
47+
SensorManager.SENSOR_DELAY_NORMAL
48+
)
49+
}
50+
return accelerometer != null
51+
}
52+
53+
/**
54+
* Stops listening. Safe to call when already stopped. Ignored on devices
55+
* without appropriate hardware.
56+
*/
57+
fun stop() {
58+
accelerometer?.let {
59+
queue.clear()
60+
sensorManager?.unregisterListener(this, it)
61+
sensorManager = null
62+
accelerometer = null
63+
}
64+
}
65+
66+
override fun onSensorChanged(event: SensorEvent) {
67+
val accelerating = isAccelerating(event)
68+
val timestamp = event.timestamp
69+
queue.add(timestamp, accelerating)
70+
if (queue.isShaking) {
71+
queue.clear()
72+
listener()
73+
}
74+
}
75+
76+
/** Returns true if the device is currently accelerating. */
77+
private fun isAccelerating(event: SensorEvent): Boolean {
78+
val ax = event.values[0]
79+
val ay = event.values[1]
80+
val az = event.values[2]
81+
82+
// Instead of comparing magnitude to ACCELERATION_THRESHOLD,
83+
// compare their squares. This is equivalent and doesn't need the
84+
// actual magnitude, which would be computed using (expensive) Math.sqrt().
85+
val magnitudeSquared = (ax * ax + ay * ay + az * az).toDouble()
86+
return magnitudeSquared > accelerationThreshold * accelerationThreshold
87+
}
88+
89+
/** Sets the acceleration threshold sensitivity. */
90+
fun setSensitivity(accelerationThreshold: Int) {
91+
this.accelerationThreshold = accelerationThreshold
92+
}
93+
94+
/** Queue of samples. Keeps a running average. */
95+
internal class SampleQueue {
96+
private val pool = SamplePool()
97+
private var oldest: Sample? = null
98+
private var newest: Sample? = null
99+
private var sampleCount = 0
100+
private var acceleratingCount = 0
101+
102+
/**
103+
* Adds a sample.
104+
*
105+
* @param timestamp in nanoseconds of sample
106+
* @param accelerating true if > [.accelerationThreshold].
107+
*/
108+
fun add(timestamp: Long, accelerating: Boolean) {
109+
// Purge samples that proceed window.
110+
purge(timestamp - MAX_WINDOW_SIZE)
111+
112+
// Add the sample to the queue.
113+
val added = pool.acquire()
114+
added.timestamp = timestamp
115+
added.accelerating = accelerating
116+
added.next = null
117+
newest?.let {
118+
it.next = added
119+
}
120+
newest = added
121+
if (oldest == null) {
122+
oldest = added
123+
}
124+
125+
// Update running average.
126+
sampleCount++
127+
if (accelerating) {
128+
acceleratingCount++
129+
}
130+
}
131+
132+
/** Removes all samples from this queue. */
133+
fun clear() {
134+
while (oldest != null) {
135+
val removed = oldest ?: continue
136+
oldest = removed.next
137+
pool.release(removed)
138+
}
139+
newest = null
140+
sampleCount = 0
141+
acceleratingCount = 0
142+
}
143+
144+
/** Purges samples with timestamps older than cutoff. */
145+
private fun purge(cutoff: Long) {
146+
while (sampleCount >= MIN_QUEUE_SIZE && oldest != null && cutoff - oldest!!.timestamp > 0) {
147+
// Remove sample.
148+
val removed = oldest ?: continue
149+
if (removed.accelerating) {
150+
acceleratingCount--
151+
}
152+
sampleCount--
153+
oldest = removed.next
154+
if (oldest == null) {
155+
newest = null
156+
}
157+
pool.release(removed)
158+
}
159+
}
160+
161+
/** Copies the samples into a list, with the oldest entry at index 0. */
162+
fun asList(): List<Sample> {
163+
val list: MutableList<Sample> = ArrayList()
164+
var s = oldest
165+
while (s != null) {
166+
list.add(s)
167+
s = s.next
168+
}
169+
return list
170+
}
171+
172+
/**
173+
* Returns true if we have enough samples and more than 3/4 of those samples
174+
* are accelerating.
175+
*/
176+
val isShaking: Boolean
177+
get() = newest != null && oldest != null && newest!!.timestamp - oldest!!.timestamp >= MIN_WINDOW_SIZE && acceleratingCount >= (sampleCount shr 1) + (sampleCount shr 2)
178+
179+
companion object {
180+
/** Window size in ns. Used to compute the average. */
181+
private const val MAX_WINDOW_SIZE: Long = 500000000 // 0.5s
182+
private const val MIN_WINDOW_SIZE = MAX_WINDOW_SIZE shr 1 // 0.25s
183+
184+
/**
185+
* Ensure the queue size never falls below this size, even if the device
186+
* fails to deliver this many events during the time window. The LG Ally
187+
* is one such device.
188+
*/
189+
private const val MIN_QUEUE_SIZE = 4
190+
}
191+
}
192+
193+
/** An accelerometer sample. */
194+
internal class Sample {
195+
/** Time sample was taken. */
196+
var timestamp: Long = 0
197+
198+
/** If acceleration > [.accelerationThreshold]. */
199+
var accelerating = false
200+
201+
/** Next sample in the queue or pool. */
202+
var next: Sample? = null
203+
}
204+
205+
/** Pools samples. Avoids garbage collection. */
206+
internal class SamplePool {
207+
private var head: Sample? = null
208+
209+
/** Acquires a sample from the pool. */
210+
fun acquire(): Sample {
211+
var acquired = head
212+
if (acquired == null) {
213+
acquired = Sample()
214+
} else {
215+
// Remove instance from pool.
216+
head = acquired.next
217+
}
218+
return acquired
219+
}
220+
221+
/** Returns a sample to the pool. */
222+
fun release(sample: Sample) {
223+
sample.next = head
224+
head = sample
225+
}
226+
}
227+
228+
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
229+
}

core/src/main/java/com/kernel/finch/core/util/extension/Context.kt

-16
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,6 @@ import java.io.File
1616
import java.io.FileOutputStream
1717
import java.io.IOException
1818

19-
internal fun Context.registerSensorEventListener(sensorEventListener: SensorEventListener) =
20-
(getSystemService(Context.SENSOR_SERVICE) as? SensorManager?)?.run {
21-
registerListener(
22-
sensorEventListener,
23-
getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
24-
SensorManager.SENSOR_DELAY_NORMAL
25-
)
26-
} ?: false
27-
28-
internal fun Context.unregisterSensorEventListener(sensorEventListener: SensorEventListener) {
29-
(getSystemService(Context.SENSOR_SERVICE) as? SensorManager?)?.unregisterListener(
30-
sensorEventListener
31-
)
32-
}
33-
3419
fun Context.applyTheme() =
3520
FinchCore.implementation.configuration.themeResourceId?.let { ContextThemeWrapper(this, it) }
3621
?: this
@@ -41,7 +26,6 @@ internal fun Context.getUriForFile(file: File) = FileProvider.getUriForFile(
4126
file
4227
)
4328

44-
@Suppress("BlockingMethodInNonBlockingContext")
4529
internal suspend fun Context.createScreenshotFromBitmap(bitmap: Bitmap, fileName: String): Uri? =
4630
withContext(Dispatchers.IO) {
4731
val file = createScreenCaptureFile(fileName)

dependencies.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ ext.versions = [
22
minSdk : 21,
33
targetSdk : 34,
44
compileSdk : 34,
5-
libraryVersion : '2.2.12',
5+
libraryVersion : '2.3.0',
66
libraryVersionCode: 15,
77

88
okhttp3 : '3.7.0',

sample/build.gradle

+6-6
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ android {
2929
}
3030

3131
dependencies {
32-
//debugImplementation 'com.github.kernel0x.finch:ui-drawer:2.2.12'
33-
//releaseImplementation 'com.github.kernel0x.finch:noop:2.2.12'
34-
//debugImplementation 'com.github.kernel0x.finch:log-okhttp:2.2.12'
35-
//releaseImplementation 'com.github.kernel0x.finch:log-okhttp-noop:2.2.12'
36-
//debugImplementation 'com.github.kernel0x.finch:log:2.2.12'
37-
//releaseImplementation 'com.github.kernel0x.finch:log-noop:2.2.12'
32+
//debugImplementation 'com.github.kernel0x.finch:ui-drawer:2.3.0'
33+
//releaseImplementation 'com.github.kernel0x.finch:noop:2.3.0'
34+
//debugImplementation 'com.github.kernel0x.finch:log-okhttp:2.3.0'
35+
//releaseImplementation 'com.github.kernel0x.finch:log-okhttp-noop:2.3.0'
36+
//debugImplementation 'com.github.kernel0x.finch:log:2.3.0'
37+
//releaseImplementation 'com.github.kernel0x.finch:log-noop:2.3.0'
3838
debugImplementation project(":ui-drawer")
3939
debugImplementation project(":log")
4040
debugImplementation project(":log-okhttp")

0 commit comments

Comments
 (0)