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.os.Handler;
17import android.os.Looper;
18import android.os.Message;
19import android.util.Log;
20import android.view.KeyEvent;
21import android.view.MotionEvent;
22import android.view.ScaleGestureDetector;
23import android.view.View;
24import android.view.inputmethod.InputMethodManager;
25
26import com.freerdp.freerdpcore.services.LibFreeRDP;
27import com.freerdp.freerdpcore.utils.KeyboardMapper;
28import com.freerdp.freerdpcore.utils.Mouse;
29
31 implements SessionView.SessionViewListener, TouchPointerView.TouchPointerListener,
32 KeyboardMapper.KeyProcessingListener, ExtendedKeyboardView.Listener
33{
34 private static final String TAG = "FreeRDP.SessionInputManager";
35
36 private static final int SCROLLING_TIMEOUT = 16;
37 private static final int SCROLLING_DISTANCE = 12;
38 private static final int SCROLLING_EDGE_MARGIN = 16;
39 private static final int MAX_DISCARDED_MOVE_EVENTS = 3;
40 private static final int SEND_MOVE_EVENT_TIMEOUT = 150;
41
42 private static final int MSG_SEND_MOVE_EVENT = 1;
43 private static final int MSG_SCROLLING_REQUESTED = 2;
44
45 private final Context context;
46 private final KeyboardMapper keyboardMapper;
47 private final ScrollView2D scrollView;
48 private final SessionView sessionView;
49 private final TouchPointerView touchPointerView;
50 private final ExtendedKeyboardView keyboard;
51 private final PinchZoomListener pinchZoomListener = new PinchZoomListener();
52
53 // Native FreeRDP instance handle. 0 until attachSession() is called (i.e. before connect).
54 private long instance = 0;
55 private Bitmap bitmap;
56 private int screenWidth;
57 private int screenHeight;
58 private int discardedMoveEvents = 0;
59 // we asked the IME to show; the window has not necessarily animated it in yet
60 private boolean softInputRequested = false;
61 // the IME reported a non-zero inset, i.e. it really is on screen
62 private boolean softInputVisible = false;
63
64 private final Handler handler;
65
66 public SessionInputManager(Context context, ScrollView2D scrollView, SessionView sessionView,
67 TouchPointerView touchPointerView, ExtendedKeyboardView keyboard)
68 {
69 this.context = context;
70 this.scrollView = scrollView;
71 this.sessionView = sessionView;
72 this.touchPointerView = touchPointerView;
73 this.keyboard = keyboard;
74 this.handler = new InputHandler();
75
76 this.keyboardMapper = new KeyboardMapper();
77 this.keyboardMapper.init(context);
78
79 keyboard.setListener(this);
80 }
81
82 // Binds this manager to a live FreeRDP session. Until called, all input events are dropped.
83 public void attachSession(long instance, Bitmap surface)
84 {
85 this.instance = instance;
86 this.bitmap = surface;
87 keyboardMapper.reset(this);
88 }
89
90 // Called when the session bitmap is created or replaced (OnSettingsChanged / OnGraphicsResize).
91 public void setBitmap(Bitmap bitmap)
92 {
93 this.bitmap = bitmap;
94 }
95
96 // Returns a listener that can be wired into a ScaleGestureDetector for pinch-to-zoom.
97 public ScaleGestureDetector.OnScaleGestureListener getPinchZoomListener()
98 {
99 return pinchZoomListener;
100 }
101
102 // Called once the screen dimensions are known (onGlobalLayout) and on bindSession.
103 public void setScreenSize(int width, int height)
104 {
105 this.screenWidth = width;
106 this.screenHeight = height;
107 }
108
109 // Shows or hides the key bar together with the system IME.
110 public void toggleKeyboard()
111 {
112 if (keyboard.getVisibility() == View.VISIBLE)
113 {
114 hideKeyboards();
115 }
116 else
117 {
118 keyboard.setExpanded(false, false);
119 keyboard.setVisibility(View.VISIBLE);
120 setSoftInputState(true);
121 }
122 }
123
124 // Called from onPause and back-press handling.
125 public void hideKeyboards()
126 {
127 keyboard.setExpanded(false, false);
128 keyboard.setVisibility(View.GONE);
129 setSoftInputState(false);
130 keyboardMapper.clearlAllModifiers();
131 // the IME dismiss animation may re-show the nav bar after the refresh above
132 scrollView.post(this::refreshSystemBars);
133 }
134
135 // Returns true if the back press was consumed by the keyboard.
136 public boolean handleKeyboardBack()
137 {
138 if (keyboard.getVisibility() != View.VISIBLE)
139 return false;
140
141 if (keyboard.isExpanded())
142 {
143 keyboard.setExpanded(false, false);
144 // deliberately no IME here, so the next back press hides the bar as well
145 refreshSystemBars();
146 scrollView.requestApplyInsets();
147 return true;
148 }
149
150 hideKeyboards();
151 return true;
152 }
153
154 // True if the system soft keyboard (IME) is up or on its way up.
155 public boolean isSoftInputActive()
156 {
157 return keyboard.getVisibility() == View.VISIBLE && (softInputRequested || softInputVisible);
158 }
159
160 // Fed from the window insets listener. The IME only counts as gone once it has been seen on
161 // screen: the insets pass right after showSoftInput() still reports a zero inset.
162 public void onImeVisibilityChanged(boolean visible)
163 {
164 if (visible == softInputVisible)
165 return;
166 softInputVisible = visible;
167 if (!visible)
168 softInputRequested = false;
169 refreshSystemBars();
170 }
171
172 private void refreshSystemBars()
173 {
174 if (context instanceof SessionActivity)
175 ((SessionActivity)context).hideSystemBars();
176 }
177
178 private void setSoftInputState(boolean state)
179 {
180 softInputRequested = state;
181 if (!state)
182 softInputVisible = false;
183 InputMethodManager mgr =
184 (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
185
186 if (state)
187 {
188 sessionView.requestFocus();
189 mgr.showSoftInput(sessionView, InputMethodManager.SHOW_IMPLICIT);
190 }
191 else
192 {
193 mgr.hideSoftInputFromWindow(sessionView.getWindowToken(), 0);
194 }
195 refreshSystemBars();
196 scrollView.requestApplyInsets();
197 }
198
199 // Cancels any pending delayed-move events; called on connection failure / disconnect.
200 public void cancelPendingEvents()
201 {
202 handler.removeMessages(MSG_SEND_MOVE_EVENT);
203 }
204
205 // Forwards a physical-mouse scroll event (e.g. external mouse wheel) into the session.
206 public boolean onGenericMotionEvent(MotionEvent e)
207 {
208 if (instance == 0)
209 return false;
210 if (e.getAction() != MotionEvent.ACTION_SCROLL)
211 return false;
212
213 final float vScroll = e.getAxisValue(MotionEvent.AXIS_VSCROLL);
214 if (vScroll < 0)
215 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getScrollEvent(context, false));
216 else if (vScroll > 0)
217 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getScrollEvent(context, true));
218 return true;
219 }
220
221 // Forwards an Android hardware-keyboard event into the session.
222 public boolean onAndroidKeyEvent(KeyEvent event)
223 {
224 if (instance == 0)
225 return false;
226 return keyboardMapper.processAndroidKeyEvent(event);
227 }
228
229 // Handles a long-press on the BACK key by disconnecting the active session.
230 // Returns true if the event was consumed.
231 public boolean onAndroidKeyLongPress(int keyCode)
232 {
233 if (instance == 0)
234 return false;
235 if (keyCode == KeyEvent.KEYCODE_BACK)
236 {
237 LibFreeRDP.disconnect(instance);
238 return true;
239 }
240 return false;
241 }
242
243 // If the "use back as Alt+F4" preference is enabled, sends Alt+F4 and returns true.
244 public boolean handleBackAsAltF4()
245 {
246 if (instance == 0)
247 return false;
248 if (!ApplicationSettingsActivity.getUseBackAsAltf4(context))
249 return false;
250 keyboardMapper.sendAltF4();
251 return true;
252 }
253
254 // Toggles touch-pointer overlay visibility (driven by the menu).
255 public void toggleTouchPointer()
256 {
257 if (touchPointerView.getVisibility() == View.VISIBLE)
258 {
259 touchPointerView.setVisibility(View.INVISIBLE);
260 sessionView.setTouchPointerPadding(0, 0);
261 }
262 else
263 {
264 touchPointerView.setVisibility(View.VISIBLE);
265 sessionView.setTouchPointerPadding(touchPointerView.getPointerWidth(),
266 touchPointerView.getPointerHeight());
267 }
268 }
269
270 // ****************************************************************************
271 // Private helpers
272
273 private void sendDelayedMoveEvent(int x, int y)
274 {
275 if (handler.hasMessages(MSG_SEND_MOVE_EVENT))
276 {
277 handler.removeMessages(MSG_SEND_MOVE_EVENT);
278 discardedMoveEvents++;
279 }
280 else
281 discardedMoveEvents = 0;
282
283 if (discardedMoveEvents > MAX_DISCARDED_MOVE_EVENTS)
284 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getMoveEvent());
285 else
286 handler.sendMessageDelayed(Message.obtain(null, MSG_SEND_MOVE_EVENT, x, y),
287 SEND_MOVE_EVENT_TIMEOUT);
288 }
289
290 private void cancelDelayedMoveEvent()
291 {
292 handler.removeMessages(MSG_SEND_MOVE_EVENT);
293 }
294
295 private Point mapScreenCoordToSessionCoord(int x, int y)
296 {
297 View container = scrollView.getChildCount() > 0 ? scrollView.getChildAt(0) : sessionView;
298 int mappedX = (int)((float)(x - container.getLeft() + scrollView.getScrollX()) /
299 sessionView.getZoom());
300 int mappedY = (int)((float)(y - container.getTop() + scrollView.getScrollY()) /
301 sessionView.getZoom());
302 if (bitmap != null)
303 {
304 if (mappedX < 0)
305 mappedX = 0;
306 if (mappedY < 0)
307 mappedY = 0;
308 if (mappedX > bitmap.getWidth())
309 mappedX = bitmap.getWidth();
310 if (mappedY > bitmap.getHeight())
311 mappedY = bitmap.getHeight();
312 }
313 return new Point(mappedX, mappedY);
314 }
315
316 // ****************************************************************************
317 // SessionView.SessionViewListener
318
319 @Override public void onSessionViewBeginTouch()
320 {
321 scrollView.setScrollEnabled(false);
322 }
323
324 @Override public void onSessionViewEndTouch()
325 {
326 scrollView.setScrollEnabled(true);
327 }
328
329 @Override public void onSessionViewLeftTouch(int x, int y, boolean down)
330 {
331 if (instance == 0)
332 return;
333 if (!down)
334 cancelDelayedMoveEvent();
335 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getLeftButtonEvent(context, down));
336 }
337
338 @Override public void onSessionViewMiddleTouch(int x, int y, boolean down)
339 {
340 if (instance == 0)
341 return;
342 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getMiddleButtonEvent(down));
343 }
344
345 @Override public void onSessionViewRightTouch(int x, int y, boolean down)
346 {
347 if (instance == 0)
348 return;
349 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getRightButtonEvent(context, down));
350 }
351
352 @Override public void onSessionViewMove(int x, int y)
353 {
354 if (instance == 0)
355 return;
356 sendDelayedMoveEvent(x, y);
357 }
358
359 @Override public void onSessionViewMouseMove(int x, int y)
360 {
361 if (instance == 0)
362 return;
363 LibFreeRDP.sendCursorEvent(instance, x, y, Mouse.getMoveEvent());
364 }
365
366 @Override public void onSessionViewScroll(boolean down)
367 {
368 if (instance == 0)
369 return;
370 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getScrollEvent(context, down));
371 }
372
373 @Override public void onSessionViewHScroll(boolean right)
374 {
375 if (instance == 0)
376 return;
377 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getHScrollEvent(context, right));
378 }
379
380 // ****************************************************************************
381 // TouchPointerView.TouchPointerListener
382
383 @Override public void onTouchPointerClose()
384 {
385 touchPointerView.setVisibility(View.INVISIBLE);
386 sessionView.setTouchPointerPadding(0, 0);
387 }
388
389 @Override public void onTouchPointerLeftClick(int x, int y, boolean down)
390 {
391 if (instance == 0)
392 return;
393 Point p = mapScreenCoordToSessionCoord(x, y);
394 LibFreeRDP.sendCursorEvent(instance, p.x, p.y, Mouse.getLeftButtonEvent(context, down));
395 }
396
397 @Override public void onTouchPointerRightClick(int x, int y, boolean down)
398 {
399 if (instance == 0)
400 return;
401 Point p = mapScreenCoordToSessionCoord(x, y);
402 LibFreeRDP.sendCursorEvent(instance, p.x, p.y, Mouse.getRightButtonEvent(context, down));
403 }
404
405 @Override public void onTouchPointerMove(int x, int y)
406 {
407 if (instance == 0)
408 return;
409 Point p = mapScreenCoordToSessionCoord(x, y);
410 LibFreeRDP.sendCursorEvent(instance, p.x, p.y, Mouse.getMoveEvent());
411
412 if (ApplicationSettingsActivity.getAutoScrollTouchPointer(context) &&
413 !handler.hasMessages(MSG_SCROLLING_REQUESTED))
414 {
415 handler.sendEmptyMessageDelayed(MSG_SCROLLING_REQUESTED, SCROLLING_TIMEOUT);
416 }
417 }
418
419 @Override public void onTouchPointerMoveEnd()
420 {
421 handler.removeMessages(MSG_SCROLLING_REQUESTED);
422 }
423
424 @Override public void onTouchPointerScroll(boolean down)
425 {
426 if (instance == 0)
427 return;
428 LibFreeRDP.sendCursorEvent(instance, 0, 0, Mouse.getScrollEvent(context, down));
429 }
430
431 @Override public void onTouchPointerToggleKeyboard()
432 {
433 toggleKeyboard();
434 }
435
436 @Override public void onTouchPointerResetScrollZoom()
437 {
438 sessionView.setZoom(1.0f);
439 scrollView.scrollTo(0, 0);
440 }
441
442 // ****************************************************************************
443 // KeyboardMapper.KeyProcessingListener
444
445 @Override public void processVirtualKey(int virtualKeyCode, boolean down)
446 {
447 if (instance == 0)
448 return;
449 LibFreeRDP.sendKeyEvent(instance, virtualKeyCode, down);
450 }
451
452 @Override public void processUnicodeKey(int unicodeKey)
453 {
454 if (instance == 0)
455 return;
456 if (LibFreeRDP.isUnicodeInputSupported(instance))
457 {
458 LibFreeRDP.sendUnicodeKeyEvent(instance, unicodeKey, true);
459 LibFreeRDP.sendUnicodeKeyEvent(instance, unicodeKey, false);
460 }
461 else
462 keyboardMapper.processUnicodeFallback(unicodeKey);
463 }
464
465 @Override public void switchKeyboard(int keyboardType)
466 {
467 switch (keyboardType)
468 {
469 case KeyboardMapper.KEYBOARD_TYPE_FUNCTIONKEYS:
470 keyboard.selectPage(ExtendedKeyboardView.PAGE_SPECIAL);
471 break;
472
473 case KeyboardMapper.KEYBOARD_TYPE_NUMPAD:
474 keyboard.selectPage(ExtendedKeyboardView.PAGE_NUM);
475 break;
476
477 default:
478 break;
479 }
480 }
481
482 @Override public void modifiersChanged()
483 {
484 keyboard.refreshModifiers();
485 }
486
487 // ****************************************************************************
488 // ExtendedKeyboardView.Listener
489
490 @Override public void onKey(int keycode)
491 {
492 keyboardMapper.processCustomKeyEvent(keycode);
493 }
494
495 @Override public void onKeyLock(int keycode)
496 {
497 keyboardMapper.processCustomKeyLock(keycode);
498 }
499
500 @Override public int getModifierState(int keycode)
501 {
502 return keyboardMapper.getModifierState(keycode);
503 }
504
505 @Override public void onExpandedChanged(boolean expanded)
506 {
507 // the expanded panel replaces the system IME; collapsing brings it back
508 setSoftInputState(!expanded);
509 }
510
511 // ****************************************************************************
512 // Internal delayed-event handler
513
514 private class InputHandler extends Handler
515 {
516 InputHandler()
517 {
518 super(Looper.getMainLooper());
519 }
520
521 @Override public void handleMessage(Message msg)
522 {
523 switch (msg.what)
524 {
525 case MSG_SEND_MOVE_EVENT:
526 if (instance == 0)
527 break;
528 LibFreeRDP.sendCursorEvent(instance, msg.arg1, msg.arg2, Mouse.getMoveEvent());
529 break;
530
531 case MSG_SCROLLING_REQUESTED:
532 {
533 int scrollX = 0;
534 int scrollY = 0;
535 float[] pointerPos = touchPointerView.getPointerPosition();
536 final int ow = touchPointerView.getWidth();
537 final int oh = touchPointerView.getHeight();
538 final int pw = touchPointerView.getPointerWidth();
539 final int ph = touchPointerView.getPointerHeight();
540
541 if (pointerPos[0] >= ow - pw - SCROLLING_EDGE_MARGIN)
542 scrollX = SCROLLING_DISTANCE;
543 else if (pointerPos[0] <= SCROLLING_EDGE_MARGIN)
544 scrollX = -SCROLLING_DISTANCE;
545
546 if (pointerPos[1] >= oh - ph - SCROLLING_EDGE_MARGIN)
547 scrollY = SCROLLING_DISTANCE;
548 else if (pointerPos[1] <= SCROLLING_EDGE_MARGIN)
549 scrollY = -SCROLLING_DISTANCE;
550
551 scrollView.scrollBy(scrollX, scrollY);
552
553 final int maxX = sessionView.getWidth() - scrollView.getWidth();
554 final int maxY = sessionView.getHeight() - scrollView.getHeight();
555 if ((scrollX < 0 && scrollView.getScrollX() <= 0) ||
556 (scrollX > 0 && scrollView.getScrollX() >= maxX))
557 scrollX = 0;
558 if ((scrollY < 0 && scrollView.getScrollY() <= 0) ||
559 (scrollY > 0 && scrollView.getScrollY() >= maxY))
560 scrollY = 0;
561
562 if (scrollX != 0 || scrollY != 0)
563 handler.sendEmptyMessageDelayed(MSG_SCROLLING_REQUESTED, SCROLLING_TIMEOUT);
564 break;
565 }
566 }
567 }
568 }
569
570 // ****************************************************************************
571 // Pinch-to-zoom listener (wired into SessionView's ScaleGestureDetector)
572
573 private class PinchZoomListener extends ScaleGestureDetector.SimpleOnScaleGestureListener
574 {
575 private float scaleFactor = 1.0f;
576
577 @Override public boolean onScaleBegin(ScaleGestureDetector detector)
578 {
579 scrollView.setScrollEnabled(false);
580 return true;
581 }
582
583 @Override public boolean onScale(ScaleGestureDetector detector)
584 {
585 // calc scale factor
586 scaleFactor *= detector.getScaleFactor();
587 scaleFactor = Math.max(SessionView.MIN_SCALE_FACTOR,
588 Math.min(scaleFactor, SessionView.MAX_SCALE_FACTOR));
589 sessionView.setZoom(scaleFactor);
590
591 if (!sessionView.isAtMinZoom() && !sessionView.isAtMaxZoom())
592 {
593 // transform scroll origin to the new zoom space
594 float transOriginX = scrollView.getScrollX() * detector.getScaleFactor();
595 float transOriginY = scrollView.getScrollY() * detector.getScaleFactor();
596
597 // transform center point to the zoomed space
598 float transCenterX =
599 (scrollView.getScrollX() + detector.getFocusX()) * detector.getScaleFactor();
600 float transCenterY =
601 (scrollView.getScrollY() + detector.getFocusY()) * detector.getScaleFactor();
602
603 // scroll by the difference between the distance of the
604 // transformed center/origin point and their old distance
605 // (focusX/Y)
606 scrollView.scrollBy((int)((transCenterX - transOriginX) - detector.getFocusX()),
607 (int)((transCenterY - transOriginY) - detector.getFocusY()));
608 }
609
610 return true;
611 }
612
613 @Override public void onScaleEnd(ScaleGestureDetector de)
614 {
615 scrollView.setScrollEnabled(true);
616 }
617 }
618}