From b368ca3cdd0cda75dcc98e740a0cbb9a80a7c71f Mon Sep 17 00:00:00 2001 From: Benjamin Wiegand <126627496+Benjamin-Wiegand@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:15:55 -0700 Subject: [PATCH] feat: add activity launcher for virtual activity displays this is nice for several reasons: - it adds a loading screen in place of what would be a broken/frozen frame buffer - it adds a way to easily re-launch an activity that crashes - the same applies when pressing back too many times and closing the activity - it also allows a relatively seamless transition for apps to go from the car display to the phone and back - in the future, I can add a callback to VirtualActivity to hide the splash screen after this activity renders its first frame to make loading even more seamless --- app/src/main/AndroidManifest.xml | 6 + .../VirtualActivityLauncherActivity.java | 166 ++++++++++++++++++ .../projection/ui/VirtualActivity.java | 2 +- .../activity_virtual_activity_launcher.xml | 42 +++++ app/src/main/res/values/strings.xml | 4 + .../projection/libprivd/IPrivd.aidl | 2 + .../projection/libprivd/ipc/IPCConstants.java | 5 + .../projection/geargrinder/privd/Privd.java | 40 +++++ 8 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/io/benwiegand/projection/geargrinder/VirtualActivityLauncherActivity.java create mode 100644 app/src/main/res/layout/activity_virtual_activity_launcher.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e702176..e108f30 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -98,6 +98,12 @@ android:theme="@style/Theme.Geargrinder.Projection" /> + + tryLaunch()); + + getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + Log.v(TAG, "back button consumed"); + } + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // this is preferable since it shows a cohesive frame on the screen that accurately indicates what's currently happening. + // without this, the first frame the user sees will be blank, broken, or outdated. + View root = findViewById(R.id.root); + root.getViewTreeObserver().registerFrameCommitCallback(new Runnable() { + @Override + public void run() { + root.getViewTreeObserver().unregisterFrameCommitCallback(this); + doFirstLaunch(); + } + }); + + handler.postDelayed(() -> { + if (!firstLaunch) return; + Log.wtf(TAG, "frame commit callback not called after " + LAUNCH_TIMEOUT + " ms! forcing first launch"); + doFirstLaunch(); + }, LAUNCH_TIMEOUT); + } else if (firstLaunch) { + doFirstLaunch(); + } + + + } + + @Override + protected void onDestroy() { + super.onDestroy(); + Log.w(TAG, "onDestroy()"); + } + + private void updateStatus(@StringRes int text) { + TextView statusText = findViewById(R.id.launch_status_text); + View relaunchButton = findViewById(R.id.relaunch_button); + ProgressBar launchingIndicator = findViewById(R.id.launching_indicator); + + statusText.setText(text); + + if (text == R.string.virtual_activity_launcher_status_launching) { + relaunchButton.setVisibility(View.GONE); + launchingIndicator.setVisibility(View.VISIBLE); + } else { + relaunchButton.setVisibility(View.VISIBLE); + launchingIndicator.setVisibility(View.GONE); + } + } + + private void doFirstLaunch() { + if (!firstLaunch) return; + Log.i(TAG, "first launch"); + tryLaunch(); + } + + private void tryLaunch() { + Log.i(TAG, "launching activity: " + componentName); + launchToken = new Object(); + firstLaunch = false; + activityLost = false; + wasPaused = false; + updateStatus(R.string.virtual_activity_launcher_status_launching); + try { + startActivity(new Intent().setComponent(componentName)); + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + } catch (Throwable t) { + Log.e(TAG, "activity launch failed", t); + updateStatus(R.string.virtual_activity_launcher_status_failed); + return; + } + + + Object token = launchToken; + handler.postDelayed(() -> { + if (launchToken != token) return; + if (wasPaused) return; + + Log.i(TAG, "no call to onPause after " + LAUNCH_TIMEOUT + " ms"); + updateStatus(R.string.virtual_activity_launcher_status_failed); + }, LAUNCH_TIMEOUT); + } + + @Override + protected void onResume() { + super.onResume(); + + if (firstLaunch) return; + if (activityLost) return; + + Log.i(TAG, "activity lost: " + componentName); + updateStatus(R.string.virtual_activity_launcher_status_closed); + activityLost = true; + } + + @Override + protected void onPause() { + super.onPause(); + Log.d(TAG, "onPause()"); + wasPaused = true; + } +} diff --git a/app/src/main/java/io/benwiegand/projection/geargrinder/projection/ui/VirtualActivity.java b/app/src/main/java/io/benwiegand/projection/geargrinder/projection/ui/VirtualActivity.java index e505c71..7712964 100644 --- a/app/src/main/java/io/benwiegand/projection/geargrinder/projection/ui/VirtualActivity.java +++ b/app/src/main/java/io/benwiegand/projection/geargrinder/projection/ui/VirtualActivity.java @@ -168,7 +168,7 @@ public class VirtualActivity implements SurfaceHolder.Callback { // launch try { - int result = privd.launchActivity(app.launchComponent(), getDisplayId()); + int result = privd.launchVirtualActivity(app.launchComponent(), getDisplayId()); Log.d(TAG, "launch result " + result + " for " + app.launchComponent().flattenToShortString()); if (result == -1) throw new RuntimeException("activity launch failed with code -1"); } catch (DeadObjectException e) { diff --git a/app/src/main/res/layout/activity_virtual_activity_launcher.xml b/app/src/main/res/layout/activity_virtual_activity_launcher.xml new file mode 100644 index 0000000..7a7add4 --- /dev/null +++ b/app/src/main/res/layout/activity_virtual_activity_launcher.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + +