Skip to content

Commit 4c8da57

Browse files
authored
Merge v2.35.0 into main
Merge the v2.35.0 release branch back into main after v2.35.0-beta.10.
2 parents 389f5d3 + dd09f86 commit 4c8da57

83 files changed

Lines changed: 5706 additions & 232 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ android {
2020
minSdk = 29
2121
targetSdk = 36
2222
versionCode = gitVersionCode()
23-
versionName = "2.34.2"
23+
versionName = "2.35.0-beta.10"
2424

2525
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2626
vectorDrawables {

app/src/main/java/com/enaboapps/switchify/backend/preferences/PreferenceManager.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class PreferenceManager(context: Context) {
2727
const val PREFERENCE_KEY_MOVE_REPEAT_DELAY = "move_repeat_delay"
2828
const val PREFERENCE_KEY_PC_MOUSE_MOVEMENT_SIZE = "pc_mouse_movement_size"
2929
const val PREFERENCE_KEY_PC_CONTROL_SURFACE = "pc_control_surface"
30+
const val PREFERENCE_KEY_PC_MOUSE_REPEAT = "pc_mouse_repeat"
31+
const val PREFERENCE_KEY_PC_MOUSE_REPEAT_INTERVAL = "pc_mouse_repeat_interval"
3032
const val PREFERENCE_KEY_AUTOMATICALLY_START_SCAN_AFTER_SELECTION =
3133
"automatically_start_scan_after_selection"
3234
const val PREFERENCE_KEY_PAUSE_ON_FIRST_ITEM = "pause_on_first_item"

app/src/main/java/com/enaboapps/switchify/nav/NavGraph.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.enaboapps.switchify.screens.account.AuthScreen
1313
import com.enaboapps.switchify.screens.onboarding.OnboardingScreen
1414
import com.enaboapps.switchify.screens.paywall.AppPaywallScreen
1515
import com.enaboapps.switchify.screens.pc.PcConnectionScreen
16+
import com.enaboapps.switchify.screens.pc.PcSettingsScreen
1617

1718
import com.enaboapps.switchify.screens.settings.CameraSettingsScreen
1819
import com.enaboapps.switchify.screens.settings.HeadControlSettingsScreen
@@ -75,6 +76,9 @@ fun NavGraph(navController: NavHostController) {
7576
composable(NavigationRoute.PcConnection.name) {
7677
PcConnectionScreen(navController)
7778
}
79+
composable(NavigationRoute.PcSettings.name) {
80+
PcSettingsScreen(navController)
81+
}
7882
composable(NavigationRoute.SwitchStability.name) {
7983
SwitchStabilityScreen(navController)
8084
}

app/src/main/java/com/enaboapps/switchify/nav/NavigationRoute.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ sealed class NavigationRoute(val name: String) {
99
data object Account : NavigationRoute("Account")
1010
data object Settings : NavigationRoute("Settings")
1111
data object PcConnection : NavigationRoute("PcConnection")
12+
data object PcSettings : NavigationRoute("PcSettings")
1213
data object SwitchStability : NavigationRoute("SwitchStability")
1314
data object AdvancedGestureSettings : NavigationRoute("AdvancedGestureSettings")
1415
data object ScrollingSettings : NavigationRoute("ScrollingSettings")

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ data class DiscoveredPc(
88
val displayName: String
99
get() = serviceName.ifBlank { "Switchify PC" }
1010

11+
val controlDeviceName: String
12+
get() = bluetoothEndpoint?.deviceName?.takeIf { it.isNotBlank() }
13+
?: bluetoothEndpoint?.displayName?.takeIf { it.isNotBlank() }
14+
?: serviceName.takeIf { it.isNotBlank() }
15+
?: "Switchify PC"
16+
1117
val primaryAddress: String
1218
get() = bluetoothEndpoint?.deviceName
1319
?: bluetoothEndpoint?.deviceAddress

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ sealed class PcControlCommand {
2525
data class TextStreamKey(val streamId: String, val seq: Int, val key: PcKeyboardKey) : PcControlCommand()
2626
data class TextStreamClose(val streamId: String, val expectedCount: Int) : PcControlCommand()
2727
data class PressKey(val key: PcKeyboardKey) : PcControlCommand()
28+
data class KeyboardShortcut(val keys: List<PcKeyboardShortcutKey>) : PcControlCommand()
2829
data class WindowControl(val action: PcWindowControlAction) : PcControlCommand()
2930
data object LeftClick : PcControlCommand()
3031
data object DoubleClick : PcControlCommand()

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ enum class PcKeyboardKey(
1313
Escape("Escape", R.string.pc_key_escape),
1414
Space("Space", R.string.pc_key_space),
1515
Tab("Tab", R.string.pc_key_tab),
16+
Meta("Meta", R.string.pc_key_start),
1617
ArrowUp("ArrowUp", R.string.pc_key_arrow_up),
1718
ArrowDown("ArrowDown", R.string.pc_key_arrow_down),
1819
ArrowLeft("ArrowLeft", R.string.pc_key_arrow_left),
@@ -34,3 +35,7 @@ enum class PcKeyboardKey(
3435
F11("F11", R.string.pc_key_f11),
3536
F12("F12", R.string.pc_key_f12)
3637
}
38+
39+
enum class PcKeyboardShortcutKey(val protocolValue: String) {
40+
Meta("Meta")
41+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package com.enaboapps.switchify.pc
2+
3+
import android.content.Context
4+
import com.enaboapps.switchify.R
5+
import com.enaboapps.switchify.service.window.MessageSeverity
6+
import com.enaboapps.switchify.service.window.ServiceMessageHUD
7+
import kotlinx.coroutines.CoroutineScope
8+
import kotlinx.coroutines.Job
9+
import kotlinx.coroutines.delay
10+
import kotlinx.coroutines.isActive
11+
import kotlinx.coroutines.launch
12+
13+
class PcMouseRepeatManager internal constructor(
14+
private var settings: PcMouseRepeatSettings? = null,
15+
private var showHudMessage: (Int, MessageSeverity) -> Unit = defaultHudMessageHandler()
16+
) {
17+
private var repeatJob: Job? = null
18+
private var repeatedCommand: PcControlCommand? = null
19+
private var repeatArmed = false
20+
21+
companion object {
22+
val instance: PcMouseRepeatManager by lazy { PcMouseRepeatManager() }
23+
24+
fun isRepeatable(command: PcControlCommand): Boolean {
25+
return command is PcControlCommand.Move || command is PcControlCommand.Scroll
26+
}
27+
}
28+
29+
fun init(context: Context) {
30+
settings = PreferencePcMouseRepeatSettings(context.applicationContext)
31+
}
32+
33+
fun canRepeat(command: PcControlCommand): Boolean {
34+
return isRepeatable(command) && currentSettings().isEnabled()
35+
}
36+
37+
fun armForInitialSend(command: PcControlCommand): Boolean {
38+
if (!canRepeat(command)) return false
39+
40+
stop(showMessage = false)
41+
repeatedCommand = command
42+
repeatArmed = true
43+
showMessage(R.string.pc_mouse_repeat_started, MessageSeverity.Success)
44+
return true
45+
}
46+
47+
fun startAfterInitialSend(
48+
command: PcControlCommand,
49+
scope: CoroutineScope,
50+
sendRepeatedCommand: suspend (PcControlCommand) -> PcCommandResult
51+
): Boolean {
52+
if (!isRepeatable(command)) return false
53+
if (!currentSettings().isEnabled()) {
54+
stop()
55+
return false
56+
}
57+
if (!repeatArmed || repeatedCommand != command) return false
58+
59+
repeatJob?.cancel()
60+
repeatedCommand = command
61+
repeatArmed = true
62+
repeatJob = scope.launch {
63+
while (isActive) {
64+
delay(currentSettings().intervalMs())
65+
if (!isActive) return@launch
66+
if (!currentSettings().isEnabled()) {
67+
stop()
68+
return@launch
69+
}
70+
if (!sendAndContinue(command, sendRepeatedCommand)) return@launch
71+
}
72+
}
73+
return true
74+
}
75+
76+
fun cancelPendingStart(showMessage: Boolean = false): Boolean {
77+
if (!isRepeating()) return false
78+
79+
repeatJob?.cancel()
80+
clearRepeatState()
81+
if (showMessage) {
82+
showMessage(R.string.pc_mouse_repeat_stopped, MessageSeverity.Info)
83+
}
84+
return true
85+
}
86+
87+
fun stop(showMessage: Boolean = true): Boolean {
88+
if (!isRepeating()) {
89+
clearRepeatState()
90+
return false
91+
}
92+
repeatJob?.cancel()
93+
clearRepeatState()
94+
if (showMessage) {
95+
showMessage(R.string.pc_mouse_repeat_stopped, MessageSeverity.Info)
96+
}
97+
return true
98+
}
99+
100+
fun stopForSwitchPress(): Boolean = stop()
101+
102+
fun isRepeating(): Boolean {
103+
return repeatArmed && repeatedCommand != null
104+
}
105+
106+
fun clearServiceState(showMessage: Boolean = false) {
107+
stop(showMessage)
108+
}
109+
110+
private suspend fun sendAndContinue(
111+
command: PcControlCommand,
112+
sendCommand: suspend (PcControlCommand) -> PcCommandResult
113+
): Boolean {
114+
return when (sendCommand(command)) {
115+
PcCommandResult.Ack -> true
116+
is PcCommandResult.AuthFailed,
117+
is PcCommandResult.Failed -> {
118+
clearRepeatState()
119+
showMessage(R.string.pc_mouse_repeat_stopped, MessageSeverity.Info)
120+
false
121+
}
122+
}
123+
}
124+
125+
private fun currentSettings(): PcMouseRepeatSettings {
126+
return settings ?: object : PcMouseRepeatSettings {
127+
override fun isEnabled(): Boolean = true
128+
override fun intervalMs(): Long = PcMouseRepeatDefaults.DEFAULT_INTERVAL_MS
129+
}
130+
}
131+
132+
private fun clearRepeatState() {
133+
repeatJob = null
134+
repeatedCommand = null
135+
repeatArmed = false
136+
}
137+
138+
private fun showMessage(messageResId: Int, severity: MessageSeverity) {
139+
showHudMessage(messageResId, severity)
140+
}
141+
142+
internal fun resetForTesting() {
143+
repeatJob?.cancel()
144+
repeatJob = null
145+
repeatedCommand = null
146+
repeatArmed = false
147+
settings = null
148+
showHudMessage = defaultHudMessageHandler()
149+
}
150+
151+
internal fun setSettingsForTesting(settings: PcMouseRepeatSettings) {
152+
this.settings = settings
153+
}
154+
155+
internal fun setHudMessageHandlerForTesting(showHudMessage: (Int, MessageSeverity) -> Unit) {
156+
this.showHudMessage = showHudMessage
157+
}
158+
}
159+
160+
private fun defaultHudMessageHandler(): (Int, MessageSeverity) -> Unit = { messageResId, severity ->
161+
ServiceMessageHUD.instance.showMessage(
162+
messageResId,
163+
ServiceMessageHUD.MessageType.DISAPPEARING,
164+
severity = severity
165+
)
166+
}
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/pc/PcProtocol.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,25 @@ object PcProtocol {
332332
)
333333
}
334334

335+
fun keyboardShortcut(
336+
id: String,
337+
deviceId: String,
338+
token: String,
339+
timestamp: Long,
340+
keys: List<PcKeyboardShortcutKey>,
341+
responseMode: PcCommandResponseMode = PcCommandResponseMode.Ack
342+
): String {
343+
return authenticatedCommand(
344+
id,
345+
deviceId,
346+
token,
347+
timestamp,
348+
"keyboard.shortcut",
349+
JSONObject().put("keys", JSONArray(keys.map { it.protocolValue })),
350+
responseMode
351+
)
352+
}
353+
335354
fun windowControl(
336355
id: String,
337356
deviceId: String,

0 commit comments

Comments
 (0)