FreeRDP
Loading...
Searching...
No Matches
SessionInputManager.java
1/*
2 Android Session Input Manager
3
4 Copyright 2026 Ibrahim Sevinc <ibrahim.sevinc.mail@gmail.com>
5
6 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
7 If a copy of the MPL was not distributed with this file, You can obtain one at
8 http://mozilla.org/MPL/2.0/.
9*/
10
11package com.freerdp.freerdpcore.presentation;
12
13import android.content.Context;
14import android.graphics.Bitmap;
15import android.graphics.Point;
16import android.inputmethodservice.Keyboard;
17import android.inputmethodservice.KeyboardView;
18import android.os.Handler;
19import android.os.Looper;
20import android.os.Message;
21import android.util.Log;
22import android.view.KeyEvent;
23import android.view.MotionEvent;
24import android.view.ScaleGestureDetector;
25import android.view.View;
26import android.view.inputmethod.InputMethodManager;
27
28import com.freerdp.freerdpcore.R;
29import com.freerdp.freerdpcore.services.LibFreeRDP;
30import com.freerdp.freerdpcore.utils.KeyboardMapper;
31import com.freerdp.freerdpcore.utils.Mouse;
32
33import java.util.List;
34
36 implements SessionView.SessionViewListener, TouchPointerView.TouchPointerListener,
37 KeyboardMapper.KeyProcessingListener, KeyboardView.OnKeyboardActionListener
38{
39 private static final String TAG = "FreeRDP.SessionInputManager";
40
41 private static final int SCROLLING_TIMEOUT = 16;
42 private static final int SCROLLING_DISTANCE = 12;
43 private static final int SCROLLING_EDGE_MARGIN = 16;
44 private static final int MAX_DISCARDED_MOVE_EVENTS = 3;
45 private static final int SEND_MOVE_EVENT_TIMEOUT = 150;
46
47 private static final int MSG_SEND_MOVE_EVENT = 1;
48 private static final int MSG_SCROLLING_REQUESTED = 2;
49
50 private final Context context;
51 private final KeyboardMapper keyboardMapper;
52 private final ScrollView2D scrollView;
53 private final SessionView sessionView;
54 private final TouchPointerView touchPointerView;
55 private final KeyboardView keyboardView;
56 private final KeyboardView modifiersKeyboardView;
57 private final PinchZoomListener pinchZoomListener = new PinchZoomListener();
58
59 private Keyboard modifiersKeyboard;
60 private Keyboard specialkeysKeyboard;
61 private Keyboard numpadKeyboard;
62 private Keyboard cursorKeyboard;
63
64 private int safeInsetLeft = 0;
65 private int safeInsetTop = 0;
66
67 public void setSafeInsets(int left, int top)
68 {
69 safeInsetLeft = left;
70 safeInsetTop = top;
71 }
72
73 // Native FreeRDP instance handle. 0 until attachSession() is called (i.e. before connect).
74 private long instance = 0;
75 private Bitmap bitmap;
76 private int screenWidth;
77 private int screenHeight;
78 private int discardedMoveEvents = 0;
79
80 // keyboard visibility flags
81 private boolean sysKeyboardVisible = false;
82 private boolean extKeyboardVisible = false;
83
84 private final Handler handler;
85
86 public SessionInputManager(Context context, ScrollView2D scrollView, SessionView sessionView,
87 TouchPointerView touchPointerView, KeyboardView keyboardView,
88 KeyboardView modifiersKeyboardView)
89 {
90 this.context = context;
91 this.scrollView = scrollView;
92 this.sessionView = sessionView;
93 this.touchPointerView = touchPointerView;
94 this.keyboardView = keyboardView;
95 this.modifiersKeyboardView = modifiersKeyboardView;
96 this.handler = new InputHandler();
97
98 this.keyboardMapper = new KeyboardMapper();
99 this.keyboardMapper.init(context);
100
101 loadKeyboards();
102 keyboardView.setKeyboard(specialkeysKeyboard);
103 modifiersKeyboardView.setKeyboard(modifiersKeyboard);
104
105 keyboardView.setOnKeyboardActionListener(this);
106 modifiersKeyboardView.setOnKeyboardActionListener(this);
107 }
108
109 private void loadKeyboards()
110 {
111 Context app = context.getApplicationContext();
112 modifiersKeyboard = new Keyboard(app, R.xml.modifiers_keyboard);
113 specialkeysKeyboard = new Keyboard(app, R.xml.specialkeys_keyboard);
114 numpadKeyboard = new Keyboard(app, R.xml.numpad_keyboard);
115 cursorKeyboard = new Keyboard(app, R.xml.cursor_keyboard);
116 }
117
118 // Binds this manager to a live FreeRDP session. Until called, all input events are dropped.
119 public void attachSession(long instance, Bitmap surface)
120 {
121 this.instance = instance;
122 this.bitmap = surface;
123 keyboardMapper.reset(this);
124 }
125
126 // Called when the session bitmap is created or replaced (OnSettingsChanged / OnGraphicsResize).
127 public void setBitmap(Bitmap bitmap)
128 {
129 this.bitmap = bitmap;
130 }
131
132 // Returns a listener that can be wired into a ScaleGestureDetector for pinch-to-zoom.
133 public ScaleGestureDetector.OnScaleGestureListener getPinchZoomListener()
134 {
135 return pinchZoomListener;
136 }
137
138 // Called once the screen dimensions are known (onGlobalLayout) and on bindSession.
139 public void setScreenSize(int width, int height)
140 {
141 this.screenWidth = width;
142 this.screenHeight = height;
143 }
144
145 // Called from onConfigurationChanged when keyboard resources need to be reloaded
146 // (e.g. after orientation change).
147 public void reloadKeyboards()
148 {
149 loadKeyboards();
150 keyboardView.setKeyboard(specialkeysKeyboard);
151 modifiersKeyboardView.setKeyboard(modifiersKeyboard);
152 }
153
154 // Toggles the system soft-keyboard (and accompanying modifiers row).
155 public void toggleSystemKeyboard()
156 {
157 showKeyboard(!sysKeyboardVisible, false);
158 }
159
160 // Toggles the extended (special keys / function / numpad / cursor) keyboard.
161 public void toggleExtendedKeyboard()
162 {
163 showKeyboard(false, !extKeyboardVisible);
164 }
165
166 // Hides any visible keyboards (called from onPause and back-press handling).
167 public void hideKeyboards()
168 {
169 showKeyboard(false, false);
170 }
171
172 // True if either the system or extended keyboard is currently shown.
173 public boolean isAnyKeyboardVisible()
174 {
175 return sysKeyboardVisible || extKeyboardVisible;
176 }
177
178 // displays either the system or the extended keyboard or none of them
179 private void showKeyboard(boolean showSystemKeyboard, boolean showExtendedKeyboard)
180 {
181 if (showSystemKeyboard)
182 {
183 // hide extended keyboard
184 keyboardView.setVisibility(View.GONE);
185 // show system keyboard
186 setSoftInputState(true);
187
188 // show modifiers keyboard
189 modifiersKeyboardView.setVisibility(View.VISIBLE);
190 }
191 else if (showExtendedKeyboard)
192 {
193 // hide system keyboard
194 setSoftInputState(false);
195
196 // show extended keyboard
197 keyboardView.setKeyboard(specialkeysKeyboard);
198 keyboardView.setVisibility(View.VISIBLE);
199 modifiersKeyboardView.setVisibility(View.VISIBLE);
200 }
201 else
202 {
203 // hide both
204 setSoftInputState(false);
205 keyboardView.setVisibility(View.GONE);
206 modifiersKeyboardView.setVisibility(View.GONE);
207
208 // clear any active key modifiers
209 keyboardMapper.clearlAllModifiers();
210 }
211
212 sysKeyboardVisible = showSystemKeyboard;
213 extKeyboardVisible = showExtendedKeyboard;
214 }
215
216 private void setSoftInputState(boolean state)
217 {
218 InputMethodManager mgr =
219 (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
220
221 if (state)
222 {
223 sessionView.requestFocus();
224 mgr.showSoftInput(sessionView, InputMethodManager.SHOW_IMPLICIT);
225 }
226 else
227 {
228 mgr.hideSoftInputFromWindow(sessionView.getWindowToken(), 0);
229 }
230 }
231
232 // Cancels any pending delayed-move events; called on connection failure / disconnect.
233 public void cancelPendingEvents()
234 {
235 handler.removeMessages(MSG_SEND_MOVE_EVENT);
236 }
237
238 // Forwards a physical-mouse scroll event (e.g. external mouse wheel) into the session.
239 public boolean onGenericMotionEvent(MotionEvent e)
240 {
241 if (instance == 0)
242 return false;
243 if (e.getAction() != MotionEvent.ACTION_SCROLL)
244 return false;
245
246 final float vScroll = e.getAxisValue(MotionEvent.AXIS_VSCROLL);
247 if (vScroll < 0)
248 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getScrollEvent(context, false));
249 else if (vScroll > 0)
250 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getScrollEvent(context, true));
251 return true;
252 }
253
254 // Forwards an Android hardware-keyboard event into the session.
255 public boolean onAndroidKeyEvent(KeyEvent event)
256 {
257 if (instance == 0)
258 return false;
259 return keyboardMapper.processAndroidKeyEvent(event);
260 }
261
262 // Handles a long-press on the BACK key by disconnecting the active session.
263 // Returns true if the event was consumed.
264 public boolean onAndroidKeyLongPress(int keyCode)
265 {
266 if (instance == 0)
267 return false;
268 if (keyCode == KeyEvent.KEYCODE_BACK)
269 {
270 LibFreeRDP.disconnect(instance);
271 return true;
272 }
273 return false;
274 }
275
276 // If the "use back as Alt+F4" preference is enabled, sends Alt+F4 and returns true.
277 public boolean handleBackAsAltF4()
278 {
279 if (instance == 0)
280 return false;
281 if (!ApplicationSettingsActivity.getUseBackAsAltf4(context))
282 return false;
283 keyboardMapper.sendAltF4();
284 return true;
285 }
286
287 // Toggles touch-pointer overlay visibility (driven by the menu).
288 public void toggleTouchPointer()
289 {
290 if (touchPointerView.getVisibility() == View.VISIBLE)
291 {
292 touchPointerView.setVisibility(View.INVISIBLE);
293 sessionView.setTouchPointerPadding(0, 0);
294 }
295 else
296 {
297 touchPointerView.setVisibility(View.VISIBLE);
298 sessionView.setTouchPointerPadding(touchPointerView.getPointerWidth(),
299 touchPointerView.getPointerHeight());
300 }
301 }
302
303 // ****************************************************************************
304 // Private helpers
305
306 private void sendDelayedMoveEvent(int x, int y)
307 {
308 if (handler.hasMessages(MSG_SEND_MOVE_EVENT))
309 {
310 handler.removeMessages(MSG_SEND_MOVE_EVENT);
311 discardedMoveEvents++;
312 }
313 else
314 discardedMoveEvents = 0;
315
316 if (discardedMoveEvents > MAX_DISCARDED_MOVE_EVENTS)
317 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getMoveEvent());
318 else
319 handler.sendMessageDelayed(Message.obtain(null, MSG_SEND_MOVE_EVENT, x, y),
320 SEND_MOVE_EVENT_TIMEOUT);
321 }
322
323 private void cancelDelayedMoveEvent()
324 {
325 handler.removeMessages(MSG_SEND_MOVE_EVENT);
326 }
327
328 private Point mapScreenCoordToSessionCoord(int x, int y)
329 {
330 int usableW =
331 scrollView.getWidth() - scrollView.getPaddingLeft() - scrollView.getPaddingRight();
332 int usableH =
333 scrollView.getHeight() - scrollView.getPaddingTop() - scrollView.getPaddingBottom();
334 int contentW = sessionView.getWidth() - sessionView.getTouchPointerPaddingWidth();
335 int contentH = sessionView.getHeight() - sessionView.getTouchPointerPaddingHeight();
336 int centerOffsetX = Math.max(0, (usableW - contentW) / 2);
337 int centerOffsetY = Math.max(0, (usableH - contentH) / 2);
338 int mappedX = (int)((float)(x - safeInsetLeft - centerOffsetX + scrollView.getScrollX()) /
339 sessionView.getZoom());
340 int mappedY = (int)((float)(y - safeInsetTop - centerOffsetY + scrollView.getScrollY()) /
341 sessionView.getZoom());
342 if (bitmap != null)
343 {
344 if (mappedX < 0)
345 mappedX = 0;
346 if (mappedY < 0)
347 mappedY = 0;
348 if (mappedX > bitmap.getWidth())
349 mappedX = bitmap.getWidth();
350 if (mappedY > bitmap.getHeight())
351 mappedY = bitmap.getHeight();
352 }
353 return new Point(mappedX, mappedY);
354 }
355
356 private void updateModifierKeyStates()
357 {
358 List<Keyboard.Key> keys = modifiersKeyboard.getKeys();
359 for (Keyboard.Key curKey : keys)
360 {
361 if (curKey.sticky)
362 {
363 switch (keyboardMapper.getModifierState(curKey.codes[0]))
364 {
365 case KeyboardMapper.KEYSTATE_ON:
366 curKey.on = true;
367 curKey.pressed = false;
368 break;
369
370 case KeyboardMapper.KEYSTATE_OFF:
371 curKey.on = false;
372 curKey.pressed = false;
373 break;
374
375 case KeyboardMapper.KEYSTATE_LOCKED:
376 curKey.on = true;
377 curKey.pressed = true;
378 break;
379 }
380 }
381 }
382 modifiersKeyboardView.invalidateAllKeys();
383 }
384
385 // ****************************************************************************
386 // SessionView.SessionViewListener
387
388 @Override public void onSessionViewBeginTouch()
389 {
390 scrollView.setScrollEnabled(false);
391 }
392
393 @Override public void onSessionViewEndTouch()
394 {
395 scrollView.setScrollEnabled(true);
396 }
397
398 @Override public void onSessionViewLeftTouch(int x, int y, boolean down)
399 {
400 if (instance == 0)
401 return;
402 if (!down)
403 cancelDelayedMoveEvent();
404 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getLeftButtonEvent(context, down));
405 }
406
407 @Override public void onSessionViewMiddleTouch(int x, int y, boolean down)
408 {
409 if (instance == 0)
410 return;
411 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getMiddleButtonEvent(down));
412 }
413
414 @Override public void onSessionViewRightTouch(int x, int y, boolean down)
415 {
416 if (instance == 0)
417 return;
418 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getRightButtonEvent(context, down));
419 }
420
421 @Override public void onSessionViewMove(int x, int y)
422 {
423 if (instance == 0)
424 return;
425 sendDelayedMoveEvent(x, y);
426 }
427
428 @Override public void onSessionViewMouseMove(int x, int y)
429 {
430 if (instance == 0)
431 return;
432 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getMoveEvent());
433 }
434
435 @Override public void onSessionViewScroll(boolean down)
436 {
437 if (instance == 0)
438 return;
439 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getScrollEvent(context, down));
440 }
441
442 @Override public void onSessionViewHScroll(boolean right)
443 {
444 if (instance == 0)
445 return;
446 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getHScrollEvent(context, right));
447 }
448
449 // ****************************************************************************
450 // TouchPointerView.TouchPointerListener
451
452 @Override public void onTouchPointerClose()
453 {
454 touchPointerView.setVisibility(View.INVISIBLE);
455 sessionView.setTouchPointerPadding(0, 0);
456 }
457
458 @Override public void onTouchPointerLeftClick(int x, int y, boolean down)
459 {
460 if (instance == 0)
461 return;
462 Point p = mapScreenCoordToSessionCoord(x, y);
463 LibFreeRDP.sendCursorEvent(instance, p.x, p.y, Mouse.getLeftButtonEvent(context, down));
464 }
465
466 @Override public void onTouchPointerRightClick(int x, int y, boolean down)
467 {
468 if (instance == 0)
469 return;
470 Point p = mapScreenCoordToSessionCoord(x, y);
471 LibFreeRDP.sendCursorEvent(instance, p.x, p.y, Mouse.getRightButtonEvent(context, down));
472 }
473
474 @Override public void onTouchPointerMove(int x, int y)
475 {
476 if (instance == 0)
477 return;
478 Point p = mapScreenCoordToSessionCoord(x, y);
479 LibFreeRDP.sendCursorEvent(instance, p.x, p.y, Mouse.getMoveEvent());
480
481 if (ApplicationSettingsActivity.getAutoScrollTouchPointer(context) &&
482 !handler.hasMessages(MSG_SCROLLING_REQUESTED))
483 {
484 handler.sendEmptyMessageDelayed(MSG_SCROLLING_REQUESTED, SCROLLING_TIMEOUT);
485 }
486 }
487
488 @Override public void onTouchPointerMoveEnd()
489 {
490 handler.removeMessages(MSG_SCROLLING_REQUESTED);
491 }
492
493 @Override public void onTouchPointerScroll(boolean down)
494 {
495 if (instance == 0)
496 return;
497 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getScrollEvent(context, down));
498 }
499
500 @Override public void onTouchPointerToggleKeyboard()
501 {
502 toggleSystemKeyboard();
503 }
504
505 @Override public void onTouchPointerToggleExtKeyboard()
506 {
507 toggleExtendedKeyboard();
508 }
509
510 @Override public void onTouchPointerResetScrollZoom()
511 {
512 sessionView.setZoom(1.0f);
513 scrollView.scrollTo(0, 0);
514 }
515
516 // ****************************************************************************
517 // KeyboardMapper.KeyProcessingListener
518
519 @Override public void processVirtualKey(int virtualKeyCode, boolean down)
520 {
521 if (instance == 0)
522 return;
523 LibFreeRDP.sendKeyEvent(instance, virtualKeyCode, down);
524 }
525
526 @Override public void processUnicodeKey(int unicodeKey)
527 {
528 if (instance == 0)
529 return;
530 if (LibFreeRDP.isUnicodeInputSupported(instance))
531 {
532 LibFreeRDP.sendUnicodeKeyEvent(instance, unicodeKey, true);
533 LibFreeRDP.sendUnicodeKeyEvent(instance, unicodeKey, false);
534 }
535 else
536 keyboardMapper.processUnicodeFallback(unicodeKey);
537 }
538
539 @Override public void switchKeyboard(int keyboardType)
540 {
541 switch (keyboardType)
542 {
543 case KeyboardMapper.KEYBOARD_TYPE_FUNCTIONKEYS:
544 keyboardView.setKeyboard(specialkeysKeyboard);
545 break;
546
547 case KeyboardMapper.KEYBOARD_TYPE_NUMPAD:
548 keyboardView.setKeyboard(numpadKeyboard);
549 break;
550
551 case KeyboardMapper.KEYBOARD_TYPE_CURSOR:
552 keyboardView.setKeyboard(cursorKeyboard);
553 break;
554
555 default:
556 break;
557 }
558 }
559
560 @Override public void modifiersChanged()
561 {
562 updateModifierKeyStates();
563 }
564
565 // ****************************************************************************
566 // KeyboardView.OnKeyboardActionListener (extended/modifiers keyboards)
567
568 @Override public void onKey(int primaryCode, int[] keyCodes)
569 {
570 keyboardMapper.processCustomKeyEvent(primaryCode);
571 }
572
573 @Override public void onText(CharSequence text)
574 {
575 }
576
577 @Override public void swipeRight()
578 {
579 }
580
581 @Override public void swipeLeft()
582 {
583 }
584
585 @Override public void swipeDown()
586 {
587 }
588
589 @Override public void swipeUp()
590 {
591 }
592
593 @Override public void onPress(int primaryCode)
594 {
595 }
596
597 @Override public void onRelease(int primaryCode)
598 {
599 }
600
601 // ****************************************************************************
602 // Internal delayed-event handler
603
604 private class InputHandler extends Handler
605 {
606 InputHandler()
607 {
608 super(Looper.getMainLooper());
609 }
610
611 @Override public void handleMessage(Message msg)
612 {
613 switch (msg.what)
614 {
615 case MSG_SEND_MOVE_EVENT:
616 if (instance == 0)
617 break;
618 LibFreeRDP.sendCursorEvent(instance, msg.arg1, msg.arg2, Mouse.getMoveEvent());
619 break;
620
621 case MSG_SCROLLING_REQUESTED:
622 {
623 int scrollX = 0;
624 int scrollY = 0;
625 float[] pointerPos = touchPointerView.getPointerPosition();
626 final int ow = touchPointerView.getWidth();
627 final int oh = touchPointerView.getHeight();
628 final int pw = touchPointerView.getPointerWidth();
629 final int ph = touchPointerView.getPointerHeight();
630
631 if (pointerPos[0] >= ow - pw - SCROLLING_EDGE_MARGIN)
632 scrollX = SCROLLING_DISTANCE;
633 else if (pointerPos[0] <= SCROLLING_EDGE_MARGIN)
634 scrollX = -SCROLLING_DISTANCE;
635
636 if (pointerPos[1] >= oh - ph - SCROLLING_EDGE_MARGIN)
637 scrollY = SCROLLING_DISTANCE;
638 else if (pointerPos[1] <= SCROLLING_EDGE_MARGIN)
639 scrollY = -SCROLLING_DISTANCE;
640
641 scrollView.scrollBy(scrollX, scrollY);
642
643 final int maxX = sessionView.getWidth() - scrollView.getWidth();
644 final int maxY = sessionView.getHeight() - scrollView.getHeight();
645 if ((scrollX < 0 && scrollView.getScrollX() <= 0) ||
646 (scrollX > 0 && scrollView.getScrollX() >= maxX))
647 scrollX = 0;
648 if ((scrollY < 0 && scrollView.getScrollY() <= 0) ||
649 (scrollY > 0 && scrollView.getScrollY() >= maxY))
650 scrollY = 0;
651
652 if (scrollX != 0 || scrollY != 0)
653 handler.sendEmptyMessageDelayed(MSG_SCROLLING_REQUESTED, SCROLLING_TIMEOUT);
654 break;
655 }
656 }
657 }
658 }
659
660 // ****************************************************************************
661 // Pinch-to-zoom listener (wired into SessionView's ScaleGestureDetector)
662
663 private class PinchZoomListener extends ScaleGestureDetector.SimpleOnScaleGestureListener
664 {
665 private float scaleFactor = 1.0f;
666
667 @Override public boolean onScaleBegin(ScaleGestureDetector detector)
668 {
669 scrollView.setScrollEnabled(false);
670 return true;
671 }
672
673 @Override public boolean onScale(ScaleGestureDetector detector)
674 {
675 // calc scale factor
676 scaleFactor *= detector.getScaleFactor();
677 scaleFactor = Math.max(SessionView.MIN_SCALE_FACTOR,
678 Math.min(scaleFactor, SessionView.MAX_SCALE_FACTOR));
679 sessionView.setZoom(scaleFactor);
680
681 if (!sessionView.isAtMinZoom() && !sessionView.isAtMaxZoom())
682 {
683 // transform scroll origin to the new zoom space
684 float transOriginX = scrollView.getScrollX() * detector.getScaleFactor();
685 float transOriginY = scrollView.getScrollY() * detector.getScaleFactor();
686
687 // transform center point to the zoomed space
688 float transCenterX =
689 (scrollView.getScrollX() + detector.getFocusX()) * detector.getScaleFactor();
690 float transCenterY =
691 (scrollView.getScrollY() + detector.getFocusY()) * detector.getScaleFactor();
692
693 // scroll by the difference between the distance of the
694 // transformed center/origin point and their old distance
695 // (focusX/Y)
696 scrollView.scrollBy((int)((transCenterX - transOriginX) - detector.getFocusX()),
697 (int)((transCenterY - transOriginY) - detector.getFocusY()));
698 }
699
700 return true;
701 }
702
703 @Override public void onScaleEnd(ScaleGestureDetector de)
704 {
705 scrollView.setScrollEnabled(true);
706 }
707 }
708}