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