Skip to content

Commit 9d50d2e

Browse files
authored
Merge pull request #2162 from keymapperorg/develop
Version 4.2.1
2 parents 9f8a41e + d7c1091 commit 9d50d2e

15 files changed

Lines changed: 279 additions & 82 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## [4.2.1](https://github.com/sds100/KeyMapper/releases/tag/v4.2.1)
2+
3+
#### 09 June 2026
4+
5+
## Fixed
6+
7+
- #2154 The expert mode debug screen is now only accessible after the expert mode warning has been acknowledged.
8+
- #2156 Do not throw an error when the Talkback application can not be found because there are many different package names out there.
9+
- #2153 Prevent Direct Boot startup from initializing credential-encrypted app storage before the user unlocks.
10+
- #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.
11+
112
## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0)
213

314
#### 03 June 2026

app/proguard-rules.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
# android.content package classes
163163
-keep class android.content.IContentProvider** { *; }
164164
-keep class android.content.IIntentReceiver** { *; }
165+
-keep class android.os.ICancellationSignal** { *; }
165166

166167
# android.content.pm package classes
167168
-keep class android.content.pm.IPackageManager** { *; }
@@ -245,3 +246,4 @@
245246
-dontwarn android.view.IWindowManager**
246247
-dontwarn com.android.internal.app.**
247248
-dontwarn com.android.internal.policy.**
249+
-dontwarn android.os.ICancellationSignal

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
<application
1919
android:name="io.github.sds100.keymapper.KeyMapperApp"
2020
android:allowBackup="true"
21-
android:directBootAware="true"
2221
android:icon="@mipmap/ic_launcher"
2322
android:label="@string/app_name"
2423
android:roundIcon="@mipmap/ic_launcher_round"
@@ -55,6 +54,7 @@
5554
<service
5655
android:name=".system.accessibility.MyAccessibilityService"
5756
android:configChanges="orientation|screenSize"
57+
android:directBootAware="false"
5858
android:exported="true"
5959
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
6060
<intent-filter>
@@ -67,4 +67,4 @@
6767
</service>
6868

6969
</application>
70-
</manifest>
70+
</manifest>

app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package io.github.sds100.keymapper.system.accessibility
22

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

1620
private var controller: AccessibilityServiceController? = null
21+
private var loggedLockedInitDelay = false
22+
23+
private val userManager: UserManager? by lazy { getSystemService<UserManager>() }
1724

1825
override fun getController(): BaseAccessibilityServiceController? {
1926
return controller
@@ -22,15 +29,47 @@ class MyAccessibilityService : BaseAccessibilityService() {
2229
override fun onServiceConnected() {
2330
super.onServiceConnected()
2431

32+
initializeControllerIfUserUnlocked()
33+
}
34+
35+
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
36+
if (!initializeControllerIfUserUnlocked()) {
37+
return
38+
}
39+
40+
super.onAccessibilityEvent(event)
41+
}
42+
43+
override fun onKeyEvent(event: KeyEvent?): Boolean {
44+
if (!initializeControllerIfUserUnlocked()) {
45+
return false
46+
}
47+
48+
return super.onKeyEvent(event)
49+
}
50+
51+
private fun initializeControllerIfUserUnlocked(): Boolean {
52+
if (userManager?.isUserUnlocked == false) {
53+
if (!loggedLockedInitDelay) {
54+
Timber.i("Accessibility service: Delay init because locked.")
55+
loggedLockedInitDelay = true
56+
}
57+
58+
return false
59+
}
60+
61+
loggedLockedInitDelay = false
62+
2563
/*
2664
I would put this in onCreate but for some reason on some devices getting the application
2765
context would return null
2866
*/
2967
if (controller == null) {
3068
controller = controllerFactory.create(this)
69+
controller?.onServiceConnected()
3170
}
3271

33-
controller?.onServiceConnected()
72+
return true
3473
}
3574

3675
override fun onUnbind(intent: Intent?): Boolean {

app/version.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
VERSION_NAME=4.2.0
2-
VERSION_CODE=253
1+
VERSION_NAME=4.2.1
2+
VERSION_CODE=256

base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.lifecycle.LifecycleObserver
1515
import androidx.lifecycle.OnLifecycleEvent
1616
import androidx.lifecycle.ProcessLifecycleOwner
1717
import androidx.multidex.MultiDexApplication
18+
import dagger.Lazy
1819
import io.github.sds100.keymapper.base.expertmode.SystemBridgeAutoStarter
1920
import io.github.sds100.keymapper.base.logging.KeyMapperLoggingTree
2021
import io.github.sds100.keymapper.base.logging.SystemBridgeLogger
@@ -51,46 +52,46 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
5152
private val tag = BaseKeyMapperApp::class.simpleName
5253

5354
@Inject
54-
lateinit var appCoroutineScope: CoroutineScope
55+
lateinit var appCoroutineScope: Lazy<CoroutineScope>
5556

5657
@Inject
57-
lateinit var notificationController: NotificationController
58+
lateinit var notificationController: Lazy<NotificationController>
5859

5960
@Inject
60-
lateinit var packageManagerAdapter: AndroidPackageManagerAdapter
61+
lateinit var packageManagerAdapter: Lazy<AndroidPackageManagerAdapter>
6162

6263
@Inject
63-
lateinit var devicesAdapter: AndroidDevicesAdapter
64+
lateinit var devicesAdapter: Lazy<AndroidDevicesAdapter>
6465

6566
@Inject
66-
lateinit var permissionAdapter: AndroidPermissionAdapter
67+
lateinit var permissionAdapter: Lazy<AndroidPermissionAdapter>
6768

6869
@Inject
69-
lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl
70+
lateinit var accessibilityServiceAdapter: Lazy<AccessibilityServiceAdapterImpl>
7071

7172
@Inject
72-
lateinit var autoGrantPermissionController: AutoGrantPermissionController
73+
lateinit var autoGrantPermissionController: Lazy<AutoGrantPermissionController>
7374

7475
@Inject
75-
lateinit var loggingTree: KeyMapperLoggingTree
76+
lateinit var loggingTree: Lazy<KeyMapperLoggingTree>
7677

7778
@Inject
78-
lateinit var settingsRepository: PreferenceRepositoryImpl
79+
lateinit var settingsRepository: Lazy<PreferenceRepositoryImpl>
7980

8081
@Inject
81-
lateinit var logRepository: LogRepository
82+
lateinit var logRepository: Lazy<LogRepository>
8283

8384
@Inject
84-
lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl
85+
lateinit var keyEventRelayServiceWrapper: Lazy<KeyEventRelayServiceWrapperImpl>
8586

8687
@Inject
87-
lateinit var systemBridgeAutoStarter: SystemBridgeAutoStarter
88+
lateinit var systemBridgeAutoStarter: Lazy<SystemBridgeAutoStarter>
8889

8990
@Inject
90-
lateinit var systemBridgeConnectionManager: SystemBridgeConnectionManagerImpl
91+
lateinit var systemBridgeConnectionManager: Lazy<SystemBridgeConnectionManagerImpl>
9192

9293
@Inject
93-
lateinit var systemBridgeLogger: SystemBridgeLogger
94+
lateinit var systemBridgeLogger: Lazy<SystemBridgeLogger>
9495

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

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

120121
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
121-
// log in a blocking manner and always log regardless of whether the setting is turned on
122-
val entry = LogEntryEntity(
123-
id = 0,
124-
time = Calendar.getInstance().timeInMillis,
125-
severity = LogEntryEntity.SEVERITY_ERROR,
126-
message = exception.stackTraceToString(),
127-
)
128-
129-
runBlocking {
130-
logRepository.insertSuspend(entry)
122+
if (userManager?.isUserUnlocked != false) {
123+
// log in a blocking manner and always log regardless of whether the setting is turned on
124+
val entry = LogEntryEntity(
125+
id = 0,
126+
time = Calendar.getInstance().timeInMillis,
127+
severity = LogEntryEntity.SEVERITY_ERROR,
128+
message = exception.stackTraceToString(),
129+
)
130+
131+
runBlocking {
132+
logRepository.get().insertSuspend(entry)
133+
}
131134
}
132135

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

169172
registerReceiver(broadcastReceiver, intentFilter)
170173

171-
settingsRepository.get(Keys.darkTheme)
174+
settingsRepository.get().get(Keys.darkTheme)
172175
.map { it?.toIntOrNull() }
173176
.map {
174177
when (it) {
@@ -178,35 +181,35 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
178181
}
179182
}
180183
.onEach { mode -> AppCompatDelegate.setDefaultNightMode(mode) }
181-
.launchIn(appCoroutineScope)
184+
.launchIn(appCoroutineScope.get())
182185

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

187-
Timber.plant(loggingTree)
190+
Timber.plant(loggingTree.get())
188191

189-
notificationController.init()
192+
notificationController.get().init()
190193

191194
processLifecycleOwner.lifecycle.addObserver(
192195
object : LifecycleObserver {
193196
@Suppress("DEPRECATION")
194197
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
195198
fun onResume() {
196199
// when the user returns to the app let everything know that the permissions could have changed
197-
notificationController.onOpenApp()
200+
notificationController.get().onOpenApp()
198201

199202
if (BuildConfig.DEBUG &&
200-
permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)
203+
permissionAdapter.get().isGranted(Permission.WRITE_SECURE_SETTINGS)
201204
) {
202-
accessibilityServiceAdapter.start()
205+
accessibilityServiceAdapter.get().start()
203206
}
204207
}
205208
},
206209
)
207210

208-
appCoroutineScope.launch {
209-
notificationController.openApp.collectLatest { intentAction ->
211+
appCoroutineScope.get().launch {
212+
notificationController.get().openApp.collectLatest { intentAction ->
210213
Intent(this@BaseKeyMapperApp, getMainActivityClass()).apply {
211214
action = intentAction
212215
flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -216,38 +219,38 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
216219
}
217220
}
218221

219-
notificationController.showToast.onEach { toast ->
222+
notificationController.get().showToast.onEach { toast ->
220223
Toast.makeText(this, toast, Toast.LENGTH_SHORT).show()
221-
}.launchIn(appCoroutineScope)
224+
}.launchIn(appCoroutineScope.get())
222225

223-
autoGrantPermissionController.start()
224-
keyEventRelayServiceWrapper.bind()
226+
autoGrantPermissionController.get().start()
227+
keyEventRelayServiceWrapper.get().bind()
225228

226-
if (systemBridgeConnectionManager.isConnected()) {
229+
if (systemBridgeConnectionManager.get().isConnected()) {
227230
Timber.i("KeyMapperApp: System bridge is connected")
228231
} else {
229232
Timber.i("KeyMapperApp: System bridge is disconnected")
230233
}
231234

232-
systemBridgeAutoStarter.init()
235+
systemBridgeAutoStarter.get().init()
233236

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

239-
appCoroutineScope.launch {
240-
systemBridgeConnectionManager.connectionState.collect { state ->
242+
appCoroutineScope.get().launch {
243+
systemBridgeConnectionManager.get().connectionState.collect { state ->
241244
if (state is SystemBridgeConnectionState.Connected) {
242245
val isUsed =
243-
settingsRepository.get(Keys.isSystemBridgeUsed).first() ?: false
246+
settingsRepository.get().get(Keys.isSystemBridgeUsed).first() ?: false
244247

245248
// Enable the setting to use PRO mode for key event actions the first time they use PRO mode.
246249
if (!isUsed) {
247-
settingsRepository.set(Keys.keyEventActionsUseSystemBridge, true)
250+
settingsRepository.get().set(Keys.keyEventActionsUseSystemBridge, true)
248251
}
249252

250-
settingsRepository.set(Keys.isSystemBridgeUsed, true)
253+
settingsRepository.get().set(Keys.isSystemBridgeUsed, true)
251254
}
252255
}
253256
}

base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ class BootBroadcastReceiver : BroadcastReceiver() {
1515
Timber.i(
1616
"Boot completed broadcast: time since boot = ${SystemClock.elapsedRealtime() / 1000}",
1717
)
18+
(context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked()
1819
}
1920

2021
Intent.ACTION_LOCKED_BOOT_COMPLETED -> {
21-
(context.applicationContext as? BaseKeyMapperApp)?.onBootUnlocked()
22+
Timber.i(
23+
"Locked boot completed broadcast: time since boot = ${SystemClock.elapsedRealtime() / 1000}",
24+
)
2225
}
2326
}
2427
}

base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class LazyActionErrorSnapshot(
4949
cameraAdapter,
5050
permissionAdapter,
5151
) {
52+
5253
private val keyMapperImeHelper =
5354
KeyMapperImeHelper(switchImeInterface, inputMethodAdapter, buildConfigProvider.packageName)
5455

@@ -265,10 +266,6 @@ class LazyActionErrorSnapshot(
265266
}
266267
}
267268

268-
is ActionData.TalkBackGesture -> {
269-
return getAppError(TALKBACK_PACKAGE_NAME)
270-
}
271-
272269
else -> {}
273270
}
274271

@@ -321,5 +318,3 @@ interface ActionErrorSnapshot {
321318
fun getError(action: ActionData): KMError?
322319
fun getErrors(actions: List<ActionData>): Map<ActionData, KMError?>
323320
}
324-
325-
private const val TALKBACK_PACKAGE_NAME = "com.google.android.marvin.talkback"

0 commit comments

Comments
 (0)