mirror of
https://github.com/Benjamin-Wiegand/Flywheel.git
synced 2026-07-20 05:22:25 -07:00
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
This commit is contained in:
@@ -98,6 +98,12 @@
|
||||
android:theme="@style/Theme.Geargrinder.Projection"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name=".VirtualActivityLauncherActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Geargrinder.Projection"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name=".PermissionGrantActivity"
|
||||
android:label="@string/permission_grant_activity_title"
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package io.benwiegand.projection.geargrinder;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import io.benwiegand.projection.libprivd.ipc.IPCConstants;
|
||||
|
||||
public class VirtualActivityLauncherActivity extends AppCompatActivity {
|
||||
private static final String TAG = VirtualActivityLauncherActivity.class.getSimpleName();
|
||||
|
||||
private static final String INTENT_EXTRA_ACTIVITY_COMPONENT = IPCConstants.VIRTUAL_ACTIVITY_LAUNCHER_INTENT_EXTRA_ACTIVITY;
|
||||
|
||||
private static final long LAUNCH_TIMEOUT = 5000;
|
||||
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private ComponentName componentName;
|
||||
|
||||
private Object launchToken = new Object();
|
||||
|
||||
private boolean firstLaunch = true;
|
||||
private boolean activityLost = false;
|
||||
private boolean wasPaused = false;
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_virtual_activity_launcher);
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (intent == null) {
|
||||
Log.wtf(TAG, "no launch intent");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
String component = intent.getStringExtra(INTENT_EXTRA_ACTIVITY_COMPONENT);
|
||||
if (component == null) {
|
||||
Log.wtf(TAG, "missing activity intent extra");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
componentName = ComponentName.unflattenFromString(component);
|
||||
|
||||
View relaunchButton = findViewById(R.id.relaunch_button);
|
||||
relaunchButton.setOnClickListener(v -> 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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/launching_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/launch_status_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
style="?textAppearanceHeadline5"
|
||||
tools:text="@string/virtual_activity_launcher_status_launching"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/relaunch_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/relaunch_button"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -186,4 +186,8 @@
|
||||
<string name="unknown_caller">Unknown</string>
|
||||
<string name="incoming_call_fallback_notice">when it is safe, answer on your phone</string>
|
||||
|
||||
<string name="virtual_activity_launcher_status_launching">launching</string>
|
||||
<string name="virtual_activity_launcher_status_failed">app launch failed</string>
|
||||
<string name="virtual_activity_launcher_status_closed">app closed</string>
|
||||
|
||||
</resources>
|
||||
@@ -13,6 +13,8 @@ interface IPrivd {
|
||||
|
||||
int launchActivity(in ComponentName component, int displayId);
|
||||
|
||||
int launchVirtualActivity(in ComponentName component, int displayId);
|
||||
|
||||
int createVirtualDisplay(in String name, int width, int height, int densityDpi, in Surface surface, int flags);
|
||||
|
||||
void releaseVirtualDisplay(int displayId);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.benwiegand.projection.libprivd.ipc;
|
||||
|
||||
import android.content.ComponentName;
|
||||
|
||||
public class IPCConstants {
|
||||
|
||||
public static final long PING_INTERVAL = 1000;
|
||||
@@ -17,4 +19,7 @@ public class IPCConstants {
|
||||
public static final String INTENT_EXTRA_BINDER = "binder";
|
||||
public static final String INTENT_EXTRA_TOKEN = "token";
|
||||
|
||||
public static final ComponentName VIRTUAL_ACTIVITY_LAUNCHER_ACTIVITY_COMPONENT = ComponentName.createRelative(APP_PKG_NAME, ".VirtualActivityLauncherActivity");
|
||||
public static final String VIRTUAL_ACTIVITY_LAUNCHER_INTENT_EXTRA_ACTIVITY = "activity";
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import static io.benwiegand.projection.geargrinder.privd.reflection.ReflectionUt
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.APP_PKG_NAME;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.BIND_TIMEOUT;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.PING_TIMEOUT;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.VIRTUAL_ACTIVITY_LAUNCHER_ACTIVITY_COMPONENT;
|
||||
import static io.benwiegand.projection.libprivd.ipc.IPCConstants.VIRTUAL_ACTIVITY_LAUNCHER_INTENT_EXTRA_ACTIVITY;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActivityOptions;
|
||||
@@ -180,6 +182,44 @@ public class Privd extends IPrivd.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int launchVirtualActivity(ComponentName component, int displayId) {
|
||||
Log.v(TAG, "launching virtual activity launcher on display " + displayId + ": " + component.flattenToShortString());
|
||||
|
||||
if (ram != null) {
|
||||
try {
|
||||
Intent intent = new Intent()
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
.putExtra(VIRTUAL_ACTIVITY_LAUNCHER_INTENT_EXTRA_ACTIVITY, component.flattenToString())
|
||||
.setComponent(VIRTUAL_ACTIVITY_LAUNCHER_ACTIVITY_COMPONENT);
|
||||
|
||||
ActivityOptions opts = ActivityOptions.makeBasic();
|
||||
opts.setLaunchDisplayId(displayId);
|
||||
|
||||
return ram.startActivity(intent, opts.toBundle());
|
||||
} catch (ReflectionException | SecurityException e) {
|
||||
Log.w(TAG, "failed to launch activity via IActivityManager", e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "falling back to shell command for activity launch");
|
||||
try {
|
||||
return new ProcessBuilder("am", "start-activity",
|
||||
"--activity-multiple-task",
|
||||
"--es", VIRTUAL_ACTIVITY_LAUNCHER_INTENT_EXTRA_ACTIVITY, component.flattenToString(),
|
||||
"--display", String.valueOf(displayId),
|
||||
VIRTUAL_ACTIVITY_LAUNCHER_ACTIVITY_COMPONENT.flattenToShortString())
|
||||
.start()
|
||||
.waitFor();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "IOException while starting activity via shell", e);
|
||||
return -1;
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "interrupted");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private VirtualDisplay getVirtualDisplay(int displayId) {
|
||||
VirtualDisplay virtualDisplay = virtualDisplays.get(displayId);
|
||||
if (virtualDisplay == null) throw new NoSuchElementException("no registered VirtualDisplay exists with id " + displayId);
|
||||
|
||||
Reference in New Issue
Block a user