File indexing completed on 2024-12-22 04:41:40
0001 /* 0002 * SPDX-FileCopyrightText: 2017 Holger Kaelberer <holger.k@elberer.de> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 package org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin; 0008 0009 import android.app.Activity; 0010 import android.content.SharedPreferences; 0011 import android.os.SystemClock; 0012 import android.preference.PreferenceManager; 0013 import android.provider.Settings; 0014 import android.util.Log; 0015 import android.util.SparseIntArray; 0016 import android.view.KeyEvent; 0017 import android.view.inputmethod.EditorInfo; 0018 import android.view.inputmethod.ExtractedText; 0019 import android.view.inputmethod.ExtractedTextRequest; 0020 import android.view.inputmethod.InputConnection; 0021 0022 import androidx.annotation.DrawableRes; 0023 import androidx.annotation.NonNull; 0024 import androidx.core.util.Pair; 0025 import androidx.fragment.app.DialogFragment; 0026 0027 import org.kde.kdeconnect.NetworkPacket; 0028 import org.kde.kdeconnect.Plugins.Plugin; 0029 import org.kde.kdeconnect.Plugins.PluginFactory; 0030 import org.kde.kdeconnect.UserInterface.MainActivity; 0031 import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; 0032 import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment; 0033 import org.kde.kdeconnect_tp.R; 0034 0035 import java.util.ArrayList; 0036 import java.util.concurrent.locks.ReentrantLock; 0037 0038 @PluginFactory.LoadablePlugin 0039 public class RemoteKeyboardPlugin extends Plugin implements SharedPreferences.OnSharedPreferenceChangeListener { 0040 0041 private final static String PACKET_TYPE_MOUSEPAD_REQUEST = "kdeconnect.mousepad.request"; 0042 private final static String PACKET_TYPE_MOUSEPAD_ECHO = "kdeconnect.mousepad.echo"; 0043 private final static String PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE = "kdeconnect.mousepad.keyboardstate"; 0044 0045 /** 0046 * Track and expose plugin instances to allow for a 'connected'-indicator in the IME: 0047 */ 0048 private static final ArrayList<RemoteKeyboardPlugin> instances = new ArrayList<>(); 0049 private static final ReentrantLock instancesLock = new ReentrantLock(true); 0050 0051 private static ArrayList<RemoteKeyboardPlugin> getInstances() { 0052 return instances; 0053 } 0054 0055 public static ArrayList<RemoteKeyboardPlugin> acquireInstances() { 0056 instancesLock.lock(); 0057 return getInstances(); 0058 } 0059 0060 public static ArrayList<RemoteKeyboardPlugin> releaseInstances() { 0061 instancesLock.unlock(); 0062 return getInstances(); 0063 } 0064 0065 public static boolean isConnected() { 0066 return instances.size() > 0; 0067 } 0068 0069 private static final SparseIntArray specialKeyMap = new SparseIntArray(); 0070 0071 static { 0072 int i = 0; 0073 specialKeyMap.put(++i, KeyEvent.KEYCODE_DEL); // 1 0074 specialKeyMap.put(++i, KeyEvent.KEYCODE_TAB); // 2 0075 ++i; //specialKeyMap.put(++i, KeyEvent.KEYCODE_ENTER, 12); // 3 is not used 0076 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_LEFT); // 4 0077 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_UP); // 5 0078 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_RIGHT); // 6 0079 specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_DOWN); // 7 0080 specialKeyMap.put(++i, KeyEvent.KEYCODE_PAGE_UP); // 8 0081 specialKeyMap.put(++i, KeyEvent.KEYCODE_PAGE_DOWN); // 9 0082 specialKeyMap.put(++i, KeyEvent.KEYCODE_MOVE_HOME); // 10 0083 specialKeyMap.put(++i, KeyEvent.KEYCODE_MOVE_END); // 11 0084 specialKeyMap.put(++i, KeyEvent.KEYCODE_ENTER); // 12 0085 specialKeyMap.put(++i, KeyEvent.KEYCODE_FORWARD_DEL); // 13 0086 specialKeyMap.put(++i, KeyEvent.KEYCODE_ESCAPE); // 14 0087 specialKeyMap.put(++i, KeyEvent.KEYCODE_SYSRQ); // 15 0088 specialKeyMap.put(++i, KeyEvent.KEYCODE_SCROLL_LOCK); // 16 0089 ++i; // 17 0090 ++i; // 18 0091 ++i; // 19 0092 ++i; // 20 0093 specialKeyMap.put(++i, KeyEvent.KEYCODE_F1); // 21 0094 specialKeyMap.put(++i, KeyEvent.KEYCODE_F2); // 22 0095 specialKeyMap.put(++i, KeyEvent.KEYCODE_F3); // 23 0096 specialKeyMap.put(++i, KeyEvent.KEYCODE_F4); // 24 0097 specialKeyMap.put(++i, KeyEvent.KEYCODE_F5); // 25 0098 specialKeyMap.put(++i, KeyEvent.KEYCODE_F6); // 26 0099 specialKeyMap.put(++i, KeyEvent.KEYCODE_F7); // 27 0100 specialKeyMap.put(++i, KeyEvent.KEYCODE_F8); // 28 0101 specialKeyMap.put(++i, KeyEvent.KEYCODE_F9); // 29 0102 specialKeyMap.put(++i, KeyEvent.KEYCODE_F10); // 30 0103 specialKeyMap.put(++i, KeyEvent.KEYCODE_F11); // 31 0104 specialKeyMap.put(++i, KeyEvent.KEYCODE_F12); // 21 0105 } 0106 0107 @Override 0108 public boolean onCreate() { 0109 Log.d("RemoteKeyboardPlugin", "Creating for device " + device.getName()); 0110 acquireInstances(); 0111 try { 0112 instances.add(this); 0113 } finally { 0114 releaseInstances(); 0115 } 0116 if (RemoteKeyboardService.instance != null) 0117 RemoteKeyboardService.instance.handler.post(() -> RemoteKeyboardService.instance.updateInputView()); 0118 0119 PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this); 0120 0121 final boolean editingOnly = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.remotekeyboard_editing_only), true); 0122 final boolean visible = RemoteKeyboardService.instance != null && RemoteKeyboardService.instance.visible; 0123 notifyKeyboardState(!editingOnly || visible); 0124 0125 return true; 0126 } 0127 0128 @Override 0129 public void onDestroy() { 0130 acquireInstances(); 0131 try { 0132 if (instances.contains(this)) { 0133 instances.remove(this); 0134 if (instances.size() < 1 && RemoteKeyboardService.instance != null) 0135 RemoteKeyboardService.instance.handler.post(() -> RemoteKeyboardService.instance.updateInputView()); 0136 } 0137 } finally { 0138 releaseInstances(); 0139 } 0140 0141 Log.d("RemoteKeyboardPlugin", "Destroying for device " + device.getName()); 0142 } 0143 0144 @Override 0145 public @NonNull String getDisplayName() { 0146 return context.getString(R.string.pref_plugin_remotekeyboard); 0147 } 0148 0149 @Override 0150 public @NonNull String getDescription() { 0151 return context.getString(R.string.pref_plugin_remotekeyboard_desc); 0152 } 0153 0154 @Override 0155 public @DrawableRes int getIcon() { 0156 return R.drawable.ic_action_keyboard_24dp; 0157 } 0158 0159 @Override 0160 public boolean hasSettings() { 0161 return true; 0162 } 0163 0164 @Override 0165 public PluginSettingsFragment getSettingsFragment(Activity activity) { 0166 return PluginSettingsFragment.newInstance(getPluginKey(), R.xml.remotekeyboardplugin_preferences); 0167 } 0168 0169 @Override 0170 public @NonNull String[] getSupportedPacketTypes() { 0171 return new String[]{PACKET_TYPE_MOUSEPAD_REQUEST}; 0172 } 0173 0174 @Override 0175 public @NonNull String[] getOutgoingPacketTypes() { 0176 return new String[]{PACKET_TYPE_MOUSEPAD_ECHO, PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE}; 0177 } 0178 0179 private boolean isValidSpecialKey(int key) { 0180 return (specialKeyMap.get(key, 0) > 0); 0181 } 0182 0183 private int getCharPos(ExtractedText extractedText, char ch, boolean forward) { 0184 int pos = -1; 0185 if (extractedText != null) { 0186 if (!forward) // backward 0187 pos = extractedText.text.toString().lastIndexOf(" ", extractedText.selectionEnd - 2); 0188 else 0189 pos = extractedText.text.toString().indexOf(" ", extractedText.selectionEnd + 1); 0190 return pos; 0191 } 0192 return pos; 0193 } 0194 0195 private int currentTextLength(ExtractedText extractedText) { 0196 if (extractedText != null) 0197 return extractedText.text.length(); 0198 return -1; 0199 } 0200 0201 private int currentCursorPos(ExtractedText extractedText) { 0202 if (extractedText != null) 0203 return extractedText.selectionEnd; 0204 return -1; 0205 } 0206 0207 private Pair<Integer, Integer> currentSelection(ExtractedText extractedText) { 0208 if (extractedText != null) 0209 return new Pair<>(extractedText.selectionStart, extractedText.selectionEnd); 0210 return new Pair<>(-1, -1); 0211 } 0212 0213 private boolean handleSpecialKey(int key, boolean shift, boolean ctrl, boolean alt) { 0214 int keyEvent = specialKeyMap.get(key, 0); 0215 if (keyEvent == 0) 0216 return false; 0217 InputConnection inputConn = RemoteKeyboardService.instance.getCurrentInputConnection(); 0218 // Log.d("RemoteKeyboardPlugin", "Handling special key " + key + " translated to " + keyEvent + " shift=" + shift + " ctrl=" + ctrl + " alt=" + alt); 0219 0220 // special sequences: 0221 if (ctrl && (keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT)) { 0222 // Ctrl + right -> next word 0223 ExtractedText extractedText = inputConn.getExtractedText(new ExtractedTextRequest(), 0); 0224 int pos = getCharPos(extractedText, ' ', keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT); 0225 if (pos == -1) 0226 pos = currentTextLength(extractedText); 0227 else 0228 pos++; 0229 int startPos = pos; 0230 int endPos = pos; 0231 if (shift) { // Shift -> select word (otherwise jump) 0232 Pair<Integer, Integer> sel = currentSelection(extractedText); 0233 int cursor = currentCursorPos(extractedText); 0234 // Log.d("RemoteKeyboardPlugin", "Selection (to right): " + sel.first + " / " + sel.second + " cursor: " + cursor); 0235 startPos = cursor; 0236 if (sel.first < cursor || // active selection from left to right -> grow 0237 sel.first > sel.second) // active selection from right to left -> shrink 0238 startPos = sel.first; 0239 } 0240 inputConn.setSelection(startPos, endPos); 0241 } else if (ctrl && keyEvent == KeyEvent.KEYCODE_DPAD_LEFT) { 0242 // Ctrl + left -> previous word 0243 ExtractedText extractedText = inputConn.getExtractedText(new ExtractedTextRequest(), 0); 0244 int pos = getCharPos(extractedText, ' ', keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT); 0245 if (pos == -1) 0246 pos = 0; 0247 else 0248 pos++; 0249 int startPos = pos; 0250 int endPos = pos; 0251 if (shift) { 0252 Pair<Integer, Integer> sel = currentSelection(extractedText); 0253 int cursor = currentCursorPos(extractedText); 0254 // Log.d("RemoteKeyboardPlugin", "Selection (to left): " + sel.first + " / " + sel.second + " cursor: " + cursor); 0255 startPos = cursor; 0256 if (cursor < sel.first || // active selection from right to left -> grow 0257 sel.first < sel.second) // active selection from right to left -> shrink 0258 startPos = sel.first; 0259 } 0260 inputConn.setSelection(startPos, endPos); 0261 } else if (shift 0262 && (keyEvent == KeyEvent.KEYCODE_DPAD_LEFT 0263 || keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT 0264 || keyEvent == KeyEvent.KEYCODE_DPAD_UP 0265 || keyEvent == KeyEvent.KEYCODE_DPAD_DOWN 0266 || keyEvent == KeyEvent.KEYCODE_MOVE_HOME 0267 || keyEvent == KeyEvent.KEYCODE_MOVE_END)) { 0268 // Shift + up/down/left/right/home/end 0269 long now = SystemClock.uptimeMillis(); 0270 inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0)); 0271 inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyEvent, 0, KeyEvent.META_SHIFT_LEFT_ON)); 0272 inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyEvent, 0, KeyEvent.META_SHIFT_LEFT_ON)); 0273 inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0)); 0274 } else if (keyEvent == KeyEvent.KEYCODE_NUMPAD_ENTER 0275 || keyEvent == KeyEvent.KEYCODE_ENTER) { 0276 // Enter key 0277 EditorInfo editorInfo = RemoteKeyboardService.instance.getCurrentInputEditorInfo(); 0278 // Log.d("RemoteKeyboardPlugin", "Enter: " + editorInfo.imeOptions); 0279 if (editorInfo != null 0280 && (((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0) 0281 || ctrl)) { // Ctrl+Return overrides IME_FLAG_NO_ENTER_ACTION (FIXME: make configurable?) 0282 // check for special DONE/GO/etc actions first: 0283 int[] actions = {EditorInfo.IME_ACTION_GO, EditorInfo.IME_ACTION_NEXT, 0284 EditorInfo.IME_ACTION_SEND, EditorInfo.IME_ACTION_SEARCH, 0285 EditorInfo.IME_ACTION_DONE}; // note: DONE should be last or we might hide the ime instead of "go" 0286 for (int action : actions) { 0287 if ((editorInfo.imeOptions & action) == action) { 0288 // Log.d("RemoteKeyboardPlugin", "Enter-action: " + actions[i]); 0289 inputConn.performEditorAction(action); 0290 return true; 0291 } 0292 } 0293 } else { 0294 // else: fall back to regular Enter-event: 0295 // Log.d("RemoteKeyboardPlugin", "Enter: normal keypress"); 0296 inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyEvent)); 0297 inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyEvent)); 0298 } 0299 } else { 0300 // default handling: 0301 inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyEvent)); 0302 inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyEvent)); 0303 } 0304 0305 return true; 0306 } 0307 0308 private boolean handleVisibleKey(String key, boolean shift, boolean ctrl, boolean alt) { 0309 // Log.d("RemoteKeyboardPlugin", "Handling visible key " + key + " shift=" + shift + " ctrl=" + ctrl + " alt=" + alt + " " + key.equalsIgnoreCase("c") + " " + key.length()); 0310 0311 if (key.isEmpty()) 0312 return false; 0313 0314 InputConnection inputConn = RemoteKeyboardService.instance.getCurrentInputConnection(); 0315 if (inputConn == null) 0316 return false; 0317 0318 // ctrl+c/v/x 0319 if (key.equalsIgnoreCase("c") && ctrl) { 0320 return inputConn.performContextMenuAction(android.R.id.copy); 0321 } else if (key.equalsIgnoreCase("v") && ctrl) 0322 return inputConn.performContextMenuAction(android.R.id.paste); 0323 else if (key.equalsIgnoreCase("x") && ctrl) 0324 return inputConn.performContextMenuAction(android.R.id.cut); 0325 else if (key.equalsIgnoreCase("a") && ctrl) 0326 return inputConn.performContextMenuAction(android.R.id.selectAll); 0327 0328 // Log.d("RemoteKeyboardPlugin", "Committing visible key '" + key + "'"); 0329 inputConn.commitText(key, key.length()); 0330 return true; 0331 } 0332 0333 private boolean handleEvent(NetworkPacket np) { 0334 if (np.has("specialKey") && isValidSpecialKey(np.getInt("specialKey"))) 0335 return handleSpecialKey(np.getInt("specialKey"), np.getBoolean("shift"), 0336 np.getBoolean("ctrl"), np.getBoolean("alt")); 0337 0338 // try visible key 0339 return handleVisibleKey(np.getString("key"), np.getBoolean("shift"), 0340 np.getBoolean("ctrl"), np.getBoolean("alt")); 0341 } 0342 0343 0344 public enum MousePadPacketType { 0345 Keyboard, 0346 Mouse, 0347 }; 0348 0349 public static MousePadPacketType getMousePadPacketType(NetworkPacket np) { 0350 if (np.has("key") || np.has("specialKey")) { 0351 return MousePadPacketType.Keyboard; 0352 } else { 0353 return MousePadPacketType.Mouse; 0354 } 0355 } 0356 0357 @Override 0358 public boolean onPacketReceived(@NonNull NetworkPacket np) { 0359 0360 if (!np.getType().equals(PACKET_TYPE_MOUSEPAD_REQUEST)) { 0361 Log.e("RemoteKeyboardPlugin", "Invalid packet type for RemoteKeyboardPlugin: "+np.getType()); 0362 return false; 0363 } 0364 0365 if (getMousePadPacketType(np) != MousePadPacketType.Keyboard) { 0366 return false; // This packet will be handled by the MouseReceiverPlugin instead, silently ignore 0367 } 0368 0369 if (RemoteKeyboardService.instance == null) { 0370 Log.i("RemoteKeyboardPlugin", "Remote keyboard is not the currently selected input method, dropping key"); 0371 return false; 0372 } 0373 0374 if (!RemoteKeyboardService.instance.visible && 0375 PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.remotekeyboard_editing_only), true)) { 0376 Log.i("RemoteKeyboardPlugin", "Remote keyboard is currently not visible, dropping key"); 0377 return false; 0378 } 0379 0380 if (!handleEvent(np)) { 0381 Log.i("RemoteKeyboardPlugin", "Could not handle event!"); 0382 return false; 0383 } 0384 0385 if (np.getBoolean("sendAck")) { 0386 NetworkPacket reply = new NetworkPacket(PACKET_TYPE_MOUSEPAD_ECHO); 0387 reply.set("key", np.getString("key")); 0388 if (np.has("specialKey")) 0389 reply.set("specialKey", np.getInt("specialKey")); 0390 if (np.has("shift")) 0391 reply.set("shift", np.getBoolean("shift")); 0392 if (np.has("ctrl")) 0393 reply.set("ctrl", np.getBoolean("ctrl")); 0394 if (np.has("alt")) 0395 reply.set("alt", np.getBoolean("alt")); 0396 reply.set("isAck", true); 0397 device.sendPacket(reply); 0398 } 0399 0400 return true; 0401 } 0402 0403 public void notifyKeyboardState(boolean state) { 0404 Log.d("RemoteKeyboardPlugin", "Keyboardstate changed to " + state); 0405 NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_KEYBOARDSTATE); 0406 np.set("state", state); 0407 device.sendPacket(np); 0408 } 0409 0410 String getDeviceId() { 0411 return device.getDeviceId(); 0412 } 0413 0414 @Override 0415 public boolean checkRequiredPermissions() { 0416 return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_INPUT_METHODS).contains("org.kde.kdeconnect_tp"); 0417 } 0418 0419 @Override 0420 public @NonNull DialogFragment getPermissionExplanationDialog() { 0421 return new StartActivityAlertDialogFragment.Builder() 0422 .setTitle(R.string.pref_plugin_remotekeyboard) 0423 .setMessage(R.string.no_permissions_remotekeyboard) 0424 .setPositiveButton(R.string.open_settings) 0425 .setNegativeButton(R.string.cancel) 0426 .setIntentAction(Settings.ACTION_INPUT_METHOD_SETTINGS) 0427 .setStartForResult(true) 0428 .setRequestCode(MainActivity.RESULT_NEEDS_RELOAD) 0429 .create(); 0430 } 0431 0432 @Override 0433 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 0434 if (key.equals(context.getString(R.string.remotekeyboard_editing_only))) { 0435 final boolean editingOnly = sharedPreferences.getBoolean(context.getString(R.string.remotekeyboard_editing_only), true); 0436 final boolean visible = RemoteKeyboardService.instance != null && RemoteKeyboardService.instance.visible; 0437 notifyKeyboardState(!editingOnly || visible); 0438 } 0439 } 0440 }