Skip to content

Developer Guide

Shyri Villar edited this page Jan 10, 2019 · 9 revisions

The following text describes the process followed to make this work without needing of root, if you just want a clear concise description of the architecture go to TLDR

How to avoid rooting the device

Android doesn't make it easy to simulate touch events. If you are familiar with Android APIs you may know there's a system service called InputManagerService that as the name says, it manages the input events. You can navigate its internals InputManagerService You can find a method called "injectInputEvent" that will in fact let you inject touch events to the system. The problem comes when you try to get the instance of the service like this:

(InputManager) getSystemService(Context.INPUT_SERVICE);

You are getting actually InputManager which is an interface that InputManagerService implements but doesn't expose the method "injectInputEvent". So the way to go is accessing that method through reflection.

The next problem comes when you try to run "injectInputEvent". It will crash and Android will require you to declare INJECT_EVENTS permission in the AndroidManifest.xml. But... Android Studio will warn you: that permission is only valid if your application is a system application (i.e. one of the crapware apps that the manufacturer decided to ship your device with). And there it is, we found the reason why every single app that simulate touch events needs the device to be rooted.

But if you've done some Android testing, you might know that there's a program called monkeyrunner. This program can be run in an Android device through adb, and it is in fact allowed to simulate touch events. So even though the unix user used to run the application by Android has no permission to inject events unless it is a system app declaring INJECT_EVENTS permission, the unix user used to run monkey through adb is allowed to do so.

So what if we run the "injectInputMethod" from adb? Running a java class from adb shell would just tell the ART (Android Java Virtual Machine) to run that class as a normal java program, but the user that would be running that java program would be the one that is allowed to inject screen events using monkeyrunner. And as it turns out, that works!

And because an apk is like a jar, just a set of classes packaged and signed, you can actually ship that particular class inside an apk, do not use it in the real Application, but invoke it through adb.

Receiving input events

The hard part is done. We have some code capable of injecting touch events. Now we need to trigger it whenever some gamepad input events arrive. Normal way to respond to a game input is just listening to KeyEvents or MotionEvents in your application, but since you want to use it while you are playing some game, our application needs to capture the gamepad input while the app is running in the background in order to tell the TouchSimulator what to do. For this we need an InputMethodService, basically it's just like telling Android that you'll have a custom keyboard, but actually this keyboard is invisible and it runs in a service that allows us to capture gamepad events.

The problem is, this InputMethodService will actually run in the context of an application and by a unix user that Android creates specifically to run this app. And our TouchSimulator will be run by adb user and in a separated process. How do we make the InputMethodService two to talk to the TouchSimulator? Android provides a few ways to perform communication between processes at a high level, but all of them require of the class Context to work. For InputMethodService that is not a problem, every Service has an application Context. But for TouchSimulator that simply doesn't exist because Android is not injecting a Context. After thinking a while, hooking them with a socket seemed to be the way to go.

TLDR

To sum up: TouchSimulator will inject touch events in the system, but for that it needs to be run through adb so we can avoid root. We'll package it inside the apk to come always along with the rest of the app even though it will never be run from an actual application context. To capture game input we'll use an InputMethodService that will run in an application context, and to communicate between them we'll hook them with a socket. The gamepad input events will be passed by the receiver to a TouchMapper actually run from adb and will use a configuration file to know which gamepad input maps to which touch events and where in the screen. And then the TouchSimulator will inject them into the InputManagerService

Component details

GameInputMethodService

The GameInputMethodService needs to be enabled as an InputMethod in the Android settings menu. This service will receive events in the form of KeyEvent and MotionEvent. For Simplicity it will only intercept events from one single device. It will read the configuration file json that will specify the id of the device to capture input from.

InputSender & InputReceiver

InputSender will get those KeyEvent and MotionEvent and serialize them to send as messages into a socket. On the other side of the socket we'll have InputReceiver running in a different process than the application and by a different user, the one used by adb. InputReceiver will deserialize the messages and turn them into KeyEvent and MotionEvent again to pass them to the TouchMapper

TouchMapper & TouchConfig

TouchConfig represents the configuration json file. Because this json file will be used both by the InputMethodService and the TouchMapper, it needs to have a specific location, this being the external files directory of the application. TouchMapper will read the file as TouchConfig. So whenever a KeyEvent or MotionEvent comes from the InputReceiver, TouchMapper will check the TouchConfig to know if any of the TouchMappings needs to respond to that event. If someone does, it will use the TouchSimulator to perform the touch event. Examples of this are TapMapping or CircleMapping. These mappings will only need to tell the TouchSimulator their ids and the action (UP, MOVE, DOWN,... ) to perform.

TouchSimulator

TouchSimulator gives a higher level way to inject touch events and manages all the hard part of keeping track of the active pointers and properties that the InputManager needs to be given in order to work properly with the multitouch events.

How to launch:

In order to facilitate running the computer adb side, an application was made named Touch Mapper Launcher. What this application does is basically this few steps:

  1. Start ADB and connect to the device It assumes you are going to use ADB over Wifi, so it'll use the device IP to connect to the Android Device

  2. Copy the config file The user will tell the app which configuration file to use, the app will upload it to the external storage directory inside the device where both the Android app and the TouchMapper will expect it to be

adb push ...
  1. Get the apk location Since the java classes to run from adb will be packaged inside the apk we need to know where the apk is, this will be returned by:
adb shell pm path es.shyri.touchmapper
  1. Start the program This command will find the class to run inside the apk returned by previous command and run it
adb shell sh -c "CLASSPATH=/data/app/<apk path> /system/bin/app_process32 /system/bin es.shyri.touchmapper.Main"

Now it should be up and running and you can follow the user guide to know how to run the application side.