diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 0000000..26dc3e5
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,50 @@
+name: Android CI
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ release:
+ types: [published, created]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ ref: master
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'adopt'
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle
+ run: ./gradlew build
+
+ - name: Build Debug APK
+ run: ./gradlew assembleDebug
+
+ - name: Upload APK
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-debug
+ path: "**/build/outputs/apk/debug/*.apk"
+
+ - name: Create Release and Upload APK
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v2
+ with:
+ files: "**/build/outputs/apk/debug/*.apk"
+ generate_release_notes: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/ADBKeyboard.apk b/ADBKeyboard.apk
index 2483dd0..6e54ba9 100644
Binary files a/ADBKeyboard.apk and b/ADBKeyboard.apk differ
diff --git a/README.md b/README.md
index 782394c..3929edf 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,12 @@ is not going to work.
ADBKeyboard will help in these cases, especially in device automation and testings.
+Download APK from release page
+---------------------
+* [v2.5-dev] With A16 fix: [https://github.com/senzhk/ADBKeyBoard/releases/download/v2.5-dev/keyboardservice-debug.apk]
+* [v2.4-dev] Stable APK download: [https://github.com/senzhk/ADBKeyBoard/releases/download/v2.4-dev/keyboardservice-debug.apk]
+* [Old] Release APK download: [https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk]
+
Build and install APK
---------------------
@@ -36,7 +42,12 @@ With one device or emulator connected, use these simple steps to install the key
How to Use
----------
- * Enable 'ADBKeyBoard' in the Language&Input Settings.
+ * Enable 'ADBKeyBoard' in the Language&Input Settings OR from adb.
+```
+adb install ADBKeyboard.apk
+adb shell ime enable com.android.adbkeyboard/.AdbIME
+adb shell ime set com.android.adbkeyboard/.AdbIME
+```
* Set it as Default Keyboard OR Select it as the current input method of certain EditText view.
* Sending Broadcast intent via Adb or your Android Services/Apps.
@@ -48,9 +59,22 @@ adb shell am broadcast -a ADB_INPUT_TEXT --es msg '你好嗎? Hello?'
* This may not work for Oreo/P, am/adb command seems not accept utf-8 text string anymore
1.1 Sending text input (base64) if (1) is not working.
-adb shell am broadcast -a ADB_INPUT_B64 --es msg `echo '你好嗎? Hello?' | base64`
-* You can use the latest base64 input type (together with Mac OS X/Linux's base64 command):
+* For Mac/Linux, you can use the latest base64 input type with base64 command line tool:
+adb shell am broadcast -a ADB_INPUT_B64 --es msg `echo -n '你好嗎? Hello?' | base64`
+
+* Or try this script (provided by LemonNekoGH):
+https://gist.github.com/LemonNekoGH/f7583e0f4fa83e29dfd96d8334144650
+
+* For Windows, please try this script (provided by ssddi456):
+https://gist.github.com/ssddi456/889d5e8a2571a33e8fcd0ff6f1288291
+
+* Sample python script to send b64 codes (provided by sunshinewithmoonlight):
+import os
+import base64
+chars = '的广告'
+charsb64 = str(base64.b64encode(chars.encode('utf-8')))[1:]
+os.system("adb shell am broadcast -a ADB_INPUT_B64 --es msg %s" %charsb64)
2. Sending keyevent code (67 = KEYCODE_DEL)
adb shell am broadcast -a ADB_INPUT_CODE --ei code 67
@@ -61,6 +85,22 @@ adb shell am broadcast -a ADB_EDITOR_CODE --ei code 2
4. Sending unicode characters
To send 😸 Cat
adb shell am broadcast -a ADB_INPUT_CHARS --eia chars '128568,32,67,97,116'
+
+5. Send meta keys
+To send Ctrl + A as below: (4096 is META_CONTROL_ON, 8192 is META_CONTROL_LEFT_ON, 29 is KEYCODE_A)
+adb shell am broadcast -a ADB_INPUT_TEXT --es mcode '4096,29' // one metaState.
+or
+adb shell am broadcast -a ADB_INPUT_TEXT --es mcode '4096+8192,29' // two metaState.
+
+
+6. CLEAR all text (starting from v2.0)
+adb shell am broadcast -a ADB_CLEAR_TEXT
+
+
+
+Enable ADBKeyBoard from adb :
+
+adb shell ime enable com.android.adbkeyboard/.AdbIME
Switch to ADBKeyBoard from adb (by [robertio](https://github.com/robertio)) :
@@ -78,6 +118,11 @@ Check your available virtual keyboards:
adb shell ime list -a
+Reset to default, don't care which keyboard was chosen before switch:
+
+adb shell ime reset
+
+
You can try the apk with my debug build: https://github.com/senzhk/ADBKeyBoard/raw/master/ADBKeyboard.apk
KeyEvent Code Ref: http://developer.android.com/reference/android/view/KeyEvent.html
diff --git a/build.gradle b/build.gradle
index b58cbd1..b4321e2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,9 +1,20 @@
buildscript {
repositories {
- jcenter()
+ maven { url 'https://maven.aliyun.com/repository/google' }
+ maven { url 'https://maven.aliyun.com/repository/central' }
google()
+ mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.0.1'
+ classpath 'com.android.tools.build:gradle:8.1.0'
+ }
+}
+
+allprojects {
+ repositories {
+ maven { url 'https://maven.aliyun.com/repository/google' }
+ maven { url 'https://maven.aliyun.com/repository/central' }
+ google()
+ mavenCentral()
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b3104b8..6f27095 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Sun Oct 14 18:04:03 HKT 2018
+#Wed Dec 03 11:08:02 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
diff --git a/keyboardservice/build.gradle b/keyboardservice/build.gradle
index be3be34..a65ab60 100644
--- a/keyboardservice/build.gradle
+++ b/keyboardservice/build.gradle
@@ -1,18 +1,18 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 23
- buildToolsVersion '28.0.3'
+ namespace 'com.android.adbkeyboard'
+ compileSdk 33
defaultConfig {
applicationId 'com.android.adbkeyboard'
- minSdkVersion 15
- targetSdkVersion 22
- versionCode 1
- versionName "1.0"
+ minSdk 21
+ targetSdk 33
+ versionCode 2
+ versionName "2.0"
}
}
dependencies {
- compile fileTree(include: ['*.jar'], dir: 'libs')
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
}
diff --git a/keyboardservice/src/main/AndroidManifest.xml b/keyboardservice/src/main/AndroidManifest.xml
index e1e3359..4213ffe 100644
--- a/keyboardservice/src/main/AndroidManifest.xml
+++ b/keyboardservice/src/main/AndroidManifest.xml
@@ -1,5 +1,4 @@
@@ -12,6 +11,7 @@
diff --git a/keyboardservice/src/main/java/com/android/adbkeyboard/AdbIME.java b/keyboardservice/src/main/java/com/android/adbkeyboard/AdbIME.java
index e7c6f6a..d251a45 100644
--- a/keyboardservice/src/main/java/com/android/adbkeyboard/AdbIME.java
+++ b/keyboardservice/src/main/java/com/android/adbkeyboard/AdbIME.java
@@ -4,54 +4,131 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.Build;
import android.inputmethodservice.InputMethodService;
import android.util.Base64;
import android.util.Log;
+import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
public class AdbIME extends InputMethodService {
- private String IME_MESSAGE = "ADB_INPUT_TEXT";
- private String IME_CHARS = "ADB_INPUT_CHARS";
- private String IME_KEYCODE = "ADB_INPUT_CODE";
- private String IME_EDITORCODE = "ADB_EDITOR_CODE";
- private String IME_MESSAGE_B64 = "ADB_INPUT_B64";
- private BroadcastReceiver mReceiver = null;
-
- @Override
- public View onCreateInputView() {
- View mInputView = getLayoutInflater().inflate(R.layout.view, null);
-
- if (mReceiver == null) {
- IntentFilter filter = new IntentFilter(IME_MESSAGE);
- filter.addAction(IME_CHARS);
- filter.addAction(IME_KEYCODE);
- filter.addAction(IME_EDITORCODE);
- filter.addAction(IME_MESSAGE_B64);
- mReceiver = new AdbReceiver();
- registerReceiver(mReceiver, filter);
- }
-
- return mInputView;
- }
-
- public void onDestroy() {
- if (mReceiver != null)
- unregisterReceiver(mReceiver);
- super.onDestroy();
- }
-
- class AdbReceiver extends BroadcastReceiver {
+ private String IME_MESSAGE = "ADB_INPUT_TEXT";
+ private String IME_CHARS = "ADB_INPUT_CHARS";
+ private String IME_KEYCODE = "ADB_INPUT_CODE";
+ private String IME_META_KEYCODE = "ADB_INPUT_MCODE";
+ private String IME_EDITORCODE = "ADB_EDITOR_CODE";
+ private String IME_MESSAGE_B64 = "ADB_INPUT_B64";
+ private String IME_CLEAR_TEXT = "ADB_CLEAR_TEXT";
+ private String IME_ACTION_SEARCH = "ADB_ACTION_SEARCH";
+ private String IME_ACTION_GO = "ADB_ACTION_GO";
+ private String IME_ACTION_DONE = "ADB_ACTION_DONE";
+ private String IME_ACTION_NEXT = "ADB_ACTION_NEXT";
+ private String IME_ACTION_SEND = "ADB_ACTION_SEND";
+ private BroadcastReceiver mReceiver = null;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ registerAdbReceiver();
+ }
+
+ private void registerAdbReceiver() {
+ if (mReceiver != null) {
+ return;
+ }
+ IntentFilter filter = new IntentFilter(IME_MESSAGE);
+ filter.addAction(IME_CHARS);
+ filter.addAction(IME_KEYCODE);
+ filter.addAction(IME_MESSAGE); // IME_META_KEYCODE // Change IME_MESSAGE to get more values.
+ filter.addAction(IME_EDITORCODE);
+ filter.addAction(IME_MESSAGE_B64);
+ filter.addAction(IME_CLEAR_TEXT);
+ filter.addAction(IME_ACTION_SEARCH);
+ filter.addAction(IME_ACTION_GO);
+ filter.addAction(IME_ACTION_DONE);
+ filter.addAction(IME_ACTION_NEXT);
+ filter.addAction(IME_ACTION_SEND);
+ mReceiver = new AdbReceiver();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // API 33+: required; shell/adb is another UID — must be exported.
+ registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED);
+ } else {
+ registerReceiver(mReceiver, filter);
+ }
+ }
+
+ @Override
+ public View onCreateInputView() {
+ return getLayoutInflater().inflate(R.layout.view, null);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mReceiver != null) {
+ unregisterReceiver(mReceiver);
+ mReceiver = null;
+ }
+ super.onDestroy();
+ }
+
+ class AdbReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(IME_MESSAGE)) {
+ // normal message
String msg = intent.getStringExtra("msg");
if (msg != null) {
InputConnection ic = getCurrentInputConnection();
if (ic != null)
ic.commitText(msg, 1);
}
+ // meta codes
+ String metaCodes = intent.getStringExtra("mcode"); // Get message.
+ if (metaCodes != null) {
+ String[] mcodes = metaCodes.split(","); // Get mcodes in string.
+ if (mcodes != null) {
+ int i;
+ InputConnection ic = getCurrentInputConnection();
+ for (i = 0; i < mcodes.length - 1; i = i + 2) {
+ if (ic != null) {
+ KeyEvent ke;
+ if (mcodes[i].contains("+")) { // Check metaState if more than one. Use '+' as delimiter
+ String[] arrCode = mcodes[i].split("\\+"); // Get metaState if more than one.
+ ke = new KeyEvent(
+ 0,
+ 0,
+ KeyEvent.ACTION_DOWN, // Action code.
+ Integer.parseInt(mcodes[i + 1].toString()), // Key code.
+ 0, // Repeat. // -1
+ Integer.parseInt(arrCode[0].toString()) | Integer.parseInt(arrCode[1].toString()), // Flag
+ 0, // The device ID that generated the key event.
+ 0, // Raw device scan code of the event.
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE, // The flags for this key event.
+ InputDevice.SOURCE_KEYBOARD // The input source such as SOURCE_KEYBOARD.
+ );
+ } else { // Only one metaState.
+ ke = new KeyEvent(
+ 0,
+ 0,
+ KeyEvent.ACTION_DOWN, // Action code.
+ Integer.parseInt(mcodes[i + 1].toString()), // Key code.
+ 0, // Repeat.
+ Integer.parseInt(mcodes[i].toString()), // Flag
+ 0, // The device ID that generated the key event.
+ 0, // Raw device scan code of the event.
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE, // The flags for this key event.
+ InputDevice.SOURCE_KEYBOARD // The input source such as SOURCE_KEYBOARD.
+ );
+ }
+ ic.sendKeyEvent(ke);
+ }
+ }
+ }
+ }
}
if (intent.getAction().equals(IME_MESSAGE_B64)) {
@@ -73,32 +150,90 @@ public void onReceive(Context context, Intent intent) {
}
if (intent.getAction().equals(IME_CHARS)) {
- int[] chars = intent.getIntArrayExtra("chars");
- if (chars != null) {
+ int[] chars = intent.getIntArrayExtra("chars");
+ if (chars != null) {
String msg = new String(chars, 0, chars.length);
InputConnection ic = getCurrentInputConnection();
if (ic != null)
ic.commitText(msg, 1);
}
}
-
- if (intent.getAction().equals(IME_KEYCODE)) {
- int code = intent.getIntExtra("code", -1);
+
+ if (intent.getAction().equals(IME_KEYCODE)) {
+ int code = intent.getIntExtra("code", -1);
if (code != -1) {
InputConnection ic = getCurrentInputConnection();
if (ic != null)
ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, code));
}
}
-
- if (intent.getAction().equals(IME_EDITORCODE)) {
- int code = intent.getIntExtra("code", -1);
+
+ if (intent.getAction().equals(IME_EDITORCODE)) {
+ int code = intent.getIntExtra("code", -1);
if (code != -1) {
InputConnection ic = getCurrentInputConnection();
if (ic != null)
ic.performEditorAction(code);
}
}
+
+ if (intent.getAction().equals(IME_CLEAR_TEXT)) {
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ // Try to get extracted text first
+ ExtractedTextRequest req = new ExtractedTextRequest();
+ req.hintMaxChars = 100000;
+ req.hintMaxLines = 10000;
+ android.view.inputmethod.ExtractedText et = ic.getExtractedText(req, 0);
+ if (et != null && et.text != null) {
+ CharSequence beforePos = ic.getTextBeforeCursor(et.text.length(), 0);
+ CharSequence afterPos = ic.getTextAfterCursor(et.text.length(), 0);
+ if (beforePos != null && afterPos != null) {
+ ic.deleteSurroundingText(beforePos.length(), afterPos.length());
+ }
+ } else {
+ // Fallback: select all and delete
+ ic.performContextMenuAction(android.R.id.selectAll);
+ ic.commitText("", 1);
+ }
+ }
+ }
+
+ // IME Actions - convenient shortcuts
+ if (intent.getAction().equals(IME_ACTION_SEARCH)) {
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ ic.performEditorAction(EditorInfo.IME_ACTION_SEARCH);
+ }
+ }
+
+ if (intent.getAction().equals(IME_ACTION_GO)) {
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ ic.performEditorAction(EditorInfo.IME_ACTION_GO);
+ }
+ }
+
+ if (intent.getAction().equals(IME_ACTION_DONE)) {
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ ic.performEditorAction(EditorInfo.IME_ACTION_DONE);
+ }
+ }
+
+ if (intent.getAction().equals(IME_ACTION_NEXT)) {
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ ic.performEditorAction(EditorInfo.IME_ACTION_NEXT);
+ }
+ }
+
+ if (intent.getAction().equals(IME_ACTION_SEND)) {
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ ic.performEditorAction(EditorInfo.IME_ACTION_SEND);
+ }
+ }
}
- }
+ }
}