<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: James Yang</title>
    <description>The latest articles on DEV Community by James Yang (@metooyang2008).</description>
    <link>https://dev.to/metooyang2008</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4091654%2F2cc9489e-c3df-4160-ac06-610d0ad7f899.png</url>
      <title>DEV Community: James Yang</title>
      <link>https://dev.to/metooyang2008</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/metooyang2008"/>
    <language>en</language>
    <item>
      <title>Why you can't type Chinese or Japanese in Android SSH terminals — and how to fix it</title>
      <dc:creator>James Yang</dc:creator>
      <pubDate>Mon, 24 Aug 2026 05:55:10 +0000</pubDate>
      <link>https://dev.to/metooyang2008/why-you-cant-type-chinese-or-japanese-in-android-ssh-terminals-and-how-to-fix-it-p2a</link>
      <guid>https://dev.to/metooyang2008/why-you-cant-type-chinese-or-japanese-in-android-ssh-terminals-and-how-to-fix-it-p2a</guid>
      <description>&lt;p&gt;&lt;em&gt;A deep dive into &lt;code&gt;TYPE_NULL&lt;/code&gt;, &lt;code&gt;InputConnection&lt;/code&gt;, and &lt;code&gt;commitText()&lt;/code&gt; — and why switching keyboards never helps.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've ever opened an SSH client on Android, switched to a Chinese, Japanese, or Korean keyboard, and watched your carefully-composed text simply &lt;strong&gt;not appear&lt;/strong&gt; in the shell — you're not imagining it, and it's not your keyboard's fault. It's a bug in how most terminal apps handle text input, and once you understand it, the fix is obvious.&lt;/p&gt;

&lt;p&gt;This post explains the root cause, why it affects far more languages than just Chinese, why changing your IME doesn't help, and how to implement it correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;You SSH into a server that fully supports UTF-8. You tap the terminal, your keyboard pops up, you switch to Pinyin, you type &lt;code&gt;nihao&lt;/code&gt;, the candidate bar shows 你好 — and when you tap it, &lt;strong&gt;nothing&lt;/strong&gt; reaches the terminal. English works fine. The keyboard clearly works in every other app. But in this one, non-English text evaporates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two layers of Android text input
&lt;/h2&gt;

&lt;p&gt;To see why, you need to know that Android has &lt;strong&gt;two completely different input paths&lt;/strong&gt;, and a terminal has to deal with both:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hardware / raw key events&lt;/strong&gt; — &lt;code&gt;KeyEvent&lt;/code&gt;s delivered to &lt;code&gt;View.onKeyDown()&lt;/code&gt;. This is how a terminal gets &lt;code&gt;Ctrl&lt;/code&gt;, &lt;code&gt;Alt&lt;/code&gt;, &lt;code&gt;Esc&lt;/code&gt;, &lt;code&gt;Tab&lt;/code&gt;, arrow keys, function keys — the control keys a shell needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The input method (IME) framework&lt;/strong&gt; — the on-screen keyboard talks to your app through an &lt;code&gt;InputConnection&lt;/code&gt;. This is how &lt;em&gt;text&lt;/em&gt; is entered, including everything that needs composition:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   pinyin:   nihao → [candidate bar] → 你好   → commitText("你好")
   japanese: nihongo → にほんご → 日本語        → commitText("日本語")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IME never sends a &lt;code&gt;KeyEvent&lt;/code&gt; for 你好 — there is no "你好 key." It sends the finished text through &lt;code&gt;InputConnection.commitText()&lt;/code&gt;, after showing you a live preview via &lt;code&gt;setComposingText()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause: terminals that behave like a keyboard, not a text field
&lt;/h2&gt;

&lt;p&gt;Terminal apps lean heavily on path #1 because they &lt;em&gt;must&lt;/em&gt; — a shell is useless without &lt;code&gt;Ctrl-C&lt;/code&gt;, &lt;code&gt;Tab&lt;/code&gt;, and &lt;code&gt;Esc&lt;/code&gt;. So many implementations take a shortcut and treat the terminal as a &lt;strong&gt;keyboard device&lt;/strong&gt; rather than a real text editor. That shortcut shows up in one of two ways:&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure mode A: declaring &lt;code&gt;TYPE_NULL&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;InputConnection&lt;/span&gt; &lt;span class="nf"&gt;onCreateInputConnection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EditorInfo&lt;/span&gt; &lt;span class="n"&gt;outAttrs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;outAttrs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InputType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TYPE_NULL&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "just give me raw key events"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BaseInputConnection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TYPE_NULL&lt;/code&gt; tells the IME: &lt;em&gt;this field doesn't accept composed text; send me key events instead.&lt;/em&gt; Modern IMEs respond by falling back to a &lt;strong&gt;Latin-only, no-composition&lt;/strong&gt; mode. The Chinese/Japanese candidate machinery is disabled at the source. You can still switch to the Chinese keyboard visually, but it can't compose — so nothing meaningful gets committed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure mode B: ignoring &lt;code&gt;commitText()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Even with a non-null input type, some terminals only wire up &lt;code&gt;KeyEvent&lt;/code&gt; handling and never properly handle the &lt;code&gt;InputConnection&lt;/code&gt; callbacks. The IME calls &lt;code&gt;commitText("你好")&lt;/code&gt;, the app… does nothing with it. The text is dropped on the floor.&lt;/p&gt;

&lt;p&gt;Either way, the observable result is identical: &lt;strong&gt;English (which arrives as key events) works; anything that arrives via &lt;code&gt;commitText()&lt;/code&gt; doesn't.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why switching keyboards doesn't help
&lt;/h2&gt;

&lt;p&gt;This is the part that trips everyone up. People try Gboard, then Sogou, then Baidu, then iFlytek — same result — and conclude their phone is broken.&lt;/p&gt;

&lt;p&gt;But the bug is in the &lt;strong&gt;app's &lt;code&gt;InputConnection&lt;/code&gt;&lt;/strong&gt;, not the keyboard. No IME can deliver composed text to a field that declared &lt;code&gt;TYPE_NULL&lt;/code&gt; or that throws away &lt;code&gt;commitText()&lt;/code&gt;. The keyboard is doing its job correctly; the app refuses to listen.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not a "Chinese" problem — it's an IME problem
&lt;/h2&gt;

&lt;p&gt;Because the real issue is the IME text path, the blast radius is huge. Anything that isn't a direct ASCII keypress is affected:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Languages&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Composition + candidate conversion&lt;/td&gt;
&lt;td&gt;Chinese (Pinyin/Zhuyin), Japanese (kana→kanji)&lt;/td&gt;
&lt;td&gt;Rely entirely on composing text + candidates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composition&lt;/td&gt;
&lt;td&gt;Korean (Hangul), Hindi/Bengali/Tamil, transliteration IMEs&lt;/td&gt;
&lt;td&gt;Build characters via composition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-ASCII Unicode commit&lt;/td&gt;
&lt;td&gt;Russian, Greek, Arabic, Persian, Hebrew, Thai, Vietnamese, accented French/German/Spanish&lt;/td&gt;
&lt;td&gt;No candidates, but still delivered via &lt;code&gt;commitText()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works everywhere&lt;/td&gt;
&lt;td&gt;English / ASCII&lt;/td&gt;
&lt;td&gt;Arrives as key events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you build "Chinese input" and stop there, you've solved a sliver of the actual problem. The correct framing is &lt;strong&gt;full Android IME / Unicode input&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to fix it
&lt;/h2&gt;

&lt;p&gt;The terminal has to serve &lt;strong&gt;both&lt;/strong&gt; input paths at once: keep raw key events for control keys, &lt;em&gt;and&lt;/em&gt; implement the &lt;code&gt;InputConnection&lt;/code&gt; contract for text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Declare a real text input type&lt;/strong&gt; (not &lt;code&gt;TYPE_NULL&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;InputConnection&lt;/span&gt; &lt;span class="nf"&gt;onCreateInputConnection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EditorInfo&lt;/span&gt; &lt;span class="n"&gt;outAttrs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// A real editable text field, so IMEs enable composition for CJK &amp;amp; friends.&lt;/span&gt;
    &lt;span class="c1"&gt;// NO_SUGGESTIONS avoids autocorrect meddling with commands.&lt;/span&gt;
    &lt;span class="n"&gt;outAttrs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InputType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TYPE_CLASS_TEXT&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;InputType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TYPE_TEXT_FLAG_NO_SUGGESTIONS&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;outAttrs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;imeOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EditorInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IME_FLAG_NO_FULLSCREEN&lt;/span&gt;
            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;EditorInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IME_FLAG_NO_EXTRACT_UI&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TerminalInputConnection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* fullEditor = */&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A subtle trap: don't reach for &lt;code&gt;TYPE_TEXT_VARIATION_VISIBLE_PASSWORD&lt;/code&gt; to "stabilize" the keyboard. Password variations make many CJK IMEs force a Latin-only layout — and on some OEM skins (e.g. MIUI) put the field into a secure mode. &lt;code&gt;NO_SUGGESTIONS&lt;/code&gt; alone is enough to stop autocorrect.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. Handle composing text — but keep it local.&lt;/strong&gt; The composing (preedit) string is &lt;em&gt;not&lt;/em&gt; final; the remote shell has no concept of "text being composed." So draw it yourself at the cursor and only send text on commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;setComposingText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CharSequence&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;newCursorPosition&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setComposingText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newCursorPosition&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;updatePreeditOverlay&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// draw the in-progress "nihao"/"にほん" near the cursor&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;                &lt;span class="c1"&gt;// do NOT send to the remote yet&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;commitText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CharSequence&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;newCursorPosition&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;commitText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newCursorPosition&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;sendToTerminal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getEditable&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;  &lt;span class="c1"&gt;// now it's final → send it&lt;/span&gt;
    &lt;span class="n"&gt;getEditable&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;clearPreeditOverlay&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Encode by Unicode code point, not &lt;code&gt;char&lt;/code&gt;.&lt;/strong&gt; Java strings are UTF-16; emoji and many CJK extension characters are surrogate pairs. Iterate code points so you don't split them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendToTerminal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CharSequence&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charAt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Character&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isHighSurrogate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Character&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toCodePoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charAt&lt;/span&gt;&lt;span class="o"&gt;(++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;writeCodePointAsUtf8&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// → SSH&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Leave the key-event path intact.&lt;/strong&gt; &lt;code&gt;Ctrl&lt;/code&gt;, &lt;code&gt;Alt&lt;/code&gt;, &lt;code&gt;Esc&lt;/code&gt;, &lt;code&gt;Tab&lt;/code&gt;, arrows, &lt;code&gt;Ctrl-C&lt;/code&gt; etc. still flow through &lt;code&gt;onKeyDown()&lt;/code&gt;/your key handler. The two paths coexist: control keys via &lt;code&gt;KeyEvent&lt;/code&gt;, text via &lt;code&gt;InputConnection&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the whole fix. The moment the app declares itself a real text field and honors &lt;code&gt;commitText()&lt;/code&gt;, every IME — Gboard, Sogou, Baidu, a Hindi or Arabic keyboard — starts working, because you're finally speaking the protocol they've been speaking all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest caveat: input vs. display
&lt;/h2&gt;

&lt;p&gt;Getting the bytes &lt;em&gt;in&lt;/em&gt; is solved by the above. &lt;strong&gt;Displaying&lt;/strong&gt; complex scripts is a separate, older terminal-emulator limitation: CJK wide characters and combining marks are fine, but right-to-left scripts (Arabic, Hebrew) and complex Indic ligatures are typically rendered cell-by-cell, left-to-right, without bidi reordering — in almost every terminal emulator, not just Android ones. So Arabic input reaches your server correctly; it may just look simplified in the terminal view. Worth stating plainly so you're not surprised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;"I can't type Chinese in my SSH client" is really "this terminal declared &lt;code&gt;TYPE_NULL&lt;/code&gt; / ignores &lt;code&gt;commitText()&lt;/code&gt;." It's an app bug, not a keyboard problem, and it quietly breaks a dozen writing systems, not one. The fix is to implement Android's &lt;code&gt;InputConnection&lt;/code&gt; contract properly while keeping the raw key-event path for control keys.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hit this exact wall doing "vibe coding" from my phone — running Claude Code and Codex over SSH — so I built *&lt;/em&gt;&lt;a href="https://github.com/metoo2008/vibeterm" rel="noopener noreferrer"&gt;VibeTerm&lt;/a&gt;*&lt;em&gt;, an open-source (GPL-3.0) Android SSH terminal with the IME implementation described above, plus tmux-backed reconnection, split-screen, and lock-screen approval for AI agents. (Disclosure: I'm the author.) If you've been fighting this bug, I'd love your feedback.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>vibecoding</category>
      <category>terminal</category>
      <category>ime</category>
    </item>
  </channel>
</rss>
