mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[android-plugin, tools] (P12): MobileGLDisplayActivity - the on-screen display server in :mglwin (software window, fullscreen SurfaceView, aspect-fit geometry, env applied before the library loads), one shared server environment with MobileGLServerService, and one server at a time (the Activity stops the service, the service kills :mglwin)
This commit is contained in:
@@ -13,6 +13,21 @@
|
||||
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" />
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
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,23 +1,29 @@
|
||||
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;
|
||||
|
||||
@@ -38,14 +44,53 @@ public final class MobileGLServerService extends Service {
|
||||
wakeLock.acquire();
|
||||
}
|
||||
|
||||
// Keep the replay runner's KEY=VALUE;KEY grammar, including unset and empty values.
|
||||
static void applyEnvironment(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));
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +99,7 @@ public final class MobileGLServerService extends Service {
|
||||
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 {
|
||||
@@ -61,17 +107,13 @@ public final class MobileGLServerService extends Service {
|
||||
ProcessBuilder builder = new ProcessBuilder(executable.getAbsolutePath(), endpoint, "--serve");
|
||||
builder.directory(getFilesDir()).redirectErrorStream(true);
|
||||
Map<String, String> env = builder.environment();
|
||||
applyEnvironment(env, intent == null ? null : intent.getStringExtra("env"));
|
||||
env.put("MOBILEGL_IPC_ROLE", "server");
|
||||
env.put("MOBILEGL_IPC_DIAL", "no");
|
||||
env.put("MOBILEGL_IPC_LOG_FORWARD", "1");
|
||||
for (String name : new String[]{"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_CONTROL", "MOBILEGL_IPC_ENDPOINT",
|
||||
"MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB", "MOBILEGL_IPC_STAGE_MB"}) {
|
||||
env.remove(name);
|
||||
}
|
||||
env.put("MOBILEGL_IPC_TOKEN", intent == null || intent.getStringExtra("token") == null
|
||||
? "" : intent.getStringExtra("token"));
|
||||
env.putIfAbsent("MOBILEGL_LOG_FILE_PATH", new File(getFilesDir(), "mgl.log").getAbsolutePath());
|
||||
// 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;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Host-side checks for ServerEnvironment: the server role's environment and the display's
|
||||
* aspect-fit, shared by MobileGLServerService (the offscreen supervisor's exec environment) and
|
||||
* MobileGLDisplayActivity (the on-screen display server's own process, P12 D7).
|
||||
*
|
||||
* <p>On a device a defect here reads as "the knob had no effect" or "the server refused to start"
|
||||
* (a scrubbed key left in: exit 65), so it is exercised here, by tools/trace_replay/
|
||||
* test_android_lifecycle.py with plain javac. Assert-free for the same reason as
|
||||
* SpawnServerPathTest: -ea is off by default, and a check compiled away checks nothing.
|
||||
*/
|
||||
public final class ServerEnvironmentTest {
|
||||
private static int failures = 0;
|
||||
|
||||
private static void expect(String what, Object actual, Object expected) {
|
||||
if (expected == null ? actual == null : expected.equals(actual)) return;
|
||||
++failures;
|
||||
System.err.println(what + "\n got: [" + actual + "]\n expected: [" + expected + "]");
|
||||
}
|
||||
|
||||
private static Map<String, String> map(String... keyValues) {
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (int i = 0; i < keyValues.length; i += 2) result.put(keyValues[i], keyValues[i + 1]);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// ---- the K=V;K grammar (the service's, unchanged) -------------------------------------------
|
||||
Map<String, String> env = new HashMap<>(map("B", "old", "E", "keep"));
|
||||
ServerEnvironment.apply(env, "A=1;B;C=;=x;;D=4;A=2");
|
||||
expect("K=V sets, a bare K unsets, K= sets empty, =x and empty entries are skipped, the last wins",
|
||||
env, new HashMap<>(map("A", "2", "C", "", "D", "4", "E", "keep")));
|
||||
Map<String, String> untouched = new HashMap<>(map("A", "1"));
|
||||
ServerEnvironment.apply(untouched, null);
|
||||
expect("no env extra changes nothing", untouched, new HashMap<>(map("A", "1")));
|
||||
|
||||
// ---- the server role ----------------------------------------------------------------------
|
||||
Map<String, String> launcher = new HashMap<>(map(
|
||||
"MOBILEGL_TRANSPORT", "spawn",
|
||||
"MOBILEGL_IPC_CONTROL", "tcp://10.0.0.2:40613",
|
||||
"MOBILEGL_IPC_ENDPOINT", "tcp://x",
|
||||
"MOBILEGL_IPC_DIAL", "yes",
|
||||
"PATH", "/system/bin"));
|
||||
ServerEnvironment.applyServerRole(launcher,
|
||||
"MOBILEGL_IPC_ROLE=client;MOBILEGL_BACKEND_TYPE=DirectGLES;MOBILEGL_IPC_RING_MB=8;"
|
||||
+ "MOBILEGL_IPC_SERVER_PATH=/x;MOBILEGL_IPC_STAGE_MB=4;FOO=bar",
|
||||
"a-token-of-sixteen-bytes", "DirectVulkan", "/data/files/mglwin.log");
|
||||
expect("the server role: forced keys win over the env extra, the scrub list is gone, the backend "
|
||||
+ "extra pins over the env's, the token and the default log path are set",
|
||||
launcher, new HashMap<>(map(
|
||||
"PATH", "/system/bin",
|
||||
"FOO", "bar",
|
||||
"MOBILEGL_BACKEND_TYPE", "DirectVulkan",
|
||||
"MOBILEGL_IPC_ROLE", "server",
|
||||
"MOBILEGL_IPC_DIAL", "no",
|
||||
"MOBILEGL_IPC_LOG_FORWARD", "1",
|
||||
"MOBILEGL_IPC_TOKEN", "a-token-of-sixteen-bytes",
|
||||
"MOBILEGL_LOG_FILE_PATH", "/data/files/mglwin.log")));
|
||||
Map<String, String> service = new HashMap<>();
|
||||
ServerEnvironment.applyServerRole(service, "MOBILEGL_BACKEND_TYPE=DirectGLES;MOBILEGL_LOG_FILE_PATH=/sdcard/x.log",
|
||||
null, null, "/data/files/mgl.log");
|
||||
expect("the service's shape: no token is an empty token, no backend extra keeps the env's, an env "
|
||||
+ "log path is kept",
|
||||
service, new HashMap<>(map(
|
||||
"MOBILEGL_BACKEND_TYPE", "DirectGLES",
|
||||
"MOBILEGL_IPC_ROLE", "server",
|
||||
"MOBILEGL_IPC_DIAL", "no",
|
||||
"MOBILEGL_IPC_LOG_FORWARD", "1",
|
||||
"MOBILEGL_IPC_TOKEN", "",
|
||||
"MOBILEGL_LOG_FILE_PATH", "/sdcard/x.log")));
|
||||
|
||||
// ---- the Os.setenv / Os.unsetenv edits the display Activity applies ---------------------------
|
||||
Map<String, String> edits = ServerEnvironment.changes(
|
||||
map("A", "1", "B", "2", "C", "3", "MOBILEGL_TRANSPORT", "spawn"),
|
||||
map("A", "1", "B", "5", "D", "4", "E", ""));
|
||||
expect("changed and new keys are set (sorted), dropped keys are unset (null), equal keys are left",
|
||||
new java.util.ArrayList<>(edits.entrySet()).toString(),
|
||||
"[B=5, D=4, E=, C=null, MOBILEGL_TRANSPORT=null]");
|
||||
|
||||
// ---- aspect-fit ----------------------------------------------------------------------------------
|
||||
expect("a portrait screen letterboxes a 640x480 buffer: full width",
|
||||
java.util.Arrays.toString(ServerEnvironment.aspectFit(1080, 2400, 640, 480)), "[1080, 810]");
|
||||
expect("a landscape screen pillarboxes it: full height",
|
||||
java.util.Arrays.toString(ServerEnvironment.aspectFit(2400, 1080, 640, 480)), "[1440, 1080]");
|
||||
expect("the same aspect fills the view",
|
||||
java.util.Arrays.toString(ServerEnvironment.aspectFit(1280, 960, 640, 480)), "[1280, 960]");
|
||||
expect("no fixed size (0/0: the layout's own) fills the view",
|
||||
java.util.Arrays.toString(ServerEnvironment.aspectFit(1080, 2400, 0, 0)), "[-1, -1]");
|
||||
expect("an unmeasured view fills the view",
|
||||
java.util.Arrays.toString(ServerEnvironment.aspectFit(0, 0, 640, 480)), "[-1, -1]");
|
||||
|
||||
if (failures != 0) {
|
||||
System.err.println("ServerEnvironmentTest: " + failures + " failure(s)");
|
||||
System.exit(1);
|
||||
}
|
||||
System.out.println("ServerEnvironmentTest: all checks passed");
|
||||
}
|
||||
}
|
||||
@@ -25,22 +25,41 @@ def main():
|
||||
raise RuntimeError("TraceReplayActivity must handle CONFIG_ASSETS_PATHS")
|
||||
if changes & 0xF80 != 0xF80:
|
||||
raise RuntimeError("TraceReplayActivity must retain the fixed-size render surface on display changes")
|
||||
# P12 (on-screen server window), D7: the display Activity. Each attribute below is load-bearing
|
||||
# and none of them is visible to a build: its own process (the in-process server's environment,
|
||||
# backend pin and EGL teardown stay out of every other process of the package, and the service
|
||||
# finds it by this name to enforce one server at a time), software window rendering (HWUI must
|
||||
# not share the EGL display Espryt terminates), and the same configChanges as the replay
|
||||
# Activity (a rotation or an overlay must not destroy the window a session renders into).
|
||||
display = next((a for a in manifest.findall("application/activity")
|
||||
if a.get(android + "name") == ".MobileGLDisplayActivity"), None)
|
||||
if display is None:
|
||||
raise RuntimeError("the trace manifest has no MobileGLDisplayActivity (P12 D7)")
|
||||
for attribute, expected in (("process", ":mglwin"), ("hardwareAccelerated", "false"),
|
||||
("exported", "true"),
|
||||
("configChanges", activity.get(android + "configChanges"))):
|
||||
if display.get(android + attribute) != expected:
|
||||
raise RuntimeError(f"MobileGLDisplayActivity android:{attribute} must be {expected!r}, "
|
||||
f"not {display.get(android + attribute)!r}")
|
||||
# Every trace-flavour Java class that has NO android.* import belongs here, because javac
|
||||
# plus `java` is the only way any of them is exercised outside an APK on a device. P7 added
|
||||
# SpawnServerPath: a hand-rolled K=V;K=V parse whose failure mode on device is "the knob had
|
||||
# no effect", which is why a defect in it survived from P6.
|
||||
# no effect", which is why a defect in it survived from P6. P12 added ServerEnvironment: the
|
||||
# server role's environment both servers build (the service's exec environment, the display
|
||||
# Activity's own process) and the display's aspect-fit.
|
||||
java_units = [
|
||||
("TraceReplaySession", "TraceReplaySessionTest"),
|
||||
("SpawnServerPath", "SpawnServerPathTest"),
|
||||
("top.mobilegl.plugin.trace", "TraceReplaySession", "TraceReplaySessionTest"),
|
||||
("top.mobilegl.plugin.trace", "SpawnServerPath", "SpawnServerPathTest"),
|
||||
("top.mobilegl.plugin", "ServerEnvironment", "ServerEnvironmentTest"),
|
||||
]
|
||||
with tempfile.TemporaryDirectory(prefix="mobilegl-replay-lifecycle-") as output:
|
||||
sources = []
|
||||
for unit, test in java_units:
|
||||
sources.append(str(trace / f"java/top/mobilegl/plugin/trace/{unit}.java"))
|
||||
for package, unit, test in java_units:
|
||||
sources.append(str(trace / "java" / package.replace(".", "/") / f"{unit}.java"))
|
||||
sources.append(str(root / f"tools/trace_replay/{test}.java"))
|
||||
subprocess.run(["javac", "--release", "11", "-d", output, *sources], check=True)
|
||||
for _, test in java_units:
|
||||
subprocess.run(["java", "-cp", output, f"top.mobilegl.plugin.trace.{test}"], check=True)
|
||||
for package, _, test in java_units:
|
||||
subprocess.run(["java", "-cp", output, f"{package}.{test}"], check=True)
|
||||
print("Android replay manifest and lifecycle checks passed")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user