diff --git a/CHANGELOG.md b/CHANGELOG.md
index 311a180edf..1b6b568eba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 1e905036f2..0f7be6ae53 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -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** { *; }
@@ -245,3 +246,4 @@
-dontwarn android.view.IWindowManager**
-dontwarn com.android.internal.app.**
-dontwarn com.android.internal.policy.**
+-dontwarn android.os.ICancellationSignal
\ No newline at end of file
diff --git a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml
index 7353dbd1fd..1084c24082 100644
--- a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 7353dbd1fd..1084c24082 100644
--- a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
index 7353dbd1fd..1084c24082 100644
--- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 7353dbd1fd..1084c24082 100644
--- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d74d55e0eb..424e398db2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,7 +18,6 @@
@@ -67,4 +67,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt
index bc47591aae..1fdae9a795 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt
@@ -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
@@ -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() }
override fun getController(): BaseAccessibilityServiceController? {
return controller
@@ -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 {
diff --git a/app/version.properties b/app/version.properties
index 0fa4ca5276..75456f40c1 100644
--- a/app/version.properties
+++ b/app/version.properties
@@ -1,2 +1,2 @@
-VERSION_NAME=4.2.0
-VERSION_CODE=252
+VERSION_NAME=4.3.0
+VERSION_CODE=257
diff --git a/base/src/main/assets/whats-new.txt b/base/src/main/assets/whats-new.txt
index fab11d04bb..35cd3d1f91 100644
--- a/base/src/main/assets/whats-new.txt
+++ b/base/src/main/assets/whats-new.txt
@@ -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
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt
index ee9e54e816..50c1a77345 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt
@@ -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
@@ -51,46 +52,46 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
private val tag = BaseKeyMapperApp::class.simpleName
@Inject
- lateinit var appCoroutineScope: CoroutineScope
+ lateinit var appCoroutineScope: Lazy
@Inject
- lateinit var notificationController: NotificationController
+ lateinit var notificationController: Lazy
@Inject
- lateinit var packageManagerAdapter: AndroidPackageManagerAdapter
+ lateinit var packageManagerAdapter: Lazy
@Inject
- lateinit var devicesAdapter: AndroidDevicesAdapter
+ lateinit var devicesAdapter: Lazy
@Inject
- lateinit var permissionAdapter: AndroidPermissionAdapter
+ lateinit var permissionAdapter: Lazy
@Inject
- lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl
+ lateinit var accessibilityServiceAdapter: Lazy
@Inject
- lateinit var autoGrantPermissionController: AutoGrantPermissionController
+ lateinit var autoGrantPermissionController: Lazy
@Inject
- lateinit var loggingTree: KeyMapperLoggingTree
+ lateinit var loggingTree: Lazy
@Inject
- lateinit var settingsRepository: PreferenceRepositoryImpl
+ lateinit var settingsRepository: Lazy
@Inject
- lateinit var logRepository: LogRepository
+ lateinit var logRepository: Lazy
@Inject
- lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl
+ lateinit var keyEventRelayServiceWrapper: Lazy
@Inject
- lateinit var systemBridgeAutoStarter: SystemBridgeAutoStarter
+ lateinit var systemBridgeAutoStarter: Lazy
@Inject
- lateinit var systemBridgeConnectionManager: SystemBridgeConnectionManagerImpl
+ lateinit var systemBridgeConnectionManager: Lazy
@Inject
- lateinit var systemBridgeLogger: SystemBridgeLogger
+ lateinit var systemBridgeLogger: Lazy
private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() }
@@ -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)
@@ -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) {
@@ -178,15 +181,15 @@ 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 {
@@ -194,19 +197,19 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
@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
@@ -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)
}
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt b/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt
index 7bb321b372..3d45969541 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/BootBroadcastReceiver.kt
@@ -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}",
+ )
}
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt
index 4dcc63aa74..6acbd9374d 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt
@@ -49,6 +49,7 @@ class LazyActionErrorSnapshot(
cameraAdapter,
permissionAdapter,
) {
+
private val keyMapperImeHelper =
KeyMapperImeHelper(switchImeInterface, inputMethodAdapter, buildConfigProvider.packageName)
@@ -265,10 +266,6 @@ class LazyActionErrorSnapshot(
}
}
- is ActionData.TalkBackGesture -> {
- return getAppError(TALKBACK_PACKAGE_NAME)
- }
-
else -> {}
}
@@ -321,5 +318,3 @@ interface ActionErrorSnapshot {
fun getError(action: ActionData): KMError?
fun getErrors(actions: List): Map
}
-
-private const val TALKBACK_PACKAGE_NAME = "com.google.android.marvin.talkback"
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt
index 2e3c343042..e8e7ad3522 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseSettingViewModel.kt
@@ -7,6 +7,9 @@ import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
import io.github.sds100.keymapper.base.utils.ui.DialogProvider
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
import io.github.sds100.keymapper.common.utils.State
+import io.github.sds100.keymapper.common.utils.Success
+import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
+import io.github.sds100.keymapper.sysbridge.manager.isConnected
import io.github.sds100.keymapper.system.settings.SettingType
import io.github.sds100.keymapper.system.settings.SettingsAdapter
import javax.inject.Inject
@@ -18,12 +21,14 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runInterruptible
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@HiltViewModel
class ChooseSettingViewModel @Inject constructor(
private val settingsAdapter: SettingsAdapter,
+ private val systemBridgeConnectionManager: SystemBridgeConnectionManager,
resourceProvider: ResourceProvider,
navigationProvider: NavigationProvider,
dialogProvider: DialogProvider,
@@ -36,7 +41,7 @@ class ChooseSettingViewModel @Inject constructor(
val selectedSettingType = MutableStateFlow(SettingType.SYSTEM)
val settings: StateFlow>> =
combine(selectedSettingType, searchQuery) { type, query ->
- val allSettings = settingsAdapter.getAll(type)
+ val allSettings = getSettings(type)
val items = allSettings
.filter { (key, _) -> query == null || key.contains(query, ignoreCase = true) }
@@ -46,6 +51,38 @@ class ChooseSettingViewModel @Inject constructor(
}.flowOn(Dispatchers.Default)
.stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading)
+ private suspend fun getSettings(type: SettingType): Map {
+ if (systemBridgeConnectionManager.isConnected()) {
+ val namespace = when (type) {
+ SettingType.SYSTEM -> "system"
+ SettingType.SECURE -> "secure"
+ SettingType.GLOBAL -> "global"
+ }
+ val result = runInterruptible(Dispatchers.IO) {
+ systemBridgeConnectionManager.run { bridge ->
+ bridge.getAllSettings(namespace)
+ }
+ }
+ if (result is Success && result.value.isNotEmpty()) {
+ return parseKeyValuePairs(result.value)
+ }
+ }
+ return settingsAdapter.getAll(type)
+ }
+
+ private fun parseKeyValuePairs(pairs: Array): Map {
+ val settings = sortedMapOf()
+ for (entry in pairs) {
+ val eqIdx = entry.indexOf('=')
+ if (eqIdx < 0) continue
+ val key = entry.substring(0, eqIdx)
+ if (key.isBlank()) continue
+ val value = if (eqIdx < entry.length - 1) entry.substring(eqIdx + 1) else null
+ settings[key] = value
+ }
+ return settings
+ }
+
fun onNavigateBack() {
viewModelScope.launch {
popBackStack()
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt
index 836834e723..eb08042bab 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt
@@ -25,6 +25,7 @@ import io.github.sds100.keymapper.common.utils.Orientation
import io.github.sds100.keymapper.common.utils.PhysicalOrientation
import io.github.sds100.keymapper.common.utils.State
import io.github.sds100.keymapper.system.camera.CameraLens
+import io.github.sds100.keymapper.system.volume.RingerMode
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -249,6 +250,15 @@ class ChooseConstraintViewModel @Inject constructor(
ConstraintId.PHONE_RINGING ->
returnResult.emit(ConstraintData.PhoneRinging)
+ ConstraintId.RINGER_MODE_NORMAL ->
+ returnResult.emit(ConstraintData.RingerMode(RingerMode.NORMAL))
+
+ ConstraintId.RINGER_MODE_VIBRATE ->
+ returnResult.emit(ConstraintData.RingerMode(RingerMode.VIBRATE))
+
+ ConstraintId.RINGER_MODE_SILENT ->
+ returnResult.emit(ConstraintData.RingerMode(RingerMode.SILENT))
+
ConstraintId.CHARGING ->
returnResult.emit(ConstraintData.Charging)
@@ -267,6 +277,12 @@ class ChooseConstraintViewModel @Inject constructor(
ConstraintId.LOCK_SCREEN_NOT_SHOWING ->
returnResult.emit(ConstraintData.LockScreenNotShowing)
+ ConstraintId.NOTIFICATION_PANEL_SHOWING ->
+ returnResult.emit(ConstraintData.NotificationPanelShowing)
+
+ ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING ->
+ returnResult.emit(ConstraintData.NotificationPanelNotShowing)
+
ConstraintId.TIME -> {
timeConstraintState = ConstraintData.Time(
startHour = 0,
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt
index a2aa471fbd..7b7fb41917 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt
@@ -8,6 +8,7 @@ import io.github.sds100.keymapper.data.entities.ConstraintEntity
import io.github.sds100.keymapper.data.entities.EntityExtra
import io.github.sds100.keymapper.data.entities.getData
import io.github.sds100.keymapper.system.camera.CameraLens
+import io.github.sds100.keymapper.system.volume.RingerMode as SystemRingerMode
import java.time.LocalTime
import java.util.UUID
import kotlinx.serialization.Serializable
@@ -189,6 +190,15 @@ sealed class ConstraintData {
override val id: ConstraintId = ConstraintId.PHONE_RINGING
}
+ @Serializable
+ data class RingerMode(val ringerMode: SystemRingerMode) : ConstraintData() {
+ override val id: ConstraintId = when (ringerMode) {
+ SystemRingerMode.NORMAL -> ConstraintId.RINGER_MODE_NORMAL
+ SystemRingerMode.VIBRATE -> ConstraintId.RINGER_MODE_VIBRATE
+ SystemRingerMode.SILENT -> ConstraintId.RINGER_MODE_SILENT
+ }
+ }
+
@Serializable
data object Charging : ConstraintData() {
override val id: ConstraintId = ConstraintId.CHARGING
@@ -209,6 +219,16 @@ sealed class ConstraintData {
override val id: ConstraintId = ConstraintId.HINGE_OPEN
}
+ @Serializable
+ data object NotificationPanelShowing : ConstraintData() {
+ override val id: ConstraintId = ConstraintId.NOTIFICATION_PANEL_SHOWING
+ }
+
+ @Serializable
+ data object NotificationPanelNotShowing : ConstraintData() {
+ override val id: ConstraintId = ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING
+ }
+
@Serializable
data class Time(
val startHour: Int,
@@ -382,12 +402,23 @@ object ConstraintEntityMapper {
ConstraintEntity.IN_PHONE_CALL -> ConstraintData.InPhoneCall
ConstraintEntity.NOT_IN_PHONE_CALL -> ConstraintData.NotInPhoneCall
+ ConstraintEntity.RINGER_MODE_NORMAL ->
+ ConstraintData.RingerMode(SystemRingerMode.NORMAL)
+ ConstraintEntity.RINGER_MODE_VIBRATE ->
+ ConstraintData.RingerMode(SystemRingerMode.VIBRATE)
+ ConstraintEntity.RINGER_MODE_SILENT ->
+ ConstraintData.RingerMode(SystemRingerMode.SILENT)
+
ConstraintEntity.CHARGING -> ConstraintData.Charging
ConstraintEntity.DISCHARGING -> ConstraintData.Discharging
ConstraintEntity.HINGE_CLOSED -> ConstraintData.HingeClosed
ConstraintEntity.HINGE_OPEN -> ConstraintData.HingeOpen
+ ConstraintEntity.NOTIFICATION_PANEL_SHOWING -> ConstraintData.NotificationPanelShowing
+ ConstraintEntity.NOTIFICATION_PANEL_NOT_SHOWING ->
+ ConstraintData.NotificationPanelNotShowing
+
ConstraintEntity.TIME -> {
val startTime =
entity.extras.getData(ConstraintEntity.EXTRA_START_TIME).valueOrNull()!!
@@ -673,6 +704,21 @@ object ConstraintEntityMapper {
ConstraintEntity.PHONE_RINGING,
)
+ is ConstraintData.RingerMode -> when (constraint.data.ringerMode) {
+ SystemRingerMode.NORMAL -> ConstraintEntity(
+ uid = constraint.uid,
+ ConstraintEntity.RINGER_MODE_NORMAL,
+ )
+ SystemRingerMode.VIBRATE -> ConstraintEntity(
+ uid = constraint.uid,
+ ConstraintEntity.RINGER_MODE_VIBRATE,
+ )
+ SystemRingerMode.SILENT -> ConstraintEntity(
+ uid = constraint.uid,
+ ConstraintEntity.RINGER_MODE_SILENT,
+ )
+ }
+
is ConstraintData.Charging -> ConstraintEntity(
uid = constraint.uid,
ConstraintEntity.CHARGING,
@@ -693,6 +739,16 @@ object ConstraintEntityMapper {
ConstraintEntity.HINGE_OPEN,
)
+ is ConstraintData.NotificationPanelShowing -> ConstraintEntity(
+ uid = constraint.uid,
+ ConstraintEntity.NOTIFICATION_PANEL_SHOWING,
+ )
+
+ is ConstraintData.NotificationPanelNotShowing -> ConstraintEntity(
+ uid = constraint.uid,
+ ConstraintEntity.NOTIFICATION_PANEL_NOT_SHOWING,
+ )
+
is ConstraintData.Time -> ConstraintEntity(
uid = constraint.uid,
type = ConstraintEntity.TIME,
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt
index 4bbbcf1721..454a3324e9 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt
@@ -16,6 +16,8 @@ enum class ConstraintDependency {
DEVICE_LOCKED_STATE,
LOCK_SCREEN_SHOWING,
PHONE_STATE,
+ RINGER_MODE,
CHARGING_STATE,
HINGE_STATE,
+ NOTIFICATION_PANEL_STATE,
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt
index 06463c6fd7..8f0512b696 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt
@@ -54,11 +54,18 @@ enum class ConstraintId {
NOT_IN_PHONE_CALL,
PHONE_RINGING,
+ RINGER_MODE_NORMAL,
+ RINGER_MODE_VIBRATE,
+ RINGER_MODE_SILENT,
+
CHARGING,
DISCHARGING,
HINGE_CLOSED,
HINGE_OPEN,
+ NOTIFICATION_PANEL_SHOWING,
+ NOTIFICATION_PANEL_NOT_SHOWING,
+
TIME,
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt
index 00ac3781ee..5326f32129 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt
@@ -21,6 +21,8 @@ import io.github.sds100.keymapper.system.network.NetworkAdapter
import io.github.sds100.keymapper.system.phone.CallState
import io.github.sds100.keymapper.system.phone.PhoneAdapter
import io.github.sds100.keymapper.system.power.PowerAdapter
+import io.github.sds100.keymapper.system.volume.RingerMode
+import io.github.sds100.keymapper.system.volume.VolumeAdapter
import java.time.LocalTime
/**
@@ -38,6 +40,7 @@ class LazyConstraintSnapshot(
phoneAdapter: PhoneAdapter,
powerAdapter: PowerAdapter,
private val foldableAdapter: FoldableAdapter,
+ volumeAdapter: VolumeAdapter,
) : ConstraintSnapshot {
private val appInForeground: String? by lazy { accessibilityService.rootNode?.packageName }
private val connectedBluetoothDevices: Set by lazy {
@@ -66,6 +69,7 @@ class LazyConstraintSnapshot(
}
private val callState: CallState by lazy { phoneAdapter.getCallState() }
private val isCharging: Boolean by lazy { powerAdapter.isCharging.value }
+ private val ringerMode: RingerMode by lazy { volumeAdapter.ringerMode }
private val isLocked: Boolean by lazy {
lockScreenAdapter.isLocked()
@@ -75,6 +79,10 @@ class LazyConstraintSnapshot(
lockScreenAdapter.isLockScreenShowing()
}
+ private val isNotificationShadeExpanded: Boolean by lazy {
+ accessibilityService.isNotificationShadeExpanded.firstBlocking()
+ }
+
private val localTime = LocalTime.now()
private fun isMediaPlaying(): Boolean {
@@ -165,6 +173,8 @@ class LazyConstraintSnapshot(
callState == CallState.RINGING ||
audioVolumeStreams.contains(AudioManager.STREAM_RING)
+ is ConstraintData.RingerMode -> ringerMode == constraint.data.ringerMode
+
is ConstraintData.Charging -> isCharging
is ConstraintData.Discharging -> !isCharging
@@ -199,6 +209,9 @@ class LazyConstraintSnapshot(
!isLockscreenShowing ||
appInForeground != "com.android.systemui"
+ is ConstraintData.NotificationPanelShowing -> isNotificationShadeExpanded
+ is ConstraintData.NotificationPanelNotShowing -> !isNotificationShadeExpanded
+
is ConstraintData.Time ->
if (constraint.data.startTime.isAfter(constraint.data.endTime)) {
localTime.isAfter(constraint.data.startTime) ||
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt
index 94b02aebc9..16a17570f9 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt
@@ -11,6 +11,7 @@ import io.github.sds100.keymapper.common.utils.TimeUtils
import io.github.sds100.keymapper.common.utils.handle
import io.github.sds100.keymapper.common.utils.valueIfFailure
import io.github.sds100.keymapper.system.camera.CameraLens
+import io.github.sds100.keymapper.system.volume.RingerMode
import java.time.format.FormatStyle
class ConstraintUiHelper(
@@ -163,6 +164,11 @@ class ConstraintUiHelper(
is ConstraintData.InPhoneCall -> getString(R.string.constraint_in_phone_call)
is ConstraintData.NotInPhoneCall -> getString(R.string.constraint_not_in_phone_call)
is ConstraintData.PhoneRinging -> getString(R.string.constraint_phone_ringing)
+ is ConstraintData.RingerMode -> when (constraint.data.ringerMode) {
+ RingerMode.NORMAL -> getString(R.string.constraint_ringer_mode_normal)
+ RingerMode.VIBRATE -> getString(R.string.constraint_ringer_mode_vibrate)
+ RingerMode.SILENT -> getString(R.string.constraint_ringer_mode_silent)
+ }
is ConstraintData.Charging -> getString(R.string.constraint_charging)
is ConstraintData.Discharging -> getString(R.string.constraint_discharging)
is ConstraintData.HingeClosed -> getString(R.string.constraint_hinge_closed_description)
@@ -171,6 +177,10 @@ class ConstraintUiHelper(
is ConstraintData.LockScreenNotShowing -> getString(
R.string.constraint_lock_screen_not_showing,
)
+ is ConstraintData.NotificationPanelShowing ->
+ getString(R.string.constraint_notification_panel_showing)
+ is ConstraintData.NotificationPanelNotShowing ->
+ getString(R.string.constraint_notification_panel_not_showing)
is ConstraintData.Time -> getString(
R.string.constraint_time_formatted,
arrayOf(
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt
index 80cd56c1c7..daabcee482 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt
@@ -15,6 +15,8 @@ import androidx.compose.material.icons.outlined.KeyboardHide
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.LockOpen
import androidx.compose.material.icons.outlined.MobileOff
+import androidx.compose.material.icons.outlined.Notifications
+import androidx.compose.material.icons.outlined.NotificationsOff
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material.icons.outlined.RingVolume
import androidx.compose.material.icons.outlined.ScreenLockPortrait
@@ -23,6 +25,7 @@ import androidx.compose.material.icons.outlined.StayCurrentLandscape
import androidx.compose.material.icons.outlined.StayCurrentPortrait
import androidx.compose.material.icons.outlined.StopCircle
import androidx.compose.material.icons.outlined.Timer
+import androidx.compose.material.icons.outlined.Vibration
import androidx.compose.material.icons.outlined.Wifi
import androidx.compose.material.icons.outlined.WifiOff
import androidx.compose.material.icons.rounded.Android
@@ -101,6 +104,9 @@ object ConstraintUtils {
ConstraintId.IN_PHONE_CALL,
ConstraintId.NOT_IN_PHONE_CALL,
ConstraintId.PHONE_RINGING,
+ ConstraintId.RINGER_MODE_NORMAL,
+ ConstraintId.RINGER_MODE_VIBRATE,
+ ConstraintId.RINGER_MODE_SILENT,
-> ConstraintCategory.PHONE
ConstraintId.CHARGING,
@@ -111,6 +117,10 @@ object ConstraintUtils {
ConstraintId.HINGE_OPEN,
-> ConstraintCategory.DEVICE
+ ConstraintId.NOTIFICATION_PANEL_SHOWING,
+ ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING,
+ -> ConstraintCategory.DISPLAY
+
ConstraintId.TIME -> ConstraintCategory.TIME
}
@@ -180,6 +190,9 @@ object ConstraintUtils {
ConstraintId.IN_PHONE_CALL -> ComposeIconInfo.Vector(Icons.Outlined.Call)
ConstraintId.NOT_IN_PHONE_CALL -> ComposeIconInfo.Vector(Icons.Outlined.CallEnd)
ConstraintId.PHONE_RINGING -> ComposeIconInfo.Vector(Icons.Outlined.RingVolume)
+ ConstraintId.RINGER_MODE_NORMAL -> ComposeIconInfo.Vector(Icons.Outlined.Notifications)
+ ConstraintId.RINGER_MODE_VIBRATE -> ComposeIconInfo.Vector(Icons.Outlined.Vibration)
+ ConstraintId.RINGER_MODE_SILENT -> ComposeIconInfo.Vector(Icons.Outlined.NotificationsOff)
ConstraintId.CHARGING -> ComposeIconInfo.Vector(Icons.Outlined.BatteryChargingFull)
ConstraintId.DISCHARGING -> ComposeIconInfo.Vector(Icons.Outlined.Battery2Bar)
@@ -191,6 +204,12 @@ object ConstraintUtils {
Icons.Outlined.ScreenLockPortrait,
)
ConstraintId.LOCK_SCREEN_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.LockOpen)
+
+ ConstraintId.NOTIFICATION_PANEL_SHOWING ->
+ ComposeIconInfo.Vector(Icons.Outlined.Notifications)
+ ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING ->
+ ComposeIconInfo.Vector(Icons.Outlined.NotificationsOff)
+
ConstraintId.TIME -> ComposeIconInfo.Vector(Icons.Outlined.Timer)
}
@@ -236,12 +255,19 @@ object ConstraintUtils {
ConstraintId.IN_PHONE_CALL -> R.string.constraint_in_phone_call
ConstraintId.NOT_IN_PHONE_CALL -> R.string.constraint_not_in_phone_call
ConstraintId.PHONE_RINGING -> R.string.constraint_phone_ringing
+ ConstraintId.RINGER_MODE_NORMAL -> R.string.constraint_ringer_mode_normal
+ ConstraintId.RINGER_MODE_VIBRATE -> R.string.constraint_ringer_mode_vibrate
+ ConstraintId.RINGER_MODE_SILENT -> R.string.constraint_ringer_mode_silent
ConstraintId.CHARGING -> R.string.constraint_charging
ConstraintId.DISCHARGING -> R.string.constraint_discharging
ConstraintId.HINGE_CLOSED -> R.string.constraint_hinge_closed
ConstraintId.HINGE_OPEN -> R.string.constraint_hinge_open
ConstraintId.LOCK_SCREEN_SHOWING -> R.string.constraint_lock_screen_showing
ConstraintId.LOCK_SCREEN_NOT_SHOWING -> R.string.constraint_lock_screen_not_showing
+ ConstraintId.NOTIFICATION_PANEL_SHOWING ->
+ R.string.constraint_notification_panel_showing
+ ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING ->
+ R.string.constraint_notification_panel_not_showing
ConstraintId.TIME -> R.string.constraint_time
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt
index 8bf75d6f24..14e2da8297 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt
@@ -16,6 +16,7 @@ import io.github.sds100.keymapper.system.media.MediaAdapter
import io.github.sds100.keymapper.system.network.NetworkAdapter
import io.github.sds100.keymapper.system.phone.PhoneAdapter
import io.github.sds100.keymapper.system.power.PowerAdapter
+import io.github.sds100.keymapper.system.volume.VolumeAdapter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
@@ -34,6 +35,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor(
private val phoneAdapter: PhoneAdapter,
private val powerAdapter: PowerAdapter,
private val foldableAdapter: FoldableAdapter,
+ private val volumeAdapter: VolumeAdapter,
) : DetectConstraintsUseCase {
@AssistedFactory
@@ -53,6 +55,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor(
phoneAdapter,
powerAdapter,
foldableAdapter,
+ volumeAdapter,
)
override fun onDependencyChanged(dependency: ConstraintDependency): Flow {
@@ -92,6 +95,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor(
).map { dependency }
ConstraintDependency.PHONE_STATE -> phoneAdapter.callStateFlow.map { dependency }
+ ConstraintDependency.RINGER_MODE -> volumeAdapter.ringerModeFlow.map { dependency }
ConstraintDependency.CHARGING_STATE -> powerAdapter.isCharging.map { dependency }
ConstraintDependency.HINGE_STATE ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -101,6 +105,8 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor(
}
ConstraintDependency.KEYBOARD_VISIBLE ->
accessibilityService.isInputMethodVisible.map { dependency }
+ ConstraintDependency.NOTIFICATION_PANEL_STATE ->
+ accessibilityService.isNotificationShadeExpanded.map { dependency }
}
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt
index eb1e4c1e8b..d843f45bc1 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt
@@ -252,6 +252,27 @@ private fun Content(
)
}
}
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ OptionsHeaderRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ icon = Icons.Outlined.BugReport,
+ text = stringResource(R.string.settings_section_debugging_title),
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ OptionPageButton(
+ modifier = Modifier.padding(horizontal = 8.dp),
+ title = stringResource(R.string.title_pref_get_event_debug),
+ text = stringResource(R.string.summary_pref_get_event_debug),
+ icon = Icons.Outlined.BugReport,
+ onClick = onGetEventClick,
+ )
+ Spacer(modifier = Modifier.height(8.dp))
} else {
Text(
modifier = Modifier.padding(horizontal = 32.dp),
@@ -259,27 +280,6 @@ private fun Content(
textAlign = TextAlign.Center,
)
}
-
- Spacer(modifier = Modifier.height(16.dp))
-
- OptionsHeaderRow(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
- icon = Icons.Outlined.BugReport,
- text = stringResource(R.string.settings_section_debugging_title),
- )
-
- Spacer(modifier = Modifier.height(8.dp))
-
- OptionPageButton(
- modifier = Modifier.padding(horizontal = 8.dp),
- title = stringResource(R.string.title_pref_get_event_debug),
- text = stringResource(R.string.summary_pref_get_event_debug),
- icon = Icons.Outlined.BugReport,
- onClick = onGetEventClick,
- )
- Spacer(modifier = Modifier.height(8.dp))
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt
index 7130911276..ecc4da5437 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt
@@ -190,6 +190,7 @@ class ExpertModeViewModel @Inject constructor(
fun onGetEventClick() {
viewModelScope.launch {
+ if (warningState.value !is ExpertModeWarningState.Understood) return@launch
navigate("get_event_debug", NavDestination.GetEvent)
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt
index a48843773b..ab4cb3dc31 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt
@@ -112,6 +112,7 @@ class KeyMapConstraintsComparator(
is ConstraintData.OrientationLandscape -> Success("")
is ConstraintData.OrientationPortrait -> Success("")
is ConstraintData.PhoneRinging -> Success("")
+ is ConstraintData.RingerMode -> Success("")
is ConstraintData.ScreenOff -> Success("")
is ConstraintData.ScreenOn -> Success("")
is ConstraintData.WifiConnected -> if (constraint.data.ssid == null) {
@@ -143,6 +144,8 @@ class KeyMapConstraintsComparator(
is ConstraintData.PhysicalOrientation -> Success(
constraint.data.physicalOrientation.toString(),
)
+ ConstraintData.NotificationPanelShowing -> Success("")
+ ConstraintData.NotificationPanelNotShowing -> Success("")
}
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt
index d247d41075..e26fd37fec 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt
@@ -27,6 +27,7 @@ import androidx.lifecycle.LifecycleRegistry
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
+import dagger.Lazy
import dagger.hilt.android.AndroidEntryPoint
import io.github.sds100.keymapper.base.R
import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType
@@ -54,10 +55,16 @@ abstract class BaseAccessibilityService :
SavedStateRegistryOwner {
@Inject
- lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl
+ lateinit var accessibilityServiceAdapterLazy: Lazy
+
+ val accessibilityServiceAdapter: AccessibilityServiceAdapterImpl
+ get() = accessibilityServiceAdapterLazy.get()
@Inject
- lateinit var inputMethodAdapter: InputMethodAdapter
+ lateinit var inputMethodAdapterLazy: Lazy
+
+ val inputMethodAdapter: InputMethodAdapter
+ get() = inputMethodAdapterLazy.get()
private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private var savedStateRegistryController: SavedStateRegistryController? =
@@ -123,6 +130,13 @@ abstract class BaseAccessibilityService :
override val isInputMethodVisible: Flow
get() = _isInputMethodVisible
+ private val _isNotificationShadeExpanded by lazy {
+ MutableStateFlow(isNotificationShadeVisible())
+ }
+
+ override val isNotificationShadeExpanded: Flow
+ get() = _isNotificationShadeExpanded
+
override var serviceFlags: Int?
get() = serviceInfo?.flags
set(value) {
@@ -288,6 +302,7 @@ abstract class BaseAccessibilityService :
_activeWindowPackage.update { rootNode?.packageName?.toString() }
_isInputMethodVisible.update { isImeWindowVisible() }
+ _isNotificationShadeExpanded.update { isNotificationShadeVisible() }
}
getController()?.onAccessibilityEvent(event)
@@ -563,6 +578,14 @@ abstract class BaseAccessibilityService :
return imeWindow != null && imeWindow.root?.isVisibleToUser == true
}
+ private fun isNotificationShadeVisible(): Boolean {
+ return windows?.any { window ->
+ if (window.type != AccessibilityWindowInfo.TYPE_SYSTEM) return@any false
+ val root = window.root ?: return@any false
+ root.packageName?.toString() == "com.android.systemui" && root.isVisibleToUser
+ } ?: false
+ }
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun injectText(text: String) {
inputMethod?.currentInputConnection?.commitText(
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt
index df4cd6c0d9..eae36db432 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt
@@ -63,6 +63,12 @@ interface IAccessibilityService : SwitchImeInterface {
*/
val isInputMethodVisible: Flow
+ /**
+ * Whether the notification shade (notification panel / status bar) is expanded.
+ * Detection is best-effort; accuracy may vary across OEMs and Android versions.
+ */
+ val isNotificationShadeExpanded: Flow
+
fun findFocussedNode(focus: Int): AccessibilityNodeModel?
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
diff --git a/base/src/main/res/drawable/ic_launcher_monochrome.xml b/base/src/main/res/drawable/ic_launcher_monochrome.xml
new file mode 100644
index 0000000000..be1d8157f9
--- /dev/null
+++ b/base/src/main/res/drawable/ic_launcher_monochrome.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 7353dbd1fd..1084c24082 100644
--- a/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 7353dbd1fd..1084c24082 100644
--- a/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml
index 3fe19c6b92..88b8ca418e 100644
--- a/base/src/main/res/values/strings.xml
+++ b/base/src/main/res/values/strings.xml
@@ -297,6 +297,9 @@
In phone call
Not in phone call
Phone ringing
+ Ringer mode: Ring
+ Ringer mode: Vibrate
+ Ringer mode: Silent
Charging
Discharging
@@ -305,6 +308,9 @@
Hinge is closed
Hinge is open
+ Notification panel is showing
+ Notification panel is not showing
+
Portrait (0°)
Landscape (90°)
Portrait (180°)
@@ -1526,13 +1532,13 @@
Unsatisfied with Key Mapper
To request a refund, please go to the Google Play Store app to find your payment receipt. Once you find the receipt, include the Transaction ID (starting with GPA) in this email.\n\nMy Transaction ID:
Key Mapper Bug report
- Please fill the following information so I can help you.\n\n1. Device model: %s\n2. Android version: %s\n3. Key Mapper version: %s\n4, Key maps (make a backup in the home screen menu)\n6. Screenshot of Key Mapper home screen\n6. Describe the problem you are having
+ Please fill the following information so I can help you.\n\n1. Device model: %s\n2. Android version: %s\n3. Key Mapper version: %s\n4. Key maps (make a backup in the home screen menu)\n5. Screenshot of Key Mapper home screen\n6. Describe the problem you are having
The advanced triggers are paid feature but you downloaded the FOSS build of Key Mapper that does not include this closed source module or Google Play billing. Please download Key Mapper from Google Play to get access to this feature.
Key Mapper FOSS support
Floating Buttons and Assistant trigger aren\'t sold in this FOSS build because RevenueCat and Google Play in-app billing aren\'t included.\n\nYou can still support development on Ko-fi.
- If you donate and later want those products,\
+ If you donate and later get Google Play on another device,\
contact us
- \ and we can usually help convert your support so it counts toward Google Play purchases.
+ \ and we will give you the paid features so you do not need to pay again.
Your support helps keep Key Mapper largely open source, without ads or tracking. We don\'t sell your personal data.
Donations help fund Android compatibility updates, bug fixes and maintenance, improvements and documentation, and keeping the project sustainable.
Support on Ko-fi
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt
index be77315302..ca6699b5c8 100644
--- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt
+++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt
@@ -11,6 +11,7 @@ import io.github.sds100.keymapper.system.foldable.HingeState
import io.github.sds100.keymapper.system.foldable.isClosed
import io.github.sds100.keymapper.system.foldable.isOpen
import io.github.sds100.keymapper.system.phone.CallState
+import io.github.sds100.keymapper.system.volume.RingerMode
import java.time.LocalTime
import timber.log.Timber
@@ -26,6 +27,7 @@ class TestConstraintSnapshot(
val chosenImeId: String? = null,
val isKeyboardShowing: Boolean = false,
val callState: CallState = CallState.NONE,
+ val ringerMode: RingerMode = RingerMode.NORMAL,
val isCharging: Boolean = false,
val isLocked: Boolean = false,
val isBackFlashlightOn: Boolean = false,
@@ -33,6 +35,7 @@ class TestConstraintSnapshot(
val isLockscreenShowing: Boolean = false,
val localTime: LocalTime = LocalTime.now(),
val hingeState: HingeState = HingeState.Unavailable,
+ val isNotificationPanelShowing: Boolean = false,
) : ConstraintSnapshot {
override fun isSatisfied(constraint: Constraint): Boolean {
@@ -107,6 +110,7 @@ class TestConstraintSnapshot(
is ConstraintData.InPhoneCall -> callState == CallState.IN_PHONE_CALL
is ConstraintData.NotInPhoneCall -> callState == CallState.NONE
is ConstraintData.PhoneRinging -> callState == CallState.RINGING
+ is ConstraintData.RingerMode -> ringerMode == data.ringerMode
is ConstraintData.Charging -> isCharging
is ConstraintData.Discharging -> !isCharging
is ConstraintData.LockScreenShowing -> isLockscreenShowing
@@ -126,6 +130,8 @@ class TestConstraintSnapshot(
hingeState is HingeState.Available && hingeState.isClosed()
ConstraintData.HingeOpen ->
hingeState is HingeState.Available && hingeState.isOpen()
+ ConstraintData.NotificationPanelShowing -> isNotificationPanelShowing
+ ConstraintData.NotificationPanelNotShowing -> !isNotificationPanelShowing
}
if (isSatisfied) {
diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt
index 3deebac611..0a2c60feba 100644
--- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt
+++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt
@@ -88,12 +88,19 @@ data class ConstraintEntity(
const val NOT_IN_PHONE_CALL = "not_in_phone_call"
const val PHONE_RINGING = "phone_ringing"
+ const val RINGER_MODE_NORMAL = "ringer_mode_normal"
+ const val RINGER_MODE_VIBRATE = "ringer_mode_vibrate"
+ const val RINGER_MODE_SILENT = "ringer_mode_silent"
+
const val CHARGING = "charging"
const val DISCHARGING = "discharging"
const val HINGE_CLOSED = "hinge_closed"
const val HINGE_OPEN = "hinge_open"
+ const val NOTIFICATION_PANEL_SHOWING = "notification_panel_showing"
+ const val NOTIFICATION_PANEL_NOT_SHOWING = "notification_panel_not_showing"
+
const val TIME = "time"
const val EXTRA_PACKAGE_NAME = "extra_package_name"
diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl
index ee04b124c8..8477527153 100644
--- a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl
+++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl
@@ -53,4 +53,11 @@ interface ISystemBridge {
void registerLogCallback(ILogCallback callback) = 23;
void unregisterLogCallback() = 24;
void setLogLevel(int level) = 25;
+
+ /**
+ * Returns all settings for the given namespace as an array of "key=value" strings.
+ * The namespace must be one of "system", "secure", or "global".
+ * Queries the Settings ContentProvider directly with the system bridge's elevated privileges.
+ */
+ String[] getAllSettings(String namespace) = 26;
}
diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt
index 888958cc41..652135af7e 100644
--- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt
+++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt
@@ -481,7 +481,7 @@ class SystemBridge : ISystemBridge.Stub() {
override fun setGrabTargets(
devices: Array?,
- ): Array? {
+ ): Array {
return setGrabTargetsNative(devices?.filterNotNull()?.toTypedArray() ?: emptyArray())
}
@@ -903,4 +903,70 @@ class SystemBridge : ISystemBridge.Stub() {
override fun setLogLevel(level: Int) {
setLogLevelNative(level)
}
+
+ override fun getAllSettings(namespace: String?): Array {
+ namespace ?: return emptyArray()
+
+ val settingsUri = when (namespace) {
+ "system" -> android.provider.Settings.System.CONTENT_URI
+ "secure" -> android.provider.Settings.Secure.CONTENT_URI
+ "global" -> android.provider.Settings.Global.CONTENT_URI
+ else -> return emptyArray()
+ }
+
+ val authority = "settings"
+ val token: IBinder? = null
+ val userId = UserHandleUtils.getCallingUserId()
+ var provider: IContentProvider? = null
+
+ try {
+ provider = ActivityManagerApis.getContentProviderExternal(
+ authority,
+ userId,
+ token,
+ authority,
+ )
+
+ if (provider == null) {
+ Log.w(
+ TAG,
+ "getAllSettings: Settings content provider is null for namespace=$namespace",
+ )
+ return emptyArray()
+ }
+
+ val cursor = IContentProviderUtils.queryCompat(
+ provider,
+ processPackageName,
+ settingsUri,
+ arrayOf("name", "value"),
+ null,
+ ) ?: return emptyArray()
+
+ val results = mutableListOf()
+ cursor.use {
+ val nameIndex = it.getColumnIndex("name")
+ val valueIndex = it.getColumnIndex("value")
+ if (nameIndex >= 0) {
+ while (it.moveToNext()) {
+ val name = it.getString(nameIndex) ?: continue
+ val value = if (valueIndex >= 0) it.getString(valueIndex) else null
+ results.add("$name=${value ?: ""}")
+ }
+ }
+ }
+ return results.toTypedArray()
+ } catch (e: Exception) {
+ Log.e(TAG, "getAllSettings: Failed to query settings for namespace=$namespace", e)
+ return emptyArray()
+ } finally {
+ if (provider != null) {
+ try {
+ ActivityManagerApis.removeContentProviderExternal(authority, token)
+ } catch (tr: Throwable) {
+ Log.w(TAG, "getAllSettings: Failed to remove content provider", tr)
+ }
+ }
+ }
+ }
}
diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt
index d83d20744d..23ded26d92 100644
--- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt
+++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt
@@ -2,6 +2,8 @@ package io.github.sds100.keymapper.sysbridge.utils
import android.content.AttributionSource
import android.content.IContentProvider
+import android.database.Cursor
+import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -38,4 +40,28 @@ internal object IContentProviderUtils {
return result
}
+
+ @Throws(android.os.RemoteException::class)
+ fun queryCompat(
+ provider: IContentProvider,
+ callingPkg: String?,
+ url: Uri,
+ projection: Array?,
+ queryArgs: Bundle?,
+ ): Cursor? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val uid = android.system.Os.getuid()
+ provider.query(
+ AttributionSource.Builder(uid).setPackageName(callingPkg).build(),
+ url,
+ projection,
+ queryArgs,
+ null,
+ )
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ provider.query(callingPkg, null as String?, url, projection, queryArgs, null)
+ } else {
+ provider.query(callingPkg, url, projection, queryArgs, null)
+ }
+ }
}
diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt
index 8a7e3ea0eb..3e6017648e 100644
--- a/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt
+++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/AndroidVolumeAdapter.kt
@@ -1,10 +1,14 @@
package io.github.sds100.keymapper.system.volume
import android.app.NotificationManager
+import android.content.BroadcastReceiver
import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
import android.media.AudioManager
import android.os.Build
import android.provider.Settings
+import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.sds100.keymapper.common.utils.KMResult
@@ -18,6 +22,9 @@ import io.github.sds100.keymapper.system.SystemError
import io.github.sds100.keymapper.system.permissions.Permission
import javax.inject.Inject
import javax.inject.Singleton
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import timber.log.Timber
@Singleton
@@ -30,6 +37,30 @@ class AndroidVolumeAdapter @Inject constructor(
private val audioManager: AudioManager by lazy { ctx.getSystemService()!! }
private val notificationManager: NotificationManager by lazy { ctx.getSystemService()!! }
+ private val ringerModeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ intent ?: return
+ if (intent.action == AudioManager.RINGER_MODE_CHANGED_ACTION) {
+ _ringerModeFlow.value = ringerMode
+ }
+ }
+ }
+
+ private val _ringerModeFlow: MutableStateFlow by lazy {
+ MutableStateFlow(ringerMode)
+ }
+ override val ringerModeFlow: StateFlow by lazy { _ringerModeFlow.asStateFlow() }
+
+ init {
+ val filter = IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION)
+ ContextCompat.registerReceiver(
+ ctx,
+ ringerModeReceiver,
+ filter,
+ ContextCompat.RECEIVER_NOT_EXPORTED,
+ )
+ }
+
override val ringerMode: RingerMode
get() {
// Get the current ringer mode with the setting because the AudioManager
diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt
index 2548902c5e..4af85213c7 100644
--- a/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt
+++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/RingerMode.kt
@@ -1,5 +1,8 @@
package io.github.sds100.keymapper.system.volume
+import kotlinx.serialization.Serializable
+
+@Serializable
enum class RingerMode {
NORMAL,
VIBRATE,
diff --git a/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt
index 7bae48cc79..c5c88f8961 100644
--- a/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt
+++ b/system/src/main/java/io/github/sds100/keymapper/system/volume/VolumeAdapter.kt
@@ -1,9 +1,11 @@
package io.github.sds100.keymapper.system.volume
import io.github.sds100.keymapper.common.utils.KMResult
+import kotlinx.coroutines.flow.Flow
interface VolumeAdapter {
val ringerMode: RingerMode
+ val ringerModeFlow: Flow
fun raiseVolume(stream: VolumeStream? = null, showVolumeUi: Boolean): KMResult<*>
fun lowerVolume(stream: VolumeStream? = null, showVolumeUi: Boolean): KMResult<*>