FreeRDP
Loading...
Searching...
No Matches
RailWindowManager.java
1/*
2 Android RAIL/RemoteApp window overlay 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.os.Handler;
16import android.os.Looper;
17import android.view.View;
18import android.view.ViewGroup;
19import android.widget.FrameLayout;
20import android.widget.ImageView;
21
22import java.util.HashMap;
23
24class RailWindowManager
25{
26 private final Context context;
27 private final FrameLayout container;
28 private final SessionView sessionView;
29 private final Handler ui = new Handler(Looper.getMainLooper());
30
31 private final HashMap<Long, ImageView> windows = new HashMap<>();
32 private final HashMap<Long, int[]> rects = new HashMap<>();
33 private final HashMap<Long, Bitmap> bitmaps = new HashMap<>();
34 private long[] zOrder = null;
35 private long activeWindowId = 0;
36
37 RailWindowManager(Context context, FrameLayout container, SessionView sessionView)
38 {
39 this.context = context;
40 this.container = container;
41 this.sessionView = sessionView;
42 sessionView.setOnZoomChangedListener(zoom -> repositionAll());
43 }
44
45 void onWindowUpdate(long windowId, int width, int height, int[] pixels)
46 {
47 if (pixels == null)
48 return;
49 ui.post(() -> handleWindowUpdate(windowId, width, height, pixels));
50 }
51
52 void onWindowMove(long windowId, int x, int y, int w, int h)
53 {
54 ui.post(() -> handleWindowMove(windowId, x, y, w, h));
55 }
56
57 void onWindowHide(long windowId)
58 {
59 ui.post(() -> {
60 ImageView iv = windows.get(windowId);
61 if (iv != null)
62 iv.setVisibility(View.INVISIBLE);
63 });
64 }
65
66 void onWindowDestroy(long windowId)
67 {
68 ui.post(() -> handleWindowDestroy(windowId));
69 }
70
71 void onSessionEnd()
72 {
73 ui.post(() -> {
74 clearWindows();
75 sessionView.setRailMode(false);
76 });
77 }
78
79 void onMonitoredDesktop(long[] windowIds, long active)
80 {
81 ui.post(() -> handleMonitoredDesktop(windowIds, active));
82 }
83
84 // Removes all overlays; must be called on the UI thread.
85 void clear()
86 {
87 clearWindows();
88 }
89
90 private void handleWindowUpdate(long windowId, int width, int height, int[] pixels)
91 {
92 if (container == null)
93 return;
94 // Reuse the per-window bitmap; reallocate only on size change.
95 Bitmap bm = bitmaps.get(windowId);
96 boolean newBitmap = bm == null || bm.getWidth() != width || bm.getHeight() != height;
97 if (newBitmap)
98 {
99 bm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
100 bitmaps.put(windowId, bm);
101 }
102 bm.setPixels(pixels, 0, width, 0, 0, width, height);
103 ImageView iv = windows.get(windowId);
104 if (iv == null)
105 {
106 iv = new ImageView(context);
107 iv.setScaleType(ImageView.ScaleType.FIT_XY);
108 iv.setVisibility(View.INVISIBLE);
109 container.addView(iv);
110 windows.put(windowId, iv);
111 // Order may have arrived before this view existed; restack now.
112 restack();
113 newBitmap = true;
114 }
115 if (newBitmap)
116 {
117 iv.setImageBitmap(bm);
118 ViewGroup.LayoutParams lp = iv.getLayoutParams();
119 lp.width = width;
120 lp.height = height;
121 iv.requestLayout();
122 }
123 else
124 {
125 iv.invalidate();
126 }
127 if (rects.containsKey(windowId))
128 {
129 position(iv, windowId);
130 iv.setVisibility(View.VISIBLE);
131 sessionView.setRailMode(true);
132 }
133 }
134
135 private void handleWindowMove(long windowId, int x, int y, int w, int h)
136 {
137 if (x != -1 || y != -1)
138 rects.put(windowId, new int[] { x, y, w, h });
139 ImageView iv = windows.get(windowId);
140 if (iv != null)
141 {
142 if (w > 0 && h > 0)
143 {
144 ViewGroup.LayoutParams lp = iv.getLayoutParams();
145 if (lp.width != w || lp.height != h)
146 {
147 lp.width = w;
148 lp.height = h;
149 iv.requestLayout();
150 }
151 }
152 position(iv, windowId);
153 iv.setVisibility(View.VISIBLE);
154 sessionView.setRailMode(true);
155 }
156 }
157
158 private void handleWindowDestroy(long windowId)
159 {
160 rects.remove(windowId);
161 bitmaps.remove(windowId);
162 ImageView iv = windows.remove(windowId);
163 if (iv != null && container != null)
164 container.removeView(iv);
165 }
166
167 private void handleMonitoredDesktop(long[] windowIds, long active)
168 {
169 if (windowIds != null)
170 {
171 zOrder = windowIds;
172 if (windowIds.length > 0)
173 activeWindowId = windowIds[0];
174 }
175 if (active != 0)
176 activeWindowId = active;
177 restack();
178 }
179
180 // Match client stacking to the server order (input is routed server-side by coordinate).
181 private void restack()
182 {
183 if (container == null)
184 return;
185 if (zOrder != null)
186 for (int i = zOrder.length - 1; i >= 0; i--)
187 {
188 ImageView iv = windows.get(zOrder[i]);
189 if (iv != null)
190 iv.bringToFront();
191 }
192 if (activeWindowId != 0 &&
193 (zOrder == null || zOrder.length == 0 || zOrder[0] != activeWindowId))
194 {
195 ImageView iv = windows.get(activeWindowId);
196 if (iv != null)
197 iv.bringToFront();
198 }
199 }
200
201 private void repositionAll()
202 {
203 for (HashMap.Entry<Long, ImageView> entry : windows.entrySet())
204 position(entry.getValue(), entry.getKey());
205 }
206
207 private void position(ImageView iv, long windowId)
208 {
209 int[] rect = rects.get(windowId);
210 if (rect == null)
211 return;
212 float zoom = sessionView != null ? sessionView.getZoom() : 1.0f;
213 iv.setPivotX(0);
214 iv.setPivotY(0);
215 iv.setScaleX(zoom);
216 iv.setScaleY(zoom);
217 iv.setX(rect[0] * zoom);
218 iv.setY(rect[1] * zoom);
219 }
220
221 private void clearWindows()
222 {
223 if (container != null)
224 for (ImageView iv : windows.values())
225 container.removeView(iv);
226 windows.clear();
227 rects.clear();
228 bitmaps.clear();
229 zOrder = null;
230 activeWindowId = 0;
231 }
232}