From 884f6aac965c4b6bd597d807464450b8241ac16e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 28 Sep 2026 21:06:44 -0400 Subject: [PATCH] [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. --- .../main/java/top/mobilegl/plugin/NavBar.java | 79 +++++++++++++++++++ .../top/mobilegl/plugin/PostActivity.java | 14 +++- .../app/src/trace/AndroidManifest.xml | 18 ++--- .../plugin/ServerControlActivity.java | 24 ++++-- 4 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 android-plugin/app/src/main/java/top/mobilegl/plugin/NavBar.java 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);