mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feat] (Android): the render server screen and binary ship in the plugin APK too, not just trace
This commit is contained in:
@@ -2,42 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application>
|
||||
<!-- Render server control screen: start/stop the offscreen TCP supervisor
|
||||
(MobileGLServerService) or the on-screen display server. Not the launcher:
|
||||
PostActivity is the entry icon and the bottom navigation bar switches between
|
||||
the two screens; exported so adb can drive it by explicit component with
|
||||
listen/token/env/onscreen/start/stop extras. -->
|
||||
<activity
|
||||
android:name=".ServerControlActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.MobileGLPlugin" />
|
||||
<service
|
||||
android:name=".MobileGLServerService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:process=":mglsrv" />
|
||||
<!-- P12 (on-screen server window): the ON-SCREEN display server. Its own process (:mglwin)
|
||||
runs the TCP server on a thread and renders a headless client's window surface
|
||||
(MOBILEGL_IPC_SURFACE=server) onto its SurfaceView. One server at a time: starting it
|
||||
stops MobileGLServerService, starting the service kills :mglwin. Software window
|
||||
rendering keeps HWUI off EGL here (Espryt's teardown terminates the default display);
|
||||
configChanges keeps the Surface through rotation and overlay changes. -->
|
||||
<activity
|
||||
android:name=".MobileGLDisplayActivity"
|
||||
android:configChanges="@integer/trace_replay_config_changes"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="false"
|
||||
android:launchMode="singleTop"
|
||||
android:process=":mglwin"
|
||||
android:taskAffinity="${applicationId}.mglwin"
|
||||
android:theme="@style/Theme.MobileGLPlugin" />
|
||||
<activity
|
||||
android:name=".PluginActivity"
|
||||
tools:node="remove" />
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
import android.os.SystemClock;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* P12 (on-screen server window), D7: the ON-SCREEN display server.
|
||||
*
|
||||
* <p>A headless client (MOBILEGL_IPC_SURFACE=server) asks the server to create its window surface
|
||||
* on the SERVER's window (WindowKind::ServerOwned) and renders as if it were drawing straight on
|
||||
* this phone's GPU and screen. That window is this Activity's SurfaceView, and because an
|
||||
* ANativeWindow survives neither exec nor fork, the TCP server runs IN THIS PROCESS
|
||||
* (android:process=":mglwin") on a thread - libMobileGL's mobilegl_server_serve_inprocess, the
|
||||
* exec'd supervisor's `--serve` with threads instead of session children. Sessions run one at a
|
||||
* time; each one's window surface goes onto the SurfaceView.
|
||||
*
|
||||
* <p>ONE SERVER AT A TIME (D8). Starting this Activity stops {@link MobileGLServerService} (the
|
||||
* offscreen supervisor, whose session children die with it); starting that service kills this
|
||||
* process. Whichever was started last owns the port.
|
||||
*
|
||||
* <p>Extras (the service's names): {@code listen} (tcp://host:port, default loopback 40613),
|
||||
* {@code token}, {@code env} (the service's {@code KEY=VALUE;KEY} grammar, {@link ServerEnvironment}),
|
||||
* and optionally {@code backend} (MOBILEGL_BACKEND_TYPE: the pin for this process; without it the
|
||||
* first session's backend is pinned). The environment is applied to this process with
|
||||
* Os.setenv / Os.unsetenv BEFORE libMobileGL is loaded - it reads its knobs at load and first use.
|
||||
*
|
||||
* <p>THE SURFACE. surfaceCreated / surfaceChanged hand the window to the native display (which takes
|
||||
* its own reference); surfaceDestroyed BLOCKS, bounded (~3 s), until a session rendering into it has
|
||||
* released its backend surface on the server's apply thread - the GLSurfaceView contract: nothing
|
||||
* renders into the window after this callback returns. That session is then latched by name
|
||||
* (ServerWindowLost) and its client reads a clean device loss; the server keeps listening and the
|
||||
* next session renders once the window is back. A size the client asks for arrives as a geometry
|
||||
* request: SurfaceHolder.setFixedSize(w, h) with the view aspect-fitted (letterboxed) into the
|
||||
* screen, or setSizeFromLayout() for 0/0.
|
||||
*
|
||||
* <p>ONE SERVER PER PROCESS LAUNCH. Finishing the Activity stops the server and ends the process, so
|
||||
* the next launch starts with a fresh environment, a fresh backend pin and no EGL state.
|
||||
* hardwareAccelerated="false" keeps HWUI off EGL in this process (Espryt's teardown terminates the
|
||||
* process-default EGL display).
|
||||
*/
|
||||
public final class MobileGLDisplayActivity extends Activity {
|
||||
private static final String TAG = "MobileGLDisplay";
|
||||
static final String DEFAULT_LISTEN = "tcp://127.0.0.1:40613";
|
||||
|
||||
// ---- process-wide: one in-process display server per :mglwin process ------------------------
|
||||
private static final Object serverLock = new Object();
|
||||
private static Thread serverThread; // guarded by serverLock
|
||||
private static String serverConfiguration; // guarded by serverLock
|
||||
private static boolean serverReturned; // guarded by serverLock: nativeServe came back
|
||||
private static volatile boolean displayInstalled;
|
||||
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
// Main thread only.
|
||||
private static MobileGLDisplayActivity current;
|
||||
private static int requestedWidth; // the last geometry request; 0/0 = the layout's size
|
||||
private static int requestedHeight;
|
||||
|
||||
private FrameLayout container;
|
||||
private SurfaceView surfaceView;
|
||||
private TextView statusView;
|
||||
|
||||
// ---- native (MobileGL/MG_Remote/Server/DisplayServerJni.cpp) ---------------------------------
|
||||
private static native void nativeInstallDisplay();
|
||||
private static native int nativeServe(String listen);
|
||||
private static native void nativeStop();
|
||||
private static native boolean nativeAttachWindow(Surface surface, int width, int height);
|
||||
private static native String nativeDetachWindow();
|
||||
private static native void nativeUninstallDisplay();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
container = new FrameLayout(this);
|
||||
container.setBackgroundColor(Color.BLACK);
|
||||
surfaceView = new SurfaceView(this);
|
||||
container.addView(surfaceView, new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
|
||||
statusView = new TextView(this);
|
||||
statusView.setTextColor(Color.WHITE);
|
||||
statusView.setPadding(24, 24, 24, 24);
|
||||
statusView.setVisibility(View.GONE);
|
||||
container.addView(statusView, new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP));
|
||||
setContentView(container);
|
||||
hideSystemBars();
|
||||
// Aspect-fit follows the screen (rotation keeps this Activity: configChanges).
|
||||
container.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
|
||||
mainHandler.post(this::applyLayout));
|
||||
current = this;
|
||||
|
||||
// D8: ONE SERVER AT A TIME. The offscreen supervisor goes first; the in-process listen
|
||||
// retries for ~5 s while its listener dies.
|
||||
if (stopService(new Intent(this, MobileGLServerService.class))) {
|
||||
Log.i(TAG, "stopped MobileGLServerService (the offscreen supervisor): one server at a time (P12 D8)");
|
||||
}
|
||||
if (!startServer(getIntent())) return;
|
||||
|
||||
SurfaceHolder holder = surfaceView.getHolder();
|
||||
// A recreated Activity keeps the size the live session asked for.
|
||||
if (requestedWidth > 0 && requestedHeight > 0) holder.setFixedSize(requestedWidth, requestedHeight);
|
||||
holder.addCallback(surfaceCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
startServer(intent); // the server is up: this only says whether the configuration differs
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus) hideSystemBars();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (current == this) current = null;
|
||||
if (isFinishing()) {
|
||||
boolean serving;
|
||||
synchronized (serverLock) {
|
||||
serving = serverThread != null && !serverReturned;
|
||||
}
|
||||
if (serving) {
|
||||
// Non-blocking: the live session ends within a control slice, serve() returns and
|
||||
// ends this process (onServerReturned).
|
||||
Log.i(TAG, "the display Activity is finishing: stopping the in-process display server");
|
||||
nativeStop();
|
||||
} else {
|
||||
endProcess();
|
||||
}
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// ---- the server ----------------------------------------------------------------------------
|
||||
|
||||
private static String extra(Intent intent, String name, String fallback) {
|
||||
String value = intent == null ? null : intent.getStringExtra(name);
|
||||
return value == null ? fallback : value;
|
||||
}
|
||||
|
||||
/** Starts this process's display server once. False (and the Activity finishes) on failure. */
|
||||
private boolean startServer(Intent intent) {
|
||||
final String listen = extra(intent, "listen", DEFAULT_LISTEN);
|
||||
String token = extra(intent, "token", "");
|
||||
String env = extra(intent, "env", "");
|
||||
String backend = extra(intent, "backend", "");
|
||||
String configuration = listen + "\n" + token + "\n" + env + "\n" + backend;
|
||||
synchronized (serverLock) {
|
||||
if (serverThread != null) {
|
||||
if (!configuration.equals(serverConfiguration)) {
|
||||
Log.w(TAG, "the in-process display server already runs in this process with another "
|
||||
+ "configuration and keeps it; stop the package (am force-stop) to reconfigure");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// THE ENVIRONMENT BEFORE THE LIBRARY: libMobileGL reads its knobs when it loads and at
|
||||
// first use, and the log role is cached at its first line.
|
||||
Map<String, String> before = new HashMap<>(System.getenv());
|
||||
Map<String, String> after = new HashMap<>(before);
|
||||
ServerEnvironment.applyServerRole(after, env, token, backend,
|
||||
new File(getFilesDir(), "mglwin.log").getAbsolutePath());
|
||||
try {
|
||||
for (Map.Entry<String, String> edit : ServerEnvironment.changes(before, after).entrySet()) {
|
||||
if (edit.getValue() == null) Os.unsetenv(edit.getKey());
|
||||
else Os.setenv(edit.getKey(), edit.getValue(), true);
|
||||
}
|
||||
} catch (ErrnoException error) {
|
||||
fail("could not set the server environment: " + error);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
System.loadLibrary("MobileGL");
|
||||
nativeInstallDisplay();
|
||||
} catch (Throwable error) {
|
||||
// UnsatisfiedLinkError: an APK whose libMobileGL is not a split build has no server.
|
||||
fail("libMobileGL has no in-process display server: " + error);
|
||||
return false;
|
||||
}
|
||||
displayInstalled = true;
|
||||
serverConfiguration = configuration;
|
||||
Log.i(TAG, "starting the in-process display server on " + listen + " (pid=" + Process.myPid()
|
||||
+ (backend.isEmpty() ? ", backend pinned by the first session" : ", backend " + backend) + ")");
|
||||
serverThread = new Thread(() -> serve(listen), "mgl-display-server");
|
||||
serverThread.start();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void serve(String listen) {
|
||||
int result = nativeServe(listen);
|
||||
Log.i(TAG, "in-process display server on " + listen + " returned " + result
|
||||
+ (result == 0 ? " (stopped)" : " (failed: see the MobileGL lines before this one)"));
|
||||
displayInstalled = false;
|
||||
nativeUninstallDisplay();
|
||||
synchronized (serverLock) {
|
||||
serverReturned = true;
|
||||
}
|
||||
mainHandler.post(() -> onServerReturned(result));
|
||||
}
|
||||
|
||||
private static void onServerReturned(int result) {
|
||||
MobileGLDisplayActivity activity = current;
|
||||
if (activity != null && !activity.isFinishing()) {
|
||||
// A display without a server shows nothing anyone can reach: say so and go.
|
||||
activity.showStatus("MobileGL display server exited " + result);
|
||||
activity.finish(); // onDestroy ends the process
|
||||
return;
|
||||
}
|
||||
endProcess();
|
||||
}
|
||||
|
||||
private static void endProcess() {
|
||||
Log.i(TAG, "the display server's process ends (one server per :mglwin launch)");
|
||||
Process.killProcess(Process.myPid());
|
||||
}
|
||||
|
||||
private void fail(String message) {
|
||||
Log.e(TAG, message);
|
||||
showStatus(message);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void showStatus(String message) {
|
||||
statusView.setText(message);
|
||||
statusView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void hideSystemBars() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
}
|
||||
|
||||
// ---- the window ----------------------------------------------------------------------------
|
||||
|
||||
private final SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
Rect frame = holder.getSurfaceFrame();
|
||||
attach(holder, frame.width(), frame.height(), "surfaceCreated");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
attach(holder, width, height, "surfaceChanged");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
if (!displayInstalled) return;
|
||||
long started = SystemClock.uptimeMillis();
|
||||
// BLOCKS (bounded) until the session rendering into this window has let it go.
|
||||
String detached = nativeDetachWindow();
|
||||
Log.i(TAG, "surfaceDestroyed: server window detached (" + detached + ") in "
|
||||
+ (SystemClock.uptimeMillis() - started) + " ms");
|
||||
}
|
||||
};
|
||||
|
||||
private void attach(SurfaceHolder holder, int width, int height, String why) {
|
||||
if (!displayInstalled) return;
|
||||
Surface surface = holder.getSurface();
|
||||
if (surface == null || !surface.isValid()) return;
|
||||
boolean attached = nativeAttachWindow(surface, width, height);
|
||||
Log.i(TAG, why + ": server window " + width + "x" + height + (attached ? " attached" : " NOT attached"));
|
||||
}
|
||||
|
||||
// Called from native (DisplayServerJni.cpp) on the server's APPLY thread while a ServerOwned
|
||||
// window surface is created: post to the UI thread and return - never block there.
|
||||
@SuppressWarnings("unused")
|
||||
private static void onGeometryRequest(int width, int height) {
|
||||
mainHandler.post(() -> {
|
||||
requestedWidth = Math.max(0, width);
|
||||
requestedHeight = Math.max(0, height);
|
||||
Log.i(TAG, "geometry request " + requestedWidth + "x" + requestedHeight
|
||||
+ (requestedWidth > 0 && requestedHeight > 0 ? " (setFixedSize, aspect-fit)" : " (setSizeFromLayout)"));
|
||||
MobileGLDisplayActivity activity = current;
|
||||
if (activity != null) activity.applyGeometry();
|
||||
});
|
||||
}
|
||||
|
||||
private void applyGeometry() {
|
||||
SurfaceHolder holder = surfaceView.getHolder();
|
||||
if (requestedWidth > 0 && requestedHeight > 0) holder.setFixedSize(requestedWidth, requestedHeight);
|
||||
else holder.setSizeFromLayout();
|
||||
applyLayout();
|
||||
}
|
||||
|
||||
private void applyLayout() {
|
||||
int[] fit = ServerEnvironment.aspectFit(container.getWidth(), container.getHeight(),
|
||||
requestedWidth, requestedHeight);
|
||||
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) surfaceView.getLayoutParams();
|
||||
int width = fit[0] > 0 ? fit[0] : ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
int height = fit[1] > 0 ? fit[1] : ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
if (params.width != width || params.height != height || params.gravity != Gravity.CENTER) {
|
||||
params.width = width;
|
||||
params.height = height;
|
||||
params.gravity = Gravity.CENTER;
|
||||
surfaceView.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Trace-only entry point for a TCP supervisor; EGL belongs to its session children. */
|
||||
public final class MobileGLServerService extends Service {
|
||||
private static final String TAG = "MobileGLServer";
|
||||
private static final String CHANNEL = "mobilegl-server";
|
||||
// P12 (D8): the on-screen display server's process. Only one server is active on the device.
|
||||
private static final long DISPLAY_SERVER_EXIT_WAIT_MS = 5000;
|
||||
private volatile Process supervisor;
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
|
||||
@Override public IBinder onBind(Intent intent) { return null; }
|
||||
|
||||
@Override public void onCreate() {
|
||||
super.onCreate();
|
||||
NotificationManager notifications = getSystemService(NotificationManager.class);
|
||||
notifications.createNotificationChannel(new NotificationChannel(
|
||||
CHANNEL, "MobileGL TCP server", NotificationManager.IMPORTANCE_LOW));
|
||||
startForeground(40613, new Notification.Builder(this, CHANNEL)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||
.setContentTitle("MobileGL TCP server")
|
||||
.setContentText("Serving remote rendering sessions")
|
||||
.setOngoing(true).build());
|
||||
wakeLock = getSystemService(PowerManager.class).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "MobileGL:TCPServer");
|
||||
wakeLock.acquire();
|
||||
}
|
||||
|
||||
// P12 (D8): ONLY ONE SERVER IS ACTIVE ON THE DEVICE. The on-screen display server runs in the
|
||||
// display Activity's process (:mglwin) and listens on the same port; before the offscreen
|
||||
// supervisor is exec'd, that Activity's task is removed (so it is not restarted from recents)
|
||||
// and its process is killed - same uid, so Process.killProcess may - and waited out, bounded,
|
||||
// so its listener is gone before the supervisor binds.
|
||||
private void stopDisplayServer() {
|
||||
ActivityManager activities = getSystemService(ActivityManager.class);
|
||||
ComponentName display = new ComponentName(this, MobileGLDisplayActivity.class);
|
||||
try {
|
||||
for (ActivityManager.AppTask task : activities.getAppTasks()) {
|
||||
ActivityManager.RecentTaskInfo info = task.getTaskInfo();
|
||||
if (info != null && display.equals(info.baseIntent.getComponent())) {
|
||||
Log.i(TAG, "removing the on-screen display server's task (P12 D8: one server at a time)");
|
||||
task.finishAndRemoveTask();
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException error) {
|
||||
Log.w(TAG, "could not inspect the display server's task", error);
|
||||
}
|
||||
String processName = getPackageName() + ":mglwin";
|
||||
long deadline = SystemClock.uptimeMillis() + DISPLAY_SERVER_EXIT_WAIT_MS;
|
||||
int killed = -1;
|
||||
for (;;) {
|
||||
int pid = -1;
|
||||
List<ActivityManager.RunningAppProcessInfo> processes = activities.getRunningAppProcesses();
|
||||
if (processes != null) {
|
||||
for (ActivityManager.RunningAppProcessInfo process : processes) {
|
||||
if (processName.equals(process.processName)) pid = process.pid;
|
||||
}
|
||||
}
|
||||
// AMS forgets a dead process a moment after it is gone; /proc is the ground truth.
|
||||
if (pid < 0 || !new File("/proc/" + pid).exists()) {
|
||||
if (killed > 0) Log.i(TAG, "on-screen display server " + processName + " pid=" + killed + " is gone");
|
||||
return;
|
||||
}
|
||||
if (pid != killed) {
|
||||
Log.i(TAG, "stopping the on-screen display server " + processName + " pid=" + pid
|
||||
+ " (P12 D8: one server at a time)");
|
||||
android.os.Process.killProcess(pid);
|
||||
killed = pid;
|
||||
}
|
||||
if (SystemClock.uptimeMillis() >= deadline) {
|
||||
Log.e(TAG, "on-screen display server " + processName + " pid=" + pid + " still exists after "
|
||||
+ DISPLAY_SERVER_EXIT_WAIT_MS + " ms; starting the supervisor anyway");
|
||||
return;
|
||||
}
|
||||
SystemClock.sleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public synchronized int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (supervisor != null && supervisor.isAlive()) {
|
||||
Log.w(TAG, "supervisor already running; stop the service before reconfiguring");
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
stopDisplayServer();
|
||||
String endpoint = intent == null ? null : intent.getStringExtra("listen");
|
||||
if (endpoint == null) endpoint = "tcp://127.0.0.1:40613";
|
||||
try {
|
||||
File executable = new File(getApplicationInfo().nativeLibraryDir, "libMobileGLServer.so");
|
||||
ProcessBuilder builder = new ProcessBuilder(executable.getAbsolutePath(), endpoint, "--serve");
|
||||
builder.directory(getFilesDir()).redirectErrorStream(true);
|
||||
Map<String, String> env = builder.environment();
|
||||
// The replay runner's KEY=VALUE;KEY grammar (unset and empty values included) and the
|
||||
// server role's forced keys, shared with MobileGLDisplayActivity (P12 D7).
|
||||
ServerEnvironment.applyServerRole(env,
|
||||
intent == null ? null : intent.getStringExtra("env"),
|
||||
intent == null ? null : intent.getStringExtra("token"),
|
||||
/*backend=*/null,
|
||||
new File(getFilesDir(), "mgl.log").getAbsolutePath());
|
||||
env.put("LD_LIBRARY_PATH", getApplicationInfo().nativeLibraryDir);
|
||||
supervisor = builder.start();
|
||||
final Process child = supervisor;
|
||||
new Thread(() -> {
|
||||
try (BufferedReader output = new BufferedReader(new InputStreamReader(child.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = output.readLine()) != null) Log.i(TAG, line);
|
||||
Log.i(TAG, "supervisor exited " + child.waitFor());
|
||||
} catch (java.io.InterruptedIOException stopped) {
|
||||
// onDestroy's Process.destroy() closes this stream under the read: the service
|
||||
// is being stopped (P12 D8: the display Activity stops it), not failing.
|
||||
Log.i(TAG, "supervisor output closed: the service is stopping");
|
||||
} catch (Exception error) {
|
||||
Log.e(TAG, "supervisor output failed", error);
|
||||
} finally {
|
||||
stopSelf(startId);
|
||||
}
|
||||
}, "mgl-supervisor-log").start();
|
||||
} catch (Exception error) {
|
||||
Log.e(TAG, "cannot start TCP supervisor", error);
|
||||
stopSelf(startId);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override public void onDestroy() {
|
||||
Process child = supervisor;
|
||||
if (child != null) child.destroy();
|
||||
if (wakeLock != null && wakeLock.isHeld()) wakeLock.release();
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -1,431 +0,0 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Control screen for the render server, in either shape: the offscreen supervisor
|
||||
* ({@link MobileGLServerService}) or the on-screen display server
|
||||
* ({@link MobileGLDisplayActivity}), which renders a client's window surface onto this
|
||||
* screen. Collects the shared {@code listen}/{@code token}/{@code env} extras and starts
|
||||
* or stops the selected one; no native code of its own. The endpoint is a tcp://host:port
|
||||
* for remote clients, or an @abstract name / filesystem path for on-device ones. One
|
||||
* server per device (P12 D8): the two shapes replace each other.
|
||||
*/
|
||||
public final class ServerControlActivity extends Activity {
|
||||
private static final String TAG = "MobileGLServerCtl";
|
||||
|
||||
private static final int COLOR_BACKGROUND = 0xFF121212;
|
||||
private static final int COLOR_TEXT = 0xFFEEEEEE;
|
||||
private static final int COLOR_PASS = 0xFF4CAF50;
|
||||
private static final int COLOR_WARN = 0xFFFF9800;
|
||||
private static final int COLOR_FAIL = 0xFFF44336;
|
||||
private static final int COLOR_INFO = 0xFF9E9E9E;
|
||||
|
||||
private static final String DEFAULT_ENDPOINT = "tcp://0.0.0.0:40613";
|
||||
private static final int MIN_TOKEN_BYTES = 16;
|
||||
private static final int PROBE_ATTEMPTS = 10;
|
||||
private static final long PROBE_INTERVAL_MS = 500;
|
||||
private static final int PROBE_TIMEOUT_MS = 500;
|
||||
|
||||
private EditText listenField;
|
||||
private EditText tokenField;
|
||||
private EditText envField;
|
||||
private CheckBox onScreenBox;
|
||||
private Button startButton;
|
||||
private Button stopButton;
|
||||
private TextView statusView;
|
||||
private volatile Thread probeThread;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
LinearLayout contentLayout = new LinearLayout(this);
|
||||
contentLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
int padding = dp(16);
|
||||
contentLayout.setPadding(padding, padding, padding, padding);
|
||||
|
||||
ScrollView scrollView = new ScrollView(this);
|
||||
scrollView.setBackgroundColor(COLOR_BACKGROUND);
|
||||
scrollView.setFillViewport(true);
|
||||
scrollView.addView(contentLayout);
|
||||
setContentView(NavBar.wrap(this, scrollView, NavBar.TAB_SERVER));
|
||||
|
||||
contentLayout.addView(makeText("MobileGL Render Server", 20, COLOR_TEXT, true));
|
||||
|
||||
TextView hint = makeText(
|
||||
"Starts the render server. Offscreen (default): the supervisor "
|
||||
+ "(libMobileGLServer.so --serve) runs as a foreground service. On-screen "
|
||||
+ "window: the server runs in a display Activity that renders the client's "
|
||||
+ "frames onto this screen (clients opt in with MOBILEGL_IPC_SURFACE=server). "
|
||||
+ "Endpoint forms: tcp://host:port for remote clients, @name for an "
|
||||
+ "on-device abstract socket, or a filesystem path (relative to the app's "
|
||||
+ "files dir) for on-device clients. Non-loopback TCP requires a token of at "
|
||||
+ "least " + MIN_TOKEN_BYTES + " bytes; unix endpoints need none. Extra env "
|
||||
+ "uses the KEY=VALUE;KEY grammar (e.g. MOBILEGL_BACKEND_TYPE=DirectVulkan). "
|
||||
+ "One server per device: the two modes replace each other.",
|
||||
12, COLOR_INFO, false);
|
||||
LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
hintParams.topMargin = dp(8);
|
||||
contentLayout.addView(hint, hintParams);
|
||||
|
||||
listenField = addField(contentLayout, "Listen endpoint", DEFAULT_ENDPOINT, dp(16));
|
||||
tokenField = addField(contentLayout, "Auth token (empty = loopback only)", "", dp(8));
|
||||
envField = addField(contentLayout, "Extra env (optional)", "", dp(8));
|
||||
|
||||
onScreenBox = new CheckBox(this);
|
||||
onScreenBox.setText("On-screen window (render client frames onto this screen)");
|
||||
onScreenBox.setTextColor(COLOR_TEXT);
|
||||
onScreenBox.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||
LinearLayout.LayoutParams boxParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
boxParams.topMargin = dp(8);
|
||||
contentLayout.addView(onScreenBox, boxParams);
|
||||
|
||||
LinearLayout buttons = new LinearLayout(this);
|
||||
buttons.setOrientation(LinearLayout.HORIZONTAL);
|
||||
LinearLayout.LayoutParams buttonsParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
buttonsParams.topMargin = dp(16);
|
||||
contentLayout.addView(buttons, buttonsParams);
|
||||
|
||||
startButton = new Button(this);
|
||||
startButton.setText("Start server");
|
||||
startButton.setAllCaps(false);
|
||||
startButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||
startButton.setOnClickListener(v -> startServer());
|
||||
buttons.addView(startButton);
|
||||
|
||||
stopButton = new Button(this);
|
||||
stopButton.setText("Stop server");
|
||||
stopButton.setAllCaps(false);
|
||||
stopButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||
LinearLayout.LayoutParams stopParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
stopParams.leftMargin = dp(8);
|
||||
buttons.addView(stopButton, stopParams);
|
||||
stopButton.setOnClickListener(v -> stopServer());
|
||||
|
||||
statusView = makeText("", 13, COLOR_INFO, false);
|
||||
LinearLayout.LayoutParams statusParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
statusParams.topMargin = dp(12);
|
||||
contentLayout.addView(statusView, statusParams);
|
||||
|
||||
if (isServerRunning()) {
|
||||
setStatus("A server is running (started outside this screen).", COLOR_WARN);
|
||||
} else {
|
||||
setStatus("Stopped.", COLOR_INFO);
|
||||
}
|
||||
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent extras prefill the form, so adb can drive the screen without the IME:
|
||||
* --es listen @name --es token T --es env "K=V;K" --ez onscreen true --ez start true
|
||||
* (--ez stop true instead stops). This mirrors how every other entry point in this
|
||||
* APK is already driven by extras.
|
||||
*/
|
||||
private void handleIntent(Intent intent) {
|
||||
if (intent != null) {
|
||||
String prefillListen = intent.getStringExtra("listen");
|
||||
if (prefillListen != null) listenField.setText(prefillListen);
|
||||
String prefillToken = intent.getStringExtra("token");
|
||||
if (prefillToken != null) tokenField.setText(prefillToken);
|
||||
String prefillEnv = intent.getStringExtra("env");
|
||||
if (prefillEnv != null) envField.setText(prefillEnv);
|
||||
if (intent.getBooleanExtra("onscreen", false)) onScreenBox.setChecked(true);
|
||||
if (intent.getBooleanExtra("stop", false)) stopServer();
|
||||
else if (intent.getBooleanExtra("start", false)) startServer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Thread probe = probeThread;
|
||||
if (probe != null) probe.interrupt();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void startServer() {
|
||||
String endpoint = listenField.getText().toString().trim();
|
||||
String token = tokenField.getText().toString();
|
||||
String env = envField.getText().toString().trim();
|
||||
|
||||
String probeDisplay;
|
||||
Probe probe;
|
||||
if (endpoint.startsWith("tcp://")) {
|
||||
String[] hostPort = parseTcpEndpoint(endpoint);
|
||||
if (hostPort == null) {
|
||||
setStatus("Invalid TCP endpoint: expected tcp://host:port", COLOR_FAIL);
|
||||
return;
|
||||
}
|
||||
if (!isLoopback(hostPort[0]) && token.getBytes().length < MIN_TOKEN_BYTES) {
|
||||
setStatus("Non-loopback listen requires a token of at least "
|
||||
+ MIN_TOKEN_BYTES + " bytes.", COLOR_FAIL);
|
||||
return;
|
||||
}
|
||||
final String probeHost = isWildcard(hostPort[0]) ? "127.0.0.1" : hostPort[0];
|
||||
final int port;
|
||||
try {
|
||||
port = Integer.parseInt(hostPort[1]);
|
||||
} catch (NumberFormatException error) {
|
||||
setStatus("Invalid port in endpoint.", COLOR_FAIL);
|
||||
return;
|
||||
}
|
||||
probeDisplay = probeHost + ":" + port;
|
||||
probe = () -> {
|
||||
try (Socket socket = new Socket()) {
|
||||
socket.connect(new InetSocketAddress(probeHost, port), PROBE_TIMEOUT_MS);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
String invalid = validateUnixEndpoint(endpoint);
|
||||
if (invalid != null) {
|
||||
setStatus(invalid, COLOR_FAIL);
|
||||
return;
|
||||
}
|
||||
final boolean abstractName = endpoint.startsWith("@");
|
||||
final String address = abstractName ? endpoint.substring(1)
|
||||
: endpoint.startsWith("/") ? endpoint
|
||||
: new File(getFilesDir(), endpoint).getAbsolutePath();
|
||||
final LocalSocketAddress.Namespace namespace = abstractName
|
||||
? LocalSocketAddress.Namespace.ABSTRACT : LocalSocketAddress.Namespace.FILESYSTEM;
|
||||
probeDisplay = endpoint;
|
||||
probe = () -> {
|
||||
LocalSocket socket = new LocalSocket();
|
||||
try {
|
||||
socket.connect(new LocalSocketAddress(address, namespace));
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
if (isServerRunning()) {
|
||||
setStatus("A server is already running; stop it before reconfiguring.", COLOR_WARN);
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean onScreen = onScreenBox.isChecked();
|
||||
Intent intent = new Intent(this,
|
||||
onScreen ? MobileGLDisplayActivity.class : MobileGLServerService.class);
|
||||
intent.putExtra("listen", endpoint);
|
||||
intent.putExtra("token", token);
|
||||
if (!env.isEmpty()) intent.putExtra("env", env);
|
||||
try {
|
||||
// The display Activity runs the server in its own process (:mglwin) and shows the
|
||||
// client's window surface; launching it brings that window to the front.
|
||||
if (onScreen) startActivity(intent);
|
||||
else startForegroundService(intent);
|
||||
} catch (RuntimeException error) {
|
||||
Log.e(TAG, "cannot start the render server", error);
|
||||
setStatus("start failed: " + error, COLOR_FAIL);
|
||||
return;
|
||||
}
|
||||
setStatus("Starting " + endpoint + (onScreen ? " (on-screen)" : "") + " ...", COLOR_INFO);
|
||||
probeUntilListening(probeDisplay, probe);
|
||||
}
|
||||
|
||||
/**
|
||||
* unix endpoint check: an @abstract name or a filesystem path (relative paths resolve
|
||||
* against the app's files dir, which is the service's working directory). Returns the
|
||||
* problem, or null when the endpoint is usable. No token: an AF_UNIX address is reachable
|
||||
* only from this device, and an abstract name only from this app.
|
||||
*/
|
||||
private String validateUnixEndpoint(String endpoint) {
|
||||
if (endpoint.isEmpty()) {
|
||||
return "Endpoint is empty.";
|
||||
}
|
||||
if (endpoint.contains("://")) {
|
||||
return "Unknown endpoint scheme; use tcp://host:port, @name or a filesystem path.";
|
||||
}
|
||||
String resolved = endpoint.startsWith("@") || endpoint.startsWith("/") ? endpoint
|
||||
: new File(getFilesDir(), endpoint).getAbsolutePath();
|
||||
// sun_path is 108 bytes on Linux/Android, and an abstract name drops the '@' into it.
|
||||
if (resolved.getBytes().length >= 108) {
|
||||
return "Endpoint is too long for an AF_UNIX address.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void stopServer() {
|
||||
Thread probe = probeThread;
|
||||
if (probe != null) probe.interrupt();
|
||||
boolean stopped = stopService(new Intent(this, MobileGLServerService.class));
|
||||
boolean displayKilled = killDisplayServer();
|
||||
setStatus(stopped || displayKilled ? "Stopped." : "Stop requested, but no server was running.",
|
||||
stopped || displayKilled ? COLOR_INFO : COLOR_WARN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills the on-screen server's process (:mglwin). Same uid, so killProcess is allowed;
|
||||
* this mirrors MobileGLServerService.stopDisplayServer for the reverse direction.
|
||||
*/
|
||||
private boolean killDisplayServer() {
|
||||
ActivityManager activities = getSystemService(ActivityManager.class);
|
||||
String displayProcess = getPackageName() + ":mglwin";
|
||||
List<ActivityManager.RunningAppProcessInfo> processes = activities.getRunningAppProcesses();
|
||||
if (processes == null) return false;
|
||||
for (ActivityManager.RunningAppProcessInfo process : processes) {
|
||||
if (displayProcess.equals(process.processName)) {
|
||||
android.os.Process.killProcess(process.pid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the supervisor bound its socket: connect to the endpoint until one attempt
|
||||
* succeeds. TCP probes go to loopback when the server listens on a wildcard address;
|
||||
* unix probes use LocalSocket. The server closes each unauthenticated probe after its
|
||||
* pre-auth timeout; at most {@link #PROBE_ATTEMPTS} probes are made, then the screen
|
||||
* reports whatever the service state is.
|
||||
*/
|
||||
private interface Probe {
|
||||
void connect() throws Exception;
|
||||
}
|
||||
|
||||
private void probeUntilListening(String display, Probe probe) {
|
||||
Thread thread = new Thread(() -> {
|
||||
for (int attempt = 1; attempt <= PROBE_ATTEMPTS; ++attempt) {
|
||||
if (Thread.currentThread().isInterrupted()) return;
|
||||
try {
|
||||
probe.connect();
|
||||
runOnUiThread(() -> setStatus("Listening on " + display, COLOR_PASS));
|
||||
return;
|
||||
} catch (Exception refused) {
|
||||
try {
|
||||
Thread.sleep(PROBE_INTERVAL_MS);
|
||||
} catch (InterruptedException stopped) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
if (isServerRunning()) {
|
||||
setStatus("Server running but " + display + " did not accept a probe; "
|
||||
+ "see logcat tags MobileGLServer/MobileGLDisplay.", COLOR_WARN);
|
||||
} else {
|
||||
setStatus("Server exited; see logcat tags MobileGLServer/MobileGLDisplay.",
|
||||
COLOR_FAIL);
|
||||
}
|
||||
});
|
||||
}, "mgl-server-probe");
|
||||
probeThread = thread;
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/** True when either server shape is up: the supervisor service or the :mglwin process. */
|
||||
private boolean isServerRunning() {
|
||||
ActivityManager activities = getSystemService(ActivityManager.class);
|
||||
List<ActivityManager.RunningServiceInfo> services =
|
||||
activities.getRunningServices(Integer.MAX_VALUE);
|
||||
if (services != null) {
|
||||
for (ActivityManager.RunningServiceInfo service : services) {
|
||||
if (MobileGLServerService.class.getName().equals(service.service.getClassName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
String displayProcess = getPackageName() + ":mglwin";
|
||||
List<ActivityManager.RunningAppProcessInfo> processes = activities.getRunningAppProcesses();
|
||||
if (processes != null) {
|
||||
for (ActivityManager.RunningAppProcessInfo process : processes) {
|
||||
if (displayProcess.equals(process.processName)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {host, port} for tcp://host:port, or null. */
|
||||
private static String[] parseTcpEndpoint(String endpoint) {
|
||||
if (endpoint == null || !endpoint.startsWith("tcp://")) return null;
|
||||
String hostPort = endpoint.substring("tcp://".length());
|
||||
int separator = hostPort.lastIndexOf(':');
|
||||
if (separator <= 0 || separator == hostPort.length() - 1) return null;
|
||||
return new String[]{hostPort.substring(0, separator), hostPort.substring(separator + 1)};
|
||||
}
|
||||
|
||||
private static boolean isLoopback(String host) {
|
||||
// A wildcard bind is reachable from the network, so the server holds it to the
|
||||
// non-loopback token requirement (AuthToken.h) just like a real interface address.
|
||||
return host.equals("127.0.0.1") || host.equals("localhost") || host.equals("::1");
|
||||
}
|
||||
|
||||
private static boolean isWildcard(String host) {
|
||||
return host.isEmpty() || host.equals("0.0.0.0") || host.equals("::") || host.equals("*");
|
||||
}
|
||||
|
||||
private EditText addField(LinearLayout parent, String label, String initial, int topMarginPx) {
|
||||
TextView labelView = makeText(label, 12, COLOR_INFO, false);
|
||||
LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
labelParams.topMargin = topMarginPx;
|
||||
parent.addView(labelView, labelParams);
|
||||
|
||||
EditText field = new EditText(this);
|
||||
field.setText(initial);
|
||||
field.setTextColor(COLOR_TEXT);
|
||||
field.setHintTextColor(COLOR_INFO);
|
||||
field.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||
field.setTypeface(Typeface.MONOSPACE);
|
||||
field.setSingleLine(true);
|
||||
field.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
||||
field.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
parent.addView(field, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
return field;
|
||||
}
|
||||
|
||||
private void setStatus(String message, int color) {
|
||||
Log.i(TAG, String.format(Locale.ROOT, "status: %s", message));
|
||||
statusView.setText(message);
|
||||
statusView.setTextColor(color);
|
||||
}
|
||||
|
||||
private TextView makeText(String text, int sizeSp, int color, boolean bold) {
|
||||
TextView view = new TextView(this);
|
||||
view.setText(text);
|
||||
view.setTextColor(color);
|
||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sizeSp);
|
||||
view.setTypeface(Typeface.MONOSPACE, bold ? Typeface.BOLD : Typeface.NORMAL);
|
||||
return view;
|
||||
}
|
||||
|
||||
private int dp(int value) {
|
||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The server role's environment, shared by the two servers the trace APK can run.
|
||||
*
|
||||
* <p>{@link MobileGLServerService} exec's the offscreen supervisor with it (a ProcessBuilder
|
||||
* environment); {@link MobileGLDisplayActivity} applies it to its OWN process with Os.setenv /
|
||||
* Os.unsetenv before libMobileGL is loaded, because the on-screen display server runs in that
|
||||
* process (P12 D7). One grammar and one set of forced keys for both, so the two servers cannot
|
||||
* drift apart on what an {@code env} extra means.
|
||||
*
|
||||
* <p>No android.* import: tools/trace_replay/test_android_lifecycle.py compiles and runs this class
|
||||
* with plain javac (ServerEnvironmentTest), which is the only place it runs outside a device.
|
||||
*/
|
||||
public final class ServerEnvironment {
|
||||
private ServerEnvironment() {}
|
||||
|
||||
public static final String BACKEND = "MOBILEGL_BACKEND_TYPE";
|
||||
public static final String TOKEN = "MOBILEGL_IPC_TOKEN";
|
||||
public static final String LOG_FILE_PATH = "MOBILEGL_LOG_FILE_PATH";
|
||||
|
||||
/**
|
||||
* What the server must never inherit: a transport (it would dial instead of serving), the
|
||||
* client's endpoint knobs, and the spawn arm's sizing (mobilegl_server_main's scrub list, exit
|
||||
* 65, plus the two endpoint names the listen extra replaces).
|
||||
*/
|
||||
static final String[] SCRUBBED = {
|
||||
"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_CONTROL", "MOBILEGL_IPC_ENDPOINT",
|
||||
"MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB", "MOBILEGL_IPC_STAGE_MB",
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies the replay runner's {@code KEY=VALUE;KEY} grammar to {@code env}: {@code K=V} sets
|
||||
* (an empty V is a set, not an unset), a bare {@code K} unsets, empty entries and entries with
|
||||
* an empty key are skipped. The last entry for a key wins.
|
||||
*/
|
||||
public static void apply(Map<String, String> env, String overrides) {
|
||||
if (overrides == null) return;
|
||||
for (String entry : overrides.split(";", -1)) {
|
||||
int separator = entry.indexOf('=');
|
||||
if (entry.isEmpty() || separator == 0) continue;
|
||||
if (separator < 0) env.remove(entry);
|
||||
else env.put(entry.substring(0, separator), entry.substring(separator + 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns {@code env} into the server role's environment, in place: the {@code env} extra first,
|
||||
* then an explicit backend pin (it wins over the extra's), then the keys every server forces -
|
||||
* ROLE=server, DIAL=no, LOG_FORWARD=1, the scrub list removed, the token (empty when none) -
|
||||
* and a log file path when none was given.
|
||||
*/
|
||||
public static void applyServerRole(Map<String, String> env, String overrides, String token, String backend,
|
||||
String defaultLogPath) {
|
||||
apply(env, overrides);
|
||||
if (backend != null && !backend.isEmpty()) env.put(BACKEND, backend);
|
||||
env.put("MOBILEGL_IPC_ROLE", "server");
|
||||
env.put("MOBILEGL_IPC_DIAL", "no");
|
||||
env.put("MOBILEGL_IPC_LOG_FORWARD", "1");
|
||||
for (String name : SCRUBBED) env.remove(name);
|
||||
env.put(TOKEN, token == null ? "" : token);
|
||||
if (defaultLogPath != null) env.putIfAbsent(LOG_FILE_PATH, defaultLogPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* The edits that turn {@code before} into {@code after}, in a stable order: a key whose value
|
||||
* is new or different maps to that value, a key {@code after} dropped maps to null (unset).
|
||||
* The display Activity applies exactly these with Os.setenv / Os.unsetenv.
|
||||
*/
|
||||
public static Map<String, String> changes(Map<String, String> before, Map<String, String> after) {
|
||||
Map<String, String> edits = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> entry : new java.util.TreeMap<>(after).entrySet()) {
|
||||
if (!entry.getValue().equals(before.get(entry.getKey()))) edits.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
for (String name : new java.util.TreeSet<>(before.keySet())) {
|
||||
if (!after.containsKey(name)) edits.put(name, null);
|
||||
}
|
||||
return edits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aspect-fit (letterbox / pillarbox): the largest {@code bufferWidth:bufferHeight} rectangle
|
||||
* inside a {@code containerWidth x containerHeight} view, as {width, height}. {-1, -1} (fill the
|
||||
* container, MATCH_PARENT) when either size is unknown or no fixed buffer size is set.
|
||||
*/
|
||||
public static int[] aspectFit(int containerWidth, int containerHeight, int bufferWidth, int bufferHeight) {
|
||||
if (containerWidth <= 0 || containerHeight <= 0 || bufferWidth <= 0 || bufferHeight <= 0) {
|
||||
return new int[]{-1, -1};
|
||||
}
|
||||
long wide = (long) containerWidth * bufferHeight;
|
||||
long tall = (long) containerHeight * bufferWidth;
|
||||
if (wide <= tall) {
|
||||
// The container is narrower than the buffer: full width, bars above and below.
|
||||
return new int[]{containerWidth, (int) Math.max(1, wide / bufferWidth)};
|
||||
}
|
||||
// The container is wider: full height, bars left and right.
|
||||
return new int[]{(int) Math.max(1, tall / bufferHeight), containerHeight};
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- CONFIG_ASSETS_PATHS (0x80000000) exists in Android 34, but that SDK has no
|
||||
public configChanges flag name for it. Package parsing resolves this integer
|
||||
through TypedArray.getInt. Keep it unqualified: overlay installation must not
|
||||
recreate the Activity and destroy the live native render Surface.
|
||||
0x0f80 = orientation | screenLayout | uiMode | screenSize | smallestScreenSize,
|
||||
matching PluginActivity. Trace framebuffer dimensions are request parameters,
|
||||
and these configuration changes need no new view or native replay. -->
|
||||
<integer name="trace_replay_config_changes">0x80000f80</integer>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user