diff --git a/app/src/ci/AndroidManifest.xml b/app/src/ci/AndroidManifest.xml index 4f8c914339..97919293ad 100644 --- a/app/src/ci/AndroidManifest.xml +++ b/app/src/ci/AndroidManifest.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 2b806b566b..291aa837f1 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/debug_release/AndroidManifest.xml b/app/src/debug_release/AndroidManifest.xml index 7ba0a469c3..9e42443cbf 100644 --- a/app/src/debug_release/AndroidManifest.xml +++ b/app/src/debug_release/AndroidManifest.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7315e86e0..ffc37c14c9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + + android:permission="io.github.sds100.keymapper.KEY_EVENT_RELAY_SERVICE" /> = + ConcurrentHashMap() + + override fun onBind(intent: Intent?): IBinder? = binderInterface.asBinder() + + private fun getCallerPackageName(): String? { + val sourceUid = Binder.getCallingUid() + return applicationContext.packageManager.getNameForUid(sourceUid) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayServiceWrapper.kt b/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayServiceWrapper.kt new file mode 100644 index 0000000000..58d0ded56b --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayServiceWrapper.kt @@ -0,0 +1,74 @@ +package io.github.sds100.keymapper.api + +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.DeadObjectException +import android.os.IBinder +import android.view.KeyEvent + +/** + * This handles connecting to the relay service and exposes an interface + * so other parts of the app can get a reference to the service even when it isn't + * bound yet. + */ +class KeyEventRelayServiceWrapperImpl( + context: Context, + private val callback: IKeyEventRelayServiceCallback, +) : KeyEventRelayServiceWrapper { + private val ctx: Context = context.applicationContext + + private val keyEventRelayServiceLock: Any = Any() + private var keyEventRelayService: IKeyEventRelayService? = null + + private val keyEventReceiverConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + synchronized(keyEventRelayServiceLock) { + keyEventRelayService = IKeyEventRelayService.Stub.asInterface(service) + keyEventRelayService?.registerCallback(callback) + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + synchronized(keyEventRelayServiceLock) { + keyEventRelayService?.unregisterCallback() + keyEventRelayService = null + } + } + } + + override fun sendKeyEvent(event: KeyEvent?, targetPackageName: String?): Boolean { + synchronized(keyEventRelayServiceLock) { + if (keyEventRelayService == null) { + return false + } + + try { + return keyEventRelayService!!.sendKeyEvent(event, targetPackageName) + } catch (e: DeadObjectException) { + keyEventRelayService = null + return false + } + } + } + + fun bind() { + Intent(ctx, KeyEventRelayService::class.java).also { intent -> + ctx.bindService(intent, keyEventReceiverConnection, Service.BIND_AUTO_CREATE) + } + } + + fun unbind() { + try { + ctx.unbindService(keyEventReceiverConnection) + } catch (e: DeadObjectException) { + // do nothing + } + } +} + +interface KeyEventRelayServiceWrapper { + fun sendKeyEvent(event: KeyEvent?, targetPackageName: String?): Boolean +} diff --git a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsController.kt b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsController.kt index 6d7e07f41a..140cbd0774 100644 --- a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsController.kt @@ -13,6 +13,12 @@ import kotlinx.coroutines.launch /** * Created by sds100 on 27/04/2021. */ + +/** + * This is used for the feature created in issue #618 to fix the device IDs of key events + * on Android 11. There was a bug in the system where enabling an accessibility service + * would reset the device ID of key events to -1. + */ class RerouteKeyEventsController( private val coroutineScope: CoroutineScope, private val useCase: RerouteKeyEventsUseCase, diff --git a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt index 634c437287..ba0d9352b4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt @@ -13,6 +13,11 @@ import kotlinx.coroutines.flow.map * Created by sds100 on 27/04/2021. */ +/** + * This is used for the feature created in issue #618 to fix the device IDs of key events + * on Android 11. There was a bug in the system where enabling an accessibility service + * would reset the device ID of key events to -1. + */ class RerouteKeyEventsUseCaseImpl( private val inputMethodAdapter: InputMethodAdapter, private val keyMapperImeMessenger: KeyMapperImeMessenger, 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 5a21deef06..fe5bafb1d5 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 @@ -5,17 +5,13 @@ import android.accessibilityservice.FingerprintGestureController import android.accessibilityservice.GestureDescription import android.accessibilityservice.GestureDescription.StrokeDescription import android.app.ActivityManager -import android.app.Service import android.content.BroadcastReceiver -import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.ServiceConnection import android.graphics.Path import android.graphics.Point import android.os.Build -import android.os.IBinder import android.view.KeyEvent import android.view.accessibility.AccessibilityEvent import androidx.core.content.getSystemService @@ -25,9 +21,8 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType import io.github.sds100.keymapper.api.Api -import io.github.sds100.keymapper.api.IKeyEventReceiver -import io.github.sds100.keymapper.api.IKeyEventReceiverCallback -import io.github.sds100.keymapper.api.KeyEventReceiver +import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback +import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapperImpl import io.github.sds100.keymapper.mappings.fingerprintmaps.FingerprintMapId import io.github.sds100.keymapper.system.devices.InputDeviceUtils import io.github.sds100.keymapper.util.Error @@ -127,10 +122,11 @@ class MyAccessibilityService : } } - private val keyEventReceiverCallback: IKeyEventReceiverCallback = - object : IKeyEventReceiverCallback.Stub() { - override fun onKeyEvent(event: KeyEvent?): Boolean { + private val keyEventReceiverCallback: IKeyEventRelayServiceCallback = + object : IKeyEventRelayServiceCallback.Stub() { + override fun onKeyEvent(event: KeyEvent?, sourcePackageName: String?): Boolean { event ?: return false + sourcePackageName ?: return false val device = if (event.device == null) { null @@ -153,23 +149,8 @@ class MyAccessibilityService : } } - private val keyEventReceiverLock: Any = Any() - private var keyEventReceiverBinder: IKeyEventReceiver? = null - - private val keyEventReceiverConnection: ServiceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - synchronized(keyEventReceiverLock) { - keyEventReceiverBinder = IKeyEventReceiver.Stub.asInterface(service) - keyEventReceiverBinder?.registerCallback(keyEventReceiverCallback) - } - } - - override fun onServiceDisconnected(name: ComponentName?) { - synchronized(keyEventReceiverLock) { - keyEventReceiverBinder?.unregisterCallback(keyEventReceiverCallback) - keyEventReceiverBinder = null - } - } + private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl by lazy { + KeyEventRelayServiceWrapperImpl(this, keyEventReceiverCallback) } private var controller: AccessibilityServiceController? = null @@ -196,9 +177,7 @@ class MyAccessibilityService : } } - Intent(this, KeyEventReceiver::class.java).also { intent -> - bindService(intent, keyEventReceiverConnection, Service.BIND_AUTO_CREATE) - } + keyEventRelayServiceWrapper.bind() } override fun onServiceConnected() { @@ -211,7 +190,7 @@ class MyAccessibilityService : context would return null */ if (controller == null) { - controller = Inject.accessibilityServiceController(this) + controller = Inject.accessibilityServiceController(this, keyEventRelayServiceWrapper) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -261,14 +240,12 @@ class MyAccessibilityService : lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } - unregisterReceiver(broadcastReceiver) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { fingerprintGestureController .unregisterFingerprintGestureCallback(fingerprintGestureCallback) } - unbindService(keyEventReceiverConnection) + keyEventRelayServiceWrapper.unbind() Timber.i("Accessibility service: onDestroy") diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt index 7fe9b03aa3..64ce844ac5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt @@ -2,8 +2,11 @@ package io.github.sds100.keymapper.system.inputmethod import android.content.Context import android.content.Intent +import android.os.Build import android.os.SystemClock +import android.view.KeyCharacterMap import android.view.KeyEvent +import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapper import io.github.sds100.keymapper.shizuku.InputEventInjector import io.github.sds100.keymapper.util.InputEventType import timber.log.Timber @@ -14,6 +17,7 @@ import timber.log.Timber class KeyMapperImeMessengerImpl( context: Context, + private val keyEventRelayService: KeyEventRelayServiceWrapper, private val inputMethodAdapter: InputMethodAdapter, ) : KeyMapperImeMessenger { @@ -46,6 +50,15 @@ class KeyMapperImeMessengerImpl( return } + // TODO use Android SDK and get version code properly + if (Build.VERSION.SDK_INT >= 34) { + inputKeyEventRelayService(model, imePackageName) + } else { + inputKeyEventBroadcast(model, imePackageName) + } + } + + private fun inputKeyEventBroadcast(model: InputKeyModel, imePackageName: String) { val intentAction = when (model.inputType) { InputEventType.DOWN -> KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN InputEventType.DOWN_UP -> KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN_UP @@ -62,16 +75,7 @@ class KeyMapperImeMessengerImpl( val eventTime = SystemClock.uptimeMillis() - val keyEvent = KeyEvent( - eventTime, - eventTime, - action, - model.keyCode, - model.repeat, - model.metaState, - model.deviceId, - model.scanCode, - ) + val keyEvent = createKeyEvent(eventTime, action, model) putExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT, keyEvent) @@ -79,6 +83,45 @@ class KeyMapperImeMessengerImpl( } } + private fun createKeyEvent( + eventTime: Long, + action: Int, + model: InputKeyModel, + ): KeyEvent = KeyEvent( + eventTime, + eventTime, + action, + model.keyCode, + model.repeat, + model.metaState, + model.deviceId, + model.scanCode, + ) + + private fun inputKeyEventRelayService(model: InputKeyModel, imePackageName: String) { + val eventTime = SystemClock.uptimeMillis() + + when (model.inputType) { + InputEventType.DOWN_UP -> { + val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) + keyEventRelayService.sendKeyEvent(downKeyEvent, imePackageName) + + val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) + keyEventRelayService.sendKeyEvent(upKeyEvent, imePackageName) + } + + InputEventType.DOWN -> { + val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) + keyEventRelayService.sendKeyEvent(downKeyEvent, imePackageName) + } + + InputEventType.UP -> { + val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) + keyEventRelayService.sendKeyEvent(upKeyEvent, imePackageName) + } + } + } + override fun inputText(text: String) { Timber.d("Input text through IME $text") @@ -89,6 +132,15 @@ class KeyMapperImeMessengerImpl( return } + // TODO use Android SDK and get version code properly + if (Build.VERSION.SDK_INT >= 34) { + inputTextRelayService(text, imePackageName) + } else { + inputTextBroadcast(text, imePackageName) + } + } + + private fun inputTextBroadcast(text: String, imePackageName: String) { Intent(KEY_MAPPER_INPUT_METHOD_ACTION_TEXT).apply { setPackage(imePackageName) @@ -96,6 +148,41 @@ class KeyMapperImeMessengerImpl( ctx.sendBroadcast(this) } } + + private fun inputTextRelayService(text: String, imePackageName: String) { + // taken from android.view.inputmethod.BaseInputConnection.sendCurrentText() + + if (text.isEmpty()) { + return + } + + if (text.length == 1) { + // If it's 1 character, we have a chance of being + // able to generate normal key events... + val keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD) + + val chars = text.toCharArray(startIndex = 0, endIndex = 1) + + val events: Array = keyCharacterMap.getEvents(chars) + + for (i in events.indices) { + keyEventRelayService.sendKeyEvent(events[i], imePackageName) + } + + return + } + + // Otherwise, revert to the special key event containing + // the actual characters. + val event = KeyEvent( + SystemClock.uptimeMillis(), + text, + KeyCharacterMap.VIRTUAL_KEYBOARD, + 0, + ) + + keyEventRelayService.sendKeyEvent(event, imePackageName) + } } interface KeyMapperImeMessenger : InputEventInjector { diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt index b8371859f6..b98c8ff666 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt @@ -1,17 +1,15 @@ package io.github.sds100.keymapper.system.inputmethod -import android.app.Service import android.content.BroadcastReceiver -import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.ServiceConnection import android.inputmethodservice.InputMethodService -import android.os.IBinder +import android.os.Build import android.view.KeyEvent -import io.github.sds100.keymapper.api.IKeyEventReceiver -import io.github.sds100.keymapper.api.KeyEventReceiver +import io.github.sds100.keymapper.Constants +import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback +import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapperImpl /** * Created by sds100 on 31/03/2020. @@ -80,21 +78,20 @@ class KeyMapperImeService : InputMethodService() { } } - private val keyEventReceiverLock: Any = Any() - private var keyEventReceiverBinder: IKeyEventReceiver? = null + private val keyEventReceiverCallback: IKeyEventRelayServiceCallback = + object : IKeyEventRelayServiceCallback.Stub() { + override fun onKeyEvent(event: KeyEvent?, sourcePackageName: String?): Boolean { + // Only accept key events from Key Mapper + if (sourcePackageName != Constants.PACKAGE_NAME) { + return false + } - private val keyEventReceiverConnection: ServiceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - synchronized(keyEventReceiverLock) { - keyEventReceiverBinder = IKeyEventReceiver.Stub.asInterface(service) + return currentInputConnection?.sendKeyEvent(event) ?: false } } - override fun onServiceDisconnected(name: ComponentName?) { - synchronized(keyEventReceiverLock) { - keyEventReceiverBinder = null - } - } + private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl by lazy { + KeyEventRelayServiceWrapperImpl(this, keyEventReceiverCallback) } override fun onCreate() { @@ -106,23 +103,26 @@ class KeyMapperImeService : InputMethodService() { addAction(KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_UP) addAction(KEY_MAPPER_INPUT_METHOD_ACTION_TEXT) - registerReceiver(broadcastReceiver, this) + // TODO use ContextCompat + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(broadcastReceiver, this, RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(broadcastReceiver, this) + } } - Intent(this, KeyEventReceiver::class.java).also { intent -> - bindService(intent, keyEventReceiverConnection, Service.BIND_AUTO_CREATE) - } + keyEventRelayServiceWrapper.bind() } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = - keyEventReceiverBinder?.onKeyEvent(event) ?: super.onKeyDown(keyCode, event) + keyEventRelayServiceWrapper.sendKeyEvent(event, Constants.PACKAGE_NAME) override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean = - keyEventReceiverBinder?.onKeyEvent(event) ?: super.onKeyUp(keyCode, event) + keyEventRelayServiceWrapper.sendKeyEvent(event, Constants.PACKAGE_NAME) override fun onDestroy() { unregisterReceiver(broadcastReceiver) - unbindService(keyEventReceiverConnection) + keyEventRelayServiceWrapper.unbind() super.onDestroy() } diff --git a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt index 4e344b6fd3..cea744e65c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt @@ -15,6 +15,7 @@ import io.github.sds100.keymapper.actions.sound.ChooseSoundFileUseCaseImpl import io.github.sds100.keymapper.actions.sound.ChooseSoundFileViewModel import io.github.sds100.keymapper.actions.swipescreen.SwipePickDisplayCoordinateViewModel import io.github.sds100.keymapper.actions.tapscreen.PickDisplayCoordinateViewModel +import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapper import io.github.sds100.keymapper.backup.BackupRestoreMappingsUseCaseImpl import io.github.sds100.keymapper.constraints.ChooseConstraintViewModel import io.github.sds100.keymapper.constraints.CreateConstraintUseCaseImpl @@ -238,20 +239,34 @@ object Inject { UseCases.controlAccessibilityService(context), ) - fun accessibilityServiceController(service: MyAccessibilityService): AccessibilityServiceController = + fun accessibilityServiceController( + service: MyAccessibilityService, + keyEventRelayService: KeyEventRelayServiceWrapper, + ): AccessibilityServiceController = AccessibilityServiceController( coroutineScope = service.lifecycleScope, accessibilityService = service, inputEvents = ServiceLocator.accessibilityServiceAdapter(service).eventsToService, outputEvents = ServiceLocator.accessibilityServiceAdapter(service).eventReceiver, detectConstraintsUseCase = UseCases.detectConstraints(service), - performActionsUseCase = UseCases.performActions(service, service), - detectKeyMapsUseCase = UseCases.detectKeyMaps(service), + performActionsUseCase = UseCases.performActions( + ctx = service, + service = service, + keyEventRelayService = keyEventRelayService, + ), + detectKeyMapsUseCase = UseCases.detectKeyMaps( + ctx = service, + service = service, + keyEventRelayService = keyEventRelayService, + ), detectFingerprintMapsUseCase = UseCases.detectFingerprintMaps(service), pauseMappingsUseCase = UseCases.pauseMappings(service), devicesAdapter = ServiceLocator.devicesAdapter(service), suAdapter = ServiceLocator.suAdapter(service), - rerouteKeyEventsUseCase = UseCases.rerouteKeyEvents(service), + rerouteKeyEventsUseCase = UseCases.rerouteKeyEvents( + ctx = service, + keyEventRelayService = keyEventRelayService, + ), inputMethodAdapter = ServiceLocator.inputMethodAdapter(service), settingsRepository = ServiceLocator.settingsRepository(service), )