Skip to content

Commit 23b04d9

Browse files
jatinkumar2409BrayanDSO
authored andcommitted
added debouncing to prevent frequent triggering
1 parent 8a30c20 commit 23b04d9

3 files changed

Lines changed: 114 additions & 13 deletions

File tree

AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import android.content.SharedPreferences
2929
import android.content.res.Configuration
3030
import android.graphics.Bitmap
3131
import android.graphics.Color
32-
import android.hardware.SensorManager
3332
import android.media.MediaPlayer
3433
import android.net.Uri
3534
import android.os.Build
@@ -87,6 +86,7 @@ import com.ichi2.anim.ActivityTransitionAnimation
8786
import com.ichi2.anki.AbstractFlashcardViewer.Signal.Companion.toSignal
8887
import com.ichi2.anki.CollectionManager.TR
8988
import com.ichi2.anki.CollectionManager.withCol
89+
import com.ichi2.anki.android.AnkiShakeDetector
9090
import com.ichi2.anki.android.back.exitViaDoubleTapBackCallback
9191
import com.ichi2.anki.backend.stripHTMLAndSpecialFields
9292
import com.ichi2.anki.cardviewer.AndroidCardRenderContext
@@ -2172,7 +2172,7 @@ abstract class AbstractFlashcardViewer :
21722172
internal inner class LinkDetectingGestureDetector :
21732173
MyGestureDetector(),
21742174
ShakeDetector.Listener {
2175-
private var shakeDetector: ShakeDetector? = null
2175+
private var shakeDetector: AnkiShakeDetector? = null
21762176

21772177
init {
21782178
initShakeDetector()
@@ -2181,11 +2181,14 @@ abstract class AbstractFlashcardViewer :
21812181
private fun initShakeDetector() {
21822182
Timber.d("Initializing shake detector")
21832183
if (gestureProcessor.isBound(Gesture.SHAKE)) {
2184-
val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
21852184
shakeDetector =
2186-
ShakeDetector(this).apply {
2187-
start(sensorManager, SensorManager.SENSOR_DELAY_UI)
2188-
}
2185+
AnkiShakeDetector
2186+
.createInstance(
2187+
context = this@AbstractFlashcardViewer,
2188+
listener = this@LinkDetectingGestureDetector,
2189+
)?.apply {
2190+
start()
2191+
}
21892192
}
21902193
}
21912194

@@ -2207,7 +2210,6 @@ abstract class AbstractFlashcardViewer :
22072210
private val dispatchedTouchEvents = hashSetInit<MotionEvent>(2)
22082211

22092212
override fun hearShake() {
2210-
Timber.d("Shake detected!")
22112213
gestureProcessor.onShake()
22122214
}
22132215

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) 2026 Jatin Kumar <jnkr2409@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki.android
18+
import android.content.Context
19+
import android.hardware.SensorManager
20+
import android.os.SystemClock
21+
import androidx.core.content.ContextCompat
22+
import androidx.core.content.getSystemService
23+
import com.squareup.seismic.ShakeDetector
24+
import timber.log.Timber
25+
import kotlin.time.Duration
26+
import kotlin.time.Duration.Companion.milliseconds
27+
28+
/*
29+
* Wrapper for a [ShakeDetector] to provide a cooldown mechanism.
30+
* This prevents the "Undo" action or other gestures from triggering multiple times
31+
* in rapid succession during a single physical shake event.
32+
*/
33+
class AnkiShakeDetector(
34+
private val sensorManager: SensorManager,
35+
/*
36+
* Sensor Delay tells how often the app should check for phone movement.
37+
*
38+
* We use [SensorManager.SENSOR_DELAY_UI] because:
39+
* - Enough Speed : It is fast enough to catch a normal shake.
40+
* - Battery Friendly: It uses less power than "Game" or "Fastest" settings.
41+
*/
42+
private val sensorDelay: Int = SensorManager.SENSOR_DELAY_UI,
43+
private val listener: ShakeDetector.Listener,
44+
/*
45+
* The minimum time between shake events to prevent accidental double-triggers.
46+
*
47+
* Through trial and error, 800ms was determined to be the optimal 'sweet spot':
48+
* - 500ms : A single physical shake often registered as two distinct events,
49+
* causing the flag to toggle on and immediately off again.
50+
*
51+
* - 1000ms+ : Felt unresponsive when the user wanted to quickly flag/unflag
52+
* multiple cards in a row.
53+
*
54+
* - 800ms : Consistently filters out the "rebound" of a single shake while
55+
* remaining responsive for intentional back-to-back actions.
56+
*/
57+
private val cooldown: Duration = 800.milliseconds,
58+
) : ShakeDetector.Listener {
59+
private val shakeDetector = ShakeDetector(this)
60+
private var lastShakeTime = 0L
61+
62+
fun start() {
63+
sensorManager.let {
64+
shakeDetector.start(it, sensorDelay)
65+
}
66+
}
67+
68+
fun stop() {
69+
shakeDetector.stop()
70+
}
71+
72+
override fun hearShake() {
73+
Timber.d("The time since the last shake was: ${SystemClock.elapsedRealtime() - lastShakeTime}")
74+
val currentTime = SystemClock.elapsedRealtime()
75+
if (currentTime - lastShakeTime < cooldown.inWholeMilliseconds) {
76+
return
77+
}
78+
try {
79+
listener.hearShake()
80+
} finally {
81+
lastShakeTime = SystemClock.elapsedRealtime()
82+
}
83+
}
84+
85+
companion object {
86+
fun createInstance(
87+
context: Context,
88+
listener: ShakeDetector.Listener,
89+
): AnkiShakeDetector? {
90+
val sensorManager = context.getSystemService<SensorManager>()
91+
return sensorManager?.let {
92+
AnkiShakeDetector(
93+
sensorManager = sensorManager,
94+
listener = listener,
95+
)
96+
}
97+
}
98+
}
99+
}

AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package com.ichi2.anki.ui.windows.reviewer
1717

1818
import android.content.Context
1919
import android.content.Intent
20-
import android.hardware.SensorManager
2120
import android.net.Uri
2221
import android.os.Bundle
2322
import android.view.KeyEvent
@@ -53,6 +52,8 @@ import com.ichi2.anki.CollectionManager
5352
import com.ichi2.anki.DispatchKeyEventListener
5453
import com.ichi2.anki.Flag
5554
import com.ichi2.anki.R
55+
import com.ichi2.anki.android.AnkiShakeDetector
56+
import com.ichi2.anki.android.back.doubleBackPressCallback
5657
import com.ichi2.anki.cardviewer.Gesture
5758
import com.ichi2.anki.common.annotations.NeedsTest
5859
import com.ichi2.anki.common.utils.android.isRobolectric
@@ -116,8 +117,7 @@ class ReviewerFragment :
116117

117118
override val webViewLayout: SafeWebViewLayout get() = binding.webViewLayout
118119
private lateinit var bindingMap: BindingMap<ReviewerBinding, ViewerAction>
119-
private var shakeDetector: ShakeDetector? = null
120-
private val sensorManager get() = ContextCompat.getSystemService(requireContext(), SensorManager::class.java)
120+
private var shakeDetector: AnkiShakeDetector? = null
121121
private val whiteboardFragment get() = childFragmentManager.findFragmentByTag(WhiteboardFragment::class.jvmName) as? WhiteboardFragment
122122
private val isBigScreen: Boolean get() = resources.configuration.smallestScreenWidthDp >= 720
123123

@@ -143,7 +143,7 @@ class ReviewerFragment :
143143
override fun onStart() {
144144
super.onStart()
145145
if (!requireActivity().isChangingConfigurations) {
146-
shakeDetector?.start(sensorManager, SensorManager.SENSOR_DELAY_UI)
146+
shakeDetector?.start()
147147
}
148148
}
149149

@@ -354,8 +354,8 @@ class ReviewerFragment :
354354
bindingMap.onGenericMotionEvent(event)
355355
}
356356
if (bindingMap.isBound(Gesture.SHAKE)) {
357-
shakeDetector = ShakeDetector(this)
358-
shakeDetector?.start(sensorManager, SensorManager.SENSOR_DELAY_UI)
357+
shakeDetector = AnkiShakeDetector.createInstance(requireContext(), this)
358+
shakeDetector?.start()
359359
}
360360
}
361361

0 commit comments

Comments
 (0)