Merge "Add *.GTS variants of our tests" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
deleted file mode 100644
index 0d70ab9..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright (C) 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 static com.android.virtualization.terminal.MainActivity.TAG;
-
-import android.app.Notification;
-import android.app.Notification.Action;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.drawable.Icon;
-
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Set;
-
-/**
- * PortNotifier is responsible for posting a notification when a new open port is detected. User can
- * enable or disable forwarding of the port in notification panel.
- */
-class PortNotifier {
- private static final String ACTION_PORT_FORWARDING = "android.virtualization.PORT_FORWARDING";
- private static final String KEY_PORT = "port";
- private static final String KEY_ENABLED = "enabled";
-
- private final Context mContext;
- private final NotificationManager mNotificationManager;
- private final BroadcastReceiver mReceiver;
- private final PortsStateManager mPortsStateManager;
- private final PortsStateManager.Listener mPortsStateListener;
-
- public PortNotifier(Context context) {
- mContext = context;
- mNotificationManager = mContext.getSystemService(NotificationManager.class);
- mReceiver = new PortForwardingRequestReceiver();
-
- mPortsStateManager = PortsStateManager.getInstance(mContext);
- mPortsStateListener =
- new PortsStateManager.Listener() {
- @Override
- public void onPortsStateUpdated(
- Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
- Set<Integer> union = new HashSet<>(oldActivePorts);
- union.addAll(newActivePorts);
- for (int port : union) {
- if (!oldActivePorts.contains(port)) {
- showNotificationFor(port);
- } else if (!newActivePorts.contains(port)) {
- discardNotificationFor(port);
- }
- }
- }
- };
- mPortsStateManager.registerListener(mPortsStateListener);
-
- IntentFilter intentFilter = new IntentFilter(ACTION_PORT_FORWARDING);
- mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED);
- }
-
- public void stop() {
- mPortsStateManager.unregisterListener(mPortsStateListener);
- mContext.unregisterReceiver(mReceiver);
- }
-
- private String getString(int resId) {
- return mContext.getString(resId);
- }
-
- private PendingIntent getPendingIntentFor(int port, boolean enabled) {
- Intent intent = new Intent(ACTION_PORT_FORWARDING);
- intent.setPackage(mContext.getPackageName());
- intent.setIdentifier(String.format(Locale.ROOT, "%d_%b", port, enabled));
- intent.putExtra(KEY_PORT, port);
- intent.putExtra(KEY_ENABLED, enabled);
- return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
- }
-
- private void showNotificationFor(int port) {
- Intent tapIntent = new Intent(mContext, SettingsPortForwardingActivity.class);
- tapIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
- PendingIntent tapPendingIntent =
- PendingIntent.getActivity(mContext, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE);
-
- String title = getString(R.string.settings_port_forwarding_notification_title);
- String content =
- mContext.getString(R.string.settings_port_forwarding_notification_content, port);
- String acceptText = getString(R.string.settings_port_forwarding_notification_accept);
- String denyText = getString(R.string.settings_port_forwarding_notification_deny);
- Icon icon = Icon.createWithResource(mContext, R.drawable.ic_launcher_foreground);
-
- Action acceptAction =
- new Action.Builder(icon, acceptText, getPendingIntentFor(port, true /* enabled */))
- .build();
- Action denyAction =
- new Action.Builder(icon, denyText, getPendingIntentFor(port, false /* enabled */))
- .build();
- Notification notification =
- new Notification.Builder(mContext, mContext.getPackageName())
- .setSmallIcon(R.drawable.ic_launcher_foreground)
- .setContentTitle(title)
- .setContentText(content)
- .setContentIntent(tapPendingIntent)
- .addAction(acceptAction)
- .addAction(denyAction)
- .build();
- mNotificationManager.notify(TAG, port, notification);
- }
-
- private void discardNotificationFor(int port) {
- mNotificationManager.cancel(TAG, port);
- }
-
- private final class PortForwardingRequestReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (ACTION_PORT_FORWARDING.equals(intent.getAction())) {
- performActionPortForwarding(context, intent);
- }
- }
-
- private void performActionPortForwarding(Context context, Intent intent) {
- int port = intent.getIntExtra(KEY_PORT, 0);
- boolean enabled = intent.getBooleanExtra(KEY_ENABLED, false);
- mPortsStateManager.updateEnabledPort(port, enabled);
- discardNotificationFor(port);
- }
- }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
new file mode 100644
index 0000000..dd58e0a
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 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.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.Icon
+import com.android.virtualization.terminal.MainActivity.TAG
+import java.util.Locale
+
+/**
+ * PortNotifier is responsible for posting a notification when a new open port is detected. User can
+ * enable or disable forwarding of the port in notification panel.
+ */
+internal class PortNotifier(val context: Context) {
+ private val notificationManager: NotificationManager =
+ context.getSystemService<NotificationManager>(NotificationManager::class.java)
+ private val receiver: BroadcastReceiver =
+ PortForwardingRequestReceiver().also {
+ val intentFilter = IntentFilter(ACTION_PORT_FORWARDING)
+ context.registerReceiver(it, intentFilter, Context.RECEIVER_NOT_EXPORTED)
+ }
+ private val portsStateListener: PortsStateManager.Listener =
+ object : PortsStateManager.Listener {
+ override fun onPortsStateUpdated(oldActivePorts: Set<Int>, newActivePorts: Set<Int>) {
+ // added active ports
+ (newActivePorts - oldActivePorts).forEach { showNotificationFor(it) }
+ // removed active ports
+ (oldActivePorts - newActivePorts).forEach { discardNotificationFor(it) }
+ }
+ }
+ private val portsStateManager: PortsStateManager =
+ PortsStateManager.getInstance(context).also { it.registerListener(portsStateListener) }
+
+ fun stop() {
+ portsStateManager.unregisterListener(portsStateListener)
+ context.unregisterReceiver(receiver)
+ }
+
+ private fun getString(resId: Int): String {
+ return context.getString(resId)
+ }
+
+ private fun getPendingIntentFor(port: Int, enabled: Boolean): PendingIntent {
+ val intent = Intent(ACTION_PORT_FORWARDING)
+ intent.setPackage(context.getPackageName())
+ intent.setIdentifier(String.format(Locale.ROOT, "%d_%b", port, enabled))
+ intent.putExtra(KEY_PORT, port)
+ intent.putExtra(KEY_ENABLED, enabled)
+ return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ }
+
+ private fun showNotificationFor(port: Int) {
+ val tapIntent = Intent(context, SettingsPortForwardingActivity::class.java)
+ tapIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ val tapPendingIntent =
+ PendingIntent.getActivity(context, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE)
+
+ val title = getString(R.string.settings_port_forwarding_notification_title)
+ val content =
+ context.getString(R.string.settings_port_forwarding_notification_content, port)
+ val acceptText = getString(R.string.settings_port_forwarding_notification_accept)
+ val denyText = getString(R.string.settings_port_forwarding_notification_deny)
+ val icon = Icon.createWithResource(context, R.drawable.ic_launcher_foreground)
+
+ val acceptAction: Notification.Action =
+ Notification.Action.Builder(
+ icon,
+ acceptText,
+ getPendingIntentFor(port, true /* enabled */),
+ )
+ .build()
+ val denyAction: Notification.Action =
+ Notification.Action.Builder(
+ icon,
+ denyText,
+ getPendingIntentFor(port, false /* enabled */),
+ )
+ .build()
+ val notification: Notification =
+ Notification.Builder(context, context.getPackageName())
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setContentTitle(title)
+ .setContentText(content)
+ .setContentIntent(tapPendingIntent)
+ .addAction(acceptAction)
+ .addAction(denyAction)
+ .build()
+ notificationManager.notify(TAG, port, notification)
+ }
+
+ private fun discardNotificationFor(port: Int) {
+ notificationManager.cancel(TAG, port)
+ }
+
+ private inner class PortForwardingRequestReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent) {
+ if (ACTION_PORT_FORWARDING == intent.action) {
+ performActionPortForwarding(intent)
+ }
+ }
+
+ fun performActionPortForwarding(intent: Intent) {
+ val port = intent.getIntExtra(KEY_PORT, 0)
+ val enabled = intent.getBooleanExtra(KEY_ENABLED, false)
+ portsStateManager.updateEnabledPort(port, enabled)
+ discardNotificationFor(port)
+ }
+ }
+
+ companion object {
+ private const val ACTION_PORT_FORWARDING = "android.virtualization.PORT_FORWARDING"
+ private const val KEY_PORT = "port"
+ private const val KEY_ENABLED = "enabled"
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java
deleted file mode 100644
index 5321d89..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 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.content.SharedPreferences;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * PortsStateManager is responsible for communicating with shared preferences and managing state of
- * ports.
- */
-public class PortsStateManager {
- private static final String PREFS_NAME = ".PORTS";
- private static final int FLAG_ENABLED = 1;
-
- private static PortsStateManager mInstance;
- private final Object mLock = new Object();
-
- private final SharedPreferences mSharedPref;
-
- @GuardedBy("mLock")
- private Set<Integer> mActivePorts;
-
- @GuardedBy("mLock")
- private final Set<Integer> mEnabledPorts;
-
- @GuardedBy("mLock")
- private final Set<Listener> mListeners;
-
- private PortsStateManager(SharedPreferences sharedPref) {
- mSharedPref = sharedPref;
- mEnabledPorts =
- mSharedPref.getAll().entrySet().stream()
- .filter(entry -> entry.getValue() instanceof Integer)
- .filter(entry -> ((int) entry.getValue() & FLAG_ENABLED) == FLAG_ENABLED)
- .map(entry -> entry.getKey())
- .filter(
- key -> {
- try {
- Integer.parseInt(key);
- return true;
- } catch (NumberFormatException e) {
- return false;
- }
- })
- .map(Integer::parseInt)
- .collect(Collectors.toSet());
- mActivePorts = new HashSet<>();
- mListeners = new HashSet<>();
- }
-
- static synchronized PortsStateManager getInstance(Context context) {
- if (mInstance == null) {
- SharedPreferences sharedPref =
- context.getSharedPreferences(
- context.getPackageName() + PREFS_NAME, Context.MODE_PRIVATE);
- mInstance = new PortsStateManager(sharedPref);
- }
- return mInstance;
- }
-
- Set<Integer> getActivePorts() {
- synchronized (mLock) {
- return new HashSet<>(mActivePorts);
- }
- }
-
- Set<Integer> getEnabledPorts() {
- synchronized (mLock) {
- return new HashSet<>(mEnabledPorts);
- }
- }
-
- void updateActivePorts(Set<Integer> ports) {
- Set<Integer> oldPorts;
- synchronized (mLock) {
- oldPorts = mActivePorts;
- mActivePorts = ports;
- }
- notifyPortsStateUpdated(oldPorts, ports);
- }
-
- void updateEnabledPort(int port, boolean enabled) {
- Set<Integer> activePorts;
- synchronized (mLock) {
- SharedPreferences.Editor editor = mSharedPref.edit();
- editor.putInt(String.valueOf(port), enabled ? FLAG_ENABLED : 0);
- editor.apply();
- if (enabled) {
- mEnabledPorts.add(port);
- } else {
- mEnabledPorts.remove(port);
- }
- activePorts = mActivePorts;
- }
- notifyPortsStateUpdated(activePorts, activePorts);
- }
-
- void clearEnabledPorts() {
- Set<Integer> activePorts;
- synchronized (mLock) {
- SharedPreferences.Editor editor = mSharedPref.edit();
- editor.clear();
- editor.apply();
- mEnabledPorts.clear();
- activePorts = mActivePorts;
- }
- notifyPortsStateUpdated(activePorts, activePorts);
- }
-
- void registerListener(Listener listener) {
- synchronized (mLock) {
- mListeners.add(listener);
- }
- }
-
- void unregisterListener(Listener listener) {
- synchronized (mLock) {
- mListeners.remove(listener);
- }
- }
-
- private void notifyPortsStateUpdated(Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
- Set<Listener> listeners;
- synchronized (mLock) {
- listeners = new HashSet<>(mListeners);
- }
- for (Listener listener : listeners) {
- listener.onPortsStateUpdated(
- new HashSet<>(oldActivePorts), new HashSet<>(newActivePorts));
- }
- }
-
- interface Listener {
- default void onPortsStateUpdated(
- Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {}
- }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt
new file mode 100644
index 0000000..7e53cce
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 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.content.SharedPreferences
+import com.android.internal.annotations.GuardedBy
+import java.util.HashSet
+
+/**
+ * PortsStateManager is responsible for communicating with shared preferences and managing state of
+ * ports.
+ */
+class PortsStateManager private constructor(private val sharedPref: SharedPreferences) {
+ private val lock = Any()
+
+ @GuardedBy("lock") private val activePorts: MutableSet<Int> = hashSetOf()
+
+ @GuardedBy("lock")
+ private val enabledPorts: MutableSet<Int> =
+ sharedPref
+ .getAll()
+ .entries
+ .filterIsInstance<MutableMap.MutableEntry<String, Int>>()
+ .filter { it.value and FLAG_ENABLED == FLAG_ENABLED }
+ .map { it.key.toIntOrNull() }
+ .filterNotNull()
+ .toMutableSet()
+
+ @GuardedBy("lock") private val listeners: MutableSet<Listener> = hashSetOf()
+
+ fun getActivePorts(): Set<Int> {
+ synchronized(lock) {
+ return HashSet<Int>(activePorts)
+ }
+ }
+
+ fun getEnabledPorts(): Set<Int> {
+ synchronized(lock) {
+ return HashSet<Int>(enabledPorts)
+ }
+ }
+
+ fun updateActivePorts(ports: Set<Int>) {
+ synchronized(lock) {
+ val oldPorts = getActivePorts()
+ activePorts.clear()
+ activePorts.addAll(ports)
+ notifyPortsStateUpdated(oldPorts, getActivePorts())
+ }
+ }
+
+ fun updateEnabledPort(port: Int, enabled: Boolean) {
+ synchronized(lock) {
+ val editor = sharedPref.edit()
+ editor.putInt(port.toString(), if (enabled) FLAG_ENABLED else 0)
+ editor.apply()
+ if (enabled) {
+ enabledPorts.add(port)
+ } else {
+ enabledPorts.remove(port)
+ }
+ }
+ notifyPortsStateUpdated(getActivePorts(), getActivePorts())
+ }
+
+ fun clearEnabledPorts() {
+ synchronized(lock) {
+ val editor = sharedPref.edit()
+ editor.clear()
+ editor.apply()
+ enabledPorts.clear()
+ }
+ notifyPortsStateUpdated(getActivePorts(), getActivePorts())
+ }
+
+ fun registerListener(listener: Listener) {
+ synchronized(lock) { listeners.add(listener) }
+ }
+
+ fun unregisterListener(listener: Listener) {
+ synchronized(lock) { listeners.remove(listener) }
+ }
+
+ // TODO: it notifies when both enabledPort and activePort are changed, but doesn't provide
+ // enabledPort's value change. Make this callback provide that information as well.
+ private fun notifyPortsStateUpdated(oldActivePorts: Set<Int>, newActivePorts: Set<Int>) {
+ synchronized(lock) { HashSet<Listener>(this@PortsStateManager.listeners) }
+ .forEach {
+ it.onPortsStateUpdated(HashSet<Int>(oldActivePorts), HashSet<Int>(newActivePorts))
+ }
+ }
+
+ interface Listener {
+ fun onPortsStateUpdated(oldActivePorts: Set<Int>, newActivePorts: Set<Int>) {}
+ }
+
+ companion object {
+ private const val PREFS_NAME = ".PORTS"
+ private const val FLAG_ENABLED = 1
+
+ private var instance: PortsStateManager? = null
+
+ @JvmStatic
+ @Synchronized
+ fun getInstance(context: Context): PortsStateManager {
+ if (instance == null) {
+ val sharedPref =
+ context.getSharedPreferences(
+ context.getPackageName() + PREFS_NAME,
+ Context.MODE_PRIVATE,
+ )
+ instance = PortsStateManager(sharedPref)
+ }
+ return instance!!
+ }
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
deleted file mode 100644
index 0ffc093..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- * Copyright (C) 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 static com.android.virtualization.terminal.MainActivity.TAG;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.text.InputType;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.View;
-import android.view.View.AccessibilityDelegate;
-import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
-import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
-import android.view.accessibility.AccessibilityNodeProvider;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.webkit.WebView;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-
-public class TerminalView extends WebView
- implements AccessibilityStateChangeListener, TouchExplorationStateChangeListener {
- // Maximum length of texts the talk back announcements can be. This value is somewhat
- // arbitrarily set. We may want to adjust this in the future.
- private static final int TEXT_TOO_LONG_TO_ANNOUNCE = 200;
-
- private final String CTRL_KEY_HANDLER;
- private final String ENABLE_CTRL_KEY;
- private final String TOUCH_TO_MOUSE_HANDLER;
-
- private final AccessibilityManager mA11yManager;
-
- public TerminalView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- mA11yManager = context.getSystemService(AccessibilityManager.class);
- mA11yManager.addTouchExplorationStateChangeListener(this);
- mA11yManager.addAccessibilityStateChangeListener(this);
- adjustToA11yStateChange();
- try {
- CTRL_KEY_HANDLER = readAssetAsString(context, "js/ctrl_key_handler.js");
- ENABLE_CTRL_KEY = readAssetAsString(context, "js/enable_ctrl_key.js");
- TOUCH_TO_MOUSE_HANDLER = readAssetAsString(context, "js/touch_to_mouse_handler.js");
- } catch (IOException e) {
- // It cannot happen
- throw new IllegalArgumentException("cannot read code from asset", e);
- }
- }
-
- private String readAssetAsString(Context context, String filePath) throws IOException {
- try (InputStream is = context.getAssets().open(filePath)) {
- return new String(is.readAllBytes());
- }
- }
-
- public void mapTouchToMouseEvent() {
- this.evaluateJavascript(TOUCH_TO_MOUSE_HANDLER, null);
- }
-
- public void mapCtrlKey() {
- this.evaluateJavascript(CTRL_KEY_HANDLER, null);
- }
-
- public void enableCtrlKey() {
- this.evaluateJavascript(ENABLE_CTRL_KEY, null);
- }
-
- @Override
- public void onAccessibilityStateChanged(boolean enabled) {
- Log.d(TAG, "accessibility " + enabled);
- adjustToA11yStateChange();
- }
-
- @Override
- public void onTouchExplorationStateChanged(boolean enabled) {
- Log.d(TAG, "touch exploration " + enabled);
- adjustToA11yStateChange();
- }
-
- private void adjustToA11yStateChange() {
- if (!mA11yManager.isEnabled()) {
- setFocusable(true);
- return;
- }
-
- // When accessibility is on, the webview itself doesn't have to be focusable. The (virtual)
- // edittext will be focusable to accept inputs. However, the webview has to be focusable for
- // an accessibility purpose so that users can read the contents in it or scroll the view.
- setFocusable(false);
- setFocusableInTouchMode(true);
- }
-
- // AccessibilityEvents for WebView are sent directly from WebContentsAccessibilityImpl to the
- // parent of WebView, without going through WebView. So, there's no WebView methods we can
- // override to intercept the event handling process. To work around this, we attach an
- // AccessibilityDelegate to the parent view where the events are sent to. And to guarantee that
- // the parent view exists, wait until the WebView is attached to the window by when the parent
- // must exist.
- private final AccessibilityDelegate mA11yEventFilter =
- new AccessibilityDelegate() {
- @Override
- public boolean onRequestSendAccessibilityEvent(
- ViewGroup host, View child, AccessibilityEvent e) {
- // We filter only the a11y events from the WebView
- if (child != TerminalView.this) {
- return super.onRequestSendAccessibilityEvent(host, child, e);
- }
- final int eventType = e.getEventType();
- switch (e.getEventType()) {
- // Skip reading texts that are too long. Right now, ttyd emits entire
- // text on the terminal to the live region, which is very annoying to
- // screen reader users.
- case AccessibilityEvent.TYPE_ANNOUNCEMENT:
- CharSequence text = e.getText().get(0); // there always is a text
- if (text.length() >= TEXT_TOO_LONG_TO_ANNOUNCE) {
- Log.i(TAG, "Announcement skipped because it's too long: " + text);
- return false;
- }
- break;
- }
- return super.onRequestSendAccessibilityEvent(host, child, e);
- }
- };
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- if (mA11yManager.isEnabled()) {
- View parent = (View) getParent();
- parent.setAccessibilityDelegate(mA11yEventFilter);
- }
- }
-
- private final AccessibilityNodeProvider mA11yNodeProvider =
- new AccessibilityNodeProvider() {
-
- /** Returns the original NodeProvider that WebView implements. */
- private AccessibilityNodeProvider getParent() {
- return TerminalView.super.getAccessibilityNodeProvider();
- }
-
- /** Convenience method for reading a string resource. */
- private String getString(int resId) {
- return TerminalView.this.getContext().getResources().getString(resId);
- }
-
- /** Checks if NodeInfo renders an empty line in the terminal. */
- private boolean isEmptyLine(AccessibilityNodeInfo info) {
- final CharSequence text = info.getText();
- // Node with no text is not consiered a line. ttyd emits at least one character,
- // which usually is NBSP.
- if (text == null) {
- return false;
- }
- for (int i = 0; i < text.length(); i++) {
- char c = text.charAt(i);
- // Note: don't use Characters.isWhitespace as it doesn't recognize NBSP as a
- // whitespace.
- if (!TextUtils.isWhitespace(c)) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public AccessibilityNodeInfo createAccessibilityNodeInfo(int id) {
- AccessibilityNodeInfo info = getParent().createAccessibilityNodeInfo(id);
- if (info == null) {
- return null;
- }
-
- final String className = info.getClassName().toString();
-
- // By default all views except the cursor is not click-able. Other views are
- // read-only. This ensures that user is not navigated to non-clickable elements
- // when using switches.
- if (!"android.widget.EditText".equals(className)) {
- info.removeAction(AccessibilityAction.ACTION_CLICK);
- }
-
- switch (className) {
- case "android.webkit.WebView":
- // There are two NodeInfo objects of class name WebView. The one is the
- // real WebView whose ID is View.NO_ID as it's at the root of the
- // virtual view hierarchy. The second one is a virtual view for the
- // iframe. The latter one's text is set to the command that we give to
- // ttyd, which is "login -f droid ...". This is an impl detail which
- // doesn't have to be announced. Replace the text with "Terminal
- // display".
- if (id != View.NO_ID) {
- info.setText(null);
- info.setContentDescription(getString(R.string.terminal_display));
- // b/376827536
- info.setHintText(getString(R.string.double_tap_to_edit_text));
- }
-
- // These two lines below are to prevent this WebView element from being
- // fousable by the screen reader, while allowing any other element in
- // the WebView to be focusable by the reader. In our case, the EditText
- // is a117_focusable.
- info.setScreenReaderFocusable(false);
- info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
- break;
- case "android.view.View":
- // Empty line was announced as "space" (via the NBSP character).
- // Localize the spoken text.
- if (isEmptyLine(info)) {
- info.setContentDescription(getString(R.string.empty_line));
- // b/376827536
- info.setHintText(getString(R.string.double_tap_to_edit_text));
- }
- break;
- case "android.widget.TextView":
- // There are several TextViews in the terminal, and one of them is an
- // invisible TextView which seems to be from the <div
- // class="live-region"> tag. Interestingly, its text is often populated
- // with the entire text on the screen. Silence this by forcibly setting
- // the text to null. Note that this TextView is identified by having a
- // zero width. This certainly is not elegant, but I couldn't find other
- // options.
- Rect rect = new Rect();
- info.getBoundsInScreen(rect);
- if (rect.width() == 0) {
- info.setText(null);
- info.setContentDescription(getString(R.string.empty_line));
- }
- info.setScreenReaderFocusable(false);
- break;
- case "android.widget.EditText":
- // This EditText is for the <textarea> accepting user input; the cursor.
- // ttyd name it as "Terminal input" but it's not i18n'ed. Override it
- // here for better i18n.
- info.setText(null);
- info.setHintText(getString(R.string.double_tap_to_edit_text));
- info.setContentDescription(getString(R.string.terminal_input));
- info.setScreenReaderFocusable(true);
- info.addAction(AccessibilityAction.ACTION_FOCUS);
- break;
- }
- return info;
- }
-
- @Override
- public boolean performAction(int id, int action, Bundle arguments) {
- return getParent().performAction(id, action, arguments);
- }
-
- @Override
- public void addExtraDataToAccessibilityNodeInfo(
- int virtualViewId,
- AccessibilityNodeInfo info,
- String extraDataKey,
- Bundle arguments) {
- getParent()
- .addExtraDataToAccessibilityNodeInfo(
- virtualViewId, info, extraDataKey, arguments);
- }
-
- @Override
- public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(
- String text, int virtualViewId) {
- return getParent().findAccessibilityNodeInfosByText(text, virtualViewId);
- }
-
- @Override
- public AccessibilityNodeInfo findFocus(int focus) {
- return getParent().findFocus(focus);
- }
- };
-
- @Override
- public AccessibilityNodeProvider getAccessibilityNodeProvider() {
- AccessibilityNodeProvider p = super.getAccessibilityNodeProvider();
- if (p != null && mA11yManager.isEnabled()) {
- return mA11yNodeProvider;
- }
- return p;
- }
-
- @Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
- if (outAttrs != null) {
- outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
- }
- return inputConnection;
- }
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
new file mode 100644
index 0000000..18a39fa
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 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.graphics.Rect
+import android.os.Bundle
+import android.text.InputType
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
+import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeProvider
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputConnection
+import android.webkit.WebView
+import com.android.virtualization.terminal.MainActivity.TAG
+import java.io.IOException
+
+class TerminalView(context: Context, attrs: AttributeSet?) :
+ WebView(context, attrs),
+ AccessibilityManager.AccessibilityStateChangeListener,
+ AccessibilityManager.TouchExplorationStateChangeListener {
+ private val ctrlKeyHandler: String = readAssetAsString(context, "js/ctrl_key_handler.js")
+ private val enableCtrlKey: String = readAssetAsString(context, "js/enable_ctrl_key.js")
+ private val touchToMouseHandler: String =
+ readAssetAsString(context, "js/touch_to_mouse_handler.js")
+ private val a11yManager =
+ context.getSystemService<AccessibilityManager>(AccessibilityManager::class.java).also {
+ it.addTouchExplorationStateChangeListener(this)
+ it.addAccessibilityStateChangeListener(this)
+ }
+
+ @Throws(IOException::class)
+ private fun readAssetAsString(context: Context, filePath: String): String {
+ return String(context.assets.open(filePath).readAllBytes())
+ }
+
+ fun mapTouchToMouseEvent() {
+ this.evaluateJavascript(touchToMouseHandler, null)
+ }
+
+ fun mapCtrlKey() {
+ this.evaluateJavascript(ctrlKeyHandler, null)
+ }
+
+ fun enableCtrlKey() {
+ this.evaluateJavascript(enableCtrlKey, null)
+ }
+
+ override fun onAccessibilityStateChanged(enabled: Boolean) {
+ Log.d(TAG, "accessibility $enabled")
+ adjustToA11yStateChange()
+ }
+
+ override fun onTouchExplorationStateChanged(enabled: Boolean) {
+ Log.d(TAG, "touch exploration $enabled")
+ adjustToA11yStateChange()
+ }
+
+ private fun adjustToA11yStateChange() {
+ if (!a11yManager.isEnabled) {
+ setFocusable(true)
+ return
+ }
+
+ // When accessibility is on, the webview itself doesn't have to be focusable. The (virtual)
+ // edittext will be focusable to accept inputs. However, the webview has to be focusable for
+ // an accessibility purpose so that users can read the contents in it or scroll the view.
+ setFocusable(false)
+ setFocusableInTouchMode(true)
+ }
+
+ // AccessibilityEvents for WebView are sent directly from WebContentsAccessibilityImpl to the
+ // parent of WebView, without going through WebView. So, there's no WebView methods we can
+ // override to intercept the event handling process. To work around this, we attach an
+ // AccessibilityDelegate to the parent view where the events are sent to. And to guarantee that
+ // the parent view exists, wait until the WebView is attached to the window by when the parent
+ // must exist.
+ private val mA11yEventFilter: AccessibilityDelegate =
+ object : AccessibilityDelegate() {
+ override fun onRequestSendAccessibilityEvent(
+ host: ViewGroup,
+ child: View,
+ e: AccessibilityEvent,
+ ): Boolean {
+ // We filter only the a11y events from the WebView
+ if (child !== this@TerminalView) {
+ return super.onRequestSendAccessibilityEvent(host, child, e)
+ }
+ when (e.eventType) {
+ AccessibilityEvent.TYPE_ANNOUNCEMENT -> {
+ val text = e.text[0] // there always is a text
+ if (text.length >= TEXT_TOO_LONG_TO_ANNOUNCE) {
+ Log.i(TAG, "Announcement skipped because it's too long: $text")
+ return false
+ }
+ }
+ }
+ return super.onRequestSendAccessibilityEvent(host, child, e)
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ if (a11yManager.isEnabled) {
+ val parent = getParent() as View
+ parent.setAccessibilityDelegate(mA11yEventFilter)
+ }
+ }
+
+ private val mA11yNodeProvider: AccessibilityNodeProvider =
+ object : AccessibilityNodeProvider() {
+ /** Returns the original NodeProvider that WebView implements. */
+ private fun getParent(): AccessibilityNodeProvider? {
+ return super@TerminalView.getAccessibilityNodeProvider()
+ }
+
+ /** Convenience method for reading a string resource. */
+ private fun getString(resId: Int): String {
+ return this@TerminalView.context.getResources().getString(resId)
+ }
+
+ /** Checks if NodeInfo renders an empty line in the terminal. */
+ private fun isEmptyLine(info: AccessibilityNodeInfo): Boolean {
+ // Node with no text is not considered a line. ttyd emits at least one character,
+ // which usually is NBSP.
+ // Note: don't use Characters.isWhitespace as it doesn't recognize NBSP as a
+ // whitespace.
+ return (info.getText()?.all { TextUtils.isWhitespace(it.code) }) == true
+ }
+
+ override fun createAccessibilityNodeInfo(id: Int): AccessibilityNodeInfo? {
+ val info: AccessibilityNodeInfo? = getParent()?.createAccessibilityNodeInfo(id)
+ if (info == null) {
+ return null
+ }
+
+ val className = info.className.toString()
+
+ // By default all views except the cursor is not click-able. Other views are
+ // read-only. This ensures that user is not navigated to non-clickable elements
+ // when using switches.
+ if ("android.widget.EditText" != className) {
+ info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)
+ }
+
+ when (className) {
+ "android.webkit.WebView" -> {
+ // There are two NodeInfo objects of class name WebView. The one is the
+ // real WebView whose ID is View.NO_ID as it's at the root of the
+ // virtual view hierarchy. The second one is a virtual view for the
+ // iframe. The latter one's text is set to the command that we give to
+ // ttyd, which is "login -f droid ...". This is an impl detail which
+ // doesn't have to be announced. Replace the text with "Terminal
+ // display".
+ if (id != NO_ID) {
+ info.setText(null)
+ info.setContentDescription(getString(R.string.terminal_display))
+ // b/376827536
+ info.setHintText(getString(R.string.double_tap_to_edit_text))
+ }
+
+ // These two lines below are to prevent this WebView element from being
+ // focusable by the screen reader, while allowing any other element in
+ // the WebView to be focusable by the reader. In our case, the EditText
+ // is a117_focusable.
+ info.isScreenReaderFocusable = false
+ info.addAction(
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS
+ )
+ }
+
+ "android.view.View" ->
+ // Empty line was announced as "space" (via the NBSP character).
+ // Localize the spoken text.
+ if (isEmptyLine(info)) {
+ info.setContentDescription(getString(R.string.empty_line))
+ // b/376827536
+ info.setHintText(getString(R.string.double_tap_to_edit_text))
+ }
+
+ "android.widget.TextView" -> {
+ // There are several TextViews in the terminal, and one of them is an
+ // invisible TextView which seems to be from the <div
+ // class="live-region"> tag. Interestingly, its text is often populated
+ // with the entire text on the screen. Silence this by forcibly setting
+ // the text to null. Note that this TextView is identified by having a
+ // zero width. This certainly is not elegant, but I couldn't find other
+ // options.
+ val rect = Rect()
+ info.getBoundsInScreen(rect)
+ if (rect.width() == 0) {
+ info.setText(null)
+ info.setContentDescription(getString(R.string.empty_line))
+ }
+ info.isScreenReaderFocusable = false
+ }
+
+ "android.widget.EditText" -> {
+ // This EditText is for the <textarea> accepting user input; the cursor.
+ // ttyd name it as "Terminal input" but it's not i18n'ed. Override it
+ // here for better i18n.
+ info.setText(null)
+ info.setHintText(getString(R.string.double_tap_to_edit_text))
+ info.setContentDescription(getString(R.string.terminal_input))
+ info.isScreenReaderFocusable = true
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_FOCUS)
+ }
+ }
+ return info
+ }
+
+ override fun performAction(id: Int, action: Int, arguments: Bundle?): Boolean {
+ return getParent()?.performAction(id, action, arguments) == true
+ }
+
+ override fun addExtraDataToAccessibilityNodeInfo(
+ virtualViewId: Int,
+ info: AccessibilityNodeInfo?,
+ extraDataKey: String?,
+ arguments: Bundle?,
+ ) {
+ getParent()
+ ?.addExtraDataToAccessibilityNodeInfo(
+ virtualViewId,
+ info,
+ extraDataKey,
+ arguments,
+ )
+ }
+
+ override fun findAccessibilityNodeInfosByText(
+ text: String?,
+ virtualViewId: Int,
+ ): MutableList<AccessibilityNodeInfo?>? {
+ return getParent()?.findAccessibilityNodeInfosByText(text, virtualViewId)
+ }
+
+ override fun findFocus(focus: Int): AccessibilityNodeInfo? {
+ return getParent()?.findFocus(focus)
+ }
+ }
+
+ override fun getAccessibilityNodeProvider(): AccessibilityNodeProvider? {
+ val p = super.getAccessibilityNodeProvider()
+ if (p != null && a11yManager.isEnabled) {
+ return mA11yNodeProvider
+ }
+ return p
+ }
+
+ override fun onCreateInputConnection(outAttrs: EditorInfo?): InputConnection? {
+ val inputConnection = super.onCreateInputConnection(outAttrs)
+ if (outAttrs != null) {
+ outAttrs.inputType = outAttrs.inputType or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
+ }
+ return inputConnection
+ }
+
+ companion object {
+ // Maximum length of texts the talk back announcements can be. This value is somewhat
+ // arbitrarily set. We may want to adjust this in the future.
+ private const val TEXT_TOO_LONG_TO_ANNOUNCE = 200
+ }
+}
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index a90c1ff..096d3b5 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -324,7 +324,7 @@
let tap =
if let Some(tap_file) = &config.tap { Some(tap_file.try_clone()?) } else { None };
- run_virtiofs(&config)?;
+ let vhost_fs_devices = run_virtiofs(&config)?;
// If this fails and returns an error, `self` will be left in the `Failed` state.
let child =
@@ -339,7 +339,13 @@
let child_clone = child.clone();
let instance_clone = instance.clone();
let monitor_vm_exit_thread = Some(thread::spawn(move || {
- instance_clone.monitor_vm_exit(child_clone, failure_pipe_read, vfio_devices, tap);
+ instance_clone.monitor_vm_exit(
+ child_clone,
+ failure_pipe_read,
+ vfio_devices,
+ tap,
+ vhost_fs_devices,
+ );
}));
if detect_hangup {
@@ -486,6 +492,7 @@
failure_pipe_read: File,
vfio_devices: Vec<VfioDevice>,
tap: Option<File>,
+ vhost_user_devices: Vec<SharedChild>,
) {
let failure_reason_thread = std::thread::spawn(move || {
// Read the pipe to see if any failure reason is written
@@ -513,6 +520,34 @@
}
}
+ // In crosvm, when vhost_user frontend is dead, vhost_user backend device will detect and
+ // exit. We can safely wait() for vhost user device after waiting crosvm main
+ // process.
+ for device in vhost_user_devices {
+ match device.wait() {
+ Ok(status) => {
+ info!("Vhost user device({}) exited with status {}", device.id(), status);
+ if !status.success() {
+ if let Some(code) = status.code() {
+ // vhost_user backend device exit with error code
+ error!(
+ "vhost user device({}) exited with error code: {}",
+ device.id(),
+ code
+ );
+ } else {
+ // The spawned child process of vhost_user backend device is
+ // killed by signal
+ error!("vhost user device({}) killed by signal", device.id());
+ }
+ }
+ }
+ Err(e) => {
+ error!("Error waiting for vhost user device({}) to die: {}", device.id(), e);
+ }
+ }
+ }
+
let failure_reason = failure_reason_thread.join().expect("failure_reason_thread panic'd");
let mut vm_state = self.vm_state.lock().unwrap();
@@ -915,7 +950,8 @@
}
}
-fn run_virtiofs(config: &CrosvmConfig) -> io::Result<()> {
+fn run_virtiofs(config: &CrosvmConfig) -> io::Result<Vec<SharedChild>> {
+ let mut devices: Vec<SharedChild> = Vec::new();
for shared_path in &config.shared_paths {
if shared_path.app_domain {
continue;
@@ -947,9 +983,10 @@
let result = SharedChild::spawn(&mut command)?;
info!("Spawned virtiofs crosvm({})", result.id());
+ devices.push(result);
}
- Ok(())
+ Ok(devices)
}
/// Starts an instance of `crosvm` to manage a new VM.
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 613f7d2..3f33ec8 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -307,9 +307,10 @@
generate_output_package() {
fdisk -l "${raw_disk_image}"
- root_partition_num=1
- bios_partition_num=14
- efi_partition_num=15
+ local vm_config="$(realpath $(dirname "$0"))/vm_config.json.${arch}"
+ local root_partition_num=1
+ local bios_partition_num=14
+ local efi_partition_num=15
pushd ${workdir} > /dev/null
@@ -373,7 +374,6 @@
config_space=${debian_cloud_image}/config_space/${debian_version}
resources_dir=${debian_cloud_image}/src/debian_cloud_images/resources
arch="$(uname -m)"
-vm_config="$(realpath $(dirname "$0"))/vm_config.json.${arch}"
mode=debug
save_workdir=0
use_custom_kernel=0
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 0a7cca6..03657ed 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -9,7 +9,7 @@
* aosp\_oriole (Pixel 6)
* aosp\_raven (Pixel 6 Pro)
* aosp\_felix (Pixel Fold)
-* aosp\_tangopro (Pixel Tablet)
+* aosp\_tangorpro (Pixel Tablet)
* aosp\_cf\_x86\_64\_phone (Cuttlefish a.k.a. Cloud Android). Follow [this
instruction](https://source.android.com/docs/setup/create/cuttlefish-use) to
use.
diff --git a/guest/pvmfw/Android.bp b/guest/pvmfw/Android.bp
index 51f7802..793204d 100644
--- a/guest/pvmfw/Android.bp
+++ b/guest/pvmfw/Android.bp
@@ -384,6 +384,8 @@
"-E",
"-P",
"-xassembler-with-cpp", // allow C preprocessor directives
+ // Suppress an error about the unused -c that precedes -S.
+ "-Wno-unused-command-line-argument",
],
visibility: ["//visibility:private"],
}
diff --git a/libs/cstr/rules.mk b/libs/cstr/rules.mk
new file mode 100644
index 0000000..2309c30
--- /dev/null
+++ b/libs/cstr/rules.mk
@@ -0,0 +1,28 @@
+# Copyright (C) 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.
+#
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+
+MODULE := $(LOCAL_DIR)
+
+SRC_DIR := packages/modules/Virtualization/libs/cstr
+
+MODULE_SRCS := $(SRC_DIR)/src/lib.rs
+
+MODULE_CRATE_NAME := cstr
+
+MODULE_RUST_EDITION := 2021
+
+include make/library.mk
diff --git a/libs/libfdt/bindgen/rules.mk b/libs/libfdt/bindgen/rules.mk
new file mode 100644
index 0000000..130a317
--- /dev/null
+++ b/libs/libfdt/bindgen/rules.mk
@@ -0,0 +1,38 @@
+# Copyright (C) 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.
+#
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+
+MODULE := $(LOCAL_DIR)
+
+MODULE_SRCS := $(LOCAL_DIR)/src/lib.rs
+
+MODULE_CRATE_NAME := libfdt_bindgen
+
+MODULE_DEPS += \
+ external/dtc/libfdt \
+
+MODULE_BINDGEN_ALLOW_FUNCTIONS := \
+ fdt_.* \
+
+MODULE_BINDGEN_ALLOW_VARS := \
+ FDT_.* \
+
+MODULE_BINDGEN_ALLOW_TYPES := \
+ fdt_.* \
+
+MODULE_BINDGEN_SRC_HEADER := $(LOCAL_DIR)/fdt.h
+
+include make/library.mk
diff --git a/libs/libfdt/bindgen/src/lib.rs b/libs/libfdt/bindgen/src/lib.rs
new file mode 100644
index 0000000..015132b
--- /dev/null
+++ b/libs/libfdt/bindgen/src/lib.rs
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 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.
+ */
+
+//! # Interface library for libfdt.
+
+#![no_std]
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+include!(env!("BINDGEN_INC_FILE"));
diff --git a/libs/libfdt/rules.mk b/libs/libfdt/rules.mk
new file mode 100644
index 0000000..2b4e470
--- /dev/null
+++ b/libs/libfdt/rules.mk
@@ -0,0 +1,37 @@
+# Copyright (C) 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.
+#
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+
+MODULE := $(LOCAL_DIR)
+
+SRC_DIR := packages/modules/Virtualization/libs/libfdt
+
+MODULE_SRCS := $(SRC_DIR)/src/lib.rs
+
+MODULE_CRATE_NAME := libfdt
+
+MODULE_RUST_EDITION := 2021
+
+MODULE_LIBRARY_DEPS += \
+ external/dtc/libfdt \
+ packages/modules/Virtualization/libs/cstr \
+ packages/modules/Virtualization/libs/libfdt/bindgen \
+ $(call FIND_CRATE,zerocopy) \
+ $(call FIND_CRATE,static_assertions) \
+
+MODULE_RUST_USE_CLIPPY := true
+
+include make/library.mk
diff --git a/tests/vts/Android.bp b/tests/vts/Android.bp
index 35fbcdc..c8e2523 100644
--- a/tests/vts/Android.bp
+++ b/tests/vts/Android.bp
@@ -20,6 +20,7 @@
"libavf_bindgen",
"libciborium",
"liblog_rust",
+ "libhypervisor_props",
"libscopeguard",
"libservice_vm_comm",
"libvsock",
diff --git a/tests/vts/src/vts_libavf_test.rs b/tests/vts/src/vts_libavf_test.rs
index ba38a2e..e30c175 100644
--- a/tests/vts/src/vts_libavf_test.rs
+++ b/tests/vts/src/vts_libavf_test.rs
@@ -177,10 +177,20 @@
#[test]
fn test_run_rialto_protected() -> Result<()> {
- run_rialto(true /* protected_vm */)
+ if hypervisor_props::is_protected_vm_supported()? {
+ run_rialto(true /* protected_vm */)
+ } else {
+ info!("pVMs are not supported on device. skipping test");
+ Ok(())
+ }
}
#[test]
fn test_run_rialto_non_protected() -> Result<()> {
- run_rialto(false /* protected_vm */)
+ if hypervisor_props::is_vm_supported()? {
+ run_rialto(false /* protected_vm */)
+ } else {
+ info!("non-pVMs are not supported on device. skipping test");
+ Ok(())
+ }
}