Skip to content

Commit

Permalink
#1218 feat: create key event relay service
Browse files Browse the repository at this point in the history
  • Loading branch information
sds100 committed Jun 29, 2024
1 parent 57325de commit 2270705
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 109 deletions.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.sds100.keymapper.api;

import android.view.KeyEvent;
import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback;

interface IKeyEventRelayService {
/**
* Send a key event to the target package that is registered with
* a callback.
*/
boolean sendKeyEvent(in KeyEvent event, in String targetPackageName);

/**
* Register a callback to receive key events from this relay service. The service
* checks the process uid of the caller to this method and only permits certain applications
* from connecting.
*/
void registerCallback(IKeyEventRelayServiceCallback client);

/**
* Unregister a callback to receive key events from this relay service. The service
* checks the process uid of the caller to this method and only permits certain applications
* from connecting.
*/
void unregisterCallback();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.github.sds100.keymapper.api;

import android.view.KeyEvent;

interface IKeyEventRelayServiceCallback {
boolean onKeyEvent(in KeyEvent event, in String sourcePackageName);
}
59 changes: 37 additions & 22 deletions app/src/main/java/io/github/sds100/keymapper/UseCases.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.actions.CreateActionUseCaseImpl
import io.github.sds100.keymapper.actions.GetActionErrorUseCaseImpl
import io.github.sds100.keymapper.actions.IsActionSupportedUseCaseImpl
import io.github.sds100.keymapper.actions.PerformActionsUseCaseImpl
import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapper
import io.github.sds100.keymapper.constraints.DetectConstraintsUseCaseImpl
import io.github.sds100.keymapper.constraints.GetConstraintErrorUseCaseImpl
import io.github.sds100.keymapper.mappings.DetectMappingUseCaseImpl
Expand Down Expand Up @@ -140,7 +141,11 @@ object UseCases {
ServiceLocator.powerAdapter(service),
)

fun performActions(ctx: Context, service: IAccessibilityService) =
fun performActions(
ctx: Context,
service: IAccessibilityService,
keyEventRelayService: KeyEventRelayServiceWrapper,
) =
PerformActionsUseCaseImpl(
(ctx.applicationContext as KeyMapperApp).appCoroutineScope,
service,
Expand All @@ -150,7 +155,7 @@ object UseCases {
Shell,
ServiceLocator.intentAdapter(ctx),
getActionError(ctx),
keyMapperImeMessenger(ctx),
keyMapperImeMessenger(ctx, keyEventRelayService),
ShizukuInputEventInjector(),
ServiceLocator.packageManagerAdapter(ctx),
ServiceLocator.appShortcutAdapter(ctx),
Expand Down Expand Up @@ -181,19 +186,23 @@ object UseCases {
ServiceLocator.resourceProvider(ctx),
)

fun detectKeyMaps(service: MyAccessibilityService) = DetectKeyMapsUseCaseImpl(
detectMappings(service),
ServiceLocator.roomKeymapRepository(service),
ServiceLocator.settingsRepository(service),
ServiceLocator.suAdapter(service),
ServiceLocator.displayAdapter(service),
ServiceLocator.audioAdapter(service),
keyMapperImeMessenger(service),
fun detectKeyMaps(
ctx: Context,
service: IAccessibilityService,
keyEventRelayService: KeyEventRelayServiceWrapper,
) = DetectKeyMapsUseCaseImpl(
detectMappings(ctx),
ServiceLocator.roomKeymapRepository(ctx),
ServiceLocator.settingsRepository(ctx),
ServiceLocator.suAdapter(ctx),
ServiceLocator.displayAdapter(ctx),
ServiceLocator.audioAdapter(ctx),
keyMapperImeMessenger(ctx, keyEventRelayService),
service,
ShizukuInputEventInjector(),
ServiceLocator.permissionAdapter(service),
ServiceLocator.phoneAdapter(service),
ServiceLocator.inputMethodAdapter(service),
ServiceLocator.permissionAdapter(ctx),
ServiceLocator.phoneAdapter(ctx),
ServiceLocator.inputMethodAdapter(ctx),
)

fun detectFingerprintMaps(ctx: Context) = DetectFingerprintMapsUseCaseImpl(
Expand All @@ -202,18 +211,24 @@ object UseCases {
detectMappings(ctx),
)

fun rerouteKeyEvents(ctx: Context) = RerouteKeyEventsUseCaseImpl(
ServiceLocator.inputMethodAdapter(ctx),
keyMapperImeMessenger(ctx),
ServiceLocator.settingsRepository(ctx),
)
fun rerouteKeyEvents(ctx: Context, keyEventRelayService: KeyEventRelayServiceWrapper) =
RerouteKeyEventsUseCaseImpl(
ServiceLocator.inputMethodAdapter(ctx),
keyMapperImeMessenger(ctx, keyEventRelayService),
ServiceLocator.settingsRepository(ctx),
)

fun createAction(ctx: Context) = CreateActionUseCaseImpl(
ServiceLocator.inputMethodAdapter(ctx),
)

private fun keyMapperImeMessenger(ctx: Context) = KeyMapperImeMessengerImpl(
ctx,
ServiceLocator.inputMethodAdapter(ctx),
)
private fun keyMapperImeMessenger(
ctx: Context,
keyEventRelayService: KeyEventRelayServiceWrapper,
) =
KeyMapperImeMessengerImpl(
ctx,
keyEventRelayService,
ServiceLocator.inputMethodAdapter(ctx),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,93 @@ package io.github.sds100.keymapper.api

import android.app.Service
import android.content.Intent
import android.os.DeadObjectException
import android.os.IBinder
import android.view.KeyEvent
import io.github.sds100.keymapper.api.IKeyEventRelayService.Stub.getCallingPid
import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeHelper
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap

/**
* This service is used as a relay between the accessibility service and input method service to pass
* key events back and forth. A separate service has to be used because you can't bind to an
* accessibility service. The input method service sends key events to this service by calling
* onKeyEvent(), and the accessibility service registers with the callback to receive the
* key events being sent.
* accessibility or input method service.
*
* This is used for key event actions. The input method service registers a callback
* and the accessibility service sends the key events.
*
* This was implemented in issue #850 for the action to answer phone calls because Android doesn't
* pass volume down key events to the accessibility service when the phone is ringing or it is
* in a phone call.
* in a phone call. The accessibility service registers a callback and the input method service
* sends the key events.
*/
class KeyEventRelayService : Service() {
val permittedPackages = KeyMapperImeHelper.KEY_MAPPER_IME_PACKAGE_LIST

private val binderInterface: IKeyEventReceiver = object : IKeyEventReceiver.Stub() {
override fun onKeyEvent(event: KeyEvent?): Boolean {
private val binderInterface: IKeyEventRelayService = object : IKeyEventRelayService.Stub() {
override fun sendKeyEvent(event: KeyEvent?, targetPackageName: String?): Boolean {
synchronized(callbackLock) {
Timber.d("KeyEventReceiver: onKeyEvent ${event?.keyCode}")

if (callback != null) {
return callback!!.onKeyEvent(event)
} else {
val callback = callbacks[targetPackageName]

if (callback == null) {
return false
} else {
try {
// Get the process ID of the app that called this service.
val sourcePackageName = getCallerPackageName() ?: return false

if (!permittedPackages.contains(sourcePackageName)) {
Timber.d("An unrecognized package $sourcePackageName tried to send a key event.")

return false
}

return callback.onKeyEvent(event, targetPackageName)
} catch (e: DeadObjectException) {
// If the application is no longer connected then delete the callback.
callbacks.remove(targetPackageName)
return false
}
}
}
}

override fun registerCallback(client: IKeyEventReceiverCallback?) {
override fun registerCallback(client: IKeyEventRelayServiceCallback?) {
val sourcePackageName = getCallerPackageName() ?: return

if (client == null || !permittedPackages.contains(sourcePackageName)) {
Timber.d("An unrecognized package $sourcePackageName tried to register a key event relay callback.")
return
}

synchronized(callbackLock) {
callback = client
Timber.d("Package $sourcePackageName registered a key event relay callback.")
callbacks[sourcePackageName] = client
}
}

override fun unregisterCallback(client: IKeyEventReceiverCallback?) {
override fun unregisterCallback() {
synchronized(callbackLock) {
callback = null
val sourcePackageName = getCallerPackageName() ?: return

Timber.d("Package $sourcePackageName unregistered a key event relay callback.")

callbacks.remove(sourcePackageName)
}
}
}

private val callbackLock: Any = Any()
private var callback: IKeyEventReceiverCallback? = null
private var callbacks: ConcurrentHashMap<String, IKeyEventRelayServiceCallback> =
ConcurrentHashMap()

override fun onBind(intent: Intent?): IBinder? = binderInterface.asBinder()

private fun getCallerPackageName(): String? {
val sourcePid = getCallingPid()
return applicationContext.packageManager.getNameForUid(sourcePid)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 2270705

Please sign in to comment.