Skip to content

Commit 8535974

Browse files
Absorb ongoing task switch releases (#2505)
- Centralize ongoing task switch press handling in Tasks - Track Gesture Repeat sessions from capture through initial delay - Absorb switch releases that start or occur during ongoing tasks - Route camera switches and head control through the shared task path - Add regression coverage for repeat startup and task absorption 🤖 Auto-generated Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com>
1 parent aebcf0f commit 8535974

10 files changed

Lines changed: 279 additions & 36 deletions

File tree

app/src/main/java/com/enaboapps/switchify/service/core/Tasks.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.enaboapps.switchify.service.core
22

33
import com.enaboapps.switchify.service.gestures.AutoScrollManager
4+
import com.enaboapps.switchify.service.gestures.GestureLockManager
5+
import com.enaboapps.switchify.service.gestures.GestureRepeatManager
46
import com.enaboapps.switchify.service.gestures.patterns.GesturePatternManager
57

68
/**
@@ -30,7 +32,9 @@ class Tasks private constructor() {
3032
*
3133
* @return True if any ongoing task was found and potentially stopped/advanced, false otherwise.
3234
*/
33-
fun checkOngoingTasks(): Boolean {
35+
fun stopOngoingTaskForSwitchPress(): Boolean {
36+
if (GestureRepeatManager.instance.stopRepeatForSwitchPress()) return true
37+
3438
// Stop auto-scrolling if active
3539
if (AutoScrollManager.getInstance().stopAutoScroll()) return true
3640

@@ -45,4 +49,22 @@ class Tasks private constructor() {
4549

4650
return false
4751
}
48-
}
52+
53+
fun shouldAbsorbSwitchRelease(): Boolean {
54+
return hasOngoingTask()
55+
}
56+
57+
fun shouldAbsorbSwitchReleaseAfterAction(): Boolean {
58+
return shouldAbsorbSwitchRelease() ||
59+
GestureRepeatManager.instance.isAutoRepeatEnabled() ||
60+
GestureLockManager.instance.isLocked()
61+
}
62+
63+
fun hasOngoingTask(): Boolean {
64+
return GestureRepeatManager.instance.isRepeatSessionActive() ||
65+
AutoScrollManager.getInstance().isAutoScrolling() ||
66+
GesturePatternManager.isGesturePatternActive()
67+
}
68+
69+
fun checkOngoingTasks(): Boolean = stopOngoingTaskForSwitchPress()
70+
}

app/src/main/java/com/enaboapps/switchify/service/gestures/GestureManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ class GestureManager private constructor() {
403403
* @return True if a locked gesture action was performed, false otherwise.
404404
*/
405405
fun performGestureLockAction(): Boolean {
406-
if (GestureRepeatManager.instance.isRepeating()) return false
406+
if (GestureRepeatManager.instance.isRepeatSessionActive()) return false
407407
if (isGestureLockEnabled()) {
408408
GestureLockManager.instance.getLockedGestureData()?.let { gestureData ->
409409
gestureData.executeGesture()

app/src/main/java/com/enaboapps/switchify/service/gestures/GestureRepeatManager.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class GestureRepeatManager private constructor() {
1818
private val scope = CoroutineScope(Dispatchers.IO)
1919
private var repeatJob: Job? = null
2020
private var repeatedGestureData: GestureData? = null
21+
private var repeatSessionActive = false
2122
private var autoRepeatEnabled = false
2223
private var initialRepeatDelayProviderForTesting: (() -> Long)? = null
2324
private var repeatDelayProviderForTesting: (() -> Long)? = null
@@ -81,10 +82,11 @@ class GestureRepeatManager private constructor() {
8182
}
8283

8384
fun stopRepeat(showMessage: Boolean = true): Boolean {
84-
if (!isRepeating()) return false
85+
if (!repeatSessionActive) return false
8586
repeatJob?.cancel()
8687
repeatJob = null
8788
repeatedGestureData = null
89+
repeatSessionActive = false
8890
if (showMessage) {
8991
showMessage(R.string.gesture_repeat_stopped, MessageSeverity.Info)
9092
}
@@ -104,12 +106,15 @@ class GestureRepeatManager private constructor() {
104106

105107
fun isRepeating(): Boolean = repeatJob?.isActive == true
106108

107-
fun isWaitingForGesture(): Boolean = isAutoRepeatEnabled() && !isRepeating()
109+
fun isRepeatSessionActive(): Boolean = repeatSessionActive
110+
111+
fun isWaitingForGesture(): Boolean = isAutoRepeatEnabled() && !repeatSessionActive
108112

109113
fun clearServiceState(showMessage: Boolean = false) {
110114
val wasEnabled = autoRepeatEnabled
111115
stopRepeat(showMessage = false)
112116
repeatedGestureData = null
117+
repeatSessionActive = false
113118
autoRepeatEnabled = false
114119
if (showMessage && wasEnabled) {
115120
showMessage(R.string.gesture_repeat_disabled, MessageSeverity.Info)
@@ -134,6 +139,7 @@ class GestureRepeatManager private constructor() {
134139

135140
private fun startRepeat(gestureData: GestureData) {
136141
repeatJob?.cancel()
142+
repeatSessionActive = true
137143
repeatedGestureData = gestureData
138144
showMessage(R.string.gesture_repeat_started, MessageSeverity.Success)
139145
repeatJob = scope.launch {
@@ -244,6 +250,7 @@ class GestureRepeatManager private constructor() {
244250
repeatJob?.cancel()
245251
repeatJob = null
246252
repeatedGestureData = null
253+
repeatSessionActive = false
247254
context = null
248255
autoRepeatEnabled = false
249256
initialRepeatDelayProviderForTesting = null

app/src/main/java/com/enaboapps/switchify/service/scanning/ScanningManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class ScanningManager(
183183
}
184184

185185
fun checkOngoingTasks(): Boolean {
186-
return Tasks.getInstance().checkOngoingTasks()
186+
return Tasks.getInstance().stopOngoingTaskForSwitchPress()
187187
}
188188

189189
/**

app/src/main/java/com/enaboapps/switchify/service/switches/camera/CameraSwitchManager.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import com.enaboapps.switchify.service.camera.CameraLifecycle
1313
import com.enaboapps.switchify.service.camera.CameraPermissionManager
1414
import com.enaboapps.switchify.service.core.ServiceCore
1515
import com.enaboapps.switchify.service.face.FaceProcessingService
16-
import com.enaboapps.switchify.service.gestures.GestureRepeatManager
16+
import com.enaboapps.switchify.service.core.Tasks
1717
import com.enaboapps.switchify.service.pauseresume.PauseManager
1818
import com.enaboapps.switchify.service.scanning.ScanningManager
1919
import com.enaboapps.switchify.service.stats.StatsCollector
@@ -423,8 +423,7 @@ class CameraSwitchManager(
423423
if (switchEvent != null) {
424424
coroutineScope.launch(Dispatchers.Main) {
425425
Log.i(TAG, "Triggering switch action for gesture: ${gesture.getName()}")
426-
if (GestureRepeatManager.instance.stopRepeatForSwitchPress()) return@launch
427-
if (scanningManager.checkOngoingTasks()) return@launch
426+
if (Tasks.getInstance().stopOngoingTaskForSwitchPress()) return@launch
428427
scanningManager.performAction(switchEvent.pressAction)
429428
}
430429
}
@@ -445,8 +444,7 @@ class CameraSwitchManager(
445444
if (switchEvent != null) {
446445
coroutineScope.launch(Dispatchers.Main) {
447446
Log.i(TAG, "Triggering head turn gesture: ${gesture.getName()}")
448-
if (GestureRepeatManager.instance.stopRepeatForSwitchPress()) return@launch
449-
if (scanningManager.checkOngoingTasks()) return@launch
447+
if (Tasks.getInstance().stopOngoingTaskForSwitchPress()) return@launch
450448
scanningManager.performAction(switchEvent.pressAction)
451449
}
452450
}

app/src/main/java/com/enaboapps/switchify/service/switches/external/ExternalSwitchListener.kt

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import android.content.Context
44
import android.util.Log
55
import com.enaboapps.switchify.backend.preferences.PreferenceManager
66
import com.enaboapps.switchify.service.core.ServiceCore
7-
import com.enaboapps.switchify.service.gestures.GestureRepeatManager
7+
import com.enaboapps.switchify.service.core.Tasks
88
import com.enaboapps.switchify.service.scanning.ScanningManager
99
import com.enaboapps.switchify.service.selection.SelectionHandler
1010
import com.enaboapps.switchify.service.stats.StatsCollector
1111
import com.enaboapps.switchify.service.switches.SwitchEventProvider
12+
import com.enaboapps.switchify.switches.SwitchAction
1213
import com.enaboapps.switchify.switches.SwitchEvent
1314

1415
/**
@@ -57,23 +58,11 @@ class ExternalSwitchListener(
5758
// Record stats for switch press
5859
StatsCollector.getInstance().recordSwitchPress("external", keyCode.toString())
5960

60-
if (GestureRepeatManager.instance.stopRepeatForSwitchPress()) {
61+
if (Tasks.getInstance().stopOngoingTaskForSwitchPress()) {
6162
latestAction = null
6263
return true
6364
}
6465

65-
val scanningManager = ServiceCore.getScanningManager()
66-
if (scanningManager != null) {
67-
if (scanningManager.checkOngoingTasks()) {
68-
// The press was consumed by stopping/advancing an ongoing task
69-
// (e.g. auto scroll). Clear the latest action so the matching
70-
// release is swallowed without firing a stale press action, and
71-
// absorb the key event so it does not leak to the foreground app.
72-
latestAction = null
73-
return true
74-
}
75-
}
76-
7766
val pauseManager = ServiceCore.getPauseManager()
7867
if (pauseManager.isPaused) {
7968
// Store the timestamp for hold-to-unpause check
@@ -110,8 +99,17 @@ class ExternalSwitchListener(
11099
return false
111100
}
112101

102+
if (Tasks.getInstance().shouldAbsorbSwitchRelease()) {
103+
latestAction = null
104+
return true
105+
}
106+
113107
if (scanningManager.stopMoveRepeat()) return true
114-
ExternalSwitchLongPressHandler.stopLongPress(scanningManager)
108+
val performedLongPressAction = ExternalSwitchLongPressHandler.stopLongPress(scanningManager)
109+
if (performedLongPressAction && Tasks.getInstance().shouldAbsorbSwitchReleaseAfterAction()) {
110+
latestAction = null
111+
return true
112+
}
115113

116114
if (handleSwitchPressedRepeat(keyCode)) {
117115
return true
@@ -194,21 +192,43 @@ class ExternalSwitchListener(
194192
val switchHoldTime =
195193
preferenceManager.getLongValue(PreferenceManager.PREFERENCE_KEY_SWITCH_HOLD_TIME)
196194

197-
if (!SelectionHandler.isAutoSelectInProgress()) {
198-
scanningManager.resumeScanning()
199-
}
195+
var performedPressAction = false
200196

201197
when {
202198
SelectionHandler.isAutoSelectInProgress() &&
203199
switchEvent.holdActions.isNotEmpty() ->
204200
SelectionHandler.performSelectionAction()
205201

206-
switchEvent.holdActions.isEmpty() ->
207-
scanningManager.performAction(switchEvent.pressAction)
202+
switchEvent.holdActions.isEmpty() -> {
203+
performReleasePressAction(switchEvent.pressAction)
204+
performedPressAction = true
205+
}
206+
207+
timeElapsed < switchHoldTime -> {
208+
performReleasePressAction(switchEvent.pressAction)
209+
performedPressAction = true
210+
}
211+
}
212+
213+
if (Tasks.getInstance().shouldAbsorbSwitchReleaseAfterAction()) {
214+
latestAction = null
215+
return
216+
}
208217

209-
timeElapsed < switchHoldTime ->
210-
scanningManager.performAction(switchEvent.pressAction)
218+
if (performedPressAction && switchEvent.pressAction.id == SwitchAction.ACTION_SELECT) {
219+
return
220+
}
221+
222+
if (!SelectionHandler.isAutoSelectInProgress()) {
223+
scanningManager.resumeScanning()
224+
}
225+
}
226+
227+
private fun performReleasePressAction(action: SwitchAction) {
228+
if (action.id == SwitchAction.ACTION_SELECT && !SelectionHandler.isAutoSelectInProgress()) {
229+
scanningManager.resumeScanning()
211230
}
231+
scanningManager.performAction(action)
212232
}
213233

214234
/**

app/src/main/java/com/enaboapps/switchify/service/switches/external/ExternalSwitchLongPressHandler.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,15 @@ object ExternalSwitchLongPressHandler {
6868
* Performs the action if it is not null.
6969
* @param scanningManager The scanning manager. Can be null (will not perform the action).
7070
*/
71-
fun stopLongPress(scanningManager: ScanningManager?) {
71+
fun stopLongPress(scanningManager: ScanningManager?): Boolean {
72+
var actionPerformed = false
7273
actionToPerform?.let {
7374
scanningManager?.performAction(it)
7475
actionToPerform = null
76+
actionPerformed = true
7577
}
7678
longPressJob?.cancel()
7779
longPressJob = null
80+
return actionPerformed
7881
}
7982
}

app/src/main/java/com/enaboapps/switchify/service/techniques/headcontrol/HeadControlManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ class HeadControlManager(private val context: Context) : MenuStateObserver {
181181
}
182182

183183
fun performSelection() {
184-
if (Tasks.getInstance().checkOngoingTasks())
184+
if (Tasks.getInstance().stopOngoingTaskForSwitchPress())
185185
return
186186
if (isInMenuMode()) {
187187
// Defensive check: only allow selection if nodes are ready
@@ -199,7 +199,7 @@ class HeadControlManager(private val context: Context) : MenuStateObserver {
199199
}
200200

201201
fun performMenuOpen() {
202-
if (Tasks.getInstance().checkOngoingTasks())
202+
if (Tasks.getInstance().stopOngoingTaskForSwitchPress())
203203
return
204204
// Don't open menu if already in menu mode
205205
if (isInMenuMode()) {

0 commit comments

Comments
 (0)