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); + } + } } - } + } }