Java tutorial
/* * Copyright 2017 Holger Kaelberer <holger.k@elberer.de> * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.content.ContextCompat; import android.support.v4.util.Pair; import android.util.Log; import android.util.SparseIntArray; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import org.kde.kdeconnect.NetworkPackage; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.concurrent.locks.ReentrantLock; public class RemoteKeyboardPlugin extends Plugin { public final static String PACKAGE_TYPE_MOUSEPAD_REQUEST = "kdeconnect.mousepad.request"; public final static String PACKAGE_TYPE_MOUSEPAD_ECHO = "kdeconnect.mousepad.echo"; public final static String PACKAGE_TYPE_MOUSEPAD_KEYBOARDSTATE = "kdeconnect.mousepad.keyboardstate"; /** * Track and expose plugin instances to allow for a 'connected'-indicator in the IME: */ private static ArrayList<RemoteKeyboardPlugin> instances = new ArrayList<RemoteKeyboardPlugin>(); private static ReentrantLock instancesLock = new ReentrantLock(true); public static ArrayList<RemoteKeyboardPlugin> getInstances() { return instances; } public static ArrayList<RemoteKeyboardPlugin> acquireInstances() { instancesLock.lock(); return getInstances(); } public static ArrayList<RemoteKeyboardPlugin> releaseInstances() { instancesLock.unlock(); return getInstances(); } public static boolean isConnected() { return instances.size() > 0; } private static SparseIntArray specialKeyMap = new SparseIntArray(); static { int i = 0; specialKeyMap.put(++i, KeyEvent.KEYCODE_DEL); // 1 specialKeyMap.put(++i, KeyEvent.KEYCODE_TAB); // 2 ++i; //specialKeyMap.put(++i, KeyEvent.KEYCODE_ENTER, 12); // 3 is not used specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_LEFT); // 4 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_UP); // 5 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_RIGHT); // 6 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_DOWN); // 7 specialKeyMap.put(++i, KeyEvent.KEYCODE_PAGE_UP); // 8 specialKeyMap.put(++i, KeyEvent.KEYCODE_PAGE_DOWN); // 9 if (Build.VERSION.SDK_INT >= 11) { specialKeyMap.put(++i, KeyEvent.KEYCODE_MOVE_HOME); // 10 specialKeyMap.put(++i, KeyEvent.KEYCODE_MOVE_END); // 11 specialKeyMap.put(++i, KeyEvent.KEYCODE_ENTER); // 12 specialKeyMap.put(++i, KeyEvent.KEYCODE_FORWARD_DEL); // 13 specialKeyMap.put(++i, KeyEvent.KEYCODE_ESCAPE); // 14 specialKeyMap.put(++i, KeyEvent.KEYCODE_SYSRQ); // 15 specialKeyMap.put(++i, KeyEvent.KEYCODE_SCROLL_LOCK); // 16 ++i; // 17 ++i; // 18 ++i; // 19 ++i; // 20 specialKeyMap.put(++i, KeyEvent.KEYCODE_F1); // 21 specialKeyMap.put(++i, KeyEvent.KEYCODE_F2); // 22 specialKeyMap.put(++i, KeyEvent.KEYCODE_F3); // 23 specialKeyMap.put(++i, KeyEvent.KEYCODE_F4); // 24 specialKeyMap.put(++i, KeyEvent.KEYCODE_F5); // 25 specialKeyMap.put(++i, KeyEvent.KEYCODE_F6); // 26 specialKeyMap.put(++i, KeyEvent.KEYCODE_F7); // 27 specialKeyMap.put(++i, KeyEvent.KEYCODE_F8); // 28 specialKeyMap.put(++i, KeyEvent.KEYCODE_F9); // 29 specialKeyMap.put(++i, KeyEvent.KEYCODE_F10); // 30 specialKeyMap.put(++i, KeyEvent.KEYCODE_F11); // 31 specialKeyMap.put(++i, KeyEvent.KEYCODE_F12); // 21 } } @Override public boolean onCreate() { Log.d("RemoteKeyboardPlugin", "Creating for device " + device.getName()); acquireInstances(); try { instances.add(this); } finally { releaseInstances(); } if (RemoteKeyboardService.instance != null) RemoteKeyboardService.instance.handler.post(new Runnable() { @Override public void run() { RemoteKeyboardService.instance.updateInputView(); } }); return true; } @Override public void onDestroy() { acquireInstances(); try { if (instances.contains(this)) { instances.remove(this); if (instances.size() < 1 && RemoteKeyboardService.instance != null) RemoteKeyboardService.instance.handler.post(new Runnable() { @Override public void run() { RemoteKeyboardService.instance.updateInputView(); } }); } } finally { releaseInstances(); } Log.d("RemoteKeyboardPlugin", "Destroying for device " + device.getName()); } @Override public String getDisplayName() { return context.getString(R.string.pref_plugin_remotekeyboard); } @Override public String getDescription() { return context.getString(R.string.pref_plugin_remotekeyboard_desc); } @Override public Drawable getIcon() { return ContextCompat.getDrawable(context, R.drawable.ic_action_keyboard); } @Override public boolean hasSettings() { return true; } @Override public boolean hasMainActivity() { return false; } @Override public String[] getSupportedPackageTypes() { return new String[] { PACKAGE_TYPE_MOUSEPAD_REQUEST }; } @Override public String[] getOutgoingPackageTypes() { return new String[] { PACKAGE_TYPE_MOUSEPAD_ECHO, PACKAGE_TYPE_MOUSEPAD_KEYBOARDSTATE }; } private boolean isValidSpecialKey(int key) { return (specialKeyMap.get(key, 0) > 0); } private int getCharPos(ExtractedText extractedText, char ch, boolean forward) { int pos = -1; if (extractedText != null) { if (!forward) // backward pos = extractedText.text.toString().lastIndexOf(" ", extractedText.selectionEnd - 2); else pos = extractedText.text.toString().indexOf(" ", extractedText.selectionEnd + 1); return pos; } return pos; } private int currentTextLength(ExtractedText extractedText) { if (extractedText != null) return extractedText.text.length(); return -1; } private int currentCursorPos(ExtractedText extractedText) { if (extractedText != null) return extractedText.selectionEnd; return -1; } private Pair<Integer, Integer> currentSelection(ExtractedText extractedText) { if (extractedText != null) return new Pair<>(extractedText.selectionStart, extractedText.selectionEnd); return new Pair<>(-1, -1); } private boolean handleSpecialKey(int key, boolean shift, boolean ctrl, boolean alt) { int keyEvent = specialKeyMap.get(key, 0); if (keyEvent == 0) return false; InputConnection inputConn = RemoteKeyboardService.instance.getCurrentInputConnection(); // Log.d("RemoteKeyboardPlugin", "Handling special key " + key + " translated to " + keyEvent + " shift=" + shift + " ctrl=" + ctrl + " alt=" + alt); // special sequences: if (ctrl && (keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT)) { // Ctrl + right -> next word ExtractedText extractedText = inputConn.getExtractedText(new ExtractedTextRequest(), 0); int pos = getCharPos(extractedText, ' ', keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT); if (pos == -1) pos = currentTextLength(extractedText); else pos++; int startPos = pos; int endPos = pos; if (shift) { // Shift -> select word (otherwise jump) Pair<Integer, Integer> sel = currentSelection(extractedText); int cursor = currentCursorPos(extractedText); // Log.d("RemoteKeyboardPlugin", "Selection (to right): " + sel.first + " / " + sel.second + " cursor: " + cursor); startPos = cursor; if (sel.first < cursor || // active selection from left to right -> grow sel.first > sel.second) // active selection from right to left -> shrink startPos = sel.first; } inputConn.setSelection(startPos, endPos); } else if (ctrl && keyEvent == KeyEvent.KEYCODE_DPAD_LEFT) { // Ctrl + left -> previous word ExtractedText extractedText = inputConn.getExtractedText(new ExtractedTextRequest(), 0); int pos = getCharPos(extractedText, ' ', keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT); if (pos == -1) pos = 0; else pos++; int startPos = pos; int endPos = pos; if (shift) { Pair<Integer, Integer> sel = currentSelection(extractedText); int cursor = currentCursorPos(extractedText); // Log.d("RemoteKeyboardPlugin", "Selection (to left): " + sel.first + " / " + sel.second + " cursor: " + cursor); startPos = cursor; if (cursor < sel.first || // active selection from right to left -> grow sel.first < sel.second) // active selection from right to left -> shrink startPos = sel.first; } inputConn.setSelection(startPos, endPos); } else if (shift && (keyEvent == KeyEvent.KEYCODE_DPAD_LEFT || keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT || keyEvent == KeyEvent.KEYCODE_DPAD_UP || keyEvent == KeyEvent.KEYCODE_DPAD_DOWN || keyEvent == KeyEvent.KEYCODE_MOVE_HOME || keyEvent == KeyEvent.KEYCODE_MOVE_END)) { // Shift + up/down/left/right/home/end long now = SystemClock.uptimeMillis(); inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0)); inputConn.sendKeyEvent( new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyEvent, 0, KeyEvent.META_SHIFT_LEFT_ON)); inputConn.sendKeyEvent( new KeyEvent(now, now, KeyEvent.ACTION_UP, keyEvent, 0, KeyEvent.META_SHIFT_LEFT_ON)); inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0)); } else if (keyEvent == KeyEvent.KEYCODE_NUMPAD_ENTER || keyEvent == KeyEvent.KEYCODE_ENTER) { // Enter key EditorInfo editorInfo = RemoteKeyboardService.instance.getCurrentInputEditorInfo(); // Log.d("RemoteKeyboardPlugin", "Enter: " + editorInfo.imeOptions); if (editorInfo != null && (((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0) || ctrl)) { // Ctrl+Return overrides IME_FLAG_NO_ENTER_ACTION (FIXME: make configurable?) // check for special DONE/GO/etc actions first: int[] actions = { EditorInfo.IME_ACTION_GO, EditorInfo.IME_ACTION_NEXT, EditorInfo.IME_ACTION_SEND, EditorInfo.IME_ACTION_SEARCH, EditorInfo.IME_ACTION_DONE }; // note: DONE should be last or we might hide the ime instead of "go" for (int i = 0; i < actions.length; i++) { if ((editorInfo.imeOptions & actions[i]) == actions[i]) { // Log.d("RemoteKeyboardPlugin", "Enter-action: " + actions[i]); inputConn.performEditorAction(actions[i]); return true; } } } else { // else: fall back to regular Enter-event: // Log.d("RemoteKeyboardPlugin", "Enter: normal keypress"); inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyEvent)); inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyEvent)); } } else { // default handling: inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyEvent)); inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyEvent)); } return true; } private boolean handleVisibleKey(String key, boolean shift, boolean ctrl, boolean alt) { // Log.d("RemoteKeyboardPlugin", "Handling visible key " + key + " shift=" + shift + " ctrl=" + ctrl + " alt=" + alt + " " + key.equalsIgnoreCase("c") + " " + key.length()); if (key.isEmpty()) return false; InputConnection inputConn = RemoteKeyboardService.instance.getCurrentInputConnection(); if (inputConn == null) return false; // ctrl+c/v/x if (key.equalsIgnoreCase("c") && ctrl) { return inputConn.performContextMenuAction(android.R.id.copy); } else if (key.equalsIgnoreCase("v") && ctrl) return inputConn.performContextMenuAction(android.R.id.paste); else if (key.equalsIgnoreCase("x") && ctrl) return inputConn.performContextMenuAction(android.R.id.cut); else if (key.equalsIgnoreCase("a") && ctrl) return inputConn.performContextMenuAction(android.R.id.selectAll); // Log.d("RemoteKeyboardPlugin", "Committing visible key '" + key + "'"); inputConn.commitText(key, key.length()); return true; } private boolean handleEvent(NetworkPackage np) { if (np.has("specialKey") && isValidSpecialKey(np.getInt("specialKey"))) return handleSpecialKey(np.getInt("specialKey"), np.getBoolean("shift"), np.getBoolean("ctrl"), np.getBoolean("alt")); // try visible key return handleVisibleKey(np.getString("key"), np.getBoolean("shift"), np.getBoolean("ctrl"), np.getBoolean("alt")); } @Override public boolean onPackageReceived(NetworkPackage np) { if (!np.getType().equals(PACKAGE_TYPE_MOUSEPAD_REQUEST) || (!np.has("key") && !np.has("specialKey"))) { // expect at least key OR specialKey Log.e("RemoteKeyboardPlugin", "Invalid package for remotekeyboard plugin!"); return false; } if (RemoteKeyboardService.instance == null) { Log.i("RemoteKeyboardPlugin", "Remote keyboard is not the currently selected input method, dropping key"); return false; } if (!RemoteKeyboardService.instance.visible && PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.remotekeyboard_editing_only), true)) { Log.i("RemoteKeyboardPlugin", "Remote keyboard is currently not visible, dropping key"); return false; } if (!handleEvent(np)) { Log.i("RemoteKeyboardPlugin", "Could not handle event!"); return false; } if (np.getBoolean("sendAck")) { NetworkPackage reply = new NetworkPackage(PACKAGE_TYPE_MOUSEPAD_ECHO); reply.set("key", np.getString("key")); if (np.has("specialKey")) reply.set("specialKey", np.getInt("specialKey")); if (np.has("shift")) reply.set("shift", np.getBoolean("shift")); if (np.has("ctrl")) reply.set("ctrl", np.getBoolean("ctrl")); if (np.has("alt")) reply.set("alt", np.getBoolean("alt")); reply.set("isAck", true); device.sendPackage(reply); } return true; } public void notifyKeyboardState(boolean state) { Log.d("RemoteKeyboardPlugin", "Keyboardstate changed to " + state); NetworkPackage np = new NetworkPackage(PACKAGE_TYPE_MOUSEPAD_KEYBOARDSTATE); np.set("state", state); device.sendPackage(np); } }