Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
248907c
#2140 feat: add monochrome app icon for themed icon support on Androi…
claude May 31, 2026
7a1e06d
Merge branch 'develop' into claude/issue-2140-monochrome-icon
sds100 Jun 1, 2026
89d4191
#1980 feat: add notification panel constraint
claude Jun 2, 2026
d26633e
fix: Support email string formatting fix
jambl3r Jun 3, 2026
43c38b0
use correct monochrome foreground icon
sds100 Jun 3, 2026
4b71c30
Merge branch 'develop' into claude/issue-2140-monochrome-icon
sds100 Jun 3, 2026
a02e99f
Merge pull request #2148 from keymapperorg/claude/issue-2140-monochro…
sds100 Jun 3, 2026
801b714
bump version code
sds100 Jun 3, 2026
7b260c4
add changelog release date
sds100 Jun 3, 2026
5197b3f
Merge pull request #2151 from keymapperorg/jambl3r-patch-1
sds100 Jun 3, 2026
f5a5ac3
update whats-new
sds100 Jun 4, 2026
ef0ae78
Delay accessibility service init until user unlock
smh786 Jun 5, 2026
47c620a
Fix Direct Boot accessibility startup
smh786 Jun 5, 2026
af57c6e
#2153 fix: add Direct Boot changelog entry
smh786 Jun 6, 2026
0f66372
#2154 fix: gate expert mode debug screen on warning acknowledgement
claude Jun 9, 2026
bb6b436
bump version to 4.2.1
sds100 Jun 9, 2026
defea69
Merge pull request #2158 from keymapperorg/claude/issue-2154-gate-deb…
sds100 Jun 9, 2026
9572773
Merge remote-tracking branch 'origin/develop' into develop
sds100 Jun 9, 2026
3e6e15e
Merge branch 'develop' into codex/direct-boot-accessibility-guard
sds100 Jun 9, 2026
6a7a752
#2156 fix: do not throw an error when the Talkback application can no…
sds100 Jun 9, 2026
c63be7a
Merge pull request #2153 from smh786/codex/direct-boot-accessibility-…
sds100 Jun 9, 2026
8816dd9
fix changelog
sds100 Jun 9, 2026
c88dec4
Merge remote-tracking branch 'origin/develop' into develop
sds100 Jun 9, 2026
aed7ca3
#2157 feat: use settings ContentProvider via system bridge for choose…
claude Jun 9, 2026
1b2a077
cleanup code
sds100 Jun 9, 2026
bf4b09b
add proguard rules for android.os.ICancellationSignal
sds100 Jun 9, 2026
9c6aeb2
Merge pull request #2159 from keymapperorg/claude/issue-2157-settings…
sds100 Jun 9, 2026
77eb149
bump version code
sds100 Jun 11, 2026
d7c1091
Merge remote-tracking branch 'origin/develop' into develop
sds100 Jun 11, 2026
61e9c72
#1980 style: fix import ordering in ConstraintUtils.kt
claude Jun 15, 2026
0c88cd6
bump version to 4.2.2
sds100 Jun 24, 2026
bc11b09
update foss paywall text
sds100 Jun 24, 2026
389a17d
#2163 feat: add ringer mode constraints
claude Jun 27, 2026
f3398a6
#2163 refactor: replace three RingerMode data objects with single dat…
claude Jun 29, 2026
30e2f1a
#2163 refactor: use single RingerMode constraint data class with ring…
claude Jun 29, 2026
f08ac66
#2163 style: fix import ordering in ConstraintUtils
claude Jun 29, 2026
56899ee
#2163 fix: use data.ringerMode not constraint.ringerMode in when bran…
claude Jun 29, 2026
1a325a7
Merge pull request #2177 from keymapperorg/claude/issue-2163-ringer-m…
sds100 Jul 9, 2026
7ad0623
bump version to 4.3.0
sds100 Jul 9, 2026
45d1778
#1980 feat: add notification panel constraint
claude Jun 2, 2026
14b5fd2
#1980 style: fix import ordering in ConstraintUtils.kt
claude Jun 15, 2026
bbcd028
Merge remote-tracking branch 'origin/claude/issue-1980-notification-p…
sds100 Jul 9, 2026
90fae20
fix changelog
sds100 Jul 9, 2026
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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
## [4.3.0](https://github.com/sds100/KeyMapper/releases/tag/v4.3.0)

#### TO BE RELEASED

## Added

- #2163 Add ringer mode constraints (Ring, Vibrate, Silent).
- #1980 Add "Notification panel is showing" and "Notification panel is not showing" constraints. Detection uses the accessibility service window list and is best-effort; accuracy may vary across OEMs and Android versions.

## [4.2.1](https://github.com/sds100/KeyMapper/releases/tag/v4.2.1)

#### 09 June 2026

## Added

- #2140 Add monochrome app icon layer for themed icon support on Android 13+.

## Fixed

- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged.
- #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there.
- #2153 Prevent Direct Boot startup from initializing credential-encrypted app storage before the user unlocks.
- #2157 The "choose setting" screen now uses `settings list` via the system bridge when expert mode is active, surfacing all device settings instead of only those visible through the ContentProvider.

## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0)

## Fixed
Expand Down
2 changes: 2 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
# android.content package classes
-keep class android.content.IContentProvider** { *; }
-keep class android.content.IIntentReceiver** { *; }
-keep class android.os.ICancellationSignal** { *; }

# android.content.pm package classes
-keep class android.content.pm.IPackageManager** { *; }
Expand Down Expand Up @@ -245,3 +246,4 @@
-dontwarn android.view.IWindowManager**
-dontwarn com.android.internal.app.**
-dontwarn com.android.internal.policy.**
-dontwarn android.os.ICancellationSignal
1 change: 1 addition & 0 deletions app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>
1 change: 1 addition & 0 deletions app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>
1 change: 1 addition & 0 deletions app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>
1 change: 1 addition & 0 deletions app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
<application
android:name="io.github.sds100.keymapper.KeyMapperApp"
android:allowBackup="true"
android:directBootAware="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
Expand Down Expand Up @@ -55,6 +54,7 @@
<service
android:name=".system.accessibility.MyAccessibilityService"
android:configChanges="orientation|screenSize"
android:directBootAware="false"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
Expand All @@ -67,4 +67,4 @@
</service>

</application>
</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package io.github.sds100.keymapper.system.accessibility

import android.content.Intent
import android.os.UserManager
import android.view.KeyEvent
import android.view.accessibility.AccessibilityEvent
import androidx.core.content.getSystemService
import dagger.hilt.android.AndroidEntryPoint
import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityService
import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController
Expand All @@ -14,6 +18,9 @@ class MyAccessibilityService : BaseAccessibilityService() {
lateinit var controllerFactory: AccessibilityServiceController.Factory

private var controller: AccessibilityServiceController? = null
private var loggedLockedInitDelay = false

private val userManager: UserManager? by lazy { getSystemService<UserManager>() }

override fun getController(): BaseAccessibilityServiceController? {
return controller
Expand All @@ -22,15 +29,47 @@ class MyAccessibilityService : BaseAccessibilityService() {
override fun onServiceConnected() {
super.onServiceConnected()

initializeControllerIfUserUnlocked()
}

override fun onAccessibilityEvent(event: AccessibilityEvent?) {
if (!initializeControllerIfUserUnlocked()) {
return
}

super.onAccessibilityEvent(event)
}

override fun onKeyEvent(event: KeyEvent?): Boolean {
if (!initializeControllerIfUserUnlocked()) {
return false
}

return super.onKeyEvent(event)
}

private fun initializeControllerIfUserUnlocked(): Boolean {
if (userManager?.isUserUnlocked == false) {
if (!loggedLockedInitDelay) {
Timber.i("Accessibility service: Delay init because locked.")
loggedLockedInitDelay = true
}

return false
}

loggedLockedInitDelay = false

/*
I would put this in onCreate but for some reason on some devices getting the application
context would return null
*/
if (controller == null) {
controller = controllerFactory.create(this)
controller?.onServiceConnected()
}

controller?.onServiceConnected()
return true
}

override fun onUnbind(intent: Intent?): Boolean {
Expand Down
4 changes: 2 additions & 2 deletions app/version.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VERSION_NAME=4.2.0
VERSION_CODE=252
VERSION_NAME=4.3.0
VERSION_CODE=257
11 changes: 4 additions & 7 deletions base/src/main/assets/whats-new.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
🎯 New Actions
• Select all text at the cursor
• Input on-screen keyboard enter/send button
• Show a toast message

🔧 Improvements
• Bug fixes
• Talkback actions
• Monochrome app icon
• "Any input device" is the default for triggers
• Bug fixes and accessibility improvements

📖 View the complete changelog at: http://keymapper.app/changelog
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import dagger.Lazy
import io.github.sds100.keymapper.base.expertmode.SystemBridgeAutoStarter
import io.github.sds100.keymapper.base.logging.KeyMapperLoggingTree
import io.github.sds100.keymapper.base.logging.SystemBridgeLogger
Expand Down Expand Up @@ -51,46 +52,46 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
private val tag = BaseKeyMapperApp::class.simpleName

@Inject
lateinit var appCoroutineScope: CoroutineScope
lateinit var appCoroutineScope: Lazy<CoroutineScope>

@Inject
lateinit var notificationController: NotificationController
lateinit var notificationController: Lazy<NotificationController>

@Inject
lateinit var packageManagerAdapter: AndroidPackageManagerAdapter
lateinit var packageManagerAdapter: Lazy<AndroidPackageManagerAdapter>

@Inject
lateinit var devicesAdapter: AndroidDevicesAdapter
lateinit var devicesAdapter: Lazy<AndroidDevicesAdapter>

@Inject
lateinit var permissionAdapter: AndroidPermissionAdapter
lateinit var permissionAdapter: Lazy<AndroidPermissionAdapter>

@Inject
lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl
lateinit var accessibilityServiceAdapter: Lazy<AccessibilityServiceAdapterImpl>

@Inject
lateinit var autoGrantPermissionController: AutoGrantPermissionController
lateinit var autoGrantPermissionController: Lazy<AutoGrantPermissionController>

@Inject
lateinit var loggingTree: KeyMapperLoggingTree
lateinit var loggingTree: Lazy<KeyMapperLoggingTree>

@Inject
lateinit var settingsRepository: PreferenceRepositoryImpl
lateinit var settingsRepository: Lazy<PreferenceRepositoryImpl>

@Inject
lateinit var logRepository: LogRepository
lateinit var logRepository: Lazy<LogRepository>

@Inject
lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl
lateinit var keyEventRelayServiceWrapper: Lazy<KeyEventRelayServiceWrapperImpl>

@Inject
lateinit var systemBridgeAutoStarter: SystemBridgeAutoStarter
lateinit var systemBridgeAutoStarter: Lazy<SystemBridgeAutoStarter>

@Inject
lateinit var systemBridgeConnectionManager: SystemBridgeConnectionManagerImpl
lateinit var systemBridgeConnectionManager: Lazy<SystemBridgeConnectionManagerImpl>

@Inject
lateinit var systemBridgeLogger: SystemBridgeLogger
lateinit var systemBridgeLogger: Lazy<SystemBridgeLogger>

private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() }

Expand Down Expand Up @@ -118,16 +119,18 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
Log.i(tag, "KeyMapperApp: OnCreate")

Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
// log in a blocking manner and always log regardless of whether the setting is turned on
val entry = LogEntryEntity(
id = 0,
time = Calendar.getInstance().timeInMillis,
severity = LogEntryEntity.SEVERITY_ERROR,
message = exception.stackTraceToString(),
)

runBlocking {
logRepository.insertSuspend(entry)
if (userManager?.isUserUnlocked != false) {
// log in a blocking manner and always log regardless of whether the setting is turned on
val entry = LogEntryEntity(
id = 0,
time = Calendar.getInstance().timeInMillis,
severity = LogEntryEntity.SEVERITY_ERROR,
message = exception.stackTraceToString(),
)

runBlocking {
logRepository.get().insertSuspend(entry)
}
}

priorExceptionHandler?.uncaughtException(thread, exception)
Expand Down Expand Up @@ -168,7 +171,7 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {

registerReceiver(broadcastReceiver, intentFilter)

settingsRepository.get(Keys.darkTheme)
settingsRepository.get().get(Keys.darkTheme)
.map { it?.toIntOrNull() }
.map {
when (it) {
Expand All @@ -178,35 +181,35 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
}
}
.onEach { mode -> AppCompatDelegate.setDefaultNightMode(mode) }
.launchIn(appCoroutineScope)
.launchIn(appCoroutineScope.get())

if (BuildConfig.BUILD_TYPE == "debug" || BuildConfig.BUILD_TYPE == "debug_release") {
Timber.plant(Timber.DebugTree())
}

Timber.plant(loggingTree)
Timber.plant(loggingTree.get())

notificationController.init()
notificationController.get().init()

processLifecycleOwner.lifecycle.addObserver(
object : LifecycleObserver {
@Suppress("DEPRECATION")
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
// when the user returns to the app let everything know that the permissions could have changed
notificationController.onOpenApp()
notificationController.get().onOpenApp()

if (BuildConfig.DEBUG &&
permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)
permissionAdapter.get().isGranted(Permission.WRITE_SECURE_SETTINGS)
) {
accessibilityServiceAdapter.start()
accessibilityServiceAdapter.get().start()
}
}
},
)

appCoroutineScope.launch {
notificationController.openApp.collectLatest { intentAction ->
appCoroutineScope.get().launch {
notificationController.get().openApp.collectLatest { intentAction ->
Intent(this@BaseKeyMapperApp, getMainActivityClass()).apply {
action = intentAction
flags = Intent.FLAG_ACTIVITY_NEW_TASK
Expand All @@ -216,38 +219,38 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
}
}

notificationController.showToast.onEach { toast ->
notificationController.get().showToast.onEach { toast ->
Toast.makeText(this, toast, Toast.LENGTH_SHORT).show()
}.launchIn(appCoroutineScope)
}.launchIn(appCoroutineScope.get())

autoGrantPermissionController.start()
keyEventRelayServiceWrapper.bind()
autoGrantPermissionController.get().start()
keyEventRelayServiceWrapper.get().bind()

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

systemBridgeAutoStarter.init()
systemBridgeAutoStarter.get().init()

// Initialize SystemBridgeLogger to start receiving log messages from SystemBridge.
// Using Lazy<> to avoid circular dependency issues and ensure it's only created
// when the API level requirement is met.
systemBridgeLogger.start()
systemBridgeLogger.get().start()

appCoroutineScope.launch {
systemBridgeConnectionManager.connectionState.collect { state ->
appCoroutineScope.get().launch {
systemBridgeConnectionManager.get().connectionState.collect { state ->
if (state is SystemBridgeConnectionState.Connected) {
val isUsed =
settingsRepository.get(Keys.isSystemBridgeUsed).first() ?: false
settingsRepository.get().get(Keys.isSystemBridgeUsed).first() ?: false

// Enable the setting to use PRO mode for key event actions the first time they use PRO mode.
if (!isUsed) {
settingsRepository.set(Keys.keyEventActionsUseSystemBridge, true)
settingsRepository.get().set(Keys.keyEventActionsUseSystemBridge, true)
}

settingsRepository.set(Keys.isSystemBridgeUsed, true)
settingsRepository.get().set(Keys.isSystemBridgeUsed, true)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ class BootBroadcastReceiver : BroadcastReceiver() {
Timber.i(
"Boot completed broadcast: time since boot = ${SystemClock.elapsedRealtime() / 1000}",
)
(context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked()
}

Intent.ACTION_LOCKED_BOOT_COMPLETED -> {
(context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked()
Timber.i(
"Locked boot completed broadcast: time since boot = ${SystemClock.elapsedRealtime() / 1000}",
)
}
}
}
Expand Down
Loading
Loading