[Feat] (Android): trace APK boots to POST; bottom nav bar switches to the render server

The trace flavor manifest removed PostActivity and made ServerControlActivity
the launcher, so the APK opened straight into the render server screen. Keep
both screens: PostActivity is the launcher again, and a bottom navigation bar
(NavBar, plugin flavor unaffected) switches between them via
FLAG_ACTIVITY_REORDER_TO_FRONT with transitions suppressed, so each screen
keeps its state. ServerControlActivity is singleTop now and handles extras in
onNewIntent so adb-driven starts still work. The POST screen also shows an
indeterminate progress bar while the self-test runs.
This commit is contained in:
2026-09-28 21:06:44 -04:00
parent b9a89967b0
commit 884f6aac96
4 changed files with 117 additions and 18 deletions
@@ -0,0 +1,79 @@
package top.mobilegl.plugin;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Typeface;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* Bottom navigation bar switching between the dev screens (POST and the render
* server control). The bar only appears in flavors that ship both screens: the
* plugin flavor has no ServerControlActivity, so wrap() returns the content
* unchanged there. Tabs switch by reordering the other activity to the front,
* so each screen keeps its state while the other is open.
*/
public final class NavBar {
public static final int TAB_POST = 0;
public static final int TAB_SERVER = 1;
private static final int COLOR_BAR = 0xFF1E1E1E;
private static final int COLOR_TAB_ACTIVE = 0xFFEEEEEE;
private static final int COLOR_TAB_INACTIVE = 0xFF9E9E9E;
private static final int BAR_HEIGHT_DP = 56;
private static final String SERVER_ACTIVITY = "top.mobilegl.plugin.ServerControlActivity";
private NavBar() {}
/** Returns content wrapped with the bottom bar, or content unchanged in plugin builds. */
public static View wrap(Activity activity, View content, int currentTab) {
final Class<?> serverClass;
try {
serverClass = Class.forName(SERVER_ACTIVITY);
} catch (ClassNotFoundException notInThisFlavor) {
return content;
}
LinearLayout root = new LinearLayout(activity);
root.setOrientation(LinearLayout.VERTICAL);
root.addView(content, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f));
LinearLayout bar = new LinearLayout(activity);
bar.setOrientation(LinearLayout.HORIZONTAL);
bar.setBackgroundColor(COLOR_BAR);
addTab(activity, bar, "POST", PostActivity.class, currentTab == TAB_POST);
addTab(activity, bar, "Render Server", serverClass, currentTab == TAB_SERVER);
root.addView(bar, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, dp(activity, BAR_HEIGHT_DP)));
return root;
}
private static void addTab(final Activity activity, LinearLayout bar, String label,
final Class<?> target, boolean active) {
TextView tab = new TextView(activity);
tab.setText(label);
tab.setGravity(Gravity.CENTER);
tab.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
tab.setTypeface(Typeface.MONOSPACE, active ? Typeface.BOLD : Typeface.NORMAL);
tab.setTextColor(active ? COLOR_TAB_ACTIVE : COLOR_TAB_INACTIVE);
if (!active) {
tab.setOnClickListener(v -> {
activity.startActivity(
new Intent(activity, target).addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
// Tab switch, not navigation: suppress the slide-in activity transition.
activity.overridePendingTransition(0, 0);
});
}
bar.addView(tab, new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.MATCH_PARENT, 1f));
}
private static int dp(Activity activity, int value) {
return Math.round(value * activity.getResources().getDisplayMetrics().density);
}
}
@@ -15,6 +15,7 @@ import android.view.View;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
@@ -69,6 +70,7 @@ public final class PostActivity extends Activity {
private LinearLayout contentLayout;
private TextView statusView;
private ProgressBar progressBar;
/**
* Per-backend bench UI state. Unlike the POST itself (single-flight, latched),
@@ -93,11 +95,20 @@ public final class PostActivity extends Activity {
scrollView.setBackgroundColor(COLOR_BACKGROUND);
scrollView.setFillViewport(true);
scrollView.addView(contentLayout);
setContentView(scrollView);
setContentView(NavBar.wrap(this, scrollView, NavBar.TAB_POST));
addText("MobileGL Driver POST", 20, COLOR_TEXT, true, 0);
statusView = addText("Running self-test...", 14, COLOR_INFO, false, dp(8));
progressBar = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
progressBar.setIndeterminate(true);
LinearLayout.LayoutParams progressParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
progressParams.topMargin = dp(8);
contentLayout.addView(progressBar, progressParams);
boolean startWorker = false;
boolean renderNow = false;
synchronized (POST_LOCK) {
@@ -298,6 +309,7 @@ public final class PostActivity extends Activity {
}
private void renderCachedResult() {
progressBar.setVisibility(View.GONE);
String json;
Throwable failure;
synchronized (POST_LOCK) {
@@ -8,17 +8,16 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application>
<!-- Launcher entry: start/stop the offscreen TCP supervisor (MobileGLServerService).
The trace flavor removes PostActivity, so without this the APK has no icon. -->
<!-- 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:theme="@style/Theme.MobileGLPlugin">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
android:launchMode="singleTop"
android:theme="@style/Theme.MobileGLPlugin" />
<service
android:name=".MobileGLServerService"
android:exported="true"
@@ -42,9 +41,6 @@
<activity
android:name=".PluginActivity"
tools:node="remove" />
<activity
android:name=".PostActivity"
tools:node="remove" />
<meta-data
android:name="fclPlugin"
tools:node="remove" />
@@ -72,7 +72,7 @@ public final class ServerControlActivity extends Activity {
scrollView.setBackgroundColor(COLOR_BACKGROUND);
scrollView.setFillViewport(true);
scrollView.addView(contentLayout);
setContentView(scrollView);
setContentView(NavBar.wrap(this, scrollView, NavBar.TAB_SERVER));
contentLayout.addView(makeText("MobileGL Render Server", 20, COLOR_TEXT, true));
@@ -142,11 +142,23 @@ public final class ServerControlActivity extends Activity {
setStatus("Stopped.", COLOR_INFO);
}
// 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.
Intent intent = getIntent();
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);