Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
75dbac6
chore: bump version to 4.0.0-beta.2
sds100 Nov 1, 2025
e2950c0
chore: set date in changelog
sds100 Nov 1, 2025
50bf540
output libevdev to a custom build directory so it can be linked to in…
sds100 Nov 1, 2025
b28bedf
chore: specify ndk version in a separate file so it can be synced bet…
sds100 Nov 2, 2025
3828795
#1688 fix: use REPLACE instead of ABORT when inserting/updating a key…
sds100 Nov 2, 2025
fff5f34
style: add ktlint to system module
sds100 Nov 2, 2025
915ed6a
style: reformat system module
sds100 Nov 2, 2025
c79f59c
fix: call getRootInActiveWindow() more safely in the accessibility se…
sds100 Nov 2, 2025
63a7a20
refactor: remove obsolete min sdk check
sds100 Nov 2, 2025
d4bfb6c
fix: check if subtype history is null in AndroidInputMethodAdapter.kt
sds100 Nov 2, 2025
01c3560
fix: send key event to relay service on IO thread
sds100 Nov 2, 2025
ff09dd1
refactor: remove obsolete min sdk check
sds100 Nov 2, 2025
63f67ff
style: invert if statement
sds100 Nov 2, 2025
3a3034c
fix: use separate thread for dispatching gestures in accessibility se…
sds100 Nov 2, 2025
b67e6f8
shorten strings for swipe/pinch actions
sds100 Nov 2, 2025
4ea72e3
remove unused method in OnboardingUseCase.kt
sds100 Nov 4, 2025
b636e62
#1875 show trigger error to migrate screen off key maps
sds100 Nov 4, 2025
a5a140e
fix: only autostart system bridge with Shizuku if Shizuku permission …
sds100 Nov 4, 2025
4047439
remove floating buttons feature notification
sds100 Nov 4, 2025
cfbdc1d
#1875 feat: show notification warning the user to migrate their scree…
sds100 Nov 4, 2025
5e05fa7
style: reformat
sds100 Nov 7, 2025
1663e3e
chore: update whats new
sds100 Nov 7, 2025
e10b653
#1889 show tip for screen off remapping volume buttons with pro mode
sds100 Nov 7, 2025
701a35e
fix: check for root permission on IO thread
sds100 Nov 7, 2025
e2fbab4
fix: do not log spam Failed to grab evdev devices
sds100 Nov 7, 2025
10d0317
improve logging system bridge connections
sds100 Nov 7, 2025
628b31e
update strings
sds100 Nov 7, 2025
ea64524
fix: create notification channel for system bridge setup in Notificat…
sds100 Nov 7, 2025
27129fb
add more logging to system bridge starting
sds100 Nov 7, 2025
af17511
fix: Starting system bridge for the first time would be janky because…
sds100 Nov 7, 2025
19900b4
style: add ktlint to systemstubs module
sds100 Nov 7, 2025
eb4b385
#1886 fix: mobile data actions work in obfuscated builds
sds100 Nov 7, 2025
a1e5744
#1890 fix: copy log to clipboard in reverse order so the most recent …
sds100 Nov 7, 2025
9e25597
delete unused menu xml files
sds100 Nov 7, 2025
8394585
#1890 feat: add button to save log to file and share it
sds100 Nov 7, 2025
288bae3
chore: bump version code
sds100 Nov 7, 2025
a79c196
chore: set release date in changelog
sds100 Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
## [4.0.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.02)

#### 08 November 2025

## Added

- #1890 add button to save log to file and share it. The clipboard button now cuts off older entries and keeps newest ones.

## Fixed

- Only autostart PRO mode with Shizuku if Shizuku permission is granted. Otherwise fallback to method with Wireless Debugging and WRITE_SECURE_SETTINGS permission.
- Starting system bridge for the first time would be janky because granting READ_LOGS kills the app process. Only grant for READ_LOGS when sharing logcat from settings.
- #1886 mobile data actions work in PRO mode.

## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01)

#### TO BE RELEASED
#### 01 November 2025

## Added

Expand Down
2 changes: 2 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
-keep class io.github.sds100.keymapper.api.IKeyEventRelayService$Stub { *; }
-keep class io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback { *; }
-keep class io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback$Stub { *; }
-keep class com.android.internal.telephony.ITelephony { *; }
-keep class com.android.internal.telephony.ITelephony$Stub { *; }

-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
Expand Down
4 changes: 2 additions & 2 deletions app/version.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION_NAME=4.0.0-beta.1
VERSION_CODE=185
VERSION_NAME=4.0.0-beta.2
VERSION_CODE=187
VERSION_NUM=01
2 changes: 1 addition & 1 deletion base/src/main/assets/whats-new.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
✨ Screen-off remapping
You can now remap buttons when the screen is off (including the power button) for free with PRO mode.
You can now remap ALL buttons when the screen is off (including the power button) for free with PRO mode.

🎯 New Actions
• Run shell commands
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import io.github.sds100.keymapper.data.repositories.LogRepository
import io.github.sds100.keymapper.data.repositories.PreferenceRepositoryImpl
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
import io.github.sds100.keymapper.sysbridge.manager.isConnected
import io.github.sds100.keymapper.system.apps.AndroidPackageManagerAdapter
import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter
import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl
Expand Down Expand Up @@ -224,6 +225,12 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
autoGrantPermissionController.start()
keyEventRelayServiceWrapper.bind()

if (systemBridgeConnectionManager.isConnected()) {
Timber.i("KeyMapperApp: System bridge is connected")
} else {
Timber.i("KeyMapperApp: System bridge is disconnected")
}

if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) {
systemBridgeAutoStarter.init()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,10 @@ abstract class BaseMainActivity : AppCompatActivity() {
// the activities have not necessarily resumed at that point.
permissionAdapter.onPermissionsChanged()
serviceAdapter.invalidateState()
suAdapter.invalidateIsRooted()
suAdapter.requestPermission()
systemBridgeSetupController.invalidateSettings()
networkAdapter.invalidateState()
onboardingUseCase.handledMigrateScreenOffKeyMapsNotification()
}

override fun onDestroy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.base.actions.swipescreen
import android.accessibilityservice.GestureDescription
import android.graphics.Bitmap
import android.graphics.Point
import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
Expand Down Expand Up @@ -96,9 +95,7 @@ class SwipePickDisplayCoordinateViewModel @Inject constructor(

var maxFingerCount = 10

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
maxFingerCount = GestureDescription.getMaxStrokeCount()
}
maxFingerCount = GestureDescription.getMaxStrokeCount()

if (count > maxFingerCount) {
return@map resourceProvider.getString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,9 @@ private fun getTriggerErrorMessage(error: TriggerError): String {
TriggerError.EVDEV_DEVICE_NOT_FOUND -> stringResource(
R.string.trigger_error_evdev_device_not_found,
)
TriggerError.MIGRATE_SCREEN_OFF_TRIGGER -> stringResource(
R.string.trigger_error_migrate_screen_off_key_map,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ class EvdevHandleCache @Inject constructor(
devicesAdapter.connectedInputDevices,
systemBridgeConnectionManager.connectionState,
) { _, connectionState ->
if (connectionState !is SystemBridgeConnectionState.Connected) {
devicesByPath.value = emptyMap()
} else {
if (connectionState is SystemBridgeConnectionState.Connected) {
invalidate()
} else {
devicesByPath.value = emptyMap()
}
}.collect()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import io.github.sds100.keymapper.sysbridge.IEvdevCallback
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
import io.github.sds100.keymapper.sysbridge.manager.isConnected
import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError
import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent
import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent
import io.github.sds100.keymapper.system.inputevents.KMInputEvent
Expand Down Expand Up @@ -304,8 +305,11 @@ class InputEventHubImpl @Inject constructor(
.onSuccess { result ->
Timber.i("Grabbed evdev devices [${evdevDevices.joinToString { it.name }}]")
}
.onFailure {
Timber.e("Failed to grab evdev devices.")
.onFailure { error ->
// Do not log if it is expected to prevent log spam.
if (error !is SystemBridgeError.Disconnected) {
Timber.e("Failed to grab evdev devices.")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ class DisplayKeyMapUseCaseImpl @Inject constructor(
TriggerError.EVDEV_DEVICE_NOT_FOUND,
TriggerError.FLOATING_BUTTON_DELETED,
TriggerError.SYSTEM_BRIDGE_UNSUPPORTED,
TriggerError.MIGRATE_SCREEN_OFF_TRIGGER,
-> {}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
package io.github.sds100.keymapper.base.logging

import android.content.Context
import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.sds100.keymapper.base.R
import io.github.sds100.keymapper.base.utils.ShareUtils
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
import io.github.sds100.keymapper.common.BuildConfigProvider
import io.github.sds100.keymapper.data.entities.LogEntryEntity
import io.github.sds100.keymapper.data.repositories.LogRepository
import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter
import io.github.sds100.keymapper.system.files.FileAdapter
import io.github.sds100.keymapper.system.files.FileUtils
import io.github.sds100.keymapper.system.files.IFile
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class DisplayLogUseCaseImpl @Inject constructor(
@ApplicationContext private val ctx: Context,
private val coroutineScope: CoroutineScope,
private val repository: LogRepository,
private val resourceProvider: ResourceProvider,
private val clipboardAdapter: ClipboardAdapter,
private val fileAdapter: FileAdapter,
private val buildConfigProvider: BuildConfigProvider,
) : DisplayLogUseCase {
private val dateFormat = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault())
private val severityString: Map<Int, String> = mapOf(
Expand All @@ -40,19 +53,53 @@ class DisplayLogUseCaseImpl @Inject constructor(

override suspend fun copyToClipboard() {
val logEntries = repository.log.first()
val logText = createLogText(logEntries)
val logText = createLogClipboardText(logEntries)

clipboardAdapter.copy(
label = resourceProvider.getString(R.string.clip_key_mapper_log),
logText,
)
}

private fun createLogText(logEntries: List<LogEntryEntity>): String {
return logEntries.joinToString(separator = "\n") { entry ->
val date = dateFormat.format(Date(entry.time))
private fun createLogClipboardText(logEntries: List<LogEntryEntity>): String {
return buildString {
append("Key Mapper log (newest first). Note: it may be cut off due to clipboard limits")
appendLine()
appendLine()

return@joinToString "$date ${severityString[entry.severity]} ${entry.message}"
logEntries
.reversed()
.joinToString(separator = "\n", transform = ::entryToString)
.also { append(it) }
}
}

private fun entryToString(entry: LogEntryEntity): String {
val date = dateFormat.format(Date(entry.time))

return "$date ${severityString[entry.severity]} ${entry.message}"
}

override fun shareFile() {
val fileName = "logs/key_mapper_log_${FileUtils.createFileDate()}.txt"

coroutineScope.launch {
withContext(Dispatchers.IO) {
val logEntries = repository.log.first()
val logText = logEntries.joinToString(separator = "\n", transform = ::entryToString)

val file: IFile = fileAdapter.getPrivateFile(fileName)
file.createFile()

with(file.outputStream()?.bufferedWriter()) {
this?.write(logText)
this?.flush()
}

val publicUri = fileAdapter.getPublicUriForPrivateFile(file)

ShareUtils.shareFile(ctx, publicUri.toUri(), buildConfigProvider.packageName)
}
}
}
}
Expand All @@ -61,4 +108,5 @@ interface DisplayLogUseCase {
val log: Flow<List<LogEntry>>
fun clearLog()
suspend fun copyToClipboard()
fun shareFile()
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
Expand Down Expand Up @@ -53,6 +54,7 @@ fun LogScreen(
modifier = modifier,
onBackClick = onBackClick,
onCopyToClipboardClick = viewModel::onCopyToClipboardClick,
onShareClick = viewModel::onShareFileClick,
onClearLogClick = viewModel::onClearLogClick,
content = {
Content(
Expand All @@ -69,6 +71,7 @@ private fun LogScreen(
modifier: Modifier = Modifier,
onBackClick: () -> Unit = {},
onCopyToClipboardClick: () -> Unit = {},
onShareClick: () -> Unit = {},
onClearLogClick: () -> Unit = {},
content: @Composable () -> Unit,
) {
Expand Down Expand Up @@ -96,6 +99,13 @@ private fun LogScreen(
)
}
Spacer(Modifier.weight(1f))
IconButton(onClick = onShareClick) {
Icon(
imageVector = Icons.Outlined.Share,
contentDescription = stringResource(R.string.action_share_log),
)
}

IconButton(onClick = onCopyToClipboardClick) {
Icon(
imageVector = Icons.Outlined.ContentCopy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ class LogViewModel @Inject constructor(private val displayLogUseCase: DisplayLog
}
}

fun onShareFileClick() {
viewModelScope.launch {
displayLogUseCase.shareFile()
}
}

fun onClearLogClick() {
displayLogUseCase.clearLog()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.sds100.keymapper.base.logging

import android.Manifest
import android.content.Context
import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -12,6 +13,8 @@ import io.github.sds100.keymapper.common.utils.then
import io.github.sds100.keymapper.system.files.FileAdapter
import io.github.sds100.keymapper.system.files.FileUtils
import io.github.sds100.keymapper.system.files.IFile
import io.github.sds100.keymapper.system.permissions.Permission
import io.github.sds100.keymapper.system.permissions.PermissionAdapter
import io.github.sds100.keymapper.system.shell.ShellAdapter
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
Expand All @@ -22,9 +25,18 @@ class ShareLogcatUseCaseImpl @Inject constructor(
@ApplicationContext private val ctx: Context,
private val fileAdapter: FileAdapter,
private val shellAdapter: ShellAdapter,
private val permissionAdapter: PermissionAdapter,
private val buildConfigProvider: BuildConfigProvider,
) : ShareLogcatUseCase {

override fun isPermissionGranted(): Boolean {
return permissionAdapter.isGranted(Permission.READ_LOGS)
}

override fun grantPermission(): KMResult<*> {
return permissionAdapter.grant(Manifest.permission.READ_LOGS)
}

override suspend fun share(): KMResult<Unit> {
val fileName = "logs/logcat_${FileUtils.createFileDate()}.txt"

Expand All @@ -45,5 +57,7 @@ class ShareLogcatUseCaseImpl @Inject constructor(
}

interface ShareLogcatUseCase {
fun isPermissionGranted(): Boolean
fun grantPermission(): KMResult<*>
suspend fun share(): KMResult<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ class OnboardingTipDelegateImpl @Inject constructor(
val hasBackKey =
trigger.keys.any { it is KeyEventTriggerKey && it.keyCode == KeyEvent.KEYCODE_BACK }
val hasImeKey = trigger.keys.any { it is KeyEventTriggerKey && it.requiresIme }
val hasVolumeKey = trigger.keys
.filterIsInstance<KeyEventTriggerKey>()
.any {
it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
it.keyCode == KeyEvent.KEYCODE_VOLUME_UP
}

when {
showPowerButtonEmergencyTip -> {
Expand Down Expand Up @@ -212,17 +218,16 @@ class OnboardingTipDelegateImpl @Inject constructor(
triggerTip.value = tipModel
}

// DISABLE UNTIL PRO MODE IS STABLE
// hasVolumeKey && !shownVolumeButtonsProModeTip -> {
// val tip = OnboardingTipModel(
// id = VOLUME_BUTTONS_PRO_MODE_TIP_ID,
// title = getString(R.string.tip_volume_buttons_pro_mode_title),
// message = getString(R.string.tip_volume_buttons_pro_mode_text),
// isDismissable = true,
// buttonText = getString(R.string.tip_volume_buttons_pro_mode_button),
// )
// triggerTip.value = tip
// }
hasVolumeKey && !shownVolumeButtonsProModeTip -> {
val tip = OnboardingTipModel(
id = VOLUME_BUTTONS_PRO_MODE_TIP_ID,
title = getString(R.string.tip_volume_buttons_pro_mode_title),
message = getString(R.string.tip_volume_buttons_pro_mode_text),
isDismissable = true,
buttonText = getString(R.string.tip_volume_buttons_pro_mode_button),
)
triggerTip.value = tip
}

hasCapsLockKey && !shownCapsLockProModeTip -> {
val tip = OnboardingTipModel(
Expand Down
Loading