VmTerminalApp: Show error activity for unrecoverable error
This CL launches error activity for any of followings:
- Uncaught exception
- Failed to connect to VM due to the timeout
- Failed to create client certs
- Failed to start VM
To achieve so, error activity is changed to be run on the separated
process because main process may be shutting down.
Bug: 381182508
Bug: 376796062
Test: Manually with KEYCODE_UNKNOWN, plus wrote local patch to throw \
exceptions from activity, service, and executor service.
Change-Id: I610136a181109a0d1bd5cb996f5c81873fcb6824
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 7dab58d..726004c 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -54,7 +54,9 @@
android:label="@string/settings_port_forwarding_title" />
<activity android:name=".SettingsRecoveryActivity"
android:label="@string/settings_recovery_title" />
- <activity android:name=".ErrorActivity" />
+ <activity android:name=".ErrorActivity"
+ android:label="@string/error_title"
+ android:process=":error" />
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
android:value="true" />
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java
index d6521be..d6ca1e6 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java
@@ -39,6 +39,15 @@
NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
+
+ if (!(this instanceof ErrorActivity)) {
+ Thread currentThread = Thread.currentThread();
+ if (!(currentThread.getUncaughtExceptionHandler()
+ instanceof TerminalExceptionHandler)) {
+ currentThread.setUncaughtExceptionHandler(
+ new TerminalExceptionHandler(getApplicationContext()));
+ }
+ }
}
@Override
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java b/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java
index fa5c382..e3d1a67 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java
@@ -62,9 +62,8 @@
}
return ((KeyStore.PrivateKeyEntry) ks.getEntry(ALIAS, null));
} catch (Exception e) {
- Log.e(TAG, "cannot generate or get key", e);
+ throw new RuntimeException("cannot generate or get key", e);
}
- return null;
}
private static void createKey()
@@ -95,7 +94,7 @@
+ end_cert;
writer.write(output.getBytes());
} catch (IOException | CertificateEncodingException e) {
- Log.d(TAG, "cannot write cert", e);
+ throw new RuntimeException("cannot write certs", e);
}
}
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index ac05d78..87ee881 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -41,7 +41,6 @@
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.file.Path;
-import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -82,7 +81,9 @@
.setContentIntent(pendingIntent)
.build();
- mExecutorService = Executors.newSingleThreadExecutor();
+ mExecutorService =
+ Executors.newSingleThreadExecutor(
+ new TerminalThreadFactory(getApplicationContext()));
mConnectivityManager = getSystemService(ConnectivityManager.class);
Network defaultNetwork = mConnectivityManager.getBoundNetworkForProcess();
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 397a546..39c98d1 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -32,6 +32,7 @@
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Environment;
+import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
import android.view.KeyEvent;
@@ -67,6 +68,8 @@
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
public class MainActivity extends BaseActivity
implements VmLauncherService.VmLauncherServiceCallback, AccessibilityStateChangeListener {
@@ -74,9 +77,11 @@
static final String KEY_DISK_SIZE = "disk_size";
private static final String VM_ADDR = "192.168.0.2";
private static final int TTYD_PORT = 7681;
+ private static final int TERMINAL_CONNECTION_TIMEOUT_MS = 10_000;
private static final int REQUEST_CODE_INSTALLER = 0x33;
private static final int FONT_SIZE_DEFAULT = 13;
+ private ExecutorService mExecutorService;
private InstalledImage mImage;
private X509Certificate[] mCertificates;
private PrivateKey mPrivateKey;
@@ -141,6 +146,11 @@
updateModifierKeysVisibility();
return insets;
});
+
+ mExecutorService =
+ Executors.newSingleThreadExecutor(
+ new TerminalThreadFactory(getApplicationContext()));
+
// if installer is launched, it will be handled in onActivityResult
if (!launchInstaller) {
if (!Environment.isExternalStorageManager()) {
@@ -323,15 +333,12 @@
handler.proceed();
}
});
- new Thread(
- () -> {
- waitUntilVmStarts();
- runOnUiThread(
- () ->
- mTerminalView.loadUrl(
- getTerminalServiceUrl().toString()));
- })
- .start();
+ mExecutorService.execute(
+ () -> {
+ // TODO(b/376793781): Remove polling
+ waitUntilVmStarts();
+ runOnUiThread(() -> mTerminalView.loadUrl(getTerminalServiceUrl().toString()));
+ });
}
private static void waitUntilVmStarts() {
@@ -341,17 +348,33 @@
} catch (UnknownHostException e) {
// this can never happen.
}
- try {
- while (!addr.isReachable(10000)) {}
- } catch (IOException e) {
- // give up on network error
- throw new RuntimeException(e);
+
+ long startTime = SystemClock.elapsedRealtime();
+ while (true) {
+ int remainingTime =
+ TERMINAL_CONNECTION_TIMEOUT_MS
+ - (int) (SystemClock.elapsedRealtime() - startTime);
+ if (remainingTime <= 0) {
+ throw new RuntimeException("Connection to terminal timedout");
+ }
+ try {
+ // Note: this quits immediately if VM is unreachable.
+ if (addr.isReachable(remainingTime)) {
+ return;
+ }
+ } catch (IOException e) {
+ // give up on network error
+ throw new RuntimeException(e);
+ }
}
- return;
}
@Override
protected void onDestroy() {
+ if (mExecutorService != null) {
+ mExecutorService.shutdown();
+ }
+
getSystemService(AccessibilityManager.class).removeAccessibilityStateChangeListener(this);
VmLauncherService.stop(this);
super.onDestroy();
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalExceptionHandler.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalExceptionHandler.java
new file mode 100644
index 0000000..4ab2b77
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalExceptionHandler.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.content.Context;
+import android.util.Log;
+
+public class TerminalExceptionHandler implements Thread.UncaughtExceptionHandler {
+ private static final String TAG = "TerminalExceptionHandler";
+
+ private final Context mContext;
+
+ public TerminalExceptionHandler(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ Exception exception;
+ if (throwable instanceof Exception) {
+ exception = (Exception) throwable;
+ } else {
+ exception = new Exception(throwable);
+ }
+ try {
+ ErrorActivity.start(mContext, exception);
+ } catch (Exception ex) {
+ Log.wtf(TAG, "Failed to launch error activity for an exception", exception);
+ }
+
+ thread.getDefaultUncaughtExceptionHandler().uncaughtException(thread, throwable);
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalThreadFactory.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalThreadFactory.java
new file mode 100644
index 0000000..5ee535d
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalThreadFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.content.Context;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+public class TerminalThreadFactory implements ThreadFactory {
+ private final Context mContext;
+
+ public TerminalThreadFactory(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread thread = Executors.defaultThreadFactory().newThread(r);
+ thread.setUncaughtExceptionHandler(new TerminalExceptionHandler(mContext));
+ return thread;
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
index 6d2c5bd..9a70605 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -153,7 +153,8 @@
Log.d(TAG, "VM instance is already started");
return START_NOT_STICKY;
}
- mExecutorService = Executors.newCachedThreadPool();
+ mExecutorService =
+ Executors.newCachedThreadPool(new TerminalThreadFactory(getApplicationContext()));
InstalledImage image = InstalledImage.getDefault(this);
ConfigJson json = ConfigJson.from(this, image.getConfigPath());
@@ -172,9 +173,7 @@
android.os.Trace.endSection();
android.os.Trace.beginAsyncSection("debianBoot", 0);
} catch (VirtualMachineException e) {
- Log.e(TAG, "cannot create runner", e);
- stopSelf();
- return START_NOT_STICKY;
+ throw new RuntimeException("cannot create runner", e);
}
mVirtualMachine = runner.getVm();
mResultReceiver =