3

Background

I want to try to send Unicode characters from the PC to the Android device via adb commands, as if they are being typed from a physical keyboard. Characters from various languages, for example, and not just English.

The problem

Such a thing is impossible using the commands I've found, as it seems to supports only a basic set of characters (probably only Ascii) :

adb shell "input keyboard text 'This goes to Android device'"

Because of this, I've decided to request it to be supported, here (please consider starring).

As a workaround, I thought that maybe I could develop an app that uses AccessibilityService and it would dispatch key events as if I'm typing via the device, and the PC would send such events using adb directly to the app via an Intent.

Thing is, after creating the app, I can't find which function I should use to do it.

What I've found

There are multiple things I've found:

  1. onAccessibilityEvent - this is not for dispatching. It's only for getting events, which I don't think I will even need in this case.
  2. getSoftKeyboardController - a function that can help with hiding the automatically shown keyboard, but that's about it...
  3. dispatchGesture - a function that seems to be used only for dispatching touch events. It seems quite cool, but I don't see that it can handle keys.
  4. performGlobalAction - seems promising, but sadly supports a very limited set of operations (back-key, home-key, etc...).
  5. findFocus - I think I could use this and then dispatch a key event on what I get, but I'm not sure if this is a valid way to do it as I want to dispatch the event globally (plus maybe I would get null object, which means it might not be reliable). Not to mention that according to the options I see, it doesn't allow me to put the text right on the caret and that's it.

The question

Is it possible for AccessibilityService to dispatch a key event of Unicode characters, as if I type some text?

What's the best option to use for this?

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • I am curious if you have seen [this question/answer](https://stackoverflow.com/questions/14224549/adb-shell-input-unicode-character) that references [this project](https://github.com/senzhk/ADBKeyBoard) for inputting Unicode characters and, if you have, why it wouldn't work for you. – Cheticamp Dec 11 '21 at 20:26
  • I have tried the AdbKeyboard project. Other than having to upgrade the version of Gradle, it works like a charm. I am addressing your root issue and not the question about using AccessibilityService. – Cheticamp Dec 12 '21 at 19:27
  • @Cheticamp Having to set the keyboard each time I want to use it, and back to what I actually use is a bad workaround. Sorry. I want to keep using my Gboard app. Besides, sometimes I do use Gboard even when the device is still being mirrored. – android developer Dec 12 '21 at 21:08
  • So, is this for testing of apps that you control or are you looking to send key events to any Android app like the "adb shell input" command can do? If the latter, you may be waiting for an update to the input command. – Cheticamp Dec 12 '21 at 21:54
  • Use the "adb shell ime" command to switch to AdbKeyboard, send the Unicode text and immediately switch the IME back to Gboard. The window when Gboard would not be available to you would be very short. You can put this all into a script. – Cheticamp Dec 13 '21 at 01:01
  • Again, I don't want such a workaround of another keyboard. What do you mean about "update to the input command" ? You say that Google will provide new commands soon, that support Unicode? – android developer Dec 13 '21 at 22:02
  • The problem is the input command (input.java) as it can only accommodate those chars found in "/system/usr/keychars/Virtual.kcm" which is, basically, ASCII. The input command would have to be reworked or a new command made available. I think that will be a long wait since it has already been years since this was first identified as a problem. If you can write a system shell command with key injection privileges, that would be one way to go. As for accessibility, my feeling (don't know) is that being able to send characters around to other apps would be a grave security risk. – Cheticamp Dec 13 '21 at 23:39
  • There is an interested note [here](https://source.android.com/devices/input/key-character-map-files.html#behaviors) about the Unicode character '\uef00' being used in a special way. There may be a way to use this if you are willing to edit the .kcm file I mention above. Unfortunately, I can't tell you more than that. – Cheticamp Dec 13 '21 at 23:45
  • Unless you want to control the device over a different network or independent from a network connection, I think you can use a socket communication (as a LAN or WLAN maybe) and send whatever has typed as an input to the device as a unicode character directly to the device as bytes. Then, with the help of an AccessibilityService, you can add that character (or remove if backspace is pressed) to the focused edit text. If you want to use the soft keyboard in the device itself, that is a different story. – Furkan Yurdakul Dec 14 '21 at 06:20
  • @Cheticamp Accessibility itself is a huge security risk. It's very often tech can be used for good and for bad. Tech is neutral. The link is interesting, but you say that if there is a solution using it, it would require root, right? – android developer Dec 14 '21 at 11:09
  • @FurkanYurdakul That's the idea I wrote. The question I ask is : How? How can the AccessibilityService dispatch the keys? If you know how, please show it in the answer with some code snippets to prove that it works. – android developer Dec 14 '21 at 11:10
  • It would require root. – Cheticamp Dec 14 '21 at 11:42
  • @Cheticamp Have you tried something using root? Your suggestion is that using a rooted device, I will change the content of one of the files there, to support more available keys to be dispatched? – android developer Dec 14 '21 at 11:55
  • No, I have not tried using root or doing anything with that .kcm file. I am just noting an interesting comment that _may_ be useful and I don't know if it would work or not.The only thing I do have is a Windows .bat file that manages Unicode data entry while managing the switch between AdbKeyboard and Gboard, but that is not apropos and does not answer the question. – Cheticamp Dec 14 '21 at 12:08
  • One last thing: Take a look at [UiAutomation](https://developer.android.com/reference/android/app/UiAutomation). It is described as a _"...a special type of AccessibilityService.."_ and it looks like it can drive the UI of another app. I am wondering if a broadcast receiver could be set up using UiAutomation to do what you want. I think that it is worth a try. – Cheticamp Dec 14 '21 at 14:21
  • @Cheticamp I don't mind having an app on the device. That's what I was asking about, actually. The app would have accessibility. I just ask how to dispatch Unicode characters ... – android developer Dec 15 '21 at 15:10
  • I have a POC using UiAutomator and a broadcast receiver. If that interests you, I can post it. It is very rough and needs some (a lot?) of work, but it lets Unicode characters come in over the adb connection and works like the shell input command. Not exactly an "app", but very similar. – Cheticamp Dec 15 '21 at 15:20

3 Answers3

4

This is an unconventional solution.

You can use the UI Automator framework to send Unicode characters through ADB to a focused text field like the input command does with ASCII characters (but fails with Unicode characters.)

First, implement an Android automation test that is capable of receiving broadcasts. Broadcasts will direct the test to do certain tasks. The implementation below will clear text and enter text using Base-64 or Unicode. I should not that the following can act like a background server until stopped.

AdbReceiver.kt

package com.example.adbreceiver

/*
 * Test that runs with a broadcast receiver that accepts commands.
 *
 * To start the test:
 * adb shell nohup am instrument -w com.example.adbreceiver.test/androidx.test.runner.AndroidJUnitRunner
 *
 * On Windows, the code page may need to be changed to UTF-8 by using the following command:
 *      chcp 65001
 *
 */
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Base64
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 18)
class AdbInterface {
    private var mDevice: UiDevice? = null
    private var mStop = false

    private val ACTION_MESSAGE = "ADB_INPUT_TEXT"
    private val ACTION_MESSAGE_B64 = "ADB_INPUT_B64"
    private val ACTION_CLEAR_TEXT = "ADB_CLEAR_TEXT"
    private val ACTION_STOP = "ADB_STOP"

    private var mReceiver: BroadcastReceiver? = null

    @Test
    fun adbListener() {
        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
        if (mReceiver == null) {
            val filter = IntentFilter(ACTION_MESSAGE)
            filter.addAction(ACTION_MESSAGE_B64)
            filter.addAction(ACTION_CLEAR_TEXT)
            filter.addAction(ACTION_STOP)
            mReceiver = AdbReceiver()
            ApplicationProvider.getApplicationContext<Context>().registerReceiver(mReceiver, filter)
        }
        try {
            // Keep us running to receive commands.
            // Really not a good way to go, but it works for the proof of concept.
            while (!mStop) {
                Thread.sleep(10000)
            }
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
    }

    fun inputMsg(s: String?) {
        mDevice?.findObject(By.focused(true))?.setText(s)
    }

    internal inner class AdbReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            when (intent.action) {
                ACTION_MESSAGE -> {
                    val msg = intent.getStringExtra("msg")
                    inputMsg(msg)
                }
                ACTION_MESSAGE_B64 -> {
                    val data = intent.getStringExtra("msg")
                    val b64 = Base64.decode(data, Base64.DEFAULT)
                    val msg: String
                    try {
                        msg = String(b64, Charsets.UTF_8)
                        inputMsg(msg)
                    } catch (e: Exception) {
                    }
                }

                ACTION_CLEAR_TEXT -> inputMsg("")

                ACTION_STOP -> {
                    mStop = true
                    ApplicationProvider.getApplicationContext<Context>()
                        .unregisterReceiver(mReceiver)
                }
            }
        }
    }
}

Here is a short demo running on an emulator. In the demo, the first text "你好嗎? Hello?" is entered with adb using Base-64 encoding. The second text, "你好嗎? Hello, again?" is entered as a straight Unicode string.

enter image description here

Here are three Windows .bat file to manage the interface. There is not reason that these can't be ported to other OSes.

start.bat

Starts the instrumented test that receives the command broadcasts. This will run until it receives the "ADB_STOP" command.

rem Start AdbReceiver and disconnect.
adb shell nohup am instrument -w com.example.adbreceiver.test/androidx.test.runner.AndroidJUnitRunner

send.bat

Used for the demo to send Unicode text but can be easily generalized

rem Send text entry commands to AdbReceiver. All text is input on the current focused element.
rem Change code page to UTF-8.
chcp 65001 
rem Clear the field.
adb shell am broadcast -a ADB_CLEAR_TEXT
rem Input the Unicode characters encode in Base-64.
adb shell am broadcast -a ADB_INPUT_B64 --es msg 5L2g5aW95ZeOPyBIZWxsbz8=
rem Input the Unicode characters withouth further encoding.
adb shell am broadcast -a ADB_INPUT_TEXT --es msg '你好嗎? Hello, again?'

stop.bat

Stops the instrumented test.

rem Stop AdbReceiver.
adb shell am broadcast -a ADB_STOP

For some reason, the code only works on API 21+. It doesn't error out on earlier APIs but just silently fails.

This is just a proof of concept and the code needs more work.

Project AdbReceiver is on GitHub.

How to Run (Windows)

  1. Start an emulator.

  2. Bring up the project in Android Studio.

  3. Under java->com.example.adbreceiver (andoidTest) right click AdbInterface.

  4. In the pop-up menu, click "Run". This will start the instrumented test.

  5. Bring up any app on the emulator and set the cursor into a data entry fields (EditText).

  6. In a terminal window, enter

    adb shell am broadcast -a ADB_INPUT_TEXT --es msg 'Hello World!'

This should enter "Hello World!" into the text field.

This can also be accomplished from the command line. See the "start.bat" file above on how to start the test and the "stop.bat" file on how to stop it.

Notes on UI Automator and Assessibility

I took a look under the hood at how UI Automator works. As the OP guessed, UI Automator does use assessibility services on Android. The AssessibilityNode is used to set text. In the posted code above, the inputMesg() function has the line:

mDevice?.findObject(By.focused(true))?.setText(s)

findObject() is in UiDevice.java which looks like this:

public UiObject2 findObject(BySelector selector) {
    AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
    return node != null ? new UiObject2(this, selector, node) : null;
}

setText() can be found in _UiObject2.java` and starts off like this:

public void setText(String text) {
        AccessibilityNodeInfo node = getAccessibilityNodeInfo();

        // Per framework convention, setText(null) means clearing it
        if (text == null) {
            text = "";
        }

        if (UiDevice.API_LEVEL_ACTUAL > Build.VERSION_CODES.KITKAT) {
            // do this for API Level above 19 (exclusive)
            Bundle args = new Bundle();
            args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
            if (!node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)) {
                // TODO: Decide if we should throw here
                Log.w(TAG, "AccessibilityNodeInfo#performAction(ACTION_SET_TEXT) failed");
            }
        } else {
        ...

So, accessibility services is integral to UI Automator.

Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • 1
    Not sure I understand what's going on here. Is this globally or not? Does it work only for EditText? How does it work, if it's global? What does it do using accessibility? What are the steps of using it? – android developer Dec 15 '21 at 18:51
  • 1
    It's works across apps if that is what "globally" means. It operates like the "input" command but can accept Unicode characters. The simplest way to use it is to bring up the project in Android Studio and start the instrumented test on an emulator. Use the "send.bat" script to send commands to it and it will populate an EditText that has focus. Being an instrumented test, it can do much more but will need additional coding. – Cheticamp Dec 15 '21 at 19:34
  • 1
    The start.bat and stop.bat macros will stop and start the instrumented test and make it operate like a background server, so it will just be available at any time. – Cheticamp Dec 15 '21 at 19:37
  • 1
    So it's not an app? And it can be for every View, and not just EditText? If so, why did you write "a focused text field"? Please explain all the steps to use this. I don't understand what do you offer here. There are no batch files file in the project either, except for "a.bat" ... – android developer Dec 15 '21 at 20:36
  • 1
    "send.bat" is in the answer not the project. My answer addresses this in your question: _"I want to try to send Unicode characters from the PC to the Android device via adb commands..."_. This does that and only that for "EditText", but there is no reason it can't address other views. If you want it to do more, it will have to be added. I suggest that you try the code. @androiddeveloper – Cheticamp Dec 15 '21 at 20:38
  • I tried to open the project but there is no code. It's just the default `setContentView(R.layout.activity_main)` etc... I don't think it's an app. Are you sure I can launch here anything without using the IDE ? – android developer Dec 15 '21 at 20:40
  • I see. It is confusing. The code is actually an instrumented test and is at app->java->com (androidTest)->example->adbreceiver->AdbInterface. Right click on that and click "Run AdbInterface". That will start the instrumented test on the emulator. You can then interact with the the emulator with the .bat commands or use your own commands. Make sure that the emulator has an app displayed with a focused EditText. I'll make a note of this in the answer. – Cheticamp Dec 15 '21 at 20:50
  • Will this mean that I need to use the IDE each time for this solution? Or there is a better way? Also how does it work exactly? It's not via accessibilityService? Maybe you should offer your solution to ScrCpy, here: https://github.com/Genymobile/scrcpy/issues/37 . I offered there to try a solution using AccessibilityService, but couldn't find a solution, so I wrote here as I thought that maybe there is some API that I just failed to find. You say that it's possible using something else, right? – android developer Dec 15 '21 at 21:01
  • No, you don't need the IDE each time. It is possible to start the test "service" from the command line. Once running, it should run until stopped or the device is restarted. See "start.bat" and "stop.bat" in the answer. I use UI Automator and it may (I suspect) use AssessibilityService under the hood., idk. What I have presented works and can be extended. I doubt that you will find a way with AccessibilityService especially considering what is noted [here](https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo#setText(java.lang.CharSequence)). – Cheticamp Dec 15 '21 at 21:28
  • See my update regarding accessibility. It turns out that my solution is very similar to the possible _findFocus_ solution you mention. It does not allow for character placement when the caret in within the text but I do confirm that Unicode text entry is possible globally (across apps.) at least when running under instrumentation. – Cheticamp Dec 16 '21 at 15:05
  • @androiddeveloper Updated project to do key injection although it now doesn't do Unicode (I will fix that later). The question now is how to create key events for Unicode characters. Solve that issue and this is a replacement for "input." – Cheticamp Dec 16 '21 at 16:57
  • Suppose I make an app that has no EditText in it (a game, for example), would it be able to capture the keys you dispatch via this method? – android developer Dec 17 '21 at 06:51
  • Depends,, I would say, on how the game captures its input. Do you have something specific in mind? – Cheticamp Dec 17 '21 at 11:53
  • @androiddeveloper I discovered that the input command has more options in Android S. No documentation that I know of, but it has some promising new commands. "adb shell input" will show the command line options. – Cheticamp Dec 17 '21 at 20:20
  • Your solution doesn't seem to work for me. I chose to run the test, but it never finishes. I tried to ignore this and run the adb command you've shown while having some place to write it (search field in the Play Store app), and it didn't write anything either. Looking at the code, it also seems it would replace the text, and not type it (append to the place of the caret) like a normal keyboard would. Bounty was granted for showing it's impossible and yet all the effort was done. I will mark "accepted" probably only when I see something really working. – android developer Dec 17 '21 at 21:55
  • It does replace the contents of the text field. No way that I have found to enter individual Unicode characters only ASCII. What is your configuration? I am running Windows 10 with a Google Android emulator with various APIs 21+. It also runs on an old Samsung Galaxy S7 API 26. – Cheticamp Dec 17 '21 at 23:24
  • I use Windows 10 with Pixel 4 and Android 12L. – android developer Dec 20 '21 at 11:50
  • @androiddeveloper I took another look and have character-by-character insertion working for Unicode (at least for Hebrew which you say you use.) Unfortunately, something changed in API 30 which propagates to API 31 that makes Unicode characters not work :-( I will post something sometime soon(ish) at the GitHub repo. I opened an issue there if you want to comment. I think that this comment section is long enough. – Cheticamp Dec 20 '21 at 17:53
  • I see. You said you've found something about accessibility that might be useful too, no? Have you found more information about it? – android developer Dec 21 '21 at 01:16
  • Just UI Automator and the IME implementation (AdbKeyboard) is all I have discovered. What I posted was UI Automator with Assessibility behind the scenes. What I will post to the GitHub repo is UI Automator with Unicode character insertion for API 29 and below. – Cheticamp Dec 21 '21 at 04:23
  • OK thanks. Thing is that I failed to even launch it. I didn't reach the phase of the command to dispatch a key. As for accessibility, I was talking about this: https://stackoverflow.com/questions/70160582/can-accessibilityservice-dispatch-key-events-including-even-unicode-characters/70368620?noredirect=1#comment124442852_70368620 . You said that maybe there is something that can be investigated there. – android developer Dec 21 '21 at 08:09
  • @androiddeveloper Right. I was just mentioning that change have been made to the shell input command that _may_ be useful. I don't know what the changes do or if Unicode outside ASCII will be accepted. It was just an observation. – Cheticamp Dec 21 '21 at 14:40
  • OK if you find anything please let me know – android developer Dec 21 '21 at 14:51
2

Here is what I found based on my research on input command.

input keyevent KEYCODE_N

These KEYCODE are predefined in here

Logs:

Input   : injectKeyEvent: KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_N, scanCode=0, metaState=0, flags=0x0, repeatCount=0, eventTime=25861196, downTime=25861196, deviceId=-1, source=0x101 }
Input   : injectKeyEvent: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_N, scanCode=0, metaState=0, flags=0x0, repeatCount=0, eventTime=25861196, downTime=25861196, deviceId=-1, source=0x101 }

To get more insight on above logs refer this source file.

Even switching the keyboard layout to other languages in the device, the keyevent only prints the ASCII characters.

Here are the resources that I found for key event in accessibility services:

In Android framework the accessibility service already dispatch the key Event. Refer here

FLAG_REQUEST_FILTER_KEY_EVENTS: If this flag is set the accessibility service will receive the key events before applications allowing it implement global shortcuts.

Note: Analysis based on Android 9.0

Fizn-Ahmd
  • 69
  • 5
  • I don't understand. Are you saying it's impossible? If not, suppose we take the character in Hebrew : "א". How do you dispatch it, exactly? – android developer Dec 15 '21 at 15:08
  • Through input command it is impossible to dispatch a unicode keyevent. As unicode character are not defined in KeyEvent.java . Whereas, for ascii character the keyevent will be dispatched by accessibility service from android framework and can be published to the accessibility service that is developed through [meta data](https://developer.android.com/reference/android/accessibilityservice/AccessibilityService#SERVICE_META_DATA). – Fizn-Ahmd Dec 15 '21 at 18:56
  • Sorry but I still don't understand. I asked a yes/no question: Are you saying it's impossible? There is an API for this or not? – android developer Dec 15 '21 at 19:01
  • No, there is no API for this, through input command it is not possible to dispatch unicode characters. – Fizn-Ahmd Dec 16 '21 at 11:57
  • input command? I asked about accessibility because I already noticed that adb commands can't do it on their own.... – android developer Dec 16 '21 at 12:49
  • dispatching keyEvent from accessibility service?? If this is your question then there are no API that can dispatch a keyEvent from accessibility service. – Fizn-Ahmd Dec 17 '21 at 10:49
  • There is a API that gets the KeyEvent that is generated by other system service. But dispatching keyEvent from accessibility is not possible. – Fizn-Ahmd Dec 17 '21 at 10:55
  • If you want to dispatch KeyEvent you can try importing [android.view.KeyEvent](https://developer.android.com/reference/android/view/KeyEvent) class to dispatch keyEvent to other services with required permissions. – Fizn-Ahmd Dec 17 '21 at 11:01
1

Not sure, if I exactly understand what you are looking for.

But, here are some resources that you can refer to perform actions on behalf of users using the Accessibility Service.

  1. performAction()
  2. performGlobalAction()
  3. Taking Actions for User

Or in case you are looking to develop a completely new accessibility service, you can refer to this:

Avinash Karhana
  • 659
  • 4
  • 16