Skip to content

Commit a7d4564

Browse files
Clean up PC mouse repeat (#2553)
- Split PC mouse repeat settings from the runtime repeat manager - Generalize the Settings entry to PC Settings while keeping Mouse Repeat inside the screen - Stop active mouse repeat when the setting is disabled before the next repeat send - Simplify compact PC command cells and preserve switch stop ordering - Add focused coverage for repeat settings, routing, stop behavior, and reconnect handling 🤖 Auto-generated Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com>
1 parent 909a6a4 commit a7d4564

13 files changed

Lines changed: 342 additions & 113 deletions

File tree

app/src/main/java/com/enaboapps/switchify/pc/PcMouseRepeatManager.kt

Lines changed: 51 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.enaboapps.switchify.pc
22

33
import android.content.Context
44
import com.enaboapps.switchify.R
5-
import com.enaboapps.switchify.backend.preferences.PreferenceManager
65
import com.enaboapps.switchify.service.window.MessageSeverity
76
import com.enaboapps.switchify.service.window.ServiceMessageHUD
87
import kotlinx.coroutines.CoroutineScope
@@ -11,21 +10,14 @@ import kotlinx.coroutines.delay
1110
import kotlinx.coroutines.isActive
1211
import kotlinx.coroutines.launch
1312

14-
class PcMouseRepeatManager private constructor() {
15-
private var context: Context? = null
13+
class PcMouseRepeatManager internal constructor(
14+
private var settings: PcMouseRepeatSettings? = null,
15+
private var showHudMessage: (Int, MessageSeverity) -> Unit = defaultHudMessageHandler()
16+
) {
1617
private var repeatJob: Job? = null
1718
private var repeatedCommand: PcControlCommand? = null
18-
private var enabledProviderForTesting: (() -> Boolean)? = null
19-
private var intervalProviderForTesting: (() -> Long)? = null
20-
private var suppressHudForTesting = false
21-
private var messageRecorderForTesting: ((Int) -> Unit)? = null
2219

2320
companion object {
24-
const val DEFAULT_REPEAT_INTERVAL = 250L
25-
const val MIN_REPEAT_INTERVAL = 100L
26-
const val MAX_REPEAT_INTERVAL = 2000L
27-
const val REPEAT_INTERVAL_STEP = 50L
28-
2921
val instance: PcMouseRepeatManager by lazy { PcMouseRepeatManager() }
3022

3123
fun isRepeatable(command: PcControlCommand): Boolean {
@@ -34,41 +26,29 @@ class PcMouseRepeatManager private constructor() {
3426
}
3527

3628
fun init(context: Context) {
37-
this.context = context.applicationContext
29+
settings = PreferencePcMouseRepeatSettings(context.applicationContext)
3830
}
3931

4032
fun start(
4133
command: PcControlCommand,
4234
scope: CoroutineScope,
4335
sendCommand: suspend (PcControlCommand) -> PcCommandResult
4436
): Boolean {
45-
if (!isRepeatable(command) || !isEnabled()) return false
37+
if (!isRepeatable(command) || !currentSettings().isEnabled()) return false
4638

4739
stop(showMessage = false)
4840
repeatedCommand = command
4941
repeatJob = scope.launch {
50-
when (sendCommand(command)) {
51-
PcCommandResult.Ack -> showMessage(R.string.pc_mouse_repeat_started, MessageSeverity.Success)
52-
is PcCommandResult.AuthFailed,
53-
is PcCommandResult.Failed -> {
54-
clearRepeatState()
55-
showMessage(R.string.pc_mouse_repeat_stopped, MessageSeverity.Info)
56-
return@launch
57-
}
58-
}
42+
if (!sendAndContinue(command, sendCommand, showStartedMessage = true)) return@launch
5943

6044
while (isActive) {
61-
delay(getRepeatInterval())
45+
delay(currentSettings().intervalMs())
6246
if (!isActive) return@launch
63-
when (sendCommand(command)) {
64-
PcCommandResult.Ack -> Unit
65-
is PcCommandResult.AuthFailed,
66-
is PcCommandResult.Failed -> {
67-
clearRepeatState()
68-
showMessage(R.string.pc_mouse_repeat_stopped, MessageSeverity.Info)
69-
return@launch
70-
}
47+
if (!currentSettings().isEnabled()) {
48+
stop()
49+
return@launch
7150
}
51+
if (!sendAndContinue(command, sendCommand)) return@launch
7252
}
7353
}
7454
return true
@@ -97,26 +77,30 @@ class PcMouseRepeatManager private constructor() {
9777
stop(showMessage)
9878
}
9979

100-
private fun isEnabled(): Boolean {
101-
enabledProviderForTesting?.let { return it() }
102-
return context?.let {
103-
PreferenceManager(it).getBooleanValue(
104-
PreferenceManager.PREFERENCE_KEY_PC_MOUSE_REPEAT,
80+
private suspend fun sendAndContinue(
81+
command: PcControlCommand,
82+
sendCommand: suspend (PcControlCommand) -> PcCommandResult,
83+
showStartedMessage: Boolean = false
84+
): Boolean {
85+
return when (sendCommand(command)) {
86+
PcCommandResult.Ack -> {
87+
if (showStartedMessage) showMessage(R.string.pc_mouse_repeat_started, MessageSeverity.Success)
10588
true
106-
)
107-
} ?: true
89+
}
90+
is PcCommandResult.AuthFailed,
91+
is PcCommandResult.Failed -> {
92+
clearRepeatState()
93+
showMessage(R.string.pc_mouse_repeat_stopped, MessageSeverity.Info)
94+
false
95+
}
96+
}
10897
}
10998

110-
private fun getRepeatInterval(): Long {
111-
val interval = intervalProviderForTesting?.invoke()
112-
?: context?.let {
113-
PreferenceManager(it).getLongValue(
114-
PreferenceManager.PREFERENCE_KEY_PC_MOUSE_REPEAT_INTERVAL,
115-
DEFAULT_REPEAT_INTERVAL
116-
)
117-
}
118-
?: DEFAULT_REPEAT_INTERVAL
119-
return interval.coerceIn(MIN_REPEAT_INTERVAL, MAX_REPEAT_INTERVAL)
99+
private fun currentSettings(): PcMouseRepeatSettings {
100+
return settings ?: object : PcMouseRepeatSettings {
101+
override fun isEnabled(): Boolean = true
102+
override fun intervalMs(): Long = PcMouseRepeatDefaults.DEFAULT_INTERVAL_MS
103+
}
120104
}
121105

122106
private fun clearRepeatState() {
@@ -125,39 +109,30 @@ class PcMouseRepeatManager private constructor() {
125109
}
126110

127111
private fun showMessage(messageResId: Int, severity: MessageSeverity) {
128-
messageRecorderForTesting?.invoke(messageResId)
129-
if (suppressHudForTesting) return
130-
ServiceMessageHUD.instance.showMessage(
131-
messageResId,
132-
ServiceMessageHUD.MessageType.DISAPPEARING,
133-
severity = severity
134-
)
112+
showHudMessage(messageResId, severity)
135113
}
136114

137-
internal fun setEnabledProviderForTesting(provider: (() -> Boolean)?) {
138-
enabledProviderForTesting = provider
139-
}
140-
141-
internal fun setIntervalProviderForTesting(provider: (() -> Long)?) {
142-
intervalProviderForTesting = provider
115+
internal fun resetForTesting() {
116+
repeatJob?.cancel()
117+
repeatJob = null
118+
repeatedCommand = null
119+
settings = null
120+
showHudMessage = defaultHudMessageHandler()
143121
}
144122

145-
internal fun setSuppressHudForTesting(suppress: Boolean) {
146-
suppressHudForTesting = suppress
123+
internal fun setSettingsForTesting(settings: PcMouseRepeatSettings) {
124+
this.settings = settings
147125
}
148126

149-
internal fun setMessageRecorderForTesting(recorder: ((Int) -> Unit)?) {
150-
messageRecorderForTesting = recorder
127+
internal fun setHudMessageHandlerForTesting(showHudMessage: (Int, MessageSeverity) -> Unit) {
128+
this.showHudMessage = showHudMessage
151129
}
130+
}
152131

153-
internal fun resetForTesting() {
154-
repeatJob?.cancel()
155-
repeatJob = null
156-
repeatedCommand = null
157-
context = null
158-
enabledProviderForTesting = null
159-
intervalProviderForTesting = null
160-
suppressHudForTesting = false
161-
messageRecorderForTesting = null
162-
}
132+
private fun defaultHudMessageHandler(): (Int, MessageSeverity) -> Unit = { messageResId, severity ->
133+
ServiceMessageHUD.instance.showMessage(
134+
messageResId,
135+
ServiceMessageHUD.MessageType.DISAPPEARING,
136+
severity = severity
137+
)
163138
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.enaboapps.switchify.pc
2+
3+
import android.content.Context
4+
import com.enaboapps.switchify.backend.preferences.PreferenceManager
5+
6+
object PcMouseRepeatDefaults {
7+
const val DEFAULT_INTERVAL_MS = 250L
8+
const val MIN_INTERVAL_MS = 100L
9+
const val MAX_INTERVAL_MS = 2000L
10+
const val INTERVAL_STEP_MS = 50L
11+
}
12+
13+
interface PcMouseRepeatSettings {
14+
fun isEnabled(): Boolean
15+
fun intervalMs(): Long
16+
}
17+
18+
class PreferencePcMouseRepeatSettings internal constructor(
19+
private val getBooleanValue: (String, Boolean) -> Boolean,
20+
private val getLongValue: (String, Long) -> Long
21+
) : PcMouseRepeatSettings {
22+
constructor(context: Context) : this(
23+
getBooleanValue = PreferenceManager(context)::getBooleanValue,
24+
getLongValue = PreferenceManager(context)::getLongValue
25+
)
26+
27+
override fun isEnabled(): Boolean {
28+
return getBooleanValue(PreferenceManager.PREFERENCE_KEY_PC_MOUSE_REPEAT, true)
29+
}
30+
31+
override fun intervalMs(): Long {
32+
return getLongValue(
33+
PreferenceManager.PREFERENCE_KEY_PC_MOUSE_REPEAT_INTERVAL,
34+
PcMouseRepeatDefaults.DEFAULT_INTERVAL_MS
35+
).coerceIn(
36+
PcMouseRepeatDefaults.MIN_INTERVAL_MS,
37+
PcMouseRepeatDefaults.MAX_INTERVAL_MS
38+
)
39+
}
40+
}

app/src/main/java/com/enaboapps/switchify/screens/pc/PcMouseCommandGrid.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ data class PcCompactCommandCell(
7474
val onClick: () -> Unit,
7575
val icon: ImageVector? = null,
7676
val iconRotationDegrees: Float = 0f,
77-
val tone: PcCommandTone = PcCommandTone.Neutral,
78-
val repeatable: Boolean = false
77+
val tone: PcCommandTone = PcCommandTone.Neutral
7978
)
8079

8180
/**
@@ -175,8 +174,7 @@ fun PcControlCommandSections(
175174
onClick = { onCommandSelected(it.command, it.repeatable) },
176175
icon = it.icon,
177176
iconRotationDegrees = it.iconRotationDegrees,
178-
tone = it.tone,
179-
repeatable = it.repeatable
177+
tone = it.tone
180178
)
181179
}
182180
},

app/src/main/java/com/enaboapps/switchify/screens/pc/PcSettingsScreen.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import com.enaboapps.switchify.components.BaseView
1414
import com.enaboapps.switchify.components.PreferenceSwitch
1515
import com.enaboapps.switchify.components.PreferenceTimeStepper
1616
import com.enaboapps.switchify.components.Section
17-
import com.enaboapps.switchify.pc.PcMouseRepeatManager
17+
import com.enaboapps.switchify.pc.PcMouseRepeatDefaults
1818

1919
@Composable
2020
fun PcSettingsScreen(navController: NavController) {
21-
val preferenceManager = PreferenceManager(LocalContext.current)
21+
val context = LocalContext.current
22+
val preferenceManager = remember(context) { PreferenceManager(context) }
2223
var mouseRepeat by remember {
2324
mutableStateOf(
2425
preferenceManager.getBooleanValue(
@@ -31,7 +32,7 @@ fun PcSettingsScreen(navController: NavController) {
3132
mutableLongStateOf(
3233
preferenceManager.getLongValue(
3334
PreferenceManager.PREFERENCE_KEY_PC_MOUSE_REPEAT_INTERVAL,
34-
PcMouseRepeatManager.DEFAULT_REPEAT_INTERVAL
35+
PcMouseRepeatDefaults.DEFAULT_INTERVAL_MS
3536
)
3637
)
3738
}
@@ -59,9 +60,9 @@ fun PcSettingsScreen(navController: NavController) {
5960
value = repeatInterval,
6061
titleResId = R.string.pc_settings_mouse_repeat_interval_title,
6162
summaryResId = R.string.pc_settings_mouse_repeat_interval_summary,
62-
min = PcMouseRepeatManager.MIN_REPEAT_INTERVAL,
63-
max = PcMouseRepeatManager.MAX_REPEAT_INTERVAL,
64-
step = PcMouseRepeatManager.REPEAT_INTERVAL_STEP,
63+
min = PcMouseRepeatDefaults.MIN_INTERVAL_MS,
64+
max = PcMouseRepeatDefaults.MAX_INTERVAL_MS,
65+
step = PcMouseRepeatDefaults.INTERVAL_STEP_MS,
6566
onValueChanged = {
6667
repeatInterval = it
6768
preferenceManager.setLongValue(

app/src/main/java/com/enaboapps/switchify/screens/settings/SettingsScreen.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.enaboapps.switchify.screens.settings
22

3+
import androidx.annotation.StringRes
34
import androidx.compose.foundation.layout.Spacer
45
import androidx.compose.foundation.layout.padding
56
import androidx.compose.material3.PrimaryTabRow
@@ -37,6 +38,12 @@ import com.enaboapps.switchify.screens.settings.sections.SelectionSection
3738
import com.enaboapps.switchify.screens.settings.shared.ScanModeSelectionSection
3839
import com.enaboapps.switchify.screens.settings.techniques.AccessTechniqueSelector
3940

41+
data class SettingsRouteLinkSpec(
42+
@param:StringRes val titleResId: Int,
43+
@param:StringRes val summaryResId: Int,
44+
val route: String
45+
)
46+
4047
@Composable
4148
fun SettingsScreen(navController: NavController) {
4249
val context = LocalContext.current
@@ -132,12 +139,13 @@ fun GeneralSettingsTab(menuSettingsModel: MenuSettingsModel, navController: NavC
132139
route = NavigationRoute.AiModel.name
133140
)
134141
}
142+
val pcSettingsLink = pcSettingsRouteLinkSpec()
135143
Section(titleResId = R.string.pc_settings_title) {
136144
NavRouteLink(
137-
titleResId = R.string.pc_settings_mouse_repeat_title,
138-
summaryResId = R.string.pc_settings_mouse_repeat_link_summary,
145+
titleResId = pcSettingsLink.titleResId,
146+
summaryResId = pcSettingsLink.summaryResId,
139147
navController = navController,
140-
route = NavigationRoute.PcSettings.name
148+
route = pcSettingsLink.route
141149
)
142150
}
143151
InputSection(navController)
@@ -147,6 +155,14 @@ fun GeneralSettingsTab(menuSettingsModel: MenuSettingsModel, navController: NavC
147155
}
148156
}
149157

158+
fun pcSettingsRouteLinkSpec(): SettingsRouteLinkSpec {
159+
return SettingsRouteLinkSpec(
160+
titleResId = R.string.pc_settings_title,
161+
summaryResId = R.string.pc_settings_link_summary,
162+
route = NavigationRoute.PcSettings.name
163+
)
164+
}
165+
150166
@Composable
151167
fun ScanningSettingsTab(navController: NavController) {
152168
ScrollableView {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ class CameraSwitchManager(
425425
if (switchEvent != null) {
426426
coroutineScope.launch(Dispatchers.Main) {
427427
Log.i(TAG, "Triggering switch action for gesture: ${gesture.getName()}")
428-
if (PcMouseRepeatManager.instance.stopForSwitchPress()) return@launch
428+
if (stopPcMouseRepeatForSwitchPress()) return@launch
429429
if (!switchEvent.pressAction.isScanMovementAction() &&
430430
Tasks.getInstance().stopActiveStoppableTask()
431431
) return@launch
@@ -449,7 +449,7 @@ class CameraSwitchManager(
449449
if (switchEvent != null) {
450450
coroutineScope.launch(Dispatchers.Main) {
451451
Log.i(TAG, "Triggering head turn gesture: ${gesture.getName()}")
452-
if (PcMouseRepeatManager.instance.stopForSwitchPress()) return@launch
452+
if (stopPcMouseRepeatForSwitchPress()) return@launch
453453
if (!switchEvent.pressAction.isScanMovementAction() &&
454454
Tasks.getInstance().stopActiveStoppableTask()
455455
) return@launch
@@ -458,6 +458,10 @@ class CameraSwitchManager(
458458
}
459459
}
460460

461+
private fun stopPcMouseRepeatForSwitchPress(): Boolean {
462+
return PcMouseRepeatManager.instance.stopForSwitchPress()
463+
}
464+
461465
/**
462466
* Clean up resources asynchronously.
463467
*/

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class ExternalSwitchListener(
6565
return false
6666
}
6767

68-
if (PcMouseRepeatManager.instance.stopForSwitchPress()) {
68+
if (stopPcMouseRepeatForSwitchPress()) {
6969
pressSession = ExternalSwitchPressSession.ReleaseSwallowed
7070
ExternalSwitchLongPressHandler.cancel()
7171
return true
@@ -306,6 +306,10 @@ class ExternalSwitchListener(
306306
scanningManager.performAction(action)
307307
}
308308

309+
private fun stopPcMouseRepeatForSwitchPress(): Boolean {
310+
return PcMouseRepeatManager.instance.stopForSwitchPress()
311+
}
312+
309313
/**
310314
* Determines if a switch repeat should be ignored based on timing and settings.
311315
*

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@
315315
<string name="pc_settings_mouse_repeat_summary">Repeat PC mouse movement and scrolling until the next switch press.</string>
316316
<string name="pc_settings_mouse_repeat_interval_title">Repeat Interval</string>
317317
<string name="pc_settings_mouse_repeat_interval_summary">Time between repeated PC mouse actions.</string>
318-
<string name="pc_settings_mouse_repeat_link_summary">Configure repeated PC mouse movement and scrolling.</string>
318+
<string name="pc_settings_link_summary">Configure PC controls and connection settings.</string>
319319
<string name="pc_mouse_repeat_started">Mouse Repeat Started</string>
320320
<string name="pc_mouse_repeat_stopped">Mouse Repeat Stopped</string>
321321
<string name="pc_pairing_code_title">Check your PC</string>

0 commit comments

Comments
 (0)