From fff6dcad39ea43f1bff68979d12d610995c7b961 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 12 Dec 2024 11:05:22 +0300 Subject: [PATCH] add shake detector --- README.md | 16 +- .../finch/common/models/Configuration.kt | 6 +- .../java/com/kernel/finch/core/FinchImpl.kt | 11 + .../core/manager/ShakeDetectorManager.kt | 229 ++++++++++++++++++ .../finch/core/util/extension/Context.kt | 16 -- dependencies.gradle | 2 +- sample/build.gradle | 12 +- 7 files changed, 257 insertions(+), 35 deletions(-) create mode 100644 core/src/main/java/com/kernel/finch/core/manager/ShakeDetectorManager.kt diff --git a/README.md b/README.md index 220f50e..e7381b2 100644 --- a/README.md +++ b/README.md @@ -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' } ```` diff --git a/common/src/main/java/com/kernel/finch/common/models/Configuration.kt b/common/src/main/java/com/kernel/finch/common/models/Configuration.kt index 694de6f..6dbc80b 100644 --- a/common/src/main/java/com/kernel/finch/common/models/Configuration.kt +++ b/common/src/main/java/com/kernel/finch/common/models/Configuration.kt @@ -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 = DEFAULT_EXCLUDED_PACKAGE_NAMES, val logger: FinchLogger? = DEFAULT_LOGGER, val networkLoggers: List = DEFAULT_NETWORK_LOGGERS, @@ -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() private val DEFAULT_LOGGER: FinchLogger? = null diff --git a/core/src/main/java/com/kernel/finch/core/FinchImpl.kt b/core/src/main/java/com/kernel/finch/core/FinchImpl.kt index b510a88..6ed036b 100644 --- a/core/src/main/java/com/kernel/finch/core/FinchImpl.kt +++ b/core/src/main/java/com/kernel/finch/core/FinchImpl.kt @@ -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 @@ -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) } diff --git a/core/src/main/java/com/kernel/finch/core/manager/ShakeDetectorManager.kt b/core/src/main/java/com/kernel/finch/core/manager/ShakeDetectorManager.kt new file mode 100644 index 0000000..42eed96 --- /dev/null +++ b/core/src/main/java/com/kernel/finch/core/manager/ShakeDetectorManager.kt @@ -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 { + val list: MutableList = 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) {} +} diff --git a/core/src/main/java/com/kernel/finch/core/util/extension/Context.kt b/core/src/main/java/com/kernel/finch/core/util/extension/Context.kt index e6f8d8d..df155cf 100644 --- a/core/src/main/java/com/kernel/finch/core/util/extension/Context.kt +++ b/core/src/main/java/com/kernel/finch/core/util/extension/Context.kt @@ -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 @@ -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) diff --git a/dependencies.gradle b/dependencies.gradle index b263899..b8cf544 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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', diff --git a/sample/build.gradle b/sample/build.gradle index ef5da8e..b46a5bc 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -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")