diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/NavBar.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/NavBar.java
new file mode 100644
index 00000000..cd65f3d6
--- /dev/null
+++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/NavBar.java
@@ -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);
+ }
+}
diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java
index c6910500..55a13898 100644
--- a/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java
+++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java
@@ -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) {
diff --git a/android-plugin/app/src/trace/AndroidManifest.xml b/android-plugin/app/src/trace/AndroidManifest.xml
index 6719a6cf..ccfe5cb3 100644
--- a/android-plugin/app/src/trace/AndroidManifest.xml
+++ b/android-plugin/app/src/trace/AndroidManifest.xml
@@ -8,17 +8,16 @@
-
+
-
-
-
-
-
+ android:launchMode="singleTop"
+ android:theme="@style/Theme.MobileGLPlugin" />
-
diff --git a/android-plugin/app/src/trace/java/top/mobilegl/plugin/ServerControlActivity.java b/android-plugin/app/src/trace/java/top/mobilegl/plugin/ServerControlActivity.java
index 3094a999..bc3fd8cb 100644
--- a/android-plugin/app/src/trace/java/top/mobilegl/plugin/ServerControlActivity.java
+++ b/android-plugin/app/src/trace/java/top/mobilegl/plugin/ServerControlActivity.java
@@ -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);