Skip to content

Commit

Permalink
add shake detector
Browse files Browse the repository at this point in the history
  • Loading branch information
kernel0x committed Dec 12, 2024
1 parent 4f09830 commit fff6dca
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 35 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ Pick a UI implementation and add the dependency:

````java
dependencies {
debugImplementation 'com.github.kernel0x.finch:ui-drawer:2.2.12'
releaseImplementation 'com.github.kernel0x.finch:noop:2.2.12'
debugImplementation 'com.github.kernel0x.finch:ui-drawer:2.3.0'
releaseImplementation 'com.github.kernel0x.finch:noop:2.3.0'
// optional only for OkHttp
debugImplementation 'com.github.kernel0x.finch:log-okhttp:2.2.12'
releaseImplementation 'com.github.kernel0x.finch:log-okhttp-noop:2.2.12'
debugImplementation 'com.github.kernel0x.finch:log-okhttp:2.3.0'
releaseImplementation 'com.github.kernel0x.finch:log-okhttp-noop:2.3.0'
// optional only for GRPC
debugImplementation 'com.github.kernel0x.finch:log-grpc:2.2.12'
releaseImplementation 'com.github.kernel0x.finch:log-grpc-noop:2.2.12'
debugImplementation 'com.github.kernel0x.finch:log-grpc:2.3.0'
releaseImplementation 'com.github.kernel0x.finch:log-grpc-noop:2.3.0'
// optional only for logs
debugImplementation 'com.github.kernel0x.finch:log:2.2.12'
releaseImplementation 'com.github.kernel0x.finch:log-noop:2.2.12'
debugImplementation 'com.github.kernel0x.finch:log:2.3.0'
releaseImplementation 'com.github.kernel0x.finch:log-noop:2.3.0'
}
````

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import java.util.*

data class Configuration(
@StyleRes val themeResourceId: Int? = DEFAULT_THEME_RESOURCE_ID,
val shakeThreshold: Int? = DEFAULT_SHAKE_THRESHOLD,
val shakeHapticFeedbackDuration: Long = DEFAULT_HAPTIC_FEEDBACK_DURATION,
val shakeDetection: Boolean = DEFAULT_SHAKE_DETECTION,
val excludedPackageNames: List<String> = DEFAULT_EXCLUDED_PACKAGE_NAMES,
val logger: FinchLogger? = DEFAULT_LOGGER,
val networkLoggers: List<FinchNetworkLogger> = DEFAULT_NETWORK_LOGGERS,
Expand All @@ -31,9 +30,8 @@ data class Configuration(
val applyInsets: ((windowInsets: Inset) -> Inset)? = DEFAULT_APPLY_INSETS
) {
companion object {
private const val DEFAULT_SHAKE_THRESHOLD = 13
private const val DEFAULT_SHOW_NOTIFICATION_NETWORK_LOGGERS = true
private const val DEFAULT_HAPTIC_FEEDBACK_DURATION = 100L
private const val DEFAULT_SHAKE_DETECTION = true
private val DEFAULT_THEME_RESOURCE_ID: Int? = null
private val DEFAULT_EXCLUDED_PACKAGE_NAMES = emptyList<String>()
private val DEFAULT_LOGGER: FinchLogger? = null
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/java/com/kernel/finch/core/FinchImpl.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.kernel.finch.core

import android.app.Application
import android.content.Context.SENSOR_SERVICE
import android.graphics.Canvas
import android.hardware.SensorManager
import android.net.Uri
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
Expand Down Expand Up @@ -92,6 +94,15 @@ class FinchImpl(val uiManager: UiManager) : Finch {
this.notificationManager = NotificationManager(application)
this.networkLogDao = FinchDatabase.getInstance(application).networkLog()
this.retentionManager = RetentionManager(application, Period.ONE_WEEK)
if (configuration.shakeDetection) {
(application.getSystemService(SENSOR_SERVICE) as SensorManager?)?.let {
ShakeDetectorManager {
show()
}.apply {
start(it)
}
}
}
debugMenuInjector.register(application)
configuration.logger?.register(::log, ::clearLogs)
configuration.networkLoggers.forEach { it.register(::logNetworkEvent, ::clearNetworkLogs) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package com.kernel.finch.core.manager

import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import java.util.*

private const val SENSITIVITY_LIGHT = 11
private const val SENSITIVITY_MEDIUM = 13
private const val SENSITIVITY_HARD = 15
private const val DEFAULT_ACCELERATION_THRESHOLD = SENSITIVITY_MEDIUM

/**
* 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)
*/
internal class ShakeDetectorManager(private val listener: () -> Unit) : SensorEventListener {
/**
* When the magnitude of total acceleration exceeds this
* value, the phone is accelerating.
*/
private var accelerationThreshold = DEFAULT_ACCELERATION_THRESHOLD

private val queue = SampleQueue()
private var sensorManager: SensorManager? = null
private var accelerometer: Sensor? = null

/**
* Starts listening for shakes on devices with appropriate hardware.
*
* @return true if the device supports shake detection.
*/
fun start(sensorManager: SensorManager): 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,
SensorManager.SENSOR_DELAY_NORMAL
)
}
return accelerometer != null
}

/**
* Stops listening. Safe to call when already stopped. Ignored on devices
* without appropriate hardware.
*/
fun stop() {
accelerometer?.let {
queue.clear()
sensorManager?.unregisterListener(this, it)
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()
}
}

/** 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
newest?.let {
it.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 = oldest ?: continue
oldest = removed.next
pool.release(removed)
}
newest = null
sampleCount = 0
acceleratingCount = 0
}

/** Purges samples with timestamps older than cutoff. */
private fun purge(cutoff: Long) {
while (sampleCount >= MIN_QUEUE_SIZE && oldest != null && cutoff - oldest!!.timestamp > 0) {
// Remove sample.
val removed = oldest ?: continue
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
}

/**
* Returns true if we have enough samples and more than 3/4 of those samples
* are accelerating.
*/
val isShaking: Boolean
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) {}
}
16 changes: 0 additions & 16 deletions core/src/main/java/com/kernel/finch/core/util/extension/Context.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,6 @@ import java.io.File
import java.io.FileOutputStream
import java.io.IOException

internal fun Context.registerSensorEventListener(sensorEventListener: SensorEventListener) =
(getSystemService(Context.SENSOR_SERVICE) as? SensorManager?)?.run {
registerListener(
sensorEventListener,
getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_NORMAL
)
} ?: false

internal fun Context.unregisterSensorEventListener(sensorEventListener: SensorEventListener) {
(getSystemService(Context.SENSOR_SERVICE) as? SensorManager?)?.unregisterListener(
sensorEventListener
)
}

fun Context.applyTheme() =
FinchCore.implementation.configuration.themeResourceId?.let { ContextThemeWrapper(this, it) }
?: this
Expand All @@ -41,7 +26,6 @@ internal fun Context.getUriForFile(file: File) = FileProvider.getUriForFile(
file
)

@Suppress("BlockingMethodInNonBlockingContext")
internal suspend fun Context.createScreenshotFromBitmap(bitmap: Bitmap, fileName: String): Uri? =
withContext(Dispatchers.IO) {
val file = createScreenCaptureFile(fileName)
Expand Down
2 changes: 1 addition & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ ext.versions = [
minSdk : 21,
targetSdk : 34,
compileSdk : 34,
libraryVersion : '2.2.12',
libraryVersion : '2.3.0',
libraryVersionCode: 15,

okhttp3 : '3.7.0',
Expand Down
12 changes: 6 additions & 6 deletions sample/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ android {
}

dependencies {
//debugImplementation 'com.github.kernel0x.finch:ui-drawer:2.2.12'
//releaseImplementation 'com.github.kernel0x.finch:noop:2.2.12'
//debugImplementation 'com.github.kernel0x.finch:log-okhttp:2.2.12'
//releaseImplementation 'com.github.kernel0x.finch:log-okhttp-noop:2.2.12'
//debugImplementation 'com.github.kernel0x.finch:log:2.2.12'
//releaseImplementation 'com.github.kernel0x.finch:log-noop:2.2.12'
//debugImplementation 'com.github.kernel0x.finch:ui-drawer:2.3.0'
//releaseImplementation 'com.github.kernel0x.finch:noop:2.3.0'
//debugImplementation 'com.github.kernel0x.finch:log-okhttp:2.3.0'
//releaseImplementation 'com.github.kernel0x.finch:log-okhttp-noop:2.3.0'
//debugImplementation 'com.github.kernel0x.finch:log:2.3.0'
//releaseImplementation 'com.github.kernel0x.finch:log-noop:2.3.0'
debugImplementation project(":ui-drawer")
debugImplementation project(":log")
debugImplementation project(":log-okhttp")
Expand Down

0 comments on commit fff6dca

Please sign in to comment.