Skip to content

Commit 8ab3f6f

Browse files
committed
#1901 prompt user to set default USB configuration to 'No data transfer' after starting pro mode
1 parent e9587d9 commit 8ab3f6f

File tree

9 files changed

+127
-7
lines changed

9 files changed

+127
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
- #1414 constraint for when the keyboard is showing.
1010
- #1900 log to logcat if extra logging is enabled.
1111

12+
## Bug fixes
13+
14+
- #1901 prompt user to set default USB configuration to 'No data transfer' after starting pro mode.
15+
1216
## [4.0.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.02)
1317

1418
#### 08 November 2025

app/proguard-rules.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@
7979
-keep class com.android.internal.telephony.ITelephony$Stub { *; }
8080
-keep class android.net.ITetheringConnector { *; }
8181
-keep class android.net.ITetheringConnector$Stub { *; }
82+
-keep class android.hardware.usb.IUsbManager { *; }
83+
-keep class android.hardware.usb.IUsbManager$Stub { *; }
8284
-keep class android.net.* { *; }
8385

8486
-keepattributes *Annotation*, InnerClasses

base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.sds100.keymapper.base.promode
22

33
import android.os.Build
4+
import android.provider.Settings
45
import androidx.compose.animation.AnimatedVisibility
56
import androidx.compose.animation.expandVertically
67
import androidx.compose.animation.fadeIn
@@ -33,6 +34,7 @@ import androidx.compose.material.icons.rounded.Notifications
3334
import androidx.compose.material.icons.rounded.Numbers
3435
import androidx.compose.material.icons.rounded.RestartAlt
3536
import androidx.compose.material.icons.rounded.Tune
37+
import androidx.compose.material.icons.rounded.Usb
3638
import androidx.compose.material.icons.rounded.WarningAmber
3739
import androidx.compose.material3.BottomAppBar
3840
import androidx.compose.material3.ButtonDefaults
@@ -54,6 +56,7 @@ import androidx.compose.runtime.getValue
5456
import androidx.compose.ui.Alignment
5557
import androidx.compose.ui.Modifier
5658
import androidx.compose.ui.graphics.Color
59+
import androidx.compose.ui.platform.LocalContext
5760
import androidx.compose.ui.platform.LocalLayoutDirection
5861
import androidx.compose.ui.res.stringResource
5962
import androidx.compose.ui.text.style.TextAlign
@@ -68,6 +71,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.SwitchPreferenceCompose
6871
import io.github.sds100.keymapper.base.utils.ui.compose.icons.FakeShizuku
6972
import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcon
7073
import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons
74+
import io.github.sds100.keymapper.common.utils.SettingsUtils
7175
import io.github.sds100.keymapper.common.utils.State
7276

7377
@Composable
@@ -295,7 +299,17 @@ private fun LoadedContent(
295299
}
296300

297301
when (state) {
298-
ProModeState.Started -> {
302+
is ProModeState.Started -> {
303+
if (!state.isDefaultUsbModeCompatible) {
304+
IncompatibleUsbModeCard(
305+
modifier = Modifier
306+
.fillMaxWidth()
307+
.padding(horizontal = 8.dp),
308+
)
309+
310+
Spacer(Modifier.height(8.dp))
311+
}
312+
299313
ProModeStartedCard(
300314
modifier = Modifier
301315
.fillMaxWidth()
@@ -429,6 +443,44 @@ private fun LoadedContent(
429443
}
430444
}
431445

446+
@Composable
447+
private fun IncompatibleUsbModeCard(modifier: Modifier = Modifier) {
448+
val ctx = LocalContext.current
449+
SetupCard(
450+
modifier = modifier,
451+
color = MaterialTheme.colorScheme.errorContainer,
452+
icon = {
453+
Icon(
454+
imageVector = Icons.Rounded.Usb,
455+
contentDescription = null,
456+
tint = MaterialTheme.colorScheme.onSurface,
457+
)
458+
},
459+
title = stringResource(
460+
R.string.pro_mode_setup_wizard_change_default_usb_configuration_title,
461+
),
462+
content = {
463+
Text(
464+
text = stringResource(
465+
R.string.pro_mode_setup_wizard_change_default_usb_configuration_description,
466+
),
467+
style = MaterialTheme.typography.bodyMedium,
468+
)
469+
},
470+
buttonText = stringResource(
471+
R.string.button_fix,
472+
),
473+
onButtonClick = {
474+
// Go to developer options and highlight the "Default USB configuration" option
475+
SettingsUtils.launchSettingsScreen(
476+
ctx,
477+
Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS,
478+
"default_usb_configuration",
479+
)
480+
},
481+
)
482+
}
483+
432484
@Composable
433485
private fun WarningCard(
434486
modifier: Modifier = Modifier,
@@ -692,7 +744,7 @@ private fun PreviewDark() {
692744
ProModeScreen {
693745
Content(
694746
warningState = ProModeWarningState.Understood,
695-
setupState = State.Data(ProModeState.Started),
747+
setupState = State.Data(ProModeState.Started(isDefaultUsbModeCompatible = true)),
696748
showInfoCard = false,
697749
onInfoCardDismiss = {},
698750
autoStartAtBoot = true,
@@ -728,7 +780,7 @@ private fun PreviewStarted() {
728780
ProModeScreen {
729781
Content(
730782
warningState = ProModeWarningState.Understood,
731-
setupState = State.Data(ProModeState.Started),
783+
setupState = State.Data(ProModeState.Started(isDefaultUsbModeCompatible = false)),
732784
showInfoCard = false,
733785
onInfoCardDismiss = {},
734786
autoStartAtBoot = false,

base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import io.github.sds100.keymapper.base.utils.navigation.navigate
1212
import io.github.sds100.keymapper.base.utils.ui.DialogProvider
1313
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
1414
import io.github.sds100.keymapper.common.utils.State
15-
import javax.inject.Inject
15+
import io.github.sds100.keymapper.common.utils.valueOrNull
1616
import kotlinx.coroutines.ExperimentalCoroutinesApi
1717
import kotlinx.coroutines.delay
1818
import kotlinx.coroutines.flow.Flow
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.flow
2525
import kotlinx.coroutines.flow.flowOf
2626
import kotlinx.coroutines.flow.stateIn
2727
import kotlinx.coroutines.launch
28+
import javax.inject.Inject
2829

2930
@HiltViewModel
3031
class ProModeViewModel @Inject constructor(
@@ -156,7 +157,12 @@ class ProModeViewModel @Inject constructor(
156157
isNotificationPermissionGranted: Boolean,
157158
): State<ProModeState> {
158159
if (isSystemBridgeConnected) {
159-
return State.Data(ProModeState.Started)
160+
return State.Data(
161+
ProModeState.Started(
162+
isDefaultUsbModeCompatible =
163+
useCase.isCompatibleUsbModeSelected().valueOrNull() ?: false,
164+
),
165+
)
160166
} else {
161167
return State.Data(
162168
ProModeState.Stopped(
@@ -182,5 +188,5 @@ sealed class ProModeState {
182188
val isNotificationPermissionGranted: Boolean,
183189
) : ProModeState()
184190

185-
data object Started : ProModeState()
191+
data class Started(val isDefaultUsbModeCompatible: Boolean) : ProModeState()
186192
}

base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package io.github.sds100.keymapper.base.promode
22

33
import android.os.Build
4+
import android.os.Process
45
import androidx.annotation.RequiresApi
56
import dagger.hilt.android.scopes.ViewModelScoped
67
import io.github.sds100.keymapper.common.utils.Constants
8+
import io.github.sds100.keymapper.common.utils.KMResult
79
import io.github.sds100.keymapper.common.utils.firstBlocking
810
import io.github.sds100.keymapper.data.Keys
911
import io.github.sds100.keymapper.data.PreferenceDefaults
@@ -19,7 +21,6 @@ import io.github.sds100.keymapper.system.permissions.Permission
1921
import io.github.sds100.keymapper.system.permissions.PermissionAdapter
2022
import io.github.sds100.keymapper.system.root.SuAdapter
2123
import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter
22-
import javax.inject.Inject
2324
import kotlinx.coroutines.Dispatchers
2425
import kotlinx.coroutines.ExperimentalCoroutinesApi
2526
import kotlinx.coroutines.flow.Flow
@@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.flatMapLatest
2930
import kotlinx.coroutines.flow.flowOf
3031
import kotlinx.coroutines.flow.flowOn
3132
import kotlinx.coroutines.flow.map
33+
import javax.inject.Inject
3234

3335
@RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API)
3436
@ViewModelScoped
@@ -42,6 +44,15 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor(
4244
private val accessibilityServiceAdapter: AccessibilityServiceAdapter,
4345
private val networkAdapter: NetworkAdapter,
4446
) : SystemBridgeSetupUseCase {
47+
48+
companion object {
49+
/**
50+
* This is specified in android/hardware/usb/UsbManager.java and called
51+
* FUNCTION_NONE in that file.
52+
*/
53+
private const val USB_FUNCTION_NONE = 0
54+
}
55+
4556
override val isWarningUnderstood: Flow<Boolean> =
4657
preferences.get(Keys.isProModeWarningUnderstood).map { it ?: false }
4758

@@ -198,6 +209,24 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor(
198209
}
199210
}
200211

212+
/**
213+
* This applies to older Android versions (tested on Android 11 and 13).
214+
* Having the "Default USB Configuration" in developer options set to something other
215+
* than "No data transfer" causes the system bridge (ADB) process to be killed when the device
216+
* is locked. Not 100% sure what the mechanic is but it can be reproduced by pressing the
217+
* power button. On Android 15 this is not the case because it seems like turning on
218+
* wireless debugging or ADB resets the setting to "No data transfer".
219+
*/
220+
override fun isCompatibleUsbModeSelected(): KMResult<Boolean> {
221+
return systemBridgeConnectionManager
222+
.run { systemBridge ->
223+
// The USB setting does not matter if the system bridge is running as root
224+
// because it doesn't rely on the ADB process.
225+
systemBridge.processUid == Process.SHELL_UID &&
226+
systemBridge.usbScreenUnlockedFunctions.toInt() == 0
227+
}
228+
}
229+
201230
@RequiresApi(Build.VERSION_CODES.R)
202231
private fun getNextStep(
203232
accessibilityServiceState: AccessibilityServiceState,
@@ -253,4 +282,6 @@ interface SystemBridgeSetupUseCase {
253282
fun startSystemBridgeWithRoot()
254283
fun startSystemBridgeWithShizuku()
255284
suspend fun startSystemBridgeWithAdb()
285+
286+
fun isCompatibleUsbModeSelected(): KMResult<Boolean>
256287
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,6 +1697,9 @@
16971697
<string name="pro_mode_setup_wizard_enable_notification_permission_description">Key Mapper needs permission to notify you if there are any issues with the set up process.</string>
16981698
<string name="pro_mode_setup_wizard_enable_notification_permission_button">Give permission</string>
16991699

1700+
<string name="pro_mode_setup_wizard_change_default_usb_configuration_title">Incompatible USB configuration</string>
1701+
<string name="pro_mode_setup_wizard_change_default_usb_configuration_description">You must select \'No data transfer\' as your default USB configuration so that PRO Mode is not killed every time you lock your device.</string>
1702+
17001703
<string name="pro_mode_setup_assistant_notification_channel">Setup assistant</string>
17011704

17021705
<string name="pro_mode_setup_wizard_complete_title">PRO mode is running</string>

sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@ interface ISystemBridge {
4646
boolean isTetheringEnabled() = 19;
4747

4848
void setTetheringEnabled(boolean enable) = 20;
49+
50+
long getUsbScreenUnlockedFunctions() = 21;
4951
}

sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import android.content.pm.ApplicationInfo
1212
import android.content.pm.IPackageManager
1313
import android.content.pm.PackageManager
1414
import android.hardware.input.IInputManager
15+
import android.hardware.usb.IUsbManager
1516
import android.media.IAudioService
1617
import android.net.IConnectivityManager
1718
import android.net.ITetheringConnector
@@ -31,6 +32,7 @@ import android.os.Handler
3132
import android.os.IBinder
3233
import android.os.Looper
3334
import android.os.Process
35+
import android.os.RemoteException
3436
import android.os.ServiceManager
3537
import android.permission.IPermissionManager
3638
import android.permission.PermissionManagerApis
@@ -188,6 +190,7 @@ internal class SystemBridge : ISystemBridge.Stub() {
188190
private val activityManager: IActivityManager
189191
private val activityTaskManager: IActivityTaskManager
190192
private val audioService: IAudioService?
193+
private val usbManager: IUsbManager?
191194

192195
private val processPackageName: String = when (Process.myUid()) {
193196
Process.ROOT_UID -> "root"
@@ -281,6 +284,10 @@ internal class SystemBridge : ISystemBridge.Stub() {
281284
tetheringConnector = null
282285
}
283286

287+
waitSystemService(Context.USB_SERVICE)
288+
usbManager =
289+
IUsbManager.Stub.asInterface(ServiceManager.getService(Context.USB_SERVICE))
290+
284291
val applicationInfo = getKeyMapperPackageInfo()
285292

286293
if (applicationInfo == null) {
@@ -767,4 +774,12 @@ internal class SystemBridge : ISystemBridge.Stub() {
767774
tetheringConnector.stopTethering(TETHERING_WIFI, processPackageName, null, null)
768775
}
769776
}
777+
778+
override fun getUsbScreenUnlockedFunctions(): Long {
779+
return try {
780+
usbManager?.screenUnlockedFunctions ?: 0
781+
} catch (_: RemoteException) {
782+
-1
783+
}
784+
}
770785
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package android.hardware.usb;
2+
3+
interface IUsbManager {
4+
long getScreenUnlockedFunctions();
5+
}

0 commit comments

Comments
 (0)