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.

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)
Start an emulator.
Bring up the project in Android Studio.
Under java->com.example.adbreceiver (andoidTest) right click AdbInterface.
In the pop-up menu, click "Run". This will start the instrumented test.
Bring up any app on the emulator and set the cursor into a data entry fields (EditText).
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.